felicity-lims/webapp/views/patient/PatientsCompact.vue
2024-11-20 18:16:49 +02:00

273 lines
No EOL
7.8 KiB
Vue

<script setup lang="ts">
import { ref, reactive, computed, defineAsyncComponent, onMounted } from "vue";
import { usePatientStore } from "@/stores/patient";
import { useLocationStore } from "@/stores/location";
import { storeToRefs } from "pinia";
import { IPatient } from "@/models/patient";
import * as shield from "@/guards";
// Constants
const GENDERS = {
MALE: 0,
FEMALE: 1,
MISSING: 2,
TRANS: 3
} as const;
const GENDER_LABELS: Record<number, string> = {
[GENDERS.MALE]: "Male",
[GENDERS.FEMALE]: "Female",
[GENDERS.MISSING]: "Missing",
[GENDERS.TRANS]: "Trans Gender"
};
// Lazy-loaded components
const components = {
LoadingMessage: defineAsyncComponent(() =>
import("@/components/ui/spinners/FelLoadingMessage.vue")
),
PageHeading: defineAsyncComponent(() =>
import("@/components/common/FelPageHeading.vue")
),
Modal: defineAsyncComponent(() =>
import("@/components/ui/FelModal.vue")
),
PatientForm: defineAsyncComponent(() =>
import("@/components/person/PatientForm.vue")
),
PatientInfo: defineAsyncComponent(() =>
import("@/components/person/PatientInfo.vue")
),
Tabs: defineAsyncComponent(() =>
import("@/components/ui/tabs/FelTabs.vue")
)
};
// Store setup
const patientStore = usePatientStore();
const locationStore = useLocationStore();
const { patients, fetchingPatients } = storeToRefs(patientStore);
// State
const showModal = ref(false);
const patientForm = ref({} as IPatient);
const searchParams = reactive({
first: 25,
before: "",
text: "",
sortBy: ["-uid"],
filterAction: false,
});
// Computed
const isPatientSelected = computed(() =>
Object.keys(patientForm.value).length > 0
);
const tabs = computed(() => [
{
id: "samples",
label: "Samples",
component: defineAsyncComponent(() =>
import("@/components/sample/FelAnalyisRequestListing.vue")
),
props: {
target: "patient-samples",
targetUid: patientForm.value.uid
}
},
{
id: "billing",
label: "Billing",
component: defineAsyncComponent(() =>
import("./_id/PatientBill.vue")
),
props: {
patientUid: patientForm.value.uid
}
},
{
id: "logs",
label: "Logs",
component: defineAsyncComponent(() =>
import("@/components/audit/FelAuditLog.vue")
),
props: {
targetType: "patient",
targetUid: patientForm.value.uid
}
}
]);
// Methods
const getPatientFullName = (patient: IPatient): string =>
`${patient.firstName} ${patient.lastName}`;
const getGender = (genderCode: number): string =>
GENDER_LABELS[genderCode] || 'Unknown';
const selectPatient = (patient: IPatient): void => {
patientForm.value = patient;
patientStore.setPatient(patient);
};
const resetPatient = (): void => {
patientForm.value = {} as IPatient;
patientStore.resetPatient();
};
const handleSearch = (event: Event): void => {
const target = event.target as HTMLInputElement;
searchParams.first = 100;
searchParams.before = "";
searchParams.text = target.value;
searchParams.filterAction = true;
patientStore.fetchPatients(searchParams);
};
const handleQuickRegistration = async (): Promise<void> => {
resetPatient();
showModal.value = true;
};
const handlePatientUpdate = (patient: IPatient): void => {
selectPatient(patient);
showModal.value = false;
};
// Lifecycle
onMounted(() => {
locationStore.fetchCountries();
patientStore.fetchPatients(searchParams);
});
</script>
<template>
<div class="p-4">
<components.PageHeading title="Patients Quick View" />
<!-- Header Actions -->
<div class="flex justify-between mb-4">
<div class="flex items-center gap-4">
<router-link
v-if="shield.hasRights(shield.actions.CREATE, shield.objects.PATIENT)"
to="/patients/search"
class="px-4 py-2 text-sm border border-sky-800 text-dark-800 rounded-sm hover:bg-sky-800 hover:text-gray-100 transition-colors duration-150"
>
Add Patient
</router-link>
<input
type="text"
placeholder="Search patients..."
class="w-64 px-4 py-2 text-sm text-gray-800 placeholder-gray-400 border border-gray-400 rounded-sm focus:border-sky-800 focus:outline-none focus:ring-1 focus:ring-sky-800"
@keyup="handleSearch"
@focus="resetPatient"
/>
</div>
<button
v-if="shield.hasRights(shield.actions.CREATE, shield.objects.PATIENT)"
class="px-4 py-2 text-sm border border-sky-800 text-dark-700 rounded-sm hover:bg-sky-800 hover:text-gray-100 transition-colors duration-150"
@click="handleQuickRegistration"
>
Quick Registration
</button>
</div>
<hr class="my-4" />
<!-- Main Content -->
<div class="grid grid-cols-12 gap-4">
<!-- Patient List -->
<section
v-motion
:initial="{ opacity: 0, y: 100 }"
:enter="{ opacity: 1, y: 0 }"
:delay="400"
class="col-span-3 h-screen overflow-y-auto scrollbar scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100"
>
<components.LoadingMessage
v-if="fetchingPatients"
message="Fetching patients..."
/>
<template v-else>
<div
v-for="patient in patients"
:key="patient.patientId"
@click="selectPatient(patient)"
class="bg-white w-full flex items-center mb-1 rounded-sm shadow border cursor-pointer transition-colors duration-150 hover:border-sky-800 hover:bg-sky-50"
:class="{ 'border-sky-800 bg-emerald-200': patient.uid === patientForm?.uid }"
>
<div class="flex-grow p-3">
<div class="flex justify-between font-semibold">
<span>{{ getPatientFullName(patient) }}</span>
<span class="text-sm text-gray-500">
{{ patient.age }} yrs, {{ getGender(patient.gender) }}
</span>
</div>
<div class="text-sm text-gray-500 flex justify-between mt-1">
<span>{{ patient.patientId }}</span>
<span>{{ patient.clientPatientId }}</span>
</div>
<div class="text-sm text-gray-500 flex justify-between mt-1">
<span>{{ patient?.client?.district?.province?.name }}</span>
<span>{{ patient?.client?.name }}</span>
</div>
</div>
</div>
</template>
</section>
<!-- Patient Details -->
<section
v-if="isPatientSelected"
v-motion
:initial="{ opacity: 0, y: -100 }"
:enter="{ opacity: 1, y: 0 }"
:delay="400"
class="col-span-9"
>
<components.PatientInfo @editPatient="showModal = true" />
<components.Tabs :tabs="tabs" initial-tab="samples" />
</section>
</div>
<!-- Edit Modal -->
<components.Modal
v-if="showModal"
@close="showModal = false"
content-width="w-3/6"
>
<template #header>
<h3>Patient Form</h3>
</template>
<template #body>
<components.PatientForm
:patient="patientForm"
:navigate="false"
@close="handlePatientUpdate"
/>
</template>
</components.Modal>
</div>
</template>
<style lang="css" scoped>
.scrollbar::-webkit-scrollbar {
width: 0.5rem;
}
.scrollbar::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
.scrollbar::-webkit-scrollbar-thumb {
background-color: #d1d1d1;
border-radius: 9999px;
}
</style>