From 382b05d98ef527738fadd3310d2ff2f0c98a32d5 Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Thu, 12 Feb 2026 16:28:24 +0700 Subject: [PATCH] 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 --- src/lib/components/Sidebar.svelte | 10 - src/routes/(app)/patients/+page.svelte | 539 +++++++++++++++- src/routes/(app)/visits/+page.svelte | 851 ------------------------- 3 files changed, 514 insertions(+), 886 deletions(-) delete mode 100644 src/routes/(app)/visits/+page.svelte diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index 9820428..fb1c74b 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -17,7 +17,6 @@ Briefcase, Hash, Globe, - Calendar, ChevronDown } from 'lucide-svelte'; import { auth } from '$lib/stores/auth.js'; @@ -252,15 +251,6 @@ Patients -
  • - - - Visits - -
  • 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 @@ - +
    + +
    +
    + + {patientVisits.length} visit(s) found +
    + +
    + {#if patientVisits.length === 0} -
    No visits found for this patient
    + +
    +
    🏥
    +

    No visits yet!

    +

    This patient hasn't had any visits recorded.

    + +
    {:else}
    - +
    - - - - - - + + + + + + + {#each patientVisits as visit} - - - + {@const status = getADTStatus(visit.PatVisitADT?.ADTCode)} + + + + {/each} @@ -1049,3 +1297,244 @@ {/snippet} + + + + {#if visitFormLoading} +
    + +

    Loading visit data...

    +
    + {:else} +
    e.preventDefault()}> + +
    + +
    + 🔒 + + {currentPatientForVisits + ? [currentPatientForVisits.Prefix, currentPatientForVisits.NameFirst, currentPatientForVisits.NameLast].filter(Boolean).join(' ') + : 'Unknown Patient'} + + + ID: {currentPatientForVisits?.PatientID || '-'} + +
    +

    Patient is locked for this visit

    +
    + + +
    +
    + + + {#if visitFormErrors.EpisodeID} + {visitFormErrors.EpisodeID} + {/if} +
    + +
    + + + {#if visitFormErrors.PatientClass} + {visitFormErrors.PatientClass} + {/if} +
    +
    + + +
    + + +
    + + +
    +
    + + +
    + +
    + + +
    +
    + + +
    + + +
    + + +
    +
    + + +
    + +
    + + +
    +
    + + {/if} + + {#snippet footer()} +
    + + +
    + {/snippet} +
    + + + +
    +

    + Are you sure you want to delete visit + #{visitToDelete?.PVID}? +

    + {#if visitToDelete?.EpisodeID} +

    Episode: {visitToDelete.EpisodeID}

    + {/if} +

    ⚠️ This action cannot be undone.

    +
    + {#snippet footer()} + + + {/snippet} +
    diff --git a/src/routes/(app)/visits/+page.svelte b/src/routes/(app)/visits/+page.svelte deleted file mode 100644 index 48b9ae8..0000000 --- a/src/routes/(app)/visits/+page.svelte +++ /dev/null @@ -1,851 +0,0 @@ - - -
    -
    -
    -
    -

    Patient Visits

    -

    Manage patient encounters and ADT workflow

    -
    - -
    - -
    -
    -
    -
    -
    - -
    - - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    - -
    -
    - - { - return { - ...v, - CreateDateFormatted: v.CreateDate ? new Date(v.CreateDate).toLocaleDateString() : '-', - StatusLabel: getVisitStatusLabel(v), - PatientName: v.PatientName || '-', - }; - })} - loading={loading} - striped={true} - /> -
    -
    -
    -
    - - - -
    e.preventDefault()}> - {#if formErrors.general} -
    - {formErrors.general} -
    - {/if} - -
    - - - -
    - - {#if currentTab === 1} -
    -
    - -
    - - -
    - {#if formErrors.InternalPID} - - {/if} -
    - -
    -
    - - -
    -
    - - -
    -
    - -
    - - -
    -
    - {/if} - - {#if currentTab === 2} -
    -
    -
    - - -
    -
    - - -
    -
    -
    - {/if} - - {#if currentTab === 3} -
    -
    - - { - if (!formData.PatVisitADT) formData.PatVisitADT = {}; - formData.PatVisitADT.ADTCode = e.target.value; - }} - options={adtCodeOptions} - placeholder="Select ADT action (optional)..." - /> -
    - - {#if (formData.PatVisitADT?.ADTCode === 'A01' || formData.PatVisitADT?.ADTCode === 'A02')} -
    - - { - if (!formData.PatVisitADT) formData.PatVisitADT = {}; - formData.PatVisitADT.LocationID = e.target.value; - }} - options={locationOptions} - placeholder="Select location..." - required={true} - /> -
    - {/if} - -
    -
    - - { - if (!formData.PatVisitADT) formData.PatVisitADT = {}; - formData.PatVisitADT.AttDoc = e.target.value; - }} - options={contactOptions} - placeholder="Select physician..." - /> -
    -
    - - { - if (!formData.PatVisitADT) formData.PatVisitADT = {}; - formData.PatVisitADT.RefDoc = e.target.value; - }} - options={contactOptions} - placeholder="Select physician..." - /> -
    -
    - - { - if (!formData.PatVisitADT) formData.PatVisitADT = {}; - formData.PatVisitADT.AdmDoc = e.target.value; - }} - options={contactOptions} - placeholder="Select physician..." - /> -
    -
    - - { - if (!formData.PatVisitADT) formData.PatVisitADT = {}; - formData.PatVisitADT.CnsDoc = e.target.value; - }} - options={contactOptions} - placeholder="Select physician..." - /> -
    -
    -
    - {/if} - - - {#snippet footer()} -
    - - -
    - {/snippet} -
    - - - -
    e.preventDefault()}> - {#if formErrors.general} -
    - {formErrors.general} -
    - {/if} - -
    - - -
    - - {#if adtFormData.ADTCode === 'A01' || adtFormData.ADTCode === 'A02'} -
    - - -
    - {/if} - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - - {#snippet footer()} -
    - - -
    - {/snippet} -
    - - - -
    -
    -
    - - -
    -
    - - {#if filteredPatients.length === 0} -
    No patients found
    - {:else} -
    -
    Visit IDCreatedEpisode IDStatusAction
    Visit IDEpisodeClassStatusCreatedActions
    {visit.PVID}{visit.CreateDate ? new Date(visit.CreateDate).toLocaleDateString() : '-'}
    {visit.PVID} {visit.EpisodeID || '-'} - {#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} + + {getPatientClassLabel(visit.PV1?.PatientClass)} + - + + {status.icon} + {status.label} + + + {visit.CreateDate ? new Date(visit.CreateDate).toLocaleDateString() : '-'} + +
    + + +
    - - - - - - - - - - {#each filteredPatients as patient} - {#if patient.visits} - - - - {#each patient.visits as visit} - - - - - - - {/each} - {:else} - - - - - - - {/if} - {/each} - -
    Patient IDNameBirthdateAction
    - {patient.PatientID} - {[patient.Prefix, patient.NameFirst, patient.NameLast].filter(Boolean).join(' ')} -
    {visit.PVID}{visit.StatusLabel}{visit.CreateDateFormatted} - -
    {patient.PatientID} - {[patient.Prefix, patient.NameFirst, patient.NameMiddle, patient.NameLast, patient.Suffix] - .filter(Boolean) - .join(' ')} - {patient.Birthdate ? new Date(patient.Birthdate).toLocaleDateString() : '-'} - -
    -
    - {/if} -
    - - {#snippet footer()} -
    - -
    - {/snippet} -
    - - - -
    -

    - Are you sure you want to delete visit {deleteItem?.PVID}? -

    -

    This action cannot be undone.

    -
    - - {#snippet footer()} -
    - - -
    - {/snippet} -