feat: Add Visits Management module and integrate with patients
- Add Visits API client and page routes - Add Calendar icon and Visits menu item to Sidebar - Add patient visits view modal in patients page - Update AGENTS.md to use pnpm commands
This commit is contained in:
parent
4641668f78
commit
1e032e5278
@ -8,16 +8,16 @@ SvelteKit frontend for Clinical Laboratory Quality Management System (CLQMS). Us
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Development server
|
# Development server
|
||||||
npm run dev
|
pnpm run dev
|
||||||
|
|
||||||
# Production build
|
# Production build
|
||||||
npm run build
|
pnpm run build
|
||||||
|
|
||||||
# Preview production build
|
# Preview production build
|
||||||
npm run preview
|
pnpm run preview
|
||||||
|
|
||||||
# Sync SvelteKit (runs automatically on install)
|
# Sync SvelteKit (runs automatically on install)
|
||||||
npm run prepare
|
pnpm run prepare
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|||||||
34
src/lib/api/visits.js
Normal file
34
src/lib/api/visits.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { get, post, patch, del } from './client.js';
|
||||||
|
|
||||||
|
export async function fetchVisits(params = {}) {
|
||||||
|
const query = new URLSearchParams(params).toString();
|
||||||
|
return get(query ? `/api/patvisit?${query}` : '/api/patvisit');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchVisit(id) {
|
||||||
|
return get(`/api/patvisit/${encodeURIComponent(id)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchVisitsByPatient(patientId) {
|
||||||
|
return get(`/api/patvisit/patient/${encodeURIComponent(patientId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createVisit(data) {
|
||||||
|
return post('/api/patvisit', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateVisit(data) {
|
||||||
|
return patch('/api/patvisit', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteVisit(id) {
|
||||||
|
return del('/api/patvisit', { body: JSON.stringify({ InternalPVID: id }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createADT(data) {
|
||||||
|
return post('/api/patvisitadt', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateADT(data) {
|
||||||
|
return patch('/api/patvisitadt', data);
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@
|
|||||||
Briefcase,
|
Briefcase,
|
||||||
Hash,
|
Hash,
|
||||||
Globe,
|
Globe,
|
||||||
|
Calendar,
|
||||||
ChevronDown
|
ChevronDown
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { auth } from '$lib/stores/auth.js';
|
import { auth } from '$lib/stores/auth.js';
|
||||||
@ -251,6 +252,15 @@
|
|||||||
<span>Patients</span>
|
<span>Patients</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="/orders"
|
href="/orders"
|
||||||
|
|||||||
@ -8,11 +8,12 @@
|
|||||||
deletePatient,
|
deletePatient,
|
||||||
} from '$lib/api/patients.js';
|
} from '$lib/api/patients.js';
|
||||||
import { fetchProvinces, fetchCities } from '$lib/api/geography.js';
|
import { fetchProvinces, fetchCities } from '$lib/api/geography.js';
|
||||||
|
import { fetchVisitsByPatient } from '$lib/api/visits.js';
|
||||||
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
|
import { success as toastSuccess, error as toastError } from '$lib/utils/toast.js';
|
||||||
import DataTable from '$lib/components/DataTable.svelte';
|
import DataTable from '$lib/components/DataTable.svelte';
|
||||||
import Modal from '$lib/components/Modal.svelte';
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
import SelectDropdown from '$lib/components/SelectDropdown.svelte';
|
import SelectDropdown from '$lib/components/SelectDropdown.svelte';
|
||||||
import { Plus, Edit2, Trash2, Search, ChevronLeft, ChevronRight, User } from 'lucide-svelte';
|
import { Plus, Edit2, Trash2, Search, ChevronLeft, ChevronRight, User, Calendar } from 'lucide-svelte';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
@ -25,6 +26,8 @@
|
|||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
let deleteConfirmOpen = $state(false);
|
let deleteConfirmOpen = $state(false);
|
||||||
let deleteItem = $state(null);
|
let deleteItem = $state(null);
|
||||||
|
let visitsModalOpen = $state(false);
|
||||||
|
let patientVisits = $state([]);
|
||||||
let currentStep = $state(1);
|
let currentStep = $state(1);
|
||||||
let formErrors = $state({});
|
let formErrors = $state({});
|
||||||
let previousProvince = $state('');
|
let previousProvince = $state('');
|
||||||
@ -344,6 +347,16 @@
|
|||||||
deleteConfirmOpen = true;
|
deleteConfirmOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openVisitsModal(patient) {
|
||||||
|
visitsModalOpen = true;
|
||||||
|
try {
|
||||||
|
const response = await fetchVisitsByPatient(patient.InternalPID);
|
||||||
|
patientVisits = Array.isArray(response.data) ? response.data : [];
|
||||||
|
} catch (err) {
|
||||||
|
toastError(err.message || 'Failed to load patient visits');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
try {
|
try {
|
||||||
await deletePatient(deleteItem.InternalPID);
|
await deletePatient(deleteItem.InternalPID);
|
||||||
@ -417,10 +430,13 @@
|
|||||||
{#snippet cell({ column, row, value })}
|
{#snippet cell({ column, row, value })}
|
||||||
{#if column.key === 'actions'}
|
{#if column.key === 'actions'}
|
||||||
<div class="flex justify-center gap-2">
|
<div class="flex justify-center gap-2">
|
||||||
<button class="btn btn-sm btn-ghost" onclick={() => openEditModal(row)}>
|
<button class="btn btn-sm btn-ghost" title="View Visits" onclick={() => openVisitsModal(row)}>
|
||||||
|
<Calendar class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-ghost" title="Edit" onclick={() => openEditModal(row)}>
|
||||||
<Edit2 class="w-4 h-4" />
|
<Edit2 class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-ghost text-error" onclick={() => confirmDelete(row)}>
|
<button class="btn btn-sm btn-ghost text-error" title="Delete" onclick={() => confirmDelete(row)}>
|
||||||
<Trash2 class="w-4 h-4" />
|
<Trash2 class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -982,3 +998,54 @@
|
|||||||
<button class="btn btn-error" onclick={handleDelete} type="button">Delete</button>
|
<button class="btn btn-error" onclick={handleDelete} type="button">Delete</button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<!-- Patient Visits Modal -->
|
||||||
|
<Modal bind:open={visitsModalOpen} title="Patient Visits" size="lg" closable={true}>
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#if patientVisits.length === 0}
|
||||||
|
<div class="text-center py-8 text-gray-500">No visits found for this patient</div>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-y-auto max-h-96">
|
||||||
|
<table class="table table-zebra table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-base-200">
|
||||||
|
<th>Visit ID</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Episode ID</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each patientVisits as visit}
|
||||||
|
<tr>
|
||||||
|
<td>{visit.PVID}</td>
|
||||||
|
<td>{visit.CreateDate ? new Date(visit.CreateDate).toLocaleDateString() : '-'}</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}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick={() => { visitsModalOpen = false; window.location.href = '/visits'; }}>
|
||||||
|
Manage
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#snippet footer()}
|
||||||
|
<div class="flex justify-end w-full">
|
||||||
|
<button class="btn btn-ghost" onclick={() => visitsModalOpen = false}>Close</button>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Modal>
|
||||||
|
|||||||
851
src/routes/(app)/visits/+page.svelte
Normal file
851
src/routes/(app)/visits/+page.svelte
Normal file
@ -0,0 +1,851 @@
|
|||||||
|
<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