felicity-lims/webapp/views/qcontrol/_id/QCSet.vue
2024-06-29 15:19:21 +02:00

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>