jws-frontend/src/pages/09_task-order/order_view/MainPage.vue
Methapon Metanipat 9eff614dbd
feat: task order (#141)
* feat: task order => routes

* feat: Page

* refactor: pagination

* refactor: taskOrder => table, card and constants

* feat: add structure select request list comp

* fix: re-export type

* refactor: edit path route of task order

* feat: trigger task order

* refactor: edit type task statss

* feat: table select request list

* feat: i18n

* refactor: quasar expansion chevron color

* refactor: type

* refactor: state btn status done

* feat: task order => order view layout

* feat: task order => remark expansion

* fix: task order => rename attachment to additional file

* feat: upload file section optional layout

* feat: task order => additional file expansion

* feat: task order => payment expansion

* feat: conditionally add urgent

* feat: send id together with link

* refactor: edit type

* feat: new form.ts

* refactor: edit url

* refactor: edit id trigger

* feat: select institution component

* feat: task order code i18n

* feat: task order => document expansion form

* feat: fallback address on null

* refactor: add type for table

* feat: add filter parameter

* refactor: edit name routes

* refactor: add type of task order payload

* refactor: by value form

* refactor: responsive quotation form info

* refactor: submit form

* refactor: add i18n

* refactor: status canceled

* refactor: handle task status

* refactor: handle mode view

* refactor: addtaskstatus

* refactor: i18n & constants

* refactor: table employee

* refactor: select ready request work

* refactor: handle save form

* refactor: edit layout btn

* feat: undo()

* cleanup delete import

* feat: closetab

* refactor: handle readonly

* fix: body edit

* refactor: handle readonly uploadfile

* feat: import manage attachment

* refactor: quotation/task-order => type

* refactor: select ready request work

* refactor: i18n & constants

* chore: clean duplicate i18n

* refactor: type according to backend relation

* refactor: edit base url

* feat: upload file

* feat: fetch file list

* feat: get url file

* refactor: set default opened

* refactor: type

* feat: removefile

* feat: task order => select product

* feat: add parameter only active branch is selectable

* refactor: add i18n

* feat: set layout

* feat: add info product expansion

* refactor: new info messenger

* refactor: add slot name value

* refactor: add i18n

* refactor: edit type task status

* refactor: use date format

* refactor: value can null

* refactor: add i18n

* cleanup

* feat: productlistinput

* refactor: edit i18n

* refactor: edit redo

* refactor: add slot

* feat: task order => i18n

* refactor: task order => constant

* refactor: taskOrder => status type and index

* feat: taskOrder => ReceiveDialog

* refactor: wording

* refactor: table employee due date

* refactor: receive task i18n

* feat: trigger receive & task stat in receive page

* refactor: receive dialog task in cart  i18n

* fix: remove task-order/receive/add

* feat: receivetabletaskorder

* refactor: fetch task on receive dialog

* feat: add separate api get user task

* refactor: receive fetch (messenger)

* refactor: edit layout table

* refactor: task order i18n & constant

* refactor: task order change tab and stat (messenger and !messenger)

* fix: task order status display & receive badge color (card)

* refactor: trigger receive view

* fix: add receive task condition

* feat: total count

* feat: prepare information

* fix: i18n error task order not found

* refacor: value

* feat: select worker

* refactor: status i18n & constant

* refactor: table employee props (check box, step)

* fix: order => select ready task

* refactor: order => toggle status

* refactor: receive => receive dialog

* feat: featch value

* refactor: task status display components

* refactor: status active can is null

* feat: update status tab

* refactor: data display

* refactor: i18n & fullTaskOrder variable

* refactor: task receive view

* refactor: add type responsible user

* refactor: set group messenger

* cleanup:

* refactor: i18n / clone full task order / service => workflow type

* refactor: receive view

* refactor: show info messenger

* refactor: handle flow step

* refactor: receive view => opacity when pending

* feat: add workflow template name and step name

* feat: display workflow data on table

* feat: add template step identifier

* fix: edit does not change workflow id if changed

* feat: detect if same template and step

* refactor: handle template

* refactor: add slot name product

* refactor: map step in list product

* refactor: bind data messenger list group

* refactor: change endpoint name

* chore: add helper package

* feat: changetaskstatus

* refactor: update type

* refactor: set color btn

* refactor: add step

* refactor: add resposible institution

* feat: disabled

* refactor: map responsible institution

* fix: order view => readonly

* chore: clean

* refactor: edit url api

* refactor: edit name type

* refactor: add slots action

* refactor: add type row

* refactor: add opts of task status

* refactor: add select status

* refactor: handle btn

* refactor: add btn change task status

* refactor: edit i18n redo th

* refactor: sort status opts

* feat: receive & order banner img

* refactor: fetch status after submit

* refactor: handle create only

* refactor: task order status type Accept (messenger only)

* feat: receive messenger profile

* refactor: receive toggle status (display only)

* fix: document expansion readonly

* feat: confirmsendingbtn

* refactor: constant and task order status

* feat: receive task list count

* refactor: post or get

* refactor: define props institution group

* refactor: fetch status after submit

* refactor: handle create

* refactor: handle query

* refactor: update endpoint to support accept multiple order

* refactor: change function name

* feat: receive => functional accept task order

* feat: task status count

* feat: receive stat card count

* refactor: order messenger profile

* refactor: edit value to be task status

* refactor: handle status of type order

* refactor: use componet task status

* refactor: handle show btn saving status

* refactor: order => task status

* refactor: edit selectStatus => changeStatus

* refactor: edit @click btn confirmssending

* refactor: add i18n

* refactor: add function get template data

* refactor: add change status

* refactor: handle type receive

* feat: order => auto change tab by status

* refactor: fetch task after change status

* feat: fail remark dialog

* refactor: display step order (table employee)

* refactor: fail remark dialog

* refactor: order => open ready request dialog map selected

* refactor: task list type & change status param

* refactor: table task order, td background when selected

* refactor: order => change status param

* refactor: order => selectedEmployee variable type

* refactor: task status component => shield btn

* refactor: receive => change status

* refactor: order => step btn waiting

* fix: step btn waiting condition

* refactor: filter selectable task (Failed)

* refactor: find index condition on check

* refactor: no request list available

* refactor: fail btn no-wrap

* refactor: fail dialog readonly

* fix: reset state on open dialog

* fix: wrong title position

* refactor: hide task status drop down icon

* fix: handle check condition

* refactor: add userTask type and status

* feat: submit task order function

* refactor: table employee checkbox display condition

* refactor: main layout

* fix: task order validate i18n

* refactor: table task order add submit status

* refactor: status list

* refactor: info product => user task status

* feat: receive => submit task & step

* refactor: i18n

* feat: complete task oder function

* refactor: task status component no action props

* refactor: info messenger status

* refactor: receive and order view

* refactor: order complete view

* refactor: order => complete color and title

* refactor: calc price on table

* refactor: quotation table i18n + product image

* refactor: remove urgent checkbox

* refactor: task status color

* feat: calc summary price

* fix: data is not available

* feat: add doc view structure

* refactor: format address text

* feat: fetch document data from api

* fix: value is null

* fix: regression cannot edit package

* feat: add document view for task order

* feat: add view document button

* feat: update type add discount

* feat: readonly on cancel

* feat: add discount from relation

* refactor: add taskProduct on submit order

* refactor: order => task product discount

* refactor: order => date, task status count, view example

* refactor: receive date

* refactor: receive task status count

---------

Co-authored-by: puriphatt <puriphat@frappet.com>
Co-authored-by: nwpptrs <jay02499@gmail.com>
Co-authored-by: Thanaphon Frappet <thanaphon@frappet.com>
Co-authored-by: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Co-authored-by: oat_dev <nattapon@frappet.com>
2024-12-25 11:59:49 +07:00

1088 lines
32 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,
CancelButton,
MainButton,
UndoButton,
EditButton,
} from 'src/components/button';
import FormGroupHead from 'src/pages/08_request-list/FormGroupHead.vue';
import FailRemarkDialog from '../receive_view/FailRemarkDialog.vue';
import { taskStatusOpts } from '../constants';
import SelectReadyRequestWork from '../SelectReadyRequestWork.vue';
import { dialogWarningClose } from 'stores/utils';
import useOptionStore from 'src/stores/options';
import { useTaskOrderForm } 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 optionStore = useOptionStore();
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;
}[]
>([]);
const refProductExpansion = ref<InstanceType<typeof ProductExpansion>>();
const taskProduct = ref<{ productId: string; discount?: number }[]>([]);
const view = ref<TaskOrderStatus>(TaskOrderStatus.Pending);
const taskStatusList = ref<
{
code?: string;
requestWorkId: string;
step: number;
failedComment?: string;
failedType?: string;
}[]
>([]);
const pageState = reactive({
productDialog: false,
failedDialog: false,
});
const formDocument = ref();
const responsibleInstitution = ref<string[]>([]);
const summaryPrice = computed(() => getPrice(taskListGroup.value));
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, i) => {
const pricePerUnit =
refProductExpansion.value?.calcPricePerUnit(c.product) ?? 0;
const price = precisionRound(pricePerUnit * c.list.length);
const disc =
taskProduct.value.find((v) => v.productId === c.product.id)?.discount ||
0;
const vat =
precisionRound(
(pricePerUnit * (disc ? c.list.length : 1) - disc) *
(config.value?.vat || 0.07),
) * (!disc ? c.list.length : 1);
a.totalPrice = precisionRound(a.totalPrice + price);
a.totalDiscount = precisionRound(a.totalDiscount + Number(disc));
a.vat = c.product.calcVat ? precisionRound(a.vat + vat) : a.vat;
a.vatExcluded = c.product.calcVat
? a.vatExcluded
: precisionRound(a.vat + vat);
a.finalPrice = precisionRound(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 (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);
router.push({
name: 'TaskOrderView',
params: { id: currentFormData.value.id },
});
}
fetchStatus();
state.value.mode = 'info';
}
function openProductDialog() {
pageState.productDialog = true;
}
onMounted(async () => {
initTheme();
initLang();
await configStore.getConfig();
let currentId = route.params['id'] === 'add' ? undefined : route.params['id'];
if (route.path === '/task-order/order/add') {
state.value.mode = 'create';
}
if (currentId !== undefined && typeof currentId === 'string') {
await taskOrderFormStore.assignFormData(currentId);
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 rse = await taskOrderStore.headAttachment({
parentId: taskId,
fileId: v,
});
let contentLength = 0;
if (rse) contentLength = Number(rse['content-length']);
return {
name: v,
progress: 1,
loaded: contentLength,
total: contentLength,
url: `/task/${taskId}/attachment/${v}`,
};
}),
);
}
async function remove(taskId: string, n: string) {
dialogWarningClose(t, {
message: t('dialog.message.confirmDelete'),
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);
}
return await Promise.all(promises);
}
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;
},
index: number,
) {
taskStatusList.value = [
{
code: `${row.productService.product.code}-${row.request.code}`,
requestWorkId: row.id || '',
step: row._template?.step || 0,
failedComment: fullTaskOrder.value?.taskList[index].failedComment || '',
failedType: fullTaskOrder.value?.taskList[index].failedType || '',
},
];
pageState.failedDialog = true;
}
function taskStatusCount(index: number, id: string) {
const task = fullTaskOrder.value?.taskList.filter(
(t) => t.requestWorkStep.requestWork.productService.productId === id,
);
if (index === 1) {
return task?.filter((t) => t.taskStatus === TaskStatus.InProgress).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.Canceled ||
t.taskStatus === TaskStatus.Redo ||
t.taskStatus === TaskStatus.Failed,
).length;
}
}
watch([currentFormData.value.taskStatus], () => {
fetchStatus();
});
function viewDocument(id: string) {
window.open(`/task-order/doc/${id}`, '_blank');
}
</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>
<q-form
ref="formDocument"
@submit="
() => {
submitForm();
}
"
>
<DocumentExpansion
:readonly="state.mode !== 'create'"
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"
: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
v-if="
view === TaskOrderStatus.Pending ||
view === TaskOrderStatus.Complete
"
:readonly="!['create', 'edit'].includes(state.mode || '')"
v-model:file-data="fileData"
:transform-url="
async (url: string) => {
const result = await api.get<string>(url);
return result.data;
}
"
@fetch-file-list="
() => {
if (!currentFormData.id) return;
getFileList(currentFormData.id);
}
"
@upload="
async (f) => {
if (!currentFormData.id) return;
fileList = f;
await uploadFile(currentFormData.id, f);
}
"
@remove="
async (n) => {
if (!currentFormData.id) return;
await remove(currentFormData.id, n);
}
"
/>
<!-- TODO: blind remark, urgent -->
<RemarkExpansion
v-if="
view === TaskOrderStatus.Pending ||
view === TaskOrderStatus.Complete
"
:readonly="false"
/>
<template
v-if="
view === TaskOrderStatus.InProgress ||
view === TaskOrderStatus.Validate
"
>
<InfoMessengerExpansion
v-for="(v, i) in messengerListGroup"
:key="i"
: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[i]?.userTaskStatus ||
UserTaskStatus.Pending
"
>
<template #product>
<FormGroupHead>
{{ $t('menu.product') }}
</FormGroupHead>
<div
v-for="{ product, list } 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>
<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>
<span class="q-ml-auto row items-center q-gutter-x-sm">
<div
v-for="v in 3"
:key="v"
class="rounded q-px-sm row items-center"
style="background: hsl(var(--text-mute) / 0.1)"
:style="`color: hsl(var(--${
v === 1
? 'warning'
: v === 2
? 'positive'
: 'negative'
}-bg))`"
>
<q-icon
name="mdi-account-group-outline"
size="xs"
class="q-pr-sm"
/>
{{ taskStatusCount(v, product.id) }}
</div>
</span>
</template>
<div>
<div class="full-width q-pa-sm">
<TableEmployee step-on :rows="list">
<template #append="{ props: subProps }">
<TaskStatusComponent
:no-action="view !== TaskOrderStatus.Validate"
type="order"
:readonly="
fullTaskOrder?.taskList[i].taskStatus ===
TaskStatus.Pending
"
:status="subProps.row.taskStatus"
@click-failed="
() => {
openFailedDialog(
subProps.row,
subProps.rowIndex,
);
}
"
@change-status="
(status) =>
taskOrderFormStore.changeStatus(
[
{
requestWorkId: subProps.row.id || '',
step: subProps.row._template?.step || 0,
},
],
status,
() => {
subProps.row.taskStatus = status;
if (fullTaskOrder) {
const target =
fullTaskOrder.taskList.find(
(t) =>
t.requestWorkId ===
subProps.row.id,
);
if (target) {
target.taskStatus = status;
}
}
},
)
"
/>
</template>
</TableEmployee>
</div>
</div>
</q-expansion-item>
<FailRemarkDialog
readonly
:fail-task-option="list"
v-model:set-task-status-list="taskStatusList"
v-model:open="pageState.failedDialog"
@close="
() => {
pageState.failedDialog = false;
taskStatusList = [];
}
"
/>
</div>
</template>
</InfoMessengerExpansion>
</template>
</div>
</section>
</article>
<!-- SEC: footer -->
<footer
class="surface-1 q-pa-md full-width"
v-if="
view === TaskOrderStatus.Validate ||
view === TaskOrderStatus.Complete ||
state.mode === 'create'
"
>
<nav class="row justify-end">
<MainButton
v-if="currentFormData.id && view === TaskOrderStatus.Complete"
outlined
icon="mdi-play-box-outline"
color="207 96% 32%"
@click="viewDocument(currentFormData.id)"
>
{{ $t('general.view', { msg: $t('general.example') }) }}
</MainButton>
<div class="row" style="gap: var(--size-2)">
<template v-if="state.mode === 'create'">
<!-- <UndoButton outlined @click="undo()" v-if="state.mode === 'edit'" />
<CancelButton
@click="closeTab()"
v-if="state.mode === 'info'"
outlined
/> -->
<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.Complete"
:disabled="
!fullTaskOrder?.taskList.every(
(t) =>
t.taskStatus === TaskStatus.Complete ||
t.taskStatus === TaskStatus.Redo ||
t.taskStatus === TaskStatus.Canceled,
) || fullTaskOrder.taskOrderStatus === TaskOrderStatus.Complete
"
@click="
dialogWarningClose(t, {
message: t('dialog.message.confirmValidate'),
action: async () => {
await completeValidate();
},
cancel: () => {},
})
"
:label="$t('taskOrder.confirmValidate')"
icon="mdi-check"
solid
></SaveButton>
</div>
</nav>
</footer>
</div>
<!-- SEC: Dialog -->
<SelectReadyRequestWork
v-model:open="pageState.productDialog"
v-model:task-list="currentFormData.taskList"
@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%);
}
}
</style>