From beb32354701df3a73bfd0713a887e948acfc16d2 Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Mon, 23 Feb 2026 16:53:52 +0700 Subject: [PATCH] feat: add test map management module --- src/lib/api/testmap.js | 178 ++++++++ src/lib/components/Sidebar.svelte | 78 +--- .../(app)/master-data/testmap/+page.svelte | 329 ++++++++++++++ .../master-data/testmap/TestMapModal.svelte | 414 ++++++++++++++++++ .../(app)/master-data/valuesets/+page.svelte | 24 +- 5 files changed, 940 insertions(+), 83 deletions(-) create mode 100644 src/lib/api/testmap.js create mode 100644 src/routes/(app)/master-data/testmap/+page.svelte create mode 100644 src/routes/(app)/master-data/testmap/TestMapModal.svelte diff --git a/src/lib/api/testmap.js b/src/lib/api/testmap.js new file mode 100644 index 0000000..30cf826 --- /dev/null +++ b/src/lib/api/testmap.js @@ -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} 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} 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} 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} 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} 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} 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} 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 + }; +} diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index 919d58f..702cb15 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -18,9 +18,9 @@ import { Globe, ChevronDown, TestTube, - ShieldCheck, FileText, - X + X, + Link } from 'lucide-svelte'; import { auth } from '$lib/stores/auth.js'; import { goto } from '$app/navigation'; @@ -30,9 +30,7 @@ import { // Collapsible section states - default collapsed let laboratoryExpanded = $state(false); - let qualityControlExpanded = $state(false); let masterDataExpanded = $state(false); - let administrationExpanded = $state(false); // Load states from localStorage on mount $effect(() => { @@ -42,9 +40,7 @@ import { try { const parsed = JSON.parse(savedStates); laboratoryExpanded = parsed.laboratory ?? false; - qualityControlExpanded = parsed.qualityControl ?? false; masterDataExpanded = parsed.masterData ?? false; - administrationExpanded = parsed.administration ?? false; } catch (e) { // Keep defaults if parsing fails } @@ -57,9 +53,7 @@ import { if (browser) { localStorage.setItem('sidebar_section_states', JSON.stringify({ laboratory: laboratoryExpanded, - qualityControl: qualityControlExpanded, - masterData: masterDataExpanded, - administration: administrationExpanded + masterData: masterDataExpanded })); } }); @@ -68,9 +62,7 @@ import { $effect(() => { if (!isOpen) { laboratoryExpanded = false; - qualityControlExpanded = false; masterDataExpanded = false; - administrationExpanded = false; } }); @@ -96,26 +88,12 @@ function toggleLaboratory() { laboratoryExpanded = !laboratoryExpanded; } - function toggleQualityControl() { - if (!isOpen) { - expandSidebar(); - } - qualityControlExpanded = !qualityControlExpanded; - } - function toggleMasterData() { if (!isOpen) { expandSidebar(); } masterDataExpanded = !masterDataExpanded; } - - function toggleAdministration() { - if (!isOpen) { - expandSidebar(); - } - administrationExpanded = !administrationExpanded; - } @@ -195,28 +173,6 @@ function toggleLaboratory() { {/if} - - - {#if isOpen} {/if} @@ -259,36 +215,16 @@ function toggleLaboratory() { - {/if} - - - - diff --git a/src/routes/(app)/master-data/testmap/+page.svelte b/src/routes/(app)/master-data/testmap/+page.svelte new file mode 100644 index 0000000..478f811 --- /dev/null +++ b/src/routes/(app)/master-data/testmap/+page.svelte @@ -0,0 +1,329 @@ + + +
+
+ + + +
+

Test Mapping

+

+ Manage test mappings between host and client systems +

+
+ +
+ + +
+
+

+ + Filters +

+ +
+
+ +
+

+ + Host +

+
+ + +
+
+ +
+

+ + Client +

+
+ + +
+
+
+
+ + +
+ {#if !loading && filteredTestMaps.length === 0} + +
+
+ +
+

+ {#if filterHostType || filterHostID || filterClientType || filterClientID} + No mappings match your filters + {:else} + No test mappings found + {/if} +

+

+ {#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} +

+ {#if filterHostType || filterHostID || filterClientType || filterClientID} + + {:else} + + {/if} +
+ {:else} + + {#snippet cell({ column, row, value })} + {#if column.key === 'actions'} +
+ + +
+ {:else} + {value || '-'} + {/if} + {/snippet} +
+ {/if} +
+
+ + + + + + +
+

Are you sure you want to delete this test mapping?

+
+

+ Host: + {deleteItem?.HostType} / {deleteItem?.HostID} +

+

+ Client: + {deleteItem?.ClientType} / {deleteItem?.ClientID} +

+
+

+ + + + This action cannot be undone. +

+
+ {#snippet footer()} + + + {/snippet} +
diff --git a/src/routes/(app)/master-data/testmap/TestMapModal.svelte b/src/routes/(app)/master-data/testmap/TestMapModal.svelte new file mode 100644 index 0000000..b795d5d --- /dev/null +++ b/src/routes/(app)/master-data/testmap/TestMapModal.svelte @@ -0,0 +1,414 @@ + + + +
+ +
+ +
+

+ + Host +

+ +
+
+ + + {#if formErrors.HostType} + {formErrors.HostType} + {/if} +
+ +
+ + + {#if formErrors.HostID} + {formErrors.HostID} + {/if} +
+
+
+ + +
+

+ + Client +

+ +
+
+ + + {#if formErrors.ClientType} + {formErrors.ClientType} + {/if} +
+ +
+ + + {#if formErrors.ClientID} + {formErrors.ClientID} + {/if} +
+
+
+
+ + +
+

+ + Test Mappings + ({modalRows.length} row{modalRows.length !== 1 ? 's' : ''}) +

+ +
+ + + + + + + + + + + + + {#each modalRows as row, index (index)} + + + + + + + + + {/each} + +
Host Test CodeHost Test NameContainerClient Test CodeClient Test Name
+ + + + + {#if modalContext.ClientType === 'INST'} + + {#if formErrors.rows?.[index]?.ConDefID} + {formErrors.rows[index].ConDefID} + {/if} + {:else} + + {/if} + + + + + + +
+
+ + {#if formErrors.rows} + {#each Object.entries(formErrors.rows) as [idx, error]} + {#if error.empty} +

Row {parseInt(idx) + 1}: {error.empty}

+ {/if} + {/each} + {/if} +
+ + +
+ +
+
+ + {#snippet footer()} + + + {/snippet} +
diff --git a/src/routes/(app)/master-data/valuesets/+page.svelte b/src/routes/(app)/master-data/valuesets/+page.svelte index f90439b..31ebfc0 100644 --- a/src/routes/(app)/master-data/valuesets/+page.svelte +++ b/src/routes/(app)/master-data/valuesets/+page.svelte @@ -34,27 +34,27 @@ try { const params = searchQuery ? { key: searchQuery } : {}; const response = await fetchValueSets(params); - + let dataArray = []; - + 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]) => ({ ValueSetKey: key, Name: key.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()), 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; } catch (err) { console.error('Load error:', err);