feat: Implement audit trail system for dual-level validation workflow
This commit adds comprehensive audit logging for specimen requests and sample collection activities across all roles. Changes Summary: New Features: - Added AUDIT_EVENTS table schema for tracking validation and sample collection events - Created ApiRequestsAuditController with /api/requests/(:any)/audit endpoint to retrieve audit history - Added dialog_audit.php view component for displaying audit trails in UI - Integrated audit logging into validation workflow (VAL1, VAL2, UNVAL events) Database: - Created AUDIT_EVENTS table with columns: ACCESSNUMBER, EVENT_TYPE, USERID, EVENT_AT, REASON - Supports tracking validation events and sample collection actions Controllers: - RequestsController: Now inserts audit records for all validation operations - ApiRequestsAuditController: New API controller returning validation and sample collection history Routes: - Added GET /api/requests/(:any)/audit endpoint for retrieving audit trail - Removed DELETE /api/samples/collect/(:any) endpoint (uncollect functionality) Views Refactoring: - Consolidated dashboard layouts into shared components: - layout.php (from layout_dashboard.php) - script_requests.php (from script_dashboard.php) - script_validation.php (from script_validate.php) - content_requests.php (from dashboard_table.php) - content_validation.php (from dashboard_validate.php) - Added content_validation_new.php for enhanced validation interface
This commit is contained in:
parent
8e82cbad52
commit
3cf4cc7f3f
244
AGENTS.md
244
AGENTS.md
@ -4,14 +4,14 @@ This file provides guidance to agents when working with code in this repository.
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is a CodeIgniter 4 PHP application for a laboratory management system (CMOD). It uses SQL Server database with role-based access control for different user types (admin, doctor/analyst, customer service).
|
CodeIgniter 4 PHP application for laboratory management (GDC CMOD). Handles specimen tracking, request validation, and result management with role-based access control. SQL Server database with Firebird legacy patient data.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Run all tests
|
||||||
./vendor/bin/phpunit
|
|
||||||
composer test
|
composer test
|
||||||
|
./vendor/bin/phpunit
|
||||||
|
|
||||||
# Run single test file
|
# Run single test file
|
||||||
./vendor/bin/phpunit tests/unit/HealthTest.php
|
./vendor/bin/phpunit tests/unit/HealthTest.php
|
||||||
@ -19,249 +19,145 @@ composer test
|
|||||||
# Run single test method
|
# Run single test method
|
||||||
./vendor/bin/phpunit tests/unit/HealthTest.php --filter testIsDefinedAppPath
|
./vendor/bin/phpunit tests/unit/HealthTest.php --filter testIsDefinedAppPath
|
||||||
|
|
||||||
# Development server (Linux/Mac)
|
# Development server
|
||||||
php spark serve
|
php spark serve
|
||||||
|
|
||||||
# List all routes
|
# List all routes
|
||||||
php spark list
|
php spark list
|
||||||
|
|
||||||
# Create controller
|
# Create controller/model
|
||||||
php spark make:controller Admin
|
php spark make:controller Admin
|
||||||
|
|
||||||
# Create model
|
|
||||||
php spark make:model User
|
php spark make:model User
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code Style Guidelines
|
## PHP Standards
|
||||||
|
|
||||||
### PHP Standards
|
- PHP 8.1+ features (typed properties, match expressions)
|
||||||
- Use PHP 8.1+ features (typed properties, match expressions where appropriate)
|
|
||||||
- Always declare return types for public methods
|
- Always declare return types for public methods
|
||||||
- Use `strict_types=1` not required (CodeIgniter doesn't use it)
|
- No comments unless explaining complex logic
|
||||||
- No comments unless explaining complex logic (per project convention)
|
- Use `esc()` when outputting user data in views
|
||||||
|
|
||||||
### Naming Conventions
|
## Naming Conventions
|
||||||
- **Classes**: PascalCase (e.g., `Admin`, `UserController`)
|
|
||||||
- **Methods**: camelCase (e.g., `index()`, `getUsers()`)
|
|
||||||
- **Variables**: camelCase (e.g., `$userId`, `$dataList`)
|
|
||||||
- **Constants**: UPPER_SNAKE_CASE (e.g., `DB_HOST`)
|
|
||||||
- **Database tables**: UPPER_SNAKE_CASE (e.g., `GDC_CMOD.dbo.USERS`)
|
|
||||||
- **Views**: lowercase with underscores (e.g., `admin/index.php`)
|
|
||||||
|
|
||||||
### Controller Patterns
|
| Type | Convention | Example |
|
||||||
|
|------|------------|---------|
|
||||||
|
| Classes | PascalCase | `Admin`, `UserController` |
|
||||||
|
| Methods/Variables | camelCase | `getUsers()`, `$userId` |
|
||||||
|
| Constants | UPPER_SNAKE_CASE | `DB_HOST` |
|
||||||
|
| Database tables | UPPER_SNAKE_CASE | `GDC_CMOD.dbo.USERS` |
|
||||||
|
| Views | lowercase_underscores | `admin/index.php` |
|
||||||
|
|
||||||
#### Base Controllers
|
## Role-Based Access Control
|
||||||
- All controllers extend `App\Controllers\BaseController`
|
|
||||||
- BaseController extends CodeIgniter\Controller
|
| Role ID | Name | Route Prefix |
|
||||||
|
|---------|------|--------------|
|
||||||
|
| 0 | Superuser | `/superuser` |
|
||||||
|
| 1 | Admin | `/admin` |
|
||||||
|
| 2 | Lab | `/lab` |
|
||||||
|
| 3 | Phlebo | `/phlebo` |
|
||||||
|
| 4 | CS | `/cs` |
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Single role
|
||||||
|
['filter' => 'role:1']
|
||||||
|
// Multiple roles
|
||||||
|
['filter' => 'role:1,2']
|
||||||
|
```
|
||||||
|
|
||||||
|
## Controller Patterns
|
||||||
|
|
||||||
```php
|
```php
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
class Admin extends BaseController {
|
class Admin extends BaseController {
|
||||||
public function index() {
|
public function index() { }
|
||||||
// Method body
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
#### API Controllers
|
|
||||||
- Controllers use `ResponseTrait` for API responses
|
|
||||||
- Use `$this->respond()` or `$this->response->setJSON()` for responses
|
|
||||||
|
|
||||||
```php
|
|
||||||
namespace App\Controllers;
|
|
||||||
|
|
||||||
|
// API Controllers use ResponseTrait
|
||||||
use App\Controllers\BaseController;
|
use App\Controllers\BaseController;
|
||||||
use CodeIgniter\API\ResponseTrait;
|
use CodeIgniter\API\ResponseTrait;
|
||||||
|
|
||||||
class Users extends BaseController {
|
class Users extends BaseController {
|
||||||
use ResponseTrait;
|
use ResponseTrait;
|
||||||
|
|
||||||
protected $db;
|
protected $db;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->db = \Config\Database::connect();
|
$this->db = \Config\Database::connect();
|
||||||
helper(['url', 'form', 'text']);
|
helper(['url', 'form', 'text']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index() {
|
|
||||||
$query = $this->db->query("SELECT * FROM table");
|
|
||||||
return $this->respond(['data' => $query->getResultArray()]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Database Operations
|
## Database Operations
|
||||||
|
|
||||||
#### Connection Pattern
|
|
||||||
```php
|
```php
|
||||||
$this->db = \Config\Database::connect();
|
$this->db = \Config\Database::connect();
|
||||||
```
|
|
||||||
|
|
||||||
#### Query Methods
|
// Parameterized queries only
|
||||||
- `getRowArray()` - returns single row as associative array
|
|
||||||
- `getResultArray()` - returns multiple rows as array of arrays
|
|
||||||
- Use parameterized queries to prevent SQL injection
|
|
||||||
|
|
||||||
```php
|
|
||||||
$query = $this->db->query("SELECT * FROM table WHERE id = ?", [$id]);
|
$query = $this->db->query("SELECT * FROM table WHERE id = ?", [$id]);
|
||||||
$row = $query->getRowArray();
|
$row = $query->getRowArray();
|
||||||
$results = $query->getResultArray();
|
$results = $query->getResultArray();
|
||||||
```
|
|
||||||
|
|
||||||
#### Transactions
|
// Transactions
|
||||||
```php
|
|
||||||
$this->db->transBegin();
|
$this->db->transBegin();
|
||||||
try {
|
try {
|
||||||
$this->db->query("INSERT INTO ...", [$data]);
|
$this->db->query("INSERT INTO ...", [$data]);
|
||||||
$this->db->transCommit();
|
$this->db->transCommit();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->db->transRollback();
|
$this->db->transRollback();
|
||||||
return $this->response->setJSON(['message' => 'Error']);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Session Management
|
## Request/Response Patterns
|
||||||
|
|
||||||
#### Session Structure
|
|
||||||
```php
|
```php
|
||||||
$session->set([
|
// GET input
|
||||||
'isLoggedIn' => true,
|
|
||||||
'userid' => (string) $user['USERID'],
|
|
||||||
'userlevel' => (int) $user['USERLEVEL'],
|
|
||||||
'userrole' => (string) $role, // 'admin', 'doctor', 'analyst', 'cs'
|
|
||||||
]);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Session Values
|
|
||||||
- `isLoggedIn`: bool
|
|
||||||
- `userid`: string
|
|
||||||
- `userlevel`: int
|
|
||||||
- `userrole`: string
|
|
||||||
|
|
||||||
### Role-Based Access Control
|
|
||||||
|
|
||||||
#### Role Values
|
|
||||||
- `1` = admin
|
|
||||||
- `2` = doctor (or lab/analyst)
|
|
||||||
- `3` = analyst
|
|
||||||
- `4` = cs (customer service)
|
|
||||||
|
|
||||||
#### Route Filter Syntax
|
|
||||||
```php
|
|
||||||
// Single role
|
|
||||||
['filter' => 'role:1']
|
|
||||||
|
|
||||||
// Multiple roles
|
|
||||||
['filter' => 'role:1,2']
|
|
||||||
```
|
|
||||||
|
|
||||||
### Request/Response Patterns
|
|
||||||
|
|
||||||
#### Getting Input
|
|
||||||
```php
|
|
||||||
// POST data
|
|
||||||
$input = $this->request->getJSON(true);
|
|
||||||
$userid = $input['userid'];
|
|
||||||
|
|
||||||
// Query parameters
|
|
||||||
$date1 = $this->request->getVar('date1') ?? date('Y-m-d');
|
$date1 = $this->request->getVar('date1') ?? date('Y-m-d');
|
||||||
```
|
|
||||||
|
|
||||||
#### JSON Response (V2 API)
|
// POST JSON
|
||||||
```php
|
$input = $this->request->getJSON(true);
|
||||||
|
|
||||||
|
// JSON response
|
||||||
return $this->respond(['data' => $results]);
|
return $this->respond(['data' => $results]);
|
||||||
return $this->response->setJSON(['message' => 'Success']);
|
return $this->response->setJSON(['message' => 'Success']);
|
||||||
```
|
|
||||||
|
|
||||||
#### View Response (Traditional)
|
// View response
|
||||||
```php
|
|
||||||
return view('admin/index', $data);
|
return view('admin/index', $data);
|
||||||
```
|
|
||||||
|
|
||||||
#### Redirect with Errors
|
// Redirect with errors
|
||||||
```php
|
|
||||||
return redirect()->back()->with('errors', ['key' => 'message']);
|
return redirect()->back()->with('errors', ['key' => 'message']);
|
||||||
```
|
```
|
||||||
|
|
||||||
### API Endpoint Patterns
|
## Session Structure
|
||||||
|
|
||||||
|
```php
|
||||||
|
session()->set([
|
||||||
|
'isLoggedIn' => true,
|
||||||
|
'userid' => (string) $user['USERID'],
|
||||||
|
'userroleid' => (int) $user['USERROLEID'],
|
||||||
|
'userrole' => (string) $role,
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Endpoints
|
||||||
|
|
||||||
#### Validation Endpoints
|
|
||||||
- `POST /api/{resource}/validate/{id}` - validate a record
|
- `POST /api/{resource}/validate/{id}` - validate a record
|
||||||
- `DELETE /api/{resource}/validate/{id}` - unvalidate a record
|
- `DELETE /api/{resource}/validate/{id}` - unvalidate a record
|
||||||
|
|
||||||
#### Route Examples
|
## Security
|
||||||
```php
|
|
||||||
// Admin routes
|
|
||||||
$routes->group('admin', ['filter' => 'role:1'], function($routes) {
|
|
||||||
$routes->get('/', 'Admin::index');
|
|
||||||
$routes->get('users', 'Admin::users');
|
|
||||||
$routes->get('api/users', 'Users::index');
|
|
||||||
$routes->post('api/users', 'Users::create');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Lab routes
|
- Use parameterized queries (never interpolate directly)
|
||||||
$routes->group('lab', ['filter' => 'role:2'], function($routes) {
|
- Hash passwords with `password_hash()` / `password_verify()`
|
||||||
$routes->get('/', 'Lab::index');
|
|
||||||
$routes->get('api/requests', 'Requests::index');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
#### Database Operations
|
|
||||||
```php
|
|
||||||
try {
|
|
||||||
// DB operations
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// Handle error
|
|
||||||
return $this->response->setJSON(['message' => 'Server error']);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Validation Errors
|
|
||||||
```php
|
|
||||||
if ($condition) {
|
|
||||||
return $this->response->setJSON(['message' => 'Error message']);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Views
|
|
||||||
|
|
||||||
#### View Pattern
|
|
||||||
- Views are plain PHP files in `app/Views/`
|
|
||||||
- Use `<?php echo $variable; ?>` syntax
|
|
||||||
- Pass data as associative array
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Controller
|
|
||||||
$data['dataList'] = $results;
|
|
||||||
return view('admin/index', $data);
|
|
||||||
|
|
||||||
// View
|
|
||||||
<?php foreach ($dataList as $item): ?>
|
|
||||||
<tr><td><?= esc($item['name']) ?></td></tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Always use parameterized queries (never interpolate directly)
|
|
||||||
- Use `esc()` when outputting user data in views
|
|
||||||
- Hash passwords with `password_hash()` and verify with `password_verify()`
|
|
||||||
- Validate and sanitize all input before use
|
- Validate and sanitize all input before use
|
||||||
|
|
||||||
### Helper Functions
|
## Database Schema
|
||||||
|
|
||||||
Available helpers loaded via `helper(['name', 'name2'])`:
|
- Primary: SQL Server (`GDC_CMOD.dbo`)
|
||||||
- `url` - URL helpers
|
- Legacy: Firebird (`GLENEAGLES` via ODBC)
|
||||||
- `form` - Form helpers
|
- No CI4 Models - raw SQL queries via `Database::connect()`
|
||||||
- `text` - Text formatting
|
|
||||||
|
|
||||||
### Database Schema
|
## Dual-Level Validation
|
||||||
|
|
||||||
- Database: SQL Server
|
Validation requires 2 different users:
|
||||||
- Schema: `dbo`
|
1. First: `ISVAL1=1`, `VAL1USER`, `VAL1DATE`
|
||||||
- Main database: `GDC_CMOD`
|
2. Second (different user): `ISVAL2=1`, `VAL2USER`, `VAL2DATE`
|
||||||
- Reference database: `glendb`
|
|
||||||
- Table naming: `GDC_CMOD.dbo.TABLENAME`
|
|
||||||
|
|||||||
@ -3,16 +3,16 @@
|
|||||||
**Last Updated:** January 22, 2026
|
**Last Updated:** January 22, 2026
|
||||||
|
|
||||||
Pending:
|
Pending:
|
||||||
|
|
||||||
- Restrict 'UnValidate' to Admin
|
|
||||||
- Restrict Print/Save-to-PDF to CS Role only (Lab can only preview, CS can print/save)
|
- Restrict Print/Save-to-PDF to CS Role only (Lab can only preview, CS can print/save)
|
||||||
|
- Add Dedicated Print Button (Trigger browser/system print dialog)
|
||||||
- Create 'Detail Unvalidated' History Log/View (Log unvalidation actions with timestamp, user ID, and reason)
|
- Create 'Detail Unvalidated' History Log/View (Log unvalidation actions with timestamp, user ID, and reason)
|
||||||
- Enhanced Patient Detail Logging (Track: Sample Collection Time, Sample Received Time, Print History)
|
- Enhanced Patient Detail Logging (Track: Sample Collection Time, Sample Received Time, Print History)
|
||||||
- Add Dedicated Print Button (Trigger browser/system print dialog)
|
|
||||||
- Add Error Handling for Preview Button (Handle empty data gracefully)
|
- Add Error Handling for Preview Button (Handle empty data gracefully)
|
||||||
- Ensure 'Uncollect' Feature Functional (Maintain Uncollect feature functionality)
|
- Ensure 'Uncollect' Feature Functional (Maintain Uncollect feature functionality)
|
||||||
- Backend Performance & Connectivity (Investigate intermittent connection issues with Server 253)
|
- Backend Performance & Connectivity (Investigate intermittent connection issues with Server 253)
|
||||||
- Update PDF Report Metadata (Replace 'Printed By' with validating user's name)
|
- Update PDF Report Metadata (Replace 'Printed By' with validating user's name)
|
||||||
|
- Reprint Label (Add functionality to reprint labels)
|
||||||
|
- **Print Result Audit** (Track when result reports are printed/exported, log user and timestamp)
|
||||||
|
|
||||||
Completed:
|
Completed:
|
||||||
- 01 : Update User Role levels (Standardize roles: Superuser, Admin, Lab, Phlebo, CS)
|
- 01 : Update User Role levels (Standardize roles: Superuser, Admin, Lab, Phlebo, CS)
|
||||||
@ -29,6 +29,9 @@ Completed:
|
|||||||
- 12 : Remove 'status' field on dashboard
|
- 12 : Remove 'status' field on dashboard
|
||||||
- 13 : Restrict 'Validate' to Lab, Admin, Superuser
|
- 13 : Restrict 'Validate' to Lab, Admin, Superuser
|
||||||
- 14 : Hide/Disable 'Validation' button after 2nd validation (Prevent redundant validation actions)
|
- 14 : Hide/Disable 'Validation' button after 2nd validation (Prevent redundant validation actions)
|
||||||
|
- 15 : Restrict 'UnValidate' to Admin, Superuser
|
||||||
|
- 16 : Remove 'UnCollect'
|
||||||
|
|
||||||
|
|
||||||
Addition on dev :
|
Addition on dev :
|
||||||
- adding init-isDev on index.php to set default date on dev dashboard
|
- adding init-isDev on index.php to set default date on dev dashboard
|
||||||
@ -47,7 +47,7 @@ This application uses role-based access control with four user roles.
|
|||||||
- **Dashboard** - View all requests with status filters (Pend, Coll, Recv, Inc, Fin, Val)
|
- **Dashboard** - View all requests with status filters (Pend, Coll, Recv, Inc, Fin, Val)
|
||||||
- **User Management** - Create, edit, delete users; assign roles
|
- **User Management** - Create, edit, delete users; assign roles
|
||||||
- **Request Management** - View, validate, unvalidate all requests
|
- **Request Management** - View, validate, unvalidate all requests
|
||||||
- **Sample Management** - Collect, uncollect, receive, unreceive samples
|
- **Sample Management** - Collect, receive, unreceive samples
|
||||||
- **Result Management** - Preview and print results
|
- **Result Management** - Preview and print results
|
||||||
|
|
||||||
#### Lab
|
#### Lab
|
||||||
|
|||||||
@ -35,6 +35,7 @@ $routes->group('api', function ($routes) {
|
|||||||
// Requests - All Roles (0,1,2,3,4)
|
// Requests - All Roles (0,1,2,3,4)
|
||||||
$routes->group('requests', ['filter' => 'role:0,1,2,3,4'], function ($routes) {
|
$routes->group('requests', ['filter' => 'role:0,1,2,3,4'], function ($routes) {
|
||||||
$routes->get('', 'RequestsController::index');
|
$routes->get('', 'RequestsController::index');
|
||||||
|
$routes->get('(:any)/audit', 'ApiRequestsAuditController::show/$1');
|
||||||
$routes->post('validate/(:any)', 'RequestsController::val/$1');
|
$routes->post('validate/(:any)', 'RequestsController::val/$1');
|
||||||
$routes->delete('validate/(:any)', 'RequestsController::unval/$1');
|
$routes->delete('validate/(:any)', 'RequestsController::unval/$1');
|
||||||
});
|
});
|
||||||
@ -52,9 +53,8 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->get('(:any)', 'SamplesController::show/$1');
|
$routes->get('(:any)', 'SamplesController::show/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Uncollect & Unreceive - Only Superuser (0) and Admin (1)
|
// Unreceive - Only Superuser (0) and Admin (1)
|
||||||
$routes->group('', ['filter' => 'role:0,1'], function ($routes) {
|
$routes->group('', ['filter' => 'role:0,1'], function ($routes) {
|
||||||
$routes->delete('collect/(:any)', 'SamplesController::uncollect/$1');
|
|
||||||
$routes->delete('receive/(:any)', 'SamplesController::unreceive/$1');
|
$routes->delete('receive/(:any)', 'SamplesController::unreceive/$1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
52
app/Controllers/ApiRequestsAuditController.php
Normal file
52
app/Controllers/ApiRequestsAuditController.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Controllers;
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
use CodeIgniter\API\ResponseTrait;
|
||||||
|
|
||||||
|
class ApiRequestsAuditController extends BaseController {
|
||||||
|
use ResponseTrait;
|
||||||
|
|
||||||
|
public function show($accessnumber) {
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'accessnumber' => $accessnumber,
|
||||||
|
'validation' => [],
|
||||||
|
'sample_collection' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
$sqlAudit = "SELECT EVENT_TYPE, USERID, EVENT_AT, REASON
|
||||||
|
FROM GDC_CMOD.dbo.AUDIT_EVENTS
|
||||||
|
WHERE ACCESSNUMBER = ?
|
||||||
|
ORDER BY EVENT_AT ASC";
|
||||||
|
$auditRows = $db->query($sqlAudit, [$accessnumber])->getResultArray();
|
||||||
|
|
||||||
|
foreach ($auditRows as $row) {
|
||||||
|
$isUnval = $row['EVENT_TYPE'] === 'UNVAL';
|
||||||
|
$result['validation'][] = [
|
||||||
|
'type' => $row['EVENT_TYPE'],
|
||||||
|
'user' => trim($row['USERID']),
|
||||||
|
'datetime' => $row['EVENT_AT'] ? date('Y-m-d H:i:s', strtotime($row['EVENT_AT'])) : null,
|
||||||
|
'reason' => $isUnval ? trim($row['REASON']) : null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sqlTube = "SELECT TUBENUMBER, USERID, STATUS, LOGDATE
|
||||||
|
FROM GDC_CMOD.dbo.AUDIT_TUBES
|
||||||
|
WHERE ACCESSNUMBER = ?
|
||||||
|
ORDER BY LOGDATE ASC";
|
||||||
|
$tubeRows = $db->query($sqlTube, [$accessnumber])->getResultArray();
|
||||||
|
|
||||||
|
foreach ($tubeRows as $row) {
|
||||||
|
$action = $row['STATUS'] == 1 ? 'COLLECTED' : 'UNRECEIVED';
|
||||||
|
$result['sample_collection'][] = [
|
||||||
|
'tubenumber' => trim($row['TUBENUMBER']),
|
||||||
|
'user' => trim($row['USERID']),
|
||||||
|
'datetime' => $row['LOGDATE'] ? date('Y-m-d H:i:s', strtotime($row['LOGDATE'])) : null,
|
||||||
|
'action' => $action
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->respond(['status' => 'success', 'data' => $result]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,8 @@ class LabelController extends BaseController
|
|||||||
public function coll($reqnum)
|
public function coll($reqnum)
|
||||||
{
|
{
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
//$reqnum = str_pad($reqnum, 10, 0, STR_PAD_LEFT);
|
$userid = session()->get('userid') ?? 'system';
|
||||||
|
|
||||||
$sql = "select p.PATNUMBER,
|
$sql = "select p.PATNUMBER,
|
||||||
[Name] = case
|
[Name] = case
|
||||||
when p.TITLEID is not null then ISNULL(p.FIRSTNAME,'') + ' ' + ISNULL(p.NAME,'') + ', ' + tx.SHORTTEXT
|
when p.TITLEID is not null then ISNULL(p.FIRSTNAME,'') + ' ' + ISNULL(p.NAME,'') + ', ' + tx.SHORTTEXT
|
||||||
@ -70,6 +71,8 @@ P1\n]";
|
|||||||
public function dispatch($reqnum, $samid)
|
public function dispatch($reqnum, $samid)
|
||||||
{
|
{
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
|
$userid = session()->get('userid') ?? 'system';
|
||||||
|
|
||||||
$sql = "select p.PATNUMBER,
|
$sql = "select p.PATNUMBER,
|
||||||
[Name] = case
|
[Name] = case
|
||||||
when p.TITLEID is not null then ISNULL(p.FIRSTNAME,'') + ' ' + ISNULL(p.NAME,'') + ', ' + tx.SHORTTEXT
|
when p.TITLEID is not null then ISNULL(p.FIRSTNAME,'') + ' ' + ISNULL(p.NAME,'') + ', ' + tx.SHORTTEXT
|
||||||
@ -147,6 +150,8 @@ P1
|
|||||||
public function print_all($accessnumber)
|
public function print_all($accessnumber)
|
||||||
{
|
{
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
|
$userid = session()->get('userid') ?? 'system';
|
||||||
|
|
||||||
$this->coll($accessnumber);
|
$this->coll($accessnumber);
|
||||||
$sql = "select SAMPCODE from GDC_CMOD.dbo.v_sp_reqtube where SP_ACCESSNUMBER='$accessnumber'";
|
$sql = "select SAMPCODE from GDC_CMOD.dbo.v_sp_reqtube where SP_ACCESSNUMBER='$accessnumber'";
|
||||||
$rows = $db->query($sql)->getResultArray();
|
$rows = $db->query($sql)->getResultArray();
|
||||||
|
|||||||
@ -4,24 +4,29 @@ namespace App\Controllers\Pages;
|
|||||||
|
|
||||||
use App\Controllers\BaseController;
|
use App\Controllers\BaseController;
|
||||||
|
|
||||||
class AdminController extends BaseController {
|
class AdminController extends BaseController
|
||||||
|
{
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct()
|
||||||
|
{
|
||||||
helper(['url', 'form', 'text']);
|
helper(['url', 'form', 'text']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index() {
|
public function index()
|
||||||
$config = require APPPATH . 'Views/shared/dashboard_config.php';
|
{
|
||||||
|
$config = require APPPATH . 'Views/shared/config.php';
|
||||||
return view('admin/index', ['roleConfig' => $config['admin']]);
|
return view('admin/index', ['roleConfig' => $config['admin']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function users() {
|
public function users()
|
||||||
$config = require APPPATH . 'Views/shared/dashboard_config.php';
|
{
|
||||||
|
$config = require APPPATH . 'Views/shared/config.php';
|
||||||
return view('admin/users', ['roleConfig' => $config['admin']]);
|
return view('admin/users', ['roleConfig' => $config['admin']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validationPage() {
|
public function validationPage()
|
||||||
$config = require APPPATH . 'Views/shared/dashboard_config.php';
|
{
|
||||||
|
$config = require APPPATH . 'Views/shared/config.php';
|
||||||
return view('admin/validate', ['roleConfig' => $config['admin']]);
|
return view('admin/validate', ['roleConfig' => $config['admin']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,17 @@ namespace App\Controllers\Pages;
|
|||||||
|
|
||||||
use App\Controllers\BaseController;
|
use App\Controllers\BaseController;
|
||||||
|
|
||||||
class CsController extends BaseController {
|
class CsController extends BaseController
|
||||||
|
{
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct()
|
||||||
|
{
|
||||||
helper(['url', 'form', 'text']);
|
helper(['url', 'form', 'text']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index() {
|
public function index()
|
||||||
$config = require APPPATH . 'Views/shared/dashboard_config.php';
|
{
|
||||||
|
$config = require APPPATH . 'Views/shared/config.php';
|
||||||
return view('cs/index', ['roleConfig' => $config['cs']]);
|
return view('cs/index', ['roleConfig' => $config['cs']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,19 +4,23 @@ namespace App\Controllers\Pages;
|
|||||||
|
|
||||||
use App\Controllers\BaseController;
|
use App\Controllers\BaseController;
|
||||||
|
|
||||||
class LabController extends BaseController {
|
class LabController extends BaseController
|
||||||
|
{
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct()
|
||||||
|
{
|
||||||
helper(['url', 'form', 'text']);
|
helper(['url', 'form', 'text']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index() {
|
public function index()
|
||||||
$config = require APPPATH . 'Views/shared/dashboard_config.php';
|
{
|
||||||
|
$config = require APPPATH . 'Views/shared/config.php';
|
||||||
return view('lab/index', ['roleConfig' => $config['lab']]);
|
return view('lab/index', ['roleConfig' => $config['lab']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validationPage() {
|
public function validationPage()
|
||||||
$config = require APPPATH . 'Views/shared/dashboard_config.php';
|
{
|
||||||
|
$config = require APPPATH . 'Views/shared/config.php';
|
||||||
return view('lab/validate', ['roleConfig' => $config['lab']]);
|
return view('lab/validate', ['roleConfig' => $config['lab']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,17 @@ namespace App\Controllers\Pages;
|
|||||||
|
|
||||||
use App\Controllers\BaseController;
|
use App\Controllers\BaseController;
|
||||||
|
|
||||||
class PhlebotomistController extends BaseController {
|
class PhlebotomistController extends BaseController
|
||||||
|
{
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct()
|
||||||
|
{
|
||||||
helper(['url', 'form', 'text']);
|
helper(['url', 'form', 'text']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index() {
|
public function index()
|
||||||
$config = require APPPATH . 'Views/shared/dashboard_config.php';
|
{
|
||||||
|
$config = require APPPATH . 'Views/shared/config.php';
|
||||||
return view('phlebo/index', ['roleConfig' => $config['phlebo']]);
|
return view('phlebo/index', ['roleConfig' => $config['phlebo']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,19 +14,19 @@ class SuperuserController extends BaseController
|
|||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$config = require APPPATH . 'Views/shared/dashboard_config.php';
|
$config = require APPPATH . 'Views/shared/config.php';
|
||||||
return view('superuser/index', ['roleConfig' => $config['superuser']]);
|
return view('superuser/index', ['roleConfig' => $config['superuser']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function users()
|
public function users()
|
||||||
{
|
{
|
||||||
$config = require APPPATH . 'Views/shared/dashboard_config.php';
|
$config = require APPPATH . 'Views/shared/config.php';
|
||||||
return view('superuser/users', ['roleConfig' => $config['superuser']]);
|
return view('superuser/users', ['roleConfig' => $config['superuser']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validatePage()
|
public function validatePage()
|
||||||
{
|
{
|
||||||
$config = require APPPATH . 'Views/shared/dashboard_config.php';
|
$config = require APPPATH . 'Views/shared/config.php';
|
||||||
return view('superuser/validate', ['roleConfig' => $config['superuser']]);
|
return view('superuser/validate', ['roleConfig' => $config['superuser']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -63,13 +63,17 @@ class RequestsController extends BaseController
|
|||||||
public function unval($accessnumber)
|
public function unval($accessnumber)
|
||||||
{
|
{
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
// Securely get userid from session
|
|
||||||
$userid = session('userid');
|
$userid = session('userid');
|
||||||
$comment = $input['comment'];
|
$comment = $input['comment'];
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
$sql = "update GDC_CMOD.dbo.CM_REQUESTS set ISVAL1=null, VAL1USER=null, VAL1DATE=null, ISVAL2=null, VAL2USER=null, VAL2DATE=null,
|
$sql = "update GDC_CMOD.dbo.CM_REQUESTS set ISVAL1=null, VAL1USER=null, VAL1DATE=null, ISVAL2=null, VAL2USER=null, VAL2DATE=null,
|
||||||
ISPENDING=1, PENDINGTEXT='$comment', PENDINGUSER='$userid', PENDINGDATE=GETDATE() where ACCESSNUMBER='$accessnumber'";
|
ISPENDING=1, PENDINGTEXT='$comment', PENDINGUSER='$userid', PENDINGDATE=GETDATE() where ACCESSNUMBER='$accessnumber'";
|
||||||
$db->query($sql);
|
$db->query($sql);
|
||||||
|
|
||||||
|
$logAudit = "INSERT INTO GDC_CMOD.dbo.AUDIT_EVENTS (ACCESSNUMBER, EVENT_TYPE, USERID, EVENT_AT, REASON)
|
||||||
|
VALUES (?, 'UNVAL', ?, GETDATE(), ?)";
|
||||||
|
$db->query($logAudit, [$accessnumber, $userid, $comment]);
|
||||||
|
|
||||||
$data = ['status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber"];
|
$data = ['status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber"];
|
||||||
|
|
||||||
return $this->response->setJSON($data);
|
return $this->response->setJSON($data);
|
||||||
@ -86,6 +90,10 @@ class RequestsController extends BaseController
|
|||||||
if (!isset($result[0])) {
|
if (!isset($result[0])) {
|
||||||
$sql = "insert into GDC_CMOD.dbo.CM_REQUESTS(ACCESSNUMBER, ISVAL1, VAL1USER, VAL1DATE) VALUES ('$accessnumber', 1, '$userid', GETDATE())";
|
$sql = "insert into GDC_CMOD.dbo.CM_REQUESTS(ACCESSNUMBER, ISVAL1, VAL1USER, VAL1DATE) VALUES ('$accessnumber', 1, '$userid', GETDATE())";
|
||||||
$db->query($sql);
|
$db->query($sql);
|
||||||
|
|
||||||
|
$logAudit = "INSERT INTO GDC_CMOD.dbo.AUDIT_EVENTS (ACCESSNUMBER, EVENT_TYPE, USERID, EVENT_AT) VALUES (?, 'VAL1', ?, GETDATE())";
|
||||||
|
$db->query($logAudit, [$accessnumber, $userid]);
|
||||||
|
|
||||||
$data['val'] = 1;
|
$data['val'] = 1;
|
||||||
$data['userid'] = $userid;
|
$data['userid'] = $userid;
|
||||||
} else {
|
} else {
|
||||||
@ -99,6 +107,10 @@ class RequestsController extends BaseController
|
|||||||
} else {
|
} else {
|
||||||
if ($val1user != $userid) {
|
if ($val1user != $userid) {
|
||||||
$sql = "update GDC_CMOD.dbo.CM_REQUESTS set ISVAL2=1, VAL2USER='$userid', VAL2DATE=GETDATE() where ACCESSNUMBER='$accessnumber'";
|
$sql = "update GDC_CMOD.dbo.CM_REQUESTS set ISVAL2=1, VAL2USER='$userid', VAL2DATE=GETDATE() where ACCESSNUMBER='$accessnumber'";
|
||||||
|
|
||||||
|
$logAudit = "INSERT INTO GDC_CMOD.dbo.AUDIT_EVENTS (ACCESSNUMBER, EVENT_TYPE, USERID, EVENT_AT) VALUES (?, 'VAL2', ?, GETDATE())";
|
||||||
|
$db->query($logAudit, [$accessnumber, $userid]);
|
||||||
|
|
||||||
$data['val'] = 2;
|
$data['val'] = 2;
|
||||||
$data['userid'] = $userid;
|
$data['userid'] = $userid;
|
||||||
} else {
|
} else {
|
||||||
@ -108,6 +120,10 @@ class RequestsController extends BaseController
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$sql = "update GDC_CMOD.dbo.CM_REQUESTS set ISVAL1=1, VAL1USER='$userid', VAL1DATE=GETDATE() where ACCESSNUMBER='$accessnumber'";
|
$sql = "update GDC_CMOD.dbo.CM_REQUESTS set ISVAL1=1, VAL1USER='$userid', VAL1DATE=GETDATE() where ACCESSNUMBER='$accessnumber'";
|
||||||
|
|
||||||
|
$logAudit = "INSERT INTO GDC_CMOD.dbo.AUDIT_EVENTS (ACCESSNUMBER, EVENT_TYPE, USERID, EVENT_AT) VALUES (?, 'VAL1', ?, GETDATE())";
|
||||||
|
$db->query($logAudit, [$accessnumber, $userid]);
|
||||||
|
|
||||||
$data['val'] = 1;
|
$data['val'] = 1;
|
||||||
$data['userid'] = $userid;
|
$data['userid'] = $userid;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,20 +76,6 @@ class SamplesController extends BaseController
|
|||||||
return $this->respondCreated(['status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber-$samplenumber"], 201);
|
return $this->respondCreated(['status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber-$samplenumber"], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function uncollect($accessnumber)
|
|
||||||
{
|
|
||||||
$db = \Config\Database::connect();
|
|
||||||
$input = $this->request->getJSON(true);
|
|
||||||
$samplenumber = $input['samplenumber'];
|
|
||||||
$userid = session('userid');
|
|
||||||
$sql = "update GDC_CMOD.dbo.TUBES set USERID='$userid',STATUS='0', COLLECTIONDATE=getdate() where ACCESSNUMBER='$accessnumber' and TUBENUMBER='$samplenumber'";
|
|
||||||
$db->query($sql);
|
|
||||||
$sql = "INSERT INTO GDC_CMOD.dbo.AUDIT_TUBES(ACCESSNUMBER, TUBENUMBER, USERID, STATUS, LOGDATE)
|
|
||||||
VALUES ('$accessnumber', '$samplenumber', '$userid', '0', getdate())";
|
|
||||||
$db->query($sql);
|
|
||||||
return $this->respondCreated(['status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber-$samplenumber"], 201);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function unreceive($accessnumber)
|
public function unreceive($accessnumber)
|
||||||
{
|
{
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
|
|||||||
@ -72,19 +72,6 @@ class Sample extends BaseController {
|
|||||||
return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber-$samplenumber" ], 201);
|
return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber-$samplenumber" ], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function uncollect($accessnumber) {
|
|
||||||
$db = \Config\Database::connect();
|
|
||||||
$input = $this->request->getJSON(true);
|
|
||||||
$samplenumber = $input['samplenumber'];
|
|
||||||
$userid = $input['userid'];
|
|
||||||
$sql = "update GDC_CMOD.dbo.TUBES set USERID='$userid',STATUS='0', COLLECTIONDATE=getdate() where ACCESSNUMBER='$accessnumber' and TUBENUMBER='$samplenumber'";
|
|
||||||
$db->query($sql);
|
|
||||||
$sql = "INSERT INTO GDC_CMOD.dbo.AUDIT_TUBES(ACCESSNUMBER, TUBENUMBER, USERID, STATUS, LOGDATE)
|
|
||||||
VALUES ('$accessnumber', '$samplenumber', '$userid', '0', getdate())";
|
|
||||||
$db->query($sql);
|
|
||||||
return $this->respondCreated([ 'status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber-$samplenumber" ], 201);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function unreceive($accessnumber) {
|
public function unreceive($accessnumber) {
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
|
|||||||
@ -1,22 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
$config = include __DIR__ . '/../shared/dashboard_config.php';
|
$config = include __DIR__ . '/../shared/config.php';
|
||||||
$roleConfig = $config['admin'];
|
$roleConfig = $config['admin'];
|
||||||
?>
|
?>
|
||||||
<?= $this->extend('shared/layout_dashboard', ['roleConfig' => $roleConfig]); ?>
|
<?= $this->extend('shared/layout', ['roleConfig' => $roleConfig]); ?>
|
||||||
|
|
||||||
<?= $this->section('content'); ?>
|
<?= $this->section('content'); ?>
|
||||||
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
|
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
|
||||||
<?= $this->include('shared/dashboard_table', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/content_requests', ['config' => $roleConfig]); ?>
|
||||||
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
|
||||||
<?= $this->include('shared/dialog_unval'); ?>
|
<?= $this->include('shared/dialog_unval'); ?>
|
||||||
<?= $this->include('shared/dialog_preview'); ?>
|
<?= $this->include('shared/dialog_preview'); ?>
|
||||||
|
<?= $this->include('shared/dialog_audit'); ?>
|
||||||
</main>
|
</main>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
|
|
||||||
<?= $this->section('script'); ?>
|
<?= $this->section('script'); ?>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Alpine from '<?= base_url("js/app.js"); ?>';
|
import Alpine from '<?= base_url("js/app.js"); ?>';
|
||||||
<?= $this->include('shared/script_dashboard'); ?>
|
<?= $this->include('shared/script_requests'); ?>
|
||||||
Alpine.start();
|
Alpine.start();
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
@ -1,19 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
$config = include __DIR__ . '/../shared/dashboard_config.php';
|
$config = include __DIR__ . '/../shared/config.php';
|
||||||
$roleConfig = $config['admin'];
|
$roleConfig = $config['admin'];
|
||||||
?>
|
?>
|
||||||
<?= $this->extend('shared/layout_dashboard', ['roleConfig' => $roleConfig]); ?>
|
<?= $this->extend('shared/layout', ['roleConfig' => $roleConfig]); ?>
|
||||||
|
|
||||||
<?= $this->section('content'); ?>
|
<?= $this->section('content'); ?>
|
||||||
<main class="p-4 flex-1 flex flex-col gap-2" x-data="validatePage">
|
<main class="p-4 flex-1 flex flex-col gap-2" x-data="validatePage">
|
||||||
<?= $this->include('shared/dashboard_validate', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/content_validation', ['config' => $roleConfig]); ?>
|
||||||
</main>
|
</main>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
|
|
||||||
<?= $this->section('script'); ?>
|
<?= $this->section('script'); ?>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Alpine from '<?= base_url("js/app.js"); ?>';
|
import Alpine from '<?= base_url("js/app.js"); ?>';
|
||||||
<?= $this->include('shared/script_validate'); ?>
|
<?= $this->include('shared/script_validation'); ?>
|
||||||
Alpine.start();
|
Alpine.start();
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
@ -1,22 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
$config = include __DIR__ . '/../shared/dashboard_config.php';
|
$config = include __DIR__ . '/../shared/config.php';
|
||||||
$roleConfig = $config['cs'];
|
$roleConfig = $config['cs'];
|
||||||
?>
|
?>
|
||||||
<?= $this->extend('shared/layout_dashboard', ['roleConfig' => $roleConfig]); ?>
|
<?= $this->extend('shared/layout', ['roleConfig' => $roleConfig]); ?>
|
||||||
|
|
||||||
<?= $this->section('content'); ?>
|
<?= $this->section('content'); ?>
|
||||||
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
|
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
|
||||||
<?= $this->include('shared/dashboard_table', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/content_requests', ['config' => $roleConfig]); ?>
|
||||||
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
|
||||||
<?= $this->include('shared/dialog_unval'); ?>
|
<?= $this->include('shared/dialog_unval'); ?>
|
||||||
<?= $this->include('shared/dialog_preview'); ?>
|
<?= $this->include('shared/dialog_preview'); ?>
|
||||||
|
<?= $this->include('shared/dialog_audit'); ?>
|
||||||
</main>
|
</main>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
|
|
||||||
<?= $this->section('script'); ?>
|
<?= $this->section('script'); ?>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Alpine from '<?= base_url("js/app.js"); ?>';
|
import Alpine from '<?= base_url("js/app.js"); ?>';
|
||||||
<?= $this->include('shared/script_dashboard'); ?>
|
<?= $this->include('shared/script_requests'); ?>
|
||||||
Alpine.start();
|
Alpine.start();
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@ -10,6 +11,7 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -22,51 +24,71 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
animation: bounce 2s infinite;
|
animation: bounce 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes bounce {
|
@keyframes bounce {
|
||||||
0%, 100% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-20px); }
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
border: 8px solid rgba(255,255,255,0.3);
|
border: 8px solid rgba(255, 255, 255, 0.3);
|
||||||
border-top-color: white;
|
border-top-color: white;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
margin: 0 auto 2rem;
|
margin: 0 auto 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cat {
|
.cat {
|
||||||
font-size: 5rem;
|
font-size: 5rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fact {
|
.fact {
|
||||||
background: rgba(255,255,255,0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fact h3 {
|
.fact h3 {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 12px 30px;
|
padding: 12px 30px;
|
||||||
@ -78,16 +100,19 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover {
|
.btn:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
box-shadow: 0 5px 20px rgba(0,0,0,0.3);
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shrug {
|
.shrug {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="cat">👀</div>
|
<div class="cat">👀</div>
|
||||||
@ -101,11 +126,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shrug">🙹 🧀 🦽</div>
|
<div class="shrug">🙹 🧀 🦽</div>
|
||||||
|
|
||||||
<div class="buttons">
|
|
||||||
<a href="javascript:history.back()" class="btn">Go Back</a>
|
|
||||||
<a href="/gdc_cmod/" class="btn">Home</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -125,4 +145,5 @@
|
|||||||
document.getElementById('fact').textContent = facts[Math.floor(Math.random() * facts.length)];
|
document.getElementById('fact').textContent = facts[Math.floor(Math.random() * facts.length)];
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
@ -1,22 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
$config = include __DIR__ . '/../shared/dashboard_config.php';
|
$config = include __DIR__ . '/../shared/config.php';
|
||||||
$roleConfig = $config['lab'];
|
$roleConfig = $config['lab'];
|
||||||
?>
|
?>
|
||||||
<?= $this->extend('shared/layout_dashboard', ['roleConfig' => $roleConfig]); ?>
|
<?= $this->extend('shared/layout', ['roleConfig' => $roleConfig]); ?>
|
||||||
|
|
||||||
<?= $this->section('content'); ?>
|
<?= $this->section('content'); ?>
|
||||||
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
|
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
|
||||||
<?= $this->include('shared/dashboard_table', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/content_requests', ['config' => $roleConfig]); ?>
|
||||||
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
|
||||||
<?= $this->include('shared/dialog_unval'); ?>
|
<?= $this->include('shared/dialog_unval'); ?>
|
||||||
<?= $this->include('shared/dialog_preview'); ?>
|
<?= $this->include('shared/dialog_preview'); ?>
|
||||||
|
<?= $this->include('shared/dialog_audit'); ?>
|
||||||
</main>
|
</main>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
|
|
||||||
<?= $this->section('script'); ?>
|
<?= $this->section('script'); ?>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Alpine from '<?= base_url("js/app.js"); ?>';
|
import Alpine from '<?= base_url("js/app.js"); ?>';
|
||||||
<?= $this->include('shared/script_dashboard'); ?>
|
<?= $this->include('shared/script_requests'); ?>
|
||||||
Alpine.start();
|
Alpine.start();
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
@ -1,19 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
$config = include __DIR__ . '/../shared/dashboard_config.php';
|
$config = include __DIR__ . '/../shared/config.php';
|
||||||
$roleConfig = $config['lab'];
|
$roleConfig = $config['lab'];
|
||||||
?>
|
?>
|
||||||
<?= $this->extend('shared/layout_dashboard', ['roleConfig' => $roleConfig]); ?>
|
<?= $this->extend('shared/layout', ['roleConfig' => $roleConfig]); ?>
|
||||||
|
|
||||||
<?= $this->section('content'); ?>
|
<?= $this->section('content'); ?>
|
||||||
<main class="p-4 flex-1 flex flex-col gap-2" x-data="validatePage">
|
<main class="p-4 flex-1 flex flex-col gap-2" x-data="validatePage">
|
||||||
<?= $this->include('shared/dashboard_validate', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/content_validation', ['config' => $roleConfig]); ?>
|
||||||
</main>
|
</main>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
|
|
||||||
<?= $this->section('script'); ?>
|
<?= $this->section('script'); ?>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Alpine from '<?= base_url("js/app.js"); ?>';
|
import Alpine from '<?= base_url("js/app.js"); ?>';
|
||||||
<?= $this->include('shared/script_validate'); ?>
|
<?= $this->include('shared/script_validation'); ?>
|
||||||
Alpine.start();
|
Alpine.start();
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
@ -1,22 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
$config = include __DIR__ . '/../shared/dashboard_config.php';
|
$config = include __DIR__ . '/../shared/config.php';
|
||||||
$roleConfig = $config['phlebo'];
|
$roleConfig = $config['phlebo'];
|
||||||
?>
|
?>
|
||||||
<?= $this->extend('shared/layout_dashboard', ['roleConfig' => $roleConfig]); ?>
|
<?= $this->extend('shared/layout', ['roleConfig' => $roleConfig]); ?>
|
||||||
|
|
||||||
<?= $this->section('content'); ?>
|
<?= $this->section('content'); ?>
|
||||||
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
|
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
|
||||||
<?= $this->include('shared/dashboard_table', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/content_requests', ['config' => $roleConfig]); ?>
|
||||||
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
|
||||||
<?= $this->include('shared/dialog_unval'); ?>
|
<?= $this->include('shared/dialog_unval'); ?>
|
||||||
<?= $this->include('shared/dialog_preview'); ?>
|
<?= $this->include('shared/dialog_preview'); ?>
|
||||||
|
<?= $this->include('shared/dialog_audit'); ?>
|
||||||
</main>
|
</main>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
|
|
||||||
<?= $this->section('script'); ?>
|
<?= $this->section('script'); ?>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Alpine from '<?= base_url("js/app.js"); ?>';
|
import Alpine from '<?= base_url("js/app.js"); ?>';
|
||||||
<?= $this->include('shared/script_dashboard'); ?>
|
<?= $this->include('shared/script_requests'); ?>
|
||||||
Alpine.start();
|
Alpine.start();
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
@ -189,6 +189,7 @@
|
|||||||
<th style='width:3%;'>ResTo</th>
|
<th style='width:3%;'>ResTo</th>
|
||||||
<th style='width:5%;'>Val</th>
|
<th style='width:5%;'>Val</th>
|
||||||
<th style='width:5%;'>Result</th>
|
<th style='width:5%;'>Result</th>
|
||||||
|
<th style='width:3%;'></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -212,12 +213,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<template x-if="req.ISVAL == 1 && req.ISPENDING != 1">
|
<template x-if="req.ISVAL == 1 && req.ISPENDING != 1">
|
||||||
<div class='text-center'>
|
<div class='text-center'>
|
||||||
|
<?php if (session()->get('userlevel') <= 1): ?>
|
||||||
<template
|
<template
|
||||||
x-if="req.VAL1USER == '<?= session('userid'); ?>' || req.VAL2USER == '<?= session('userid'); ?>'">
|
x-if="req.VAL1USER == '<?= session('userid'); ?>' || req.VAL2USER == '<?= session('userid'); ?>'">
|
||||||
<button class="btn btn-xs btn-outline btn-secondary"
|
<button class="btn btn-xs btn-outline btn-secondary"
|
||||||
@click="openUnvalDialog(req.SP_ACCESSNUMBER)"><i
|
@click="openUnvalDialog(req.SP_ACCESSNUMBER)">
|
||||||
class="fa-solid fa-rotate-right"></i></button>
|
<span class="text-error font-bold">UnVal</span>
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -228,6 +232,13 @@
|
|||||||
@click="openPreviewDialog(req.SP_ACCESSNUMBER, 'preview', req)">Preview</button>
|
@click="openPreviewDialog(req.SP_ACCESSNUMBER, 'preview', req)">Preview</button>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-xs btn-ghost"
|
||||||
|
@click="openAuditDialog(req.SP_ACCESSNUMBER)"
|
||||||
|
title="View Audit Trail">
|
||||||
|
<i class="fa fa-history text-info"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -2,15 +2,18 @@
|
|||||||
<div class="card-body p-0 h-full flex flex-col">
|
<div class="card-body p-0 h-full flex flex-col">
|
||||||
|
|
||||||
<!-- Header & Filters -->
|
<!-- Header & Filters -->
|
||||||
<div class="p-4 border-b border-base-200 bg-base-50">
|
<div class="p-4 border-b border-base-200 bg-gradient-to-r from-primary/10 to-base-100">
|
||||||
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-4">
|
<div class="flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
<div class="flex-1">
|
<h2 class="text-2xl font-bold flex items-center gap-3">
|
||||||
<h2 class="text-2xl font-bold flex items-center gap-2 text-base-content">
|
<span class="w-10 h-10 rounded-lg bg-primary text-primary-content flex items-center justify-center shadow-lg">
|
||||||
<i class="fa fa-check-circle text-primary"></i> Pending Validation
|
<i class="fa fa-clipboard-check"></i>
|
||||||
</h2>
|
</span>
|
||||||
</div>
|
Pending Validation
|
||||||
<div class="badge badge-lg badge-primary">
|
</h2>
|
||||||
<span x-text="unvalidatedCount"></span> requests
|
<div class="badge badge-lg badge-primary gap-2 px-4 py-3 shadow-lg">
|
||||||
|
<i class="fa fa-layer-group animate-pulse"></i>
|
||||||
|
<span class="font-bold text-lg" x-text="unvalidatedCount"></span>
|
||||||
|
<span class="font-medium">requests</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -51,36 +54,30 @@
|
|||||||
<table class="table table-xs table-zebra w-full">
|
<table class="table table-xs table-zebra w-full">
|
||||||
<thead class="bg-base-100 sticky top-0 z-10">
|
<thead class="bg-base-100 sticky top-0 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 12%;">
|
<th style="width: 18%;">
|
||||||
<div class="skeleton h-4 w-24"></div>
|
<div class="skeleton h-4 w-24"></div>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 18%;">
|
<th style="width: 25%;">
|
||||||
<div class="skeleton h-4 w-32"></div>
|
<div class="skeleton h-4 w-32"></div>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 10%;">
|
<th style="width: 12%;">
|
||||||
<div class="skeleton h-4 w-20"></div>
|
<div class="skeleton h-4 w-20"></div>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 12%;">
|
<th style="width: 12%;">
|
||||||
<div class="skeleton h-4 w-24"></div>
|
<div class="skeleton h-4 w-24"></div>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 10%;">
|
<th style="width: 13%;">
|
||||||
<div class="skeleton h-4 w-20"></div>
|
<div class="skeleton h-4 w-20"></div>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 10%;">
|
<th style="width: 20%;">
|
||||||
<div class="skeleton h-4 w-20"></div>
|
|
||||||
</th>
|
|
||||||
<th style="width: 18%;">
|
|
||||||
<div class="skeleton h-4 w-full"></div>
|
<div class="skeleton h-4 w-full"></div>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 10%;">
|
|
||||||
<div class="skeleton h-4 w-16"></div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template x-for="i in 5" :key="i">
|
<template x-for="i in 5" :key="i">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8">
|
<td colspan="6">
|
||||||
<div class="skeleton h-4 w-full"></div>
|
<div class="skeleton h-4 w-full"></div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -100,7 +97,7 @@
|
|||||||
<table class="table table-xs table-zebra w-full">
|
<table class="table table-xs table-zebra w-full">
|
||||||
<thead class="bg-base-100 sticky top-0 z-10">
|
<thead class="bg-base-100 sticky top-0 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 12%;" @click="sort('REQDATE')"
|
<th style="width: 18%;" @click="sort('REQDATE')"
|
||||||
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
|
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
Order Datetime
|
Order Datetime
|
||||||
@ -108,7 +105,7 @@
|
|||||||
:class="sortCol === 'REQDATE' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
:class="sortCol === 'REQDATE' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 18%;" @click="sort('Name')"
|
<th style="width: 25%;" @click="sort('Name')"
|
||||||
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
|
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
Patient Name
|
Patient Name
|
||||||
@ -116,7 +113,7 @@
|
|||||||
:class="sortCol === 'Name' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
:class="sortCol === 'Name' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 10%;" @click="sort('SP_ACCESSNUMBER')"
|
<th style="width: 12%;" @click="sort('SP_ACCESSNUMBER')"
|
||||||
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
|
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
No Lab
|
No Lab
|
||||||
@ -132,7 +129,7 @@
|
|||||||
:class="sortCol === 'HOSTORDERNUMBER' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
:class="sortCol === 'HOSTORDERNUMBER' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 10%;" @click="sort('REFF')"
|
<th style="width: 13%;" @click="sort('REFF')"
|
||||||
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
|
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
Reff
|
Reff
|
||||||
@ -140,7 +137,7 @@
|
|||||||
:class="sortCol === 'REFF' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
:class="sortCol === 'REFF' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 10%;" @click="sort('DOC')"
|
<th style="width: 20%;" @click="sort('DOC')"
|
||||||
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
|
class="cursor-pointer hover:bg-base-200 transition-colors select-none">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
Doctor
|
Doctor
|
||||||
@ -148,8 +145,6 @@
|
|||||||
:class="sortCol === 'DOC' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
:class="sortCol === 'DOC' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 18%;">Tests</th>
|
|
||||||
<th style="width: 10%;">ResTo</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -165,8 +160,6 @@
|
|||||||
<td x-text="req.HOSTORDERNUMBER" class="font-bold"></td>
|
<td x-text="req.HOSTORDERNUMBER" class="font-bold"></td>
|
||||||
<td x-text="req.REFF"></td>
|
<td x-text="req.REFF"></td>
|
||||||
<td x-text="req.DOC"></td>
|
<td x-text="req.DOC"></td>
|
||||||
<td x-text="req.TESTNAMES || req.TESTS"></td>
|
|
||||||
<td x-text="req.ODR_CRESULT_TO"></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
276
app/Views/shared/content_validation_new.php
Normal file
276
app/Views/shared/content_validation_new.php
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
<div class="card bg-base-100 shadow-xl h-full border border-base-200 overflow-hidden">
|
||||||
|
<div class="card-body p-0 h-full flex flex-col">
|
||||||
|
|
||||||
|
<!-- Header & Statistics -->
|
||||||
|
<div class="p-4 border-b border-base-200 bg-gradient-to-r from-blue-50 to-indigo-50">
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h2 class="text-2xl font-bold flex items-center gap-2 text-primary">
|
||||||
|
<i class="fa fa-shield-alt"></i> Pending Validation
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2 text-xs">
|
||||||
|
<div class="badge badge-lg badge-outline gap-1 bg-base-100">
|
||||||
|
<span class="inline-flex items-center justify-center w-2 h-2 rounded-full bg-base-300"></span>
|
||||||
|
<span class="text-base-content/70">Not Started</span>
|
||||||
|
<span class="badge badge-sm badge-ghost ml-1" x-text="valStats.notStarted"></span>
|
||||||
|
</div>
|
||||||
|
<div class="badge badge-lg badge-primary gap-1 bg-base-100 border-primary">
|
||||||
|
<span class="inline-flex items-center justify-center w-2 h-2 rounded-full bg-primary"></span>
|
||||||
|
<span class="text-primary font-medium">1st Val</span>
|
||||||
|
<span class="badge badge-sm badge-primary ml-1" x-text="valStats.firstVal"></span>
|
||||||
|
</div>
|
||||||
|
<div class="badge badge-lg badge-success gap-1 bg-base-100 border-success">
|
||||||
|
<span class="inline-flex items-center justify-center w-2 h-2 rounded-full bg-success"></span>
|
||||||
|
<span class="text-success font-medium">Done</span>
|
||||||
|
<span class="badge badge-sm badge-success ml-1" x-text="valStats.fullyValidated"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<div class="bg-base-100 rounded-lg p-3 border border-base-200 shadow-sm">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<span class="text-xs font-medium text-base-content/70">Overall Progress</span>
|
||||||
|
<span class="text-xs font-bold text-primary" x-text="valStats.progress + '%'"></span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-base-200 rounded-full h-2 overflow-hidden">
|
||||||
|
<div class="h-full rounded-full transition-all duration-500 flex"
|
||||||
|
:style="'width: ' + valStats.progress + '%'">
|
||||||
|
<div class="h-full bg-primary flex-1 first-val-progress"></div>
|
||||||
|
<div class="h-full bg-success flex-1 second-val-progress"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mt-1 text-xs text-base-content/60">
|
||||||
|
<span x-text="valStats.fullyValidated + ' fully validated'"></span>
|
||||||
|
<span x-text="valStats.firstVal + ' need 2nd validation'"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Filter -->
|
||||||
|
<div class="flex flex-col md:flex-row gap-3 items-end mt-4 bg-base-100 p-3 rounded-lg border border-base-200 shadow-sm">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label text-xs font-bold py-1 text-base-content/60">Date Range</label>
|
||||||
|
<div class="join">
|
||||||
|
<input type="date" class="input input-sm input-bordered join-item" x-model="filter.date1" />
|
||||||
|
<span class="join-item btn btn-sm btn-ghost no-animation bg-base-200 font-normal px-2">-</span>
|
||||||
|
<input type="date" class="input input-sm input-bordered join-item" x-model="filter.date2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button class="btn btn-sm btn-primary" @click="fetchUnvalidated()">
|
||||||
|
<i class="fa fa-search"></i> Search
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-neutral" @click="resetUnvalidated()">
|
||||||
|
<i class="fa fa-sync-alt"></i> Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="flex-1"></span>
|
||||||
|
|
||||||
|
<div class="form-control w-full md:w-auto">
|
||||||
|
<label class="input input-sm input-bordered">
|
||||||
|
<i class="fa fa-filter"></i>
|
||||||
|
<input type="text" placeholder="Type to filter..." x-model="filterTable" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table Section -->
|
||||||
|
<div class="flex-1 overflow-y-auto px-4 pb-4">
|
||||||
|
<!-- Legend -->
|
||||||
|
<div class="flex items-center gap-4 mb-2 text-xs text-base-content/60">
|
||||||
|
<span class="font-medium">Legend:</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="inline-flex items-center justify-center w-3 h-3 rounded-full bg-base-300"></span>
|
||||||
|
<span>Pending</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="inline-flex items-center justify-center w-3 h-3 rounded-full bg-primary"></span>
|
||||||
|
<span>1st Val</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="inline-flex items-center justify-center w-3 h-3 rounded-full bg-success"></span>
|
||||||
|
<span>Done</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="isLoading">
|
||||||
|
<table class="table table-xs table-zebra w-full">
|
||||||
|
<thead class="bg-base-100 sticky top-0 z-10">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 15%;">
|
||||||
|
<div class="skeleton h-4 w-28"></div>
|
||||||
|
</th>
|
||||||
|
<th style="width: 12%;">
|
||||||
|
<div class="skeleton h-4 w-24"></div>
|
||||||
|
</th>
|
||||||
|
<th style="width: 10%;">
|
||||||
|
<div class="skeleton h-4 w-20"></div>
|
||||||
|
</th>
|
||||||
|
<th style="width: 10%;">
|
||||||
|
<div class="skeleton h-4 w-20"></div>
|
||||||
|
</th>
|
||||||
|
<th style="width: 10%;">
|
||||||
|
<div class="skeleton h-4 w-20"></div>
|
||||||
|
</th>
|
||||||
|
<th style="width: 10%;">
|
||||||
|
<div class="skeleton h-4 w-20"></div>
|
||||||
|
</th>
|
||||||
|
<th style="width: 13%;">
|
||||||
|
<div class="skeleton h-4 w-24"></div>
|
||||||
|
</th>
|
||||||
|
<th style="width: 8%;">
|
||||||
|
<div class="skeleton h-4 w-16"></div>
|
||||||
|
</th>
|
||||||
|
<th style="width: 12%;">
|
||||||
|
<div class="skeleton h-4 w-full"></div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="i in 5" :key="i">
|
||||||
|
<tr>
|
||||||
|
<td colspan="9">
|
||||||
|
<div class="skeleton h-4 w-full"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="!isLoading && !unvalidatedList.length">
|
||||||
|
<div class="flex flex-col items-center justify-center py-16">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="w-24 h-24 rounded-full bg-success/10 flex items-center justify-center animate-pulse">
|
||||||
|
<i class="fa fa-check-circle text-6xl text-success"></i>
|
||||||
|
</div>
|
||||||
|
<div class="absolute -bottom-2 -right-2 w-8 h-8 bg-success rounded-full flex items-center justify-center text-white text-sm font-bold">
|
||||||
|
<i class="fa fa-check"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-bold text-success mt-4">All Caught Up!</h3>
|
||||||
|
<p class="text-base-content/60 mt-1">No pending validations for this date range</p>
|
||||||
|
<div class="flex gap-4 mt-4 text-sm text-base-content/70">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-2xl font-bold text-success" x-text="valStats.fullyValidated"></div>
|
||||||
|
<div>Fully Validated</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="!isLoading && unvalidatedList.length">
|
||||||
|
<table class="table table-xs table-zebra w-full">
|
||||||
|
<thead class="bg-base-100 sticky top-0 z-10 shadow-sm">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 15%;" @click="sort('Name')"
|
||||||
|
class="cursor-pointer hover:bg-blue-100 transition-colors select-none">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
Patient
|
||||||
|
<i class="fa text-xs"
|
||||||
|
:class="sortCol === 'Name' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th style="width: 12%;" @click="sort('SP_ACCESSNUMBER')"
|
||||||
|
class="cursor-pointer hover:bg-blue-100 transition-colors select-none">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
Lab No
|
||||||
|
<i class="fa text-xs"
|
||||||
|
:class="sortCol === 'SP_ACCESSNUMBER' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th style="width: 10%;" @click="sort('HOSTORDERNUMBER')"
|
||||||
|
class="cursor-pointer hover:bg-blue-100 transition-colors select-none">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
Reg No
|
||||||
|
<i class="fa text-xs"
|
||||||
|
:class="sortCol === 'HOSTORDERNUMBER' ? (sortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort opacity-20'"></i>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th style="width: 10%;">Reff</th>
|
||||||
|
<th style="width: 10%;">Doctor</th>
|
||||||
|
<th style="width: 10%;">ResTo</th>
|
||||||
|
<th style="width: 13%;">Status</th>
|
||||||
|
<th style="width: 8%;">Action</th>
|
||||||
|
<th style="width: 12%;">Tests</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="req in unvalidatedPaginated" :key="req.SP_ACCESSNUMBER">
|
||||||
|
<tr class="hover:bg-blue-50 cursor-pointer transition-colors"
|
||||||
|
@click="openValDialog(req.SP_ACCESSNUMBER)"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown.enter="openValDialog(req.SP_ACCESSNUMBER)"
|
||||||
|
@keydown.escape="closeValDialog()">
|
||||||
|
<td>
|
||||||
|
<div class="font-medium" x-text="req.Name"></div>
|
||||||
|
<div class="text-xs opacity-60" x-text="req.PATNUMBER?.substring(14) || req.PATNUMBER"></div>
|
||||||
|
</td>
|
||||||
|
<td x-text="req.SP_ACCESSNUMBER" class="font-bold font-mono text-xs"></td>
|
||||||
|
<td x-text="req.HOSTORDERNUMBER" class="font-bold font-mono text-xs"></td>
|
||||||
|
<td x-text="req.REFF" class="text-xs"></td>
|
||||||
|
<td x-text="req.DOC" class="text-xs truncate max-w-[80px]" :title="req.DOC"></td>
|
||||||
|
<td x-text="req.ODR_CRESULT_TO" class="text-xs"></td>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="flex gap-0.5" :title="getValTooltip(req)">
|
||||||
|
<span class="w-3 h-3 rounded-full flex items-center justify-center text-[8px]"
|
||||||
|
:class="req.ISVAL1 == 1 ? 'bg-primary text-white' : 'bg-base-300 text-transparent'">
|
||||||
|
<i class="fa fa-check"></i>
|
||||||
|
</span>
|
||||||
|
<span class="w-3 h-3 rounded-full flex items-center justify-center text-[8px]"
|
||||||
|
:class="req.ISVAL2 == 1 ? 'bg-success text-white' : 'bg-base-300 text-transparent'">
|
||||||
|
<i class="fa fa-check"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs ml-1" :class="getValStatusClass(req)">
|
||||||
|
<span x-text="getValStatusText(req)"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] opacity-60 mt-0.5" x-show="req.VAL1USER || req.VAL2USER">
|
||||||
|
<span x-show="req.VAL1USER">1: <span x-text="req.VAL1USER"></span></span>
|
||||||
|
<span x-show="req.VAL2USER" class="ml-2">2: <span x-text="req.VAL2USER"></span></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-xs btn-primary btn-outline" @click.stop="openValDialog(req.SP_ACCESSNUMBER)">
|
||||||
|
<i class="fa fa-check"></i> <span x-text="req.ISVAL1 == 1 ? '2nd' : '1st'"></span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td class="max-w-[100px] truncate text-xs" :title="req.TESTNAMES || req.TESTS" x-text="req.TESTNAMES || req.TESTS"></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Control -->
|
||||||
|
<div class="p-2 border-t border-base-200 bg-base-50 flex flex-col sm:flex-row justify-between items-center gap-2">
|
||||||
|
<div class="text-xs text-base-content/80">
|
||||||
|
Showing <span class="font-bold" x-text="((currentPage - 1) * pageSize) + 1"></span> to
|
||||||
|
<span class="font-bold" x-text="Math.min(currentPage * pageSize, unvalidatedFiltered.length)"></span> of
|
||||||
|
<span class="font-bold" x-text="unvalidatedFiltered.length"></span> entries
|
||||||
|
</div>
|
||||||
|
<div class="join">
|
||||||
|
<button class="join-item btn btn-sm" @click="prevPage()" :disabled="currentPage === 1">
|
||||||
|
<i class="fa fa-chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
<button class="join-item btn btn-sm no-animation bg-base-100 cursor-default">
|
||||||
|
Page <span x-text="currentPage"></span> / <span x-text="unvalidatedTotalPages"></span>
|
||||||
|
</button>
|
||||||
|
<button class="join-item btn btn-sm" @click="nextPage()" :disabled="currentPage === unvalidatedTotalPages">
|
||||||
|
<i class="fa fa-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Validate Dialog -->
|
||||||
|
<?= $this->include('shared/dialog_val'); ?>
|
||||||
89
app/Views/shared/dialog_audit.php
Normal file
89
app/Views/shared/dialog_audit.php
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<dialog class="modal" :open="isDialogAuditOpen">
|
||||||
|
<template x-if="auditAccessnumber">
|
||||||
|
<div class="modal-box w-11/12 max-w-4xl h-[80vh] flex flex-col p-0 overflow-hidden bg-base-100">
|
||||||
|
<div class="flex justify-between items-center p-3 bg-base-200 border-b border-base-300">
|
||||||
|
<h3 class="font-bold text-lg flex items-center gap-2">
|
||||||
|
<i class="fa fa-history text-primary"></i>
|
||||||
|
Audit Trail
|
||||||
|
<span class="badge badge-ghost text-xs" x-text="auditAccessnumber"></span>
|
||||||
|
</h3>
|
||||||
|
<button class="btn btn-sm btn-circle btn-ghost" @click="closeAuditDialog()">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto p-4 bg-base-100">
|
||||||
|
<div class="flex gap-2 mb-4">
|
||||||
|
<button @click="auditTab = 'all'"
|
||||||
|
:class="auditTab === 'all' ? 'btn-active btn-primary text-white' : 'btn-ghost'"
|
||||||
|
class="btn btn-sm join-item">All</button>
|
||||||
|
<button @click="auditTab = 'validation'"
|
||||||
|
:class="auditTab === 'validation' ? 'btn-active btn-primary text-white' : 'btn-ghost'"
|
||||||
|
class="btn btn-sm join-item">Validation</button>
|
||||||
|
<button @click="auditTab = 'sample'"
|
||||||
|
:class="auditTab === 'sample' ? 'btn-active btn-primary text-white' : 'btn-ghost'"
|
||||||
|
class="btn btn-sm join-item">Sample</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<template x-if="!auditData">
|
||||||
|
<div class="text-center py-10">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
<p class="mt-2">Loading audit data...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="auditData && getAllAuditEvents.length === 0">
|
||||||
|
<div class="text-center py-10 text-base-content/50">
|
||||||
|
<i class="fa fa-inbox text-4xl mb-2"></i>
|
||||||
|
<p>No audit events found</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="auditData && getAllAuditEvents.length > 0">
|
||||||
|
<div class="relative border-l-2 border-base-300 ml-3 space-y-4">
|
||||||
|
<template x-for="event in getFilteredAuditEvents" :key="event.id">
|
||||||
|
<div class="ml-6 relative">
|
||||||
|
<div class="absolute -left-9 w-6 h-6 rounded-full flex items-center justify-center"
|
||||||
|
:class="{
|
||||||
|
'bg-success': event.category === 'validation' && event.type !== 'UNVAL',
|
||||||
|
'bg-info': event.category === 'sample',
|
||||||
|
'bg-error': event.category === 'validation' && event.type === 'UNVAL'
|
||||||
|
}">
|
||||||
|
<i class="fa text-xs text-white"
|
||||||
|
:class="{
|
||||||
|
'fa-check': event.category === 'validation' && event.type !== 'UNVAL',
|
||||||
|
'fa-vial': event.category === 'sample',
|
||||||
|
'fa-times': event.category === 'validation' && event.type === 'UNVAL'
|
||||||
|
}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="bg-base-200 rounded-lg p-3 shadow-sm">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<span class="badge badge-sm mb-1"
|
||||||
|
:class="{
|
||||||
|
'badge-success': event.category === 'validation' && event.type !== 'UNVAL',
|
||||||
|
'badge-info': event.category === 'sample',
|
||||||
|
'badge-error': event.category === 'validation' && event.type === 'UNVAL'
|
||||||
|
}"
|
||||||
|
x-text="event.type"></span>
|
||||||
|
<p class="font-medium text-sm" x-text="event.description"></p>
|
||||||
|
<template x-if="event.reason">
|
||||||
|
<p class="text-xs text-error mt-1" x-text="'Reason: ' + event.reason"></p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="text-right text-xs text-base-content/60">
|
||||||
|
<p x-text="event.datetime"></p>
|
||||||
|
<p x-text="event.user"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</dialog>
|
||||||
@ -1,6 +1,15 @@
|
|||||||
<dialog class="modal" :open="isDialogSampleOpen">
|
<dialog class="modal" :open="isDialogSampleOpen">
|
||||||
<div class="modal-box w-2/3 max-w-5xl">
|
<div class="modal-box w-2/3 max-w-5xl">
|
||||||
<p class='text-right'><button class="btn btn-xs btn-neutral" @click="closeSampleDialog()">X</button></p>
|
<div class='flex justify-between items-center mb-2'>
|
||||||
|
<div class='flex gap-2 ml-auto'>
|
||||||
|
<template x-if="item.accessnumber">
|
||||||
|
<button class="btn btn-xs btn-outline btn-info" @click="openAuditDialog(item.accessnumber)">
|
||||||
|
<i class="fa fa-history"></i> Audit
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<button class="btn btn-xs btn-neutral" @click="closeSampleDialog()">X</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template x-if="isSampleLoading">
|
<template x-if="isSampleLoading">
|
||||||
<div class="text-center py-10">
|
<div class="text-center py-10">
|
||||||
@ -11,90 +20,99 @@
|
|||||||
|
|
||||||
<template x-if="!isSampleLoading">
|
<template x-if="!isSampleLoading">
|
||||||
<div>
|
<div>
|
||||||
<table class="table table-xs table-compact w-full mb-4">
|
<table class="table table-xs table-compact w-full mb-4">
|
||||||
<tr>
|
|
||||||
<td>MR# </td> <td x-text="': '+item.patnumber"></td>
|
|
||||||
<td>Patient Name </td> <td x-text="': '+item.patname"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>KTP# </td> <td x-text="': '+item.ktp"></td>
|
|
||||||
<td>Sex / Age </td> <td x-text="': '+item.placeofbirth+' '+item.gender+' / '+item.age"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Note</td>
|
|
||||||
<td colspan='3'>
|
|
||||||
<textarea x-text="item.comment" class="textarea textarea-bordered w-full"
|
|
||||||
<?= ($config['sampleDialog']['commentEditable'] ?? true) ? '' : 'disabled' ?>></textarea>
|
|
||||||
<?php if ($config['sampleDialog']['commentEditable'] ?? true): ?>
|
|
||||||
<button class="btn btn-sm btn-primary mt-2" @click="saveComment(item.accessnumber)">Save</button>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table class="table table-xs table-compact w-full">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>Sample Code</th>
|
<td>MR# </td>
|
||||||
<th>Sample Name</th>
|
<td x-text="': '+item.patnumber"></td>
|
||||||
<th class='text-center'>Collected</th>
|
<td>Patient Name </td>
|
||||||
<th class='text-center'>Received</th>
|
<td x-text="': '+item.patname"></td>
|
||||||
<th>Action</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td></td> <td>All</td> <td></td> <td></td>
|
<td>KTP# </td>
|
||||||
<td>
|
<td x-text="': '+item.ktp"></td>
|
||||||
<button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button>
|
<td>Sex / Age </td>
|
||||||
<?php if ($config['sampleDialog']['showCollectButtons'] ?? true): ?>
|
<td x-text="': '+item.placeofbirth+' '+item.gender+' / '+item.age"></td>
|
||||||
<button class="btn btn-sm btn-success px-2 py-1" onclick=""><h6 class="p-0 m-0">Coll.</h6></button>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Note</td>
|
||||||
|
<td colspan='3'>
|
||||||
|
<textarea x-text="item.comment" class="textarea textarea-bordered w-full"
|
||||||
|
<?= ($config['sampleDialog']['commentEditable'] ?? true) ? '' : 'disabled' ?>></textarea>
|
||||||
|
<?php if ($config['sampleDialog']['commentEditable'] ?? true): ?>
|
||||||
|
<button class="btn btn-sm btn-primary mt-2" @click="saveComment(item.accessnumber)">Save</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
</table>
|
||||||
<td></td> <td>Collection</td> <td></td> <td></td>
|
|
||||||
<td><button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<template x-for="sample in item.samples">
|
<table class="table table-xs table-compact w-full">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td x-text="sample.sampcode"></td>
|
<th>Sample Code</th>
|
||||||
<td x-text="sample.name"></td>
|
<th>Sample Name</th>
|
||||||
<td class='text-center'>
|
<th class='text-center'>Collected</th>
|
||||||
<input type="checkbox" class="checkbox" x-bind:checked="sample.colstatus == 1" disabled>
|
<th class='text-center'>Received</th>
|
||||||
</td>
|
<th>Action</th>
|
||||||
<td class='text-center'>
|
<th></th>
|
||||||
<input type="checkbox" class="checkbox" x-bind:checked="sample.tubestatus != 0" disabled>
|
</tr>
|
||||||
</td>
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>All</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button>
|
<button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button>
|
||||||
<?php if ($config['sampleDialog']['showCollectButtons'] ?? true): ?>
|
<?php if ($config['sampleDialog']['showCollectButtons'] ?? true): ?>
|
||||||
<template x-if="sample.colstatus == 0">
|
<button class="btn btn-sm btn-success px-2 py-1" onclick="">
|
||||||
<button class="btn btn-sm btn-success px-2 py-1" @click="collect(sample.sampcode, item.accessnumber)">
|
|
||||||
<h6 class="p-0 m-0">Coll.</h6>
|
<h6 class="p-0 m-0">Coll.</h6>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
|
||||||
<template x-if="sample.colstatus == 1">
|
|
||||||
<button class="btn btn-sm btn-error px-2 py-1" @click="uncollect(sample.sampcode, item.accessnumber)">
|
|
||||||
<h6 class="p-0 m-0">Un-Coll.</h6>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<template x-if="sample.tubestatus != 0">
|
|
||||||
<button class="btn btn-sm btn-error px-2 py-1" @click="unreceive(sample.sampcode, item.accessnumber)">
|
|
||||||
<h6 class="p-0 m-0">Un-Recv.</h6>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
<tr>
|
||||||
</tbody>
|
<td></td>
|
||||||
</table>
|
<td>Collection</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td><button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<template x-for="sample in item.samples">
|
||||||
|
<tr>
|
||||||
|
<td x-text="sample.sampcode"></td>
|
||||||
|
<td x-text="sample.name"></td>
|
||||||
|
<td class='text-center'>
|
||||||
|
<input type="checkbox" class="checkbox" x-bind:checked="sample.colstatus == 1" disabled>
|
||||||
|
</td>
|
||||||
|
<td class='text-center'>
|
||||||
|
<input type="checkbox" class="checkbox" x-bind:checked="sample.tubestatus != 0" disabled>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-secondary px-2 py-1"><i class="fa-solid fa-print"></i></button>
|
||||||
|
<?php if ($config['sampleDialog']['showCollectButtons'] ?? true): ?>
|
||||||
|
<template x-if="sample.colstatus == 0">
|
||||||
|
<button class="btn btn-sm btn-success px-2 py-1"
|
||||||
|
@click="collect(sample.sampcode, item.accessnumber)">
|
||||||
|
<h6 class="p-0 m-0">Coll.</h6>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="sample.tubestatus != 0">
|
||||||
|
<button class="btn btn-sm btn-error px-2 py-1"
|
||||||
|
@click="unreceive(sample.sampcode, item.accessnumber)">
|
||||||
|
<h6 class="p-0 m-0">Un-Recv.</h6>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
@ -9,11 +9,13 @@
|
|||||||
x-model="unvalReason" placeholder="Enter reason for unvalidation..."></textarea>
|
x-model="unvalReason" placeholder="Enter reason for unvalidation..."></textarea>
|
||||||
<p class='text-right mt-3'>
|
<p class='text-right mt-3'>
|
||||||
<button class="btn btn-sm btn-ghost" @click="closeUnvalDialog()">Cancel</button>
|
<button class="btn btn-sm btn-ghost" @click="closeUnvalDialog()">Cancel</button>
|
||||||
|
<?php if (session()->get('userlevel') <= 1): ?>
|
||||||
<button id="unvalidate-btn" class="btn btn-sm btn-warning"
|
<button id="unvalidate-btn" class="btn btn-sm btn-warning"
|
||||||
@click="unvalidate(unvalAccessnumber, '<?=session('userid');?>')"
|
@click="unvalidate(unvalAccessnumber, '<?=session('userid');?>')"
|
||||||
:disabled="!unvalReason.trim()">
|
:disabled="!unvalReason.trim()">
|
||||||
<i class="fa fa-undo mr-1"></i>Unvalidate
|
<span class="text-error font-bold">UnVal</span>
|
||||||
</button>
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -193,17 +193,6 @@ document.addEventListener('alpine:init', () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
uncollect(sampcode, accessnumber) {
|
|
||||||
if (!confirm(`Uncollect sample ${sampcode} from request ${accessnumber}?`)) { return; }
|
|
||||||
fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, {
|
|
||||||
method: 'DELETE', headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ samplenumber: sampcode, userid: '<?= session('userid'); ?>' })
|
|
||||||
})
|
|
||||||
.then(res => res.json()).then(data => {
|
|
||||||
this.fetchItem(accessnumber);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
unreceive(sampcode, accessnumber) {
|
unreceive(sampcode, accessnumber) {
|
||||||
if (!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return; }
|
if (!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return; }
|
||||||
fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, {
|
fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, {
|
||||||
@ -283,5 +272,76 @@ document.addEventListener('alpine:init', () => {
|
|||||||
closeUnvalDialog() {
|
closeUnvalDialog() {
|
||||||
this.isDialogUnvalOpen = false;
|
this.isDialogUnvalOpen = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
audit dialog
|
||||||
|
*/
|
||||||
|
isDialogAuditOpen: false,
|
||||||
|
auditData: null,
|
||||||
|
auditAccessnumber: null,
|
||||||
|
auditTab: 'all',
|
||||||
|
openAuditDialog(accessnumber) {
|
||||||
|
this.auditAccessnumber = accessnumber;
|
||||||
|
this.auditData = null;
|
||||||
|
this.auditTab = 'all';
|
||||||
|
this.isDialogAuditOpen = true;
|
||||||
|
this.fetchAudit(accessnumber);
|
||||||
|
},
|
||||||
|
closeAuditDialog() {
|
||||||
|
this.isDialogAuditOpen = false;
|
||||||
|
this.auditData = null;
|
||||||
|
this.auditAccessnumber = null;
|
||||||
|
},
|
||||||
|
fetchAudit(accessnumber) {
|
||||||
|
fetch(`${BASEURL}/api/requests/${accessnumber}/audit`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
this.auditData = data.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
get getAllAuditEvents() {
|
||||||
|
if (!this.auditData) return [];
|
||||||
|
let events = [];
|
||||||
|
let id = 0;
|
||||||
|
|
||||||
|
this.auditData.validation.forEach(v => {
|
||||||
|
let desc = `Validated by ${v.user}`;
|
||||||
|
if (v.type === 'UNVAL') {
|
||||||
|
desc = `Unvalidated by ${v.user}`;
|
||||||
|
}
|
||||||
|
events.push({
|
||||||
|
id: id++,
|
||||||
|
category: 'validation',
|
||||||
|
type: v.type,
|
||||||
|
description: desc,
|
||||||
|
datetime: v.datetime,
|
||||||
|
user: v.user,
|
||||||
|
reason: v.reason || null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.auditData.sample_collection.forEach(s => {
|
||||||
|
events.push({
|
||||||
|
id: id++,
|
||||||
|
category: 'sample',
|
||||||
|
type: s.action === 'COLLECTED' ? 'COLLECT' : 'UNRECEIVE',
|
||||||
|
description: `Tube ${s.tubenumber}: ${s.action} by ${s.user}`,
|
||||||
|
datetime: s.datetime,
|
||||||
|
user: s.user,
|
||||||
|
reason: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return events.sort((a, b) => {
|
||||||
|
if (!a.datetime) return 1;
|
||||||
|
if (!b.datetime) return -1;
|
||||||
|
return new Date(a.datetime) - new Date(b.datetime);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
get getFilteredAuditEvents() {
|
||||||
|
if (this.auditTab === 'all') return this.getAllAuditEvents;
|
||||||
|
return this.getAllAuditEvents.filter(e => e.category === this.auditTab);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@ -1,18 +1,19 @@
|
|||||||
<?= $this->extend('shared/layout_dashboard'); ?>
|
<?= $this->extend('shared/layout'); ?>
|
||||||
|
|
||||||
<?= $this->section('content'); ?>
|
<?= $this->section('content'); ?>
|
||||||
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
|
<main class="p-4 flex-1 flex flex-col gap-2" x-data="dashboard">
|
||||||
<?= $this->include('shared/dashboard_table', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/content_requests', ['config' => $roleConfig]); ?>
|
||||||
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/dialog_sample', ['config' => $roleConfig]); ?>
|
||||||
<?= $this->include('shared/dialog_unval'); ?>
|
<?= $this->include('shared/dialog_unval'); ?>
|
||||||
<?= $this->include('shared/dialog_preview'); ?>
|
<?= $this->include('shared/dialog_preview'); ?>
|
||||||
|
<?= $this->include('shared/dialog_audit'); ?>
|
||||||
</main>
|
</main>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
|
|
||||||
<?= $this->section('script'); ?>
|
<?= $this->section('script'); ?>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Alpine from '<?= base_url("js/app.js"); ?>';
|
import Alpine from '<?= base_url("js/app.js"); ?>';
|
||||||
<?= $this->include('shared/script_dashboard'); ?>
|
<?= $this->include('shared/script_requests'); ?>
|
||||||
Alpine.start();
|
Alpine.start();
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
@ -1,4 +1,4 @@
|
|||||||
<?= $this->extend('shared/layout_dashboard'); ?>
|
<?= $this->extend('shared/layout'); ?>
|
||||||
|
|
||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
<div x-data="users" class="contents">
|
<div x-data="users" class="contents">
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
$config = include __DIR__ . '/../shared/dashboard_config.php';
|
$config = include __DIR__ . '/../shared/config.php';
|
||||||
$roleConfig = $config['superuser'];
|
$roleConfig = $config['superuser'];
|
||||||
?>
|
?>
|
||||||
<?= $this->extend('shared/layout_dashboard', ['roleConfig' => $roleConfig]); ?>
|
<?= $this->extend('shared/layout', ['roleConfig' => $roleConfig]); ?>
|
||||||
|
|
||||||
<?= $this->section('content'); ?>
|
<?= $this->section('content'); ?>
|
||||||
<main class="p-4 flex-1 flex flex-col gap-2" x-data="validatePage">
|
<main class="p-4 flex-1 flex flex-col gap-2" x-data="validatePage">
|
||||||
<?= $this->include('shared/dashboard_validate', ['config' => $roleConfig]); ?>
|
<?= $this->include('shared/content_validation', ['config' => $roleConfig]); ?>
|
||||||
</main>
|
</main>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
|
|
||||||
<?= $this->section('script'); ?>
|
<?= $this->section('script'); ?>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Alpine from '<?= base_url("js/app.js"); ?>';
|
import Alpine from '<?= base_url("js/app.js"); ?>';
|
||||||
<?= $this->include('shared/script_validate'); ?>
|
<?= $this->include('shared/script_validation'); ?>
|
||||||
Alpine.start();
|
Alpine.start();
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection(); ?>
|
<?= $this->endSection(); ?>
|
||||||
29
new_table.sql
Normal file
29
new_table.sql
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
USE [GDC_CMOD]
|
||||||
|
GO
|
||||||
|
|
||||||
|
/****** Object: Table [dbo].[AUDIT_EVENTS] Script Date: 1/23/2026 4:38:31 PM ******/
|
||||||
|
SET ANSI_NULLS ON
|
||||||
|
GO
|
||||||
|
|
||||||
|
SET QUOTED_IDENTIFIER ON
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE TABLE [dbo].[AUDIT_EVENTS](
|
||||||
|
[ACCESSNUMBER] [varchar](20) NOT NULL,
|
||||||
|
[EVENT_TYPE] [varchar](20) NOT NULL,
|
||||||
|
[USERID] [varchar](50) NOT NULL,
|
||||||
|
[EVENT_AT] [datetime] NOT NULL,
|
||||||
|
[REASON] [varchar](500) NULL,
|
||||||
|
CONSTRAINT [PK_AUDIT_EVENTS] PRIMARY KEY CLUSTERED
|
||||||
|
(
|
||||||
|
[ACCESSNUMBER] ASC,
|
||||||
|
[EVENT_TYPE] ASC,
|
||||||
|
[EVENT_AT] ASC
|
||||||
|
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
|
||||||
|
) ON [PRIMARY]
|
||||||
|
GO
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[AUDIT_EVENTS] ADD DEFAULT (getdate()) FOR [EVENT_AT]
|
||||||
|
GO
|
||||||
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user