feat: add Thai date selector, HTML prototypes, and SQL script
- Replace xlsx with exceljs in construction calculator; switch date input to Thai Buddhist Era selectors - Add disbursement plan preview HTML and schedule table prototype - Add sync_budget_come_from SQL script - Add budget type select menu design plan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>คำนวณงวดงานสิ่งก่อสร้าง</title>
|
||||
<script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
@@ -54,8 +54,13 @@ body {
|
||||
font-family: inherit;
|
||||
}
|
||||
.form-group input[type="number"] { width: 90px; }
|
||||
.form-group input[type="date"] { width: 160px; }
|
||||
.form-group input#contractAmount { width: 180px; }
|
||||
.date-sel {
|
||||
border: 1.5px solid #c5d3e8; border-radius: 6px;
|
||||
padding: 0 6px; font-size: 14px; background: white;
|
||||
color: #333; font-family: inherit;
|
||||
height: 37px; box-sizing: border-box; cursor: pointer;
|
||||
}
|
||||
.ratio-group { display: flex; align-items: center; gap: 6px; }
|
||||
.ratio-group span { font-size: 12px; color: #666; }
|
||||
|
||||
@@ -154,24 +159,34 @@ tr.total-row td {
|
||||
<input type="text" id="contractAmount" placeholder="เช่น 120000000" value="">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>วันเริ่มก่อสร้าง</label>
|
||||
<input type="date" id="startDate" value="">
|
||||
<label>วันเริ่มก่อสร้าง (พ.ศ.)</label>
|
||||
<div style="display:flex;gap:4px;">
|
||||
<select id="startDay" class="date-sel" style="width:58px" onchange="onStartDateChange()"><option value="">วัน</option></select>
|
||||
<select id="startMonth" class="date-sel" style="width:80px" onchange="onStartDateChange()">
|
||||
<option value="">เดือน</option>
|
||||
<option value="1">ม.ค.</option><option value="2">ก.พ.</option><option value="3">มี.ค.</option>
|
||||
<option value="4">เม.ย.</option><option value="5">พ.ค.</option><option value="6">มิ.ย.</option>
|
||||
<option value="7">ก.ค.</option><option value="8">ส.ค.</option><option value="9">ก.ย.</option>
|
||||
<option value="10">ต.ค.</option><option value="11">พ.ย.</option><option value="12">ธ.ค.</option>
|
||||
</select>
|
||||
<select id="startYear" class="date-sel" style="width:80px" onchange="onStartDateChange()"><option value="">ปี</option></select>
|
||||
</div>
|
||||
<input type="hidden" id="startDate" value="">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>สัดส่วน งปม. / เงินนอก (%)</label>
|
||||
<div class="ratio-group">
|
||||
<input type="number" id="ngpmPct" value="95" min="0" max="100" style="width:70px">
|
||||
<span>งปม. /</span>
|
||||
<input type="number" id="nookPct" value="5" min="0" max="100" style="width:70px" readonly>
|
||||
<span>นอก</span>
|
||||
</div>
|
||||
<label>เงินงบประมาณ (%)</label>
|
||||
<input type="number" id="ngpmPct" value="95" min="0" max="100" style="width:80px">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>เงินนอกงบ (%)</label>
|
||||
<input type="number" id="nookPct" value="5" min="0" max="100" style="width:80px" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>จำนวนงวด</label>
|
||||
<input type="number" id="installmentCount" value="27" min="1" max="100" style="width:75px">
|
||||
</div>
|
||||
<div class="form-group" style="flex-direction:row;gap:10px;align-items:flex-end">
|
||||
<button class="btn btn-export" id="btnExport" onclick="exportExcel()" disabled>⬇ Export Excel</button>
|
||||
<button class="btn btn-export" id="btnExport" onclick="exportExcel().catch(console.error)" disabled>⬇ Export Excel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -267,9 +282,10 @@ function formatNumber(n) {
|
||||
}
|
||||
|
||||
function addDays(dateStr, days) {
|
||||
const d = new Date(dateStr);
|
||||
d.setDate(d.getDate() + days);
|
||||
return d;
|
||||
const [y, m, d] = String(dateStr).split('-').map(Number);
|
||||
const date = new Date(y, m - 1, d);
|
||||
date.setDate(date.getDate() + days);
|
||||
return date;
|
||||
}
|
||||
|
||||
function toThaiDate(date) {
|
||||
@@ -440,57 +456,172 @@ function onCountChange(n) {
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function exportExcel() {
|
||||
async function exportExcel() {
|
||||
const {contract, startDate, ngpmPct, nookPct} = getInputs();
|
||||
const wb = XLSX.utils.book_new();
|
||||
const wb = new ExcelJS.Workbook();
|
||||
const ws = wb.addWorksheet('งวดงาน', {
|
||||
views: [{ state: 'frozen', xSplit: 0, ySplit: 4 }]
|
||||
});
|
||||
|
||||
const headers = [
|
||||
'งวดที่','จำนวนวัน','ร้อยละ (%)','วันส่งมอบงาน',
|
||||
'รวม (บาท)','เงิน งปม. (บาท)','เงินนอก (บาท)',
|
||||
'รวมสะสม (บาท)','งปม. สะสม (บาท)','เงินนอก สะสม (บาท)'
|
||||
const NCOLS = 10;
|
||||
ws.columns = [
|
||||
{width:8},{width:10},{width:10},{width:16},
|
||||
{width:18},{width:18},{width:16},
|
||||
{width:18},{width:18},{width:18}
|
||||
];
|
||||
|
||||
const rows = [headers];
|
||||
let cumDays = 0, cumTotal = 0, cumNgpm = 0, cumNook = 0;
|
||||
const thin = { style: 'thin', color: { argb: 'FFB0C4DE' } };
|
||||
const medT = { style: 'medium', color: { argb: 'FF1A56A0' } };
|
||||
const bord = { top: thin, left: thin, bottom: thin, right: thin };
|
||||
|
||||
function fill(cell, argb) {
|
||||
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb } };
|
||||
}
|
||||
function font(cell, opts) {
|
||||
cell.font = { name: 'Sarabun', ...opts };
|
||||
}
|
||||
function align(cell, h) {
|
||||
cell.alignment = { horizontal: h, vertical: 'middle', wrapText: true };
|
||||
}
|
||||
|
||||
// ── Row 1: Title ──────────────────────────────────────
|
||||
ws.addRow(['การคำนวณงวดงานสิ่งก่อสร้าง']);
|
||||
ws.mergeCells(1, 1, 1, NCOLS);
|
||||
ws.getRow(1).height = 30;
|
||||
const tc = ws.getCell(1, 1);
|
||||
fill(tc, 'FF1A56A0');
|
||||
font(tc, { bold: true, size: 14, color: { argb: 'FFFFFFFF' } });
|
||||
align(tc, 'center');
|
||||
|
||||
// ── Row 2: Info ───────────────────────────────────────
|
||||
const startDisp = startDate ? (() => {
|
||||
const [y, m, d] = startDate.split('-').map(Number);
|
||||
return `${String(d).padStart(2,'0')}/${String(m).padStart(2,'0')}/${y + 543}`;
|
||||
})() : '—';
|
||||
ws.addRow([`วงเงินสัญญา: ${Math.round(contract).toLocaleString('th-TH')} บาท วันเริ่มก่อสร้าง: ${startDisp} สัดส่วน งปม./เงินนอก: ${ngpmPct}% / ${nookPct}%`]);
|
||||
ws.mergeCells(2, 1, 2, NCOLS);
|
||||
ws.getRow(2).height = 20;
|
||||
const ic = ws.getCell(2, 1);
|
||||
fill(ic, 'FFE8F0FB');
|
||||
font(ic, { size: 10, color: { argb: 'FF1A56A0' } });
|
||||
align(ic, 'center');
|
||||
|
||||
// ── Row 3: Header row 1 (merged spans) ───────────────
|
||||
ws.addRow(['งวดที่','จำนวนวัน','ร้อยละ (%)','วันส่งมอบงาน',
|
||||
'จำนวนเงินงวดนั้น (บาท)','','','จำนวนเงินสะสม (บาท)','','']);
|
||||
ws.mergeCells(3,1,4,1); ws.mergeCells(3,2,4,2);
|
||||
ws.mergeCells(3,3,4,3); ws.mergeCells(3,4,4,4);
|
||||
ws.mergeCells(3,5,3,7); ws.mergeCells(3,8,3,10);
|
||||
ws.getRow(3).height = 22;
|
||||
for (let c = 1; c <= NCOLS; c++) {
|
||||
const cell = ws.getCell(3, c);
|
||||
fill(cell, 'FF1A56A0');
|
||||
font(cell, { bold: true, size: 10, color: { argb: 'FFFFFFFF' } });
|
||||
align(cell, 'center');
|
||||
cell.border = bord;
|
||||
}
|
||||
|
||||
// ── Row 4: Header row 2 (sub-header) ─────────────────
|
||||
ws.addRow(['','','','','รวม','เงิน งปม.','เงินนอก','รวม','เงิน งปม.','เงินนอก']);
|
||||
ws.getRow(4).height = 18;
|
||||
for (let c = 5; c <= NCOLS; c++) {
|
||||
const cell = ws.getCell(4, c);
|
||||
fill(cell, 'FF2A6ABF');
|
||||
font(cell, { bold: true, size: 10, color: { argb: 'FFFFFFFF' } });
|
||||
align(cell, 'center');
|
||||
cell.border = bord;
|
||||
}
|
||||
|
||||
// ── Data rows ─────────────────────────────────────────
|
||||
let cumDays = 0, cumTotal = 0, cumNgpm = 0, cumNook = 0;
|
||||
installments.forEach((inst, i) => {
|
||||
const days = parseFloat(inst.days) || 0;
|
||||
const pct = parseFloat(inst.percent) || 0;
|
||||
const isLast = i === installments.length - 1;
|
||||
cumDays += days;
|
||||
|
||||
const total = isLast ? (contract - cumTotal) : Math.round(contract * pct / 100);
|
||||
const total = isLast ? (contract - cumTotal) : Math.round(contract * pct / 100);
|
||||
const ngpm = isLast ? (Math.round(contract * ngpmPct / 100) - cumNgpm) : Math.round(total * ngpmPct / 100);
|
||||
const nook = total - ngpm;
|
||||
cumTotal += total; cumNgpm += ngpm; cumNook += nook;
|
||||
|
||||
const delivery = toThaiDate(addDays(startDate, cumDays));
|
||||
rows.push([i+1, days, pct, delivery, total, ngpm, nook, cumTotal, cumNgpm, cumNook]);
|
||||
const even = i % 2 === 1;
|
||||
ws.addRow([i+1, days, pct/100, delivery, total, ngpm, nook, cumTotal, cumNgpm, cumNook]);
|
||||
const rn = 5 + i;
|
||||
ws.getRow(rn).height = 18;
|
||||
for (let c = 1; c <= NCOLS; c++) {
|
||||
const cell = ws.getCell(rn, c);
|
||||
const isAcc = c >= 8;
|
||||
fill(cell, isAcc ? (even ? 'FFD8E8F5' : 'FFE8F0FB') : (even ? 'FFF5F8FF' : 'FFFFFFFF'));
|
||||
font(cell, { size: 10, color: { argb: isAcc ? 'FF1A56A0' : 'FF222222' }, bold: isAcc });
|
||||
cell.border = bord;
|
||||
if (c === 1 || c === 2 || c === 4) { align(cell, 'center'); }
|
||||
else if (c === 3) { cell.numFmt = '0.00%'; align(cell, 'center'); }
|
||||
else { cell.numFmt = '#,##0'; align(cell, 'right'); }
|
||||
}
|
||||
});
|
||||
|
||||
rows.push([
|
||||
'รวม', '', installments.reduce((s,r) => s + (parseFloat(r.percent)||0), 0).toFixed(2),
|
||||
'',
|
||||
// ── Total row ─────────────────────────────────────────
|
||||
const sumPct = installments.reduce((s, r) => s + (parseFloat(r.percent) || 0), 0);
|
||||
const tRn = 5 + installments.length;
|
||||
ws.addRow(['รวมทั้งสิ้น','', sumPct/100,'',
|
||||
Math.round(contract), Math.round(contract*ngpmPct/100), Math.round(contract*nookPct/100),
|
||||
'', '', ''
|
||||
]);
|
||||
'','','']);
|
||||
ws.mergeCells(tRn, 1, tRn, 2);
|
||||
ws.getRow(tRn).height = 22;
|
||||
for (let c = 1; c <= NCOLS; c++) {
|
||||
const cell = ws.getCell(tRn, c);
|
||||
fill(cell, 'FFDCE8F8');
|
||||
font(cell, { bold: true, size: 10, color: { argb: 'FF1A56A0' } });
|
||||
cell.border = { ...bord, top: medT };
|
||||
if (c === 1) { align(cell, 'center'); }
|
||||
else if (c === 3) { cell.numFmt = '0.00%'; align(cell, 'center'); }
|
||||
else if (c >= 5 && c <= 7) { cell.numFmt = '#,##0'; align(cell, 'right'); }
|
||||
}
|
||||
|
||||
const ws = XLSX.utils.aoa_to_sheet(rows);
|
||||
ws['!cols'] = [
|
||||
{wch:8},{wch:12},{wch:12},{wch:18},
|
||||
{wch:18},{wch:18},{wch:16},
|
||||
{wch:18},{wch:18},{wch:18}
|
||||
];
|
||||
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'งวดงาน');
|
||||
|
||||
const today = new Date();
|
||||
const fname = `งวดงาน_${today.getFullYear()+543}${String(today.getMonth()+1).padStart(2,'0')}${String(today.getDate()).padStart(2,'0')}.xlsx`;
|
||||
XLSX.writeFile(wb, fname);
|
||||
// ── Download ──────────────────────────────────────────
|
||||
const buf = await wb.xlsx.writeBuffer();
|
||||
const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const now = new Date();
|
||||
a.download = `งวดงาน_${now.getFullYear()+543}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}.xlsx`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
document.getElementById('contractAmount').addEventListener('input', recalcAll);
|
||||
document.getElementById('startDate').addEventListener('change', recalcAll);
|
||||
// Init date dropdowns
|
||||
(function() {
|
||||
const dayEl = document.getElementById('startDay');
|
||||
for (let d = 1; d <= 31; d++) {
|
||||
const o = document.createElement('option');
|
||||
o.value = d; o.textContent = d;
|
||||
dayEl.appendChild(o);
|
||||
}
|
||||
const yearEl = document.getElementById('startYear');
|
||||
const curBE = new Date().getFullYear() + 543;
|
||||
for (let y = 2540; y <= 2590; y++) {
|
||||
const o = document.createElement('option');
|
||||
o.value = y; o.textContent = y;
|
||||
yearEl.appendChild(o);
|
||||
}
|
||||
})();
|
||||
|
||||
function onStartDateChange() {
|
||||
const d = parseInt(document.getElementById('startDay').value) || 0;
|
||||
const m = parseInt(document.getElementById('startMonth').value) || 0;
|
||||
const y = parseInt(document.getElementById('startYear').value) || 0;
|
||||
const hidden = document.getElementById('startDate');
|
||||
if (d && m && y) {
|
||||
const ce = y - 543;
|
||||
hidden.value = `${ce}-${String(m).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
||||
} else {
|
||||
hidden.value = '';
|
||||
}
|
||||
recalcAll();
|
||||
}
|
||||
document.getElementById('ngpmPct').addEventListener('input', function() {
|
||||
const v = parseFloat(this.value) || 0;
|
||||
document.getElementById('nookPct').value = (100 - v).toFixed(2);
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="th">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Preview: แผนการเบิกจ่ายเป็นงวด</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: 'Sarabun', sans-serif; background: #f5f5f5; padding: 20px; color: #333; }
|
||||
|
||||
/* ─── LIST ROW PREVIEW ─── */
|
||||
.section-label { font-size: 13px; color: #666; margin-bottom: 8px; text-transform: uppercase; letter-spacing: .5px; }
|
||||
.list-preview { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.1); overflow: hidden; margin-bottom: 32px; }
|
||||
.list-preview table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.list-preview th { background: #1976d2; color: white; padding: 8px 10px; text-align: center; font-weight: 500; border: 1px solid #1565c0; }
|
||||
.list-preview td { padding: 7px 10px; border: 1px solid #e0e0e0; vertical-align: middle; }
|
||||
.list-preview tr:nth-child(even) td { background: #fafafa; }
|
||||
.icon-btn {
|
||||
width: 32px; height: 32px; border-radius: 50%; border: none; cursor: pointer;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 16px; margin: 0 2px; transition: background .2s;
|
||||
}
|
||||
.icon-btn.edit { background: #e3f2fd; color: #1976d2; }
|
||||
.icon-btn.edit:hover { background: #bbdefb; }
|
||||
.icon-btn.disburse { background: #e8f5e9; color: #388e3c; }
|
||||
.icon-btn.disburse:hover { background: #c8e6c9; }
|
||||
.icon-btn.disburse.has-data { background: #388e3c; color: white; }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; }
|
||||
.badge.filled { background: #e8f5e9; color: #388e3c; }
|
||||
.badge.empty { background: #f5f5f5; color: #9e9e9e; }
|
||||
|
||||
/* ─── OVERLAY ─── */
|
||||
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); display: flex; align-items: center; justify-content: center; z-index: 100; }
|
||||
.overlay.hidden { display: none; }
|
||||
|
||||
/* ─── DIALOG ─── */
|
||||
.dialog {
|
||||
background: white; border-radius: 8px; box-shadow: 0 8px 32px rgba(0,0,0,.3);
|
||||
width: 780px; max-width: 95vw; max-height: 90vh; overflow-y: auto;
|
||||
}
|
||||
.dialog-header {
|
||||
background: #1976d2; color: white; padding: 16px 20px;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
.dialog-header h2 { font-size: 16px; font-weight: 600; }
|
||||
.dialog-close { background: none; border: none; color: white; font-size: 20px; cursor: pointer; line-height: 1; }
|
||||
.dialog-body { padding: 20px; }
|
||||
.project-info { background: #f3f8ff; border-left: 4px solid #1976d2; padding: 10px 14px; border-radius: 0 4px 4px 0; margin-bottom: 20px; }
|
||||
.project-info .label { font-size: 11px; color: #666; }
|
||||
.project-info .value { font-size: 14px; font-weight: 600; color: #1976d2; }
|
||||
|
||||
/* ─── DISBURSE TABLE ─── */
|
||||
.disburse-table-wrap { overflow-x: auto; margin-bottom: 20px; }
|
||||
.disburse-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.disburse-table th, .disburse-table td { border: 1px solid #ccc; padding: 0; text-align: center; }
|
||||
.disburse-table .th-main { background: #1565c0; color: white; padding: 8px; font-weight: 600; font-size: 14px; }
|
||||
.disburse-table .th-sub { background: #1976d2; color: white; padding: 7px 6px; font-weight: 500; font-size: 12px; }
|
||||
.disburse-table .th-col { background: #e3f2fd; color: #0d47a1; padding: 6px 4px; font-size: 12px; font-weight: 500; }
|
||||
.disburse-table .td-total { background: #fff8e1; font-weight: 700; font-size: 14px; color: #e65100; padding: 10px 8px; }
|
||||
.disburse-table input[type=number], .disburse-table input[type=date] {
|
||||
width: 100%; border: none; padding: 8px 6px; text-align: right; font-size: 13px;
|
||||
background: transparent; outline: none; font-family: inherit;
|
||||
}
|
||||
.disburse-table input[type=date] { text-align: center; }
|
||||
.disburse-table input:focus { background: #fff8e1; }
|
||||
.disburse-table tr:hover td { background: #f9f9f9; }
|
||||
.disburse-table tr:hover td.td-total { background: #fff3e0; }
|
||||
|
||||
/* ─── ACTIONS ─── */
|
||||
.dialog-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 12px 20px; border-top: 1px solid #e0e0e0; }
|
||||
.btn { padding: 8px 20px; border-radius: 4px; border: none; cursor: pointer; font-size: 14px; font-family: inherit; font-weight: 500; }
|
||||
.btn-cancel { background: #f5f5f5; color: #666; border: 1px solid #ddd; }
|
||||
.btn-cancel:hover { background: #eeeeee; }
|
||||
.btn-save { background: #1976d2; color: white; }
|
||||
.btn-save:hover { background: #1565c0; }
|
||||
|
||||
/* ─── TOTAL SUMMARY ─── */
|
||||
.total-bar { background: #fff3e0; border: 1px solid #ffcc80; border-radius: 6px; padding: 10px 16px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.total-bar .label { font-size: 13px; color: #795548; }
|
||||
.total-bar .amount { font-size: 18px; font-weight: 700; color: #e65100; }
|
||||
|
||||
h1 { font-size: 18px; color: #1976d2; margin-bottom: 6px; }
|
||||
p.desc { font-size: 13px; color: #666; margin-bottom: 20px; }
|
||||
.arrow { font-size: 24px; color: #1976d2; margin: 0 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>🔍 Preview: แผนการเบิกจ่ายเป็นงวด</h1>
|
||||
<p class="desc">ตัวอย่างการแสดงผลใน Angular — ไอคอน <span style="background:#e8f5e9;color:#388e3c;padding:2px 8px;border-radius:12px;font-size:12px;">💰</span> ในตารางรายโครงการ และ dialog form</p>
|
||||
|
||||
<!-- ─── LIST PREVIEW ─── -->
|
||||
<div class="section-label">📋 ตารางรายโครงการ (list20-research) — มีไอคอน 💰 ต่อโครงการ</div>
|
||||
<div class="list-preview">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ลำดับ</th>
|
||||
<th>ปีงบ</th>
|
||||
<th>ชื่อโครงการ</th>
|
||||
<th>หน่วยงาน</th>
|
||||
<th>แหล่งเงิน</th>
|
||||
<th>งบประมาณ</th>
|
||||
<th style="background:#155724;">แผนเบิกจ่าย</th>
|
||||
<th>เครื่องมือ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align:center">1</td>
|
||||
<td style="text-align:center">2567</td>
|
||||
<td>โครงการพัฒนาคุณภาพการศึกษาด้านวิทยาศาสตร์</td>
|
||||
<td>คณะวิศวกรรมศาสตร์</td>
|
||||
<td>กองทุนพัฒนา</td>
|
||||
<td style="text-align:right">95,000</td>
|
||||
<td style="text-align:center">
|
||||
<span class="badge filled">✓ กรอกแล้ว</span>
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<button class="icon-btn edit" title="แก้ไข">✏️</button>
|
||||
<button class="icon-btn disburse has-data" title="แผนการเบิกจ่าย" onclick="openDialog('โครงการพัฒนาคุณภาพการศึกษาด้านวิทยาศาสตร์', 66500, '2567-11-15', 19000, '2568-01-20', 9500, '2568-03-30')">💰</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align:center">2</td>
|
||||
<td style="text-align:center">2567</td>
|
||||
<td>โครงการวิจัยนวัตกรรมดิจิทัล</td>
|
||||
<td>คณะเทคโนโลยีสารสนเทศ</td>
|
||||
<td>งบประมาณแผ่นดิน</td>
|
||||
<td style="text-align:right">150,000</td>
|
||||
<td style="text-align:center">
|
||||
<span class="badge empty">ยังไม่กรอก</span>
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<button class="icon-btn edit" title="แก้ไข">✏️</button>
|
||||
<button class="icon-btn disburse" title="แผนการเบิกจ่าย" onclick="openDialog('โครงการวิจัยนวัตกรรมดิจิทัล', null, null, null, null, null, null)">💰</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ─── DIALOG ─── -->
|
||||
<div class="overlay hidden" id="overlay" onclick="closeOnOverlay(event)">
|
||||
<div class="dialog" onclick="event.stopPropagation()">
|
||||
<div class="dialog-header">
|
||||
<h2>📋 แผนการเบิกจ่ายเงินเป็นงวด</h2>
|
||||
<button class="dialog-close" onclick="closeDialog()">✕</button>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div class="project-info">
|
||||
<div class="label">ชื่อโครงการ</div>
|
||||
<div class="value" id="project-name">—</div>
|
||||
</div>
|
||||
|
||||
<div class="disburse-table-wrap">
|
||||
<table class="disburse-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="th-main" colspan="7">แผนการเบิกจ่ายเงิน</th>
|
||||
<th class="th-main" rowspan="2" style="width:130px; vertical-align:middle;">รวมเบิกจ่าย<br>ทั้งสิ้น (บาท)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="th-sub" colspan="2">งวดที่ 1</th>
|
||||
<th class="th-sub" colspan="2">งวดที่ 2</th>
|
||||
<th class="th-sub" colspan="2">งวดสุดท้าย</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="th-col" style="width:120px">จำนวนเงิน (บาท)</th>
|
||||
<th class="th-col" style="width:130px">ว/ด/ป</th>
|
||||
<th class="th-col" style="width:120px">จำนวนเงิน (บาท)</th>
|
||||
<th class="th-col" style="width:130px">ว/ด/ป</th>
|
||||
<th class="th-col" style="width:120px">จำนวนเงิน (บาท)</th>
|
||||
<th class="th-col" style="width:130px">ว/ด/ป</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><input type="number" id="amt1" placeholder="0.00" oninput="updateTotal()"></td>
|
||||
<td><input type="date" id="date1"></td>
|
||||
<td><input type="number" id="amt2" placeholder="0.00" oninput="updateTotal()"></td>
|
||||
<td><input type="date" id="date2"></td>
|
||||
<td><input type="number" id="amtLast" placeholder="0.00" oninput="updateTotal()"></td>
|
||||
<td><input type="date" id="dateLast"></td>
|
||||
<td class="td-total" id="total-display">0.00</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="total-bar">
|
||||
<span class="label">💰 รวมเบิกจ่ายทั้งสิ้น</span>
|
||||
<span class="amount" id="total-bar-amount">0.00 บาท</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button class="btn btn-cancel" onclick="closeDialog()">ยกเลิก</button>
|
||||
<button class="btn btn-save" onclick="save()">💾 บันทึก</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── AFTER SAVE PREVIEW ─── -->
|
||||
<div class="section-label" style="margin-top:32px;">✅ หลังบันทึก — ข้อมูลที่กรอกจะแสดงใน tooltip หรือ badge</div>
|
||||
<div class="list-preview">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ชื่อโครงการ</th>
|
||||
<th>งวด 1</th>
|
||||
<th>ว/ด/ป</th>
|
||||
<th>งวด 2</th>
|
||||
<th>ว/ด/ป</th>
|
||||
<th>งวดสุดท้าย</th>
|
||||
<th>ว/ด/ป</th>
|
||||
<th style="background:#e65100;">รวมเบิกจ่าย</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>โครงการพัฒนาคุณภาพการศึกษาฯ</td>
|
||||
<td style="text-align:right">66,500</td>
|
||||
<td style="text-align:center">15/11/2567</td>
|
||||
<td style="text-align:right">19,000</td>
|
||||
<td style="text-align:center">20/01/2568</td>
|
||||
<td style="text-align:right">9,500</td>
|
||||
<td style="text-align:center">30/03/2568</td>
|
||||
<td style="text-align:right; background:#fff3e0; font-weight:700; color:#e65100;">95,000</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fmt(n) {
|
||||
if (!n && n !== 0) return '—';
|
||||
return Number(n).toLocaleString('th-TH', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
function openDialog(name, a1, d1, a2, d2, aL, dL) {
|
||||
document.getElementById('project-name').textContent = name;
|
||||
document.getElementById('amt1').value = a1 || '';
|
||||
document.getElementById('date1').value = d1 || '';
|
||||
document.getElementById('amt2').value = a2 || '';
|
||||
document.getElementById('date2').value = d2 || '';
|
||||
document.getElementById('amtLast').value = aL || '';
|
||||
document.getElementById('dateLast').value = dL || '';
|
||||
updateTotal();
|
||||
document.getElementById('overlay').classList.remove('hidden');
|
||||
}
|
||||
function closeDialog() {
|
||||
document.getElementById('overlay').classList.add('hidden');
|
||||
}
|
||||
function closeOnOverlay(e) {
|
||||
if (e.target === document.getElementById('overlay')) closeDialog();
|
||||
}
|
||||
function updateTotal() {
|
||||
const a1 = parseFloat(document.getElementById('amt1').value) || 0;
|
||||
const a2 = parseFloat(document.getElementById('amt2').value) || 0;
|
||||
const aL = parseFloat(document.getElementById('amtLast').value) || 0;
|
||||
const total = a1 + a2 + aL;
|
||||
document.getElementById('total-display').textContent = fmt(total);
|
||||
document.getElementById('total-bar-amount').textContent = fmt(total) + ' บาท';
|
||||
}
|
||||
function save() {
|
||||
alert('✅ บันทึกสำเร็จ!\n\nในระบบจริงจะเรียก PUT /api/request_budget/change_project_research_detail/{uid}\nพร้อม field: disbursement_1_amount, disbursement_1_date, disbursement_2_amount, disbursement_2_date, disbursement_last_amount, disbursement_last_date');
|
||||
closeDialog();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,456 @@
|
||||
# Budget Type Select Menu Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** แยก 3 ปุ่ม dropdown (งบลงทุน / งบเงินอุดหนุน / งบรายจ่ายอื่น) ออกจาก search component ไปเป็นหน้าเลือกประเภทงบประมาณใหม่ที่ route `app/request-budget/select` ออกแบบตาม design ที่กำหนด
|
||||
|
||||
**Architecture:** สร้าง `BudgetTypeSelectComponent` ใหม่เป็น child route ใน `RequestBudgetModule` ที่ path `select` — component นี้เป็น presenter ล้วนๆ ไม่มี API call, ทำหน้าที่แสดง 3 card พร้อม sub-item แล้ว navigate ไปยัง route เดิมที่ 3 ปุ่มเดิมชี้อยู่ จากนั้นแทนที่ 3 ปุ่มใน search component ด้วยปุ่มเดียว
|
||||
|
||||
**Tech Stack:** Angular 13+, Angular Material (mat-button, mat-icon), Tailwind CSS, TypeScript
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| Action | Path |
|
||||
|--------|------|
|
||||
| **Create** | `rmutr-web/src/app/feature/budget-request/request-budget/presenter/budget-type-select/budget-type-select.component.ts` |
|
||||
| **Create** | `rmutr-web/src/app/feature/budget-request/request-budget/presenter/budget-type-select/budget-type-select.component.html` |
|
||||
| **Create** | `rmutr-web/src/app/feature/budget-request/request-budget/presenter/budget-type-select/budget-type-select.component.scss` |
|
||||
| **Modify** | `rmutr-web/src/app/feature/budget-request/request-budget/request-budget.module.ts` |
|
||||
| **Modify** | `rmutr-web/src/app/feature/budget-request/request-budget/presenter/search/request-budget-search/request-budget-search.component.html` (lines 86–118) |
|
||||
| **Modify** | `rmutr-web/src/app/feature/budget-request/request-budget/presenter/search/request-budget-search/request-budget-search.component.ts` |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create BudgetTypeSelectComponent (TypeScript)
|
||||
|
||||
**Files:**
|
||||
- Create: `rmutr-web/src/app/feature/budget-request/request-budget/presenter/budget-type-select/budget-type-select.component.ts`
|
||||
|
||||
- [ ] **Step 1: สร้างไฟล์ component TypeScript**
|
||||
|
||||
```typescript
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { RequestPlanUidService } from 'src/app/core/service/request-budget/request-plan-uid.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-budget-type-select',
|
||||
templateUrl: './budget-type-select.component.html',
|
||||
styleUrls: ['./budget-type-select.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BudgetTypeSelectComponent {
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private requestPlanUidSV: RequestPlanUidService,
|
||||
) {}
|
||||
|
||||
goBack() {
|
||||
this.router.navigate(['app/request-budget']);
|
||||
}
|
||||
|
||||
// งบลงทุน
|
||||
add_invest_asset() {
|
||||
this.router.navigate(['app/durable-articles-manage']);
|
||||
}
|
||||
|
||||
add_invest() {
|
||||
this.router.navigate(['app/invest-construct-manage']);
|
||||
}
|
||||
|
||||
// งบเงินอุดหนุน
|
||||
add_request_actualize() {
|
||||
this.router.navigate(['app/request-actualize']);
|
||||
}
|
||||
|
||||
add() {
|
||||
this.router.navigate(['app/menu-icon']);
|
||||
}
|
||||
|
||||
addrequest7() {
|
||||
this.requestPlanUidSV.request_budget_type$.next('944e7cec-d666-447f-8809-bb13d21f2ca4');
|
||||
this.router.navigate(['app/request-budget-list']);
|
||||
}
|
||||
|
||||
// งบรายจ่ายอื่น
|
||||
add_request_other_expenses() {
|
||||
this.router.navigate(['app/request-other-expenses/add']);
|
||||
}
|
||||
|
||||
add2() {
|
||||
this.router.navigate(['app/menu-icon-2']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: สร้างไฟล์ SCSS (ว่างไว้ก่อน)**
|
||||
|
||||
สร้างไฟล์ `budget-type-select.component.scss` เนื้อหาว่างเปล่า (ใช้ Tailwind ทั้งหมด)
|
||||
|
||||
```scss
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Create BudgetTypeSelectComponent (HTML Template)
|
||||
|
||||
**Files:**
|
||||
- Create: `rmutr-web/src/app/feature/budget-request/request-budget/presenter/budget-type-select/budget-type-select.component.html`
|
||||
|
||||
Design จากรูปภาพ:
|
||||
- พื้นหลังสีครีม `bg-gray-50`
|
||||
- Header มีปุ่ม back + title + subtitle
|
||||
- Section title + คำอธิบาย + hint ขวา
|
||||
- 3 card แบบ flex row: งบลงทุน(แดง), งบเงินอุดหนุน(น้ำเงิน), งบรายจ่ายอื่น(ม่วง)
|
||||
- แต่ละ card: icon + ชื่อ + คำอธิบาย + sub-item list
|
||||
|
||||
- [ ] **Step 3: สร้าง HTML template**
|
||||
|
||||
```html
|
||||
<div class="min-h-screen bg-gray-50 p-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-start gap-4 mb-8">
|
||||
<button mat-icon-button class="border border-gray-300 rounded-lg bg-white" (click)="goBack()">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">จัดทำคำของบประมาณแผ่นดิน</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">คำของบประมาณแผ่นดิน</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Title Row -->
|
||||
<div class="flex items-start justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-800">เลือกประเภทคำของบประมาณ</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">เลือกหมวดงบและประเภทย่อยเพื่อเริ่มจัดทำรายการคำของบประมาณ</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-sm text-gray-500">
|
||||
<mat-icon class="text-blue-400 text-base leading-none">auto_awesome</mat-icon>
|
||||
<span>เลือกได้ในที่เดียว ไม่ต้องเปิดเมนูซ้อน</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards Row -->
|
||||
<div class="flex gap-5">
|
||||
|
||||
<!-- Card 1: งบลงทุน (red) -->
|
||||
<div class="flex-1 bg-white rounded-2xl border-2 border-red-200 p-5 flex flex-col gap-4">
|
||||
<!-- Card Header -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-14 h-14 rounded-xl bg-red-50 flex items-center justify-center flex-shrink-0">
|
||||
<mat-icon class="text-red-400" style="font-size:28px;width:28px;height:28px;">domain</mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 text-base">งบลงทุน</h3>
|
||||
<p class="text-xs text-gray-400 mt-0.5">ครุภัณฑ์ · ที่ดินและสิ่งก่อสร้าง</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sub-items label -->
|
||||
<p class="text-xs text-gray-400 font-medium -mb-1">เลือกประเภทย่อย</p>
|
||||
|
||||
<!-- Sub-item: ค่าครุภัณฑ์ -->
|
||||
<button class="w-full flex items-center justify-between bg-gray-50 hover:bg-red-50 border border-gray-200 hover:border-red-300 rounded-xl px-4 py-3 transition-colors cursor-pointer text-left"
|
||||
(click)="add_invest_asset()">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-red-100 flex items-center justify-center">
|
||||
<mat-icon class="text-red-400 text-base" style="font-size:18px;width:18px;height:18px;">build</mat-icon>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">ค่าครุภัณฑ์</span>
|
||||
</div>
|
||||
<mat-icon class="text-gray-400 text-base">arrow_forward</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Sub-item: ค่าที่ดินและสิ่งก่อสร้าง -->
|
||||
<button class="w-full flex items-center justify-between bg-gray-50 hover:bg-red-50 border border-gray-200 hover:border-red-300 rounded-xl px-4 py-3 transition-colors cursor-pointer text-left"
|
||||
(click)="add_invest()">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-red-100 flex items-center justify-center">
|
||||
<mat-icon class="text-red-400 text-base" style="font-size:18px;width:18px;height:18px;">domain</mat-icon>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">ค่าที่ดินและสิ่งก่อสร้าง</span>
|
||||
</div>
|
||||
<mat-icon class="text-gray-400 text-base">arrow_forward</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: งบเงินอุดหนุน (blue) -->
|
||||
<div class="flex-1 bg-white rounded-2xl border-2 border-blue-400 p-5 flex flex-col gap-4">
|
||||
<!-- Card Header -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-14 h-14 rounded-xl bg-blue-50 flex items-center justify-center flex-shrink-0">
|
||||
<mat-icon class="text-blue-400" style="font-size:28px;width:28px;height:28px;">pool</mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 text-base">งบเงินอุดหนุน</h3>
|
||||
<p class="text-xs text-gray-400 mt-0.5">ดำเนินงาน · โครงการ · อื่น ๆ</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sub-items label -->
|
||||
<p class="text-xs text-gray-400 font-medium -mb-1">เลือกประเภทย่อย</p>
|
||||
|
||||
<!-- Sub-item: ค่าใช้จ่ายดำเนินการ -->
|
||||
<button class="w-full flex items-center justify-between bg-gray-50 hover:bg-blue-50 border border-gray-200 hover:border-blue-300 rounded-xl px-4 py-3 transition-colors cursor-pointer text-left"
|
||||
(click)="add_request_actualize()">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<mat-icon class="text-blue-400 text-base" style="font-size:18px;width:18px;height:18px;">settings</mat-icon>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">ค่าใช้จ่ายดำเนินการ</span>
|
||||
</div>
|
||||
<mat-icon class="text-gray-400 text-base">arrow_forward</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Sub-item: ค่าใช้จ่ายโครงการ -->
|
||||
<button class="w-full flex items-center justify-between bg-blue-500 border border-blue-500 rounded-xl px-4 py-3 transition-colors cursor-pointer text-left"
|
||||
(click)="add()">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-blue-400 flex items-center justify-center">
|
||||
<mat-icon class="text-white text-base" style="font-size:18px;width:18px;height:18px;">check</mat-icon>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-white">ค่าใช้จ่ายโครงการ</span>
|
||||
</div>
|
||||
<mat-icon class="text-white text-base">arrow_forward</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Sub-item: ค่าใช้จ่ายอื่น ๆ -->
|
||||
<button class="w-full flex items-center justify-between bg-gray-50 hover:bg-blue-50 border border-gray-200 hover:border-blue-300 rounded-xl px-4 py-3 transition-colors cursor-pointer text-left"
|
||||
(click)="addrequest7()">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<mat-icon class="text-blue-400 text-base" style="font-size:18px;width:18px;height:18px;">description</mat-icon>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">ค่าใช้จ่ายอื่น ๆ</span>
|
||||
</div>
|
||||
<mat-icon class="text-gray-400 text-base">arrow_forward</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: งบรายจ่ายอื่น (purple) -->
|
||||
<div class="flex-1 bg-white rounded-2xl border-2 border-purple-200 p-5 flex flex-col gap-4">
|
||||
<!-- Card Header -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-14 h-14 rounded-xl bg-purple-50 flex items-center justify-center flex-shrink-0">
|
||||
<mat-icon class="text-purple-400" style="font-size:28px;width:28px;height:28px;">card_giftcard</mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 text-base">งบรายจ่ายอื่น</h3>
|
||||
<p class="text-xs text-gray-400 mt-0.5">ดำเนินงาน · โครงการ</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sub-items label -->
|
||||
<p class="text-xs text-gray-400 font-medium -mb-1">เลือกประเภทย่อย</p>
|
||||
|
||||
<!-- Sub-item: ค่าใช้จ่ายดำเนินการ -->
|
||||
<button class="w-full flex items-center justify-between bg-gray-50 hover:bg-purple-50 border border-gray-200 hover:border-purple-300 rounded-xl px-4 py-3 transition-colors cursor-pointer text-left"
|
||||
(click)="add_request_other_expenses()">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-purple-100 flex items-center justify-center">
|
||||
<mat-icon class="text-purple-400 text-base" style="font-size:18px;width:18px;height:18px;">settings</mat-icon>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">ค่าใช้จ่ายดำเนินการ</span>
|
||||
</div>
|
||||
<mat-icon class="text-gray-400 text-base">arrow_forward</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Sub-item: ค่าใช้จ่ายโครงการ -->
|
||||
<button class="w-full flex items-center justify-between bg-gray-50 hover:bg-purple-50 border border-gray-200 hover:border-purple-300 rounded-xl px-4 py-3 transition-colors cursor-pointer text-left"
|
||||
(click)="add2()">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-purple-100 flex items-center justify-center">
|
||||
<mat-icon class="text-purple-400 text-base" style="font-size:18px;width:18px;height:18px;">layers</mat-icon>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">ค่าใช้จ่ายโครงการ</span>
|
||||
</div>
|
||||
<mat-icon class="text-gray-400 text-base">arrow_forward</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Register Component in Module + Add Child Route
|
||||
|
||||
**Files:**
|
||||
- Modify: `rmutr-web/src/app/feature/budget-request/request-budget/request-budget.module.ts`
|
||||
|
||||
- [ ] **Step 4: เพิ่ม import + declaration + route**
|
||||
|
||||
เปิดไฟล์ `request-budget.module.ts` แล้วแก้ไขดังนี้:
|
||||
|
||||
```typescript
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RequestBudgetContainer } from './container/request-budget/request-budget.container';
|
||||
import { RequestBudgetListComponent } from './presenter/list/request-budget-list/request-budget-list.component';
|
||||
import { RequestBudgetRouterComponent } from './router/request-budget-router/request-budget-router.component';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { SharedModule } from 'src/app/shared/shared.module';
|
||||
import { RequestBudgetSearchComponent } from './presenter/search/request-budget-search/request-budget-search.component';
|
||||
import { BudgetTypeSelectComponent } from './presenter/budget-type-select/budget-type-select.component'; // NEW
|
||||
import { TotalFootShowPipe } from './presenter/pipe/total-foot-table-show.pipe';
|
||||
import { TotalInvestShowPipe } from './presenter/pipe/total-invest-show.pipe copy';
|
||||
import { TotalRequestTopicShowPipe } from './presenter/pipe/total-request-topic-show.pipe2';
|
||||
import { TotalPlanInvestShowPipe } from './presenter/pipe/total-plan-invest-table-show.pipe';
|
||||
import { TotalPlanRequestShowPipe } from './presenter/pipe/total-plan-request-table-show.pipe';
|
||||
import { TotalPlanShowPipe } from './presenter/pipe/total-plan-table-show.pipe';
|
||||
import { TotalBudgetShowPipe } from './presenter/pipe/total-budget-show.pipe';
|
||||
import { TotalProgressPipe } from './presenter/pipe/total-progress.pipe copy';
|
||||
import { TotalHeadInvestShowPipe } from './presenter/pipe/total-head-invest-table-show.pipe';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: RequestBudgetRouterComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: RequestBudgetContainer
|
||||
},
|
||||
{
|
||||
path: 'select', // NEW — หน้าเลือกประเภทงบ
|
||||
component: BudgetTypeSelectComponent
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
RequestBudgetContainer,
|
||||
RequestBudgetRouterComponent,
|
||||
RequestBudgetListComponent,
|
||||
RequestBudgetSearchComponent,
|
||||
BudgetTypeSelectComponent, // NEW
|
||||
TotalFootShowPipe,
|
||||
TotalInvestShowPipe,
|
||||
TotalRequestTopicShowPipe,
|
||||
TotalPlanRequestShowPipe,
|
||||
TotalPlanInvestShowPipe,
|
||||
TotalPlanShowPipe,
|
||||
TotalBudgetShowPipe,
|
||||
TotalProgressPipe,
|
||||
TotalHeadInvestShowPipe
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
RouterModule.forChild(routes),
|
||||
]
|
||||
})
|
||||
export class RequestBudgetModule { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Replace 3 Buttons in Search Component
|
||||
|
||||
**Files:**
|
||||
- Modify: `rmutr-web/src/app/feature/budget-request/request-budget/presenter/search/request-budget-search/request-budget-search.component.html` (lines 86–118)
|
||||
- Modify: `rmutr-web/src/app/feature/budget-request/request-budget/presenter/search/request-budget-search/request-budget-search.component.ts`
|
||||
|
||||
- [ ] **Step 5: แทนที่ 3 ปุ่มใน search component HTML**
|
||||
|
||||
แทนที่บรรทัด 86–118 ใน `request-budget-search.component.html`:
|
||||
|
||||
**เดิม (ลบออก):**
|
||||
```html
|
||||
<div style="display: flex;justify-content: flex-end" *ngIf="keyUrl == 1">
|
||||
<!-- <button mat-raised-button class="buttonColorRed" (click)="add_invest()">งบลงทุน</button> -->
|
||||
<button mat-raised-button class="buttonColorReject" [matMenuTriggerFor]="menuinvest">งบลงทุน
|
||||
<span class="material-icons" style="color:white;">
|
||||
keyboard_arrow_down
|
||||
</span>
|
||||
</button>
|
||||
<mat-menu #menuinvest="matMenu" class="bg-white" >
|
||||
<button mat-menu-item (click)="add_invest_asset()">ค่าครุภัณฑ์</button>
|
||||
<button mat-menu-item (click)="add_invest()"> ค่าที่ดินและสิ่งก่อสร้าง </button>
|
||||
</mat-menu>
|
||||
<button mat-raised-button class="buttonColorBlue" [matMenuTriggerFor]="menu">งบเงินอุดหนุน
|
||||
<span class="material-icons" style="color:white;">
|
||||
keyboard_arrow_down
|
||||
</span>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu" class="bg-white">
|
||||
<button mat-menu-item (click)="add_request_actualize()">ค่าใช้จ่ายดำเนินการ</button>
|
||||
<button mat-menu-item (click)="add()"> ค่าใช้จ่ายโครงการ </button>
|
||||
<button mat-menu-item (click)="addrequest7()"> ค่าใช้จ่ายอื่น ๆ </button>
|
||||
</mat-menu>
|
||||
|
||||
<button mat-raised-button class="buttonColorPurple" [matMenuTriggerFor]="menu2">งบรายจ่ายอื่น
|
||||
<span class="material-icons" style="color:white;">
|
||||
keyboard_arrow_down
|
||||
</span>
|
||||
</button>
|
||||
<mat-menu #menu2="matMenu" class="bg-white">
|
||||
<button mat-menu-item (click)="add_request_other_expenses()">ค่าใช้จ่ายดำเนินการ</button>
|
||||
<button mat-menu-item (click)="add2()"> ค่าใช้จ่ายโครงการ </button>
|
||||
<!-- <button mat-menu-item (click)="addrequest7()"> ค่าใช้จ่ายอื่น ๆ </button> -->
|
||||
</mat-menu>
|
||||
</div>
|
||||
```
|
||||
|
||||
**ใหม่ (ใส่แทน):**
|
||||
```html
|
||||
<div style="display: flex;justify-content: flex-end" *ngIf="keyUrl == 1">
|
||||
<button mat-raised-button class="buttonColorBlue" (click)="openBudgetTypeSelect()">
|
||||
<span class="material-icons" style="color:white;margin-right:4px;">add</span>
|
||||
จัดทำคำของบประมาณ
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 6: เพิ่ม method ใน search component TS**
|
||||
|
||||
เปิดไฟล์ `request-budget-search.component.ts` แล้วเพิ่ม method `openBudgetTypeSelect()` ต่อท้าย method `add_request_other_expenses()` (ก่อนปิด class):
|
||||
|
||||
```typescript
|
||||
openBudgetTypeSelect() {
|
||||
this.router.navigate(['app/request-budget/select']);
|
||||
}
|
||||
```
|
||||
|
||||
> **หมายเหตุ:** methods เดิม (add, add2, add_invest_asset, add_invest, addrequest7, add_request_actualize, add_request_other_expenses) ไม่ต้องลบออก เผื่อยังมีที่อื่นเรียกใช้
|
||||
|
||||
- [ ] **Step 7: ตรวจสอบ Angular dev server compile ไม่มี error**
|
||||
|
||||
เปิด terminal แล้วดู log ที่ `/tmp/rmutr-web.log` — ต้องไม่มีบรรทัดที่ขึ้นต้นด้วย `ERROR`
|
||||
|
||||
- [ ] **Step 8: เปิด browser ที่ `http://localhost:4200/app/request-budget`**
|
||||
|
||||
ตรวจสอบ:
|
||||
1. ปุ่ม "จัดทำคำของบประมาณ" ปรากฏแทนที่ 3 ปุ่มเดิม
|
||||
2. กดปุ่มแล้ว navigate ไปที่ `/app/request-budget/select`
|
||||
3. หน้า select แสดง 3 card ครบถ้วน (งบลงทุน, งบเงินอุดหนุน, งบรายจ่ายอื่น)
|
||||
4. กด back arrow กลับมาหน้า list ได้
|
||||
5. กด sub-item ใดก็ navigate ไปยัง route ที่ถูกต้อง
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add rmutr-web/src/app/feature/budget-request/request-budget/presenter/budget-type-select/
|
||||
git add rmutr-web/src/app/feature/budget-request/request-budget/request-budget.module.ts
|
||||
git add rmutr-web/src/app/feature/budget-request/request-budget/presenter/search/request-budget-search/request-budget-search.component.html
|
||||
git add rmutr-web/src/app/feature/budget-request/request-budget/presenter/search/request-budget-search/request-budget-search.component.ts
|
||||
git commit -m "feat: add budget-type-select menu page, replace 3 dropdown buttons"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
- [x] **Spec coverage:** 3 ปุ่มถูกแยกออก ✓ | หน้าใหม่ออกแบบตาม design image ✓ | back button ✓ | sub-item navigation ครบ 7 รายการ ✓
|
||||
- [x] **No placeholders:** ทุก step มี code จริง ✓
|
||||
- [x] **Type consistency:** `BudgetTypeSelectComponent` ใช้ชื่อเดียวกันในทุก task ✓ | route path `select` ตรงกัน ✓ | method names ตรงกับ `RequestPlanUidService` ✓
|
||||
- [x] **`addrequest7` ใช้ `RequestPlanUidService`** — inject ใน Task 1 และใช้ใน method ✓
|
||||
- [x] **`RequestBudgetRouterComponent` มี `<router-outlet>`** — child route `select` จะทำงานได้ ✓
|
||||
@@ -0,0 +1,458 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="th">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>25. ตารางกำหนดการดำเนินโครงการ — Prototype</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: 'Sarabun', sans-serif; background: #f5f5f5; padding: 24px; color: #333; }
|
||||
@import url('https://fonts.googleapis.com/css2?family=Sarabun:wght@400;500;600&display=swap');
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #BD413A;
|
||||
}
|
||||
|
||||
/* ปุ่มหลัก */
|
||||
.btn-primary {
|
||||
background: #BD413A;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.btn-primary:hover { background: #a33630; }
|
||||
|
||||
.btn-secondary {
|
||||
background: #6d7da5;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.btn-secondary:hover { background: #566082; }
|
||||
|
||||
.btn-text {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6d7da5;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
padding: 4px 6px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.btn-text:hover { text-decoration: underline; }
|
||||
|
||||
.btn-danger-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #BD413A;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
.btn-danger-icon:hover { color: #a33630; }
|
||||
|
||||
/* บล็อกวันที่ */
|
||||
.date-block {
|
||||
margin-bottom: 24px;
|
||||
border-top: 1px dashed #d7d7d7;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.date-block:first-child { border-top: none; padding-top: 0; }
|
||||
|
||||
.date-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.date-row label { font-weight: 500; font-size: 14px; color: #555; white-space: nowrap; }
|
||||
|
||||
/* input วันที่ */
|
||||
.input-date {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
width: 160px;
|
||||
}
|
||||
.input-date:focus { outline: none; border-color: #6d7da5; }
|
||||
|
||||
/* ตาราง */
|
||||
.schedule-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
.schedule-table th {
|
||||
background: #f3f4f6;
|
||||
color: #555;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.schedule-table td {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #e8e8e8;
|
||||
vertical-align: top;
|
||||
}
|
||||
.schedule-table tr:hover td { background: #fafafa; }
|
||||
|
||||
/* input ในตาราง */
|
||||
.tbl-input {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 5px 8px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
.tbl-input:focus { outline: none; border-color: #6d7da5; box-shadow: 0 0 0 2px rgba(109,125,165,.15); }
|
||||
|
||||
.tbl-input-time { width: 90px; }
|
||||
|
||||
/* badge ประเภท */
|
||||
.type-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.type-บรรยาย { background: #e3f0fb; color: #1565c0; }
|
||||
.type-ปฏิบัติ { background: #e8f5e9; color: #2e7d32; }
|
||||
.type-แบ่งกลุ่ม { background: #fff3e0; color: #e65100; }
|
||||
.type-นำเสนอ { background: #f3e5f5; color: #6a1b9a; }
|
||||
.type-ศึกษาดูงาน{ background: #fce4ec; color: #ad1457; }
|
||||
.type-อื่นๆ { background: #f5f5f5; color: #555; }
|
||||
|
||||
/* lecturer row */
|
||||
.lec-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.lec-row .tbl-input { width: 130px; }
|
||||
.radio-group { display: flex; gap: 8px; white-space: nowrap; font-size: 12px; align-items: center; }
|
||||
.radio-group label { display: flex; align-items: center; gap: 3px; cursor: pointer; }
|
||||
.radio-group input[type=radio] { accent-color: #BD413A; cursor: pointer; }
|
||||
|
||||
/* แถว เพิ่มแถว */
|
||||
.add-row-cell {
|
||||
text-align: right;
|
||||
padding: 6px 8px !important;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
/* hint */
|
||||
.hint {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* no data */
|
||||
.empty-row td {
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
font-size: 13px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* wrapper autocomplete */
|
||||
.autocomplete-wrap { position: relative; }
|
||||
.autocomplete-wrap datalist { display: none; }
|
||||
|
||||
.type-preview-wrap { min-height: 20px; }
|
||||
|
||||
/* footer */
|
||||
.page-footer {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="card">
|
||||
|
||||
<div class="section-header">
|
||||
<span class="section-title">25. ตารางกำหนดการดำเนินโครงการ</span>
|
||||
<button class="btn-primary" onclick="addDateBlock()">+ เพิ่มวันที่</button>
|
||||
</div>
|
||||
|
||||
<div id="date-blocks-container">
|
||||
<!-- Date blocks inserted here -->
|
||||
</div>
|
||||
|
||||
<div class="page-footer">
|
||||
<button class="btn-secondary" style="background:#888;">ย้อนกลับ</button>
|
||||
<button class="btn-primary" onclick="alert('บันทึกสำเร็จ!')">บันทึก</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Datalist สำหรับ autocomplete ประเภท -->
|
||||
<datalist id="topic-types-list">
|
||||
<option value="บรรยาย">
|
||||
<option value="ปฏิบัติ">
|
||||
<option value="อภิปราย">
|
||||
<option value="แบ่งกลุ่มปฏิบัติ">
|
||||
<option value="นำเสนอผลงาน">
|
||||
<option value="ศึกษาดูงาน">
|
||||
</datalist>
|
||||
|
||||
<!-- Datalist สำหรับ autocomplete เวลา -->
|
||||
<datalist id="time-list">
|
||||
<option value="08:00">
|
||||
<option value="08:30">
|
||||
<option value="09:00">
|
||||
<option value="09:30">
|
||||
<option value="10:00">
|
||||
<option value="10:30">
|
||||
<option value="11:00">
|
||||
<option value="11:30">
|
||||
<option value="12:00">
|
||||
<option value="12:30">
|
||||
<option value="13:00">
|
||||
<option value="13:30">
|
||||
<option value="14:00">
|
||||
<option value="14:30">
|
||||
<option value="15:00">
|
||||
<option value="15:30">
|
||||
<option value="16:00">
|
||||
<option value="16:30">
|
||||
<option value="17:00">
|
||||
<option value="17:30">
|
||||
<option value="18:00">
|
||||
<option value="18:30">
|
||||
<option value="19:00">
|
||||
<option value="19:30">
|
||||
<option value="20:00">
|
||||
</datalist>
|
||||
|
||||
<script>
|
||||
let dateBlockCount = 0;
|
||||
|
||||
function typeBadgeClass(v) {
|
||||
if (!v) return '';
|
||||
if (v.includes('บรรยาย')) return 'type-บรรยาย';
|
||||
if (v.includes('ปฏิบัติ') && !v.includes('แบ่ง')) return 'type-ปฏิบัติ';
|
||||
if (v.includes('แบ่งกลุ่ม')) return 'type-แบ่งกลุ่ม';
|
||||
if (v.includes('นำเสนอ')) return 'type-นำเสนอ';
|
||||
if (v.includes('ศึกษาดูงาน')) return 'type-ศึกษาดูงาน';
|
||||
if (v.includes('อภิปราย')) return 'type-นำเสนอ';
|
||||
return 'type-อื่นๆ';
|
||||
}
|
||||
|
||||
function onTypeInput(input) {
|
||||
const wrap = input.parentElement.querySelector('.type-preview-wrap');
|
||||
const v = input.value.trim();
|
||||
if (v) {
|
||||
wrap.innerHTML = `<span class="type-badge ${typeBadgeClass(v)}">${v}</span>`;
|
||||
} else {
|
||||
wrap.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function addDateBlock() {
|
||||
dateBlockCount++;
|
||||
const id = 'db-' + dateBlockCount;
|
||||
const today = new Date().toLocaleDateString('th-TH', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
||||
|
||||
const container = document.getElementById('date-blocks-container');
|
||||
|
||||
const block = document.createElement('div');
|
||||
block.className = 'date-block';
|
||||
block.id = id;
|
||||
|
||||
block.innerHTML = `
|
||||
<div class="date-row">
|
||||
<label>วันที่</label>
|
||||
<input type="date" class="input-date" value="${new Date().toISOString().split('T')[0]}">
|
||||
<button class="btn-secondary" onclick="addRow('${id}')">+ เพิ่มแถว</button>
|
||||
<button class="btn-danger-icon" title="ลบวันที่นี้" onclick="removeDateBlock('${id}')">✕</button>
|
||||
</div>
|
||||
|
||||
<table class="schedule-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:100px">เวลาเริ่มต้น</th>
|
||||
<th style="width:100px">เวลาสิ้นสุด</th>
|
||||
<th style="width:150px">ประเภท</th>
|
||||
<th>หัวข้อ</th>
|
||||
<th style="width:220px">วิทยากร</th>
|
||||
<th style="width:40px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="${id}-tbody">
|
||||
<tr class="empty-row" id="${id}-empty">
|
||||
<td colspan="6">ยังไม่มีรายการ — กด <strong>+ เพิ่มแถว</strong> เพื่อเพิ่ม</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
container.appendChild(block);
|
||||
}
|
||||
|
||||
let rowCount = 0;
|
||||
|
||||
function addRow(blockId) {
|
||||
rowCount++;
|
||||
const rid = 'row-' + rowCount;
|
||||
const tbody = document.getElementById(blockId + '-tbody');
|
||||
const empty = document.getElementById(blockId + '-empty');
|
||||
if (empty) empty.remove();
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.id = rid;
|
||||
tr.innerHTML = `
|
||||
<td><input type="text" class="tbl-input tbl-input-time" list="time-list" placeholder="08:00" oninput="syncTime(this, '${rid}')"></td>
|
||||
<td><input type="text" class="tbl-input tbl-input-time" list="time-list" placeholder="09:00" id="${rid}-totime"></td>
|
||||
<td>
|
||||
<input type="text" class="tbl-input" list="topic-types-list" placeholder="เลือกหรือพิมพ์เอง" oninput="onTypeInput(this)">
|
||||
<div class="type-preview-wrap"></div>
|
||||
<div class="hint">เลือกจากรายการหรือพิมพ์เอง</div>
|
||||
</td>
|
||||
<td><input type="text" class="tbl-input" placeholder="หัวข้อ / รายละเอียด"></td>
|
||||
<td>
|
||||
<div id="${rid}-lecturers"></div>
|
||||
<button class="btn-text" onclick="addLecturer('${rid}')">+ วิทยากร</button>
|
||||
</td>
|
||||
<td style="text-align:center;">
|
||||
<button class="btn-danger-icon" title="ลบแถว" onclick="removeRow('${rid}', '${blockId}')">✕</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
function syncTime(fromInput, rid) {
|
||||
// auto-fill เวลาสิ้นสุดถ้ายังว่าง
|
||||
const toInput = document.getElementById(rid + '-totime');
|
||||
if (toInput && !toInput.value) {
|
||||
const [h, m] = (fromInput.value || '').split(':').map(Number);
|
||||
if (!isNaN(h)) {
|
||||
const endH = h + 1;
|
||||
toInput.value = `${String(endH).padStart(2,'0')}:${String(m||0).padStart(2,'0')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lecCount = 0;
|
||||
|
||||
function addLecturer(rowId) {
|
||||
lecCount++;
|
||||
const lid = 'lec-' + lecCount;
|
||||
const container = document.getElementById(rowId + '-lecturers');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'lec-row';
|
||||
div.id = lid;
|
||||
div.innerHTML = `
|
||||
<input type="text" class="tbl-input" placeholder="ชื่อวิทยากร" style="width:130px">
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="${lid}-type" value="inside" checked> ภายใน</label>
|
||||
<label><input type="radio" name="${lid}-type" value="outside"> ภายนอก</label>
|
||||
</div>
|
||||
<button class="btn-danger-icon" style="font-size:14px" title="ลบวิทยากร" onclick="removeLec('${lid}')">✕</button>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function removeLec(id) {
|
||||
document.getElementById(id)?.remove();
|
||||
}
|
||||
|
||||
function removeRow(rowId, blockId) {
|
||||
document.getElementById(rowId)?.remove();
|
||||
const tbody = document.getElementById(blockId + '-tbody');
|
||||
if (!tbody) return;
|
||||
const rows = tbody.querySelectorAll('tr:not(.empty-row)');
|
||||
if (rows.length === 0) {
|
||||
const empty = document.createElement('tr');
|
||||
empty.className = 'empty-row';
|
||||
empty.id = blockId + '-empty';
|
||||
empty.innerHTML = '<td colspan="6">ยังไม่มีรายการ — กด <strong>+ เพิ่มแถว</strong> เพื่อเพิ่ม</td>';
|
||||
tbody.appendChild(empty);
|
||||
}
|
||||
}
|
||||
|
||||
function removeDateBlock(id) {
|
||||
if (confirm('ต้องการลบวันที่นี้และรายการทั้งหมดใช่ไหม?')) {
|
||||
document.getElementById(id)?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// เพิ่ม demo block เริ่มต้น 1 วัน
|
||||
addDateBlock();
|
||||
// เพิ่ม demo row
|
||||
setTimeout(() => {
|
||||
addRow('db-1');
|
||||
// fill demo data
|
||||
const rows = document.querySelectorAll('#db-1-tbody tr:not(.empty-row)');
|
||||
if (rows[0]) {
|
||||
const inputs = rows[0].querySelectorAll('input');
|
||||
inputs[0].value = '09:00';
|
||||
inputs[1].value = '12:00';
|
||||
inputs[2].value = 'บรรยาย';
|
||||
onTypeInput(inputs[2]);
|
||||
inputs[3].value = 'การเรียนรู้เชิงสร้างสรรค์ในยุคดิจิทัล';
|
||||
}
|
||||
addRow('db-1');
|
||||
setTimeout(() => {
|
||||
const rows2 = document.querySelectorAll('#db-1-tbody tr:not(.empty-row)');
|
||||
if (rows2[1]) {
|
||||
const inputs = rows2[1].querySelectorAll('input');
|
||||
inputs[0].value = '13:00';
|
||||
inputs[1].value = '16:00';
|
||||
inputs[2].value = 'แบ่งกลุ่มปฏิบัติ';
|
||||
onTypeInput(inputs[2]);
|
||||
inputs[3].value = 'Workshop: ออกแบบกิจกรรมการเรียนรู้';
|
||||
addLecturer('row-2');
|
||||
setTimeout(() => {
|
||||
const lec = document.querySelector('#row-2-lecturers .lec-row input');
|
||||
if (lec) lec.value = 'ผศ.ดร.สมชาย ใจดี';
|
||||
}, 0);
|
||||
}
|
||||
}, 0);
|
||||
}, 0);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
-- Sync budget_come_from = result for all affected tables
|
||||
-- Run this script when DB connection is restored
|
||||
|
||||
-- 1. analysis_5 status_id=1,2,3 (non-research, แผ่นดิน)
|
||||
UPDATE "BUDGET"."request_budget_analysis_5" a5
|
||||
SET budget_come_from = rbs3.result
|
||||
FROM "BUDGET"."request_budget" rb3
|
||||
JOIN "BUDGET"."request_budget_summarize" rbs3 ON rbs3.request_budget_uid = rb3.request_budget_uid
|
||||
WHERE rb3.status_id = 3
|
||||
AND rb3.project_name_th = a5.project_name_th
|
||||
AND a5.api = 'request_budget'
|
||||
AND a5.status_id IN (1,2,3)
|
||||
AND COALESCE(a5.budget_come_from,'') != COALESCE(rbs3.result,'');
|
||||
|
||||
-- 2. analysis_5 status_id=1,2,3 (non-research, รายได้)
|
||||
UPDATE "BUDGET"."request_budget_analysis_5" a5
|
||||
SET budget_come_from = rbs3.result
|
||||
FROM "BUDGET"."request_budget_income" rb3
|
||||
JOIN "BUDGET"."request_budget_income_summarize" rbs3 ON rbs3.request_budget_income_uid = rb3.request_budget_income_uid
|
||||
WHERE rb3.status_id = 3
|
||||
AND rb3.project_name_th = a5.project_name_th
|
||||
AND a5.api = 'request_budget_income'
|
||||
AND a5.status_id IN (1,2,3)
|
||||
AND COALESCE(a5.budget_come_from,'') != COALESCE(rbs3.result,'');
|
||||
|
||||
-- 3. analysis_5_research (via project_name_th join)
|
||||
UPDATE "BUDGET"."request_budget_analysis_5_research" a5r
|
||||
SET budget_come_from = rbs3.result
|
||||
FROM "BUDGET"."request_budget" rb3
|
||||
JOIN "BUDGET"."request_budget_summarize" rbs3 ON rbs3.request_budget_uid = rb3.request_budget_uid
|
||||
WHERE rb3.status_id = 3
|
||||
AND rb3.project_name_th = a5r.project_name_th
|
||||
AND COALESCE(a5r.budget_come_from,'') != COALESCE(rbs3.result,'');
|
||||
|
||||
-- 4. change_project_detail (แผ่นดิน)
|
||||
UPDATE "BUDGET"."change_project_detail" cpd
|
||||
SET budget_come_from = rbs3.result
|
||||
FROM "BUDGET"."request_budget" rb3
|
||||
JOIN "BUDGET"."request_budget_summarize" rbs3 ON rbs3.request_budget_uid = rb3.request_budget_uid
|
||||
WHERE rb3.status_id = 3
|
||||
AND rb3.project_name_th = cpd.project_name_th
|
||||
AND (cpd.api = 'request_budget' OR cpd.api IS NULL)
|
||||
AND COALESCE(cpd.budget_come_from,'') != COALESCE(rbs3.result,'');
|
||||
|
||||
-- 5. change_project_detail (รายได้)
|
||||
UPDATE "BUDGET"."change_project_detail" cpd
|
||||
SET budget_come_from = rbs3.result
|
||||
FROM "BUDGET"."request_budget_income" rb3
|
||||
JOIN "BUDGET"."request_budget_income_summarize" rbs3 ON rbs3.request_budget_income_uid = rb3.request_budget_income_uid
|
||||
WHERE rb3.status_id = 3
|
||||
AND rb3.project_name_th = cpd.project_name_th
|
||||
AND cpd.api = 'request_budget_income'
|
||||
AND COALESCE(cpd.budget_come_from,'') != COALESCE(rbs3.result,'');
|
||||
Reference in New Issue
Block a user