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
|
||||
|
||||
# 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:
|
||||
type: object
|
||||
description: User-defined value set definition (from database)
|
||||
properties:
|
||||
id:
|
||||
VSetID:
|
||||
type: integer
|
||||
VSetCode:
|
||||
description: Primary key
|
||||
SiteID:
|
||||
type: integer
|
||||
description: Site reference
|
||||
VSName:
|
||||
type: string
|
||||
VSetName:
|
||||
description: Value set name
|
||||
VSDesc:
|
||||
type: string
|
||||
Description:
|
||||
description: Value set description
|
||||
CreateDate:
|
||||
type: string
|
||||
Category:
|
||||
format: date-time
|
||||
description: Creation timestamp
|
||||
EndDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: Soft delete timestamp
|
||||
ItemCount:
|
||||
type: integer
|
||||
description: Number of items in this value set
|
||||
|
||||
ValueSetItem:
|
||||
type: object
|
||||
description: User-defined value set item (from database)
|
||||
properties:
|
||||
id:
|
||||
VID:
|
||||
type: integer
|
||||
description: Primary key
|
||||
SiteID:
|
||||
type: integer
|
||||
description: Site reference
|
||||
VSetID:
|
||||
type: integer
|
||||
description: Reference to value set definition
|
||||
VOrder:
|
||||
type: integer
|
||||
description: Display order
|
||||
VValue:
|
||||
type: string
|
||||
VLabel:
|
||||
description: The value code
|
||||
VDesc:
|
||||
type: string
|
||||
VSeq:
|
||||
type: integer
|
||||
IsActive:
|
||||
type: boolean
|
||||
description: The display description/label
|
||||
VCategory:
|
||||
type: string
|
||||
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
|
||||
Location:
|
||||
@ -1391,10 +1439,21 @@ paths:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$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:
|
||||
tags: [Patient Visits]
|
||||
@ -2863,7 +2922,7 @@ paths:
|
||||
get:
|
||||
tags: [ValueSets]
|
||||
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:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
@ -2871,7 +2930,7 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
description: Optional search term to filter value sets
|
||||
description: Optional search term to filter value set names
|
||||
responses:
|
||||
'200':
|
||||
description: List of lib value sets with item counts
|
||||
@ -2882,17 +2941,72 @@ paths:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: success
|
||||
data:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
description: Number of items in each value set
|
||||
example:
|
||||
sex: 3
|
||||
marital_status: 6
|
||||
order_status: 6
|
||||
|
||||
/api/valueset/{key}:
|
||||
get:
|
||||
tags: [ValueSets]
|
||||
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:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
@ -2901,7 +3015,8 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
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:
|
||||
'200':
|
||||
description: Lib value set details
|
||||
@ -2915,23 +3030,29 @@ paths:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
value:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
$ref: '#/components/schemas/ValueSetLibItem'
|
||||
|
||||
/api/valueset/refresh:
|
||||
post:
|
||||
tags: [ValueSets]
|
||||
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:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
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:
|
||||
get:
|
||||
@ -2946,6 +3067,16 @@ paths:
|
||||
schema:
|
||||
type: integer
|
||||
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:
|
||||
'200':
|
||||
description: List of user value set items
|
||||
@ -2954,6 +3085,8 @@ paths:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
@ -2970,10 +3103,39 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
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:
|
||||
'201':
|
||||
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}:
|
||||
get:
|
||||
@ -2991,6 +3153,15 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: User value set item details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/ValueSetItem'
|
||||
|
||||
put:
|
||||
tags: [ValueSets]
|
||||
@ -3009,10 +3180,37 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
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:
|
||||
'200':
|
||||
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:
|
||||
tags: [ValueSets]
|
||||
@ -3029,6 +3227,15 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: User value set item deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
/api/valueset/user/def:
|
||||
get:
|
||||
@ -3037,9 +3244,47 @@ paths:
|
||||
description: List value set definitions from database (user-defined)
|
||||
security:
|
||||
- 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:
|
||||
'200':
|
||||
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:
|
||||
tags: [ValueSets]
|
||||
@ -3052,10 +3297,31 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
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:
|
||||
'201':
|
||||
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}:
|
||||
get:
|
||||
@ -3073,6 +3339,15 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: User value set definition details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/ValueSetDef'
|
||||
|
||||
put:
|
||||
tags: [ValueSets]
|
||||
@ -3091,10 +3366,31 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
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:
|
||||
'200':
|
||||
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:
|
||||
tags: [ValueSets]
|
||||
@ -3111,6 +3407,15 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: User value set definition deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
# ========================================
|
||||
# Master Data Routes
|
||||
|
||||
@ -63,6 +63,11 @@
|
||||
dropdownOptions = options;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if current value exists in options
|
||||
let hasValidValue = $derived(
|
||||
!value || dropdownOptions.some(opt => opt.value === value)
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="form-control w-full {className}">
|
||||
@ -87,7 +92,11 @@
|
||||
>
|
||||
<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>
|
||||
{/each}
|
||||
</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