From b693f279e89f58583ca23e0de1fc85f45e43d900 Mon Sep 17 00:00:00 2001
From: mahdahar <89adham@gmail.com>
Date: Fri, 27 Feb 2026 16:32:55 +0700
Subject: [PATCH] feat(organization): add coding system, host app, and host
compara sub-pages
- Add new organization sub-pages: codingsys, hostapp, hostcompara
- Update organization API client and main page
- Update Sidebar navigation for organization section
- Remove deprecated backup test files
- Update testmap and tests components
---
backup/tests_backup/+page.svelte | 432 -----------
backup/tests_backup/TestModal.svelte | 153 ----
backup/tests_backup/referenceRange.js | 119 ---
.../test-modal/BasicInfoForm.svelte | 181 -----
.../test-modal/GroupMembersTab.svelte | 158 ----
.../test-modal/NumericRefRange.svelte | 245 ------
.../test-modal/ReferenceRangeSection.svelte | 285 -------
.../test-modal/TechnicalConfigForm.svelte | 277 -------
.../test-modal/TestTypeSelector.svelte | 43 --
.../test-modal/TextRefRange.svelte | 173 -----
.../test-modal/ThresholdRefRange.svelte | 200 -----
.../test-modal/ValueSetRefRange.svelte | 174 -----
docs/organization.yaml | 707 ++++++++++++++++++
src/lib/api/organization.js | 140 ++++
src/lib/components/Sidebar.svelte | 8 +-
.../master-data/organization/+page.svelte | 26 +-
.../organization/codingsys/+page.svelte | 296 ++++++++
.../organization/hostapp/+page.svelte | 298 ++++++++
.../organization/hostcompara/+page.svelte | 340 +++++++++
.../(app)/master-data/testmap/+page.svelte | 12 +-
.../master-data/testmap/TestMapModal.svelte | 198 +++--
.../(app)/master-data/tests/+page.svelte | 212 +++++-
.../tests/test-modal/TestFormModal.svelte | 1 +
.../tests/test-modal/tabs/MappingsTab.svelte | 404 ++++++++--
24 files changed, 2525 insertions(+), 2557 deletions(-)
delete mode 100644 backup/tests_backup/+page.svelte
delete mode 100644 backup/tests_backup/TestModal.svelte
delete mode 100644 backup/tests_backup/referenceRange.js
delete mode 100644 backup/tests_backup/test-modal/BasicInfoForm.svelte
delete mode 100644 backup/tests_backup/test-modal/GroupMembersTab.svelte
delete mode 100644 backup/tests_backup/test-modal/NumericRefRange.svelte
delete mode 100644 backup/tests_backup/test-modal/ReferenceRangeSection.svelte
delete mode 100644 backup/tests_backup/test-modal/TechnicalConfigForm.svelte
delete mode 100644 backup/tests_backup/test-modal/TestTypeSelector.svelte
delete mode 100644 backup/tests_backup/test-modal/TextRefRange.svelte
delete mode 100644 backup/tests_backup/test-modal/ThresholdRefRange.svelte
delete mode 100644 backup/tests_backup/test-modal/ValueSetRefRange.svelte
create mode 100644 docs/organization.yaml
create mode 100644 src/routes/(app)/master-data/organization/codingsys/+page.svelte
create mode 100644 src/routes/(app)/master-data/organization/hostapp/+page.svelte
create mode 100644 src/routes/(app)/master-data/organization/hostcompara/+page.svelte
diff --git a/backup/tests_backup/+page.svelte b/backup/tests_backup/+page.svelte
deleted file mode 100644
index 66cc4e2..0000000
--- a/backup/tests_backup/+page.svelte
+++ /dev/null
@@ -1,432 +0,0 @@
-
-
-
modalOpen = false}
- onupdateFormData={(data) => formData = data}
-/>
-
-
-
-
Are you sure you want to delete this test?
-
Code: {testToDelete?.TestSiteCode} Name: {testToDelete?.TestSiteName}
-
This will deactivate the test. Historical data will be preserved.
-
- {#snippet footer()} deleteModalOpen = false} type="button">Cancel {#if deleting} {/if}{deleting ? 'Deleting...' : 'Delete'} {/snippet}
-
\ No newline at end of file
diff --git a/backup/tests_backup/TestModal.svelte b/backup/tests_backup/TestModal.svelte
deleted file mode 100644
index 8a8266b..0000000
--- a/backup/tests_backup/TestModal.svelte
+++ /dev/null
@@ -1,153 +0,0 @@
-
-
-
-
-
- activeTab = 'basic'}
- >
- Basic Information
-
- {#if canHaveTechnical}
- activeTab = 'technical'}
- >
- Technical
-
- {/if}
- {#if canHaveRefRange}
- activeTab = 'refrange'}
- >
- Reference Range
- {#if getRefRangeCount() > 0}
- {getRefRangeCount()}
- {/if}
-
- {/if}
- {#if isGroupTest}
- activeTab = 'members'}
- >
- Group Members
- {#if getGroupMemberCount() > 0}
- {getGroupMemberCount()}
- {/if}
-
- {/if}
-
-
- {#if activeTab === 'basic'}
-
- {:else if activeTab === 'technical' && canHaveTechnical}
-
- {:else if activeTab === 'refrange' && canHaveRefRange}
-
- {:else if activeTab === 'members' && isGroupTest}
-
- {/if}
-
- {#snippet footer()}
- Cancel
-
- {#if saving}
-
- {/if}
- {saving ? 'Saving...' : 'Save'}
-
- {/snippet}
-
diff --git a/backup/tests_backup/referenceRange.js b/backup/tests_backup/referenceRange.js
deleted file mode 100644
index 7937b92..0000000
--- a/backup/tests_backup/referenceRange.js
+++ /dev/null
@@ -1,119 +0,0 @@
-// Reference Range Management Functions
-
-export const signOptions = [
- { value: 'GE', label: '≥', description: 'Greater than or equal to' },
- { value: 'GT', label: '>', description: 'Greater than' },
- { value: 'LE', label: '≤', description: 'Less than or equal to' },
- { value: 'LT', label: '<', description: 'Less than' }
-];
-
-export const flagOptions = [
- { value: 'N', label: 'N', description: 'Normal' },
- { value: 'L', label: 'L', description: 'Low' },
- { value: 'H', label: 'H', description: 'High' },
- { value: 'C', label: 'C', description: 'Critical' }
-];
-
-export const refTypeOptions = [
- { value: 'REF', label: 'REF', description: 'Reference Range' },
- { value: 'CRTC', label: 'CRTC', description: 'Critical Range' },
- { value: 'VAL', label: 'VAL', description: 'Validation Range' },
- { value: 'RERUN', label: 'RERUN', description: 'Rerun Range' },
- { value: 'THOLD', label: 'THOLD', description: 'Threshold Range' }
-];
-
-export const textRefTypeOptions = [
- { value: 'TEXT', label: 'TEXT', description: 'Text Reference' },
- { value: 'VSET', label: 'VSET', description: 'Value Set Reference' }
-];
-
-export const sexOptions = [
- { value: '2', label: 'Male' },
- { value: '1', label: 'Female' },
- { value: '0', label: 'Any' }
-];
-
-export function createNumRef() {
- return {
- RefType: 'REF',
- Sex: '0',
- LowSign: 'GE',
- HighSign: 'LE',
- Low: null,
- High: null,
- AgeStart: 0,
- AgeEnd: 120,
- Flag: 'N',
- Interpretation: '',
- SpcType: '',
- Criteria: ''
- };
-}
-
-export function createTholdRef() {
- return {
- RefType: 'THOLD',
- Sex: '0',
- LowSign: 'GE',
- HighSign: 'LE',
- Low: null,
- High: null,
- AgeStart: 0,
- AgeEnd: 120,
- Flag: 'N',
- Interpretation: '',
- SpcType: '',
- Criteria: ''
- };
-}
-
-export function createTextRef() {
- return {
- RefType: 'TEXT',
- Sex: '0',
- AgeStart: 0,
- AgeEnd: 120,
- RefTxt: '',
- Flag: 'N',
- SpcType: '',
- Criteria: ''
- };
-}
-
-export function createVsetRef() {
- return {
- RefType: 'VSET',
- Sex: '0',
- AgeStart: 0,
- AgeEnd: 120,
- RefTxt: '',
- Flag: 'N',
- SpcType: '',
- Criteria: ''
- };
-}
-
-export function validateNumericRange(ref, index) {
- const errors = [];
- if (ref.Low !== null && ref.High !== null && ref.Low > ref.High) {
- errors.push(`Range ${index + 1}: Low value cannot be greater than High value`);
- }
- if (ref.AgeStart !== null && ref.AgeEnd !== null && ref.AgeStart > ref.AgeEnd) {
- errors.push(`Range ${index + 1}: Age start cannot be greater than Age end`);
- }
- return errors;
-}
-
-// Alias for threshold validation (same logic)
-export const validateTholdRange = validateNumericRange;
-
-export function validateTextRange(ref, index) {
- const errors = [];
- if (ref.AgeStart !== null && ref.AgeEnd !== null && ref.AgeStart > ref.AgeEnd) {
- errors.push(`Range ${index + 1}: Age start cannot be greater than Age end`);
- }
- return errors;
-}
-
-// Alias for value set validation (same logic)
-export const validateVsetRange = validateTextRange;
diff --git a/backup/tests_backup/test-modal/BasicInfoForm.svelte b/backup/tests_backup/test-modal/BasicInfoForm.svelte
deleted file mode 100644
index 91865db..0000000
--- a/backup/tests_backup/test-modal/BasicInfoForm.svelte
+++ /dev/null
@@ -1,181 +0,0 @@
-
-
-
diff --git a/backup/tests_backup/test-modal/GroupMembersTab.svelte b/backup/tests_backup/test-modal/GroupMembersTab.svelte
deleted file mode 100644
index 698735b..0000000
--- a/backup/tests_backup/test-modal/GroupMembersTab.svelte
+++ /dev/null
@@ -1,158 +0,0 @@
-
-
-
-
-
-
-
-
Add Group Members
-
-
-
-
-
- {#if searchQuery}
- {#if filteredTests.length === 0}
-
- No tests found matching "{searchQuery}"
-
- {:else}
- {#each filteredTests as test}
-
addMember(test)}
- >
-
- {test.TestSiteCode}
- {test.TestSiteName}
-
- {test.TestType}
-
-
-
-
- {/each}
- {/if}
- {:else}
-
-
-
Type to search for tests
-
- {/if}
-
-
-
-
-
-
-
- Group Members
- {formData.groupMembers?.length || 0}
-
-
-
-
- {#if !formData.groupMembers || formData.groupMembers.length === 0}
-
-
-
No members added yet
-
Search and add tests from the left
-
- {:else}
-
- {#each formData.groupMembers as member, index (`${member.TestSiteID}-${index}`)}
-
-
-
- {member.TestSiteCode}
- {member.TestSiteName}
-
-
-
-
- {member.TestType}
-
-
-
removeMember(index)}
- >
-
-
-
- {/each}
-
- {/if}
-
-
-
diff --git a/backup/tests_backup/test-modal/NumericRefRange.svelte b/backup/tests_backup/test-modal/NumericRefRange.svelte
deleted file mode 100644
index b897b3a..0000000
--- a/backup/tests_backup/test-modal/NumericRefRange.svelte
+++ /dev/null
@@ -1,245 +0,0 @@
-
-
-
-
-
-
-
-
Numeric Reference Ranges
- {#if refnum?.length > 0}
- {refnum.length}
- {/if}
-
-
-
- Add Range
-
-
-
-
- {#if refnum?.length === 0}
-
-
-
No numeric ranges defined
-
-
- Add First Range
-
-
- {/if}
-
-
- {#if refnum?.length > 0}
-
-
-
-
#
-
Type
-
Low
-
High
-
-
-
-
- {#each refnum || [] as ref, index (index)}
-
-
-
-
-
- {index + 1}
-
-
-
-
-
- {#each refTypeOptions as option (option.value)}
- {option.label}
- {/each}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- toggleAdvanced(index)}
- title={showAdvanced[index] ? 'Hide details' : 'Show details'}
- >
- {#if hasAdvancedData(ref)}
-
- {/if}
- {#if showAdvanced[index]}
-
- {:else}
-
- {/if}
-
- removeRefRange(index)}
- title="Remove"
- >
-
-
-
-
-
-
- {#if showAdvanced[index]}
-
-
-
-
- Sex
-
- Any
- Female
- Male
-
-
-
-
-
-
-
-
-
-
- Specimen Type
-
-
- Any specimen
- {#each specimenTypeOptions as option (option.value)}
- {option.label}
- {/each}
-
-
-
-
-
- Interpretation
-
-
-
-
-
- Criteria
-
-
-
-
- {/if}
-
- {/each}
-
- {/if}
-
diff --git a/backup/tests_backup/test-modal/ReferenceRangeSection.svelte b/backup/tests_backup/test-modal/ReferenceRangeSection.svelte
deleted file mode 100644
index 6ad4fd4..0000000
--- a/backup/tests_backup/test-modal/ReferenceRangeSection.svelte
+++ /dev/null
@@ -1,285 +0,0 @@
-
-
-
-
-
-
-
-
Reference Range Type
-
-
-
-
-
- updateRefRangeType(e.target.value)}
- disabled={testType === 'GROUP' || testType === 'TITLE'}
- >
- {#each refTypeOptions() as option (option.value)}
- {option.label}
- {/each}
-
-
-
-
- {#if formData.refRangeType && formData.refRangeType !== 'none'}
-
-
-
- Selected:
- {refTypeLabels[formData.refRangeType]}
-
-
- {/if}
-
-
-
- {#if formData.refRangeType === 'num'}
-
- {/if}
-
-
- {#if formData.refRangeType === 'thold'}
-
- {/if}
-
-
- {#if formData.refRangeType === 'text'}
-
- {/if}
-
-
- {#if formData.refRangeType === 'vset'}
-
- {/if}
-
diff --git a/backup/tests_backup/test-modal/TechnicalConfigForm.svelte b/backup/tests_backup/test-modal/TechnicalConfigForm.svelte
deleted file mode 100644
index e2daf02..0000000
--- a/backup/tests_backup/test-modal/TechnicalConfigForm.svelte
+++ /dev/null
@@ -1,277 +0,0 @@
-
-
-{#if loading}
-
-
-
-{:else}
-
-
-
-
-
-
Result Configuration
-
-
-
-
- Result Type
-
- handleResultTypeChange(e.target.value)}
- disabled={testType === 'GROUP' || testType === 'TITLE'}
- >
- {#if testType !== 'GROUP' && testType !== 'TITLE'}
- Select result type...
- {/if}
- {#each resultTypeOptions as option}
- {option.label}
- {/each}
-
-
-
-
-
-
-
-
-
Units and Precision
-
-
-
-
- {#if formData.Factor}
-
- Formula: {formData.Unit1 || 'Unit1'} × {formData.Factor} = {formData.Unit2 || 'Unit2'}
-
- {/if}
-
-
-
- {#if !isCalculated}
-
-
-
-
Specimen Requirements
-
-
-
-
- {/if}
-
-
-
-
-
-
Method and Turnaround
-
-
-
-
-
-{/if}
diff --git a/backup/tests_backup/test-modal/TestTypeSelector.svelte b/backup/tests_backup/test-modal/TestTypeSelector.svelte
deleted file mode 100644
index 6f92e85..0000000
--- a/backup/tests_backup/test-modal/TestTypeSelector.svelte
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
Select test type to create
-
- {#each types as type}
-
onselect(type.value)}
- >
-
-
-
- {type.label}
-
- {/each}
-
-
- Cancel
-
-
diff --git a/backup/tests_backup/test-modal/TextRefRange.svelte b/backup/tests_backup/test-modal/TextRefRange.svelte
deleted file mode 100644
index d24d792..0000000
--- a/backup/tests_backup/test-modal/TextRefRange.svelte
+++ /dev/null
@@ -1,173 +0,0 @@
-
-
-
-
-
-
-
Text Reference Ranges
-
-
-
- Add Range
-
-
-
- {#if reftxt?.length === 0}
-
-
-
No text ranges defined
-
-
- Add First Range
-
-
- {/if}
-
- {#each reftxt || [] as ref, index (index)}
-
-
-
- Range {index + 1}
- removeRefRange(index)}>
-
- Remove
-
-
-
-
-
- Sex
-
- {#each sexOptions as option (option.value)}
- {option.label}
- {/each}
-
-
-
-
- Age From
-
-
-
-
- Age To
-
-
-
-
- Flag
-
- N - Normal
- A - Abnormal
-
-
-
-
-
- Reference Text
-
-
-
-
-
-
toggleSpecimenExpand(index)}
- >
-
-
- Specimen & Criteria
- {#if ref.SpcType || ref.Criteria}
- (Custom)
- {:else}
- (Optional)
- {/if}
-
- {#if expandedRanges[index]?.specimen}
-
- {:else}
-
- {/if}
-
-
- {#if expandedRanges[index]?.specimen}
-
-
-
-
- Specimen Type
-
-
- Any specimen
- {#each specimenTypeOptions as option}
- {option.label}
- {/each}
-
-
-
-
-
- Criteria
-
-
-
-
- {/if}
-
-
-
- {/each}
-
diff --git a/backup/tests_backup/test-modal/ThresholdRefRange.svelte b/backup/tests_backup/test-modal/ThresholdRefRange.svelte
deleted file mode 100644
index 9869962..0000000
--- a/backup/tests_backup/test-modal/ThresholdRefRange.svelte
+++ /dev/null
@@ -1,200 +0,0 @@
-
-
-
-
-
-
-
Threshold Reference Ranges
-
-
-
- Add Range
-
-
-
- {#if refthold?.length === 0}
-
-
-
No threshold ranges defined
-
-
- Add First Range
-
-
- {/if}
-
- {#each refthold || [] as ref, index (index)}
-
-
-
- Range {index + 1}
- removeRefRange(index)}>
-
- Remove
-
-
-
-
-
- Sex
-
- {#each sexOptions as option (option.value)}
- {option.label}
- {/each}
-
-
-
-
- Age From
-
-
-
-
- Age To
-
-
-
-
- Flag
-
- {#each flagOptions as option (option.value)}
- {option.label} - {option.description}
- {/each}
-
-
-
-
-
-
-
- Interpretation
-
-
-
-
-
-
toggleSpecimenExpand(index)}
- >
-
-
- Specimen & Criteria
- {#if ref.SpcType || ref.Criteria}
- (Custom)
- {:else}
- (Optional)
- {/if}
-
- {#if expandedRanges[index]?.specimen}
-
- {:else}
-
- {/if}
-
-
- {#if expandedRanges[index]?.specimen}
-
-
-
-
- Specimen Type
-
-
- Any specimen
- {#each specimenTypeOptions as option}
- {option.label}
- {/each}
-
-
-
-
-
- Criteria
-
-
-
-
- {/if}
-
-
-
- {/each}
-
diff --git a/backup/tests_backup/test-modal/ValueSetRefRange.svelte b/backup/tests_backup/test-modal/ValueSetRefRange.svelte
deleted file mode 100644
index 45e64fe..0000000
--- a/backup/tests_backup/test-modal/ValueSetRefRange.svelte
+++ /dev/null
@@ -1,174 +0,0 @@
-
-
-
-
-
-
-
Value Set Reference Ranges
-
-
-
- Add Range
-
-
-
- {#if refvset?.length === 0}
-
-
-
No value set ranges defined
-
-
- Add First Range
-
-
- {/if}
-
- {#each refvset || [] as ref, index (index)}
-
-
-
- Range {index + 1}
- removeRefRange(index)}>
-
- Remove
-
-
-
-
-
- Sex
-
- {#each sexOptions as option (option.value)}
- {option.label}
- {/each}
-
-
-
-
- Age From
-
-
-
-
- Age To
-
-
-
-
- Flag
-
- N - Normal
- A - Abnormal
-
-
-
-
-
- Value Set
-
- Comma-separated list of allowed values
-
-
-
-
-
toggleSpecimenExpand(index)}
- >
-
-
- Specimen & Criteria
- {#if ref.SpcType || ref.Criteria}
- (Custom)
- {:else}
- (Optional)
- {/if}
-
- {#if expandedRanges[index]?.specimen}
-
- {:else}
-
- {/if}
-
-
- {#if expandedRanges[index]?.specimen}
-
-
-
-
- Specimen Type
-
-
- Any specimen
- {#each specimenTypeOptions as option}
- {option.label}
- {/each}
-
-
-
-
-
- Criteria
-
-
-
-
- {/if}
-
-
-
- {/each}
-
diff --git a/docs/organization.yaml b/docs/organization.yaml
new file mode 100644
index 0000000..0093e04
--- /dev/null
+++ b/docs/organization.yaml
@@ -0,0 +1,707 @@
+/api/organization/account/{id}:
+ get:
+ tags: [Organization]
+ summary: Get account by ID
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: Account details
+ content:
+ application/json:
+ schema:
+ $ref: '../components/schemas/organization.yaml#/Account'
+
+/api/organization/site:
+ get:
+ tags: [Organization]
+ summary: List sites
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: List of sites
+
+ post:
+ tags: [Organization]
+ summary: Create site
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '../components/schemas/organization.yaml#/Site'
+ responses:
+ '201':
+ description: Site created
+
+ patch:
+ tags: [Organization]
+ summary: Update site
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ SiteName:
+ type: string
+ SiteCode:
+ type: string
+ AccountID:
+ type: integer
+ responses:
+ '200':
+ description: Site updated
+
+ delete:
+ tags: [Organization]
+ summary: Delete site
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ responses:
+ '200':
+ description: Site deleted
+
+/api/organization/site/{id}:
+ get:
+ tags: [Organization]
+ summary: Get site by ID
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: Site details
+
+/api/organization/discipline:
+ get:
+ tags: [Organization]
+ summary: List disciplines
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: List of disciplines
+
+ post:
+ tags: [Organization]
+ summary: Create discipline
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '../components/schemas/organization.yaml#/Discipline'
+ responses:
+ '201':
+ description: Discipline created
+
+ patch:
+ tags: [Organization]
+ summary: Update discipline
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ DisciplineName:
+ type: string
+ DisciplineCode:
+ type: string
+ responses:
+ '200':
+ description: Discipline updated
+
+ delete:
+ tags: [Organization]
+ summary: Delete discipline
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ responses:
+ '200':
+ description: Discipline deleted
+
+/api/organization/discipline/{id}:
+ get:
+ tags: [Organization]
+ summary: Get discipline by ID
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: Discipline details
+
+/api/organization/department:
+ get:
+ tags: [Organization]
+ summary: List departments
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: List of departments
+
+ post:
+ tags: [Organization]
+ summary: Create department
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '../components/schemas/organization.yaml#/Department'
+ responses:
+ '201':
+ description: Department created
+
+ patch:
+ tags: [Organization]
+ summary: Update department
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ DeptName:
+ type: string
+ DeptCode:
+ type: string
+ SiteID:
+ type: integer
+ responses:
+ '200':
+ description: Department updated
+
+ delete:
+ tags: [Organization]
+ summary: Delete department
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ responses:
+ '200':
+ description: Department deleted
+
+/api/organization/department/{id}:
+ get:
+ tags: [Organization]
+ summary: Get department by ID
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: Department details
+
+/api/organization/workstation:
+ get:
+ tags: [Organization]
+ summary: List workstations
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: List of workstations
+
+ post:
+ tags: [Organization]
+ summary: Create workstation
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '../components/schemas/organization.yaml#/Workstation'
+ responses:
+ '201':
+ description: Workstation created
+
+ patch:
+ tags: [Organization]
+ summary: Update workstation
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ WorkstationName:
+ type: string
+ WorkstationCode:
+ type: string
+ SiteID:
+ type: integer
+ DepartmentID:
+ type: integer
+ responses:
+ '200':
+ description: Workstation updated
+
+ delete:
+ tags: [Organization]
+ summary: Delete workstation
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: integer
+ responses:
+ '200':
+ description: Workstation deleted
+
+/api/organization/workstation/{id}:
+ get:
+ tags: [Organization]
+ summary: Get workstation by ID
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: Workstation details
+
+# HostApp
+/api/organization/hostapp:
+ get:
+ tags: [Organization]
+ summary: List host applications
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: HostAppID
+ in: query
+ schema:
+ type: string
+ - name: HostAppName
+ in: query
+ schema:
+ type: string
+ responses:
+ '200':
+ description: List of host applications
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ message:
+ type: string
+ data:
+ type: array
+ items:
+ $ref: '../components/schemas/organization.yaml#/HostApp'
+
+ post:
+ tags: [Organization]
+ summary: Create host application
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '../components/schemas/organization.yaml#/HostApp'
+ responses:
+ '201':
+ description: Host application created
+
+ patch:
+ tags: [Organization]
+ summary: Update host application
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - HostAppID
+ properties:
+ HostAppID:
+ type: string
+ HostAppName:
+ type: string
+ SiteID:
+ type: integer
+ responses:
+ '200':
+ description: Host application updated
+
+ delete:
+ tags: [Organization]
+ summary: Delete host application (soft delete)
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - HostAppID
+ properties:
+ HostAppID:
+ type: string
+ responses:
+ '200':
+ description: Host application deleted
+
+/api/organization/hostapp/{id}:
+ get:
+ tags: [Organization]
+ summary: Get host application by ID
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Host application details
+ content:
+ application/json:
+ schema:
+ $ref: '../components/schemas/organization.yaml#/HostApp'
+
+# HostComPara
+/api/organization/hostcompara:
+ get:
+ tags: [Organization]
+ summary: List host communication parameters
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: HostAppID
+ in: query
+ schema:
+ type: string
+ - name: HostIP
+ in: query
+ schema:
+ type: string
+ responses:
+ '200':
+ description: List of host communication parameters
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ message:
+ type: string
+ data:
+ type: array
+ items:
+ $ref: '../components/schemas/organization.yaml#/HostComPara'
+
+ post:
+ tags: [Organization]
+ summary: Create host communication parameters
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '../components/schemas/organization.yaml#/HostComPara'
+ responses:
+ '201':
+ description: Host communication parameters created
+
+ patch:
+ tags: [Organization]
+ summary: Update host communication parameters
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - HostAppID
+ properties:
+ HostAppID:
+ type: string
+ HostIP:
+ type: string
+ HostPort:
+ type: string
+ HostPwd:
+ type: string
+ responses:
+ '200':
+ description: Host communication parameters updated
+
+ delete:
+ tags: [Organization]
+ summary: Delete host communication parameters (soft delete)
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - HostAppID
+ properties:
+ HostAppID:
+ type: string
+ responses:
+ '200':
+ description: Host communication parameters deleted
+
+/api/organization/hostcompara/{id}:
+ get:
+ tags: [Organization]
+ summary: Get host communication parameters by HostAppID
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Host communication parameters details
+ content:
+ application/json:
+ schema:
+ $ref: '../components/schemas/organization.yaml#/HostComPara'
+
+# CodingSys
+/api/organization/codingsys:
+ get:
+ tags: [Organization]
+ summary: List coding systems
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: CodingSysAbb
+ in: query
+ schema:
+ type: string
+ - name: FullText
+ in: query
+ schema:
+ type: string
+ responses:
+ '200':
+ description: List of coding systems
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ message:
+ type: string
+ data:
+ type: array
+ items:
+ $ref: '../components/schemas/organization.yaml#/CodingSys'
+
+ post:
+ tags: [Organization]
+ summary: Create coding system
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '../components/schemas/organization.yaml#/CodingSys'
+ responses:
+ '201':
+ description: Coding system created
+
+ patch:
+ tags: [Organization]
+ summary: Update coding system
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - CodingSysID
+ properties:
+ CodingSysID:
+ type: integer
+ CodingSysAbb:
+ type: string
+ FullText:
+ type: string
+ Description:
+ type: string
+ responses:
+ '200':
+ description: Coding system updated
+
+ delete:
+ tags: [Organization]
+ summary: Delete coding system (soft delete)
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - CodingSysID
+ properties:
+ CodingSysID:
+ type: integer
+ responses:
+ '200':
+ description: Coding system deleted
+
+/api/organization/codingsys/{id}:
+ get:
+ tags: [Organization]
+ summary: Get coding system by ID
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: Coding system details
+ content:
+ application/json:
+ schema:
+ $ref: '../components/schemas/organization.yaml#/CodingSys'
diff --git a/src/lib/api/organization.js b/src/lib/api/organization.js
index 693510e..8f2a8b3 100644
--- a/src/lib/api/organization.js
+++ b/src/lib/api/organization.js
@@ -65,3 +65,143 @@ export async function updateDepartment(data) {
export async function deleteDepartment(id) {
return del('/api/organization/department', { id });
}
+
+// Sites
+export async function fetchSites(params = {}) {
+ const query = new URLSearchParams(params).toString();
+ return get(query ? `/api/organization/site?${query}` : '/api/organization/site');
+}
+
+export async function fetchSite(id) {
+ return get(`/api/organization/site/${id}`);
+}
+
+export async function createSite(data) {
+ const payload = {
+ SiteCode: data.SiteCode,
+ SiteName: data.SiteName,
+ };
+ return post('/api/organization/site', payload);
+}
+
+export async function updateSite(data) {
+ const payload = {
+ id: data.SiteID,
+ SiteCode: data.SiteCode,
+ SiteName: data.SiteName,
+ };
+ return patch('/api/organization/site', payload);
+}
+
+export async function deleteSite(id) {
+ return del('/api/organization/site', { id });
+}
+
+// Workstations
+export async function fetchWorkstations(params = {}) {
+ const query = new URLSearchParams(params).toString();
+ return get(query ? `/api/organization/workstation?${query}` : '/api/organization/workstation');
+}
+
+export async function fetchWorkstation(id) {
+ return get(`/api/organization/workstation/${id}`);
+}
+
+// HostApps
+export async function fetchHostApps(params = {}) {
+ const query = new URLSearchParams(params).toString();
+ return get(query ? `/api/organization/hostapp?${query}` : '/api/organization/hostapp');
+}
+
+export async function fetchHostApp(id) {
+ return get(`/api/organization/hostapp/${id}`);
+}
+
+export async function createHostApp(data) {
+ const payload = {
+ HostAppName: data.HostAppName,
+ SiteID: data.SiteID,
+ };
+ return post('/api/organization/hostapp', payload);
+}
+
+export async function updateHostApp(data) {
+ const payload = {
+ id: data.HostAppID,
+ HostAppName: data.HostAppName,
+ SiteID: data.SiteID,
+ };
+ return patch('/api/organization/hostapp', payload);
+}
+
+export async function deleteHostApp(id) {
+ return del('/api/organization/hostapp', { id });
+}
+
+// HostComParas
+export async function fetchHostComParas(params = {}) {
+ const query = new URLSearchParams(params).toString();
+ return get(query ? `/api/organization/hostcompara?${query}` : '/api/organization/hostcompara');
+}
+
+export async function fetchHostComPara(id) {
+ return get(`/api/organization/hostcompara/${id}`);
+}
+
+export async function createHostComPara(data) {
+ const payload = {
+ HostAppID: data.HostAppID,
+ HostIP: data.HostIP,
+ HostPort: data.HostPort,
+ HostPwd: data.HostPwd,
+ };
+ return post('/api/organization/hostcompara', payload);
+}
+
+export async function updateHostComPara(data) {
+ const payload = {
+ id: data.HostComParaID,
+ HostAppID: data.HostAppID,
+ HostIP: data.HostIP,
+ HostPort: data.HostPort,
+ HostPwd: data.HostPwd,
+ };
+ return patch('/api/organization/hostcompara', payload);
+}
+
+export async function deleteHostComPara(id) {
+ return del('/api/organization/hostcompara', { id });
+}
+
+// CodingSystems
+export async function fetchCodingSystems(params = {}) {
+ const query = new URLSearchParams(params).toString();
+ return get(query ? `/api/organization/codingsys?${query}` : '/api/organization/codingsys');
+}
+
+export async function fetchCodingSystem(id) {
+ return get(`/api/organization/codingsys/${id}`);
+}
+
+export async function createCodingSystem(data) {
+ const payload = {
+ CodingSysAbb: data.CodingSysAbb,
+ FullText: data.FullText,
+ Description: data.Description,
+ };
+ return post('/api/organization/codingsys', payload);
+}
+
+export async function updateCodingSystem(data) {
+ const payload = {
+ id: data.CodingSysID,
+ CodingSysAbb: data.CodingSysAbb,
+ FullText: data.FullText,
+ Description: data.Description,
+ };
+ return patch('/api/organization/codingsys', payload);
+}
+
+export async function deleteCodingSystem(id) {
+ return del('/api/organization/codingsys', { id });
+}
diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte
index 08b512a..a2f10a4 100644
--- a/src/lib/components/Sidebar.svelte
+++ b/src/lib/components/Sidebar.svelte
@@ -24,7 +24,10 @@ import {
LandPlot,
Monitor,
Activity,
- User
+ User,
+ Server,
+ Network,
+ FileCode
} from 'lucide-svelte';
import { auth } from '$lib/stores/auth.js';
import { goto } from '$app/navigation';
@@ -257,6 +260,9 @@ function toggleLaboratory() {
+
+
+
{/if}
diff --git a/src/routes/(app)/master-data/organization/+page.svelte b/src/routes/(app)/master-data/organization/+page.svelte
index aea5c69..ec9c00c 100644
--- a/src/routes/(app)/master-data/organization/+page.svelte
+++ b/src/routes/(app)/master-data/organization/+page.svelte
@@ -6,7 +6,10 @@
Users,
Building2,
Monitor,
- Activity
+ Activity,
+ Server,
+ Network,
+ FileCode
} from 'lucide-svelte';
import { ArrowLeft } from 'lucide-svelte';
@@ -53,6 +56,27 @@
href: '/master-data/organization/instrument',
color: 'bg-orange-500',
},
+ {
+ title: 'Host Application',
+ description: 'Manage host applications and systems',
+ icon: Server,
+ href: '/master-data/organization/hostapp',
+ color: 'bg-rose-500',
+ },
+ {
+ title: 'Host Connection',
+ description: 'Manage host connection parameters',
+ icon: Network,
+ href: '/master-data/organization/hostcompara',
+ color: 'bg-teal-500',
+ },
+ {
+ title: 'Coding System',
+ description: 'Manage coding systems and terminologies',
+ icon: FileCode,
+ href: '/master-data/organization/codingsys',
+ color: 'bg-amber-500',
+ },
];
diff --git a/src/routes/(app)/master-data/organization/codingsys/+page.svelte b/src/routes/(app)/master-data/organization/codingsys/+page.svelte
new file mode 100644
index 0000000..9d080bb
--- /dev/null
+++ b/src/routes/(app)/master-data/organization/codingsys/+page.svelte
@@ -0,0 +1,296 @@
+
+
+
+
+
+
+
+
+
Coding Systems
+
Manage coding systems and terminologies
+
+
+
+ Add Coding System
+
+
+
+
+
+
+
+
+
+
+ {#if searchQuery}
+ (searchQuery = '')}>
+ Clear
+
+ {/if}
+
+
+
+ {#if !loading && filteredItems.length === 0}
+
+
+
+
+
+ {searchQuery ? 'No coding systems found' : 'No coding systems yet'}
+
+
+ {searchQuery
+ ? `No coding systems match your search "${searchQuery}". Try a different search term.`
+ : 'Get started by adding your first coding system.'}
+
+ {#if !searchQuery}
+
+
+ Add Your First Coding System
+
+ {/if}
+
+ {:else}
+
+ {#snippet cell({ column, row })}
+ {#if column.key === 'actions'}
+
+ openEditModal(row)} aria-label="Edit {row.CodingSysAbb}">
+
+
+ confirmDelete(row)} aria-label="Delete {row.CodingSysAbb}">
+
+
+
+ {:else}
+ {row[column.key]}
+ {/if}
+ {/snippet}
+
+ {/if}
+
+
+
+
+ { e.preventDefault(); handleSave(); }}>
+
+
+
+ Description
+
+
+ Additional information about this coding system
+
+
+ {#snippet footer()}
+ (modalOpen = false)} type="button">Cancel
+
+ {#if saving}
+
+ {/if}
+ {saving ? 'Saving...' : 'Save'}
+
+ {/snippet}
+
+
+
+
+
+ Are you sure you want to delete the following coding system?
+
+
+
{deleteItem?.CodingSysAbb}
+
{deleteItem?.FullText}
+
+
This action cannot be undone.
+
+ {#snippet footer()}
+ (deleteConfirmOpen = false)} disabled={deleting} type="button">
+ Cancel
+
+
+ {#if deleting}
+
+ {/if}
+ {deleting ? 'Deleting...' : 'Delete'}
+
+ {/snippet}
+
diff --git a/src/routes/(app)/master-data/organization/hostapp/+page.svelte b/src/routes/(app)/master-data/organization/hostapp/+page.svelte
new file mode 100644
index 0000000..69691c8
--- /dev/null
+++ b/src/routes/(app)/master-data/organization/hostapp/+page.svelte
@@ -0,0 +1,298 @@
+
+
+
+
+
+
+
+
+
Host Applications
+
Manage host applications
+
+
+
+ Add Host Application
+
+
+
+
+
+
+
+
+
+
+ {#if searchQuery}
+ (searchQuery = '')}>
+ Clear
+
+ {/if}
+
+
+
+ {#if !loading && filteredItems.length === 0}
+
+
+
+
+
+ {searchQuery ? 'No host applications found' : 'No host applications yet'}
+
+
+ {searchQuery
+ ? `No host applications match your search "${searchQuery}". Try a different search term.`
+ : 'Get started by adding your first host application.'}
+
+ {#if !searchQuery}
+
+
+ Add Your First Host Application
+
+ {/if}
+
+ {:else}
+
+ {#snippet cell({ column, row })}
+ {#if column.key === 'actions'}
+
+ openEditModal(row)} aria-label="Edit {row.HostAppName}">
+
+
+ confirmDelete(row)} aria-label="Delete {row.HostAppName}">
+
+
+
+ {:else}
+ {row[column.key]}
+ {/if}
+ {/snippet}
+
+ {/if}
+
+
+
+
+ { e.preventDefault(); handleSave(); }}>
+
+
+ Host Application Name
+ *
+
+
+ Display name for this host application
+
+
+
+ Site
+ *
+
+
+ Select site...
+ {#each sites as site}
+ {site.SiteName}
+ {/each}
+
+ The site this host application belongs to
+
+
+ {#snippet footer()}
+ (modalOpen = false)} type="button">Cancel
+
+ {#if saving}
+
+ {/if}
+ {saving ? 'Saving...' : 'Save'}
+
+ {/snippet}
+
+
+
+
+
+ Are you sure you want to delete the following host application?
+
+
+
{deleteItem?.HostAppName}
+
+
This action cannot be undone.
+
+ {#snippet footer()}
+ (deleteConfirmOpen = false)} disabled={deleting} type="button">
+ Cancel
+
+
+ {#if deleting}
+
+ {/if}
+ {deleting ? 'Deleting...' : 'Delete'}
+
+ {/snippet}
+
diff --git a/src/routes/(app)/master-data/organization/hostcompara/+page.svelte b/src/routes/(app)/master-data/organization/hostcompara/+page.svelte
new file mode 100644
index 0000000..522941d
--- /dev/null
+++ b/src/routes/(app)/master-data/organization/hostcompara/+page.svelte
@@ -0,0 +1,340 @@
+
+
+
+
+
+
+
+
+
Host Connection Parameters
+
Manage host connection settings
+
+
+
+ Add Connection Parameters
+
+
+
+
+
+
+
+
+
+
+ {#if searchQuery}
+ (searchQuery = '')}>
+ Clear
+
+ {/if}
+
+
+
+ {#if !loading && filteredItems.length === 0}
+
+
+
+
+
+ {searchQuery ? 'No connection parameters found' : 'No connection parameters yet'}
+
+
+ {searchQuery
+ ? `No connection parameters match your search "${searchQuery}". Try a different search term.`
+ : 'Get started by adding your first host connection parameters.'}
+
+ {#if !searchQuery}
+
+
+ Add Your First Connection Parameters
+
+ {/if}
+
+ {:else}
+
+ {#snippet cell({ column, row })}
+ {#if column.key === 'actions'}
+
+ openEditModal(row)} aria-label="Edit {row.HostAppName}">
+
+
+ confirmDelete(row)} aria-label="Delete {row.HostAppName}">
+
+
+
+ {:else}
+ {row[column.key]}
+ {/if}
+ {/snippet}
+
+ {/if}
+
+
+
+
+ { e.preventDefault(); handleSave(); }}>
+
+
+ Host Application
+ *
+
+
+ Select host application...
+ {#each hostApps as hostApp}
+ {hostApp.HostAppName}
+ {/each}
+
+ The host application for these connection parameters
+
+
+
+
+ Password
+
+
+ Connection password (optional)
+
+
+ {#snippet footer()}
+ (modalOpen = false)} type="button">Cancel
+
+ {#if saving}
+
+ {/if}
+ {saving ? 'Saving...' : 'Save'}
+
+ {/snippet}
+
+
+
+
+
+ Are you sure you want to delete the following connection parameters?
+
+
+
{deleteItem?.HostAppName}
+
IP: {deleteItem?.HostIP}:{deleteItem?.HostPort}
+
+
This action cannot be undone.
+
+ {#snippet footer()}
+ (deleteConfirmOpen = false)} disabled={deleting} type="button">
+ Cancel
+
+
+ {#if deleting}
+
+ {/if}
+ {deleting ? 'Deleting...' : 'Delete'}
+
+ {/snippet}
+
diff --git a/src/routes/(app)/master-data/testmap/+page.svelte b/src/routes/(app)/master-data/testmap/+page.svelte
index f514391..2969184 100644
--- a/src/routes/(app)/master-data/testmap/+page.svelte
+++ b/src/routes/(app)/master-data/testmap/+page.svelte
@@ -182,14 +182,22 @@
- {row.HostName || row.HostID || '-'}
+ {#if row.HostType}
+ {row.HostType} - {row.HostName || row.HostID || '-'}
+ {:else}
+ {row.HostName || row.HostID || '-'}
+ {/if}
{:else if column.key === 'ClientInfo'}
- {row.ClientName || row.ClientID || '-'}
+ {#if row.ClientType}
+ {row.ClientType} - {row.ClientName || row.ClientID || '-'}
+ {:else}
+ {row.ClientName || row.ClientID || '-'}
+ {/if}
{:else if column.key === 'actions'}
diff --git a/src/routes/(app)/master-data/testmap/TestMapModal.svelte b/src/routes/(app)/master-data/testmap/TestMapModal.svelte
index cb08a5b..150c098 100644
--- a/src/routes/(app)/master-data/testmap/TestMapModal.svelte
+++ b/src/routes/(app)/master-data/testmap/TestMapModal.svelte
@@ -10,6 +10,8 @@
batchUpdateTestMapDetails,
batchDeleteTestMapDetails,
} from '$lib/api/testmap.js';
+ import { fetchHostApps, fetchSites, fetchWorkstations } from '$lib/api/organization.js';
+ import { fetchEquipmentList } from '$lib/api/equipment.js';
import {
Plus,
Trash2,
@@ -45,37 +47,111 @@
// Track previous mode and groupData to detect actual changes
let previousMode = $state(mode);
let previousGroupData = $state(null);
+ let previousHostType = $state('');
+ let previousClientType = $state('');
+
+ // Host apps for HIS dropdown
+ let hostApps = $state([]);
+
+ // Dropdown data from APIs
+ let sites = $state([]);
+ let workstations = $state([]);
+ let instruments = $state([]);
const hostTypes = ['HIS', 'SITE', 'WST', 'INST'];
const clientTypes = ['HIS', 'SITE', 'WST', 'INST'];
+ async function fetchDropdownData() {
+ try {
+ const [hostAppsRes, sitesRes, workstationsRes, instrumentsRes] = await Promise.all([
+ fetchHostApps(),
+ fetchSites(),
+ fetchWorkstations(),
+ fetchEquipmentList(),
+ ]);
+ hostApps = hostAppsRes.data?.map((h) => ({ id: h.HostAppID, name: h.HostAppName })) || [];
+ sites = sitesRes.data?.map((s) => ({ id: s.SiteID?.toString(), name: s.SiteName })) || [];
+ workstations = workstationsRes.data?.map((w) => ({ id: w.WorkstationID?.toString(), name: w.WorkstationName })) || [];
+ instruments = instrumentsRes.data?.map((i) => ({ id: i.EID?.toString(), name: i.InstrumentName || i.IEID })) || [];
+ } catch (err) {
+ console.error('Error fetching dropdown data:', err);
+ toastError('Failed to load dropdown options');
+ }
+ }
+
+ function getClientOptions(clientType) {
+ switch (clientType) {
+ case 'HIS': return hostApps;
+ case 'SITE': return sites;
+ case 'WST': return workstations;
+ case 'INST': return instruments;
+ default: return [];
+ }
+ }
+
+ function getHostOptions(hostType) {
+ switch (hostType) {
+ case 'HIS': return hostApps;
+ case 'SITE': return sites;
+ case 'WST': return workstations;
+ case 'INST': return instruments;
+ default: return [];
+ }
+ }
+
+ // Track if modal was ever opened
+ let hasInitialized = $state(false);
+
// Initialize modal when open changes to true, or when mode/groupData actually change
$effect(() => {
const isOpen = open;
const currentMode = mode;
const currentGroupData = groupData;
- if (!isOpen) return;
+ if (!isOpen) {
+ hasInitialized = false;
+ return;
+ }
- // Only initialize if:
- // 1. Just opened (open changed from false to true)
- // 2. Mode actually changed
- // 3. GroupData actually changed (different reference)
+ // Initialize on first open or when mode/groupData changes
const shouldInitialize =
+ !hasInitialized ||
currentMode !== untrack(() => previousMode) ||
currentGroupData !== untrack(() => previousGroupData);
if (shouldInitialize) {
+ hasInitialized = true;
previousMode = currentMode;
previousGroupData = currentGroupData;
initializeModal();
}
});
- function initializeModal() {
+ // Clear HostID when HostType changes in create mode
+ $effect(() => {
+ const currentHostType = modalContext.HostType;
+ if (mode === 'create' && currentHostType !== previousHostType) {
+ previousHostType = currentHostType;
+ modalContext.HostID = '';
+ }
+ });
+
+ // Clear ClientID when ClientType changes in create mode
+ $effect(() => {
+ const currentClientType = modalContext.ClientType;
+ if (mode === 'create' && currentClientType !== previousClientType) {
+ previousClientType = currentClientType;
+ modalContext.ClientID = '';
+ }
+ });
+
+ async function initializeModal() {
formErrors = {};
originalRows = [];
+ // Fetch all dropdown data
+ await fetchDropdownData();
+
if (mode === 'edit' && groupData) {
// Edit mode with group data - load all mappings in the group
modalContext = {
@@ -410,19 +486,35 @@
ID
*
-
+ {#if mode === 'create' && ['HIS', 'SITE', 'WST', 'INST'].includes(modalContext.HostType)}
+
+ Select...
+ {#each getHostOptions(modalContext.HostType) as option (option.id)}
+ {option.name} ({option.id})
+ {/each}
+
+ {:else}
+
+ {/if}
{#if formErrors.HostID}
{formErrors.HostID}
{/if}
+ {#if mode === 'edit' && groupData?.HostName}
+ {groupData.HostName}
+ {/if}
@@ -459,19 +551,35 @@
ID
*
-
+ {#if mode === 'create' && ['HIS', 'SITE', 'WST', 'INST'].includes(modalContext.ClientType)}
+
+ Select...
+ {#each getClientOptions(modalContext.ClientType) as option (option.id)}
+ {option.name} ({option.id})
+ {/each}
+
+ {:else}
+
+ {/if}
{#if formErrors.ClientID}
{formErrors.ClientID}
{/if}
+ {#if mode === 'edit' && groupData?.ClientName}
+ {groupData.ClientName}
+ {/if}
@@ -490,9 +598,9 @@
Host Test Code
Host Test Name
- Container
Client Test Code
Client Test Name
+ Container
@@ -515,29 +623,6 @@
placeholder="Name"
/>
-
- {#if modalContext.ClientType === 'INST'}
- updateRowField(index, 'ConDefID', e.target.value === '' ? null : parseInt(e.target.value))}
- >
- Select container...
- {#each containers as container (container.ConDefID)}
-
- {container.ConName}
-
- {/each}
-
- {#if formErrors.rows?.[index]?.ConDefID}
- {formErrors.rows[index].ConDefID}
- {/if}
- {:else}
-
- Only for INST
-
- {/if}
-
+
+ {#if modalContext.ClientType === 'INST'}
+ updateRowField(index, 'ConDefID', e.target.value === '' ? null : parseInt(e.target.value))}
+ >
+ Select container...
+ {#each containers as container (container.ConDefID)}
+
+ {container.ConName}
+
+ {/each}
+
+ {#if formErrors.rows?.[index]?.ConDefID}
+ {formErrors.rows[index].ConDefID}
+ {/if}
+ {:else}
+
+ {/if}
+
{
- if (!searchQuery.trim()) return tests;
- const query = searchQuery.toLowerCase().trim();
- return tests.filter(test => {
- const code = (test.TestSiteCode || '').toLowerCase();
- const name = (test.TestSiteName || '').toLowerCase();
- return code.includes(query) || name.includes(query);
- });
+ return tests;
});
+ function getSearchParams() {
+ const params = { page: currentPage, perPage };
+ const query = searchQuery.trim();
+ if (query) {
+ if (searchType === 'code') {
+ params.testCode = query;
+ } else if (searchType === 'name') {
+ params.testName = query;
+ } else {
+ params.search = query;
+ }
+ }
+ return params;
+ }
+
+ function handleSearch() {
+ if (searchDebounceTimer) {
+ clearTimeout(searchDebounceTimer);
+ }
+ searchDebounceTimer = setTimeout(() => {
+ currentPage = 1;
+ loadTests();
+ }, 300);
+ }
+
onMount(async () => {
await Promise.all([loadTests(), loadDisciplines(), loadDepartments()]);
});
- async function loadTests() {
+ function handleSearchInput() {
+ handleSearch();
+ }
+
+ function handleSearchTypeChange(newType) {
+ searchType = newType;
+ handleSearch();
+ }
+
+ function clearSearch() {
+ searchQuery = '';
+ currentPage = 1;
+ loadTests();
+ }
+
+ async function loadTests(reset = false) {
+ if (reset) {
+ currentPage = 1;
+ }
loading = true;
try {
- const response = await fetchTests();
- tests = Array.isArray(response.data) ? response.data.filter(t => t.IsActive !== '0' && t.IsActive !== 0) : [];
+ const params = getSearchParams();
+ const response = await fetchTests(params);
+ if (response.data && Array.isArray(response.data.data)) {
+ tests = response.data.data.filter(t => t.IsActive !== '0' && t.IsActive !== 0);
+ totalItems = response.data.total || 0;
+ totalPages = Math.ceil(totalItems / perPage);
+ hasMore = currentPage < totalPages;
+ } else if (Array.isArray(response.data)) {
+ // Fallback for old API format
+ tests = response.data.filter(t => t.IsActive !== '0' && t.IsActive !== 0);
+ totalItems = tests.length;
+ totalPages = 1;
+ hasMore = false;
+ } else {
+ tests = [];
+ totalItems = 0;
+ totalPages = 0;
+ hasMore = false;
+ }
} catch (err) {
toastError(err.message || 'Failed to load tests');
tests = [];
+ totalItems = 0;
+ totalPages = 0;
+ hasMore = false;
} finally {
loading = false;
}
}
+ async function loadNextPage() {
+ if (currentPage < totalPages && !loading) {
+ currentPage++;
+ loading = true;
+ try {
+ const params = getSearchParams();
+ const response = await fetchTests(params);
+ if (response.data && Array.isArray(response.data.data)) {
+ const newTests = response.data.data.filter(t => t.IsActive !== '0' && t.IsActive !== 0);
+ tests = [...tests, ...newTests];
+ totalItems = response.data.total || 0;
+ totalPages = Math.ceil(totalItems / perPage);
+ hasMore = currentPage < totalPages;
+ }
+ } catch (err) {
+ toastError(err.message || 'Failed to load more tests');
+ currentPage--;
+ } finally {
+ loading = false;
+ }
+ }
+ }
+
+ function goToPage(page) {
+ if (page >= 1 && page <= totalPages && page !== currentPage) {
+ currentPage = page;
+ loadTests();
+ }
+ }
+
async function loadDisciplines() {
try {
const response = await fetchDisciplines();
@@ -144,26 +240,50 @@
-
-
+
+
+ handleSearchTypeChange('all')}
+ >
+ All
+
+ handleSearchTypeChange('code')}
+ >
+ Code
+
+ handleSearchTypeChange('name')}
+ >
+ Name
+
+
+
{#if searchQuery}
searchQuery = ''}
+ onclick={clearSearch}
>
×
{/if}
+
+ {totalItems} total
+
@@ -196,8 +316,8 @@
@@ -228,6 +348,64 @@
{/if}
{/snippet}
+
+
+ {#if totalPages > 1}
+
+
+ Showing {(currentPage - 1) * perPage + 1} - {Math.min(currentPage * perPage, totalItems)} of {totalItems}
+
+
+
goToPage(currentPage - 1)}
+ disabled={currentPage === 1 || loading}
+ >
+
+
+
+ {#each Array(Math.min(5, totalPages)) as _, i (i)}
+ {@const pageNum = currentPage <= 3
+ ? i + 1
+ : currentPage >= totalPages - 2
+ ? totalPages - 4 + i
+ : currentPage - 2 + i}
+ {#if pageNum <= totalPages}
+ goToPage(pageNum)}
+ disabled={loading}
+ >
+ {pageNum}
+
+ {/if}
+ {/each}
+
+
goToPage(currentPage + 1)}
+ disabled={currentPage === totalPages || loading}
+ >
+
+
+
+
+ {:else if hasMore}
+
+
+ {#if loading}
+
+ Loading...
+ {:else}
+ Load More
+ {/if}
+
+
+ {/if}
{/if}
diff --git a/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte b/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte
index 52adf9c..a185711 100644
--- a/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte
+++ b/src/routes/(app)/master-data/tests/test-modal/TestFormModal.svelte
@@ -379,6 +379,7 @@
{:else if currentTab === 'refnum'}
+ import { onMount } from 'svelte';
import { Plus, Edit2, Trash2, Link } from 'lucide-svelte';
import Modal from '$lib/components/Modal.svelte';
+ import { get } from '$lib/api/client.js';
+ import { error as toastError } from '$lib/utils/toast.js';
- let { formData = $bindable(), isDirty = $bindable(false) } = $props();
+ let { formData = $bindable(), isDirty = $bindable(false), testCode = '' } = $props();
let modalOpen = $state(false);
let modalMode = $state('create');
let selectedMapping = $state(null);
+ let loading = $state(false);
+ let mappingsData = $state([]);
+
+ // Dropdown data
+ let sites = $state([]);
+ let workstations = $state([]);
+ let departments = $state([]);
+ let equipment = $state([]);
+ let containers = $state([]);
+ let hostApps = $state([]);
+
+ // Cache for names
+ let namesCache = $state({
+ sites: {},
+ workstations: {},
+ departments: {},
+ equipment: {},
+ containers: {},
+ hostApps: {}
+ });
+
let editingMapping = $state({
HostType: '',
HostID: '',
- HostDataSource: '',
HostTestCode: '',
HostTestName: '',
ClientType: '',
ClientID: '',
- ClientDataSource: '',
ConDefID: '',
ClientTestCode: '',
ClientTestName: ''
@@ -24,6 +46,242 @@
const hostTypes = ['HIS', 'SITE', 'WST', 'INST'];
const clientTypes = ['HIS', 'SITE', 'WST', 'INST'];
+ // Fetch mappings when testCode changes
+ $effect(() => {
+ if (testCode) {
+ fetchMappings();
+ }
+ });
+
+ onMount(() => {
+ if (testCode) {
+ fetchMappings();
+ }
+ // Load dropdown data
+ loadDropdownData();
+ });
+
+ async function loadDropdownData() {
+ try {
+ // Load sites
+ const sitesRes = await get('/api/organization/site');
+ if (sitesRes.status === 'success' && sitesRes.data) {
+ sites = sitesRes.data;
+ sites.forEach(s => namesCache.sites[s.SiteID] = s.SiteName);
+ }
+
+ // Load workstations
+ const wstRes = await get('/api/organization/workstation');
+ if (wstRes.status === 'success' && wstRes.data) {
+ workstations = wstRes.data;
+ workstations.forEach(w => namesCache.workstations[w.WorkstationID] = w.WorkstationName);
+ }
+
+ // Load departments
+ const deptRes = await get('/api/organization/department');
+ if (deptRes.status === 'success' && deptRes.data) {
+ departments = deptRes.data;
+ departments.forEach(d => namesCache.departments[d.DepartmentID] = d.DeptName);
+ }
+
+ // Load equipment
+ const equipRes = await get('/api/equipmentlist');
+ if (equipRes.status === 'success' && equipRes.data) {
+ equipment = equipRes.data;
+ equipment.forEach(e => namesCache.equipment[e.EID] = e.InstrumentName || e.IEID);
+ }
+
+ // Load containers
+ const contRes = await get('/api/specimen/container');
+ if (contRes.status === 'success' && contRes.data) {
+ containers = contRes.data;
+ containers.forEach(c => namesCache.containers[c.ConDefID] = c.ConName);
+ }
+
+ // Load host applications (HIS)
+ const hostRes = await get('/api/organization/hostapp');
+ if (hostRes.status === 'success' && hostRes.data) {
+ hostApps = hostRes.data;
+ hostApps.forEach(h => namesCache.hostApps[h.HostAppID] = h.HostAppName);
+ }
+ } catch (err) {
+ console.error('Failed to load dropdown data:', err);
+ }
+ }
+
+ function getHostOptions() {
+ switch (editingMapping.HostType) {
+ case 'HIS':
+ return hostApps.map(h => ({ value: h.HostAppID, label: h.HostAppName }));
+ case 'SITE':
+ return sites.map(s => ({ value: s.SiteID, label: s.SiteName }));
+ case 'WST':
+ return workstations.map(w => ({ value: w.WorkstationID, label: w.WorkstationName }));
+ case 'DEPT':
+ return departments.map(d => ({ value: d.DepartmentID, label: d.DeptName }));
+ case 'INST':
+ return equipment.map(e => ({ value: e.EID, label: e.InstrumentName || e.IEID }));
+ default:
+ return [];
+ }
+ }
+
+ function getClientOptions() {
+ switch (editingMapping.ClientType) {
+ case 'HIS':
+ return hostApps.map(h => ({ value: h.HostAppID, label: h.HostAppName }));
+ case 'SITE':
+ return sites.map(s => ({ value: s.SiteID, label: s.SiteName }));
+ case 'WST':
+ return workstations.map(w => ({ value: w.WorkstationID, label: w.WorkstationName }));
+ case 'DEPT':
+ return departments.map(d => ({ value: d.DepartmentID, label: d.DeptName }));
+ case 'INST':
+ return equipment.map(e => ({ value: e.EID, label: e.InstrumentName || e.IEID }));
+ default:
+ return [];
+ }
+ }
+
+ async function fetchEntityName(type, id) {
+ if (!id) return null;
+
+ // Check cache first
+ if (type === 'HIS' && namesCache.hostApps[id]) return namesCache.hostApps[id];
+ if (type === 'SITE' && namesCache.sites[id]) return namesCache.sites[id];
+ if (type === 'WST' && namesCache.workstations[id]) return namesCache.workstations[id];
+ if (type === 'DEPT' && namesCache.departments[id]) return namesCache.departments[id];
+ if (type === 'INST' && namesCache.equipment[id]) return namesCache.equipment[id];
+
+ try {
+ let response;
+ switch (type) {
+ case 'HIS':
+ response = await get(`/api/organization/hostapp/${id}`);
+ if (response.status === 'success' && response.data) {
+ namesCache.hostApps[id] = response.data.HostAppName || `HIS ${id}`;
+ return namesCache.hostApps[id];
+ }
+ break;
+ case 'SITE':
+ response = await get(`/api/organization/site/${id}`);
+ if (response.status === 'success' && response.data) {
+ namesCache.sites[id] = response.data.SiteName || `Site ${id}`;
+ return namesCache.sites[id];
+ }
+ break;
+ case 'WST':
+ response = await get(`/api/organization/workstation/${id}`);
+ if (response.status === 'success' && response.data) {
+ namesCache.workstations[id] = response.data.WorkstationName || `Workstation ${id}`;
+ return namesCache.workstations[id];
+ }
+ break;
+ case 'DEPT':
+ response = await get(`/api/organization/department/${id}`);
+ if (response.status === 'success' && response.data) {
+ namesCache.departments[id] = response.data.DeptName || `Department ${id}`;
+ return namesCache.departments[id];
+ }
+ break;
+ case 'INST':
+ response = await get(`/api/equipmentlist/${id}`);
+ if (response.status === 'success' && response.data) {
+ namesCache.equipment[id] = response.data.InstrumentName || response.data.IEID || `Equipment ${id}`;
+ return namesCache.equipment[id];
+ }
+ break;
+ }
+ } catch (err) {
+ console.error(`Failed to fetch ${type} name for ID ${id}:`, err);
+ }
+ return `${type} ${id}`;
+ }
+
+ async function fetchContainerName(conDefId) {
+ if (!conDefId) return null;
+ if (namesCache.containers[conDefId]) return namesCache.containers[conDefId];
+
+ try {
+ const response = await get(`/api/specimen/container/${conDefId}`);
+ if (response.status === 'success' && response.data) {
+ namesCache.containers[conDefId] = response.data.ConName || `Container ${conDefId}`;
+ return namesCache.containers[conDefId];
+ }
+ } catch (err) {
+ console.error(`Failed to fetch container name for ID ${conDefId}:`, err);
+ }
+ return `Container ${conDefId}`;
+ }
+
+ async function fetchMappingDetails(testMapId) {
+ try {
+ const response = await get(`/api/test/testmap/detail/by-testmap/${testMapId}`);
+ if (response.status === 'success' && response.data && response.data.length > 0) {
+ return response.data[0];
+ }
+ } catch (err) {
+ console.error(`Failed to fetch mapping details for TestMapID ${testMapId}:`, err);
+ }
+ return null;
+ }
+
+ async function fetchMappings() {
+ if (!testCode) return;
+
+ loading = true;
+ try {
+ const response = await get(`/api/test/testmap/by-testcode/${testCode}`);
+ if (response.status === 'success' && response.data) {
+ const transformedData = await Promise.all(response.data.map(async item => {
+ const [hostName, clientName, details] = await Promise.all([
+ fetchEntityName(item.HostType, item.HostID),
+ fetchEntityName(item.ClientType, item.ClientID),
+ fetchMappingDetails(item.TestMapID)
+ ]);
+
+ let containerName = null;
+ if (details && details.ConDefID) {
+ containerName = await fetchContainerName(details.ConDefID);
+ }
+
+ return {
+ TestMapID: item.TestMapID,
+ TestSiteID: item.TestSiteID,
+ HostType: item.HostType,
+ HostID: item.HostID,
+ HostName: hostName,
+ HostTypeLabel: item.HostTypeLabel,
+ ClientType: item.ClientType,
+ ClientID: item.ClientID,
+ ClientName: clientName,
+ ClientTypeLabel: item.ClientTypeLabel,
+ ConDefID: details?.ConDefID || null,
+ ContainerName: containerName,
+ CreateDate: item.CreateDate,
+ EndDate: item.EndDate,
+ HostTestCode: details?.HostTestCode || '',
+ HostTestName: details?.HostTestName || '',
+ ClientTestCode: details?.ClientTestCode || '',
+ ClientTestName: details?.ClientTestName || ''
+ };
+ }));
+
+ mappingsData = transformedData;
+ formData.testmap = [...mappingsData];
+ } else {
+ mappingsData = [];
+ formData.testmap = [];
+ }
+ } catch (err) {
+ toastError(err.message || 'Failed to load mappings');
+ mappingsData = [];
+ formData.testmap = [];
+ } finally {
+ loading = false;
+ }
+ }
+
function handleFieldChange() {
isDirty = true;
}
@@ -33,12 +291,10 @@
editingMapping = {
HostType: '',
HostID: '',
- HostDataSource: '',
HostTestCode: '',
HostTestName: '',
ClientType: '',
ClientID: '',
- ClientDataSource: '',
ConDefID: '',
ClientTestCode: '',
ClientTestName: ''
@@ -54,20 +310,26 @@
}
function removeMapping(index) {
- const newMappings = formData.testmap?.filter((_, i) => i !== index) || [];
- formData.testmap = newMappings;
+ const newMappings = mappingsData.filter((_, i) => i !== index);
+ mappingsData = newMappings;
+ formData.testmap = [...newMappings];
handleFieldChange();
}
function saveMapping() {
if (modalMode === 'create') {
- formData.testmap = [...(formData.testmap || []), { ...editingMapping }];
+ const newMapping = {
+ ...editingMapping,
+ TestMapID: `temp-${Date.now()}`
+ };
+ mappingsData = [...mappingsData, newMapping];
} else {
- const newMappings = formData.testmap?.map(m =>
+ const newMappings = mappingsData.map(m =>
m === selectedMapping ? { ...editingMapping } : m
- ) || [];
- formData.testmap = newMappings;
+ );
+ mappingsData = newMappings;
}
+ formData.testmap = [...mappingsData];
modalOpen = false;
handleFieldChange();
}
@@ -84,9 +346,13 @@
-
Current Mappings ({formData.testmap?.length || 0})
+
Current Mappings ({mappingsData?.length || 0})
- {#if !formData.testmap || formData.testmap.length === 0}
+ {#if loading}
+
+
+
+ {:else if !mappingsData || mappingsData.length === 0}
No mappings configured
@@ -98,37 +364,28 @@
Host System
- Host Code
Client System
- Client Code
Actions
- {#each formData.testmap as mapping, idx (idx)}
+ {#each mappingsData as mapping, idx (idx)}
-
{mapping.HostType}
-
ID: {mapping.HostID || '-'}
+
{mapping.HostName || mapping.HostTypeLabel || mapping.HostType || '-'}
+
{mapping.HostTypeLabel || mapping.HostType} (ID: {mapping.HostID || '-'})
-
{mapping.HostTestCode || '-'}
-
{mapping.HostTestName || '-'}
-
-
-
-
-
{mapping.ClientType}
-
ID: {mapping.ClientID || '-'}
-
-
-
-
-
{mapping.ClientTestCode || '-'}
-
{mapping.ClientTestName || '-'}
+
{mapping.ClientName || mapping.ClientTypeLabel || mapping.ClientType || '-'}
+
+ {mapping.ClientTypeLabel || mapping.ClientType} (ID: {mapping.ClientID || '-'})
+ {#if mapping.ClientType === 'INST' && mapping.ContainerName}
+ • {mapping.ContainerName}
+ {/if}
+
@@ -173,7 +430,11 @@