mirror of
https://github.com/beak-insights/felicity-lims.git
synced 2025-02-23 08:23:00 +08:00
273 lines
No EOL
7.8 KiB
Vue
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> |