felicity-lims/webapp/views/sample/_id/Results.vue

504 lines
19 KiB
Vue
Raw Normal View History

2023-11-10 14:05:15 +08:00
<script setup lang="ts">
import { onMounted, watch, reactive, computed, defineAsyncComponent, ref } from "vue";
2023-11-10 14:05:15 +08:00
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
import { useSampleStore } from "@/stores/sample";
import useAnalysisComposable from "@/composables/analysis";
2023-11-10 14:05:15 +08:00
import {
IAnalysisProfile,
IAnalysisResult,
IAnalysisService,
} from "@/models/analysis";
2024-11-24 00:30:27 +08:00
import { isNullOrWs, parseDate } from "@/utils/helpers";
2023-11-10 14:05:15 +08:00
import * as shield from "@/guards";
const FelButton = defineAsyncComponent(
() => import("@/components/ui/buttons/FelButton.vue")
)
2023-11-10 14:05:15 +08:00
const LoadingMessage = defineAsyncComponent(
() => import("@/components/ui/spinners/FelLoadingMessage.vue")
)
const FelDrawer = defineAsyncComponent(
() => import("@/components/ui/FelDrawer.vue")
)
const AnalysisSneak = defineAsyncComponent(
() => import("@/components/analysis/AnalysisSneak.vue")
)
const ResultDetail = defineAsyncComponent(
() => import("@/components/result/ResultDetail.vue")
2023-11-10 14:05:15 +08:00
)
const route = useRoute();
const sampleStore = useSampleStore();
const { sample, analysisResults, fetchingResults } = storeToRefs(sampleStore);
const state = reactive({
can_submit: false,
can_cancel: false,
can_retract: false,
can_approve: false,
can_retest: false,
can_reinstate: false,
allChecked: false,
});
2024-01-24 01:13:37 +08:00
onMounted(() => {
sampleStore.fetchAnalysisResultsForSample(route.params.sampleUid)
});
2023-11-10 14:05:15 +08:00
watch(
() => route.params.sampleUid,
(sampleUid, prev) => {
sampleStore.resetSample();
sampleStore.fetchAnalysisResultsForSample(route.params.sampleUid);
}
);
function getResultsChecked(): any {
let results: IAnalysisResult[] = [];
analysisResults?.value?.forEach((result) => {
if (result.checked) results.push(result);
});
return results;
}
function prepareResults(): IAnalysisResult[] {
let results = getResultsChecked();
let ready: any[] = [];
results?.forEach((result: IAnalysisResult) =>
2024-02-16 23:48:19 +08:00
ready.push({ uid: result.uid, result: result.result, methodUid: result.methodUid, laboratoryInstrumentUid: result.laboratoryInstrumentUid })
2023-11-10 14:05:15 +08:00
);
return ready;
}
function getResultsUids(): string[] {
const results = getResultsChecked();
let ready: string[] = [];
results?.forEach((result: IAnalysisResult) => ready.push(result.uid!));
return ready;
}
// Analysis CheckMark Management
function checkCheck(result: IAnalysisResult): void {
if (areAllChecked()) {
state.allChecked = true;
} else {
state.allChecked = false;
}
resetAnalysesPermissions();
}
function check(result: IAnalysisResult): void {
if(isDisabledRowCheckBox(result)) return;
// if(!result.editable) return;
result.checked = true;
resetAnalysesPermissions();
}
function unCheck(result: IAnalysisResult): void {
result.checked = false;
resetAnalysesPermissions();
}
async function toggleCheckAll() {
await analysisResults?.value?.forEach((result) =>
state.allChecked ? check(result) : unCheck(result)
);
resetAnalysesPermissions();
}
async function unCheckAll() {
await analysisResults?.value?.forEach((result) => unCheck(result));
resetAnalysesPermissions();
}
function areAllChecked(): Boolean {
return analysisResults?.value?.every((item: IAnalysisResult) => item.checked === true);
}
function isDisabledRowCheckBox(result: any): boolean {
switch (result?.status) {
case "retracted":
return true;
case "approved":
return true;
case "cancelled":
if (sample?.value?.status !== "received") return true;
return false;
default:
return false;
}
}
// Analysis Edit Management
function editResult(result: any): void {
result.editable = true;
}
function isEditable(result: IAnalysisResult): Boolean {
if (!["received", "paired"].includes(sample?.value?.status ?? "")) {
return false;
}
if (result.status !== "pending") {
return false;
}
if (result?.editable || isNullOrWs(result?.result)) {
editResult(result);
return true;
}
return false;
}
//
function getResultRowColor(result: any): string {
switch (result?.status) {
case "retracted":
return "bg-gray-300";
case "aproved":
if (result?.reportable === false) {
return "bg-orange-600";
} else {
return "";
}
default:
return "";
}
}
//
function resetAnalysesPermissions(): void {
// reset
state.can_cancel = false;
state.can_submit = false;
state.can_retract = false;
state.can_approve = false;
state.can_retest = false;
state.can_reinstate = false;
const checked = getResultsChecked();
if (checked.length === 0) return;
// can reinstate
if (checked.every((result: IAnalysisResult) => result.status === "cancelled")) {
state.can_reinstate = true;
}
// can cancel
if (checked.every((result: IAnalysisResult) => result.status === "pending")) {
state.can_cancel = true;
}
// can submit
if (
checked.every(
(result: IAnalysisResult) => ["pending"].includes(result.status ?? "") && !isNullOrWs(result.result)
)
) {
state.can_submit = true;
}
// can verify/retract/retest
if (checked.every((result: IAnalysisResult) => result.status === "resulted")) {
state.can_retract = true;
state.can_approve = true;
state.can_retest = true;
}
}
// _updateSample if state has changed
const _updateSample = async () => {
const sample = computed(() => sampleStore.getSample);
if (sample.value) {
sampleStore.fetchSampleStatus(sample?.value?.uid);
}
};
const profileAnalysesText = (
profiles: IAnalysisProfile[],
analyses: IAnalysisService[]
) => {
let names: string[] = [];
profiles?.forEach((p) => names.push(p.name!));
analyses?.forEach((a) => names.push(a.name!));
return names.join(", ");
};
// viewAnalysisInfo
const viewInfo = ref(false)
const viewResultInfo = ref<IAnalysisResult | undefined>(undefined)
const viewAnalysisInfo = (result: IAnalysisResult,) => {
viewInfo.value = true
viewResultInfo.value = result;
}
2023-11-10 14:05:15 +08:00
// Sample Actions
let {
submitResults: submitter_,
cancelResults: canceller_,
reInstateResults: reInstater_,
approveResults: approver_,
retractResults: retracter_,
retestResults: retester_,
} = useAnalysisComposable();
const submitResults = () =>
submitter_(prepareResults(), "sample", sample?.value?.uid!)
.then(() => _updateSample())
.finally(() => unCheckAll());
const cancelResults = () =>
canceller_(getResultsUids())
.then(() => _updateSample())
.finally(() => unCheckAll());
const reInstateResults = () =>
reInstater_(getResultsUids())
.then(() => _updateSample())
.finally(() => unCheckAll());
const approveResults = () =>
approver_(getResultsUids(), "sample", sample?.value?.uid!)
.then(() => _updateSample())
.finally(() => unCheckAll());
const retractResults = () =>
retracter_(getResultsUids())
.then(() => _updateSample())
.finally(() => unCheckAll());
const retestResults = () =>
retester_(getResultsUids())
.then(() => _updateSample())
.finally(() => unCheckAll());
</script>
<template>
<hr class="mt-4 mb-2" />
<h3 class="font-bold">Analyses/Results</h3>
<hr class="mb-4 mt-2" />
<div class="overflow-x-auto">
<div class="align-middle inline-block min-w-full shadow overflow-hidden bg-white shadow-dashboard px-2 pt-1 rounded-bl-lg rounded-br-lg">
2023-11-10 14:05:15 +08:00
<div v-if="fetchingResults" class="py-4 text-center">
<LoadingMessage message="Fetching analytes ..." />
</div>
<table class="min-w-full" v-else>
<thead>
<tr>
<th class="px-1 py-1 border-b-2 border-gray-300 text-left leading-4 text-gray-800 tracking-wider">
<input type="checkbox" class="" @change="toggleCheckAll" v-model="state.allChecked" />
</th>
<th class="px-1 py-1 border-b-2 border-gray-300 text-left leading-4 text-gray-800 tracking-wider"></th>
<th class="px-1 py-1 border-b-2 border-gray-300 text-left leading-4 text-gray-800 tracking-wider">
Analysis
</th>
<th class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider">
2024-01-24 01:13:37 +08:00
Instrument
2023-11-10 14:05:15 +08:00
</th>
<th class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider">
2024-01-24 01:13:37 +08:00
Method
2023-11-10 14:05:15 +08:00
</th>
<th class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider">
Analyst
</th>
2024-01-24 01:13:37 +08:00
<th class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider">
Reviewer(s)
</th>
2023-11-10 14:05:15 +08:00
<th class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider">
Interim
</th>
<th class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider">
Result
</th>
<th class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider">
Retest
</th>
2024-01-24 01:13:37 +08:00
<th class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider">
Due Date
</th>
2023-11-10 14:05:15 +08:00
<th class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider">
Submitted
</th>
<th class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider">
2024-01-24 01:13:37 +08:00
Approved
2023-11-10 14:05:15 +08:00
</th>
<th class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider">
Status
</th>
<th class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider">
Reportable
</th>
<th class="px-1 py-1 border-b-2 border-gray-300"></th>
</tr>
</thead>
<tbody class="bg-white">
<tr v-for="result in analysisResults" :key="result.uid" :class="[getResultRowColor(result)]"
v-motion-slide-right>
<td>
<input type="checkbox" class="border-red-500" v-model="result.checked" @change="checkCheck(result)"
:disabled="isDisabledRowCheckBox(result)" /><font-awesome-icon v-if="result.status === 'pending'"
icon="fa-question" class="ml-1 text-xs"></font-awesome-icon>
<font-awesome-icon v-if="result.status === 'resulted'" icon="fa-question"
class="ml-1 text-xs text-orange"></font-awesome-icon>
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500"></td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
<div class="text-sm leading-5 text-sky-800 font-semibold">
<span class="mr-1 hover:cursor-pointer" @click="viewAnalysisInfo(result)"><font-awesome-icon icon="fa-info-circle"></font-awesome-icon></span>
2023-11-10 14:05:15 +08:00
{{ result.analysis?.name }}
</div>
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
2024-01-24 01:13:37 +08:00
<div v-if="!isEditable(result)" class="text-sm leading-5 text-sky-800">
2024-02-16 23:48:19 +08:00
{{ result.laboratoryInstrument?.labName || "---" }}
2024-01-24 01:13:37 +08:00
</div>
<label v-else class="block col-span-2 mb-2">
2024-02-16 23:48:19 +08:00
<select class="form-input mt-1 block w-full" v-model="result.laboratoryInstrumentUid" @change="check(result)">
2024-01-24 01:13:37 +08:00
<option value=""></option>
2024-11-20 04:56:17 +08:00
<template v-for="instrument in result.analysis?.instruments" :key="instrument.uid">
<option
v-for="lab_instrument in instrument.laboratoryInstruments"
:key="lab_instrument.uid"
:value="lab_instrument.uid"
>
{{ lab_instrument.labName }} ({{ instrument?.name }})
</option>
</template>
2024-01-24 01:13:37 +08:00
</select>
</label>
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
<div v-if="!isEditable(result)" class="text-sm leading-5 text-sky-800">
{{ result.method?.name || "---" }}
2023-11-10 14:05:15 +08:00
</div>
2024-01-24 01:13:37 +08:00
<label v-else class="block col-span-2 mb-2">
<select class="form-input mt-1 block w-full" v-model="result.methodUid" @change="check(result)">
<option value=""></option>
2024-11-20 04:56:17 +08:00
<option v-for="method in result.analysis?.methods" :key="method.uid"
2024-01-24 01:13:37 +08:00
:value="method.uid">
{{ method.name }}
</option>
</select>
</label>
2023-11-10 14:05:15 +08:00
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
<div class="text-sm leading-5 text-sky-800">
2024-01-24 01:13:37 +08:00
{{ `${result.submittedBy?.firstName ?? '--'} ${result.submittedBy?.lastName ?? '--'}` }}
2023-11-10 14:05:15 +08:00
</div>
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
<div class="text-sm leading-5 text-sky-800">
2024-01-24 01:13:37 +08:00
<span v-for="reviewer in result.verifiedBy" :key="reviewer.firstName" class="ml-1">
{{ `${reviewer?.firstName ?? '--'} ${reviewer?.lastName ?? '--'},` }}
</span>
2023-11-10 14:05:15 +08:00
</div>
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
<div v-if="!isEditable(result) || result?.analysis?.interims?.length === 0"
class="text-sm leading-5 text-sky-800">
---
</div>
<label v-else class="block col-span-2 mb-2">
<select class="form-input mt-1 block w-full" v-model="result.result" @change="check(result)">
<option value=""></option>
<option v-for="interim in result?.analysis?.interims" :key="interim.key"
2023-11-10 14:05:15 +08:00
:value="interim.value">
{{ interim.value }}
</option>
</select>
</label>
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
<div v-if="!isEditable(result)" class="text-sm leading-5 text-sky-800">
{{ result?.result }}
</div>
<label v-else-if="result?.analysis?.resultOptions?.length === 0" class="block">
<input class="form-input mt-1 block w-full" v-model="result.result" @keyup="check(result)" />
</label>
<label v-else class="block col-span-2 mb-2">
<select class="form-input mt-1 block w-full" v-model="result.result" @change="check(result)">
<option value=""></option>
<option v-for="option in result?.analysis?.resultOptions" :key="option.optionKey"
2023-11-10 14:05:15 +08:00
:value="option.value">
{{ option.value }}
</option>
</select>
</label>
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
<div class="text-sm leading-5 text-sky-800">
<span v-if="result?.retest" class="text-sky-800">
2024-01-24 01:13:37 +08:00
<font-awesome-icon icon="fa-check-circle"></font-awesome-icon>
2023-11-10 14:05:15 +08:00
</span>
<span v-else class="text-orange-600">
2024-01-24 01:13:37 +08:00
<font-awesome-icon icon="fa-times-circle"></font-awesome-icon>
2023-11-10 14:05:15 +08:00
</span>
</div>
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
2024-01-24 01:13:37 +08:00
<div class="text-sm leading-5 text-sky-800">{{ parseDate(result?.dueDate) }}</div>
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
<div class="text-sm leading-5 text-sky-800">{{ parseDate(result?.dateSubmitted) }}</div>
2023-11-10 14:05:15 +08:00
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
2024-01-24 01:13:37 +08:00
<div class="text-sm leading-5 text-sky-800">{{ parseDate(result?.dateVerified) }}</div>
2023-11-10 14:05:15 +08:00
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
<button type="button" class="bg-sky-800 text-white px-2 py-1 rounded-sm leading-none">
{{ result.status }}
</button>
</td>
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
<div class="text-sm leading-5 text-sky-800">
<span v-if="result?.reportable" class="text-emerald-600">
2024-01-24 01:13:37 +08:00
<font-awesome-icon icon="fa-thumbs-up" aria-hidden="true"></font-awesome-icon>
2023-11-10 14:05:15 +08:00
</span>
<span v-else class="text-orange-600">
2024-01-24 01:13:37 +08:00
<font-awesome-icon icon="fa-thumbs-down" aria-hidden="true"></font-awesome-icon>
2023-11-10 14:05:15 +08:00
</span>
</div>
</td>
<td class="px-1 py-1 whitespace-no-wrap text-right border-b border-gray-500 text-sm leading-5">
</td>
</tr>
</tbody>
</table>
</div>
</div>
<section class="my-4">
<FelButton v-show="
2023-11-10 14:05:15 +08:00
shield.hasRights(shield.actions.UPDATE, shield.objects.RESULT) && state.can_cancel
" key="cancel" @click.prevent="cancelResults" :color="'sky-800'">Cancel</FelButton>
<FelButton v-show="
2023-11-10 14:05:15 +08:00
shield.hasRights(shield.actions.UPDATE, shield.objects.RESULT) &&
state.can_reinstate
" key="reinstate" @click.prevent="reInstateResults" :color="'orange-600'">Re-Instate</FelButton>
<FelButton v-show="
2023-11-10 14:05:15 +08:00
shield.hasRights(shield.actions.UPDATE, shield.objects.RESULT) && state.can_submit
" key="submit" @click.prevent="submitResults" :color="'orange-600'">Submit</FelButton>
<FelButton v-show="
2023-11-10 14:05:15 +08:00
shield.hasRights(shield.actions.UPDATE, shield.objects.RESULT) &&
state.can_retract
" key="retract" @click.prevent="retractResults" :color="'orange-600'">Retract</FelButton>
<FelButton v-show="
2023-11-10 14:05:15 +08:00
shield.hasRights(shield.actions.UPDATE, shield.objects.RESULT) &&
state.can_approve
" key="verify" @click.prevent="approveResults" :color="'orange-600'">Verify</FelButton>
<FelButton v-show="
2023-11-10 14:05:15 +08:00
shield.hasRights(shield.actions.UPDATE, shield.objects.RESULT) && state.can_retest
" key="retest" @click.prevent="retestResults" :color="'orange-600'">Retest</FelButton>
2023-11-10 14:05:15 +08:00
</section>
<FelDrawer :show="viewInfo" @close="viewInfo = false" :content-width="'w-2/4'">
<template v-slot:header>
<h3>Result Information</h3>
</template>
<template v-slot:body>
<AnalysisSneak v-if="viewResultInfo?.analysisUid" :analysisUid="viewResultInfo?.analysisUid" />
<ResultDetail v-if="viewResultInfo?.uid" :analysisResultesultUid="viewResultInfo?.uid" />
</template>
</FelDrawer>
2023-11-10 14:05:15 +08:00
</template>