clqms-fe1/src/routes/(app)/patients/VisitFormModal.svelte

429 lines
14 KiB
Svelte
Raw Normal View History

<script>
import { createVisit, updateVisit, fetchVisit, createADT } from '$lib/api/visits.js';
import { fetchLocations } from '$lib/api/locations.js';
import { fetchContacts } from '$lib/api/contacts.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';
import { Calendar, MapPin, User, FileText, Activity } from 'lucide-svelte';
/** @type {{ open: boolean, patient: any | null, visit: any | null, onSave: () => void }} */
let { open = $bindable(false), patient = null, visit = null, onSave } = $props();
let loading = $state(false);
let saving = $state(false);
let locations = $state([]);
let contacts = $state([]);
let formErrors = $state({});
let activeTab = $state('info'); // 'info', 'diagnosis'
let formData = $state({
InternalPVID: null,
PVID: '',
PatientID: '',
PVCreateDate: '',
ADTCode: '',
LocationID: '',
LocCode: '',
AttDoc: '',
AdmDoc: '',
RefDoc: '',
CnsDoc: '',
DiagCode: '',
Diagnosis: '',
EndDate: '',
ArchivedDate: '',
});
const isEdit = $derived(!!visit?.InternalPVID);
const adtTypeOptions = [
{ value: 'A04', label: 'Register (Outpatient/ER)' },
{ value: 'A01', label: 'Admit (Inpatient)' },
{ value: 'A02', label: 'Transfer' },
{ value: 'A03', label: 'Discharge' },
{ value: 'A08', label: 'Update Information' },
{ value: 'A11', label: 'Cancel Admit' },
{ value: 'A13', label: 'Cancel Discharge' },
];
$effect(() => {
if (open) {
loadLocations();
loadContacts();
if (visit) {
// Edit mode - populate form
formData = {
InternalPVID: visit.InternalPVID || null,
PVID: visit.PVID || '',
PatientID: visit.PatientID || patient?.PatientID || '',
PVCreateDate: visit.PVCreateDate || visit.PVACreateDate || '',
ADTCode: visit.ADTCode || '',
LocationID: visit.LocationID || '',
LocCode: visit.LocCode || '',
AttDoc: visit.AttDoc || '',
AdmDoc: visit.AdmDoc || '',
RefDoc: visit.RefDoc || '',
CnsDoc: visit.CnsDoc || '',
DiagCode: visit.DiagCode || '',
Diagnosis: visit.Diagnosis || '',
EndDate: visit.EndDate || '',
ArchivedDate: visit.ArchivedDate || '',
};
} else {
// Create mode - reset form
formData = {
InternalPVID: null,
PVID: '',
PatientID: patient?.PatientID || '',
PVCreateDate: new Date().toISOString().slice(0, 16),
ADTCode: '',
LocationID: '',
LocCode: '',
AttDoc: '',
AdmDoc: '',
RefDoc: '',
CnsDoc: '',
DiagCode: '',
Diagnosis: '',
EndDate: '',
ArchivedDate: '',
};
}
}
});
async function loadLocations() {
try {
const response = await fetchLocations();
locations = Array.isArray(response.data) ? response.data : [];
} catch (err) {
console.error('Failed to load locations:', err);
}
}
async function loadContacts() {
try {
const response = await fetchContacts();
contacts = Array.isArray(response.data) ? response.data : [];
} catch (err) {
console.error('Failed to load contacts:', err);
}
}
const locationOptions = $derived(
locations.map((l) => ({
value: l.LocationID || l.LocCode || '',
label: l.LocFull || l.LocCode || 'Unknown',
}))
);
const doctorOptions = $derived(
contacts.map((c) => ({
value: c.ContactID || c.ContactCode || '',
label: [c.NamePrefix, c.NameFirst, c.NameMiddle, c.NameLast].filter(Boolean).join(' ') || c.ContactCode || 'Unknown',
}))
);
function validateForm() {
const errors = {};
if (!formData.PatientID?.trim()) {
errors.PatientID = 'Patient ID is required';
}
if (!formData.PVCreateDate) {
errors.PVCreateDate = 'Visit date is required';
}
if (!formData.ADTCode?.trim()) {
errors.ADTCode = 'Visit type is required';
}
formErrors = errors;
return Object.keys(errors).length === 0;
}
async function handleSubmit() {
if (!validateForm()) return;
saving = true;
try {
const payload = { ...formData };
// Remove empty fields
Object.keys(payload).forEach((key) => {
if (payload[key] === '' || payload[key] === null) {
delete payload[key];
}
});
let savedVisit;
if (isEdit) {
savedVisit = await updateVisit(payload);
toastSuccess('Visit updated successfully');
} else {
savedVisit = await createVisit(payload);
toastSuccess('Visit created successfully');
}
// Create ADT record for the visit action
try {
const adtPayload = {
InternalPVID: savedVisit?.InternalPVID || payload.InternalPVID,
ADTCode: payload.ADTCode || 'A08', // Default to update if no code
LocationID: payload.LocationID,
LocCode: payload.LocCode,
AttDoc: payload.AttDoc,
AdmDoc: payload.AdmDoc,
RefDoc: payload.RefDoc,
CnsDoc: payload.CnsDoc,
};
await createADT(adtPayload);
} catch (adtErr) {
console.warn('Failed to create ADT record:', adtErr);
// Don't fail the whole operation if ADT creation fails
}
open = false;
onSave?.();
} catch (err) {
toastError(err.message || 'Failed to save visit');
} finally {
saving = false;
}
}
</script>
<Modal bind:open title={isEdit ? 'Edit Visit' : 'New Visit'} size="lg">
{#if loading}
<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-3" onsubmit={(e) => e.preventDefault()}>
<!-- Patient Info Display -->
{#if patient}
<div class="p-4 bg-base-200 rounded-lg">
<div class="flex items-center gap-2">
<User class="w-4 h-4 text-gray-500" />
<span class="font-medium">
{[patient.Prefix, patient.NameFirst, patient.NameMiddle, patient.NameLast].filter(Boolean).join(' ')}
</span>
<span class="text-gray-500">({patient.PatientID})</span>
</div>
</div>
{/if}
<!-- Tabs -->
<div class="tabs tabs-bordered">
<button
type="button"
class="tab tab-lg {activeTab === 'info' ? 'tab-active' : ''}"
onclick={() => activeTab = 'info'}
>
<Calendar class="w-4 h-4 mr-2" />
Visit Info
</button>
<button
type="button"
class="tab tab-lg {activeTab === 'diagnosis' ? 'tab-active' : ''}"
onclick={() => activeTab = 'diagnosis'}
>
<Activity class="w-4 h-4 mr-2" />
Diagnosis & Status
</button>
</div>
<!-- Tab: Visit Info -->
{#if activeTab === 'info'}
<div class="space-y-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{#if isEdit}
<div class="form-control">
<label class="label" for="pvid">
<span class="label-text font-medium">Visit ID</span>
</label>
<input
id="pvid"
type="text"
class="input input-sm input-bordered w-full"
bind:value={formData.PVID}
placeholder="Enter visit ID"
/>
</div>
{/if}
<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-sm input-bordered w-full"
class:input-error={formErrors.PatientID}
bind:value={formData.PatientID}
placeholder="Enter patient ID"
disabled={!!patient}
/>
{#if formErrors.PatientID}
<span class="text-error text-sm mt-1">{formErrors.PatientID}</span>
{/if}
</div>
<div class="form-control">
<label class="label" for="visitDate">
<span class="label-text font-medium">Visit Date</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
id="visitDate"
type="datetime-local"
class="input input-sm input-bordered w-full"
class:input-error={formErrors.PVCreateDate}
bind:value={formData.PVCreateDate}
/>
{#if formErrors.PVCreateDate}
<span class="text-error text-sm mt-1">{formErrors.PVCreateDate}</span>
{/if}
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<SelectDropdown
label="Visit Type"
name="adtCode"
bind:value={formData.ADTCode}
options={adtTypeOptions}
placeholder="Select visit type..."
required={true}
error={formErrors.ADTCode}
/>
<SelectDropdown
label="Location"
name="location"
bind:value={formData.LocationID}
options={locationOptions}
placeholder="Select location..."
/>
</div>
<div class="border-t border-base-200 pt-4 mt-4">
<h4 class="font-medium text-gray-700 mb-4">Doctors</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<SelectDropdown
label="Attending Doctor"
name="attDoc"
bind:value={formData.AttDoc}
options={doctorOptions}
placeholder="Select attending doctor..."
/>
<SelectDropdown
label="Admitting Doctor"
name="admDoc"
bind:value={formData.AdmDoc}
options={doctorOptions}
placeholder="Select admitting doctor..."
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<SelectDropdown
label="Referring Doctor"
name="refDoc"
bind:value={formData.RefDoc}
options={doctorOptions}
placeholder="Select referring doctor..."
/>
<SelectDropdown
label="Consulting Doctor"
name="cnsDoc"
bind:value={formData.CnsDoc}
options={doctorOptions}
placeholder="Select consulting doctor..."
/>
</div>
</div>
</div>
{/if}
<!-- Tab: Diagnosis & Status -->
{#if activeTab === 'diagnosis'}
<div class="space-y-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="diagCode">
<span class="label-text font-medium">Diagnosis Code</span>
</label>
<input
id="diagCode"
type="text"
class="input input-sm input-bordered w-full"
bind:value={formData.DiagCode}
placeholder="Enter diagnosis code"
/>
</div>
</div>
<div class="form-control">
<label class="label" for="diagnosis">
<span class="label-text font-medium">Diagnosis Description</span>
</label>
<textarea
id="diagnosis"
class="textarea textarea-bordered w-full"
bind:value={formData.Diagnosis}
placeholder="Enter diagnosis description"
rows="3"
></textarea>
</div>
<div class="border-t border-base-200 pt-4 mt-4">
<h4 class="font-medium text-gray-700 mb-4">Visit Status</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="endDate">
<span class="label-text font-medium">End Date</span>
</label>
<input
id="endDate"
type="datetime-local"
class="input input-sm input-bordered w-full"
bind:value={formData.EndDate}
/>
</div>
<div class="form-control">
<label class="label" for="archivedDate">
<span class="label-text font-medium">Archived Date</span>
</label>
<input
id="archivedDate"
type="datetime-local"
class="input input-sm input-bordered w-full"
bind:value={formData.ArchivedDate}
/>
</div>
</div>
</div>
</div>
{/if}
</form>
{/if}
{#snippet footer()}
<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>
{/snippet}
</Modal>