2fbb0b3d51
- 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>
459 lines
14 KiB
HTML
459 lines
14 KiB
HTML
<!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>
|