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: oklch(60% 0.25 25);
|
||||||
--color-error-content: oklch(98% 0.01 25);
|
--color-error-content: oklch(98% 0.01 25);
|
||||||
|
|
||||||
/* Border radius */
|
/* Border radius - smaller for compact look */
|
||||||
--radius-selector: 0.5rem;
|
--radius-selector: 0.25rem;
|
||||||
--radius-field: 0.375rem;
|
--radius-field: 0.25rem;
|
||||||
--radius-box: 0.5rem;
|
--radius-box: 0.375rem;
|
||||||
|
|
||||||
/* Base sizes */
|
/* Base sizes - reduced for compact UI */
|
||||||
--size-selector: 0.25rem;
|
--size-selector: 0.2rem;
|
||||||
--size-field: 0.25rem;
|
--size-field: 0.2rem;
|
||||||
|
|
||||||
/* Border size */
|
/* Border size */
|
||||||
--border: 1px;
|
--border: 1px;
|
||||||
|
|
||||||
/* Effects */
|
/* Effects */
|
||||||
--depth: 1;
|
--depth: 0;
|
||||||
--noise: 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 {
|
@theme {
|
||||||
/* Custom color helpers */
|
/* Custom color helpers */
|
||||||
--color-emerald-50: #ecfdf5;
|
--color-emerald-50: #ecfdf5;
|
||||||
|
|||||||
@ -32,9 +32,24 @@ export async function createTest(data) {
|
|||||||
// Type-specific fields
|
// Type-specific fields
|
||||||
Unit: data.Unit,
|
Unit: data.Unit,
|
||||||
Formula: data.Formula,
|
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)
|
// Reference ranges (only for TEST and CALC)
|
||||||
refnum: data.refnum,
|
refnum: data.refnum,
|
||||||
reftxt: data.reftxt,
|
reftxt: data.reftxt,
|
||||||
|
refvset: data.refvset,
|
||||||
|
refthold: data.refthold,
|
||||||
};
|
};
|
||||||
return post('/api/tests', payload);
|
return post('/api/tests', payload);
|
||||||
}
|
}
|
||||||
@ -54,9 +69,24 @@ export async function updateTest(data) {
|
|||||||
// Type-specific fields
|
// Type-specific fields
|
||||||
Unit: data.Unit,
|
Unit: data.Unit,
|
||||||
Formula: data.Formula,
|
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)
|
// Reference ranges (only for TEST and CALC)
|
||||||
refnum: data.refnum,
|
refnum: data.refnum,
|
||||||
reftxt: data.reftxt,
|
reftxt: data.reftxt,
|
||||||
|
refvset: data.refvset,
|
||||||
|
refthold: data.refthold,
|
||||||
};
|
};
|
||||||
return patch('/api/tests', payload);
|
return patch('/api/tests', payload);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,7 +64,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="overflow-x-auto {className}">
|
<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>
|
<thead>
|
||||||
<tr class="bg-base-200">
|
<tr class="bg-base-200">
|
||||||
{#each columns as column}
|
{#each columns as column}
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
* @property {string} [title] - Modal title
|
* @property {string} [title] - Modal title
|
||||||
* @property {string} [size] - Modal size (sm, md, lg, xl, full)
|
* @property {string} [size] - Modal size (sm, md, lg, xl, full)
|
||||||
* @property {boolean} [closable] - Whether modal can be closed by clicking backdrop
|
* @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 {Function} [onClose] - Callback when modal is closed
|
||||||
* @property {import('svelte').Snippet} [children] - Modal content
|
* @property {import('svelte').Snippet} [children] - Modal content
|
||||||
* @property {import('svelte').Snippet} [footer] - Modal footer
|
* @property {import('svelte').Snippet} [footer] - Modal footer
|
||||||
@ -18,6 +19,7 @@
|
|||||||
title = '',
|
title = '',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
closable = true,
|
closable = true,
|
||||||
|
position = 'center',
|
||||||
onClose = null,
|
onClose = null,
|
||||||
children,
|
children,
|
||||||
footer,
|
footer,
|
||||||
@ -33,12 +35,12 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const widthStyles = {
|
const widthStyles = {
|
||||||
sm: 'max-width: 400px;',
|
sm: 'max-width: 350px;',
|
||||||
md: 'max-width: 500px;',
|
md: 'max-width: 450px;',
|
||||||
lg: 'max-width: 800px;',
|
lg: 'max-width: 700px;',
|
||||||
xl: 'max-width: 1200px;',
|
xl: 'max-width: 1000px;',
|
||||||
full: 'max-width: 100%; width: 100%; height: 100%;',
|
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} />
|
<svelte:window onkeydown={handleKeydown} />
|
||||||
|
|
||||||
<dialog class="modal {sizeClasses[size] || ''}" class:modal-open={open}>
|
<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 -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
{#if title}
|
{#if title}
|
||||||
|
|||||||
@ -88,7 +88,7 @@
|
|||||||
bind:value
|
bind:value
|
||||||
{required}
|
{required}
|
||||||
{disabled}
|
{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}
|
class:opacity-50={loading}
|
||||||
>
|
>
|
||||||
<option value="">{placeholder}</option>
|
<option value="">{placeholder}</option>
|
||||||
|
|||||||
@ -68,276 +68,171 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Function to expand sidebar when clicking dropdown in collapsed mode
|
||||||
|
function expandSidebar() {
|
||||||
|
isOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
auth.logout();
|
auth.logout();
|
||||||
goto('/login');
|
goto('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMasterData() {
|
function toggleMasterData() {
|
||||||
|
if (!isOpen) {
|
||||||
|
expandSidebar();
|
||||||
|
}
|
||||||
masterDataExpanded = !masterDataExpanded;
|
masterDataExpanded = !masterDataExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLaboratory() {
|
function toggleLaboratory() {
|
||||||
|
if (!isOpen) {
|
||||||
|
expandSidebar();
|
||||||
|
}
|
||||||
laboratoryExpanded = !laboratoryExpanded;
|
laboratoryExpanded = !laboratoryExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAdministration() {
|
function toggleAdministration() {
|
||||||
|
if (!isOpen) {
|
||||||
|
expandSidebar();
|
||||||
|
}
|
||||||
administrationExpanded = !administrationExpanded;
|
administrationExpanded = !administrationExpanded;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div
|
<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-expanded={isOpen}
|
||||||
class:sidebar-collapsed={!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="h-screen overflow-y-auto flex flex-col sidebar-content" class:expanded={isOpen} class:collapsed={!isOpen}>
|
||||||
<div class="p-3">
|
<div>
|
||||||
<!-- Navigation Menu -->
|
<!-- Navigation Menu -->
|
||||||
<ul class="menu w-full gap-1" class:menu-collapsed={!isOpen}>
|
<ul class="menu w-full gap-1" class:menu-collapsed={!isOpen}>
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<li class="menu-title uppercase font-bold text-xs text-secondary/70 mt-2">Main</li>
|
<li class="menu-title uppercase font-bold text-xs text-secondary/70 mt-2">Main</li>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Dashboard -->
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="/dashboard"
|
href="/dashboard"
|
||||||
class="menu-item text-gray-700 hover:bg-secondary/10 hover:text-secondary-focus"
|
class="nav-link"
|
||||||
class:centered={!isOpen}
|
class:centered={!isOpen}
|
||||||
title={!isOpen ? 'Dashboard' : ''}
|
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}
|
{#if isOpen}
|
||||||
<span class="menu-text whitespace-nowrap overflow-hidden">Dashboard</span>
|
<span class="nav-text">Dashboard</span>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="collapsible-section">
|
|
||||||
|
<!-- Master Data -->
|
||||||
|
<li class="nav-group" class:collapsed={!isOpen}>
|
||||||
<button
|
<button
|
||||||
onclick={toggleMasterData}
|
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}
|
class:centered={!isOpen}
|
||||||
title={!isOpen ? 'Master Data' : ''}
|
title={!isOpen ? 'Master Data' : ''}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<Database size={20} class="text-secondary flex-shrink-0" />
|
||||||
<Database class="w-5 h-5 text-secondary flex-shrink-0" />
|
|
||||||
{#if isOpen}
|
|
||||||
<span class="menu-text whitespace-nowrap overflow-hidden">Master Data</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if isOpen}
|
{#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}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if isOpen && masterDataExpanded}
|
{#if isOpen && masterDataExpanded}
|
||||||
<ul class="ml-6 mt-1 space-y-1 collapsible-content">
|
<ul class="submenu">
|
||||||
<li>
|
<li><a href="/master-data/containers" class="submenu-link"><FlaskConical size={16} /> Containers</a></li>
|
||||||
<a
|
<li><a href="/master-data/tests" class="submenu-link"><TestTube size={16} /> Test Definitions</a></li>
|
||||||
href="/master-data/containers"
|
<li><a href="/master-data/valuesets" class="submenu-link"><List size={16} /> ValueSets</a></li>
|
||||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
<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>
|
||||||
<FlaskConical class="w-4 h-4 flex-shrink-0" />
|
<li><a href="/master-data/specialties" class="submenu-link"><Stethoscope size={16} /> Specialties</a></li>
|
||||||
<span>Containers</span>
|
<li><a href="/master-data/occupations" class="submenu-link"><Briefcase size={16} /> Occupations</a></li>
|
||||||
</a>
|
<li><a href="/master-data/counters" class="submenu-link"><Hash size={16} /> Counters</a></li>
|
||||||
</li>
|
<li><a href="/master-data/geography" class="submenu-link"><Globe size={16} /> Geography</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>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<!-- Result Entry -->
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="/result-entry"
|
href="/result-entry"
|
||||||
class="menu-item text-gray-700 hover:bg-secondary/10 hover:text-secondary-focus"
|
class="nav-link"
|
||||||
class:centered={!isOpen}
|
class:centered={!isOpen}
|
||||||
title={!isOpen ? 'Result Entry' : ''}
|
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}
|
{#if isOpen}
|
||||||
<span class="menu-text whitespace-nowrap overflow-hidden">Result Entry</span>
|
<span class="nav-text">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>
|
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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
|
<button
|
||||||
onclick={toggleLaboratory}
|
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}
|
class:centered={!isOpen}
|
||||||
title={!isOpen ? 'Laboratory' : ''}
|
title={!isOpen ? 'Laboratory' : ''}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<FlaskConical size={20} class="text-secondary flex-shrink-0" />
|
||||||
<FlaskConical class="w-5 h-5 text-secondary flex-shrink-0" />
|
|
||||||
{#if isOpen}
|
|
||||||
<span class="menu-text whitespace-nowrap overflow-hidden">Laboratory</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if isOpen}
|
{#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}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if isOpen && laboratoryExpanded}
|
{#if isOpen && laboratoryExpanded}
|
||||||
<ul class="ml-6 mt-1 space-y-1 collapsible-content">
|
<ul class="submenu">
|
||||||
<li>
|
<li><a href="/patients" class="submenu-link"><Users size={16} /> Patients</a></li>
|
||||||
<a
|
<li><a href="/orders" class="submenu-link"><ClipboardList size={16} /> Orders</a></li>
|
||||||
href="/patients"
|
<li><a href="/specimens" class="submenu-link"><FlaskConical size={16} /> Specimens</a></li>
|
||||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-secondary/10 hover:text-secondary-focus"
|
<li><a href="/results" class="submenu-link"><CheckCircle2 size={16} /> Results</a></li>
|
||||||
>
|
|
||||||
<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>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="collapsible-section">
|
<!-- Administration -->
|
||||||
|
<li class="nav-group" class:collapsed={!isOpen}>
|
||||||
<button
|
<button
|
||||||
onclick={toggleAdministration}
|
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}
|
class:centered={!isOpen}
|
||||||
title={!isOpen ? 'Administration' : ''}
|
title={!isOpen ? 'Administration' : ''}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<Building2 size={20} class="text-secondary flex-shrink-0" />
|
||||||
<Building2 class="w-5 h-5 text-secondary flex-shrink-0" />
|
|
||||||
{#if isOpen}
|
|
||||||
<span class="menu-text whitespace-nowrap overflow-hidden">Administration</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if isOpen}
|
{#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}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if isOpen && administrationExpanded}
|
{#if isOpen && administrationExpanded}
|
||||||
<ul class="ml-6 mt-1 space-y-1 collapsible-content">
|
<ul class="submenu">
|
||||||
<li>
|
<li><a href="/organization" class="submenu-link"><Building2 size={16} /> Organization</a></li>
|
||||||
<a
|
<li><a href="/users" class="submenu-link"><UserCircle size={16} /> Users</a></li>
|
||||||
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>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
@ -349,13 +244,13 @@
|
|||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onclick={handleLogout}
|
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}
|
class:centered={!isOpen}
|
||||||
title={!isOpen ? 'Logout' : ''}
|
title={!isOpen ? 'Logout' : ''}
|
||||||
>
|
>
|
||||||
<LogOut class="w-5 h-5 flex-shrink-0" />
|
<LogOut size={20} class="flex-shrink-0" />
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<span class="menu-text whitespace-nowrap overflow-hidden">Logout</span>
|
<span class="nav-text">Logout</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@ -371,11 +266,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-expanded {
|
.sidebar-expanded {
|
||||||
width: 14rem;
|
width: 16rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-collapsed {
|
.sidebar-collapsed {
|
||||||
width: 4rem;
|
width: 3.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content {
|
.sidebar-content {
|
||||||
@ -383,55 +278,97 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content.expanded {
|
.sidebar-content.expanded {
|
||||||
width: 14rem;
|
width: 16rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content.collapsed {
|
.sidebar-content.collapsed {
|
||||||
width: 4rem;
|
width: 3.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item {
|
.nav-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
transition: all 0.2s;
|
color: hsl(var(--bc));
|
||||||
gap: 0.5rem;
|
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;
|
justify-content: center;
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem;
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
margin-left: auto;
|
height: 2.5rem;
|
||||||
margin-right: auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-text {
|
/* Fix for collapsed menu items - override DaisyUI .menu styles */
|
||||||
flex: 1;
|
:global(.menu-collapsed li) {
|
||||||
transition: opacity 300ms ease-out;
|
display: flex !important;
|
||||||
}
|
|
||||||
|
|
||||||
/* Collapsed menu styling */
|
|
||||||
.menu-collapsed :global(li > a),
|
|
||||||
.menu-collapsed :global(li > button) {
|
|
||||||
justify-content: center !important;
|
justify-content: center !important;
|
||||||
|
align-items: center !important;
|
||||||
|
width: 100% !important;
|
||||||
padding-left: 0 !important;
|
padding-left: 0 !important;
|
||||||
padding-right: 0 !important;
|
padding-right: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Collapsible section styles */
|
:global(.menu-collapsed li > a),
|
||||||
.collapsible-section button {
|
: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;
|
display: flex;
|
||||||
align-items: center;
|
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 {
|
.submenu-link:hover {
|
||||||
justify-content: center;
|
background-color: hsl(var(--b3));
|
||||||
}
|
color: hsl(var(--bc));
|
||||||
|
|
||||||
.collapsible-content {
|
|
||||||
animation: slideDown 0.2s ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideDown {
|
@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>
|
</script>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-4">
|
||||||
<h1 class="text-3xl font-bold text-gray-800 mb-2">Master Data</h1>
|
<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>
|
<p class="text-gray-600 mb-8">Manage reference data and lookup values used throughout the system</p>
|
||||||
|
|
||||||
|
|||||||
@ -166,7 +166,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-4">
|
||||||
<div class="flex items-center gap-4 mb-6">
|
<div class="flex items-center gap-4 mb-6">
|
||||||
<a href="/master-data" class="btn btn-ghost btn-circle">
|
<a href="/master-data" class="btn btn-ghost btn-circle">
|
||||||
<ArrowLeft class="w-5 h-5" />
|
<ArrowLeft class="w-5 h-5" />
|
||||||
@ -262,7 +262,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'Add Contact' : 'Edit Contact'} size="md">
|
<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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="initial">
|
<label class="label" for="initial">
|
||||||
@ -279,7 +279,7 @@
|
|||||||
<input
|
<input
|
||||||
id="initial"
|
id="initial"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.Initial}
|
bind:value={formData.Initial}
|
||||||
placeholder="e.g., JS, AB, MK"
|
placeholder="e.g., JS, AB, MK"
|
||||||
maxlength="10"
|
maxlength="10"
|
||||||
@ -296,7 +296,7 @@
|
|||||||
<input
|
<input
|
||||||
id="title"
|
id="title"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.Title}
|
bind:value={formData.Title}
|
||||||
placeholder="e.g., Dr., Prof., Mr., Ms."
|
placeholder="e.g., Dr., Prof., Mr., Ms."
|
||||||
/>
|
/>
|
||||||
@ -310,7 +310,7 @@
|
|||||||
<input
|
<input
|
||||||
id="nameFirst"
|
id="nameFirst"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.NameFirst}
|
bind:value={formData.NameFirst}
|
||||||
placeholder="Enter first name"
|
placeholder="Enter first name"
|
||||||
/>
|
/>
|
||||||
@ -322,7 +322,7 @@
|
|||||||
<input
|
<input
|
||||||
id="nameLast"
|
id="nameLast"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.NameLast}
|
bind:value={formData.NameLast}
|
||||||
placeholder="Enter last name"
|
placeholder="Enter last name"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -146,7 +146,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-4">
|
||||||
<div class="flex items-center gap-4 mb-6">
|
<div class="flex items-center gap-4 mb-6">
|
||||||
<a href="/master-data" class="btn btn-ghost btn-circle">
|
<a href="/master-data" class="btn btn-ghost btn-circle">
|
||||||
<ArrowLeft class="w-5 h-5" />
|
<ArrowLeft class="w-5 h-5" />
|
||||||
@ -249,7 +249,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'Add Container' : 'Edit Container'} size="md">
|
<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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="code">
|
<label class="label" for="code">
|
||||||
@ -259,7 +259,7 @@
|
|||||||
<input
|
<input
|
||||||
id="code"
|
id="code"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.ConCode}
|
bind:value={formData.ConCode}
|
||||||
placeholder="e.g., SST, EDTA, HEP"
|
placeholder="e.g., SST, EDTA, HEP"
|
||||||
required
|
required
|
||||||
@ -274,7 +274,7 @@
|
|||||||
<input
|
<input
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.ConName}
|
bind:value={formData.ConName}
|
||||||
placeholder="e.g., Serum Separator Tube"
|
placeholder="e.g., Serum Separator Tube"
|
||||||
required
|
required
|
||||||
@ -289,7 +289,7 @@
|
|||||||
<input
|
<input
|
||||||
id="desc"
|
id="desc"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.ConDesc}
|
bind:value={formData.ConDesc}
|
||||||
placeholder="e.g., Evacuated blood collection tube with gel separator"
|
placeholder="e.g., Evacuated blood collection tube with gel separator"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -197,7 +197,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-4">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center gap-4 mb-6">
|
<div class="flex items-center gap-4 mb-6">
|
||||||
<a href="/master-data" class="btn btn-ghost btn-circle">
|
<a href="/master-data" class="btn btn-ghost btn-circle">
|
||||||
@ -327,7 +327,7 @@
|
|||||||
|
|
||||||
<!-- Create/Edit Modal -->
|
<!-- Create/Edit Modal -->
|
||||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'Add Counter' : 'Edit Counter'} size="md">
|
<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 -->
|
<!-- Description -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="counterDesc">
|
<label class="label" for="counterDesc">
|
||||||
@ -337,7 +337,7 @@
|
|||||||
<input
|
<input
|
||||||
id="counterDesc"
|
id="counterDesc"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
class:input-error={formErrors.CounterDesc}
|
class:input-error={formErrors.CounterDesc}
|
||||||
bind:value={formData.CounterDesc}
|
bind:value={formData.CounterDesc}
|
||||||
placeholder="e.g., Sample ID Counter, Order Number"
|
placeholder="e.g., Sample ID Counter, Order Number"
|
||||||
@ -367,7 +367,7 @@
|
|||||||
<input
|
<input
|
||||||
id="counterValue"
|
id="counterValue"
|
||||||
type="number"
|
type="number"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
class:input-error={formErrors.CounterValue}
|
class:input-error={formErrors.CounterValue}
|
||||||
bind:value={formData.CounterValue}
|
bind:value={formData.CounterValue}
|
||||||
placeholder="e.g., 1000"
|
placeholder="e.g., 1000"
|
||||||
@ -396,7 +396,7 @@
|
|||||||
<input
|
<input
|
||||||
id="counterStart"
|
id="counterStart"
|
||||||
type="number"
|
type="number"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
class:input-error={formErrors.CounterStart}
|
class:input-error={formErrors.CounterStart}
|
||||||
bind:value={formData.CounterStart}
|
bind:value={formData.CounterStart}
|
||||||
placeholder="e.g., 1"
|
placeholder="e.g., 1"
|
||||||
@ -424,7 +424,7 @@
|
|||||||
<input
|
<input
|
||||||
id="counterEnd"
|
id="counterEnd"
|
||||||
type="number"
|
type="number"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
class:input-error={formErrors.CounterEnd}
|
class:input-error={formErrors.CounterEnd}
|
||||||
bind:value={formData.CounterEnd}
|
bind:value={formData.CounterEnd}
|
||||||
placeholder="e.g., 9999"
|
placeholder="e.g., 9999"
|
||||||
@ -452,7 +452,7 @@
|
|||||||
<select
|
<select
|
||||||
id="counterReset"
|
id="counterReset"
|
||||||
name="counterReset"
|
name="counterReset"
|
||||||
class="select select-bordered w-full"
|
class="select select-sm select-bordered w-full"
|
||||||
bind:value={formData.CounterReset}
|
bind:value={formData.CounterReset}
|
||||||
>
|
>
|
||||||
{#each resetOptions as option (option.value)}
|
{#each resetOptions as option (option.value)}
|
||||||
|
|||||||
@ -183,7 +183,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-4">
|
||||||
<div class="flex items-center gap-4 mb-6">
|
<div class="flex items-center gap-4 mb-6">
|
||||||
<a href="/master-data" class="btn btn-ghost btn-circle">
|
<a href="/master-data" class="btn btn-ghost btn-circle">
|
||||||
<ArrowLeft class="w-5 h-5" />
|
<ArrowLeft class="w-5 h-5" />
|
||||||
@ -244,7 +244,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if activeTab === 'provinces'}
|
{#if activeTab === 'provinces'}
|
||||||
<div class="space-y-4">
|
<div class="space-y-3">
|
||||||
<!-- Tab Description -->
|
<!-- Tab Description -->
|
||||||
<div class="flex items-start gap-2 text-sm text-gray-600 bg-base-200 p-3 rounded-lg">
|
<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" />
|
<MapPin class="w-4 h-4 mt-0.5 shrink-0" />
|
||||||
@ -302,7 +302,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{:else if activeTab === 'cities'}
|
{:else if activeTab === 'cities'}
|
||||||
<div class="space-y-4">
|
<div class="space-y-3">
|
||||||
<!-- Tab Description -->
|
<!-- Tab Description -->
|
||||||
<div class="flex items-start gap-2 text-sm text-gray-600 bg-base-200 p-3 rounded-lg">
|
<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" />
|
<Building2 class="w-4 h-4 mt-0.5 shrink-0" />
|
||||||
@ -392,7 +392,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{:else if activeTab === 'areas'}
|
{:else if activeTab === 'areas'}
|
||||||
<div class="space-y-4">
|
<div class="space-y-3">
|
||||||
<!-- Tab Description -->
|
<!-- Tab Description -->
|
||||||
<div class="flex items-start gap-2 text-sm text-gray-600 bg-base-200 p-3 rounded-lg">
|
<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" />
|
<Globe class="w-4 h-4 mt-0.5 shrink-0" />
|
||||||
|
|||||||
@ -163,7 +163,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-4">
|
||||||
<div class="flex items-center gap-4 mb-6">
|
<div class="flex items-center gap-4 mb-6">
|
||||||
<a href="/master-data" class="btn btn-ghost btn-circle">
|
<a href="/master-data" class="btn btn-ghost btn-circle">
|
||||||
<ArrowLeft class="w-5 h-5" />
|
<ArrowLeft class="w-5 h-5" />
|
||||||
@ -254,7 +254,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'Add Location' : 'Edit Location'} size="md">
|
<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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="code">
|
<label class="label" for="code">
|
||||||
@ -264,7 +264,7 @@
|
|||||||
<input
|
<input
|
||||||
id="code"
|
id="code"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.Code}
|
bind:value={formData.Code}
|
||||||
placeholder="e.g., BLDG-01, ROOM-101"
|
placeholder="e.g., BLDG-01, ROOM-101"
|
||||||
required
|
required
|
||||||
@ -281,7 +281,7 @@
|
|||||||
<input
|
<input
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.Name}
|
bind:value={formData.Name}
|
||||||
placeholder="e.g., Main Building, Laboratory Room A"
|
placeholder="e.g., Main Building, Laboratory Room A"
|
||||||
required
|
required
|
||||||
@ -300,7 +300,7 @@
|
|||||||
<select
|
<select
|
||||||
id="type"
|
id="type"
|
||||||
name="type"
|
name="type"
|
||||||
class="select select-bordered w-full"
|
class="select select-sm select-bordered w-full"
|
||||||
bind:value={formData.Type}
|
bind:value={formData.Type}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
|
|||||||
@ -154,7 +154,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-4">
|
||||||
<div class="flex items-center gap-4 mb-6">
|
<div class="flex items-center gap-4 mb-6">
|
||||||
<a href="/master-data" class="btn btn-ghost btn-circle">
|
<a href="/master-data" class="btn btn-ghost btn-circle">
|
||||||
<ArrowLeft class="w-5 h-5" />
|
<ArrowLeft class="w-5 h-5" />
|
||||||
@ -260,7 +260,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'Add Occupation' : 'Edit Occupation'} size="md">
|
<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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="occCode">
|
<label class="label" for="occCode">
|
||||||
|
|||||||
@ -143,7 +143,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-4">
|
||||||
<div class="flex items-center gap-4 mb-6">
|
<div class="flex items-center gap-4 mb-6">
|
||||||
<a href="/master-data" class="btn btn-ghost btn-circle">
|
<a href="/master-data" class="btn btn-ghost btn-circle">
|
||||||
<ArrowLeft class="w-5 h-5" />
|
<ArrowLeft class="w-5 h-5" />
|
||||||
@ -252,7 +252,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'Add Specialty' : 'Edit Specialty'} size="md">
|
<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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="specialtyText">
|
<label class="label" for="specialtyText">
|
||||||
@ -262,7 +262,7 @@
|
|||||||
<input
|
<input
|
||||||
id="specialtyText"
|
id="specialtyText"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.SpecialtyText}
|
bind:value={formData.SpecialtyText}
|
||||||
placeholder="e.g., Cardiology, Internal Medicine"
|
placeholder="e.g., Cardiology, Internal Medicine"
|
||||||
required
|
required
|
||||||
@ -278,7 +278,7 @@
|
|||||||
<input
|
<input
|
||||||
id="title"
|
id="title"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.Title}
|
bind:value={formData.Title}
|
||||||
placeholder="e.g., Sp. PD, Sp. A, Sp. And"
|
placeholder="e.g., Sp. PD, Sp. A, Sp. And"
|
||||||
/>
|
/>
|
||||||
@ -324,7 +324,7 @@
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:open={deleteConfirmOpen} title="Confirm Deletion" size="md">
|
<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="flex items-start gap-3">
|
||||||
<div class="bg-error/10 rounded-full p-2">
|
<div class="bg-error/10 rounded-full p-2">
|
||||||
<Trash2 class="w-6 h-6 text-error" />
|
<Trash2 class="w-6 h-6 text-error" />
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount, onDestroy } from 'svelte';
|
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 { fetchDisciplines, fetchDepartments } from '$lib/api/organization.js';
|
||||||
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
|
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
|
||||||
import DataTable from '$lib/components/DataTable.svelte';
|
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 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 modalMode = $state('create'), saving = $state(false), selectedType = $state(''), searchQuery = $state(''), searchInputRef = $state(null);
|
||||||
let deleteModalOpen = $state(false), testToDelete = $state(null), deleting = $state(false);
|
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 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 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 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 disciplineOptions = $derived(disciplines.map(d => ({ value: d.DisciplineID, label: d.DisciplineName })));
|
||||||
const departmentOptions = $derived(departments.map(d => ({ value: d.DepartmentID, label: d.DepartmentName })));
|
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 getVisibleTests() { return tests.filter(t => t.IsActive !== '0' && t.IsActive !== 0); }
|
||||||
function getTestTypeConfig(type) { return testTypeConfig[type] || testTypeConfig.TEST; }
|
function getTestTypeConfig(type) { return testTypeConfig[type] || testTypeConfig.TEST; }
|
||||||
function formatReferenceRange(test) { return '-'; }
|
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 openCreateModal() {
|
||||||
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; }
|
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); }
|
function isDuplicateCode(code, excludeId = null) { return tests.some(test => test.TestSiteCode.toLowerCase() === code.toLowerCase() && test.TestSiteID !== excludeId); }
|
||||||
|
|
||||||
async function handleSave() {
|
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 === '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 === '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; } } }
|
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; }
|
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); }
|
function toggleGroup(testId) { if (expandedGroups.has(testId)) expandedGroups.delete(testId); else expandedGroups.add(testId); expandedGroups = new Set(expandedGroups); }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-4">
|
||||||
<div class="flex items-center gap-4 mb-6">
|
<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>
|
<a href="/master-data" class="btn btn-ghost btn-circle"><ArrowLeft class="w-5 h-5" /></a>
|
||||||
<div class="flex-1">
|
<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" />
|
<Search class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full sm:w-48">
|
<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="">All Types</option>
|
||||||
<option value="TEST">Technical Test</option>
|
<option value="TEST">Technical Test</option>
|
||||||
<option value="PARAM">Parameter</option>
|
<option value="PARAM">Parameter</option>
|
||||||
@ -103,7 +323,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<Modal bind:open={deleteModalOpen} title="Confirm Delete Test" size="sm">
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
import Modal from '$lib/components/Modal.svelte';
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
import BasicInfoForm from './test-modal/BasicInfoForm.svelte';
|
import BasicInfoForm from './test-modal/BasicInfoForm.svelte';
|
||||||
import ReferenceRangeSection from './test-modal/ReferenceRangeSection.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
|
* @typedef {Object} Props
|
||||||
@ -10,9 +12,11 @@
|
|||||||
* @property {Object} formData - Form data object
|
* @property {Object} formData - Form data object
|
||||||
* @property {boolean} canHaveRefRange - Whether test can have reference ranges
|
* @property {boolean} canHaveRefRange - Whether test can have reference ranges
|
||||||
* @property {boolean} canHaveFormula - Whether test can have a formula
|
* @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}>} disciplineOptions - Discipline dropdown options
|
||||||
* @property {Array<{value: string, label: string}>} departmentOptions - Department 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
|
* @property {boolean} [saving] - Whether save is in progress
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -23,9 +27,11 @@
|
|||||||
formData = $bindable({}),
|
formData = $bindable({}),
|
||||||
canHaveRefRange = false,
|
canHaveRefRange = false,
|
||||||
canHaveFormula = false,
|
canHaveFormula = false,
|
||||||
canHaveUnit = false,
|
canHaveTechnical = false,
|
||||||
|
isGroupTest = false,
|
||||||
disciplineOptions = [],
|
disciplineOptions = [],
|
||||||
departmentOptions = [],
|
departmentOptions = [],
|
||||||
|
availableTests = [],
|
||||||
saving = false,
|
saving = false,
|
||||||
onsave = () => {},
|
onsave = () => {},
|
||||||
oncancel = () => {},
|
oncancel = () => {},
|
||||||
@ -50,9 +56,20 @@
|
|||||||
activeTab = 'basic';
|
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>
|
</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 -->
|
<!-- Tabs -->
|
||||||
<div class="tabs tabs-bordered mb-4">
|
<div class="tabs tabs-bordered mb-4">
|
||||||
<button
|
<button
|
||||||
@ -62,6 +79,15 @@
|
|||||||
>
|
>
|
||||||
Basic Information
|
Basic Information
|
||||||
</button>
|
</button>
|
||||||
|
{#if canHaveTechnical}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tab tab-lg {activeTab === 'technical' ? 'tab-active' : ''}"
|
||||||
|
onclick={() => activeTab = 'technical'}
|
||||||
|
>
|
||||||
|
Technical
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{#if canHaveRefRange}
|
{#if canHaveRefRange}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -69,8 +95,20 @@
|
|||||||
onclick={() => activeTab = 'refrange'}
|
onclick={() => activeTab = 'refrange'}
|
||||||
>
|
>
|
||||||
Reference Range
|
Reference Range
|
||||||
{#if formData.refnum?.length > 0 || formData.reftxt?.length > 0}
|
{#if getRefRangeCount() > 0}
|
||||||
<span class="badge badge-sm badge-primary ml-2">{(formData.refnum?.length || 0) + (formData.reftxt?.length || 0)}</span>
|
<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}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
@ -80,16 +118,26 @@
|
|||||||
<BasicInfoForm
|
<BasicInfoForm
|
||||||
bind:formData
|
bind:formData
|
||||||
{canHaveFormula}
|
{canHaveFormula}
|
||||||
{canHaveUnit}
|
|
||||||
{disciplineOptions}
|
{disciplineOptions}
|
||||||
{departmentOptions}
|
{departmentOptions}
|
||||||
onsave={handleSave}
|
onsave={handleSave}
|
||||||
/>
|
/>
|
||||||
|
{:else if activeTab === 'technical' && canHaveTechnical}
|
||||||
|
<TechnicalConfigForm
|
||||||
|
bind:formData
|
||||||
|
{onupdateFormData}
|
||||||
|
/>
|
||||||
{:else if activeTab === 'refrange' && canHaveRefRange}
|
{:else if activeTab === 'refrange' && canHaveRefRange}
|
||||||
<ReferenceRangeSection
|
<ReferenceRangeSection
|
||||||
bind:formData
|
bind:formData
|
||||||
{onupdateFormData}
|
{onupdateFormData}
|
||||||
/>
|
/>
|
||||||
|
{:else if activeTab === 'members' && isGroupTest}
|
||||||
|
<GroupMembersTab
|
||||||
|
bind:formData
|
||||||
|
{availableTests}
|
||||||
|
{onupdateFormData}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#snippet footer()}
|
{#snippet footer()}
|
||||||
|
|||||||
@ -30,7 +30,9 @@ export function createNumRef() {
|
|||||||
AgeStart: 0,
|
AgeStart: 0,
|
||||||
AgeEnd: 120,
|
AgeEnd: 120,
|
||||||
Flag: 'N',
|
Flag: 'N',
|
||||||
Interpretation: 'Normal'
|
Interpretation: 'Normal',
|
||||||
|
SpcType: '',
|
||||||
|
Criteria: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +46,9 @@ export function createTholdRef() {
|
|||||||
AgeStart: 0,
|
AgeStart: 0,
|
||||||
AgeEnd: 120,
|
AgeEnd: 120,
|
||||||
Flag: 'N',
|
Flag: 'N',
|
||||||
Interpretation: 'Normal'
|
Interpretation: 'Normal',
|
||||||
|
SpcType: '',
|
||||||
|
Criteria: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +58,9 @@ export function createTextRef() {
|
|||||||
AgeStart: 0,
|
AgeStart: 0,
|
||||||
AgeEnd: 120,
|
AgeEnd: 120,
|
||||||
RefTxt: '',
|
RefTxt: '',
|
||||||
Flag: 'N'
|
Flag: 'N',
|
||||||
|
SpcType: '',
|
||||||
|
Criteria: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,8 +69,10 @@ export function createVsetRef() {
|
|||||||
Sex: '2',
|
Sex: '2',
|
||||||
AgeStart: 0,
|
AgeStart: 0,
|
||||||
AgeEnd: 120,
|
AgeEnd: 120,
|
||||||
valueset: '',
|
RefTxt: '',
|
||||||
Flag: 'N'
|
Flag: 'N',
|
||||||
|
SpcType: '',
|
||||||
|
Criteria: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
* @typedef {Object} Props
|
* @typedef {Object} Props
|
||||||
* @property {Object} formData - Form data object
|
* @property {Object} formData - Form data object
|
||||||
* @property {boolean} canHaveFormula - Whether test can have a formula
|
* @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}>} disciplineOptions - Discipline dropdown options
|
||||||
* @property {Array<{value: string, label: string}>} departmentOptions - Department dropdown options
|
* @property {Array<{value: string, label: string}>} departmentOptions - Department dropdown options
|
||||||
* @property {() => void} onsave - Save handler
|
* @property {() => void} onsave - Save handler
|
||||||
@ -16,7 +15,6 @@
|
|||||||
let {
|
let {
|
||||||
formData = $bindable({}),
|
formData = $bindable({}),
|
||||||
canHaveFormula = false,
|
canHaveFormula = false,
|
||||||
canHaveUnit = false,
|
|
||||||
disciplineOptions = [],
|
disciplineOptions = [],
|
||||||
departmentOptions = [],
|
departmentOptions = [],
|
||||||
onsave = () => {}
|
onsave = () => {}
|
||||||
@ -28,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="space-y-5" onsubmit={handleSubmit}>
|
<form class="space-y-3" onsubmit={handleSubmit}>
|
||||||
<!-- Basic Info -->
|
<!-- Basic Info -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
@ -39,7 +37,7 @@
|
|||||||
<input
|
<input
|
||||||
id="testCode"
|
id="testCode"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.TestSiteCode}
|
bind:value={formData.TestSiteCode}
|
||||||
placeholder="e.g., GLU"
|
placeholder="e.g., GLU"
|
||||||
required
|
required
|
||||||
@ -53,7 +51,7 @@
|
|||||||
<input
|
<input
|
||||||
id="testName"
|
id="testName"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.TestSiteName}
|
bind:value={formData.TestSiteName}
|
||||||
placeholder="e.g., Glucose"
|
placeholder="e.g., Glucose"
|
||||||
required
|
required
|
||||||
@ -70,7 +68,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="testType"
|
id="testType"
|
||||||
class="select select-bordered w-full"
|
class="select select-sm select-bordered w-full"
|
||||||
bind:value={formData.TestType}
|
bind:value={formData.TestType}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
@ -88,7 +86,7 @@
|
|||||||
<input
|
<input
|
||||||
id="seqScr"
|
id="seqScr"
|
||||||
type="number"
|
type="number"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.SeqScr}
|
bind:value={formData.SeqScr}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
/>
|
/>
|
||||||
@ -114,48 +112,48 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Type-specific fields -->
|
<!-- Type-specific fields -->
|
||||||
{#if canHaveUnit}
|
{#if canHaveFormula}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-1 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">
|
<div class="form-control">
|
||||||
<label class="label" for="unit">
|
<label class="label" for="formula">
|
||||||
<span class="label-text font-medium">Unit</span>
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="unit"
|
id="formula"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.Unit}
|
bind:value={formData.Formula}
|
||||||
placeholder="e.g., mg/dL"
|
placeholder="e.g., BUN / Creatinine"
|
||||||
|
required={canHaveFormula}
|
||||||
/>
|
/>
|
||||||
|
<span class="label-text-alt text-gray-500">Use test codes with operators: +, -, *, /</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Report Sequence and Visibility -->
|
<!-- Description -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<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">
|
<div class="form-control">
|
||||||
<label class="label" for="seqRpt">
|
<label class="label" for="seqRpt">
|
||||||
<span class="label-text font-medium">Report Sequence</span>
|
<span class="label-text font-medium">Report Sequence</span>
|
||||||
@ -163,7 +161,7 @@
|
|||||||
<input
|
<input
|
||||||
id="seqRpt"
|
id="seqRpt"
|
||||||
type="number"
|
type="number"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.SeqRpt}
|
bind:value={formData.SeqRpt}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
/>
|
/>
|
||||||
@ -181,5 +179,12 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</form>
|
</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>
|
<script>
|
||||||
import { PlusCircle, Calculator, X, ChevronDown, ChevronUp, Info } from 'lucide-svelte';
|
import { PlusCircle, Calculator, X, ChevronDown, ChevronUp, Info, Beaker, Filter } from 'lucide-svelte';
|
||||||
import { signOptions, flagOptions, sexOptions, createNumRef } from '../referenceRange.js';
|
import { flagOptions, sexOptions, createNumRef } from '../referenceRange.js';
|
||||||
|
import { fetchValueSetByKey } from '$lib/api/valuesets.js';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Props
|
* @typedef {Object} Props
|
||||||
@ -16,6 +18,19 @@
|
|||||||
|
|
||||||
// Track expanded state for each range's optional fields
|
// Track expanded state for each range's optional fields
|
||||||
let expandedRanges = $state({});
|
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() {
|
function addRefRange() {
|
||||||
const newRef = createNumRef();
|
const newRef = createNumRef();
|
||||||
@ -24,7 +39,7 @@
|
|||||||
newRef.Flag = 'N'; // Normal
|
newRef.Flag = 'N'; // Normal
|
||||||
onupdateRefnum([...refnum, newRef]);
|
onupdateRefnum([...refnum, newRef]);
|
||||||
// Auto-expand the new range
|
// Auto-expand the new range
|
||||||
expandedRanges[refnum.length] = { age: false, interpretation: false };
|
expandedRanges[refnum.length] = { age: false, interpretation: false, specimen: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeRefRange(index) {
|
function removeRefRange(index) {
|
||||||
@ -40,8 +55,8 @@
|
|||||||
expandedRanges[index] = { ...expandedRanges[index], interpretation: !expandedRanges[index]?.interpretation };
|
expandedRanges[index] = { ...expandedRanges[index], interpretation: !expandedRanges[index]?.interpretation };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSignLabel(value) {
|
function toggleSpecimenExpand(index) {
|
||||||
return signOptions.find(o => o.value === value)?.label || value;
|
expandedRanges[index] = { ...expandedRanges[index], specimen: !expandedRanges[index]?.specimen };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSexLabel(value) {
|
function getSexLabel(value) {
|
||||||
@ -72,11 +87,11 @@
|
|||||||
|
|
||||||
let rangeText = '';
|
let rangeText = '';
|
||||||
if (ref.Low !== null && ref.High !== null) {
|
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) {
|
} else if (ref.Low !== null) {
|
||||||
rangeText = `${getSignLabel(ref.LowSign)}${ref.Low}`;
|
rangeText = `${ref.Low}`;
|
||||||
} else if (ref.High !== null) {
|
} else if (ref.High !== null) {
|
||||||
rangeText = `${getSignLabel(ref.HighSign)}${ref.High}`;
|
rangeText = `${ref.High}`;
|
||||||
} else {
|
} else {
|
||||||
rangeText = 'Not set';
|
rangeText = 'Not set';
|
||||||
}
|
}
|
||||||
@ -85,7 +100,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-3">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Calculator class="w-5 h-5 text-primary" />
|
<Calculator class="w-5 h-5 text-primary" />
|
||||||
@ -133,38 +148,24 @@
|
|||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<span class="label-text text-xs mb-1 font-medium">Lower Bound</span>
|
<span class="label-text text-xs mb-1 font-medium">Lower Bound</span>
|
||||||
<div class="flex gap-2">
|
<input
|
||||||
<select class="select select-sm select-bordered w-16" bind:value={ref.LowSign}>
|
type="number"
|
||||||
{#each signOptions as option (option.value)}
|
step="0.01"
|
||||||
<option value={option.value}>{option.label}</option>
|
class="input input-sm input-bordered w-full"
|
||||||
{/each}
|
bind:value={ref.Low}
|
||||||
</select>
|
placeholder="70"
|
||||||
<input
|
/>
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
class="input input-sm input-bordered flex-1"
|
|
||||||
bind:value={ref.Low}
|
|
||||||
placeholder="70"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<span class="label-text text-xs mb-1 font-medium">Upper Bound</span>
|
<span class="label-text text-xs mb-1 font-medium">Upper Bound</span>
|
||||||
<div class="flex gap-2">
|
<input
|
||||||
<select class="select select-sm select-bordered w-16" bind:value={ref.HighSign}>
|
type="number"
|
||||||
{#each signOptions as option (option.value)}
|
step="0.01"
|
||||||
<option value={option.value}>{option.label}</option>
|
class="input input-sm input-bordered w-full"
|
||||||
{/each}
|
bind:value={ref.High}
|
||||||
</select>
|
placeholder="100"
|
||||||
<input
|
/>
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
class="input input-sm input-bordered flex-1"
|
|
||||||
bind:value={ref.High}
|
|
||||||
placeholder="100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -228,7 +229,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Expandable: Interpretation -->
|
<!-- 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
|
<button
|
||||||
type="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"
|
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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@ -19,6 +19,69 @@
|
|||||||
onupdateFormData = () => {}
|
onupdateFormData = () => {}
|
||||||
} = $props();
|
} = $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) {
|
function updateRefRangeType(type) {
|
||||||
let newFormData = {
|
let newFormData = {
|
||||||
...formData,
|
...formData,
|
||||||
@ -60,7 +123,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-3">
|
||||||
<!-- Reference Range Type Selection -->
|
<!-- Reference Range Type Selection -->
|
||||||
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
<div class="bg-base-100 rounded-lg border border-base-200 p-4">
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
@ -159,21 +222,21 @@
|
|||||||
|
|
||||||
<!-- Numeric Reference Ranges -->
|
<!-- Numeric Reference Ranges -->
|
||||||
{#if formData.refRangeType === 'num'}
|
{#if formData.refRangeType === 'num'}
|
||||||
<NumericRefRange refnum={formData.refnum} onupdateRefnum={updateRefnum} />
|
<NumericRefRange refnum={normalizedRefnum} onupdateRefnum={updateRefnum} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Threshold Reference Ranges -->
|
<!-- Threshold Reference Ranges -->
|
||||||
{#if formData.refRangeType === 'thold'}
|
{#if formData.refRangeType === 'thold'}
|
||||||
<ThresholdRefRange refthold={formData.refthold} onupdateRefthold={updateRefthold} />
|
<ThresholdRefRange refthold={normalizedRefthold} onupdateRefthold={updateRefthold} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Text Reference Ranges -->
|
<!-- Text Reference Ranges -->
|
||||||
{#if formData.refRangeType === 'text'}
|
{#if formData.refRangeType === 'text'}
|
||||||
<TextRefRange reftxt={formData.reftxt} onupdateReftxt={updateReftxt} />
|
<TextRefRange reftxt={normalizedReftxt} onupdateReftxt={updateReftxt} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Value Set Reference Ranges -->
|
<!-- Value Set Reference Ranges -->
|
||||||
{#if formData.refRangeType === 'vset'}
|
{#if formData.refRangeType === 'vset'}
|
||||||
<ValueSetRefRange refvset={formData.refvset} onupdateRefvset={updateRefvset} />
|
<ValueSetRefRange refvset={normalizedRefvset} onupdateRefvset={updateRefvset} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
<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 { sexOptions, createTextRef } from '../referenceRange.js';
|
||||||
|
import { fetchValueSetByKey } from '$lib/api/valuesets.js';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Props
|
* @typedef {Object} Props
|
||||||
@ -14,17 +16,38 @@
|
|||||||
onupdateReftxt = () => {}
|
onupdateReftxt = () => {}
|
||||||
} = $props();
|
} = $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() {
|
function addRefRange() {
|
||||||
const newRef = createTextRef();
|
const newRef = createTextRef();
|
||||||
onupdateReftxt([...reftxt, newRef]);
|
onupdateReftxt([...reftxt, newRef]);
|
||||||
|
expandedRanges[reftxt.length] = { specimen: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeRefRange(index) {
|
function removeRefRange(index) {
|
||||||
onupdateReftxt(reftxt.filter((_, i) => i !== index));
|
onupdateReftxt(reftxt.filter((_, i) => i !== index));
|
||||||
|
delete expandedRanges[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSpecimenExpand(index) {
|
||||||
|
expandedRanges[index] = { ...expandedRanges[index], specimen: !expandedRanges[index]?.specimen };
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-3">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<FileText class="w-5 h-5 text-primary" />
|
<FileText class="w-5 h-5 text-primary" />
|
||||||
@ -91,6 +114,59 @@
|
|||||||
<span class="label-text text-xs mb-1">Reference Text</span>
|
<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>
|
<textarea class="textarea textarea-bordered w-full" rows="2" bind:value={ref.RefTxt} placeholder="e.g., Negative for glucose"></textarea>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<script>
|
<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 { signOptions, flagOptions, sexOptions, createTholdRef } from '../referenceRange.js';
|
||||||
|
import { fetchValueSetByKey } from '$lib/api/valuesets.js';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Props
|
* @typedef {Object} Props
|
||||||
@ -14,17 +16,38 @@
|
|||||||
onupdateRefthold = () => {}
|
onupdateRefthold = () => {}
|
||||||
} = $props();
|
} = $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() {
|
function addRefRange() {
|
||||||
const newRef = createTholdRef();
|
const newRef = createTholdRef();
|
||||||
onupdateRefthold([...refthold, newRef]);
|
onupdateRefthold([...refthold, newRef]);
|
||||||
|
expandedRanges[refthold.length] = { specimen: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeRefRange(index) {
|
function removeRefRange(index) {
|
||||||
onupdateRefthold(refthold.filter((_, i) => i !== index));
|
onupdateRefthold(refthold.filter((_, i) => i !== index));
|
||||||
|
delete expandedRanges[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSpecimenExpand(index) {
|
||||||
|
expandedRanges[index] = { ...expandedRanges[index], specimen: !expandedRanges[index]?.specimen };
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-3">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Ruler class="w-5 h-5 text-primary" />
|
<Ruler class="w-5 h-5 text-primary" />
|
||||||
@ -118,6 +141,59 @@
|
|||||||
<span class="label-text text-xs mb-1">Interpretation</span>
|
<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" />
|
<input type="text" class="input input-sm input-bordered w-full" bind:value={ref.Interpretation} placeholder="e.g., Alert threshold" />
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<script>
|
<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 { sexOptions, createVsetRef } from '../referenceRange.js';
|
||||||
|
import { fetchValueSetByKey } from '$lib/api/valuesets.js';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Props
|
* @typedef {Object} Props
|
||||||
@ -14,17 +16,38 @@
|
|||||||
onupdateRefvset = () => {}
|
onupdateRefvset = () => {}
|
||||||
} = $props();
|
} = $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() {
|
function addRefRange() {
|
||||||
const newRef = createVsetRef();
|
const newRef = createVsetRef();
|
||||||
onupdateRefvset([...refvset, newRef]);
|
onupdateRefvset([...refvset, newRef]);
|
||||||
|
expandedRanges[refvset.length] = { specimen: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeRefRange(index) {
|
function removeRefRange(index) {
|
||||||
onupdateRefvset(refvset.filter((_, i) => i !== index));
|
onupdateRefvset(refvset.filter((_, i) => i !== index));
|
||||||
|
delete expandedRanges[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSpecimenExpand(index) {
|
||||||
|
expandedRanges[index] = { ...expandedRanges[index], specimen: !expandedRanges[index]?.specimen };
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-3">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Box class="w-5 h-5 text-primary" />
|
<Box class="w-5 h-5 text-primary" />
|
||||||
@ -89,9 +112,62 @@
|
|||||||
|
|
||||||
<div class="form-control mt-3">
|
<div class="form-control mt-3">
|
||||||
<span class="label-text text-xs mb-1">Value Set</span>
|
<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>
|
<span class="label-text-alt text-gray-500 mt-1">Comma-separated list of allowed values</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@ -210,7 +210,7 @@
|
|||||||
<p class="text-gray-500 mt-4">Loading ValueSet details...</p>
|
<p class="text-gray-500 mt-4">Loading ValueSet details...</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if selectedValueSet}
|
{:else if selectedValueSet}
|
||||||
<div class="space-y-6">
|
<div class="space-y-3">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="border-b border-base-300 pb-4">
|
<div class="border-b border-base-300 pb-4">
|
||||||
<h2 class="text-2xl font-bold text-gray-800">{selectedValueSet.Name}</h2>
|
<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>
|
<p class="text-sm">This patient has no visit records.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-4">
|
<div class="space-y-3">
|
||||||
{#each visits as visit}
|
{#each visits as visit}
|
||||||
<div class="card bg-base-100 shadow border border-base-200 hover:shadow-md transition-shadow">
|
<div class="card bg-base-100 shadow border border-base-200 hover:shadow-md transition-shadow">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
@ -512,7 +512,7 @@
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:open={dischargeModalOpen} title="Discharge Patient" size="sm">
|
<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>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>
|
<p class="text-sm text-gray-600">Visit ID: {visitToDischarge?.PVID || visitToDischarge?.InternalPVID}</p>
|
||||||
|
|
||||||
@ -524,7 +524,7 @@
|
|||||||
<input
|
<input
|
||||||
id="dischargeDate"
|
id="dischargeDate"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={dischargeDate}
|
bind:value={dischargeDate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -267,7 +267,7 @@
|
|||||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<form class="space-y-6" onsubmit={(e) => e.preventDefault()}>
|
<form class="space-y-3" onsubmit={(e) => e.preventDefault()}>
|
||||||
<!-- DaisyUI Tabs -->
|
<!-- DaisyUI Tabs -->
|
||||||
<div class="tabs tabs-bordered">
|
<div class="tabs tabs-bordered">
|
||||||
<button
|
<button
|
||||||
@ -290,7 +290,7 @@
|
|||||||
|
|
||||||
<!-- Personal Info Tab (Basic + Demographics) -->
|
<!-- Personal Info Tab (Basic + Demographics) -->
|
||||||
{#if activeTab === 'personal'}
|
{#if activeTab === 'personal'}
|
||||||
<div class="space-y-6">
|
<div class="space-y-3">
|
||||||
<!-- Basic Info Section -->
|
<!-- Basic Info Section -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="font-medium text-gray-700 mb-4">Basic Information</h4>
|
<h4 class="font-medium text-gray-700 mb-4">Basic Information</h4>
|
||||||
@ -303,7 +303,7 @@
|
|||||||
<input
|
<input
|
||||||
id="patientId"
|
id="patientId"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
class:input-error={formErrors.PatientID}
|
class:input-error={formErrors.PatientID}
|
||||||
bind:value={formData.PatientID}
|
bind:value={formData.PatientID}
|
||||||
placeholder="Enter patient ID"
|
placeholder="Enter patient ID"
|
||||||
@ -332,7 +332,7 @@
|
|||||||
<input
|
<input
|
||||||
id="nameFirst"
|
id="nameFirst"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
class:input-error={formErrors.NameFirst}
|
class:input-error={formErrors.NameFirst}
|
||||||
bind:value={formData.NameFirst}
|
bind:value={formData.NameFirst}
|
||||||
placeholder="Enter first name"
|
placeholder="Enter first name"
|
||||||
@ -349,7 +349,7 @@
|
|||||||
<input
|
<input
|
||||||
id="nameMiddle"
|
id="nameMiddle"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.NameMiddle}
|
bind:value={formData.NameMiddle}
|
||||||
placeholder="Enter middle name"
|
placeholder="Enter middle name"
|
||||||
/>
|
/>
|
||||||
@ -362,7 +362,7 @@
|
|||||||
<input
|
<input
|
||||||
id="nameLast"
|
id="nameLast"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.NameLast}
|
bind:value={formData.NameLast}
|
||||||
placeholder="Enter last name"
|
placeholder="Enter last name"
|
||||||
/>
|
/>
|
||||||
@ -377,7 +377,7 @@
|
|||||||
<input
|
<input
|
||||||
id="nameMaiden"
|
id="nameMaiden"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.NameMaiden}
|
bind:value={formData.NameMaiden}
|
||||||
placeholder="Enter maiden name"
|
placeholder="Enter maiden name"
|
||||||
/>
|
/>
|
||||||
@ -390,7 +390,7 @@
|
|||||||
<input
|
<input
|
||||||
id="suffix"
|
id="suffix"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.Suffix}
|
bind:value={formData.Suffix}
|
||||||
placeholder="Enter suffix (e.g., Jr, Sr, III)"
|
placeholder="Enter suffix (e.g., Jr, Sr, III)"
|
||||||
/>
|
/>
|
||||||
@ -416,7 +416,7 @@
|
|||||||
<input
|
<input
|
||||||
id="birthdate"
|
id="birthdate"
|
||||||
type="date"
|
type="date"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
class:input-error={formErrors.Birthdate}
|
class:input-error={formErrors.Birthdate}
|
||||||
bind:value={formData.Birthdate}
|
bind:value={formData.Birthdate}
|
||||||
/>
|
/>
|
||||||
@ -432,7 +432,7 @@
|
|||||||
<input
|
<input
|
||||||
id="placeOfBirth"
|
id="placeOfBirth"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.PlaceOfBirth}
|
bind:value={formData.PlaceOfBirth}
|
||||||
placeholder="Enter place of birth"
|
placeholder="Enter place of birth"
|
||||||
/>
|
/>
|
||||||
@ -485,7 +485,7 @@
|
|||||||
<input
|
<input
|
||||||
id="citizenship"
|
id="citizenship"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.Citizenship}
|
bind:value={formData.Citizenship}
|
||||||
placeholder="Enter citizenship"
|
placeholder="Enter citizenship"
|
||||||
/>
|
/>
|
||||||
@ -497,7 +497,7 @@
|
|||||||
|
|
||||||
<!-- Location & Contact Tab -->
|
<!-- Location & Contact Tab -->
|
||||||
{#if activeTab === 'location'}
|
{#if activeTab === 'location'}
|
||||||
<div class="space-y-6">
|
<div class="space-y-3">
|
||||||
<!-- Address Section -->
|
<!-- Address Section -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="font-medium text-gray-700 mb-4">Address Information</h4>
|
<h4 class="font-medium text-gray-700 mb-4">Address Information</h4>
|
||||||
@ -509,7 +509,7 @@
|
|||||||
<input
|
<input
|
||||||
id="street1"
|
id="street1"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.Street_1}
|
bind:value={formData.Street_1}
|
||||||
placeholder="Enter street address"
|
placeholder="Enter street address"
|
||||||
/>
|
/>
|
||||||
@ -522,7 +522,7 @@
|
|||||||
<input
|
<input
|
||||||
id="street2"
|
id="street2"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.Street_2}
|
bind:value={formData.Street_2}
|
||||||
placeholder="Enter street address line 2"
|
placeholder="Enter street address line 2"
|
||||||
/>
|
/>
|
||||||
@ -535,7 +535,7 @@
|
|||||||
<input
|
<input
|
||||||
id="street3"
|
id="street3"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.Street_3}
|
bind:value={formData.Street_3}
|
||||||
placeholder="Enter street address line 3"
|
placeholder="Enter street address line 3"
|
||||||
/>
|
/>
|
||||||
@ -566,7 +566,7 @@
|
|||||||
<input
|
<input
|
||||||
id="zip"
|
id="zip"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.ZIP}
|
bind:value={formData.ZIP}
|
||||||
placeholder="Enter ZIP code"
|
placeholder="Enter ZIP code"
|
||||||
/>
|
/>
|
||||||
@ -596,7 +596,7 @@
|
|||||||
<input
|
<input
|
||||||
id="phone"
|
id="phone"
|
||||||
type="tel"
|
type="tel"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.Phone}
|
bind:value={formData.Phone}
|
||||||
placeholder="Enter phone number"
|
placeholder="Enter phone number"
|
||||||
/>
|
/>
|
||||||
@ -609,7 +609,7 @@
|
|||||||
<input
|
<input
|
||||||
id="mobilePhone"
|
id="mobilePhone"
|
||||||
type="tel"
|
type="tel"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.MobilePhone}
|
bind:value={formData.MobilePhone}
|
||||||
placeholder="Enter mobile number"
|
placeholder="Enter mobile number"
|
||||||
/>
|
/>
|
||||||
@ -624,7 +624,7 @@
|
|||||||
<input
|
<input
|
||||||
id="email1"
|
id="email1"
|
||||||
type="email"
|
type="email"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.EmailAddress1}
|
bind:value={formData.EmailAddress1}
|
||||||
placeholder="Enter email address"
|
placeholder="Enter email address"
|
||||||
/>
|
/>
|
||||||
@ -637,7 +637,7 @@
|
|||||||
<input
|
<input
|
||||||
id="email2"
|
id="email2"
|
||||||
type="email"
|
type="email"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.EmailAddress2}
|
bind:value={formData.EmailAddress2}
|
||||||
placeholder="Enter secondary email"
|
placeholder="Enter secondary email"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -197,7 +197,7 @@
|
|||||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<form class="space-y-6" onsubmit={(e) => e.preventDefault()}>
|
<form class="space-y-3" onsubmit={(e) => e.preventDefault()}>
|
||||||
<!-- Patient Info Display -->
|
<!-- Patient Info Display -->
|
||||||
{#if patient}
|
{#if patient}
|
||||||
<div class="p-4 bg-base-200 rounded-lg">
|
<div class="p-4 bg-base-200 rounded-lg">
|
||||||
@ -233,7 +233,7 @@
|
|||||||
|
|
||||||
<!-- Tab: Visit Info -->
|
<!-- Tab: Visit Info -->
|
||||||
{#if activeTab === '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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{#if isEdit}
|
{#if isEdit}
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
@ -243,7 +243,7 @@
|
|||||||
<input
|
<input
|
||||||
id="pvid"
|
id="pvid"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.PVID}
|
bind:value={formData.PVID}
|
||||||
placeholder="Enter visit ID"
|
placeholder="Enter visit ID"
|
||||||
/>
|
/>
|
||||||
@ -258,7 +258,7 @@
|
|||||||
<input
|
<input
|
||||||
id="patientId"
|
id="patientId"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
class:input-error={formErrors.PatientID}
|
class:input-error={formErrors.PatientID}
|
||||||
bind:value={formData.PatientID}
|
bind:value={formData.PatientID}
|
||||||
placeholder="Enter patient ID"
|
placeholder="Enter patient ID"
|
||||||
@ -277,7 +277,7 @@
|
|||||||
<input
|
<input
|
||||||
id="visitDate"
|
id="visitDate"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
class:input-error={formErrors.PVCreateDate}
|
class:input-error={formErrors.PVCreateDate}
|
||||||
bind:value={formData.PVCreateDate}
|
bind:value={formData.PVCreateDate}
|
||||||
/>
|
/>
|
||||||
@ -350,7 +350,7 @@
|
|||||||
|
|
||||||
<!-- Tab: Diagnosis & Status -->
|
<!-- Tab: Diagnosis & Status -->
|
||||||
{#if activeTab === 'diagnosis'}
|
{#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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="diagCode">
|
<label class="label" for="diagCode">
|
||||||
@ -359,7 +359,7 @@
|
|||||||
<input
|
<input
|
||||||
id="diagCode"
|
id="diagCode"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.DiagCode}
|
bind:value={formData.DiagCode}
|
||||||
placeholder="Enter diagnosis code"
|
placeholder="Enter diagnosis code"
|
||||||
/>
|
/>
|
||||||
@ -389,7 +389,7 @@
|
|||||||
<input
|
<input
|
||||||
id="endDate"
|
id="endDate"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.EndDate}
|
bind:value={formData.EndDate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -401,7 +401,7 @@
|
|||||||
<input
|
<input
|
||||||
id="archivedDate"
|
id="archivedDate"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.ArchivedDate}
|
bind:value={formData.ArchivedDate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -284,7 +284,7 @@
|
|||||||
<p class="text-sm">This patient has no visit records.</p>
|
<p class="text-sm">This patient has no visit records.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-4">
|
<div class="space-y-3">
|
||||||
{#each visits as visit}
|
{#each visits as visit}
|
||||||
<div class="card bg-base-100 shadow border border-base-200 hover:shadow-md transition-shadow">
|
<div class="card bg-base-100 shadow border border-base-200 hover:shadow-md transition-shadow">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
@ -383,7 +383,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="space-y-6" onsubmit={(e) => e.preventDefault()}>
|
<form class="space-y-3" onsubmit={(e) => e.preventDefault()}>
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="tabs tabs-bordered">
|
<div class="tabs tabs-bordered">
|
||||||
<button
|
<button
|
||||||
@ -406,7 +406,7 @@
|
|||||||
|
|
||||||
<!-- Tab: Visit Info -->
|
<!-- Tab: Visit Info -->
|
||||||
{#if activeTab === '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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="patientId">
|
<label class="label" for="patientId">
|
||||||
@ -416,7 +416,7 @@
|
|||||||
<input
|
<input
|
||||||
id="patientId"
|
id="patientId"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
class:input-error={formErrors.PatientID}
|
class:input-error={formErrors.PatientID}
|
||||||
bind:value={formData.PatientID}
|
bind:value={formData.PatientID}
|
||||||
placeholder="Enter patient ID"
|
placeholder="Enter patient ID"
|
||||||
@ -435,7 +435,7 @@
|
|||||||
<input
|
<input
|
||||||
id="visitDate"
|
id="visitDate"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
class:input-error={formErrors.PVCreateDate}
|
class:input-error={formErrors.PVCreateDate}
|
||||||
bind:value={formData.PVCreateDate}
|
bind:value={formData.PVCreateDate}
|
||||||
/>
|
/>
|
||||||
@ -508,7 +508,7 @@
|
|||||||
|
|
||||||
<!-- Tab: Diagnosis & Status -->
|
<!-- Tab: Diagnosis & Status -->
|
||||||
{#if activeTab === 'diagnosis'}
|
{#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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="diagCode">
|
<label class="label" for="diagCode">
|
||||||
@ -517,7 +517,7 @@
|
|||||||
<input
|
<input
|
||||||
id="diagCode"
|
id="diagCode"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.DiagCode}
|
bind:value={formData.DiagCode}
|
||||||
placeholder="Enter diagnosis code"
|
placeholder="Enter diagnosis code"
|
||||||
/>
|
/>
|
||||||
@ -547,7 +547,7 @@
|
|||||||
<input
|
<input
|
||||||
id="endDate"
|
id="endDate"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.EndDate}
|
bind:value={formData.EndDate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -559,7 +559,7 @@
|
|||||||
<input
|
<input
|
||||||
id="archivedDate"
|
id="archivedDate"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
class="input input-bordered w-full"
|
class="input input-sm input-bordered w-full"
|
||||||
bind:value={formData.ArchivedDate}
|
bind:value={formData.ArchivedDate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user