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:
mahdahar 2026-02-18 16:31:20 +07:00
parent f0f5889df4
commit 8d77370357
40 changed files with 2161 additions and 1838 deletions

640
docs/clqms_database.dbml Normal file
View 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
}

View File

@ -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`

View File

@ -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;

View File

@ -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);
} }

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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} {#if isOpen}
<span class="menu-text whitespace-nowrap overflow-hidden">Master Data</span> <span class="nav-text">Master Data</span>
{/if} <ChevronDown size={16} class="chevron {masterDataExpanded ? 'expanded' : ''}" />
</div>
{#if isOpen}
<ChevronDown class="w-4 h-4 flex-shrink-0 transition-transform duration-200 {masterDataExpanded ? 'rotate-180' : ''}" />
{/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} {#if isOpen}
<span class="menu-text whitespace-nowrap overflow-hidden">Laboratory</span> <span class="nav-text">Laboratory</span>
{/if} <ChevronDown size={16} class="chevron {laboratoryExpanded ? 'expanded' : ''}" />
</div>
{#if isOpen}
<ChevronDown class="w-4 h-4 flex-shrink-0 transition-transform duration-200 {laboratoryExpanded ? 'rotate-180' : ''}" />
{/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} {#if isOpen}
<span class="menu-text whitespace-nowrap overflow-hidden">Administration</span> <span class="nav-text">Administration</span>
{/if} <ChevronDown size={16} class="chevron {administrationExpanded ? 'expanded' : ''}" />
</div>
{#if isOpen}
<ChevronDown class="w-4 h-4 flex-shrink-0 transition-transform duration-200 {administrationExpanded ? 'rotate-180' : ''}" />
{/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 {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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' }
];

View File

@ -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>

View File

@ -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"
/> />

View File

@ -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"
/> />

View File

@ -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)}

View File

@ -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" />

View File

@ -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
> >

View File

@ -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">

View File

@ -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" />

View File

@ -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">

View File

@ -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()}

View File

@ -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: ''
}; };
} }

View File

@ -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,9 +112,8 @@
</div> </div>
<!-- Type-specific fields --> <!-- Type-specific fields -->
{#if canHaveUnit}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{#if canHaveFormula} {#if canHaveFormula}
<div class="grid grid-cols-1 md:grid-cols-1 gap-4">
<div class="form-control"> <div class="form-control">
<label class="label" for="formula"> <label class="label" for="formula">
<span class="label-text font-medium flex items-center gap-2"> <span class="label-text font-medium flex items-center gap-2">
@ -131,31 +128,32 @@
<input <input
id="formula" id="formula"
type="text" type="text"
class="input input-bordered w-full" class="input input-sm input-bordered w-full"
bind:value={formData.Formula} bind:value={formData.Formula}
placeholder="e.g., BUN / Creatinine" placeholder="e.g., BUN / Creatinine"
required={canHaveFormula} required={canHaveFormula}
/> />
<span class="label-text-alt text-gray-500">Use test codes with operators: +, -, *, /</span> <span class="label-text-alt text-gray-500">Use test codes with operators: +, -, *, /</span>
</div> </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> </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>

View File

@ -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>

View File

@ -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,40 +148,26 @@
<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">
<select class="select select-sm select-bordered w-16" bind:value={ref.LowSign}>
{#each signOptions as option (option.value)}
<option value={option.value}>{option.label}</option>
{/each}
</select>
<input <input
type="number" type="number"
step="0.01" step="0.01"
class="input input-sm input-bordered flex-1" class="input input-sm input-bordered w-full"
bind:value={ref.Low} bind:value={ref.Low}
placeholder="70" 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">
<select class="select select-sm select-bordered w-16" bind:value={ref.HighSign}>
{#each signOptions as option (option.value)}
<option value={option.value}>{option.label}</option>
{/each}
</select>
<input <input
type="number" type="number"
step="0.01" step="0.01"
class="input input-sm input-bordered flex-1" class="input input-sm input-bordered w-full"
bind:value={ref.High} bind:value={ref.High}
placeholder="100" placeholder="100"
/> />
</div> </div>
</div> </div>
</div>
<!-- Sex & Flag Row --> <!-- Sex & Flag Row -->
<div class="grid grid-cols-2 gap-3 mb-3"> <div class="grid grid-cols-2 gap-3 mb-3">
@ -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}

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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"
/> />

View File

@ -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>

View File

@ -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>