315 lines
11 KiB
PHP
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>";
|
|
}
|