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>
This commit is contained in:
Methapon Metanipat 2024-12-25 11:59:49 +07:00 committed by GitHub
parent cd0831bac1
commit 9eff614dbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 6981 additions and 361 deletions

View file

@ -0,0 +1,299 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Lang } from 'src/utils/ui';
import { TaskStatus } from 'src/stores/task-order/types';
import { RequestData, RequestWork } from 'src/stores/request-list';
import useOptionStore from 'src/stores/options';
import { CancelButton, SaveButton } from 'src/components/button';
import SelectInput from 'src/components/shared/SelectInput.vue';
import DialogFormContainer from 'src/components/dialog/DialogFormContainer.vue';
import DialogHeader from 'src/components/dialog/DialogHeader.vue';
const emit = defineEmits<{
(e: 'submit'): void;
(e: 'close'): void;
}>();
const open = defineModel<boolean>('open', { default: false });
const taskStatusList = defineModel<
{
code?: string;
requestWorkId: string;
step: number;
failedComment?: string;
failedType?: string;
}[]
>('setTaskStatusList', { default: [] });
const failedType = ref<string>('');
const failedComment = ref<string>('');
const props = defineProps<{
readonly?: boolean;
failTaskOption: (RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
} | null;
taskStatus?: TaskStatus;
})[];
}>();
function onDialogOpen() {
failedType.value = taskStatusList.value[0].failedType || '';
failedComment.value = taskStatusList.value[0].failedComment || '';
}
function onDialogClose() {
emit('close');
}
function getEmployeeName(
record?: RequestData,
opts?: {
locale?: string;
},
) {
if (!record) return;
const employee = record.employee;
return (
{
[Lang.English]: `${useOptionStore().mapOption(employee.namePrefix)} ${employee.firstNameEN} ${employee.lastNameEN}`,
[Lang.Thai]: `${useOptionStore().mapOption(employee.namePrefix)} ${employee.firstName} ${employee.lastName}`,
}[opts?.locale || Lang.English] || '-'
);
}
function submit() {
taskStatusList.value.forEach((v) => {
v.failedComment = failedComment.value;
v.failedType = failedType.value;
});
emit('submit');
open.value = false;
}
function tooltip(id: string) {
const currTask = props.failTaskOption.find((t) => t.id === id);
return currTask;
}
</script>
<template>
<DialogFormContainer
v-on:open="onDialogOpen"
v-on:close="onDialogClose"
v-model="open"
height="330px"
width="60vw"
>
<template #header>
<DialogHeader :title="$t('general.remark')" />
</template>
<main class="q-py-sm q-px-md scroll">
<q-select
:readonly
dense
outlined
multiple
use-chips
:model-value="taskStatusList"
:label="$t('taskOrder.failTaskOrderCode')"
hide-dropdown-icon
class="q-mb-sm"
:options="
failTaskOption.filter(
(v) => v.taskStatus !== TaskStatus.Success && TaskStatus.Complete,
)
"
@remove="(v) => taskStatusList.splice(v.index, 1)"
@add="
(v) => {
const index = taskStatusList.findIndex(
(item) => item.requestWorkId === v.value.id,
);
if (index !== -1) {
taskStatusList.splice(index, 1);
return;
}
taskStatusList.push({
code: `${v.value.productService.product.code}-${v.value.request.code}`,
requestWorkId: v.value.id,
step: v.value._template.step,
});
}
"
>
<template #selected-item="scope">
<q-chip
dense
:removable="!readonly"
@remove="scope.removeAtIndex(scope.index)"
>
{{ taskStatusList[scope.index].code }}
<q-tooltip :delay="300">
<section class="column">
<div>
{{ $t('productService.title') }}
</div>
<div class="text-caption">
{{
tooltip(scope.opt.requestWorkId)?.productService.product
.name
}}
({{
tooltip(scope.opt.requestWorkId)?.productService.product
.code
}})
</div>
<div class="q-pt-xs">
{{ $t('quotation.employeeName') }}
</div>
<div class="text-caption">
{{
getEmployeeName(tooltip(scope.opt.requestWorkId)?.request, {
locale: $i18n.locale,
})
}}
({{ tooltip(scope.opt.requestWorkId)?.request.code }})
</div>
</section>
</q-tooltip>
</q-chip>
</template>
<template #option="scope">
<q-item
clickable
v-bind="scope.itemProps"
class="row items-start col-12 no-padding"
:active="
scope.opt.id === taskStatusList[scope.index]?.requestWorkId
"
active-class="text-brand1"
>
<div class="q-ma-sm">
<q-icon
name="mdi-shopping-outline"
style="color: var(--teal-10)"
/>
</div>
<div class="q-mt-sm">
<div>
<span>
<span style="font-weight: 600">
{{ $t('productService.title') }}:
</span>
{{ scope.opt.productService.product.name }}
({{ scope.opt.productService.product.code }})
</span>
</div>
<div class="text-caption app-text-muted-2 q-mb-xs">
{{ $t('quotation.employeeName') }}:
{{
getEmployeeName(scope.opt.request, { locale: $i18n.locale })
}}
({{ scope.opt.request.code }})
</div>
</div>
<!-- <section class="column">
<div class="text-caption app-text-muted">
{{ $t('productService.title') }}
</div>
<div>
{{ scope.opt.productService.product.name }} ({{
scope.opt.productService.product.code
}})
</div>
<div class="text-caption app-text-muted q-pt-xs">
{{ $t('quotation.employeeName') }}
</div>
<div>
{{
getEmployeeName(scope.opt.request, { locale: $i18n.locale })
}}
({{ scope.opt.request.code }})
</div>
</section> -->
</q-item>
<q-separator class="q-mx-sm" />
</template>
</q-select>
<SelectInput
:readonly
:option="[
{
label: $t('taskOrder.documentSubmitFailed'),
value: 'documentSubmitFailed',
},
{
label: $t('taskOrder.taskNotFullyCompleted'),
value: 'taskNotFullyCompleted',
},
{
label: $t('general.other'),
value: 'other',
},
]"
:label="$t('taskOrder.describeIssue')"
v-model="failedType"
>
<template #option="{ opt, scope }">
<q-item
class="items-center"
v-bind="scope.itemProps"
:class="{ 'bordered-t': opt.value === 'other' }"
>
{{ opt.label }}
</q-item>
</template>
</SelectInput>
<div v-if="failedType === 'other'" class="q-mt-sm rounded">
<q-editor
dense
flat
v-model="failedComment"
min-height="5rem"
class="q-mt-sm q-mb-xs"
:toolbar="[['left', 'center', 'justify']]"
:toolbar-color="$q.dark.isActive ? 'white' : ''"
:toolbar-toggle-color="'primary'"
style="
cursor: auto;
color: var(--foreground);
border-color: var(--surface-3);
"
:style="`width: ${$q.screen.gt.xs ? '100%' : '63vw'}`"
/>
</div>
</main>
<template #footer v-if="!readonly">
<CancelButton class="q-ml-auto" outlined @click="$emit('close')" />
<SaveButton class="q-ml-sm" solid @click="submit" />
</template>
</DialogFormContainer>
</template>
<style scoped>
.q-editor__toolbar {
border-bottom: none !important;
}
:deep(.q-editor__toolbar.row.no-wrap.scroll-x) {
background-color: var(--surface-2) !important;
}
:deep(.q-editor__toolbar) {
border-color: var(--surface-3) !important;
}
</style>

View file

@ -0,0 +1,774 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useRoute } from 'vue-router';
import { computed, onMounted, ref, watch } from 'vue';
import { getUserId } from 'src/services/keycloak';
// NOTE: Import Components
import { SaveButton } from 'src/components/button';
import { StateButton } from 'components/button';
import InfoMessengerExpansion from '../expansion/receive/InfoMessengerExpansion.vue';
import InfoProductExpansion from '../expansion/receive/InfoProductExpansion.vue';
import FormGroupHead from 'src/pages/08_request-list/FormGroupHead.vue';
import TableEmployee from '../TableEmployee.vue';
import TaskStatusComponent from '../TaskStatusComponent.vue';
import FailRemarkDialog from './FailRemarkDialog.vue';
// NOTE: Import Types and Store
import { dateFormatJS, dateFormat } from 'src/utils/datetime';
import { useTaskOrderForm } from '../form';
import { initLang, initTheme, Lang } from 'src/utils/ui';
import { RequestWork } from 'src/stores/request-list';
import { baseUrl, dialogWarningClose } from 'src/stores/utils';
import {
TaskOrder,
TaskOrderStatus,
TaskStatus,
UserTaskStatus,
} from 'src/stores/task-order/types';
import useOptionStore from 'src/stores/options';
import { useTaskOrderStore } from 'src/stores/task-order';
const route = useRoute();
const taskOrderFormStore = useTaskOrderForm();
const { currentFormData, state, fullTaskOrder } =
storeToRefs(taskOrderFormStore);
const statusTabForm = ref<
{
title: string;
status: 'done' | 'doing' | 'waiting';
handler: () => void;
active?: () => boolean;
}[]
>([
{
title: 'receive',
status: 'doing',
active: () => view.value === UserTaskStatus.Pending,
handler: () => (
(view.value = UserTaskStatus.Pending), console.log(view.value)
),
},
{
title: 'sendTaskOrder',
status: 'waiting',
active: () => view.value === UserTaskStatus.Submit,
handler: () => (
(view.value = UserTaskStatus.Submit), console.log(view.value)
),
},
]);
const failedDialog = ref(false);
const taskStatusRecords = ref<
{
requestWorkId: string;
step: number;
failedComment?: string;
failedType?: string;
code?: string;
}[]
>([]);
const selectedEmployee = ref<
(RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
} | null;
})[][]
>([]);
const view = ref<UserTaskStatus>(UserTaskStatus.Pending);
const TAB_STATUS = ['Pending', 'Accept', 'Submit'];
function getStatus(
status: UserTaskStatus,
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() {
if (!fullTaskOrder.value) return;
statusTabForm.value = [
{
title: 'receive',
status: getStatus(
fullTaskOrder.value?.userTask[0]?.userTaskStatus ||
UserTaskStatus.Pending,
2,
-1,
),
active: () => view.value === UserTaskStatus.Pending,
handler: () => (
(view.value = UserTaskStatus.Pending), console.log(view.value)
),
},
{
title: 'sendTaskOrder',
status: getStatus(
fullTaskOrder.value?.userTask[0]?.userTaskStatus ||
UserTaskStatus.Pending,
2,
2,
),
active: () => view.value === UserTaskStatus.Submit,
handler: () => (
(view.value = UserTaskStatus.Submit), console.log(view.value)
),
},
];
}
function getTemplateData(
requestWork: RequestWork,
targetStep: number,
): {
id: string;
templateName: string;
templateStepName: string;
step: number;
} | 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,
};
}
let taskListGroup = computed(() => {
const cacheData = currentFormData.value.taskList.reduce<
{
product: RequestWork['productService']['product'];
list: (RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
} | null;
taskStatus?: TaskStatus;
})[];
}[]
>((acc, curr) => {
const taskStatus = curr.taskStatus;
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),
taskStatus: taskStatus ?? TaskStatus.Pending,
});
if (exist) {
exist.list.push(task.requestWork);
} else {
acc.push({
product: task.requestWork.productService.product,
list: [record],
});
if (selectedEmployee.value.length < acc.length) {
selectedEmployee.value.push([]);
}
}
}
return acc;
}, []);
return cacheData;
});
// NOTE: Function
async function sendTask() {
if (!fullTaskOrder.value) return;
if (
!fullTaskOrder.value.taskList.every(
(t) =>
t.taskStatus === TaskStatus.Success ||
t.taskStatus === TaskStatus.Failed,
)
)
return;
await useTaskOrderStore().submitTaskOrder(fullTaskOrder.value.id);
await taskOrderFormStore.assignFormData(
fullTaskOrder.value.id,
'info',
getUserId(),
);
await fetchStatus();
view.value = UserTaskStatus.Submit;
}
function getMessengerName(
record: TaskOrder,
opts?: {
gender?: boolean;
url?: boolean;
locale?: string;
},
) {
const user = record.taskList[0].requestWorkStep.responsibleUser;
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 handleChangeStatus(
records: {
data: (RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
} | null;
})[];
status: TaskStatus;
},
index: 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[index] = [];
taskOrderFormStore.assignFormData(
currentFormData.value.id,
'info',
getUserId(),
);
});
}
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;
}
}
onMounted(async () => {
initTheme();
initLang();
const currentId = route.params['id'];
state.value.mode = 'info';
if (currentId !== undefined && typeof currentId === 'string') {
await taskOrderFormStore.assignFormData(
currentId,
state.value.mode,
getUserId(),
);
await fetchStatus();
}
});
watch([currentFormData.value.taskStatus], () => {
fetchStatus();
});
</script>
<template>
<div v-if="fullTaskOrder" class="column surface-0 fullscreen">
<div class="color-bar" :class="{ dark: $q.dark.isActive }">
<div class="pink-segment"></div>
<div class="light-pink-segment"></div>
<div class="gray-segment"></div>
</div>
<!-- SEC: Header -->
<header
class="row q-px-md q-py-sm items-center full 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">
{{ $t('taskOrder.receive') }}
<!-- {{ 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}`)"
:statusActive="i.active?.()"
:statusDone="i.status === 'done'"
:status-waiting="i.status === 'waiting'"
@click="i.handler()"
/>
</nav>
<article
v-if="
fullTaskOrder.taskOrderStatus !== TaskOrderStatus.Pending &&
view !== UserTaskStatus.Submit
"
class="row items-center surface-1 q-pa-md rounded gradient-stat"
>
<span
class="row col rounded q-px-sm q-py-md info"
style="border: 1px solid hsl(var(--info-bg))"
>
{{ $t('taskOrder.allProduct') }}
<span class="q-ml-auto">{{ fullTaskOrder.taskList.length }}</span>
</span>
<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.alreadySentTask') }}
<span class="q-ml-auto">
{{
fullTaskOrder.taskList.filter(
(t) =>
t.taskStatus === TaskStatus.Complete ||
t.taskStatus === TaskStatus.Success ||
t.taskStatus === TaskStatus.Validate ||
t.taskStatus === TaskStatus.Redo ||
t.taskStatus === TaskStatus.Failed,
).length
}}
</span>
</span>
<span
class="row col rounded q-px-sm q-py-md warning"
style="border: 1px solid hsl(var(--warning-bg))"
>
{{ $t('taskOrder.status.Pending') }}
<span class="q-ml-auto">
{{
fullTaskOrder.taskList.filter(
(t) => t.taskStatus === TaskStatus.InProgress,
).length
}}
</span>
</span>
</article>
<InfoMessengerExpansion
:gender="getMessengerName(fullTaskOrder, { gender: true })"
:contact-url="getMessengerName(fullTaskOrder, { url: true })"
:contact-name="
getMessengerName(fullTaskOrder, { locale: $i18n.locale })
"
:contact-tel="
fullTaskOrder.taskList[0].requestWorkStep.responsibleUser
?.telephoneNo
"
:email="
fullTaskOrder.taskList[0].requestWorkStep.responsibleUser?.email
"
:status="
fullTaskOrder.userTask[0]?.userTaskStatus ||
UserTaskStatus.Pending
"
/>
<InfoProductExpansion
:code="currentFormData.code"
:task-name="currentFormData.taskName"
:issue-branch="currentFormData.registeredBranchId"
:agencies="
$i18n.locale === 'eng'
? fullTaskOrder.institution?.nameEN
: fullTaskOrder.institution?.name
"
:made-by="
$i18n.locale === 'eng'
? `${fullTaskOrder?.createdBy.firstNameEN} ${fullTaskOrder?.createdBy.lastNameEN}`
: `${fullTaskOrder?.createdBy.firstName} ${fullTaskOrder?.createdBy.lastName}`
"
:contact-tel="currentFormData.contactTel"
:contact-name="currentFormData.contactName"
:userTaskStatus="
fullTaskOrder?.userTask[0]?.userTaskStatus ||
UserTaskStatus.Pending
"
>
<div
v-for="({ product, list }, i) in taskListGroup"
:key="product.id"
class="bordered-b"
>
<q-expansion-item
dense
class="overflow-hidden"
switch-toggle-side
style="border-radius: var(--radius-2)"
expand-icon="mdi-chevron-down-circle"
header-class="q-py-sm text-medium text-body items-center rounded q-mx-md q-my-sm"
>
<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
v-if="
fullTaskOrder.taskOrderStatus === TaskOrderStatus.Pending
"
class="q-ml-auto"
>
<div
class="rounded q-px-sm row items-center"
style="background: hsl(var(--text-mute) / 0.15)"
>
<q-icon
name="mdi-account-group-outline"
size="xs"
class="q-pr-sm"
/>
{{ list.length }}
</div>
</span>
<span v-else 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>
<FormGroupHead>
{{ $t('quotation.employeeList') }}
</FormGroupHead>
<div class="q-pa-md full-width">
<TableEmployee
:checkbox-on="
fullTaskOrder.taskOrderStatus !==
TaskOrderStatus.Pending &&
fullTaskOrder.userTask.every(
(v) => v.userTaskStatus !== UserTaskStatus.Submit,
)
"
:check-all="
fullTaskOrder.taskOrderStatus !==
TaskOrderStatus.Pending &&
fullTaskOrder.userTask.every(
(v) => v.userTaskStatus !== UserTaskStatus.Submit,
)
"
step-on
:rows="list"
@change-all-status="(v) => handleChangeStatus(v, i)"
v-model:selected-employee="selectedEmployee[i]"
>
<template #append="{ props: subProps }">
<TaskStatusComponent
type="receive"
:readonly="
fullTaskOrder.taskOrderStatus ===
TaskOrderStatus.Pending
"
:status="subProps.row.taskStatus"
@click-failed="
() => {
taskStatusRecords = [
{
code: `${subProps.row.productService.product.code}-${subProps.row.request.code}`,
requestWorkId: subProps.row.id || '',
step: subProps.row._template?.step || 0,
failedComment:
fullTaskOrder?.taskList[subProps.rowIndex]
.failedComment || '',
failedType:
fullTaskOrder?.taskList[subProps.rowIndex]
.failedType || '',
},
];
failedDialog = true;
}
"
@change-status="
(status) => {
handleChangeStatus(
{
data: [subProps.row],
status: status,
},
i,
);
}
"
/>
</template>
</TableEmployee>
</div>
</div>
</q-expansion-item>
<FailRemarkDialog
:fail-task-option="list"
v-model:open="failedDialog"
v-model:set-task-status-list="taskStatusRecords"
@submit="
async () => {
await taskOrderFormStore.changeStatus(
taskStatusRecords,
TaskStatus.Failed,
() => {
if (!currentFormData.id) return;
taskOrderFormStore.assignFormData(
currentFormData.id,
'info',
getUserId(),
);
},
);
selectedEmployee = [];
}
"
@close="
() => {
failedDialog = false;
taskStatusRecords = [];
}
"
/>
</div>
</InfoProductExpansion>
</div>
</section>
</article>
<!-- SEC: footer -->
<footer
v-if="fullTaskOrder.taskOrderStatus !== TaskOrderStatus.Pending"
class="surface-1 q-pa-md full-width"
>
<nav class="row justify-end">
<SaveButton
:disabled="
!fullTaskOrder.taskList.every(
(t) =>
t.taskStatus === TaskStatus.Success ||
t.taskStatus === TaskStatus.Failed,
)
"
@click="
dialogWarningClose($t, {
message: $t('dialog.message.confirmSending'),
action: async () => {
await sendTask();
},
cancel: () => {},
})
"
:label="$t('taskOrder.sentTask')"
icon="mdi-check"
solid
/>
</nav>
</footer>
</div>
</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;
}
.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;
}
.gray-segment {
background-color: #ccc;
flex-grow: 1;
}
.pink-segment,
.light-pink-segment,
.gray-segment {
transform: skewX(-60deg);
}
.gradient-stat {
& .info {
background-image: linear-gradient(
to right,
hsl(var(--info-bg) / 0.15),
var(--surface-1)
);
}
& .positive {
background-image: linear-gradient(
to right,
hsl(var(--positive-bg) / 0.15),
var(--surface-1)
);
}
& .warning {
background-image: linear-gradient(
to right,
hsl(var(--warning-bg) / 0.15),
var(--surface-1)
);
}
}
.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>

View file

@ -0,0 +1,250 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import DialogFormContainer from 'src/components/dialog/DialogFormContainer.vue';
import DialogHeader from 'src/components/dialog/DialogHeader.vue';
import TableTaskOrder from '../TableTaskOrder.vue';
import FormGroupHead from 'src/pages/08_request-list/FormGroupHead.vue';
import TableEmployee from '../TableEmployee.vue';
import { SaveButton, CancelButton } from 'src/components/button';
import TaskStatusComponent from '../TaskStatusComponent.vue';
import { baseUrl } from 'src/stores/utils';
import { TaskOrder, UserTaskStatus } from 'src/stores/task-order/types';
import { useTaskOrderStore } from 'src/stores/task-order';
import { RequestWork } from 'src/stores/request-list';
const taskOrderStore = useTaskOrderStore();
withDefaults(
defineProps<{
scan: boolean;
}>(),
{
scan: false,
},
);
const emit = defineEmits<{
(e: 'submit', data: TaskOrder[]): void;
}>();
const open = defineModel<boolean>('open', { default: false });
const rows = ref<TaskOrder[]>([]);
const selectedTask = ref<TaskOrder[]>([]);
const currTaskOrder = ref<(TaskOrder | null)[]>([]);
let taskListGroup = computed(() => {
let acc: {
taskId: string;
list: {
product: RequestWork['productService']['product'];
list: RequestWork[];
}[];
}[] = [];
let list = [];
currTaskOrder.value.forEach((v, i) => {
if (!v) {
acc[i] = {
taskId: '',
list: [],
};
return;
}
list = v.taskList.reduce<
{
product: RequestWork['productService']['product'];
list: RequestWork[];
}[]
>((acc2, curr2) => {
const task = curr2.requestWorkStep;
if (!task) return acc2;
if (task.requestWork) {
let exist = acc2.find(
(item) =>
task.requestWork.productService.productId == item.product.id,
);
if (exist) {
exist.list.push(task.requestWork);
} else {
acc2.push({
product: task.requestWork.productService.product,
list: [task.requestWork],
});
}
}
return acc2;
}, []);
acc[i] = {
taskId: v.id,
list: list,
};
});
return acc;
});
async function fetchTaskOrderList() {
const res = await taskOrderStore.getUserTaskOrderList({
pageSize: 999,
userTaskStatus: UserTaskStatus.Pending,
});
if (res) {
rows.value = res.result;
}
}
async function onDialogOpen() {
await fetchTaskOrderList();
currTaskOrder.value = Array(rows.value.length).fill(null);
}
function submit() {
emit('submit', selectedTask.value);
clearState();
}
function close() {
open.value = false;
clearState();
}
function clearState() {
selectedTask.value = [];
currTaskOrder.value = [];
}
async function handleSubRow(index: number, data: TaskOrder) {
const ret = await taskOrderStore.getTaskOrderById(data.id);
if (ret) {
currTaskOrder.value[index] = ret;
}
}
</script>
<template>
<DialogFormContainer
v-model="open"
v-on:open="onDialogOpen"
v-on:close="close"
>
<template #header>
<DialogHeader
:title="$t(`taskOrder.${scan ? 'receiveScan' : 'receiveCustom'}`)"
/>
</template>
<section class="q-pa-sm full-width scroll">
<TableTaskOrder
receive
:rows="rows"
:selection="'multiple'"
v-model:selected-task="selectedTask"
@click-sub-row="handleSubRow"
>
<template #subRow="{ props }">
<FormGroupHead>
{{ $t('productService.title') }}
</FormGroupHead>
<div
v-if="taskListGroup?.[props.rowIndex]?.list.length === 0"
class="app-text-muted q-pt-xs"
>
{{ $t('productService.product.noProduct') }}
</div>
<template v-if="taskListGroup">
<div
v-for="{ product, list } in taskListGroup?.[props.rowIndex]?.list"
:key="product.id"
:class="{ 'bordered-b': taskListGroup.length > 1 }"
>
<q-expansion-item
dense
class="overflow-hidden surface-1"
switch-toggle-side
style="border-radius: var(--radius-2)"
expand-icon="mdi-chevron-down-circle"
header-class="q-py-sm text-medium text-body items-center q-mb-sm surface-1"
header-style="margin: 0"
>
<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">
<div
class="rounded q-px-sm row items-center"
style="background: hsl(var(--text-mute) / 0.15)"
>
<q-icon
name="mdi-account-group-outline"
size="xs"
class="q-pr-sm"
/>
{{ list.length }}
</div>
</span>
</template>
<div>
<FormGroupHead>
{{ $t('quotation.employeeList') }}
</FormGroupHead>
<div class="q-pa-md full-width surface-1">
<TableEmployee step-on :rows="list">
<template #append="{ props: subProps }">
<TaskStatusComponent
readonly
:status="
currTaskOrder[props.rowIndex]?.taskList[
subProps.rowIndex
].taskStatus
"
/>
</template>
</TableEmployee>
</div>
</div>
</q-expansion-item>
</div>
</template>
</template>
</TableTaskOrder>
</section>
<template #footer>
<CancelButton class="q-ml-auto" outlined @click="close" />
<SaveButton
:label="$t('taskOrder.receiveTask')"
class="q-ml-sm"
icon="mdi-check"
solid
@click="submit"
/>
</template>
</DialogFormContainer>
</template>
<style scoped></style>