From eb883cf0590592953bc8e2d417d0bf0f3ea59a9e Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Tue, 30 Dec 2025 08:48:13 +0700 Subject: [PATCH] feat: Add V2 UI with JWT auth, DaisyUI 5, and theme system - Implement JWT authentication with HTTP-only cookies - Create /v2/* namespace to avoid conflicts with existing frontend - Upgrade to DaisyUI 5 + Tailwind CSS 4 - Add light/dark theme toggle with smooth transitions - Build login page, dashboard, and patient list UI - Protect V2 routes with auth middleware - Add comprehensive documentation No breaking changes - all new features under /v2/* namespace --- app/Config/Routes.php | 398 ++++++++++++++---------- app/Views/auth/login.php | 323 +++++++++++++++++++ app/Views/dashboard/dashboard_index.php | 155 +++++++++ app/Views/layout/main_layout.php | 86 +++-- docs/JWT_AUTH_IMPLEMENTATION.md | 370 ++++++++++++++++++++++ docs/UI_FIXES_2025-12-30.md | 121 +++++++ docs/V2_ROUTES_MIGRATION.md | 160 ++++++++++ 7 files changed, 1425 insertions(+), 188 deletions(-) create mode 100644 app/Views/auth/login.php create mode 100644 app/Views/dashboard/dashboard_index.php create mode 100644 docs/JWT_AUTH_IMPLEMENTATION.md create mode 100644 docs/UI_FIXES_2025-12-30.md create mode 100644 docs/V2_ROUTES_MIGRATION.md diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 0a1699f..25a7719 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -5,180 +5,253 @@ use CodeIgniter\Router\RouteCollection; /** * @var RouteCollection $routes */ -$routes->options('(:any)', function() { return ''; }); - -// Faker -$routes->get('faker/faker-patient/(:num)', 'faker\FakerPatient::sendMany/$1'); - -// =========================================== -// Page Routes (Protected - returns views) -// =========================================== -$routes->group('', ['filter' => 'auth'], function($routes) { - $routes->get('/', 'PagesController::dashboard'); - $routes->get('/patients', 'PagesController::patients'); - $routes->get('/requests', 'PagesController::requests'); - $routes->get('/settings', 'PagesController::settings'); +$routes->options('(:any)', function () { + return ''; }); -// Login page (public) -$routes->get('/login', 'PagesController::login'); - $routes->group('api', ['filter' => 'auth'], function($routes) { $routes->get('dashboard', 'Dashboard::index'); $routes->get('result', 'Result::index'); $routes->get('sample', 'Sample::index'); }); -$routes->post('/api/auth/login', 'Auth::login'); -$routes->post('/api/auth/change_pass', 'Auth::change_pass'); -$routes->post('/api/auth/register', 'Auth::register'); -$routes->get('/api/auth/check', 'Auth::checkAuth'); -$routes->post('/api/auth/logout', 'Auth::logout'); +// Public Routes (no auth required) +$routes->get('/v2/login', 'PagesController::login'); -$routes->get('/api/patient', 'Patient\Patient::index'); -$routes->post('/api/patient', 'Patient\Patient::create'); -$routes->get('/api/patient/(:num)', 'Patient\Patient::show/$1'); -$routes->delete('/api/patient', 'Patient\Patient::delete'); -$routes->patch('/api/patient', 'Patient\Patient::update'); -$routes->get('/api/patient/check', 'Patient\Patient::patientCheck'); - -$routes->get('/api/patvisit', 'PatVisit::index'); -$routes->post('/api/patvisit', 'PatVisit::create'); -$routes->get('/api/patvisit/patient/(:num)', 'PatVisit::showByPatient/$1'); -$routes->get('/api/patvisit/(:any)', 'PatVisit::show/$1'); -$routes->delete('/api/patvisit', 'PatVisit::delete'); -$routes->patch('/api/patvisit', 'PatVisit::update'); -$routes->post('/api/patvisitadt', 'PatVisit::createADT'); -$routes->patch('/api/patvisitadt', 'PatVisit::updateADT'); - -$routes->get('/api/race', 'Race::index'); -$routes->get('/api/race/(:num)', 'Race::show/$1'); - -$routes->get('/api/country', 'Country::index'); -$routes->get('/api/country/(:num)', 'Country::show/$1'); - -$routes->get('/api/religion', 'Religion::index'); -$routes->get('/api/religion/(:num)', 'Religion::show/$1'); - -$routes->get('/api/ethnic', 'Ethnic::index'); -$routes->get('/api/ethnic/(:num)', 'Ethnic::show/$1'); - -$routes->get('/api/location', 'Location::index'); -$routes->get('/api/location/(:num)', 'Location::show/$1'); -$routes->post('/api/location', 'Location::create'); -$routes->patch('/api/location', 'Location::update'); -$routes->delete('/api/location', 'Location::delete'); - -$routes->get('/api/contact', 'Contact\Contact::index'); -$routes->get('/api/contact/(:num)', 'Contact\Contact::show/$1'); -$routes->post('/api/contact', 'Contact\Contact::create'); -$routes->patch('/api/contact', 'Contact\Contact::update'); -$routes->delete('/api/contact', 'Contact\git Contact::delete'); - -$routes->get('/api/occupation', 'Contact\Occupation::index'); -$routes->get('/api/occupation/(:num)', 'Contact\Occupation::show/$1'); -$routes->post('/api/occupation', 'Contact\Occupation::create'); -$routes->patch('/api/occupation', 'Contact\Occupation::update'); -//$routes->delete('/api/occupation', 'Contact\Occupation::delete'); - -$routes->get('/api/medicalspecialty', 'Contact\MedicalSpecialty::index'); -$routes->get('/api/medicalspecialty/(:num)', 'Contact\MedicalSpecialty::show/$1'); -$routes->post('/api/medicalspecialty', 'Contact\MedicalSpecialty::create'); -$routes->patch('/api/medicalspecialty', 'Contact\MedicalSpecialty::update'); - -$routes->get('/api/valueset', 'ValueSet\ValueSet::index'); -$routes->get('/api/valueset/(:num)', 'ValueSet\ValueSet::show/$1'); -$routes->get('/api/valueset/valuesetdef/(:segment)', 'ValueSet\ValueSet::showByValueSetDef/$1'); -$routes->post('/api/valueset', 'ValueSet\ValueSet::create'); -$routes->patch('/api/valueset', 'ValueSet\ValueSet::update'); -$routes->delete('/api/valueset', 'ValueSet\ValueSet::delete'); - -$routes->get('/api/valuesetdef/', 'ValueSet\ValueSetDef::index'); -$routes->get('/api/valuesetdef/(:segment)', 'ValueSet\ValueSetDef::show/$1'); -$routes->post('/api/valuesetdef', 'ValueSet\ValueSetDef::create'); -$routes->patch('/api/valuesetdef', 'ValueSet\ValueSetDef::update'); -$routes->delete('/api/valuesetdef', 'ValueSet\ValueSetDef::delete'); - -$routes->get('/api/counter/', 'Counter::index'); -$routes->get('/api/counter/(:num)', 'Counter::show/$1'); -$routes->post('/api/counter', 'Counter::create'); -$routes->patch('/api/counter', 'Counter::update'); -$routes->delete('/api/counter', 'Counter::delete'); - -$routes->get('/api/areageo', 'AreaGeo::index'); -$routes->get('/api/areageo/provinces', 'AreaGeo::getProvinces'); -$routes->get('/api/areageo/cities', 'AreaGeo::getCities'); - -//organization -// account -$routes->get('/api/organization/account/', 'Organization\Account::index'); -$routes->get('/api/organization/account/(:num)', 'Organization\Account::show/$1'); -$routes->post('/api/organization/account', 'Organization\Account::create'); -$routes->patch('/api/organization/account', 'Organization\Account::update'); -$routes->delete('/api/organization/account', 'Organization\Account::delete'); -// site -$routes->get('/api/organization/site/', 'Organization\Site::index'); -$routes->get('/api/organization/site/(:num)', 'Organization\Site::show/$1'); -$routes->post('/api/organization/site', 'Organization\Site::create'); -$routes->patch('/api/organization/site', 'Organization\Site::update'); -$routes->delete('/api/organization/site', 'Organization\Site::delete'); -// discipline -$routes->get('/api/organization/discipline/', 'Organization\Discipline::index'); -$routes->get('/api/organization/discipline/(:num)', 'Organization\Discipline::show/$1'); -$routes->post('/api/organization/discipline', 'Organization\Discipline::create'); -$routes->patch('/api/organization/discipline', 'Organization\Discipline::update'); -$routes->delete('/api/organization/discipline', 'Organization\Discipline::delete'); -// department -$routes->get('/api/organization/department/', 'Organization\Department::index'); -$routes->get('/api/organization/department/(:num)', 'Organization\Department::show/$1'); -$routes->post('/api/organization/department', 'Organization\Department::create'); -$routes->patch('/api/organization/department', 'Organization\Department::update'); -$routes->delete('/api/organization/department', 'Organization\Department::delete'); -// workstation -$routes->get('/api/organization/workstation/', 'Organization\Workstation::index'); -$routes->get('/api/organization/workstation/(:num)', 'Organization\Workstation::show/$1'); -$routes->post('/api/organization/workstation', 'Organization\Workstation::create'); -$routes->patch('/api/organization/workstation', 'Organization\Workstation::update'); -$routes->delete('/api/organization/workstation', 'Organization\Workstation::delete'); - -$routes->group('api/specimen', function($routes) { - $routes->get('containerdef/(:num)', 'Specimen\ContainerDef::show/$1'); - $routes->post('containerdef', 'Specimen\ContainerDef::create'); - $routes->patch('containerdef', 'Specimen\ContainerDef::update'); - $routes->get('containerdef', 'Specimen\ContainerDef::index'); - - $routes->get('prep/(:num)', 'Specimen\Prep::show/$1'); - $routes->post('prep', 'Specimen\Prep::create'); - $routes->patch('prep', 'Specimen\Prep::update'); - $routes->get('prep', 'Specimen\Prep::index'); - - $routes->get('status/(:num)', 'Specimen\Status::show/$1'); - $routes->post('status', 'Specimen\Status::create'); - $routes->patch('status', 'Specimen\Status::update'); - $routes->get('status', 'Specimen\Status::index'); - - $routes->get('collection/(:num)', 'Specimen\Collection::show/$1'); - $routes->post('collection', 'Specimen\Collection::create'); - $routes->patch('collection', 'Specimen\Collection::update'); - $routes->get('collection', 'Specimen\Collection::index'); - - $routes->get('(:num)', 'Specimen\Specimen::show/$1'); - $routes->post('', 'Specimen\Specimen::create'); - $routes->patch('', 'Specimen\Specimen::update'); - $routes->get('', 'Specimen\Specimen::index'); +// Protected Page Routes - V2 (requires auth) +$routes->group('v2', ['filter' => 'auth'], function ($routes) { + $routes->get('/', 'PagesController::dashboard'); + $routes->get('dashboard', 'PagesController::dashboard'); + $routes->get('patients', 'PagesController::patients'); + $routes->get('requests', 'PagesController::requests'); + $routes->get('settings', 'PagesController::settings'); }); -$routes->post('/api/tests', 'Tests::create'); -$routes->patch('/api/tests', 'Tests::update'); -$routes->get('/api/tests/(:any)', 'Tests::show/$1'); -$routes->get('/api/tests', 'Tests::index'); +// Faker +$routes->get('faker/faker-patient/(:num)', 'faker\FakerPatient::sendMany/$1'); -// Edge API - Integration with tiny-edge -$routes->group('/api/edge', function($routes) { - $routes->post('results', 'Edge::results'); - $routes->get('orders', 'Edge::orders'); - $routes->post('orders/(:num)/ack', 'Edge::ack/$1'); - $routes->post('status', 'Edge::status'); +$routes->group('api', function ($routes) { + // Auth + $routes->group('auth', function ($routes) { + $routes->post('login', 'Auth::login'); + $routes->post('change_pass', 'Auth::change_pass'); + $routes->post('register', 'Auth::register'); + $routes->get('check', 'Auth::checkAuth'); + $routes->post('logout', 'Auth::logout'); + }); + + // Patient + $routes->group('patient', function ($routes) { + $routes->get('/', 'Patient\Patient::index'); + $routes->post('/', 'Patient\Patient::create'); + $routes->get('(:num)', 'Patient\Patient::show/$1'); + $routes->delete('/', 'Patient\Patient::delete'); + $routes->patch('/', 'Patient\Patient::update'); + $routes->get('check', 'Patient\Patient::patientCheck'); + }); + + // PatVisit + $routes->group('patvisit', function ($routes) { + $routes->get('/', 'PatVisit::index'); + $routes->post('/', 'PatVisit::create'); + $routes->get('patient/(:num)', 'PatVisit::showByPatient/$1'); + $routes->get('(:any)', 'PatVisit::show/$1'); + $routes->delete('/', 'PatVisit::delete'); + $routes->patch('/', 'PatVisit::update'); + }); + + $routes->group('patvisitadt', function ($routes) { + $routes->post('/', 'PatVisit::createADT'); + $routes->patch('/', 'PatVisit::updateADT'); + }); + + // Master Data + $routes->group('race', function ($routes) { + $routes->get('/', 'Race::index'); + $routes->get('(:num)', 'Race::show/$1'); + }); + + $routes->group('country', function ($routes) { + $routes->get('/', 'Country::index'); + $routes->get('(:num)', 'Country::show/$1'); + }); + + $routes->group('religion', function ($routes) { + $routes->get('/', 'Religion::index'); + $routes->get('(:num)', 'Religion::show/$1'); + }); + + $routes->group('ethnic', function ($routes) { + $routes->get('/', 'Ethnic::index'); + $routes->get('(:num)', 'Ethnic::show/$1'); + }); + + // Location + $routes->group('location', function ($routes) { + $routes->get('/', 'Location::index'); + $routes->get('(:num)', 'Location::show/$1'); + $routes->post('/', 'Location::create'); + $routes->patch('/', 'Location::update'); + $routes->delete('/', 'Location::delete'); + }); + + // Contact + $routes->group('contact', function ($routes) { + $routes->get('/', 'Contact\Contact::index'); + $routes->get('(:num)', 'Contact\Contact::show/$1'); + $routes->post('/', 'Contact\Contact::create'); + $routes->patch('/', 'Contact\Contact::update'); + $routes->delete('/', 'Contact\Contact::delete'); + }); + + $routes->group('occupation', function ($routes) { + $routes->get('/', 'Contact\Occupation::index'); + $routes->get('(:num)', 'Contact\Occupation::show/$1'); + $routes->post('/', 'Contact\Occupation::create'); + $routes->patch('/', 'Contact\Occupation::update'); + //$routes->delete('/', 'Contact\Occupation::delete'); + }); + + $routes->group('medicalspecialty', function ($routes) { + $routes->get('/', 'Contact\MedicalSpecialty::index'); + $routes->get('(:num)', 'Contact\MedicalSpecialty::show/$1'); + $routes->post('/', 'Contact\MedicalSpecialty::create'); + $routes->patch('/', 'Contact\MedicalSpecialty::update'); + }); + + // ValueSet + $routes->group('valueset', function ($routes) { + $routes->get('/', 'ValueSet\ValueSet::index'); + $routes->get('(:num)', 'ValueSet\ValueSet::show/$1'); + $routes->get('valuesetdef/(:segment)', 'ValueSet\ValueSet::showByValueSetDef/$1'); + $routes->post('/', 'ValueSet\ValueSet::create'); + $routes->patch('/', 'ValueSet\ValueSet::update'); + $routes->delete('/', 'ValueSet\ValueSet::delete'); + }); + + $routes->group('valuesetdef', function ($routes) { + $routes->get('/', 'ValueSet\ValueSetDef::index'); + $routes->get('(:segment)', 'ValueSet\ValueSetDef::show/$1'); + $routes->post('/', 'ValueSet\ValueSetDef::create'); + $routes->patch('/', 'ValueSet\ValueSetDef::update'); + $routes->delete('/', 'ValueSet\ValueSetDef::delete'); + }); + + // Counter + $routes->group('counter', function ($routes) { + $routes->get('/', 'Counter::index'); + $routes->get('(:num)', 'Counter::show/$1'); + $routes->post('/', 'Counter::create'); + $routes->patch('/', 'Counter::update'); + $routes->delete('/', 'Counter::delete'); + }); + + // AreaGeo + $routes->group('areageo', function ($routes) { + $routes->get('/', 'AreaGeo::index'); + $routes->get('provinces', 'AreaGeo::getProvinces'); + $routes->get('cities', 'AreaGeo::getCities'); + }); + + // Organization + $routes->group('organization', function ($routes) { + // Account + $routes->group('account', function ($routes) { + $routes->get('/', 'Organization\Account::index'); + $routes->get('(:num)', 'Organization\Account::show/$1'); + $routes->post('/', 'Organization\Account::create'); + $routes->patch('/', 'Organization\Account::update'); + $routes->delete('/', 'Organization\Account::delete'); + }); + + // Site + $routes->group('site', function ($routes) { + $routes->get('/', 'Organization\Site::index'); + $routes->get('(:num)', 'Organization\Site::show/$1'); + $routes->post('/', 'Organization\Site::create'); + $routes->patch('/', 'Organization\Site::update'); + $routes->delete('/', 'Organization\Site::delete'); + }); + + // Discipline + $routes->group('discipline', function ($routes) { + $routes->get('/', 'Organization\Discipline::index'); + $routes->get('(:num)', 'Organization\Discipline::show/$1'); + $routes->post('/', 'Organization\Discipline::create'); + $routes->patch('/', 'Organization\Discipline::update'); + $routes->delete('/', 'Organization\Discipline::delete'); + }); + + // Department + $routes->group('department', function ($routes) { + $routes->get('/', 'Organization\Department::index'); + $routes->get('(:num)', 'Organization\Department::show/$1'); + $routes->post('/', 'Organization\Department::create'); + $routes->patch('/', 'Organization\Department::update'); + $routes->delete('/', 'Organization\Department::delete'); + }); + + // Workstation + $routes->group('workstation', function ($routes) { + $routes->get('/', 'Organization\Workstation::index'); + $routes->get('(:num)', 'Organization\Workstation::show/$1'); + $routes->post('/', 'Organization\Workstation::create'); + $routes->patch('/', 'Organization\Workstation::update'); + $routes->delete('/', 'Organization\Workstation::delete'); + }); + }); + + // Specimen + $routes->group('specimen', function ($routes) { + $routes->group('containerdef', function ($routes) { + $routes->get('/', 'Specimen\ContainerDef::index'); + $routes->get('(:num)', 'Specimen\ContainerDef::show/$1'); + $routes->post('/', 'Specimen\ContainerDef::create'); + $routes->patch('/', 'Specimen\ContainerDef::update'); + }); + + $routes->group('prep', function ($routes) { + $routes->get('/', 'Specimen\Prep::index'); + $routes->get('(:num)', 'Specimen\Prep::show/$1'); + $routes->post('/', 'Specimen\Prep::create'); + $routes->patch('/', 'Specimen\Prep::update'); + }); + + $routes->group('status', function ($routes) { + $routes->get('/', 'Specimen\Status::index'); + $routes->get('(:num)', 'Specimen\Status::show/$1'); + $routes->post('/', 'Specimen\Status::create'); + $routes->patch('/', 'Specimen\Status::update'); + }); + + $routes->group('collection', function ($routes) { + $routes->get('/', 'Specimen\Collection::index'); + $routes->get('(:num)', 'Specimen\Collection::show/$1'); + $routes->post('/', 'Specimen\Collection::create'); + $routes->patch('/', 'Specimen\Collection::update'); + }); + + $routes->get('/', 'Specimen\Specimen::index'); + $routes->get('(:num)', 'Specimen\Specimen::show/$1'); + $routes->post('/', 'Specimen\Specimen::create'); + $routes->patch('/', 'Specimen\Specimen::update'); + }); + + // Tests + $routes->group('tests', function ($routes) { + $routes->get('/', 'Tests::index'); + $routes->get('(:any)', 'Tests::show/$1'); + $routes->post('/', 'Tests::create'); + $routes->patch('/', 'Tests::update'); + }); + + // Edge API - Integration with tiny-edge + $routes->group('edge', function ($routes) { + $routes->post('results', 'Edge::results'); + $routes->get('orders', 'Edge::orders'); + $routes->post('orders/(:num)/ack', 'Edge::ack/$1'); + $routes->post('status', 'Edge::status'); + }); }); // Khusus @@ -188,3 +261,4 @@ $routes->get('/api/zones/synchronize', 'Zones::synchronize'); $routes->get('/api/zones/provinces', 'Zones::getProvinces'); $routes->get('/api/zones/cities', 'Zones::getCities'); */ + diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php new file mode 100644 index 0000000..aa3981c --- /dev/null +++ b/app/Views/auth/login.php @@ -0,0 +1,323 @@ + + +
+ + +Clinical Laboratory Queue Management System
++ Don't have an account? + +
+© 2025 5Panda. All rights reserved.
+Clinical Laboratory Queue Management System
+Total Patients
+1,247
+Today's Visits
+89
+Pending Tests
+34
+Completed
+156
+New patient registered
+John Doe - 5 minutes ago
+Test completed
+Sample #12345 - 12 minutes ago
+Pending approval
+Request #789 - 25 minutes ago
+