Files
T
2026-09-16 23:20:08 +07:00

315 lines
11 KiB
PHP

<?php
/**
* Thai Traditional Massage Queue Management System (TTMQMS)
* Enterprise Front Controller & REST API Gateway (PSR-4 / MVC / PWA)
*
* @package App
* @version 2.0.0
*/
declare(strict_types=1);
// 0. Enable PHP Error Display for Debugging (Prevent Blank HTTP 500 in Local / XAMPP)
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
// 0.1 PHP 7.x Polyfills for PHP 8.0+ String Functions (ป้องกัน Error บน XAMPP ที่ใช้ PHP 7.4 หรือต่ำกว่า)
if (!function_exists('str_contains')) {
function str_contains($haystack, $needle) {
return (string)$needle !== '' && strpos((string)$haystack, (string)$needle) !== false;
}
}
if (!function_exists('str_starts_with')) {
function str_starts_with($haystack, $needle) {
return (string)$needle !== '' && strncmp((string)$haystack, (string)$needle, strlen((string)$needle)) === 0;
}
}
if (!function_exists('str_ends_with')) {
function str_ends_with($haystack, $needle) {
$needle = (string)$needle;
$haystack = (string)$haystack;
if ($needle === '') return false;
$len = strlen($needle);
return substr($haystack, -$len) === $needle;
}
}
if (!function_exists('getallheaders')) {
function getallheaders() {
$headers = [];
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
} elseif ($name == 'CONTENT_TYPE') {
$headers['Content-Type'] = $value;
} elseif ($name == 'CONTENT_LENGTH') {
$headers['Content-Length'] = $value;
} elseif ($name == 'AUTHORIZATION') {
$headers['Authorization'] = $value;
}
}
if (!isset($headers['Authorization']) && isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
}
return $headers;
}
}
// 1. PSR-4 Autoloader
spl_autoload_register(function ($class) {
$prefix = 'App\\';
$base_dir = __DIR__ . '/../app/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relative_class = substr($class, $len);
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
if (file_exists($file)) {
require $file;
}
});
// 2. Load Global Handler & Configurations
\App\Exceptions\Handler::register();
$securityConfig = require __DIR__ . '/../config/security.php';
$appConfig = require __DIR__ . '/../config/app.php';
// 3. Set Security Headers
foreach ($securityConfig['headers'] as $header => $value) {
header("{$header}: {$value}");
}
// 4. Handle CORS & OPTIONS Preflight
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-CSRF-Token");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
// 5. Simple Custom Router & Subdirectory Base URL Adaptation
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
$baseDir = rtrim(str_replace('/index.php', '', $scriptName), '/'); // e.g. "/ttm/public" or "/ttm"
$appBaseDir = preg_replace('#/public$#', '', $baseDir); // e.g. "/ttm" or ""
if (!defined('BASE_URL')) {
define('BASE_URL', $appBaseDir);
}
$rawUri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$uri = $rawUri;
if (BASE_URL !== '' && strncmp($uri, BASE_URL, strlen(BASE_URL)) === 0) {
$uri = substr($uri, strlen(BASE_URL));
}
if (strncmp($uri, '/public', 7) === 0) {
$uri = substr($uri, 7);
}
if ($uri === '' || $uri === false) {
$uri = '/';
}
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
// Support fallback query parameter API routing (?api=/api/v1/...) for XAMPP servers without Apache mod_rewrite
$queryApi = $_GET['api'] ?? $_GET['endpoint'] ?? null;
if ($queryApi !== null && $queryApi !== '') {
$uri = '/' . ltrim((string)$queryApi, '/');
if (strncmp($uri, '/api/v1/', 8) !== 0 && strncmp($uri, 'api/v1/', 7) !== 0) {
$uri = '/api/v1/' . ltrim($uri, '/');
}
if (($pos = strpos($uri, '?')) !== false) {
$uri = substr($uri, 0, $pos);
}
}
// 5.1 REST API Routes (/api/v1/...)
if (strncmp($uri, '/api/v1/', 8) === 0) {
ini_set('display_errors', '0');
header('Content-Type: application/json; charset=utf-8');
// Auth Routes
if ($uri === '/api/v1/auth/login' && $method === 'POST') {
(new \App\Controllers\AuthController())->login();
exit();
}
if ($uri === '/api/v1/auth/verify-2fa' && $method === 'POST') {
(new \App\Controllers\AuthController())->verifyTwoFactor();
exit();
}
// Queue & Smart Queue AI Routes
if ($uri === '/api/v1/queues' && $method === 'GET') {
(new \App\Controllers\QueueController())->index();
exit();
}
if ($uri === '/api/v1/queues/walkin' && $method === 'POST') {
(new \App\Controllers\QueueController())->walkin();
exit();
}
if ($uri === '/api/v1/queues/smart-assign' && $method === 'POST') {
(new \App\Controllers\QueueController())->smartAssign();
exit();
}
if (preg_match('#^/api/v1/queues/(\d+)/status$#', $uri, $matches) && $method === 'PUT') {
(new \App\Controllers\QueueController())->updateStatus((int)$matches[1]);
exit();
}
// Clinical & SOAP Note Routes
if ($uri === '/api/v1/clinical/soap' && $method === 'POST') {
(new \App\Controllers\ClinicalController())->storeSoap();
exit();
}
if (preg_match('#^/api/v1/clinical/patient/(\d+)$#', $uri, $matches) && $method === 'GET') {
(new \App\Controllers\ClinicalController())->getPatientHistory((int)$matches[1]);
exit();
}
// HIS / HOSxP & HL7 FHIR Integration Routes
if ($uri === '/api/v1/his/sync' && $method === 'POST') {
(new \App\Controllers\HisController())->syncPatient();
exit();
}
if ($uri === '/api/v1/his/smartcard' && $method === 'GET') {
(new \App\Controllers\HisController())->readSmartCard();
exit();
}
if ($uri === '/api/v1/fhir/encounter' && $method === 'POST') {
(new \App\Controllers\HisController())->exportFhir();
exit();
}
// POS Billing & Thermal Printer Routes
if ($uri === '/api/v1/billing/checkout' && $method === 'POST') {
(new \App\Controllers\BillingController())->checkout();
exit();
}
if (preg_match('#^/api/v1/billing/print-ticket/(\d+)$#', $uri, $matches) && $method === 'GET') {
(new \App\Controllers\BillingController())->printTicket((int)$matches[1]);
exit();
}
// Dashboard KPI & Report Routes
if ($uri === '/api/v1/reports/kpi' && $method === 'GET') {
(new \App\Controllers\ReportController())->kpi();
exit();
}
if ($uri === '/api/v1/reports/export' && $method === 'GET') {
(new \App\Controllers\ReportController())->export();
exit();
}
// Services & Rooms Routes
if ($uri === '/api/v1/services' && $method === 'GET') {
(new \App\Controllers\ServiceController())->index();
exit();
}
if ($uri === '/api/v1/rooms' && $method === 'GET') {
(new \App\Controllers\RoomController())->index();
exit();
}
// Therapists Routes
if ($uri === '/api/v1/therapists' && $method === 'GET') {
(new \App\Controllers\TherapistController())->index();
exit();
}
if (preg_match('#^/api/v1/therapists/(\d+)/availability$#', $uri, $matches) && ($method === 'PUT' || $method === 'PATCH')) {
(new \App\Controllers\TherapistController())->toggleAvailability((int)$matches[1]);
exit();
}
// Patients Routes
if ($uri === '/api/v1/patients/search' && $method === 'GET') {
(new \App\Controllers\PatientController())->search();
exit();
}
if ($uri === '/api/v1/patients' && $method === 'POST') {
(new \App\Controllers\PatientController())->store();
exit();
}
// Settings Routes
if ($uri === '/api/v1/settings' && $method === 'GET') {
(new \App\Controllers\SettingController())->index();
exit();
}
if ($uri === '/api/v1/settings' && ($method === 'PUT' || $method === 'POST')) {
(new \App\Controllers\SettingController())->update();
exit();
}
// Default 404 API Not Found
\App\Helpers\Response::error("API Endpoint Not Found: {$method} {$uri}", 404);
}
// 5.2 Frontend GUI & PWA View Router
session_start();
// Support fallback query parameter routing (?page=... or ?route=...) for servers without Apache mod_rewrite
$queryPage = $_GET['page'] ?? $_GET['p'] ?? $_GET['route'] ?? $_GET['url'] ?? null;
if ($queryPage !== null && $queryPage !== '') {
$uri = '/' . ltrim((string)$queryPage, '/');
// Remove query parameters if any were included in uri string
if (($pos = strpos($uri, '?')) !== false) {
$uri = substr($uri, 0, $pos);
}
}
$viewMap = [
'/' => 'dashboard/index.php',
'/index.php' => 'dashboard/index.php',
'/dashboard' => 'dashboard/index.php',
'/login' => 'auth/login.php',
'/verify-2fa' => 'auth/verify_2fa.php',
'/queue' => 'queue/index.php',
'/tv' => 'display/tv.php',
'/clinical' => 'clinical/soap.php',
'/clinical/soap' => 'clinical/soap.php',
'/patient' => 'patient/index.php',
'/room' => 'room/index.php',
'/pos' => 'billing/pos.php',
'/billing' => 'billing/pos.php',
'/reports' => 'report/index.php',
'/report' => 'report/index.php',
'/his' => 'his/index.php',
'/his-sync' => 'his/index.php',
'/settings' => 'admin/settings.php',
'/admin/settings' => 'admin/settings.php',
];
$activeMenuMap = [
'/' => 'dashboard',
'/index.php' => 'dashboard',
'/dashboard' => 'dashboard',
'/queue' => 'queue',
'/tv' => 'tv',
'/clinical' => 'soap',
'/clinical/soap' => 'soap',
'/patient' => 'patient',
'/room' => 'room',
'/pos' => 'billing',
'/billing' => 'billing',
'/reports' => 'reports',
'/report' => 'reports',
'/his' => 'his',
'/his-sync' => 'his',
'/settings' => 'settings',
'/admin/settings' => 'settings',
];
$viewFile = $viewMap[$uri] ?? 'dashboard/index.php'; // Default dashboard or 404
$activeMenu = $activeMenuMap[$uri] ?? 'dashboard';
$fullPath = __DIR__ . '/views/' . $viewFile;
if (file_exists($fullPath)) {
$contentView = $fullPath;
// Include master layout which will include the $contentView view inside
require __DIR__ . '/views/layout/master.php';
} else {
http_response_code(404);
echo "<h1 style='color:red; text-align:center; margin-top:50px;'>404 Not Found - View ({$viewFile}) does not exist for URI: {$uri}</h1>";
}