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,48 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
require_once '../includes/logger.php';
require_once '../includes/notifier.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
$id = $_POST['id'] ?? '';
if (empty($id)) {
jsonResponse(false, 'ไม่พบข้อมูลที่ต้องการลบ');
}
try {
// ตรวจสอบว่ามีการจองผูกอยู่หรือไม่
$stmt = $pdo->prepare("SELECT id FROM schedules WHERE driver_id = ? LIMIT 1");
$stmt->execute([$id]);
if ($stmt->fetch()) {
jsonResponse(false, 'ไม่สามารถลบได้ เนื่องจากพนักงานขับรถคนนี้มีคิวการจองอยู่ในระบบ');
}
$stmt = $pdo->prepare("SELECT first_name, last_name FROM drivers WHERE id = ?");
$stmt->execute([$id]);
$driverInfo = $stmt->fetch();
$driverName = $driverInfo ? trim($driverInfo['first_name'] . ' ' . $driverInfo['last_name']) : $id;
$stmt = $pdo->prepare("DELETE FROM drivers WHERE id = ?");
$stmt->execute([$id]);
if ($stmt->rowCount() > 0) {
logActivity("Delete Driver ({$driverName})", 'drivers', $id);
notifyEvent('driver', "👤 แจ้งเตือนพนักงานขับรถ\nลบข้อมูลพนักงานออกจากระบบ: {$driverName}");
jsonResponse(true, 'ลบข้อมูลสำเร็จ');
} else {
jsonResponse(false, 'ไม่พบข้อมูลในฐานข้อมูล');
}
} catch (PDOException $e) {
jsonResponse(false, 'Database Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,47 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
// รับข้อมูลแบบ JSON หรือ Form Data
$data = json_decode(file_get_contents("php://input"), true) ?: $_POST;
$doc_id = $data['id'] ?? '';
if (empty($doc_id)) {
jsonResponse(false, 'Document ID is required');
}
try {
// ดึงข้อมูลไฟล์เพื่อเอา path ไปลบไฟล์จริง
$stmt = $pdo->prepare("SELECT file_path FROM driver_documents WHERE id = ?");
$stmt->execute([$doc_id]);
$doc = $stmt->fetch();
if ($doc) {
$filePath = '../' . $doc['file_path'];
// ลบข้อมูลจากฐานข้อมูล
$deleteStmt = $pdo->prepare("DELETE FROM driver_documents WHERE id = ?");
$deleteStmt->execute([$doc_id]);
// ลบไฟล์จริงออกจากเซิร์ฟเวอร์
if (file_exists($filePath)) {
unlink($filePath);
}
jsonResponse(true, 'ลบไฟล์สำเร็จ');
} else {
jsonResponse(false, 'ไม่พบเอกสารที่ต้องการลบ');
}
} catch (PDOException $e) {
jsonResponse(false, 'Database Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,25 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
$driver_id = $_GET['driver_id'] ?? '';
if (empty($driver_id)) {
jsonResponse(false, 'Driver ID is required');
}
try {
$stmt = $pdo->prepare("SELECT * FROM driver_documents WHERE driver_id = ? ORDER BY created_at DESC");
$stmt->execute([$driver_id]);
$docs = $stmt->fetchAll();
jsonResponse(true, 'Success', $docs);
} catch (PDOException $e) {
jsonResponse(false, 'Database Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,81 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
$driver_id = $_POST['driver_id'] ?? '';
if (empty($driver_id)) {
jsonResponse(false, 'Driver ID is required');
}
// ตรวจสอบว่ามีไฟล์ส่งมาหรือไม่
if (!isset($_FILES['document']) || $_FILES['document']['error'] === UPLOAD_ERR_NO_FILE) {
jsonResponse(false, 'กรุณาเลือกไฟล์เอกสาร (PDF)');
}
$file = $_FILES['document'];
// ตรวจสอบ Error ของไฟล์
if ($file['error'] !== UPLOAD_ERR_OK) {
jsonResponse(false, 'เกิดข้อผิดพลาดในการอัปโหลดไฟล์: ' . $file['error']);
}
// ตรวจสอบประเภทไฟล์ (ต้องเป็น PDF เท่านั้น)
$fileType = mime_content_type($file['tmp_name']);
$allowedTypes = ['application/pdf'];
if (!in_array($fileType, $allowedTypes)) {
jsonResponse(false, 'อนุญาตให้อัปโหลดเฉพาะไฟล์ PDF เท่านั้น');
}
// ตรวจสอบขนาดไฟล์ (เช่น ไม่เกิน 5MB)
$maxSize = 5 * 1024 * 1024;
if ($file['size'] > $maxSize) {
jsonResponse(false, 'ขนาดไฟล์ต้องไม่เกิน 5MB');
}
// สร้างโฟลเดอร์สำหรับเก็บไฟล์ถ้ายังไม่มี
$uploadDir = '../uploads/driver_docs/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// สร้างชื่อไฟล์ใหม่เพื่อป้องกันชื่อซ้ำ และปัญหาภาษาไทย
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$newFileName = 'drv_' . $driver_id . '_' . time() . '_' . uniqid() . '.' . $extension;
$destination = $uploadDir . $newFileName;
$dbPath = 'uploads/driver_docs/' . $newFileName;
// ย้ายไฟล์ไปยังโฟลเดอร์ปลายทาง
if (move_uploaded_file($file['tmp_name'], $destination)) {
try {
// บันทึกข้อมูลลงฐานข้อมูล
$stmt = $pdo->prepare("INSERT INTO driver_documents (driver_id, original_name, file_name, file_path) VALUES (?, ?, ?, ?)");
$stmt->execute([
$driver_id,
$file['name'],
$newFileName,
$dbPath
]);
jsonResponse(true, 'อัปโหลดไฟล์สำเร็จ');
} catch (PDOException $e) {
// หากบันทึกฐานข้อมูลลบเหลว ให้ลบไฟล์ที่อัปโหลดไปแล้วด้วย
if (file_exists($destination)) {
unlink($destination);
}
jsonResponse(false, 'Database Error: ' . $e->getMessage());
}
} else {
jsonResponse(false, 'ไม่สามารถบันทึกไฟล์ได้ กรุณาลองใหม่อีกครั้ง');
}
?>
@@ -0,0 +1,67 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
require_once '../includes/logger.php';
require_once '../includes/notifier.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
$id = $_POST['id'] ?? '';
$first_name = trim($_POST['first_name'] ?? '');
$last_name = trim($_POST['last_name'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$status = $_POST['status'] ?? 'available';
// New HR fields
$hr_cid = trim($_POST['hr_cid'] ?? '');
$sex = trim($_POST['sex'] ?? '');
$position = trim($_POST['position'] ?? '');
$department = trim($_POST['department'] ?? '');
$person_type = trim($_POST['person_type'] ?? '');
$license_type = trim($_POST['license_type'] ?? '');
$license_expiry_date = trim($_POST['license_expiry_date'] ?? '');
if (empty($license_expiry_date)) {
$license_expiry_date = null;
}
if (empty($first_name) || empty($last_name)) {
jsonResponse(false, 'กรุณากรอกชื่อและนามสกุล');
}
// Generate random avatar if empty
$avatar = 'https://ui-avatars.com/api/?name=' . urlencode($first_name . ' ' . $last_name) . '&background=random';
try {
if (empty($id)) {
// สร้างใหม่
$stmt = $pdo->prepare("INSERT INTO drivers (first_name, last_name, phone, status, avatar, hr_cid, sex, position, department, person_type, license_type, license_expiry_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$first_name, $last_name, $phone, $status, $avatar, $hr_cid, $sex, $position, $department, $person_type, $license_type, $license_expiry_date]);
$newId = $pdo->lastInsertId();
logActivity("Create Driver ({$first_name} {$last_name})", 'drivers', $newId);
notifyEvent('driver', "👤 แจ้งเตือนพนักงานขับรถ\nมีพนักงานใหม่เพิ่มเข้าระบบ: {$first_name} {$last_name}");
jsonResponse(true, 'เพิ่มพนักงานใหม่สำเร็จ');
} else {
// แก้ไข
$stmt = $pdo->prepare("UPDATE drivers SET first_name=?, last_name=?, phone=?, status=?, avatar=?, hr_cid=?, sex=?, position=?, department=?, person_type=?, license_type=?, license_expiry_date=? WHERE id=?");
$stmt->execute([$first_name, $last_name, $phone, $status, $avatar, $hr_cid, $sex, $position, $department, $person_type, $license_type, $license_expiry_date, $id]);
logActivity("Update Driver ({$first_name} {$last_name})", 'drivers', $id);
notifyEvent('driver', "👤 แจ้งเตือนพนักงานขับรถ\nแก้ไขข้อมูลพนักงาน: {$first_name} {$last_name}");
jsonResponse(true, 'แก้ไขข้อมูลพนักงานสำเร็จ');
}
} catch (PDOException $e) {
jsonResponse(false, 'Database Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,33 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
$id = $_POST['id'] ?? '';
$status = $_POST['status'] ?? '';
if (empty($id) || empty($status)) {
jsonResponse(false, 'ข้อมูลไม่ครบถ้วน');
}
try {
$stmt = $pdo->prepare("UPDATE drivers SET status = ? WHERE id = ?");
$stmt->execute([$status, $id]);
if ($stmt->rowCount() > 0) {
jsonResponse(true, 'อัปเดตสถานะสำเร็จ');
} else {
jsonResponse(false, 'ไม่พบพนักงานในระบบ หรือสถานะเหมือนเดิม');
}
} catch (PDOException $e) {
jsonResponse(false, 'Database Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,36 @@
<?php
session_start();
require_once '../includes/db_hr.php';
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
exit;
}
if (!$pdo_hr) {
http_response_code(500);
exit;
}
$cid = $_GET['cid'] ?? '';
if (empty($cid)) {
http_response_code(400);
exit;
}
try {
$stmt = $pdo_hr->prepare("SELECT HR_IMAGE FROM hr_person WHERE HR_CID = ?");
$stmt->execute([$cid]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result && !empty($result['HR_IMAGE'])) {
// Output image
header("Content-Type: image/jpeg");
echo $result['HR_IMAGE'];
} else {
http_response_code(404);
}
} catch (PDOException $e) {
http_response_code(500);
}
?>
@@ -0,0 +1,82 @@
<?php
session_start();
require_once '../includes/db_hr.php'; // โหลดการเชื่อมต่อ DB ตัวที่ 2
require_once '../includes/api_helper.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if (!$pdo_hr) {
jsonResponse(false, 'ไม่สามารถเชื่อมต่อฐานข้อมูล hosoffice_2566 ได้ กรุณาตั้งค่าใน includes/db_hr.php');
}
$keyword = $_GET['q'] ?? '';
if (empty($keyword) || mb_strlen($keyword, 'UTF-8') < 2) {
jsonResponse(true, 'Success', []); // คืนค่าว่างถ้าคำค้นหาสั้นไป
}
try {
$sql = "SELECT a.HR_CID, a.HR_FNAME, a.HR_LNAME, a.HR_STARTWORK_DATE, a.HR_PHONE, a.SEX,
(a.HR_IMAGE IS NOT NULL AND LENGTH(a.HR_IMAGE) > 0) as HAS_IMAGE,
e.HR_PREFIX_NAME, b.HR_DEPARTMENT_SUB_SUB_NAME,
c.HR_LEVEL_NAME, d.HR_PERSON_TYPE_NAME, f.HR_POSITION_NAME
FROM hr_person a
LEFT OUTER JOIN hr_department_sub_sub b on a.HR_DEPARTMENT_SUB_SUB_ID = b.HR_DEPARTMENT_SUB_SUB_ID
LEFT OUTER JOIN hr_level c ON a.HR_LEVEL_ID = c.HR_LEVEL_ID
LEFT OUTER JOIN hr_person_type d ON a.HR_PERSON_TYPE_ID = d.HR_PERSON_TYPE_ID
LEFT OUTER JOIN hr_prefix e ON a.HR_PREFIX_ID=e.HR_PREFIX_ID
LEFT OUTER JOIN hr_position f ON a.HR_POSITION_ID=f.HR_POSITION_ID
WHERE (a.HR_FNAME LIKE ? OR a.HR_LNAME LIKE ? OR a.HR_CID LIKE ?) AND a.HR_STATUS_ID='01'
LIMIT 15";
$stmt = $pdo_hr->prepare($sql);
$searchTerm = "%$keyword%";
$stmt->execute([$searchTerm, $searchTerm, $searchTerm]);
$raw_results = $stmt->fetchAll(PDO::FETCH_ASSOC);
$results = [];
foreach ($raw_results as $person) {
$prefix = $person['HR_PREFIX_NAME'] ?? '';
$fname = $person['HR_FNAME'] ?? '';
$lname = $person['HR_LNAME'] ?? '';
$fullName = trim($prefix . $fname . ' ' . $lname);
$department = $person['HR_DEPARTMENT_SUB_SUB_NAME'] ?? 'ไม่ระบุ';
$position = trim(($person['HR_POSITION_NAME'] ?? '') . ' ' . ($person['HR_LEVEL_NAME'] ?? ''));
$person_type = $person['HR_PERSON_TYPE_NAME'] ?? 'ไม่ระบุ';
$work_duration = 'ไม่ระบุวันที่เริ่มงาน';
if (!empty($person['HR_STARTWORK_DATE'])) {
try {
$startDate = new DateTime($person['HR_STARTWORK_DATE']);
$currentDate = new DateTime();
if ($startDate <= $currentDate) {
$interval = $currentDate->diff($startDate);
$work_duration = $interval->format('%y ปี %m เดือน %d วัน');
}
} catch (Exception $e) { }
}
$results[] = [
'prefix' => $prefix,
'fname' => $fname,
'lname' => $lname,
'full_name' => $fullName,
'department' => $department,
'position' => $position,
'person_type' => $person_type,
'work_duration' => $work_duration,
'phone' => $person['HR_PHONE'] ?? '',
'sex' => $person['SEX'] ?? '',
'has_image' => (bool)$person['HAS_IMAGE'],
'cid' => $person['HR_CID']
];
}
jsonResponse(true, 'Success', $results);
} catch (PDOException $e) {
jsonResponse(false, 'Database Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,35 @@
<?php
require_once '../includes/auth.php';
require_once '../includes/db.php';
header('Content-Type: application/json; charset=utf-8');
$vehicle_id = isset($_GET['vehicle_id']) ? intval($_GET['vehicle_id']) : 0;
if ($vehicle_id === 0) {
echo json_encode(['success' => false, 'message' => 'Invalid vehicle ID']);
exit;
}
try {
$stmt = $pdo->prepare("
SELECT vi.*, d.first_name, d.last_name
FROM vehicle_inspections vi
LEFT JOIN drivers d ON vi.inspector_cid COLLATE utf8mb4_unicode_ci = d.hr_cid COLLATE utf8mb4_unicode_ci
WHERE vi.vehicle_id = ?
ORDER BY vi.inspection_date DESC, vi.inspection_time DESC
LIMIT 50
");
$stmt->execute([$vehicle_id]);
$history = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decode JSON items for each record
foreach ($history as &$record) {
$record['items'] = json_decode($record['items_json'], true);
unset($record['items_json']);
}
echo json_encode(['success' => true, 'data' => $history]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
}
@@ -0,0 +1,60 @@
<?php
require_once '../includes/auth.php';
require_once '../includes/db.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();
echo json_encode(['success' => true, 'message' => 'บันทึกข้อมูลและอัปเดตสถานะรถเรียบร้อยแล้ว']);
} catch (PDOException $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
}
@@ -0,0 +1,37 @@
<?php
require_once '../includes/auth.php';
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
// Check if user is admin
if (!isAdmin()) {
sendJson(403, ['error' => 'Permission denied. Admins only.']);
}
$method = $_SERVER['REQUEST_METHOD'];
function sendJson($statusCode, $data) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit();
}
if ($method === 'GET') {
try {
$stmt = $pdo->query("
SELECT al.id, al.user_id, al.action, al.table_name, al.record_id, al.ip_address, al.created_at, u.full_name as user_name
FROM activity_logs al
LEFT JOIN users u ON al.user_id = u.id
ORDER BY al.created_at DESC
LIMIT 1000
");
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
sendJson(200, ['logs' => $logs]);
} catch (PDOException $e) {
sendJson(500, ['error' => 'Database error: ' . $e->getMessage()]);
}
} else {
sendJson(405, ['error' => 'Method Not Allowed']);
}
?>
@@ -0,0 +1,38 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
require_once '../includes/notifier.php';
require_once '../includes/logger.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
$id = $_POST['id'] ?? null;
if (!$id) {
jsonResponse(false, 'Missing maintenance record ID');
}
try {
$stmt = $pdo->prepare("SELECT v.license_plate FROM maintenance_records m JOIN vehicles v ON m.vehicle_id = v.id WHERE m.id = ?");
$stmt->execute([$id]);
$veh = $stmt->fetch();
$plate = $veh ? $veh['license_plate'] : "ID {$id}";
$stmt = $pdo->prepare("DELETE FROM maintenance_records WHERE id = ?");
$stmt->execute([$id]);
logActivity("Delete Maintenance Record", 'maintenance_records', $id);
notifyEvent('maintenance', "🛠 แจ้งเตือนซ่อมบำรุง\nลบข้อมูลประวัติการซ่อม ทะเบียน: {$plate}");
jsonResponse(true, 'ลบประวัติการซ่อมสำเร็จ');
} catch (PDOException $e) {
jsonResponse(false, 'Database Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,42 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
$doc_id = $_POST['id'] ?? null;
if (!$doc_id) {
jsonResponse(false, 'Missing doc id');
}
try {
$stmt = $pdo->prepare("SELECT file_path FROM maintenance_documents WHERE id = ?");
$stmt->execute([$doc_id]);
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
if ($doc) {
// Delete physical file
$physical_path = '../' . $doc['file_path'];
if (file_exists($physical_path)) {
unlink($physical_path);
}
// Delete DB record
$stmtDelete = $pdo->prepare("DELETE FROM maintenance_documents WHERE id = ?");
$stmtDelete->execute([$doc_id]);
jsonResponse(true, 'ลบเอกสารสำเร็จ');
} else {
jsonResponse(false, 'ไม่พบเอกสารที่ต้องการลบ');
}
} catch (PDOException $e) {
jsonResponse(false, 'DB Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,21 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
$maintenance_record_id = $_GET['maintenance_record_id'] ?? null;
if (!$maintenance_record_id) jsonResponse(false, 'Missing maintenance_record_id');
try {
$stmt = $pdo->prepare("SELECT * FROM maintenance_documents WHERE maintenance_record_id = ? ORDER BY id DESC");
$stmt->execute([$maintenance_record_id]);
$docs = $stmt->fetchAll(PDO::FETCH_ASSOC);
jsonResponse(true, 'Success', $docs);
} catch (PDOException $e) {
jsonResponse(false, 'DB Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,80 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
$maintenance_record_id = $_POST['maintenance_record_id'] ?? null;
if (!$maintenance_record_id) {
jsonResponse(false, 'Missing maintenance_record_id');
}
if (!isset($_FILES['documents']) || empty($_FILES['documents']['name'][0])) {
jsonResponse(false, 'No files uploaded');
}
$uploadDir = '../uploads/maintenance/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$successFiles = [];
$errors = [];
foreach ($_FILES['documents']['tmp_name'] as $key => $tmp_name) {
if ($_FILES['documents']['error'][$key] === UPLOAD_ERR_OK) {
$original_name = basename($_FILES['documents']['name'][$key]);
$file_size = $_FILES['documents']['size'][$key];
$file_type = mime_content_type($tmp_name);
// Validate PDF
$ext = strtolower(pathinfo($original_name, PATHINFO_EXTENSION));
if ($ext !== 'pdf' || $file_type !== 'application/pdf') {
$errors[] = "$original_name ไม่ใช่ไฟล์ PDF";
continue;
}
// Max 10MB
if ($file_size > 10 * 1024 * 1024) {
$errors[] = "$original_name ขนาดเกิน 10MB";
continue;
}
// Generate unique filename
$new_filename = uniqid('maint_') . '_' . time() . '.pdf';
$dest_path = $uploadDir . $new_filename;
if (move_uploaded_file($tmp_name, $dest_path)) {
// Save to DB
try {
$stmt = $pdo->prepare("INSERT INTO maintenance_documents (maintenance_record_id, original_name, file_name, file_path) VALUES (?, ?, ?, ?)");
$relative_path = 'uploads/maintenance/' . $new_filename;
$stmt->execute([$maintenance_record_id, $original_name, $new_filename, $relative_path]);
$successFiles[] = $original_name;
} catch (PDOException $e) {
unlink($dest_path);
$errors[] = "DB Error for $original_name: " . $e->getMessage();
}
} else {
$errors[] = "ไม่สามารถบันทึกไฟล์ $original_name ได้";
}
}
}
if (count($successFiles) > 0) {
$msg = "อัปโหลดสำเร็จ " . count($successFiles) . " ไฟล์";
if (count($errors) > 0) {
$msg .= " (ไม่สำเร็จ " . count($errors) . " ไฟล์)";
}
jsonResponse(true, $msg);
} else {
jsonResponse(false, 'อัปโหลดไม่สำเร็จ: ' . implode(', ', $errors));
}
?>
@@ -0,0 +1,32 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
$vehicle_id = $_GET['vehicle_id'] ?? null;
if (!$vehicle_id) {
jsonResponse(false, 'Missing vehicle_id');
}
try {
$stmt = $pdo->prepare("SELECT * FROM maintenance_records WHERE vehicle_id = ? ORDER BY repair_date DESC, id DESC");
$stmt->execute([$vehicle_id]);
$records = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Fetch documents for each record
foreach ($records as &$record) {
$docStmt = $pdo->prepare("SELECT * FROM maintenance_documents WHERE maintenance_record_id = ? ORDER BY id ASC");
$docStmt->execute([$record['id']]);
$record['documents'] = $docStmt->fetchAll(PDO::FETCH_ASSOC);
}
jsonResponse(true, 'Success', $records);
} catch (PDOException $e) {
jsonResponse(false, 'Database Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,72 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
require_once '../includes/notifier.php';
require_once '../includes/logger.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
$vehicle_id = $_POST['vehicle_id'] ?? null;
$repair_date = $_POST['repair_date'] ?? null;
$description = trim($_POST['description'] ?? '');
$mileage = empty($_POST['mileage']) ? null : (int)$_POST['mileage'];
$cost = empty($_POST['cost']) ? null : (float)$_POST['cost'];
$garage = trim($_POST['garage'] ?? '');
if (!$vehicle_id || !$repair_date || empty($description)) {
jsonResponse(false, 'กรุณากรอกข้อมูลที่จำเป็นให้ครบถ้วน (วันที่ และ รายละเอียด)');
}
try {
$stmt = $pdo->prepare("INSERT INTO maintenance_records (vehicle_id, repair_date, description, mileage, cost, garage) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$vehicle_id, $repair_date, $description, $mileage, $cost, $garage]);
$maintenance_record_id = $pdo->lastInsertId();
// Handle file uploads if any
$upload_msg = '';
if (isset($_FILES['documents']) && !empty($_FILES['documents']['name'][0])) {
$uploadDir = '../uploads/maintenance/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$successFiles = 0;
foreach ($_FILES['documents']['tmp_name'] as $key => $tmp_name) {
if ($_FILES['documents']['error'][$key] === UPLOAD_ERR_OK) {
$original_name = basename($_FILES['documents']['name'][$key]);
$file_size = $_FILES['documents']['size'][$key];
$file_type = mime_content_type($tmp_name);
$ext = strtolower(pathinfo($original_name, PATHINFO_EXTENSION));
if ($ext === 'pdf' && $file_type === 'application/pdf' && $file_size <= 10 * 1024 * 1024) {
$new_filename = uniqid('maint_') . '_' . time() . '.pdf';
$dest_path = $uploadDir . $new_filename;
if (move_uploaded_file($tmp_name, $dest_path)) {
$docStmt = $pdo->prepare("INSERT INTO maintenance_documents (maintenance_record_id, original_name, file_name, file_path) VALUES (?, ?, ?, ?)");
$docStmt->execute([$maintenance_record_id, $original_name, $new_filename, 'uploads/maintenance/' . $new_filename]);
$successFiles++;
}
}
}
}
if ($successFiles > 0) $upload_msg = " (และแนบเอกสาร $successFiles ไฟล์)";
}
// Fetch vehicle plate for notification
$vehStmt = $pdo->prepare("SELECT license_plate FROM vehicles WHERE id = ?");
$vehStmt->execute([$vehicle_id]);
$veh = $vehStmt->fetch();
$plate = $veh ? $veh['license_plate'] : "ID {$vehicle_id}";
notifyEvent('maintenance', "🛠 แจ้งเตือนซ่อมบำรุง\nมีการบันทึกประวัติการซ่อมรถ ทะเบียน: {$plate}\nรายละเอียด: {$description}");
jsonResponse(true, 'บันทึกประวัติการซ่อมสำเร็จ' . $upload_msg);
} catch (PDOException $e) {
jsonResponse(false, 'Database Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,77 @@
<?php
require_once '../includes/db_intranet.php';
header('Content-Type: application/json');
if (!$intranet_pdo) {
echo json_encode([]);
exit;
}
$start = $_GET['start'] ?? '';
$end = $_GET['end'] ?? '';
try {
$sql = "SELECT * FROM booking_car";
$params = [];
if ($start && $end) {
$sql .= " WHERE booking_car_request_date_start >= ? AND booking_car_request_date_start <= ?";
$params = [
date('Y-m-d', strtotime($start)),
date('Y-m-d', strtotime($end))
];
}
$stmt = $intranet_pdo->prepare($sql);
$stmt->execute($params);
$bookings = $stmt->fetchAll();
$events = [];
foreach ($bookings as $b) {
$statusText = trim($b['booking_car_request_status'] ?? '');
$area = trim($b['booking_car_request_area'] ?? '');
$color = '#3b82f6'; // blue-500 (ในสมุย)
if ($area === 'นอกเกาะสมุย') {
$color = '#f97316'; // orange-500 (นอกสมุย)
}
$startTime = !empty($b['booking_car_request_time_start']) ? $b['booking_car_request_time_start'] : '00:00:00';
$endTime = !empty($b['booking_car_request_time_end']) ? $b['booking_car_request_time_end'] : '23:59:59';
$startDate = $b['booking_car_request_date_start'];
$endDate = !empty($b['booking_car_request_date_end']) ? $b['booking_car_request_date_end'] : $startDate;
$currentDate = $startDate;
while (strtotime($currentDate) <= strtotime($endDate)) {
$events[] = [
'id' => $b['booking_car_id'] . '_' . $currentDate,
'title' => $title,
'start' => $currentDate . 'T' . $startTime,
'end' => $currentDate . 'T' . $endTime,
'backgroundColor' => 'transparent',
'borderColor' => 'transparent',
'extendedProps' => [
'iconColor' => $color,
'name' => $b['booking_car_request_name'],
'phone' => $b['booking_car_request_phone'],
'purpose' => $b['booking_car_request_title'],
'type' => $b['booking_car_request_type'],
'status' => $statusText,
'station' => $b['booking_car_request_station'],
'desc' => $b['booking_car_request_description'],
'link' => $b['booking_car_request_link_view'],
'date' => date('d/m/', strtotime($currentDate)) . (date('Y', strtotime($currentDate)) + 543) . ($startTime != '00:00:00' ? ' ' . date('H:i', strtotime($startTime)) : '')
]
];
$currentDate = date('Y-m-d', strtotime($currentDate . ' +1 day'));
}
}
echo json_encode($events);
} catch (PDOException $e) {
echo json_encode([]);
}
?>
@@ -0,0 +1,121 @@
<?php
require_once '../includes/auth.php';
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
require_once '../includes/logger.php';
require_once '../includes/notifier.php';
function sendJson($statusCode, $data) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit();
}
// Check if user is admin
if (!isAdmin()) {
sendJson(403, ['error' => 'Permission denied. Admins only.']);
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
try {
// Auto-create table if not exists
try {
$pdo->query("SELECT 1 FROM settings LIMIT 1");
} catch (PDOException $e) {
$pdo->exec("
CREATE TABLE IF NOT EXISTS settings (
setting_key VARCHAR(50) PRIMARY KEY,
setting_value TEXT NULL,
description VARCHAR(255) NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
");
$defaultSettings = [
['line_notify_token', '', 'LINE Notify Token สำหรับส่งการแจ้งเตือน'],
['telegram_bot_token', '', 'Telegram Bot Token ที่ได้จาก BotFather'],
['telegram_chat_id', '', 'Telegram Chat ID หรือ Group ID ที่ต้องการให้บอทส่งข้อความ'],
['notify_events', '["vehicle","driver","schedule","maintenance"]', 'เหตุการณ์ที่ต้องการให้แจ้งเตือน (JSON array)']
];
$stmt = $pdo->prepare("INSERT IGNORE INTO settings (setting_key, setting_value, description) VALUES (?, ?, ?)");
foreach ($defaultSettings as $setting) {
$stmt->execute($setting);
}
}
$stmt = $pdo->query("SELECT setting_key, setting_value FROM settings");
$settingsData = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
sendJson(200, ['settings' => $settingsData]);
} catch (PDOException $e) {
sendJson(500, ['error' => 'Database error: ' . $e->getMessage()]);
}
} elseif ($method === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
$action = $data['action'] ?? '';
if ($action === 'save') {
$settings = $data['settings'] ?? [];
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = ?");
foreach ($settings as $key => $value) {
// Ensure the setting key exists to avoid SQL errors if someone injects keys
$checkStmt = $pdo->prepare("SELECT setting_key FROM settings WHERE setting_key = ?");
$checkStmt->execute([$key]);
if ($checkStmt->fetch()) {
$stmt->execute([$value, $key]);
} else {
$insertStmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?)");
$insertStmt->execute([$key, $value]);
}
}
$pdo->commit();
logActivity("Updated Notification Settings", 'settings', null);
sendJson(200, ['success' => true]);
} catch (PDOException $e) {
$pdo->rollBack();
sendJson(500, ['error' => 'Database error: ' . $e->getMessage()]);
}
}
elseif ($action === 'test_line') {
$token = $data['token'] ?? '';
if (empty($token)) {
sendJson(400, ['error' => 'LINE Notify Token is required']);
}
$message = "✅ ทดสอบระบบแจ้งเตือนผ่าน LINE Notify\nจากระบบจัดการข้อมูลยานพาหนะ";
$result = sendLineNotify($token, $message);
if ($result === true) {
sendJson(200, ['success' => true]);
} else {
sendJson(400, ['error' => 'เกิดข้อผิดพลาดในการส่ง LINE Notify: ' . $result]);
}
}
elseif ($action === 'test_telegram') {
$token = $data['token'] ?? '';
$chat_id = $data['chat_id'] ?? '';
if (empty($token) || empty($chat_id)) {
sendJson(400, ['error' => 'Telegram Bot Token and Chat ID are required']);
}
$message = "✅ ทดสอบระบบแจ้งเตือนผ่าน Telegram\nจากระบบจัดการข้อมูลยานพาหนะ";
$result = sendTelegramMessage($token, $chat_id, $message);
if ($result === true) {
sendJson(200, ['success' => true]);
} else {
sendJson(400, ['error' => 'เกิดข้อผิดพลาดในการส่ง Telegram: ' . $result]);
}
}
else {
sendJson(400, ['error' => 'Invalid action']);
}
} else {
sendJson(405, ['error' => 'Method Not Allowed']);
}
@@ -0,0 +1,167 @@
<?php
require_once '../includes/auth.php';
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
require_once '../includes/logger.php';
// Check if user is admin
if (!isAdmin()) {
sendJson(403, ['error' => 'Permission denied. Admins only.']);
}
$method = $_SERVER['REQUEST_METHOD'];
// Function to send JSON response
function sendJson($statusCode, $data) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit();
}
// Function to validate password strength
function validatePassword($password) {
if (strlen($password) < 6) return false;
return true;
}
switch ($method) {
case 'GET':
// Read (List)
try {
$stmt = $pdo->query("SELECT id, username, id_card, full_name, role, created_at FROM users ORDER BY created_at DESC");
$users = $stmt->fetchAll();
sendJson(200, ['users' => $users]);
} catch (PDOException $e) {
sendJson(500, ['error' => 'Database error: ' . $e->getMessage()]);
}
break;
case 'POST':
// Create
$data = json_decode(file_get_contents('php://input'), true);
$id_card = trim($data['id_card'] ?? '');
$full_name = trim($data['full_name'] ?? '');
$role = trim($data['role'] ?? 'user');
$password = $data['password'] ?? '';
if (empty($id_card) || empty($full_name) || empty($password)) {
sendJson(400, ['error' => 'Missing required fields']);
}
if (strlen($id_card) !== 13) {
sendJson(400, ['error' => 'ID card must be 13 digits']);
}
if (!validatePassword($password)) {
sendJson(400, ['error' => 'รหัสผ่านต้องมีความยาวไม่น้อยกว่า 6 ตัวอักษร']);
}
try {
// Check if id_card already exists
$stmt = $pdo->prepare("SELECT id FROM users WHERE id_card = ?");
$stmt->execute([$id_card]);
if ($stmt->fetch()) {
sendJson(400, ['error' => 'ID card already exists']);
}
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$username = $id_card; // Use id_card as username fallback if username is NOT NULL
$stmt = $pdo->prepare("INSERT INTO users (username, id_card, password, full_name, role) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$username, $id_card, $hashedPassword, $full_name, $role]);
$newUserId = $pdo->lastInsertId();
logActivity("Create User ({$id_card})", 'users', $newUserId);
sendJson(201, ['message' => 'User created successfully', 'id' => $newUserId]);
} catch (PDOException $e) {
sendJson(500, ['error' => 'Database error: ' . $e->getMessage()]);
}
break;
case 'PUT':
// Update
$data = json_decode(file_get_contents('php://input'), true);
$id = $data['id'] ?? null;
if (!$id) {
sendJson(400, ['error' => 'User ID is required']);
}
$id_card = trim($data['id_card'] ?? '');
$full_name = trim($data['full_name'] ?? '');
$role = trim($data['role'] ?? 'user');
$password = $data['password'] ?? '';
if (empty($id_card) || empty($full_name)) {
sendJson(400, ['error' => 'Missing required fields']);
}
if (strlen($id_card) !== 13) {
sendJson(400, ['error' => 'ID card must be 13 digits']);
}
try {
// Check if id_card is taken by someone else
$stmt = $pdo->prepare("SELECT id FROM users WHERE id_card = ? AND id != ?");
$stmt->execute([$id_card, $id]);
if ($stmt->fetch()) {
sendJson(400, ['error' => 'ID card is already in use by another user']);
}
if (!empty($password)) {
if (!validatePassword($password)) {
sendJson(400, ['error' => 'รหัสผ่านต้องมีความยาวไม่น้อยกว่า 6 ตัวอักษร']);
}
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE users SET id_card = ?, full_name = ?, role = ?, password = ? WHERE id = ?");
$stmt->execute([$id_card, $full_name, $role, $hashedPassword, $id]);
} else {
$stmt = $pdo->prepare("UPDATE users SET id_card = ?, full_name = ?, role = ? WHERE id = ?");
$stmt->execute([$id_card, $full_name, $role, $id]);
}
logActivity("Update User ({$id_card})", 'users', $id);
sendJson(200, ['message' => 'User updated successfully']);
} catch (PDOException $e) {
sendJson(500, ['error' => 'Database error: ' . $e->getMessage()]);
}
break;
case 'DELETE':
// Delete
$id = $_GET['id'] ?? null;
if (!$id) {
sendJson(400, ['error' => 'User ID is required']);
}
// Prevent deleting oneself
if ($id == $_SESSION['user_id']) {
sendJson(400, ['error' => 'Cannot delete your own account']);
}
try {
// ดึงชื่อก่อนลบ
$stmtName = $pdo->prepare("SELECT id_card FROM users WHERE id = ?");
$stmtName->execute([$id]);
$delUser = $stmtName->fetch();
$delIdCard = $delUser ? $delUser['id_card'] : $id;
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([$id]);
logActivity("Delete User ({$delIdCard})", 'users', $id);
sendJson(200, ['message' => 'User deleted successfully']);
} catch (PDOException $e) {
sendJson(500, ['error' => 'Database error: ' . $e->getMessage()]);
}
break;
default:
sendJson(405, ['error' => 'Method Not Allowed']);
}
@@ -0,0 +1,56 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
require_once '../includes/logger.php';
require_once '../includes/notifier.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
$id = $_POST['id'] ?? '';
if (empty($id)) {
jsonResponse(false, 'ไม่พบข้อมูลที่ต้องการลบ');
}
try {
// ตรวจสอบว่ามีการจองผูกอยู่หรือไม่
$stmt = $pdo->prepare("SELECT id FROM schedules WHERE vehicle_id = ? LIMIT 1");
$stmt->execute([$id]);
if ($stmt->fetch()) {
jsonResponse(false, 'ไม่สามารถลบได้ เนื่องจากรถคันนี้มีการจองอยู่ในระบบ');
}
// ตรวจสอบสถานะจำหน่าย
$stmt = $pdo->prepare("SELECT status FROM vehicles WHERE id = ?");
$stmt->execute([$id]);
$vehicle = $stmt->fetch();
if ($vehicle && $vehicle['status'] === 'disposed') {
jsonResponse(false, 'ไม่สามารถลบได้ เนื่องจากรถคันนี้อยู่ในสถานะจำหน่ายแล้ว');
}
$stmt = $pdo->prepare("SELECT license_plate FROM vehicles WHERE id = ?");
$stmt->execute([$id]);
$vehicleInfo = $stmt->fetch();
$license_plate = $vehicleInfo ? $vehicleInfo['license_plate'] : $id;
$stmt = $pdo->prepare("DELETE FROM vehicles WHERE id = ?");
$stmt->execute([$id]);
if ($stmt->rowCount() > 0) {
logActivity("Delete Vehicle ({$license_plate})", 'vehicles', $id);
notifyEvent('vehicle', "🚗 แจ้งเตือนยานพาหนะ\nลบข้อมูลรถออกจากระบบ: {$license_plate}");
jsonResponse(true, 'ลบข้อมูลสำเร็จ');
} else {
jsonResponse(false, 'ไม่พบข้อมูลในฐานข้อมูล');
}
} catch (PDOException $e) {
jsonResponse(false, 'Database Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,42 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
$doc_id = $_POST['id'] ?? null;
if (!$doc_id) {
jsonResponse(false, 'Missing doc id');
}
try {
$stmt = $pdo->prepare("SELECT file_path FROM vehicle_documents WHERE id = ?");
$stmt->execute([$doc_id]);
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
if ($doc) {
// Delete physical file
$physical_path = '../' . $doc['file_path'];
if (file_exists($physical_path)) {
unlink($physical_path);
}
// Delete DB record
$stmtDelete = $pdo->prepare("DELETE FROM vehicle_documents WHERE id = ?");
$stmtDelete->execute([$doc_id]);
jsonResponse(true, 'ลบเอกสารสำเร็จ');
} else {
jsonResponse(false, 'ไม่พบเอกสารที่ต้องการลบ');
}
} catch (PDOException $e) {
jsonResponse(false, 'DB Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,21 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
$vehicle_id = $_GET['vehicle_id'] ?? null;
if (!$vehicle_id) jsonResponse(false, 'Missing vehicle_id');
try {
$stmt = $pdo->prepare("SELECT * FROM vehicle_documents WHERE vehicle_id = ? ORDER BY id DESC");
$stmt->execute([$vehicle_id]);
$docs = $stmt->fetchAll(PDO::FETCH_ASSOC);
jsonResponse(true, 'Success', $docs);
} catch (PDOException $e) {
jsonResponse(false, 'DB Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,81 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
$vehicle_id = $_POST['vehicle_id'] ?? null;
if (!$vehicle_id) {
jsonResponse(false, 'Missing vehicle_id');
}
if (!isset($_FILES['documents']) || empty($_FILES['documents']['name'][0])) {
jsonResponse(false, 'No files uploaded');
}
$uploadDir = '../uploads/vehicles/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$successFiles = [];
$errors = [];
foreach ($_FILES['documents']['tmp_name'] as $key => $tmp_name) {
if ($_FILES['documents']['error'][$key] === UPLOAD_ERR_OK) {
$original_name = basename($_FILES['documents']['name'][$key]);
$file_size = $_FILES['documents']['size'][$key];
$file_type = mime_content_type($tmp_name);
// Validate PDF
$ext = strtolower(pathinfo($original_name, PATHINFO_EXTENSION));
if ($ext !== 'pdf' || $file_type !== 'application/pdf') {
$errors[] = "$original_name ไม่ใช่ไฟล์ PDF";
continue;
}
// Max 10MB
if ($file_size > 10 * 1024 * 1024) {
$errors[] = "$original_name ขนาดเกิน 10MB";
continue;
}
// Generate unique filename
$new_filename = uniqid('veh_') . '_' . time() . '.pdf';
$dest_path = $uploadDir . $new_filename;
if (move_uploaded_file($tmp_name, $dest_path)) {
// Save to DB
try {
$stmt = $pdo->prepare("INSERT INTO vehicle_documents (vehicle_id, original_name, file_name, file_path) VALUES (?, ?, ?, ?)");
$relative_path = 'uploads/vehicles/' . $new_filename;
$stmt->execute([$vehicle_id, $original_name, $new_filename, $relative_path]);
$successFiles[] = $original_name;
} catch (PDOException $e) {
// Remove file if DB insert fails
unlink($dest_path);
$errors[] = "DB Error for $original_name: " . $e->getMessage();
}
} else {
$errors[] = "ไม่สามารถบันทึกไฟล์ $original_name ได้";
}
}
}
if (count($successFiles) > 0) {
$msg = "อัปโหลดสำเร็จ " . count($successFiles) . " ไฟล์";
if (count($errors) > 0) {
$msg .= " (ไม่สำเร็จ " . count($errors) . " ไฟล์)";
}
jsonResponse(true, $msg);
} else {
jsonResponse(false, 'อัปโหลดไม่สำเร็จ: ' . implode(', ', $errors));
}
?>
@@ -0,0 +1,128 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
require_once '../includes/logger.php';
require_once '../includes/notifier.php';
// เช็คสิทธิ์ (อย่างน้อยต้องล็อกอิน)
if (!isset($_SESSION['user_id'])) {
jsonResponse(false, 'Unauthorized', [], 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(false, 'Invalid request method');
}
$id = $_POST['id'] ?? '';
$license_plate = trim($_POST['license_plate'] ?? '');
$type = trim($_POST['type'] ?? '');
$brand = trim($_POST['brand'] ?? '');
$model = trim($_POST['model'] ?? '');
$status = $_POST['status'] ?? 'available';
$tax_expiry = empty($_POST['tax_expiry']) ? null : $_POST['tax_expiry'];
// New fields
$chassis_number = trim($_POST['chassis_number'] ?? '');
$engine_number = trim($_POST['engine_number'] ?? '');
$engine_cc = empty($_POST['engine_cc']) ? null : (int)$_POST['engine_cc'];
$fuel_capacity = trim($_POST['fuel_capacity'] ?? '');
$fuel_type = trim($_POST['fuel_type'] ?? '');
$color = trim($_POST['color'] ?? '#FFFFFF');
$asset_number = trim($_POST['asset_number'] ?? '');
$vehicle_style = trim($_POST['vehicle_style'] ?? '');
$model_year = trim($_POST['model_year'] ?? '');
if (empty($license_plate) || empty($type)) {
jsonResponse(false, 'กรุณากรอกข้อมูลที่จำเป็นให้ครบถ้วน');
}
// สร้างคอลัมน์ image_path อัตโนมัติหากยังไม่มี
try {
$pdo->exec("ALTER TABLE vehicles ADD COLUMN image_path VARCHAR(255) NULL");
} catch (PDOException $e) {
// Column might already exist
}
// อัปเดต ENUM สำหรับ status ให้รองรับ disposed
try {
$pdo->exec("ALTER TABLE vehicles MODIFY COLUMN status ENUM('available', 'in_use', 'maintenance', 'disposed') DEFAULT 'available'");
} catch (PDOException $e) {
// ENUM might already be updated
}
// อัปโหลดรูปภาพใหม่
$image_path = null;
$upload_dir = '../uploads/vehicles/';
if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
if (isset($_FILES['image']) && $_FILES['image']['error'] == UPLOAD_ERR_OK) {
$file_ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
$allowed_ext = ['jpg', 'jpeg', 'png', 'webp'];
if (in_array($file_ext, $allowed_ext)) {
// ลบรูปเก่าถ้ามีการอัปโหลดใหม่ (กรณีแก้ไข)
if (!empty($id)) {
$stmt = $pdo->prepare("SELECT image_path FROM vehicles WHERE id = ?");
$stmt->execute([$id]);
$old_vehicle = $stmt->fetch();
if ($old_vehicle && !empty($old_vehicle['image_path'])) {
$old_path = '../' . $old_vehicle['image_path'];
if (file_exists($old_path)) unlink($old_path);
}
}
$new_filename = 'veh_' . time() . '_' . rand(1000, 9999) . '.' . $file_ext;
$dest = $upload_dir . $new_filename;
if (move_uploaded_file($_FILES['image']['tmp_name'], $dest)) {
$image_path = 'uploads/vehicles/' . $new_filename;
}
}
}
try {
if (empty($id)) {
// สร้างใหม่
// เช็คทะเบียนซ้ำ
$stmt = $pdo->prepare("SELECT id FROM vehicles WHERE license_plate = ?");
$stmt->execute([$license_plate]);
if ($stmt->fetch()) {
jsonResponse(false, 'ทะเบียนรถนี้มีในระบบแล้ว');
}
$stmt = $pdo->prepare("INSERT INTO vehicles (license_plate, type, brand, model, status, tax_expiry, chassis_number, engine_number, engine_cc, fuel_capacity, fuel_type, color, asset_number, vehicle_style, model_year, image_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$license_plate, $type, $brand, $model, $status, $tax_expiry, $chassis_number, $engine_number, $engine_cc, $fuel_capacity, $fuel_type, $color, $asset_number, $vehicle_style, $model_year, $image_path]);
$newId = $pdo->lastInsertId();
logActivity("Create Vehicle ({$license_plate})", 'vehicles', $newId);
notifyEvent('vehicle', "🚗 แจ้งเตือนยานพาหนะ\nมีรถใหม่เพิ่มเข้าระบบ ทะเบียน: {$license_plate} ({$brand} {$model})");
jsonResponse(true, 'เพิ่มรถใหม่สำเร็จ');
} else {
// แก้ไข
$stmt = $pdo->prepare("SELECT id FROM vehicles WHERE license_plate = ? AND id != ?");
$stmt->execute([$license_plate, $id]);
if ($stmt->fetch()) {
jsonResponse(false, 'ทะเบียนรถนี้มีในระบบแล้ว (ซ้ำกับคันอื่น)');
}
if ($image_path) {
$stmt = $pdo->prepare("UPDATE vehicles SET license_plate=?, type=?, brand=?, model=?, status=?, tax_expiry=?, chassis_number=?, engine_number=?, engine_cc=?, fuel_capacity=?, fuel_type=?, color=?, asset_number=?, vehicle_style=?, model_year=?, image_path=? WHERE id=?");
$stmt->execute([$license_plate, $type, $brand, $model, $status, $tax_expiry, $chassis_number, $engine_number, $engine_cc, $fuel_capacity, $fuel_type, $color, $asset_number, $vehicle_style, $model_year, $image_path, $id]);
} else {
$stmt = $pdo->prepare("UPDATE vehicles SET license_plate=?, type=?, brand=?, model=?, status=?, tax_expiry=?, chassis_number=?, engine_number=?, engine_cc=?, fuel_capacity=?, fuel_type=?, color=?, asset_number=?, vehicle_style=?, model_year=? WHERE id=?");
$stmt->execute([$license_plate, $type, $brand, $model, $status, $tax_expiry, $chassis_number, $engine_number, $engine_cc, $fuel_capacity, $fuel_type, $color, $asset_number, $vehicle_style, $model_year, $id]);
}
logActivity("Update Vehicle ({$license_plate})", 'vehicles', $id);
notifyEvent('vehicle', "🚗 แจ้งเตือนยานพาหนะ\nมีการอัปเดตข้อมูลรถ ทะเบียน: {$license_plate}");
jsonResponse(true, 'แก้ไขข้อมูลรถสำเร็จ');
}
} catch (PDOException $e) {
jsonResponse(false, 'Database Error: ' . $e->getMessage());
}
?>
@@ -0,0 +1,20 @@
<?php
require_once '../includes/db.php';
require_once '../includes/api_helper.php';
try {
// Fetch unique brands
$stmtBrands = $pdo->query("SELECT DISTINCT brand FROM vehicles WHERE brand IS NOT NULL AND brand != '' ORDER BY brand");
$brands = $stmtBrands->fetchAll(PDO::FETCH_COLUMN);
// Fetch unique models
$stmtModels = $pdo->query("SELECT DISTINCT model FROM vehicles WHERE model IS NOT NULL AND model != '' ORDER BY model");
$models = $stmtModels->fetchAll(PDO::FETCH_COLUMN);
echo jsonResponse(true, 'Suggestions loaded', [
'brands' => $brands,
'models' => $models
]);
} catch (PDOException $e) {
echo jsonResponse(false, 'Database error: ' . $e->getMessage());
}
@@ -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()]);
}
@@ -0,0 +1,11 @@
{
"src/js/app.js": {
"file": "assets/app-BYxElXcr.js",
"name": "app",
"src": "src/js/app.js",
"isEntry": true,
"css": [
"assets/app-zATCI3Qy.css"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

@@ -0,0 +1,21 @@
{
"name": "ระบบจัดการข้อมูลยานพาหนะ โรงพยาบาลเกาะสมุย",
"short_name": "จัดการรถ รพ.",
"description": "ระบบจัดการข้อมูลยานพาหนะและพนักงานขับรถ โรงพยาบาลเกาะสมุย",
"start_url": "/index.html",
"display": "standalone",
"background_color": "#F8FAFC",
"theme_color": "#0F6CBD",
"icons": [
{
"src": "/images/logo.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/logo.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
@@ -0,0 +1,30 @@
const CACHE_NAME = 'vehicle-management-v1';
const urlsToCache = [
'/',
'/index.html',
'/login.html',
'/manifest.json',
'/images/logo.jpg',
'/images/bg.jpg'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
@@ -0,0 +1,326 @@
<?php
require_once 'includes/auth.php';
require_once 'includes/db.php';
$pageTitle = 'พนักงานขับรถ | ระบบจัดการข้อมูลยานพาหนะ โรงพยาบาลเกาะสมุย';
$pageTitleDisplay = 'พนักงานขับรถ';
$breadcrumbs = ['พนักงานขับรถ'];
require_once 'includes/header.php';
// ดึงข้อมูลพนักงานขับรถ
$stmt = $pdo->query("SELECT * FROM drivers ORDER BY first_name ASC");
$drivers = $stmt->fetchAll();
?>
<!-- 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>
<!-- Layout Wrapper -->
<div x-data="driverManager()" class="flex h-screen overflow-hidden">
<!-- Sidebar -->
<?php include 'includes/sidebar.php'; ?>
<!-- Main Content -->
<main class="flex-1 flex flex-col min-w-0 overflow-hidden relative z-10">
<!-- Top Navbar -->
<?php include 'includes/navbar.php'; ?>
<!-- Dashboard Content -->
<div class="flex-1 overflow-y-auto p-4 pt-0 custom-scrollbar page-transition">
<div class="mb-6 flex justify-between items-end">
<div>
<h1 class="text-2xl font-bold dark:text-white tracking-tight">พนักงานขับรถ</h1>
<p class="text-slate-500 dark:text-slate-400 mt-1">รายชื่อและสถานะของพนักงานขับรถทั้งหมด</p>
</div>
<button @click="openModal()" class="btn-gradient px-4 py-2 text-sm 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-user-plus mr-2"></i> เพิ่มพนักงาน
</button>
</div>
<!-- Grid Section -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
<?php if(count($drivers) > 0): ?>
<?php foreach($drivers as $d): ?>
<div class="glass-card p-6 flex flex-col hover:-translate-y-1 transition-transform duration-300 relative overflow-hidden">
<!-- Status Indicator (Top Right) -->
<div class="absolute top-4 right-4">
<select @change="updateStatus(<?= $d['id'] ?>, $event.target.value)" class="text-xs bg-white/50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-lg px-2 py-1 focus:outline-none focus:ring-1 focus:ring-primary-500">
<option value="available" <?= $d['status'] == 'available' ? 'selected' : '' ?>>🟢 ว่าง</option>
<option value="busy" <?= $d['status'] == 'busy' ? 'selected' : '' ?>>🟠 ติดภารกิจ</option>
<option value="leave" <?= $d['status'] == 'leave' ? 'selected' : '' ?>>⚪ ลางาน</option>
</select>
</div>
<div class="flex items-center space-x-4 mb-4">
<img src="<?= !empty($d['hr_cid']) ? 'api/hr_image.php?cid='.urlencode($d['hr_cid']) : ($d['avatar'] ? htmlspecialchars($d['avatar']) : 'https://ui-avatars.com/api/?name='.urlencode($d['first_name'].' '.$d['last_name']).'&background=random') ?>" alt="Avatar" class="w-16 h-16 rounded-full object-cover border-2 border-primary-100 dark:border-primary-900 shadow-md bg-slate-100">
<div>
<h3 class="text-lg font-bold dark:text-white flex items-center gap-2">
<?= htmlspecialchars($d['first_name'] . ' ' . $d['last_name']) ?>
<?php if(!empty($d['sex']) && $d['sex'] === 'M'): ?><i class="fa-solid fa-mars text-blue-500 text-sm" title="ชาย"></i><?php endif; ?>
<?php if(!empty($d['sex']) && $d['sex'] === 'F'): ?><i class="fa-solid fa-venus text-pink-500 text-sm" title="หญิง"></i><?php endif; ?>
</h3>
<p class="text-sm text-slate-500 dark:text-slate-400"><?= !empty($d['position']) ? htmlspecialchars($d['position']) : 'พนักงานขับรถประจำ' ?></p>
<?php if(!empty($d['department'])): ?>
<p class="text-xs text-primary-600/80 dark:text-primary-400/80 mt-0.5 font-medium"><?= htmlspecialchars($d['department']) ?></p>
<?php endif; ?>
</div>
</div>
<div class="space-y-2 mt-2 mb-4">
<div class="flex items-center text-sm text-slate-600 dark:text-slate-300">
<i class="fa-solid fa-phone w-5 text-center text-slate-400"></i>
<span class="ml-2"><?= htmlspecialchars($d['phone'] ?: 'ไม่ได้ระบุ') ?></span>
</div>
<?php if(!empty($d['license_type'])): ?>
<div class="flex items-center text-sm text-slate-600 dark:text-slate-300 mb-1">
<i class="fa-solid fa-id-badge w-5 text-center text-slate-400"></i>
<span class="ml-2">ประเภทใบขับขี่: <?= htmlspecialchars($d['license_type']) ?></span>
</div>
<?php endif; ?>
<?php if(!empty($d['license_expiry_date'])): ?>
<div class="flex items-center text-sm <?php echo (strtotime($d['license_expiry_date']) < time()) ? 'text-danger font-medium' : 'text-slate-600 dark:text-slate-300'; ?>">
<i class="fa-solid fa-id-card w-5 text-center <?php echo (strtotime($d['license_expiry_date']) < time()) ? 'text-danger' : 'text-slate-400'; ?>"></i>
<span class="ml-2">หมดอายุ: <span x-text="formatThaiDate('<?= $d['license_expiry_date'] ?>')"></span></span>
</div>
<?php endif; ?>
</div>
<div class="mt-auto pt-4 border-t border-slate-200 dark:border-slate-700/50 flex justify-end space-x-2">
<button @click="deleteDriver(<?= $d['id'] ?>)" class="px-3 py-1.5 text-xs font-medium text-danger bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 rounded-lg transition-colors"><i class="fa-solid fa-trash mr-1"></i> ลบ</button>
<button @click="openModal(<?= htmlspecialchars(json_encode($d), ENT_QUOTES, 'UTF-8') ?>)" class="px-3 py-1.5 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors shadow-sm shadow-primary-600/30"><i class="fa-solid fa-pen-to-square mr-1"></i> แก้ไข</button>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="col-span-full glass-card p-10 flex flex-col items-center justify-center text-slate-500">
<i class="fa-solid fa-users-slash text-4xl mb-4 opacity-50"></i>
<p>ไม่พบข้อมูลพนักงานขับรถ</p>
</div>
<?php endif; ?>
</div>
</div>
<!-- Driver Modal -->
<div x-show="showModal" 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-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div x-show="showModal" x-transition.opacity class="fixed inset-0 bg-slate-900/60 dark:bg-slate-900/80 backdrop-blur-sm transition-opacity" @click="closeModal()"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div x-show="showModal"
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="relative z-10 inline-block align-bottom bg-white dark:bg-slate-800 rounded-[20px] text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-md w-full border border-slate-200 dark:border-slate-700/50">
<div class="px-6 py-5 border-b border-slate-100 dark:border-slate-700/50 flex justify-between items-center bg-slate-50/50 dark:bg-slate-800/50">
<h3 class="text-lg leading-6 font-bold text-slate-900 dark:text-white" id="modal-title" x-text="isEdit ? 'แก้ไขข้อมูลพนักงาน' : 'เพิ่มพนักงานใหม่'"></h3>
<button @click="closeModal()" class="text-slate-400 hover:text-slate-500 focus:outline-none bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 rounded-full p-2 transition-colors">
<i class="fa-solid fa-xmark w-4 h-4 flex items-center justify-center"></i>
</button>
</div>
<div class="px-6 py-5">
<form @submit.prevent="saveDriver">
<div class="grid grid-cols-1 gap-y-4 gap-x-4 sm:grid-cols-2">
<div class="sm:col-span-2 relative" x-show="!isEdit">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"><i class="fa-solid fa-hospital-user text-primary-500 mr-1"></i> ค้นหาพนักงานจากระบบ HIS (hosoffice)</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<i class="fa-solid fa-search text-slate-400"></i>
</div>
<input type="text" x-model="hrSearchQuery" @input.debounce.500ms="searchHrPerson" class="glass-input block w-full pl-10 p-2.5 text-sm" placeholder="พิมพ์ชื่อหรือนามสกุล (อย่างน้อย 2 ตัวอักษร)...">
</div>
<!-- Autocomplete Dropdown -->
<div x-show="hrSearchResults.length > 0" @click.away="hrSearchResults = []" style="display: none;" class="absolute z-10 w-full mt-1 bg-white dark:bg-slate-800 rounded-md shadow-lg shadow-slate-200/50 dark:shadow-slate-900/50 border border-slate-200 dark:border-slate-700 max-h-80 overflow-auto">
<ul class="py-1 text-sm text-slate-700 dark:text-slate-200">
<template x-for="person in hrSearchResults">
<li>
<button type="button" @click="selectHrPerson(person)" class="w-full text-left px-4 py-3 hover:bg-primary-50 dark:hover:bg-slate-700 transition-colors border-b border-slate-100 dark:border-slate-700/50 last:border-0 flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<img x-show="person.has_image" :src="'api/hr_image.php?cid=' + person.cid" class="w-10 h-10 rounded-full object-cover border border-slate-200 dark:border-slate-600">
<div x-show="!person.has_image" class="w-10 h-10 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-500 dark:text-slate-400">
<i class="fa-solid fa-user"></i>
</div>
</div>
<div class="flex-1 min-w-0">
<div class="font-bold text-primary-700 dark:text-primary-400 flex items-center gap-2">
<span x-text="person.full_name"></span>
<i x-show="person.sex === 'M'" class="fa-solid fa-mars text-blue-500 text-xs" title="ชาย"></i>
<i x-show="person.sex === 'F'" class="fa-solid fa-venus text-pink-500 text-xs" title="หญิง"></i>
</div>
<div class="text-xs text-slate-500 dark:text-slate-400 mt-1 flex flex-col gap-0.5">
<span x-show="person.position" x-text="'ตำแหน่ง: ' + person.position"></span>
<span x-show="person.department" x-text="'หน่วยงาน: ' + person.department"></span>
<span x-show="person.work_duration" x-text="'อายุงาน: ' + person.work_duration"></span>
</div>
</div>
</button>
</li>
</template>
</ul>
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ชื่อ <span class="text-danger">*</span></label>
<input type="text" x-model="form.first_name" 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 dark:text-slate-300 mb-1">นามสกุล <span class="text-danger">*</span></label>
<input type="text" x-model="form.last_name" required class="glass-input block w-full p-2.5 text-sm" placeholder="เช่น รักดี">
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">เบอร์โทรศัพท์</label>
<input type="text" x-model="form.phone" class="glass-input block w-full p-2.5 text-sm" placeholder="เช่น 0812345678">
</div>
<!-- ข้อมูลจาก HIS (Readonly) -->
<div class="sm:col-span-2 grid grid-cols-1 sm:grid-cols-2 gap-4 mt-2 p-4 bg-primary-50/50 dark:bg-primary-900/10 rounded-xl border border-primary-100 dark:border-primary-900/30" x-show="form.hr_cid" style="display: none;">
<div class="sm:col-span-2 mb-1">
<h4 class="text-xs font-bold text-primary-600 dark:text-primary-400 uppercase tracking-wider"><i class="fa-solid fa-link mr-1"></i> ข้อมูลที่เชื่อมโยงจาก HIS</h4>
</div>
<div>
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">ตำแหน่ง</label>
<input type="text" x-model="form.position" readonly class="glass-input block w-full p-2 text-sm bg-slate-50 dark:bg-slate-800/50 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-700 cursor-not-allowed">
</div>
<div>
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">หน่วยงาน</label>
<input type="text" x-model="form.department" readonly class="glass-input block w-full p-2 text-sm bg-slate-50 dark:bg-slate-800/50 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-700 cursor-not-allowed">
</div>
<div>
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">ประเภทพนักงาน</label>
<input type="text" x-model="form.person_type" readonly class="glass-input block w-full p-2 text-sm bg-slate-50 dark:bg-slate-800/50 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-700 cursor-not-allowed">
</div>
<div>
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">รหัสอ้างอิง (CID)</label>
<input type="text" x-model="form.hr_cid" readonly class="glass-input block w-full p-2 text-sm bg-slate-50 dark:bg-slate-800/50 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-700 cursor-not-allowed">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">สถานะเริ่มต้น</label>
<select x-model="form.status" class="glass-input block w-full p-2.5 text-sm">
<option value="available">ว่าง</option>
<option value="busy">ติดภารกิจ</option>
<option value="leave">ลางาน</option>
</select>
</div>
<div class="sm:col-span-2">
<div class="flex items-center mb-1">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">ประเภทใบขับขี่</label>
<div class="relative ml-2 flex items-center group">
<i class="fa-solid fa-circle-info text-slate-400 group-hover:text-primary-500 cursor-help transition-colors"></i>
<div class="absolute left-6 top-1/2 -translate-y-1/2 z-50 w-72 p-3 text-xs leading-relaxed bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 rounded-lg shadow-xl border border-slate-200 dark:border-slate-700 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
<div class="font-bold mb-1 border-b pb-1 dark:border-slate-700">รายละเอียดใบขับขี่แต่ละประเภท</div>
<ul class="list-disc pl-4 space-y-1 mt-1">
<li><b>ท.1 / บ.1</b>: รถยนต์ส่วนบุคคล / รถตู้ (น้ำหนักไม่เกิน 3,500 กก.)</li>
<li><b>ท.2 / บ.2</b>: รถบรรทุก 6 ล้อ, 10 ล้อ</li>
<li><b>ท.3 / บ.3</b>: รถพ่วง, รถลากจูง</li>
<li><b>ท.4 / บ.4</b>: รถวัตถุอันตราย</li>
</ul>
<div class="mt-2 text-[10px] text-slate-400 bg-slate-50 dark:bg-slate-700/50 p-1.5 rounded">
<span class="font-bold text-primary-500">ท</span> = ประเภททุกประเภท (รับจ้าง/สาธารณะ)<br>
<span class="font-bold text-primary-500">บ</span> = ประเภทส่วนบุคคล
</div>
</div>
</div>
</div>
<select x-model="form.license_type" class="glass-input block w-full p-2.5 text-sm mb-3">
<option value="">-- ไม่ระบุ --</option>
<option value="ท.1">ท.1 (รถยนต์ทุกประเภท)</option>
<option value="บ.1">บ.1 (รถยนต์ส่วนบุคคล)</option>
<option value="ท.2">ท.2 (รถบรรทุกทุกประเภท)</option>
<option value="บ.2">บ.2 (รถบรรทุกส่วนบุคคล)</option>
<option value="ท.3">ท.3 (รถลากจูงทุกประเภท)</option>
<option value="บ.3">บ.3 (รถลากจูงส่วนบุคคล)</option>
<option value="ท.4">ท.4 (รถวัตถุอันตรายทุกประเภท)</option>
<option value="บ.4">บ.4 (รถวัตถุอันตรายส่วนบุคคล)</option>
</select>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">วันหมดอายุใบขับขี่</label>
<input type="date" x-model="form.license_expiry_date" class="glass-input block w-full p-2.5 text-sm">
</div>
<div class="sm:col-span-2 text-xs text-slate-500 mt-2">
<i class="fa-solid fa-circle-info mr-1"></i> ระบบจะสร้างรูปโปรไฟล์จากชื่อให้อัตโนมัติ
</div>
</div>
<!-- Document Section (Only show when editing an existing driver) -->
<div class="mt-8 border-t border-slate-200 dark:border-slate-700 pt-6" x-show="isEdit" style="display: none;">
<div class="flex justify-between items-center mb-4">
<h4 class="text-md font-bold text-slate-800 dark:text-white"><i class="fa-solid fa-file-pdf text-red-500 mr-2"></i> เอกสารใบขับขี่</h4>
<div class="relative">
<input type="file" id="driverDocInput" accept="application/pdf" class="hidden" @change="uploadDocument($event)">
<button type="button" @click="document.getElementById('driverDocInput').click()" class="btn-gradient px-3 py-1.5 text-xs flex items-center shadow-md shadow-primary-500/20" :disabled="isUploadingDoc">
<i class="fa-solid" :class="isUploadingDoc ? 'fa-spinner fa-spin' : 'fa-upload'"></i>
<span class="ml-2" x-text="isUploadingDoc ? 'กำลังอัปโหลด...' : 'อัปโหลด PDF'"></span>
</button>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-800/50 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
<!-- Loading state -->
<div x-show="isLoadingDocs" class="text-center py-4 text-slate-500">
<i class="fa-solid fa-spinner fa-spin text-2xl mb-2 text-primary-500"></i>
<p class="text-xs">กำลังโหลดเอกสาร...</p>
</div>
<!-- Empty state -->
<div x-show="!isLoadingDocs && documents.length === 0" class="text-center py-6 text-slate-500" style="display: none;">
<i class="fa-solid fa-folder-open text-3xl mb-2 opacity-30"></i>
<p class="text-sm">ยังไม่มีเอกสารแนบ</p>
</div>
<!-- Document List -->
<ul x-show="!isLoadingDocs && documents.length > 0" class="space-y-2">
<template x-for="doc in documents" :key="doc.id">
<li class="flex items-center justify-between p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 shadow-sm hover:border-primary-300 dark:hover:border-primary-700 transition-colors group">
<a :href="doc.file_path" target="_blank" class="flex items-center flex-1 min-w-0 hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
<i class="fa-solid fa-file-pdf text-red-500 text-lg mr-3"></i>
<div class="truncate">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300 truncate" x-text="doc.original_name"></p>
<p class="text-xs text-slate-500" x-text="formatDate(doc.created_at)"></p>
</div>
</a>
<button type="button" @click="deleteDocument(doc.id)" class="ml-4 p-2 text-slate-400 hover:text-danger hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100" title="ลบเอกสาร">
<i class="fa-solid fa-trash-can"></i>
</button>
</li>
</template>
</ul>
</div>
</div>
<div class="mt-6 pt-5 border-t border-slate-100 dark:border-slate-700/50 flex justify-end space-x-3">
<button type="button" @click="closeModal()" class="px-4 py-2.5 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-xl hover:bg-slate-50 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-600 dark:hover:bg-slate-700 transition-colors">
ยกเลิก
</button>
<button type="submit" :disabled="isLoading" class="btn-gradient px-4 py-2.5 text-sm flex items-center justify-center min-w-[100px] shadow-lg shadow-primary-500/30">
<span x-show="!isLoading" x-text="isEdit ? 'บันทึกการแก้ไข' : 'เพิ่มข้อมูล'"></span>
<span x-show="isLoading" style="display: none;"><i class="fa-solid fa-spinner fa-spin mr-2"></i> กำลังบันทึก...</span>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</main>
</div>
<?php require_once 'includes/footer.php'; ?>
@@ -0,0 +1,13 @@
<?php
// ฟังก์ชันสำหรับส่งคืนค่าเป็น JSON
function jsonResponse($success, $message, $data = [], $statusCode = 200) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode([
'success' => $success,
'message' => $message,
'data' => $data
]);
exit();
}
?>
@@ -0,0 +1,16 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// เช็คว่าผู้ใช้ล็อกอินหรือยัง ถ้ายังให้เตะไปหน้า login
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit();
}
// ฟังก์ชันสำหรับเช็คว่าเป็นแอดมินหรือไม่
function isAdmin() {
return isset($_SESSION['role']) && $_SESSION['role'] === 'admin';
}
?>
@@ -0,0 +1,20 @@
<?php
$host = 'localhost';
$db = 'gso_cars';
$user = 'root';
$pass = '@Samui@10742'; // เปลี่ยนรหัสผ่านตรงนี้ให้ตรงกับเซิร์ฟเวอร์ของคุณ
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
// ใน production ควรบันทึก error ลง log และไม่แสดงข้อความ error โดยตรง
die("Database connection failed: " . $e->getMessage());
}
@@ -0,0 +1,23 @@
<?php
// ข้อมูลการเชื่อมต่อฐานข้อมูลที่ 2 (hosoffice_2566)
// กรุณาแก้ไขข้อมูลด้านล่างนี้ให้ตรงกับเซิร์ฟเวอร์จริงของคุณ
$hr_host = '10.0.250.115'; // เปลี่ยนเป็น IP ของเซิร์ฟเวอร์ hosoffice_2566
$hr_db = 'hosoffice_2566';
$hr_user = 'hosoffice'; // เปลี่ยนเป็น Username ของคุณ
$hr_pass = 'hosoffice10742'; // เปลี่ยนเป็น Password ของคุณ
$hr_charset = 'utf8mb4';
$hr_dsn = "mysql:host=$hr_host;dbname=$hr_db;charset=$hr_charset";
$hr_options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo_hr = new PDO($hr_dsn, $hr_user, $hr_pass, $hr_options);
} catch (\PDOException $e) {
// ใน production ไม่ควรแสดงข้อความ error ของ DB โดยตรง
// die("Database HR connection failed: " . $e->getMessage());
$pdo_hr = null; // หากเชื่อมต่อไม่ได้ให้ค่าเป็น null เพื่อจัดการ error ต่อใน API
}
@@ -0,0 +1,15 @@
<?php
$host = 'localhost'; // หรือ IP ของเซิร์ฟเวอร์ Intranet
$dbname = 'intranet';
$username = 'root'; // เปลี่ยนเป็น Username ที่ใช้งานจริง
$password = '@Samui@10742'; // เปลี่ยนเป็น Password ที่ใช้งานจริง
try {
$intranet_pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $username, $password);
$intranet_pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$intranet_pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
// ระงับการแสดงข้อผิดพลาดทางหน้าเว็บเพื่อความปลอดภัย แต่ให้คืนค่า false หรือ null ไว้เพื่อตรวจสอบ
$intranet_pdo = null;
error_log("Connection failed (Intranet): " . $e->getMessage());
}
@@ -0,0 +1,31 @@
<?php require_once __DIR__ . '/vite.php'; ?>
<!DOCTYPE html>
<html lang="th" class="antialiased">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $pageTitle ?? 'ระบบจัดการข้อมูลยานพาหนะ โรงพยาบาลเกาะสมุย' ?></title>
<link rel="manifest" href="public/manifest.json">
<meta name="theme-color" content="#0F6CBD">
<link rel="icon" type="image/png" href="public/images/logo.png">
<link rel="apple-touch-icon" href="public/images/logo.png">
<link href="https://fonts.googleapis.com/css2?family=Prompt:wght@300;400;500;600;700&family=Noto+Sans+Thai:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Vite Assets -->
<?= vite('src/js/app.js') ?>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Theme Script -->
<script>
if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
</script>
</head>
<body x-data="themeStore" class="font-sans transition-colors duration-300 <?= $bodyClass ?? 'bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200' ?>" <?= $bodyAttributes ?? '' ?>>
@@ -0,0 +1,24 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/db.php';
function logActivity($action, $table_name = null, $record_id = null) {
global $pdo;
$user_id = $_SESSION['user_id'] ?? null;
$ip_address = $_SERVER['REMOTE_ADDR'] ?? null;
if ($ip_address === '::1') {
$ip_address = '127.0.0.1';
}
try {
$stmt = $pdo->prepare("INSERT INTO activity_logs (user_id, action, table_name, record_id, ip_address) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$user_id, $action, $table_name, $record_id, $ip_address]);
} catch (PDOException $e) {
error_log("Failed to log activity: " . $e->getMessage());
}
}
?>
@@ -0,0 +1,93 @@
<?php
$fullName = $_SESSION['full_name'] ?? 'Admin User';
$roleName = ($_SESSION['role'] ?? '') === 'admin' ? 'ผู้ดูแลระบบ' : 'เจ้าหน้าที่';
$pageTitleDisplay = $pageTitleDisplay ?? 'ระบบจัดการข้อมูลยานพาหนะ';
if (!isset($_SESSION['user_avatar'])) {
$_SESSION['user_avatar'] = "https://ui-avatars.com/api/?name=" . urlencode($fullName) . "&background=0F6CBD&color=fff";
// ค้นหารูปจากฐานข้อมูล HR ด้วยชื่อ-สกุล
try {
require_once 'includes/db_hr.php';
if ($pdo_hr) {
// ลบคำนำหน้าชื่อถ้ามี เช่น นาย นาง นางสาว
$searchName = preg_replace('/^(นาย|นางสาว|นาง|ว่าที่ร\.ต\.|นพ\.|พญ\.)/', '', $fullName);
$nameParts = explode(' ', trim($searchName));
$fname = $nameParts[0] ?? '';
$lname = $nameParts[1] ?? '';
if (!empty($fname)) {
$stmt = $pdo_hr->prepare("SELECT HR_CID FROM hr_person WHERE HR_FNAME LIKE ? AND (HR_LNAME LIKE ? OR ? = '') AND HR_IMAGE IS NOT NULL LIMIT 1");
$stmt->execute(["%$fname%", "%$lname%", $lname]);
$hr_person = $stmt->fetch(PDO::FETCH_ASSOC);
if ($hr_person && !empty($hr_person['HR_CID'])) {
$_SESSION['user_avatar'] = "api/hr_image.php?cid=" . urlencode($hr_person['HR_CID']);
}
}
}
} catch (Exception $e) {
// Ignore error
}
}
$userAvatar = $_SESSION['user_avatar'];
?>
<header class="h-20 glass-card m-4 px-6 flex items-center justify-between z-20 border-slate-200 dark:border-slate-700/50 rounded-[16px] shrink-0">
<div class="flex items-center">
<button @click="toggleSidebar()" class="text-slate-500 hover:text-slate-900 dark:hover:text-white focus:outline-none hidden sm:block p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
<i class="fa-solid fa-bars text-xl"></i>
</button>
<!-- Mobile Menu Button -->
<button class="text-slate-500 sm:hidden p-2">
<i class="fa-solid fa-bars text-xl"></i>
</button>
<!-- Breadcrumbs -->
<nav class="hidden md:flex ml-4" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 md:space-x-3">
<li class="inline-flex items-center">
<a href="index.php" class="inline-flex items-center text-sm font-medium text-slate-700 hover:text-primary-600 dark:text-slate-400 dark:hover:text-white">
<i class="fa-solid fa-house mr-2"></i>
หน้าหลัก
</a>
</li>
<?php if(isset($breadcrumbs) && is_array($breadcrumbs)): ?>
<?php foreach($breadcrumbs as $breadcrumb): ?>
<li aria-current="page">
<div class="flex items-center">
<i class="fa-solid fa-chevron-right text-slate-400 mx-2 text-xs"></i>
<span class="text-sm font-medium text-slate-500 dark:text-slate-400"><?= htmlspecialchars($breadcrumb) ?></span>
</div>
</li>
<?php endforeach; ?>
<?php else: ?>
<li aria-current="page">
<div class="flex items-center">
<i class="fa-solid fa-chevron-right text-slate-400 mx-2 text-xs"></i>
<span class="text-sm font-medium text-slate-500 dark:text-slate-400"><?= htmlspecialchars($pageTitleDisplay) ?></span>
</div>
</li>
<?php endif; ?>
</ol>
</nav>
</div>
<div class="flex items-center space-x-4">
<!-- Dark Mode Toggle -->
<button @click="toggleTheme()" class="p-2 rounded-lg text-slate-500 hover:text-primary-600 hover:bg-primary-50 dark:hover:text-primary-400 dark:hover:bg-slate-800 transition-colors relative overflow-hidden group">
<i class="fa-solid fa-sun text-xl absolute transition-all duration-500 transform" :class="darkMode ? 'opacity-0 scale-50 -rotate-90' : 'opacity-100 scale-100 rotate-0'"></i>
<i class="fa-solid fa-moon text-xl transition-all duration-500 transform" :class="darkMode ? 'opacity-100 scale-100 rotate-0' : 'opacity-0 scale-50 rotate-90'"></i>
</button>
<!-- Profile Dropdown -->
<div class="flex items-center cursor-pointer pl-2 border-l border-slate-200 dark:border-slate-700">
<img class="w-9 h-9 rounded-full object-cover border-2 border-primary-100 dark:border-primary-900" src="<?= htmlspecialchars($userAvatar) ?>" alt="User avatar">
<div class="hidden md:block ml-3">
<p class="text-sm font-medium dark:text-white"><?= htmlspecialchars($fullName) ?></p>
<p class="text-xs text-slate-500 dark:text-slate-400"><?= htmlspecialchars($roleName) ?></p>
</div>
<i class="fa-solid fa-chevron-down text-xs ml-2 text-slate-400"></i>
</div>
</div>
</header>
@@ -0,0 +1,112 @@
<?php
require_once __DIR__ . '/db.php';
/**
* Send message via LINE Notify
*/
function sendLineNotify($token, $message) {
if (empty($token)) return false;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://notify-api.line.me/api/notify");
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, "message=" . urlencode($message));
$headers = [
'Content-type: application/x-www-form-urlencoded',
'Authorization: Bearer ' . $token,
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return $error;
}
$res = json_decode($result, true);
if ($res && isset($res['status']) && $res['status'] == 200) {
return true;
}
return $res['message'] ?? 'Unknown error';
}
/**
* Send message via Telegram Bot API
*/
function sendTelegramMessage($token, $chat_id, $message) {
if (empty($token) || empty($chat_id)) return false;
$url = "https://api.telegram.org/bot{$token}/sendMessage";
$data = [
'chat_id' => $chat_id,
'text' => $message,
'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($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$result = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return $error;
}
$res = json_decode($result, true);
if ($res && isset($res['ok']) && $res['ok'] === true) {
return true;
}
return $res['description'] ?? 'Unknown error';
}
/**
* Main function to send notifications based on event settings
*
* @param string $eventType e.g., 'vehicle', 'driver', 'schedule', 'maintenance'
* @param string $message The message to send
*/
function notifyEvent($eventType, $message) {
global $pdo;
try {
// Fetch settings
$stmt = $pdo->query("SELECT setting_key, setting_value FROM settings");
$settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
$lineToken = $settings['line_notify_token'] ?? '';
$telegramToken = $settings['telegram_bot_token'] ?? '';
$telegramChatId = $settings['telegram_chat_id'] ?? '';
$notifyEventsStr = $settings['notify_events'] ?? '[]';
$notifyEvents = json_decode($notifyEventsStr, true) ?: [];
// Check if this event type is enabled
if (in_array($eventType, $notifyEvents)) {
// Send LINE
if (!empty($lineToken)) {
sendLineNotify($lineToken, $message);
}
// Send Telegram
if (!empty($telegramToken) && !empty($telegramChatId)) {
sendTelegramMessage($telegramToken, $telegramChatId, $message);
}
}
} catch (Exception $e) {
// Silently fail for notifications so it doesn't break main app flow
error_log("Notification Error: " . $e->getMessage());
}
}
@@ -0,0 +1,79 @@
<?php
// กำหนดหน้าปัจจุบันเพื่อทำ Active State ใน Sidebar
$currentPage = basename($_SERVER['PHP_SELF']);
?>
<aside :class="isExpanded ? 'w-64' : 'w-20'" class="glass-card hidden sm:flex flex-col m-4 mr-0 rounded-[16px] z-20 transition-all duration-300 ease-in-out border-slate-200 dark:border-slate-700/50">
<!-- Logo area -->
<div class="h-20 flex items-center justify-center border-b border-slate-200 dark:border-slate-700/50 px-4 shrink-0">
<img src="public/images/logo.png" alt="Logo" class="w-10 h-10 rounded-full flex-shrink-0 bg-white">
<span x-show="isExpanded" x-transition.opacity.duration.300ms class="ml-3 font-semibold text-lg truncate dark:text-white">รพ.เกาะสมุย</span>
</div>
<!-- Menu -->
<nav class="flex-1 overflow-y-auto py-4 space-y-2 px-3 custom-scrollbar">
<!-- หน้าหลัก (ภาพรวม) -->
<a href="index.php" class="flex items-center px-3 py-3 rounded-lg group transition-colors <?= $currentPage === 'index.php' ? 'text-primary-600 bg-primary-50 dark:bg-primary-900/20' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800/50' ?>">
<i class="fa-solid fa-chart-pie w-6 text-center text-lg <?= $currentPage !== 'index.php' ? 'group-hover:text-primary-500 transition-colors' : '' ?>"></i>
<span x-show="isExpanded" class="ml-3 font-medium <?= $currentPage !== 'index.php' ? 'group-hover:text-slate-900 dark:group-hover:text-white transition-colors' : '' ?>">ภาพรวม</span>
</a>
<!-- ยานพาหนะ -->
<a href="vehicles.php" class="flex items-center px-3 py-3 rounded-lg group transition-colors <?= $currentPage === 'vehicles.php' ? 'text-primary-600 bg-primary-50 dark:bg-primary-900/20' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800/50' ?>">
<i class="fa-solid fa-car w-6 text-center text-lg <?= $currentPage !== 'vehicles.php' ? 'group-hover:text-primary-500 transition-colors' : '' ?>"></i>
<span x-show="isExpanded" class="ml-3 font-medium <?= $currentPage !== 'vehicles.php' ? 'group-hover:text-slate-900 dark:group-hover:text-white transition-colors' : '' ?>">ยานพาหนะ</span>
</a>
<!-- พนักงานขับรถ -->
<a href="drivers.php" class="flex items-center px-3 py-3 rounded-lg group transition-colors <?= $currentPage === 'drivers.php' ? 'text-primary-600 bg-primary-50 dark:bg-primary-900/20' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800/50' ?>">
<i class="fa-solid fa-id-card w-6 text-center text-lg <?= $currentPage !== 'drivers.php' ? 'group-hover:text-primary-500 transition-colors' : '' ?>"></i>
<span x-show="isExpanded" class="ml-3 font-medium <?= $currentPage !== 'drivers.php' ? 'group-hover:text-slate-900 dark:group-hover:text-white transition-colors' : '' ?>">พนักงานขับรถ</span>
</a>
<!-- ตารางการเดินรถ -->
<a href="schedule.php" class="flex items-center px-3 py-3 rounded-lg group transition-colors <?= $currentPage === 'schedule.php' ? 'text-primary-600 bg-primary-50 dark:bg-primary-900/20' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800/50' ?>">
<i class="fa-regular fa-calendar-check w-6 text-center text-lg <?= $currentPage !== 'schedule.php' ? 'group-hover:text-primary-500 transition-colors' : '' ?>"></i>
<span x-show="isExpanded" class="ml-3 font-medium <?= $currentPage !== 'schedule.php' ? 'group-hover:text-slate-900 dark:group-hover:text-white transition-colors' : '' ?>">ตารางการเดินรถ</span>
</a>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<!-- หมวดหมู่การตั้งค่าระบบ -->
<div x-data="{ settingsOpen: <?= in_array($currentPage, ['users.php', 'settings_notifications.php', 'logs.php']) ? 'true' : 'false' ?> }">
<button @click="settingsOpen = !settingsOpen; if(!isExpanded) toggleSidebar()"
class="w-full flex items-center justify-between px-3 py-3 rounded-lg group transition-colors <?= in_array($currentPage, ['users.php', 'settings_notifications.php', 'logs.php']) ? 'text-primary-600 bg-primary-50 dark:bg-primary-900/20' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800/50' ?>">
<div class="flex items-center">
<i class="fa-solid fa-gear w-6 text-center text-lg <?= !in_array($currentPage, ['users.php', 'settings_notifications.php', 'logs.php']) ? 'group-hover:text-primary-500 transition-colors' : '' ?>"></i>
<span x-show="isExpanded" class="ml-3 font-medium <?= !in_array($currentPage, ['users.php', 'settings_notifications.php', 'logs.php']) ? 'group-hover:text-slate-900 dark:group-hover:text-white transition-colors' : '' ?>">ตั้งค่าระบบ</span>
</div>
<i x-show="isExpanded" class="fa-solid fa-chevron-down text-xs transition-transform duration-300" :class="settingsOpen ? 'rotate-180' : ''"></i>
</button>
<!-- เมนูย่อย -->
<div x-show="isExpanded && settingsOpen" x-transition.opacity class="pl-11 pr-3 py-2 space-y-1">
<a href="users.php" class="block px-3 py-2 rounded-lg text-sm transition-colors <?= $currentPage === 'users.php' ? 'text-primary-600 font-medium bg-primary-50 dark:bg-primary-900/20' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-50 dark:hover:text-white dark:hover:bg-slate-800/50' ?>">
<i class="fa-solid fa-users-gear mr-2 w-4"></i> ตั้งค่าผู้ใช้งาน
</a>
<a href="settings_notifications.php" class="block px-3 py-2 rounded-lg text-sm transition-colors <?= $currentPage === 'settings_notifications.php' ? 'text-primary-600 font-medium bg-primary-50 dark:bg-primary-900/20' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-50 dark:hover:text-white dark:hover:bg-slate-800/50' ?>">
<i class="fa-solid fa-bell mr-2 w-4"></i> การแจ้งเตือน
</a>
<a href="logs.php" class="block px-3 py-2 rounded-lg text-sm transition-colors <?= $currentPage === 'logs.php' ? 'text-primary-600 font-medium bg-primary-50 dark:bg-primary-900/20' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-50 dark:hover:text-white dark:hover:bg-slate-800/50' ?>">
<i class="fa-solid fa-list-check mr-2 w-4"></i> ประวัติการใช้งาน
</a>
</div>
</div>
<?php endif; ?>
<!-- คู่มือการใช้งาน -->
<a href="manual.php" class="flex items-center px-3 py-3 rounded-lg group transition-colors <?= $currentPage === 'manual.php' ? 'text-primary-600 bg-primary-50 dark:bg-primary-900/20' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800/50' ?>">
<i class="fa-solid fa-book w-6 text-center text-lg <?= $currentPage !== 'manual.php' ? 'group-hover:text-primary-500 transition-colors' : '' ?>"></i>
<span x-show="isExpanded" class="ml-3 font-medium <?= $currentPage !== 'manual.php' ? 'group-hover:text-slate-900 dark:group-hover:text-white transition-colors' : '' ?>">คู่มือการใช้งาน</span>
</a>
</nav>
<!-- Bottom Actions -->
<div class="p-4 border-t border-slate-200 dark:border-slate-700/50 shrink-0">
<a href="logout.php" class="flex items-center px-3 py-2 text-danger hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors">
<i class="fa-solid fa-arrow-right-from-bracket w-6 text-center text-lg"></i>
<span x-show="isExpanded" class="ml-3 font-medium">ออกจากระบบ</span>
</a>
</div>
</aside>
@@ -0,0 +1,43 @@
<?php
// กำหนดว่า Environment ปัจจุบันเป็น Development หรือไม่
// (ควรตั้งค่าเป็น false เมื่อนำไปใช้งานบนเซิร์ฟเวอร์จริง)
define('IS_DEV', false);
define('VITE_HOST', 'http://localhost:5173');
function vite($entry = 'src/js/app.js') {
$html = '';
// ตั้งค่า Base URL ให้เป็น path สัมพัทธ์ (Relative Path) เพื่อรองรับการรันในโฟลเดอร์ย่อย (เช่น /intranet/.../)
$dist_url = 'dist/';
if (IS_DEV) {
$html .= '<script type="module" src="' . VITE_HOST . '/@vite/client"></script>';
$html .= '<script type="module" src="' . VITE_HOST . '/' . $entry . '"></script>';
} else {
// ในโหมด Production เราจะอ่านไฟล์จาก manifest.json ที่ถูก Build แล้ว
$manifestPath = __DIR__ . '/../dist/.vite/manifest.json';
if (file_exists($manifestPath)) {
$manifest = json_decode(file_get_contents($manifestPath), true);
if (isset($manifest[$entry])) {
$file = $manifest[$entry]['file'];
// ตรวจสอบว่ามีไฟล์ CSS ที่ผูกกับ JS ตัวนี้หรือไม่
if (isset($manifest[$entry]['css'])) {
foreach ($manifest[$entry]['css'] as $cssFile) {
$html .= '<link rel="stylesheet" href="' . $dist_url . $cssFile . '">';
}
}
// โหลดไฟล์ JS หลัก
$html .= '<script type="module" src="' . $dist_url . $file . '?v=' . time() . '"></script>';
}
} else {
$html .= '<!-- Vite Manifest Not Found! Please run `npm run build` -->';
}
}
return $html;
}
?>
@@ -0,0 +1,243 @@
<?php
require_once 'includes/auth.php';
require_once 'includes/db.php';
// ดึงสถิติยานพาหนะ
$stmt = $pdo->query("SELECT status, COUNT(*) as count FROM vehicles GROUP BY status");
$vehicle_stats = ['total' => 0, 'available' => 0, 'in_use' => 0, 'maintenance' => 0];
while($row = $stmt->fetch()) {
$vehicle_stats[$row['status']] = $row['count'];
$vehicle_stats['total'] += $row['count'];
}
// ดึงรายการเดินรถวันนี้
$stmt = $pdo->query("
SELECT s.*, v.license_plate, v.type as vehicle_type, d.first_name, d.last_name
FROM schedules s
LEFT JOIN vehicles v ON s.vehicle_id = v.id
LEFT JOIN drivers d ON s.driver_id = d.id
WHERE DATE(s.start_time) = CURDATE()
ORDER BY s.start_time ASC
LIMIT 5
");
$today_schedules = $stmt->fetchAll();
$pageTitle = 'ภาพรวมระบบ | ระบบจัดการข้อมูลยานพาหนะ โรงพยาบาลเกาะสมุย';
$pageTitleDisplay = 'ภาพรวม';
require_once 'includes/header.php';
?>
<!-- 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>
<!-- Layout Wrapper -->
<div x-data="sidebarStore" class="flex h-screen overflow-hidden">
<!-- Sidebar -->
<?php include 'includes/sidebar.php'; ?>
<!-- Main Content -->
<main class="flex-1 flex flex-col min-w-0 overflow-hidden relative z-10">
<!-- Top Navbar -->
<?php include 'includes/navbar.php'; ?>
<!-- Dashboard Content -->
<div class="flex-1 overflow-y-auto p-4 pt-0 custom-scrollbar page-transition">
<div class="mb-6 flex justify-between items-end">
<div>
<h1 class="text-2xl font-bold dark:text-white tracking-tight">ภาพรวมระบบยานพาหนะ</h1>
<p class="text-slate-500 dark:text-slate-400 mt-1">ข้อมูลสถานะรถยนต์และพนักงานขับรถวันนี้</p>
</div>
<button class="btn-gradient px-4 py-2 text-sm flex items-center">
<i class="fa-solid fa-plus mr-2"></i> เพิ่มการจอง
</button>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
<!-- Stat Card 1 -->
<div class="glass-card p-6 flex items-center group">
<div class="w-12 h-12 rounded-xl bg-primary-100 dark:bg-primary-900/30 text-primary-600 flex items-center justify-center text-xl group-hover:scale-110 transition-transform">
<i class="fa-solid fa-van-shuttle"></i>
</div>
<div class="ml-4">
<h3 class="text-slate-500 dark:text-slate-400 text-sm font-medium">รถทั้งหมด</h3>
<p class="text-2xl font-bold dark:text-white mt-1"><?= $vehicle_stats['total'] ?> <span class="text-sm font-normal text-slate-400">คัน</span></p>
</div>
</div>
<!-- Stat Card 2 -->
<div class="glass-card p-6 flex items-center group">
<div class="w-12 h-12 rounded-xl bg-success/10 text-success flex items-center justify-center text-xl group-hover:scale-110 transition-transform">
<i class="fa-solid fa-circle-check"></i>
</div>
<div class="ml-4">
<h3 class="text-slate-500 dark:text-slate-400 text-sm font-medium">พร้อมใช้งาน</h3>
<p class="text-2xl font-bold dark:text-white mt-1"><?= $vehicle_stats['available'] ?> <span class="text-sm font-normal text-slate-400">คัน</span></p>
</div>
</div>
<!-- Stat Card 3 -->
<div class="glass-card p-6 flex items-center group">
<div class="w-12 h-12 rounded-xl bg-warning/10 text-warning flex items-center justify-center text-xl group-hover:scale-110 transition-transform">
<i class="fa-solid fa-route"></i>
</div>
<div class="ml-4">
<h3 class="text-slate-500 dark:text-slate-400 text-sm font-medium">กำลังปฏิบัติงาน</h3>
<p class="text-2xl font-bold dark:text-white mt-1"><?= $vehicle_stats['in_use'] ?> <span class="text-sm font-normal text-slate-400">คัน</span></p>
</div>
</div>
<!-- Stat Card 4 -->
<div class="glass-card p-6 flex items-center group">
<div class="w-12 h-12 rounded-xl bg-danger/10 text-danger flex items-center justify-center text-xl group-hover:scale-110 transition-transform">
<i class="fa-solid fa-wrench"></i>
</div>
<div class="ml-4">
<h3 class="text-slate-500 dark:text-slate-400 text-sm font-medium">ส่งซ่อม/บำรุง</h3>
<p class="text-2xl font-bold dark:text-white mt-1"><?= $vehicle_stats['maintenance'] ?> <span class="text-sm font-normal text-slate-400">คัน</span></p>
</div>
</div>
</div>
<!-- Main Section -->
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
<!-- Table Section -->
<div class="xl:col-span-2 glass-card rounded-[16px] overflow-hidden flex flex-col">
<div class="p-5 border-b border-slate-200 dark:border-slate-700/50 flex justify-between items-center bg-white/50 dark:bg-slate-800/50">
<h2 class="text-lg font-semibold dark:text-white">รายการเดินรถวันนี้</h2>
<button class="text-primary-600 hover:text-primary-700 text-sm font-medium">ดูทั้งหมด <i class="fa-solid fa-arrow-right text-xs ml-1"></i></button>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm text-left text-slate-500 dark:text-slate-400">
<thead class="text-xs text-slate-700 uppercase bg-slate-50 dark:bg-slate-700/50 dark:text-slate-300 sticky top-0 shadow-sm">
<tr>
<th scope="col" class="px-6 py-4">ทะเบียนรถ</th>
<th scope="col" class="px-6 py-4">ประเภท</th>
<th scope="col" class="px-6 py-4">พนักงานขับรถ</th>
<th scope="col" class="px-6 py-4">สถานะ</th>
<th scope="col" class="px-6 py-4">เวลา</th>
</tr>
</thead>
<tbody>
<?php if(count($today_schedules) > 0): ?>
<?php foreach($today_schedules as $s): ?>
<tr class="bg-white/40 dark:bg-slate-800/40 border-b border-slate-100 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-6 py-4 font-medium text-slate-900 dark:text-white"><?= htmlspecialchars($s['license_plate']) ?></td>
<td class="px-6 py-4"><?= htmlspecialchars($s['vehicle_type']) ?></td>
<td class="px-6 py-4 flex items-center">
<img class="w-6 h-6 rounded-full mr-2" src="https://ui-avatars.com/api/?name=<?= urlencode($s['first_name']) ?>&background=random" alt="Avatar">
<?= htmlspecialchars($s['first_name'] . ' ' . $s['last_name']) ?>
</td>
<td class="px-6 py-4">
<?php if($s['status'] == 'approved'): ?>
<span class="bg-success/10 text-success text-xs font-medium px-2.5 py-1 rounded-full border border-success/20">สแตนด์บาย</span>
<?php elseif($s['status'] == 'pending'): ?>
<span class="bg-warning/10 text-warning text-xs font-medium px-2.5 py-1 rounded-full border border-warning/20">รออนุมัติ</span>
<?php else: ?>
<span class="bg-slate-100 text-slate-600 text-xs font-medium px-2.5 py-1 rounded-full border border-slate-200">เสร็จสิ้น</span>
<?php endif; ?>
</td>
<td class="px-6 py-4"><?= date('H:i', strtotime($s['start_time'])) ?> <?= $s['end_time'] ? '- ' . date('H:i', strtotime($s['end_time'])) : '' ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="5" class="px-6 py-8 text-center text-slate-500">ไม่มีคิวเดินรถวันนี้</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination Placeholder -->
<div class="p-4 border-t border-slate-200 dark:border-slate-700/50 flex items-center justify-between mt-auto">
<span class="text-sm text-slate-500">แสดง 1 ถึง 3 จาก 12 รายการ</span>
<div class="flex space-x-1">
<button class="px-3 py-1 border border-slate-200 dark:border-slate-700 rounded-md text-slate-400 cursor-not-allowed">ก่อนหน้า</button>
<button class="px-3 py-1 bg-primary-600 text-white rounded-md">1</button>
<button class="px-3 py-1 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 rounded-md">2</button>
<button class="px-3 py-1 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 rounded-md">ถัดไป</button>
</div>
</div>
</div>
<!-- Right Column (Charts / Mini Stats) -->
<div class="space-y-6">
<!-- Chart Card -->
<div class="glass-card p-5 h-64 flex flex-col">
<h2 class="text-lg font-semibold dark:text-white mb-4">สถิติการใช้งานรายสัปดาห์</h2>
<div class="flex-1 flex items-end justify-between space-x-2 pb-2">
<!-- Fake Bar Chart with Animation -->
<div class="w-1/6 bg-primary-200 dark:bg-primary-900/40 rounded-t-md h-[40%] hover:bg-primary-400 transition-all cursor-pointer relative group">
<div class="absolute -top-8 left-1/2 -translate-x-1/2 bg-slate-800 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity">12</div>
</div>
<div class="w-1/6 bg-primary-300 dark:bg-primary-800/50 rounded-t-md h-[60%] hover:bg-primary-400 transition-all cursor-pointer relative group"></div>
<div class="w-1/6 bg-primary-500 dark:bg-primary-600 rounded-t-md h-[90%] hover:bg-primary-400 transition-all cursor-pointer shadow-lg shadow-primary-500/20 relative group"></div>
<div class="w-1/6 bg-primary-300 dark:bg-primary-800/50 rounded-t-md h-[55%] hover:bg-primary-400 transition-all cursor-pointer relative group"></div>
<div class="w-1/6 bg-primary-200 dark:bg-primary-900/40 rounded-t-md h-[30%] hover:bg-primary-400 transition-all cursor-pointer relative group"></div>
<div class="w-1/6 bg-primary-200 dark:bg-primary-900/40 rounded-t-md h-[20%] hover:bg-primary-400 transition-all cursor-pointer relative group"></div>
<div class="w-1/6 bg-primary-200 dark:bg-primary-900/40 rounded-t-md h-[10%] hover:bg-primary-400 transition-all cursor-pointer relative group"></div>
</div>
<div class="flex justify-between text-xs text-slate-400 mt-2">
<span>จ.</span><span>อ.</span><span>พ.</span><span>พฤ.</span><span>ศ.</span><span>ส.</span><span>อา.</span>
</div>
</div>
<!-- Mini Card -->
<div class="glass-card p-5">
<h2 class="text-lg font-semibold dark:text-white mb-4">แจ้งเตือนล่าสุด</h2>
<ul class="space-y-4">
<li class="flex items-start">
<div class="w-8 h-8 rounded-full bg-danger/10 text-danger flex items-center justify-center mt-0.5">
<i class="fa-solid fa-triangle-exclamation text-xs"></i>
</div>
<div class="ml-3">
<p class="text-sm font-medium dark:text-slate-200">รถตู้ กค 1234 ใกล้ถึงกำหนดต่อภาษี</p>
<p class="text-xs text-slate-500 mt-0.5">2 ชั่วโมงที่แล้ว</p>
</div>
</li>
<li class="flex items-start">
<div class="w-8 h-8 rounded-full bg-info/10 text-info flex items-center justify-center mt-0.5">
<i class="fa-solid fa-oil-can text-xs"></i>
</div>
<div class="ml-3">
<p class="text-sm font-medium dark:text-slate-200">รถกระบะ ขข 5678 ถึงกำหนดถ่ายน้ำมันเครื่อง</p>
<p class="text-xs text-slate-500 mt-0.5">เมื่อวานนี้</p>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Additional Custom Styles for Scrollbar inside index.html for quick fix -->
<style>
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 10px;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background: #475569;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>
<?php require_once 'includes/footer.php'; ?>
@@ -0,0 +1,190 @@
<?php
session_start();
// ถ้าล็อกอินอยู่แล้ว ให้ไปหน้า dashboard ทันที
if (isset($_SESSION['user_id'])) {
header("Location: index.php");
exit();
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_once 'includes/db.php';
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$error = 'กรุณากรอกข้อมูลให้ครบถ้วน';
} else {
$stmt = $pdo->prepare("SELECT * FROM users WHERE id_card = ? OR username = ?");
$stmt->execute([$username, $username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
// รหัสผ่านถูกต้อง สร้าง Session
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['full_name'] = $user['full_name'];
$_SESSION['role'] = $user['role'];
require_once 'includes/logger.php';
logActivity('Login', 'users', $user['id']);
// ส่ง JSON กลับไปให้ Alpine.js ทำงานต่อ
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit();
} else {
$error = 'เลขบัตรประชาชน/ชื่อผู้ใช้งาน หรือรหัสผ่านไม่ถูกต้อง';
}
}
// ส่ง Error กลับไป
if (!empty($error)) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => $error]);
exit();
}
}
$pageTitle = 'เข้าสู่ระบบ | ระบบจัดการข้อมูลยานพาหนะ โรงพยาบาลเกาะสมุย';
$bodyClass = 'bg-slate-50 dark:bg-slate-900 min-h-screen relative overflow-hidden flex flex-col items-center justify-center';
$bodyAttributes = '';
require_once 'includes/header.php';
?>
<!-- Animated Soft Background -->
<div class="absolute inset-0 z-0 overflow-hidden pointer-events-none">
<!-- Animated Blobs -->
<div class="absolute top-[-10%] left-[-10%] w-[50vw] h-[50vw] rounded-full bg-primary-200/40 dark:bg-primary-900/30 blur-[100px] animate-blob mix-blend-multiply dark:mix-blend-screen"></div>
<div class="absolute top-[20%] right-[-10%] w-[40vw] h-[40vw] rounded-full bg-teal-200/40 dark:bg-teal-900/20 blur-[100px] animate-blob mix-blend-multiply dark:mix-blend-screen" style="animation-delay: 2s;"></div>
<div class="absolute bottom-[-20%] left-[20%] w-[60vw] h-[60vw] rounded-full bg-blue-300/30 dark:bg-blue-900/20 blur-[120px] animate-blob mix-blend-multiply dark:mix-blend-screen" style="animation-delay: 4s;"></div>
</div>
<!-- Main Content -->
<div class="relative z-10 w-full flex flex-col items-center justify-center min-h-screen p-4 sm:p-6 lg:p-8">
<!-- Logo Header -->
<div class="mb-8 text-center page-transition">
<img src="public/images/logo.png" alt="Logo" class="w-[120px] h-[120px] rounded-full object-cover border-4 border-white shadow-xl mx-auto mb-4 hover:scale-105 transition-transform duration-300 bg-white relative z-10">
<h1 class="text-3xl font-bold text-slate-800 dark:text-white tracking-tight drop-shadow-sm">โรงพยาบาลเกาะสมุย</h1>
<p class="text-lg text-slate-600 dark:text-slate-300 font-medium mt-1">ระบบจัดการข้อมูลยานพาหนะ-พนักงานขับรถ</p>
</div>
<!-- Login Card -->
<div class="w-full max-w-md page-transition" style="animation-delay: 0.1s;">
<div class="glass-card p-8 relative overflow-hidden group">
<!-- Inner Glow -->
<div class="absolute inset-0 bg-white/10 dark:bg-black/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"></div>
<h2 class="text-2xl font-semibold mb-6 text-center dark:text-white">เข้าสู่ระบบ / Login</h2>
<!-- Alpine Component สำหรับระบบ Login แบบไม่โหลดหน้าเว็บใหม่ -->
<form class="space-y-6"
x-data="{
isLoading: false,
isSuccess: false,
errorMsg: '',
submitForm() {
this.isLoading = true;
this.errorMsg = '';
let formData = new FormData(this.$refs.form);
fetch('login.php', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
if(data.success) {
this.isSuccess = true;
setTimeout(() => window.location.href='index.php', 1000);
} else {
this.isLoading = false;
this.errorMsg = data.message;
// สั่นกล่องข้อความเมื่อมี Error
this.$refs.errorBox.classList.add('animate-shake');
setTimeout(() => this.$refs.errorBox.classList.remove('animate-shake'), 500);
}
})
.catch(err => {
this.isLoading = false;
this.errorMsg = 'เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์';
});
}
}"
x-ref="form"
@submit.prevent="submitForm">
<!-- Error Message Box -->
<div x-show="errorMsg" x-transition.opacity x-ref="errorBox" class="bg-danger/10 border border-danger/30 text-danger text-sm rounded-lg p-3 text-center mb-4 font-medium" style="display: none;">
<i class="fa-solid fa-triangle-exclamation mr-1"></i> <span x-text="errorMsg"></span>
</div>
<!-- Username -->
<div class="relative z-0 w-full mb-6 group">
<input type="text" name="username" id="username" class="glass-input block py-3 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-primary-500 focus:outline-none focus:ring-0 focus:border-primary-600 peer pl-8" placeholder=" " required />
<label for="username" class="peer-focus:font-medium absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto peer-focus:text-primary-600 peer-focus:dark:text-primary-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6 pl-8">เลขบัตรประชาชน / ชื่อผู้ใช้งาน</label>
<div class="absolute inset-y-0 left-0 flex items-center pl-2 pointer-events-none">
<i class="fa-regular fa-user text-gray-400 peer-focus:text-primary-600 transition-colors"></i>
</div>
</div>
<!-- Password -->
<div class="relative z-0 w-full mb-6 group">
<input type="password" name="password" id="password" class="glass-input block py-3 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-primary-500 focus:outline-none focus:ring-0 focus:border-primary-600 peer pl-8" placeholder=" " required />
<label for="password" class="peer-focus:font-medium absolute text-sm text-gray-500 dark:text-gray-400 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-primary-600 peer-focus:dark:text-primary-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6 pl-8">รหัสผ่าน</label>
<div class="absolute inset-y-0 left-0 flex items-center pl-2 pointer-events-none">
<i class="fa-solid fa-lock text-gray-400 peer-focus:text-primary-600 transition-colors"></i>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="remember" type="checkbox" value="" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800" />
</div>
<label for="remember" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300 cursor-pointer">จดจำฉัน</label>
</div>
<a href="#" class="text-sm text-primary-600 hover:underline dark:text-primary-500">ลืมรหัสผ่าน?</a>
</div>
<button type="submit" :disabled="isLoading || isSuccess" class="btn-gradient w-full py-3 flex items-center justify-center space-x-2 relative overflow-hidden"
:class="{ 'opacity-80 cursor-not-allowed': isLoading || isSuccess }">
<span x-show="!isLoading && !isSuccess">เข้าสู่ระบบ</span>
<span x-show="isLoading" class="flex items-center" style="display: none;">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
กำลังตรวจสอบ...
</span>
<span x-show="isSuccess" class="flex items-center text-white" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 scale-50" x-transition:enter-end="opacity-100 scale-100" style="display: none;">
<i class="fa-solid fa-check-circle mr-2"></i> เข้าสู่ระบบสำเร็จ
</span>
</button>
</form>
</div>
<div class="mt-6 text-center text-sm text-slate-300">
<p>&copy; 2026 Koh Samui Hospital. All rights reserved.</p>
<p class="mt-1 opacity-70">Version 1.0.0 (Production)</p>
</div>
</div>
</div>
<!-- Add animation for error shake -->
<style>
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
.animate-shake {
animation: shake 0.5s;
}
</style>
<?php require_once 'includes/footer.php'; ?>
@@ -0,0 +1,10 @@
<?php
session_start();
require_once 'includes/logger.php';
logActivity('Logout');
session_unset();
session_destroy();
header("Location: login.php");
exit();
?>
@@ -0,0 +1,196 @@
<?php
require_once 'includes/auth.php';
// Check if user is admin
if (!isAdmin()) {
header("Location: index.php");
exit();
}
$pageTitle = 'ประวัติการใช้งาน | ระบบจัดการข้อมูลยานพาหนะ โรงพยาบาลเกาะสมุย';
$bodyClass = 'bg-slate-50 dark:bg-slate-900 overflow-hidden';
require_once 'includes/header.php';
?>
<div class="flex h-screen overflow-hidden" x-data="logManagement()">
<?php require_once 'includes/sidebar.php'; ?>
<!-- Main Content -->
<main class="flex-1 flex flex-col min-w-0 overflow-hidden page-transition">
<?php require_once 'includes/navbar.php'; ?>
<!-- Content Area -->
<div class="flex-1 overflow-auto p-4 sm:p-6 lg:p-8 custom-scrollbar relative">
<!-- Background decoration -->
<div class="absolute top-[0%] right-[10%] w-[30vw] h-[30vw] rounded-full bg-blue-300/10 dark:bg-blue-900/10 blur-[100px] pointer-events-none"></div>
<div class="max-w-7xl mx-auto relative z-10">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8">
<div>
<h1 class="text-2xl font-bold text-slate-800 dark:text-white flex items-center gap-2 drop-shadow-sm">
<i class="fa-solid fa-list-check text-primary-600 dark:text-primary-400"></i>
ประวัติการใช้งานระบบ (Activity Logs)
</h1>
<p class="text-slate-500 dark:text-slate-400 mt-1">แสดงประวัติการใช้งานระบบ และการกระทำต่างๆ ของผู้ใช้</p>
</div>
</div>
<!-- Search and Filter -->
<div class="glass-card p-4 mb-6 rounded-2xl flex flex-col sm:flex-row gap-4">
<div class="relative flex-1">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fa-solid fa-search text-slate-400"></i>
</div>
<input type="text" x-model="searchQuery" class="glass-input block w-full pl-10 py-2.5 rounded-xl text-sm" placeholder="ค้นหาจากชื่อผู้ใช้, การกระทำ, หรือ IP Address...">
</div>
<div class="w-full sm:w-48">
<input type="date" x-model="dateFilter" class="glass-input block w-full py-2.5 px-3 rounded-xl text-sm text-slate-700 dark:text-slate-300">
</div>
</div>
<!-- Table -->
<div class="glass-card rounded-2xl overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700/50">
<thead class="bg-slate-50/50 dark:bg-slate-800/50">
<tr>
<th scope="col" class="px-6 py-4 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider">วัน/เวลา</th>
<th scope="col" class="px-6 py-4 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider">ผู้ใช้งาน</th>
<th scope="col" class="px-6 py-4 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider">การกระทำ</th>
<th scope="col" class="px-6 py-4 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider">ข้อมูลอ้างอิง</th>
<th scope="col" class="px-6 py-4 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider">IP Address</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700/50 bg-white/30 dark:bg-slate-800/30">
<template x-if="isLoading">
<tr>
<td colspan="5" class="px-6 py-8 text-center text-slate-500">
<i class="fa-solid fa-spinner fa-spin mr-2"></i> กำลังโหลดข้อมูล...
</td>
</tr>
</template>
<template x-if="!isLoading && filteredLogs.length === 0">
<tr>
<td colspan="5" class="px-6 py-8 text-center text-slate-500">
<div class="flex flex-col items-center justify-center">
<i class="fa-solid fa-inbox text-4xl mb-3 text-slate-300 dark:text-slate-600"></i>
<p>ไม่พบข้อมูลประวัติการใช้งาน</p>
</div>
</td>
</tr>
</template>
<template x-for="log in filteredLogs" :key="log.id">
<tr class="hover:bg-primary-50/50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400" x-text="formatDate(log.created_at)"></td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-8 w-8 rounded-full bg-primary-100 dark:bg-primary-900/50 flex items-center justify-center text-primary-600 dark:text-primary-400">
<i class="fa-solid fa-user text-xs"></i>
</div>
<div class="ml-3">
<div class="text-sm font-medium text-slate-900 dark:text-white" x-text="log.user_name || 'System / Unknown'"></div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2.5 py-1 inline-flex text-xs leading-5 font-semibold rounded-full"
:class="getActionBadgeClass(log.action)"
x-text="log.action">
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
<span x-show="log.table_name" class="font-mono text-xs bg-slate-100 dark:bg-slate-800 px-2 py-0.5 rounded" x-text="log.table_name"></span>
<span x-show="log.record_id" class="ml-1 text-xs" x-text="'#' + log.record_id"></span>
<span x-show="!log.table_name">-</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400 font-mono" x-text="log.ip_address || '-'"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
function logManagement() {
return {
isExpanded: true,
toggleSidebar() { this.isExpanded = !this.isExpanded; },
logs: [],
searchQuery: '',
dateFilter: '',
isLoading: false,
init() {
this.fetchLogs();
},
fetchLogs() {
this.isLoading = true;
fetch('api/logs.php')
.then(res => res.json())
.then(data => {
if (data.logs) {
this.logs = data.logs;
}
this.isLoading = false;
})
.catch(err => {
console.error(err);
this.isLoading = false;
});
},
get filteredLogs() {
let result = this.logs;
if (this.dateFilter) {
result = result.filter(log => {
if(!log.created_at) return false;
return log.created_at.startsWith(this.dateFilter);
});
}
if (this.searchQuery !== '') {
const q = this.searchQuery.toLowerCase();
result = result.filter(log =>
(log.user_name && log.user_name.toLowerCase().includes(q)) ||
(log.action && log.action.toLowerCase().includes(q)) ||
(log.ip_address && log.ip_address.includes(q))
);
}
return result;
},
formatDate(dateStr) {
if(!dateStr) return '-';
const d = new Date(dateStr);
return d.toLocaleString('th-TH', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute:'2-digit'
});
},
getActionBadgeClass(action) {
const act = action.toLowerCase();
if (act.includes('login')) return 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400';
if (act.includes('logout')) return 'bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-300';
if (act.includes('create')) return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400';
if (act.includes('delete')) return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
if (act.includes('update')) return 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400';
return 'bg-primary-100 text-primary-800 dark:bg-primary-900/30 dark:text-primary-400';
}
}
}
</script>
<?php require_once 'includes/footer.php'; ?>
@@ -0,0 +1,218 @@
<?php
require_once 'includes/auth.php';
$pageTitle = 'คู่มือการใช้งาน | ระบบจัดการข้อมูลยานพาหนะ โรงพยาบาลเกาะสมุย';
$bodyClass = 'bg-slate-50 dark:bg-slate-900 overflow-hidden';
require_once 'includes/header.php';
?>
<div class="flex h-screen overflow-hidden" x-data="sidebarStore">
<?php require_once 'includes/sidebar.php'; ?>
<!-- Main Content -->
<main class="flex-1 flex flex-col min-w-0 overflow-hidden page-transition">
<?php require_once 'includes/navbar.php'; ?>
<!-- Content Area -->
<div class="flex-1 overflow-auto p-4 sm:p-6 lg:p-8 custom-scrollbar relative" x-data="{ activeTab: 'overview' }">
<!-- Background decoration -->
<div class="absolute top-[0%] left-[20%] w-[40vw] h-[40vw] rounded-full bg-blue-300/10 dark:bg-blue-900/10 blur-[100px] pointer-events-none"></div>
<div class="max-w-6xl mx-auto relative z-10">
<div class="text-center mb-10">
<h1 class="text-3xl md:text-4xl font-bold text-slate-800 dark:text-white drop-shadow-sm mb-4">
คู่มือการใช้งานระบบ
</h1>
<p class="text-slate-600 dark:text-slate-400 text-lg max-w-2xl mx-auto">ระบบจัดการข้อมูลยานพาหนะ-พนักงานขับรถ โรงพยาบาลเกาะสมุย ถูกออกแบบมาเพื่อให้ใช้งานง่ายและมีประสิทธิภาพสูงสุด</p>
</div>
<div class="flex flex-col lg:flex-row gap-8">
<!-- Sidebar Navigation -->
<div class="lg:w-1/4">
<div class="glass-card p-2 sticky top-4">
<nav class="space-y-1">
<button @click="activeTab = 'overview'" :class="activeTab === 'overview' ? 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800/50'" class="w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-colors">
<i class="fa-solid fa-chart-pie w-6 text-lg"></i>
ภาพรวมระบบ
</button>
<button @click="activeTab = 'vehicles'" :class="activeTab === 'vehicles' ? 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800/50'" class="w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-colors">
<i class="fa-solid fa-car w-6 text-lg"></i>
การจัดการยานพาหนะ
</button>
<button @click="activeTab = 'drivers'" :class="activeTab === 'drivers' ? 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800/50'" class="w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-colors">
<i class="fa-solid fa-id-card w-6 text-lg"></i>
ข้อมูลพนักงานขับรถ
</button>
<button @click="activeTab = 'schedules'" :class="activeTab === 'schedules' ? 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800/50'" class="w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-colors">
<i class="fa-regular fa-calendar-check w-6 text-lg"></i>
ตารางการเดินรถ
</button>
<?php if(isAdmin()): ?>
<button @click="activeTab = 'settings'" :class="activeTab === 'settings' ? 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800/50'" class="w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-colors">
<i class="fa-solid fa-users-gear w-6 text-lg"></i>
การตั้งค่าผู้ใช้งาน
</button>
<?php endif; ?>
</nav>
</div>
</div>
<!-- Content Area -->
<div class="lg:w-3/4">
<!-- Overview Tab -->
<div x-show="activeTab === 'overview'" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4" x-transition:enter-end="opacity-100 translate-y-0" class="glass-card p-6 md:p-8">
<h2 class="text-2xl font-bold text-slate-800 dark:text-white mb-6 flex items-center gap-3">
<div class="bg-primary-100 dark:bg-primary-900/50 text-primary-600 dark:text-primary-400 p-2 rounded-lg">
<i class="fa-solid fa-chart-pie"></i>
</div>
ภาพรวมระบบ (Dashboard)
</h2>
<div class="prose prose-slate dark:prose-invert max-w-none">
<p>หน้า Dashboard เป็นหน้าแรกหลังจากที่คุณเข้าสู่ระบบ หน้านี้จะสรุปข้อมูลสำคัญทั้งหมดเพื่อให้คุณเห็นภาพรวมของการทำงานได้อย่างรวดเร็ว</p>
<h3 class="text-xl font-semibold mt-6 mb-3 text-slate-700 dark:text-slate-200">ส่วนประกอบหลัก</h3>
<ul class="space-y-3 list-none pl-0">
<li class="flex items-start gap-3">
<i class="fa-solid fa-check-circle text-primary-500 mt-1"></i>
<div>
<strong>การ์ดสรุปข้อมูล (Summary Cards):</strong> แสดงจำนวนรวมของยานพาหนะ, พนักงานขับรถ, และตารางงานที่กำลังดำเนินการ
</div>
</li>
<li class="flex items-start gap-3">
<i class="fa-solid fa-check-circle text-primary-500 mt-1"></i>
<div>
<strong>สถานะยานพาหนะแบบเรียลไทม์:</strong> กราฟแสดงสัดส่วนรถที่ว่าง, กำลังใช้งาน, และกำลังซ่อมบำรุง
</div>
</li>
<li class="flex items-start gap-3">
<i class="fa-solid fa-check-circle text-primary-500 mt-1"></i>
<div>
<strong>สถานะพนักงานขับรถ:</strong> สรุปจำนวนพนักงานที่พร้อมปฏิบัติงาน
</div>
</li>
</ul>
</div>
</div>
<!-- Vehicles Tab -->
<div x-show="activeTab === 'vehicles'" style="display: none;" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4" x-transition:enter-end="opacity-100 translate-y-0" class="glass-card p-6 md:p-8">
<h2 class="text-2xl font-bold text-slate-800 dark:text-white mb-6 flex items-center gap-3">
<div class="bg-primary-100 dark:bg-primary-900/50 text-primary-600 dark:text-primary-400 p-2 rounded-lg">
<i class="fa-solid fa-car"></i>
</div>
การจัดการยานพาหนะ
</h2>
<div class="prose prose-slate dark:prose-invert max-w-none">
<p>เมนูนี้ใช้สำหรับจัดการข้อมูลรถยนต์ทั้งหมดของโรงพยาบาล รวมถึงการบันทึกประวัติการซ่อมบำรุงและเอกสารต่างๆ</p>
<div class="bg-slate-50 dark:bg-slate-800/50 p-4 rounded-xl mt-6 border border-slate-200 dark:border-slate-700">
<h4 class="font-semibold text-slate-800 dark:text-white mb-2"><i class="fa-solid fa-plus text-primary-500 mr-2"></i>การเพิ่มยานพาหนะ</h4>
<p class="text-sm">คลิกปุ่ม "เพิ่มยานพาหนะ" ที่มุมบนขวา กรอกข้อมูลป้ายทะเบียน, ยี่ห้อ, รุ่น, ประเภท และวันต่อภาษี สามารถแนบเอกสาร (PDF) ประจำรถได้ในหน้าแก้ไข</p>
</div>
<div class="bg-slate-50 dark:bg-slate-800/50 p-4 rounded-xl mt-4 border border-slate-200 dark:border-slate-700">
<h4 class="font-semibold text-slate-800 dark:text-white mb-2"><i class="fa-solid fa-wrench text-primary-500 mr-2"></i>การซ่อมบำรุง (Maintenance)</h4>
<p class="text-sm">ในหน้ารายละเอียดยานพาหนะ คุณสามารถเพิ่ม "บันทึกการซ่อมบำรุง" ได้ โดยระบุวันที่, รายละเอียดการซ่อม, เลขไมล์, ค่าใช้จ่าย และอู่ซ่อม พร้อมแนบใบเสร็จได้</p>
</div>
</div>
</div>
<!-- Drivers Tab -->
<div x-show="activeTab === 'drivers'" style="display: none;" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4" x-transition:enter-end="opacity-100 translate-y-0" class="glass-card p-6 md:p-8">
<h2 class="text-2xl font-bold text-slate-800 dark:text-white mb-6 flex items-center gap-3">
<div class="bg-primary-100 dark:bg-primary-900/50 text-primary-600 dark:text-primary-400 p-2 rounded-lg">
<i class="fa-solid fa-id-card"></i>
</div>
ข้อมูลพนักงานขับรถ
</h2>
<div class="prose prose-slate dark:prose-invert max-w-none">
<p>จัดการประวัติของพนักงานขับรถ การเชื่อมโยงกับระบบ HIS และวันหมดอายุใบขับขี่</p>
<ul class="space-y-4 mt-6">
<li class="p-4 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-sm">
<h4 class="font-bold text-primary-600 dark:text-primary-400 mb-1">การเพิ่มพนักงานใหม่</h4>
<p class="text-sm">สามารถทำได้ 2 วิธี:</p>
<ol class="text-sm mt-2 ml-4 list-decimal">
<li><strong>ค้นหาจาก HIS:</strong> พิมพ์ชื่อ ระบบจะดึงข้อมูล ตำแหน่ง, หน่วยงาน มาให้อัตโนมัติ</li>
<li><strong>กรอกเอง:</strong> หากไม่มีในระบบ สามารถพิมพ์ชื่อ-สกุล และเบอร์โทรศัพท์ได้โดยตรง</li>
</ol>
</li>
<li class="p-4 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-sm">
<h4 class="font-bold text-primary-600 dark:text-primary-400 mb-1">การตั้งสถานะ</h4>
<p class="text-sm">ผู้ดูแลระบบสามารถปรับสถานะ (ว่าง, ติดภารกิจ, ลางาน) ได้จาก Dropdown ที่มุมขวาของการ์ดพนักงานแต่ละคน</p>
</li>
</ul>
</div>
</div>
<!-- Schedules Tab -->
<div x-show="activeTab === 'schedules'" style="display: none;" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4" x-transition:enter-end="opacity-100 translate-y-0" class="glass-card p-6 md:p-8">
<h2 class="text-2xl font-bold text-slate-800 dark:text-white mb-6 flex items-center gap-3">
<div class="bg-primary-100 dark:bg-primary-900/50 text-primary-600 dark:text-primary-400 p-2 rounded-lg">
<i class="fa-regular fa-calendar-check"></i>
</div>
ตารางการเดินรถ
</h2>
<div class="prose prose-slate dark:prose-invert max-w-none">
<p>ดูและจัดการคิวการเดินรถทั้งหมดในรูปแบบปฏิทิน และรายการ (List View)</p>
<div class="mt-6 flex flex-col md:flex-row gap-4">
<div class="flex-1 bg-slate-50 dark:bg-slate-800/50 p-4 rounded-xl border border-slate-200 dark:border-slate-700">
<div class="text-center mb-3 text-primary-500 text-3xl"><i class="fa-solid fa-calendar-days"></i></div>
<h4 class="font-bold text-center mb-2 text-slate-800 dark:text-white">มุมมองปฏิทิน (Calendar)</h4>
<p class="text-sm text-center text-slate-600 dark:text-slate-400">ดูคิวงานแบบรายเดือน รายสัปดาห์ หรือรายวัน สามารถคลิกที่กิจกรรมเพื่อดูรายละเอียดเพิ่มเติม</p>
</div>
<div class="flex-1 bg-slate-50 dark:bg-slate-800/50 p-4 rounded-xl border border-slate-200 dark:border-slate-700">
<div class="text-center mb-3 text-teal-500 text-3xl"><i class="fa-solid fa-list-ul"></i></div>
<h4 class="font-bold text-center mb-2 text-slate-800 dark:text-white">มุมมองรายการ (List)</h4>
<p class="text-sm text-center text-slate-600 dark:text-slate-400">ค้นหาและกรองตารางงานได้ง่ายขึ้น เรียงลำดับตามเวลา หรือค้นหาจากชื่อสถานที่</p>
</div>
</div>
<h3 class="text-lg font-bold mt-6 mb-2">การเพิ่มตารางงานใหม่</h3>
<p class="text-sm">คลิก "เพิ่มตารางเดินรถ" เลือกรถยนต์, เลือกพนักงานขับรถ, ระบุสถานที่, จุดประสงค์ และเวลาไป-กลับ จากนั้นกดบันทึก</p>
</div>
</div>
<?php if(isAdmin()): ?>
<!-- Settings Tab -->
<div x-show="activeTab === 'settings'" style="display: none;" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4" x-transition:enter-end="opacity-100 translate-y-0" class="glass-card p-6 md:p-8 border-t-4 border-t-primary-500">
<h2 class="text-2xl font-bold text-slate-800 dark:text-white mb-6 flex items-center gap-3">
<div class="bg-primary-100 dark:bg-primary-900/50 text-primary-600 dark:text-primary-400 p-2 rounded-lg">
<i class="fa-solid fa-users-gear"></i>
</div>
การตั้งค่าผู้ใช้งาน
<span class="text-xs bg-primary-100 text-primary-700 px-2 py-1 rounded-md ml-2 font-normal">เฉพาะผู้ดูแลระบบ</span>
</h2>
<div class="prose prose-slate dark:prose-invert max-w-none">
<p>จัดการสิทธิ์การเข้าใช้งานระบบสำหรับเจ้าหน้าที่โรงพยาบาล</p>
<div class="bg-warning/10 border-l-4 border-warning p-4 my-4 rounded-r-xl">
<p class="text-sm text-warning-700 dark:text-warning-400 m-0"><strong>ข้อควรระวัง:</strong> รหัสผ่านจะต้องมีความยาวอย่างน้อย 8 ตัวอักษร, ประกอบด้วยตัวพิมพ์ใหญ่, ตัวพิมพ์เล็ก, ตัวเลข และอักขระพิเศษ เพื่อความปลอดภัยสูงสุด</p>
</div>
<h3 class="text-lg font-bold mt-6 mb-2">ระดับสิทธิ์ (Roles)</h3>
<ul class="space-y-2">
<li class="flex items-start gap-2">
<span class="bg-primary-100 text-primary-800 px-2 py-0.5 rounded text-xs font-bold mt-1 min-w-[100px] text-center">ผู้ดูแลระบบ</span>
<span class="text-sm mt-0.5">สามารถเข้าถึงทุกเมนู รวมถึงการตั้งค่าผู้ใช้งาน</span>
</li>
<li class="flex items-start gap-2">
<span class="bg-slate-100 text-slate-800 dark:bg-slate-700 dark:text-slate-300 px-2 py-0.5 rounded text-xs font-bold mt-1 min-w-[100px] text-center">ผู้ใช้ทั่วไป</span>
<span class="text-sm mt-0.5">สามารถจัดการรถ, พนักงาน และตารางเดินรถได้ แต่ไม่สามารถเพิ่มหรือลบผู้ใช้งานระบบได้</span>
</li>
</ul>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</main>
</div>
<?php require_once 'includes/footer.php'; ?>
@@ -0,0 +1 @@
../esbuild/bin/esbuild
@@ -0,0 +1 @@
../jiti/lib/jiti-cli.mjs
@@ -0,0 +1 @@
../mini-svg-data-uri/cli.js
@@ -0,0 +1 @@
../nanoid/bin/nanoid.cjs
@@ -0,0 +1 @@
../resolve/bin/resolve
@@ -0,0 +1 @@
../rollup/dist/bin/rollup
@@ -0,0 +1 @@
../vite/bin/vite.js
@@ -0,0 +1,741 @@
{
"name": "vehicle-driver-management",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
"integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
"engines": {
"node": ">=6"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
"integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
"integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.63.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz",
"integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@tailwindcss/node": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
"integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"enhanced-resolve": "^5.24.1",
"jiti": "^2.7.0",
"lightningcss": "1.32.0",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.3.3"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
"integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.3.3",
"@tailwindcss/oxide-darwin-arm64": "4.3.3",
"@tailwindcss/oxide-darwin-x64": "4.3.3",
"@tailwindcss/oxide-freebsd-x64": "4.3.3",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3",
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.3",
"@tailwindcss/oxide-linux-arm64-musl": "4.3.3",
"@tailwindcss/oxide-linux-x64-gnu": "4.3.3",
"@tailwindcss/oxide-linux-x64-musl": "4.3.3",
"@tailwindcss/oxide-wasm32-wasi": "4.3.3",
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.3",
"@tailwindcss/oxide-win32-x64-msvc": "4.3.3"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz",
"integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz",
"integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.3.3",
"@tailwindcss/oxide": "4.3.3",
"tailwindcss": "4.3.3"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"license": "MIT"
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT"
},
"node_modules/@vue/reactivity": {
"version": "3.5.42",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz",
"integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.42"
}
},
"node_modules/@vue/shared": {
"version": "3.5.42",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz",
"integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==",
"license": "MIT"
},
"node_modules/alpinejs": {
"version": "3.17.2",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.17.2.tgz",
"integrity": "sha512-xbnG3LlnmFkiNO49C+qhqjEDqOdB0snKqvT48ZVp8TakpZOpsoQbc/jAxpptQRjZi7c9rLeCfvwQ/XOSirIZFQ==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "~3.5.40"
}
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/enhanced-resolve": {
"version": "5.24.5",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz",
"integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.3.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/flowbite": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/flowbite/-/flowbite-2.5.2.tgz",
"integrity": "sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA==",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.9.3",
"flowbite-datepicker": "^1.3.0",
"mini-svg-data-uri": "^1.4.3"
}
},
"node_modules/flowbite-datepicker": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/flowbite-datepicker/-/flowbite-datepicker-1.3.2.tgz",
"integrity": "sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g==",
"license": "MIT",
"dependencies": {
"@rollup/plugin-node-resolve": "^15.2.3",
"flowbite": "^2.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-core-module": {
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"license": "MIT"
},
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
"license": "MIT",
"bin": {
"mini-svg-data-uri": "cli.js"
}
},
"node_modules/nanoid": {
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
"integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.18",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/resolve": {
"version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": {
"version": "4.63.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz",
"integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.9"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@napi-rs/lzma-linux-x64-gnu": "1.5.1",
"@rollup/rollup-android-arm-eabi": "4.63.1",
"@rollup/rollup-android-arm64": "4.63.1",
"@rollup/rollup-darwin-arm64": "4.63.1",
"@rollup/rollup-darwin-x64": "4.63.1",
"@rollup/rollup-freebsd-arm64": "4.63.1",
"@rollup/rollup-freebsd-x64": "4.63.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.63.1",
"@rollup/rollup-linux-arm-musleabihf": "4.63.1",
"@rollup/rollup-linux-arm64-gnu": "4.63.1",
"@rollup/rollup-linux-arm64-musl": "4.63.1",
"@rollup/rollup-linux-loong64-gnu": "4.63.1",
"@rollup/rollup-linux-loong64-musl": "4.63.1",
"@rollup/rollup-linux-ppc64-gnu": "4.63.1",
"@rollup/rollup-linux-ppc64-musl": "4.63.1",
"@rollup/rollup-linux-riscv64-gnu": "4.63.1",
"@rollup/rollup-linux-riscv64-musl": "4.63.1",
"@rollup/rollup-linux-s390x-gnu": "4.63.1",
"@rollup/rollup-linux-x64-gnu": "4.63.1",
"@rollup/rollup-linux-x64-musl": "4.63.1",
"@rollup/rollup-openbsd-x64": "4.63.1",
"@rollup/rollup-openharmony-arm64": "4.63.1",
"@rollup/rollup-win32-arm64-msvc": "4.63.1",
"@rollup/rollup-win32-ia32-msvc": "4.63.1",
"@rollup/rollup-win32-x64-gnu": "4.63.1",
"@rollup/rollup-win32-x64-msvc": "4.63.1",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tailwindcss": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
"integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
"dev": true,
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}
@@ -0,0 +1,3 @@
# esbuild
This is the macOS ARM 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
@@ -0,0 +1,20 @@
{
"name": "@esbuild/darwin-arm64",
"version": "0.21.5",
"description": "The macOS ARM 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
"url": "git+https://github.com/evanw/esbuild.git"
},
"license": "MIT",
"preferUnplugged": true,
"engines": {
"node": ">=12"
},
"os": [
"darwin"
],
"cpu": [
"arm64"
]
}
@@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2024 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**
@@ -0,0 +1,38 @@
# @fortawesome/fontawesome-free - The Official Font Awesome 6 NPM package
> "I came here to chew bubblegum and install Font Awesome 6 - and I'm all out of bubblegum"
[![npm](https://img.shields.io/npm/v/@fortawesome/fontawesome-free.svg?style=flat-square)](https://www.npmjs.com/package/@fortawesome/fontawesome-free)
## Installation
```
$ npm i --save @fortawesome/fontawesome-free
```
Or
```
$ yarn add @fortawesome/fontawesome-free
```
## What's included?
**This package includes all the same files available through our Free and Pro CDN.**
* /js - All JavaScript files associated with Font Awesome 6 SVG with JS
* /css - All CSS using the classic Web Fonts with CSS implementation
* /sprites - SVG icons packaged in a convenient sprite
* /scss, /less - CSS Pre-processor files for Web Fonts with CSS
* /webfonts - Accompanying files for Web Fonts with CSS
* /svg - Individual icon files in SVG format
## Documentation
Get started [here](https://docs.fontawesome.com/web/setup/get-started). Continue your journey [here](https://docs.fontawesome.com/web/setup/packages).
Or go straight to the [API documentation](https://docs.fontawesome.com/apis/javascript/get-started).
## Issues and support
Start with [GitHub issues](https://github.com/FortAwesome/Font-Awesome/issues) and ping us on [Twitter](https://twitter.com/fontawesome) if you need to.
@@ -0,0 +1,19 @@
/*!
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
.far,
.fa-regular {
font-weight: 400; }
@@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}
@@ -0,0 +1,19 @@
/*!
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 900;
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
.fas,
.fa-solid {
font-weight: 900; }
@@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}
@@ -0,0 +1,461 @@
/*!
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
:root, :host {
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free';
--fa-font-light: normal 300 1em/1 'Font Awesome 6 Pro';
--fa-font-thin: normal 100 1em/1 'Font Awesome 6 Pro';
--fa-font-duotone: normal 900 1em/1 'Font Awesome 6 Duotone';
--fa-font-duotone-regular: normal 400 1em/1 'Font Awesome 6 Duotone';
--fa-font-duotone-light: normal 300 1em/1 'Font Awesome 6 Duotone';
--fa-font-duotone-thin: normal 100 1em/1 'Font Awesome 6 Duotone';
--fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands';
--fa-font-sharp-solid: normal 900 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-regular: normal 400 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-light: normal 300 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-thin: normal 100 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-duotone-solid: normal 900 1em/1 'Font Awesome 6 Sharp Duotone';
--fa-font-sharp-duotone-regular: normal 400 1em/1 'Font Awesome 6 Sharp Duotone';
--fa-font-sharp-duotone-light: normal 300 1em/1 'Font Awesome 6 Sharp Duotone';
--fa-font-sharp-duotone-thin: normal 100 1em/1 'Font Awesome 6 Sharp Duotone'; }
svg.svg-inline--fa:not(:root), svg.svg-inline--fa:not(:host) {
overflow: visible;
box-sizing: content-box; }
.svg-inline--fa {
display: var(--fa-display, inline-block);
height: 1em;
overflow: visible;
vertical-align: -.125em; }
.svg-inline--fa.fa-2xs {
vertical-align: 0.1em; }
.svg-inline--fa.fa-xs {
vertical-align: 0em; }
.svg-inline--fa.fa-sm {
vertical-align: -0.07143em; }
.svg-inline--fa.fa-lg {
vertical-align: -0.2em; }
.svg-inline--fa.fa-xl {
vertical-align: -0.25em; }
.svg-inline--fa.fa-2xl {
vertical-align: -0.3125em; }
.svg-inline--fa.fa-pull-left {
margin-right: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-pull-right {
margin-left: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-li {
width: var(--fa-li-width, 2em);
top: 0.25em; }
.svg-inline--fa.fa-fw {
width: var(--fa-fw-width, 1.25em); }
.fa-layers svg.svg-inline--fa {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0; }
.fa-layers-counter, .fa-layers-text {
display: inline-block;
position: absolute;
text-align: center; }
.fa-layers {
display: inline-block;
height: 1em;
position: relative;
text-align: center;
vertical-align: -.125em;
width: 1em; }
.fa-layers svg.svg-inline--fa {
transform-origin: center center; }
.fa-layers-text {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
transform-origin: center center; }
.fa-layers-counter {
background-color: var(--fa-counter-background-color, #ff253a);
border-radius: var(--fa-counter-border-radius, 1em);
box-sizing: border-box;
color: var(--fa-inverse, #fff);
line-height: var(--fa-counter-line-height, 1);
max-width: var(--fa-counter-max-width, 5em);
min-width: var(--fa-counter-min-width, 1.5em);
overflow: hidden;
padding: var(--fa-counter-padding, 0.25em 0.5em);
right: var(--fa-right, 0);
text-overflow: ellipsis;
top: var(--fa-top, 0);
transform: scale(var(--fa-counter-scale, 0.25));
transform-origin: top right; }
.fa-layers-bottom-right {
bottom: var(--fa-bottom, 0);
right: var(--fa-right, 0);
top: auto;
transform: scale(var(--fa-layers-scale, 0.25));
transform-origin: bottom right; }
.fa-layers-bottom-left {
bottom: var(--fa-bottom, 0);
left: var(--fa-left, 0);
right: auto;
top: auto;
transform: scale(var(--fa-layers-scale, 0.25));
transform-origin: bottom left; }
.fa-layers-top-right {
top: var(--fa-top, 0);
right: var(--fa-right, 0);
transform: scale(var(--fa-layers-scale, 0.25));
transform-origin: top right; }
.fa-layers-top-left {
left: var(--fa-left, 0);
right: auto;
top: var(--fa-top, 0);
transform: scale(var(--fa-layers-scale, 0.25));
transform-origin: top left; }
.fa-1x {
font-size: 1em; }
.fa-2x {
font-size: 2em; }
.fa-3x {
font-size: 3em; }
.fa-4x {
font-size: 4em; }
.fa-5x {
font-size: 5em; }
.fa-6x {
font-size: 6em; }
.fa-7x {
font-size: 7em; }
.fa-8x {
font-size: 8em; }
.fa-9x {
font-size: 9em; }
.fa-10x {
font-size: 10em; }
.fa-2xs {
font-size: 0.625em;
line-height: 0.1em;
vertical-align: 0.225em; }
.fa-xs {
font-size: 0.75em;
line-height: 0.08333em;
vertical-align: 0.125em; }
.fa-sm {
font-size: 0.875em;
line-height: 0.07143em;
vertical-align: 0.05357em; }
.fa-lg {
font-size: 1.25em;
line-height: 0.05em;
vertical-align: -0.075em; }
.fa-xl {
font-size: 1.5em;
line-height: 0.04167em;
vertical-align: -0.125em; }
.fa-2xl {
font-size: 2em;
line-height: 0.03125em;
vertical-align: -0.1875em; }
.fa-fw {
text-align: center;
width: 1.25em; }
.fa-ul {
list-style-type: none;
margin-left: var(--fa-li-margin, 2.5em);
padding-left: 0; }
.fa-ul > li {
position: relative; }
.fa-li {
left: calc(-1 * var(--fa-li-width, 2em));
position: absolute;
text-align: center;
width: var(--fa-li-width, 2em);
line-height: inherit; }
.fa-border {
border-color: var(--fa-border-color, #eee);
border-radius: var(--fa-border-radius, 0.1em);
border-style: var(--fa-border-style, solid);
border-width: var(--fa-border-width, 0.08em);
padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); }
.fa-pull-left {
float: left;
margin-right: var(--fa-pull-margin, 0.3em); }
.fa-pull-right {
float: right;
margin-left: var(--fa-pull-margin, 0.3em); }
.fa-beat {
animation-name: fa-beat;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-bounce {
animation-name: fa-bounce;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); }
.fa-fade {
animation-name: fa-fade;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-beat-fade {
animation-name: fa-beat-fade;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-flip {
animation-name: fa-flip;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-shake {
animation-name: fa-shake;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin {
animation-name: fa-spin;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 2s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin-reverse {
--fa-animation-direction: reverse; }
.fa-pulse,
.fa-spin-pulse {
animation-name: fa-spin;
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, steps(8)); }
@media (prefers-reduced-motion: reduce) {
.fa-beat,
.fa-bounce,
.fa-fade,
.fa-beat-fade,
.fa-flip,
.fa-pulse,
.fa-shake,
.fa-spin,
.fa-spin-pulse {
animation-delay: -1ms;
animation-duration: 1ms;
animation-iteration-count: 1;
transition-delay: 0s;
transition-duration: 0s; } }
@keyframes fa-beat {
0%, 90% {
transform: scale(1); }
45% {
transform: scale(var(--fa-beat-scale, 1.25)); } }
@keyframes fa-bounce {
0% {
transform: scale(1, 1) translateY(0); }
10% {
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
transform: scale(1, 1) translateY(0); }
100% {
transform: scale(1, 1) translateY(0); } }
@keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
transform: scale(1); }
50% {
opacity: 1;
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@keyframes fa-flip {
50% {
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@keyframes fa-shake {
0% {
transform: rotate(-15deg); }
4% {
transform: rotate(15deg); }
8%, 24% {
transform: rotate(-18deg); }
12%, 28% {
transform: rotate(18deg); }
16% {
transform: rotate(-22deg); }
20% {
transform: rotate(22deg); }
32% {
transform: rotate(-12deg); }
36% {
transform: rotate(12deg); }
40%, 100% {
transform: rotate(0deg); } }
@keyframes fa-spin {
0% {
transform: rotate(0deg); }
100% {
transform: rotate(360deg); } }
.fa-rotate-90 {
transform: rotate(90deg); }
.fa-rotate-180 {
transform: rotate(180deg); }
.fa-rotate-270 {
transform: rotate(270deg); }
.fa-flip-horizontal {
transform: scale(-1, 1); }
.fa-flip-vertical {
transform: scale(1, -1); }
.fa-flip-both,
.fa-flip-horizontal.fa-flip-vertical {
transform: scale(-1, -1); }
.fa-rotate-by {
transform: rotate(var(--fa-rotate-angle, 0)); }
.fa-stack {
display: inline-block;
vertical-align: middle;
height: 2em;
position: relative;
width: 2.5em; }
.fa-stack-1x,
.fa-stack-2x {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
z-index: var(--fa-stack-z-index, auto); }
.svg-inline--fa.fa-stack-1x {
height: 1em;
width: 1.25em; }
.svg-inline--fa.fa-stack-2x {
height: 2em;
width: 2.5em; }
.fa-inverse {
color: var(--fa-inverse, #fff); }
.sr-only,
.fa-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.sr-only-focusable:not(:focus),
.fa-sr-only-focusable:not(:focus) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.svg-inline--fa .fa-primary {
fill: var(--fa-primary-color, currentColor);
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa .fa-secondary {
fill: var(--fa-secondary-color, currentColor);
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-primary {
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-secondary {
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa mask .fa-primary,
.svg-inline--fa mask .fa-secondary {
fill: black; }
@@ -0,0 +1,26 @@
/*!
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype");
unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype");
unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; }
@@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}
@@ -0,0 +1,22 @@
/*!
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
@font-face {
font-family: 'Font Awesome 5 Brands';
font-display: block;
font-weight: 400;
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
@font-face {
font-family: 'Font Awesome 5 Free';
font-display: block;
font-weight: 900;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
@font-face {
font-family: 'Font Awesome 5 Free';
font-display: block;
font-weight: 400;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
@@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}

Some files were not shown because too many files have changed in this diff Show More