1401 lines
42 KiB
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>
|