* 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>
1088 lines
32 KiB
Vue
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>
|