mirror of
https://github.com/beak-insights/felicity-lims.git
synced 2025-02-23 16:33:11 +08:00
626 lines
22 KiB
Vue
626 lines
22 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed } from "vue";
|
|
import { useRoute } from "vue-router";
|
|
import { useSampleStore } from "@/stores";
|
|
import { isNullOrWs } from "@/utils/helpers";
|
|
import { useAnalysisComposable } from "@/composables";
|
|
import { IAnalysisResult, IQCLevel, IQCSet, ISample } from "@/models/analysis";
|
|
|
|
import * as shield from "@/guards";
|
|
|
|
let sampleStore = useSampleStore();
|
|
let route = useRoute();
|
|
|
|
let can_submit = ref<boolean>(false);
|
|
let can_retract = ref<boolean>(false);
|
|
let can_verify = ref<boolean>(false);
|
|
let can_retest = ref<boolean>(false);
|
|
let can_reinstate = ref<boolean>(false);
|
|
|
|
let allChecked = ref<boolean>(false);
|
|
|
|
sampleStore.fetchQCSetByUid(route.params.qcSetUid);
|
|
|
|
let qcSet = computed(() => {
|
|
let set = sampleStore.getQCSet as IQCSet;
|
|
if (!set) return;
|
|
let final = { levels: [], analytes: [] } as any;
|
|
set?.samples?.forEach((sample) => {
|
|
if (!sample.assigned) {
|
|
if (!final.levels.some((l: IQCLevel) => l.uid == sample?.qcLevel?.uid)) {
|
|
final.levels.push(sample?.qcLevel);
|
|
}
|
|
sample?.analysisResults?.forEach((result) => {
|
|
if (
|
|
!final.analytes.some((a: IAnalysisResult) => a.uid == result?.analysis?.uid)
|
|
) {
|
|
final.analytes.push(result?.analysis);
|
|
}
|
|
const index = final.analytes.findIndex(
|
|
(a: IAnalysisResult) => a.uid == result?.analysis?.uid
|
|
);
|
|
if (final.analytes[index]["items"]) {
|
|
if (
|
|
!final.analytes[index]["items"]?.some(
|
|
(a: IAnalysisResult) => a.sampleUid === result.sampleUid
|
|
)
|
|
) {
|
|
final.analytes[index]["items"].push({ ...result, sample });
|
|
}
|
|
} else {
|
|
final.analytes[index]["items"] = [{ ...result, sample }];
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
toggleView("grid");
|
|
|
|
return {
|
|
levels: final.levels as IQCLevel[],
|
|
analytes: final.analytes as any[],
|
|
};
|
|
});
|
|
|
|
function getResults(): IAnalysisResult[] {
|
|
let results: IAnalysisResult[] = [];
|
|
qcSet?.value!["analytes"]?.forEach((analyte: any) =>
|
|
analyte["items"].forEach((result: IAnalysisResult) => results.push(result))
|
|
);
|
|
return results;
|
|
}
|
|
|
|
function getAllAnalysisResults(): IAnalysisResult[] {
|
|
let results: IAnalysisResult[] = [];
|
|
if (!qcSet?.value!["analytes"]) return [];
|
|
qcSet?.value["analytes"]?.forEach((analyte) => {
|
|
analyte?.items?.forEach((result: IAnalysisResult) => results.push(result));
|
|
});
|
|
return results;
|
|
}
|
|
|
|
function getResultsChecked(): IAnalysisResult[] {
|
|
let results: IAnalysisResult[] = [];
|
|
if (!qcSet?.value!["analytes"]) return [];
|
|
qcSet?.value["analytes"]?.forEach((analyte) => {
|
|
analyte?.items?.forEach((result: IAnalysisResult) => {
|
|
if (result.checked) results.push(result);
|
|
});
|
|
});
|
|
return results;
|
|
}
|
|
|
|
function getResultsUids(): number[] {
|
|
const results = getResultsChecked();
|
|
let ready: number[] = [];
|
|
results?.forEach((result: IAnalysisResult) => ready.push(result.uid!));
|
|
return ready;
|
|
}
|
|
|
|
function resetAnalysesPermissions(): void {
|
|
// reset
|
|
can_submit.value = false;
|
|
can_retract.value = false;
|
|
can_verify.value = false;
|
|
can_retest.value = false;
|
|
can_reinstate.value = false;
|
|
|
|
const checked = getResultsChecked();
|
|
if (checked.length === 0) return;
|
|
|
|
// can reinstate
|
|
if (checked.every((result) => result.status === "cancelled")) {
|
|
can_reinstate.value = true;
|
|
}
|
|
|
|
// can submit
|
|
if (checked.every((result) => result.status === "pending")) {
|
|
can_submit.value = true;
|
|
}
|
|
|
|
// can verify/ retract/retest
|
|
if (checked.every((result) => result.status === "resulted")) {
|
|
can_retract.value = true;
|
|
can_verify.value = true;
|
|
can_retest.value = true;
|
|
}
|
|
}
|
|
|
|
function areAllChecked(): Boolean {
|
|
const results = getAllAnalysisResults();
|
|
return results?.every((item) => item.checked === true);
|
|
}
|
|
|
|
function check(result: IAnalysisResult): void {
|
|
result.checked = true;
|
|
resetAnalysesPermissions();
|
|
}
|
|
|
|
function unCheck(result: IAnalysisResult): void {
|
|
result.checked = false;
|
|
resetAnalysesPermissions();
|
|
}
|
|
|
|
function toggleCheckAll(): void {
|
|
const analysisResults = getResults();
|
|
analysisResults?.forEach((result: IAnalysisResult) =>
|
|
allChecked.value ? check(result) : unCheck(result)
|
|
);
|
|
resetAnalysesPermissions();
|
|
}
|
|
|
|
function checkCheck(): void {
|
|
if (areAllChecked()) {
|
|
allChecked.value = true;
|
|
} else {
|
|
allChecked.value = false;
|
|
}
|
|
resetAnalysesPermissions();
|
|
}
|
|
|
|
function editResult(result: any): void {
|
|
result.editable = true;
|
|
}
|
|
|
|
function isEditable(result: IAnalysisResult): Boolean {
|
|
if (result?.editable || isNullOrWs(result?.result)) {
|
|
if (
|
|
["cancelled", "verified", "retracted", "to_be_verified"].includes(result.status!)
|
|
) {
|
|
result.editable = false;
|
|
return false;
|
|
} else {
|
|
editResult(result);
|
|
return true;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isDisabledRowCheckBox(result: any): boolean {
|
|
switch (result?.status) {
|
|
case "retracted":
|
|
return true;
|
|
case "verified":
|
|
if (result?.reportable === false) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Sample Actions
|
|
function prepareResults(): IAnalysisResult[] {
|
|
let results = getResultsChecked();
|
|
let ready: any[] = [];
|
|
results?.forEach((result: IAnalysisResult) =>
|
|
ready.push({ uid: result.uid, result: result.result })
|
|
);
|
|
return ready;
|
|
}
|
|
|
|
// _updateSample if state has changed
|
|
const _updateSample = async () => {
|
|
const sample = computed(() => sampleStore.getSample);
|
|
if (sample.value) {
|
|
sampleStore.fetchSampleStatus(sample?.value?.uid);
|
|
}
|
|
};
|
|
|
|
let {
|
|
submitResults: submitter_,
|
|
cancelResults: canceller_,
|
|
reInstateResults: reInstater_,
|
|
approveResults: verifier_,
|
|
retractResults: retracter_,
|
|
retestResults: retester_,
|
|
} = useAnalysisComposable();
|
|
|
|
const submitResults = () =>
|
|
submitter_(prepareResults(), "qcset", sampleStore.qcSet?.uid!).then(
|
|
(_) => (_updateSample(), resetAnalysesPermissions())
|
|
);
|
|
const cancelResults = () =>
|
|
canceller_(getResultsUids()).then((_) => (_updateSample(), resetAnalysesPermissions()));
|
|
const reInstateResults = () =>
|
|
reInstater_(getResultsUids()).then(
|
|
(_) => (_updateSample(), resetAnalysesPermissions())
|
|
);
|
|
const verifyResults = () =>
|
|
verifier_(getResultsUids(), "qcset", sampleStore.qcSet?.uid!).then(
|
|
(_) => (_updateSample(), resetAnalysesPermissions())
|
|
);
|
|
const retractResults = () =>
|
|
retracter_(getResultsUids()).then((_) => (_updateSample(), resetAnalysesPermissions()));
|
|
const retestResults = () =>
|
|
retester_(getResultsUids()).then((_) => (_updateSample(), resetAnalysesPermissions()));
|
|
|
|
// View selection
|
|
let gridView = ref<boolean>(true);
|
|
let view = ref<string>("grid");
|
|
let hasDuplicates = ref<boolean>(false);
|
|
|
|
function toggleView(choice: string): void {
|
|
let results: IAnalysisResult[] = [];
|
|
let samples: ISample[] = [];
|
|
|
|
let set = sampleStore.getQCSet;
|
|
|
|
// for all results in a sample
|
|
// if analyses is dublicated then a retract/retest has hapenned
|
|
set?.samples?.forEach((sample: ISample) => {
|
|
samples.push(sample);
|
|
if (!sample.assigned) {
|
|
sample?.analysisResults?.forEach((result: IAnalysisResult) => results.push(result));
|
|
}
|
|
});
|
|
|
|
for (let sample of samples) {
|
|
const filtered: IAnalysisResult[] = results.filter((r) => r.sampleUid === sample.uid);
|
|
let analysisUids: number[] = [];
|
|
filtered?.forEach((result) => analysisUids.push(result.analysisUid!));
|
|
hasDuplicates.value = new Set(analysisUids).size !== analysisUids.length;
|
|
if (hasDuplicates.value === true) break;
|
|
}
|
|
|
|
if (hasDuplicates.value) {
|
|
gridView.value = false;
|
|
view.value = "list";
|
|
} else {
|
|
if (choice === "grid") {
|
|
gridView.value = true;
|
|
view.value = "grid";
|
|
} else {
|
|
gridView.value = false;
|
|
view.value = "list";
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<hr />
|
|
<div class="flex justify-end">
|
|
<button
|
|
:class="[
|
|
'focus:outline-none',
|
|
{ 'fill-current text-gray-900': gridView === true },
|
|
{ 'fill-current text-gray-200': gridView === false },
|
|
]"
|
|
@click.prevent="toggleView('grid')"
|
|
>
|
|
<font-awesome-icon icon="th" />
|
|
</button>
|
|
<button
|
|
:class="[
|
|
'focus:outline-none ml-2',
|
|
{ 'fill-current text-gray-900': gridView === false },
|
|
{ 'fill-current text-gray-200': gridView === true },
|
|
]"
|
|
@click.prevent="toggleView('list')"
|
|
>
|
|
<font-awesome-icon icon="th-list" />
|
|
</button>
|
|
</div>
|
|
<section v-if="gridView" class="overflow-x-auto mt-4">
|
|
<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"
|
|
>
|
|
<table class="min-w-full">
|
|
<thead>
|
|
<tr>
|
|
<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
|
|
v-for="level in qcSet?.levels"
|
|
:key="level.uid"
|
|
class="px-1 py-1 border-b-2 border-gray-300 text-left leading-4 text-gray-800 tracking-wider"
|
|
>
|
|
{{ level?.level }}
|
|
</th>
|
|
<th class="px-1 py-1 border-b-2 border-gray-300"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white">
|
|
<tr v-for="analyte in qcSet?.analytes" :key="analyte.uid">
|
|
<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-gray-800">
|
|
{{ analyte?.name }}
|
|
</div>
|
|
</td>
|
|
<td
|
|
v-for="result in analyte?.items"
|
|
:key="result.uid"
|
|
class="px-1 py-1 whitespace-no-wrap border-b border-gray-500"
|
|
>
|
|
<div class="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
class="mr-2"
|
|
v-model="result.checked"
|
|
@change="checkCheck()"
|
|
:disabled="isDisabledRowCheckBox(result)"
|
|
/>
|
|
<div>
|
|
<div v-if="!isEditable(result)" class="text-sm leading-5 text-sky-800">
|
|
{{ result?.result }}
|
|
</div>
|
|
<label
|
|
v-else-if="result?.analysis?.resultOptions?.length < 1"
|
|
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">
|
|
<select
|
|
class="form-input mt-1 block w-full"
|
|
v-model="result.result"
|
|
@change="check(result)"
|
|
>
|
|
<option value=""></option>
|
|
<option
|
|
v-for="(option, index) in result?.analysis?.resultOptions"
|
|
:key="option.optionKey"
|
|
:value="option.value"
|
|
>
|
|
{{ option.value }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<!-- <div class="text-sm italics text-sky-800"> {{ result?.sample?.qcLevel?.level }}</div> -->
|
|
<div class="text-sm italics text-sky-800">{{ result?.status }}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section v-else 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"
|
|
>
|
|
<table class="min-w-full">
|
|
<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="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"
|
|
>
|
|
Methods
|
|
</th>
|
|
<th
|
|
class="px-1 py-1 border-b-2 border-gray-300 text-left text-sm leading-4 text-gray-800 tracking-wider"
|
|
>
|
|
Instrument
|
|
</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>
|
|
<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>
|
|
<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"
|
|
>
|
|
Due Date
|
|
</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 getResults()" :key="result.uid">
|
|
<td>
|
|
<input
|
|
type="checkbox"
|
|
class=""
|
|
v-model="result.checked"
|
|
@change="checkCheck()"
|
|
:disabled="isDisabledRowCheckBox(result)"
|
|
/>
|
|
</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">
|
|
{{ result.analysis?.name }}
|
|
</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">
|
|
{{ result.method?.name || "None" }}
|
|
</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">
|
|
{{ result.instrument?.name || "None" }}
|
|
</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">
|
|
{{ result.analyst?.name || "moyoza" }}
|
|
</div>
|
|
</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, index) in result?.analysis?.resultOptions"
|
|
:key="option.optionKey"
|
|
: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">
|
|
<i class="fa fa-check-circle" aria-hidden="true"></i>
|
|
</span>
|
|
<span v-else class="text-orange-600">
|
|
<i class="fa fa-times-circle" aria-hidden="true"></i>
|
|
</span>
|
|
</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">2020-10-10</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">2020-10-10</div>
|
|
</td>
|
|
<td class="px-1 py-1 whitespace-no-wrap border-b border-gray-500">
|
|
<button
|
|
type="button"
|
|
class="bg-sky-800 text-white p-1rounded-smleading-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-sky-800">
|
|
<i class="fa fa-thumbs-up" aria-hidden="true"></i>
|
|
</span>
|
|
<span v-else class="text-orange-600">
|
|
<i class="fa fa-thumbs-down" aria-hidden="true"></i>
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td
|
|
class="px-1 py-1 whitespace-no-wrap text-right border-b border-gray-500 text-sm leading-5"
|
|
>
|
|
<!-- <button @click.prevent="submitResult(result)"
|
|
class="p-1 ml-2 border-white border text-gray-500rounded-smtransition duration-300 hover:border-sky-800 hover:text-sky-800 focus:outline-none">
|
|
submit
|
|
</button> -->
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="my-4">
|
|
<button
|
|
v-if="shield.hasRights(shield.actions.UPDATE, shield.objects.RESULT) && can_submit"
|
|
@click.prevent="cancelResults()"
|
|
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-if="
|
|
shield.hasRights(shield.actions.UPDATE, shield.objects.RESULT) && can_reinstate
|
|
"
|
|
@click.prevent="reInstateResults()"
|
|
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"
|
|
>
|
|
Re-Instate
|
|
</button>
|
|
<button
|
|
v-if="shield.hasRights(shield.actions.UPDATE, shield.objects.RESULT) && can_submit"
|
|
@click.prevent="submitResults()"
|
|
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"
|
|
>
|
|
Submit
|
|
</button>
|
|
<button
|
|
v-if="shield.hasRights(shield.actions.UPDATE, shield.objects.RESULT) && can_retract"
|
|
@click.prevent="retractResults()"
|
|
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"
|
|
>
|
|
Retract
|
|
</button>
|
|
<button
|
|
v-if="shield.hasRights(shield.actions.UPDATE, shield.objects.RESULT) && can_verify"
|
|
@click.prevent="verifyResults()"
|
|
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"
|
|
>
|
|
Verify
|
|
</button>
|
|
<button
|
|
v-if="shield.hasRights(shield.actions.UPDATE, shield.objects.RESULT) && can_retest"
|
|
@click.prevent="retestResults()"
|
|
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"
|
|
>
|
|
Retest
|
|
</button>
|
|
</section>
|
|
</template>
|