felicity-lims/webapp/components/sample/FelSampleListing.vue
2024-07-31 19:38:20 +02:00

703 lines
20 KiB
Vue

<script setup lang="ts">
import { h, ref, reactive, computed, defineAsyncComponent } from "vue";
import { RouterLink } from "vue-router";
import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
import { IAnalysisProfile, IAnalysisService, ISample } from "@/models/analysis";
import { ifZeroEmpty, parseDate } from "@/utils/helpers";
import { useSampleStore, useAnalysisStore } from "@/stores";
import { useSampleComposable } from "@/composables";
const DataTable = defineAsyncComponent(
() => import("@/components/ui/datatable/FelDataTable.vue")
)
import * as shield from "@/guards";
const sampleStore = useSampleStore();
const analysisStore = useAnalysisStore();
const { samplePageInfo, fetchingSamples } = storeToRefs(sampleStore);
let route = useRoute();
let router = useRouter();
const state = reactive({
barcodes: false,
can_cancel: false,
can_receive: false,
can_reinstate: false,
can_reject: false,
can_copy_to: false,
can_download: false,
can_print: false,
can_publish: false,
can_store: false,
can_recover: false,
});
// samples
const samples = computed<ISample[]>(() => sampleStore.getSamples);
const filterOptions = ref([
{ name: "All", value: "" },
{ name: "Expected", value: "expected" },
{ name: "Received", value: "received" },
{ name: "Awaiting", value: "awaiting" },
{ name: "Approved", value: "approved" },
{ name: "Published", value: "published" },
{ name: "Invalidated", value: "invalidated" },
{ name: "Cancelled", value: "cancelled" },
{ name: "Rejected", value: "rejected" },
{ name: "Stored", value: "stored" },
{ name: "Referred", value: "referred" },
{ name: "Paired", value: "paired" },
]);
const tableColumns = ref([
{
name: "UID",
value: "uid",
sortable: true,
sortBy: "asc",
defaultSort: true,
showInToggler: false,
hidden: true,
},
{
name: "",
value: "",
sortable: false,
showInToggler: false,
hidden: false,
customRender: function (sample, _) {
return h("div", [
sample.priority! > 1
? h(
"span",
{ class: [{ "text-orange-600": sample.priority! > 1 }] },
h("i", { class: "fa fa-star" })
)
: "",
sample.status === "stored" ? h("span", h("i", { class: "fa-briefcase" })) : "",
]);
},
},
{
name: "Sampe ID",
value: "sampleId",
sortable: true,
sortBy: "asc",
hidden: false,
customRender: function (sample, _) {
return h(RouterLink, {
to: {
name: "sample-detail",
params: {
patientUid: sample?.analysisRequest?.patient?.uid,
sampleUid: sample?.uid,
},
},
innerHTML: sample?.sampleId,
});
},
},
{
name: "Sample Type",
value: "sampleType.name",
sortable: false,
sortBy: "asc",
hidden: true,
},
{
name: "Test(s)",
value: "",
sortable: false,
sortBy: "asc",
hidden: false,
customRender: function (sample, _) {
return h(
"span",
{
innerHTML: profileAnalysesText(sample["profiles"], sample["analyses"]),
},
[]
);
},
},
{
name: "Patient",
value: "",
sortable: false,
sortBy: "asc",
hidden: false,
customRender: function (sample, _) {
const firstName = "analysisRequest.patient.firstName"
.split(".")
.reduce((acc, val) => acc?.[val], sample);
const lasstName = "analysisRequest.patient.lastName"
.split(".")
.reduce((acc, val) => acc?.[val], sample);
return h(
"span",
{
innerHTML: `${firstName} ${lasstName}`,
},
[]
);
},
},
{
name: "Gender",
value: "analysisRequest.patient.gender",
sortable: false,
sortBy: "asc",
hidden: true,
},
{
name: "Age",
value: "analysisRequest.patient.age",
sortable: false,
sortBy: "asc",
hidden: true,
},
{
name: "Client Patient ID",
value: "analysisRequest.patient.clientPatientId",
sortable: false,
sortBy: "asc",
hidden: false,
},
{
name: "Client",
value: "analysisRequest.client.name",
sortable: false,
sortBy: "asc",
hidden: false,
},
{
name: "Client Code",
value: "analysisRequest.client.code",
sortable: false,
sortBy: "asc",
hidden: true,
},
{
name: "Province",
value: "analysisRequest.client.district.province.name",
sortable: false,
sortBy: "asc",
hidden: true,
},
{
name: "District",
value: "analysisRequest.client.district.name",
sortable: false,
sortBy: "asc",
hidden: true,
},
{
name: "Client Request Id",
value: "analysisRequest.clientRequestId",
sortable: false,
sortBy: "asc",
hidden: false,
},
{
name: "Date Collected",
value: "dateCollected",
sortable: false,
sortBy: "asc",
hidden: true,
customRender: function (sample, column) {
const value = column.value.split(".").reduce((acc, val) => acc?.[val], sample);
return h(
"span",
{
innerHTML: parseDate(value),
},
[]
);
},
},
{
name: "Date Created",
value: "createdAt",
sortable: false,
sortBy: "asc",
hidden: true,
customRender: function (sample, column) {
const value = column.value.split(".").reduce((acc, val) => acc?.[val], sample);
return h(
"span",
{
innerHTML: parseDate(value),
},
[]
);
},
},
{
name: "Creator",
value: "createdBy.firstName",
sortable: false,
sortBy: "asc",
hidden: false,
},
{
name: "Date Received",
value: "dateReceived",
sortable: false,
sortBy: "asc",
hidden: true,
customRender: function (sample, column) {
const value = column.value.split(".").reduce((acc, val) => acc?.[val], sample);
return h(
"span",
{
innerHTML: parseDate(value),
},
[]
);
},
},
{
name: "Date Submitted",
value: "dateSubmitted",
sortable: false,
sortBy: "asc",
hidden: true,
customRender: function (sample, column) {
const value = column.value.split(".").reduce((acc, val) => acc?.[val], sample);
return h(
"span",
{
innerHTML: parseDate(value),
},
[]
);
},
},
{
name: "Date Verified",
value: "dateVerified",
sortable: false,
sortBy: "asc",
hidden: true,
customRender: function (sample, column) {
const value = column.value.split(".").reduce((acc, val) => acc?.[val], sample);
return h(
"span",
{
innerHTML: parseDate(value),
},
[]
);
},
},
{
name: "Date Published",
value: "datePublished",
sortable: false,
sortBy: "asc",
hidden: true,
customRender: function (sample, column) {
const value = column.value.split(".").reduce((acc, val) => acc?.[val], sample);
return h(
"span",
{
innerHTML: parseDate(value),
},
[]
);
},
},
{
name: "Date Printed",
value: "datePrinted",
sortable: false,
sortBy: "asc",
hidden: true,
customRender: function (sample, column) {
const value = column.value.split(".").reduce((acc, val) => acc?.[val], sample);
return h(
"span",
{
innerHTML: parseDate(value),
},
[]
);
},
},
{
name: "Printed",
value: "printed",
sortable: false,
sortBy: "asc",
hidden: true,
},
{
name: "Status",
value: "status",
sortable: false,
sortBy: "asc",
hidden: false,
customRender: function (sample, column) {
const value = column.value.split(".").reduce((acc, val) => acc?.[val], sample);
return h(
"button",
{
type: "button",
class: "bg-sky-800 text-white py-1 px-2 rounded-sm leading-none",
innerHTML: value,
},
[]
);
},
},
]);
if (route?.query?.clientUid) {
sampleStore.resetSamples();
}
sampleStore.fetchSampleTypes();
let analysesParams = reactive({
first: undefined,
after: "",
text: "",
sortBy: ["name"],
});
analysisStore.fetchAnalysesServices(analysesParams);
analysisStore.fetchAnalysesProfiles();
function profileAnalysesText(
profiles: IAnalysisProfile[],
analyses: IAnalysisService[]
): string {
let names: string[] = [];
profiles.forEach((p) => names.push(p.name!));
analyses.forEach((a) => names.push(a.name!));
return names.join(", ");
}
let sampleParams = reactive({
first: 50,
before: "",
status: "received",
text: "",
sortBy: ["-uid"],
clientUid: ifZeroEmpty(route?.query?.clientUid),
filterAction: false,
});
sampleStore.fetchSamples(sampleParams);
function showMoreSamples(opts: any): void {
sampleParams.first = opts.fetchCount;
sampleParams.before = samplePageInfo?.value?.endCursor ?? "";
sampleParams.text = opts.filterText;
sampleParams.status = opts.filterStatus;
sampleParams.filterAction = false;
sampleStore.fetchSamples(sampleParams);
}
function filterSamples(opts: any): void {
sampleParams.first = 50;
sampleParams.before = "";
sampleParams.text = opts.filterText;
sampleParams.status = opts.filterStatus;
sampleParams.filterAction = true;
sampleStore.fetchSamples(sampleParams);
}
// sample tooltip
const sampleToolTip = (sample: ISample) => {
const sc = sample?.storageContainer;
const ss = sc?.storageSection;
const sl = ss?.storageLocation;
const sr = sl?.storeRoom;
return `${sr?.name} > ${sl?.name} > ${ss?.name} > ${sc?.name} > ${sample?.storageSlot}`;
};
// user actions perms
const allChecked = ref(false);
function toggleCheckAll(opts: any): void {
samples.value?.forEach((sample: ISample) => (sample.checked = opts.checked));
allChecked.value = opts.checked;
checkUserActionPermissios();
}
function toggleCheck(sample: ISample): void {
const index = samples.value.findIndex((s) => s.uid === sample.uid);
samples.value[index].checked = sample.checked;
if (areAllChecked()) {
allChecked.value = true;
} else {
allChecked.value = false;
}
checkUserActionPermissios();
}
function unCheck(sample: ISample): void {
const index = samples.value.findIndex((s) => s.uid === sample.uid);
samples.value[index].checked = false;
allChecked.value = false;
checkUserActionPermissios();
}
async function unCheckAll() {
samples.value?.forEach((sample: ISample) => (sample.checked = false));
allChecked.value = false;
checkUserActionPermissios();
}
function areAllChecked(): Boolean {
return samples.value?.every((sample: ISample) => sample.checked === true);
}
function getSamplesChecked(): ISample[] {
let box: ISample[] = [];
samples.value?.forEach((sample: ISample) => {
if (sample.checked) box.push(sample);
});
return box;
}
function checkUserActionPermissios(): void {
// reset
state.can_cancel = false;
state.can_receive = false;
state.can_reinstate = false;
state.can_download = false;
state.can_publish = false;
state.can_print = false;
state.can_reject = false;
state.can_store = false;
state.can_recover = false;
state.can_copy_to = false;
state.barcodes = false;
const checked: ISample[] = getSamplesChecked();
if (checked.length === 0) {
return;
} else {
state.barcodes = true;
};
// can_receive
if (checked.every((sample: ISample) => sample.status === "expected")) {
state.can_receive = true;
}
// can_cancel
if (
checked.every((sample: ISample) => ["received", "expected"].includes(sample.status!))
) {
state.can_cancel = true;
state.can_reject = true;
}
// can_store;
if (checked.every((sample: ISample) => ["received"].includes(sample.status!))) {
state.can_store = true;
state.can_copy_to = true;
}
if (checked.every((sample: ISample) => ["stored"].includes(sample.status!))) {
state.can_recover = true;
}
// can_reinstate
if (checked.every((sample: ISample) => sample.status === "cancelled")) {
state.can_reinstate = true;
}
// can_download
if (
checked.every((sample: ISample) => ["approved", "published"].includes(sample.status!))
) {
state.can_copy_to = true;
}
// can_print
if (checked.every((sample: ISample) => sample.status === "approved")) {
state.can_publish = true;
}
// can_print
if (checked.every((sample: ISample) => sample.status === "published")) {
state.can_print = true;
state.can_download = true;
}
}
function getSampleUids(): string[] {
const items: ISample[] = getSamplesChecked();
let ready: string[] = [];
items?.forEach((item) => ready.push(item.uid!));
return ready;
}
function getSampleIds(): string[] {
const items: ISample[] = getSamplesChecked();
let ready: string[] = [];
items?.forEach((item) => ready.push(item.sampleId!));
return ready;
}
//
const {
cancelSamples,
reInstateSamples,
receiveSamples,
printSamples,
downloadSamplesImpress,
publishSamples,
recoverSamples,
cloneSamples
} = useSampleComposable();
const countNone = computed(
() => sampleStore.getSamples?.length + " of " + sampleStore.getSampleCount + " samples"
);
const cloneSamples_ = async () =>
cloneSamples(getSampleUids()).finally(() => unCheckAll());
const cancelSamples_ = async () =>
cancelSamples(getSampleUids()).finally(() => unCheckAll());
const reInstateSamples_ = async () =>
reInstateSamples(getSampleUids()).finally(() => unCheckAll());
const receiveSamples_ = async () =>
receiveSamples(getSampleUids()).finally(() => unCheckAll());
const publishReports_ = async () => {
const samples = getSampleUids().map((uid) => ({ uid, action: "publish" }));
await publishSamples(samples).finally(() => unCheckAll());
};
const printReports_ = async () =>
await printSamples(getSampleUids()).finally(() => unCheckAll());
const impressDownload_ = async () =>
await downloadSamplesImpress(getSampleIds()).finally(() => unCheckAll());
const prepareRejections = async () => {
const selection = getSamplesChecked();
router.push({ name: "reject-samples", state: { samples: JSON.stringify(selection) } });
};
const prepareStorages = async () => {
const selection = getSamplesChecked();
router.push({ name: "store-samples", state: { samples: JSON.stringify(selection) } });
};
const recoverSamples_ = async () =>
recoverSamples(getSampleUids()).finally(() => unCheckAll());
const printBarCodes = async () => router.push({
name: "print-barcodes",
state: { sampleUids: JSON.stringify(getSampleUids()) }}
)
</script>
<template>
<div class="mb-4 flex justify-start">
<router-link v-show="shield.hasRights(shield.actions.CREATE, shield.objects.SAMPLE)" to="/patients/search"
class="px-2 py-1 border-sky-800 border text-sky-800 rounded-sm transition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none">Add
Laboratory Request</router-link>
</div>
<hr />
<DataTable :columns="tableColumns" :data="samples" :toggleColumns="true" :loading="fetchingSamples" :paginable="true"
:pageMeta="{
fetchCount: sampleParams.first,
hasNextPage: samplePageInfo?.hasNextPage,
countNone,
}" :searchable="true" :filterable="true" :filterMeta="{
defaultFilter: sampleParams.status,
filters: filterOptions,
}" :selectable="true" :allChecked="allChecked" @onSearch="filterSamples" @onPaginate="showMoreSamples"
@onCheck="toggleCheck" @onCheckAll="toggleCheckAll">
<template v-slot:footer>
<div>
<button v-show="
shield.hasRights(shield.actions.CANCEL, shield.objects.SAMPLE) &&
state.can_cancel
" @click.prevent="cancelSamples_()"
class="px-2 py-1 mr-2 border-sky-800 border text-sky-800 rounded-sm transition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none">
Cancel
</button>
<button v-show="
shield.hasRights(shield.actions.CANCEL, shield.objects.SAMPLE) &&
state.can_reinstate
" @click.prevent="reInstateSamples_()"
class="px-2 py-1 mr-2 border-sky-800 border text-sky-800 rounded-sm transition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none">
ReInstate
</button>
<button v-show="
shield.hasRights(shield.actions.CANCEL, shield.objects.SAMPLE) &&
state.can_receive
" @click.prevent="receiveSamples_()"
class="px-2 py-1 mr-2 border-sky-800 border text-sky-800 rounded-sm transition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none">
Reveive
</button>
<button v-show="
shield.hasRights(shield.actions.CANCEL, shield.objects.SAMPLE) &&
state.can_store
" @click.prevent="prepareStorages()"
class="px-2 py-1 mr-2 border-sky-800 border text-sky-800 rounded-sm transition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none">
Store
</button>
<button v-show="
shield.hasRights(shield.actions.CANCEL, shield.objects.SAMPLE) &&
state.can_recover
" @click.prevent="recoverSamples_()"
class="px-2 py-1 mr-2 border-sky-800 border text-sky-800 rounded-sm transition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none">
Recover
</button>
<button v-show="
shield.hasRights(shield.actions.CANCEL, shield.objects.SAMPLE) &&
state.can_reject
" @click.prevent="prepareRejections()"
class="px-2 py-1 mr-2 border-sky-800 border text-sky-800 rounded-sm transition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none">
Reject
</button>
<button v-show="
shield.hasRights(shield.actions.CANCEL, shield.objects.SAMPLE) &&
state.can_copy_to
" @click.prevent="cloneSamples_()"
class="px-2 py-1 mr-2 border-sky-800 border text-sky-800 rounded-sm transition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none">
Copy to New
</button>
<button v-show="
shield.hasRights(shield.actions.CANCEL, shield.objects.SAMPLE) &&
state.can_download
" @click.prevent="impressDownload_()"
class="px-2 py-1 mr-2 border-sky-800 border text-sky-800 rounded-sm transition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none">
Download
</button>
<button v-show="
shield.hasRights(shield.actions.CANCEL, shield.objects.SAMPLE) &&
state.can_publish
" @click.prevent="publishReports_()"
class="px-2 py-1 mr-2 border-sky-800 border text-sky-800 rounded-sm transition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none">
Publish
</button>
<button v-show="
shield.hasRights(shield.actions.CANCEL, shield.objects.SAMPLE) &&
state.can_print
" @click.prevent="printReports_()"
class="px-2 py-1 mr-2 border-sky-800 border text-sky-800 rounded-sm transition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none">
Print
</button>
<button
v-show="state.barcodes"
@click.prevent="printBarCodes"
class="px-2 py-1 mr-2 border-sky-800 border text-sky-800 rounded-sm transition duration-300 hover:bg-sky-800 hover:text-white focus:outline-none">
Print Barcodes
</button>
</div>
</template>
</DataTable>
</template>