jws-frontend/src/pages/09_task-order/order_view/MainPage.vue

1401 lines
42 KiB
Vue

<script lang="ts" setup>
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { api } from 'src/boot/axios';
import { Lang } from 'src/utils/ui';
import { baseUrl } from 'stores/utils';
import TaskStatusComponent from '../TaskStatusComponent.vue';
import StateButton from 'src/components/button/StateButton.vue';
import DocumentExpansion from '../expansion/DocumentExpansion.vue';
import ProductExpansion from '../expansion/ProductExpansion.vue';
import PaymentExpansion from '../expansion/PaymentExpansion.vue';
import AdditionalFileExpansion from '../expansion/AdditionalFileExpansion.vue';
import RemarkExpansion from '../expansion/RemarkExpansion.vue';
import InfoMessengerExpansion from '../expansion/receive/InfoMessengerExpansion.vue';
import TableEmployee from '../TableEmployee.vue';
import {
SaveButton,
MainButton,
EditButton,
UndoButton,
CancelButton,
} from 'src/components/button';
import FormGroupHead from 'src/pages/08_request-list/FormGroupHead.vue';
import FailRemarkDialog from '../receive_view/FailRemarkDialog.vue';
import SelectReadyRequestWork from '../SelectReadyRequestWork.vue';
import { dialogWarningClose } from 'stores/utils';
import useOptionStore from 'src/stores/options';
import { useTaskOrderForm, DEFAULT_DATA } from '../form';
import { useTaskOrderStore } from 'src/stores/task-order';
import { dateFormatJS, dateFormat } from 'src/utils/datetime';
import { initLang, initTheme } from 'src/utils/ui';
import { useConfigStore } from 'src/stores/config';
import { storeToRefs } from 'pinia';
import { User } from 'src/stores/user';
import {
TaskOrderStatus,
TaskStatus,
UserTaskStatus,
} from 'src/stores/task-order/types';
import { RequestWork } from 'src/stores/request-list';
import { precisionRound } from 'src/utils/arithmetic';
const taskOrderFormStore = useTaskOrderForm();
const taskOrderStore = useTaskOrderStore();
const route = useRoute();
const router = useRouter();
const configStore = useConfigStore();
const { data: config } = storeToRefs(configStore);
const { t } = useI18n();
const { currentFormData, state, fullTaskOrder } =
storeToRefs(taskOrderFormStore);
const fileList = ref<FileList>();
const fileData = ref<
{
name: string;
progress: number;
loaded: number;
total: number;
url?: string;
placeholder?: boolean;
}[]
>([]);
const refProductExpansion = ref<InstanceType<typeof ProductExpansion>>();
const taskProduct = ref<{ productId: string; discount?: number }[]>([]);
const view = ref<TaskOrderStatus>(TaskOrderStatus.Pending);
const taskStatusRecords = ref<
{
code?: string;
requestWorkId: string;
step: number;
failedComment?: string;
failedType?: string;
}[]
>([]);
const tempGroupEdit = ref<
{
product: RequestWork['productService']['product'];
list: (RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
responsibleInstitution: (string | { group: string })[];
} | null;
})[];
}[]
>([]);
const pageState = reactive({
productDialog: false,
failedDialog: false,
});
const formDocument = ref();
const responsibleInstitution = ref<string[]>([]);
const summaryPrice = computed(() => getPrice(taskListGroup.value));
const selectedEmployee = ref<
(RequestWork & {
taskStatus: TaskStatus;
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
} | null;
})[][][]
>([]);
const failedDialog = ref(false);
function getPrice(
list: {
product: RequestWork['productService']['product'];
list: (RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
responsibleInstitution: (string | { group: string })[];
} | null;
})[];
}[],
) {
return list.reduce(
(a, c) => {
const pricePerUnit = c.product.serviceCharge;
const amount = c.list.length;
const discount =
taskProduct.value.find((v) => v.productId === c.product.id)?.discount ||
0;
const priceNoVat = c.product.serviceChargeVatIncluded
? pricePerUnit / (1 + (config.value?.vat || 0.07))
: pricePerUnit;
const adjustedPriceWithVat = precisionRound(
priceNoVat * (1 + (config.value?.vat || 0.07)),
);
const adjustedPriceNoVat =
adjustedPriceWithVat / (1 + (config.value?.vat || 0.07));
const priceDiscountNoVat = adjustedPriceNoVat * amount - discount;
const rawVatTotal = priceDiscountNoVat * (config.value?.vat || 0.07);
a.totalPrice = a.totalPrice + priceDiscountNoVat;
a.totalDiscount = a.totalDiscount + Number(discount);
a.vat = c.product.serviceChargeCalcVat ? a.vat + rawVatTotal : a.vat;
a.vatExcluded = c.product.serviceChargeCalcVat
? a.vatExcluded
: precisionRound(a.vatExcluded + priceDiscountNoVat);
a.finalPrice = a.totalPrice - a.totalDiscount + a.vat;
return a;
},
{
totalPrice: 0,
totalDiscount: 0,
vat: 0,
vatExcluded: 0,
finalPrice: 0,
},
);
}
function getTemplateData(
requestWork: RequestWork,
targetStep: number,
): {
id: string;
templateName: string;
templateStepName: string;
step: number;
responsibleInstitution: (string | { group: string })[];
} | null {
const flow = requestWork.productService.service?.workflow;
if (!flow) return null;
const step = flow.step.find((v) => v.order === targetStep);
if (!step) return null;
return {
id: step.id,
templateName: flow.name,
templateStepName: step.name || '-',
step: step.order,
responsibleInstitution: step.responsibleInstitution.map((v) =>
typeof v === 'string' ? v : v.group,
),
};
}
const messengerListGroup = computed(() =>
currentFormData.value.taskList.reduce<
{
responsibleUser: User;
list: {
product: RequestWork['productService']['product'];
list: (RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
responsibleInstitution: (string | { group: string })[];
} | null;
taskStatus: TaskStatus;
})[];
}[];
}[]
>((acc, curr) => {
const taskStatus = curr.taskStatus;
const step = curr.step;
const task = curr.requestWorkStep;
if (!task) return acc;
if (task.responsibleUser && task.requestWork) {
const indexUser = acc.findIndex(
(v) => v.responsibleUser.id === task.responsibleUser?.id,
);
const record = Object.assign(task.requestWork, {
_template: getTemplateData(task.requestWork, step),
taskStatus: taskStatus ?? TaskStatus.Pending,
});
if (indexUser === -1) {
acc.push({
responsibleUser: task.responsibleUser,
list: [
{
product: task.requestWork.productService.product,
list: [record],
},
],
});
if (selectedEmployee.value.length < acc.length) {
selectedEmployee.value.push([[]]);
}
}
if (indexUser !== -1) {
const indexProduct = acc[indexUser].list.findIndex(
(v) => v.product.id === task.requestWork.productService.product.id,
);
if (indexProduct === -1) {
acc[indexUser].list.push({
product: task.requestWork.productService.product,
list: [record],
});
}
if (indexProduct !== -1) {
acc[indexUser].list[indexProduct].list.push(record);
}
}
}
return acc;
}, []),
);
let taskListGroup = computed(() => {
let tempValueInstitution: string[] = [];
const cacheData = currentFormData.value.taskList.reduce<
{
product: RequestWork['productService']['product'];
list: (RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
responsibleInstitution: (string | { group: string })[];
} | null;
})[];
}[]
>((acc, curr) => {
if (
fullTaskOrder.value?.taskOrderStatus === TaskOrderStatus.Complete &&
curr.taskStatus !== TaskStatus.Complete
) {
return acc;
}
const task = curr.requestWorkStep;
const step = curr.step;
if (!task) return acc;
if (task.requestWork) {
let exist = acc.find(
(item) => task.requestWork.productService.productId == item.product.id,
);
const record = Object.assign(task.requestWork, {
_template: getTemplateData(task.requestWork, step),
});
const template = getTemplateData(task.requestWork, step);
if (template) {
tempValueInstitution.push(
...template.responsibleInstitution.map((v) =>
typeof v === 'string' ? v : (v.group ?? 'unknown'),
),
);
}
if (exist) {
exist.list.push(task.requestWork);
} else {
acc.push({
product: task.requestWork.productService.product,
list: [record],
});
}
}
return acc;
}, []);
responsibleInstitution.value = Array.from(new Set(tempValueInstitution));
return cacheData;
});
const statusTabForm = ref<
{
title: string;
status: 'done' | 'doing' | 'waiting';
handler: () => void;
active?: () => boolean;
}[]
>([
{
title: 'title',
status: currentFormData.value.id !== undefined ? 'done' : 'doing',
active: () => view.value === TaskOrderStatus.Pending,
handler: () => {},
},
{
title: 'inProgress',
status: 'waiting',
active: () => view.value === TaskOrderStatus.InProgress,
handler: () => {},
},
{
title: 'validate',
status: 'waiting',
active: () => view.value === TaskOrderStatus.Validate,
handler: () => {},
},
{
title: 'goodReceipt',
status: 'waiting',
active: () => view.value === TaskOrderStatus.Complete,
handler: () => {},
},
]);
const TAB_STATUS = ['Pending', 'InProgress', 'Validate', 'Complete'];
function getStatus(
status: typeof currentFormData.value.taskStatus,
doneIndex: number,
doingIndex: number,
) {
return TAB_STATUS.findIndex((v) => v === status) >= doneIndex
? 'done'
: TAB_STATUS.findIndex((v) => v === status) >= doingIndex
? 'doing'
: 'waiting';
}
async function fetchStatus() {
statusTabForm.value = [
{
title: 'title',
status:
fullTaskOrder.value?.taskOrderStatus !== undefined ? 'done' : 'doing',
active: () => view.value === TaskOrderStatus.Pending,
handler: () => (view.value = TaskOrderStatus.Pending),
},
{
title: 'inProgress',
status:
fullTaskOrder.value?.taskOrderStatus === TaskOrderStatus.Pending ||
fullTaskOrder.value?.taskOrderStatus === TaskOrderStatus.InProgress
? 'doing'
: 'done',
active: () => view.value === TaskOrderStatus.InProgress,
handler: () => (view.value = TaskOrderStatus.InProgress),
},
{
title: 'validate',
status:
fullTaskOrder.value?.taskOrderStatus === TaskOrderStatus.Pending ||
fullTaskOrder.value?.taskOrderStatus === TaskOrderStatus.InProgress
? 'doing'
: 'done',
active: () => view.value === TaskOrderStatus.Validate,
handler: () => (view.value = TaskOrderStatus.Validate),
},
{
title: 'goodReceipt',
status: getStatus(fullTaskOrder.value?.taskOrderStatus, 3, 3),
active: () => view.value === TaskOrderStatus.Complete,
handler: () => (view.value = TaskOrderStatus.Complete),
},
];
}
async function submitForm() {
const res = await taskOrderFormStore.submitTask({
taskProduct: taskProduct.value,
});
if (res && currentFormData.value.id) {
await taskOrderFormStore.assignFormData(currentFormData.value.id);
if (fileList.value) {
await uploadFile(currentFormData.value.id, fileList.value);
}
router.push({
name: 'TaskOrderView',
params: { id: currentFormData.value.id },
});
}
fetchStatus();
state.value.mode = 'info';
}
function openProductDialog() {
pageState.productDialog = true;
}
async function closeTab() {
if (state.value.mode === 'edit' && !!currentFormData.value.id) {
taskOrderFormStore.resetForm();
await taskOrderFormStore.assignFormData(currentFormData.value.id, 'edit');
} else {
dialogWarningClose(t, {
message: t('dialog.message.close'),
action: () => {
window.close();
},
cancel: () => {},
});
}
}
function undo() {
if (!currentFormData.value) return;
taskOrderFormStore.assignFormData(currentFormData.value?.id || '', 'info');
}
onMounted(async () => {
initTheme();
initLang();
await configStore.getConfig();
let currentId = route.params['id'] === 'add' ? undefined : route.params['id'];
if (route.path === '/task-order/add') {
state.value.mode = 'create';
}
if (route.query['edit'] === 'true') {
state.value.mode = 'edit';
router.push({
path: route.path,
});
}
if (currentId !== undefined && typeof currentId === 'string') {
await taskOrderFormStore.assignFormData(
currentId,
state.value.mode === 'edit' ? 'edit' : 'info',
);
await fetchStatus();
if (fullTaskOrder.value) {
taskProduct.value = fullTaskOrder.value.taskProduct;
}
if (fullTaskOrder.value?.taskOrderStatus === TaskOrderStatus.InProgress)
view.value = TaskOrderStatus.InProgress;
if (fullTaskOrder.value?.taskOrderStatus === TaskOrderStatus.Complete)
view.value = TaskOrderStatus.Complete;
if (fullTaskOrder.value?.taskOrderStatus === TaskOrderStatus.Validate)
view.value = TaskOrderStatus.Validate;
}
});
async function getFileList(taskId: string) {
const fileList = await taskOrderStore.listAttachment({ parentId: taskId });
if (fileList)
fileData.value = await Promise.all(
fileList.map(async (v) => {
const res = await taskOrderStore.headAttachment({
parentId: taskId,
fileId: v,
});
let contentLength = 0;
if (res) contentLength = Number(res['content-length']);
return {
name: v,
progress: 1,
loaded: contentLength,
total: contentLength,
url: `/task-order/${taskId}/attachment/${v}`,
};
}),
);
}
function fileToUrl(file: File) {
return URL.createObjectURL(file);
}
async function remove(taskId: string, n: string) {
dialogWarningClose(t, {
message: t('dialog.message.confirmDelete'),
actionText: t('dialog.action.ok'),
action: async () => {
const res = await taskOrderStore.delAttachment({
parentId: taskId,
name: n,
});
if (res) {
getFileList(taskId);
}
},
cancel: () => {},
});
}
async function uploadFile(taskId: string, list: FileList) {
const promises: ReturnType<typeof taskOrderStore.putAttachment>[] = [];
for (let i = 0; i < list.length; i++) {
const data = {
name: list[i].name,
progress: 1,
loaded: 0,
total: 0,
url: `/task/${taskId}/attachment/${list[i].name}`,
};
promises.push(
taskOrderStore.putAttachment({
parentId: taskId,
name: list[i].name,
file: list[i],
onUploadProgress: (e) => {
const exists = fileData?.value.find((v) => v.name === data.name);
if (!exists) return fileData?.value.push(data);
exists.total = e.total || 0;
exists.progress = e.progress || 0;
exists.loaded = e.loaded;
},
}),
);
fileData?.value.push(data);
}
fileList.value = undefined;
const beforeUnloadHandler = (e: Event) => {
e.preventDefault();
};
window.addEventListener('beforeunload', beforeUnloadHandler);
return await Promise.all(promises).then((v) => {
window.removeEventListener('beforeunload', beforeUnloadHandler);
return v;
});
}
async function completeValidate() {
if (!fullTaskOrder.value) return;
const res = await taskOrderStore.completeTaskOrder(fullTaskOrder.value.id);
if (res) {
await taskOrderFormStore.assignFormData(fullTaskOrder.value.id);
await fetchStatus();
}
}
function getMessengerName(
record: User,
opts?: {
gender?: boolean;
url?: boolean;
locale?: string;
},
) {
const user = record;
if (!user) return '-';
const url = `${baseUrl}/user/${user.id}/profile-image/${user.selectedImage}`;
return opts?.gender
? user.gender
: opts?.url
? url
: {
[Lang.English]: `${useOptionStore().mapOption(user.namePrefix as string)} ${user.firstNameEN} ${user.lastNameEN}`,
[Lang.Thai]: `${useOptionStore().mapOption(user.namePrefix as string)} ${user.firstName} ${user.lastName}`,
}[opts?.locale || Lang.English] || '-';
}
function openFailedDialog(
row: RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
};
taskStatus: TaskStatus;
},
) {
const task = fullTaskOrder.value?.taskList.find(
(t) => t.requestWorkId === row.id,
);
taskStatusRecords.value = [
{
code: `${row.productService.product.code}-${row.request.code}`,
requestWorkId: row.id || '',
step: row._template?.step || 0,
failedComment: (task && task.failedComment) || '',
failedType: (task && task.failedType) || '',
},
];
pageState.failedDialog = true;
}
function taskStatusCount(index: number, id: string, responsibleUserId: string) {
const task = fullTaskOrder.value?.taskList.filter(
(t) =>
t.requestWorkStep.requestWork.productService.productId === id &&
t.requestWorkStep.responsibleUserId === responsibleUserId,
);
if (index === 1) {
return task?.filter(
(t) =>
t.taskStatus === TaskStatus.InProgress ||
t.taskStatus === TaskStatus.Pending,
).length;
} else if (index === 2) {
return task?.filter(
(t) =>
t.taskStatus === TaskStatus.Success ||
t.taskStatus === TaskStatus.Complete ||
t.taskStatus === TaskStatus.Validate,
).length;
} else {
return task?.filter(
(t) =>
t.taskStatus === TaskStatus.Redo ||
t.taskStatus === TaskStatus.Failed ||
t.taskStatus === TaskStatus.Canceled,
).length;
}
}
function viewDocument(id: string, routeName: 'order' | 'receive') {
window.open(`/task-order/${id}/doc-product-${routeName}`, '_blank');
}
function handleChangeStatus(
records: {
data: (RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
} | null;
})[];
status: TaskStatus;
},
messengerIndex: number,
productIndex: number,
) {
const { data, status } = records;
if (data.length === 0) return;
taskStatusRecords.value = data.map((v) => ({
code: `${v.productService.product.code}-${v.request.code}`,
requestWorkId: v.id || '',
step: v._template?.step || 0,
}));
if (status === TaskStatus.Failed) {
failedDialog.value = true;
return;
}
taskOrderFormStore.changeStatus(taskStatusRecords.value, status, () => {
if (!currentFormData.value.id) return;
selectedEmployee.value[messengerIndex][productIndex] = [];
taskOrderFormStore.assignFormData(currentFormData.value.id, 'info');
});
}
function sortList(
list: (RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
responsibleInstitution: (string | { group: string })[];
} | null;
taskStatus: TaskStatus;
})[],
) {
const prioritizedStatuses = new Set([
TaskStatus.InProgress,
TaskStatus.Redo,
TaskStatus.Success,
TaskStatus.Complete,
TaskStatus.Canceled,
]);
return list.sort((a, b) => {
const aPriority = prioritizedStatuses.has(a.taskStatus) ? 1 : 0;
const bPriority = prioritizedStatuses.has(b.taskStatus) ? 1 : 0;
return aPriority - bPriority;
});
}
watch(
() => [currentFormData.value.taskStatus],
() => {
fetchStatus();
},
);
</script>
<template>
<div class="column surface-0 fullscreen">
<div
class="color-bar"
:class="{
dark: $q.dark.isActive,
complete: view === TaskOrderStatus.Complete,
}"
>
<div
:class="{
'pink-segment': view !== TaskOrderStatus.Complete,
'yellow-segment': view === TaskOrderStatus.Complete,
}"
></div>
<div
:class="{
'light-pink-segment': view !== TaskOrderStatus.Complete,
'light-yellow-segment': view === TaskOrderStatus.Complete,
}"
></div>
<div class="gray-segment"></div>
</div>
<!-- SEC: Header -->
<header
class="row q-px-md q-py-sm items-center justify-between relative-position"
>
<section class="banner" :class="{ dark: $q.dark.isActive }"></section>
<div style="flex: 1" class="row items-center">
<RouterLink to="/task-order">
<q-img src="/icons/favicon-512x512.png" width="3rem" />
</RouterLink>
<span class="column text-h6 text-bold q-ml-md">
{{
view === TaskOrderStatus.Complete
? $t('taskOrder.goodReceipt')
: $t('taskOrder.title')
}}
<!-- {{ code || '' }} -->
<span class="text-caption text-regular app-text-muted">
{{
$t('quotation.processOn', {
msg: dateFormatJS({
date: fullTaskOrder
? fullTaskOrder.createdAt
: new Date(Date.now()),
dayStyle: 'numeric',
monthStyle: 'long',
locale: $i18n.locale === 'tha' ? 'th-Th' : 'en-US',
}),
})
}}
{{
dateFormat(
fullTaskOrder ? fullTaskOrder.createdAt : new Date(Date.now()),
false,
true,
)
}}
</span>
</span>
</div>
</header>
<!-- SEC: Body -->
<article
class="col full-width q-pa-md"
style="flex-grow: 1; overflow-y: auto"
>
<section class="col-sm col-12">
<div class="col q-gutter-y-md">
<nav
class="surface-1 q-pa-sm row no-wrap full-width scroll rounded"
style="gap: 10px"
>
<!-- TODO: replace step and status -->
<StateButton
v-for="i in statusTabForm"
:key="i.title"
:label="$t(`taskOrder.${i.title}`)"
:status-active="i.active?.()"
:status-done="i.status === 'done'"
:status-waiting="i.status === 'waiting'"
@click="i.handler()"
/>
</nav>
<article
v-if="view === TaskOrderStatus.Complete"
class="row items-center surface-1 q-py-md rounded gradient-stat"
>
<span
class="row col rounded q-px-sm q-py-md q-mx-md positive"
style="border: 1px solid hsl(var(--positive-bg))"
>
{{ $t('taskOrder.status.Complete') }}
<span class="q-ml-auto">
{{
fullTaskOrder?.taskList.filter(
(t) => t.taskStatus === TaskStatus.Complete,
).length
}}
</span>
</span>
<span
class="row col rounded q-px-sm q-mr-md q-py-md negative"
style="border: 1px solid hsl(var(--negative-bg))"
>
{{ $t('taskOrder.status.Failed') }}
<span class="q-ml-auto">
{{
fullTaskOrder?.taskList.filter(
(t) =>
t.taskStatus === TaskStatus.Redo ||
t.taskStatus === TaskStatus.Failed ||
t.taskStatus === TaskStatus.Canceled,
).length
}}
</span>
</span>
</article>
<q-form
ref="formDocument"
@submit="
() => {
submitForm();
}
"
>
<DocumentExpansion
:readonly="!['create', 'edit'].includes(state.mode || '')"
v-model:registered-branch-id="currentFormData.registeredBranchId"
v-model:institution-id="currentFormData.institutionId"
v-model:task-name="currentFormData.taskName"
v-model:code="currentFormData.code"
v-model:contact-name="currentFormData.contactName"
v-model:contact-tel="currentFormData.contactTel"
:task-list-group="
taskListGroup.length === 0 && state.mode === 'create'
"
:institution-group="responsibleInstitution"
/>
</q-form>
<ProductExpansion
ref="refProductExpansion"
v-if="
view === TaskOrderStatus.Pending ||
view === TaskOrderStatus.Complete
"
:readonly="!['create', 'edit'].includes(state.mode || '')"
:task-list="taskListGroup"
v-model:task-product="taskProduct"
@add-product="openProductDialog"
/>
<PaymentExpansion
v-model:summary-price="summaryPrice"
:complete="view === TaskOrderStatus.Complete"
v-if="
view === TaskOrderStatus.Pending ||
view === TaskOrderStatus.Complete
"
/>
<AdditionalFileExpansion
:readonly="!['create', 'edit'].includes(state.mode || '')"
v-if="
view === TaskOrderStatus.Pending ||
view === TaskOrderStatus.Complete
"
v-model:file-data="fileData"
:transform-url="
async (url: string) => {
if (state.mode === 'create') {
return url;
} else {
const result = await api.get<string>(url);
return result.data;
}
}
"
@fetch-file-list="
() => {
if (!currentFormData.id) return;
getFileList(currentFormData.id);
}
"
@upload="
async (f) => {
fileList = f;
if (!currentFormData.id) {
fileData = [];
Array.from(f).forEach((el) => {
fileData.push({
name: el.name,
progress: 1,
loaded: 0,
total: el.size,
placeholder: true,
url: fileToUrl(el),
});
});
} else {
await uploadFile(currentFormData.id, f);
}
}
"
@remove="
async (n) => {
if (!currentFormData.id) {
const attIndex = fileData.findIndex((v) => v.name === n);
fileData.splice(attIndex, 1);
} else {
await remove(currentFormData.id, n);
}
}
"
/>
<!-- TODO: blind remark, urgent -->
<RemarkExpansion
v-if="
view === TaskOrderStatus.Pending ||
view === TaskOrderStatus.Complete
"
v-model:remark="currentFormData.remark"
:readonly="!['create', 'edit'].includes(state.mode || '')"
:items="taskListGroup"
:items-discount="taskProduct"
:default-remark="DEFAULT_DATA.remark"
>
<template #hint>
{{ $t('general.hintRemark') }}
<code>#[order-detail]</code>
{{ $t('general.orderDetail') }}
</template>
</RemarkExpansion>
<template
v-if="
view === TaskOrderStatus.InProgress ||
view === TaskOrderStatus.Validate
"
>
<InfoMessengerExpansion
v-for="(v, messengerIndex) in messengerListGroup"
:key="messengerIndex"
:gender="getMessengerName(v.responsibleUser, { gender: true })"
:contact-name="
getMessengerName(v.responsibleUser, { locale: $i18n.locale })
"
:contact-url="getMessengerName(v.responsibleUser, { url: true })"
:contact-tel="v.responsibleUser.telephoneNo"
:email="v.responsibleUser.email"
:status="
fullTaskOrder?.userTask.find(
(l) => l.userId === v.responsibleUser.id,
)?.userTaskStatus || UserTaskStatus.Pending
"
:accepted-at="
fullTaskOrder?.userTask.find(
(l) => l.userId === v.responsibleUser.id,
)?.acceptedAt
"
:submitted-at="
fullTaskOrder?.userTask.find(
(l) => l.userId === v.responsibleUser.id,
)?.submittedAt
"
>
<template #product>
<FormGroupHead>
{{ $t('menu.product') }}
</FormGroupHead>
<div
v-for="({ product, list }, productIndex) in v.list"
:key="product.id"
class="bordered-b"
>
<q-expansion-item
dense
class="overflow-hidden"
switch-toggle-side
expand-icon="mdi-chevron-down-circle"
header-class="text-medium text-body items-center bordered-b "
>
<template #header>
<section class="row items-center full-width">
<div class="flex items-center col-sm col-12 no-wrap">
<q-avatar class="q-mr-md" size="md">
<q-img
class="text-center"
:ratio="1"
:src="`${baseUrl}/product/${product.id}/image/${product.selectedImage}`"
>
<template #error>
<q-icon
class="full-width full-height"
name="mdi-shopping-outline"
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
/>
</template>
</q-img>
</q-avatar>
<span>
{{ product.name }}
<div class="app-text-muted text-caption">
{{ product.code }}
</div>
</span>
</div>
<span
class="row items-center q-gutter-x-sm"
:class="{ 'q-py-xs': $q.screen.lt.sm }"
>
<div
v-for="taskStatus in 3"
:key="taskStatus"
class="rounded q-px-sm row items-center"
style="background: hsl(var(--text-mute) / 0.1)"
:style="`color: hsl(var(--${
taskStatus === 1
? 'warning'
: taskStatus === 2
? 'positive'
: 'negative'
}-bg))`"
>
<q-icon
name="mdi-account-group-outline"
size="xs"
class="q-pr-sm"
/>
{{
taskStatusCount(
taskStatus,
product.id,
v.responsibleUser.id,
)
}}
</div>
</span>
</section>
</template>
<div>
<div class="full-width q-pa-sm">
<TableEmployee
validate
step-on
:checkbox-on="
view === TaskOrderStatus.Validate &&
fullTaskOrder?.userTask.find(
(l) => l.userId === v.responsibleUser.id,
)?.userTaskStatus === UserTaskStatus.Submit
"
:check-all="
view === TaskOrderStatus.Validate &&
fullTaskOrder?.userTask.find(
(l) => l.userId === v.responsibleUser.id,
)?.userTaskStatus === UserTaskStatus.Submit
"
:rows="sortList(list)"
@change-all-status="
(v) =>
handleChangeStatus(
v,
messengerIndex,
productIndex,
)
"
v-model:selected-employee="
selectedEmployee[messengerIndex][productIndex]
"
>
<template #append="{ props: subProps }">
<TaskStatusComponent
:key="subProps.row.id"
:no-action="view !== TaskOrderStatus.Validate"
type="order"
:readonly="
(() => {
const _userStatus =
fullTaskOrder?.userTask.find(
(l) => l.userId === v.responsibleUser.id,
)?.userTaskStatus;
return (
_userStatus !== UserTaskStatus.Submit &&
_userStatus !== UserTaskStatus.Restart
);
})()
"
:status="subProps.row.taskStatus"
@click-failed="
() => {
openFailedDialog(subProps.row);
}
"
@change-status="
(status) => {
handleChangeStatus(
{
data: [subProps.row],
status: status,
},
messengerIndex,
productIndex,
);
}
"
/>
</template>
</TableEmployee>
</div>
</div>
</q-expansion-item>
<FailRemarkDialog
readonly
:fail-task-option="list"
v-model:set-task-status-list="taskStatusRecords"
v-model:open="pageState.failedDialog"
@close="
() => {
pageState.failedDialog = false;
taskStatusRecords = [];
}
"
/>
</div>
</template>
</InfoMessengerExpansion>
</template>
</div>
</section>
</article>
<!-- SEC: footer -->
<footer class="surface-1 q-pa-md full-width">
<nav class="row justify-end">
<MainButton
class="q-mr-auto"
v-if="currentFormData.id"
outlined
icon="mdi-play-box-outline"
color="207 96% 32%"
@click="
viewDocument(
currentFormData.id,
view === TaskOrderStatus.Complete ? 'receive' : 'order',
)
"
>
{{ $t('general.view', { msg: $t('general.example') }) }}
</MainButton>
<div class="row" style="gap: var(--size-2)">
<UndoButton outlined @click="undo()" v-if="state.mode === 'edit'" />
<CancelButton
v-if="state.mode !== 'edit'"
:label="$t('dialog.action.close')"
outlined
@click="closeTab()"
/>
<template
v-if="
fullTaskOrder?.taskOrderStatus === TaskOrderStatus.Pending ||
state.mode === 'create'
"
>
<SaveButton
v-if="state.mode && ['create', 'edit'].includes(state.mode)"
@click="() => formDocument.submit()"
solid
/>
<EditButton
v-else
class="no-print"
@click="state.mode = 'edit'"
solid
/>
</template>
<SaveButton
v-if="
state.mode !== 'create' &&
view === TaskOrderStatus.Validate &&
fullTaskOrder?.taskOrderStatus !== TaskOrderStatus.Pending
"
:disabled="
!fullTaskOrder?.taskList.some((t) =>
[
TaskStatus.Complete,
TaskStatus.Redo,
TaskStatus.Canceled,
TaskStatus.Validate,
].includes(t.taskStatus),
) || fullTaskOrder?.taskOrderStatus === TaskOrderStatus.Complete
"
@click="
dialogWarningClose(t, {
message: $t(
`${
fullTaskOrder?.taskList.some(
(t) =>
t.taskStatus === TaskStatus.Pending ||
t.taskStatus === TaskStatus.InProgress ||
t.taskStatus === TaskStatus.Restart,
)
? 'dialog.message.confirmEndWorkWarning'
: 'dialog.message.confirmEndWork'
}`,
),
actionText: $t('dialog.action.ok'),
action: async () => {
await completeValidate();
},
cancel: () => {},
})
"
:label="$t('taskOrder.confirmEndWork')"
icon="mdi-check"
solid
></SaveButton>
</div>
</nav>
</footer>
</div>
<!-- SEC: Dialog -->
<SelectReadyRequestWork
:task-list-group="taskListGroup"
:fetch-params="{ readyToTask: true }"
v-model:open="pageState.productDialog"
v-model:task-list="currentFormData.taskList"
v-model:temp-group-edit="tempGroupEdit"
@after-submit="
() => {
taskProduct = [];
const taskP = currentFormData.taskList
.map((v) => ({
productId:
v.requestWorkStep?.requestWork.productService.productId ?? '',
discount: 0,
}))
.filter(
(value, index, self) =>
index === self.findIndex((t) => t.productId === value.productId),
);
taskProduct = taskP;
}
"
/>
</template>
<style scoped>
.color-bar {
width: 100%;
height: 1vh;
background: linear-gradient(
90deg,
rgb(214, 51, 108) 0%,
rgba(255, 255, 255, 1) 77%,
rgba(204, 204, 204, 1) 100%
);
display: flex;
overflow: hidden;
&.complete {
background: linear-gradient(
90deg,
rgba(245, 159, 0, 1) 0%,
rgba(255, 255, 255, 1) 77%,
rgba(204, 204, 204, 1) 100%
);
}
}
.color-bar.dark {
opacity: 0.7;
}
.pink-segment {
background-color: var(--pink-7);
flex-grow: 4;
}
.light-pink-segment {
background-color: hsla(var(--pink-7-hsl) / 0.2);
flex-grow: 0.5;
}
.yellow-segment {
background-color: var(--yellow-7);
flex-grow: 4;
}
.light-yellow-segment {
background-color: hsla(var(--yellow-7-hsl) / 0.2);
flex-grow: 0.5;
}
.gray-segment {
background-color: #ccc;
flex-grow: 1;
}
.yellow-segment,
.light-yellow-segment,
.pink-segment,
.light-pink-segment,
.gray-segment {
transform: skewX(-60deg);
}
.banner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url('/images/building-banner.png');
background-repeat: no-repeat;
background-size: cover;
z-index: -1;
&.dark {
filter: invert(100%);
}
}
.gradient-stat {
& .positive {
background-image: linear-gradient(
to right,
hsl(var(--positive-bg) / 0.15),
var(--surface-1)
);
}
& .negative {
background-image: linear-gradient(
to right,
hsl(var(--negative-bg) / 0.15),
var(--surface-1)
);
}
}
</style>