feat: add test map management module
This commit is contained in:
parent
3b8a935b46
commit
beb3235470
178
src/lib/api/testmap.js
Normal file
178
src/lib/api/testmap.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { get, post, patch, del } from './client.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} TestMap
|
||||||
|
* @property {number} TestMapID - Mapping ID
|
||||||
|
* @property {number} TestSiteID - Test Site ID
|
||||||
|
* @property {string} TestSiteCode - Test Code
|
||||||
|
* @property {string} TestSiteName - Test Name
|
||||||
|
* @property {string} HostType - Host Type (HIS, SITE, WST, INST)
|
||||||
|
* @property {string} HostID - Host ID
|
||||||
|
* @property {string} HostTestCode - Host Test Code
|
||||||
|
* @property {string} HostTestName - Host Test Name
|
||||||
|
* @property {string} ClientType - Client Type (HIS, SITE, WST, INST)
|
||||||
|
* @property {string} ClientID - Client ID
|
||||||
|
* @property {number} ConDefID - Container Definition ID
|
||||||
|
* @property {string} ClientTestCode - Client Test Code
|
||||||
|
* @property {string} ClientTestName - Client Test Name
|
||||||
|
* @property {string} IsActive - Active status ('1' or '0')
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} TestMapFilterOptions
|
||||||
|
* @property {string} [hostType] - Filter by Host Type
|
||||||
|
* @property {string} [hostID] - Filter by Host ID
|
||||||
|
* @property {string} [clientType] - Filter by Client Type
|
||||||
|
* @property {string} [clientID] - Filter by Client ID
|
||||||
|
* @property {string} [search] - Search term
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} TestMapListResponse
|
||||||
|
* @property {boolean} success
|
||||||
|
* @property {TestMap[]} data
|
||||||
|
* @property {string} [message]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} TestMapResponse
|
||||||
|
* @property {boolean} success
|
||||||
|
* @property {TestMap} data
|
||||||
|
* @property {string} [message]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} CreateTestMapPayload
|
||||||
|
* @property {number} TestSiteID - Test Site ID
|
||||||
|
* @property {string} HostType - Host Type
|
||||||
|
* @property {string} HostID - Host ID
|
||||||
|
* @property {string} HostTestCode - Host Test Code
|
||||||
|
* @property {string} HostTestName - Host Test Name
|
||||||
|
* @property {string} ClientType - Client Type
|
||||||
|
* @property {string} ClientID - Client ID
|
||||||
|
* @property {number} ConDefID - Container Definition ID
|
||||||
|
* @property {string} ClientTestCode - Client Test Code
|
||||||
|
* @property {string} ClientTestName - Client Test Name
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} UpdateTestMapPayload
|
||||||
|
* @property {number} TestMapID - Mapping ID
|
||||||
|
* @property {number} TestSiteID - Test Site ID
|
||||||
|
* @property {string} HostType - Host Type
|
||||||
|
* @property {string} HostID - Host ID
|
||||||
|
* @property {string} HostTestCode - Host Test Code
|
||||||
|
* @property {string} HostTestName - Host Test Name
|
||||||
|
* @property {string} ClientType - Client Type
|
||||||
|
* @property {string} ClientID - Client ID
|
||||||
|
* @property {number} ConDefID - Container Definition ID
|
||||||
|
* @property {string} ClientTestCode - Client Test Code
|
||||||
|
* @property {string} ClientTestName - Client Test Name
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all test mappings
|
||||||
|
* @returns {Promise<TestMapListResponse>} API response with mappings list
|
||||||
|
*/
|
||||||
|
export async function fetchTestMaps() {
|
||||||
|
return get('/api/test/testmap');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single test mapping by ID
|
||||||
|
* @param {number} id - Test Map ID
|
||||||
|
* @returns {Promise<TestMapResponse>} API response with mapping detail
|
||||||
|
*/
|
||||||
|
export async function fetchTestMap(id) {
|
||||||
|
return get(`/api/test/testmap/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch test mappings by test site ID
|
||||||
|
* @param {number} testSiteID - Test Site ID
|
||||||
|
* @returns {Promise<TestMapListResponse>} API response with mappings list
|
||||||
|
*/
|
||||||
|
export async function fetchTestMapsByTestSite(testSiteID) {
|
||||||
|
return get(`/api/test/testmap/by-testsite/${testSiteID}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch test mappings by host
|
||||||
|
* @param {string} hostType - Host Type
|
||||||
|
* @param {string} hostID - Host ID
|
||||||
|
* @returns {Promise<TestMapListResponse>} API response with mappings list
|
||||||
|
*/
|
||||||
|
export async function fetchTestMapsByHost(hostType, hostID) {
|
||||||
|
return get(`/api/test/testmap/by-host/${hostType}/${hostID}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch test mappings by client
|
||||||
|
* @param {string} clientType - Client Type
|
||||||
|
* @param {string} clientID - Client ID
|
||||||
|
* @returns {Promise<TestMapListResponse>} API response with mappings list
|
||||||
|
*/
|
||||||
|
export async function fetchTestMapsByClient(clientType, clientID) {
|
||||||
|
return get(`/api/test/testmap/by-client/${clientType}/${clientID}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new test mapping
|
||||||
|
* @param {CreateTestMapPayload} data - Mapping data
|
||||||
|
* @returns {Promise<TestMapResponse>} API response
|
||||||
|
*/
|
||||||
|
export async function createTestMap(data) {
|
||||||
|
return post('/api/test/testmap', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing test mapping
|
||||||
|
* @param {UpdateTestMapPayload} data - Mapping data
|
||||||
|
* @returns {Promise<TestMapResponse>} API response
|
||||||
|
*/
|
||||||
|
export async function updateTestMap(data) {
|
||||||
|
return patch('/api/test/testmap', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft delete a test mapping (set IsActive to '0')
|
||||||
|
* @param {number} id - Test Map ID
|
||||||
|
* @returns {Promise<{success: boolean, message?: string}>} API response
|
||||||
|
*/
|
||||||
|
export async function deleteTestMap(id) {
|
||||||
|
return del('/api/test/testmap', { body: JSON.stringify({ TestMapID: id }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate mapping form data
|
||||||
|
* @param {Object} data - Form data to validate
|
||||||
|
* @returns {{valid: boolean, errors: Object}}
|
||||||
|
*/
|
||||||
|
export function validateTestMap(data) {
|
||||||
|
const errors = {};
|
||||||
|
|
||||||
|
if (!data.TestSiteID) {
|
||||||
|
errors.TestSiteID = 'Test is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.HostType) {
|
||||||
|
errors.HostType = 'Host Type is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.HostID) {
|
||||||
|
errors.HostID = 'Host ID is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.ClientType) {
|
||||||
|
errors.ClientType = 'Client Type is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.ClientID) {
|
||||||
|
errors.ClientID = 'Client ID is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: Object.keys(errors).length === 0,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -18,9 +18,9 @@ import {
|
|||||||
Globe,
|
Globe,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
TestTube,
|
TestTube,
|
||||||
ShieldCheck,
|
|
||||||
FileText,
|
FileText,
|
||||||
X
|
X,
|
||||||
|
Link
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { auth } from '$lib/stores/auth.js';
|
import { auth } from '$lib/stores/auth.js';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
@ -30,9 +30,7 @@ import {
|
|||||||
|
|
||||||
// Collapsible section states - default collapsed
|
// Collapsible section states - default collapsed
|
||||||
let laboratoryExpanded = $state(false);
|
let laboratoryExpanded = $state(false);
|
||||||
let qualityControlExpanded = $state(false);
|
|
||||||
let masterDataExpanded = $state(false);
|
let masterDataExpanded = $state(false);
|
||||||
let administrationExpanded = $state(false);
|
|
||||||
|
|
||||||
// Load states from localStorage on mount
|
// Load states from localStorage on mount
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@ -42,9 +40,7 @@ import {
|
|||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(savedStates);
|
const parsed = JSON.parse(savedStates);
|
||||||
laboratoryExpanded = parsed.laboratory ?? false;
|
laboratoryExpanded = parsed.laboratory ?? false;
|
||||||
qualityControlExpanded = parsed.qualityControl ?? false;
|
|
||||||
masterDataExpanded = parsed.masterData ?? false;
|
masterDataExpanded = parsed.masterData ?? false;
|
||||||
administrationExpanded = parsed.administration ?? false;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Keep defaults if parsing fails
|
// Keep defaults if parsing fails
|
||||||
}
|
}
|
||||||
@ -57,9 +53,7 @@ import {
|
|||||||
if (browser) {
|
if (browser) {
|
||||||
localStorage.setItem('sidebar_section_states', JSON.stringify({
|
localStorage.setItem('sidebar_section_states', JSON.stringify({
|
||||||
laboratory: laboratoryExpanded,
|
laboratory: laboratoryExpanded,
|
||||||
qualityControl: qualityControlExpanded,
|
masterData: masterDataExpanded
|
||||||
masterData: masterDataExpanded,
|
|
||||||
administration: administrationExpanded
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -68,9 +62,7 @@ import {
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
laboratoryExpanded = false;
|
laboratoryExpanded = false;
|
||||||
qualityControlExpanded = false;
|
|
||||||
masterDataExpanded = false;
|
masterDataExpanded = false;
|
||||||
administrationExpanded = false;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -96,26 +88,12 @@ function toggleLaboratory() {
|
|||||||
laboratoryExpanded = !laboratoryExpanded;
|
laboratoryExpanded = !laboratoryExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleQualityControl() {
|
|
||||||
if (!isOpen) {
|
|
||||||
expandSidebar();
|
|
||||||
}
|
|
||||||
qualityControlExpanded = !qualityControlExpanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleMasterData() {
|
function toggleMasterData() {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
expandSidebar();
|
expandSidebar();
|
||||||
}
|
}
|
||||||
masterDataExpanded = !masterDataExpanded;
|
masterDataExpanded = !masterDataExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAdministration() {
|
|
||||||
if (!isOpen) {
|
|
||||||
expandSidebar();
|
|
||||||
}
|
|
||||||
administrationExpanded = !administrationExpanded;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Mobile Overlay Backdrop -->
|
<!-- Mobile Overlay Backdrop -->
|
||||||
@ -195,28 +173,6 @@ function toggleLaboratory() {
|
|||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Quality Control -->
|
|
||||||
<li class="nav-group" class:collapsed={!isOpen}>
|
|
||||||
<button
|
|
||||||
onclick={toggleQualityControl}
|
|
||||||
class="nav-link"
|
|
||||||
class:centered={!isOpen}
|
|
||||||
title={!isOpen ? 'Quality Control' : ''}
|
|
||||||
>
|
|
||||||
<ShieldCheck size={20} class="text-secondary flex-shrink-0" />
|
|
||||||
{#if isOpen}
|
|
||||||
<span class="nav-text">Quality Control</span>
|
|
||||||
<ChevronDown size={16} class="chevron {qualityControlExpanded ? 'expanded' : ''}" />
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if isOpen && qualityControlExpanded}
|
|
||||||
<ul class="submenu">
|
|
||||||
<li><a href="/master-data/tests" class="submenu-link"><TestTube size={16} /> Test Definitions</a></li>
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<li class="menu-title uppercase font-bold text-xs text-secondary/70 mt-4">Analytics</li>
|
<li class="menu-title uppercase font-bold text-xs text-secondary/70 mt-4">Analytics</li>
|
||||||
{/if}
|
{/if}
|
||||||
@ -259,36 +215,16 @@ function toggleLaboratory() {
|
|||||||
<ul class="submenu">
|
<ul class="submenu">
|
||||||
<li><a href="/master-data/organization" class="submenu-link"><Building2 size={16} /> Organization</a></li>
|
<li><a href="/master-data/organization" class="submenu-link"><Building2 size={16} /> Organization</a></li>
|
||||||
<li><a href="/master-data/containers" class="submenu-link"><FlaskConical size={16} /> Containers</a></li>
|
<li><a href="/master-data/containers" class="submenu-link"><FlaskConical size={16} /> Containers</a></li>
|
||||||
|
<li><a href="/master-data/tests" class="submenu-link"><TestTube size={16} /> Test Definitions</a></li>
|
||||||
|
<li><a href="/master-data/testmap" class="submenu-link"><Link size={16} /> Test Mapping</a></li>
|
||||||
|
<li><a href="/master-data/users" class="submenu-link"><UserCircle size={16} /> Users</a></li>
|
||||||
<li><a href="/master-data/valuesets" class="submenu-link"><List size={16} /> ValueSets</a></li>
|
<li><a href="/master-data/valuesets" class="submenu-link"><List size={16} /> ValueSets</a></li>
|
||||||
<li><a href="/master-data/locations" class="submenu-link"><MapPin size={16} /> Locations</a></li>
|
<li><a href="/master-data/locations" class="submenu-link"><MapPin size={16} /> Locations</a></li>
|
||||||
<li><a href="/master-data/contacts" class="submenu-link"><Users size={16} /> Contacts</a></li>
|
<li><a href="/master-data/contacts" class="submenu-link"><Users size={16} /> Contacts</a></li>
|
||||||
<li><a href="/master-data/specialties" class="submenu-link"><Stethoscope size={16} /> Specialties</a></li>
|
<li><a href="/master-data/specialties" class="submenu-link"><Stethoscope size={16} /> Specialties</a></li>
|
||||||
<li><a href="/master-data/occupations" class="submenu-link"><Briefcase size={16} /> Occupations</a></li>
|
<li><a href="/master-data/occupations" class="submenu-link"><Briefcase size={16} /> Occupations</a></li>
|
||||||
<li><a href="/master-data/geography" class="submenu-link"><Globe size={16} /> Geography</a></li>
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Administration -->
|
|
||||||
<li class="nav-group" class:collapsed={!isOpen}>
|
|
||||||
<button
|
|
||||||
onclick={toggleAdministration}
|
|
||||||
class="nav-link"
|
|
||||||
class:centered={!isOpen}
|
|
||||||
title={!isOpen ? 'Administration' : ''}
|
|
||||||
>
|
|
||||||
<Building2 size={20} class="text-secondary flex-shrink-0" />
|
|
||||||
{#if isOpen}
|
|
||||||
<span class="nav-text">Administration</span>
|
|
||||||
<ChevronDown size={16} class="chevron {administrationExpanded ? 'expanded' : ''}" />
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if isOpen && administrationExpanded}
|
|
||||||
<ul class="submenu">
|
|
||||||
<li><a href="/organization" class="submenu-link"><Building2 size={16} /> Organization</a></li>
|
|
||||||
<li><a href="/users" class="submenu-link"><UserCircle size={16} /> Users</a></li>
|
|
||||||
<li><a href="/master-data/counters" class="submenu-link"><Hash size={16} /> Counters</a></li>
|
<li><a href="/master-data/counters" class="submenu-link"><Hash size={16} /> Counters</a></li>
|
||||||
|
<li><a href="/master-data/geography" class="submenu-link"><Globe size={16} /> Geography</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
329
src/routes/(app)/master-data/testmap/+page.svelte
Normal file
329
src/routes/(app)/master-data/testmap/+page.svelte
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import {
|
||||||
|
fetchTestMaps,
|
||||||
|
deleteTestMap,
|
||||||
|
} from '$lib/api/testmap.js';
|
||||||
|
import { fetchContainers } from '$lib/api/containers.js';
|
||||||
|
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
|
||||||
|
import DataTable from '$lib/components/DataTable.svelte';
|
||||||
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
|
import TestMapModal from './TestMapModal.svelte';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Edit2,
|
||||||
|
Trash2,
|
||||||
|
ArrowLeft,
|
||||||
|
Link,
|
||||||
|
Server,
|
||||||
|
Monitor,
|
||||||
|
Filter,
|
||||||
|
X,
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
|
let loading = $state(false);
|
||||||
|
let testMaps = $state([]);
|
||||||
|
let containers = $state([]);
|
||||||
|
let modalOpen = $state(false);
|
||||||
|
let modalMode = $state('create');
|
||||||
|
let modalData = $state(null);
|
||||||
|
let deleting = $state(false);
|
||||||
|
let deleteConfirmOpen = $state(false);
|
||||||
|
let deleteItem = $state(null);
|
||||||
|
|
||||||
|
// Filter states
|
||||||
|
let filterHostType = $state('');
|
||||||
|
let filterHostID = $state('');
|
||||||
|
let filterClientType = $state('');
|
||||||
|
let filterClientID = $state('');
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ key: 'HostType', label: 'Host Type', class: 'w-20' },
|
||||||
|
{ key: 'HostID', label: 'Host ID', class: 'w-32' },
|
||||||
|
{ key: 'ClientType', label: 'Client Type', class: 'w-24' },
|
||||||
|
{ key: 'ClientID', label: 'Client ID', class: 'w-32' },
|
||||||
|
{ key: 'actions', label: 'Actions', class: 'w-24 text-center' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Derived filtered test maps
|
||||||
|
let filteredTestMaps = $derived(
|
||||||
|
testMaps.filter((mapping) => {
|
||||||
|
const matchesHostType =
|
||||||
|
!filterHostType ||
|
||||||
|
(mapping.HostType && mapping.HostType.toLowerCase().includes(filterHostType.toLowerCase()));
|
||||||
|
const matchesHostID =
|
||||||
|
!filterHostID ||
|
||||||
|
(mapping.HostID && mapping.HostID.toLowerCase().includes(filterHostID.toLowerCase()));
|
||||||
|
const matchesClientType =
|
||||||
|
!filterClientType ||
|
||||||
|
(mapping.ClientType && mapping.ClientType.toLowerCase().includes(filterClientType.toLowerCase()));
|
||||||
|
const matchesClientID =
|
||||||
|
!filterClientID ||
|
||||||
|
(mapping.ClientID && mapping.ClientID.toLowerCase().includes(filterClientID.toLowerCase()));
|
||||||
|
|
||||||
|
return matchesHostType && matchesHostID && matchesClientType && matchesClientID;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await Promise.all([loadTestMaps(), loadContainers()]);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadTestMaps() {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const response = await fetchTestMaps();
|
||||||
|
testMaps = Array.isArray(response.data) ? response.data : [];
|
||||||
|
} catch (err) {
|
||||||
|
toastError(err.message || 'Failed to load test mappings');
|
||||||
|
testMaps = [];
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadContainers() {
|
||||||
|
try {
|
||||||
|
const response = await fetchContainers();
|
||||||
|
containers = Array.isArray(response.data) ? response.data : [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load containers:', err);
|
||||||
|
containers = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateModal() {
|
||||||
|
modalMode = 'create';
|
||||||
|
modalData = null;
|
||||||
|
modalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditModal(row) {
|
||||||
|
modalMode = 'edit';
|
||||||
|
modalData = row;
|
||||||
|
modalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModalSave() {
|
||||||
|
loadTestMaps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete(row) {
|
||||||
|
deleteItem = row;
|
||||||
|
deleteConfirmOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
deleting = true;
|
||||||
|
try {
|
||||||
|
await deleteTestMap(deleteItem.TestMapID);
|
||||||
|
toastSuccess('Test mapping deleted successfully');
|
||||||
|
deleteConfirmOpen = false;
|
||||||
|
deleteItem = null;
|
||||||
|
await loadTestMaps();
|
||||||
|
} catch (err) {
|
||||||
|
toastError(err.message || 'Failed to delete test mapping');
|
||||||
|
} finally {
|
||||||
|
deleting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
filterHostType = '';
|
||||||
|
filterHostID = '';
|
||||||
|
filterClientType = '';
|
||||||
|
filterClientID = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex items-center gap-4 mb-6">
|
||||||
|
<a href="/master-data" class="btn btn-ghost btn-circle">
|
||||||
|
<ArrowLeft class="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h1 class="text-xl font-bold text-gray-800">Test Mapping</h1>
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
Manage test mappings between host and client systems
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick={openCreateModal}>
|
||||||
|
<Plus class="w-4 h-4 mr-2" />
|
||||||
|
Add Mapping
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="bg-base-100 rounded-lg shadow border border-base-200 p-4 mb-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||||
|
<Filter class="w-4 h-4" />
|
||||||
|
Filters
|
||||||
|
</h3>
|
||||||
|
<button class="btn btn-sm btn-ghost" onclick={clearFilters}>
|
||||||
|
<X class="w-3 h-3 mr-1" />
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- Host Filters -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h4 class="text-xs font-medium text-gray-500 uppercase flex items-center gap-2">
|
||||||
|
<Server class="w-3 h-3" />
|
||||||
|
Host
|
||||||
|
</h4>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Type"
|
||||||
|
class="input input-sm input-bordered w-full"
|
||||||
|
bind:value={filterHostType}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="ID"
|
||||||
|
class="input input-sm input-bordered w-full"
|
||||||
|
bind:value={filterHostID}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Client Filters -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h4 class="text-xs font-medium text-gray-500 uppercase flex items-center gap-2">
|
||||||
|
<Monitor class="w-3 h-3" />
|
||||||
|
Client
|
||||||
|
</h4>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Type"
|
||||||
|
class="input input-sm input-bordered w-full"
|
||||||
|
bind:value={filterClientType}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="ID"
|
||||||
|
class="input input-sm input-bordered w-full"
|
||||||
|
bind:value={filterClientID}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data Table -->
|
||||||
|
<div class="bg-base-100 rounded-lg shadow border border-base-200">
|
||||||
|
{#if !loading && filteredTestMaps.length === 0}
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div class="flex flex-col items-center justify-center py-16 px-4">
|
||||||
|
<div class="bg-base-200 rounded-full p-6 mb-4">
|
||||||
|
<Link class="w-12 h-12 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-base font-semibold text-gray-700 mb-2">
|
||||||
|
{#if filterHostType || filterHostID || filterClientType || filterClientID}
|
||||||
|
No mappings match your filters
|
||||||
|
{:else}
|
||||||
|
No test mappings found
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-gray-500 text-center max-w-md mb-6">
|
||||||
|
{#if filterHostType || filterHostID || filterClientType || filterClientID}
|
||||||
|
Try adjusting your filter criteria or clear the filters to see all mappings.
|
||||||
|
{:else}
|
||||||
|
Test mappings connect tests between host systems (like HIS) and client systems. Add your first mapping to get started.
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
{#if filterHostType || filterHostID || filterClientType || filterClientID}
|
||||||
|
<button class="btn btn-outline" onclick={clearFilters}>
|
||||||
|
Clear Filters
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button class="btn btn-primary" onclick={openCreateModal}>
|
||||||
|
<Plus class="w-4 h-4 mr-2" />
|
||||||
|
Add Mapping
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<DataTable
|
||||||
|
{columns}
|
||||||
|
data={filteredTestMaps}
|
||||||
|
{loading}
|
||||||
|
emptyMessage="No test mappings found"
|
||||||
|
hover={true}
|
||||||
|
bordered={false}
|
||||||
|
>
|
||||||
|
{#snippet cell({ column, row, value })}
|
||||||
|
{#if column.key === 'actions'}
|
||||||
|
<div class="flex justify-center gap-1">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-ghost"
|
||||||
|
onclick={() => openEditModal(row)}
|
||||||
|
title="Edit mapping"
|
||||||
|
>
|
||||||
|
<Edit2 class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-ghost text-error"
|
||||||
|
onclick={() => confirmDelete(row)}
|
||||||
|
title="Delete mapping"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{value || '-'}
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</DataTable>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test Mapping Modal -->
|
||||||
|
<TestMapModal
|
||||||
|
bind:open={modalOpen}
|
||||||
|
mode={modalMode}
|
||||||
|
initialData={modalData}
|
||||||
|
{containers}
|
||||||
|
onSave={handleModalSave}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<Modal bind:open={deleteConfirmOpen} title="Confirm Delete Mapping" size="sm">
|
||||||
|
<div class="py-2">
|
||||||
|
<p class="text-base-content/80">Are you sure you want to delete this test mapping?</p>
|
||||||
|
<div class="bg-base-200 rounded-lg p-3 mt-3 space-y-1">
|
||||||
|
<p class="text-sm">
|
||||||
|
<span class="text-gray-500">Host:</span>
|
||||||
|
<strong class="text-base-content">{deleteItem?.HostType} / {deleteItem?.HostID}</strong>
|
||||||
|
</p>
|
||||||
|
<p class="text-sm">
|
||||||
|
<span class="text-gray-500">Client:</span>
|
||||||
|
<strong class="text-base-content">{deleteItem?.ClientType} / {deleteItem?.ClientID}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-error mt-3 flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{#snippet footer()}
|
||||||
|
<button class="btn btn-ghost" onclick={() => (deleteConfirmOpen = false)} type="button" disabled={deleting}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-error" onclick={handleDelete} disabled={deleting} type="button">
|
||||||
|
{#if deleting}
|
||||||
|
<span class="loading loading-spinner loading-sm mr-2"></span>
|
||||||
|
{/if}
|
||||||
|
{deleting ? 'Deleting...' : 'Delete'}
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</Modal>
|
||||||
414
src/routes/(app)/master-data/testmap/TestMapModal.svelte
Normal file
414
src/routes/(app)/master-data/testmap/TestMapModal.svelte
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
<script>
|
||||||
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
|
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
|
||||||
|
import { createTestMap, updateTestMap } from '$lib/api/testmap.js';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Server,
|
||||||
|
Monitor,
|
||||||
|
FlaskConical,
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
|
let { open = $bindable(false), mode = 'create', initialData = null, containers = [], onSave } = $props();
|
||||||
|
|
||||||
|
let saving = $state(false);
|
||||||
|
|
||||||
|
// Modal context (shared for all rows)
|
||||||
|
let modalContext = $state({
|
||||||
|
HostType: '',
|
||||||
|
HostID: '',
|
||||||
|
ClientType: '',
|
||||||
|
ClientID: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Editable rows in modal
|
||||||
|
let modalRows = $state([]);
|
||||||
|
|
||||||
|
// Form errors
|
||||||
|
let formErrors = $state({});
|
||||||
|
|
||||||
|
const hostTypes = ['HIS', 'SITE', 'WST', 'INST'];
|
||||||
|
const clientTypes = ['HIS', 'SITE', 'WST', 'INST'];
|
||||||
|
|
||||||
|
// Initialize modal when open changes
|
||||||
|
$effect(() => {
|
||||||
|
if (open) {
|
||||||
|
initializeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function initializeModal() {
|
||||||
|
formErrors = {};
|
||||||
|
|
||||||
|
if (mode === 'edit' && initialData) {
|
||||||
|
modalContext = {
|
||||||
|
HostType: initialData.HostType || '',
|
||||||
|
HostID: initialData.HostID || '',
|
||||||
|
ClientType: initialData.ClientType || '',
|
||||||
|
ClientID: initialData.ClientID || '',
|
||||||
|
};
|
||||||
|
modalRows = [{
|
||||||
|
TestMapID: initialData.TestMapID,
|
||||||
|
HostTestCode: initialData.HostTestCode || '',
|
||||||
|
HostTestName: initialData.HostTestName || '',
|
||||||
|
ConDefID: initialData.ConDefID || null,
|
||||||
|
ClientTestCode: initialData.ClientTestCode || '',
|
||||||
|
ClientTestName: initialData.ClientTestName || '',
|
||||||
|
isNew: false,
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
modalContext = {
|
||||||
|
HostType: '',
|
||||||
|
HostID: '',
|
||||||
|
ClientType: '',
|
||||||
|
ClientID: '',
|
||||||
|
};
|
||||||
|
modalRows = [createEmptyRow()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyRow() {
|
||||||
|
return {
|
||||||
|
TestMapID: null,
|
||||||
|
HostTestCode: '',
|
||||||
|
HostTestName: '',
|
||||||
|
ConDefID: null,
|
||||||
|
ClientTestCode: '',
|
||||||
|
ClientTestName: '',
|
||||||
|
isNew: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMappingRow() {
|
||||||
|
modalRows = [...modalRows, createEmptyRow()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMappingRow(index) {
|
||||||
|
modalRows = modalRows.filter((_, i) => i !== index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRowField(index, field, value) {
|
||||||
|
modalRows = modalRows.map((row, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
return { ...row, [field]: value };
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateModal() {
|
||||||
|
const errors = {};
|
||||||
|
|
||||||
|
if (!modalContext.HostType) {
|
||||||
|
errors.HostType = 'Host Type is required';
|
||||||
|
}
|
||||||
|
if (!modalContext.HostID) {
|
||||||
|
errors.HostID = 'Host ID is required';
|
||||||
|
}
|
||||||
|
if (!modalContext.ClientType) {
|
||||||
|
errors.ClientType = 'Client Type is required';
|
||||||
|
}
|
||||||
|
if (!modalContext.ClientID) {
|
||||||
|
errors.ClientID = 'Client ID is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each row
|
||||||
|
const rowErrors = [];
|
||||||
|
modalRows.forEach((row, index) => {
|
||||||
|
const rowError = {};
|
||||||
|
const hasAnyField = row.HostTestCode || row.HostTestName || row.ClientTestCode || row.ClientTestName;
|
||||||
|
if (!hasAnyField) {
|
||||||
|
rowError.empty = 'At least one field must be filled';
|
||||||
|
}
|
||||||
|
if (modalContext.ClientType === 'INST' && !row.ConDefID) {
|
||||||
|
rowError.ConDefID = 'Container is required for INST type';
|
||||||
|
}
|
||||||
|
if (Object.keys(rowError).length > 0) {
|
||||||
|
rowErrors[index] = rowError;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rowErrors.length > 0) {
|
||||||
|
errors.rows = rowErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
formErrors = errors;
|
||||||
|
return Object.keys(errors).length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!validateModal()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saving = true;
|
||||||
|
try {
|
||||||
|
const promises = modalRows.map(async (row) => {
|
||||||
|
const payload = {
|
||||||
|
HostType: modalContext.HostType,
|
||||||
|
HostID: modalContext.HostID,
|
||||||
|
HostTestCode: row.HostTestCode,
|
||||||
|
HostTestName: row.HostTestName,
|
||||||
|
ClientType: modalContext.ClientType,
|
||||||
|
ClientID: modalContext.ClientID,
|
||||||
|
ConDefID: modalContext.ClientType === 'INST' ? row.ConDefID : null,
|
||||||
|
ClientTestCode: row.ClientTestCode,
|
||||||
|
ClientTestName: row.ClientTestName,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mode === 'create' || row.isNew) {
|
||||||
|
return createTestMap(payload);
|
||||||
|
} else {
|
||||||
|
return updateTestMap({ ...payload, TestMapID: row.TestMapID });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
toastSuccess(`Test mapping${modalRows.length > 1 ? 's' : ''} saved successfully`);
|
||||||
|
open = false;
|
||||||
|
onSave?.();
|
||||||
|
} catch (err) {
|
||||||
|
toastError(err.message || 'Failed to save test mapping');
|
||||||
|
} finally {
|
||||||
|
saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContainerName(conDefId) {
|
||||||
|
if (!conDefId) return '-';
|
||||||
|
const container = containers.find((c) => c.ConDefID === conDefId);
|
||||||
|
return container ? container.ConName : '-';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
bind:open
|
||||||
|
title={mode === 'create' ? 'Add Test Mapping' : 'Edit Test Mapping'}
|
||||||
|
size="xl"
|
||||||
|
>
|
||||||
|
<div class="space-y-4 max-h-[70vh] overflow-y-auto">
|
||||||
|
<!-- Top Section: Host and Client side-by-side -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 border-b border-base-300 pb-4">
|
||||||
|
<!-- Host Section -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h3 class="text-xs font-medium text-gray-600 uppercase tracking-wide flex items-center gap-1">
|
||||||
|
<Server class="w-3 h-3" />
|
||||||
|
Host
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label text-xs" for="hostType">
|
||||||
|
<span class="label-text">Type</span>
|
||||||
|
<span class="label-text-alt text-error">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="hostType"
|
||||||
|
class="select select-xs select-bordered"
|
||||||
|
bind:value={modalContext.HostType}
|
||||||
|
disabled={mode === 'edit'}
|
||||||
|
>
|
||||||
|
<option value="">Select...</option>
|
||||||
|
{#each hostTypes as type (type)}
|
||||||
|
<option value={type}>{type}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{#if formErrors.HostType}
|
||||||
|
<span class="text-xs text-error">{formErrors.HostType}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label text-xs" for="hostID">
|
||||||
|
<span class="label-text">ID</span>
|
||||||
|
<span class="label-text-alt text-error">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="hostID"
|
||||||
|
type="text"
|
||||||
|
class="input input-xs input-bordered"
|
||||||
|
bind:value={modalContext.HostID}
|
||||||
|
placeholder="Host ID"
|
||||||
|
disabled={mode === 'edit'}
|
||||||
|
/>
|
||||||
|
{#if formErrors.HostID}
|
||||||
|
<span class="text-xs text-error">{formErrors.HostID}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Client Section -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h3 class="text-xs font-medium text-gray-600 uppercase tracking-wide flex items-center gap-1">
|
||||||
|
<Monitor class="w-3 h-3" />
|
||||||
|
Client
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label text-xs" for="clientType">
|
||||||
|
<span class="label-text">Type</span>
|
||||||
|
<span class="label-text-alt text-error">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="clientType"
|
||||||
|
class="select select-xs select-bordered"
|
||||||
|
bind:value={modalContext.ClientType}
|
||||||
|
disabled={mode === 'edit'}
|
||||||
|
>
|
||||||
|
<option value="">Select...</option>
|
||||||
|
{#each clientTypes as type (type)}
|
||||||
|
<option value={type}>{type}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{#if formErrors.ClientType}
|
||||||
|
<span class="text-xs text-error">{formErrors.ClientType}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label text-xs" for="clientID">
|
||||||
|
<span class="label-text">ID</span>
|
||||||
|
<span class="label-text-alt text-error">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="clientID"
|
||||||
|
type="text"
|
||||||
|
class="input input-xs input-bordered"
|
||||||
|
bind:value={modalContext.ClientID}
|
||||||
|
placeholder="Client ID"
|
||||||
|
disabled={mode === 'edit'}
|
||||||
|
/>
|
||||||
|
{#if formErrors.ClientID}
|
||||||
|
<span class="text-xs text-error">{formErrors.ClientID}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle Section: Editable Table -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h3 class="text-xs font-medium text-gray-600 uppercase tracking-wide flex items-center gap-1">
|
||||||
|
<FlaskConical class="w-3 h-3" />
|
||||||
|
Test Mappings
|
||||||
|
<span class="text-gray-400 font-normal">({modalRows.length} row{modalRows.length !== 1 ? 's' : ''})</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto border border-base-300 rounded-lg">
|
||||||
|
<table class="table table-compact w-full">
|
||||||
|
<thead class="bg-base-200">
|
||||||
|
<tr>
|
||||||
|
<th class="text-xs">Host Test Code</th>
|
||||||
|
<th class="text-xs">Host Test Name</th>
|
||||||
|
<th class="text-xs w-48">Container</th>
|
||||||
|
<th class="text-xs">Client Test Code</th>
|
||||||
|
<th class="text-xs">Client Test Name</th>
|
||||||
|
<th class="text-xs w-10"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each modalRows as row, index (index)}
|
||||||
|
<tr class="hover:bg-base-100">
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-xs input-bordered w-full"
|
||||||
|
bind:value={row.HostTestCode}
|
||||||
|
placeholder="Code"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-xs input-bordered w-full"
|
||||||
|
bind:value={row.HostTestName}
|
||||||
|
placeholder="Name"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{#if modalContext.ClientType === 'INST'}
|
||||||
|
<select
|
||||||
|
class="select select-xs select-bordered w-full"
|
||||||
|
bind:value={row.ConDefID}
|
||||||
|
>
|
||||||
|
<option value={null}>Select container...</option>
|
||||||
|
{#each containers as container (container.ConDefID)}
|
||||||
|
<option value={container.ConDefID}>
|
||||||
|
{container.ConName}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{#if formErrors.rows?.[index]?.ConDefID}
|
||||||
|
<span class="text-xs text-error">{formErrors.rows[index].ConDefID}</span>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<select class="select select-xs select-bordered w-full" disabled>
|
||||||
|
<option>Only for INST</option>
|
||||||
|
</select>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-xs input-bordered w-full"
|
||||||
|
bind:value={row.ClientTestCode}
|
||||||
|
placeholder="Code"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-xs input-bordered w-full"
|
||||||
|
bind:value={row.ClientTestName}
|
||||||
|
placeholder="Name"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-xs text-error"
|
||||||
|
onclick={() => removeMappingRow(index)}
|
||||||
|
disabled={modalRows.length === 1}
|
||||||
|
title="Remove row"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if formErrors.rows}
|
||||||
|
{#each Object.entries(formErrors.rows) as [idx, error]}
|
||||||
|
{#if error.empty}
|
||||||
|
<p class="text-xs text-error">Row {parseInt(idx) + 1}: {error.empty}</p>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Section: Add Button -->
|
||||||
|
<div class="flex justify-center pt-2 border-t border-base-300">
|
||||||
|
<button class="btn btn-sm btn-outline" onclick={addMappingRow}>
|
||||||
|
<Plus class="w-4 h-4 mr-1" />
|
||||||
|
Add Mapping
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#snippet footer()}
|
||||||
|
<button class="btn btn-ghost" onclick={() => (open = false)} type="button" disabled={saving}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" onclick={handleSave} disabled={saving} type="button">
|
||||||
|
{#if saving}
|
||||||
|
<span class="loading loading-spinner loading-sm mr-2"></span>
|
||||||
|
{/if}
|
||||||
|
{saving ? 'Saving...' : `Save ${modalRows.length} Mapping${modalRows.length !== 1 ? 's' : ''}`}
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</Modal>
|
||||||
@ -38,23 +38,23 @@
|
|||||||
let dataArray = [];
|
let dataArray = [];
|
||||||
|
|
||||||
if (response.status === 'success' && response.data) {
|
if (response.status === 'success' && response.data) {
|
||||||
if (typeof response.data === 'object' && !Array.isArray(response.data)) {
|
if (Array.isArray(response.data)) {
|
||||||
|
// New API format: { value, label, count }
|
||||||
|
dataArray = response.data.map((item) => ({
|
||||||
|
ValueSetKey: item.value,
|
||||||
|
Name: item.label,
|
||||||
|
ItemCount: item.count,
|
||||||
|
}));
|
||||||
|
} else if (typeof response.data === 'object') {
|
||||||
|
// Fallback for old format: { key: count }
|
||||||
dataArray = Object.entries(response.data).map(([key, count]) => ({
|
dataArray = Object.entries(response.data).map(([key, count]) => ({
|
||||||
ValueSetKey: key,
|
ValueSetKey: key,
|
||||||
Name: key.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()),
|
Name: key.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()),
|
||||||
ItemCount: count,
|
ItemCount: count,
|
||||||
}));
|
}));
|
||||||
} else if (Array.isArray(response.data)) {
|
|
||||||
dataArray = response.data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchQuery) {
|
|
||||||
dataArray = dataArray.filter((vs) =>
|
|
||||||
vs.ValueSetKey?.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
valueSets = dataArray;
|
valueSets = dataArray;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Load error:', err);
|
console.error('Load error:', err);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user