feat: Refactor patients page into modular components with separate modals
This commit is contained in:
parent
382b05d98e
commit
d5864d40ec
@ -819,35 +819,83 @@ components:
|
|||||||
format: date-time
|
format: date-time
|
||||||
|
|
||||||
# ValueSets
|
# ValueSets
|
||||||
|
ValueSetLibItem:
|
||||||
|
type: object
|
||||||
|
description: Library/system value set item from JSON files
|
||||||
|
properties:
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
description: The value/key code
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
description: The display label
|
||||||
|
|
||||||
ValueSetDef:
|
ValueSetDef:
|
||||||
type: object
|
type: object
|
||||||
|
description: User-defined value set definition (from database)
|
||||||
properties:
|
properties:
|
||||||
id:
|
VSetID:
|
||||||
type: integer
|
type: integer
|
||||||
VSetCode:
|
description: Primary key
|
||||||
|
SiteID:
|
||||||
|
type: integer
|
||||||
|
description: Site reference
|
||||||
|
VSName:
|
||||||
type: string
|
type: string
|
||||||
VSetName:
|
description: Value set name
|
||||||
|
VSDesc:
|
||||||
type: string
|
type: string
|
||||||
Description:
|
description: Value set description
|
||||||
|
CreateDate:
|
||||||
type: string
|
type: string
|
||||||
Category:
|
format: date-time
|
||||||
|
description: Creation timestamp
|
||||||
|
EndDate:
|
||||||
type: string
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
description: Soft delete timestamp
|
||||||
|
ItemCount:
|
||||||
|
type: integer
|
||||||
|
description: Number of items in this value set
|
||||||
|
|
||||||
ValueSetItem:
|
ValueSetItem:
|
||||||
type: object
|
type: object
|
||||||
|
description: User-defined value set item (from database)
|
||||||
properties:
|
properties:
|
||||||
id:
|
VID:
|
||||||
type: integer
|
type: integer
|
||||||
|
description: Primary key
|
||||||
|
SiteID:
|
||||||
|
type: integer
|
||||||
|
description: Site reference
|
||||||
VSetID:
|
VSetID:
|
||||||
type: integer
|
type: integer
|
||||||
|
description: Reference to value set definition
|
||||||
|
VOrder:
|
||||||
|
type: integer
|
||||||
|
description: Display order
|
||||||
VValue:
|
VValue:
|
||||||
type: string
|
type: string
|
||||||
VLabel:
|
description: The value code
|
||||||
|
VDesc:
|
||||||
type: string
|
type: string
|
||||||
VSeq:
|
description: The display description/label
|
||||||
type: integer
|
VCategory:
|
||||||
IsActive:
|
type: string
|
||||||
type: boolean
|
description: Category code
|
||||||
|
CreateDate:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Creation timestamp
|
||||||
|
EndDate:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
description: Soft delete timestamp
|
||||||
|
VSName:
|
||||||
|
type: string
|
||||||
|
description: Value set name (from joined definition)
|
||||||
|
|
||||||
# Master Data
|
# Master Data
|
||||||
Location:
|
Location:
|
||||||
@ -1391,10 +1439,21 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
data:
|
data:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/PatientVisit'
|
$ref: '#/components/schemas/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:
|
post:
|
||||||
tags: [Patient Visits]
|
tags: [Patient Visits]
|
||||||
@ -2863,7 +2922,7 @@ paths:
|
|||||||
get:
|
get:
|
||||||
tags: [ValueSets]
|
tags: [ValueSets]
|
||||||
summary: List lib value sets
|
summary: List lib value sets
|
||||||
description: List all library/system value sets from JSON files
|
description: List all library/system value sets from JSON files with item counts. Returns an object where keys are value set names and values are item counts.
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -2871,7 +2930,7 @@ paths:
|
|||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
description: Optional search term to filter value sets
|
description: Optional search term to filter value set names
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: List of lib value sets with item counts
|
description: List of lib value sets with item counts
|
||||||
@ -2882,17 +2941,72 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
example: success
|
||||||
data:
|
data:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: integer
|
type: integer
|
||||||
description: Number of items in each value set
|
description: Number of items in each value set
|
||||||
|
example:
|
||||||
|
sex: 3
|
||||||
|
marital_status: 6
|
||||||
|
order_status: 6
|
||||||
|
|
||||||
/api/valueset/{key}:
|
/api/valueset/{key}:
|
||||||
get:
|
get:
|
||||||
tags: [ValueSets]
|
tags: [ValueSets]
|
||||||
summary: Get lib value set by key
|
summary: Get lib value set by key
|
||||||
description: Get a specific library/system value set from JSON files
|
description: |
|
||||||
|
Get a specific library/system value set from JSON files.
|
||||||
|
|
||||||
|
**Available value set keys:**
|
||||||
|
- `activity_result` - Activity Result
|
||||||
|
- `additive` - Additive
|
||||||
|
- `adt_event` - ADT Event
|
||||||
|
- `area_class` - Area Class
|
||||||
|
- `body_site` - Body Site
|
||||||
|
- `collection_method` - Collection Method
|
||||||
|
- `container_cap_color` - Container Cap Color
|
||||||
|
- `container_class` - Container Class
|
||||||
|
- `container_size` - Container Size
|
||||||
|
- `country` - Country
|
||||||
|
- `death_indicator` - Death Indicator
|
||||||
|
- `did_type` - DID Type
|
||||||
|
- `enable_disable` - Enable/Disable
|
||||||
|
- `entity_type` - Entity Type
|
||||||
|
- `ethnic` - Ethnic
|
||||||
|
- `fasting_status` - Fasting Status
|
||||||
|
- `formula_language` - Formula Language
|
||||||
|
- `generate_by` - Generate By
|
||||||
|
- `identifier_type` - Identifier Type
|
||||||
|
- `location_type` - Location Type
|
||||||
|
- `marital_status` - Marital Status
|
||||||
|
- `math_sign` - Math Sign
|
||||||
|
- `numeric_ref_type` - Numeric Reference Type
|
||||||
|
- `operation` - Operation (CRUD)
|
||||||
|
- `order_priority` - Order Priority
|
||||||
|
- `order_status` - Order Status
|
||||||
|
- `race` - Race (Ethnicity)
|
||||||
|
- `range_type` - Range Type
|
||||||
|
- `reference_type` - Reference Type
|
||||||
|
- `religion` - Religion
|
||||||
|
- `requested_entity` - Requested Entity
|
||||||
|
- `result_type` - Result Type
|
||||||
|
- `result_unit` - Result Unit
|
||||||
|
- `sex` - Sex
|
||||||
|
- `site_class` - Site Class
|
||||||
|
- `site_type` - Site Type
|
||||||
|
- `specimen_activity` - Specimen Activity
|
||||||
|
- `specimen_condition` - Specimen Condition
|
||||||
|
- `specimen_role` - Specimen Role
|
||||||
|
- `specimen_status` - Specimen Status
|
||||||
|
- `specimen_type` - Specimen Type
|
||||||
|
- `test_activity` - Test Activity
|
||||||
|
- `test_type` - Test Type
|
||||||
|
- `text_ref_type` - Text Reference Type
|
||||||
|
- `unit` - Unit
|
||||||
|
- `v_category` - VCategory
|
||||||
|
- `ws_type` - Workstation Type
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -2901,7 +3015,8 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
description: Value set key (e.g., marital_status, sex)
|
enum: [activity_result, additive, adt_event, area_class, body_site, collection_method, container_cap_color, container_class, container_size, country, death_indicator, did_type, enable_disable, entity_type, ethnic, fasting_status, formula_language, generate_by, identifier_type, location_type, marital_status, math_sign, numeric_ref_type, operation, order_priority, order_status, race, range_type, reference_type, religion, requested_entity, result_type, result_unit, sex, site_class, site_type, specimen_activity, specimen_condition, specimen_role, specimen_status, specimen_type, test_activity, test_type, text_ref_type, unit, v_category, ws_type]
|
||||||
|
description: Value set key name
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Lib value set details
|
description: Lib value set details
|
||||||
@ -2915,23 +3030,29 @@ paths:
|
|||||||
data:
|
data:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: object
|
$ref: '#/components/schemas/ValueSetLibItem'
|
||||||
properties:
|
|
||||||
value:
|
|
||||||
type: string
|
|
||||||
label:
|
|
||||||
type: string
|
|
||||||
|
|
||||||
/api/valueset/refresh:
|
/api/valueset/refresh:
|
||||||
post:
|
post:
|
||||||
tags: [ValueSets]
|
tags: [ValueSets]
|
||||||
summary: Refresh lib ValueSet cache
|
summary: Refresh lib ValueSet cache
|
||||||
description: Clear and reload the library/system ValueSet cache from JSON files
|
description: Clear and reload the library/system ValueSet cache from JSON files. Call this after modifying JSON files in app/Libraries/Data/.
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Lib ValueSet cache refreshed
|
description: Lib ValueSet cache refreshed
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
example: success
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: Cache cleared
|
||||||
|
|
||||||
/api/valueset/user/items:
|
/api/valueset/user/items:
|
||||||
get:
|
get:
|
||||||
@ -2946,6 +3067,16 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
description: Filter by ValueSet ID
|
description: Filter by ValueSet ID
|
||||||
|
- name: search
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Search term to filter by VValue, VDesc, or VSName
|
||||||
|
- name: param
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Alternative search parameter (alias for search)
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: List of user value set items
|
description: List of user value set items
|
||||||
@ -2954,6 +3085,8 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
data:
|
data:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -2970,10 +3103,39 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ValueSetItem'
|
type: object
|
||||||
|
required:
|
||||||
|
- VSetID
|
||||||
|
properties:
|
||||||
|
SiteID:
|
||||||
|
type: integer
|
||||||
|
description: Site reference (default 1)
|
||||||
|
VSetID:
|
||||||
|
type: integer
|
||||||
|
description: Reference to value set definition (required)
|
||||||
|
VOrder:
|
||||||
|
type: integer
|
||||||
|
description: Display order (default 0)
|
||||||
|
VValue:
|
||||||
|
type: string
|
||||||
|
description: The value code
|
||||||
|
VDesc:
|
||||||
|
type: string
|
||||||
|
description: The display description/label
|
||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
description: User value set item created
|
description: User value set item created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/ValueSetItem'
|
||||||
|
|
||||||
/api/valueset/user/items/{id}:
|
/api/valueset/user/items/{id}:
|
||||||
get:
|
get:
|
||||||
@ -2991,6 +3153,15 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: User value set item details
|
description: User value set item details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/ValueSetItem'
|
||||||
|
|
||||||
put:
|
put:
|
||||||
tags: [ValueSets]
|
tags: [ValueSets]
|
||||||
@ -3009,10 +3180,37 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ValueSetItem'
|
type: object
|
||||||
|
properties:
|
||||||
|
SiteID:
|
||||||
|
type: integer
|
||||||
|
description: Site reference
|
||||||
|
VSetID:
|
||||||
|
type: integer
|
||||||
|
description: Reference to value set definition
|
||||||
|
VOrder:
|
||||||
|
type: integer
|
||||||
|
description: Display order
|
||||||
|
VValue:
|
||||||
|
type: string
|
||||||
|
description: The value code
|
||||||
|
VDesc:
|
||||||
|
type: string
|
||||||
|
description: The display description/label
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: User value set item updated
|
description: User value set item updated
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/ValueSetItem'
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags: [ValueSets]
|
tags: [ValueSets]
|
||||||
@ -3029,6 +3227,15 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: User value set item deleted
|
description: User value set item deleted
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
|
||||||
/api/valueset/user/def:
|
/api/valueset/user/def:
|
||||||
get:
|
get:
|
||||||
@ -3037,9 +3244,47 @@ paths:
|
|||||||
description: List value set definitions from database (user-defined)
|
description: List value set definitions from database (user-defined)
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: search
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Optional search term to filter definitions
|
||||||
|
- name: page
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 1
|
||||||
|
description: Page number for pagination
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 100
|
||||||
|
description: Number of items per page
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: List of user value set definitions
|
description: List of user value set definitions
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ValueSetDef'
|
||||||
|
meta:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
total:
|
||||||
|
type: integer
|
||||||
|
page:
|
||||||
|
type: integer
|
||||||
|
limit:
|
||||||
|
type: integer
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags: [ValueSets]
|
tags: [ValueSets]
|
||||||
@ -3052,10 +3297,31 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ValueSetDef'
|
type: object
|
||||||
|
properties:
|
||||||
|
SiteID:
|
||||||
|
type: integer
|
||||||
|
description: Site reference (default 1)
|
||||||
|
VSName:
|
||||||
|
type: string
|
||||||
|
description: Value set name
|
||||||
|
VSDesc:
|
||||||
|
type: string
|
||||||
|
description: Value set description
|
||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
description: User value set definition created
|
description: User value set definition created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/ValueSetDef'
|
||||||
|
|
||||||
/api/valueset/user/def/{id}:
|
/api/valueset/user/def/{id}:
|
||||||
get:
|
get:
|
||||||
@ -3073,6 +3339,15 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: User value set definition details
|
description: User value set definition details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/ValueSetDef'
|
||||||
|
|
||||||
put:
|
put:
|
||||||
tags: [ValueSets]
|
tags: [ValueSets]
|
||||||
@ -3091,10 +3366,31 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ValueSetDef'
|
type: object
|
||||||
|
properties:
|
||||||
|
SiteID:
|
||||||
|
type: integer
|
||||||
|
description: Site reference
|
||||||
|
VSName:
|
||||||
|
type: string
|
||||||
|
description: Value set name
|
||||||
|
VSDesc:
|
||||||
|
type: string
|
||||||
|
description: Value set description
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: User value set definition updated
|
description: User value set definition updated
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/ValueSetDef'
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags: [ValueSets]
|
tags: [ValueSets]
|
||||||
@ -3111,6 +3407,15 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: User value set definition deleted
|
description: User value set definition deleted
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# Master Data Routes
|
# Master Data Routes
|
||||||
|
|||||||
@ -63,6 +63,11 @@
|
|||||||
dropdownOptions = options;
|
dropdownOptions = options;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if current value exists in options
|
||||||
|
let hasValidValue = $derived(
|
||||||
|
!value || dropdownOptions.some(opt => opt.value === value)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="form-control w-full {className}">
|
<div class="form-control w-full {className}">
|
||||||
@ -87,7 +92,11 @@
|
|||||||
>
|
>
|
||||||
<option value="">{placeholder}</option>
|
<option value="">{placeholder}</option>
|
||||||
|
|
||||||
{#each dropdownOptions as option}
|
{#if value && !hasValidValue}
|
||||||
|
<option value={value} selected>{value}</option>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each dropdownOptions as option (option.value)}
|
||||||
<option value={option.value}>{option.label}</option>
|
<option value={option.value}>{option.label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
127
src/routes/(app)/patients/PatientDetailModal.svelte
Normal file
127
src/routes/(app)/patients/PatientDetailModal.svelte
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<script>
|
||||||
|
import { User, MapPin, Activity, FileText } from 'lucide-svelte';
|
||||||
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
|
|
||||||
|
/** @type {{ open: boolean, patient: any | null }} */
|
||||||
|
let { open = $bindable(false), patient = null } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:open title="Patient Details" size="xl">
|
||||||
|
{#if patient}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div class="card bg-base-100 shadow border border-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-lg flex items-center gap-2">
|
||||||
|
<User class="w-5 h-5 text-primary" />
|
||||||
|
Basic Information
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3 mt-4">
|
||||||
|
<div class="flex justify-between py-2 border-b border-base-200">
|
||||||
|
<span class="text-gray-500">Patient ID</span>
|
||||||
|
<span class="font-medium">{patient.PatientID}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2 border-b border-base-200">
|
||||||
|
<span class="text-gray-500">Full Name</span>
|
||||||
|
<span class="font-medium">
|
||||||
|
{[patient.Prefix, patient.NameFirst, patient.NameMiddle, patient.NameLast, patient.Suffix]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2 border-b border-base-200">
|
||||||
|
<span class="text-gray-500">Sex</span>
|
||||||
|
<span class="font-medium">{patient.Sex === '1' ? 'Female' : patient.Sex === '2' ? 'Male' : '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2 border-b border-base-200">
|
||||||
|
<span class="text-gray-500">Birthdate</span>
|
||||||
|
<span class="font-medium">{patient.Birthdate ? new Date(patient.Birthdate).toLocaleDateString() : '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2">
|
||||||
|
<span class="text-gray-500">Citizenship</span>
|
||||||
|
<span class="font-medium">{patient.Citizenship || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow border border-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-lg flex items-center gap-2">
|
||||||
|
<MapPin class="w-5 h-5 text-primary" />
|
||||||
|
Address
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3 mt-4">
|
||||||
|
<div class="flex justify-between py-2 border-b border-base-200">
|
||||||
|
<span class="text-gray-500">Street</span>
|
||||||
|
<span class="font-medium text-right">
|
||||||
|
{[patient.Street_1, patient.Street_2, patient.Street_3].filter(Boolean).join(', ') || '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2 border-b border-base-200">
|
||||||
|
<span class="text-gray-500">City</span>
|
||||||
|
<span class="font-medium">{patient.City || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2 border-b border-base-200">
|
||||||
|
<span class="text-gray-500">Province</span>
|
||||||
|
<span class="font-medium">{patient.Province || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2">
|
||||||
|
<span class="text-gray-500">ZIP</span>
|
||||||
|
<span class="font-medium">{patient.ZIP || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow border border-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-lg flex items-center gap-2">
|
||||||
|
<Activity class="w-5 h-5 text-primary" />
|
||||||
|
Contact
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3 mt-4">
|
||||||
|
<div class="flex justify-between py-2 border-b border-base-200">
|
||||||
|
<span class="text-gray-500">Phone</span>
|
||||||
|
<span class="font-medium">{patient.Phone || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2 border-b border-base-200">
|
||||||
|
<span class="text-gray-500">Mobile</span>
|
||||||
|
<span class="font-medium">{patient.MobilePhone || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2">
|
||||||
|
<span class="text-gray-500">Email</span>
|
||||||
|
<span class="font-medium">{patient.EmailAddress1 || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow border border-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-lg flex items-center gap-2">
|
||||||
|
<FileText class="w-5 h-5 text-primary" />
|
||||||
|
Additional
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3 mt-4">
|
||||||
|
<div class="flex justify-between py-2 border-b border-base-200">
|
||||||
|
<span class="text-gray-500">Marital Status</span>
|
||||||
|
<span class="font-medium">{patient.MaritalStatus || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2 border-b border-base-200">
|
||||||
|
<span class="text-gray-500">Religion</span>
|
||||||
|
<span class="font-medium">{patient.Religion || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2">
|
||||||
|
<span class="text-gray-500">Race</span>
|
||||||
|
<span class="font-medium">{patient.Race || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#snippet footer()}
|
||||||
|
<button class="btn btn-ghost" onclick={() => (open = false)} type="button">Close</button>
|
||||||
|
{/snippet}
|
||||||
|
</Modal>
|
||||||
637
src/routes/(app)/patients/PatientFormModal.svelte
Normal file
637
src/routes/(app)/patients/PatientFormModal.svelte
Normal file
@ -0,0 +1,637 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { fetchProvinces, fetchCities } from '$lib/api/geography.js';
|
||||||
|
import { createPatient, updatePatient } from '$lib/api/patients.js';
|
||||||
|
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
|
||||||
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
|
import SelectDropdown from '$lib/components/SelectDropdown.svelte';
|
||||||
|
|
||||||
|
/** @type {{ open: boolean, patient: any | null, onSave: () => void, patientLoading?: boolean }} */
|
||||||
|
let { open = $bindable(false), patient = null, onSave, patientLoading = false } = $props();
|
||||||
|
|
||||||
|
let loading = $state(false);
|
||||||
|
let saving = $state(false);
|
||||||
|
let provinces = $state([]);
|
||||||
|
let cities = $state([]);
|
||||||
|
let formErrors = $state({});
|
||||||
|
|
||||||
|
let formData = $state({
|
||||||
|
InternalPID: null,
|
||||||
|
PatientID: '',
|
||||||
|
Prefix: '',
|
||||||
|
NameFirst: '',
|
||||||
|
NameMiddle: '',
|
||||||
|
NameLast: '',
|
||||||
|
NameMaiden: '',
|
||||||
|
Suffix: '',
|
||||||
|
Sex: '',
|
||||||
|
Birthdate: '',
|
||||||
|
PlaceOfBirth: '',
|
||||||
|
Citizenship: '',
|
||||||
|
Street_1: '',
|
||||||
|
Street_2: '',
|
||||||
|
Street_3: '',
|
||||||
|
Province: '',
|
||||||
|
City: '',
|
||||||
|
ZIP: '',
|
||||||
|
Country: '',
|
||||||
|
Phone: '',
|
||||||
|
MobilePhone: '',
|
||||||
|
EmailAddress1: '',
|
||||||
|
EmailAddress2: '',
|
||||||
|
Race: '',
|
||||||
|
MaritalStatus: '',
|
||||||
|
Religion: '',
|
||||||
|
Ethnic: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track last loaded province to prevent infinite loops
|
||||||
|
let lastLoadedProvince = $state('');
|
||||||
|
// Track which patient we've initialized the form for
|
||||||
|
let initializedPatientId = $state(null);
|
||||||
|
|
||||||
|
const prefixOptions = [
|
||||||
|
{ value: 'Mr', label: 'Mr' },
|
||||||
|
{ value: 'Mrs', label: 'Mrs' },
|
||||||
|
{ value: 'Ms', label: 'Ms' },
|
||||||
|
{ value: 'Dr', label: 'Dr' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const provinceOptions = $derived(
|
||||||
|
provinces.map((p) => ({
|
||||||
|
value: p.AreaCode || p.value || p.code || p.Code || p.id || p.ID || '',
|
||||||
|
label: p.AreaName || p.label || p.name || p.Name || p.description || p.Description || ''
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const cityOptions = $derived(
|
||||||
|
cities.map((c) => ({
|
||||||
|
value: c.AreaCode || c.value || c.code || c.Code || c.id || c.ID || '',
|
||||||
|
label: c.AreaName || c.label || c.name || c.Name || c.description || c.Description || ''
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const isEdit = $derived(!!patient);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await loadProvinces();
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const currentPatientId = patient?.InternalPID || null;
|
||||||
|
|
||||||
|
if (open && patient && !patientLoading && provinces.length > 0 && initializedPatientId !== currentPatientId) {
|
||||||
|
// Edit mode - populate form (only when provinces are loaded and for a new patient)
|
||||||
|
initializedPatientId = currentPatientId;
|
||||||
|
const patientProvince = patient.Province || '';
|
||||||
|
const patientCity = patient.City || '';
|
||||||
|
|
||||||
|
formData = {
|
||||||
|
InternalPID: patient.InternalPID,
|
||||||
|
PatientID: patient.PatientID || '',
|
||||||
|
Prefix: patient.Prefix || '',
|
||||||
|
NameFirst: patient.NameFirst || '',
|
||||||
|
NameMiddle: patient.NameMiddle || '',
|
||||||
|
NameLast: patient.NameLast || '',
|
||||||
|
NameMaiden: patient.NameMaiden || '',
|
||||||
|
Suffix: patient.Suffix || '',
|
||||||
|
Sex: patient.Sex || '',
|
||||||
|
Birthdate: patient.Birthdate ? patient.Birthdate.split('T')[0] : '',
|
||||||
|
PlaceOfBirth: patient.PlaceOfBirth || '',
|
||||||
|
Citizenship: patient.Citizenship || '',
|
||||||
|
Street_1: patient.Street_1 || '',
|
||||||
|
Street_2: patient.Street_2 || '',
|
||||||
|
Street_3: patient.Street_3 || '',
|
||||||
|
Province: patientProvince,
|
||||||
|
City: patientCity, // Set immediately, will be validated once cities load
|
||||||
|
ZIP: patient.ZIP || '',
|
||||||
|
Country: patient.Country || '',
|
||||||
|
Phone: patient.Phone || '',
|
||||||
|
MobilePhone: patient.MobilePhone || '',
|
||||||
|
EmailAddress1: patient.EmailAddress1 || '',
|
||||||
|
EmailAddress2: patient.EmailAddress2 || '',
|
||||||
|
Race: patient.Race || '',
|
||||||
|
MaritalStatus: patient.MaritalStatus || '',
|
||||||
|
Religion: patient.Religion || '',
|
||||||
|
Ethnic: patient.Ethnic || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load cities for this province
|
||||||
|
lastLoadedProvince = patientProvince;
|
||||||
|
if (patientProvince) {
|
||||||
|
loadCities(patientProvince);
|
||||||
|
}
|
||||||
|
} else if (open && !patient && !patientLoading && initializedPatientId !== 'create') {
|
||||||
|
// Create mode - reset form
|
||||||
|
initializedPatientId = 'create';
|
||||||
|
formData = {
|
||||||
|
InternalPID: null,
|
||||||
|
PatientID: '',
|
||||||
|
Prefix: '',
|
||||||
|
NameFirst: '',
|
||||||
|
NameMiddle: '',
|
||||||
|
NameLast: '',
|
||||||
|
NameMaiden: '',
|
||||||
|
Suffix: '',
|
||||||
|
Sex: '',
|
||||||
|
Birthdate: '',
|
||||||
|
PlaceOfBirth: '',
|
||||||
|
Citizenship: '',
|
||||||
|
Street_1: '',
|
||||||
|
Street_2: '',
|
||||||
|
Street_3: '',
|
||||||
|
Province: '',
|
||||||
|
City: '',
|
||||||
|
ZIP: '',
|
||||||
|
Country: '',
|
||||||
|
Phone: '',
|
||||||
|
MobilePhone: '',
|
||||||
|
EmailAddress1: '',
|
||||||
|
EmailAddress2: '',
|
||||||
|
Race: '',
|
||||||
|
MaritalStatus: '',
|
||||||
|
Religion: '',
|
||||||
|
Ethnic: '',
|
||||||
|
};
|
||||||
|
cities = [];
|
||||||
|
lastLoadedProvince = '';
|
||||||
|
} else if (!open) {
|
||||||
|
// Reset initialization when modal closes
|
||||||
|
initializedPatientId = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadProvinces() {
|
||||||
|
try {
|
||||||
|
const response = await fetchProvinces();
|
||||||
|
provinces = Array.isArray(response.data) ? response.data : [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load provinces:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCities(provinceCode) {
|
||||||
|
if (!provinceCode) {
|
||||||
|
cities = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetchCities(provinceCode);
|
||||||
|
cities = Array.isArray(response.data) ? response.data : [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load cities:', err);
|
||||||
|
cities = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateForm() {
|
||||||
|
const errors = {};
|
||||||
|
if (!formData.PatientID?.trim()) {
|
||||||
|
errors.PatientID = 'Patient ID is required';
|
||||||
|
}
|
||||||
|
if (!formData.NameFirst?.trim()) {
|
||||||
|
errors.NameFirst = 'First name is required';
|
||||||
|
}
|
||||||
|
if (!formData.Sex) {
|
||||||
|
errors.Sex = 'Sex is required';
|
||||||
|
}
|
||||||
|
if (!formData.Birthdate) {
|
||||||
|
errors.Birthdate = 'Birthdate is required';
|
||||||
|
}
|
||||||
|
formErrors = errors;
|
||||||
|
return Object.keys(errors).length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!validateForm()) return;
|
||||||
|
|
||||||
|
saving = true;
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
Birthdate: formData.Birthdate ? new Date(formData.Birthdate).toISOString() : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove empty fields
|
||||||
|
Object.keys(payload).forEach((key) => {
|
||||||
|
if (payload[key] === '' || payload[key] === null) {
|
||||||
|
delete payload[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
await updatePatient(payload);
|
||||||
|
toastSuccess('Patient updated successfully');
|
||||||
|
} else {
|
||||||
|
await createPatient(payload);
|
||||||
|
toastSuccess('Patient created successfully');
|
||||||
|
}
|
||||||
|
open = false;
|
||||||
|
onSave?.();
|
||||||
|
} catch (err) {
|
||||||
|
toastError(err.message || 'Failed to save patient');
|
||||||
|
} finally {
|
||||||
|
saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const province = formData.Province;
|
||||||
|
if (province && province !== lastLoadedProvince) {
|
||||||
|
lastLoadedProvince = province;
|
||||||
|
// Clear city when province changes (only after form is initialized)
|
||||||
|
if (initializedPatientId !== null) {
|
||||||
|
formData.City = '';
|
||||||
|
}
|
||||||
|
loadCities(province);
|
||||||
|
} else if (!province) {
|
||||||
|
cities = [];
|
||||||
|
lastLoadedProvince = '';
|
||||||
|
if (initializedPatientId !== null) {
|
||||||
|
formData.City = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
bind:open
|
||||||
|
title={isEdit ? 'Edit Patient' : 'Add Patient'}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{#if patientLoading}
|
||||||
|
<div class="flex items-center justify-center py-12">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<form class="space-y-6" onsubmit={(e) => e.preventDefault()}>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="patientId">
|
||||||
|
<span class="label-text font-medium">Patient ID</span>
|
||||||
|
<span class="label-text-alt text-error">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="patientId"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
class:input-error={formErrors.PatientID}
|
||||||
|
bind:value={formData.PatientID}
|
||||||
|
placeholder="Enter patient ID"
|
||||||
|
disabled={isEdit}
|
||||||
|
/>
|
||||||
|
{#if formErrors.PatientID}
|
||||||
|
<span class="text-error text-sm mt-1">{formErrors.PatientID}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SelectDropdown
|
||||||
|
label="Prefix"
|
||||||
|
name="prefix"
|
||||||
|
bind:value={formData.Prefix}
|
||||||
|
options={prefixOptions}
|
||||||
|
placeholder="Select..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="nameFirst">
|
||||||
|
<span class="label-text font-medium">First Name</span>
|
||||||
|
<span class="label-text-alt text-error">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nameFirst"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
class:input-error={formErrors.NameFirst}
|
||||||
|
bind:value={formData.NameFirst}
|
||||||
|
placeholder="Enter first name"
|
||||||
|
/>
|
||||||
|
{#if formErrors.NameFirst}
|
||||||
|
<span class="text-error text-sm mt-1">{formErrors.NameFirst}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="nameMiddle">
|
||||||
|
<span class="label-text font-medium">Middle Name</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nameMiddle"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.NameMiddle}
|
||||||
|
placeholder="Enter middle name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="nameLast">
|
||||||
|
<span class="label-text font-medium">Last Name</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nameLast"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.NameLast}
|
||||||
|
placeholder="Enter last name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="nameMaiden">
|
||||||
|
<span class="label-text font-medium">Maiden Name</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nameMaiden"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.NameMaiden}
|
||||||
|
placeholder="Enter maiden name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="suffix">
|
||||||
|
<span class="label-text font-medium">Suffix</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="suffix"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.Suffix}
|
||||||
|
placeholder="Enter suffix (e.g., Jr, Sr, III)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<SelectDropdown
|
||||||
|
label="Sex"
|
||||||
|
name="sex"
|
||||||
|
bind:value={formData.Sex}
|
||||||
|
valueSetKey="sex"
|
||||||
|
placeholder="Select sex..."
|
||||||
|
required={true}
|
||||||
|
error={formErrors.Sex}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="birthdate">
|
||||||
|
<span class="label-text font-medium">Birthdate</span>
|
||||||
|
<span class="label-text-alt text-error">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="birthdate"
|
||||||
|
type="date"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
class:input-error={formErrors.Birthdate}
|
||||||
|
bind:value={formData.Birthdate}
|
||||||
|
/>
|
||||||
|
{#if formErrors.Birthdate}
|
||||||
|
<span class="text-error text-sm mt-1">{formErrors.Birthdate}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="placeOfBirth">
|
||||||
|
<span class="label-text font-medium">Place of Birth</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="placeOfBirth"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.PlaceOfBirth}
|
||||||
|
placeholder="Enter place of birth"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-base-200 pt-6">
|
||||||
|
<h4 class="font-medium text-gray-700 mb-4">Demographics</h4>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||||
|
<SelectDropdown
|
||||||
|
label="Race"
|
||||||
|
name="race"
|
||||||
|
bind:value={formData.Race}
|
||||||
|
valueSetKey="race"
|
||||||
|
placeholder="Select race..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectDropdown
|
||||||
|
label="Marital Status"
|
||||||
|
name="maritalStatus"
|
||||||
|
bind:value={formData.MaritalStatus}
|
||||||
|
valueSetKey="marital_status"
|
||||||
|
placeholder="Select marital status..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectDropdown
|
||||||
|
label="Religion"
|
||||||
|
name="religion"
|
||||||
|
bind:value={formData.Religion}
|
||||||
|
valueSetKey="religion"
|
||||||
|
placeholder="Select religion..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<SelectDropdown
|
||||||
|
label="Ethnicity"
|
||||||
|
name="ethnic"
|
||||||
|
bind:value={formData.Ethnic}
|
||||||
|
valueSetKey="ethnic"
|
||||||
|
placeholder="Select ethnicity..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="citizenship">
|
||||||
|
<span class="label-text font-medium">Citizenship</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="citizenship"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.Citizenship}
|
||||||
|
placeholder="Enter citizenship"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-base-200 pt-6">
|
||||||
|
<h4 class="font-medium text-gray-700 mb-4">Address Information</h4>
|
||||||
|
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label class="label" for="street1">
|
||||||
|
<span class="label-text font-medium">Street Address</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="street1"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.Street_1}
|
||||||
|
placeholder="Enter street address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label class="label" for="street2">
|
||||||
|
<span class="label-text font-medium">Street Address Line 2</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="street2"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.Street_2}
|
||||||
|
placeholder="Enter street address line 2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label class="label" for="street3">
|
||||||
|
<span class="label-text font-medium">Street Address Line 3</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="street3"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.Street_3}
|
||||||
|
placeholder="Enter street address line 3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<SelectDropdown
|
||||||
|
label="Province"
|
||||||
|
name="province"
|
||||||
|
bind:value={formData.Province}
|
||||||
|
options={provinceOptions}
|
||||||
|
placeholder="Select province..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectDropdown
|
||||||
|
label="City"
|
||||||
|
name="city"
|
||||||
|
bind:value={formData.City}
|
||||||
|
options={cityOptions}
|
||||||
|
placeholder={formData.Province ? "Select city..." : "Select province first"}
|
||||||
|
disabled={!formData.Province}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="zip">
|
||||||
|
<span class="label-text font-medium">ZIP Code</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="zip"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.ZIP}
|
||||||
|
placeholder="Enter ZIP code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
|
||||||
|
<SelectDropdown
|
||||||
|
label="Country"
|
||||||
|
name="country"
|
||||||
|
bind:value={formData.Country}
|
||||||
|
valueSetKey="country"
|
||||||
|
placeholder="Select country..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-base-200 pt-6">
|
||||||
|
<h4 class="font-medium text-gray-700 mb-4">Contact Information</h4>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="phone">
|
||||||
|
<span class="label-text font-medium">Phone</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="phone"
|
||||||
|
type="tel"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.Phone}
|
||||||
|
placeholder="Enter phone number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="mobilePhone">
|
||||||
|
<span class="label-text font-medium">Mobile Phone</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="mobilePhone"
|
||||||
|
type="tel"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.MobilePhone}
|
||||||
|
placeholder="Enter mobile number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="email1">
|
||||||
|
<span class="label-text font-medium">Email Address</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email1"
|
||||||
|
type="email"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.EmailAddress1}
|
||||||
|
placeholder="Enter email address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="email2">
|
||||||
|
<span class="label-text font-medium">Secondary Email</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email2"
|
||||||
|
type="email"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={formData.EmailAddress2}
|
||||||
|
placeholder="Enter secondary email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
{#snippet footer()}
|
||||||
|
{#if patientLoading}
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button class="btn btn-ghost" disabled type="button">Cancel</button>
|
||||||
|
<button class="btn btn-primary" disabled type="button">Loading...</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button class="btn btn-ghost" onclick={() => (open = false)} type="button">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
onclick={handleSubmit}
|
||||||
|
disabled={saving}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{#if saving}
|
||||||
|
<span class="loading loading-spinner loading-sm mr-2"></span>
|
||||||
|
{/if}
|
||||||
|
{saving ? 'Saving...' : 'Save'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</Modal>
|
||||||
153
src/routes/(app)/patients/VisitListModal.svelte
Normal file
153
src/routes/(app)/patients/VisitListModal.svelte
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { fetchVisitsByPatient } from '$lib/api/visits.js';
|
||||||
|
import { error as toastError } from '$lib/utils/toast.js';
|
||||||
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
|
import { Calendar, Clock, MapPin, FileText, Plus } from 'lucide-svelte';
|
||||||
|
|
||||||
|
/** @type {{ open: boolean, patient: any | null }} */
|
||||||
|
let { open = $bindable(false), patient = null } = $props();
|
||||||
|
|
||||||
|
let visits = $state([]);
|
||||||
|
let loading = $state(false);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (patient && open) {
|
||||||
|
loadVisits();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (open && patient) {
|
||||||
|
loadVisits();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadVisits() {
|
||||||
|
if (!patient?.InternalPID) return;
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const response = await fetchVisitsByPatient(patient.InternalPID);
|
||||||
|
visits = Array.isArray(response.data) ? response.data : [];
|
||||||
|
} catch (err) {
|
||||||
|
toastError(err.message || 'Failed to load visits');
|
||||||
|
visits = [];
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return '-';
|
||||||
|
return new Date(dateStr).toLocaleDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(dateStr) {
|
||||||
|
if (!dateStr) return '-';
|
||||||
|
return new Date(dateStr).toLocaleString();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:open title="Patient Visits" size="xl">
|
||||||
|
{#if patient}
|
||||||
|
<div class="mb-4 p-4 bg-base-200 rounded-lg">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-lg">
|
||||||
|
{[patient.Prefix, patient.NameFirst, patient.NameMiddle, patient.NameLast].filter(Boolean).join(' ')}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600">Patient ID: {patient.PatientID}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="flex items-center justify-center py-12">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
</div>
|
||||||
|
{:else if visits.length === 0}
|
||||||
|
<div class="text-center py-12 text-gray-500">
|
||||||
|
<Calendar class="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||||
|
<p class="text-lg">No visits found</p>
|
||||||
|
<p class="text-sm">This patient has no visit records.</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each visits as visit}
|
||||||
|
<div class="card bg-base-100 shadow border border-base-200 hover:shadow-md transition-shadow">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<Calendar class="w-4 h-4 text-primary" />
|
||||||
|
<span class="font-semibold">{formatDate(visit.AdmissionDateTime)}</span>
|
||||||
|
{#if visit.VisitStatus}
|
||||||
|
<span class="badge badge-sm {visit.VisitStatus === 'Active' ? 'badge-success' : 'badge-ghost'}">
|
||||||
|
{visit.VisitStatus}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
|
{#if visit.AdmissionType}
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-500">Type:</span>
|
||||||
|
<span class="ml-1">{visit.AdmissionType}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if visit.Location}
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<MapPin class="w-3 h-3 text-gray-400" />
|
||||||
|
<span class="text-gray-500">Location:</span>
|
||||||
|
<span class="ml-1">{visit.Location}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if visit.DoctorName}
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-500">Doctor:</span>
|
||||||
|
<span class="ml-1">{visit.DoctorName}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if visit.AdmissionDateTime}
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Clock class="w-3 h-3 text-gray-400" />
|
||||||
|
<span class="text-gray-500">Time:</span>
|
||||||
|
<span class="ml-1">{formatDateTime(visit.AdmissionDateTime)}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if visit.Diagnosis}
|
||||||
|
<div class="mt-3 pt-3 border-t border-base-200">
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<FileText class="w-4 h-4 text-gray-400 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-500 text-sm">Diagnosis:</span>
|
||||||
|
<p class="text-sm mt-1">{visit.Diagnosis}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#snippet footer()}
|
||||||
|
<div class="flex justify-between items-center w-full">
|
||||||
|
<button class="btn btn-primary btn-sm" onclick={() => {}} disabled>
|
||||||
|
<Plus class="w-4 h-4 mr-1" />
|
||||||
|
New Visit
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost" onclick={() => (open = false)} type="button">Close</button>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Modal>
|
||||||
Loading…
x
Reference in New Issue
Block a user