Initial commit

This commit is contained in:
Porawit Dongwang
2026-09-16 23:20:08 +07:00
commit 0041668dbb
32577 changed files with 3687927 additions and 0 deletions
@@ -0,0 +1,154 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-gray-800">ตั้งค่า Google Sheets Sync</h1>
<a href="<?= BASE_URL ?>/settings" class="btn btn-outline-secondary shadow-sm"><i class="fa-solid fa-arrow-left me-1"></i> กลับ</a>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card shadow-sm border-0 rounded-4 mb-4 overflow-hidden">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-success"><i class="fa-solid fa-plug me-2"></i>รายละเอียดการเชื่อมต่อ</h6>
</div>
<div class="card-body">
<form action="<?= BASE_URL ?>/settings/google-sheets/save" method="POST">
<input type="hidden" name="csrf_token" value="<?= $csrf_token ?>">
<div class="mb-3">
<label class="form-label fw-bold">รูปแบบการเชื่อมต่อ (Connection Type)</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="connection_type" id="connAPI" value="API" <?= ($setting['connection_type'] ?? 'API') === 'API' ? 'checked' : '' ?>>
<label class="form-check-label" for="connAPI">ใช้ Google API Key (มาตรฐาน)</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="connection_type" id="connPublic" value="Public" <?= ($setting['connection_type'] ?? '') === 'Public' ? 'checked' : '' ?>>
<label class="form-check-label" for="connPublic">ดึงจากไฟล์ Public (ไม่ต้องใช้ API Key)</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Spreadsheet ID</label>
<input type="text" class="form-control" name="spreadsheet_id" value="<?= htmlspecialchars($setting['spreadsheet_id'] ?? '') ?>" placeholder="เช่น 1BxiMVs0XRYFgwnTE...">
<div class="form-text">รหัส 44 ตัวอักษรที่อยู่ใน URL ของ Google Sheets</div>
</div>
<div class="mb-3" id="apiKeyContainer">
<label class="form-label fw-bold">Google API Key</label>
<input type="text" class="form-control" name="api_key" value="<?= htmlspecialchars($setting['api_key'] ?? '') ?>" placeholder="เช่น AIzaSyC...">
<div class="form-text">รหัส API Key จาก Google Cloud Console (จำเป็นสำหรับการดึงข้อมูลจริง)</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label fw-bold">Sheet Name</label>
<input type="text" class="form-control" name="sheet_name" value="<?= htmlspecialchars($setting['sheet_name'] ?? 'Sheet1') ?>" placeholder="เช่น Sheet1">
</div>
<div class="col-md-6">
<label class="form-label fw-bold">Sync Range</label>
<input type="text" class="form-control" name="sync_range" value="<?= htmlspecialchars($setting['sync_range'] ?? 'A2:B') ?>" placeholder="เช่น A2:B">
</div>
</div>
<div class="mb-4">
<label class="form-label fw-bold">Column Mapping (JSON)</label>
<textarea class="form-control text-monospace" name="column_mapping" rows="4"><?= htmlspecialchars($setting['column_mapping'] ?? '{"1": "doc_number", "4": "doc_no", "5": "title", "10": "doc_date", "11": "doc_link"}') ?></textarea>
<div class="form-text">รูปแบบ JSON เพื่อแมปคอลัมน์จาก Sheet (Index 0) เข้าสู่ฐานข้อมูล</div>
</div>
<button type="submit" class="btn btn-success"><i class="fa-solid fa-save me-1"></i> บันทึกการตั้งค่า</button>
<button type="button" class="btn btn-outline-primary ms-2" id="testSyncBtn"><i class="fa-solid fa-rotate me-1"></i> ทดสอบดึงข้อมูล</button>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm border-0 rounded-4 bg-success text-white overflow-hidden">
<div class="card-body p-4 text-center">
<i class="fa-solid fa-cloud-arrow-down fa-3x mb-3 opacity-75"></i>
<h5>สถานะการเชื่อมต่อ</h5>
<p class="mb-0">
<?php if(!empty($setting['last_synced_at'])): ?>
<span class="badge bg-light text-success mb-2">ทำงานปกติ</span><br>
<small>ดึงข้อมูลล่าสุด: <?= date('d/m/Y H:i', strtotime($setting['last_synced_at'])) ?></small>
<?php else: ?>
<span class="badge bg-warning text-dark mb-2">ยังไม่เคยดึงข้อมูล</span>
<?php endif; ?>
</p>
</div>
</div>
<div class="card shadow-sm border-0 rounded-4 mt-4 overflow-hidden">
<div class="card-body">
<h6 class="fw-bold"><i class="fa-solid fa-circle-info text-info me-2"></i>ข้อควรระวัง</h6>
<ul class="small text-muted ps-3 mb-0">
<li class="mb-1">ต้องแชร์ Spreadsheet ให้อ่านได้ (Anyone with the link) หรือผ่าน Service Account</li>
<li class="mb-1">วันที่ใน Google Sheets ควรใช้ Format `YYYY-MM-DD` เพื่อง่ายต่อการนำเข้า MySQL</li>
<li>หากต้องการดึงอัตโนมัติ ต้องตั้ง Cron Job เรียกใช้ API Endpoint ของระบบ</li>
</ul>
</div>
</div>
</div>
</div>
<?php
$extraScripts = '
<script>
document.addEventListener("DOMContentLoaded", function() {
const connAPI = document.getElementById("connAPI");
const connPublic = document.getElementById("connPublic");
const apiKeyContainer = document.getElementById("apiKeyContainer");
function toggleApiKey() {
if (connAPI.checked) {
apiKeyContainer.style.display = "block";
} else {
apiKeyContainer.style.display = "none";
}
}
connAPI.addEventListener("change", toggleApiKey);
connPublic.addEventListener("change", toggleApiKey);
toggleApiKey(); // Initial check
});
document.getElementById("testSyncBtn").addEventListener("click", function() {
const btn = this;
btn.disabled = true;
btn.innerHTML = \'<i class="fa-solid fa-spinner fa-spin me-1"></i> กำลังทดสอบ...\';
fetch("' . BASE_URL . '/settings/google-sheets/test", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "csrf_token=' . $csrf_token . '"
})
.then(async res => {
const text = await res.text();
try {
return JSON.parse(text);
} catch (e) {
// คืนค่า Text ดิบๆ ไปเลยเพื่อให้ User เห็นว่า Server ฟ้องว่าอะไร
throw new Error("Server ไม่ได้ตอบกลับเป็น JSON: " + text.substring(0, 100));
}
})
.then(data => {
btn.disabled = false;
btn.innerHTML = \'<i class="fa-solid fa-rotate me-1"></i> ทดสอบดึงข้อมูล\';
if (data.status === "success") {
alert("เชื่อมต่อสำเร็จ! พบข้อมูล " + data.rows + " แถว\\nตัวอย่างรายการแรก: " + JSON.stringify(data.preview));
} else {
alert("เกิดข้อผิดพลาด: " + data.message);
}
})
.catch(err => {
btn.disabled = false;
btn.innerHTML = \'<i class="fa-solid fa-rotate me-1"></i> ทดสอบดึงข้อมูล\';
alert("Error: " + err.message);
});
});
</script>
';
?>
@@ -0,0 +1,129 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-gray-800">ตั้งค่าสายงานอนุมัติ (Approval Hierarchy)</h1>
<a href="<?= BASE_URL ?>/settings" class="btn btn-outline-secondary shadow-sm"><i class="fa-solid fa-arrow-left me-1"></i> กลับไปตั้งค่า</a>
</div>
<div class="row">
<!-- ฟอร์มเพิ่มสายงานอนุมัติ -->
<div class="col-lg-4">
<div class="card shadow-sm border-0 rounded-4 mb-4 overflow-hidden">
<div class="card-header bg-primary text-white py-3">
<h6 class="m-0 font-weight-bold"><i class="fa-solid fa-sitemap me-2"></i>กำหนดผู้อนุมัติ</h6>
</div>
<div class="card-body">
<form action="<?= BASE_URL ?>/settings/hierarchy/add" method="POST">
<input type="hidden" name="csrf_token" value="<?= $csrf_token ?>">
<div class="mb-3">
<label class="form-label fw-bold">พนักงาน (ผู้ขออนุมัติ)</label>
<select class="form-control userSelect" name="user_id" required>
<option value="">-- พิมพ์ชื่อเพื่อค้นหาพนักงาน --</option>
</select>
</div>
<div class="mb-4">
<label class="form-label fw-bold">ผู้อนุมัติ (Approver)</label>
<select class="form-control userSelect" name="approver_id" required>
<option value="">-- พิมพ์ชื่อเพื่อค้นหาผู้อนุมัติ --</option>
</select>
</div>
<button type="submit" class="btn btn-primary w-100"><i class="fa-solid fa-save me-1"></i> บันทึกข้อมูล</button>
</form>
</div>
</div>
</div>
<!-- ตารางสายงานอนุมัติ -->
<div class="col-lg-8">
<div class="card shadow-sm border-0 rounded-4 mb-4 overflow-hidden">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary"><i class="fa-solid fa-list me-2"></i>รายชื่อสายงานอนุมัติในระบบ</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover align-middle" id="hierarchyTable">
<thead class="table-light">
<tr>
<th>พนักงาน</th>
<th>ผู้อนุมัติ</th>
<th class="text-center" width="100">จัดการ</th>
</tr>
</thead>
<tbody>
<?php if (empty($hierarchy)): ?>
<tr>
<td colspan="3" class="text-center py-3">ยังไม่มีการกำหนดสายงานอนุมัติ</td>
</tr>
<?php else: ?>
<?php foreach ($hierarchy as $h): ?>
<tr>
<td>
<i class="fa-solid fa-user me-2 text-muted"></i>
<?= htmlspecialchars($h['user_name']) ?>
<small class="text-muted d-block">(<?= htmlspecialchars($h['user_id']) ?>)</small>
</td>
<td>
<i class="fa-solid fa-user-check me-2 text-success"></i>
<?= htmlspecialchars($h['approver_name']) ?>
<small class="text-muted d-block">(<?= htmlspecialchars($h['approver_id']) ?>)</small>
</td>
<td class="text-center">
<form action="<?= BASE_URL ?>/settings/hierarchy/remove" method="POST" onsubmit="return confirm('ยืนยันการลบข้อมูลนี้?');">
<input type="hidden" name="csrf_token" value="<?= $csrf_token ?>">
<input type="hidden" name="id" value="<?= $h['id'] ?>">
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="fa-solid fa-trash"></i></button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php
// เพิ่ม Select2 CSS & JS
$extraScripts = '
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
if($("#hierarchyTable tbody tr td").length > 1) {
$("#hierarchyTable").DataTable({
"language": {
"url": "//cdn.datatables.net/plug-ins/1.13.6/i18n/th.json"
}
});
}
$(".userSelect").select2({
theme: "bootstrap-5",
allowClear: true,
ajax: {
url: "'.BASE_URL.'/settings/search-user",
dataType: "json",
delay: 250,
data: function (params) {
return {
q: params.term
};
},
processResults: function (data) {
return {
results: data
};
},
cache: true
},
minimumInputLength: 2
});
});
</script>
';
?>
@@ -0,0 +1,136 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-gray-800">ตั้งค่าระบบ</h1>
</div>
<div class="row">
<!-- Roles Management Card -->
<div class="col-md-4 mb-4">
<div class="card shadow-sm border-0 h-100 rounded-4">
<div class="card-body text-center p-5 d-flex flex-column">
<i class="fa-solid fa-users-gear fa-4x text-info mb-3"></i>
<h4 class="card-title fw-bold">จัดการสิทธิ์ (Roles)</h4>
<p class="card-text text-muted mb-4">
กำหนดสิทธิ์ผู้ใช้งาน เช่น ผู้ดูแลระบบ, ผู้อนุมัติระดับ 1, 2
</p>
<a href="<?= BASE_URL ?>/roles" class="btn btn-outline-info px-4 mt-auto">
<i class="fa-solid fa-user-plus me-1"></i> จัดการสิทธิ์
</a>
</div>
</div>
</div>
<!-- PDF Template Setup Card -->
<div class="col-md-4 mb-4">
<div class="card shadow-sm border-0 h-100 rounded-4">
<div class="card-body text-center p-5 d-flex flex-column">
<i class="fa-solid fa-file-pdf fa-4x text-danger mb-3"></i>
<h4 class="card-title fw-bold">ฟอร์ม PDF ต้นฉบับ</h4>
<p class="card-text text-muted mb-4">
ออกแบบและจัดวางตำแหน่งข้อมูลบนฟอร์ม PDF ด้วยระบบลากวาง (Drag & Drop)
</p>
<a href="<?= BASE_URL ?>/settings/pdf-designer" class="btn btn-outline-danger px-4 mt-auto">
<i class="fa-solid fa-pen-ruler me-1"></i> เข้าสู่หน้าออกแบบ
</a>
</div>
</div>
</div>
<!-- Google Sheets Setup Card -->
<div class="col-md-4 mb-4">
<div class="card shadow-sm border-0 h-100 rounded-4">
<div class="card-body text-center p-5 d-flex flex-column">
<i class="fa-solid fa-table fa-4x text-success mb-3"></i>
<h4 class="card-title fw-bold">Google Sheets Integration</h4>
<p class="card-text text-muted mb-4">
ตั้งค่าการเชื่อมต่อ API ของ Google Sheets เพี่อซิงค์ข้อมูลลงฐานข้อมูล กำหนด Column Mapping และ Range ของข้อมูล
</p>
<a href="<?= BASE_URL ?>/settings/google-sheets" class="btn btn-outline-success px-4 mt-auto">
<i class="fa-solid fa-plug me-1"></i> ตั้งค่าการเชื่อมต่อ
</a>
</div>
</div>
</div>
<!-- Approval Hierarchy Card -->
<div class="col-md-4 mb-4">
<div class="card shadow-sm border-0 h-100 rounded-4">
<div class="card-body text-center p-5 d-flex flex-column">
<i class="fa-solid fa-sitemap fa-4x text-warning mb-3"></i>
<h4 class="card-title fw-bold">สายงานอนุมัติ</h4>
<p class="card-text text-muted mb-4">
ตั้งค่าผู้อนุมัติสำหรับพนักงานแต่ละคน เพื่อให้ระบบส่งเอกสารไปยังผู้อนุมัติได้โดยอัตโนมัติ
</p>
<a href="<?= BASE_URL ?>/settings/hierarchy" class="btn btn-outline-warning px-4 mt-auto">
<i class="fa-solid fa-users me-1"></i> ตั้งค่าผู้อนุมัติ
</a>
</div>
</div>
</div>
<!-- LINE Notify Card -->
<div class="col-md-4 mb-4">
<div class="card shadow-sm border-0 h-100 rounded-4">
<div class="card-body text-center p-5 d-flex flex-column">
<i class="fa-brands fa-line fa-4x text-success mb-3"></i>
<h4 class="card-title fw-bold">แจ้งเตือน (LINE)</h4>
<p class="card-text text-muted mb-4">
ตั้งค่า Token ของ LINE Notify เพื่อให้ระบบสามารถส่งข้อความแจ้งเตือนเมื่อมีคำขอหรือมีการอนุมัติ
</p>
<a href="<?= BASE_URL ?>/settings/line-notify" class="btn btn-outline-success px-4 mt-auto">
<i class="fa-solid fa-bell me-1"></i> ตั้งค่าแจ้งเตือน
</a>
</div>
</div>
</div>
<!-- Telegram Notify Card -->
<div class="col-md-4 mb-4">
<div class="card shadow-sm border-0 h-100 rounded-4">
<div class="card-body text-center p-5 d-flex flex-column">
<i class="fa-brands fa-telegram fa-4x text-info mb-3"></i>
<h4 class="card-title fw-bold">แจ้งเตือน (Telegram)</h4>
<p class="card-text text-muted mb-4">
เชื่อมต่อกับบอท Telegram ของคุณ เพื่อส่งข้อความแจ้งเตือนแบบเรียลไทม์
</p>
<a href="<?= BASE_URL ?>/settings/telegram-notify" class="btn btn-outline-info px-4 mt-auto">
<i class="fa-solid fa-paper-plane me-1"></i> ตั้งค่า Telegram
</a>
</div>
</div>
</div>
<!-- Appearance & Theme Setup Card -->
<div class="col-md-4 mb-4">
<div class="card shadow-sm border-0 h-100 rounded-4">
<div class="card-body text-center p-5 d-flex flex-column">
<i class="fa-solid fa-palette fa-4x text-primary mb-3"></i>
<h4 class="card-title fw-bold">ตั้งค่าความสวยงาม</h4>
<p class="card-text text-muted mb-4">
เปิดใช้งาน <strong>Premium Theme</strong> สำหรับระบบ (ดีไซน์ Glassmorphism + Animated Background)
</p>
<div class="mt-auto d-flex justify-content-center align-items-center">
<div class="form-check form-switch fs-4">
<input class="form-check-input cursor-pointer" type="checkbox" role="switch" id="premiumThemeToggle">
<label class="form-check-label fs-5 ms-2" for="premiumThemeToggle" style="cursor: pointer;">เปิดใช้งาน</label>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.getElementById('premiumThemeToggle');
// Set initial state from localStorage
if (localStorage.getItem('app_premium_theme') === 'true') {
themeToggle.checked = true;
}
// Listen for changes
themeToggle.addEventListener('change', function(e) {
if (typeof window.togglePremiumTheme === 'function') {
window.togglePremiumTheme(e.target.checked);
}
});
});
</script>
@@ -0,0 +1,65 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-gray-800">ตั้งค่า LINE Notify</h1>
<a href="<?= BASE_URL ?>/settings" class="btn btn-secondary"><i class="fa-solid fa-arrow-left me-1"></i> กลับ</a>
</div>
<div class="row">
<div class="col-lg-6">
<div class="card shadow border-0 mb-4">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-success"><i class="fa-brands fa-line me-2"></i>การเชื่อมต่อ LINE Notify</h6>
</div>
<div class="card-body">
<p class="text-muted">ระบบจะส่งการแจ้งเตือนไปยังผู้มีอำนาจพิจารณาเมื่อมีคำขอใหม่ และแจ้งเตือนผู้ขอเมื่อมีการอนุมัติ/ปฏิเสธ</p>
<form action="<?= BASE_URL ?>/settings/line-notify/save" method="POST">
<input type="hidden" name="csrf_token" value="<?= $csrf_token ?>">
<div class="mb-3">
<label class="form-label">LINE Notify Token <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="line_token" value="<?= htmlspecialchars($line_token ?? '') ?>" placeholder="กรอก Token ที่ได้จาก LINE Notify">
<div class="form-text">คุณสามารถรับ Token ได้จาก <a href="https://notify-bot.line.me/" target="_blank">LINE Notify</a></div>
</div>
<button type="submit" class="btn btn-success"><i class="fa-solid fa-save me-1"></i> บันทึกตั้งค่า</button>
<button type="button" class="btn btn-outline-primary ms-2" id="testLineBtn"><i class="fa-solid fa-paper-plane me-1"></i> ทดสอบส่งข้อความ</button>
</form>
</div>
</div>
</div>
</div>
<?php
$extraScripts = '
<script>
document.getElementById("testLineBtn").addEventListener("click", function() {
const btn = this;
btn.disabled = true;
btn.innerHTML = \'<i class="fa-solid fa-spinner fa-spin me-1"></i> กำลังทดสอบ...\';
fetch("' . BASE_URL . '/settings/line-notify/test", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "csrf_token=' . $csrf_token . '"
})
.then(res => res.json())
.then(data => {
btn.disabled = false;
btn.innerHTML = \'<i class="fa-solid fa-paper-plane me-1"></i> ทดสอบส่งข้อความ\';
if (data.status === "success") {
alert(data.message);
} else {
alert("เกิดข้อผิดพลาด: " + data.message);
}
})
.catch(err => {
btn.disabled = false;
btn.innerHTML = \'<i class="fa-solid fa-paper-plane me-1"></i> ทดสอบส่งข้อความ\';
alert("Error: " + err.message);
});
});
</script>
';
?>
@@ -0,0 +1,419 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-gray-800">PDF Template Designer</h1>
<a href="<?= BASE_URL ?>/settings" class="btn btn-outline-secondary shadow-sm"><i class="fa-solid fa-arrow-left me-1"></i> กลับ</a>
</div>
<div class="row">
<!-- Tools & Fields Panel -->
<div class="col-lg-3">
<div class="card shadow-sm rounded-4 border-0 mb-4 overflow-hidden">
<div class="card-header bg-danger text-white py-3">
<h6 class="m-0 font-weight-bold"><i class="fa-solid fa-toolbox me-2"></i>เครื่องมือสร้างฟิลด์</h6>
</div>
<div class="card-body">
<p class="small text-muted mb-3">ลากฟิลด์ที่ต้องการไปวางบนแบบฟอร์ม PDF</p>
<div class="d-grid gap-2 mb-4" style="max-height: 400px; overflow-y: auto; padding-right: 5px;">
<!-- ข้อมูลบุคคล -->
<button class="btn btn-outline-primary btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{fullname}">
<i class="fa-solid fa-user me-2"></i> ชื่อ-นามสกุล <code>{fullname}</code>
</button>
<button class="btn btn-outline-primary btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{position}">
<i class="fa-solid fa-briefcase me-2"></i> ตำแหน่ง <code>{position}</code>
</button>
<button class="btn btn-outline-primary btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{department}">
<i class="fa-solid fa-sitemap me-2"></i> หน่วยงาน <code>{department}</code>
</button>
<button class="btn btn-outline-primary btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{personnel_type}">
<i class="fa-solid fa-id-badge me-2"></i> ประเภทบุคลากร <code>{personnel_type}</code>
</button>
<!-- ข้อมูลหนังสือและวันที่ -->
<button class="btn btn-outline-info btn-sm draggable-field text-start mt-2" draggable="true" data-type="Text" data-field="{doc_number}">
<i class="fa-solid fa-hashtag me-2"></i> เลขอ้างอิงหนังสือ <code>{doc_number}</code>
</button>
<button class="btn btn-outline-info btn-sm draggable-field text-start mt-1" draggable="true" data-type="Text" data-field="{doc_no}">
<i class="fa-solid fa-hashtag me-2"></i> เลขที่หนังสือ <code>{doc_no}</code>
</button>
<button class="btn btn-outline-info btn-sm draggable-field text-start mt-1" draggable="true" data-type="Text" data-field="{trip_running_number}">
<i class="fa-solid fa-list-ol me-2"></i> เลขที่หนังสือเดินทาง <code>{trip_running_number}</code>
</button>
<button class="btn btn-outline-info btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{doc_date}">
<i class="fa-solid fa-calendar-day me-2"></i> วันที่หนังสือ <code>{doc_date}</code>
</button>
<button class="btn btn-outline-info btn-sm draggable-field text-start mt-1" draggable="true" data-type="Text" data-field="{title}">
<i class="fa-solid fa-heading me-2"></i> เรื่อง <code>{title}</code>
</button>
<button class="btn btn-outline-info btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{meeting_date}">
<i class="fa-solid fa-calendar-week me-2"></i> วันที่ประชุม/เดินทาง <code>{meeting_date}</code>
</button>
<button class="btn btn-outline-info btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{current_date}">
<i class="fa-regular fa-clock me-2"></i> วันที่ปัจจุบัน (พิมพ์) <code>{current_date}</code>
</button>
<!-- ข้อมูลสถานที่/ผู้จัด -->
<button class="btn btn-outline-secondary btn-sm draggable-field text-start mt-2" draggable="true" data-type="Text" data-field="{location}">
<i class="fa-solid fa-location-dot me-2"></i> สถานที่ <code>{location}</code>
</button>
<button class="btn btn-outline-secondary btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{organizer}">
<i class="fa-solid fa-building me-2"></i> ผู้จัด <code>{organizer}</code>
</button>
<!-- ข้อมูลวันเดินทาง (แยกย่อย) -->
<button class="btn btn-outline-warning btn-sm draggable-field text-start mt-2" draggable="true" data-type="Text" data-field="{depart_datetime}">
<i class="fa-solid fa-plane-departure me-2"></i> (รวม) วันเวลาออกเดินทาง <code>{depart_datetime}</code>
</button>
<button class="btn btn-outline-warning btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{arrive_datetime}">
<i class="fa-solid fa-plane-arrival me-2"></i> (รวม) วันเวลากลับถึง <code>{arrive_datetime}</code>
</button>
<button class="btn btn-outline-warning btn-sm draggable-field text-start mt-1" draggable="true" data-type="Text" data-field="{depart_date}">
<i class="fa-regular fa-calendar-check me-2"></i> วันที่ออกเดินทาง <code>{depart_date}</code>
</button>
<button class="btn btn-outline-warning btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{depart_time}">
<i class="fa-regular fa-clock me-2"></i> เวลาออกเดินทาง <code>{depart_time}</code>
</button>
<button class="btn btn-outline-warning btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{arrive_date}">
<i class="fa-regular fa-calendar-check me-2"></i> วันที่กลับถึง <code>{arrive_date}</code>
</button>
<button class="btn btn-outline-warning btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{arrive_time}">
<i class="fa-regular fa-clock me-2"></i> เวลากลับถึง <code>{arrive_time}</code>
</button>
<button class="btn btn-outline-warning btn-sm draggable-field text-start mt-1" draggable="true" data-type="Text" data-field="{total_days}">
<i class="fa-solid fa-stopwatch me-2"></i> รวมจำนวนวัน <code>{total_days}</code>
</button>
<button class="btn btn-outline-success btn-sm draggable-field text-start mt-2" draggable="true" data-type="Text" data-field="{vehicle_type}">
<i class="fa-solid fa-car me-2"></i> ประเภทยานพาหนะ <code>{vehicle_type}</code>
</button>
<button class="btn btn-outline-success btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{vehicle_plate}">
<i class="fa-solid fa-id-card me-2"></i> ทะเบียนรถ (หากมี) <code>{vehicle_plate}</code>
</button>
<!-- งบประมาณ -->
<button class="btn btn-outline-success btn-sm draggable-field text-start mt-2" draggable="true" data-type="Text" data-field="{budget_source}">
<i class="fa-solid fa-sack-dollar me-2"></i> เบิกค่าใช้จ่ายจาก <code>{budget_source}</code>
</button>
<button class="btn btn-outline-success btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{organizer_support_amount}">
<i class="fa-solid fa-hand-holding-dollar me-2"></i> จำนวนเงินสนับสนุน <code>{organizer_support_amount}</code>
</button>
<button class="btn btn-outline-success btn-sm draggable-field text-start mt-1" draggable="true" data-type="Text" data-field="{budget}">
<i class="fa-solid fa-money-bill me-2"></i> จำนวนเงินรวม <code>{budget}</code>
</button>
<button class="btn btn-outline-success btn-sm draggable-field text-start mt-1" draggable="true" data-type="Text" data-field="{expense_accommodations_text}">
<i class="fa-solid fa-bed me-2"></i> รายละเอียดที่พัก <code>{expense_accommodations_text}</code>
</button>
<button class="btn btn-outline-success btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{expense_allowances_text}">
<i class="fa-solid fa-utensils me-2"></i> รายละเอียดเบี้ยเลี้ยง <code>{expense_allowances_text}</code>
</button>
<button class="btn btn-outline-success btn-sm draggable-field text-start mt-1" draggable="true" data-type="Text" data-field="{expense_transport}">
<i class="fa-solid fa-ticket me-2"></i> ค่าโดยสารทั้งหมด <code>{expense_transport}</code>
</button>
<button class="btn btn-outline-success btn-sm draggable-field text-start" draggable="true" data-type="Text" data-field="{expense_registration}">
<i class="fa-solid fa-address-card me-2"></i> ค่าลงทะเบียน <code>{expense_registration}</code>
</button>
<!-- ลายเซ็น -->
<button class="btn btn-outline-dark btn-sm draggable-field text-start mt-2" draggable="true" data-type="Signature" data-field="{signature_1}">
<i class="fa-solid fa-signature me-2"></i> ลายเซ็นผู้อนุมัติ <code>{signature_1}</code>
</button>
<button class="btn btn-outline-dark btn-sm draggable-field text-start" draggable="true" data-type="QR" data-field="{qr_code}">
<i class="fa-solid fa-qrcode me-2"></i> QR Code อ้างอิง
</button>
</div>
<hr>
<!-- Property Editor Card (Hidden by default) -->
<div id="propertyEditor" class="card shadow-sm border-info mb-4 d-none">
<div class="card-header bg-info text-white py-2">
<h6 class="m-0 font-weight-bold small"><i class="fa-solid fa-palette me-2"></i>ปรับแต่งข้อความที่เลือก</h6>
</div>
<div class="card-body py-2 px-3">
<div class="mb-2">
<label class="form-label small fw-bold text-muted mb-1">ขนาดตัวอักษร</label>
<div class="input-group input-group-sm">
<input type="number" id="fieldSize" class="form-control" value="13" min="8" max="72">
<span class="input-group-text">pt</span>
</div>
</div>
<div class="mb-2">
<label class="form-label small fw-bold text-muted mb-1">สีตัวอักษร</label>
<input type="color" id="fieldColor" class="form-control form-control-color w-100" value="#000000" style="height: 38px;">
</div>
<small class="text-danger d-block mt-2"><i class="fa-solid fa-circle-info me-1"></i>กดปุ่ม Delete บนคีย์บอร์ดเพื่อลบ</small>
</div>
</div>
<form action="<?= BASE_URL ?>/settings/pdf-designer/upload" method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= $csrf_token ?>">
<div class="mb-3">
<label class="form-label fw-bold">อัปโหลดแบบฟอร์ม PDF ใหม่</label>
<input class="form-control form-control-sm" type="file" name="pdf_file" accept=".pdf" required>
</div>
<button type="submit" class="btn btn-sm btn-danger w-100"><i class="fa-solid fa-upload me-1"></i> อัปโหลด</button>
</form>
</div>
</div>
<button id="saveLayoutBtn" class="btn btn-success w-100 shadow-sm rounded-4 py-2"><i class="fa-solid fa-floppy-disk me-1"></i> บันทึกตำแหน่งฟิลด์</button>
</div>
<!-- Canvas Editor Area -->
<div class="col-lg-9">
<div class="card shadow-sm rounded-4 border-0 mb-4 overflow-hidden">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<h6 class="m-0 font-weight-bold text-danger"><i class="fa-solid fa-object-group me-2"></i>พื้นที่ออกแบบ</h6>
</div>
<div>
<?php if($template): ?>
<a href="<?= BASE_URL . $template['file_path'] ?>" target="_blank" class="btn btn-sm btn-outline-info me-2">
<i class="fa-solid fa-file-pdf"></i> ต้นฉบับ
</a>
<a href="<?= BASE_URL ?>/settings/pdf-designer/preview" target="_blank" class="btn btn-sm btn-success me-2">
<i class="fa-solid fa-eye"></i> ดูตัวอย่าง (Preview)
</a>
<?php endif; ?>
<span class="badge bg-secondary me-2">แบบฟอร์ม: <?= $template ? htmlspecialchars($template['name']) : 'ยังไม่ได้อัปโหลด' ?></span>
<?php if($template): ?>
<form action="<?= BASE_URL ?>/settings/pdf-designer/delete" method="POST" class="d-inline" onsubmit="return confirm('คุณแน่ใจหรือไม่ว่าต้องการลบแม่แบบนี้?');">
<input type="hidden" name="csrf_token" value="<?= $csrf_token ?>">
<input type="hidden" name="id" value="<?= $template['id'] ?>">
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="fa-solid fa-trash"></i> ลบ</button>
</form>
<?php endif; ?>
</div>
</div>
<div class="card-body bg-light text-center p-0" style="height: 800px; overflow: auto; position: relative;" id="canvasContainer">
<div style="position: relative; margin: 20px auto; width: 800px; display: inline-block;" id="canvasWrapper">
<canvas id="pdfRenderer" width="800" height="1131" style="position: absolute; top: 0; left: 0; box-shadow: 0 4px 8px rgba(0,0,0,0.1); background-color: white; z-index: 1;"></canvas>
<canvas id="pdfCanvas" width="800" height="1131" style="position: absolute; top: 0; left: 0; z-index: 2;"></canvas>
</div>
</div>
</div>
</div>
</div>
<?php
$pdfUrl = $template ? BASE_URL . $template['file_path'] : '';
$templateId = $template ? $template['id'] : 0;
$fieldMapping = !empty($template['field_mapping']) ? $template['field_mapping'] : '[]';
$extraScripts = '
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
var pdfUrl = "' . $pdfUrl . '";
var templateId = ' . $templateId . ';
var existingMapping = ' . $fieldMapping . ';
var pdfRenderer = document.getElementById("pdfRenderer");
var canvasWrapper = document.getElementById("canvasWrapper");
// Initialize Fabric
var canvas = new fabric.Canvas("pdfCanvas");
canvas.preserveObjectStacking = true; // Keep objects in order
if (pdfUrl) {
pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js";
var loadingTask = pdfjsLib.getDocument(pdfUrl);
loadingTask.promise.then(function(pdf) {
return pdf.getPage(1);
}).then(function(page) {
var unscaledViewport = page.getViewport({scale: 1});
var scale = 800 / unscaledViewport.width;
var viewport = page.getViewport({scale: scale});
// Set underlying PDF canvas
pdfRenderer.height = viewport.height;
pdfRenderer.width = viewport.width;
// Adjust wrapper and Fabric canvas to match
canvasWrapper.style.width = viewport.width + "px";
canvasWrapper.style.height = viewport.height + "px";
canvas.setWidth(viewport.width);
canvas.setHeight(viewport.height);
var renderContext = {
canvasContext: pdfRenderer.getContext("2d"),
viewport: viewport
};
return page.render(renderContext).promise.then(function() {
// PDF is now rendered natively to the bottom canvas!
// We just load existing fields onto the transparent top canvas.
loadExistingMapping();
});
}).catch(function(err) {
console.error("Error rendering PDF", err);
Swal.fire("ข้อผิดพลาด PDF.js", "ไม่สามารถแสดง PDF ได้: " + err.message, "error");
loadDummyBackground();
});
} else {
loadDummyBackground();
}
function loadDummyBackground() {
fabric.Image.fromURL("https://via.placeholder.com/800x1131.png?text=Please+Upload+PDF+Template", function(img) {
img.set({ selectable: false, evented: false, opacity: 0.3 });
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
loadExistingMapping();
});
}
function loadExistingMapping() {
if (!existingMapping || existingMapping.length === 0) return;
existingMapping.forEach(function(item) {
var textObj;
if (item.field.includes("signature") || item.field.includes("qr")) {
textObj = new fabric.Rect({
left: 0, top: 0, width: item.width, height: item.height,
fill: "rgba(255,0,0,0.2)", stroke: "red", strokeWidth: 2
});
var label = new fabric.Text(item.field, { left: 10, top: 15, fontSize: 16, fill: "red" });
textObj = new fabric.Group([textObj, label], { left: item.x, top: item.y });
textObj.customField = item.field;
} else {
textObj = new fabric.Text(item.field, {
left: item.x, top: item.y, fontSize: item.font_size || 13,
fontFamily: "Sarabun", fill: item.color || "#000000", borderColor: "blue"
});
textObj.customField = item.field;
}
canvas.add(textObj);
});
}
var fields = document.querySelectorAll(".draggable-field");
fields.forEach(function(field) {
field.addEventListener("dragstart", function(e) {
e.dataTransfer.setData("text/plain", JSON.stringify({
type: field.dataset.type,
field: field.dataset.field
}));
});
});
var container = document.getElementById("canvasContainer");
container.addEventListener("dragover", function(e) { e.preventDefault(); });
container.addEventListener("drop", function(e) {
e.preventDefault();
var data = JSON.parse(e.dataTransfer.getData("text/plain"));
var rect = document.querySelector(".canvas-container").getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
var textObj;
if(data.type === "Text") {
textObj = new fabric.Text(data.field, {
left: x, top: y, fontSize: 13, fontFamily: "Sarabun", fill: "#000000"
});
} else {
textObj = new fabric.Rect({
left: 0, top: 0, width: 150, height: 50,
fill: "rgba(255,0,0,0.2)", stroke: "red", strokeWidth: 2
});
var label = new fabric.Text(data.field, { left: 10, top: 15, fontSize: 16, fill: "red" });
textObj = new fabric.Group([textObj, label], { left: x, top: y });
}
textObj.customField = data.field;
canvas.add(textObj);
canvas.setActiveObject(textObj);
});
window.addEventListener("keydown", function(e) {
if(e.key === "Delete" || e.key === "Backspace") {
var activeObject = canvas.getActiveObject();
if (activeObject && !activeObject.isEditing) {
canvas.remove(activeObject);
document.getElementById("propertyEditor").classList.add("d-none");
}
}
});
// Handle selection for property editor
canvas.on("selection:created", showProperties);
canvas.on("selection:updated", showProperties);
canvas.on("selection:cleared", function() {
document.getElementById("propertyEditor").classList.add("d-none");
});
function showProperties(e) {
var obj = e.selected[0];
if (obj && obj.type === "text") {
document.getElementById("propertyEditor").classList.remove("d-none");
document.getElementById("fieldSize").value = Math.round(obj.fontSize * obj.scaleY);
document.getElementById("fieldColor").value = obj.fill;
} else {
document.getElementById("propertyEditor").classList.add("d-none");
}
}
document.getElementById("fieldSize").addEventListener("input", function() {
var obj = canvas.getActiveObject();
if (obj && obj.type === "text") {
obj.set("fontSize", parseInt(this.value) / obj.scaleY);
canvas.renderAll();
}
});
document.getElementById("fieldColor").addEventListener("input", function() {
var obj = canvas.getActiveObject();
if (obj && obj.type === "text") {
obj.set("fill", this.value);
canvas.renderAll();
}
});
document.getElementById("saveLayoutBtn").addEventListener("click", function() {
if (!templateId) {
Swal.fire("แจ้งเตือน", "กรุณาอัปโหลดไฟล์ PDF ต้นฉบับก่อนตั้งค่าพิกัด", "warning");
return;
}
var btn = this;
btn.innerHTML = "<i class=\'fa-solid fa-spinner fa-spin\'></i> กำลังบันทึก...";
btn.disabled = true;
var objects = canvas.getObjects();
var layoutData = [];
objects.forEach(function(obj) {
if(obj.customField) {
var isText = obj.type === "text";
layoutData.push({
field: obj.customField,
x: Math.round(obj.left),
y: Math.round(obj.top),
width: Math.round(obj.width * obj.scaleX),
height: Math.round(obj.height * obj.scaleY),
font_size: isText ? Math.round(obj.fontSize * obj.scaleY) : 0,
color: obj.fill || "#000000"
});
}
});
fetch("' . BASE_URL . '/settings/pdf-designer/save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ template_id: templateId, fields: layoutData })
})
.then(res => res.json())
.then(data => {
btn.innerHTML = "<i class=\'fa-solid fa-floppy-disk me-1\'></i> บันทึกตำแหน่งฟิลด์";
btn.disabled = false;
if(data.status === "success") {
Swal.fire("สำเร็จ", "บันทึกตำแหน่ง (X,Y) เข้าสู่ฐานข้อมูลเรียบร้อยแล้ว จำนวน " + layoutData.length + " ฟิลด์", "success");
} else {
Swal.fire("ผิดพลาด", data.message || "เกิดข้อผิดพลาดในการบันทึก", "error");
}
});
});
});
</script>
';
?>
@@ -0,0 +1,71 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-gray-800">ตั้งค่า Telegram Notify</h1>
<a href="<?= BASE_URL ?>/settings" class="btn btn-secondary"><i class="fa-solid fa-arrow-left me-1"></i> กลับ</a>
</div>
<div class="row">
<div class="col-lg-6">
<div class="card shadow border-0 mb-4">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-info"><i class="fa-brands fa-telegram me-2"></i>การเชื่อมต่อ Telegram Bot</h6>
</div>
<div class="card-body">
<p class="text-muted">ระบบจะส่งการแจ้งเตือนไปยังแชทที่ระบุเมื่อมีคำขอใหม่ หรือมีการพิจารณาอนุมัติ/ปฏิเสธ</p>
<form action="<?= BASE_URL ?>/settings/telegram-notify/save" method="POST">
<input type="hidden" name="csrf_token" value="<?= $csrf_token ?>">
<div class="mb-3">
<label class="form-label">Telegram Bot Token <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="bot_token" value="<?= htmlspecialchars($bot_token ?? '') ?>" placeholder="e.g. 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11">
<div class="form-text">รับได้จาก <a href="https://t.me/BotFather" target="_blank">@BotFather</a> ใน Telegram</div>
</div>
<div class="mb-4">
<label class="form-label">Chat ID <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="chat_id" value="<?= htmlspecialchars($chat_id ?? '') ?>" placeholder="e.g. 123456789 หรือ -100123456789">
<div class="form-text">รหัสห้องแชทที่จะให้บอทส่งข้อความไปหา</div>
</div>
<button type="submit" class="btn btn-info text-white"><i class="fa-solid fa-save me-1"></i> บันทึกตั้งค่า</button>
<button type="button" class="btn btn-outline-primary ms-2" id="testTelegramBtn"><i class="fa-solid fa-paper-plane me-1"></i> ทดสอบส่งข้อความ</button>
</form>
</div>
</div>
</div>
</div>
<?php
$extraScripts = '
<script>
document.getElementById("testTelegramBtn").addEventListener("click", function() {
const btn = this;
btn.disabled = true;
btn.innerHTML = \'<i class="fa-solid fa-spinner fa-spin me-1"></i> กำลังทดสอบ...\';
fetch("' . BASE_URL . '/settings/telegram-notify/test", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "csrf_token=' . $csrf_token . '"
})
.then(res => res.json())
.then(data => {
btn.disabled = false;
btn.innerHTML = \'<i class="fa-solid fa-paper-plane me-1"></i> ทดสอบส่งข้อความ\';
if (data.status === "success") {
alert(data.message);
} else {
alert("เกิดข้อผิดพลาด: " + data.message);
}
})
.catch(err => {
btn.disabled = false;
btn.innerHTML = \'<i class="fa-solid fa-paper-plane me-1"></i> ทดสอบส่งข้อความ\';
alert("Error: " + err.message);
});
});
</script>
';
?>