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
277 lines
12 KiB
PHP
277 lines
12 KiB
PHP
<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'); ?>
|