feat(reports,results): add complete reports and results management modules

Add new Reports module:
- Create reports page with listing and viewer functionality
- Add ReportViewerModal for viewing generated reports
- Implement reports API client with endpoints

Add new Results module:
- Create results page for lab result entry and management
- Add ResultEntryModal for entering test results
- Implement results API client with validation support

API and Store Updates:
- Update auth.js API client with improved error handling
- Enhance client.js with new request utilities
- Update auth store for better session management

UI/UX Improvements:
- Update dashboard page layout and styling
- Enhance OrderFormModal with better test selection
- Improve login page styling and validation
- Update main app layout with new navigation items

Documentation:
- Add bundled API documentation (api-docs.bundled.yaml)
- Remove outdated component organization docs
- Delete deprecated YAML specification files

Cleanup:
- Remove cookies.txt from tracking
- Delete COMPONENT_ORGANIZATION.md
- Consolidate documentation files
This commit is contained in:
mahdahar 2026-03-04 16:48:03 +07:00
parent 807cfc8e7a
commit afd8028a21
22 changed files with 8061 additions and 3467 deletions

View File

@ -1,155 +0,0 @@
# Component Organization Guide
Guide for splitting large components and modals into manageable files.
## When to Split Components
Split a component when:
- File exceeds 200 lines
- Component has multiple distinct sections (tabs, steps, panels)
- Logic becomes hard to follow
- Multiple developers work on different parts
## Modal Organization Pattern
### Structure for Large Modals
```
src/routes/(app)/feature/
├── +page.svelte # Main page
├── FeatureModal.svelte # Main modal container
└── feature-modal/ # Modal sub-components (kebab-case folder)
├── modals/ # Nested modals
│ └── PickerModal.svelte
└── tabs/ # Tab content components
├── BasicInfoTab.svelte
├── SettingsTab.svelte
└── AdvancedTab.svelte
```
### Example: Test Form Modal
**Location**: `src/routes/(app)/master-data/tests/test-modal/`
```svelte
<!-- TestFormModal.svelte -->
<script>
import Modal from '$lib/components/Modal.svelte';
import BasicInfoTab from './test-modal/tabs/BasicInfoTab.svelte';
import TechDetailsTab from './test-modal/tabs/TechDetailsTab.svelte';
import CalcDetailsTab from './test-modal/tabs/CalcDetailsTab.svelte';
let { open = $bindable(false), test = null } = $props();
let activeTab = $state('basic');
let formData = $state({});
</script>
<Modal bind:open title={test ? 'Edit Test' : 'New Test'} size="xl">
{#snippet children()}
<div class="tabs tabs-boxed mb-4">
<button class="tab" class:tab-active={activeTab === 'basic'} onclick={() => activeTab = 'basic'}>Basic</button>
<button class="tab" class:tab-active={activeTab === 'technical'} onclick={() => activeTab = 'technical'}>Technical</button>
<button class="tab" class:tab-active={activeTab === 'calculation'} onclick={() => activeTab = 'calculation'}>Calculation</button>
</div>
{#if activeTab === 'basic'}
<BasicInfoTab bind:formData />
{:else if activeTab === 'technical'}
<TechDetailsTab bind:formData />
{:else if activeTab === 'calculation'}
<CalcDetailsTab bind:formData />
{/if}
{/snippet}
</Modal>
```
```svelte
<!-- test-modal/tabs/BasicInfoTab.svelte -->
<script>
let { formData = $bindable({}) } = $props();
</script>
<div class="space-y-4">
<div class="form-control">
<label class="label">Test Name</label>
<input class="input input-bordered" bind:value={formData.name} />
</div>
<div class="form-control">
<label class="label">Description</label>
<textarea class="textarea textarea-bordered" bind:value={formData.description}></textarea>
</div>
</div>
```
## Data Flow
### Parent to Child
- Pass data via props (`bind:formData`)
- Use `$bindable()` for two-way binding
- Keep state in parent when shared across tabs
### Child to Parent
- Use callbacks for actions (`onSave`, `onClose`)
- Modify bound data directly (with `$bindable`)
- Emit events for complex interactions
## Props Interface Pattern
```javascript
// Define props with JSDoc
/** @type {{ formData: Object, onValidate: Function, readonly: boolean }} */
let {
formData = $bindable({}),
onValidate = () => true,
readonly = false
} = $props();
```
## Naming Conventions
- **Main modal**: `{Feature}Modal.svelte` (e.g., `TestFormModal.svelte`)
- **Tab components**: `{TabName}Tab.svelte` (e.g., `BasicInfoTab.svelte`)
- **Nested modals**: `{Action}Modal.svelte` (e.g., `ConfirmDeleteModal.svelte`)
- **Folder names**: kebab-case matching the modal name (e.g., `test-modal/`)
## Shared State Management
For complex modals with shared state across tabs:
```javascript
// In main modal
let sharedState = $state({
dirty: false,
errors: {},
selectedItems: []
});
// Pass to tabs
<TabName formData={formData} sharedState={sharedState} />
```
## Import Order in Sub-components
Same as main components:
1. Svelte imports
2. `$lib/*` imports
3. External libraries
4. Relative imports (other tabs/modals)
## Testing Split Components
```bash
# Test individual tab component
vitest run src/routes/feature/modal-tabs/BasicInfoTab.test.js
# Test main modal integration
vitest run src/routes/feature/FeatureModal.test.js
```
## Benefits
- **Maintainability**: Each file has single responsibility
- **Collaboration**: Multiple developers can work on different tabs
- **Testing**: Test individual sections in isolation
- **Performance**: Only render visible tab content
- **Reusability**: Tabs can be used in different modals

5
cookies.txt Normal file
View File

@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 1773480246 token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyaWQiOiIxIiwicm9sZWlkIjoiMSIsInVzZXJuYW1lIjoibGlzZnNlIiwiZXhwIjoxNzczNDgwMjQ2fQ.EvHSR_Gp86vlK3swjVU8KNv2EjWgwnSUhkeZM3_4Rhs

6451
docs/api-docs.bundled.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,640 +0,0 @@
// 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,265 +0,0 @@
/api/ordertest:
get:
tags: [Orders]
summary: List orders
security:
- bearerAuth: []
parameters:
- name: page
in: query
schema:
type: integer
- name: perPage
in: query
schema:
type: integer
- name: InternalPID
in: query
schema:
type: integer
description: Filter by internal patient ID
- name: OrderStatus
in: query
schema:
type: string
enum: [ORD, SCH, ANA, VER, REV, REP]
description: |
ORD: Ordered
SCH: Scheduled
ANA: Analysis
VER: Verified
REV: Reviewed
REP: Reported
- name: include
in: query
schema:
type: string
enum: [details]
description: Include specimens and tests in response
responses:
'200':
description: List of orders
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
$ref: '../components/schemas/orders.yaml#/OrderTest'
post:
tags: [Orders]
summary: Create order with specimens and tests
description: Creates an order with associated specimens and patres records. Tests are grouped by container type to minimize specimen creation.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- InternalPID
- Tests
properties:
OrderID:
type: string
description: Optional custom order ID (auto-generated if not provided)
InternalPID:
type: integer
description: Patient internal ID
PatVisitID:
type: integer
description: Visit ID
SiteID:
type: integer
default: 1
PlacerID:
type: string
Priority:
type: string
enum: [R, S, U]
default: R
description: |
R: Routine
S: Stat
U: Urgent
ReqApp:
type: string
description: Requesting application
Comment:
type: string
Tests:
type: array
items:
type: object
required:
- TestSiteID
properties:
TestSiteID:
type: integer
description: Test definition site ID
TestID:
type: integer
description: Alias for TestSiteID
responses:
'201':
description: Order created successfully with specimens and tests
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
data:
$ref: '../components/schemas/orders.yaml#/OrderTest'
'400':
description: Validation error
'500':
description: Server error
patch:
tags: [Orders]
summary: Update order
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- OrderID
properties:
OrderID:
type: string
Priority:
type: string
enum: [R, S, U]
OrderStatus:
type: string
enum: [ORD, SCH, ANA, VER, REV, REP]
OrderingProvider:
type: string
DepartmentID:
type: integer
WorkstationID:
type: integer
responses:
'200':
description: Order updated
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '../components/schemas/orders.yaml#/OrderTest'
delete:
tags: [Orders]
summary: Delete order
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- OrderID
properties:
OrderID:
type: string
responses:
'200':
description: Order deleted
/api/ordertest/status:
post:
tags: [Orders]
summary: Update order status
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- OrderID
- OrderStatus
properties:
OrderID:
type: string
OrderStatus:
type: string
enum: [ORD, SCH, ANA, VER, REV, REP]
description: |
ORD: Ordered
SCH: Scheduled
ANA: Analysis
VER: Verified
REV: Reviewed
REP: Reported
responses:
'200':
description: Order status updated
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '../components/schemas/orders.yaml#/OrderTest'
/api/ordertest/{id}:
get:
tags: [Orders]
summary: Get order by ID
description: Returns order details with associated specimens and tests
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
description: Order ID (e.g., 0025030300001)
responses:
'200':
description: Order details with specimens and tests
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '../components/schemas/orders.yaml#/OrderTest'

View File

@ -1,707 +0,0 @@
/api/organization/account/{id}:
get:
tags: [Organization]
summary: Get account by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Account details
content:
application/json:
schema:
$ref: '../components/schemas/organization.yaml#/Account'
/api/organization/site:
get:
tags: [Organization]
summary: List sites
security:
- bearerAuth: []
responses:
'200':
description: List of sites
post:
tags: [Organization]
summary: Create site
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '../components/schemas/organization.yaml#/Site'
responses:
'201':
description: Site created
patch:
tags: [Organization]
summary: Update site
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- id
properties:
id:
type: integer
SiteName:
type: string
SiteCode:
type: string
AccountID:
type: integer
responses:
'200':
description: Site updated
delete:
tags: [Organization]
summary: Delete site
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- id
properties:
id:
type: integer
responses:
'200':
description: Site deleted
/api/organization/site/{id}:
get:
tags: [Organization]
summary: Get site by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Site details
/api/organization/discipline:
get:
tags: [Organization]
summary: List disciplines
security:
- bearerAuth: []
responses:
'200':
description: List of disciplines
post:
tags: [Organization]
summary: Create discipline
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '../components/schemas/organization.yaml#/Discipline'
responses:
'201':
description: Discipline created
patch:
tags: [Organization]
summary: Update discipline
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- id
properties:
id:
type: integer
DisciplineName:
type: string
DisciplineCode:
type: string
responses:
'200':
description: Discipline updated
delete:
tags: [Organization]
summary: Delete discipline
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- id
properties:
id:
type: integer
responses:
'200':
description: Discipline deleted
/api/organization/discipline/{id}:
get:
tags: [Organization]
summary: Get discipline by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Discipline details
/api/organization/department:
get:
tags: [Organization]
summary: List departments
security:
- bearerAuth: []
responses:
'200':
description: List of departments
post:
tags: [Organization]
summary: Create department
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '../components/schemas/organization.yaml#/Department'
responses:
'201':
description: Department created
patch:
tags: [Organization]
summary: Update department
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- id
properties:
id:
type: integer
DeptName:
type: string
DeptCode:
type: string
SiteID:
type: integer
responses:
'200':
description: Department updated
delete:
tags: [Organization]
summary: Delete department
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- id
properties:
id:
type: integer
responses:
'200':
description: Department deleted
/api/organization/department/{id}:
get:
tags: [Organization]
summary: Get department by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Department details
/api/organization/workstation:
get:
tags: [Organization]
summary: List workstations
security:
- bearerAuth: []
responses:
'200':
description: List of workstations
post:
tags: [Organization]
summary: Create workstation
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '../components/schemas/organization.yaml#/Workstation'
responses:
'201':
description: Workstation created
patch:
tags: [Organization]
summary: Update workstation
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- id
properties:
id:
type: integer
WorkstationName:
type: string
WorkstationCode:
type: string
SiteID:
type: integer
DepartmentID:
type: integer
responses:
'200':
description: Workstation updated
delete:
tags: [Organization]
summary: Delete workstation
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- id
properties:
id:
type: integer
responses:
'200':
description: Workstation deleted
/api/organization/workstation/{id}:
get:
tags: [Organization]
summary: Get workstation by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Workstation details
# HostApp
/api/organization/hostapp:
get:
tags: [Organization]
summary: List host applications
security:
- bearerAuth: []
parameters:
- name: HostAppID
in: query
schema:
type: string
- name: HostAppName
in: query
schema:
type: string
responses:
'200':
description: List of host applications
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
$ref: '../components/schemas/organization.yaml#/HostApp'
post:
tags: [Organization]
summary: Create host application
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '../components/schemas/organization.yaml#/HostApp'
responses:
'201':
description: Host application created
patch:
tags: [Organization]
summary: Update host application
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- HostAppID
properties:
HostAppID:
type: string
HostAppName:
type: string
SiteID:
type: integer
responses:
'200':
description: Host application updated
delete:
tags: [Organization]
summary: Delete host application (soft delete)
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- HostAppID
properties:
HostAppID:
type: string
responses:
'200':
description: Host application deleted
/api/organization/hostapp/{id}:
get:
tags: [Organization]
summary: Get host application by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Host application details
content:
application/json:
schema:
$ref: '../components/schemas/organization.yaml#/HostApp'
# HostComPara
/api/organization/hostcompara:
get:
tags: [Organization]
summary: List host communication parameters
security:
- bearerAuth: []
parameters:
- name: HostAppID
in: query
schema:
type: string
- name: HostIP
in: query
schema:
type: string
responses:
'200':
description: List of host communication parameters
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
$ref: '../components/schemas/organization.yaml#/HostComPara'
post:
tags: [Organization]
summary: Create host communication parameters
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '../components/schemas/organization.yaml#/HostComPara'
responses:
'201':
description: Host communication parameters created
patch:
tags: [Organization]
summary: Update host communication parameters
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- HostAppID
properties:
HostAppID:
type: string
HostIP:
type: string
HostPort:
type: string
HostPwd:
type: string
responses:
'200':
description: Host communication parameters updated
delete:
tags: [Organization]
summary: Delete host communication parameters (soft delete)
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- HostAppID
properties:
HostAppID:
type: string
responses:
'200':
description: Host communication parameters deleted
/api/organization/hostcompara/{id}:
get:
tags: [Organization]
summary: Get host communication parameters by HostAppID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Host communication parameters details
content:
application/json:
schema:
$ref: '../components/schemas/organization.yaml#/HostComPara'
# CodingSys
/api/organization/codingsys:
get:
tags: [Organization]
summary: List coding systems
security:
- bearerAuth: []
parameters:
- name: CodingSysAbb
in: query
schema:
type: string
- name: FullText
in: query
schema:
type: string
responses:
'200':
description: List of coding systems
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
$ref: '../components/schemas/organization.yaml#/CodingSys'
post:
tags: [Organization]
summary: Create coding system
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '../components/schemas/organization.yaml#/CodingSys'
responses:
'201':
description: Coding system created
patch:
tags: [Organization]
summary: Update coding system
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- CodingSysID
properties:
CodingSysID:
type: integer
CodingSysAbb:
type: string
FullText:
type: string
Description:
type: string
responses:
'200':
description: Coding system updated
delete:
tags: [Organization]
summary: Delete coding system (soft delete)
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- CodingSysID
properties:
CodingSysID:
type: integer
responses:
'200':
description: Coding system deleted
/api/organization/codingsys/{id}:
get:
tags: [Organization]
summary: Get coding system by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Coding system details
content:
application/json:
schema:
$ref: '../components/schemas/organization.yaml#/CodingSys'

View File

@ -1,519 +0,0 @@
/api/patvisit:
get:
tags: [Patient Visits]
summary: List patient visits
security:
- bearerAuth: []
parameters:
- name: InternalPID
in: query
schema:
type: integer
description: Filter by internal patient ID (exact match)
- name: PVID
in: query
schema:
type: string
description: Filter by visit ID (partial match)
- name: PatientID
in: query
schema:
type: string
description: Filter by patient ID (partial match)
- name: PatientName
in: query
schema:
type: string
description: Search by patient name (searches in both first and last name)
- name: CreateDateFrom
in: query
schema:
type: string
format: date-time
description: Filter visits created on or after this date
- name: CreateDateTo
in: query
schema:
type: string
format: date-time
description: Filter visits created on or before this date
- name: page
in: query
schema:
type: integer
- name: perPage
in: query
schema:
type: integer
responses:
'200':
description: List of patient visits
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
$ref: '../components/schemas/patient-visit.yaml#/PatientVisit'
total:
type: integer
description: Total number of records
page:
type: integer
description: Current page number
per_page:
type: integer
description: Number of records per page
post:
tags: [Patient Visits]
summary: Create patient visit
description: |
Creates a new patient visit. PVID is auto-generated with 'DV' prefix if not provided.
Can optionally include PatDiag (diagnosis) and PatVisitADT (ADT information).
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- InternalPID
properties:
PVID:
type: string
description: Visit ID (auto-generated with DV prefix if not provided)
InternalPID:
type: integer
description: Patient ID (required)
EpisodeID:
type: string
description: Episode identifier
SiteID:
type: integer
description: Site reference
PatDiag:
type: object
description: Optional diagnosis information
properties:
DiagCode:
type: string
Diagnosis:
type: string
PatVisitADT:
type: object
description: Optional ADT information
properties:
ADTCode:
type: string
enum: [A01, A02, A03, A04, A08]
LocationID:
type: integer
AttDoc:
type: integer
RefDoc:
type: integer
AdmDoc:
type: integer
CnsDoc:
type: integer
responses:
'201':
description: Visit created successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: object
properties:
PVID:
type: string
InternalPVID:
type: integer
patch:
tags: [Patient Visits]
summary: Update patient visit
description: |
Updates an existing patient visit. InternalPVID is required.
Can update main visit data, PatDiag, and add new PatVisitADT records.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- InternalPVID
properties:
InternalPVID:
type: integer
description: Visit ID (required)
PVID:
type: string
InternalPID:
type: integer
EpisodeID:
type: string
SiteID:
type: integer
PatDiag:
type: object
description: Diagnosis information (will update if exists)
properties:
DiagCode:
type: string
Diagnosis:
type: string
PatVisitADT:
type: array
description: Array of ADT records to add (new records only)
items:
type: object
properties:
ADTCode:
type: string
enum: [A01, A02, A03, A04, A08]
LocationID:
type: integer
AttDoc:
type: integer
RefDoc:
type: integer
AdmDoc:
type: integer
CnsDoc:
type: integer
sequence:
type: integer
description: Used for ordering multiple ADT records
responses:
'200':
description: Visit updated successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: object
properties:
PVID:
type: string
InternalPVID:
type: integer
delete:
tags: [Patient Visits]
summary: Delete patient visit
security:
- bearerAuth: []
responses:
'200':
description: Visit deleted successfully
/api/patvisit/{id}:
get:
tags: [Patient Visits]
summary: Get visit by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
description: PVID (visit identifier like DV00001)
responses:
'200':
description: Visit details
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '../components/schemas/patient-visit.yaml#/PatientVisit'
/api/patvisit/patient/{patientId}:
get:
tags: [Patient Visits]
summary: Get visits by patient ID
security:
- bearerAuth: []
parameters:
- name: patientId
in: path
required: true
schema:
type: integer
description: Internal Patient ID (InternalPID)
responses:
'200':
description: Patient visits list
content:
application/json:
schema:
type: object
properties:
status:
type: string
data:
type: array
items:
$ref: '../components/schemas/patient-visit.yaml#/PatientVisit'
/api/patvisitadt:
post:
tags: [Patient Visits]
summary: Create ADT record
description: Create a new Admission/Discharge/Transfer record
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '../components/schemas/patient-visit.yaml#/PatVisitADT'
responses:
'201':
description: ADT record created successfully
content:
application/json:
schema:
$ref: '../components/schemas/common.yaml#/SuccessResponse'
patch:
tags: [Patient Visits]
summary: Update ADT record
description: Update an existing ADT record
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '../components/schemas/patient-visit.yaml#/PatVisitADT'
responses:
'200':
description: ADT record updated successfully
content:
application/json:
schema:
$ref: '../components/schemas/common.yaml#/SuccessResponse'
delete:
tags: [Patient Visits]
summary: Delete ADT visit (soft delete)
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- PVADTID
properties:
PVADTID:
type: integer
description: ADT record ID to delete
responses:
'200':
description: ADT visit deleted successfully
/api/patvisitadt/visit/{visitId}:
get:
tags: [Patient Visits]
summary: Get ADT history by visit ID
description: Retrieve the complete Admission/Discharge/Transfer history for a visit, including all locations and doctors
security:
- bearerAuth: []
parameters:
- name: visitId
in: path
required: true
schema:
type: integer
description: Internal Visit ID (InternalPVID)
responses:
'200':
description: ADT history retrieved successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: ADT history retrieved
data:
type: array
items:
type: object
properties:
PVADTID:
type: integer
InternalPVID:
type: integer
ADTCode:
type: string
enum: [A01, A02, A03, A04, A08]
LocationID:
type: integer
LocationName:
type: string
AttDoc:
type: integer
AttDocFirstName:
type: string
AttDocLastName:
type: string
RefDoc:
type: integer
RefDocFirstName:
type: string
RefDocLastName:
type: string
AdmDoc:
type: integer
AdmDocFirstName:
type: string
AdmDocLastName:
type: string
CnsDoc:
type: integer
CnsDocFirstName:
type: string
CnsDocLastName:
type: string
CreateDate:
type: string
format: date-time
EndDate:
type: string
format: date-time
delete:
tags: [Patient Visits]
summary: Delete ADT visit (soft delete)
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- PVADTID
properties:
PVADTID:
type: integer
description: ADT record ID to delete
responses:
'200':
description: ADT visit deleted successfully
/api/patvisitadt/{id}:
get:
tags: [Patient Visits]
summary: Get ADT record by ID
description: Retrieve a single ADT record by its ID, including location and doctor details
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: ADT record ID (PVADTID)
responses:
'200':
description: ADT record retrieved successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: ADT record retrieved
data:
type: object
properties:
PVADTID:
type: integer
InternalPVID:
type: integer
ADTCode:
type: string
enum: [A01, A02, A03, A04, A08]
LocationID:
type: integer
LocationName:
type: string
AttDoc:
type: integer
AttDocFirstName:
type: string
AttDocLastName:
type: string
RefDoc:
type: integer
RefDocFirstName:
type: string
RefDocLastName:
type: string
AdmDoc:
type: integer
AdmDocFirstName:
type: string
AdmDocLastName:
type: string
CnsDoc:
type: integer
CnsDocFirstName:
type: string
CnsDocLastName:
type: string
CreateDate:
type: string
format: date-time
EndDate:
type: string
format: date-time

View File

@ -1,503 +0,0 @@
/api/test/testmap:
get:
tags: [Tests]
summary: List all test mappings (unique groupings)
security:
- bearerAuth: []
responses:
'200':
description: List of unique test mapping groupings
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
data:
type: array
items:
type: object
properties:
HostType:
type: string
HostID:
type: string
HostName:
type: string
ClientType:
type: string
ClientID:
type: string
ClientName:
type: string
post:
tags: [Tests]
summary: Create test mapping (header only)
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
TestSiteID:
type: integer
description: Test Site ID (required)
HostType:
type: string
description: Host type code
HostID:
type: string
description: Host identifier
ClientType:
type: string
description: Client type code
ClientID:
type: string
description: Client identifier
details:
type: array
description: Optional detail records to create
items:
type: object
properties:
HostTestCode:
type: string
HostTestName:
type: string
ConDefID:
type: integer
ClientTestCode:
type: string
ClientTestName:
type: string
required:
- TestSiteID
responses:
'201':
description: Test mapping created
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
data:
type: integer
description: Created TestMapID
patch:
tags: [Tests]
summary: Update test mapping
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
TestMapID:
type: integer
description: Test Map ID (required)
TestSiteID:
type: integer
HostType:
type: string
HostID:
type: string
ClientType:
type: string
ClientID:
type: string
required:
- TestMapID
responses:
'200':
description: Test mapping updated
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
data:
type: integer
description: Updated TestMapID
delete:
tags: [Tests]
summary: Soft delete test mapping (cascades to details)
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
TestMapID:
type: integer
description: Test Map ID to delete (required)
required:
- TestMapID
responses:
'200':
description: Test mapping deleted successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
message:
type: string
data:
type: integer
description: Deleted TestMapID
'404':
description: Test mapping not found or already deleted
/api/test/testmap/{id}:
get:
tags: [Tests]
summary: Get test mapping by ID with details
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: Test Map ID
responses:
'200':
description: Test mapping details with nested detail records
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '../components/schemas/tests.yaml#/TestMap'
'404':
description: Test mapping not found
/api/test/testmap/by-testsite/{testSiteID}:
get:
tags: [Tests]
summary: Get test mappings by test site with details
security:
- bearerAuth: []
parameters:
- name: testSiteID
in: path
required: true
schema:
type: integer
description: Test Site ID
responses:
'200':
description: List of test mappings with details for the test site
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
$ref: '../components/schemas/tests.yaml#/TestMap'
/api/test/testmap/detail:
get:
tags: [Tests]
summary: List test mapping details
security:
- bearerAuth: []
parameters:
- name: TestMapID
in: query
schema:
type: integer
description: Filter by TestMapID
responses:
'200':
description: List of test mapping details
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
$ref: '../components/schemas/tests.yaml#/TestMapDetail'
post:
tags: [Tests]
summary: Create test mapping detail
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
TestMapID:
type: integer
description: Test Map ID (required)
HostTestCode:
type: string
HostTestName:
type: string
ConDefID:
type: integer
ClientTestCode:
type: string
ClientTestName:
type: string
required:
- TestMapID
responses:
'201':
description: Test mapping detail created
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: integer
description: Created TestMapDetailID
patch:
tags: [Tests]
summary: Update test mapping detail
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
TestMapDetailID:
type: integer
description: Test Map Detail ID (required)
TestMapID:
type: integer
HostTestCode:
type: string
HostTestName:
type: string
ConDefID:
type: integer
ClientTestCode:
type: string
ClientTestName:
type: string
required:
- TestMapDetailID
responses:
'200':
description: Test mapping detail updated
delete:
tags: [Tests]
summary: Soft delete test mapping detail
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
TestMapDetailID:
type: integer
description: Test Map Detail ID to delete (required)
required:
- TestMapDetailID
responses:
'200':
description: Test mapping detail deleted
/api/test/testmap/detail/{id}:
get:
tags: [Tests]
summary: Get test mapping detail by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
description: Test Map Detail ID
responses:
'200':
description: Test mapping detail
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
$ref: '../components/schemas/tests.yaml#/TestMapDetail'
/api/test/testmap/detail/by-testmap/{testMapID}:
get:
tags: [Tests]
summary: Get test mapping details by test map ID
security:
- bearerAuth: []
parameters:
- name: testMapID
in: path
required: true
schema:
type: integer
description: Test Map ID
responses:
'200':
description: List of test mapping details
content:
application/json:
schema:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
$ref: '../components/schemas/tests.yaml#/TestMapDetail'
/api/test/testmap/detail/batch:
post:
tags: [Tests]
summary: Batch create test mapping details
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: object
properties:
TestMapID:
type: integer
HostTestCode:
type: string
HostTestName:
type: string
ConDefID:
type: integer
ClientTestCode:
type: string
ClientTestName:
type: string
responses:
'200':
description: Batch create results
patch:
tags: [Tests]
summary: Batch update test mapping details
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: object
properties:
TestMapDetailID:
type: integer
TestMapID:
type: integer
HostTestCode:
type: string
HostTestName:
type: string
ConDefID:
type: integer
ClientTestCode:
type: string
ClientTestName:
type: string
responses:
'200':
description: Batch update results
delete:
tags: [Tests]
summary: Batch delete test mapping details
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: integer
description: TestMapDetailIDs to delete
responses:
'200':
description: Batch delete results

View File

@ -1,514 +0,0 @@
Here is the converted Markdown format of the Use Case document. I have structured it with headers and lists to make it easily parsable by an AI agent.
```markdown
# Use Case Document
## Use Case Authentication
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-01 |
| **Use Case Name** | Authentication |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Phlebotomist Lab, Perawat, DSPK/Konsulen, Supervisor Lab, Manajer Lab, Database Administrator, System Administrator |
| **Aktor Sekunder** | - |
| **Tujuan** | Verifikasi identitas pengguna, yang mencoba mengakses sistem, memastikan bahwa mereka adalah orang yang mereka klaim. Bertindak sebagai mekanisme keamanan primer untuk mencegah akses tidak sah, melindungi data, dan mengurangi risiko seperti pencurian identitas dan pelanggaran keamanan. |
| **Prasyarat** | Data pengguna sudah terdefinisi dalam sistem sebagai User atau Contact. |
### Alur Utama
1. Aktor klik tombol Login.
2. System menampilkan login dialog yang terdiri dari User ID dan Password.
3. Aktor memasukkan email address sebagai User ID.
4. System memeriksa email address di table User, SiteStatus, Contact dan ContactDetail:
- Jika Aktor menggunakan email pribadi, maka System menampilkan pilihan sites dimana Aktor memiliki akses. Aktor, kemudian memilih salah satu site.
- Jika Aktor menggunakan email site, maka System langsung mengarahkan ke site yang bersangkutan.
5. Aktor memasukkan password.
6. System memeriksa kebenaran User ID dan password.
7. System memeriksa role dan menu apa saja yang bisa diakses Aktor.
8. System menampilkan halaman utama dengan menu sesuai role Aktor.
### Alur Alternatif
-
### Alur Pengecualian
* **Aktor tidak terdaftar:**
* System menampilkan pesan: “Unregistered user, please contact system administrator”.
* **Aktor ditemukan tetapi:**
* **Disabled:** System menampilkan pesan: “Disabled user, please contact system administrator”.
* **Password expired:** System menampilkan pesan: “Your password is expired, please contact system administrator”.
* **Password salah:**
* System menampilkan pesan: “Invalid login”.
* System menghitung jumlah percobaan login password yang gagal dan mencatat dalam Audit log (device dimana login attempt dilakukan, waktu).
* System menghentikan proses login untuk User ID tersebut selama x jam dan menampilkan pesan, ”Please try again in x hours or contact system administrator”.
### Kondisi Akhir
* Aktor masuk ke halaman utama dan mendapat akses menu-menu system yang sesuai.
* Audit mencatat User ID, waktu, device dimana Aktor melakukan login.
---
## Use Case Patient Registration
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-02a |
| **Use Case Name** | Patient Registration |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Mencatatkan data demografi pasien baru ke oleh System |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi dalam System.<br>3. Pasien menunjukkan identitas yang sah (kartu identitas, atau rujukan). |
### Alur Utama
1. Aktor membuka halaman Patient Management Patient Registration.
2. Aktor memasukkan PID.
3. System memeriksa apakah PID sudah ada.
4. System meminta detail pasien (nama, tanggal lahir, jenis kelamin, informasi kontak, nomor identitas, dst).
5. Aktor memasukkan informasi demografis pasien, dengan mandatory data:
- patient.NameFirst
- patient.Gender
- patient.Birthdate
6. Jika Aktor memasukkan `patidt.IdentifierType` dan `patidt.Identifier`, maka System memeriksa apakah sudah ada record pasien yang menggunakan Identifier yang sama.
7. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A04` (Register), mengkonfirmasi registrasi berhasil dan menampilkan ringkasan pasien.
8. Aktor memberikan konfirmasi pendaftaran kepada Pasien (misalnya, slip cetak atau ID digital barcode, QRIS, dll).
### Alur Alternatif
* **Record pasien sudah ada:**
1. Aktor memasukkan PID.
2. System mengambil record yang sudah ada dan menampilkan data di halaman Patient Management Patient Search & Update.
3. Aktor memperbarui data jika diperlukan.
4. Sistem menyimpan perubahan dan membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A08` (Update patient information).
### Alur Pengecualian
* **Mandatory data tidak ada:**
* System menolak menyimpan record.
* Aktor diminta untuk melengkapi, setidaknya mandatory data.
* **Record pasien tidak ada tetapi ditemukan record yang menggunakan `patidt.IdentifierType` dan `patidt.Identifier` yang sama:**
* System menampilkan pesan Multiple IDs found”.
* System menampilkan records dengan `patidt.IdentifierType` dan `patidt.Identifier` yang sama.
* Aktor melakukan review.
* Aktor memilih salah satu dari kemungkinan berikut:
1. Melanjutkan membuat record pasien baru, mengabaikan record ganda.
2. Melanjutkan membuat record pasien baru, kemudian menggabungkan record pasien (lihat UC-02b).
3. Membatalkan pendaftaran.
### Kondisi Akhir
* Record pasien dibuat atau diperbarui di System.
* PID pasien tersedia untuk test ordering & tracing.
* Audit mencatat bahwa record dibuat/diperbarui secara manual, User ID yang mendaftarkan/memperbarui data pasien, device dimana, kapan, dan data apa yang dimasukkan.
---
## Use Case Patient Link
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-02b |
| **Use Case Name** | Patient Link |
| **Aktor Utama** | Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Link (menghubungkan) satu atau beberapa record (PID) pasien (source) dengan record pasien lainnya (destination). PatientID destination adalah surviving entity, yang akan digunakan dalam semua aktivitas laboratorium. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi dalam System.<br>3. PID source dan destination telah tercatat dalam System.<br>4. PID source dan destination memiliki `patidt.IdentifierType` dan `patidt.Identifier` yang sama atau nama, alamat dan tanggal lahir yang sama. |
### Alur Utama
1. Aktor membuka halaman Patient Management - Patient Link.
2. Aktor mencari PID menggunakan:
- `patidt.IdentifierType` dan `patidt.Identifier`
- Nama, alamat dan tanggal lahir
3. System menampilkan semua PID dengan `patidt.IdentifierType` dan `patidt.Identifier` dan/atau nama, alamat dan tanggal lahir yang sama.
4. Aktor memilih dan menentukan satu PID untuk menjadi destination. (Lihat Alur Pengecualian).
5. Aktor memilih dan menentukan satu atau lebih, PID yang menjadi source. (Lihat Alur Pengecualian).
6. Aktor menghubungkan PID-PID tersebut dengan menekan tombol Link.
7. System meminta konfirmasi dari Aktor dengan menampilkan pesan,” Please confirm to link these patient records”. Disertai pilihan “Confirm” dan “Cancel”.
8. Aktor mengkonfirmasi Patient Link dengan menekan tombol Confirm.
9. System melakukan:
- Membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A24` (Link Patient Information).
- Mengkonfirmasi Patient Link berhasil.
- Menampilkan ringkasan PID yang dihubungkan.
### Alur Alternatif
-
### Alur Pengecualian
* **System menemukan bahwa suatu record pasien telah menjadi source, ditandai dengan field `patient.LinkTo` telah terisi dengan PID dari record lain (ditampilkan):**
* **Multiple link:**
* Aktor memilih dan menunjuk record tersebut sebagai source bagi PID yang berbeda.
* System menampilkan peringatan “Multiple link” di samping PID tersebut dan pilihan (check mark) tidak bisa dilakukan.
* **Multi-level Link:**
* Aktor memilih dan menunjuk record tersebut sebagai destination bagi PID yang berbeda.
* System menampilkan peringatan “Multi-level link” di samping PID tersebut dan pilihan (check mark) tidak bisa dilakukan.
* Jika semua atau satu-satunya PID mendapat peringatan tersebut maka, proses Patient Link sama sekali tidak bisa dilanjutkan.
* Jika ada PID lain yang tidak mendapat peringatan, maka proses Patient Link dilanjutkan atas PID tanpa peringatan.
### Kondisi Akhir
* PID source terhubung dengan PID destination.
* Relasi source dengan test order dan lain-lain tidak berubah sebelum dan sesudah proses Patient Link.
* Semua test order milik PID source dan destination bisa ditampilkan dalam satu cumulative view/report.
* PID destination tersedia untuk test ordering & tracing.
* PID source tetap bisa dicari tetapi tidak bisa di-edit maupun digunakan untuk test ordering.
* Audit mencatat Patient Link dilakukan secara manual, waktu, User ID yang melakukan Patient Link serta device dimana aktivitas tersebut dilakukan.
---
## Use Case Patient Unlink
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-02c |
| **Use Case Name** | Patient Unlink |
| **Aktor Utama** | Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Melepaskan link antara source PID dan destination PID. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi dalam System.<br>3. Pasien sudah pernah registrasi di System, ditandai dengan adanya PID dengan `patidt.IdentifierType` dan `patidt.Identifier` yang sama. |
### Alur Utama
1. Aktor membuka halaman Patient Management Patient Unlink.
2. Aktor mencari record pasien menggunakan PID.
3. System menampilkan PID berikut data demografinya dan semua linked PID.
4. Aktor uncheck source PID(s) yang hendak dilepaskan dari destination PID.
5. Aktor melepas hubungan PID-PID tersebut dengan menekan tombol Unlink.
6. System meminta konfirmasi dari Aktor dengan menampilkan pesan,” Please confirm to unlink these patient records”. Disertai pilihan “Confirm” dan “Cancel”.
7. Aktor mengkonfirmasi Patient Unink dengan menekan tombol Confirm.
8. System melakukan:
- Mengosongkan field `patient.LinkTo` dari source PID.
- Membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A37` (Unlink Patient Information).
- Mengkonfirmasi Patient Unlink berhasil.
- Menampilkan ringkasan destination dan source PID yang unlinked.
### Alur Alternatif
-
### Alur Pengecualian
-
### Kondisi Akhir
* Source PID aktif kembali, bisa diedit dan tersedia untuk test ordering & tracing.
* Unlink source terjadi bisa isi field LinkTo dikosongkan Kembali.
* Audit mencatat Patient Unlink dilakukan secara manual, waktu, User ID yang melakukan Patient Unlink dan device dimana aktivitas tersebut dilakukan.
---
## Use Case Patient Admission
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-03a |
| **Use Case Name** | Patient Admission |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Menerima pasien di fasyankes untuk perawatan atau observasi. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Record pasien tersedia di System, ditandai dengan adanya PID. |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Patient Admission.
2. Aktor memasukkan PID.
3. System memeriksa apakah PID ada.
4. System menampilkan data demografi pasien dan meminta data-data:
- **Mandatory data:** PVID, dokter, location.
- **Optional data:** EpisodeID, diagnosis (bisa lebih dari satu), lampiran-lampiran.
5. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A01` (Admit), mengkonfirmasi admission berhasil dan menampilkan ringkasan admission.
6. Aktor memberikan konfirmasi admission kepada Pasien (misalnya, slip cetak atau ID digital barcode, QRIS, dll).
### Alur Alternatif
* **PID tidak ada:**
1. System menampilkan pesan, “PID does not exist. Proceed to Patient Registration?”.
2. Aktor memilih “Yes” dan System membuka halaman Patient Management Patient Registration.
3. Aktor melakukan activity patient registration dilanjutkan patient admission.
* **Pembaruan optional data:**
1. Aktor membuka halaman Patient Visit Management Admission Search & Update.
2. Aktor memasukkan PVID.
3. System mengambil record yang sudah ada.
4. Aktor memperbarui data jika diperlukan.
5. Sistem menyimpan perubahan.
### Alur Pengecualian
* **Mandatory data tidak ada:**
* System menolak menyimpan record.
* Aktor diminta untuk melengkapi, setidaknya mandatory data.
### Kondisi Akhir
* Kunjungan pasien ke fasyankes tercatat (patvisit records) di System, ditandai dengan adanya PVID dan direlasikan dengan dokter dan ruangan di fasyankes.
* Audit mencatat admission/perubahannya dilakukan secara manual, User ID yang melakukan, device dimana, kapan, dan data apa saja yang dimasukkan.
---
## Use Case Cancel Admission
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-03b |
| **Use Case Name** | Cancel Patient Admission |
| **Aktor Utama** | Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Membatalkan penerimaan pasien di fasyankes. Pembatalan bisa disebabkan: Data registrasi salah atau tidak lengkap, Cakupan asuransi tidak valid, Pasien menolak rawat inap, Pasien dialihkan, Permintaan rawat inap salah, Kondisi pasien berubah, Permintaan pasien, dll. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID. |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Admission Search & Update.
2. Aktor memasukkan PVID.
3. System menampilkan data admission pasien.
4. Aktor mengkonfirmasi pembatalan admission ke pihak terkait dan melakukan pembatalan.
5. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A11` (Cancel Admit), mengkonfirmasi cancel patient admission berhasil dan menampilkan ringkasan cancel patient admission.
6. Aktor memberikan konfirmasi cancel patient admission kepada pihak terkait (misalnya, slip cetak atau ID digital barcode, QRIS, dll).
### Alur Alternatif
-
### Alur Pengecualian
-
### Kondisi Akhir
* Cancel patient admission tercatat (patvisit records) di System, ditandai dengan record di `patvisitadt` dengan `patvisitadt.Code: A11`.
* Audit mencatat cancel patient admission dilakukan secara manual, User ID yang melakukan, device dimana, kapan, dan data apa yang dimasukkan.
---
## Use Case Change Attending Doctor
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-03c |
| **Use Case Name** | Change Attending Doctor |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Mengganti dokter yang bertanggung jawab atas pengobatan pasien (DPJP). |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID dan telah memiliki data Attending Doctor (`patvisitadt.AttDoc`). |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Admission Search & Update.
2. Aktor memasukkan PVID.
3. System menampilkan data admission pasien.
4. Aktor mengganti Attending Doctor.
5. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A54` (Change Attending Doctor), mengkonfirmasi penggantian dokter berhasil dan menampilkan data admission yang telah diperbarui.
### Alur Alternatif
-
### Alur Pengecualian
-
### Kondisi Akhir
* Penggantian Attending Doctor di System sehingga bisa dilakukan pelacakan Attending Doctor sekarang dan sebelumnya.
* Audit mencatat User ID yang melakukan perubahan Attending Doctor, device dimana perubahan dilakukan, kapan.
---
## Use Case Change Consulting Doctor
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-03d |
| **Use Case Name** | Change Consulting Doctor |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Mengganti dokter konsulen. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID dan telah memiliki data Consulting Doctor (`patvisitadt.CnsDoc`). |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Admission Search & Update.
2. Aktor memasukkan PVID.
3. System menampilkan data admission pasien.
4. Aktor mengganti Consulting Doctor.
5. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A61` (Change Consulting Doctor), mengkonfirmasi penggantian dokter berhasil dan menampilkan data admission yang telah diperbarui.
### Alur Alternatif
-
### Alur Pengecualian
-
### Kondisi Akhir
* Penggantian Consulting Doctor di System sehingga bisa dilakukan pelacakan Consulting Doctor sekarang dan sebelumnya.
* Audit mencatat User ID yang melakukan perubahan Consulting Doctor, device dimana perubahan dilakukan, kapan.
---
## Use Case Patient Transfer
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-04 |
| **Use Case Name** | Patient Transfer |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Memindahkan pasien dari satu lokasi ke lokasi lainnya. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID dan telah memiliki data Location ID (`patvisitadt.LocationID`). |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Transfer.
2. Aktor memasukkan PVID.
3. System menampilkan data admission pasien.
4. Aktor mengganti Location ID.
5. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A02` (Patient Transfer), mengkonfirmasi perpindahan lokasi berhasil dan menampilkan data admission yang telah diperbarui.
### Alur Alternatif
-
### Alur Pengecualian
-
### Kondisi Akhir
* Penggantian Location ID di System sehingga bisa dilakukan pelacakan Location ID sekarang dan sebelumnya.
* Audit mencatat User ID yang melakukan perubahan Location ID, device dimana perubahan dilakukan dan kapan.
---
## Use Case Patient Discharge
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-04a |
| **Use Case Name** | Patient Discharge |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Mengakhiri kunjungan pasien. Close billing. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID. |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Discharge.
2. Aktor memasukkan PVID.
3. System memeriksa apakah PVID tersebut memiliki test order.
4. System memeriksa `orderstatus.OrderStatus` dari test order tsb.
5. System menampilkan data admission pasien.
6. Aktor mengisikan tanggal discharge.
7. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A03` (Discharge), mengkonfirmasi discharge/end visit berhasil dan menampilkan data admission yang telah di-discharge.
### Alur Alternatif
-
### Alur Pengecualian
* **Open test order:**
* System menolak discharge, jika menemukan `orderstatus.OrderStatus` bernilai ”A” atau “IP” atau “SC” atau “HD”.
* Aktor diminta untuk menyelesaikan test order terkait.
### Kondisi Akhir
* Discharge visit di System.
* Audit mencatat User ID yang melakukan discharge, device dimana discharge dilakukan dan kapan.
* Semua record terkait visit tersebut tidak bisa diedit/update lagi data-data pada `patvisit`, `patdiag`, `patvisitbill`. Hal-hal berikut tidak bisa dilakukan lagi:
- Perpindahan lokasi dan/atau dokter.
- Test order.
- Billing is closed.
* **Cancel discharge:**
- Bisa dilakukan atas instruksi dari HIS, misalnya berupa ADT message.
- Oleh orang tertentu saja di lab.
- Tidak meng-update existing record tetapi men-trigger tambahan `patvisitadt` record dengan Code: A13 (cancel discharge).
---
## Use Case Cancel Discharge
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-04b |
| **Use Case Name** | Cancel Patient Discharge |
| **Aktor Utama** | Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Membatalkan Patient Discharge. Open billing. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID dan telah discharge. |
### Alur Utama
1. Aktor membuka halaman Patient Visit Management Cancel Patient Discharge.
2. Aktor memasukkan PVID.
3. System menampilkan data admission pasien.
4. Aktor membatalkan discharge dengan menekan tombol Cancel Discharge.
5. System membuat record baru di `patvisitadt` dengan `patvisitadt.Code: A13` (Cancel Discharge), mengkonfirmasi cancel discharge berhasil dan menampilkan data admission yang telah dibatalkan discharge-nya.
### Alur Alternatif
-
### Alur Pengecualian
-
### Kondisi Akhir
* Pembatalan discharge di System.
* Audit mencatat cancel discharge dilakukan secara manual, User ID yang melakukan, device dimana activity dilakukan dan kapan.
* Semua record terkait visit tersebut kembali bisa diedit/update lagi data-data pada `patvisit`, `patdiag`, `patvisitbill`. Hal-hal berikut bisa dilakukan lagi:
- Perpindahan lokasi dan/atau dokter.
- Test order.
- Billing is re-open.
---
## Use Case Test Ordering
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-5a |
| **Use Case Name** | Test Ordering |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Membuat test order untuk pasien. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Patient Visit Record tersedia di System, ditandai dengan adanya PVID. |
### Alur Utama
1. Aktor membuka halaman Test Ordering New Test Order.
2. Aktor memasukkan PVID.
3. System menampilkan data demografi, daftar PVID pasien berikut daftar OrderID (OID) yang telah dibuat sebelumnya (menghindari test order berlebihan).
4. Aktor bisa menambahkan komentar dan/atau lampiran ke test order.
5. Aktor memilih test yang diperlukan.
6. Aktor bisa memilih mencetak labels segera setelah klik tombol Save, dengan mencentang Print Patient Label, Print Order Label, Print Specimen Label check boxes.
7. Aktor menyimpan test order dengan menekan tombol Save.
8. System secara otomatis memberi OID.
9. System otomatis membuat records di table specimens.
10. System, mengkonfirmasi test ordering berhasil dan menampilkan data test order berikut daftar SID.
### Alur Alternatif
* **PVID belum ada:**
1. System mengarahkan Aktor ke halaman Patient Visit Management Patient Admission.
2. Aktor melakukan activity patient admission.
3. Aktir kembali ke test ordering.
* **Test ordering menggunakan PID:**
1. Aktor membuka halaman Test Ordering New Test Order.
2. Aktor memasukkan PID.
3. System menampilkan daftar PVID yang belum discharge.
4. Aktor memilih salah satu PVID dan melanjutkan activity test ordering.
* **Future Order:**
1. Aktor mengisi Effective Date (`ordertest.EffDate`) untuk menjadwalkan kapan test order mulai dikerjakan.
2. Aktor menyimpan test order dengan menekan tombol Save.
3. System memberikan OID yang sesuai dengan Effective Date.
* **OID sudah ada:**
1. Aktor membuka halaman Test Ordering Test Order Search & Update.
2. Aktor memasukan OID.
3. System menampilkan data-data test order.
4. Aktor melakukan update dan menyimpannya.
* **Non patient option** (Not detailed in text).
### Alur Pengecualian
-
### Kondisi Akhir
* Test order terbentuk di System ditandai dengan adanya OID dengan status (`orderstatus.OrderStatus`) ”SC” (In process, scheduled).
* SID terbentuk dan specimen label bisa dicetak atau tercetak otomatis.
* Audit mencatat test order dilakukan secara manual, User ID yang melakukan, device dimana activity dilakukan dan kapan.
---
## Use Case Update Test Order
| **Field** | **Description** |
| :--- | :--- |
| **Use Case ID** | UC-5b |
| **Use Case Name** | Update Test Order |
| **Aktor Utama** | Admin Lab / Clerk, Analis Lab, Analis Lab Senior, Super User, Supervisor Lab, Manajer Lab |
| **Aktor Sekunder** | Pasien |
| **Tujuan** | Memperbarui test order untuk pasien. |
| **Prasyarat** | 1. System beroperasi dan dapat diakses.<br>2. Petugas pendaftaran telah terautentikasi oleh System.<br>3. Test order tersedia di System, ditandai dengan adanya OID. |
### Alur Utama
1. Aktor membuka halaman Test Ordering Test Order Search & Update.
2. Aktor memasukkan OID.
3. System menampilkan data-data test order.
4. Aktor melakukan update dan menyimpannya.
5. System, mengkonfirmasi update test order berhasil dan menampilkan data test order berikut daftar SID.
### Alur Alternatif
* **PVID belum ada:**
1. System mengarahkan Aktor ke halaman Patient Visit Management Patient Admission.
2. Aktor melakukan activity patient admission.
3. Aktir kembali ke test ordering.
* **Non patient option** (Not detailed in text).
### Alur Pengecualian
* **Test order tidak ada:**
* System menolak melakukan update dan menampilkan pesan,” Test order does not exists”.
* **Test order berstatus closed:** Ditandai dengan `ordertest.EndDate` memiliki value dan `orderstatus.OrderStatus` bernilai CL (Closed).
* System menolak melakukan update dan menampilkan pesan,” This test order is inaccessible”.
* **Test order berstatus archived:** Ditandai dengan `ordertest.ArchiveDate` memiliki value dan `orderstatus.OrderStatus` bernilai AC (Archived).
* System menolak melakukan update dan menampilkan pesan,” This test order is already archived”.
* **Test order berstatus deleted:** Ditandai dengan `ordertest.DelDate` memiliki value dan `orderstatus.OrderStatus` bernilai DL (Deleted).
* System menolak melakukan update dan menampilkan pesan,” This test order is already deleted”.
* **Update dilakukan dengan menghapus test yang telah ada hasilnya:**
* System menampilkan data-data test order.
* Aktor mengganti test yang telah ada hasilnya.
* System menolak melakukan update dan menampilkan pesan,” This test order is inaccessible”.
### Kondisi Akhir
* Test order terbentuk di System ditandai dengan adanya OID dengan status (`orderstatus.OrderStatus`) ”SC” (In process, scheduled).
* SID terbentuk dan specimen label bisa dicetak atau tercetak otomatis.
* Audit mencatat test order dilakukan secara manual, User ID yang melakukan, device dimana activity dilakukan dan kapan.
```

View File

@ -1,4 +1,4 @@
import { post } from './client.js';
import { post, get } from './client.js';
/**
* Authentication API endpoints
@ -33,5 +33,5 @@ export async function logout() {
* @returns {Promise<Object>}
*/
export async function getCurrentUser() {
return post('/api/auth/check', {});
return get('/api/auth/check');
}

View File

@ -13,29 +13,18 @@ function getApiUrl() {
}
/**
* Base API client with JWT handling
* Base API client with cookie-based authentication
* @param {string} endpoint - API endpoint (without base URL)
* @param {Object} options - Fetch options
* @returns {Promise<any>} - JSON response
*/
export async function apiClient(endpoint, options = {}) {
// Get token from store
let token = null;
auth.subscribe((authState) => {
token = authState.token;
})();
// Build headers
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
// Add Authorization header if token exists
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// Build full URL using runtime config
const url = `${getApiUrl()}${endpoint}`;
@ -43,6 +32,7 @@ export async function apiClient(endpoint, options = {}) {
const response = await fetch(url, {
...options,
headers,
credentials: 'include',
});
// Handle 401 Unauthorized

44
src/lib/api/reports.js Normal file
View File

@ -0,0 +1,44 @@
import { config } from '$lib/stores/config.js';
/**
* Get the API URL from runtime config
* @returns {string}
*/
function getApiUrl() {
const runtimeUrl = config.getApiUrl();
return runtimeUrl || import.meta.env.VITE_API_URL || '';
}
/**
* Generate lab report URL for an order
* @param {string|number} orderId - Order ID
* @returns {string} Report URL
*/
export function getReportUrl(orderId) {
return `${getApiUrl()}/api/reports/${orderId}`;
}
/**
* Open report in new window
* @param {string|number} orderId - Order ID
* @param {string} [target='_blank'] - Window target
* @returns {Window|null} Window reference
*/
export function openReport(orderId, target = '_blank') {
const url = getReportUrl(orderId);
return window.open(url, target);
}
/**
* Print report for an order
* @param {string|number} orderId - Order ID
*/
export function printReport(orderId) {
const url = getReportUrl(orderId);
const printWindow = window.open(url, '_blank');
if (printWindow) {
printWindow.onload = () => {
printWindow.print();
};
}
}

75
src/lib/api/results.js Normal file
View File

@ -0,0 +1,75 @@
import { get, patch, del } from './client.js';
/**
* Fetch results with optional filters
* @param {Object} params - Query parameters
* @param {number} [params.order_id] - Filter by order ID
* @param {number} [params.patient_id] - Filter by patient ID
* @param {number} [params.page] - Page number
* @param {number} [params.per_page] - Items per page
* @returns {Promise<Object>} Results list response
*/
export async function fetchResults(params = {}) {
const query = new URLSearchParams();
if (params.order_id) query.append('order_id', params.order_id);
if (params.patient_id) query.append('patient_id', params.patient_id);
if (params.page) query.append('page', params.page);
if (params.per_page) query.append('per_page', params.per_page);
const queryString = query.toString();
return get(queryString ? `/api/results?${queryString}` : '/api/results');
}
/**
* Fetch a single result by ID
* @param {number} id - Result ID
* @returns {Promise<Object>} Result details
*/
export async function fetchResultById(id) {
return get(`/api/results/${id}`);
}
/**
* Update a result value with auto-validation
* @param {number} id - Result ID
* @param {Object} data - Result data
* @param {string} data.Result - The result value
* @param {number} [data.RefNumID] - Reference range ID
* @param {string} [data.SampleType] - Sample type
* @param {number} [data.WorkstationID] - Workstation ID
* @param {number} [data.EquipmentID] - Equipment ID
* @returns {Promise<Object>} Updated result with flag
*/
export async function updateResult(id, data) {
return patch(`/api/results/${id}`, data);
}
/**
* Delete a result (soft delete)
* @param {number} id - Result ID
* @returns {Promise<Object>} Success response
*/
export async function deleteResult(id) {
return del(`/api/results/${id}`);
}
/**
* Fetch results by order ID
* @param {number} orderId - Order ID
* @returns {Promise<Object>} Results list for order
*/
export async function fetchResultsByOrder(orderId) {
return fetchResults({ order_id: orderId });
}
/**
* Fetch cumulative results by patient ID
* @param {number} patientId - Patient ID
* @param {number} [page] - Page number
* @param {number} [perPage] - Items per page
* @returns {Promise<Object>} Patient results history
*/
export async function fetchResultsByPatient(patientId, page = 1, perPage = 20) {
return fetchResults({ patient_id: patientId, page, per_page: perPage });
}

View File

@ -1,59 +1,85 @@
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
const STORAGE_KEY = 'auth_token';
import { get } from '$lib/api/client.js';
/**
* Create auth store with localStorage persistence
* Create auth store with cookie-based authentication
*/
function createAuthStore() {
// Get initial state from localStorage (only in browser)
const getInitialState = () => {
if (!browser) {
return { token: null, user: null, isAuthenticated: false };
}
const token = localStorage.getItem(STORAGE_KEY);
return {
token,
const { subscribe, set, update } = writable({
user: null,
isAuthenticated: !!token,
};
};
const { subscribe, set, update } = writable(getInitialState());
isAuthenticated: false,
loading: true,
});
return {
subscribe,
/**
* Set authentication data after login
* @param {string} token - JWT token
* Set user data after successful login
* Token is stored in HTTP-only cookie by backend
* @param {Object} user - User object
*/
login: (token, user) => {
if (browser) {
localStorage.setItem(STORAGE_KEY, token);
}
set({ token, user, isAuthenticated: true });
login: (user) => {
set({ user, isAuthenticated: true, loading: false });
},
/**
* Clear authentication data on logout
*/
logout: () => {
if (browser) {
localStorage.removeItem(STORAGE_KEY);
}
set({ token: null, user: null, isAuthenticated: false });
set({ user: null, isAuthenticated: false, loading: false });
},
/**
* Update user data without changing token
* Update user data without changing auth status
* @param {Object} user - Updated user object
*/
setUser: (user) => {
update((state) => ({ ...state, user }));
},
/**
* Check if user is authenticated via cookie
* @returns {Promise<boolean>}
*/
checkAuth: async () => {
if (!browser) return false;
try {
const response = await get('/api/auth/check');
// Backend returns user data nested under response.data
const userData = response.data || response;
if (response.status === 'success' && userData.userid) {
const user = {
id: userData.userid,
username: userData.username,
name: userData.NameFirst && userData.NameLast
? `${userData.NameFirst} ${userData.NameLast}`
: userData.username,
...userData
};
set({ user, isAuthenticated: true, loading: false });
return true;
} else {
set({ user: null, isAuthenticated: false, loading: false });
return false;
}
} catch (error) {
console.error('Auth check failed:', error.message);
set({ user: null, isAuthenticated: false, loading: false });
return false;
}
},
/**
* Set loading state
* @param {boolean} loading
*/
setLoading: (loading) => {
update((state) => ({ ...state, loading }));
},
};
}

View File

@ -1,6 +1,7 @@
<script>
import { goto } from '$app/navigation';
import { auth } from '$lib/stores/auth.js';
import { logout as logoutApi } from '$lib/api/auth.js';
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import Sidebar from '$lib/components/Sidebar.svelte';
@ -11,9 +12,10 @@
// Start closed on mobile, open on desktop
let sidebarOpen = $state(true);
onMount(() => {
// Check authentication
if (!$auth.isAuthenticated) {
onMount(async () => {
// Check authentication via cookie
const isAuthenticated = await auth.checkAuth();
if (!isAuthenticated) {
goto('/login');
} else {
checking = false;
@ -29,7 +31,14 @@
sidebarOpen = !sidebarOpen;
}
function handleLogout() {
async function handleLogout() {
try {
// Call server logout to clear the cookie
await logoutApi();
} catch (error) {
console.error('Logout error:', error);
}
// Clear client-side auth state
auth.logout();
goto('/login');
}

View File

@ -1,4 +1,5 @@
<script>
import { onMount } from 'svelte';
import {
Clock,
CheckCircle2,
@ -7,120 +8,313 @@
TrendingUp,
PieChart,
Plus,
UserCircle
UserCircle,
FlaskConical,
FileText,
ChevronRight,
RefreshCw
} from 'lucide-svelte';
import { fetchOrders } from '$lib/api/orders.js';
import { fetchResults } from '$lib/api/results.js';
import { fetchPatients } from '$lib/api/patients.js';
import { goto } from '$app/navigation';
import { error as toastError } from '$lib/utils/toast.js';
// Stats state
let stats = $state({
pendingOrders: 0,
todayResults: 0,
criticalResults: 0,
activePatients: 0
});
let loading = $state(true);
let recentActivity = $state([]);
// Fetch dashboard data
async function loadDashboardData() {
loading = true;
try {
// Fetch pending orders (SCH - Scheduled status)
const ordersResponse = await fetchOrders({
OrderStatus: 'SCH',
perPage: 1
});
stats.pendingOrders = ordersResponse.total || 0;
// Fetch today's results (we'll get all and filter)
const today = new Date().toISOString().split('T')[0];
const resultsResponse = await fetchResults({
per_page: 100
});
const allResults = resultsResponse.data || [];
// Count today's results
const todayResults = allResults.filter(r => {
if (!r.ResultDateTime) return false;
const resultDate = new Date(r.ResultDateTime).toISOString().split('T')[0];
return resultDate === today;
});
stats.todayResults = todayResults.length;
// Count critical results (H or L flags)
const criticalResults = allResults.filter(r => {
const value = parseFloat(r.Result);
if (isNaN(value)) return false;
const isHigh = r.High !== null && value > r.High;
const isLow = r.Low !== null && value < r.Low;
return isHigh || isLow;
});
stats.criticalResults = criticalResults.length;
// Fetch active patients
const patientsResponse = await fetchPatients({ perPage: 1 });
stats.activePatients = patientsResponse.total || 0;
// Generate recent activity from data
recentActivity = generateRecentActivity(allResults, ordersResponse.data || []);
} catch (err) {
toastError('Failed to load dashboard data');
console.error('Dashboard error:', err);
} finally {
loading = false;
}
}
function generateRecentActivity(results, orders) {
const activities = [];
// Add recent results
const recentResults = results
.filter(r => r.ResultDateTime)
.sort((a, b) => new Date(b.ResultDateTime) - new Date(a.ResultDateTime))
.slice(0, 3);
recentResults.forEach(r => {
activities.push({
type: 'result',
time: new Date(r.ResultDateTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
title: `Result: ${r.TestSiteCode}`,
description: `Order: ${r.OrderNumber || 'N/A'}`,
icon: CheckCircle2,
color: 'secondary'
});
});
// Add recent orders
const recentOrders = orders
.filter(o => o.CreateDate)
.sort((a, b) => new Date(b.CreateDate) - new Date(a.CreateDate))
.slice(0, 2);
recentOrders.forEach(o => {
activities.push({
type: 'order',
time: new Date(o.CreateDate).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
title: `Order #${o.OrderID}`,
description: `Patient: ${o.InternalPID || 'N/A'}`,
icon: Plus,
color: 'primary'
});
});
// Sort by time and take top 5
return activities
.sort((a, b) => {
const timeA = new Date(`2024-01-01 ${a.time}`);
const timeB = new Date(`2024-01-01 ${b.time}`);
return timeB - timeA;
})
.slice(0, 5);
}
function navigateTo(path) {
goto(path);
}
onMount(() => {
loadDashboardData();
});
</script>
<div class="p-4">
<!-- Summary Stats -->
<!-- Header with refresh -->
<div class="flex items-center justify-between mb-4">
<h1 class="text-2xl font-bold text-primary">Dashboard</h1>
<button
class="btn btn-sm btn-ghost"
onclick={loadDashboardData}
disabled={loading}
>
<RefreshCw class="w-4 h-4 {loading ? 'animate-spin' : ''}" />
</button>
</div>
<!-- Summary Stats -->
<div class="stats stats-vertical lg:stats-horizontal shadow w-full mb-4 bg-base-100/80 backdrop-blur">
<div class="stat border-l-4 border-primary hover:bg-primary/5 transition-colors py-2">
<!-- Pending Orders -->
<div
class="stat border-l-4 border-primary hover:bg-primary/5 transition-colors py-2 cursor-pointer"
onclick={() => navigateTo('/orders')}
>
<div class="stat-figure text-primary">
<Clock class="w-6 h-6" />
</div>
<div class="stat-title text-primary font-medium text-sm">Pending Orders</div>
<div class="stat-value text-primary text-2xl">24</div>
<div class="stat-desc text-primary/70 text-xs">Jan 1 - Feb 8</div>
<div class="stat-value text-primary text-2xl">
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
{:else}
{stats.pendingOrders}
{/if}
</div>
<div class="stat border-l-4 border-secondary hover:bg-secondary/5 transition-colors py-2">
<div class="stat-desc text-primary/70 text-xs">Scheduled for analysis</div>
</div>
<!-- Today's Results -->
<div
class="stat border-l-4 border-secondary hover:bg-secondary/5 transition-colors py-2 cursor-pointer"
onclick={() => navigateTo('/results')}
>
<div class="stat-figure text-secondary">
<CheckCircle2 class="w-6 h-6" />
</div>
<div class="stat-title text-secondary font-medium text-sm">Today's Results</div>
<div class="stat-value text-secondary text-2xl">156</div>
<div class="stat-desc text-secondary/70 text-xs">↗︎ 14% more than yesterday</div>
<div class="stat-value text-secondary text-2xl">
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
{:else}
{stats.todayResults}
{/if}
</div>
<div class="stat border-l-4 border-error hover:bg-error/5 transition-colors py-2">
<div class="stat-desc text-secondary/70 text-xs">Results entered today</div>
</div>
<!-- Critical Results -->
<div
class="stat border-l-4 border-error hover:bg-error/5 transition-colors py-2 cursor-pointer"
onclick={() => navigateTo('/results')}
>
<div class="stat-figure text-error">
<AlertTriangle class="w-6 h-6" />
</div>
<div class="stat-title text-error font-medium text-sm">Critical Results</div>
<div class="stat-value text-error text-2xl">3</div>
<div class="stat-value text-error text-2xl">
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
{:else}
{stats.criticalResults}
{/if}
</div>
<div class="stat-desc text-error/70 text-xs">Requires attention</div>
</div>
<div class="stat border-l-4 border-accent hover:bg-accent/5 transition-colors py-2">
<!-- Active Patients -->
<div
class="stat border-l-4 border-accent hover:bg-accent/5 transition-colors py-2 cursor-pointer"
onclick={() => navigateTo('/patients')}
>
<div class="stat-figure text-accent">
<Users class="w-6 h-6" />
</div>
<div class="stat-title text-accent font-medium text-sm">Active Patients</div>
<div class="stat-value text-accent text-2xl">89</div>
<div class="stat-value text-accent text-2xl">
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
{:else}
{stats.activePatients}
{/if}
</div>
<div class="stat-desc text-accent/70 text-xs">Currently in system</div>
</div>
</div>
<!-- Charts Section -->
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
<!-- Orders Trend Card -->
<div class="card bg-base-100 shadow border-t-4 border-primary hover:shadow-lg transition-shadow">
<div class="card-body p-4">
<h2 class="card-title text-primary text-base">
<TrendingUp class="w-4 h-4 mr-2" />
<div class="flex items-center justify-between mb-3">
<h2 class="card-title text-primary text-base flex items-center gap-2">
<TrendingUp class="w-4 h-4" />
Orders Trend
</h2>
<div class="h-48 flex items-center justify-center bg-gradient-to-br from-primary/10 to-accent/10 rounded-lg border border-primary/20">
<p class="text-primary/60">[Chart: Orders over time]</p>
<button class="btn btn-xs btn-ghost" onclick={() => navigateTo('/orders')}>
View All <ChevronRight class="w-3 h-3" />
</button>
</div>
</div>
</div>
<div class="card bg-base-100 shadow border-t-4 border-secondary hover:shadow-lg transition-shadow">
<div class="card-body p-4">
<h2 class="card-title text-secondary text-base">
<PieChart class="w-4 h-4 mr-2" />
Results Volume
</h2>
<div class="h-48 flex items-center justify-center bg-gradient-to-br from-secondary/10 to-accent/10 rounded-lg border border-secondary/20">
<p class="text-secondary/60">[Chart: Results by department]</p>
<div class="h-48 flex flex-col items-center justify-center bg-gradient-to-br from-primary/10 to-accent/10 rounded-lg border border-primary/20">
<div class="text-center">
<FlaskConical class="w-12 h-12 text-primary/30 mb-2" />
<p class="text-primary/60 text-sm">Order analytics coming soon</p>
<p class="text-primary/40 text-xs mt-1">Track orders over time</p>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<!-- Results Volume Card -->
<div class="card bg-base-100 shadow border-t-4 border-secondary hover:shadow-lg transition-shadow">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-3">
<h2 class="card-title text-secondary text-base flex items-center gap-2">
<PieChart class="w-4 h-4" />
Results Volume
</h2>
<button class="btn btn-xs btn-ghost" onclick={() => navigateTo('/results')}>
View All <ChevronRight class="w-3 h-3" />
</button>
</div>
<div class="h-48 flex flex-col items-center justify-center bg-gradient-to-br from-secondary/10 to-accent/10 rounded-lg border border-secondary/20">
<div class="text-center">
<FileText class="w-12 h-12 text-secondary/30 mb-2" />
<p class="text-secondary/60 text-sm">Results analytics coming soon</p>
<p class="text-secondary/40 text-xs mt-1">Results by test type</p>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="card bg-base-100 shadow border-t-4 border-primary hover:shadow-lg transition-shadow">
<div class="card-body p-4">
<h2 class="card-title mb-2 text-primary text-base">
<Clock class="w-4 h-4 mr-2" />
<h2 class="card-title mb-3 text-primary text-base flex items-center gap-2">
<Clock class="w-4 h-4" />
Recent Activity
</h2>
{#if loading}
<div class="flex justify-center p-4">
<span class="loading loading-spinner loading-md text-primary"></span>
</div>
{:else if recentActivity.length === 0}
<div class="text-center p-4 text-base-content/50">
<Clock class="w-8 h-8 mx-auto mb-2 opacity-30" />
<p class="text-sm">No recent activity</p>
</div>
{:else}
<ul class="timeline timeline-vertical timeline-compact">
{#each recentActivity as activity, i}
<li>
<div class="timeline-middle">
<div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
<Plus class="w-4 h-4 text-primary" />
<div class="w-8 h-8 rounded-full bg-{activity.color}/10 flex items-center justify-center">
<svelte:component this={activity.icon} class="w-4 h-4 text-{activity.color}" />
</div>
</div>
<div class="timeline-start timeline-box bg-gradient-to-r from-primary/5 to-white border-l-4 border-primary p-2">
<time class="text-xs font-mono text-primary">09:30 AM</time>
<div class="text-base font-bold text-primary/90">Order #12345 created</div>
<div class="text-xs text-primary/70">Patient: John Doe (P-1001)</div>
</div>
<hr class="bg-primary/30"/>
</li>
<li>
<div class="timeline-middle">
<div class="w-8 h-8 rounded-full bg-secondary/10 flex items-center justify-center">
<CheckCircle2 class="w-4 h-4 text-secondary" />
</div>
</div>
<div class="timeline-start timeline-box bg-gradient-to-r from-secondary/5 to-white border-l-4 border-secondary p-2">
<time class="text-xs font-mono text-secondary">09:15 AM</time>
<div class="text-base font-bold text-secondary/90">Result received</div>
<div class="text-xs text-secondary/70">Sample: ABC123 - Instrument: CBC-M01</div>
</div>
<hr class="bg-secondary/30"/>
</li>
<li>
<div class="timeline-middle">
<div class="w-8 h-8 rounded-full bg-accent/10 flex items-center justify-center">
<UserCircle class="w-4 h-4 text-accent" />
</div>
</div>
<div class="timeline-start timeline-box bg-gradient-to-r from-accent/5 to-white border-l-4 border-accent p-2">
<time class="text-xs font-mono text-accent">09:00 AM</time>
<div class="text-base font-bold text-accent/90">Patient registered</div>
<div class="text-xs text-accent/70">Patient ID: P-1001 - Jane Smith</div>
<div class="timeline-start timeline-box bg-gradient-to-r from-{activity.color}/5 to-white border-l-4 border-{activity.color} p-2">
<time class="text-xs font-mono text-{activity.color}">{activity.time}</time>
<div class="text-base font-bold text-{activity.color}/90">{activity.title}</div>
<div class="text-xs text-{activity.color}/70">{activity.description}</div>
</div>
{#if i < recentActivity.length - 1}
<hr class="bg-{activity.color}/30"/>
{/if}
</li>
{/each}
</ul>
{/if}
</div>
</div>
</div>

View File

@ -3,6 +3,7 @@
import Modal from '$lib/components/Modal.svelte';
import { ORDER_STATUS, ORDER_PRIORITY } from '$lib/api/orders.js';
import { fetchPatients } from '$lib/api/patients.js';
import { fetchTests } from '$lib/api/tests.js';
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
import { User, FlaskConical, Building2, Hash, FileText, AlertCircle, Plus, X, Search, Beaker } from 'lucide-svelte';
@ -40,7 +41,9 @@
let patientSearchResults = $state([]);
let showPatientSearch = $state(false);
let selectedPatient = $state(null);
let testInput = $state({ TestSiteID: '' });
let testSearchQuery = $state('');
let testSearchResults = $state([]);
let showTestSearch = $state(false);
// Reset form when modal opens
$effect(() => {
@ -82,6 +85,9 @@
}
formError = '';
showPatientSearch = false;
testSearchQuery = '';
testSearchResults = [];
showTestSearch = false;
}
});
@ -112,21 +118,42 @@
patientSearchResults = [];
}
function addTest() {
const testSiteId = parseInt(testInput.TestSiteID);
if (!testSiteId || isNaN(testSiteId)) {
formError = 'Please enter a valid Test Site ID';
return;
async function searchTests() {
if (!testSearchQuery.trim()) return;
formLoading = true;
try {
const query = testSearchQuery.trim();
const response = await fetchTests({
TestSiteCode: query,
TestSiteName: query,
perPage: 10
});
testSearchResults = response.data || [];
showTestSearch = true;
} catch (err) {
toastError('Failed to search tests');
testSearchResults = [];
} finally {
formLoading = false;
}
}
function addTest(test) {
// Check for duplicates
if (formData.Tests.some(t => t.TestSiteID === testSiteId)) {
if (formData.Tests.some(t => t.TestSiteID === test.TestSiteID)) {
formError = 'Test already added';
return;
}
formData.Tests = [...formData.Tests, { TestSiteID: testSiteId }];
testInput.TestSiteID = '';
formData.Tests = [...formData.Tests, {
TestSiteID: test.TestSiteID,
TestSiteCode: test.TestSiteCode,
TestSiteName: test.TestSiteName
}];
testSearchQuery = '';
testSearchResults = [];
showTestSearch = false;
formError = '';
}
@ -181,12 +208,13 @@
const priorityOptions = Object.values(ORDER_PRIORITY);
</script>
<Modal
<div class="tall-modal">
<Modal
bind:open
title={order ? 'Edit Order' : 'Create Order'}
size="xl"
onClose={handleClose}
>
>
<div class="space-y-4">
<!-- Error Alert -->
{#if formError}
@ -386,55 +414,81 @@
{/if}
</h4>
<!-- Add Test Input -->
<div class="flex gap-2 mb-3">
<!-- Add Test Search -->
<div class="space-y-2 mb-3">
<div class="flex gap-2">
<div class="input input-sm input-bordered flex items-center gap-2 flex-1 bg-base-100 focus-within:input-primary">
<FlaskConical class="w-4 h-4 text-gray-400" />
<Search class="w-4 h-4 text-gray-400" />
<input
type="number"
type="text"
class="grow bg-transparent outline-none"
placeholder="Enter Test Site ID"
bind:value={testInput.TestSiteID}
onkeydown={(e) => e.key === 'Enter' && (e.preventDefault(), addTest())}
placeholder="Search test by code or name..."
bind:value={testSearchQuery}
onkeydown={(e) => e.key === 'Enter' && searchTests()}
/>
</div>
<button
class="btn btn-primary btn-sm"
onclick={addTest}
onclick={searchTests}
disabled={formLoading || !testSearchQuery.trim()}
>
<Plus class="w-4 h-4" />
{#if formLoading}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Search class="w-4 h-4" />
{/if}
</button>
</div>
{#if showTestSearch && testSearchResults.length > 0}
<div class="border border-base-300 rounded-lg max-h-40 overflow-auto bg-base-100">
{#each testSearchResults as test (test.TestSiteID)}
<button
class="w-full text-left p-2 hover:bg-base-200 border-b border-base-200 last:border-b-0"
onclick={() => addTest(test)}
>
<div class="flex items-center gap-2">
<span class="text-xs font-mono bg-primary/10 text-primary px-1.5 py-0.5 rounded">{test.TestSiteCode}</span>
<span class="text-sm font-medium">{test.TestSiteName}</span>
</div>
</button>
{/each}
</div>
{:else if showTestSearch}
<p class="text-sm text-gray-500">No tests found</p>
{/if}
</div>
<!-- Tests List -->
<div class="flex-1 min-h-[200px] max-h-[400px] overflow-auto">
{#if formData.Tests.length > 0}
<div class="space-y-2">
<div class="space-y-1">
{#each formData.Tests as test, index (test.TestSiteID)}
<div class="flex items-center justify-between p-3 bg-base-100 rounded-lg border border-base-300">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
<span class="text-xs font-semibold text-primary">{index + 1}</span>
</div>
<div>
<p class="font-medium text-sm">Test Site ID</p>
<p class="text-lg font-bold text-primary">{test.TestSiteID}</p>
</div>
<div class="flex items-center justify-between p-2 bg-base-100 rounded border border-base-300 hover:border-primary/50 transition-colors">
<div class="flex items-center gap-2 min-w-0">
{#if test.TestSiteCode}
<span class="text-xs font-mono bg-primary/10 text-primary px-1.5 py-0.5 rounded shrink-0">{test.TestSiteCode}</span>
{:else}
<span class="text-xs font-mono bg-base-200 px-1.5 py-0.5 rounded shrink-0">ID:{test.TestSiteID}</span>
{/if}
<span class="text-sm truncate" title={test.TestSiteName || ''}>
{test.TestSiteName || `Test #${test.TestSiteID}`}
</span>
</div>
<button
class="btn btn-ghost btn-sm btn-square text-error hover:bg-error/10"
class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/10 shrink-0"
onclick={() => removeTest(index)}
>
<X class="w-4 h-4" />
<X class="w-3 h-3" />
</button>
</div>
{/each}
</div>
{:else}
<div class="flex flex-col items-center justify-center h-full text-gray-400 py-8">
<Beaker class="w-12 h-12 mb-2 opacity-50" />
<Beaker class="w-10 h-10 mb-2 opacity-50" />
<p class="text-sm">No tests added yet</p>
<p class="text-xs mt-1">Enter a Test Site ID and click Add</p>
<p class="text-xs mt-1">Search and select tests above</p>
</div>
{/if}
</div>
@ -462,3 +516,10 @@
</button>
{/snippet}
</Modal>
</div>
<style>
.tall-modal :global(.modal-box) {
max-height: 90vh;
}
</style>

View File

@ -0,0 +1,311 @@
<script>
import { onMount } from 'svelte';
import {
Search,
FileText,
Printer,
Eye,
ChevronLeft,
ChevronRight,
AlertCircle,
CheckCircle2,
Clock,
Download
} from 'lucide-svelte';
import { fetchOrders } from '$lib/api/orders.js';
import { getReportUrl } from '$lib/api/reports.js';
import { error as toastError } from '$lib/utils/toast.js';
import ReportViewerModal from './ReportViewerModal.svelte';
// State
let orders = $state([]);
let loading = $state(false);
let currentPage = $state(1);
let perPage = $state(20);
let totalItems = $state(0);
// Filters
let filterStatus = $state('');
let filterOrderId = $state('');
let filterPatientId = $state('');
// Modal state
let selectedOrderId = $state(null);
let showReportModal = $state(false);
const orderStatuses = [
{ value: 'ORD', label: 'Ordered', color: 'badge-neutral' },
{ value: 'SCH', label: 'Scheduled', color: 'badge-info' },
{ value: 'ANA', label: 'Analysis', color: 'badge-warning' },
{ value: 'VER', label: 'Verified', color: 'badge-success' },
{ value: 'REV', label: 'Reviewed', color: 'badge-primary' },
{ value: 'REP', label: 'Reported', color: 'badge-secondary' }
];
async function loadOrders() {
loading = true;
try {
const params = {
page: currentPage,
perPage: perPage
};
if (filterStatus) params.OrderStatus = filterStatus;
if (filterOrderId) params.OrderID = filterOrderId;
if (filterPatientId) params.InternalPID = filterPatientId;
const response = await fetchOrders(params);
if (response.status === 'success') {
orders = response.data || [];
totalItems = response.total || orders.length;
} else {
orders = [];
totalItems = 0;
}
} catch (err) {
toastError(err.message || 'Failed to load orders');
orders = [];
} finally {
loading = false;
}
}
function handleViewReport(orderId) {
selectedOrderId = orderId;
showReportModal = true;
}
function handlePrintReport(orderId) {
const url = getReportUrl(orderId);
const printWindow = window.open(url, '_blank');
if (printWindow) {
printWindow.onload = () => {
printWindow.print();
};
}
}
function handlePageChange(newPage) {
currentPage = newPage;
loadOrders();
}
function applyFilters() {
currentPage = 1;
loadOrders();
}
function clearFilters() {
filterStatus = '';
filterOrderId = '';
filterPatientId = '';
currentPage = 1;
loadOrders();
}
function getStatusBadge(status) {
const statusConfig = orderStatuses.find(s => s.value === status);
return statusConfig?.color || 'badge-neutral';
}
function getStatusLabel(status) {
const statusConfig = orderStatuses.find(s => s.value === status);
return statusConfig?.label || status;
}
onMount(() => {
loadOrders();
});
const totalPages = $derived(Math.ceil(totalItems / perPage));
</script>
<div class="p-4 space-y-4">
<!-- Header -->
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-primary flex items-center gap-2">
<FileText class="w-6 h-6" />
Lab Reports
</h1>
<div class="text-sm text-base-content/60">
Total: {totalItems} orders
</div>
</div>
<!-- Filters -->
<div class="card bg-base-100 shadow compact-card">
<div class="card-body p-3">
<div class="flex flex-wrap gap-3 items-end">
<div class="form-control w-32">
<label class="label compact-label">
<span class="label-text text-xs">Order ID</span>
</label>
<input
type="text"
class="input input-sm input-bordered"
placeholder="Search..."
bind:value={filterOrderId}
/>
</div>
<div class="form-control w-32">
<label class="label compact-label">
<span class="label-text text-xs">Patient ID</span>
</label>
<input
type="text"
class="input input-sm input-bordered"
placeholder="Filter..."
bind:value={filterPatientId}
/>
</div>
<div class="form-control w-36">
<label class="label compact-label">
<span class="label-text text-xs">Status</span>
</label>
<select class="select select-sm select-bordered" bind:value={filterStatus}>
<option value="">All Statuses</option>
{#each orderStatuses as status}
<option value={status.value}>{status.label}</option>
{/each}
</select>
</div>
<div class="flex gap-2 ml-auto">
<button class="btn btn-sm btn-primary" onclick={applyFilters}>
<Search class="w-4 h-4" />
Search
</button>
<button class="btn btn-sm btn-ghost" onclick={clearFilters}>
Clear
</button>
</div>
</div>
</div>
</div>
<!-- Orders Table -->
<div class="card bg-base-100 shadow compact-card">
<div class="card-body p-0">
{#if loading}
<div class="flex justify-center p-8">
<span class="loading loading-spinner loading-md text-primary"></span>
</div>
{:else if orders.length === 0}
<div class="text-center p-8 text-base-content/50">
<FileText class="w-12 h-12 mx-auto mb-2 opacity-30" />
<p>No orders found</p>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr class="bg-base-200">
<th class="text-xs font-semibold">Order ID</th>
<th class="text-xs font-semibold">Patient ID</th>
<th class="text-xs font-semibold">Visit ID</th>
<th class="text-xs font-semibold">Status</th>
<th class="text-xs font-semibold">Priority</th>
<th class="text-xs font-semibold">Created</th>
<th class="text-xs font-semibold text-right">Actions</th>
</tr>
</thead>
<tbody>
{#each orders as order}
<tr class="hover:bg-base-200/50 transition-colors">
<td class="text-xs font-mono font-medium">{order.OrderID}</td>
<td class="text-xs">{order.InternalPID || '-'}</td>
<td class="text-xs">{order.PatVisitID || '-'}</td>
<td class="text-xs">
<span class="badge {getStatusBadge(order.OrderStatus)} badge-sm">
{getStatusLabel(order.OrderStatus)}
</span>
</td>
<td class="text-xs">
{#if order.Priority === 'U'}
<span class="badge badge-error badge-sm">Urgent</span>
{:else if order.Priority === 'S'}
<span class="badge badge-warning badge-sm">Stat</span>
{:else}
<span class="badge badge-neutral badge-sm">Routine</span>
{/if}
</td>
<td class="text-xs text-base-content/60">
{order.CreateDate ? new Date(order.CreateDate).toLocaleDateString() : '-'}
</td>
<td class="text-right">
<div class="flex gap-1 justify-end">
<button
class="btn btn-ghost btn-xs text-primary"
title="View Report"
onclick={() => handleViewReport(order.OrderID)}
>
<Eye class="w-3 h-3" />
</button>
<button
class="btn btn-ghost btn-xs"
title="Print Report"
onclick={() => handlePrintReport(order.OrderID)}
>
<Printer class="w-3 h-3" />
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Pagination -->
{#if totalPages > 1}
<div class="flex justify-center p-3 border-t border-base-200">
<div class="join">
<button
class="join-item btn btn-sm"
disabled={currentPage === 1}
onclick={() => handlePageChange(currentPage - 1)}
>
<ChevronLeft class="w-4 h-4" />
</button>
<span class="join-item btn btn-sm btn-ghost">
Page {currentPage} of {totalPages}
</span>
<button
class="join-item btn btn-sm"
disabled={currentPage >= totalPages}
onclick={() => handlePageChange(currentPage + 1)}
>
<ChevronRight class="w-4 h-4" />
</button>
</div>
</div>
{/if}
{/if}
</div>
</div>
<!-- Status Legend -->
<div class="card bg-base-100 shadow compact-card">
<div class="card-body p-3">
<h3 class="text-sm font-semibold mb-2">Order Status Pipeline</h3>
<div class="flex flex-wrap gap-2 text-xs">
{#each orderStatuses as status}
<div class="flex items-center gap-1">
<span class="badge {status.color} badge-sm">{status.label}</span>
</div>
{/each}
</div>
</div>
</div>
</div>
<!-- Report Viewer Modal -->
{#if showReportModal && selectedOrderId}
<ReportViewerModal
orderId={selectedOrderId}
bind:open={showReportModal}
/>
{/if}

View File

@ -0,0 +1,137 @@
<script>
import { onMount } from 'svelte';
import { FileText, Printer, Download, X, ExternalLink } from 'lucide-svelte';
import { getReportUrl } from '$lib/api/reports.js';
import Modal from '$lib/components/Modal.svelte';
let {
orderId = $bindable(),
open = $bindable(false)
} = $props();
let reportUrl = $state('');
let loading = $state(true);
let error = $state('');
$effect(() => {
if (open && orderId) {
loadReport();
}
});
function loadReport() {
loading = true;
error = '';
// Build report URL - use the direct API endpoint
const baseUrl = PUBLIC_API_URL || 'http://localhost/clqms01';
reportUrl = `${baseUrl}/api/reports/${orderId}`;
// Simulate loading delay to show spinner
setTimeout(() => {
loading = false;
}, 500);
}
function handlePrint() {
const printWindow = window.open(reportUrl, '_blank');
if (printWindow) {
printWindow.onload = () => {
printWindow.print();
};
}
}
function handleOpenInNewTab() {
window.open(reportUrl, '_blank');
}
function handleClose() {
open = false;
error = '';
}
function handleIframeLoad() {
loading = false;
}
function handleIframeError() {
loading = false;
error = 'Failed to load report. The order may not have results yet.';
}
</script>
<Modal bind:open={open} title="Lab Report" size="xl">
{#snippet children()}
<div class="flex flex-col h-[80vh]">
<!-- Toolbar -->
<div class="flex items-center justify-between gap-3 pb-3 border-b border-base-200 mb-3">
<div class="flex items-center gap-2">
<FileText class="w-5 h-5 text-primary" />
<span class="font-semibold">Order: {orderId}</span>
</div>
<div class="flex gap-2">
<button
class="btn btn-sm btn-ghost"
onclick={handleOpenInNewTab}
title="Open in new tab"
>
<ExternalLink class="w-4 h-4" />
</button>
<button
class="btn btn-sm btn-ghost"
onclick={handlePrint}
title="Print report"
>
<Printer class="w-4 h-4" />
</button>
</div>
</div>
<!-- Report Content -->
<div class="flex-1 relative bg-white rounded-lg border border-base-200 overflow-hidden">
{#if loading}
<div class="absolute inset-0 flex items-center justify-center bg-base-100">
<div class="text-center">
<span class="loading loading-spinner loading-lg text-primary mb-4"></span>
<p class="text-base-content/60">Loading report...</p>
</div>
</div>
{/if}
{#if error}
<div class="absolute inset-0 flex items-center justify-center bg-base-100 p-8">
<div class="text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-error/10 flex items-center justify-center">
<FileText class="w-8 h-8 text-error" />
</div>
<h3 class="text-lg font-semibold text-error mb-2">Report Error</h3>
<p class="text-base-content/60 mb-4">{error}</p>
<p class="text-sm text-base-content/40">Note: Reports are only available for orders that have been reported (REP status)</p>
</div>
</div>
{:else}
<iframe
src={reportUrl}
class="w-full h-full border-0"
title="Lab Report"
onload={handleIframeLoad}
onerror={handleIframeError}
sandbox="allow-same-origin allow-scripts"
></iframe>
{/if}
</div>
</div>
{/snippet}
{#snippet footer()}
<button class="btn btn-ghost btn-sm" onclick={handleClose}>
<X class="w-4 h-4" />
Close
</button>
<button class="btn btn-primary btn-sm" onclick={handlePrint}>
<Printer class="w-4 h-4" />
Print Report
</button>
{/snippet}
</Modal>

View File

@ -0,0 +1,287 @@
<script>
import { onMount } from 'svelte';
import {
Search,
Filter,
FileText,
AlertTriangle,
CheckCircle2,
Clock,
ChevronLeft,
ChevronRight,
User,
FlaskConical,
Edit3,
ClipboardList
} from 'lucide-svelte';
import { fetchOrders, getStatusInfo, getPriorityInfo } from '$lib/api/orders.js';
import { error as toastError, success as toastSuccess } from '$lib/utils/toast.js';
import ResultEntryModal from './ResultEntryModal.svelte';
// State
let orders = $state([]);
let loading = $state(false);
let currentPage = $state(1);
let perPage = $state(20);
let totalItems = $state(0);
// Filters
let filterOrderId = $state('');
let filterPatientId = $state('');
let filterStatus = $state(''); // Empty = all statuses
// Modal state
let selectedOrder = $state(null);
let showEntryModal = $state(false);
async function loadOrders() {
loading = true;
try {
const params = {
page: currentPage,
perPage: perPage,
include: 'details'
};
if (filterOrderId) params.OrderID = filterOrderId;
if (filterPatientId) params.InternalPID = parseInt(filterPatientId);
if (filterStatus) params.OrderStatus = filterStatus;
const response = await fetchOrders(params);
if (response.status === 'success') {
orders = response.data || [];
totalItems = response.total || orders.length;
} else {
orders = [];
totalItems = 0;
}
} catch (err) {
toastError(err.message || 'Failed to load orders');
orders = [];
} finally {
loading = false;
}
}
function handleEnterResults(order) {
selectedOrder = order;
showEntryModal = true;
}
function handleResultsSaved() {
showEntryModal = false;
selectedOrder = null;
loadOrders();
toastSuccess('Results saved successfully');
}
function handlePageChange(newPage) {
currentPage = newPage;
loadOrders();
}
function applyFilters() {
currentPage = 1;
loadOrders();
}
function clearFilters() {
filterOrderId = '';
filterPatientId = '';
filterStatus = '';
currentPage = 1;
loadOrders();
}
onMount(() => {
loadOrders();
});
const totalPages = $derived(Math.ceil(totalItems / perPage));
</script>
<div class="p-4 space-y-4">
<!-- Header -->
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-primary flex items-center gap-2">
<FlaskConical class="w-6 h-6" />
Results Entry
</h1>
<div class="text-sm text-base-content/60">
Total: {totalItems} orders
</div>
</div>
<!-- Filters -->
<div class="card bg-base-100 shadow compact-card">
<div class="card-body p-3">
<div class="flex flex-wrap gap-3 items-end">
<div class="form-control w-48">
<span class="label-text text-xs mb-1">Order ID</span>
<label class="input input-sm input-bordered flex items-center gap-2">
<FileText class="w-3 h-3 text-gray-400" />
<input
type="text"
id="filter-order-id"
class="grow bg-transparent outline-none text-sm"
placeholder="Filter by Order ID..."
bind:value={filterOrderId}
/>
</label>
</div>
<div class="form-control w-40">
<span class="label-text text-xs mb-1">Patient ID</span>
<label class="input input-sm input-bordered flex items-center gap-2">
<User class="w-3 h-3 text-gray-400" />
<input
type="text"
id="filter-patient-id"
class="grow bg-transparent outline-none text-sm"
placeholder="Filter..."
bind:value={filterPatientId}
/>
</label>
</div>
<div class="form-control w-40">
<label class="label-text text-xs mb-1" for="filter-status">Status</label>
<select
id="filter-status"
class="select select-sm select-bordered"
bind:value={filterStatus}
>
<option value="">All Statuses</option>
<option value="ORD">Ordered</option>
<option value="SCH">Scheduled</option>
<option value="ANA">Analysis</option>
<option value="VER">Verified</option>
</select>
</div>
<div class="flex gap-2 ml-auto">
<button class="btn btn-sm btn-primary" onclick={applyFilters}>
<Filter class="w-4 h-4" />
Apply
</button>
<button class="btn btn-sm btn-ghost" onclick={clearFilters}>
Clear
</button>
</div>
</div>
</div>
</div>
<!-- Orders Table -->
<div class="card bg-base-100 shadow compact-card">
<div class="card-body p-0">
{#if loading}
<div class="flex justify-center p-8">
<span class="loading loading-spinner loading-md text-primary"></span>
</div>
{:else if orders.length === 0}
<div class="text-center p-8 text-base-content/50">
<ClipboardList class="w-12 h-12 mx-auto mb-2 opacity-30" />
<p>No orders found</p>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr class="bg-base-200">
<th class="text-xs font-semibold">Order ID</th>
<th class="text-xs font-semibold">Patient</th>
<th class="text-xs font-semibold">Order Date</th>
<th class="text-xs font-semibold">Status</th>
<th class="text-xs font-semibold">Priority</th>
<th class="text-xs font-semibold">Tests</th>
<th class="text-xs font-semibold text-right">Actions</th>
</tr>
</thead>
<tbody>
{#each orders as order (order.OrderID)}
{@const statusInfo = getStatusInfo(order.OrderStatus)}
{@const priorityInfo = getPriorityInfo(order.Priority)}
{@const testCount = order.Tests?.length || 0}
{@const pendingCount = order.Tests?.filter(t => !t.Result || t.Result === '').length || 0}
<tr class="hover:bg-base-200/50 transition-colors">
<td class="text-xs font-mono font-medium">{order.OrderID}</td>
<td class="text-xs">
<div class="flex flex-col">
<span class="font-medium">{order.PatientName || '-'}</span>
<span class="text-base-content/50">ID: {order.InternalPID}</span>
</div>
</td>
<td class="text-xs text-base-content/70">
{order.OrderDateTime ? new Date(order.OrderDateTime).toLocaleDateString() : '-'}
</td>
<td class="text-xs">
<span class="badge {statusInfo.color} badge-sm">{statusInfo.label}</span>
</td>
<td class="text-xs">
<span class="badge {priorityInfo.color} badge-sm">{priorityInfo.label}</span>
</td>
<td class="text-xs">
<div class="flex items-center gap-1">
<span class="font-medium">{testCount}</span>
{#if pendingCount > 0}
<span class="text-warning">({pendingCount} pending)</span>
{:else}
<CheckCircle2 class="w-3 h-3 text-success" />
{/if}
</div>
</td>
<td class="text-right">
<button
class="btn btn-primary btn-sm"
title="Enter Results"
onclick={() => handleEnterResults(order)}
>
<Edit3 class="w-3 h-3" />
Enter Results
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Pagination -->
{#if totalPages > 1}
<div class="flex justify-center p-3 border-t border-base-200">
<div class="join">
<button
class="join-item btn btn-sm"
disabled={currentPage === 1}
onclick={() => handlePageChange(currentPage - 1)}
>
<ChevronLeft class="w-4 h-4" />
</button>
<span class="join-item btn btn-sm btn-ghost">
Page {currentPage} of {totalPages}
</span>
<button
class="join-item btn btn-sm"
disabled={currentPage >= totalPages}
onclick={() => handlePageChange(currentPage + 1)}
>
<ChevronRight class="w-4 h-4" />
</button>
</div>
</div>
{/if}
{/if}
</div>
</div>
</div>
<!-- Result Entry Modal -->
{#if showEntryModal && selectedOrder}
<ResultEntryModal
order={selectedOrder}
bind:open={showEntryModal}
onSaved={handleResultsSaved}
/>
{/if}

View File

@ -0,0 +1,301 @@
<script>
import { FlaskConical, AlertTriangle, CheckCircle2, X, Save, User, FileText } from 'lucide-svelte';
import { updateResult } from '$lib/api/results.js';
import { error as toastError, success as toastSuccess } from '$lib/utils/toast.js';
import Modal from '$lib/components/Modal.svelte';
let {
order = $bindable(),
open = $bindable(false),
onSaved
} = $props();
// Form state - array of result entries
let results = $state([]);
let formLoading = $state(false);
let saveProgress = $state({ current: 0, total: 0 });
// Initialize results when order changes
$effect(() => {
if (order && open) {
// Map tests to editable result entries
results = (order.Tests || []).map(test => ({
ResultID: test.ResultID,
TestSiteID: test.TestSiteID,
TestSiteCode: test.TestSiteCode,
TestSiteName: test.TestSiteName,
Result: test.Result || '',
SampleType: test.SampleType || '',
WorkstationID: test.WorkstationID || '',
EquipmentID: test.EquipmentID || '',
Unit1: test.Unit1,
Low: test.Low,
High: test.High,
LowSign: test.LowSign,
HighSign: test.HighSign,
RefDisplay: test.RefDisplay,
RefNumID: test.RefNumID,
flag: calculateFlag(test.Result, test.Low, test.High),
saved: false,
error: null
}));
saveProgress = { current: 0, total: 0 };
}
});
function calculateFlag(value, low, high) {
if (!value || value === '') return null;
const numValue = parseFloat(value);
if (isNaN(numValue)) return null;
if (low !== null && numValue < low) return 'L';
if (high !== null && numValue > high) return 'H';
return null;
}
function updateResultFlag(index) {
const entry = results[index];
results[index].flag = calculateFlag(entry.Result, entry.Low, entry.High);
}
function getFlagColor(flag) {
if (flag === 'H') return 'text-error';
if (flag === 'L') return 'text-warning';
return 'text-success';
}
function getFlagBg(flag) {
if (flag === 'H') return 'bg-error/10';
if (flag === 'L') return 'bg-warning/10';
return 'bg-success/10';
}
function getInputBg(flag) {
if (flag === 'H') return 'bg-error/5 border-error/30';
if (flag === 'L') return 'bg-warning/5 border-warning/30';
return '';
}
async function handleSaveAll() {
// Filter to only entries with values that haven't been saved yet
const entriesToSave = results.filter(r => r.Result && r.Result.trim() !== '' && !r.saved);
if (entriesToSave.length === 0) {
toastError('No results to save');
return;
}
formLoading = true;
saveProgress = { current: 0, total: entriesToSave.length };
try {
// Save each result sequentially
for (let i = 0; i < entriesToSave.length; i++) {
const entry = entriesToSave[i];
saveProgress.current = i + 1;
const data = {
Result: entry.Result,
SampleType: entry.SampleType || null,
WorkstationID: entry.WorkstationID ? parseInt(entry.WorkstationID) : null,
EquipmentID: entry.EquipmentID ? parseInt(entry.EquipmentID) : null,
RefNumID: entry.RefNumID || null
};
const response = await updateResult(entry.ResultID, data);
if (response.status === 'success') {
// Mark as saved
const resultIndex = results.findIndex(r => r.ResultID === entry.ResultID);
if (resultIndex !== -1) {
results[resultIndex].saved = true;
results[resultIndex].error = null;
}
} else {
// Mark with error
const resultIndex = results.findIndex(r => r.ResultID === entry.ResultID);
if (resultIndex !== -1) {
results[resultIndex].error = response.message || 'Failed to save';
}
}
}
// Check if all were saved successfully
const failedCount = results.filter(r => r.error).length;
if (failedCount === 0) {
toastSuccess(`Saved ${entriesToSave.length} result(s) successfully`);
onSaved?.();
} else {
toastError(`${failedCount} result(s) failed to save`);
}
} catch (err) {
toastError(err.message || 'Failed to save results');
} finally {
formLoading = false;
saveProgress = { current: 0, total: 0 };
}
}
function handleClose() {
open = false;
}
function handleKeyDown(event, index) {
// Navigate with Enter/Arrow keys
if (event.key === 'Enter') {
event.preventDefault();
const nextIndex = index + 1;
if (nextIndex < results.length) {
const nextInput = document.getElementById(`result-input-${nextIndex}`);
nextInput?.focus();
nextInput?.select();
}
}
}
const pendingCount = $derived(results.filter(r => !r.Result || r.Result === '').length);
const savedCount = $derived(results.filter(r => r.saved).length);
</script>
<Modal bind:open={open} title="Enter Results - Order {order?.OrderID}" size="xl">
{#snippet children()}
<div class="space-y-4">
<!-- Order Info Header -->
<div class="bg-base-200/50 rounded-lg p-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<User class="w-4 h-4 text-primary" />
<span class="text-sm font-medium">{order?.PatientName || 'Unknown'}</span>
<span class="text-xs text-base-content/50">(ID: {order?.InternalPID})</span>
</div>
<div class="flex items-center gap-2">
<FileText class="w-4 h-4 text-primary" />
<span class="text-xs text-base-content/70">{order?.Tests?.length || 0} tests</span>
</div>
</div>
<div class="flex items-center gap-2 text-xs">
{#if pendingCount > 0}
<span class="badge badge-warning badge-sm">{pendingCount} pending</span>
{/if}
{#if savedCount > 0}
<span class="badge badge-success badge-sm">{savedCount} saved</span>
{/if}
</div>
</div>
</div>
<!-- Results Table -->
<div class="overflow-x-auto max-h-[400px] overflow-y-auto">
<table class="table table-sm">
<thead class="sticky top-0 bg-base-100 z-10">
<tr class="bg-base-200">
<th class="text-xs font-semibold w-16">Code</th>
<th class="text-xs font-semibold">Test Name</th>
<th class="text-xs font-semibold w-32">Result</th>
<th class="text-xs font-semibold w-16">Flag</th>
<th class="text-xs font-semibold w-24">Reference</th>
<th class="text-xs font-semibold w-20">Unit</th>
</tr>
</thead>
<tbody>
{#each results as result, index (result.ResultID)}
<tr class="hover:bg-base-200/30 {result.saved ? 'opacity-60' : ''}">
<td class="text-xs font-mono">{result.TestSiteCode}</td>
<td class="text-xs">
{result.TestSiteName}
{#if result.error}
<div class="text-error text-xs">{result.error}</div>
{/if}
</td>
<td class="p-1">
<label class="input input-sm input-bordered flex items-center gap-1 w-full {getInputBg(result.flag)}">
{#if result.flag === 'H'}
<AlertTriangle class="w-3 h-3 text-error flex-shrink-0" />
{:else if result.flag === 'L'}
<AlertTriangle class="w-3 h-3 text-warning flex-shrink-0" />
{:else if result.Result}
<CheckCircle2 class="w-3 h-3 text-success flex-shrink-0" />
{/if}
<input
id="result-input-{index}"
type="text"
class="grow bg-transparent outline-none text-sm font-mono"
placeholder="..."
bind:value={result.Result}
oninput={() => updateResultFlag(index)}
onkeydown={(e) => handleKeyDown(e, index)}
disabled={result.saved || formLoading}
/>
{#if result.Unit1}
<span class="text-xs text-base-content/50 flex-shrink-0">{result.Unit1}</span>
{/if}
</label>
</td>
<td class="text-xs text-center">
{#if result.flag}
<span class="badge {result.flag === 'H' ? 'badge-error' : 'badge-warning'} badge-sm">
{result.flag}
</span>
{:else}
<span class="text-base-content/30">-</span>
{/if}
</td>
<td class="text-xs text-base-content/60">{result.RefDisplay || '-'}</td>
<td class="text-xs">{result.Unit1 || '-'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Legend -->
<div class="flex gap-4 text-xs text-base-content/60 pt-2 border-t">
<div class="flex items-center gap-1">
<span class="badge badge-error badge-sm">H</span>
<span>High</span>
</div>
<div class="flex items-center gap-1">
<span class="badge badge-warning badge-sm">L</span>
<span>Low</span>
</div>
<div class="flex items-center gap-1">
<CheckCircle2 class="w-3 h-3 text-success" />
<span>Normal</span>
</div>
<div class="flex-1 text-right">
<span class="text-base-content/40">Press Enter to move to next field</span>
</div>
</div>
<!-- Progress -->
{#if formLoading && saveProgress.total > 0}
<div class="flex items-center gap-2 text-sm">
<span class="loading loading-spinner loading-xs"></span>
<span>Saving {saveProgress.current} of {saveProgress.total}...</span>
</div>
{/if}
</div>
{/snippet}
{#snippet footer()}
<button class="btn btn-ghost btn-sm" onclick={handleClose} disabled={formLoading}>
<X class="w-4 h-4" />
Close
</button>
<button
class="btn btn-primary btn-sm"
onclick={handleSaveAll}
disabled={formLoading || pendingCount === results.length}
>
{#if formLoading}
<span class="loading loading-spinner loading-xs"></span>
Saving...
{:else}
<Save class="w-4 h-4" />
Save All Results
{/if}
</button>
{/snippet}
</Modal>

View File

@ -62,7 +62,13 @@
try {
const response = await login(username, password);
auth.login(response.token, response.user);
// Cookie is set by backend, now fetch user data
const isAuthenticated = await auth.checkAuth();
if (!isAuthenticated) {
throw new Error('Failed to verify authentication');
}
if (rememberMe) {
localStorage.setItem('clqms_username', username);
@ -72,7 +78,7 @@
localStorage.setItem('clqms_remember', 'false');
}
goto('/dashboard');
await goto('/dashboard');
} catch (err) {
error = err.message || 'Login failed. Please try again.';
} finally {