- Add new dialog forms for test calc, group, param, and title management - Refactor test_dialog.php to new location (master/tests/) - Update TestDefCalModel, TestDefSiteModel, TestDefTechModel, TestMapModel - Modify Tests controller and Routes for new dialog handlers - Update migration schema for test definitions - Add new styles for v2 test management interface - Include Test Management documentation files
797 lines
25 KiB
PHP
797 lines
25 KiB
PHP
<?= $this->extend("v2/layout/main_layout"); ?>
|
|
|
|
<?= $this->section("content") ?>
|
|
<div x-data="labTests()" 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-blue-600 to-blue-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));">Lab Test Catalog</h2>
|
|
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage lab test definitions, methods, and types</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 lab tests..."
|
|
class="input flex-1 sm:w-80"
|
|
x-model="keyword"
|
|
@keyup.enter="fetchList()"
|
|
/>
|
|
<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 New 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 test catalog...</p>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div class="overflow-x-auto" x-show="!loading">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Test Name</th>
|
|
<th>Code</th>
|
|
<th>Type</th>
|
|
<th>Seq</th>
|
|
<th class="text-center">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<!-- Empty State -->
|
|
<template x-if="!list || list.length === 0">
|
|
<tr>
|
|
<td colspan="6" 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 lab 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>
|
|
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="test.TestSiteName || '-'"</div>
|
|
</td>
|
|
<td class="font-mono text-sm" x-text="test.TestSiteCode || '-'"></td>
|
|
<td>
|
|
<span class="badge badge-sm" :class="getTestTypeBadgeClass(test.TypeCode)" x-text="test.TypeName || '-'"></span>
|
|
</td>
|
|
<td class="text-sm font-mono" x-text="test.SeqScr !== undefined ? test.SeqScr + ' / ' + test.SeqRpt : '-'"></td>
|
|
<td class="text-center">
|
|
<div class="flex items-center justify-center gap-1">
|
|
<button class="btn btn-ghost btn-sm btn-square" @click="editTest(test.TestSiteID)" title="Edit">
|
|
<i class="fa-solid fa-pen text-sky-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>
|
|
</div>
|
|
|
|
<!-- Include Form Dialogs (all types) -->
|
|
<?= $this->include('v2/master/tests/test_dialog') ?>
|
|
<?= $this->include('v2/master/tests/param_dialog') ?>
|
|
<?= $this->include('v2/master/tests/group_dialog') ?>
|
|
<?= $this->include('v2/master/tests/calc_dialog') ?>
|
|
<?= $this->include('v2/master/tests/title_dialog') ?>
|
|
|
|
<!-- 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 labTests() {
|
|
return {
|
|
// State
|
|
loading: false,
|
|
list: [],
|
|
sitesList: [],
|
|
typesList: [],
|
|
disciplinesList: [],
|
|
departmentsList: [],
|
|
methodsList: [],
|
|
specimenTypesList: [],
|
|
workstationsList: [],
|
|
equipmentList: [],
|
|
containersList: [],
|
|
availableTests: [],
|
|
keyword: "",
|
|
|
|
// Form Modal
|
|
showModal: false,
|
|
currentDialogType: 'TEST', // TEST, PARAM, GROUP, CALC, TITLE
|
|
isEditing: false,
|
|
saving: false,
|
|
errors: {},
|
|
form: {
|
|
TestSiteID: null,
|
|
SiteID: 1,
|
|
TestSiteCode: "",
|
|
TestSiteName: "",
|
|
TestType: "",
|
|
TestTypeName: "",
|
|
TypeCode: "TEST",
|
|
Description: "",
|
|
SeqScr: 0,
|
|
SeqRpt: 0,
|
|
IndentLeft: 0,
|
|
FontStyle: "",
|
|
VisibleScr: 1,
|
|
VisibleRpt: 1,
|
|
CountStat: 1,
|
|
StartDate: "",
|
|
// Technical fields (TEST, PARAM)
|
|
DisciplineID: "",
|
|
DepartmentID: "",
|
|
WorkstationID: "",
|
|
EquipmentID: "",
|
|
ResultType: "",
|
|
RefType: "",
|
|
VSet: "",
|
|
SpcType: "",
|
|
SpcDesc: "",
|
|
ReqQty: "",
|
|
ReqQtyUnit: "mL",
|
|
Unit1: "",
|
|
Factor: "",
|
|
Unit2: "",
|
|
Decimal: 2,
|
|
CollReq: "",
|
|
Method: "",
|
|
ExpectedTAT: "",
|
|
// GROUP fields
|
|
members: [],
|
|
// CALC fields
|
|
FormulaInput: "",
|
|
FormulaCode: "",
|
|
// Mapping fields
|
|
testmap: []
|
|
},
|
|
|
|
// Test selector for GROUP dialog
|
|
showTestSelector: false,
|
|
testSearch: "",
|
|
selectedTestIds: [],
|
|
|
|
// Delete Modal
|
|
showDeleteModal: false,
|
|
deleteTarget: null,
|
|
deleting: false,
|
|
|
|
// Lifecycle
|
|
async init() {
|
|
await this.fetchList();
|
|
await this.fetchSites();
|
|
await this.fetchTypes();
|
|
await this.fetchDisciplines();
|
|
await this.fetchDepartments();
|
|
await this.fetchMethods();
|
|
// Additional data can be loaded on demand when specific dialogs are opened
|
|
|
|
// Watch for typesList changes to ensure dropdown is populated
|
|
this.$watch('typesList', (newVal) => {
|
|
if (newVal.length > 0 && this.isEditing && this.form.TestType) {
|
|
const found = newVal.find(t => String(t.VID) === String(this.form.TestType));
|
|
if (!found) {
|
|
const byVid = newVal.find(t => t.VID == this.form.TestType);
|
|
if (byVid) {
|
|
this.form.TestType = byVid.VID;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
// Fetch disciplines
|
|
async fetchDisciplines() {
|
|
try {
|
|
const res = await fetch(`${BASEURL}api/organization/discipline`, {
|
|
credentials: 'include'
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
this.disciplinesList = data.data || [];
|
|
}
|
|
} catch (err) {
|
|
// Silently fail
|
|
}
|
|
},
|
|
|
|
// Fetch departments
|
|
async fetchDepartments() {
|
|
try {
|
|
const res = await fetch(`${BASEURL}api/organization/department`, {
|
|
credentials: 'include'
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
this.departmentsList = data.data || [];
|
|
}
|
|
} catch (err) {
|
|
// Silently fail
|
|
}
|
|
},
|
|
|
|
// Fetch methods
|
|
async fetchMethods() {
|
|
try {
|
|
// Methods could be fetched from a valueset or endpoint
|
|
const res = await fetch(`${BASEURL}api/valueset/valuesetdef/28`, {
|
|
credentials: 'include'
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
this.methodsList = data.data || [];
|
|
}
|
|
} catch (err) {
|
|
// Silently fail
|
|
}
|
|
},
|
|
|
|
// Fetch lab test list
|
|
async fetchList() {
|
|
this.loading = true;
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (this.keyword) params.append('TestSiteName', this.keyword);
|
|
|
|
const res = await fetch(`${BASEURL}api/tests?${params}`, {
|
|
credentials: 'include'
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
this.list = data.data || [];
|
|
} else {
|
|
this.list = [];
|
|
}
|
|
} catch (err) {
|
|
this.list = [];
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
// Fetch site list for dropdown
|
|
async fetchSites() {
|
|
try {
|
|
const res = await fetch(`${BASEURL}api/organization/site`, {
|
|
credentials: 'include'
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
this.sitesList = data.data || [];
|
|
}
|
|
} catch (err) {
|
|
// Silently fail - will use default site
|
|
}
|
|
},
|
|
|
|
// Fetch test types from valueset
|
|
async fetchTypes() {
|
|
try {
|
|
const res = await fetch(`${BASEURL}api/valueset/valuesetdef/27`, {
|
|
credentials: 'include'
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
this.typesList = data.data || [];
|
|
}
|
|
} catch (err) {
|
|
// Silently fail - types will use default mapping
|
|
}
|
|
},
|
|
|
|
// Show form for new test
|
|
showForm(typeCode = 'TEST') {
|
|
this.isEditing = false;
|
|
this.currentDialogType = typeCode;
|
|
this.form = {
|
|
TestSiteID: null,
|
|
SiteID: 1,
|
|
TestSiteCode: "",
|
|
TestSiteName: "",
|
|
TestType: this.getTypeIdByCode(typeCode),
|
|
TestTypeName: this.getTypeNameByCode(typeCode),
|
|
TypeCode: typeCode,
|
|
Description: "",
|
|
SeqScr: 0,
|
|
SeqRpt: 0,
|
|
IndentLeft: 0,
|
|
FontStyle: "",
|
|
VisibleScr: 1,
|
|
VisibleRpt: 1,
|
|
CountStat: 1,
|
|
StartDate: new Date().toISOString().split('T')[0],
|
|
// Technical fields
|
|
DisciplineID: "",
|
|
DepartmentID: "",
|
|
WorkstationID: "",
|
|
EquipmentID: "",
|
|
ResultType: "",
|
|
RefType: "",
|
|
VSet: "",
|
|
SpcType: "",
|
|
SpcDesc: "",
|
|
ReqQty: "",
|
|
ReqQtyUnit: "mL",
|
|
Unit1: "",
|
|
Factor: "",
|
|
Unit2: "",
|
|
Decimal: 2,
|
|
CollReq: "",
|
|
Method: "",
|
|
ExpectedTAT: "",
|
|
// GROUP fields
|
|
members: [],
|
|
// CALC fields
|
|
FormulaInput: "",
|
|
FormulaCode: "",
|
|
// Mapping fields
|
|
testmap: []
|
|
};
|
|
this.errors = {};
|
|
this.showModal = true;
|
|
},
|
|
|
|
// Get type ID by code
|
|
getTypeIdByCode(typeCode) {
|
|
const typeMap = {
|
|
'TEST': 100,
|
|
'PARAM': 101,
|
|
'GROUP': 102,
|
|
'CALC': 103,
|
|
'TITLE': 104
|
|
};
|
|
return typeMap[typeCode] || 100;
|
|
},
|
|
|
|
// Get type name by code
|
|
getTypeNameByCode(typeCode) {
|
|
const nameMap = {
|
|
'TEST': 'Test',
|
|
'PARAM': 'Parameter',
|
|
'GROUP': 'Group',
|
|
'CALC': 'Calculated',
|
|
'TITLE': 'Title'
|
|
};
|
|
return nameMap[typeCode] || 'Test';
|
|
},
|
|
|
|
// Edit test
|
|
async editTest(id) {
|
|
this.isEditing = true;
|
|
this.errors = {};
|
|
try {
|
|
const res = await fetch(`${BASEURL}api/tests/${id}`, {
|
|
credentials: 'include'
|
|
});
|
|
const data = await res.json();
|
|
if (data.data) {
|
|
const testData = data.data;
|
|
const testType = testData.TestType;
|
|
const typeCode = testData.TypeCode || this.getTypeCodeById(testType);
|
|
this.currentDialogType = typeCode;
|
|
|
|
// Merge with default form structure
|
|
this.form = {
|
|
TestSiteID: testData.TestSiteID,
|
|
SiteID: testData.SiteID || 1,
|
|
TestSiteCode: testData.TestSiteCode || "",
|
|
TestSiteName: testData.TestSiteName || "",
|
|
TestType: testType,
|
|
TestTypeName: testData.TypeName || this.getTypeNameByCode(typeCode),
|
|
TypeCode: typeCode,
|
|
Description: testData.Description || "",
|
|
SeqScr: testData.SeqScr || 0,
|
|
SeqRpt: testData.SeqRpt || 0,
|
|
IndentLeft: testData.IndentLeft || 0,
|
|
FontStyle: testData.FontStyle || "",
|
|
VisibleScr: testData.VisibleScr !== undefined ? testData.VisibleScr : 1,
|
|
VisibleRpt: testData.VisibleRpt !== undefined ? testData.VisibleRpt : 1,
|
|
CountStat: testData.CountStat !== undefined ? testData.CountStat : 1,
|
|
StartDate: testData.StartDate ? testData.StartDate.split('T')[0] : "",
|
|
// Technical fields
|
|
DisciplineID: "",
|
|
DepartmentID: "",
|
|
WorkstationID: "",
|
|
EquipmentID: "",
|
|
ResultType: "",
|
|
RefType: "",
|
|
VSet: "",
|
|
SpcType: "",
|
|
SpcDesc: "",
|
|
ReqQty: "",
|
|
ReqQtyUnit: "mL",
|
|
Unit1: "",
|
|
Factor: "",
|
|
Unit2: "",
|
|
Decimal: 2,
|
|
CollReq: "",
|
|
Method: "",
|
|
ExpectedTAT: "",
|
|
// GROUP fields
|
|
members: [],
|
|
// CALC fields
|
|
FormulaInput: "",
|
|
FormulaCode: "",
|
|
// Mapping fields
|
|
testmap: []
|
|
};
|
|
|
|
// Load technical/calculation/group details based on type
|
|
if (typeCode === 'CALC' && testData.testdefcal && testData.testdefcal.length > 0) {
|
|
const calData = testData.testdefcal[0];
|
|
this.form.DisciplineID = calData.DisciplineID || "";
|
|
this.form.DepartmentID = calData.DepartmentID || "";
|
|
this.form.FormulaInput = calData.FormulaInput || "";
|
|
this.form.FormulaCode = calData.FormulaCode || "";
|
|
this.form.Unit1 = calData.Unit1 || "";
|
|
this.form.Unit2 = calData.Unit2 || "";
|
|
this.form.Decimal = calData.Decimal || 2;
|
|
this.form.Method = calData.Method || "";
|
|
} else if (typeCode === 'GROUP' && testData.testdefgrp) {
|
|
this.form.members = testData.testdefgrp.map(m => ({
|
|
TestSiteID: m.Member,
|
|
TestSiteCode: m.TestSiteCode || "",
|
|
TestSiteName: m.TestSiteName || "",
|
|
MemberTypeCode: m.MemberTypeCode || ""
|
|
}));
|
|
} else if (['TEST', 'PARAM'].includes(typeCode) && testData.testdeftech && testData.testdeftech.length > 0) {
|
|
const techData = testData.testdeftech[0];
|
|
this.form.DisciplineID = techData.DisciplineID || "";
|
|
this.form.DepartmentID = techData.DepartmentID || "";
|
|
this.form.WorkstationID = techData.WorkstationID || "";
|
|
this.form.EquipmentID = techData.EquipmentID || "";
|
|
this.form.ResultType = techData.ResultType || "";
|
|
this.form.RefType = techData.RefType || "";
|
|
this.form.VSet = techData.VSet || "";
|
|
this.form.SpcType = techData.SpcType || "";
|
|
this.form.SpcDesc = techData.SpcDesc || "";
|
|
this.form.ReqQty = techData.ReqQty || "";
|
|
this.form.ReqQtyUnit = techData.ReqQtyUnit || "mL";
|
|
this.form.Unit1 = techData.Unit1 || "";
|
|
this.form.Factor = techData.Factor || "";
|
|
this.form.Unit2 = techData.Unit2 || "";
|
|
this.form.Decimal = techData.Decimal || 2;
|
|
this.form.CollReq = techData.CollReq || "";
|
|
this.form.Method = techData.Method || "";
|
|
this.form.ExpectedTAT = techData.ExpectedTAT || "";
|
|
}
|
|
|
|
// Load test mappings
|
|
if (testData.testmap) {
|
|
this.form.testmap = testData.testmap;
|
|
}
|
|
|
|
this.$nextTick(() => {
|
|
if (this.typesList.length > 0) {
|
|
const found = this.typesList.find(t => String(t.VID) === String(testType));
|
|
if (found) {
|
|
this.form.TestType = found.VID;
|
|
this.form.TestTypeName = found.VDesc;
|
|
}
|
|
} else {
|
|
this.form.TestType = testType;
|
|
}
|
|
});
|
|
this.showModal = true;
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Failed to load lab test data');
|
|
}
|
|
},
|
|
|
|
// Get type code by ID
|
|
getTypeCodeById(typeId) {
|
|
const idMap = {
|
|
100: 'TEST',
|
|
101: 'PARAM',
|
|
102: 'GROUP',
|
|
103: 'CALC',
|
|
104: 'TITLE'
|
|
};
|
|
// Try to find in typesList
|
|
if (this.typesList.length > 0) {
|
|
const found = this.typesList.find(t => t.VID == typeId);
|
|
if (found) return found.VCode;
|
|
}
|
|
return idMap[typeId] || 'TEST';
|
|
},
|
|
|
|
// 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";
|
|
if (!this.form.SiteID) e.SiteID = "Site is required";
|
|
|
|
// Validate formula for CALC type
|
|
if (this.form.TypeCode === 'CALC' && !this.form.FormulaCode?.trim()) {
|
|
e.FormulaCode = "Formula is required for calculated tests";
|
|
}
|
|
|
|
// Validate members for GROUP type
|
|
if (this.form.TypeCode === 'GROUP' && (!this.form.members || this.form.members.length === 0)) {
|
|
e.members = "At least one member is required for group tests";
|
|
}
|
|
|
|
this.errors = e;
|
|
return Object.keys(e).length === 0;
|
|
},
|
|
|
|
// Close modal
|
|
closeModal() {
|
|
this.showModal = false;
|
|
this.errors = {};
|
|
},
|
|
|
|
// Save test
|
|
async save() {
|
|
if (!this.validate()) return;
|
|
|
|
this.saving = true;
|
|
try {
|
|
const method = this.isEditing ? 'PUT' : 'POST';
|
|
|
|
// Prepare payload based on test type
|
|
const payload = {
|
|
SiteID: this.form.SiteID,
|
|
TestSiteCode: this.form.TestSiteCode,
|
|
TestSiteName: this.form.TestSiteName,
|
|
TestType: this.form.TestType,
|
|
Description: this.form.Description,
|
|
SeqScr: this.form.SeqScr,
|
|
SeqRpt: this.form.SeqRpt,
|
|
IndentLeft: this.form.IndentLeft,
|
|
FontStyle: this.form.FontStyle,
|
|
VisibleScr: this.form.VisibleScr,
|
|
VisibleRpt: this.form.VisibleRpt,
|
|
CountStat: this.form.CountStat,
|
|
StartDate: this.form.StartDate
|
|
};
|
|
|
|
// Add type-specific details
|
|
if (this.form.TypeCode === 'CALC') {
|
|
payload.details = {
|
|
DisciplineID: this.form.DisciplineID,
|
|
DepartmentID: this.form.DepartmentID,
|
|
FormulaInput: this.form.FormulaInput,
|
|
FormulaCode: this.form.FormulaCode,
|
|
RefType: this.form.RefType || 'NMRC',
|
|
Unit1: this.form.Unit1,
|
|
Factor: this.form.Factor,
|
|
Unit2: this.form.Unit2,
|
|
Decimal: this.form.Decimal,
|
|
Method: this.form.Method
|
|
};
|
|
} else if (this.form.TypeCode === 'GROUP') {
|
|
payload.details = {
|
|
members: this.form.members.map(m => ({
|
|
Member: m.TestSiteID
|
|
}))
|
|
};
|
|
} else if (['TEST', 'PARAM'].includes(this.form.TypeCode)) {
|
|
payload.details = {
|
|
DisciplineID: this.form.DisciplineID,
|
|
DepartmentID: this.form.DepartmentID,
|
|
WorkstationID: this.form.WorkstationID,
|
|
EquipmentID: this.form.EquipmentID,
|
|
ResultType: this.form.ResultType,
|
|
RefType: this.form.RefType,
|
|
VSet: this.form.VSet,
|
|
SpcType: this.form.SpcType,
|
|
SpcDesc: this.form.SpcDesc,
|
|
ReqQty: this.form.ReqQty,
|
|
ReqQtyUnit: this.form.ReqQtyUnit,
|
|
Unit1: this.form.Unit1,
|
|
Factor: this.form.Factor,
|
|
Unit2: this.form.Unit2,
|
|
Decimal: this.form.Decimal,
|
|
CollReq: this.form.CollReq,
|
|
Method: this.form.Method,
|
|
ExpectedTAT: this.form.ExpectedTAT
|
|
};
|
|
}
|
|
|
|
// Add test mappings if present
|
|
if (this.form.testmap && this.form.testmap.length > 0) {
|
|
payload.testmap = this.form.testmap;
|
|
}
|
|
|
|
const res = await fetch(`${BASEURL}api/tests${this.isEditing ? '/' + this.form.TestSiteID : ''}`, {
|
|
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 lab test");
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
// Confirm delete
|
|
confirmDelete(test) {
|
|
this.deleteTarget = test;
|
|
this.showDeleteModal = true;
|
|
},
|
|
|
|
// Get badge class for test type
|
|
getTestTypeBadgeClass(typeCode) {
|
|
const colorMap = {
|
|
'GROUP': 'badge-primary',
|
|
'CALC': 'badge-secondary',
|
|
'TEST': 'badge-accent',
|
|
'PARAM': 'badge-info',
|
|
'TITLE': 'badge-warning'
|
|
};
|
|
return colorMap[typeCode] || 'badge-ghost';
|
|
},
|
|
|
|
// 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 {
|
|
alert("Failed to delete");
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("Failed to delete lab test");
|
|
} finally {
|
|
this.deleting = false;
|
|
this.deleteTarget = null;
|
|
}
|
|
},
|
|
|
|
// ========== GROUP Dialog Helper Functions ==========
|
|
|
|
// Open test selector for group members
|
|
async openTestSelector() {
|
|
// Use existing list of tests, filter for TEST and PARAM types only
|
|
this.availableTests = this.list.filter(t =>
|
|
t.TypeCode === 'TEST' || t.TypeCode === 'PARAM'
|
|
);
|
|
this.selectedTestIds = this.form.members?.map(m => m.TestSiteID) || [];
|
|
this.showTestSelector = true;
|
|
},
|
|
|
|
// Check if test is selected
|
|
isTestSelected(testId) {
|
|
return this.selectedTestIds.includes(testId);
|
|
},
|
|
|
|
// Toggle test selection
|
|
toggleTestSelection(test) {
|
|
const index = this.selectedTestIds.indexOf(test.TestSiteID);
|
|
if (index > -1) {
|
|
this.selectedTestIds.splice(index, 1);
|
|
} else {
|
|
this.selectedTestIds.push(test.TestSiteID);
|
|
}
|
|
},
|
|
|
|
// Confirm test selection and add to members
|
|
confirmTestSelection() {
|
|
const newMembers = this.availableTests.filter(t =>
|
|
this.selectedTestIds.includes(t.TestSiteID) &&
|
|
!this.form.members?.some(m => m.TestSiteID === t.TestSiteID)
|
|
);
|
|
|
|
newMembers.forEach(test => {
|
|
this.form.members.push({
|
|
TestSiteID: test.TestSiteID,
|
|
TestSiteCode: test.TestSiteCode,
|
|
TestSiteName: test.TestSiteName,
|
|
SeqScr: test.SeqScr || 0
|
|
});
|
|
});
|
|
|
|
this.showTestSelector = false;
|
|
},
|
|
|
|
// Remove member from group
|
|
removeMember(index) {
|
|
this.form.members.splice(index, 1);
|
|
},
|
|
|
|
// Helper function to check if value is in array
|
|
inArray(value, array) {
|
|
return array.includes(value);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<?= $this->endSection() ?>
|