clqms-be/app/Views/v2/master/tests/tests_index.php

758 lines
27 KiB
PHP
Raw Normal View History

<?= $this->extend("v2/layout/main_layout"); ?>
<?= $this->section("content") ?>
<div x-data="tests()" x-init="init()">
<!-- Page Header -->
<div class="card-glass p-6 animate-fadeIn mb-6">
<div class="flex items-center gap-4">
<div
class="w-14 h-14 rounded-2xl bg-gradient-to-br from-indigo-600 to-indigo-900 flex items-center justify-center shadow-lg">
<i class="fa-solid fa-microscope text-2xl text-white"></i>
</div>
<div>
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Laboratory Tests</h2>
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage test definitions, parameters, and groups
</p>
</div>
</div>
</div>
<!-- Search & Actions Bar -->
<div class="card mb-6">
<div class="p-4">
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
<div class="flex w-full sm:w-auto gap-2">
<input type="text" placeholder="Search tests..." class="input flex-1 sm:w-80" x-model="keyword"
@keyup.enter="fetchList()" />
<select class="select w-40" x-model="filterType" @change="fetchList()">
<option value="">All Types</option>
feat(valueset): refactor from ID-based to name-based lookups Complete overhaul of the valueset system to use human-readable names instead of numeric IDs for improved maintainability and API consistency. - PatientController: Renamed 'Gender' field to 'Sex' in validation rules - ValuesetController: Changed API endpoints from ID-based (/:num) to name-based (/:any) - TestsController: Refactored to use ValueSet library instead of direct valueset queries - Added ValueSet library (app/Libraries/ValueSet.php) with static lookup methods: - getOptions() - returns dropdown format [{value, label}] - getLabel(, ) - returns label for a value - transformLabels(, ) - batch transform records - get() and getRaw() for Lookups compatibility - Added ValueSetApiController for public valueset API endpoints - Added ValueSet refresh endpoint (POST /api/valueset/refresh) - Added DemoOrderController for testing order creation without auth - 2026-01-12-000001: Convert valueset references from VID to VValue - 2026-01-12-000002: Rename patient.Gender column to Sex - OrderTestController: Now uses OrderTestModel with proper model pattern - TestsController: Uses ValueSet library for all lookup operations - ValueSetController: Simplified to use name-based lookups - Updated all organization (account/site/workstation) dialogs and index views - Updated specimen container dialogs and index views - Updated tests_index.php with ValueSet integration - Updated patient dialog form and index views - Removed .factory/config.json and CLAUDE.md (replaced by AGENTS.md) - Consolidated lookups in Lookups.php (removed inline valueset constants) - Updated all test files to match new field names - 32 modified files, 17 new files, 2 deleted files - Net: +661 insertions, -1443 deletions (significant cleanup)
2026-01-12 16:53:41 +07:00
<template x-for="(type, index) in (testTypes || [])" :key="(type?.key ?? index)">
<option :value="type?.key" x-text="(type?.value || '')"></option>
</template>
</select>
<button class="btn btn-primary" @click="fetchList()">
<i class="fa-solid fa-search"></i>
</button>
</div>
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
<i class="fa-solid fa-plus mr-2"></i>
Add Test
</button>
</div>
</div>
</div>
<!-- Content Card -->
<div class="card overflow-hidden">
<!-- Loading State -->
<div x-show="loading" class="p-12 text-center" x-cloak>
<div class="spinner spinner-lg mx-auto mb-4"></div>
<p style="color: rgb(var(--color-text-muted));">Loading tests...</p>
</div>
<!-- Table -->
<div class="overflow-x-auto" x-show="!loading">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Code</th>
<th>Test Name</th>
<th>Type</th>
<th>Seq</th>
<th>Visible</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
<!-- Empty State -->
<template x-if="!list || list.length === 0">
<tr>
<td colspan="7" class="text-center py-12">
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
<i class="fa-solid fa-inbox text-5xl opacity-40"></i>
<p class="text-lg">No tests found</p>
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
<i class="fa-solid fa-plus mr-1"></i> Add First Test
</button>
</div>
</td>
</tr>
</template>
<!-- Test Rows -->
<template x-for="test in list" :key="test.TestSiteID">
<tr class="hover:bg-opacity-50">
<td>
<span class="badge badge-ghost font-mono text-xs" x-text="test.TestSiteID || '-'"></span>
</td>
<td>
<code class="text-sm font-mono px-2 py-1 rounded"
style="background: rgb(var(--color-bg-secondary)); color: rgb(var(--color-primary));"
x-text="test.TestSiteCode || '-'"></code>
</td>
<td>
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="test.TestSiteName || '-'"></div>
<div class="text-xs mt-1" style="color: rgb(var(--color-text-muted));" x-text="test.Description || ''">
</div>
</td>
<td>
<span class="badge" :class="{
'badge-info': test.TypeCode === 'TEST',
'badge-success': test.TypeCode === 'PARAM',
'badge-warning': test.TypeCode === 'CALC',
'badge-primary': test.TypeCode === 'GROUP',
'badge-secondary': test.TypeCode === 'TITLE'
}" x-text="test.TypeName || test.TypeCode || '-'"></span>
</td>
<td x-text="test.SeqScr || '-'"></td>
<td>
<div class="flex gap-1">
<span class="badge badge-ghost badge-xs" title="Screen" x-show="test.VisibleScr == 1">
<i class="fa-solid fa-desktop text-green-500"></i>
</span>
<span class="badge badge-ghost badge-xs" title="Report" x-show="test.VisibleRpt == 1">
<i class="fa-solid fa-file-alt text-blue-500"></i>
</span>
</div>
</td>
<td class="text-center">
<div class="flex items-center justify-center gap-1">
<button class="btn btn-ghost btn-sm btn-square" @click="viewTest(test.TestSiteID)"
title="View Details">
<i class="fa-solid fa-eye text-sky-500"></i>
</button>
<button class="btn btn-ghost btn-sm btn-square" @click="editTest(test.TestSiteID)" title="Edit">
<i class="fa-solid fa-pen text-indigo-500"></i>
</button>
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(test)" title="Delete">
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Summary -->
<div class="p-4 flex items-center justify-between" style="border-top: 1px solid rgb(var(--color-border));"
x-show="list && list.length > 0">
<span class="text-sm" style="color: rgb(var(--color-text-muted));"
x-text="'Showing ' + list.length + ' tests'"></span>
</div>
</div>
<!-- Include Form Dialogs -->
<?= $this->include('v2/master/tests/test_dialog') ?>
<?= $this->include('v2/master/tests/param_dialog') ?>
<?= $this->include('v2/master/tests/calc_dialog') ?>
<?= $this->include('v2/master/tests/grp_dialog') ?>
<!-- View Details Modal -->
<div x-show="showViewModal" x-cloak class="modal-overlay" @click.self="showViewModal = false">
<div class="modal-content p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto"
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
<i class="fa-solid fa-microscope" style="color: rgb(var(--color-primary));"></i>
<span x-text="viewData?.TestSiteName || 'Test Details'"></span>
</h3>
<button class="btn btn-ghost btn-sm btn-square" @click="showViewModal = false">
<i class="fa-solid fa-times"></i>
</button>
</div>
<!-- Details Content -->
<template x-if="viewData">
<div class="space-y-6">
<!-- Basic Info -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Code</div>
<div class="font-mono font-semibold" x-text="viewData.TestSiteCode || '-'"></div>
</div>
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Type</div>
<span class="badge" :class="{
'badge-info': viewData.TypeCode === 'TEST',
'badge-success': viewData.TypeCode === 'PARAM',
'badge-warning': viewData.TypeCode === 'CALC',
'badge-primary': viewData.TypeCode === 'GROUP',
'badge-secondary': viewData.TypeCode === 'TITLE'
}" x-text="viewData.TypeName || viewData.TypeCode || '-'"></span>
</div>
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Sequence (Screen)</div>
<div class="font-semibold" x-text="viewData.SeqScr || '-'"></div>
</div>
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Sequence (Report)</div>
<div class="font-semibold" x-text="viewData.SeqRpt || '-'"></div>
</div>
</div>
<!-- Type-specific Details -->
<template x-if="viewData.TypeCode === 'TEST' || viewData.TypeCode === 'PARAM'">
<div>
<h4 class="font-semibold mb-3 flex items-center gap-2">
<i class="fa-solid fa-flask"></i> Technical Details
</h4>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Result Type</div>
<div x-text="viewData.testdeftech?.[0]?.ResultType || '-'"></div>
</div>
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Unit</div>
<div x-text="viewData.testdeftech?.[0]?.Unit1 || '-'"></div>
</div>
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Method</div>
<div x-text="viewData.testdeftech?.[0]?.Method || '-'"></div>
</div>
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Expected TAT</div>
<div x-text="(viewData.testdeftech?.[0]?.ExpectedTAT || '-') + ' min'"></div>
</div>
</div>
</div>
</template>
<template x-if="viewData.TypeCode === 'CALC'">
<div>
<h4 class="font-semibold mb-3 flex items-center gap-2">
<i class="fa-solid fa-calculator"></i> Calculation Details
</h4>
<div class="grid grid-cols-2 gap-4">
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Formula Input</div>
<div class="font-mono text-sm" x-text="viewData.testdefcal?.[0]?.FormulaInput || '-'"></div>
</div>
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Formula Code</div>
<div class="font-mono text-sm" x-text="viewData.testdefcal?.[0]?.FormulaCode || '-'"></div>
</div>
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Result Unit</div>
<div x-text="viewData.testdefcal?.[0]?.Unit1 || '-'"></div>
</div>
<div class="p-3 rounded-lg" style="background: rgb(var(--color-bg-secondary));">
<div class="text-xs" style="color: rgb(var(--color-text-muted));">Decimal</div>
<div x-text="viewData.testdefcal?.[0]?.Decimal || '-'"></div>
</div>
</div>
</div>
</template>
<template x-if="viewData.TypeCode === 'GROUP'">
<div>
<h4 class="font-semibold mb-3 flex items-center gap-2">
<i class="fa-solid fa-layer-group"></i> Group Members
</h4>
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Type</th>
</tr>
</thead>
<tbody>
<template x-for="member in (viewData.testdefgrp || [])" :key="member.TestGrpID">
<tr>
<td><code x-text="member.TestSiteCode"></code></td>
<td x-text="member.TestSiteName"></td>
<td>
<span class="badge badge-xs" :class="{
'badge-info': member.MemberTypeCode === 'TEST',
'badge-success': member.MemberTypeCode === 'PARAM'
}" x-text="member.MemberTypeCode"></span>
</td>
</tr>
</template>
<template x-if="!viewData.testdefgrp || viewData.testdefgrp.length === 0">
<tr>
<td colspan="3" class="text-center text-muted">No members</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<!-- Test Mappings -->
<template x-if="viewData.testmap && viewData.testmap.length > 0">
<div>
<h4 class="font-semibold mb-3 flex items-center gap-2">
<i class="fa-solid fa-link"></i> Test Mappings
</h4>
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>Host Type</th>
<th>Host Code</th>
<th>Client Type</th>
<th>Client Code</th>
</tr>
</thead>
<tbody>
<template x-for="map in viewData.testmap" :key="map.TestMapID">
<tr>
<td x-text="map.HostType || '-'"></td>
<td x-text="map.HostTestCode || '-'"></td>
<td x-text="map.ClientType || '-'"></td>
<td><code x-text="map.ClientTestCode || '-'"></code></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
</div>
</template>
<!-- Actions -->
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
<button class="btn btn-ghost flex-1" @click="showViewModal = false">Close</button>
<button class="btn btn-primary flex-1" @click="editTest(viewData?.TestSiteID)">
<i class="fa-solid fa-edit mr-2"></i> Edit Test
</button>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div x-show="showDeleteModal" x-cloak class="modal-overlay" @click.self="showDeleteModal = false">
<div class="modal-content p-6 max-w-md">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
<i class="fa-solid fa-exclamation-triangle"></i>
Confirm Delete
</h3>
<p class="mb-6" style="color: rgb(var(--color-text));">
Are you sure you want to delete test <strong x-text="deleteTarget?.TestSiteName"></strong>?
This action cannot be undone.
</p>
<div class="flex gap-2">
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteTest()"
:disabled="deleting">
<span x-show="deleting" class="spinner spinner-sm"></span>
<span x-show="!deleting">Delete</span>
</button>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section("script") ?>
<script>
function tests() {
return {
// State
loading: false,
list: [],
testTypes: [],
keyword: "",
filterType: "",
// View Modal
showViewModal: false,
viewData: null,
// Form Modal
showModal: false,
isEditing: false,
saving: false,
errors: {},
activeTab: 'basic',
// Member Selector
showMemberSelector: false,
memberSearch: '',
availableTests: [],
form: {
TestSiteID: null,
SiteID: 1,
TestSiteCode: "",
TestSiteName: "",
TestType: "",
Description: "",
SeqScr: 0,
SeqRpt: 0,
IndentLeft: 0,
VisibleScr: 1,
VisibleRpt: 1,
CountStat: 1,
// Technical fields
DisciplineID: "",
DepartmentID: "",
ResultType: "",
RefType: "",
Unit1: "",
Unit2: "",
Decimal: 2,
Method: "",
SampleType: "",
ExpectedTAT: 60,
RefMin: null,
RefMax: null,
RefText: "",
CriticalLow: null,
CriticalHigh: null,
// Calculation fields
FormulaInput: "",
FormulaCode: "",
// Value Set fields
ValueSetID: "",
DefaultValue: "",
// Group members
groupMembers: []
},
// Delete Modal
showDeleteModal: false,
deleteTarget: null,
deleting: false,
// Lifecycle
async init() {
await this.fetchTestTypes();
await this.fetchAvailableTests();
// Small delay to ensure Alpine has processed testTypes
setTimeout(() => {
this.fetchList();
}, 50);
},
// Fetch test types from valueset
async fetchTestTypes() {
try {
feat(valueset): refactor from ID-based to name-based lookups Complete overhaul of the valueset system to use human-readable names instead of numeric IDs for improved maintainability and API consistency. - PatientController: Renamed 'Gender' field to 'Sex' in validation rules - ValuesetController: Changed API endpoints from ID-based (/:num) to name-based (/:any) - TestsController: Refactored to use ValueSet library instead of direct valueset queries - Added ValueSet library (app/Libraries/ValueSet.php) with static lookup methods: - getOptions() - returns dropdown format [{value, label}] - getLabel(, ) - returns label for a value - transformLabels(, ) - batch transform records - get() and getRaw() for Lookups compatibility - Added ValueSetApiController for public valueset API endpoints - Added ValueSet refresh endpoint (POST /api/valueset/refresh) - Added DemoOrderController for testing order creation without auth - 2026-01-12-000001: Convert valueset references from VID to VValue - 2026-01-12-000002: Rename patient.Gender column to Sex - OrderTestController: Now uses OrderTestModel with proper model pattern - TestsController: Uses ValueSet library for all lookup operations - ValueSetController: Simplified to use name-based lookups - Updated all organization (account/site/workstation) dialogs and index views - Updated specimen container dialogs and index views - Updated tests_index.php with ValueSet integration - Updated patient dialog form and index views - Removed .factory/config.json and CLAUDE.md (replaced by AGENTS.md) - Consolidated lookups in Lookups.php (removed inline valueset constants) - Updated all test files to match new field names - 32 modified files, 17 new files, 2 deleted files - Net: +661 insertions, -1443 deletions (significant cleanup)
2026-01-12 16:53:41 +07:00
const res = await fetch(`${BASEURL}api/valueset/test_type`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.testTypes = data.data || [];
} catch (err) {
console.error('Failed to fetch test types:', err);
this.testTypes = [
feat(valueset): refactor from ID-based to name-based lookups Complete overhaul of the valueset system to use human-readable names instead of numeric IDs for improved maintainability and API consistency. - PatientController: Renamed 'Gender' field to 'Sex' in validation rules - ValuesetController: Changed API endpoints from ID-based (/:num) to name-based (/:any) - TestsController: Refactored to use ValueSet library instead of direct valueset queries - Added ValueSet library (app/Libraries/ValueSet.php) with static lookup methods: - getOptions() - returns dropdown format [{value, label}] - getLabel(, ) - returns label for a value - transformLabels(, ) - batch transform records - get() and getRaw() for Lookups compatibility - Added ValueSetApiController for public valueset API endpoints - Added ValueSet refresh endpoint (POST /api/valueset/refresh) - Added DemoOrderController for testing order creation without auth - 2026-01-12-000001: Convert valueset references from VID to VValue - 2026-01-12-000002: Rename patient.Gender column to Sex - OrderTestController: Now uses OrderTestModel with proper model pattern - TestsController: Uses ValueSet library for all lookup operations - ValueSetController: Simplified to use name-based lookups - Updated all organization (account/site/workstation) dialogs and index views - Updated specimen container dialogs and index views - Updated tests_index.php with ValueSet integration - Updated patient dialog form and index views - Removed .factory/config.json and CLAUDE.md (replaced by AGENTS.md) - Consolidated lookups in Lookups.php (removed inline valueset constants) - Updated all test files to match new field names - 32 modified files, 17 new files, 2 deleted files - Net: +661 insertions, -1443 deletions (significant cleanup)
2026-01-12 16:53:41 +07:00
{ key: 'TEST', value: 'Test' },
{ key: 'PARAM', value: 'Parameter' },
{ key: 'CALC', value: 'Calculated Test' },
{ key: 'GROUP', value: 'Group Test' },
{ key: 'TITLE', value: 'Title' }
];
}
},
// Fetch available tests for group members
async fetchAvailableTests() {
try {
const res = await fetch(`${BASEURL}api/tests`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.availableTests = (data.data || []).filter(t =>
t.TypeCode === 'TEST' || t.TypeCode === 'PARAM'
);
} catch (err) {
console.error('Failed to fetch available tests:', err);
this.availableTests = [];
}
},
// Get type display name
feat(valueset): refactor from ID-based to name-based lookups Complete overhaul of the valueset system to use human-readable names instead of numeric IDs for improved maintainability and API consistency. - PatientController: Renamed 'Gender' field to 'Sex' in validation rules - ValuesetController: Changed API endpoints from ID-based (/:num) to name-based (/:any) - TestsController: Refactored to use ValueSet library instead of direct valueset queries - Added ValueSet library (app/Libraries/ValueSet.php) with static lookup methods: - getOptions() - returns dropdown format [{value, label}] - getLabel(, ) - returns label for a value - transformLabels(, ) - batch transform records - get() and getRaw() for Lookups compatibility - Added ValueSetApiController for public valueset API endpoints - Added ValueSet refresh endpoint (POST /api/valueset/refresh) - Added DemoOrderController for testing order creation without auth - 2026-01-12-000001: Convert valueset references from VID to VValue - 2026-01-12-000002: Rename patient.Gender column to Sex - OrderTestController: Now uses OrderTestModel with proper model pattern - TestsController: Uses ValueSet library for all lookup operations - ValueSetController: Simplified to use name-based lookups - Updated all organization (account/site/workstation) dialogs and index views - Updated specimen container dialogs and index views - Updated tests_index.php with ValueSet integration - Updated patient dialog form and index views - Removed .factory/config.json and CLAUDE.md (replaced by AGENTS.md) - Consolidated lookups in Lookups.php (removed inline valueset constants) - Updated all test files to match new field names - 32 modified files, 17 new files, 2 deleted files - Net: +661 insertions, -1443 deletions (significant cleanup)
2026-01-12 16:53:41 +07:00
getTypeName(value) {
const typeMap = {
'TEST': 'Test',
'PARAM': 'Parameter',
'CALC': 'Calculated Test',
'GROUP': 'Group',
'TITLE': 'Title'
};
feat(valueset): refactor from ID-based to name-based lookups Complete overhaul of the valueset system to use human-readable names instead of numeric IDs for improved maintainability and API consistency. - PatientController: Renamed 'Gender' field to 'Sex' in validation rules - ValuesetController: Changed API endpoints from ID-based (/:num) to name-based (/:any) - TestsController: Refactored to use ValueSet library instead of direct valueset queries - Added ValueSet library (app/Libraries/ValueSet.php) with static lookup methods: - getOptions() - returns dropdown format [{value, label}] - getLabel(, ) - returns label for a value - transformLabels(, ) - batch transform records - get() and getRaw() for Lookups compatibility - Added ValueSetApiController for public valueset API endpoints - Added ValueSet refresh endpoint (POST /api/valueset/refresh) - Added DemoOrderController for testing order creation without auth - 2026-01-12-000001: Convert valueset references from VID to VValue - 2026-01-12-000002: Rename patient.Gender column to Sex - OrderTestController: Now uses OrderTestModel with proper model pattern - TestsController: Uses ValueSet library for all lookup operations - ValueSetController: Simplified to use name-based lookups - Updated all organization (account/site/workstation) dialogs and index views - Updated specimen container dialogs and index views - Updated tests_index.php with ValueSet integration - Updated patient dialog form and index views - Removed .factory/config.json and CLAUDE.md (replaced by AGENTS.md) - Consolidated lookups in Lookups.php (removed inline valueset constants) - Updated all test files to match new field names - 32 modified files, 17 new files, 2 deleted files - Net: +661 insertions, -1443 deletions (significant cleanup)
2026-01-12 16:53:41 +07:00
return typeMap[value] || value || 'Test';
},
// Get type code from value
getTypeCode(value) {
return value || '';
},
// Fetch test list
async fetchList() {
this.loading = true;
try {
const params = new URLSearchParams();
if (this.keyword) params.append('TestSiteName', this.keyword);
if (this.filterType) params.append('TestType', this.filterType);
const res = await fetch(`${BASEURL}api/tests?${params}`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.list = data.data || [];
} catch (err) {
console.error(err);
this.list = [];
} finally {
this.loading = false;
}
},
// View test details
async viewTest(id) {
this.viewData = null;
try {
const res = await fetch(`${BASEURL}api/tests/${id}`, {
credentials: 'include'
});
const data = await res.json();
if (data.data) {
this.viewData = data.data;
this.showViewModal = true;
}
} catch (err) {
console.error(err);
alert('Failed to load test details');
}
},
// Show form for new test
showForm() {
this.isEditing = false;
this.activeTab = 'basic';
this.form = {
TestSiteID: null,
SiteID: 1,
TestSiteCode: "",
TestSiteName: "",
TestType: "",
Description: "",
SeqScr: 0,
SeqRpt: 0,
IndentLeft: 0,
VisibleScr: 1,
VisibleRpt: 1,
CountStat: 1,
DisciplineID: "",
DepartmentID: "",
ResultType: "",
RefType: "",
Unit1: "",
Unit2: "",
Decimal: 2,
Method: "",
SampleType: "",
ExpectedTAT: 60,
RefMin: null,
RefMax: null,
RefText: "",
CriticalLow: null,
CriticalHigh: null,
FormulaInput: "",
FormulaCode: "",
ValueSetID: "",
DefaultValue: "",
groupMembers: []
};
this.errors = {};
this.showModal = true;
},
// Edit test
async editTest(id) {
this.isEditing = true;
this.errors = {};
this.activeTab = 'basic';
try {
const res = await fetch(`${BASEURL}api/tests/${id}`, {
credentials: 'include'
});
const data = await res.json();
if (data.data) {
const testData = data.data;
this.form = {
...this.form,
...testData,
feat(valueset): refactor from ID-based to name-based lookups Complete overhaul of the valueset system to use human-readable names instead of numeric IDs for improved maintainability and API consistency. - PatientController: Renamed 'Gender' field to 'Sex' in validation rules - ValuesetController: Changed API endpoints from ID-based (/:num) to name-based (/:any) - TestsController: Refactored to use ValueSet library instead of direct valueset queries - Added ValueSet library (app/Libraries/ValueSet.php) with static lookup methods: - getOptions() - returns dropdown format [{value, label}] - getLabel(, ) - returns label for a value - transformLabels(, ) - batch transform records - get() and getRaw() for Lookups compatibility - Added ValueSetApiController for public valueset API endpoints - Added ValueSet refresh endpoint (POST /api/valueset/refresh) - Added DemoOrderController for testing order creation without auth - 2026-01-12-000001: Convert valueset references from VID to VValue - 2026-01-12-000002: Rename patient.Gender column to Sex - OrderTestController: Now uses OrderTestModel with proper model pattern - TestsController: Uses ValueSet library for all lookup operations - ValueSetController: Simplified to use name-based lookups - Updated all organization (account/site/workstation) dialogs and index views - Updated specimen container dialogs and index views - Updated tests_index.php with ValueSet integration - Updated patient dialog form and index views - Removed .factory/config.json and CLAUDE.md (replaced by AGENTS.md) - Consolidated lookups in Lookups.php (removed inline valueset constants) - Updated all test files to match new field names - 32 modified files, 17 new files, 2 deleted files - Net: +661 insertions, -1443 deletions (significant cleanup)
2026-01-12 16:53:41 +07:00
TestType: testData.TestType || '',
TypeCode: testData.TestType || '',
groupMembers: testData.testdefgrp || []
};
this.showModal = true;
this.showViewModal = false;
}
} catch (err) {
console.error(err);
alert('Failed to load test data');
}
},
// Validate form
validate() {
const e = {};
if (!this.form.TestSiteName?.trim()) e.TestSiteName = "Test name is required";
if (!this.form.TestSiteCode?.trim()) e.TestSiteCode = "Test code is required";
if (!this.form.TestType) e.TestType = "Test type is required";
this.errors = e;
return Object.keys(e).length === 0;
},
// Close modal
closeModal() {
this.showModal = false;
this.showMemberSelector = false;
this.errors = {};
this.memberSearch = '';
},
// Toggle group member
toggleMember(test) {
if (!this.form.groupMembers) {
this.form.groupMembers = [];
}
const index = this.form.groupMembers.findIndex(m => m.TestSiteID === test.TestSiteID);
if (index > -1) {
this.form.groupMembers.splice(index, 1);
} else {
this.form.groupMembers.push({
TestSiteID: test.TestSiteID,
TestSiteCode: test.TestSiteCode,
TestSiteName: test.TestSiteName,
MemberTypeCode: test.TypeCode || 'TEST',
SeqScr: 0
});
}
this.$forceUpdate();
},
// Remove group member
removeMember(index) {
this.form.groupMembers.splice(index, 1);
this.$forceUpdate();
},
// Add common member quickly
addCommonMember(code, type) {
const test = this.availableTests.find(t => t.TestSiteCode === code);
if (test) {
if (!this.form.groupMembers) {
this.form.groupMembers = [];
}
const exists = this.form.groupMembers.some(m => m.TestSiteID === test.TestSiteID);
if (!exists) {
this.form.groupMembers.push({
TestSiteID: test.TestSiteID,
TestSiteCode: test.TestSiteCode,
TestSiteName: test.TestSiteName,
MemberTypeCode: type,
SeqScr: this.form.groupMembers.length + 1
});
this.$forceUpdate();
}
} else {
// Add as custom entry if not found
if (!this.form.groupMembers) {
this.form.groupMembers = [];
}
this.form.groupMembers.push({
TestSiteID: null,
TestSiteCode: code,
TestSiteName: code,
MemberTypeCode: type,
SeqScr: this.form.groupMembers.length + 1
});
this.$forceUpdate();
}
},
// Save test
async save() {
if (!this.validate()) return;
this.saving = true;
try {
const method = this.isEditing ? 'PATCH' : 'POST';
const payload = { ...this.form };
if (this.getTypeCode(this.form.TestType) === 'GROUP' && this.form.groupMembers?.length > 0) {
payload.groupMembers = this.form.groupMembers;
}
const res = await fetch(`${BASEURL}api/tests`, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'include'
});
const data = await res.json();
if (res.ok) {
this.closeModal();
await this.fetchList();
} else {
alert(data.message || "Failed to save");
}
} catch (err) {
console.error(err);
alert("Failed to save test");
} finally {
this.saving = false;
}
},
// Confirm delete
confirmDelete(test) {
this.deleteTarget = test;
this.showDeleteModal = true;
},
// Delete test
async deleteTest() {
if (!this.deleteTarget) return;
this.deleting = true;
try {
const res = await fetch(`${BASEURL}api/tests`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ TestSiteID: this.deleteTarget.TestSiteID }),
credentials: 'include'
});
if (res.ok) {
this.showDeleteModal = false;
await this.fetchList();
} else {
const data = await res.json();
alert(data.message || "Failed to delete");
}
} catch (err) {
console.error(err);
alert("Failed to delete test");
} finally {
this.deleting = false;
this.deleteTarget = null;
}
},
}
}
</script>
<?= $this->endSection() ?>