refactor(tests): Move TestModal to route folder and add technical config support
- Move TestModal from lib/components to routes/(app)/master-data/tests - Add technical configuration form (ResultType, RefType, SpcType, units, etc.) - Add GroupMembersTab for managing group test members - Enhance reference ranges with refvset and refthold support - Update API to handle new test fields (ReqQty, Factor, Decimal, TAT, etc.) - Add database schema documentation (DBML format) - Remove old test-types-reference.md documentation - UI improvements: compact design, updated sidebar, modal sizing - Update DataTable, Modal, SelectDropdown components for compact style - Enhance patient and visit modals with compact layout
This commit is contained in:
parent
f0f5889df4
commit
8d77370357
640
docs/clqms_database.dbml
Normal file
640
docs/clqms_database.dbml
Normal file
@ -0,0 +1,640 @@
|
||||
// CLQMS Database Schema
|
||||
// Generated from app/Models/ directory
|
||||
// Database Markup Language (DBML) for dbdiagram.io and other tools
|
||||
|
||||
// ============================================
|
||||
// TABLE 1: Patient Management
|
||||
// ============================================
|
||||
|
||||
Table patient {
|
||||
InternalPID int [pk, increment]
|
||||
PatientID varchar(255)
|
||||
AlternatePID varchar(255)
|
||||
Prefix varchar(50)
|
||||
NameFirst varchar(255)
|
||||
NameMiddle varchar(255)
|
||||
NameMaiden varchar(255)
|
||||
NameLast varchar(255)
|
||||
Suffix varchar(50)
|
||||
NameAlias varchar(255)
|
||||
Sex varchar(10)
|
||||
Birthdate datetime
|
||||
PlaceOfBirth varchar(255)
|
||||
Street_1 varchar(255)
|
||||
Street_2 varchar(255)
|
||||
Street_3 varchar(255)
|
||||
City varchar(100)
|
||||
Province varchar(100)
|
||||
ZIP varchar(20)
|
||||
Country varchar(100)
|
||||
EmailAddress1 varchar(255)
|
||||
EmailAddress2 varchar(255)
|
||||
Phone varchar(50)
|
||||
MobilePhone varchar(50)
|
||||
AccountNumber varchar(100)
|
||||
Race varchar(50)
|
||||
MaritalStatus varchar(50)
|
||||
Religion varchar(50)
|
||||
Ethnic varchar(50)
|
||||
Citizenship varchar(100)
|
||||
DeathIndicator boolean
|
||||
TimeOfDeath datetime
|
||||
Custodian int [ref: > patient.InternalPID]
|
||||
LinkTo int [ref: > patient.InternalPID]
|
||||
CreateDate datetime [not null]
|
||||
DelDate datetime
|
||||
}
|
||||
|
||||
Table patidt {
|
||||
PatIdtID int [pk, increment]
|
||||
InternalPID int [not null, ref: > patient.InternalPID]
|
||||
IdentifierType varchar(100)
|
||||
Identifier varchar(255)
|
||||
EffectiveDate datetime
|
||||
ExpirationDate datetime
|
||||
CreateDate datetime [not null]
|
||||
DelDate datetime
|
||||
}
|
||||
|
||||
Table patcom {
|
||||
PatComID int [pk, increment]
|
||||
InternalPID int [not null, ref: > patient.InternalPID]
|
||||
Comment text
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table patatt {
|
||||
PatAttID int [pk, increment]
|
||||
InternalPID int [not null, ref: > patient.InternalPID]
|
||||
UserID int
|
||||
Address text
|
||||
CreateDate datetime [not null]
|
||||
DelDate datetime
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLE 2: Visit Management
|
||||
// ============================================
|
||||
|
||||
Table patvisit {
|
||||
InternalPVID int [pk, increment]
|
||||
InternalPID int [not null, ref: > patient.InternalPID]
|
||||
EpisodeID int
|
||||
PVID varchar(100)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
ArchivedDate datetime
|
||||
DelDate datetime
|
||||
}
|
||||
|
||||
Table patvisitadt {
|
||||
PVADTID int [pk, increment]
|
||||
InternalPVID int [not null, ref: > patvisit.InternalPVID]
|
||||
LocationID int [ref: > location.LocationID]
|
||||
ADTCode varchar(50)
|
||||
AttDoc varchar(255)
|
||||
RefDoc varchar(255)
|
||||
AdmDoc varchar(255)
|
||||
CnsDoc varchar(255)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
ArchivedDate datetime
|
||||
DelDate datetime
|
||||
}
|
||||
|
||||
Table patdiag {
|
||||
InternalPVID int [pk, increment]
|
||||
InternalPVID int [not null, ref: > patvisit.InternalPVID]
|
||||
InternalPID int [not null, ref: > patient.InternalPID]
|
||||
DiagCode varchar(50)
|
||||
Diagnosis varchar(255)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
ArchivedDate datetime
|
||||
DelDate datetime
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLE 3: Organization Structure
|
||||
// ============================================
|
||||
|
||||
Table account {
|
||||
AccountID int [pk, increment]
|
||||
AccountName varchar(255)
|
||||
Initial varchar(50)
|
||||
Street_1 varchar(255)
|
||||
Street_2 varchar(255)
|
||||
Street_3 varchar(255)
|
||||
City varchar(100)
|
||||
Province varchar(100)
|
||||
ZIP varchar(20)
|
||||
Country varchar(100)
|
||||
AreaCode varchar(20)
|
||||
EmailAddress1 varchar(255)
|
||||
EmailAddress2 varchar(255)
|
||||
Phone varchar(50)
|
||||
Fax varchar(50)
|
||||
Parent int [ref: > account.AccountID]
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table site {
|
||||
SiteID int [pk, increment]
|
||||
AccountID int [not null, ref: > account.AccountID]
|
||||
Parent int [ref: > site.SiteID]
|
||||
SiteTypeID int
|
||||
SiteClassID int
|
||||
SiteCode varchar(50)
|
||||
SiteName varchar(255)
|
||||
ME varchar(50)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table department {
|
||||
DepartmentID int [pk, increment]
|
||||
DisciplineID int [ref: > discipline.DisciplineID]
|
||||
SiteID int [ref: > site.SiteID]
|
||||
DepartmentCode varchar(50)
|
||||
DepartmentName varchar(255)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table discipline {
|
||||
DisciplineID int [pk, increment]
|
||||
SiteID int [ref: > site.SiteID]
|
||||
Parent int [ref: > discipline.DisciplineID]
|
||||
DisciplineCode varchar(50)
|
||||
DisciplineName varchar(255)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table workstation {
|
||||
WorkstationID int [pk, increment]
|
||||
DepartmentID int [ref: > department.DepartmentID]
|
||||
LinkTo int [ref: > workstation.WorkstationID]
|
||||
EquipmentID int
|
||||
WorkstationCode varchar(50)
|
||||
WorkstationName varchar(255)
|
||||
Type varchar(50)
|
||||
Enable boolean
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLE 4: Location Management
|
||||
// ============================================
|
||||
|
||||
Table location {
|
||||
LocationID int [pk, increment]
|
||||
SiteID int [not null, ref: > site.SiteID]
|
||||
Parent int [ref: > location.LocationID]
|
||||
LocCode varchar(50)
|
||||
LocFull varchar(255)
|
||||
Description text
|
||||
LocType varchar(50)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table locationaddress {
|
||||
LocationID int [pk, ref: > location.LocationID]
|
||||
Province int [ref: > areageo.AreaGeoID]
|
||||
City int [ref: > areageo.AreaGeoID]
|
||||
Street1 varchar(255)
|
||||
Street2 varchar(255)
|
||||
PostCode varchar(20)
|
||||
GeoLocationSystem varchar(50)
|
||||
GeoLocationData text
|
||||
Phone varchar(50)
|
||||
Email varchar(255)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table areageo {
|
||||
AreaGeoID int [pk, increment]
|
||||
Parent int [ref: > areageo.AreaGeoID]
|
||||
AreaCode varchar(50)
|
||||
Class varchar(50)
|
||||
AreaName varchar(255)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLE 5: Test Management
|
||||
// ============================================
|
||||
|
||||
Table testdefsite {
|
||||
TestSiteID int [pk, increment]
|
||||
SiteID int [not null, ref: > site.SiteID]
|
||||
TestSiteCode varchar(50)
|
||||
TestSiteName varchar(255)
|
||||
TestType varchar(50) // TEST, PARAM, CALC, GROUP, TITLE
|
||||
Description text
|
||||
SeqScr int
|
||||
SeqRpt int
|
||||
IndentLeft int
|
||||
FontStyle varchar(50)
|
||||
VisibleScr boolean
|
||||
VisibleRpt boolean
|
||||
CountStat boolean
|
||||
CreateDate datetime [not null]
|
||||
StartDate datetime
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table testdeftech {
|
||||
TestTechID int [pk, increment]
|
||||
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
|
||||
DisciplineID int [ref: > discipline.DisciplineID]
|
||||
DepartmentID int [ref: > department.DepartmentID]
|
||||
VSet int
|
||||
ResultType varchar(50) // NM, TX, DT, TM, VS, HL7
|
||||
RefType varchar(50) // NUM, TXT, VSET
|
||||
ReqQty decimal(10,4)
|
||||
ReqQtyUnit varchar(20)
|
||||
Unit1 varchar(50)
|
||||
Factor decimal(10,6)
|
||||
Unit2 varchar(50)
|
||||
Decimal int
|
||||
CollReq text
|
||||
Method varchar(255)
|
||||
ExpectedTAT int
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table testdefcal {
|
||||
TestCalID int [pk, increment]
|
||||
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
|
||||
DisciplineID int [ref: > discipline.DisciplineID]
|
||||
DepartmentID int [ref: > department.DepartmentID]
|
||||
FormulaInput varchar(500)
|
||||
FormulaCode text
|
||||
RefType varchar(50)
|
||||
Unit1 varchar(50)
|
||||
Factor decimal(10,6)
|
||||
Unit2 varchar(50)
|
||||
Decimal int
|
||||
Method varchar(255)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table testdefgrp {
|
||||
TestGrpID int [pk, increment]
|
||||
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
|
||||
Member int [ref: > testdefsite.TestSiteID]
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table testmap {
|
||||
TestMapID int [pk, increment]
|
||||
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
|
||||
ConDefID int [ref: > containerdef.ConDefID]
|
||||
HostType varchar(50)
|
||||
HostID varchar(100)
|
||||
HostDataSource varchar(100)
|
||||
HostTestCode varchar(100)
|
||||
HostTestName varchar(255)
|
||||
ClientType varchar(50)
|
||||
ClientID varchar(100)
|
||||
ClientDataSource varchar(100)
|
||||
ClientTestCode varchar(100)
|
||||
ClientTestName varchar(255)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLE 6: Reference Ranges
|
||||
// ============================================
|
||||
|
||||
Table refnum {
|
||||
RefNumID int [pk, increment]
|
||||
SiteID int [not null, ref: > site.SiteID]
|
||||
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
|
||||
SpcType varchar(50)
|
||||
Sex varchar(10)
|
||||
Criteria varchar(255)
|
||||
AgeStart int
|
||||
AgeEnd int
|
||||
NumRefType varchar(50) // NR, CR
|
||||
RangeType varchar(50) // LL-UL, LL, UL, ABS
|
||||
LowSign varchar(5)
|
||||
Low decimal(15,5)
|
||||
HighSign varchar(5)
|
||||
High decimal(15,5)
|
||||
Display varchar(50)
|
||||
Flag varchar(10)
|
||||
Interpretation text
|
||||
Notes text
|
||||
CreateDate datetime [not null]
|
||||
StartDate datetime
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table reftxt {
|
||||
RefTxtID int [pk, increment]
|
||||
SiteID int [not null, ref: > site.SiteID]
|
||||
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
|
||||
SpcType varchar(50)
|
||||
Sex varchar(10)
|
||||
Criteria varchar(255)
|
||||
AgeStart int
|
||||
AgeEnd int
|
||||
TxtRefType varchar(50) // TX, VS
|
||||
RefTxt text
|
||||
Flag varchar(10)
|
||||
Notes text
|
||||
CreateDate datetime [not null]
|
||||
StartDate datetime
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table refvset {
|
||||
RefVSetID int [pk, increment]
|
||||
SiteID int [not null, ref: > site.SiteID]
|
||||
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
|
||||
SpcType varchar(50)
|
||||
Sex varchar(10)
|
||||
AgeStart int
|
||||
AgeEnd int
|
||||
RefTxt varchar(255)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table refthold {
|
||||
RefTHoldID int [pk, increment]
|
||||
SiteID int [not null, ref: > site.SiteID]
|
||||
TestSiteID int [not null, ref: > testdefsite.TestSiteID]
|
||||
SpcType varchar(50)
|
||||
Sex varchar(10)
|
||||
AgeStart int
|
||||
AgeEnd int
|
||||
Threshold decimal(15,5)
|
||||
BelowTxt text
|
||||
AboveTxt text
|
||||
GrayzoneLow decimal(15,5)
|
||||
GrayzoneHigh decimal(15,5)
|
||||
GrayzoneTxt text
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLE 7: Specimen Management
|
||||
// ============================================
|
||||
|
||||
Table specimen {
|
||||
InternalSID int [pk, increment]
|
||||
SID varchar(17) [not null, unique]
|
||||
SiteID int [not null, ref: > site.SiteID]
|
||||
OrderID varchar(13)
|
||||
ConDefID int [ref: > containerdef.ConDefID]
|
||||
Parent int [ref: > specimen.InternalSID]
|
||||
Qty decimal(10,4)
|
||||
Unit varchar(20)
|
||||
GenerateBy varchar(100)
|
||||
SchDateTime datetime
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
ArchiveDate datetime
|
||||
}
|
||||
|
||||
Table specimenstatus {
|
||||
SpcStaID int [pk, increment]
|
||||
SID varchar(17) [not null, ref: > specimen.SID]
|
||||
OrderID varchar(13)
|
||||
CurrSiteID int [ref: > site.SiteID]
|
||||
CurrLocID int [ref: > location.LocationID]
|
||||
UserID int
|
||||
SpcAct varchar(50)
|
||||
ActRes varchar(50)
|
||||
SpcStatus varchar(50)
|
||||
Qty decimal(10,4)
|
||||
Unit varchar(20)
|
||||
SpcCon varchar(50)
|
||||
Comment text
|
||||
Origin varchar(100)
|
||||
GeoLocationSystem varchar(50)
|
||||
GeoLocationData text
|
||||
DIDType varchar(50)
|
||||
DID varchar(100)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
ArchiveDate datetime
|
||||
}
|
||||
|
||||
Table specimencollection {
|
||||
SpcColID int [pk, increment]
|
||||
SpcStaID int [not null, ref: > specimenstatus.SpcStaID]
|
||||
SpRole varchar(50)
|
||||
ColMethod varchar(50)
|
||||
BodySite varchar(50)
|
||||
CntSize varchar(50)
|
||||
FastingVolume decimal(10,4)
|
||||
ColStart datetime
|
||||
ColEnd datetime
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
ArchiveDate datetime
|
||||
}
|
||||
|
||||
Table specimenprep {
|
||||
SpcPrpID int [pk, increment]
|
||||
SpcStaID int [not null, ref: > specimenstatus.SpcStaID]
|
||||
Description text
|
||||
Method varchar(255)
|
||||
Additive varchar(100)
|
||||
AddQty decimal(10,4)
|
||||
AddUnit varchar(20)
|
||||
PrepStart datetime
|
||||
PrepEnd datetime
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
ArchiveDate datetime
|
||||
}
|
||||
|
||||
Table containerdef {
|
||||
ConDefID int [pk, increment]
|
||||
SiteID int [not null, ref: > site.SiteID]
|
||||
ConCode varchar(50)
|
||||
ConName varchar(255)
|
||||
ConDesc text
|
||||
Additive varchar(100)
|
||||
ConClass varchar(50)
|
||||
Color varchar(50)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLE 8: Order Management
|
||||
// ============================================
|
||||
|
||||
Table ordertest {
|
||||
InternalOID int [pk, increment]
|
||||
OrderID varchar(13) [not null, unique]
|
||||
PlacerID varchar(100)
|
||||
InternalPID int [not null, ref: > patient.InternalPID]
|
||||
SiteID int [ref: > site.SiteID]
|
||||
PVADTID int [ref: > patvisitadt.PVADTID]
|
||||
ReqApp varchar(100)
|
||||
Priority varchar(50)
|
||||
TrnDate datetime
|
||||
EffDate datetime
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
ArchiveDate datetime
|
||||
DelDate datetime
|
||||
}
|
||||
|
||||
Table patres {
|
||||
ResultID int [pk, increment]
|
||||
SiteID int [ref: > site.SiteID]
|
||||
OrderID varchar(13)
|
||||
InternalSID int [ref: > specimen.InternalSID]
|
||||
SID varchar(17)
|
||||
SampleID varchar(100)
|
||||
TestSiteID int [ref: > testdefsite.TestSiteID]
|
||||
WorkstationID int [ref: > workstation.WorkstationID]
|
||||
EquipmentID int
|
||||
RefNumID int [ref: > refnum.RefNumID]
|
||||
RefTxtID int [ref: > reftxt.RefTxtID]
|
||||
TestSiteCode varchar(50)
|
||||
AspCnt int
|
||||
Result text
|
||||
SampleType varchar(50)
|
||||
ResultDateTime datetime
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
ArchiveDate datetime
|
||||
DelDate datetime
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLE 9: Contact Management
|
||||
// ============================================
|
||||
|
||||
Table contact {
|
||||
ContactID int [pk, increment]
|
||||
NameFirst varchar(255)
|
||||
NameLast varchar(255)
|
||||
Title varchar(100)
|
||||
Initial varchar(50)
|
||||
Birthdate datetime
|
||||
EmailAddress1 varchar(255)
|
||||
EmailAddress2 varchar(255)
|
||||
Phone varchar(50)
|
||||
MobilePhone1 varchar(50)
|
||||
MobilePhone2 varchar(50)
|
||||
Specialty varchar(100)
|
||||
SubSpecialty varchar(100)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table contactdetail {
|
||||
ContactDetID int [pk, increment]
|
||||
ContactID int [not null, ref: > contact.ContactID]
|
||||
SiteID int [ref: > site.SiteID]
|
||||
OccupationID int [ref: > occupation.OccupationID]
|
||||
ContactCode varchar(50)
|
||||
ContactEmail varchar(255)
|
||||
JobTitle varchar(100)
|
||||
Department varchar(100)
|
||||
ContactStartDate datetime
|
||||
ContactEndDate datetime
|
||||
}
|
||||
|
||||
Table medicalspecialty {
|
||||
SpecialtyID int [pk, increment]
|
||||
Parent int [ref: > medicalspecialty.SpecialtyID]
|
||||
SpecialtyText varchar(255)
|
||||
Title varchar(100)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table occupation {
|
||||
OccupationID int [pk, increment]
|
||||
OccCode varchar(50)
|
||||
OccText varchar(255)
|
||||
Description text
|
||||
CreateDate datetime [not null]
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLE 10: Value Sets
|
||||
// ============================================
|
||||
|
||||
Table valuesetdef {
|
||||
VSetID int [pk, increment]
|
||||
SiteID int [not null, ref: > site.SiteID]
|
||||
VSName varchar(255)
|
||||
VSDesc text
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table valueset {
|
||||
VID int [pk, increment]
|
||||
SiteID int [not null, ref: > site.SiteID]
|
||||
VSetID int [not null, ref: > valuesetdef.VSetID]
|
||||
VCategory varchar(100)
|
||||
VOrder int
|
||||
VValue varchar(255)
|
||||
VDesc varchar(255)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TABLE 11: System / Counter
|
||||
// ============================================
|
||||
|
||||
Table counter {
|
||||
CounterID int [pk, increment]
|
||||
CounterValue bigint
|
||||
CounterStart bigint
|
||||
CounterEnd bigint
|
||||
CounterReset varchar(50)
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
}
|
||||
|
||||
Table edgeres {
|
||||
EdgeResID int [pk, increment]
|
||||
SiteID int [ref: > site.SiteID]
|
||||
InstrumentID int
|
||||
SampleID varchar(100)
|
||||
PatientID varchar(100)
|
||||
Payload text
|
||||
Status varchar(50)
|
||||
AutoProcess boolean
|
||||
ProcessedAt datetime
|
||||
ErrorMessage text
|
||||
CreateDate datetime [not null]
|
||||
EndDate datetime
|
||||
ArchiveDate datetime
|
||||
DelDate datetime
|
||||
}
|
||||
|
||||
Table zones {
|
||||
id int [pk, increment]
|
||||
name varchar(255)
|
||||
code varchar(50)
|
||||
type varchar(50)
|
||||
description text
|
||||
status varchar(50)
|
||||
created_at datetime
|
||||
updated_at datetime
|
||||
}
|
||||
@ -1,417 +0,0 @@
|
||||
# 📋 Test Types & Reference Types Guide
|
||||
|
||||
> **Quick Overview**: This guide helps you understand the different types of tests and how to display them in the frontend.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What Are Test Types?
|
||||
|
||||
Think of test types as "categories" that determine how a test behaves and what information it needs.
|
||||
|
||||
### Quick Reference Card
|
||||
|
||||
```
|
||||
┌─────────────┬────────────────────────────┬────────────────────────┐
|
||||
│ Type │ Use This For... │ Example │
|
||||
├─────────────┼────────────────────────────┼────────────────────────┤
|
||||
│ TEST │ Standard lab tests │ Blood Glucose, CBC │
|
||||
│ PARAM │ Components of a test │ WBC count (in CBC) │
|
||||
│ CALC │ Formula-based results │ BMI, eGFR │
|
||||
│ GROUP │ Panels/batteries │ Lipid Panel, CMP │
|
||||
└─────────────┴────────────────────────────┴────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Detailed Test Types
|
||||
|
||||
### 1. TEST - Standard Laboratory Test
|
||||
**Icon**: 🧫 **Color**: Blue
|
||||
|
||||
Use this for regular tests that have:
|
||||
- Reference ranges (normal values)
|
||||
- Units (mg/dL, mmol/L, etc.)
|
||||
- Collection requirements
|
||||
|
||||
**Example**: Blood Glucose, Hemoglobin, Cholesterol
|
||||
|
||||
**What to Display**:
|
||||
- Test code and name
|
||||
- Reference range
|
||||
- Units
|
||||
- Collection instructions
|
||||
- Expected turnaround time
|
||||
|
||||
---
|
||||
|
||||
### 2. PARAM - Parameter
|
||||
**Icon**: 📊 **Color**: Light Blue
|
||||
|
||||
Use this for individual components within a larger test.
|
||||
|
||||
**Example**:
|
||||
- Complete Blood Count (GROUP) contains:
|
||||
- WBC (PARAM)
|
||||
- RBC (PARAM)
|
||||
- Hemoglobin (PARAM)
|
||||
|
||||
**What to Display**:
|
||||
- Same as TEST, but shown indented under parent
|
||||
- Often part of a GROUP
|
||||
|
||||
---
|
||||
|
||||
### 3. CALC - Calculated Test
|
||||
**Icon**: 🧮 **Color**: Purple
|
||||
|
||||
Use this for tests computed from other test results using formulas.
|
||||
|
||||
**Example**:
|
||||
- BMI (calculated from height & weight)
|
||||
- eGFR (calculated from creatinine, age, etc.)
|
||||
|
||||
**What to Display**:
|
||||
- Formula description
|
||||
- Input parameters (which tests feed into this)
|
||||
- Result value
|
||||
- Reference range (if applicable)
|
||||
|
||||
**Special Fields**:
|
||||
- `FormulaInput` - What values go in?
|
||||
- `FormulaCode` - How is it calculated?
|
||||
|
||||
---
|
||||
|
||||
### 4. GROUP - Group Test (Panel/Battery)
|
||||
**Icon**: 📦 **Color**: Green
|
||||
|
||||
Use this to bundle multiple related tests together.
|
||||
|
||||
**Example**:
|
||||
- Lipid Panel (GROUP) contains:
|
||||
- Total Cholesterol (PARAM)
|
||||
- HDL (PARAM)
|
||||
- LDL (PARAM)
|
||||
- Triglycerides (PARAM)
|
||||
|
||||
**What to Display**:
|
||||
- Group name
|
||||
- List of member tests
|
||||
- Individual results for each member
|
||||
|
||||
**Structure**:
|
||||
```
|
||||
📦 Lipid Panel (GROUP)
|
||||
├── Total Cholesterol (PARAM): 180 mg/dL
|
||||
├── HDL (PARAM): 55 mg/dL
|
||||
├── LDL (PARAM): 110 mg/dL
|
||||
└── Triglycerides (PARAM): 150 mg/dL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📐 Reference Types Explained
|
||||
|
||||
Reference types tell you **how to interpret the results** - what "normal" looks like.
|
||||
|
||||
### Choose Your Reference Type:
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Reference Type? │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌───────────────────┼───────────────────┐
|
||||
│ │ │
|
||||
Numbers? Text values? Threshold?
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
|
||||
│ NMRC │ │ TEXT │ │ THOLD │
|
||||
│ (Numeric) │ │ (Text) │ │ (Threshold) │
|
||||
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
|
||||
│ RANGE │ │ Free │ │ Positive/ │
|
||||
│ (Min-Max) │ │ Text │ │ Negative │
|
||||
│ OR │ │ OR │ │ OR │
|
||||
│ THOLD │ │ VSET │ │ < > = │
|
||||
│ (Threshold) │ │ (Dropdown) │ │ cutoff │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
### Reference Type Details
|
||||
|
||||
| Type | Visual | Example |
|
||||
|------|--------|---------|
|
||||
| **NMRC** - Numeric Range | `70 - 100 mg/dL` | Blood glucose: 70-100 mg/dL |
|
||||
| **TEXT** - Text Value | `"Normal"` or `"Positive"` | Urinalysis: "Clear", "Cloudy" |
|
||||
| **THOLD** - Threshold | `> 60` or `< 5.5` | eGFR: > 60 (normal) |
|
||||
| **VSET** - Value Set | Dropdown options | Organism: [E.coli, Staph, etc.] |
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Frontend Display Patterns
|
||||
|
||||
### Pattern 1: Test List View
|
||||
|
||||
```javascript
|
||||
// When showing a list of tests
|
||||
function renderTestCard(test) {
|
||||
const typeIcon = getIcon(test.TestType);
|
||||
const typeColor = getColor(test.TestType);
|
||||
|
||||
return `
|
||||
<div class="test-card ${typeColor}">
|
||||
<span class="icon">${typeIcon}</span>
|
||||
<h3>${test.TestSiteName}</h3>
|
||||
<span class="badge">${test.TestTypeLabel}</span>
|
||||
<code>${test.TestSiteCode}</code>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Reference Range Display
|
||||
|
||||
```javascript
|
||||
// Show reference range based on type
|
||||
function renderReferenceRange(test) {
|
||||
switch(test.RefType) {
|
||||
case 'NMRC':
|
||||
return `${test.MinValue} - ${test.MaxValue} ${test.Unit}`;
|
||||
|
||||
case 'TEXT':
|
||||
return test.ReferenceText || 'See report';
|
||||
|
||||
case 'THOLD':
|
||||
return `${test.ThresholdOperator} ${test.ThresholdValue}`;
|
||||
|
||||
case 'VSET':
|
||||
return 'Select from list';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Group Test Expansion
|
||||
|
||||
```javascript
|
||||
// Expandable group test
|
||||
function renderGroupTest(test) {
|
||||
return `
|
||||
<div class="group-test">
|
||||
<button onclick="toggleGroup(${test.TestSiteID})">
|
||||
📦 ${test.TestSiteName} (${test.testdefgrp.length} tests)
|
||||
</button>
|
||||
<div class="members" id="group-${test.TestSiteID}">
|
||||
${test.testdefgrp.map(member => renderTestRow(member)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Data Structure Visualization
|
||||
|
||||
### Test Hierarchy
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ TEST DEFINITION │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ TestSiteID: 12345 │
|
||||
│ TestSiteCode: GLUC │
|
||||
│ TestSiteName: Glucose │
|
||||
│ TestType: TEST │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 📎 Additional Info (loaded based on TestType): │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ TestDefTech (for TEST/PARAM) │ │
|
||||
│ │ - DisciplineID, DepartmentID │ │
|
||||
│ │ - ResultType, RefType │ │
|
||||
│ │ - Unit, Method, ExpectedTAT │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ TestDefCal (for CALC) │ │
|
||||
│ │ - FormulaInput, FormulaCode │ │
|
||||
│ │ - RefType, Unit, Decimal │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ TestDefGrp (for GROUP) │ │
|
||||
│ │ - Array of member tests │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ TestMap (for ALL types) │ │
|
||||
│ │ - Mapping to external systems │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Common Scenarios
|
||||
|
||||
### Scenario 1: Creating a Simple Test
|
||||
|
||||
**Need**: Add "Hemoglobin" test
|
||||
**Choose**: TEST type with NMRC reference
|
||||
|
||||
```javascript
|
||||
const hemoglobin = {
|
||||
TestSiteCode: 'HGB',
|
||||
TestSiteName: 'Hemoglobin',
|
||||
TestType: 'TEST',
|
||||
testdeftech: {
|
||||
RefType: 'NMRC',
|
||||
Unit: 'g/dL',
|
||||
// Reference range: 12-16 g/dL
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Scenario 2: Creating a Panel
|
||||
|
||||
**Need**: "Complete Blood Count" with multiple components
|
||||
**Choose**: GROUP type with PARAM children
|
||||
|
||||
```javascript
|
||||
const cbc = {
|
||||
TestSiteCode: 'CBC',
|
||||
TestSiteName: 'Complete Blood Count',
|
||||
TestType: 'GROUP',
|
||||
testdefgrp: [
|
||||
{ TestSiteCode: 'WBC', TestType: 'PARAM' },
|
||||
{ TestSiteCode: 'RBC', TestType: 'PARAM' },
|
||||
{ TestSiteCode: 'HGB', TestType: 'PARAM' },
|
||||
{ TestSiteCode: 'PLT', TestType: 'PARAM' }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### Scenario 3: Calculated Result
|
||||
|
||||
**Need**: BMI from height and weight
|
||||
**Choose**: CALC type with formula
|
||||
|
||||
```javascript
|
||||
const bmi = {
|
||||
TestSiteCode: 'BMI',
|
||||
TestSiteName: 'Body Mass Index',
|
||||
TestType: 'CALC',
|
||||
testdefcal: {
|
||||
FormulaInput: 'HEIGHT,WEIGHT',
|
||||
FormulaCode: 'WEIGHT / ((HEIGHT/100) * (HEIGHT/100))',
|
||||
RefType: 'NMRC',
|
||||
Unit: 'kg/m²'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Visual Style Guide
|
||||
|
||||
### Color Coding
|
||||
|
||||
| Type | Primary Color | Background | Usage |
|
||||
|------|---------------|------------|-------|
|
||||
| TEST | `#0066CC` | `#E6F2FF` | Main test cards |
|
||||
| PARAM | `#3399FF` | `#F0F8FF` | Component rows |
|
||||
| CALC | `#9933CC` | `#F5E6FF` | Calculated fields |
|
||||
| GROUP | `#00AA44` | `#E6F9EE` | Expandable panels |
|
||||
|
||||
### Icons
|
||||
|
||||
| Type | Icon | Unicode |
|
||||
|------|------|---------|
|
||||
| TEST | 🧫 | `\u{1F9EB}` |
|
||||
| PARAM | 📊 | `\u{1F4CA}` |
|
||||
| CALC | 🧮 | `\u{1F9EE}` |
|
||||
| GROUP | 📦 | `\u{1F4E6}` |
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Quick API Reference
|
||||
|
||||
### Fetch Tests
|
||||
|
||||
```javascript
|
||||
// Get all tests for a site
|
||||
GET /api/tests?siteId=123
|
||||
|
||||
// Get specific test type
|
||||
GET /api/tests?testType=GROUP
|
||||
|
||||
// Search tests
|
||||
GET /api/tests?search=glucose
|
||||
|
||||
// Get single test with details
|
||||
GET /api/tests/12345
|
||||
```
|
||||
|
||||
### Value Set Helpers
|
||||
|
||||
```javascript
|
||||
// Get human-readable labels
|
||||
const labels = {
|
||||
testType: valueSet.getLabel('test_type', test.TestType),
|
||||
refType: valueSet.getLabel('reference_type', test.RefType),
|
||||
resultType: valueSet.getLabel('result_type', test.ResultType)
|
||||
};
|
||||
|
||||
// Get dropdown options
|
||||
const testTypes = valueSet.get('test_type');
|
||||
// Returns: [{value: "TEST", label: "Test"}, ...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Value Set File Locations
|
||||
|
||||
```
|
||||
app/Libraries/Data/
|
||||
├── test_type.json # TEST, PARAM, CALC, GROUP
|
||||
├── reference_type.json # NMRC, TEXT, THOLD, VSET
|
||||
├── result_type.json # NMRIC, RANGE, TEXT, VSET
|
||||
├── numeric_ref_type.json # RANGE, THOLD
|
||||
└── text_ref_type.json # VSET, TEXT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist for Frontend Developers
|
||||
|
||||
- [ ] Show correct icon for each test type
|
||||
- [ ] Display reference ranges based on RefType
|
||||
- [ ] Handle GROUP tests as expandable panels
|
||||
|
||||
- [ ] Display CALC formulas when available
|
||||
- [ ] Use ValueSet labels for all coded fields
|
||||
- [ ] Respect VisibleScr/VisibleRpt flags
|
||||
- [ ] Handle soft-deleted tests (EndDate check)
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Need Help?
|
||||
|
||||
**Q: When do I use PARAM vs TEST?**
|
||||
A: Use PARAM for sub-components of a GROUP. Use TEST for standalone tests.
|
||||
|
||||
**Q: What's the difference between NMRC and THOLD?**
|
||||
A: NMRC shows a range (70-100). THOLD shows a threshold (> 60 or < 5.5).
|
||||
|
||||
**Q: Can a GROUP contain other GROUPs?**
|
||||
A: Yes! Groups can be nested (though typically only 1-2 levels deep).
|
||||
|
||||
**Q: How do I know which details to fetch?**
|
||||
A: Check `TestType` first, then look for the corresponding property:
|
||||
- TEST/PARAM → `testdeftech`
|
||||
- CALC → `testdefcal`
|
||||
- GROUP → `testdefgrp`
|
||||
- All types → `testmap`
|
||||
|
||||
34
src/app.css
34
src/app.css
@ -37,23 +37,41 @@
|
||||
--color-error: oklch(60% 0.25 25);
|
||||
--color-error-content: oklch(98% 0.01 25);
|
||||
|
||||
/* Border radius */
|
||||
--radius-selector: 0.5rem;
|
||||
--radius-field: 0.375rem;
|
||||
--radius-box: 0.5rem;
|
||||
/* Border radius - smaller for compact look */
|
||||
--radius-selector: 0.25rem;
|
||||
--radius-field: 0.25rem;
|
||||
--radius-box: 0.375rem;
|
||||
|
||||
/* Base sizes */
|
||||
--size-selector: 0.25rem;
|
||||
--size-field: 0.25rem;
|
||||
/* Base sizes - reduced for compact UI */
|
||||
--size-selector: 0.2rem;
|
||||
--size-field: 0.2rem;
|
||||
|
||||
/* Border size */
|
||||
--border: 1px;
|
||||
|
||||
/* Effects */
|
||||
--depth: 1;
|
||||
--depth: 0;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
/* Global compact utility classes */
|
||||
@layer utilities {
|
||||
/* Compact spacing */
|
||||
.compact-y { @apply space-y-3; }
|
||||
.compact-gap { @apply gap-3; }
|
||||
.compact-p { @apply p-4; }
|
||||
.compact-py { @apply py-3; }
|
||||
.compact-px { @apply px-3; }
|
||||
|
||||
/* Compact form elements */
|
||||
.compact-input { @apply input-sm; }
|
||||
.compact-btn { @apply btn-sm; }
|
||||
.compact-select { @apply select-sm; }
|
||||
|
||||
/* Compact cards */
|
||||
.compact-card { @apply p-4; }
|
||||
}
|
||||
|
||||
@theme {
|
||||
/* Custom color helpers */
|
||||
--color-emerald-50: #ecfdf5;
|
||||
|
||||
@ -32,9 +32,24 @@ export async function createTest(data) {
|
||||
// Type-specific fields
|
||||
Unit: data.Unit,
|
||||
Formula: data.Formula,
|
||||
// Technical Config
|
||||
ResultType: data.ResultType,
|
||||
RefType: data.RefType,
|
||||
SpcType: data.SpcType,
|
||||
ReqQty: data.ReqQty,
|
||||
ReqQtyUnit: data.ReqQtyUnit,
|
||||
Unit1: data.Unit1,
|
||||
Factor: data.Factor,
|
||||
Unit2: data.Unit2,
|
||||
Decimal: data.Decimal,
|
||||
CollReq: data.CollReq,
|
||||
Method: data.Method,
|
||||
ExpectedTAT: data.ExpectedTAT,
|
||||
// Reference ranges (only for TEST and CALC)
|
||||
refnum: data.refnum,
|
||||
reftxt: data.reftxt,
|
||||
refvset: data.refvset,
|
||||
refthold: data.refthold,
|
||||
};
|
||||
return post('/api/tests', payload);
|
||||
}
|
||||
@ -54,9 +69,24 @@ export async function updateTest(data) {
|
||||
// Type-specific fields
|
||||
Unit: data.Unit,
|
||||
Formula: data.Formula,
|
||||
// Technical Config
|
||||
ResultType: data.ResultType,
|
||||
RefType: data.RefType,
|
||||
SpcType: data.SpcType,
|
||||
ReqQty: data.ReqQty,
|
||||
ReqQtyUnit: data.ReqQtyUnit,
|
||||
Unit1: data.Unit1,
|
||||
Factor: data.Factor,
|
||||
Unit2: data.Unit2,
|
||||
Decimal: data.Decimal,
|
||||
CollReq: data.CollReq,
|
||||
Method: data.Method,
|
||||
ExpectedTAT: data.ExpectedTAT,
|
||||
// Reference ranges (only for TEST and CALC)
|
||||
refnum: data.refnum,
|
||||
reftxt: data.reftxt,
|
||||
refvset: data.refvset,
|
||||
refthold: data.refthold,
|
||||
};
|
||||
return patch('/api/tests', payload);
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
</script>
|
||||
|
||||
<div class="overflow-x-auto {className}">
|
||||
<table class="table w-full" class:table-zebra={striped} class:table-bordered={bordered}>
|
||||
<table class="table table-compact w-full" class:table-zebra={striped} class:table-bordered={bordered}>
|
||||
<thead>
|
||||
<tr class="bg-base-200">
|
||||
{#each columns as column}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
* @property {string} [title] - Modal title
|
||||
* @property {string} [size] - Modal size (sm, md, lg, xl, full)
|
||||
* @property {boolean} [closable] - Whether modal can be closed by clicking backdrop
|
||||
* @property {'center' | 'top'} [position] - Modal position (center or top)
|
||||
* @property {Function} [onClose] - Callback when modal is closed
|
||||
* @property {import('svelte').Snippet} [children] - Modal content
|
||||
* @property {import('svelte').Snippet} [footer] - Modal footer
|
||||
@ -18,6 +19,7 @@
|
||||
title = '',
|
||||
size = 'md',
|
||||
closable = true,
|
||||
position = 'center',
|
||||
onClose = null,
|
||||
children,
|
||||
footer,
|
||||
@ -33,12 +35,12 @@
|
||||
};
|
||||
|
||||
const widthStyles = {
|
||||
sm: 'max-width: 400px;',
|
||||
md: 'max-width: 500px;',
|
||||
lg: 'max-width: 800px;',
|
||||
xl: 'max-width: 1200px;',
|
||||
sm: 'max-width: 350px;',
|
||||
md: 'max-width: 450px;',
|
||||
lg: 'max-width: 700px;',
|
||||
xl: 'max-width: 1000px;',
|
||||
full: 'max-width: 100%; width: 100%; height: 100%;',
|
||||
wide: 'max-width: 90vw; width: 1200px;',
|
||||
wide: 'max-width: 90vw; width: 1000px;',
|
||||
};
|
||||
|
||||
/**
|
||||
@ -77,7 +79,7 @@
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<dialog class="modal {sizeClasses[size] || ''}" class:modal-open={open}>
|
||||
<div class="modal-box" style={widthStyles[size] || ''} role="dialog" aria-modal="true" aria-labelledby={title ? 'modal-title' : undefined}>
|
||||
<div class="modal-box" style="{widthStyles[size] || ''} {position === 'top' ? 'align-self: flex-start; margin-top: 2rem;' : ''}" role="dialog" aria-modal="true" aria-labelledby={title ? 'modal-title' : undefined}>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
{#if title}
|
||||
|
||||
@ -88,7 +88,7 @@
|
||||
bind:value
|
||||
{required}
|
||||
{disabled}
|
||||
class="select select-bordered w-full pr-10 {error ? 'select-error' : ''}"
|
||||
class="select select-sm select-bordered w-full pr-10 bg-none {error ? 'select-error' : ''}"
|
||||
class:opacity-50={loading}
|
||||
>
|
||||
<option value="">{placeholder}</option>
|
||||
|
||||
@ -68,276 +68,171 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Function to expand sidebar when clicking dropdown in collapsed mode
|
||||
function expandSidebar() {
|
||||
isOpen = true;
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
auth.logout();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
function toggleMasterData() {
|
||||
if (!isOpen) {
|
||||
expandSidebar();
|
||||
}
|
||||
masterDataExpanded = !masterDataExpanded;
|
||||
}
|
||||
|
||||
function toggleLaboratory() {
|
||||
if (!isOpen) {
|
||||
expandSidebar();
|
||||
}
|
||||
laboratoryExpanded = !laboratoryExpanded;
|
||||
}
|
||||
|
||||
function toggleAdministration() {
|
||||
if (!isOpen) {
|
||||
expandSidebar();
|
||||
}
|
||||
administrationExpanded = !administrationExpanded;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div
|
||||
class="sidebar-container fixed lg:sticky left-0 top-0 h-screen max-h-screen z-40 bg-base-200 shadow-xl border-r border-base-300 transition-all duration-300 ease-out"
|
||||
class="sidebar-container fixed lg:sticky left-0 top-0 h-screen max-h-screen z-40 bg-base-200 border-r border-base-300 transition-all duration-300 ease-out"
|
||||
class:sidebar-expanded={isOpen}
|
||||
class:sidebar-collapsed={!isOpen}
|
||||
>
|
||||
<div class="h-screen overflow-y-auto flex flex-col sidebar-content" class:expanded={isOpen} class:collapsed={!isOpen}>
|
||||
<div class="p-3">
|
||||
<div>
|
||||
<!-- Navigation Menu -->
|
||||
<ul class="menu w-full gap-1" class:menu-collapsed={!isOpen}>
|
||||
{#if isOpen}
|
||||
<li class="menu-title uppercase font-bold text-xs text-secondary/70 mt-2">Main</li>
|
||||
{/if}
|
||||
|
||||
<!-- Dashboard -->
|
||||
<li>
|
||||
<a
|
||||
href="/dashboard"
|
||||
class="menu-item text-gray-700 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
class="nav-link"
|
||||
class:centered={!isOpen}
|
||||
title={!isOpen ? 'Dashboard' : ''}
|
||||
>
|
||||
<LayoutDashboard class="w-5 h-5 text-secondary flex-shrink-0" />
|
||||
<LayoutDashboard size={20} class="text-secondary flex-shrink-0" />
|
||||
{#if isOpen}
|
||||
<span class="menu-text whitespace-nowrap overflow-hidden">Dashboard</span>
|
||||
<span class="nav-text">Dashboard</span>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
<li class="collapsible-section">
|
||||
|
||||
<!-- Master Data -->
|
||||
<li class="nav-group" class:collapsed={!isOpen}>
|
||||
<button
|
||||
onclick={toggleMasterData}
|
||||
class="menu-item text-gray-700 hover:bg-secondary/10 hover:text-secondary-focus w-full text-left justify-between"
|
||||
class="nav-link"
|
||||
class:centered={!isOpen}
|
||||
title={!isOpen ? 'Master Data' : ''}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Database class="w-5 h-5 text-secondary flex-shrink-0" />
|
||||
<Database size={20} class="text-secondary flex-shrink-0" />
|
||||
{#if isOpen}
|
||||
<span class="menu-text whitespace-nowrap overflow-hidden">Master Data</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isOpen}
|
||||
<ChevronDown class="w-4 h-4 flex-shrink-0 transition-transform duration-200 {masterDataExpanded ? 'rotate-180' : ''}" />
|
||||
<span class="nav-text">Master Data</span>
|
||||
<ChevronDown size={16} class="chevron {masterDataExpanded ? 'expanded' : ''}" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if isOpen && masterDataExpanded}
|
||||
<ul class="ml-6 mt-1 space-y-1 collapsible-content">
|
||||
<li>
|
||||
<a
|
||||
href="/master-data/containers"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<FlaskConical class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Containers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/master-data/tests"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<TestTube class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Test Definitions</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/master-data/valuesets"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<List class="w-4 h-4 flex-shrink-0" />
|
||||
<span>ValueSets</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/master-data/locations"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<MapPin class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Locations</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/master-data/contacts"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<Users class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Contacts</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/master-data/specialties"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<Stethoscope class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Specialties</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/master-data/occupations"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<Briefcase class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Occupations</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/master-data/counters"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<Hash class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Counters</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/master-data/geography"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<Globe class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Geography</span>
|
||||
</a>
|
||||
</li>
|
||||
<ul class="submenu">
|
||||
<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/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/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/occupations" class="submenu-link"><Briefcase size={16} /> Occupations</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>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
<!-- Result Entry -->
|
||||
<li>
|
||||
<a
|
||||
href="/result-entry"
|
||||
class="menu-item text-gray-700 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
class="nav-link"
|
||||
class:centered={!isOpen}
|
||||
title={!isOpen ? 'Result Entry' : ''}
|
||||
>
|
||||
<FileText class="w-5 h-5 text-secondary flex-shrink-0" />
|
||||
<FileText size={20} class="text-secondary flex-shrink-0" />
|
||||
{#if isOpen}
|
||||
<span class="menu-text whitespace-nowrap overflow-hidden">Result Entry</span>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/reports"
|
||||
class="menu-item text-gray-700 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
class:centered={!isOpen}
|
||||
title={!isOpen ? 'Reports' : ''}
|
||||
>
|
||||
<Printer class="w-5 h-5 text-secondary flex-shrink-0" />
|
||||
{#if isOpen}
|
||||
<span class="menu-text whitespace-nowrap overflow-hidden">Reports</span>
|
||||
<span class="nav-text">Result Entry</span>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="collapsible-section">
|
||||
<!-- Reports -->
|
||||
<li>
|
||||
<a
|
||||
href="/reports"
|
||||
class="nav-link"
|
||||
class:centered={!isOpen}
|
||||
title={!isOpen ? 'Reports' : ''}
|
||||
>
|
||||
<Printer size={20} class="text-secondary flex-shrink-0" />
|
||||
{#if isOpen}
|
||||
<span class="nav-text">Reports</span>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Laboratory -->
|
||||
<li class="nav-group" class:collapsed={!isOpen}>
|
||||
<button
|
||||
onclick={toggleLaboratory}
|
||||
class="menu-item text-gray-700 hover:bg-secondary/10 hover:text-secondary-focus w-full text-left justify-between"
|
||||
class="nav-link"
|
||||
class:centered={!isOpen}
|
||||
title={!isOpen ? 'Laboratory' : ''}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<FlaskConical class="w-5 h-5 text-secondary flex-shrink-0" />
|
||||
<FlaskConical size={20} class="text-secondary flex-shrink-0" />
|
||||
{#if isOpen}
|
||||
<span class="menu-text whitespace-nowrap overflow-hidden">Laboratory</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isOpen}
|
||||
<ChevronDown class="w-4 h-4 flex-shrink-0 transition-transform duration-200 {laboratoryExpanded ? 'rotate-180' : ''}" />
|
||||
<span class="nav-text">Laboratory</span>
|
||||
<ChevronDown size={16} class="chevron {laboratoryExpanded ? 'expanded' : ''}" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if isOpen && laboratoryExpanded}
|
||||
<ul class="ml-6 mt-1 space-y-1 collapsible-content">
|
||||
<li>
|
||||
<a
|
||||
href="/patients"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<Users class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Patients</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/orders"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<ClipboardList class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Orders</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/specimens"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<FlaskConical class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Specimens</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/results"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<CheckCircle2 class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Results</span>
|
||||
</a>
|
||||
</li>
|
||||
<ul class="submenu">
|
||||
<li><a href="/patients" class="submenu-link"><Users size={16} /> Patients</a></li>
|
||||
<li><a href="/orders" class="submenu-link"><ClipboardList size={16} /> Orders</a></li>
|
||||
<li><a href="/specimens" class="submenu-link"><FlaskConical size={16} /> Specimens</a></li>
|
||||
<li><a href="/results" class="submenu-link"><CheckCircle2 size={16} /> Results</a></li>
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
<li class="collapsible-section">
|
||||
<!-- Administration -->
|
||||
<li class="nav-group" class:collapsed={!isOpen}>
|
||||
<button
|
||||
onclick={toggleAdministration}
|
||||
class="menu-item text-gray-700 hover:bg-secondary/10 hover:text-secondary-focus w-full text-left justify-between"
|
||||
class="nav-link"
|
||||
class:centered={!isOpen}
|
||||
title={!isOpen ? 'Administration' : ''}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Building2 class="w-5 h-5 text-secondary flex-shrink-0" />
|
||||
<Building2 size={20} class="text-secondary flex-shrink-0" />
|
||||
{#if isOpen}
|
||||
<span class="menu-text whitespace-nowrap overflow-hidden">Administration</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isOpen}
|
||||
<ChevronDown class="w-4 h-4 flex-shrink-0 transition-transform duration-200 {administrationExpanded ? 'rotate-180' : ''}" />
|
||||
<span class="nav-text">Administration</span>
|
||||
<ChevronDown size={16} class="chevron {administrationExpanded ? 'expanded' : ''}" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if isOpen && administrationExpanded}
|
||||
<ul class="ml-6 mt-1 space-y-1 collapsible-content">
|
||||
<li>
|
||||
<a
|
||||
href="/organization"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<Building2 class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Organization</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/users"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
||||
>
|
||||
<UserCircle class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Users</span>
|
||||
</a>
|
||||
</li>
|
||||
<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>
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
@ -349,13 +244,13 @@
|
||||
<li>
|
||||
<button
|
||||
onclick={handleLogout}
|
||||
class="menu-item text-red-500 hover:bg-red-50 w-full text-left"
|
||||
class="nav-link text-red-500 hover:bg-red-50"
|
||||
class:centered={!isOpen}
|
||||
title={!isOpen ? 'Logout' : ''}
|
||||
>
|
||||
<LogOut class="w-5 h-5 flex-shrink-0" />
|
||||
<LogOut size={20} class="flex-shrink-0" />
|
||||
{#if isOpen}
|
||||
<span class="menu-text whitespace-nowrap overflow-hidden">Logout</span>
|
||||
<span class="nav-text">Logout</span>
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
@ -371,11 +266,11 @@
|
||||
}
|
||||
|
||||
.sidebar-expanded {
|
||||
width: 14rem;
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
.sidebar-collapsed {
|
||||
width: 4rem;
|
||||
width: 3.5rem;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
@ -383,55 +278,97 @@
|
||||
}
|
||||
|
||||
.sidebar-content.expanded {
|
||||
width: 14rem;
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
.sidebar-content.collapsed {
|
||||
width: 4rem;
|
||||
width: 3.5rem;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s;
|
||||
gap: 0.5rem;
|
||||
color: hsl(var(--bc));
|
||||
transition: background-color 0.2s;
|
||||
width: 100%;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.menu-item.centered {
|
||||
.nav-link:hover {
|
||||
background-color: hsl(var(--b3));
|
||||
}
|
||||
|
||||
.nav-link.centered {
|
||||
justify-content: center;
|
||||
padding: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
width: 2.5rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
height: 2.5rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
flex: 1;
|
||||
transition: opacity 300ms ease-out;
|
||||
}
|
||||
|
||||
/* Collapsed menu styling */
|
||||
.menu-collapsed :global(li > a),
|
||||
.menu-collapsed :global(li > button) {
|
||||
/* Fix for collapsed menu items - override DaisyUI .menu styles */
|
||||
:global(.menu-collapsed li) {
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
width: 100% !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Collapsible section styles */
|
||||
.collapsible-section button {
|
||||
:global(.menu-collapsed li > a),
|
||||
:global(.menu-collapsed li > button) {
|
||||
margin: 0 !important;
|
||||
border: none !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.chevron.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.submenu {
|
||||
margin-left: 1.5rem;
|
||||
margin-top: 0.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
}
|
||||
|
||||
.submenu-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--bc) / 0.8);
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.collapsible-section button.centered {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.collapsible-content {
|
||||
animation: slideDown 0.2s ease-out;
|
||||
.submenu-link:hover {
|
||||
background-color: hsl(var(--b3));
|
||||
color: hsl(var(--bc));
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
|
||||
@ -1,104 +0,0 @@
|
||||
<script>
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import BasicInfoForm from '$lib/components/test-modal/BasicInfoForm.svelte';
|
||||
import ReferenceRangeSection from '$lib/components/test-modal/ReferenceRangeSection.svelte';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {boolean} open - Whether modal is open
|
||||
* @property {string} mode - 'create' or 'edit'
|
||||
* @property {Object} formData - Form data object
|
||||
* @property {boolean} canHaveRefRange - Whether test can have reference ranges
|
||||
* @property {boolean} canHaveFormula - Whether test can have a formula
|
||||
* @property {boolean} canHaveUnit - Whether test can have a unit
|
||||
* @property {Array<{value: string, label: string}>} disciplineOptions - Discipline dropdown options
|
||||
* @property {Array<{value: string, label: string}>} departmentOptions - Department dropdown options
|
||||
* @property {boolean} [saving] - Whether save is in progress
|
||||
*/
|
||||
|
||||
/** @type {Props & { onsave?: () => void, oncancel?: () => void, onupdateFormData?: (formData: Object) => void }} */
|
||||
let {
|
||||
open = $bindable(false),
|
||||
mode = 'create',
|
||||
formData = $bindable({}),
|
||||
canHaveRefRange = false,
|
||||
canHaveFormula = false,
|
||||
canHaveUnit = false,
|
||||
disciplineOptions = [],
|
||||
departmentOptions = [],
|
||||
saving = false,
|
||||
onsave = () => {},
|
||||
oncancel = () => {},
|
||||
onupdateFormData = () => {}
|
||||
} = $props();
|
||||
|
||||
// Local state
|
||||
let activeTab = $state('basic');
|
||||
|
||||
function handleCancel() {
|
||||
activeTab = 'basic';
|
||||
oncancel();
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
onsave();
|
||||
}
|
||||
|
||||
// Reactive update when modal opens
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
activeTab = 'basic';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal bind:open title={mode === 'create' ? 'Add Test' : 'Edit Test'} size="xl">
|
||||
<!-- Tabs -->
|
||||
<div class="tabs tabs-bordered mb-4">
|
||||
<button
|
||||
type="button"
|
||||
class="tab tab-lg {activeTab === 'basic' ? 'tab-active' : ''}"
|
||||
onclick={() => activeTab = 'basic'}
|
||||
>
|
||||
Basic Information
|
||||
</button>
|
||||
{#if canHaveRefRange}
|
||||
<button
|
||||
type="button"
|
||||
class="tab tab-lg {activeTab === 'refrange' ? 'tab-active' : ''}"
|
||||
onclick={() => activeTab = 'refrange'}
|
||||
>
|
||||
Reference Range
|
||||
{#if formData.refnum?.length > 0 || formData.reftxt?.length > 0}
|
||||
<span class="badge badge-sm badge-primary ml-2">{(formData.refnum?.length || 0) + (formData.reftxt?.length || 0)}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if activeTab === 'basic'}
|
||||
<BasicInfoForm
|
||||
bind:formData
|
||||
{canHaveFormula}
|
||||
{canHaveUnit}
|
||||
{disciplineOptions}
|
||||
{departmentOptions}
|
||||
onsave={handleSave}
|
||||
/>
|
||||
{:else if activeTab === 'refrange' && canHaveRefRange}
|
||||
<ReferenceRangeSection
|
||||
bind:formData
|
||||
{onupdateFormData}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#snippet footer()}
|
||||
<button class="btn btn-ghost" onclick={handleCancel} type="button">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'}
|
||||
</button>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
@ -1,185 +0,0 @@
|
||||
<script>
|
||||
import SelectDropdown from '$lib/components/SelectDropdown.svelte';
|
||||
import HelpTooltip from '$lib/components/HelpTooltip.svelte';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {Object} formData - Form data object
|
||||
* @property {boolean} canHaveFormula - Whether test can have a formula
|
||||
* @property {boolean} canHaveUnit - Whether test can have a unit
|
||||
* @property {Array<{value: string, label: string}>} disciplineOptions - Discipline dropdown options
|
||||
* @property {Array<{value: string, label: string}>} departmentOptions - Department dropdown options
|
||||
* @property {() => void} onsave - Save handler
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
formData = $bindable({}),
|
||||
canHaveFormula = false,
|
||||
canHaveUnit = false,
|
||||
disciplineOptions = [],
|
||||
departmentOptions = [],
|
||||
onsave = () => {}
|
||||
} = $props();
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
onsave();
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="space-y-5" onsubmit={handleSubmit}>
|
||||
<!-- Basic Info -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="testCode">
|
||||
<span class="label-text font-medium">Test Code</span>
|
||||
<span class="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="testCode"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.TestSiteCode}
|
||||
placeholder="e.g., GLU"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="testName">
|
||||
<span class="label-text font-medium">Test Name</span>
|
||||
<span class="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="testName"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.TestSiteName}
|
||||
placeholder="e.g., Glucose"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Type and Sequence -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="testType">
|
||||
<span class="label-text font-medium">Test Type</span>
|
||||
<span class="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="testType"
|
||||
class="select select-bordered w-full"
|
||||
bind:value={formData.TestType}
|
||||
required
|
||||
>
|
||||
<option value="TEST">🧫 Technical Test</option>
|
||||
<option value="PARAM">📊 Parameter</option>
|
||||
<option value="CALC">🧮 Calculated</option>
|
||||
<option value="GROUP">📦 Panel/Profile</option>
|
||||
<option value="TITLE">📑 Section Header</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="seqScr">
|
||||
<span class="label-text font-medium">Screen Sequence</span>
|
||||
</label>
|
||||
<input
|
||||
id="seqScr"
|
||||
type="number"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.SeqScr}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discipline and Department -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<SelectDropdown
|
||||
label="Discipline"
|
||||
name="discipline"
|
||||
bind:value={formData.DisciplineID}
|
||||
options={disciplineOptions}
|
||||
placeholder="Select discipline..."
|
||||
/>
|
||||
<SelectDropdown
|
||||
label="Department"
|
||||
name="department"
|
||||
bind:value={formData.DepartmentID}
|
||||
options={departmentOptions}
|
||||
placeholder="Select department..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Type-specific fields -->
|
||||
{#if canHaveUnit}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{#if canHaveFormula}
|
||||
<div class="form-control">
|
||||
<label class="label" for="formula">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
Formula
|
||||
<HelpTooltip
|
||||
text="Enter a mathematical formula using test codes (e.g., BUN / Creatinine). Supported operators: +, -, *, /, parentheses."
|
||||
title="Formula Help"
|
||||
/>
|
||||
</span>
|
||||
<span class="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="formula"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.Formula}
|
||||
placeholder="e.g., BUN / Creatinine"
|
||||
required={canHaveFormula}
|
||||
/>
|
||||
<span class="label-text-alt text-gray-500">Use test codes with operators: +, -, *, /</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="form-control">
|
||||
<label class="label" for="unit">
|
||||
<span class="label-text font-medium">Unit</span>
|
||||
</label>
|
||||
<input
|
||||
id="unit"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.Unit}
|
||||
placeholder="e.g., mg/dL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Report Sequence and Visibility -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="seqRpt">
|
||||
<span class="label-text font-medium">Report Sequence</span>
|
||||
</label>
|
||||
<input
|
||||
id="seqRpt"
|
||||
type="number"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.SeqRpt}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<span class="label-text font-medium mb-2 block">Visibility</span>
|
||||
<div class="flex gap-4">
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input type="checkbox" class="checkbox" bind:checked={formData.VisibleScr} />
|
||||
<span class="label-text">Screen</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input type="checkbox" class="checkbox" bind:checked={formData.VisibleRpt} />
|
||||
<span class="label-text">Report</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@ -1,134 +0,0 @@
|
||||
<script>
|
||||
import { PlusCircle, Calculator, X } from 'lucide-svelte';
|
||||
import { signOptions, flagOptions, sexOptions } from './refRangeConstants.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {Array} refnum - Numeric reference ranges array
|
||||
* @property {(refnum: Array) => void} onupdateRefnum - Update handler
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
refnum = [],
|
||||
onupdateRefnum = () => {}
|
||||
} = $props();
|
||||
|
||||
function addRefRange() {
|
||||
const newRef = {
|
||||
Sex: '2',
|
||||
LowSign: 'GE',
|
||||
HighSign: 'LE',
|
||||
Low: null,
|
||||
High: null,
|
||||
AgeStart: 0,
|
||||
AgeEnd: 120,
|
||||
Flag: 'N',
|
||||
Interpretation: 'Normal'
|
||||
};
|
||||
onupdateRefnum([...refnum, newRef]);
|
||||
}
|
||||
|
||||
function removeRefRange(index) {
|
||||
onupdateRefnum(refnum.filter((_, i) => i !== index));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Calculator class="w-5 h-5 text-primary" />
|
||||
<h3 class="font-semibold">Numeric Reference Ranges</h3>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick={addRefRange}>
|
||||
<PlusCircle class="w-4 h-4 mr-1" />
|
||||
Add Range
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if refnum?.length === 0}
|
||||
<div class="text-center py-8 bg-base-200 rounded-lg border-2 border-dashed border-base-300">
|
||||
<Calculator class="w-12 h-12 mx-auto text-gray-400 mb-2" />
|
||||
<p class="text-gray-500">No numeric ranges defined</p>
|
||||
<button type="button" class="btn btn-sm btn-outline mt-2" onclick={addRefRange}>
|
||||
<PlusCircle class="w-4 h-4 mr-1" />
|
||||
Add First Range
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each refnum || [] as ref, index (index)}
|
||||
<div class="card bg-base-100 border-2 border-base-200 hover:border-primary/50 transition-colors">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-center mb-4 pb-3 border-b border-base-200">
|
||||
<span class="badge badge-primary badge-lg">Range {index + 1}</span>
|
||||
<button type="button" class="btn btn-sm btn-ghost text-error" onclick={() => removeRefRange(index)}>
|
||||
<X class="w-4 h-4" />
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Sex</span>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={ref.Sex}>
|
||||
{#each sexOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Age From</span>
|
||||
<input type="number" min="0" max="120" class="input input-sm input-bordered w-full" bind:value={ref.AgeStart} placeholder="0" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Age To</span>
|
||||
<input type="number" min="0" max="120" class="input input-sm input-bordered w-full" bind:value={ref.AgeEnd} placeholder="120" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Flag</span>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={ref.Flag}>
|
||||
{#each flagOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label} - {option.description}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-3">
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Lower Bound</span>
|
||||
<div class="flex gap-2">
|
||||
<select class="select select-sm select-bordered w-20" bind:value={ref.LowSign}>
|
||||
{#each signOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<input type="number" step="0.01" class="input input-sm input-bordered flex-1" bind:value={ref.Low} placeholder="e.g., 70" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Upper Bound</span>
|
||||
<div class="flex gap-2">
|
||||
<select class="select select-sm select-bordered w-20" bind:value={ref.HighSign}>
|
||||
{#each signOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<input type="number" step="0.01" class="input input-sm input-bordered flex-1" bind:value={ref.High} placeholder="e.g., 100" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-3">
|
||||
<span class="label-text text-xs mb-1">Interpretation</span>
|
||||
<input type="text" class="input input-sm input-bordered w-full" bind:value={ref.Interpretation} placeholder="e.g., Normal range" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@ -1,210 +0,0 @@
|
||||
<script>
|
||||
import { Ruler, Calculator, FileText, Box } from 'lucide-svelte';
|
||||
import HelpTooltip from '$lib/components/HelpTooltip.svelte';
|
||||
import NumericRefRange from './NumericRefRange.svelte';
|
||||
import ThresholdRefRange from './ThresholdRefRange.svelte';
|
||||
import TextRefRange from './TextRefRange.svelte';
|
||||
import ValueSetRefRange from './ValueSetRefRange.svelte';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {Object} formData - Form data object
|
||||
* @property {(formData: Object) => void} onupdateFormData - Update handler
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
formData = $bindable({}),
|
||||
onupdateFormData = () => {}
|
||||
} = $props();
|
||||
|
||||
function updateRefRangeType(type) {
|
||||
let newFormData = {
|
||||
...formData,
|
||||
refRangeType: type,
|
||||
refnum: [],
|
||||
refthold: [],
|
||||
reftxt: [],
|
||||
refvset: []
|
||||
};
|
||||
|
||||
// Initialize the selected type
|
||||
if (type === 'num') {
|
||||
newFormData.refnum = [{
|
||||
Sex: '2',
|
||||
LowSign: 'GE',
|
||||
HighSign: 'LE',
|
||||
Low: null,
|
||||
High: null,
|
||||
AgeStart: 0,
|
||||
AgeEnd: 120,
|
||||
Flag: 'N',
|
||||
Interpretation: 'Normal'
|
||||
}];
|
||||
} else if (type === 'thold') {
|
||||
newFormData.refthold = [{
|
||||
Sex: '2',
|
||||
LowSign: 'GE',
|
||||
HighSign: 'LE',
|
||||
Low: null,
|
||||
High: null,
|
||||
AgeStart: 0,
|
||||
AgeEnd: 120,
|
||||
Flag: 'N',
|
||||
Interpretation: 'Normal'
|
||||
}];
|
||||
} else if (type === 'text') {
|
||||
newFormData.reftxt = [{
|
||||
Sex: '2',
|
||||
AgeStart: 0,
|
||||
AgeEnd: 120,
|
||||
RefTxt: '',
|
||||
Flag: 'N'
|
||||
}];
|
||||
} else if (type === 'vset') {
|
||||
newFormData.refvset = [{
|
||||
Sex: '2',
|
||||
AgeStart: 0,
|
||||
AgeEnd: 120,
|
||||
valueset: '',
|
||||
Flag: 'N'
|
||||
}];
|
||||
}
|
||||
|
||||
onupdateFormData(newFormData);
|
||||
}
|
||||
|
||||
function updateRefnum(refnum) {
|
||||
onupdateFormData({ ...formData, refnum });
|
||||
}
|
||||
|
||||
function updateRefthold(refthold) {
|
||||
onupdateFormData({ ...formData, refthold });
|
||||
}
|
||||
|
||||
function updateReftxt(reftxt) {
|
||||
onupdateFormData({ ...formData, reftxt });
|
||||
}
|
||||
|
||||
function updateRefvset(refvset) {
|
||||
onupdateFormData({ ...formData, refvset });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Reference Range Type Selection -->
|
||||
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<Ruler class="w-5 h-5 text-primary" />
|
||||
<h3 class="font-semibold">Reference Range Type</h3>
|
||||
<HelpTooltip
|
||||
text="Choose how to define normal/abnormal ranges for this test."
|
||||
title="Reference Range Help"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<label class="label cursor-pointer gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors flex-1 min-w-[100px]">
|
||||
<input
|
||||
type="radio"
|
||||
name="refRangeType"
|
||||
class="radio radio-primary"
|
||||
value="none"
|
||||
checked={formData.refRangeType === 'none'}
|
||||
onchange={() => updateRefRangeType('none')}
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="label-text font-medium">None</span>
|
||||
<span class="text-xs text-gray-500">No reference range</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors flex-1 min-w-[100px]">
|
||||
<input
|
||||
type="radio"
|
||||
name="refRangeType"
|
||||
class="radio radio-primary"
|
||||
value="num"
|
||||
checked={formData.refRangeType === 'num'}
|
||||
onchange={() => updateRefRangeType('num')}
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="label-text font-medium flex items-center gap-1">
|
||||
Numeric
|
||||
<Calculator class="w-3 h-3" />
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">Range (e.g., 70-100)</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors flex-1 min-w-[100px]">
|
||||
<input
|
||||
type="radio"
|
||||
name="refRangeType"
|
||||
class="radio radio-primary"
|
||||
value="thold"
|
||||
checked={formData.refRangeType === 'thold'}
|
||||
onchange={() => updateRefRangeType('thold')}
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="label-text font-medium flex items-center gap-1">
|
||||
Threshold
|
||||
<Ruler class="w-3 h-3" />
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">Limit values</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors flex-1 min-w-[100px]">
|
||||
<input
|
||||
type="radio"
|
||||
name="refRangeType"
|
||||
class="radio radio-primary"
|
||||
value="text"
|
||||
checked={formData.refRangeType === 'text'}
|
||||
onchange={() => updateRefRangeType('text')}
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="label-text font-medium flex items-center gap-1">
|
||||
Text
|
||||
<FileText class="w-3 h-3" />
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">Descriptive</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors flex-1 min-w-[100px]">
|
||||
<input
|
||||
type="radio"
|
||||
name="refRangeType"
|
||||
class="radio radio-primary"
|
||||
value="vset"
|
||||
checked={formData.refRangeType === 'vset'}
|
||||
onchange={() => updateRefRangeType('vset')}
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="label-text font-medium flex items-center gap-1">
|
||||
Value Set
|
||||
<Box class="w-3 h-3" />
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">Predefined values</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Numeric Reference Ranges -->
|
||||
{#if formData.refRangeType === 'num'}
|
||||
<NumericRefRange refnum={formData.refnum} onupdateRefnum={updateRefnum} />
|
||||
{/if}
|
||||
|
||||
<!-- Threshold Reference Ranges -->
|
||||
{#if formData.refRangeType === 'thold'}
|
||||
<ThresholdRefRange refthold={formData.refthold} onupdateRefthold={updateRefthold} />
|
||||
{/if}
|
||||
|
||||
<!-- Text Reference Ranges -->
|
||||
{#if formData.refRangeType === 'text'}
|
||||
<TextRefRange reftxt={formData.reftxt} onupdateReftxt={updateReftxt} />
|
||||
{/if}
|
||||
|
||||
<!-- Value Set Reference Ranges -->
|
||||
{#if formData.refRangeType === 'vset'}
|
||||
<ValueSetRefRange refvset={formData.refvset} onupdateRefvset={updateRefvset} />
|
||||
{/if}
|
||||
</div>
|
||||
@ -1,103 +0,0 @@
|
||||
<script>
|
||||
import { PlusCircle, FileText, X } from 'lucide-svelte';
|
||||
import { sexOptions } from './refRangeConstants.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {Array} reftxt - Text reference ranges array
|
||||
* @property {(reftxt: Array) => void} onupdateReftxt - Update handler
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
reftxt = [],
|
||||
onupdateReftxt = () => {}
|
||||
} = $props();
|
||||
|
||||
function addRefRange() {
|
||||
const newRef = {
|
||||
Sex: '2',
|
||||
AgeStart: 0,
|
||||
AgeEnd: 120,
|
||||
RefTxt: '',
|
||||
Flag: 'N'
|
||||
};
|
||||
onupdateReftxt([...reftxt, newRef]);
|
||||
}
|
||||
|
||||
function removeRefRange(index) {
|
||||
onupdateReftxt(reftxt.filter((_, i) => i !== index));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<FileText class="w-5 h-5 text-primary" />
|
||||
<h3 class="font-semibold">Text Reference Ranges</h3>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick={addRefRange}>
|
||||
<PlusCircle class="w-4 h-4 mr-1" />
|
||||
Add Range
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if reftxt?.length === 0}
|
||||
<div class="text-center py-8 bg-base-200 rounded-lg border-2 border-dashed border-base-300">
|
||||
<FileText class="w-12 h-12 mx-auto text-gray-400 mb-2" />
|
||||
<p class="text-gray-500">No text ranges defined</p>
|
||||
<button type="button" class="btn btn-sm btn-outline mt-2" onclick={addRefRange}>
|
||||
<PlusCircle class="w-4 h-4 mr-1" />
|
||||
Add First Range
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each reftxt || [] as ref, index (index)}
|
||||
<div class="card bg-base-100 border-2 border-base-200 hover:border-primary/50 transition-colors">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-center mb-4 pb-3 border-b border-base-200">
|
||||
<span class="badge badge-primary badge-lg">Range {index + 1}</span>
|
||||
<button type="button" class="btn btn-sm btn-ghost text-error" onclick={() => removeRefRange(index)}>
|
||||
<X class="w-4 h-4" />
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Sex</span>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={ref.Sex}>
|
||||
{#each sexOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Age From</span>
|
||||
<input type="number" min="0" max="120" class="input input-sm input-bordered w-full" bind:value={ref.AgeStart} placeholder="0" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Age To</span>
|
||||
<input type="number" min="0" max="120" class="input input-sm input-bordered w-full" bind:value={ref.AgeEnd} placeholder="120" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Flag</span>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={ref.Flag}>
|
||||
<option value="N">N - Normal</option>
|
||||
<option value="A">A - Abnormal</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-3">
|
||||
<span class="label-text text-xs mb-1">Reference Text</span>
|
||||
<textarea class="textarea textarea-bordered w-full" rows="2" bind:value={ref.RefTxt} placeholder="e.g., Negative for glucose"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@ -1,134 +0,0 @@
|
||||
<script>
|
||||
import { PlusCircle, Ruler, X } from 'lucide-svelte';
|
||||
import { signOptions, flagOptions, sexOptions } from './refRangeConstants.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {Array} refthold - Threshold reference ranges array
|
||||
* @property {(refthold: Array) => void} onupdateRefthold - Update handler
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
refthold = [],
|
||||
onupdateRefthold = () => {}
|
||||
} = $props();
|
||||
|
||||
function addRefRange() {
|
||||
const newRef = {
|
||||
Sex: '2',
|
||||
LowSign: 'GE',
|
||||
HighSign: 'LE',
|
||||
Low: null,
|
||||
High: null,
|
||||
AgeStart: 0,
|
||||
AgeEnd: 120,
|
||||
Flag: 'N',
|
||||
Interpretation: 'Normal'
|
||||
};
|
||||
onupdateRefthold([...refthold, newRef]);
|
||||
}
|
||||
|
||||
function removeRefRange(index) {
|
||||
onupdateRefthold(refthold.filter((_, i) => i !== index));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Ruler class="w-5 h-5 text-primary" />
|
||||
<h3 class="font-semibold">Threshold Reference Ranges</h3>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick={addRefRange}>
|
||||
<PlusCircle class="w-4 h-4 mr-1" />
|
||||
Add Range
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if refthold?.length === 0}
|
||||
<div class="text-center py-8 bg-base-200 rounded-lg border-2 border-dashed border-base-300">
|
||||
<Ruler class="w-12 h-12 mx-auto text-gray-400 mb-2" />
|
||||
<p class="text-gray-500">No threshold ranges defined</p>
|
||||
<button type="button" class="btn btn-sm btn-outline mt-2" onclick={addRefRange}>
|
||||
<PlusCircle class="w-4 h-4 mr-1" />
|
||||
Add First Range
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each refthold || [] as ref, index (index)}
|
||||
<div class="card bg-base-100 border-2 border-base-200 hover:border-primary/50 transition-colors">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-center mb-4 pb-3 border-b border-base-200">
|
||||
<span class="badge badge-primary badge-lg">Range {index + 1}</span>
|
||||
<button type="button" class="btn btn-sm btn-ghost text-error" onclick={() => removeRefRange(index)}>
|
||||
<X class="w-4 h-4" />
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Sex</span>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={ref.Sex}>
|
||||
{#each sexOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Age From</span>
|
||||
<input type="number" min="0" max="120" class="input input-sm input-bordered w-full" bind:value={ref.AgeStart} placeholder="0" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Age To</span>
|
||||
<input type="number" min="0" max="120" class="input input-sm input-bordered w-full" bind:value={ref.AgeEnd} placeholder="120" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Flag</span>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={ref.Flag}>
|
||||
{#each flagOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label} - {option.description}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-3">
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Lower Value</span>
|
||||
<div class="flex gap-2">
|
||||
<select class="select select-sm select-bordered w-20" bind:value={ref.LowSign}>
|
||||
{#each signOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<input type="number" step="0.01" class="input input-sm input-bordered flex-1" bind:value={ref.Low} placeholder="e.g., 5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Upper Value</span>
|
||||
<div class="flex gap-2">
|
||||
<select class="select select-sm select-bordered w-20" bind:value={ref.HighSign}>
|
||||
{#each signOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<input type="number" step="0.01" class="input input-sm input-bordered flex-1" bind:value={ref.High} placeholder="e.g., 10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-3">
|
||||
<span class="label-text text-xs mb-1">Interpretation</span>
|
||||
<input type="text" class="input input-sm input-bordered w-full" bind:value={ref.Interpretation} placeholder="e.g., Alert threshold" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@ -1,104 +0,0 @@
|
||||
<script>
|
||||
import { PlusCircle, Box, X } from 'lucide-svelte';
|
||||
import { sexOptions } from './refRangeConstants.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {Array} refvset - Value set reference ranges array
|
||||
* @property {(refvset: Array) => void} onupdateRefvset - Update handler
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
refvset = [],
|
||||
onupdateRefvset = () => {}
|
||||
} = $props();
|
||||
|
||||
function addRefRange() {
|
||||
const newRef = {
|
||||
Sex: '2',
|
||||
AgeStart: 0,
|
||||
AgeEnd: 120,
|
||||
valueset: '',
|
||||
Flag: 'N'
|
||||
};
|
||||
onupdateRefvset([...refvset, newRef]);
|
||||
}
|
||||
|
||||
function removeRefRange(index) {
|
||||
onupdateRefvset(refvset.filter((_, i) => i !== index));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Box class="w-5 h-5 text-primary" />
|
||||
<h3 class="font-semibold">Value Set Reference Ranges</h3>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick={addRefRange}>
|
||||
<PlusCircle class="w-4 h-4 mr-1" />
|
||||
Add Range
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if refvset?.length === 0}
|
||||
<div class="text-center py-8 bg-base-200 rounded-lg border-2 border-dashed border-base-300">
|
||||
<Box class="w-12 h-12 mx-auto text-gray-400 mb-2" />
|
||||
<p class="text-gray-500">No value set ranges defined</p>
|
||||
<button type="button" class="btn btn-sm btn-outline mt-2" onclick={addRefRange}>
|
||||
<PlusCircle class="w-4 h-4 mr-1" />
|
||||
Add First Range
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each refvset || [] as ref, index (index)}
|
||||
<div class="card bg-base-100 border-2 border-base-200 hover:border-primary/50 transition-colors">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-center mb-4 pb-3 border-b border-base-200">
|
||||
<span class="badge badge-primary badge-lg">Range {index + 1}</span>
|
||||
<button type="button" class="btn btn-sm btn-ghost text-error" onclick={() => removeRefRange(index)}>
|
||||
<X class="w-4 h-4" />
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Sex</span>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={ref.Sex}>
|
||||
{#each sexOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Age From</span>
|
||||
<input type="number" min="0" max="120" class="input input-sm input-bordered w-full" bind:value={ref.AgeStart} placeholder="0" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Age To</span>
|
||||
<input type="number" min="0" max="120" class="input input-sm input-bordered w-full" bind:value={ref.AgeEnd} placeholder="120" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1">Flag</span>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={ref.Flag}>
|
||||
<option value="N">N - Normal</option>
|
||||
<option value="A">A - Abnormal</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-3">
|
||||
<span class="label-text text-xs mb-1">Value Set</span>
|
||||
<input type="text" class="input input-sm input-bordered w-full" bind:value={ref.valueset} placeholder="e.g., Positive, Negative, Borderline" />
|
||||
<span class="label-text-alt text-gray-500 mt-1">Comma-separated list of allowed values</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Reference range shared constants
|
||||
*/
|
||||
|
||||
/** @type {Array<{value: string, label: string, description: string}>} */
|
||||
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' }
|
||||
];
|
||||
|
||||
/** @type {Array<{value: string, label: string, description: string}>} */
|
||||
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' }
|
||||
];
|
||||
|
||||
/** @type {Array<{value: string, label: string}>} */
|
||||
export const sexOptions = [
|
||||
{ value: '2', label: 'Male' },
|
||||
{ value: '1', label: 'Female' },
|
||||
{ value: '0', label: 'Any' }
|
||||
];
|
||||
@ -79,7 +79,7 @@
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="p-4">
|
||||
<h1 class="text-3xl font-bold text-gray-800 mb-2">Master Data</h1>
|
||||
<p class="text-gray-600 mb-8">Manage reference data and lookup values used throughout the system</p>
|
||||
|
||||
|
||||
@ -166,7 +166,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<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" />
|
||||
@ -262,7 +262,7 @@
|
||||
</div>
|
||||
|
||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'Add Contact' : 'Edit Contact'} size="md">
|
||||
<form class="space-y-5" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
<form class="space-y-3" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="initial">
|
||||
@ -279,7 +279,7 @@
|
||||
<input
|
||||
id="initial"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.Initial}
|
||||
placeholder="e.g., JS, AB, MK"
|
||||
maxlength="10"
|
||||
@ -296,7 +296,7 @@
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.Title}
|
||||
placeholder="e.g., Dr., Prof., Mr., Ms."
|
||||
/>
|
||||
@ -310,7 +310,7 @@
|
||||
<input
|
||||
id="nameFirst"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.NameFirst}
|
||||
placeholder="Enter first name"
|
||||
/>
|
||||
@ -322,7 +322,7 @@
|
||||
<input
|
||||
id="nameLast"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.NameLast}
|
||||
placeholder="Enter last name"
|
||||
/>
|
||||
|
||||
@ -146,7 +146,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<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" />
|
||||
@ -249,7 +249,7 @@
|
||||
</div>
|
||||
|
||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'Add Container' : 'Edit Container'} size="md">
|
||||
<form class="space-y-5" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
<form class="space-y-3" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="code">
|
||||
@ -259,7 +259,7 @@
|
||||
<input
|
||||
id="code"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.ConCode}
|
||||
placeholder="e.g., SST, EDTA, HEP"
|
||||
required
|
||||
@ -274,7 +274,7 @@
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.ConName}
|
||||
placeholder="e.g., Serum Separator Tube"
|
||||
required
|
||||
@ -289,7 +289,7 @@
|
||||
<input
|
||||
id="desc"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.ConDesc}
|
||||
placeholder="e.g., Evacuated blood collection tube with gel separator"
|
||||
/>
|
||||
|
||||
@ -197,7 +197,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="p-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<a href="/master-data" class="btn btn-ghost btn-circle">
|
||||
@ -327,7 +327,7 @@
|
||||
|
||||
<!-- Create/Edit Modal -->
|
||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'Add Counter' : 'Edit Counter'} size="md">
|
||||
<form class="space-y-5" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
<form class="space-y-3" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
<!-- Description -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="counterDesc">
|
||||
@ -337,7 +337,7 @@
|
||||
<input
|
||||
id="counterDesc"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
class:input-error={formErrors.CounterDesc}
|
||||
bind:value={formData.CounterDesc}
|
||||
placeholder="e.g., Sample ID Counter, Order Number"
|
||||
@ -367,7 +367,7 @@
|
||||
<input
|
||||
id="counterValue"
|
||||
type="number"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
class:input-error={formErrors.CounterValue}
|
||||
bind:value={formData.CounterValue}
|
||||
placeholder="e.g., 1000"
|
||||
@ -396,7 +396,7 @@
|
||||
<input
|
||||
id="counterStart"
|
||||
type="number"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
class:input-error={formErrors.CounterStart}
|
||||
bind:value={formData.CounterStart}
|
||||
placeholder="e.g., 1"
|
||||
@ -424,7 +424,7 @@
|
||||
<input
|
||||
id="counterEnd"
|
||||
type="number"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
class:input-error={formErrors.CounterEnd}
|
||||
bind:value={formData.CounterEnd}
|
||||
placeholder="e.g., 9999"
|
||||
@ -452,7 +452,7 @@
|
||||
<select
|
||||
id="counterReset"
|
||||
name="counterReset"
|
||||
class="select select-bordered w-full"
|
||||
class="select select-sm select-bordered w-full"
|
||||
bind:value={formData.CounterReset}
|
||||
>
|
||||
{#each resetOptions as option (option.value)}
|
||||
|
||||
@ -183,7 +183,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<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" />
|
||||
@ -244,7 +244,7 @@
|
||||
</div>
|
||||
|
||||
{#if activeTab === 'provinces'}
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<!-- Tab Description -->
|
||||
<div class="flex items-start gap-2 text-sm text-gray-600 bg-base-200 p-3 rounded-lg">
|
||||
<MapPin class="w-4 h-4 mt-0.5 shrink-0" />
|
||||
@ -302,7 +302,7 @@
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'cities'}
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<!-- Tab Description -->
|
||||
<div class="flex items-start gap-2 text-sm text-gray-600 bg-base-200 p-3 rounded-lg">
|
||||
<Building2 class="w-4 h-4 mt-0.5 shrink-0" />
|
||||
@ -392,7 +392,7 @@
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'areas'}
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<!-- Tab Description -->
|
||||
<div class="flex items-start gap-2 text-sm text-gray-600 bg-base-200 p-3 rounded-lg">
|
||||
<Globe class="w-4 h-4 mt-0.5 shrink-0" />
|
||||
|
||||
@ -163,7 +163,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<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" />
|
||||
@ -254,7 +254,7 @@
|
||||
</div>
|
||||
|
||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'Add Location' : 'Edit Location'} size="md">
|
||||
<form class="space-y-5" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
<form class="space-y-3" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="code">
|
||||
@ -264,7 +264,7 @@
|
||||
<input
|
||||
id="code"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.Code}
|
||||
placeholder="e.g., BLDG-01, ROOM-101"
|
||||
required
|
||||
@ -281,7 +281,7 @@
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.Name}
|
||||
placeholder="e.g., Main Building, Laboratory Room A"
|
||||
required
|
||||
@ -300,7 +300,7 @@
|
||||
<select
|
||||
id="type"
|
||||
name="type"
|
||||
class="select select-bordered w-full"
|
||||
class="select select-sm select-bordered w-full"
|
||||
bind:value={formData.Type}
|
||||
required
|
||||
>
|
||||
|
||||
@ -154,7 +154,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<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" />
|
||||
@ -260,7 +260,7 @@
|
||||
</div>
|
||||
|
||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'Add Occupation' : 'Edit Occupation'} size="md">
|
||||
<form class="space-y-5" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
<form class="space-y-3" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="occCode">
|
||||
|
||||
@ -143,7 +143,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<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" />
|
||||
@ -252,7 +252,7 @@
|
||||
</div>
|
||||
|
||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'Add Specialty' : 'Edit Specialty'} size="md">
|
||||
<form class="space-y-5" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
<form class="space-y-3" onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="specialtyText">
|
||||
@ -262,7 +262,7 @@
|
||||
<input
|
||||
id="specialtyText"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.SpecialtyText}
|
||||
placeholder="e.g., Cardiology, Internal Medicine"
|
||||
required
|
||||
@ -278,7 +278,7 @@
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.Title}
|
||||
placeholder="e.g., Sp. PD, Sp. A, Sp. And"
|
||||
/>
|
||||
@ -324,7 +324,7 @@
|
||||
</Modal>
|
||||
|
||||
<Modal bind:open={deleteConfirmOpen} title="Confirm Deletion" size="md">
|
||||
<div class="py-4 space-y-4">
|
||||
<div class="py-4 space-y-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="bg-error/10 rounded-full p-2">
|
||||
<Trash2 class="w-6 h-6 text-error" />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { fetchTests, createTest, updateTest, deleteTest } from '$lib/api/tests.js';
|
||||
import { fetchTests, fetchTest, createTest, updateTest, deleteTest } from '$lib/api/tests.js';
|
||||
import { fetchDisciplines, fetchDepartments } from '$lib/api/organization.js';
|
||||
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
|
||||
import DataTable from '$lib/components/DataTable.svelte';
|
||||
@ -14,12 +14,48 @@
|
||||
let currentPage = $state(1), perPage = $state(20), totalItems = $state(0), totalPages = $state(1);
|
||||
let modalMode = $state('create'), saving = $state(false), selectedType = $state(''), searchQuery = $state(''), searchInputRef = $state(null);
|
||||
let deleteModalOpen = $state(false), testToDelete = $state(null), deleting = $state(false);
|
||||
let formData = $state({ TestSiteID: null, TestSiteCode: '', TestSiteName: '', TestType: 'TEST', DisciplineID: null, DepartmentID: null, SeqScr: '0', SeqRpt: '0', VisibleScr: true, VisibleRpt: true, Unit: '', Formula: '', refnum: [], refthold: [], reftxt: [], refvset: [], refRangeType: 'none' });
|
||||
let formData = $state({
|
||||
// Basic Info (testdefsite)
|
||||
TestSiteID: null,
|
||||
TestSiteCode: '',
|
||||
TestSiteName: '',
|
||||
TestType: 'TEST',
|
||||
DisciplineID: null,
|
||||
DepartmentID: null,
|
||||
SeqScr: '0',
|
||||
SeqRpt: '0',
|
||||
VisibleScr: true,
|
||||
VisibleRpt: true,
|
||||
Description: '',
|
||||
CountStat: false,
|
||||
Unit: '',
|
||||
Formula: '',
|
||||
refnum: [],
|
||||
refthold: [],
|
||||
reftxt: [],
|
||||
refvset: [],
|
||||
refRangeType: 'none',
|
||||
// Technical Config (testdeftech)
|
||||
ResultType: '',
|
||||
RefType: '',
|
||||
ReqQty: null,
|
||||
ReqQtyUnit: '',
|
||||
Unit1: '',
|
||||
Factor: null,
|
||||
Unit2: '',
|
||||
Decimal: 0,
|
||||
CollReq: '',
|
||||
Method: '',
|
||||
ExpectedTAT: null,
|
||||
// Group Members (testdefgrp)
|
||||
groupMembers: []
|
||||
});
|
||||
|
||||
const testTypeConfig = { TEST: { label: 'Test', badgeClass: 'badge-primary', icon: Microscope, color: '#0066CC', bgColor: '#E6F2FF' }, PARAM: { label: 'Parameter', badgeClass: 'badge-secondary', icon: Variable, color: '#3399FF', bgColor: '#F0F8FF' }, CALC: { label: 'Calculated', badgeClass: 'badge-accent', icon: Calculator, color: '#9933CC', bgColor: '#F5E6FF' }, GROUP: { label: 'Panel', badgeClass: 'badge-info', icon: Box, color: '#00AA44', bgColor: '#E6F9EE' }, TITLE: { label: 'Header', badgeClass: 'badge-ghost', icon: Layers, color: '#666666', bgColor: '#F5F5F5' } };
|
||||
const canHaveRefRange = $derived(formData.TestType === 'TEST' || formData.TestType === 'CALC');
|
||||
const canHaveRefRange = $derived(formData.TestType === 'TEST' || formData.TestType === 'CALC');
|
||||
const canHaveFormula = $derived(formData.TestType === 'CALC');
|
||||
const canHaveUnit = $derived(formData.TestType === 'TEST' || formData.TestType === 'PARAM' || formData.TestType === 'CALC');
|
||||
const canHaveTechnical = $derived(formData.TestType === 'TEST' || formData.TestType === 'PARAM' || formData.TestType === 'CALC');
|
||||
const isGroupTest = $derived(formData.TestType === 'GROUP');
|
||||
const columns = [{ key: 'expand', label: '', class: 'w-8' }, { key: 'TestSiteCode', label: 'Code', class: 'font-medium w-24' }, { key: 'TestSiteName', label: 'Name', class: 'min-w-[200px]' }, { key: 'TestType', label: 'Type', class: 'w-28' }, { key: 'ReferenceRange', label: 'Reference Range', class: 'w-40' }, { key: 'Unit', label: 'Unit', class: 'w-20' }, { key: 'actions', label: 'Actions', class: 'w-24 text-center' }];
|
||||
const disciplineOptions = $derived(disciplines.map(d => ({ value: d.DisciplineID, label: d.DisciplineName })));
|
||||
const departmentOptions = $derived(departments.map(d => ({ value: d.DepartmentID, label: d.DepartmentName })));
|
||||
@ -34,8 +70,149 @@
|
||||
function getVisibleTests() { return tests.filter(t => t.IsActive !== '0' && t.IsActive !== 0); }
|
||||
function getTestTypeConfig(type) { return testTypeConfig[type] || testTypeConfig.TEST; }
|
||||
function formatReferenceRange(test) { return '-'; }
|
||||
function openCreateModal() { modalMode = 'create'; formData = { TestSiteID: null, TestSiteCode: '', TestSiteName: '', TestType: 'TEST', DisciplineID: null, DepartmentID: null, SeqScr: '0', SeqRpt: '0', VisibleScr: true, VisibleRpt: true, Unit: '', Formula: '', refnum: [], refthold: [], reftxt: [], refvset: [], refRangeType: 'none' }; modalOpen = true; }
|
||||
function openEditModal(row) { modalMode = 'edit'; let refRangeType = 'none'; if (row.refnum?.length > 0) refRangeType = 'num'; else if (row.refthold?.length > 0) refRangeType = 'thold'; else if (row.reftxt?.length > 0) refRangeType = 'text'; else if (row.refvset?.length > 0) refRangeType = 'vset'; formData = { TestSiteID: row.TestSiteID, TestSiteCode: row.TestSiteCode, TestSiteName: row.TestSiteName, TestType: row.TestType, DisciplineID: row.DisciplineID, DepartmentID: row.DepartmentID, SeqScr: row.SeqScr || '0', SeqRpt: row.SeqRpt || '0', VisibleScr: row.VisibleScr === '1' || row.VisibleScr === 1 || row.VisibleScr === true, VisibleRpt: row.VisibleRpt === '1' || row.VisibleRpt === 1 || row.VisibleRpt === true, Unit: row.Unit || '', Formula: row.Formula || '', refnum: row.refnum || [], refthold: row.refthold || [], reftxt: row.reftxt || [], refvset: row.refvset || [], refRangeType }; modalOpen = true; }
|
||||
function openCreateModal() {
|
||||
modalMode = 'create';
|
||||
formData = {
|
||||
// Basic Info
|
||||
TestSiteID: null,
|
||||
TestSiteCode: '',
|
||||
TestSiteName: '',
|
||||
TestType: 'TEST',
|
||||
DisciplineID: null,
|
||||
DepartmentID: null,
|
||||
SeqScr: '0',
|
||||
SeqRpt: '0',
|
||||
VisibleScr: true,
|
||||
VisibleRpt: true,
|
||||
Description: '',
|
||||
CountStat: false,
|
||||
Unit: '',
|
||||
Formula: '',
|
||||
refnum: [],
|
||||
refthold: [],
|
||||
reftxt: [],
|
||||
refvset: [],
|
||||
refRangeType: 'none',
|
||||
// Technical Config
|
||||
ResultType: '',
|
||||
RefType: '',
|
||||
ReqQty: null,
|
||||
ReqQtyUnit: '',
|
||||
Unit1: '',
|
||||
Factor: null,
|
||||
Unit2: '',
|
||||
Decimal: 0,
|
||||
CollReq: '',
|
||||
Method: '',
|
||||
ExpectedTAT: null,
|
||||
// Group Members
|
||||
groupMembers: []
|
||||
};
|
||||
modalOpen = true;
|
||||
}
|
||||
async function openEditModal(row) {
|
||||
try {
|
||||
// Fetch full test details including reference ranges, technical config, group members
|
||||
const response = await fetchTest(row.TestSiteID);
|
||||
const testDetail = response.data;
|
||||
|
||||
modalMode = 'edit';
|
||||
let refRangeType = 'none';
|
||||
if (testDetail.refnum?.length > 0) refRangeType = 'num';
|
||||
else if (testDetail.refthold?.length > 0) refRangeType = 'thold';
|
||||
else if (testDetail.reftxt?.length > 0) refRangeType = 'text';
|
||||
else if (testDetail.refvset?.length > 0) refRangeType = 'vset';
|
||||
|
||||
// Normalize reference range data to ensure all fields have values (not undefined)
|
||||
const normalizeRefNum = (ref) => ({
|
||||
Sex: ref.Sex ?? '2',
|
||||
LowSign: ref.LowSign ?? 'GE',
|
||||
HighSign: ref.HighSign ?? 'LE',
|
||||
Low: ref.Low ?? null,
|
||||
High: ref.High ?? null,
|
||||
AgeStart: ref.AgeStart ?? 0,
|
||||
AgeEnd: ref.AgeEnd ?? 120,
|
||||
Flag: ref.Flag ?? 'N',
|
||||
Interpretation: ref.Interpretation ?? 'Normal',
|
||||
SpcType: ref.SpcType ?? '',
|
||||
Criteria: ref.Criteria ?? ''
|
||||
});
|
||||
|
||||
const normalizeRefThold = (ref) => ({
|
||||
Sex: ref.Sex ?? '2',
|
||||
LowSign: ref.LowSign ?? 'GE',
|
||||
HighSign: ref.HighSign ?? 'LE',
|
||||
Low: ref.Low ?? null,
|
||||
High: ref.High ?? null,
|
||||
AgeStart: ref.AgeStart ?? 0,
|
||||
AgeEnd: ref.AgeEnd ?? 120,
|
||||
Flag: ref.Flag ?? 'N',
|
||||
Interpretation: ref.Interpretation ?? 'Normal',
|
||||
SpcType: ref.SpcType ?? '',
|
||||
Criteria: ref.Criteria ?? ''
|
||||
});
|
||||
|
||||
const normalizeRefTxt = (ref) => ({
|
||||
Sex: ref.Sex ?? '2',
|
||||
AgeStart: ref.AgeStart ?? 0,
|
||||
AgeEnd: ref.AgeEnd ?? 120,
|
||||
RefTxt: ref.RefTxt ?? '',
|
||||
Flag: ref.Flag ?? 'N',
|
||||
SpcType: ref.SpcType ?? '',
|
||||
Criteria: ref.Criteria ?? ''
|
||||
});
|
||||
|
||||
const normalizeRefVset = (ref) => ({
|
||||
Sex: ref.Sex ?? '2',
|
||||
AgeStart: ref.AgeStart ?? 0,
|
||||
AgeEnd: ref.AgeEnd ?? 120,
|
||||
RefTxt: ref.RefTxt ?? '',
|
||||
Flag: ref.Flag ?? 'N',
|
||||
SpcType: ref.SpcType ?? '',
|
||||
Criteria: ref.Criteria ?? ''
|
||||
});
|
||||
|
||||
formData = {
|
||||
// Basic Info
|
||||
TestSiteID: testDetail.TestSiteID,
|
||||
TestSiteCode: testDetail.TestSiteCode,
|
||||
TestSiteName: testDetail.TestSiteName,
|
||||
TestType: testDetail.TestType,
|
||||
DisciplineID: testDetail.testdeftech?.[0]?.DisciplineID || null,
|
||||
DepartmentID: testDetail.testdeftech?.[0]?.DepartmentID || null,
|
||||
SeqScr: testDetail.SeqScr || '0',
|
||||
SeqRpt: testDetail.SeqRpt || '0',
|
||||
VisibleScr: testDetail.VisibleScr === '1' || testDetail.VisibleScr === 1 || testDetail.VisibleScr === true,
|
||||
VisibleRpt: testDetail.VisibleRpt === '1' || testDetail.VisibleRpt === 1 || testDetail.VisibleRpt === true,
|
||||
Description: testDetail.Description || '',
|
||||
CountStat: testDetail.CountStat === '1' || testDetail.CountStat === 1 || testDetail.CountStat === true,
|
||||
Unit: testDetail.Unit || '',
|
||||
Formula: testDetail.Formula || '',
|
||||
refnum: (testDetail.refnum || []).map(normalizeRefNum),
|
||||
refthold: (testDetail.refthold || []).map(normalizeRefThold),
|
||||
reftxt: (testDetail.reftxt || []).map(normalizeRefTxt),
|
||||
refvset: (testDetail.refvset || []).map(normalizeRefVset),
|
||||
refRangeType,
|
||||
// Technical Config (from testdeftech[0])
|
||||
ResultType: testDetail.testdeftech?.[0]?.ResultType || '',
|
||||
RefType: testDetail.testdeftech?.[0]?.RefType || '',
|
||||
ReqQty: testDetail.testdeftech?.[0]?.ReqQty || null,
|
||||
ReqQtyUnit: testDetail.testdeftech?.[0]?.ReqQtyUnit || '',
|
||||
Unit1: testDetail.testdeftech?.[0]?.Unit1 || '',
|
||||
Factor: testDetail.testdeftech?.[0]?.Factor || null,
|
||||
Unit2: testDetail.testdeftech?.[0]?.Unit2 || '',
|
||||
Decimal: testDetail.testdeftech?.[0]?.Decimal || 0,
|
||||
Method: testDetail.testdeftech?.[0]?.Method || '',
|
||||
ExpectedTAT: testDetail.testdeftech?.[0]?.ExpectedTAT || null,
|
||||
// Group Members - API returns as testdefgrp
|
||||
groupMembers: testDetail.testdefgrp || []
|
||||
};
|
||||
modalOpen = true;
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to load test details');
|
||||
console.error('Failed to fetch test details:', err);
|
||||
}
|
||||
}
|
||||
function isDuplicateCode(code, excludeId = null) { return tests.some(test => test.TestSiteCode.toLowerCase() === code.toLowerCase() && test.TestSiteID !== excludeId); }
|
||||
|
||||
async function handleSave() {
|
||||
@ -45,7 +222,50 @@
|
||||
else if (formData.refRangeType === 'thold') { for (let i = 0; i < formData.refthold.length; i++) { const errors = validateTholdRange(formData.refthold[i], i); if (errors.length > 0) { toastError(errors[0]); return; } } }
|
||||
else if (formData.refRangeType === 'text') { for (let i = 0; i < formData.reftxt.length; i++) { const errors = validateTextRange(formData.reftxt[i], i); if (errors.length > 0) { toastError(errors[0]); return; } } }
|
||||
else if (formData.refRangeType === 'vset') { for (let i = 0; i < formData.refvset.length; i++) { const errors = validateVsetRange(formData.refvset[i], i); if (errors.length > 0) { toastError(errors[0]); return; } } }
|
||||
saving = true; try { const payload = { ...formData }; if (!canHaveUnit) delete payload.Unit; if (!canHaveFormula) delete payload.Formula; if (!canHaveRefRange) { delete payload.refnum; delete payload.refthold; delete payload.reftxt; delete payload.refvset; } delete payload.refRangeType; if (modalMode === 'create') { await createTest(payload); toastSuccess('Test created successfully'); } else { await updateTest(payload); toastSuccess('Test updated successfully'); } modalOpen = false; await loadTests(); } catch (err) { toastError(err.message || 'Failed to save test'); } finally { saving = false; }
|
||||
saving = true;
|
||||
try {
|
||||
const payload = { ...formData };
|
||||
|
||||
// Remove fields based on test type
|
||||
if (!canHaveFormula) delete payload.Formula;
|
||||
if (!canHaveRefRange) {
|
||||
delete payload.refnum;
|
||||
delete payload.refthold;
|
||||
delete payload.reftxt;
|
||||
delete payload.refvset;
|
||||
}
|
||||
if (!canHaveTechnical) {
|
||||
delete payload.ResultType;
|
||||
delete payload.RefType;
|
||||
delete payload.Unit1;
|
||||
delete payload.Factor;
|
||||
delete payload.Unit2;
|
||||
delete payload.Decimal;
|
||||
delete payload.ReqQty;
|
||||
delete payload.ReqQtyUnit;
|
||||
delete payload.CollReq;
|
||||
delete payload.Method;
|
||||
delete payload.ExpectedTAT;
|
||||
}
|
||||
if (!isGroupTest) {
|
||||
delete payload.groupMembers;
|
||||
}
|
||||
delete payload.refRangeType;
|
||||
|
||||
if (modalMode === 'create') {
|
||||
await createTest(payload);
|
||||
toastSuccess('Test created successfully');
|
||||
} else {
|
||||
await updateTest(payload);
|
||||
toastSuccess('Test updated successfully');
|
||||
}
|
||||
modalOpen = false;
|
||||
await loadTests();
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to save test');
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openDeleteModal(row) { testToDelete = row; deleteModalOpen = true; }
|
||||
@ -57,7 +277,7 @@
|
||||
function toggleGroup(testId) { if (expandedGroups.has(testId)) expandedGroups.delete(testId); else expandedGroups.add(testId); expandedGroups = new Set(expandedGroups); }
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<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">
|
||||
@ -74,7 +294,7 @@
|
||||
<Search class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
</div>
|
||||
<div class="w-full sm:w-48">
|
||||
<select class="select select-bordered w-full" bind:value={selectedType} onchange={handleFilter}>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={selectedType} onchange={handleFilter}>
|
||||
<option value="">All Types</option>
|
||||
<option value="TEST">Technical Test</option>
|
||||
<option value="PARAM">Parameter</option>
|
||||
@ -103,7 +323,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TestModal bind:open={modalOpen} mode={modalMode} bind:formData {canHaveRefRange} {canHaveFormula} {canHaveUnit} {disciplineOptions} departmentOptions={departmentOptions} {saving} onsave={handleSave} oncancel={() => modalOpen = false} onupdateFormData={(data) => formData = data} />
|
||||
<TestModal
|
||||
bind:open={modalOpen}
|
||||
mode={modalMode}
|
||||
bind:formData
|
||||
{canHaveRefRange}
|
||||
{canHaveFormula}
|
||||
{canHaveTechnical}
|
||||
{isGroupTest}
|
||||
{disciplineOptions}
|
||||
departmentOptions={departmentOptions}
|
||||
availableTests={tests}
|
||||
{saving}
|
||||
onsave={handleSave}
|
||||
oncancel={() => modalOpen = false}
|
||||
onupdateFormData={(data) => formData = data}
|
||||
/>
|
||||
|
||||
<Modal bind:open={deleteModalOpen} title="Confirm Delete Test" size="sm">
|
||||
<div class="py-2">
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import BasicInfoForm from './test-modal/BasicInfoForm.svelte';
|
||||
import ReferenceRangeSection from './test-modal/ReferenceRangeSection.svelte';
|
||||
import TechnicalConfigForm from './test-modal/TechnicalConfigForm.svelte';
|
||||
import GroupMembersTab from './test-modal/GroupMembersTab.svelte';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
@ -10,9 +12,11 @@
|
||||
* @property {Object} formData - Form data object
|
||||
* @property {boolean} canHaveRefRange - Whether test can have reference ranges
|
||||
* @property {boolean} canHaveFormula - Whether test can have a formula
|
||||
* @property {boolean} canHaveUnit - Whether test can have a unit
|
||||
* @property {boolean} canHaveTechnical - Whether test can have technical config
|
||||
* @property {boolean} isGroupTest - Whether test is a group test
|
||||
* @property {Array<{value: string, label: string}>} disciplineOptions - Discipline dropdown options
|
||||
* @property {Array<{value: string, label: string}>} departmentOptions - Department dropdown options
|
||||
* @property {Array} availableTests - Available tests for group member selection
|
||||
* @property {boolean} [saving] - Whether save is in progress
|
||||
*/
|
||||
|
||||
@ -23,9 +27,11 @@
|
||||
formData = $bindable({}),
|
||||
canHaveRefRange = false,
|
||||
canHaveFormula = false,
|
||||
canHaveUnit = false,
|
||||
canHaveTechnical = false,
|
||||
isGroupTest = false,
|
||||
disciplineOptions = [],
|
||||
departmentOptions = [],
|
||||
availableTests = [],
|
||||
saving = false,
|
||||
onsave = () => {},
|
||||
oncancel = () => {},
|
||||
@ -50,9 +56,20 @@
|
||||
activeTab = 'basic';
|
||||
}
|
||||
});
|
||||
|
||||
// Get tab count badge for reference range
|
||||
function getRefRangeCount() {
|
||||
return (formData.refnum?.length || 0) + (formData.reftxt?.length || 0) +
|
||||
(formData.refthold?.length || 0) + (formData.refvset?.length || 0);
|
||||
}
|
||||
|
||||
// Get tab count badge for group members
|
||||
function getGroupMemberCount() {
|
||||
return formData.groupMembers?.length || 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:open title={mode === 'create' ? 'Add Test' : 'Edit Test'} size="xl">
|
||||
<Modal bind:open title={mode === 'create' ? 'Add Test' : 'Edit Test'} size="xl" position="top">
|
||||
<!-- Tabs -->
|
||||
<div class="tabs tabs-bordered mb-4">
|
||||
<button
|
||||
@ -62,6 +79,15 @@
|
||||
>
|
||||
Basic Information
|
||||
</button>
|
||||
{#if canHaveTechnical}
|
||||
<button
|
||||
type="button"
|
||||
class="tab tab-lg {activeTab === 'technical' ? 'tab-active' : ''}"
|
||||
onclick={() => activeTab = 'technical'}
|
||||
>
|
||||
Technical
|
||||
</button>
|
||||
{/if}
|
||||
{#if canHaveRefRange}
|
||||
<button
|
||||
type="button"
|
||||
@ -69,8 +95,20 @@
|
||||
onclick={() => activeTab = 'refrange'}
|
||||
>
|
||||
Reference Range
|
||||
{#if formData.refnum?.length > 0 || formData.reftxt?.length > 0}
|
||||
<span class="badge badge-sm badge-primary ml-2">{(formData.refnum?.length || 0) + (formData.reftxt?.length || 0)}</span>
|
||||
{#if getRefRangeCount() > 0}
|
||||
<span class="badge badge-sm badge-primary ml-2">{getRefRangeCount()}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{#if isGroupTest}
|
||||
<button
|
||||
type="button"
|
||||
class="tab tab-lg {activeTab === 'members' ? 'tab-active' : ''}"
|
||||
onclick={() => activeTab = 'members'}
|
||||
>
|
||||
Group Members
|
||||
{#if getGroupMemberCount() > 0}
|
||||
<span class="badge badge-sm badge-primary ml-2">{getGroupMemberCount()}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
@ -80,16 +118,26 @@
|
||||
<BasicInfoForm
|
||||
bind:formData
|
||||
{canHaveFormula}
|
||||
{canHaveUnit}
|
||||
{disciplineOptions}
|
||||
{departmentOptions}
|
||||
onsave={handleSave}
|
||||
/>
|
||||
{:else if activeTab === 'technical' && canHaveTechnical}
|
||||
<TechnicalConfigForm
|
||||
bind:formData
|
||||
{onupdateFormData}
|
||||
/>
|
||||
{:else if activeTab === 'refrange' && canHaveRefRange}
|
||||
<ReferenceRangeSection
|
||||
bind:formData
|
||||
{onupdateFormData}
|
||||
/>
|
||||
{:else if activeTab === 'members' && isGroupTest}
|
||||
<GroupMembersTab
|
||||
bind:formData
|
||||
{availableTests}
|
||||
{onupdateFormData}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#snippet footer()}
|
||||
|
||||
@ -30,7 +30,9 @@ export function createNumRef() {
|
||||
AgeStart: 0,
|
||||
AgeEnd: 120,
|
||||
Flag: 'N',
|
||||
Interpretation: 'Normal'
|
||||
Interpretation: 'Normal',
|
||||
SpcType: '',
|
||||
Criteria: ''
|
||||
};
|
||||
}
|
||||
|
||||
@ -44,7 +46,9 @@ export function createTholdRef() {
|
||||
AgeStart: 0,
|
||||
AgeEnd: 120,
|
||||
Flag: 'N',
|
||||
Interpretation: 'Normal'
|
||||
Interpretation: 'Normal',
|
||||
SpcType: '',
|
||||
Criteria: ''
|
||||
};
|
||||
}
|
||||
|
||||
@ -54,7 +58,9 @@ export function createTextRef() {
|
||||
AgeStart: 0,
|
||||
AgeEnd: 120,
|
||||
RefTxt: '',
|
||||
Flag: 'N'
|
||||
Flag: 'N',
|
||||
SpcType: '',
|
||||
Criteria: ''
|
||||
};
|
||||
}
|
||||
|
||||
@ -63,8 +69,10 @@ export function createVsetRef() {
|
||||
Sex: '2',
|
||||
AgeStart: 0,
|
||||
AgeEnd: 120,
|
||||
valueset: '',
|
||||
Flag: 'N'
|
||||
RefTxt: '',
|
||||
Flag: 'N',
|
||||
SpcType: '',
|
||||
Criteria: ''
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
* @typedef {Object} Props
|
||||
* @property {Object} formData - Form data object
|
||||
* @property {boolean} canHaveFormula - Whether test can have a formula
|
||||
* @property {boolean} canHaveUnit - Whether test can have a unit
|
||||
* @property {Array<{value: string, label: string}>} disciplineOptions - Discipline dropdown options
|
||||
* @property {Array<{value: string, label: string}>} departmentOptions - Department dropdown options
|
||||
* @property {() => void} onsave - Save handler
|
||||
@ -16,7 +15,6 @@
|
||||
let {
|
||||
formData = $bindable({}),
|
||||
canHaveFormula = false,
|
||||
canHaveUnit = false,
|
||||
disciplineOptions = [],
|
||||
departmentOptions = [],
|
||||
onsave = () => {}
|
||||
@ -28,7 +26,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="space-y-5" onsubmit={handleSubmit}>
|
||||
<form class="space-y-3" onsubmit={handleSubmit}>
|
||||
<!-- Basic Info -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
@ -39,7 +37,7 @@
|
||||
<input
|
||||
id="testCode"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.TestSiteCode}
|
||||
placeholder="e.g., GLU"
|
||||
required
|
||||
@ -53,7 +51,7 @@
|
||||
<input
|
||||
id="testName"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.TestSiteName}
|
||||
placeholder="e.g., Glucose"
|
||||
required
|
||||
@ -70,7 +68,7 @@
|
||||
</label>
|
||||
<select
|
||||
id="testType"
|
||||
class="select select-bordered w-full"
|
||||
class="select select-sm select-bordered w-full"
|
||||
bind:value={formData.TestType}
|
||||
required
|
||||
>
|
||||
@ -88,7 +86,7 @@
|
||||
<input
|
||||
id="seqScr"
|
||||
type="number"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.SeqScr}
|
||||
placeholder="0"
|
||||
/>
|
||||
@ -114,9 +112,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Type-specific fields -->
|
||||
{#if canHaveUnit}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{#if canHaveFormula}
|
||||
<div class="grid grid-cols-1 md:grid-cols-1 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="formula">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
@ -131,31 +128,32 @@
|
||||
<input
|
||||
id="formula"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.Formula}
|
||||
placeholder="e.g., BUN / Creatinine"
|
||||
required={canHaveFormula}
|
||||
/>
|
||||
<span class="label-text-alt text-gray-500">Use test codes with operators: +, -, *, /</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="form-control">
|
||||
<label class="label" for="unit">
|
||||
<span class="label-text font-medium">Unit</span>
|
||||
</label>
|
||||
<input
|
||||
id="unit"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.Unit}
|
||||
placeholder="e.g., mg/dL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Report Sequence and Visibility -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Description -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="description">
|
||||
<span class="label-text font-medium">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
class="textarea textarea-bordered w-full"
|
||||
bind:value={formData.Description}
|
||||
placeholder="Enter test description..."
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Report Sequence, Visibility, and CountStat -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="seqRpt">
|
||||
<span class="label-text font-medium">Report Sequence</span>
|
||||
@ -163,7 +161,7 @@
|
||||
<input
|
||||
id="seqRpt"
|
||||
type="number"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.SeqRpt}
|
||||
placeholder="0"
|
||||
/>
|
||||
@ -181,5 +179,12 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<span class="label-text font-medium mb-2 block">Statistics</span>
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input type="checkbox" class="checkbox" bind:checked={formData.CountStat} />
|
||||
<span class="label-text">Count in Statistics</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -0,0 +1,207 @@
|
||||
<script>
|
||||
import { Search, Plus, X, GripVertical, Microscope } from 'lucide-svelte';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {Object} formData - Form data object
|
||||
* @property {Array} availableTests - Available tests for selection
|
||||
* @property {(formData: Object) => void} onupdateFormData - Update handler
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
formData = $bindable({}),
|
||||
availableTests = [],
|
||||
onupdateFormData = () => {}
|
||||
} = $props();
|
||||
|
||||
let searchQuery = $state('');
|
||||
let draggedIndex = $state(-1);
|
||||
|
||||
// Filter out the current test and already selected tests
|
||||
let filteredTests = $derived(
|
||||
availableTests.filter(test =>
|
||||
test.TestSiteID !== formData.TestSiteID &&
|
||||
!formData.groupMembers?.some(member => member.TestSiteID === test.TestSiteID) &&
|
||||
(test.TestSiteName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
test.TestSiteCode.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
)
|
||||
);
|
||||
|
||||
function addMember(test) {
|
||||
const newMembers = [
|
||||
...(formData.groupMembers || []),
|
||||
{
|
||||
TestSiteID: test.TestSiteID,
|
||||
TestSiteCode: test.TestSiteCode,
|
||||
TestSiteName: test.TestSiteName,
|
||||
TestType: test.TestType,
|
||||
Sequence: (formData.groupMembers?.length || 0) + 1
|
||||
}
|
||||
];
|
||||
onupdateFormData({ ...formData, groupMembers: newMembers });
|
||||
searchQuery = '';
|
||||
}
|
||||
|
||||
function removeMember(index) {
|
||||
const newMembers = formData.groupMembers.filter((_, i) => i !== index);
|
||||
// Reorder sequences
|
||||
newMembers.forEach((member, i) => {
|
||||
member.Sequence = i + 1;
|
||||
});
|
||||
onupdateFormData({ ...formData, groupMembers: newMembers });
|
||||
}
|
||||
|
||||
function moveMember(fromIndex, toIndex) {
|
||||
if (toIndex < 0 || toIndex >= formData.groupMembers.length) return;
|
||||
|
||||
const members = [...formData.groupMembers];
|
||||
const [moved] = members.splice(fromIndex, 1);
|
||||
members.splice(toIndex, 0, moved);
|
||||
|
||||
// Reorder sequences
|
||||
members.forEach((member, i) => {
|
||||
member.Sequence = i + 1;
|
||||
});
|
||||
|
||||
onupdateFormData({ ...formData, groupMembers: members });
|
||||
}
|
||||
|
||||
function handleDragStart(index) {
|
||||
draggedIndex = index;
|
||||
}
|
||||
|
||||
function handleDragOver(e, index) {
|
||||
e.preventDefault();
|
||||
if (draggedIndex === -1 || draggedIndex === index) return;
|
||||
moveMember(draggedIndex, index);
|
||||
draggedIndex = index;
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
draggedIndex = -1;
|
||||
}
|
||||
|
||||
function getTestTypeBadge(testType) {
|
||||
const badges = {
|
||||
'TEST': 'badge-primary',
|
||||
'PARAM': 'badge-secondary',
|
||||
'CALC': 'badge-accent',
|
||||
'GROUP': 'badge-info',
|
||||
'TITLE': 'badge-ghost'
|
||||
};
|
||||
return badges[testType] || 'badge-ghost';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Add Member Section -->
|
||||
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Microscope class="w-5 h-5 text-primary" />
|
||||
<h3 class="font-semibold">Add Group Members</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<div class="relative">
|
||||
<Search class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full pl-10"
|
||||
bind:value={searchQuery}
|
||||
placeholder="Search by test name or code..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if searchQuery}
|
||||
<div class="mt-2 max-h-60 overflow-y-auto border border-base-200 rounded-lg">
|
||||
{#if filteredTests.length === 0}
|
||||
<div class="p-4 text-center text-gray-500">
|
||||
No tests found matching "{searchQuery}"
|
||||
</div>
|
||||
{:else}
|
||||
{#each filteredTests as test}
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left p-3 hover:bg-base-200 flex items-center justify-between group border-b border-base-200 last:border-0"
|
||||
onclick={() => addMember(test)}
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-mono text-sm text-gray-600">{test.TestSiteCode}</span>
|
||||
<span class="font-medium">{test.TestSiteName}</span>
|
||||
<span class="badge badge-sm {getTestTypeBadge(test.TestType)}">
|
||||
{test.TestType}
|
||||
</span>
|
||||
</div>
|
||||
<Plus class="w-4 h-4 text-gray-400 group-hover:text-primary" />
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Selected Members -->
|
||||
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">Group Members</span>
|
||||
<span class="badge badge-sm badge-primary">{formData.groupMembers?.length || 0}</span>
|
||||
</div>
|
||||
{#if formData.groupMembers?.length > 0}
|
||||
<span class="text-sm text-gray-500">Drag to reorder</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !formData.groupMembers || formData.groupMembers.length === 0}
|
||||
<div class="text-center py-8 bg-base-200 rounded-lg border-2 border-dashed border-base-300">
|
||||
<Microscope class="w-12 h-12 mx-auto text-gray-400 mb-2" />
|
||||
<p class="text-gray-500">No members added yet</p>
|
||||
<p class="text-sm text-gray-400 mt-1">Search and add tests above</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#each formData.groupMembers as member, index (member.TestSiteID)}
|
||||
<div
|
||||
class="card bg-base-100 border border-base-200 hover:border-primary/50 transition-colors"
|
||||
draggable="true"
|
||||
ondragstart={() => handleDragStart(index)}
|
||||
ondragover={(e) => handleDragOver(e, index)}
|
||||
ondragend={handleDragEnd}
|
||||
class:opacity-50={draggedIndex === index}
|
||||
>
|
||||
<div class="card-body p-3 flex flex-row items-center gap-3">
|
||||
<div class="cursor-move text-gray-400 hover:text-gray-600">
|
||||
<GripVertical class="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
<div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-sm">
|
||||
{member.Sequence}
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-mono text-sm text-gray-600">{member.TestSiteCode}</span>
|
||||
<span class="font-medium">{member.TestSiteName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="badge badge-sm {getTestTypeBadge(member.TestType)}">
|
||||
{member.TestType}
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-ghost text-error"
|
||||
onclick={() => removeMember(index)}
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@ -1,6 +1,8 @@
|
||||
<script>
|
||||
import { PlusCircle, Calculator, X, ChevronDown, ChevronUp, Info } from 'lucide-svelte';
|
||||
import { signOptions, flagOptions, sexOptions, createNumRef } from '../referenceRange.js';
|
||||
import { PlusCircle, Calculator, X, ChevronDown, ChevronUp, Info, Beaker, Filter } from 'lucide-svelte';
|
||||
import { flagOptions, sexOptions, createNumRef } from '../referenceRange.js';
|
||||
import { fetchValueSetByKey } from '$lib/api/valuesets.js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
@ -16,6 +18,19 @@
|
||||
|
||||
// Track expanded state for each range's optional fields
|
||||
let expandedRanges = $state({});
|
||||
let specimenTypeOptions = $state([]);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetchValueSetByKey('specimen_type');
|
||||
specimenTypeOptions = response.data?.items?.map(item => ({
|
||||
value: item.itemCode,
|
||||
label: item.itemValue
|
||||
})) || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to load specimen types:', err);
|
||||
}
|
||||
});
|
||||
|
||||
function addRefRange() {
|
||||
const newRef = createNumRef();
|
||||
@ -24,7 +39,7 @@
|
||||
newRef.Flag = 'N'; // Normal
|
||||
onupdateRefnum([...refnum, newRef]);
|
||||
// Auto-expand the new range
|
||||
expandedRanges[refnum.length] = { age: false, interpretation: false };
|
||||
expandedRanges[refnum.length] = { age: false, interpretation: false, specimen: false };
|
||||
}
|
||||
|
||||
function removeRefRange(index) {
|
||||
@ -40,8 +55,8 @@
|
||||
expandedRanges[index] = { ...expandedRanges[index], interpretation: !expandedRanges[index]?.interpretation };
|
||||
}
|
||||
|
||||
function getSignLabel(value) {
|
||||
return signOptions.find(o => o.value === value)?.label || value;
|
||||
function toggleSpecimenExpand(index) {
|
||||
expandedRanges[index] = { ...expandedRanges[index], specimen: !expandedRanges[index]?.specimen };
|
||||
}
|
||||
|
||||
function getSexLabel(value) {
|
||||
@ -72,11 +87,11 @@
|
||||
|
||||
let rangeText = '';
|
||||
if (ref.Low !== null && ref.High !== null) {
|
||||
rangeText = `${getSignLabel(ref.LowSign)}${ref.Low} to ${getSignLabel(ref.HighSign)}${ref.High}`;
|
||||
rangeText = `${ref.Low} - ${ref.High}`;
|
||||
} else if (ref.Low !== null) {
|
||||
rangeText = `${getSignLabel(ref.LowSign)}${ref.Low}`;
|
||||
rangeText = `${ref.Low}`;
|
||||
} else if (ref.High !== null) {
|
||||
rangeText = `${getSignLabel(ref.HighSign)}${ref.High}`;
|
||||
rangeText = `${ref.High}`;
|
||||
} else {
|
||||
rangeText = 'Not set';
|
||||
}
|
||||
@ -85,7 +100,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Calculator class="w-5 h-5 text-primary" />
|
||||
@ -133,40 +148,26 @@
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3">
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1 font-medium">Lower Bound</span>
|
||||
<div class="flex gap-2">
|
||||
<select class="select select-sm select-bordered w-16" bind:value={ref.LowSign}>
|
||||
{#each signOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="input input-sm input-bordered flex-1"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={ref.Low}
|
||||
placeholder="70"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1 font-medium">Upper Bound</span>
|
||||
<div class="flex gap-2">
|
||||
<select class="select select-sm select-bordered w-16" bind:value={ref.HighSign}>
|
||||
{#each signOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="input input-sm input-bordered flex-1"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={ref.High}
|
||||
placeholder="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sex & Flag Row -->
|
||||
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||
@ -228,7 +229,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Expandable: Interpretation -->
|
||||
<div class="border border-base-200 rounded-lg overflow-hidden">
|
||||
<div class="border border-base-200 rounded-lg overflow-hidden mb-2">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 bg-base-200 hover:bg-base-300 flex items-center justify-between text-sm transition-colors"
|
||||
@ -260,6 +261,59 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Expandable: Specimen and Criteria -->
|
||||
<div class="border border-base-200 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 bg-base-200 hover:bg-base-300 flex items-center justify-between text-sm transition-colors"
|
||||
onclick={() => toggleSpecimenExpand(index)}
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<Beaker class="w-3 h-3" />
|
||||
<span class="text-xs">Specimen & Criteria</span>
|
||||
{#if ref.SpcType || ref.Criteria}
|
||||
<span class="text-xs text-primary">(Custom)</span>
|
||||
{:else}
|
||||
<span class="text-xs text-gray-500">(Optional)</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if expandedRanges[index]?.specimen}
|
||||
<ChevronUp class="w-4 h-4" />
|
||||
{:else}
|
||||
<ChevronDown class="w-4 h-4" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if expandedRanges[index]?.specimen}
|
||||
<div class="p-3 bg-base-100 space-y-3">
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1 flex items-center gap-1">
|
||||
<Beaker class="w-3 h-3" />
|
||||
Specimen Type
|
||||
</span>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={ref.SpcType}>
|
||||
<option value="">Any specimen</option>
|
||||
{#each specimenTypeOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1 flex items-center gap-1">
|
||||
<Filter class="w-3 h-3" />
|
||||
Criteria
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={ref.Criteria}
|
||||
placeholder="e.g., Fasting, Morning sample"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@ -19,6 +19,69 @@
|
||||
onupdateFormData = () => {}
|
||||
} = $props();
|
||||
|
||||
// Ensure all reference range items have defined values, never undefined
|
||||
function normalizeRefNum(ref) {
|
||||
return {
|
||||
Sex: ref.Sex ?? '2',
|
||||
LowSign: ref.LowSign ?? 'GE',
|
||||
HighSign: ref.HighSign ?? 'LE',
|
||||
Low: ref.Low ?? null,
|
||||
High: ref.High ?? null,
|
||||
AgeStart: ref.AgeStart ?? 0,
|
||||
AgeEnd: ref.AgeEnd ?? 120,
|
||||
Flag: ref.Flag ?? 'N',
|
||||
Interpretation: ref.Interpretation ?? 'Normal',
|
||||
SpcType: ref.SpcType ?? '',
|
||||
Criteria: ref.Criteria ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRefThold(ref) {
|
||||
return {
|
||||
Sex: ref.Sex ?? '2',
|
||||
LowSign: ref.LowSign ?? 'GE',
|
||||
HighSign: ref.HighSign ?? 'LE',
|
||||
Low: ref.Low ?? null,
|
||||
High: ref.High ?? null,
|
||||
AgeStart: ref.AgeStart ?? 0,
|
||||
AgeEnd: ref.AgeEnd ?? 120,
|
||||
Flag: ref.Flag ?? 'N',
|
||||
Interpretation: ref.Interpretation ?? 'Normal',
|
||||
SpcType: ref.SpcType ?? '',
|
||||
Criteria: ref.Criteria ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRefTxt(ref) {
|
||||
return {
|
||||
Sex: ref.Sex ?? '2',
|
||||
AgeStart: ref.AgeStart ?? 0,
|
||||
AgeEnd: ref.AgeEnd ?? 120,
|
||||
RefTxt: ref.RefTxt ?? '',
|
||||
Flag: ref.Flag ?? 'N',
|
||||
SpcType: ref.SpcType ?? '',
|
||||
Criteria: ref.Criteria ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRefVset(ref) {
|
||||
return {
|
||||
Sex: ref.Sex ?? '2',
|
||||
AgeStart: ref.AgeStart ?? 0,
|
||||
AgeEnd: ref.AgeEnd ?? 120,
|
||||
RefTxt: ref.RefTxt ?? '',
|
||||
Flag: ref.Flag ?? 'N',
|
||||
SpcType: ref.SpcType ?? '',
|
||||
Criteria: ref.Criteria ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
// Reactive normalized data
|
||||
let normalizedRefnum = $derived((formData.refnum || []).map(normalizeRefNum));
|
||||
let normalizedRefthold = $derived((formData.refthold || []).map(normalizeRefThold));
|
||||
let normalizedReftxt = $derived((formData.reftxt || []).map(normalizeRefTxt));
|
||||
let normalizedRefvset = $derived((formData.refvset || []).map(normalizeRefVset));
|
||||
|
||||
function updateRefRangeType(type) {
|
||||
let newFormData = {
|
||||
...formData,
|
||||
@ -60,7 +123,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-3">
|
||||
<!-- Reference Range Type Selection -->
|
||||
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
@ -159,21 +222,21 @@
|
||||
|
||||
<!-- Numeric Reference Ranges -->
|
||||
{#if formData.refRangeType === 'num'}
|
||||
<NumericRefRange refnum={formData.refnum} onupdateRefnum={updateRefnum} />
|
||||
<NumericRefRange refnum={normalizedRefnum} onupdateRefnum={updateRefnum} />
|
||||
{/if}
|
||||
|
||||
<!-- Threshold Reference Ranges -->
|
||||
{#if formData.refRangeType === 'thold'}
|
||||
<ThresholdRefRange refthold={formData.refthold} onupdateRefthold={updateRefthold} />
|
||||
<ThresholdRefRange refthold={normalizedRefthold} onupdateRefthold={updateRefthold} />
|
||||
{/if}
|
||||
|
||||
<!-- Text Reference Ranges -->
|
||||
{#if formData.refRangeType === 'text'}
|
||||
<TextRefRange reftxt={formData.reftxt} onupdateReftxt={updateReftxt} />
|
||||
<TextRefRange reftxt={normalizedReftxt} onupdateReftxt={updateReftxt} />
|
||||
{/if}
|
||||
|
||||
<!-- Value Set Reference Ranges -->
|
||||
{#if formData.refRangeType === 'vset'}
|
||||
<ValueSetRefRange refvset={formData.refvset} onupdateRefvset={updateRefvset} />
|
||||
<ValueSetRefRange refvset={normalizedRefvset} onupdateRefvset={updateRefvset} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,265 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { FlaskConical, Ruler, Clock, Beaker } from 'lucide-svelte';
|
||||
import { fetchValueSetByKey } from '$lib/api/valuesets.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {Object} formData - Form data object
|
||||
* @property {(formData: Object) => void} onupdateFormData - Update handler
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
formData = $bindable({}),
|
||||
onupdateFormData = () => {}
|
||||
} = $props();
|
||||
|
||||
// Value set options
|
||||
let resultTypeOptions = $state([]);
|
||||
let refTypeOptions = $state([]);
|
||||
let loading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
loading = true;
|
||||
const [resultTypeRes, refTypeRes] = await Promise.all([
|
||||
fetchValueSetByKey('result_type'),
|
||||
fetchValueSetByKey('reference_type')
|
||||
]);
|
||||
|
||||
console.log('result_type response:', resultTypeRes);
|
||||
console.log('reference_type response:', refTypeRes);
|
||||
|
||||
// Handle different response structures
|
||||
const resultItems = resultTypeRes.data?.items || resultTypeRes.data?.ValueSetItems || (Array.isArray(resultTypeRes.data) ? resultTypeRes.data : []) || [];
|
||||
const refItems = refTypeRes.data?.items || refTypeRes.data?.ValueSetItems || (Array.isArray(refTypeRes.data) ? refTypeRes.data : []) || [];
|
||||
|
||||
resultTypeOptions = resultItems.map(item => ({
|
||||
value: item.value || item.itemCode || item.ItemCode || item.code || item.Code,
|
||||
label: item.label || item.itemValue || item.ItemValue || item.value || item.Value || item.description || item.Description
|
||||
})).filter(opt => opt.value);
|
||||
|
||||
refTypeOptions = refItems.map(item => ({
|
||||
value: item.value || item.itemCode || item.ItemCode || item.code || item.Code,
|
||||
label: item.label || item.itemValue || item.ItemValue || item.value || item.Value || item.description || item.Description
|
||||
})).filter(opt => opt.value);
|
||||
|
||||
console.log('resultTypeOptions:', resultTypeOptions);
|
||||
console.log('refTypeOptions:', refTypeOptions);
|
||||
} catch (err) {
|
||||
console.error('Failed to load value sets:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
function updateField(field, value) {
|
||||
onupdateFormData({ ...formData, [field]: value });
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
<!-- Result Configuration -->
|
||||
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<FlaskConical class="w-5 h-5 text-primary" />
|
||||
<h3 class="font-semibold">Result Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="resultType">
|
||||
<span class="label-text font-medium">Result Type</span>
|
||||
</label>
|
||||
<select
|
||||
id="resultType"
|
||||
class="select select-sm select-bordered w-full"
|
||||
bind:value={formData.ResultType}
|
||||
onchange={(e) => updateField('ResultType', e.target.value)}
|
||||
>
|
||||
<option value="">Select result type...</option>
|
||||
{#each resultTypeOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="refType">
|
||||
<span class="label-text font-medium">Reference Type</span>
|
||||
</label>
|
||||
<select
|
||||
id="refType"
|
||||
class="select select-sm select-bordered w-full"
|
||||
bind:value={formData.RefType}
|
||||
onchange={(e) => updateField('RefType', e.target.value)}
|
||||
>
|
||||
<option value="">Select reference type...</option>
|
||||
{#each refTypeOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Units and Precision -->
|
||||
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Ruler class="w-5 h-5 text-primary" />
|
||||
<h3 class="font-semibold">Units and Precision</h3>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="unit1">
|
||||
<span class="label-text font-medium">Unit 1</span>
|
||||
</label>
|
||||
<input
|
||||
id="unit1"
|
||||
type="text"
|
||||
class="input input-sm input-bordered w-full"
|
||||
value={formData.Unit1}
|
||||
oninput={(e) => updateField('Unit1', e.target.value)}
|
||||
placeholder="e.g., mg/dL"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="factor">
|
||||
<span class="label-text font-medium">Factor</span>
|
||||
</label>
|
||||
<input
|
||||
id="factor"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
class="input input-sm input-bordered w-full"
|
||||
value={formData.Factor}
|
||||
oninput={(e) => updateField('Factor', e.target.value ? parseFloat(e.target.value) : null)}
|
||||
placeholder="Conversion factor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="unit2">
|
||||
<span class="label-text font-medium">Unit 2</span>
|
||||
</label>
|
||||
<input
|
||||
id="unit2"
|
||||
type="text"
|
||||
class="input input-sm input-bordered w-full"
|
||||
value={formData.Unit2}
|
||||
oninput={(e) => updateField('Unit2', e.target.value)}
|
||||
placeholder="e.g., mmol/L"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="decimal">
|
||||
<span class="label-text font-medium">Decimal Places</span>
|
||||
</label>
|
||||
<input
|
||||
id="decimal"
|
||||
type="number"
|
||||
min="0"
|
||||
max="6"
|
||||
class="input input-sm input-bordered w-full"
|
||||
value={formData.Decimal}
|
||||
oninput={(e) => updateField('Decimal', parseInt(e.target.value) || 0)}
|
||||
placeholder="0-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if formData.Factor}
|
||||
<div class="mt-3 text-sm text-gray-600 bg-base-200 p-2 rounded">
|
||||
Formula: {formData.Unit1 || 'Unit1'} × {formData.Factor} = {formData.Unit2 || 'Unit2'}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Specimen Requirements -->
|
||||
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Beaker class="w-5 h-5 text-primary" />
|
||||
<h3 class="font-semibold">Specimen Requirements</h3>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="reqQty">
|
||||
<span class="label-text font-medium">Required Quantity</span>
|
||||
</label>
|
||||
<input
|
||||
id="reqQty"
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="input input-sm input-bordered w-full"
|
||||
value={formData.ReqQty}
|
||||
oninput={(e) => updateField('ReqQty', e.target.value ? parseFloat(e.target.value) : null)}
|
||||
placeholder="Amount"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="reqQtyUnit">
|
||||
<span class="label-text font-medium">Quantity Unit</span>
|
||||
</label>
|
||||
<input
|
||||
id="reqQtyUnit"
|
||||
type="text"
|
||||
class="input input-sm input-bordered w-full"
|
||||
value={formData.ReqQtyUnit}
|
||||
oninput={(e) => updateField('ReqQtyUnit', e.target.value)}
|
||||
placeholder="e.g., mL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Method and TAT -->
|
||||
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Clock class="w-5 h-5 text-primary" />
|
||||
<h3 class="font-semibold">Method and Turnaround</h3>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="method">
|
||||
<span class="label-text font-medium">Method</span>
|
||||
</label>
|
||||
<input
|
||||
id="method"
|
||||
type="text"
|
||||
class="input input-sm input-bordered w-full"
|
||||
value={formData.Method}
|
||||
oninput={(e) => updateField('Method', e.target.value)}
|
||||
placeholder="e.g., Enzymatic"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="expectedTAT">
|
||||
<span class="label-text font-medium">Expected TAT (minutes)</span>
|
||||
</label>
|
||||
<input
|
||||
id="expectedTAT"
|
||||
type="number"
|
||||
min="0"
|
||||
class="input input-sm input-bordered w-full"
|
||||
value={formData.ExpectedTAT}
|
||||
oninput={(e) => updateField('ExpectedTAT', e.target.value ? parseInt(e.target.value) : null)}
|
||||
placeholder="e.g., 60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@ -1,6 +1,8 @@
|
||||
<script>
|
||||
import { PlusCircle, FileText, X } from 'lucide-svelte';
|
||||
import { PlusCircle, FileText, X, ChevronDown, ChevronUp, Beaker, Filter } from 'lucide-svelte';
|
||||
import { sexOptions, createTextRef } from '../referenceRange.js';
|
||||
import { fetchValueSetByKey } from '$lib/api/valuesets.js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
@ -14,17 +16,38 @@
|
||||
onupdateReftxt = () => {}
|
||||
} = $props();
|
||||
|
||||
let expandedRanges = $state({});
|
||||
let specimenTypeOptions = $state([]);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetchValueSetByKey('specimen_type');
|
||||
specimenTypeOptions = response.data?.items?.map(item => ({
|
||||
value: item.itemCode,
|
||||
label: item.itemValue
|
||||
})) || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to load specimen types:', err);
|
||||
}
|
||||
});
|
||||
|
||||
function addRefRange() {
|
||||
const newRef = createTextRef();
|
||||
onupdateReftxt([...reftxt, newRef]);
|
||||
expandedRanges[reftxt.length] = { specimen: false };
|
||||
}
|
||||
|
||||
function removeRefRange(index) {
|
||||
onupdateReftxt(reftxt.filter((_, i) => i !== index));
|
||||
delete expandedRanges[index];
|
||||
}
|
||||
|
||||
function toggleSpecimenExpand(index) {
|
||||
expandedRanges[index] = { ...expandedRanges[index], specimen: !expandedRanges[index]?.specimen };
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<FileText class="w-5 h-5 text-primary" />
|
||||
@ -91,6 +114,59 @@
|
||||
<span class="label-text text-xs mb-1">Reference Text</span>
|
||||
<textarea class="textarea textarea-bordered w-full" rows="2" bind:value={ref.RefTxt} placeholder="e.g., Negative for glucose"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Expandable: Specimen and Criteria -->
|
||||
<div class="border border-base-200 rounded-lg overflow-hidden mt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 bg-base-200 hover:bg-base-300 flex items-center justify-between text-sm transition-colors"
|
||||
onclick={() => toggleSpecimenExpand(index)}
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<Beaker class="w-3 h-3" />
|
||||
<span class="text-xs">Specimen & Criteria</span>
|
||||
{#if ref.SpcType || ref.Criteria}
|
||||
<span class="text-xs text-primary">(Custom)</span>
|
||||
{:else}
|
||||
<span class="text-xs text-gray-500">(Optional)</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if expandedRanges[index]?.specimen}
|
||||
<ChevronUp class="w-4 h-4" />
|
||||
{:else}
|
||||
<ChevronDown class="w-4 h-4" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if expandedRanges[index]?.specimen}
|
||||
<div class="p-3 bg-base-100 space-y-3">
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1 flex items-center gap-1">
|
||||
<Beaker class="w-3 h-3" />
|
||||
Specimen Type
|
||||
</span>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={ref.SpcType}>
|
||||
<option value="">Any specimen</option>
|
||||
{#each specimenTypeOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1 flex items-center gap-1">
|
||||
<Filter class="w-3 h-3" />
|
||||
Criteria
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={ref.Criteria}
|
||||
placeholder="e.g., Fasting, Morning sample"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
<script>
|
||||
import { PlusCircle, Ruler, X } from 'lucide-svelte';
|
||||
import { PlusCircle, Ruler, X, ChevronDown, ChevronUp, Beaker, Filter } from 'lucide-svelte';
|
||||
import { signOptions, flagOptions, sexOptions, createTholdRef } from '../referenceRange.js';
|
||||
import { fetchValueSetByKey } from '$lib/api/valuesets.js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
@ -14,17 +16,38 @@
|
||||
onupdateRefthold = () => {}
|
||||
} = $props();
|
||||
|
||||
let expandedRanges = $state({});
|
||||
let specimenTypeOptions = $state([]);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetchValueSetByKey('specimen_type');
|
||||
specimenTypeOptions = response.data?.items?.map(item => ({
|
||||
value: item.itemCode,
|
||||
label: item.itemValue
|
||||
})) || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to load specimen types:', err);
|
||||
}
|
||||
});
|
||||
|
||||
function addRefRange() {
|
||||
const newRef = createTholdRef();
|
||||
onupdateRefthold([...refthold, newRef]);
|
||||
expandedRanges[refthold.length] = { specimen: false };
|
||||
}
|
||||
|
||||
function removeRefRange(index) {
|
||||
onupdateRefthold(refthold.filter((_, i) => i !== index));
|
||||
delete expandedRanges[index];
|
||||
}
|
||||
|
||||
function toggleSpecimenExpand(index) {
|
||||
expandedRanges[index] = { ...expandedRanges[index], specimen: !expandedRanges[index]?.specimen };
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Ruler class="w-5 h-5 text-primary" />
|
||||
@ -118,6 +141,59 @@
|
||||
<span class="label-text text-xs mb-1">Interpretation</span>
|
||||
<input type="text" class="input input-sm input-bordered w-full" bind:value={ref.Interpretation} placeholder="e.g., Alert threshold" />
|
||||
</div>
|
||||
|
||||
<!-- Expandable: Specimen and Criteria -->
|
||||
<div class="border border-base-200 rounded-lg overflow-hidden mt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 bg-base-200 hover:bg-base-300 flex items-center justify-between text-sm transition-colors"
|
||||
onclick={() => toggleSpecimenExpand(index)}
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<Beaker class="w-3 h-3" />
|
||||
<span class="text-xs">Specimen & Criteria</span>
|
||||
{#if ref.SpcType || ref.Criteria}
|
||||
<span class="text-xs text-primary">(Custom)</span>
|
||||
{:else}
|
||||
<span class="text-xs text-gray-500">(Optional)</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if expandedRanges[index]?.specimen}
|
||||
<ChevronUp class="w-4 h-4" />
|
||||
{:else}
|
||||
<ChevronDown class="w-4 h-4" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if expandedRanges[index]?.specimen}
|
||||
<div class="p-3 bg-base-100 space-y-3">
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1 flex items-center gap-1">
|
||||
<Beaker class="w-3 h-3" />
|
||||
Specimen Type
|
||||
</span>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={ref.SpcType}>
|
||||
<option value="">Any specimen</option>
|
||||
{#each specimenTypeOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1 flex items-center gap-1">
|
||||
<Filter class="w-3 h-3" />
|
||||
Criteria
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={ref.Criteria}
|
||||
placeholder="e.g., Fasting, Morning sample"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
<script>
|
||||
import { PlusCircle, Box, X } from 'lucide-svelte';
|
||||
import { PlusCircle, Box, X, ChevronDown, ChevronUp, Beaker, Filter } from 'lucide-svelte';
|
||||
import { sexOptions, createVsetRef } from '../referenceRange.js';
|
||||
import { fetchValueSetByKey } from '$lib/api/valuesets.js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
@ -14,17 +16,38 @@
|
||||
onupdateRefvset = () => {}
|
||||
} = $props();
|
||||
|
||||
let expandedRanges = $state({});
|
||||
let specimenTypeOptions = $state([]);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetchValueSetByKey('specimen_type');
|
||||
specimenTypeOptions = response.data?.items?.map(item => ({
|
||||
value: item.itemCode,
|
||||
label: item.itemValue
|
||||
})) || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to load specimen types:', err);
|
||||
}
|
||||
});
|
||||
|
||||
function addRefRange() {
|
||||
const newRef = createVsetRef();
|
||||
onupdateRefvset([...refvset, newRef]);
|
||||
expandedRanges[refvset.length] = { specimen: false };
|
||||
}
|
||||
|
||||
function removeRefRange(index) {
|
||||
onupdateRefvset(refvset.filter((_, i) => i !== index));
|
||||
delete expandedRanges[index];
|
||||
}
|
||||
|
||||
function toggleSpecimenExpand(index) {
|
||||
expandedRanges[index] = { ...expandedRanges[index], specimen: !expandedRanges[index]?.specimen };
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Box class="w-5 h-5 text-primary" />
|
||||
@ -89,9 +112,62 @@
|
||||
|
||||
<div class="form-control mt-3">
|
||||
<span class="label-text text-xs mb-1">Value Set</span>
|
||||
<input type="text" class="input input-sm input-bordered w-full" bind:value={ref.valueset} placeholder="e.g., Positive, Negative, Borderline" />
|
||||
<input type="text" class="input input-sm input-bordered w-full" bind:value={ref.RefTxt} placeholder="e.g., Positive, Negative, Borderline" />
|
||||
<span class="label-text-alt text-gray-500 mt-1">Comma-separated list of allowed values</span>
|
||||
</div>
|
||||
|
||||
<!-- Expandable: Specimen and Criteria -->
|
||||
<div class="border border-base-200 rounded-lg overflow-hidden mt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 bg-base-200 hover:bg-base-300 flex items-center justify-between text-sm transition-colors"
|
||||
onclick={() => toggleSpecimenExpand(index)}
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<Beaker class="w-3 h-3" />
|
||||
<span class="text-xs">Specimen & Criteria</span>
|
||||
{#if ref.SpcType || ref.Criteria}
|
||||
<span class="text-xs text-primary">(Custom)</span>
|
||||
{:else}
|
||||
<span class="text-xs text-gray-500">(Optional)</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if expandedRanges[index]?.specimen}
|
||||
<ChevronUp class="w-4 h-4" />
|
||||
{:else}
|
||||
<ChevronDown class="w-4 h-4" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if expandedRanges[index]?.specimen}
|
||||
<div class="p-3 bg-base-100 space-y-3">
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1 flex items-center gap-1">
|
||||
<Beaker class="w-3 h-3" />
|
||||
Specimen Type
|
||||
</span>
|
||||
<select class="select select-sm select-bordered w-full" bind:value={ref.SpcType}>
|
||||
<option value="">Any specimen</option>
|
||||
{#each specimenTypeOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<span class="label-text text-xs mb-1 flex items-center gap-1">
|
||||
<Filter class="w-3 h-3" />
|
||||
Criteria
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={ref.Criteria}
|
||||
placeholder="e.g., Fasting, Morning sample"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@ -210,7 +210,7 @@
|
||||
<p class="text-gray-500 mt-4">Loading ValueSet details...</p>
|
||||
</div>
|
||||
{:else if selectedValueSet}
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-3">
|
||||
<!-- Header -->
|
||||
<div class="border-b border-base-300 pb-4">
|
||||
<h2 class="text-2xl font-bold text-gray-800">{selectedValueSet.Name}</h2>
|
||||
|
||||
@ -368,7 +368,7 @@
|
||||
<p class="text-sm">This patient has no visit records.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
{#each visits as visit}
|
||||
<div class="card bg-base-100 shadow border border-base-200 hover:shadow-md transition-shadow">
|
||||
<div class="card-body p-4">
|
||||
@ -512,7 +512,7 @@
|
||||
</Modal>
|
||||
|
||||
<Modal bind:open={dischargeModalOpen} title="Discharge Patient" size="sm">
|
||||
<div class="py-2 space-y-4">
|
||||
<div class="py-2 space-y-3">
|
||||
<p>Discharge patient <strong>{selectedPatient ? [selectedPatient.Prefix, selectedPatient.NameFirst, selectedPatient.NameMiddle, selectedPatient.NameLast].filter(Boolean).join(' ') : ''}</strong></p>
|
||||
<p class="text-sm text-gray-600">Visit ID: {visitToDischarge?.PVID || visitToDischarge?.InternalPVID}</p>
|
||||
|
||||
@ -524,7 +524,7 @@
|
||||
<input
|
||||
id="dischargeDate"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={dischargeDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -267,7 +267,7 @@
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<form class="space-y-6" onsubmit={(e) => e.preventDefault()}>
|
||||
<form class="space-y-3" onsubmit={(e) => e.preventDefault()}>
|
||||
<!-- DaisyUI Tabs -->
|
||||
<div class="tabs tabs-bordered">
|
||||
<button
|
||||
@ -290,7 +290,7 @@
|
||||
|
||||
<!-- Personal Info Tab (Basic + Demographics) -->
|
||||
{#if activeTab === 'personal'}
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-3">
|
||||
<!-- Basic Info Section -->
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-700 mb-4">Basic Information</h4>
|
||||
@ -303,7 +303,7 @@
|
||||
<input
|
||||
id="patientId"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
class:input-error={formErrors.PatientID}
|
||||
bind:value={formData.PatientID}
|
||||
placeholder="Enter patient ID"
|
||||
@ -332,7 +332,7 @@
|
||||
<input
|
||||
id="nameFirst"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
class:input-error={formErrors.NameFirst}
|
||||
bind:value={formData.NameFirst}
|
||||
placeholder="Enter first name"
|
||||
@ -349,7 +349,7 @@
|
||||
<input
|
||||
id="nameMiddle"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.NameMiddle}
|
||||
placeholder="Enter middle name"
|
||||
/>
|
||||
@ -362,7 +362,7 @@
|
||||
<input
|
||||
id="nameLast"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.NameLast}
|
||||
placeholder="Enter last name"
|
||||
/>
|
||||
@ -377,7 +377,7 @@
|
||||
<input
|
||||
id="nameMaiden"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.NameMaiden}
|
||||
placeholder="Enter maiden name"
|
||||
/>
|
||||
@ -390,7 +390,7 @@
|
||||
<input
|
||||
id="suffix"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.Suffix}
|
||||
placeholder="Enter suffix (e.g., Jr, Sr, III)"
|
||||
/>
|
||||
@ -416,7 +416,7 @@
|
||||
<input
|
||||
id="birthdate"
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
class:input-error={formErrors.Birthdate}
|
||||
bind:value={formData.Birthdate}
|
||||
/>
|
||||
@ -432,7 +432,7 @@
|
||||
<input
|
||||
id="placeOfBirth"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.PlaceOfBirth}
|
||||
placeholder="Enter place of birth"
|
||||
/>
|
||||
@ -485,7 +485,7 @@
|
||||
<input
|
||||
id="citizenship"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.Citizenship}
|
||||
placeholder="Enter citizenship"
|
||||
/>
|
||||
@ -497,7 +497,7 @@
|
||||
|
||||
<!-- Location & Contact Tab -->
|
||||
{#if activeTab === 'location'}
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-3">
|
||||
<!-- Address Section -->
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-700 mb-4">Address Information</h4>
|
||||
@ -509,7 +509,7 @@
|
||||
<input
|
||||
id="street1"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.Street_1}
|
||||
placeholder="Enter street address"
|
||||
/>
|
||||
@ -522,7 +522,7 @@
|
||||
<input
|
||||
id="street2"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.Street_2}
|
||||
placeholder="Enter street address line 2"
|
||||
/>
|
||||
@ -535,7 +535,7 @@
|
||||
<input
|
||||
id="street3"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.Street_3}
|
||||
placeholder="Enter street address line 3"
|
||||
/>
|
||||
@ -566,7 +566,7 @@
|
||||
<input
|
||||
id="zip"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.ZIP}
|
||||
placeholder="Enter ZIP code"
|
||||
/>
|
||||
@ -596,7 +596,7 @@
|
||||
<input
|
||||
id="phone"
|
||||
type="tel"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.Phone}
|
||||
placeholder="Enter phone number"
|
||||
/>
|
||||
@ -609,7 +609,7 @@
|
||||
<input
|
||||
id="mobilePhone"
|
||||
type="tel"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.MobilePhone}
|
||||
placeholder="Enter mobile number"
|
||||
/>
|
||||
@ -624,7 +624,7 @@
|
||||
<input
|
||||
id="email1"
|
||||
type="email"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.EmailAddress1}
|
||||
placeholder="Enter email address"
|
||||
/>
|
||||
@ -637,7 +637,7 @@
|
||||
<input
|
||||
id="email2"
|
||||
type="email"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.EmailAddress2}
|
||||
placeholder="Enter secondary email"
|
||||
/>
|
||||
|
||||
@ -197,7 +197,7 @@
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<form class="space-y-6" onsubmit={(e) => e.preventDefault()}>
|
||||
<form class="space-y-3" onsubmit={(e) => e.preventDefault()}>
|
||||
<!-- Patient Info Display -->
|
||||
{#if patient}
|
||||
<div class="p-4 bg-base-200 rounded-lg">
|
||||
@ -233,7 +233,7 @@
|
||||
|
||||
<!-- Tab: Visit Info -->
|
||||
{#if activeTab === 'info'}
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{#if isEdit}
|
||||
<div class="form-control">
|
||||
@ -243,7 +243,7 @@
|
||||
<input
|
||||
id="pvid"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.PVID}
|
||||
placeholder="Enter visit ID"
|
||||
/>
|
||||
@ -258,7 +258,7 @@
|
||||
<input
|
||||
id="patientId"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
class:input-error={formErrors.PatientID}
|
||||
bind:value={formData.PatientID}
|
||||
placeholder="Enter patient ID"
|
||||
@ -277,7 +277,7 @@
|
||||
<input
|
||||
id="visitDate"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
class:input-error={formErrors.PVCreateDate}
|
||||
bind:value={formData.PVCreateDate}
|
||||
/>
|
||||
@ -350,7 +350,7 @@
|
||||
|
||||
<!-- Tab: Diagnosis & Status -->
|
||||
{#if activeTab === 'diagnosis'}
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="diagCode">
|
||||
@ -359,7 +359,7 @@
|
||||
<input
|
||||
id="diagCode"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.DiagCode}
|
||||
placeholder="Enter diagnosis code"
|
||||
/>
|
||||
@ -389,7 +389,7 @@
|
||||
<input
|
||||
id="endDate"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.EndDate}
|
||||
/>
|
||||
</div>
|
||||
@ -401,7 +401,7 @@
|
||||
<input
|
||||
id="archivedDate"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.ArchivedDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -284,7 +284,7 @@
|
||||
<p class="text-sm">This patient has no visit records.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
{#each visits as visit}
|
||||
<div class="card bg-base-100 shadow border border-base-200 hover:shadow-md transition-shadow">
|
||||
<div class="card-body p-4">
|
||||
@ -383,7 +383,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="space-y-6" onsubmit={(e) => e.preventDefault()}>
|
||||
<form class="space-y-3" onsubmit={(e) => e.preventDefault()}>
|
||||
<!-- Tabs -->
|
||||
<div class="tabs tabs-bordered">
|
||||
<button
|
||||
@ -406,7 +406,7 @@
|
||||
|
||||
<!-- Tab: Visit Info -->
|
||||
{#if activeTab === 'info'}
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="patientId">
|
||||
@ -416,7 +416,7 @@
|
||||
<input
|
||||
id="patientId"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
class:input-error={formErrors.PatientID}
|
||||
bind:value={formData.PatientID}
|
||||
placeholder="Enter patient ID"
|
||||
@ -435,7 +435,7 @@
|
||||
<input
|
||||
id="visitDate"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
class:input-error={formErrors.PVCreateDate}
|
||||
bind:value={formData.PVCreateDate}
|
||||
/>
|
||||
@ -508,7 +508,7 @@
|
||||
|
||||
<!-- Tab: Diagnosis & Status -->
|
||||
{#if activeTab === 'diagnosis'}
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="diagCode">
|
||||
@ -517,7 +517,7 @@
|
||||
<input
|
||||
id="diagCode"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.DiagCode}
|
||||
placeholder="Enter diagnosis code"
|
||||
/>
|
||||
@ -547,7 +547,7 @@
|
||||
<input
|
||||
id="endDate"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.EndDate}
|
||||
/>
|
||||
</div>
|
||||
@ -559,7 +559,7 @@
|
||||
<input
|
||||
id="archivedDate"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full"
|
||||
class="input input-sm input-bordered w-full"
|
||||
bind:value={formData.ArchivedDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user