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,58 @@
<?php
require_once __DIR__ . '/db.php';
header('Content-Type: application/json; charset=utf-8');
$uid = $_GET['uid'] ?? '';
if (empty($uid)) {
echo json_encode(['success' => false, 'message' => 'No UID provided']);
exit;
}
if (!$line_pdo) {
echo json_encode(['success' => false, 'message' => 'Cannot connect to LINE database']);
exit;
}
try {
// 1. ตรวจสอบ lineUID จากฐานข้อมูล line_bot (ตาราง line_staff_register)
$stmt = $line_pdo->prepare("SELECT cid, fname, lname FROM line_staff_register WHERE user_id = ? LIMIT 1");
$stmt->execute([$uid]);
$line_user = $stmt->fetch();
if (!$line_user || empty($line_user['cid'])) {
echo json_encode(['success' => false, 'message' => 'ไม่พบข้อมูลลงทะเบียน LINE']);
exit;
}
$cid = $line_user['cid'];
// ชื่อ-สกุลที่ลงทะเบียนในระบบ LINE
$line_name = trim(($line_user['fname'] ?? '') . ' ' . ($line_user['lname'] ?? ''));
// 2. นำ cid มา mapping กับ hr_cid ในระบบหลัก (ตาราง drivers)
$stmt2 = $pdo->prepare("SELECT id, first_name, last_name FROM drivers WHERE hr_cid = ? LIMIT 1");
$stmt2->execute([$cid]);
$driver = $stmt2->fetch();
if ($driver) {
echo json_encode([
'success' => true,
'cid' => $cid,
'driver_id' => $driver['id'],
'driver_name' => trim($driver['first_name'] . ' ' . $driver['last_name']),
'is_driver' => true
]);
} else {
// แม้จะไม่พบในระบบหลัก แต่มี cid ให้ส่งกลับไปให้กรอกในฟอร์มได้
// และใช้ชื่อ-สกุลจากฐานข้อมูล line_bot แทน
echo json_encode([
'success' => true,
'cid' => $cid,
'driver_id' => null,
'driver_name' => $line_name ?: null,
'is_driver' => false
]);
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
}
@@ -0,0 +1,19 @@
<?php
// ตั้งค่าฐานข้อมูลสำหรับเชื่อมต่อไปยังโฮสต์หลัก (gso_cars)
define('DB_HOST', '10.0.250.112');
define('DB_NAME', 'gso_cars');
define('DB_USER', 'root');
define('DB_PASS', '@Samui@10742');
// ตั้งค่าฐานข้อมูล Line Bot (ตรวจสอบ lineUID)
define('LINE_DB_HOST', 'localhost'); // เปลี่ยนเป็น IP ของ Host ที่เก็บฐานข้อมูล line_bot
define('LINE_DB_NAME', 'line_bot');
define('LINE_DB_USER', 'root'); // เปลี่ยนเป็น User ของฐานข้อมูล line_bot
define('LINE_DB_PASS', '@Samui@1074200'); // เปลี่ยนเป็น Password ของฐานข้อมูล line_bot
// ตั้งค่า LINE LIFF
define('LIFF_ID', '1653852260-rIBH9rUe'); // นำ LIFF ID จาก LINE Developers มาใส่ตรงนี้
// ตั้งค่า Telegram Bot
define('TELEGRAM_BOT_TOKEN', '8765832860:AAFeRYWiXF4NwbqGE93qUVK8MRUxD53C5Wk');
define('TELEGRAM_CHAT_ID', '-5117522526');
@@ -0,0 +1,20 @@
<?php
require_once __DIR__ . '/config.php';
try {
$pdo = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", DB_USER, DB_PASS);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die("Connection failed: " . $e->getMessage());
}
$line_pdo = null;
try {
$line_pdo = new PDO("mysql:host=" . LINE_DB_HOST . ";dbname=" . LINE_DB_NAME . ";charset=utf8mb4", LINE_DB_USER, LINE_DB_PASS);
$line_pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$line_pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
// ไม่ให้หยุดการทำงานถ้าระบบ line_bot ล่ม หรือต่อไม่ได้
// แต่ปล่อยให้ $line_pdo เป็น null เพื่อจัดการต่อ
}
@@ -0,0 +1,658 @@
<?php
require_once __DIR__ . '/db.php';
// Fetch vehicles and check if they have been inspected today
$stmt = $pdo->query("
SELECT v.id, v.license_plate, v.type, v.brand, v.model, v.image_path,
(SELECT COUNT(*) FROM vehicle_inspections vi WHERE vi.vehicle_id = v.id AND vi.inspection_date = CURDATE()) as checked_today
FROM vehicles v
WHERE v.status != 'disposed'
ORDER BY v.license_plate ASC
");
$vehicles = $stmt->fetchAll();
$vehicleImages = [
'รถพยาบาล' => 'uploads/vehicles/defaults/ambulance.jpg',
'รถตู้พยาบาล' => 'uploads/vehicles/defaults/van.jpg',
'รถตู้โดยสาร' => 'uploads/vehicles/defaults/van.jpg',
'รถกระบะ' => 'uploads/vehicles/defaults/pickup.jpg',
'SUV' => 'uploads/vehicles/defaults/suv.jpg',
'รถเก๋ง' => 'uploads/vehicles/defaults/suv.jpg',
];
function getBaseUrl() {
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http";
$host = $_SERVER['HTTP_HOST'];
// ลบ /car_daily_check ออกจาก path เพื่อให้ได้ root URL ของโปรเจกต์หลัก
$uri = str_replace('/car_daily_check', '', dirname($_SERVER['PHP_SELF']));
return $protocol . "://" . $host . $uri . '/';
}
function getVehicleImage($v, $images) {
$baseUrl = getBaseUrl();
if (!empty($v['image_path']) && file_exists('../' . $v['image_path'])) {
return $baseUrl . $v['image_path'] . '?v=' . time();
}
$path = $images[$v['type']] ?? 'uploads/vehicles/defaults/suv.jpg';
return $baseUrl . $path;
}
$vehiclesJson = array_map(function($v) use ($vehicleImages) {
return [
'id' => $v['id'],
'license_plate' => $v['license_plate'],
'brand' => $v['brand'],
'model' => $v['model'],
'type' => $v['type'],
'image' => getVehicleImage($v, $vehicleImages),
'checked_today' => (int)$v['checked_today'] > 0
];
}, $vehicles);
?>
<!DOCTYPE html>
<html lang="th" class="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>แบบรายงานตรวจสภาพรถยนต์ประจำวัน</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Flatpickr CSS & JS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/th.js"></script>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script charset="utf-8" src="https://static.line-scdn.net/liff/edge/2/sdk.js"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: { sans: ['Sarabun', 'sans-serif'] },
colors: {
primary: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a' },
secondary: '#64748b',
success: '#10b981',
danger: '#ef4444',
warning: '#f59e0b',
info: '#0ea5e9'
}
}
}
}
</script>
<style>
body { font-family: 'Sarabun', sans-serif; background-color: #f8fafc; }
.glass-input {
background: rgba(255, 255, 255, 0.9);
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
transition: all 0.2s ease;
}
.glass-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.btn-gradient {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
border: none;
border-radius: 0.75rem;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 8px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 8px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>
</head>
<body class="text-slate-800">
<!-- Floating Background -->
<div class="fixed top-0 left-0 w-full h-full overflow-hidden -z-10 pointer-events-none">
<div class="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-primary-500/10 rounded-full blur-[100px]"></div>
<div class="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-secondary/10 rounded-full blur-[100px]"></div>
</div>
<div class="min-h-screen p-4 md:p-6" x-data="inspectionForm()">
<div class="max-w-4xl mx-auto bg-white/80 backdrop-blur-xl p-6 md:p-8 rounded-2xl shadow-xl border border-white">
<!-- Header part -->
<div class="text-center mb-8 border-b border-slate-200 pb-6">
<h1 class="text-2xl md:text-3xl font-bold mb-2">แบบรายงานตรวจสภาพรถยนต์ราชการ</h1>
<p class="text-lg text-slate-700">หน่วยงานยานพาหนะ โรงพยาบาลเกาะสมุย กระทรวงสาธารณสุข</p>
<p class="text-md text-primary-600 font-medium italic mt-2">"บริการมีน้ำใจ ปลอดภัย ทันเวลา"</p>
</div>
<form @submit.prevent="submitForm" class="space-y-6">
<!-- General Info Section -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-5 bg-slate-50/50 rounded-xl border border-slate-200">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">ชื่อ-สกุล (ผู้ตรวจ) <span class="text-danger">*</span></label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fa-solid fa-user text-slate-400"></i>
</div>
<input type="text" x-model="driverName" readonly class="glass-input block w-full pl-10 p-2.5 text-sm font-semibold text-primary-700 bg-slate-50" placeholder="กำลังโหลดข้อมูลจาก LINE...">
<!-- ซ่อนเลขบัตรประชาชนไว้สำหรับส่งไปบันทึก -->
<input type="hidden" x-model="form.inspector_cid" required>
</div>
<template x-if="driverName && isLineLoggedIn">
<div class="mt-2 text-sm flex items-center transition-colors" :class="isDriver ? 'text-success' : 'text-danger font-bold'">
<i class="fa-solid mr-2" :class="isDriver ? 'fa-circle-check' : 'fa-triangle-exclamation'"></i>
<span x-text="isDriver ? 'พนักงานขับรถในระบบ' : 'ไม่ใช่พนักงานขับรถในระบบ'"></span>
</div>
</template>
<template x-if="isLineLoggedIn && !driverName">
<div class="mt-2 text-sm text-info flex items-center">
<i class="fa-solid fa-spinner fa-spin mr-2"></i>
<span>กำลังตรวจสอบสิทธิ์...</span>
</div>
</template>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">รถยนต์หมายเลขทะเบียน <span class="text-danger">*</span></label>
<button type="button" @click="showVehicleModal = true" class="glass-input flex items-center justify-between w-full p-2.5 text-sm text-left h-[62px]">
<div class="flex items-center gap-3">
<template x-if="selectedVehicle">
<div class="flex items-center gap-3">
<img :src="selectedVehicle.image" class="w-10 h-10 object-cover rounded-md border border-slate-200">
<div>
<div class="font-semibold text-slate-800" x-text="selectedVehicle.license_plate"></div>
<div class="text-xs text-slate-500" x-text="selectedVehicle.brand + ' ' + selectedVehicle.model"></div>
</div>
</div>
</template>
<template x-if="!selectedVehicle">
<span class="text-slate-400">-- เลือกรถยนต์ --</span>
</template>
</div>
<i class="fa-solid fa-chevron-down text-slate-400"></i>
</button>
</div>
<div x-init="initDatepicker($refs.dateInput)">
<label class="block text-sm font-medium text-slate-700 mb-1">ประจำวันที่ <span class="text-danger">*</span></label>
<input type="text" x-ref="dateInput" required class="glass-input block w-full p-2.5 text-sm" placeholder="เลือกวันที่">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">เวลา (น.) <span class="text-danger">*</span></label>
<input type="time" x-model="form.inspection_time" required class="glass-input block w-full p-2.5 text-sm">
</div>
</div>
<!-- Desktop Inspection Table -->
<div class="hidden md:block overflow-x-auto border border-slate-200 rounded-xl shadow-sm">
<table class="w-full text-sm text-left text-slate-600">
<thead class="text-xs text-slate-700 uppercase bg-slate-100">
<tr>
<th scope="col" class="sticky left-0 z-20 bg-slate-100 px-3 py-3 text-center whitespace-nowrap border-r border-slate-200 w-[60px] min-w-[60px] shadow-[1px_0_0_0_#e2e8f0]">ลำดับที่</th>
<th scope="col" class="sticky left-[60px] z-20 bg-slate-100 px-4 py-3 text-center whitespace-nowrap border-r border-slate-200 min-w-[200px] shadow-[1px_0_0_0_#e2e8f0]">รายงานสภาพรถยนต์</th>
<th scope="col" class="px-3 py-3 text-center whitespace-nowrap border-r border-slate-200 w-20">ปกติ</th>
<th scope="col" class="px-3 py-3 text-center whitespace-nowrap border-r border-slate-200 w-20">ไม่ปกติ</th>
<th scope="col" class="px-4 py-3 text-center whitespace-nowrap border-r border-slate-200 min-w-[150px]">ไม่ปกติเนื่องจาก</th>
<th scope="col" class="px-4 py-3 text-center whitespace-nowrap min-w-[150px]">หมายเหตุ</th>
</tr>
</thead>
<tbody>
<template x-for="(item, index) in inspectionItems" :key="'desk-'+index">
<tr class="group bg-white border-b border-slate-200 hover:bg-slate-50 transition-colors">
<td class="sticky left-0 z-10 bg-white group-hover:bg-slate-50 px-3 py-3 text-center border-r border-slate-200 font-medium shadow-[1px_0_0_0_#e2e8f0]" x-text="index + 1"></td>
<td class="sticky left-[60px] z-10 bg-white group-hover:bg-slate-50 px-4 py-3 border-r border-slate-200 font-medium text-slate-800 shadow-[1px_0_0_0_#e2e8f0]" x-text="item.name"></td>
<td class="px-3 py-3 text-center border-r border-slate-200">
<div class="flex items-center justify-center">
<input type="radio" :name="'desk_status_'+index" value="normal" x-model="item.status" @change="item.reason = ''" class="w-5 h-5 cursor-pointer" style="appearance: auto; -webkit-appearance: auto; accent-color: #2563eb;">
</div>
</td>
<td class="px-3 py-3 text-center border-r border-slate-200">
<div class="flex items-center justify-center">
<input type="radio" :name="'desk_status_'+index" value="abnormal" x-model="item.status" class="w-5 h-5 cursor-pointer" style="appearance: auto; -webkit-appearance: auto; accent-color: #dc2626;">
</div>
</td>
<td class="px-3 py-2 border-r border-slate-200">
<input type="text" x-model="item.reason" :disabled="item.status !== 'abnormal'" :required="item.status === 'abnormal'" class="glass-input block w-full p-1.5 text-xs disabled:opacity-50 disabled:bg-slate-100" placeholder="ระบุสาเหตุ...">
</td>
<td class="px-3 py-2">
<input type="text" x-model="item.remark" class="glass-input block w-full p-1.5 text-xs" placeholder="หมายเหตุเพิ่มเติม">
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Mobile Inspection Cards -->
<div class="block md:hidden space-y-4">
<template x-for="(item, index) in inspectionItems" :key="'mob-'+index">
<div class="bg-white border border-slate-200 rounded-xl p-4 shadow-sm relative overflow-hidden transition-all"
:class="{'border-success bg-success/5': item.status === 'normal', 'border-danger bg-danger/5': item.status === 'abnormal'}">
<!-- Status indicator line -->
<div class="absolute top-0 left-0 w-1.5 h-full bg-slate-200 transition-colors"
:class="{'bg-success': item.status === 'normal', 'bg-danger': item.status === 'abnormal'}"></div>
<div class="flex justify-between items-start mb-4 pl-3">
<div class="font-bold text-slate-800 text-base" x-text="(index + 1) + '. ' + item.name"></div>
</div>
<div class="grid grid-cols-2 gap-3 pl-3 mb-4">
<label class="flex flex-col items-center justify-center p-3 border rounded-xl cursor-pointer transition-all"
:class="item.status === 'normal' ? 'border-success bg-success/10 text-success shadow-sm' : 'border-slate-200 bg-white hover:bg-slate-50'">
<input type="radio" :name="'mob_status_'+index" value="normal" x-model="item.status" @change="item.reason = ''" class="hidden">
<i class="fa-solid fa-check-circle text-2xl mb-1" :class="item.status === 'normal' ? 'text-success' : 'text-slate-300'"></i>
<span class="font-semibold text-sm" :class="item.status === 'normal' ? 'text-success' : 'text-slate-600'">ปกติ</span>
</label>
<label class="flex flex-col items-center justify-center p-3 border rounded-xl cursor-pointer transition-all"
:class="item.status === 'abnormal' ? 'border-danger bg-danger/10 text-danger shadow-sm' : 'border-slate-200 bg-white hover:bg-slate-50'">
<input type="radio" :name="'mob_status_'+index" value="abnormal" x-model="item.status" class="hidden">
<i class="fa-solid fa-times-circle text-2xl mb-1" :class="item.status === 'abnormal' ? 'text-danger' : 'text-slate-300'"></i>
<span class="font-semibold text-sm" :class="item.status === 'abnormal' ? 'text-danger' : 'text-slate-600'">ไม่ปกติ</span>
</label>
</div>
<div class="space-y-3 pl-3" x-show="item.status === 'abnormal'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 -translate-y-2" x-transition:enter-end="opacity-100 translate-y-0" style="display: none;">
<div>
<label class="block text-xs font-bold text-danger mb-1"><i class="fa-solid fa-triangle-exclamation mr-1"></i> สาเหตุที่ไม่ปกติ <span class="text-danger">*</span></label>
<input type="text" x-model="item.reason" class="glass-input block w-full p-2.5 text-sm border-danger focus:border-danger focus:ring-1 focus:ring-danger bg-white" placeholder="โปรดระบุสาเหตุ...">
</div>
</div>
<div class="mt-3 pl-3">
<input type="text" x-model="item.remark" class="glass-input block w-full p-2.5 text-sm bg-white/80 text-slate-600" placeholder="หมายเหตุเพิ่มเติม (ถ้ามี)">
</div>
</div>
</template>
</div>
<!-- Additional Details -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-white border border-slate-200 rounded-xl p-4 flex flex-col justify-center shadow-sm">
<label class="flex items-center justify-between text-sm font-medium text-slate-700 mb-2">
<span>12. เลขไมล์ประจำวันก่อนนำรถออกใช้บริการ</span>
<span class="text-xs text-slate-500">กิโลเมตร</span>
</label>
<input type="number" x-model="form.mileage" min="0" step="1" required class="glass-input block w-full p-2.5 text-sm font-medium" placeholder="ระบุเลขไมล์">
</div>
<div class="bg-white border border-slate-200 rounded-xl p-4 flex flex-col justify-center shadow-sm">
<label class="flex items-center justify-between text-sm font-medium text-slate-700 mb-2">
<span>13. น้ำมันเชื้อเพลิงเหลือในถังประจำวันประมาณ</span>
<span class="text-xs text-slate-500">ลิตร</span>
</label>
<input type="number" x-model="form.fuel_level" min="0" step="0.1" required class="glass-input block w-full p-2.5 text-sm font-medium" placeholder="ระบุปริมาณน้ำมัน">
</div>
<div class="md:col-span-2 bg-white border border-slate-200 rounded-xl p-4 shadow-sm">
<label class="block text-sm font-medium text-slate-700 mb-2">14. รายการอื่นๆ</label>
<textarea x-model="form.other_items" rows="3" class="glass-input block w-full p-2.5 text-sm" placeholder="ระบุรายการตรวจเช็คอื่นๆ (ถ้ามี)"></textarea>
</div>
<!-- New Status Field -->
<div class="md:col-span-2 bg-white border border-slate-200 rounded-xl p-4 shadow-sm mt-2 relative overflow-hidden transition-all"
:class="{'border-success bg-success/5': form.vehicle_status === 'available', 'border-danger bg-danger/5': form.vehicle_status === 'maintenance'}">
<div class="absolute top-0 left-0 w-1.5 h-full bg-slate-200 transition-colors"
:class="{'bg-success': form.vehicle_status === 'available', 'bg-danger': form.vehicle_status === 'maintenance'}"></div>
<label class="block text-base md:text-sm font-bold md:font-medium text-slate-800 md:text-slate-700 mb-3 pl-3">15. สรุปสถานะรถยนต์หลังการตรวจสอบ <span class="text-danger">*</span></label>
<div class="grid grid-cols-2 md:flex md:gap-4 gap-3 pl-3">
<label class="flex flex-col md:flex-row items-center justify-center md:justify-start p-3 md:px-5 md:py-3 border rounded-xl cursor-pointer transition-all"
:class="form.vehicle_status === 'available' ? 'border-success bg-success/10 text-success shadow-sm' : 'border-slate-200 bg-white hover:bg-slate-50'">
<input type="radio" x-model="form.vehicle_status" value="available" class="hidden">
<i class="fa-solid fa-check-circle text-2xl md:text-xl mb-1 md:mb-0 md:mr-2" :class="form.vehicle_status === 'available' ? 'text-success' : 'text-slate-300'"></i>
<span class="font-semibold text-sm md:text-base text-center" :class="form.vehicle_status === 'available' ? 'text-success' : 'text-slate-600'">พร้อมใช้งาน</span>
</label>
<label class="flex flex-col md:flex-row items-center justify-center md:justify-start p-3 md:px-5 md:py-3 border rounded-xl cursor-pointer transition-all"
:class="form.vehicle_status === 'maintenance' ? 'border-danger bg-danger/10 text-danger shadow-sm' : 'border-slate-200 bg-white hover:bg-slate-50'">
<input type="radio" x-model="form.vehicle_status" value="maintenance" class="hidden">
<i class="fa-solid fa-wrench text-2xl md:text-xl mb-1 md:mb-0 md:mr-2" :class="form.vehicle_status === 'maintenance' ? 'text-danger' : 'text-slate-300'"></i>
<span class="font-semibold text-sm md:text-base text-center" :class="form.vehicle_status === 'maintenance' ? 'text-danger' : 'text-slate-600'">ไม่พร้อมใช้งาน / ส่งซ่อม</span>
</label>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="flex justify-end pt-4 border-t border-slate-200 mt-6">
<button type="button" @click="resetForm" class="px-5 py-2.5 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-xl hover:bg-slate-50 mr-3 transition-colors">
ล้างข้อมูล
</button>
<button type="submit" :disabled="isSubmitting" class="btn-gradient px-6 py-2.5 text-sm font-medium flex items-center shadow-lg shadow-primary-500/30 hover:shadow-primary-500/50 transition-all hover:-translate-y-0.5">
<i class="fa-solid fa-save mr-2" x-show="!isSubmitting"></i>
<i class="fa-solid fa-spinner fa-spin mr-2" x-show="isSubmitting" style="display: none;"></i>
<span x-text="isSubmitting ? 'กำลังบันทึก...' : 'บันทึกแบบรายงาน'"></span>
</button>
</div>
<!-- Vehicle Selection Modal -->
<div x-show="showVehicleModal" style="display: none;" class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div x-show="showVehicleModal" x-transition.opacity class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm transition-opacity" @click="showVehicleModal = false"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div x-show="showVehicleModal"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="flex justify-between items-center mb-4 border-b border-slate-100 pb-3">
<h3 class="text-xl leading-6 font-bold text-slate-900" id="modal-title">เลือกรถยนต์ที่ต้องการตรวจสภาพ</h3>
<button type="button" @click="showVehicleModal = false" class="text-slate-400 hover:text-slate-500 focus:outline-none bg-slate-100 hover:bg-slate-200 rounded-full w-8 h-8 flex items-center justify-center transition-colors">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="mt-4 max-h-[60vh] overflow-y-auto pr-2 custom-scrollbar">
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 md:gap-4">
<template x-for="v in vehicles" :key="v.id">
<div @click="selectVehicle(v)" class="border border-slate-200 rounded-xl p-3 cursor-pointer hover:border-primary-500 hover:shadow-md transition-all group relative bg-white overflow-hidden">
<!-- Badge ตรวจแล้ววันนี้ -->
<template x-if="v.checked_today">
<div class="absolute top-2 left-2 z-10 bg-success text-white text-[10px] font-bold px-2 py-1 rounded-full shadow-sm">
<i class="fa-solid fa-check-circle mr-1"></i> ตรวจแล้ว
</div>
</template>
<!-- Badge รอเช็ค -->
<template x-if="!v.checked_today">
<div class="absolute top-2 left-2 z-10 bg-warning text-slate-800 text-[10px] font-bold px-2 py-1 rounded-full shadow-sm">
<i class="fa-solid fa-clock mr-1"></i> รอเช็ค
</div>
</template>
<div class="aspect-video w-full rounded-lg overflow-hidden mb-3 bg-slate-100 relative">
<img :src="v.image" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300">
</div>
<div class="text-center">
<div class="font-bold text-slate-800 text-sm mb-1" x-text="v.license_plate"></div>
<div class="text-xs text-slate-500" x-text="v.brand + ' ' + v.model"></div>
</div>
<!-- checkmark if selected -->
<div x-show="form.vehicle_id == v.id" class="absolute top-2 right-2 bg-primary-500 text-white rounded-full w-7 h-7 flex items-center justify-center shadow-md">
<i class="fa-solid fa-check text-sm"></i>
</div>
<!-- selection outline -->
<div x-show="form.vehicle_id == v.id" class="absolute inset-0 border-2 border-primary-500 rounded-xl pointer-events-none"></div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('inspectionForm', () => ({
isSubmitting: false,
isLineLoggedIn: false,
driverName: '',
isDriver: true,
selectedVehicle: null,
showVehicleModal: false,
vehicles: <?= json_encode($vehiclesJson) ?>,
liffId: "<?= defined('LIFF_ID') ? LIFF_ID : '' ?>",
form: {
inspector_cid: '',
vehicle_id: '',
inspection_date: new Date().toISOString().split('T')[0],
inspection_time: new Date().toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit' }),
mileage: '',
fuel_level: '',
other_items: '',
vehicle_status: 'available'
},
async init() {
if (this.liffId && this.liffId !== 'YOUR_LIFF_ID_HERE') {
await this.initLiff();
}
},
async initLiff() {
try {
await liff.init({ liffId: this.liffId });
if (liff.isLoggedIn()) {
this.isLineLoggedIn = true;
const profile = await liff.getProfile();
await this.fetchDriverData(profile);
}
} catch (err) {
console.error('LIFF Init Error:', err);
}
},
async fetchDriverData(profile) {
try {
const res = await fetch(`api_get_driver.php?uid=${profile.userId}`);
const data = await res.json();
if (data.success) {
this.form.inspector_cid = data.cid;
this.driverName = data.driver_name || profile.displayName;
this.isDriver = data.is_driver;
if (!this.isDriver) {
Swal.fire({
title: 'พบข้อผิดพลาด',
text: 'คุณไม่ใช่พนักงานขับรถในระบบ คุณต้องการดำเนินการตรวจเช็คสภาพรถต่อไปหรือไม่?',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#64748b',
confirmButtonText: 'ยืนยันดำเนินการต่อ',
cancelButtonText: 'ยกเลิก'
}).then((result) => {
if (!result.isConfirmed) {
// If user cancels, clear form and maybe close LIFF
this.form.inspector_cid = '';
this.driverName = '';
if (typeof liff !== 'undefined' && liff.isLoggedIn()) {
liff.closeWindow();
}
}
});
}
} else {
Swal.fire({
icon: 'warning',
title: 'ไม่พบข้อมูลลงทะเบียน',
text: data.message || 'ไม่พบข้อมูลการลงทะเบียน LINE ของคุณ กรุณาติดต่อผู้ดูแลระบบ'
});
this.driverName = profile.displayName;
this.isLineLoggedIn = false; // Allow manual input if needed, though CID is hidden
}
} catch (e) {
console.error(e);
this.isLineLoggedIn = false;
}
},
selectVehicle(v) {
if (v.checked_today) {
Swal.fire({
title: 'รถคันนี้ถูกตรวจเช็คไปแล้ว',
text: 'รถทะเบียน ' + v.license_plate + ' ได้รับการตรวจเช็คสภาพไปแล้วในวันนี้ คุณต้องการตรวจเช็คซ้ำอีกครั้งหรือไม่?',
icon: 'info',
showCancelButton: true,
confirmButtonColor: '#3b82f6',
cancelButtonColor: '#64748b',
confirmButtonText: 'ตรวจเช็คซ้ำ',
cancelButtonText: 'ยกเลิก'
}).then((result) => {
if (result.isConfirmed) {
this.form.vehicle_id = v.id;
this.selectedVehicle = v;
this.showVehicleModal = false;
}
});
} else {
this.form.vehicle_id = v.id;
this.selectedVehicle = v;
this.showVehicleModal = false;
}
},
initDatepicker(element) {
const self = this;
flatpickr(element, {
locale: "th",
defaultDate: this.form.inspection_date,
dateFormat: "Y-m-d",
altInput: true,
altFormat: "j F Y",
formatDate: (date, format, locale) => {
if (format === "Y-m-d") {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
const d = date.getDate();
const m = flatpickr.l10ns.th.months.longhand[date.getMonth()];
const y = date.getFullYear() + 543;
return `${d} ${m} ${y}`;
},
onChange: function(selectedDates, dateStr) {
self.form.inspection_date = dateStr;
},
onReady: function(selectedDates, dateStr, instance) {
const updateYear = () => {
if (instance.currentYearElement) {
instance.currentYearElement.value = instance.currentYear + 543;
}
};
updateYear();
instance.config.onMonthChange.push(updateYear);
instance.config.onYearChange.push(updateYear);
}
});
},
inspectionItems: [
{ name: 'ความสะอาดของรถยนต์', status: '', reason: '', remark: '' },
{ name: 'น้ำมันเครื่อง', status: '', reason: '', remark: '' },
{ name: 'น้ำมันครัทช์', status: '', reason: '', remark: '' },
{ name: 'น้ำหม้อน้ำ', status: '', reason: '', remark: '' },
{ name: 'น้ำกลั่นแบตเตอรี่', status: '', reason: '', remark: '' },
{ name: 'น้ำมันเบรค', status: '', reason: '', remark: '' },
{ name: 'ระบบเบรค', status: '', reason: '', remark: '' },
{ name: 'ระบบไฟรถยนต์', status: '', reason: '', remark: '' },
{ name: 'ระบบจ่ายน้ำมันเชื้อเพลิง', status: '', reason: '', remark: '' },
{ name: 'ระบบเครื่องปรับอากาศ (แอร์)', status: '', reason: '', remark: '' },
{ name: 'ยางรถยนต์', status: '', reason: '', remark: '' }
],
resetForm() {
if(confirm('ต้องการล้างข้อมูลในฟอร์มทั้งหมดใช่หรือไม่?')) {
this.form.vehicle_id = '';
this.selectedVehicle = null;
this.form.mileage = '';
this.form.fuel_level = '';
this.form.other_items = '';
this.inspectionItems.forEach(item => {
item.status = '';
item.reason = '';
item.remark = '';
});
}
},
async submitForm() {
for(let i=0; i<this.inspectionItems.length; i++) {
let item = this.inspectionItems[i];
if(!item.status) {
Swal.fire({ icon: 'warning', title: 'ข้อมูลไม่ครบถ้วน', text: `กรุณาระบุสถานะ ปกติ/ไม่ปกติ สำหรับรายการที่ ${i+1} "${item.name}"`, confirmButtonColor: '#3b82f6' });
return;
}
if(item.status === 'abnormal' && !item.reason.trim()) {
Swal.fire({ icon: 'warning', title: 'ข้อมูลไม่ครบถ้วน', text: `กรุณาระบุสาเหตุที่รายการ "${item.name}" ไม่ปกติ`, confirmButtonColor: '#3b82f6' });
return;
}
}
if(!this.form.vehicle_id) {
Swal.fire({ icon: 'warning', title: 'ข้อมูลไม่ครบถ้วน', text: 'กรุณาเลือกรถยนต์ที่ต้องการตรวจเช็ค', confirmButtonColor: '#3b82f6' });
return;
}
this.isSubmitting = true;
const payload = {
...this.form,
inspector_name: this.driverName,
items: this.inspectionItems
};
try {
const response = await fetch('save.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if(result.success) {
Swal.fire({
icon: 'success',
title: 'บันทึกสำเร็จ',
text: 'บันทึกแบบรายงานตรวจสภาพรถยนต์เรียบร้อยแล้ว',
timer: 2000,
showConfirmButton: false
}).then(() => {
window.location.reload();
});
} else {
Swal.fire({ icon: 'error', title: 'เกิดข้อผิดพลาด', text: result.message || 'ไม่สามารถบันทึกข้อมูลได้', confirmButtonColor: '#ef4444' });
}
} catch (error) {
Swal.fire({ icon: 'error', title: 'เกิดข้อผิดพลาด', text: 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้ หรือเกิดข้อผิดพลาดในการบันทึกข้อมูล', confirmButtonColor: '#ef4444' });
console.error(error);
} finally {
this.isSubmitting = false;
}
}
}));
});
</script>
</body>
</html>
@@ -0,0 +1,129 @@
<?php
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/config.php';
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Invalid request method']);
exit;
}
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode(['success' => false, 'message' => 'No data provided']);
exit;
}
try {
// Add vehicle_status column to history if it doesn't exist
try {
$pdo->exec("ALTER TABLE vehicle_inspections ADD COLUMN vehicle_status VARCHAR(50) NULL");
} catch (PDOException $e) {}
$pdo->beginTransaction();
// 1. Save Inspection History
$stmt = $pdo->prepare("
INSERT INTO vehicle_inspections
(vehicle_id, inspector_cid, inspection_date, inspection_time, mileage, fuel_level, other_items, items_json, vehicle_status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$data['vehicle_id'],
$data['inspector_cid'],
$data['inspection_date'],
$data['inspection_time'],
$data['mileage'],
$data['fuel_level'],
$data['other_items'] ?? '',
json_encode($data['items'], JSON_UNESCAPED_UNICODE),
$data['vehicle_status'] ?? 'available'
]);
// 2. Update Vehicle Status
if (!empty($data['vehicle_status'])) {
$updateStmt = $pdo->prepare("UPDATE vehicles SET status = ? WHERE id = ?");
$updateStmt->execute([$data['vehicle_status'], $data['vehicle_id']]);
}
$pdo->commit();
// 3. Send Telegram Notification
if (defined('TELEGRAM_BOT_TOKEN') && defined('TELEGRAM_CHAT_ID') && !empty(TELEGRAM_BOT_TOKEN) && !empty(TELEGRAM_CHAT_ID)) {
// Get vehicle info
$vStmt = $pdo->prepare("SELECT license_plate, brand, model FROM vehicles WHERE id = ?");
$vStmt->execute([$data['vehicle_id']]);
$vehicle = $vStmt->fetch();
// Get driver info
$dStmt = $pdo->prepare("SELECT first_name, last_name FROM drivers WHERE hr_cid = ?");
$dStmt->execute([$data['inspector_cid']]);
$driver = $dStmt->fetch();
$vName = $vehicle ? trim("{$vehicle['brand']} {$vehicle['model']}") : "ID: {$data['vehicle_id']}";
$vPlate = $vehicle ? $vehicle['license_plate'] : "-";
// Use inspector_name from frontend payload if provided
$dName = !empty($data['inspector_name']) ? $data['inspector_name'] : ($driver ? "{$driver['first_name']} {$driver['last_name']}" : $data['inspector_cid']);
$abnormalCount = count(array_filter($data['items'], function($i) { return $i['status'] === 'abnormal'; }));
$statusText = $data['vehicle_status'] === 'available' ? '✅ พร้อมใช้งาน' : '❌ ไม่พร้อมใช้งาน/ส่งซ่อม';
// Convert date to Thai format (DD-MM-YYYY in BE)
$dateParts = explode('-', $data['inspection_date']);
$thaiDate = count($dateParts) == 3 ? $dateParts[2] . '-' . $dateParts[1] . '-' . ((int)$dateParts[0] + 543) : $data['inspection_date'];
$msg = "📋 <b>รายงานตรวจเช็ครถประจำวัน</b>\n\n";
$msg .= "🚗 <b>รถ:</b> {$vName}\n";
$msg .= "📝 <b>ทะเบียน:</b> {$vPlate}\n";
$msg .= "👤 <b>ผู้ตรวจ:</b> {$dName}\n";
$msg .= "📅 <b>วันที่:</b> {$thaiDate} เวลา {$data['inspection_time']} น.\n";
$formattedMileage = number_format((float)$data['mileage']);
$formattedFuel = number_format((float)$data['fuel_level'], 2); // Assuming fuel could have decimals, or we can use 0 decimals if preferred, but usually float is fine. Wait, let me just format it intelligently.
// If fuel_level has decimal part like .00, we might want to trim it, or just use number_format($data['fuel_level'], strpos($data['fuel_level'], '.') !== false ? 2 : 0)
// A simple way is to use float cast
$formattedFuel = rtrim(rtrim(number_format((float)$data['fuel_level'], 2, '.', ','), '0'), '.');
if ($formattedFuel === '') $formattedFuel = '0';
$msg .= "🛣️ <b>เลขไมล์:</b> {$formattedMileage} กม.\n";
$msg .= "⛽ <b>น้ำมัน:</b> {$formattedFuel} ลิตร\n";
$msg .= "📊 <b>สถานะรถ:</b> {$statusText}\n";
if ($abnormalCount > 0) {
$msg .= "\n⚠️ <b>พบจุดผิดปกติ {$abnormalCount} จุด:</b>\n";
foreach($data['items'] as $item) {
if($item['status'] === 'abnormal') {
$msg .= "- {$item['name']}: {$item['reason']}\n";
}
}
} else {
$msg .= "\n✨ <b>ผลการตรวจ:</b> ปกติทุกจุด";
}
$url = "https://api.telegram.org/bot" . TELEGRAM_BOT_TOKEN . "/sendMessage";
$tgData = [
'chat_id' => TELEGRAM_CHAT_ID,
'text' => $msg,
'parse_mode' => 'HTML'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($tgData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_exec($ch);
curl_close($ch);
}
echo json_encode(['success' => true, 'message' => 'บันทึกข้อมูลและอัปเดตสถานะรถเรียบร้อยแล้ว']);
} catch (PDOException $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
}