feat: Integrate visits management into patients page with full CRUD operations
- Add visit creation, editing, and deletion directly from patient details - Add patient class selection and ADT status display - Remove standalone visits page and sidebar navigation - Update visit form with Episode ID, location, doctors, and dates
This commit is contained in:
parent
1e032e5278
commit
382b05d98e
@ -17,7 +17,6 @@
|
||||
Briefcase,
|
||||
Hash,
|
||||
Globe,
|
||||
Calendar,
|
||||
ChevronDown
|
||||
} from 'lucide-svelte';
|
||||
import { auth } from '$lib/stores/auth.js';
|
||||
@ -252,15 +251,6 @@
|
||||
<span>Patients</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/visits"
|
||||
class="menu-item py-1 px-2 text-sm text-gray-600 hover:bg-emerald-50 hover:text-emerald-700"
|
||||
>
|
||||
<Calendar class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Visits</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/orders"
|
||||
|
||||
@ -8,12 +8,18 @@
|
||||
deletePatient,
|
||||
} from '$lib/api/patients.js';
|
||||
import { fetchProvinces, fetchCities } from '$lib/api/geography.js';
|
||||
import { fetchVisitsByPatient } from '$lib/api/visits.js';
|
||||
import {
|
||||
fetchVisitsByPatient,
|
||||
fetchVisit,
|
||||
createVisit,
|
||||
updateVisit,
|
||||
deleteVisit,
|
||||
} from '$lib/api/visits.js';
|
||||
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
|
||||
import DataTable from '$lib/components/DataTable.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import SelectDropdown from '$lib/components/SelectDropdown.svelte';
|
||||
import { Plus, Edit2, Trash2, Search, ChevronLeft, ChevronRight, User, Calendar } from 'lucide-svelte';
|
||||
import { Plus, Edit2, Trash2, Search, ChevronLeft, ChevronRight, User, Calendar, Stethoscope, MapPin, Clock } from 'lucide-svelte';
|
||||
|
||||
// State
|
||||
let loading = $state(false);
|
||||
@ -28,6 +34,13 @@
|
||||
let deleteItem = $state(null);
|
||||
let visitsModalOpen = $state(false);
|
||||
let patientVisits = $state([]);
|
||||
let currentPatientForVisits = $state(null);
|
||||
let visitModalOpen = $state(false);
|
||||
let visitModalMode = $state('create');
|
||||
let visitFormLoading = $state(false);
|
||||
let visitFormErrors = $state({});
|
||||
let visitDeleteConfirmOpen = $state(false);
|
||||
let visitToDelete = $state(null);
|
||||
let currentStep = $state(1);
|
||||
let formErrors = $state({});
|
||||
let previousProvince = $state('');
|
||||
@ -74,7 +87,22 @@
|
||||
PatCom: '',
|
||||
});
|
||||
|
||||
|
||||
// Visit form data
|
||||
let visitFormData = $state({
|
||||
InternalPVID: null,
|
||||
PatientID: '',
|
||||
EpisodeID: '',
|
||||
InternalPID: null,
|
||||
PV1: {
|
||||
PatientClass: '',
|
||||
AssignedPatientLocation: '',
|
||||
AttendingDoctor: '',
|
||||
ReferringDoctor: '',
|
||||
AdmittingDoctor: '',
|
||||
AdmitDateTime: '',
|
||||
DischargeDateTime: '',
|
||||
},
|
||||
});
|
||||
|
||||
// Dropdown options (prefix - valueset not available yet)
|
||||
const prefixOptions = [
|
||||
@ -85,6 +113,37 @@
|
||||
{ value: 'Prof', label: 'Prof' },
|
||||
];
|
||||
|
||||
// Patient class options
|
||||
const patientClassOptions = [
|
||||
{ value: 'E', label: 'Emergency' },
|
||||
{ value: 'I', label: 'Inpatient' },
|
||||
{ value: 'O', label: 'Outpatient' },
|
||||
{ value: 'P', label: 'Preadmit' },
|
||||
{ value: 'R', label: 'Recurring Patient' },
|
||||
{ value: 'B', label: 'Obstetrics' },
|
||||
{ value: 'C', label: 'Commercial Account' },
|
||||
{ value: 'N', label: 'Not Applicable' },
|
||||
{ value: 'U', label: 'Unknown' },
|
||||
];
|
||||
|
||||
// ADT Status options with colors
|
||||
const adtStatusOptions = [
|
||||
{ value: 'A01', label: 'Admitted', color: 'badge-error', icon: '🏥' },
|
||||
{ value: 'A02', label: 'Transferred', color: 'badge-warning', icon: '🔄' },
|
||||
{ value: 'A03', label: 'Discharged', color: 'badge-success', icon: '✅' },
|
||||
{ value: 'A04', label: 'Registered', color: 'badge-info', icon: '📝' },
|
||||
{ value: 'A08', label: 'Updated', color: 'badge-ghost', icon: '✏️' },
|
||||
];
|
||||
|
||||
function getADTStatus(code) {
|
||||
return adtStatusOptions.find((s) => s.value === code) || { label: 'Unknown', color: 'badge-ghost', icon: '❓' };
|
||||
}
|
||||
|
||||
function getPatientClassLabel(code) {
|
||||
const option = patientClassOptions.find((o) => o.value === code);
|
||||
return option ? option.label : code || '-';
|
||||
}
|
||||
|
||||
// Derived values
|
||||
const provinceOptions = $derived(
|
||||
provinces.map((p) => ({ value: p.value, label: p.label }))
|
||||
@ -348,12 +407,149 @@
|
||||
}
|
||||
|
||||
async function openVisitsModal(patient) {
|
||||
currentPatientForVisits = patient;
|
||||
visitsModalOpen = true;
|
||||
await loadVisitsForPatient(patient.InternalPID);
|
||||
}
|
||||
|
||||
async function loadVisitsForPatient(patientId) {
|
||||
try {
|
||||
const response = await fetchVisitsByPatient(patient.InternalPID);
|
||||
const response = await fetchVisitsByPatient(patientId);
|
||||
patientVisits = Array.isArray(response.data) ? response.data : [];
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to load patient visits');
|
||||
patientVisits = [];
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateVisitModal() {
|
||||
visitModalMode = 'create';
|
||||
visitFormErrors = {};
|
||||
visitFormData = {
|
||||
InternalPVID: null,
|
||||
PatientID: currentPatientForVisits?.PatientID || '',
|
||||
EpisodeID: '',
|
||||
InternalPID: currentPatientForVisits?.InternalPID || null,
|
||||
PV1: {
|
||||
PatientClass: '',
|
||||
AssignedPatientLocation: '',
|
||||
AttendingDoctor: '',
|
||||
ReferringDoctor: '',
|
||||
AdmittingDoctor: '',
|
||||
AdmitDateTime: '',
|
||||
DischargeDateTime: '',
|
||||
},
|
||||
};
|
||||
visitModalOpen = true;
|
||||
}
|
||||
|
||||
async function openEditVisitModal(visit) {
|
||||
visitModalMode = 'edit';
|
||||
visitFormErrors = {};
|
||||
visitFormLoading = true;
|
||||
visitModalOpen = true;
|
||||
|
||||
try {
|
||||
const response = await fetchVisit(visit.PVID);
|
||||
const visitData = response.data || response;
|
||||
|
||||
visitFormData = {
|
||||
InternalPVID: visitData.InternalPVID,
|
||||
PatientID: visitData.PatientID || currentPatientForVisits?.PatientID || '',
|
||||
EpisodeID: visitData.EpisodeID || '',
|
||||
InternalPID: visitData.InternalPID || currentPatientForVisits?.InternalPID || null,
|
||||
PV1: {
|
||||
PatientClass: visitData.PV1?.PatientClass || '',
|
||||
AssignedPatientLocation: visitData.PV1?.AssignedPatientLocation || '',
|
||||
AttendingDoctor: visitData.PV1?.AttendingDoctor || '',
|
||||
ReferringDoctor: visitData.PV1?.ReferringDoctor || '',
|
||||
AdmittingDoctor: visitData.PV1?.AdmittingDoctor || '',
|
||||
AdmitDateTime: visitData.PV1?.AdmitDateTime
|
||||
? new Date(visitData.PV1.AdmitDateTime).toISOString().slice(0, 16)
|
||||
: '',
|
||||
DischargeDateTime: visitData.PV1?.DischargeDateTime
|
||||
? new Date(visitData.PV1.DischargeDateTime).toISOString().slice(0, 16)
|
||||
: '',
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to load visit details');
|
||||
visitModalOpen = false;
|
||||
} finally {
|
||||
visitFormLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateVisitForm() {
|
||||
const errors = {};
|
||||
|
||||
if (!visitFormData.EpisodeID?.trim()) {
|
||||
errors.EpisodeID = 'Episode ID is required';
|
||||
}
|
||||
if (!visitFormData.PV1?.PatientClass) {
|
||||
errors.PatientClass = 'Patient class is required';
|
||||
}
|
||||
|
||||
visitFormErrors = errors;
|
||||
return Object.keys(errors).length === 0;
|
||||
}
|
||||
|
||||
async function handleSaveVisit() {
|
||||
if (!validateVisitForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
saving = true;
|
||||
try {
|
||||
const payload = {
|
||||
...visitFormData,
|
||||
PV1: {
|
||||
...visitFormData.PV1,
|
||||
AdmitDateTime: visitFormData.PV1.AdmitDateTime
|
||||
? new Date(visitFormData.PV1.AdmitDateTime).toISOString()
|
||||
: undefined,
|
||||
DischargeDateTime: visitFormData.PV1.DischargeDateTime
|
||||
? new Date(visitFormData.PV1.DischargeDateTime).toISOString()
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
// Remove empty optional fields from PV1
|
||||
Object.keys(payload.PV1).forEach((key) => {
|
||||
if (payload.PV1[key] === '' || payload.PV1[key] === null) {
|
||||
delete payload.PV1[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (visitModalMode === 'create') {
|
||||
await createVisit(payload);
|
||||
toastSuccess('🎉 Visit created successfully!');
|
||||
} else {
|
||||
await updateVisit(payload);
|
||||
toastSuccess('✨ Visit updated successfully!');
|
||||
}
|
||||
visitModalOpen = false;
|
||||
await loadVisitsForPatient(currentPatientForVisits.InternalPID);
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to save visit');
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteVisit(visit) {
|
||||
visitToDelete = visit;
|
||||
visitDeleteConfirmOpen = true;
|
||||
}
|
||||
|
||||
async function handleDeleteVisit() {
|
||||
try {
|
||||
await deleteVisit(visitToDelete.InternalPVID);
|
||||
toastSuccess('🗑️ Visit deleted successfully');
|
||||
visitDeleteConfirmOpen = false;
|
||||
await loadVisitsForPatient(currentPatientForVisits.InternalPID);
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to delete visit');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1000,40 +1196,92 @@
|
||||
</Modal>
|
||||
|
||||
<!-- Patient Visits Modal -->
|
||||
<Modal bind:open={visitsModalOpen} title="Patient Visits" size="lg" closable={true}>
|
||||
<Modal
|
||||
bind:open={visitsModalOpen}
|
||||
title={currentPatientForVisits ? `Visits for ${[currentPatientForVisits.Prefix, currentPatientForVisits.NameFirst, currentPatientForVisits.NameLast].filter(Boolean).join(' ')}` : 'Patient Visits'}
|
||||
size="lg"
|
||||
closable={true}
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<!-- Fun Header with Add Button -->
|
||||
<div class="flex justify-between items-center pb-4 border-b border-base-200">
|
||||
<div class="flex items-center gap-2">
|
||||
<Stethoscope class="w-5 h-5 text-emerald-600" />
|
||||
<span class="text-sm text-gray-600">{patientVisits.length} visit(s) found</span>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-sm btn-primary gap-2 hover:scale-105 transition-transform"
|
||||
onclick={openCreateVisitModal}
|
||||
>
|
||||
<Plus class="w-4 h-4 animate-bounce" />
|
||||
Add Visit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if patientVisits.length === 0}
|
||||
<div class="text-center py-8 text-gray-500">No visits found for this patient</div>
|
||||
<!-- Fun Empty State -->
|
||||
<div class="text-center py-12">
|
||||
<div class="text-6xl mb-4">🏥</div>
|
||||
<h3 class="text-xl font-semibold text-gray-700 mb-2">No visits yet!</h3>
|
||||
<p class="text-gray-500 mb-6">This patient hasn't had any visits recorded.</p>
|
||||
<button
|
||||
class="btn btn-primary gap-2 hover:scale-105 transition-transform"
|
||||
onclick={openCreateVisitModal}
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
Create First Visit
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-y-auto max-h-96">
|
||||
<table class="table table-zebra table-hover">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr class="bg-base-200">
|
||||
<th>Visit ID</th>
|
||||
<th>Created</th>
|
||||
<th>Episode ID</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
<tr class="bg-emerald-50">
|
||||
<th class="text-emerald-800">Visit ID</th>
|
||||
<th class="text-emerald-800">Episode</th>
|
||||
<th class="text-emerald-800">Class</th>
|
||||
<th class="text-emerald-800">Status</th>
|
||||
<th class="text-emerald-800">Created</th>
|
||||
<th class="text-emerald-800 text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each patientVisits as visit}
|
||||
<tr>
|
||||
<td>{visit.PVID}</td>
|
||||
<td>{visit.CreateDate ? new Date(visit.CreateDate).toLocaleDateString() : '-'}</td>
|
||||
{@const status = getADTStatus(visit.PatVisitADT?.ADTCode)}
|
||||
<tr class="hover:bg-emerald-50/50 transition-colors">
|
||||
<td class="font-mono text-sm">{visit.PVID}</td>
|
||||
<td>{visit.EpisodeID || '-'}</td>
|
||||
<td>
|
||||
{#if visit.PatVisitADT?.ADTCode === 'A01'}Admit{/if}
|
||||
{#if visit.PatVisitADT?.ADTCode === 'A02'}Transfer{/if}
|
||||
{#if visit.PatVisitADT?.ADTCode === 'A03'}Discharge{/if}
|
||||
{#if visit.PatVisitADT?.ADTCode === 'A04'}Register{/if}
|
||||
{#if visit.PatVisitADT?.ADTCode === 'A08'}Update{/if}
|
||||
{#if !visit.PatVisitADT?.ADTCode}-{/if}
|
||||
<span class="badge badge-sm badge-outline">
|
||||
{getPatientClassLabel(visit.PV1?.PatientClass)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick={() => { visitsModalOpen = false; window.location.href = '/visits'; }}>
|
||||
Manage
|
||||
</button>
|
||||
<span class="badge badge-sm {status.color} gap-1">
|
||||
<span>{status.icon}</span>
|
||||
{status.label}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-sm text-gray-600">
|
||||
{visit.CreateDate ? new Date(visit.CreateDate).toLocaleDateString() : '-'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-center gap-1">
|
||||
<button
|
||||
class="btn btn-ghost btn-xs hover:bg-emerald-100 hover:text-emerald-700"
|
||||
title="Edit Visit"
|
||||
onclick={() => openEditVisitModal(visit)}
|
||||
>
|
||||
<Edit2 class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost btn-xs hover:bg-red-100 hover:text-red-600"
|
||||
title="Delete Visit"
|
||||
onclick={() => confirmDeleteVisit(visit)}
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
@ -1049,3 +1297,244 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<!-- Visit Form Modal -->
|
||||
<Modal
|
||||
bind:open={visitModalOpen}
|
||||
title={visitModalMode === 'create' ? '🏥 Create New Visit' : '✏️ Edit Visit'}
|
||||
size="lg"
|
||||
>
|
||||
{#if visitFormLoading}
|
||||
<div class="flex flex-col items-center justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg text-emerald-600"></span>
|
||||
<p class="text-gray-500 mt-4">Loading visit data...</p>
|
||||
</div>
|
||||
{:else}
|
||||
<form class="space-y-5" onsubmit={(e) => e.preventDefault()}>
|
||||
<!-- Locked Patient Display -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="patient">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<User class="w-4 h-4" />
|
||||
Patient
|
||||
</span>
|
||||
</label>
|
||||
<div class="input input-bordered w-full bg-base-200 flex items-center gap-2 opacity-75">
|
||||
<span class="text-xl">🔒</span>
|
||||
<span class="font-medium">
|
||||
{currentPatientForVisits
|
||||
? [currentPatientForVisits.Prefix, currentPatientForVisits.NameFirst, currentPatientForVisits.NameLast].filter(Boolean).join(' ')
|
||||
: 'Unknown Patient'}
|
||||
</span>
|
||||
<span class="text-sm text-gray-500 ml-auto">
|
||||
ID: {currentPatientForVisits?.PatientID || '-'}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">Patient is locked for this visit</p>
|
||||
</div>
|
||||
|
||||
<!-- Episode ID & Patient Class -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="episodeId">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<Calendar class="w-4 h-4" />
|
||||
Episode ID
|
||||
<span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="episodeId"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
class:input-error={visitFormErrors.EpisodeID}
|
||||
bind:value={visitFormData.EpisodeID}
|
||||
placeholder="Enter episode ID"
|
||||
/>
|
||||
{#if visitFormErrors.EpisodeID}
|
||||
<span class="text-error text-sm mt-1">{visitFormErrors.EpisodeID}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="patientClass">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<Stethoscope class="w-4 h-4" />
|
||||
Patient Class
|
||||
<span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<select
|
||||
id="patientClass"
|
||||
class="select select-bordered w-full"
|
||||
class:select-error={visitFormErrors.PatientClass}
|
||||
bind:value={visitFormData.PV1.PatientClass}
|
||||
>
|
||||
<option value="">Select class...</option>
|
||||
{#each patientClassOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if visitFormErrors.PatientClass}
|
||||
<span class="text-error text-sm mt-1">{visitFormErrors.PatientClass}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="location">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<MapPin class="w-4 h-4" />
|
||||
Assigned Location
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="location"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={visitFormData.PV1.AssignedPatientLocation}
|
||||
placeholder="e.g., Ward A, Room 101, ER Bay 3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Doctors -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="attendingDoctor">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<User class="w-4 h-4" />
|
||||
Attending Doctor
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="attendingDoctor"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={visitFormData.PV1.AttendingDoctor}
|
||||
placeholder="Dr. Smith"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="referringDoctor">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<User class="w-4 h-4" />
|
||||
Referring Doctor
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="referringDoctor"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={visitFormData.PV1.ReferringDoctor}
|
||||
placeholder="Dr. Johnson"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admitting Doctor -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="admittingDoctor">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<User class="w-4 h-4" />
|
||||
Admitting Doctor
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="admittingDoctor"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={visitFormData.PV1.AdmittingDoctor}
|
||||
placeholder="Dr. Williams"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Dates -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="admitDate">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<Clock class="w-4 h-4" />
|
||||
Admit Date & Time
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="admitDate"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={visitFormData.PV1.AdmitDateTime}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="dischargeDate">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<Clock class="w-4 h-4" />
|
||||
Discharge Date & Time
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="dischargeDate"
|
||||
type="datetime-local"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={visitFormData.PV1.DischargeDateTime}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#snippet footer()}
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-ghost" onclick={() => (visitModalOpen = false)} type="button">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary gap-2 hover:scale-105 transition-transform"
|
||||
onclick={handleSaveVisit}
|
||||
disabled={saving}
|
||||
type="button"
|
||||
>
|
||||
{#if saving}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Saving...
|
||||
{:else}
|
||||
<span>{visitModalMode === 'create' ? '🎉' : '✨'}</span>
|
||||
{visitModalMode === 'create' ? 'Create Visit' : 'Update Visit'}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<!-- Visit Delete Confirmation Modal -->
|
||||
<Modal bind:open={visitDeleteConfirmOpen} title="🗑️ Delete Visit?" size="sm">
|
||||
<div class="py-2">
|
||||
<p class="text-base-content/80">
|
||||
Are you sure you want to delete visit
|
||||
<strong class="text-base-content">#{visitToDelete?.PVID}</strong>?
|
||||
</p>
|
||||
{#if visitToDelete?.EpisodeID}
|
||||
<p class="text-sm text-gray-500 mt-2">Episode: {visitToDelete.EpisodeID}</p>
|
||||
{/if}
|
||||
<p class="text-sm text-error mt-3">⚠️ This action cannot be undone.</p>
|
||||
</div>
|
||||
{#snippet footer()}
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
onclick={() => (visitDeleteConfirmOpen = false)}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-error gap-2"
|
||||
onclick={handleDeleteVisit}
|
||||
type="button"
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
Delete
|
||||
</button>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
@ -1,851 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
fetchVisits,
|
||||
fetchVisit,
|
||||
createVisit,
|
||||
updateVisit,
|
||||
deleteVisit,
|
||||
createADT,
|
||||
updateADT,
|
||||
fetchVisitsByPatient,
|
||||
} from '$lib/api/visits.js';
|
||||
import { fetchPatients } from '$lib/api/patients.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 DataTable from '$lib/components/DataTable.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import SelectDropdown from '$lib/components/SelectDropdown.svelte';
|
||||
import { Plus, Edit2, Trash2, Search, LogIn, ArrowRight, LogOut, Calendar, CheckCircle2 } from 'lucide-svelte';
|
||||
|
||||
let loading = $state(false);
|
||||
let saving = $state(false);
|
||||
let visits = $state([]);
|
||||
let modalOpen = $state(false);
|
||||
let adtModalOpen = $state(false);
|
||||
let patientModalOpen = $state(false);
|
||||
let deleteConfirmOpen = $state(false);
|
||||
let modalMode = $state('create');
|
||||
let currentTab = $state(1);
|
||||
let deleteItem = $state(null);
|
||||
let selectedVisit = $state(null);
|
||||
let formErrors = $state({});
|
||||
|
||||
let patients = $state([]);
|
||||
let locations = $state([]);
|
||||
let contacts = $state([]);
|
||||
|
||||
let searchQuery = $state('');
|
||||
let visitIdQuery = $state('');
|
||||
let dateFrom = $state('');
|
||||
let dateTo = $state('');
|
||||
|
||||
let formData = $state({
|
||||
InternalPVID: null,
|
||||
PVID: '',
|
||||
InternalPID: '',
|
||||
EpisodeID: '',
|
||||
SiteID: '',
|
||||
PatDiag: {
|
||||
DiagCode: '',
|
||||
Diagnosis: '',
|
||||
},
|
||||
PatVisitADT: null,
|
||||
});
|
||||
|
||||
let adtFormData = $state({
|
||||
InternalPVID: null,
|
||||
ADTCode: '',
|
||||
LocationID: '',
|
||||
AttDoc: '',
|
||||
RefDoc: '',
|
||||
AdmDoc: '',
|
||||
CnsDoc: '',
|
||||
});
|
||||
|
||||
let patientSearchQuery = $state('');
|
||||
let filteredPatients = $state([]);
|
||||
|
||||
const adtCodeOptions = [
|
||||
{ value: 'A01', label: 'Admit' },
|
||||
{ value: 'A02', label: 'Transfer' },
|
||||
{ value: 'A03', label: 'Discharge' },
|
||||
{ value: 'A04', label: 'Register' },
|
||||
{ value: 'A08', label: 'Update' },
|
||||
];
|
||||
|
||||
const locationOptions = $derived(
|
||||
locations.map((l) => ({ value: l.LocationID, label: l.LocFull || l.LocCode }))
|
||||
);
|
||||
|
||||
const contactOptions = $derived(
|
||||
contacts.map((c) => ({
|
||||
value: c.ContactID,
|
||||
label: [c.NameFirst, c.NameLast].filter(Boolean).join(' ') || c.NameFirst || '-',
|
||||
}))
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{ key: 'PVID', label: 'Visit ID', class: 'font-medium' },
|
||||
{ key: 'PatientName', label: 'Patient' },
|
||||
{ key: 'EpisodeID', label: 'Episode ID' },
|
||||
{ key: 'CreateDateFormatted', label: 'Created' },
|
||||
{ key: 'StatusLabel', label: 'Status' },
|
||||
{ key: 'actions', label: 'Actions', class: 'w-48 text-center', render: (row) => {
|
||||
return `
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button class="btn btn-ghost btn-xs text-blue-600" title="Admit" onclick="window.openVisitADT('${row.PVID}', 'A01')">
|
||||
<LogIn class="w-4 h-4" />
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-xs text-orange-600" title="Transfer" onclick="window.openVisitADT('${row.PVID}', 'A02')">
|
||||
<ArrowRight class="w-4 h-4" />
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-xs text-red-600" title="Discharge" onclick="window.openVisitADT('${row.PVID}', 'A03')">
|
||||
<LogOut class="w-4 h-4" />
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-xs" title="Edit" onclick="window.openVisitEdit('${row.PVID}')">
|
||||
<Edit2 class="w-4 h-4" />
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-xs text-red-500" title="Delete" onclick="window.openVisitDelete('${row.PVID}', '${row.InternalPVID}')">
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}},
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
await Promise.all([loadVisits(), loadLocations(), loadContacts()]);
|
||||
window.openVisitADT = (pvid, adtCode) => {
|
||||
const visit = visits.find(v => v.PVID === pvid);
|
||||
if (visit) openADTModal(visit, adtCode);
|
||||
};
|
||||
window.openVisitEdit = (pvid) => {
|
||||
const visit = visits.find(v => v.PVID === pvid);
|
||||
if (visit) openEditModal(visit);
|
||||
};
|
||||
window.openVisitDelete = (pvid, internalPVID) => {
|
||||
const visit = visits.find(v => v.PVID === pvid);
|
||||
if (visit) {
|
||||
deleteItem = visit;
|
||||
deleteConfirmOpen = true;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
async function loadVisits() {
|
||||
loading = true;
|
||||
try {
|
||||
const params = {};
|
||||
if (searchQuery.trim()) {
|
||||
params.PatientName = searchQuery.trim();
|
||||
}
|
||||
if (visitIdQuery.trim()) {
|
||||
params.PVID = visitIdQuery.trim();
|
||||
}
|
||||
if (dateFrom) {
|
||||
params.DateFrom = dateFrom;
|
||||
}
|
||||
if (dateTo) {
|
||||
params.DateTo = dateTo;
|
||||
}
|
||||
|
||||
const response = await fetchVisits(params);
|
||||
visits = Array.isArray(response.data) ? response.data : [];
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to load visits');
|
||||
visits = [];
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLocations() {
|
||||
try {
|
||||
const response = await fetchLocations();
|
||||
locations = Array.isArray(response.data) ? response.data : [];
|
||||
} catch (err) {
|
||||
console.error('Failed to load locations:', err);
|
||||
locations = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadContacts() {
|
||||
try {
|
||||
const response = await fetchContacts();
|
||||
contacts = Array.isArray(response.data) ? response.data : [];
|
||||
} catch (err) {
|
||||
console.error('Failed to load contacts:', err);
|
||||
contacts = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPatients() {
|
||||
try {
|
||||
const params = {};
|
||||
if (patientSearchQuery.trim()) {
|
||||
params.Name = patientSearchQuery.trim();
|
||||
}
|
||||
const response = await fetchPatients(params);
|
||||
patients = Array.isArray(response.data) ? response.data : [];
|
||||
filteredPatients = patients;
|
||||
} catch (err) {
|
||||
console.error('Failed to load patients:', err);
|
||||
patients = [];
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
modalMode = 'create';
|
||||
currentTab = 1;
|
||||
formErrors = {};
|
||||
formData = {
|
||||
InternalPVID: null,
|
||||
PVID: '',
|
||||
InternalPID: '',
|
||||
EpisodeID: '',
|
||||
SiteID: '',
|
||||
PatDiag: {
|
||||
DiagCode: '',
|
||||
Diagnosis: '',
|
||||
},
|
||||
PatVisitADT: null,
|
||||
};
|
||||
modalOpen = true;
|
||||
}
|
||||
|
||||
async function openEditModal(row) {
|
||||
modalMode = 'edit';
|
||||
currentTab = 1;
|
||||
formErrors = {};
|
||||
try {
|
||||
const response = await fetchVisit(row.PVID);
|
||||
const visit = response.data || response;
|
||||
|
||||
formData = {
|
||||
InternalPVID: visit.InternalPVID,
|
||||
PVID: visit.PVID || '',
|
||||
InternalPID: visit.InternalPID || '',
|
||||
EpisodeID: visit.EpisodeID || '',
|
||||
SiteID: visit.SiteID || '',
|
||||
PatDiag: visit.PatDiag || { DiagCode: '', Diagnosis: '' },
|
||||
PatVisitADT: visit.PatVisitADT || null,
|
||||
};
|
||||
modalOpen = true;
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to load visit details');
|
||||
}
|
||||
}
|
||||
|
||||
async function openPatientVisitsModal(patient) {
|
||||
patientModalOpen = true;
|
||||
try {
|
||||
const response = await fetchVisitsByPatient(patient.InternalPID);
|
||||
const patientVisits = Array.isArray(response.data) ? response.data : [];
|
||||
filteredPatients = [
|
||||
{
|
||||
...patient,
|
||||
visits: patientVisits.map((v) => ({
|
||||
...v,
|
||||
CreateDateFormatted: v.CreateDate ? new Date(v.CreateDate).toLocaleDateString() : '-',
|
||||
StatusLabel: getVisitStatusLabel(v),
|
||||
PatientName: [patient.Prefix, patient.NameFirst, patient.NameMiddle, patient.NameLast, patient.Suffix]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
})),
|
||||
},
|
||||
];
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to load patient visits');
|
||||
}
|
||||
}
|
||||
|
||||
function openADTModal(visit, preselectedADTCode = '') {
|
||||
selectedVisit = visit;
|
||||
adtFormData = {
|
||||
InternalPVID: visit.InternalPVID,
|
||||
ADTCode: preselectedADTCode,
|
||||
LocationID: '',
|
||||
AttDoc: '',
|
||||
RefDoc: '',
|
||||
AdmDoc: '',
|
||||
CnsDoc: '',
|
||||
};
|
||||
adtModalOpen = true;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
saving = true;
|
||||
formErrors = {};
|
||||
try {
|
||||
const dataToSubmit = { ...formData };
|
||||
if (!dataToSubmit.PatVisitADT || Object.keys(dataToSubmit.PatVisitADT).length === 0) {
|
||||
delete dataToSubmit.PatVisitADT;
|
||||
}
|
||||
|
||||
if (modalMode === 'create') {
|
||||
await createVisit(dataToSubmit);
|
||||
toastSuccess('Visit created successfully');
|
||||
} else {
|
||||
await updateVisit(dataToSubmit);
|
||||
toastSuccess('Visit updated successfully');
|
||||
}
|
||||
modalOpen = false;
|
||||
await loadVisits();
|
||||
} catch (err) {
|
||||
formErrors = err.errors || { general: err.message || 'Failed to save visit' };
|
||||
toastError(formErrors.general || 'Failed to save visit');
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleADTSubmit() {
|
||||
saving = true;
|
||||
formErrors = {};
|
||||
try {
|
||||
await createADT(adtFormData);
|
||||
toastSuccess(`ADT action completed: ${adtCodeOptions.find((o) => o.value === adtFormData.ADTCode)?.label}`);
|
||||
adtModalOpen = false;
|
||||
await loadVisits();
|
||||
} catch (err) {
|
||||
formErrors = err.errors || { general: err.message || 'Failed to complete ADT action' };
|
||||
toastError(formErrors.general || 'Failed to complete ADT action');
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await deleteVisit(deleteItem.InternalPVID);
|
||||
toastSuccess('Visit deleted successfully');
|
||||
deleteConfirmOpen = false;
|
||||
deleteItem = null;
|
||||
await loadVisits();
|
||||
} catch (err) {
|
||||
toastError(err.message || 'Failed to delete visit');
|
||||
}
|
||||
}
|
||||
|
||||
function getVisitStatusLabel(visit) {
|
||||
if (visit.PatVisitADT && visit.PatVisitADT.ADTCode) {
|
||||
const adtCode = visit.PatVisitADT.ADTCode;
|
||||
return adtCodeOptions.find((o) => o.value === adtCode)?.label || adtCode;
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (patientSearchQuery.trim()) {
|
||||
const query = patientSearchQuery.toLowerCase();
|
||||
filteredPatients = patients.filter((p) => {
|
||||
const name = [p.NameFirst, p.NameLast, p.PatientID].join(' ').toLowerCase();
|
||||
return name.includes(query);
|
||||
});
|
||||
} else if (patients.length > 0) {
|
||||
filteredPatients = patients;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">Patient Visits</h1>
|
||||
<p class="text-gray-500 mt-1">Manage patient encounters and ADT workflow</p>
|
||||
</div>
|
||||
<button class="btn btn-primary gap-2" onclick={openCreateModal}>
|
||||
<Plus class="w-5 h-5" />
|
||||
New Visit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-md">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-col md:flex-row gap-4 mb-6">
|
||||
<div class="flex-1">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Search</span>
|
||||
</label>
|
||||
<div class="join">
|
||||
<span class="btn btn-disabled join-item"><Search class="w-4 h-4" /></span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Patient name..."
|
||||
class="input input-bordered join-item flex-1"
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Visit ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g., DV00001"
|
||||
class="input input-bordered"
|
||||
bind:value={visitIdQuery}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">From</span>
|
||||
</label>
|
||||
<input type="date" class="input input-bordered" bind:value={dateFrom} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">To</span>
|
||||
</label>
|
||||
<input type="date" class="input input-bordered" bind:value={dateTo} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button class="btn btn-primary" onclick={loadVisits}>Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={visits.map((v) => {
|
||||
return {
|
||||
...v,
|
||||
CreateDateFormatted: v.CreateDate ? new Date(v.CreateDate).toLocaleDateString() : '-',
|
||||
StatusLabel: getVisitStatusLabel(v),
|
||||
PatientName: v.PatientName || '-',
|
||||
};
|
||||
})}
|
||||
loading={loading}
|
||||
striped={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visit Form Modal -->
|
||||
<Modal bind:open={modalOpen} title={modalMode === 'create' ? 'New Visit' : 'Edit Visit'} size="lg">
|
||||
<form class="space-y-5" onsubmit={(e) => e.preventDefault()}>
|
||||
{#if formErrors.general}
|
||||
<div class="alert alert-error text-sm">
|
||||
<span>{formErrors.general}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-sm {currentTab === 1 ? 'btn-primary' : ''}" onclick={() => currentTab = 1}>Basic Info</button>
|
||||
<button class="btn btn-sm {currentTab === 2 ? 'btn-primary' : ''}" onclick={() => currentTab = 2}>Diagnosis</button>
|
||||
<button class="btn btn-sm {currentTab === 3 ? 'btn-primary' : ''}" onclick={() => currentTab = 3}>Initial ADT</button>
|
||||
</div>
|
||||
|
||||
{#if currentTab === 1}
|
||||
<div class="space-y-5">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Patient *</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered flex-1"
|
||||
bind:value={formData.InternalPID}
|
||||
placeholder="Enter Internal Patient ID"
|
||||
required
|
||||
/>
|
||||
<button type="button" class="btn btn-outline gap-2" onclick={() => { patientSearchQuery = ''; loadPatients(); patientModalOpen = true; }}>
|
||||
<Search class="w-4 h-4" />
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
{#if formErrors.InternalPID}
|
||||
<label class="label text-error text-xs">{formErrors.InternalPID}</label>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Episode ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
bind:value={formData.EpisodeID}
|
||||
placeholder="Enter episode ID"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Visit ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
bind:value={formData.PVID}
|
||||
placeholder="Auto-generated (DV prefix)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Site</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="site"
|
||||
bind:value={formData.SiteID}
|
||||
options={locationOptions}
|
||||
placeholder="Select site..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if currentTab === 2}
|
||||
<div class="space-y-5">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Diagnosis Code</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
bind:value={formData.PatDiag.DiagCode}
|
||||
placeholder="e.g., I10"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Diagnosis</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
bind:value={formData.PatDiag.Diagnosis}
|
||||
placeholder="Enter diagnosis description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if currentTab === 3}
|
||||
<div class="space-y-5">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Initial ADT Action</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="adtCode"
|
||||
value={formData.PatVisitADT?.ADTCode || ''}
|
||||
oninput={(e) => {
|
||||
if (!formData.PatVisitADT) formData.PatVisitADT = {};
|
||||
formData.PatVisitADT.ADTCode = e.target.value;
|
||||
}}
|
||||
options={adtCodeOptions}
|
||||
placeholder="Select ADT action (optional)..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if (formData.PatVisitADT?.ADTCode === 'A01' || formData.PatVisitADT?.ADTCode === 'A02')}
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Location *</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="location"
|
||||
value={formData.PatVisitADT?.LocationID || ''}
|
||||
oninput={(e) => {
|
||||
if (!formData.PatVisitADT) formData.PatVisitADT = {};
|
||||
formData.PatVisitADT.LocationID = e.target.value;
|
||||
}}
|
||||
options={locationOptions}
|
||||
placeholder="Select location..."
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Attending Physician</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="attDoc"
|
||||
value={formData.PatVisitADT?.AttDoc || ''}
|
||||
oninput={(e) => {
|
||||
if (!formData.PatVisitADT) formData.PatVisitADT = {};
|
||||
formData.PatVisitADT.AttDoc = e.target.value;
|
||||
}}
|
||||
options={contactOptions}
|
||||
placeholder="Select physician..."
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Referring Physician</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="refDoc"
|
||||
value={formData.PatVisitADT?.RefDoc || ''}
|
||||
oninput={(e) => {
|
||||
if (!formData.PatVisitADT) formData.PatVisitADT = {};
|
||||
formData.PatVisitADT.RefDoc = e.target.value;
|
||||
}}
|
||||
options={contactOptions}
|
||||
placeholder="Select physician..."
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Admitting Physician</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="admDoc"
|
||||
value={formData.PatVisitADT?.AdmDoc || ''}
|
||||
oninput={(e) => {
|
||||
if (!formData.PatVisitADT) formData.PatVisitADT = {};
|
||||
formData.PatVisitADT.AdmDoc = e.target.value;
|
||||
}}
|
||||
options={contactOptions}
|
||||
placeholder="Select physician..."
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Consulting Physician</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="cnsDoc"
|
||||
value={formData.PatVisitADT?.CnsDoc || ''}
|
||||
oninput={(e) => {
|
||||
if (!formData.PatVisitADT) formData.PatVisitADT = {};
|
||||
formData.PatVisitADT.CnsDoc = e.target.value;
|
||||
}}
|
||||
options={contactOptions}
|
||||
placeholder="Select physician..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
{#snippet footer()}
|
||||
<div class="flex justify-between w-full">
|
||||
<button class="btn btn-ghost" onclick={() => modalOpen = false}>Cancel</button>
|
||||
<button class="btn btn-primary gap-2" disabled={saving} onclick={handleSave}>
|
||||
{saving ? '<span class="loading loading-spinner loading-sm"></span>' : ''}
|
||||
{modalMode === 'create' ? 'Create Visit' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<!-- ADT Action Modal -->
|
||||
<Modal bind:open={adtModalOpen} title="ADT Action" size="md">
|
||||
<form class="space-y-5" onsubmit={(e) => e.preventDefault()}>
|
||||
{#if formErrors.general}
|
||||
<div class="alert alert-error text-sm">
|
||||
<span>{formErrors.general}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">ADT Code *</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="adtCode"
|
||||
bind:value={adtFormData.ADTCode}
|
||||
options={adtCodeOptions}
|
||||
placeholder="Select ADT action..."
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if adtFormData.ADTCode === 'A01' || adtFormData.ADTCode === 'A02'}
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Location *</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="location"
|
||||
bind:value={adtFormData.LocationID}
|
||||
options={locationOptions}
|
||||
placeholder="Select location..."
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Attending Physician</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="attDoc"
|
||||
bind:value={adtFormData.AttDoc}
|
||||
options={contactOptions}
|
||||
placeholder="Select physician..."
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Referring Physician</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="refDoc"
|
||||
bind:value={adtFormData.RefDoc}
|
||||
options={contactOptions}
|
||||
placeholder="Select physician..."
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Admitting Physician</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="admDoc"
|
||||
bind:value={adtFormData.AdmDoc}
|
||||
options={contactOptions}
|
||||
placeholder="Select physician..."
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Consulting Physician</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
name="cnsDoc"
|
||||
bind:value={adtFormData.CnsDoc}
|
||||
options={contactOptions}
|
||||
placeholder="Select physician..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{#snippet footer()}
|
||||
<div class="flex justify-between w-full">
|
||||
<button class="btn btn-ghost" onclick={() => adtModalOpen = false}>Cancel</button>
|
||||
<button class="btn btn-primary gap-2" disabled={saving} onclick={handleADTSubmit}>
|
||||
{saving ? '<span class="loading loading-spinner loading-sm"></span>' : ''}
|
||||
Complete Action
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<!-- Patient Selection Modal -->
|
||||
<Modal bind:open={patientModalOpen} title="Select Patient" size="lg" closable={true}>
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<div class="join">
|
||||
<span class="btn btn-disabled join-item"><Search class="w-4 h-4" /></span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or patient ID..."
|
||||
class="input input-bordered join-item flex-1"
|
||||
bind:value={patientSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if filteredPatients.length === 0}
|
||||
<div class="text-center py-8 text-gray-500">No patients found</div>
|
||||
{:else}
|
||||
<div class="overflow-y-auto max-h-96">
|
||||
<table class="table table-zebra table-hover">
|
||||
<thead>
|
||||
<tr class="bg-base-200">
|
||||
<th>Patient ID</th>
|
||||
<th>Name</th>
|
||||
<th>Birthdate</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredPatients as patient}
|
||||
{#if patient.visits}
|
||||
<tr class="bg-base-100 font-semibold">
|
||||
<td colspan="4" class="text-emerald-600">
|
||||
{patient.PatientID} - {[patient.Prefix, patient.NameFirst, patient.NameLast].filter(Boolean).join(' ')}
|
||||
</td>
|
||||
</tr>
|
||||
{#each patient.visits as visit}
|
||||
<tr>
|
||||
<td class="pl-8">{visit.PVID}</td>
|
||||
<td class="pl-8">{visit.StatusLabel}</td>
|
||||
<td class="pl-8">{visit.CreateDateFormatted}</td>
|
||||
<td class="pl-8">
|
||||
<button class="btn btn-ghost btn-xs" onclick={() => { patientModalOpen = false; openEditModal(visit); }}>
|
||||
Edit
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else}
|
||||
<tr>
|
||||
<td>{patient.PatientID}</td>
|
||||
<td>
|
||||
{[patient.Prefix, patient.NameFirst, patient.NameMiddle, patient.NameLast, patient.Suffix]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
</td>
|
||||
<td>{patient.Birthdate ? new Date(patient.Birthdate).toLocaleDateString() : '-'}</td>
|
||||
<td>
|
||||
<button class="btn btn-primary btn-xs gap-1" onclick={() => { formData.InternalPID = patient.InternalPID; patientModalOpen = false; }}>
|
||||
<CheckCircle2 class="w-3 h-3" />
|
||||
Select
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#snippet footer()}
|
||||
<div class="flex justify-end w-full">
|
||||
<button class="btn btn-ghost" onclick={() => patientModalOpen = false}>Cancel</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<Modal bind:open={deleteConfirmOpen} title="Confirm Delete" size="sm" closable={true}>
|
||||
<div class="space-y-4">
|
||||
<p class="text-gray-700">
|
||||
Are you sure you want to delete visit <span class="font-bold">{deleteItem?.PVID}</span>?
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">This action cannot be undone.</p>
|
||||
</div>
|
||||
|
||||
{#snippet footer()}
|
||||
<div class="flex justify-between w-full">
|
||||
<button class="btn btn-ghost" onclick={() => deleteConfirmOpen = false}>Cancel</button>
|
||||
<button class="btn btn-error gap-2" onclick={handleDelete}>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
Loading…
x
Reference in New Issue
Block a user