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,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>
';
?>