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:
parent
cd0831bac1
commit
9eff614dbd
56 changed files with 6981 additions and 361 deletions
|
|
@ -54,7 +54,8 @@
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
"eslint-plugin-vue": "^9.27.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4",
|
||||||
|
"vue-component-type-helpers": "^2.1.10"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^24 || ^22 || ^20 || ^18",
|
"node": "^24 || ^22 || ^20 || ^18",
|
||||||
|
|
|
||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
|
@ -123,6 +123,9 @@ importers:
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.5.4
|
specifier: ^5.5.4
|
||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
|
vue-component-type-helpers:
|
||||||
|
specifier: ^2.1.10
|
||||||
|
version: 2.1.10
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
|
@ -3516,6 +3519,9 @@ packages:
|
||||||
vm-browserify@1.1.2:
|
vm-browserify@1.1.2:
|
||||||
resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==}
|
resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==}
|
||||||
|
|
||||||
|
vue-component-type-helpers@2.1.10:
|
||||||
|
resolution: {integrity: sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==}
|
||||||
|
|
||||||
vue-demi@0.14.10:
|
vue-demi@0.14.10:
|
||||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
@ -7445,6 +7451,8 @@ snapshots:
|
||||||
|
|
||||||
vm-browserify@1.1.2: {}
|
vm-browserify@1.1.2: {}
|
||||||
|
|
||||||
|
vue-component-type-helpers@2.1.10: {}
|
||||||
|
|
||||||
vue-demi@0.14.10(vue@3.4.38(typescript@5.5.4)):
|
vue-demi@0.14.10(vue@3.4.38(typescript@5.5.4)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.4.38(typescript@5.5.4)
|
vue: 3.4.38(typescript@5.5.4)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { QTableProps, QTableSlots } from 'quasar';
|
import { QTableProps, QTableSlots } from 'quasar';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { baseUrl } from 'stores/utils';
|
||||||
|
|
||||||
import WorkerItem from './WorkerItem.vue';
|
import WorkerItem from './WorkerItem.vue';
|
||||||
import DeleteButton from '../button/DeleteButton.vue';
|
import DeleteButton from '../button/DeleteButton.vue';
|
||||||
|
|
@ -81,19 +82,19 @@ const columns = [
|
||||||
{
|
{
|
||||||
name: 'code',
|
name: 'code',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'general.code',
|
label: 'productService.product.code',
|
||||||
field: (v) => v.product.code,
|
field: (v) => v.product.code,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
label: 'productService.service.list',
|
label: 'quotation.productList',
|
||||||
field: (v) => v.product.name,
|
field: (v) => v.product.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'amount',
|
name: 'amount',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
label: 'general.amount',
|
label: 'taskOrder.amountOfEmployee',
|
||||||
field: 'amount',
|
field: 'amount',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -115,15 +116,15 @@ const columns = [
|
||||||
field: 'priceBeforeVat',
|
field: 'priceBeforeVat',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tax',
|
name: 'vat',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
label: 'general.vat',
|
label: 'general.vat',
|
||||||
field: 'tax',
|
field: 'vat',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'sumPrice',
|
name: 'sumPrice',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
label: 'quotation.sumPrice',
|
label: 'quotation.totalPriceBaht',
|
||||||
field: 'sumPrice',
|
field: 'sumPrice',
|
||||||
},
|
},
|
||||||
] satisfies QTableProps['columns'];
|
] satisfies QTableProps['columns'];
|
||||||
|
|
@ -335,14 +336,22 @@ watch(
|
||||||
"
|
"
|
||||||
></q-input>
|
></q-input>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td>{{ props.row.product.code }}</q-td>
|
<q-td class="text-center">{{ props.row.product.code }}</q-td>
|
||||||
<q-td style="width: 100%">
|
<q-td style="width: 100%">
|
||||||
<q-avatar class="q-mr-sm" size="md">
|
<q-avatar class="q-mr-sm" size="md">
|
||||||
<q-icon
|
<q-img
|
||||||
class="full-width full-height"
|
class="text-center"
|
||||||
name="mdi-shopping-outline"
|
:ratio="1"
|
||||||
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
|
:src="`${baseUrl}/product/${props.row.product.id}/image/${props.row.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>
|
</q-avatar>
|
||||||
{{ props.row.product.name }}
|
{{ props.row.product.name }}
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
@ -366,7 +375,6 @@ watch(
|
||||||
/>
|
/>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td align="right">
|
<q-td align="right">
|
||||||
<!-- TODO: -->
|
|
||||||
{{
|
{{
|
||||||
formatNumberDecimal(
|
formatNumberDecimal(
|
||||||
props.row.pricePerUnit +
|
props.row.pricePerUnit +
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ defineProps<{
|
||||||
badgeColor?: string;
|
badgeColor?: string;
|
||||||
hideKebabView?: boolean;
|
hideKebabView?: boolean;
|
||||||
hideKebabEdit?: boolean;
|
hideKebabEdit?: boolean;
|
||||||
|
hideAction?: boolean;
|
||||||
|
|
||||||
customData?: {
|
customData?: {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -88,6 +89,7 @@ const rand = Math.random();
|
||||||
@click.stop="$emit('view')"
|
@click.stop="$emit('view')"
|
||||||
/>
|
/>
|
||||||
<KebabAction
|
<KebabAction
|
||||||
|
v-if="!hideAction"
|
||||||
:idName="code"
|
:idName="code"
|
||||||
status="ACTIVE"
|
status="ACTIVE"
|
||||||
hide-toggle
|
hide-toggle
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
iconSize?: string;
|
iconSize?: string;
|
||||||
tooltip?: boolean;
|
tooltip?: boolean;
|
||||||
|
|
@ -17,7 +17,7 @@ withDefaults(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<article class="row items-center full-width">
|
<article class="row items-center">
|
||||||
<Icon
|
<Icon
|
||||||
v-if="icon"
|
v-if="icon"
|
||||||
:icon
|
:icon
|
||||||
|
|
@ -25,12 +25,14 @@ withDefaults(
|
||||||
:width="iconSize || '2rem'"
|
:width="iconSize || '2rem'"
|
||||||
/>
|
/>
|
||||||
<span class="row col">
|
<span class="row col">
|
||||||
<span class="col-12 app-text-muted-2 text-caption">
|
<span class="col-12 app-text-muted-2" style="font-size: 10px">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</span>
|
</span>
|
||||||
<span class="col-12 text-weight-medium ellipsis">
|
<span class="col-12 ellipsis">
|
||||||
{{ value }}
|
<slot name="value">
|
||||||
<q-tooltip v-if="tooltip" :delay="500">{{ value }}</q-tooltip>
|
{{ value }}
|
||||||
|
<q-tooltip v-if="tooltip" :delay="500">{{ value }}</q-tooltip>
|
||||||
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ defineProps<{
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="text-center">
|
||||||
<q-img
|
<q-img
|
||||||
src="/no-data.png"
|
src="/no-data.png"
|
||||||
:style="{
|
:style="{
|
||||||
|
|
|
||||||
7
src/components/SelectWorker.vue
Normal file
7
src/components/SelectWorker.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{}>();
|
||||||
|
</script>
|
||||||
|
<template></template>
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -1,7 +1,83 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
withDefaults(defineProps<{}>(), {});
|
import { RequestWork, RequestWorkStatus } from 'src/stores/request-list/types';
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
statusDone?: boolean;
|
||||||
|
statusActive?: boolean;
|
||||||
|
statusWaiting?: boolean;
|
||||||
|
|
||||||
|
label: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
statusDone: false,
|
||||||
|
statusActive: false,
|
||||||
|
label: '',
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="status-color q-pa-sm bordered row items-center cursor-pointer no-wrap"
|
||||||
|
style="text-wrap: nowrap"
|
||||||
|
:class="{
|
||||||
|
['status-color-done']: statusDone,
|
||||||
|
['status-color-doing']: true,
|
||||||
|
['status-color-waiting']: statusWaiting,
|
||||||
|
['step-status-active']: statusActive,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="q-px-sm">
|
||||||
|
<q-icon
|
||||||
|
class="icon-color quotation-status"
|
||||||
|
style="border-radius: 50%"
|
||||||
|
:name="`${statusActive ? 'mdi-circle-slice-8' : statusDone ? 'mdi-check-circle' : 'mdi-checkbox-blank-circle-outline'}`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-left">{{ label }}</div>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.status-color {
|
||||||
|
--_color: var(--gray-0);
|
||||||
|
border-color: hsla(var(--_color));
|
||||||
|
background: hsla(var(--_color) / 0.05);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
<template></template>
|
.icon-color {
|
||||||
|
color: hsla(var(--_color));
|
||||||
|
}
|
||||||
|
|
||||||
<style scoped></style>
|
&.status-color-doing {
|
||||||
|
--_color: var(--blue-5-hsl);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
&.status-color-done {
|
||||||
|
--_color: var(--green-5-hsl);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
&.status-color-waiting {
|
||||||
|
--_color: var(--gray-4-hsl);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-status-active {
|
||||||
|
opacity: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: 1s box-shadow ease-in-out;
|
||||||
|
animation: status 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes status {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0x 0px 0px hsla(var(--_color) / 0.5);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0px 0px 1px 4px hsla(var(--_color) / 0.2);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0px 0px 4px 7px hsla(var(--_color) / 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
107
src/components/shared/select/SelectInstitution.vue
Normal file
107
src/components/shared/select/SelectInstitution.vue
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { createSelect, SelectProps } from './select';
|
||||||
|
import SelectInput from '../SelectInput.vue';
|
||||||
|
|
||||||
|
import { Institution } from 'src/stores/institution/types';
|
||||||
|
|
||||||
|
import { useInstitution } from 'src/stores/institution';
|
||||||
|
|
||||||
|
type SelectOption = Institution;
|
||||||
|
|
||||||
|
const value = defineModel<string | null | undefined>('value', {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const valueOption = defineModel<SelectOption>('valueOption', {
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectOptions = ref<SelectOption[]>([]);
|
||||||
|
|
||||||
|
const { getInstitutionList: getList, getInstitution: getById } =
|
||||||
|
useInstitution();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'create'): void;
|
||||||
|
(e: 'updateValue', val: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
type ExclusiveProps = {
|
||||||
|
selectFirstValue?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
|
||||||
|
|
||||||
|
const { getOptions, setFirstValue, getSelectedOption, filter } =
|
||||||
|
createSelect<SelectOption>(
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
valueOption,
|
||||||
|
selectOptions,
|
||||||
|
getList: async (query) => {
|
||||||
|
const ret = await getList({
|
||||||
|
query: query === '' ? undefined : query,
|
||||||
|
...props.params,
|
||||||
|
});
|
||||||
|
if (ret) return ret.result;
|
||||||
|
},
|
||||||
|
getByValue: async (id) => {
|
||||||
|
const ret = await getById(id);
|
||||||
|
if (ret) return ret;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ valueField: 'id' },
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getOptions();
|
||||||
|
|
||||||
|
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
|
||||||
|
setFirstValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.selectFirstValue) {
|
||||||
|
setDefaultValue();
|
||||||
|
} else await getSelectedOption();
|
||||||
|
});
|
||||||
|
|
||||||
|
function setDefaultValue() {
|
||||||
|
setFirstValue();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<SelectInput
|
||||||
|
v-model="value"
|
||||||
|
incremental
|
||||||
|
:label
|
||||||
|
:placeholder
|
||||||
|
:readonly
|
||||||
|
:disable="disabled"
|
||||||
|
:option="
|
||||||
|
selectOptions.map((v) => {
|
||||||
|
const ret = {
|
||||||
|
label: v.name,
|
||||||
|
value: v.id,
|
||||||
|
};
|
||||||
|
return ret;
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:hide-selected="false"
|
||||||
|
:fill-input="false"
|
||||||
|
:rules="
|
||||||
|
required ? [(v: string) => !!v || $t('form.error.required')] : undefined
|
||||||
|
"
|
||||||
|
@filter="filter"
|
||||||
|
@update:model-value="(v) => $emit('updateValue', v as string)"
|
||||||
|
>
|
||||||
|
<template #append v-if="clearable">
|
||||||
|
<q-icon
|
||||||
|
v-if="!readonly && value"
|
||||||
|
name="mdi-close-circle"
|
||||||
|
@click.stop="value = ''"
|
||||||
|
class="cursor-pointer clear-btn"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</SelectInput>
|
||||||
|
</template>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { QFile } from 'quasar';
|
import { QFile } from 'quasar';
|
||||||
|
|
||||||
import { baseUrl } from 'stores/utils';
|
import { baseUrl } from 'stores/utils';
|
||||||
|
|
@ -11,9 +11,11 @@ const props = withDefaults(
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
layout?: 'row' | 'column';
|
||||||
transformUrl?: (url: string) => string | Promise<string>;
|
transformUrl?: (url: string) => string | Promise<string>;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
|
layout: 'row',
|
||||||
label: 'Upload',
|
label: 'Upload',
|
||||||
readonly: false,
|
readonly: false,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
|
|
@ -50,9 +52,9 @@ function pickFile() {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div :class="{ row: layout === 'column' }">
|
||||||
<div
|
<div
|
||||||
class="upload-section column rounded q-py-md full-height items-center justify-center no-wrap surface-2"
|
class="upload-section column rounded q-py-md items-center justify-center no-wrap surface-2 col"
|
||||||
>
|
>
|
||||||
<q-img src="/images/upload.png" width="150px" />
|
<q-img src="/images/upload.png" width="150px" />
|
||||||
{{ label }}
|
{{ label }}
|
||||||
|
|
@ -74,13 +76,13 @@ function pickFile() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- upload card -->
|
<!-- upload card -->
|
||||||
<section class="row">
|
<section class="row col" :class="{ 'q-pl-md': layout === 'column' }">
|
||||||
<div
|
<div
|
||||||
v-for="(d, j) in fileData"
|
v-for="(d, j) in fileData"
|
||||||
:key="j"
|
:key="j"
|
||||||
class="col-12"
|
class="col-12"
|
||||||
:class="{
|
:class="{
|
||||||
'q-pt-md': j === 0,
|
'q-pt-md': layout === 'row' && j === 0,
|
||||||
'q-pt-sm': j > 0,
|
'q-pt-sm': j > 0,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -181,3 +181,16 @@ div.fullscreen.q-drawer__backdrop {
|
||||||
i.q-icon.mdi.mdi-alert.q-table__bottom-nodata-icon {
|
i.q-icon.mdi.mdi-alert.q-table__bottom-nodata-icon {
|
||||||
color: #ffc224 !important;
|
color: #ffc224 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon {
|
||||||
|
color: hsl(var(--text-mute));
|
||||||
|
}
|
||||||
|
|
||||||
|
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated {
|
||||||
|
color: var(--brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.surface-1
|
||||||
|
.q-focus-helper {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,8 @@ export default {
|
||||||
selected: 'Selected {number} {msg}',
|
selected: 'Selected {number} {msg}',
|
||||||
next: 'Next',
|
next: 'Next',
|
||||||
import: 'Import',
|
import: 'Import',
|
||||||
|
numberOfDay: 'Number of days',
|
||||||
|
other: 'Other',
|
||||||
},
|
},
|
||||||
|
|
||||||
menu: {
|
menu: {
|
||||||
|
|
@ -905,6 +907,7 @@ export default {
|
||||||
InProgress: 'In Progress',
|
InProgress: 'In Progress',
|
||||||
Completed: 'Completed',
|
Completed: 'Completed',
|
||||||
Canceled: 'Canceled',
|
Canceled: 'Canceled',
|
||||||
|
|
||||||
AwaitOrder: 'Awaiting Order',
|
AwaitOrder: 'Awaiting Order',
|
||||||
ReadyOrder: 'Ready for Order',
|
ReadyOrder: 'Ready for Order',
|
||||||
EndOrder: 'Order Completed',
|
EndOrder: 'Order Completed',
|
||||||
|
|
@ -919,17 +922,62 @@ export default {
|
||||||
|
|
||||||
taskOrder: {
|
taskOrder: {
|
||||||
title: 'Task Order',
|
title: 'Task Order',
|
||||||
|
receive: 'Job Receipt ',
|
||||||
caption: 'All Task Order',
|
caption: 'All Task Order',
|
||||||
|
code: 'Task Order Code',
|
||||||
|
|
||||||
receiveTaskOrder: 'Receive Task Order',
|
receiveTaskOrder: 'Receive Task Order',
|
||||||
|
tasktobeDone: 'Task to be Done',
|
||||||
|
inProgress: 'In Progress',
|
||||||
|
completed: 'Completed',
|
||||||
sendTaskOrder: 'Send Task Order',
|
sendTaskOrder: 'Send Task Order',
|
||||||
payment: 'Payment',
|
payment: 'Payment',
|
||||||
goodReceipt: 'Good Receipt',
|
goodReceipt: 'Good Receipt',
|
||||||
|
|
||||||
|
canceled: 'Canceled',
|
||||||
issueBranch: 'Issue Branch',
|
issueBranch: 'Issue Branch',
|
||||||
issueDate: 'Issue Date',
|
issueDate: 'Issue Date',
|
||||||
madeBy: 'Made By',
|
madeBy: 'Made By',
|
||||||
contactName: 'Contact Name',
|
contactName: 'Contact Name',
|
||||||
|
workOrderCode: 'Work Order Code',
|
||||||
|
workOrderName: 'Work Order Name',
|
||||||
|
telephone: 'Telephone',
|
||||||
|
|
||||||
|
productList: 'Product List',
|
||||||
|
amountOfEmployee: 'Number of workers',
|
||||||
|
|
||||||
|
recipientOrSender: 'Recipient/Sender',
|
||||||
|
workStartDate: 'Work Start Date & Time',
|
||||||
|
workSubmissionDate: 'Work Submission Date & Time',
|
||||||
|
status: {
|
||||||
|
Pending: 'Pending',
|
||||||
|
InProgress: 'InProgress',
|
||||||
|
Success: 'Success',
|
||||||
|
Failed: 'Failed',
|
||||||
|
Redo: 'Redo',
|
||||||
|
Validate: 'Validate',
|
||||||
|
Complete: 'Complete',
|
||||||
|
Canceled: 'Canceled',
|
||||||
|
},
|
||||||
|
|
||||||
|
receiveTask: 'Receive Task',
|
||||||
|
receiveScan: 'Receive Task (Scan)',
|
||||||
|
receiveCustom: 'Receive Task (Custom)',
|
||||||
|
|
||||||
|
allProduct: 'All Product',
|
||||||
|
alreadySentTask: 'Already Sent Task',
|
||||||
|
sentTask: 'Sent Task',
|
||||||
|
taskInCart: 'Task in Cart',
|
||||||
|
waitReceive: 'Waiting to accept task',
|
||||||
|
failTaskOrderCode: 'Task order with issue',
|
||||||
|
describeIssue: 'Describe issue',
|
||||||
|
documentSubmitFailed: 'Document submission failed',
|
||||||
|
taskNotFullyCompleted: 'Task was not fully completed',
|
||||||
|
|
||||||
|
noRequestAvailable: 'There is no request list available for processing',
|
||||||
|
validate: 'Validate',
|
||||||
|
done: 'Done',
|
||||||
|
confirmValidate: 'Confirm Validate',
|
||||||
},
|
},
|
||||||
|
|
||||||
dialog: {
|
dialog: {
|
||||||
|
|
@ -957,8 +1005,11 @@ export default {
|
||||||
headquartersNotEstablished: 'Headoffice not established',
|
headquartersNotEstablished: 'Headoffice not established',
|
||||||
warningClose: 'Incomplte edit data, Do you want to close?',
|
warningClose: 'Incomplte edit data, Do you want to close?',
|
||||||
close: 'Do you want to close this window?',
|
close: 'Do you want to close this window?',
|
||||||
|
|
||||||
confirmChangeStatus: 'Do you want to change your status?',
|
confirmChangeStatus: 'Do you want to change your status?',
|
||||||
|
confirmSavingStatus:
|
||||||
|
'Do you want to confirm the saving of the status change data?',
|
||||||
|
confirmSending: 'Do you confirm the submission of the task?',
|
||||||
|
confirmValidate: 'Do you confirm the validation?',
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
ok: 'OK',
|
ok: 'OK',
|
||||||
|
|
@ -1036,6 +1087,7 @@ export default {
|
||||||
installmentsValidateFailed:
|
installmentsValidateFailed:
|
||||||
'Validation failed. Each installment must include at least one product. Please review and update the installments accordingly.',
|
'Validation failed. Each installment must include at least one product. Please review and update the installments accordingly.',
|
||||||
flowTemplateNotFound: 'Workflow template cannot be found',
|
flowTemplateNotFound: 'Workflow template cannot be found',
|
||||||
|
taskOrderNotFound: 'Task order cannot be found.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,8 @@ export default {
|
||||||
selected: '{number} {msg}ถูกเลือก',
|
selected: '{number} {msg}ถูกเลือก',
|
||||||
import: 'นำเข้า',
|
import: 'นำเข้า',
|
||||||
next: 'ถัดไป',
|
next: 'ถัดไป',
|
||||||
|
numberOfDay: 'จำนวนวัน',
|
||||||
|
other: 'อื่นๆ',
|
||||||
},
|
},
|
||||||
|
|
||||||
menu: {
|
menu: {
|
||||||
|
|
@ -894,6 +896,7 @@ export default {
|
||||||
InProgress: 'ดำเนินการ',
|
InProgress: 'ดำเนินการ',
|
||||||
Completed: 'เสร็จสิ้น',
|
Completed: 'เสร็จสิ้น',
|
||||||
Canceled: 'ยกเลิก',
|
Canceled: 'ยกเลิก',
|
||||||
|
|
||||||
AwaitOrder: 'รอสั่งงาน',
|
AwaitOrder: 'รอสั่งงาน',
|
||||||
ReadyOrder: 'พร้อมสั่งงาน',
|
ReadyOrder: 'พร้อมสั่งงาน',
|
||||||
EndOrder: 'จบงาน',
|
EndOrder: 'จบงาน',
|
||||||
|
|
@ -908,17 +911,60 @@ export default {
|
||||||
|
|
||||||
taskOrder: {
|
taskOrder: {
|
||||||
title: 'ใบสั่งงาน',
|
title: 'ใบสั่งงาน',
|
||||||
|
receive: 'ใบรับงาน',
|
||||||
caption: 'ใบสั่งงานทั้งหมด',
|
caption: 'ใบสั่งงานทั้งหมด',
|
||||||
|
code: 'เลขใบสั่งงาน',
|
||||||
|
|
||||||
receiveTaskOrder: 'รับใบสั่งงาน',
|
receiveTaskOrder: 'รับใบสั่งงาน',
|
||||||
|
tasktobeDone: 'งานที่ต้องทำ',
|
||||||
|
inProgress: 'ดำเนินการ',
|
||||||
|
completed: 'เรียบร้อย',
|
||||||
sendTaskOrder: 'ส่งใบสั่งงาน',
|
sendTaskOrder: 'ส่งใบสั่งงาน',
|
||||||
payment: 'ขำระเงิน',
|
payment: 'ขำระเงิน',
|
||||||
goodReceipt: 'ใบรับสินค้า',
|
goodReceipt: 'ใบรับสินค้า',
|
||||||
|
canceled: 'ยกเลิก',
|
||||||
issueBranch: 'สาขาที่ออก',
|
issueBranch: 'สาขาที่ออก',
|
||||||
issueDate: 'วันที่ออก',
|
issueDate: 'วันที่ออก',
|
||||||
madeBy: 'ผู้ที่ทำรายการ',
|
madeBy: 'ผู้ที่ทำรายการ',
|
||||||
contactName: 'ชื่อผู้ติดต่อ',
|
contactName: 'ชื่อผู้ติดต่อ',
|
||||||
|
workOrderCode: 'รหัสใบสั่งงาน',
|
||||||
|
workOrderName: 'ชื่อใบสั่งงาน',
|
||||||
|
telephone: 'เบอร์โทร',
|
||||||
|
|
||||||
|
productList: 'รายการสินค้า',
|
||||||
|
amountOfEmployee: 'จำนวนแรงงาน (คน)',
|
||||||
|
recipientOrSender: 'คนรับ/ส่งงาน',
|
||||||
|
workStartDate: 'วันที่เวลารับงาน',
|
||||||
|
workSubmissionDate: 'วันที่เวลาส่งงาน',
|
||||||
|
status: {
|
||||||
|
Pending: 'รอดำเนินการ',
|
||||||
|
InProgress: 'กำลังดำเนินการ ',
|
||||||
|
Success: 'ดำเนินการสำเร็จ',
|
||||||
|
Failed: 'ดำเนินการไม่สำเร็จ',
|
||||||
|
Redo: 'ทำใหม่',
|
||||||
|
Validate: 'ตรวจสอบความถูกต้อง',
|
||||||
|
Complete: 'ดำเนินการเสร็จสิ้น',
|
||||||
|
Canceled: ' ยกเลิกรายการคำขอ',
|
||||||
|
},
|
||||||
|
|
||||||
|
receiveTask: 'รับงาน',
|
||||||
|
receiveScan: 'รับงานแบบสแกน',
|
||||||
|
receiveCustom: 'รับงานแบบเลือกเอง',
|
||||||
|
|
||||||
|
allProduct: 'สินค้าทั้งหมด',
|
||||||
|
alreadySentTask: 'ส่งงานแล้ว',
|
||||||
|
sentTask: 'ส่งงาน',
|
||||||
|
taskInCart: 'งานในตะกร้า',
|
||||||
|
waitReceive: 'รอรับงาน',
|
||||||
|
failTaskOrderCode: 'เลขที่ใบรายการที่พบปัญหา',
|
||||||
|
describeIssue: 'ระบุปัญหา',
|
||||||
|
documentSubmitFailed: 'ยื่นเอกสารไม่ผ่าน',
|
||||||
|
taskNotFullyCompleted: 'ทำรายการไม่ครบ',
|
||||||
|
|
||||||
|
noRequestAvailable: 'ไม่มีใบรายการคำขอที่สามารถดำเนินการได้',
|
||||||
|
validate: 'ตรวจสอบสินค้า',
|
||||||
|
done: 'ดำเนินการแล้ว',
|
||||||
|
confirmValidate: 'ยืนยันการตรวจสอบ',
|
||||||
},
|
},
|
||||||
|
|
||||||
dialog: {
|
dialog: {
|
||||||
|
|
@ -946,6 +992,10 @@ export default {
|
||||||
warningClose: 'มีการแก้ไขที่ยังไม่ได้บันทึก คุณต้องการปิดใช่หรือไม่',
|
warningClose: 'มีการแก้ไขที่ยังไม่ได้บันทึก คุณต้องการปิดใช่หรือไม่',
|
||||||
close: 'คุณต้องการปิดหน้าต่างนี้ใช่หรือไม่',
|
close: 'คุณต้องการปิดหน้าต่างนี้ใช่หรือไม่',
|
||||||
confirmChangeStatus: 'คุณต้องการเปลี่ยนสถานะใช่หรือไม่',
|
confirmChangeStatus: 'คุณต้องการเปลี่ยนสถานะใช่หรือไม่',
|
||||||
|
confirmSavingStatus:
|
||||||
|
'คุณต้องการยืนยันการบันทึกข้อมูลการเปลี่ยนสถานะใช่หรือไม่',
|
||||||
|
confirmSending: 'ยืนยันการส่งงานใช่หรือไม่',
|
||||||
|
confirmValidate: 'ยืนยันการตรวจสอบใช่หรือไม่',
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
ok: 'ยืนยัน',
|
ok: 'ยืนยัน',
|
||||||
|
|
@ -1020,6 +1070,7 @@ export default {
|
||||||
installmentsValidateFailed:
|
installmentsValidateFailed:
|
||||||
'ข้อมูลงวดไม่ถูกต้อง กรุณาตรวจสอบและยืนยันว่าแต่ละงวดมีสินค้าอย่างน้อยหนึ่งรายการ',
|
'ข้อมูลงวดไม่ถูกต้อง กรุณาตรวจสอบและยืนยันว่าแต่ละงวดมีสินค้าอย่างน้อยหนึ่งรายการ',
|
||||||
flowTemplateNotFound: 'ไม่พบขั้นตอนการทำงาน',
|
flowTemplateNotFound: 'ไม่พบขั้นตอนการทำงาน',
|
||||||
|
taskOrderNotFound: 'ไม่พบใบสั่งงาน',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Icon } from '@iconify/vue';
|
||||||
import { BranchContact } from 'stores/branch-contact/types';
|
import { BranchContact } from 'stores/branch-contact/types';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import type { QTableProps } from 'quasar';
|
import type { QTableProps, QTableSlots } from 'quasar';
|
||||||
import { resetScrollBar } from 'src/stores/utils';
|
import { resetScrollBar } from 'src/stores/utils';
|
||||||
import useBranchStore from 'stores/branch';
|
import useBranchStore from 'stores/branch';
|
||||||
import useFlowStore from 'stores/flow';
|
import useFlowStore from 'stores/flow';
|
||||||
|
|
@ -1330,7 +1330,11 @@ watch(currentHq, () => {
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-slot:body="props">
|
<template
|
||||||
|
v-slot:body="props: {
|
||||||
|
row: (typeof treeData.value)[number];
|
||||||
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||||
|
>
|
||||||
<q-tr
|
<q-tr
|
||||||
:class="{
|
:class="{
|
||||||
'app-text-muted': props.row.status === 'INACTIVE',
|
'app-text-muted': props.row.status === 'INACTIVE',
|
||||||
|
|
@ -1470,34 +1474,14 @@ watch(currentHq, () => {
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
formatAddress({
|
formatAddress({
|
||||||
address: props.row.address,
|
...props.row,
|
||||||
addressEN: props.row.addressEN,
|
|
||||||
moo: props.row.moo,
|
|
||||||
mooEN: props.row.mooEN,
|
|
||||||
soi: props.row.soi,
|
|
||||||
soiEN: props.row.soiEN,
|
|
||||||
street: props.row.street,
|
|
||||||
streetEN: props.row.streetEN,
|
|
||||||
province: props.row.province,
|
|
||||||
district: props.row.district,
|
|
||||||
subDistrict: props.row.subDistrict,
|
|
||||||
en: $i18n.locale === 'eng',
|
en: $i18n.locale === 'eng',
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
<q-tooltip>
|
<q-tooltip>
|
||||||
{{
|
{{
|
||||||
formatAddress({
|
formatAddress({
|
||||||
address: props.row.address,
|
...props.row,
|
||||||
addressEN: props.row.addressEN,
|
|
||||||
moo: props.row.moo,
|
|
||||||
mooEN: props.row.mooEN,
|
|
||||||
soi: props.row.soi,
|
|
||||||
soiEN: props.row.soiEN,
|
|
||||||
street: props.row.street,
|
|
||||||
streetEN: props.row.streetEN,
|
|
||||||
province: props.row.province,
|
|
||||||
district: props.row.district,
|
|
||||||
subDistrict: props.row.subDistrict,
|
|
||||||
en: $i18n.locale === 'eng',
|
en: $i18n.locale === 'eng',
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1158,6 +1158,7 @@ async function submitService(notClose = false) {
|
||||||
if (dialogServiceEdit.value) {
|
if (dialogServiceEdit.value) {
|
||||||
const res = await editService(currentIdService.value, {
|
const res = await editService(currentIdService.value, {
|
||||||
...formService.value,
|
...formService.value,
|
||||||
|
workflowId: currWorkflow.value?.id || '',
|
||||||
status: statusToggle.value ? formService.value.status : 'INACTIVE',
|
status: statusToggle.value ? formService.value.status : 'INACTIVE',
|
||||||
});
|
});
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ const props = defineProps<{
|
||||||
totalVatIncluded: number;
|
totalVatIncluded: number;
|
||||||
totalAfterDiscount: number;
|
totalAfterDiscount: number;
|
||||||
};
|
};
|
||||||
|
taskOrder?: boolean;
|
||||||
|
taskOrderComplete?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
@ -184,10 +186,13 @@ function calculateInstallments(param: {
|
||||||
<AppBox
|
<AppBox
|
||||||
no-padding
|
no-padding
|
||||||
bordered
|
bordered
|
||||||
class="row main-color"
|
class="main-color"
|
||||||
:class="{
|
:class="{
|
||||||
|
row: $q.screen.gt.sm,
|
||||||
|
column: $q.screen.lt.md,
|
||||||
'invoice-color': view === View.Invoice,
|
'invoice-color': view === View.Invoice,
|
||||||
'receipt-color': view === View.Receipt,
|
'receipt-color': view === View.Receipt,
|
||||||
|
'task-order-color': taskOrder && !taskOrderComplete,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="col bordered-r">
|
<div class="col bordered-r">
|
||||||
|
|
@ -203,9 +208,13 @@ function calculateInstallments(param: {
|
||||||
<div class="row q-col-gutter-sm">
|
<div class="row q-col-gutter-sm">
|
||||||
<section class="row q-col-gutter-sm col-12 items-center">
|
<section class="row q-col-gutter-sm col-12 items-center">
|
||||||
<SelectInput
|
<SelectInput
|
||||||
class="col-6"
|
class="col-md-6 col-12"
|
||||||
:label="$t('quotation.payType')"
|
:label="$t('quotation.payType')"
|
||||||
:option="payTypeOption"
|
:option="
|
||||||
|
taskOrder
|
||||||
|
? payTypeOption.filter((v) => v.value === 'Full')
|
||||||
|
: payTypeOption
|
||||||
|
"
|
||||||
:readonly
|
:readonly
|
||||||
id="pay-type"
|
id="pay-type"
|
||||||
:model-value="payType"
|
:model-value="payType"
|
||||||
|
|
@ -218,7 +227,8 @@ function calculateInstallments(param: {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="col-6"
|
class="col-md-6 col-12"
|
||||||
|
:class="{ 'q-pl-md': $q.screen.lt.md }"
|
||||||
v-if="payType === 'Split' || payType === 'SplitCustom'"
|
v-if="payType === 'Split' || payType === 'SplitCustom'"
|
||||||
>
|
>
|
||||||
<div class="row full-width items-center justify-between q-py-xs">
|
<div class="row full-width items-center justify-between q-py-xs">
|
||||||
|
|
@ -485,6 +495,10 @@ function calculateInstallments(param: {
|
||||||
--_color: var(--green-6-hsl);
|
--_color: var(--green-6-hsl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-order-color {
|
||||||
|
--_color: var(--pink-7-hsl);
|
||||||
|
}
|
||||||
|
|
||||||
.bg-color {
|
.bg-color {
|
||||||
color: white;
|
color: white;
|
||||||
background: hsla(var(--_color));
|
background: hsla(var(--_color));
|
||||||
|
|
|
||||||
|
|
@ -493,6 +493,7 @@ watch(
|
||||||
|
|
||||||
<div class="row items-center q-gutter-x-sm">
|
<div class="row items-center q-gutter-x-sm">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
color="primary"
|
||||||
padding="4px"
|
padding="4px"
|
||||||
flat
|
flat
|
||||||
rounded
|
rounded
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@ function clean() {
|
||||||
workerList.value = [];
|
workerList.value = [];
|
||||||
workerSelected.value = [];
|
workerSelected.value = [];
|
||||||
open.value = false;
|
open.value = false;
|
||||||
|
state.step = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectedIndex(item: Employee) {
|
function selectedIndex(item: Employee) {
|
||||||
|
|
@ -300,6 +301,7 @@ watch(() => state.search, getWorkerList);
|
||||||
@click="state.importWorker = true"
|
@click="state.importWorker = true"
|
||||||
:label="$t('quotation.importWorker')"
|
:label="$t('quotation.importWorker')"
|
||||||
/>
|
/>
|
||||||
|
<span v-else class="q-ml-auto"></span>
|
||||||
</template>
|
</template>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,12 @@ watch([() => pageState.inputSearch, () => pageState.statusFilter], () =>
|
||||||
label: 'requestList.status.Completed',
|
label: 'requestList.status.Completed',
|
||||||
color: 'light-green',
|
color: 'light-green',
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// icon: 'mdi-close-circle-outline',
|
||||||
|
// count: 0,
|
||||||
|
// label: 'requestList.status.Canceled',
|
||||||
|
// color: 'red',
|
||||||
|
// },
|
||||||
]"
|
]"
|
||||||
:dark="$q.dark.isActive"
|
:dark="$q.dark.isActive"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ defineProps<{
|
||||||
installmentNo?: number;
|
installmentNo?: number;
|
||||||
paySuccess: boolean;
|
paySuccess: boolean;
|
||||||
payCondition: PayCondition;
|
payCondition: PayCondition;
|
||||||
|
readonly?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// NOTE: Function
|
// NOTE: Function
|
||||||
|
|
@ -85,6 +86,7 @@ defineProps<{
|
||||||
<div class="justify-end flex">
|
<div class="justify-end flex">
|
||||||
<q-btn-dropdown
|
<q-btn-dropdown
|
||||||
:disable="
|
:disable="
|
||||||
|
readonly ||
|
||||||
status?.workStatus === 'Waiting' ||
|
status?.workStatus === 'Waiting' ||
|
||||||
status?.workStatus === 'InProgress'
|
status?.workStatus === 'InProgress'
|
||||||
"
|
"
|
||||||
|
|
@ -103,27 +105,33 @@ defineProps<{
|
||||||
:class="{
|
:class="{
|
||||||
disable:
|
disable:
|
||||||
$q.screen.gt.xs &&
|
$q.screen.gt.xs &&
|
||||||
(status?.workStatus === RequestWorkStatus.Waiting ||
|
(readonly ||
|
||||||
|
status?.workStatus === RequestWorkStatus.Waiting ||
|
||||||
status?.workStatus === RequestWorkStatus.InProgress),
|
status?.workStatus === RequestWorkStatus.InProgress),
|
||||||
pending:
|
pending:
|
||||||
($q.screen.gt.xs && !status?.workStatus) ||
|
$q.screen.gt.xs &&
|
||||||
status?.workStatus === RequestWorkStatus.Pending ||
|
!readonly &&
|
||||||
status?.workStatus === RequestWorkStatus.Ready,
|
(!status?.workStatus ||
|
||||||
|
status?.workStatus === RequestWorkStatus.Pending ||
|
||||||
|
status?.workStatus === RequestWorkStatus.Ready),
|
||||||
progress:
|
progress:
|
||||||
$q.screen.gt.xs &&
|
$q.screen.gt.xs &&
|
||||||
|
!readonly &&
|
||||||
status?.workStatus === RequestWorkStatus.Validate,
|
status?.workStatus === RequestWorkStatus.Validate,
|
||||||
complete:
|
complete:
|
||||||
$q.screen.gt.xs &&
|
$q.screen.gt.xs &&
|
||||||
|
!readonly &&
|
||||||
(status?.workStatus === RequestWorkStatus.Ended ||
|
(status?.workStatus === RequestWorkStatus.Ended ||
|
||||||
status?.workStatus === RequestWorkStatus.Completed),
|
status?.workStatus === RequestWorkStatus.Completed),
|
||||||
canceled:
|
canceled:
|
||||||
$q.screen.gt.xs &&
|
$q.screen.gt.xs &&
|
||||||
|
!readonly &&
|
||||||
status?.workStatus === RequestWorkStatus.Canceled,
|
status?.workStatus === RequestWorkStatus.Canceled,
|
||||||
}"
|
}"
|
||||||
:style="
|
:style="
|
||||||
$q.screen.xs &&
|
readonly ||
|
||||||
(status?.workStatus === RequestWorkStatus.Waiting ||
|
status?.workStatus === RequestWorkStatus.Waiting ||
|
||||||
status?.workStatus === RequestWorkStatus.InProgress)
|
status?.workStatus === RequestWorkStatus.InProgress
|
||||||
? `opacity: 30% !important`
|
? `opacity: 30% !important`
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import FormExpansion from './FormExpansion.vue';
|
||||||
import PropertiesExpansion from './PropertiesExpansion.vue';
|
import PropertiesExpansion from './PropertiesExpansion.vue';
|
||||||
import FormGroupHead from './FormGroupHead.vue';
|
import FormGroupHead from './FormGroupHead.vue';
|
||||||
import AvatarGroup from 'src/components/shared/AvatarGroup.vue';
|
import AvatarGroup from 'src/components/shared/AvatarGroup.vue';
|
||||||
|
import { StateButton } from 'components/button';
|
||||||
|
|
||||||
// NOTE: Store
|
// NOTE: Store
|
||||||
import { dateFormatJS } from 'src/utils/datetime';
|
import { dateFormatJS } from 'src/utils/datetime';
|
||||||
|
|
@ -21,6 +22,7 @@ import {
|
||||||
DocStatus,
|
DocStatus,
|
||||||
Step,
|
Step,
|
||||||
RequestWorkStatus,
|
RequestWorkStatus,
|
||||||
|
RequestDataStatus,
|
||||||
} from 'src/stores/request-list/types';
|
} from 'src/stores/request-list/types';
|
||||||
import useOptionStore from 'src/stores/options';
|
import useOptionStore from 'src/stores/options';
|
||||||
import ProductExpansion from './ProductExpansion.vue';
|
import ProductExpansion from './ProductExpansion.vue';
|
||||||
|
|
@ -32,12 +34,7 @@ import {
|
||||||
EmployeePassportPayload,
|
EmployeePassportPayload,
|
||||||
EmployeeVisaPayload,
|
EmployeeVisaPayload,
|
||||||
} from 'stores/employee/types';
|
} from 'stores/employee/types';
|
||||||
import {
|
import { PropVariant } from 'src/stores/product-service/types';
|
||||||
PropDate,
|
|
||||||
PropNumber,
|
|
||||||
PropOptions,
|
|
||||||
PropString,
|
|
||||||
} from 'src/stores/product-service/types';
|
|
||||||
import { Invoice } from 'src/stores/payment/types';
|
import { Invoice } from 'src/stores/payment/types';
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
|
|
@ -382,7 +379,7 @@ function isInstallmentPaySuccess(installmentNo: number) {
|
||||||
v-if="
|
v-if="
|
||||||
workList?.every((v) =>
|
workList?.every((v) =>
|
||||||
v.productService.work?.attributes?.workflowStep?.every(
|
v.productService.work?.attributes?.workflowStep?.every(
|
||||||
(s) => {
|
(s: any) => {
|
||||||
s.attributes?.properties.length === 0;
|
s.attributes?.properties.length === 0;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -393,18 +390,27 @@ function isInstallmentPaySuccess(installmentNo: number) {
|
||||||
{{ $t('requestList.noWorkflowTemplate') }}
|
{{ $t('requestList.noWorkflowTemplate') }}
|
||||||
</span>
|
</span>
|
||||||
<template v-for="(value, i) in flow.step" :key="value.id">
|
<template v-for="(value, i) in flow.step" :key="value.id">
|
||||||
<button
|
<StateButton
|
||||||
v-if="
|
@click="() => (pageState.currentStep = value.order)"
|
||||||
workList?.some(
|
:status-waiting="
|
||||||
(v) =>
|
!workList
|
||||||
v.productService.work?.attributes?.workflowStep?.[i]
|
?.filter((v) => {
|
||||||
?.attributes.properties.length > 0,
|
return v.productService.work?.attributes.workflowStep?.[
|
||||||
)
|
i - 1
|
||||||
|
]?.productsId.includes(v.productService.productId);
|
||||||
|
})
|
||||||
|
.every((v) => {
|
||||||
|
const status = v.stepStatus.find(
|
||||||
|
({ step }) => step === i,
|
||||||
|
)?.workStatus;
|
||||||
|
return (
|
||||||
|
status === RequestWorkStatus.Completed ||
|
||||||
|
status === RequestWorkStatus.Ended
|
||||||
|
);
|
||||||
|
})
|
||||||
"
|
"
|
||||||
class="status-color q-pa-sm bordered row items-center cursor-pointer no-wrap"
|
:status-done="
|
||||||
style="text-wrap: nowrap"
|
workList
|
||||||
:class="{
|
|
||||||
['status-color-done']: workList
|
|
||||||
?.filter((v) => {
|
?.filter((v) => {
|
||||||
return v.productService.work?.attributes.workflowStep?.[
|
return v.productService.work?.attributes.workflowStep?.[
|
||||||
i
|
i
|
||||||
|
|
@ -414,28 +420,17 @@ function isInstallmentPaySuccess(installmentNo: number) {
|
||||||
const status = v.stepStatus.find(
|
const status = v.stepStatus.find(
|
||||||
({ step }) => step === i + 1,
|
({ step }) => step === i + 1,
|
||||||
)?.workStatus;
|
)?.workStatus;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
status === RequestWorkStatus.Completed ||
|
status === RequestWorkStatus.Completed ||
|
||||||
status === RequestWorkStatus.Ended
|
status === RequestWorkStatus.Ended
|
||||||
);
|
);
|
||||||
}),
|
})
|
||||||
['status-color-doing']: true,
|
"
|
||||||
['step-status-active']: pageState.currentStep === value.order,
|
:status-active="pageState.currentStep === value.order"
|
||||||
}"
|
:label="value.name"
|
||||||
@click="() => (pageState.currentStep = value.order)"
|
/>
|
||||||
>
|
<!-- 'quotation-status-active': value.active?.(), -->
|
||||||
<!-- 'quotation-status-active': value.active?.(), -->
|
<!-- @click="'waiting' !== 'waiting' && value.handler()" -->
|
||||||
<!-- @click="'waiting' !== 'waiting' && value.handler()" -->
|
|
||||||
<div class="q-px-sm">
|
|
||||||
<q-icon
|
|
||||||
class="icon-color quotation-status"
|
|
||||||
style="border-radius: 50%"
|
|
||||||
:name="`${pageState.currentStep === value.order ? 'mdi-circle-slice-8' : 'mdi-checkbox-blank-circle-outline'}`"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="text-left">{{ value.name }}</div>
|
|
||||||
</button>
|
|
||||||
</template>
|
</template>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
@ -543,106 +538,10 @@ function isInstallmentPaySuccess(installmentNo: number) {
|
||||||
/>
|
/>
|
||||||
<div v-if="$q.screen.gt.sm" class="col"></div>
|
<div v-if="$q.screen.gt.sm" class="col"></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div
|
|
||||||
class="col q-gutter-y-md"
|
|
||||||
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
|
|
||||||
>
|
|
||||||
<DataDisplay
|
|
||||||
class="col"
|
|
||||||
icon="mdi-file-document-outline"
|
|
||||||
:label="$t('requestList.requestListCode')"
|
|
||||||
:value="data.code || '-'"
|
|
||||||
/>
|
|
||||||
<DataDisplay
|
|
||||||
class="col"
|
|
||||||
icon="mdi-account-settings-outline"
|
|
||||||
:label="$t('customer.employer')"
|
|
||||||
:value="
|
|
||||||
getCustomerName(data, { locale: locale, noCode: true }) ||
|
|
||||||
'-'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="col q-gutter-y-md"
|
|
||||||
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
|
|
||||||
>
|
|
||||||
<DataDisplay
|
|
||||||
class="col"
|
|
||||||
icon="mdi-file-document-outline"
|
|
||||||
:label="$t('requestList.quotationCode')"
|
|
||||||
:value="data.quotation.code || '-'"
|
|
||||||
/>
|
|
||||||
<DataDisplay
|
|
||||||
class="col"
|
|
||||||
icon="mdi-account-settings-outline"
|
|
||||||
:label="$t('customer.employee')"
|
|
||||||
:value="
|
|
||||||
getEmployeeName(data, { locale: $i18n.locale }) || '-'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="col q-gutter-y-md"
|
|
||||||
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
|
|
||||||
>
|
|
||||||
<DataDisplay
|
|
||||||
class="col"
|
|
||||||
icon="mdi-file-document-outline"
|
|
||||||
tooltip
|
|
||||||
:label="$t('requestList.invoiceCode')"
|
|
||||||
:value="
|
|
||||||
data.quotation?.invoice
|
|
||||||
?.map((i: Invoice) => i.code)
|
|
||||||
.join(', ') || '-'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<span class="col"></span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="col q-gutter-y-md"
|
|
||||||
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
|
|
||||||
>
|
|
||||||
<DataDisplay
|
|
||||||
class="col"
|
|
||||||
icon="mdi-file-document-outline"
|
|
||||||
tooltip
|
|
||||||
:label="$t('requestList.receiptCode')"
|
|
||||||
:value="
|
|
||||||
data.quotation?.invoice
|
|
||||||
?.flatMap((i: Invoice) => i.payment?.code || [])
|
|
||||||
.join(', ') || '-'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<DataDisplay
|
|
||||||
class="col"
|
|
||||||
icon="mdi-passport"
|
|
||||||
:label="$t('customerEmployee.form.passportNo')"
|
|
||||||
:value="data.employee.employeePassport?.[0]?.number || '-'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="col q-gutter-y-md"
|
|
||||||
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
|
|
||||||
>
|
|
||||||
<DataDisplay
|
|
||||||
class="col"
|
|
||||||
icon="mdi-card-account-details-outline"
|
|
||||||
:label="$t('requestList.alienIdCard')"
|
|
||||||
:value="data.employee.nrcNo"
|
|
||||||
/>
|
|
||||||
<DataDisplay
|
|
||||||
class="col"
|
|
||||||
icon="mdi-account-settings-outline"
|
|
||||||
:label="$t('flow.responsiblePerson')"
|
|
||||||
:value="'-'"
|
|
||||||
/>
|
|
||||||
</div> -->
|
|
||||||
</section>
|
</section>
|
||||||
</transition>
|
</transition>
|
||||||
</article>
|
</article>
|
||||||
<!-- product -->
|
<!-- product -->
|
||||||
|
|
||||||
<template
|
<template
|
||||||
v-for="(value, index) in workList
|
v-for="(value, index) in workList
|
||||||
?.filter((v) =>
|
?.filter((v) =>
|
||||||
|
|
@ -650,6 +549,21 @@ function isInstallmentPaySuccess(installmentNo: number) {
|
||||||
pageState.currentStep - 1
|
pageState.currentStep - 1
|
||||||
]?.productsId.includes(v.productService.productId),
|
]?.productsId.includes(v.productService.productId),
|
||||||
)
|
)
|
||||||
|
.map((v) => {
|
||||||
|
const _props =
|
||||||
|
v.productService.work?.attributes?.workflowStep[
|
||||||
|
pageState.currentStep - 1
|
||||||
|
]?.attributes?.properties;
|
||||||
|
|
||||||
|
return Object.assign(v, {
|
||||||
|
_documentExpansion: _props.some(
|
||||||
|
(v: PropVariant) => v.fieldName === 'documentCheck',
|
||||||
|
),
|
||||||
|
_formExpansion: _props.some(
|
||||||
|
(v: PropVariant) => v.fieldName === 'designForm',
|
||||||
|
),
|
||||||
|
});
|
||||||
|
})
|
||||||
.sort(
|
.sort(
|
||||||
(lhs, rhs) =>
|
(lhs, rhs) =>
|
||||||
lhs.productService.installmentNo -
|
lhs.productService.installmentNo -
|
||||||
|
|
@ -658,6 +572,7 @@ function isInstallmentPaySuccess(installmentNo: number) {
|
||||||
:key="value"
|
:key="value"
|
||||||
>
|
>
|
||||||
<ProductExpansion
|
<ProductExpansion
|
||||||
|
:readonly="data.requestDataStatus === RequestDataStatus.Canceled"
|
||||||
:installment-info="getInstallmentInfo()"
|
:installment-info="getInstallmentInfo()"
|
||||||
:pay-success="
|
:pay-success="
|
||||||
isInstallmentPaySuccess(value.productService.installmentNo)
|
isInstallmentPaySuccess(value.productService.installmentNo)
|
||||||
|
|
@ -689,14 +604,7 @@ function isInstallmentPaySuccess(installmentNo: number) {
|
||||||
class="column surface-1 q-px-sm bordered-t q-pb-sm q-gutter-y-sm"
|
class="column surface-1 q-px-sm bordered-t q-pb-sm q-gutter-y-sm"
|
||||||
>
|
>
|
||||||
<DocumentExpansion
|
<DocumentExpansion
|
||||||
v-if="
|
v-if="value._documentExpansion"
|
||||||
value.productService.work?.attributes?.workflowStep[
|
|
||||||
pageState.currentStep - 1
|
|
||||||
].attributes?.properties.some(
|
|
||||||
(v: PropString | PropNumber | PropDate | PropOptions) =>
|
|
||||||
v.fieldName === 'documentCheck',
|
|
||||||
)
|
|
||||||
"
|
|
||||||
ref="refDocumentExpansion"
|
ref="refDocumentExpansion"
|
||||||
:attributes="value.attributes"
|
:attributes="value.attributes"
|
||||||
@change-status="
|
@change-status="
|
||||||
|
|
@ -744,14 +652,7 @@ function isInstallmentPaySuccess(installmentNo: number) {
|
||||||
:listDocument="product?.document"
|
:listDocument="product?.document"
|
||||||
/>
|
/>
|
||||||
<FormExpansion
|
<FormExpansion
|
||||||
v-if="
|
v-if="value._formExpansion"
|
||||||
value.productService.work?.attributes?.workflowStep[
|
|
||||||
pageState.currentStep - 1
|
|
||||||
].attributes?.properties.some(
|
|
||||||
(v: PropString | PropNumber | PropDate | PropOptions) =>
|
|
||||||
v.fieldName === 'designForm',
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:step="{
|
:step="{
|
||||||
step: pageState.currentStep,
|
step: pageState.currentStep,
|
||||||
requestWorkId: value.id || '',
|
requestWorkId: value.id || '',
|
||||||
|
|
@ -782,47 +683,4 @@ function isInstallmentPaySuccess(installmentNo: number) {
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.status-color {
|
|
||||||
--_color: var(--gray-0);
|
|
||||||
border-color: hsla(var(--_color));
|
|
||||||
background: hsla(var(--_color) / 0.05);
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
.icon-color {
|
|
||||||
color: hsla(var(--_color));
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-color-waiting {
|
|
||||||
--_color: var(--gray-4-hsl);
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
&.status-color-doing {
|
|
||||||
--_color: var(--blue-5-hsl);
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
&.status-color-done {
|
|
||||||
--_color: var(--green-5-hsl);
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-status-active {
|
|
||||||
opacity: 1;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: 1s box-shadow ease-in-out;
|
|
||||||
animation: status 1s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes status {
|
|
||||||
0% {
|
|
||||||
box-shadow: 0x 0px 0px hsla(var(--_color) / 0.5);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0px 0px 1px 4px hsla(var(--_color) / 0.2);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
box-shadow: 0px 0px 4px 7px hsla(var(--_color) / 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,8 @@ function getCustomerName(
|
||||||
}[opts?.locale || 'eng'],
|
}[opts?.locale || 'eng'],
|
||||||
['PERS']:
|
['PERS']:
|
||||||
{
|
{
|
||||||
['eng']: `${useOptionStore().mapOption(customer?.namePrefix)} ${customer?.firstNameEN} ${customer?.lastNameEN}`,
|
['eng']: `${useOptionStore().mapOption(customer?.namePrefix || '')} ${customer?.firstNameEN} ${customer?.lastNameEN}`,
|
||||||
['tha']: `${useOptionStore().mapOption(customer?.namePrefix)} ${customer?.firstName} ${customer?.lastName}`,
|
['tha']: `${useOptionStore().mapOption(customer?.namePrefix || '')} ${customer?.firstName} ${customer?.lastName}`,
|
||||||
}[opts?.locale || 'eng'] || '-',
|
}[opts?.locale || 'eng'] || '-',
|
||||||
}[customer.customer.customerType] +
|
}[customer.customer.customerType] +
|
||||||
(opts?.noCode ? '' : ' ' + `(${customer.code})`)
|
(opts?.noCode ? '' : ' ' + `(${customer.code})`)
|
||||||
|
|
@ -66,8 +66,8 @@ function getEmployeeName(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
['eng']: `${useOptionStore().mapOption(employee?.namePrefix)} ${employee?.firstNameEN} ${employee?.lastNameEN}`,
|
['eng']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstNameEN} ${employee?.lastNameEN}`,
|
||||||
['tha']: `${useOptionStore().mapOption(employee?.namePrefix)} ${employee?.firstName} ${employee?.lastName}`,
|
['tha']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstName} ${employee?.lastName}`,
|
||||||
}[opts?.locale || 'eng'] || '-'
|
}[opts?.locale || 'eng'] || '-'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -132,13 +132,12 @@ function getEmployeeName(
|
||||||
<q-td v-if="visibleColumns.includes('status')">
|
<q-td v-if="visibleColumns.includes('status')">
|
||||||
<BadgeComponent
|
<BadgeComponent
|
||||||
:hsla-color="
|
:hsla-color="
|
||||||
props.row.requestDataStatus === RequestDataStatus.Pending
|
{
|
||||||
? '--orange-5-hsl'
|
[RequestDataStatus.Pending]: '--orange-5-hsl',
|
||||||
: props.row.requestDataStatus === RequestDataStatus.InProgress
|
[RequestDataStatus.InProgress]: '--blue-6-hsl',
|
||||||
? '--blue-6-hsl'
|
[RequestDataStatus.Completed]: '--green-8-hsl',
|
||||||
: props.row.requestDataStatus === RequestDataStatus.Completed
|
[RequestDataStatus.Canceled]: '--red-5-hsl',
|
||||||
? '--green-8-hsl'
|
}[props.row.requestDataStatus]
|
||||||
: '--red-5-hsl'
|
|
||||||
"
|
"
|
||||||
:title="
|
:title="
|
||||||
$t(`requestList.status.${props.row.requestDataStatus}`) || '-'
|
$t(`requestList.status.${props.row.requestDataStatus}`) || '-'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// NOTE: Library
|
// NOTE: Library
|
||||||
import { computed, onMounted, reactive } from 'vue';
|
import { computed, onMounted, reactive, watch, ref } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
// NOTE: Components
|
// NOTE: Components
|
||||||
|
|
@ -8,26 +8,47 @@ import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||||
import NoData from 'src/components/NoData.vue';
|
import NoData from 'src/components/NoData.vue';
|
||||||
import PaginationComponent from 'src/components/PaginationComponent.vue';
|
import PaginationComponent from 'src/components/PaginationComponent.vue';
|
||||||
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||||
|
import TableTaskOrder from './TableTaskOrder.vue';
|
||||||
|
import FloatingActionButton from 'components/FloatingActionButton.vue';
|
||||||
|
import ReceiveDialog from './receive_view/ReceiveDialog.vue';
|
||||||
|
|
||||||
// NOTE: Stores & Type
|
// NOTE: Stores & Type
|
||||||
|
import {
|
||||||
|
TaskOrder,
|
||||||
|
TaskOrderStatus,
|
||||||
|
UserTaskStatus,
|
||||||
|
} from 'src/stores/task-order/types';
|
||||||
import { useNavigator } from 'src/stores/navigator';
|
import { useNavigator } from 'src/stores/navigator';
|
||||||
|
import { useTaskOrderStore } from 'src/stores/task-order';
|
||||||
|
import { useTaskOrderForm } from './form';
|
||||||
import useFlowStore from 'src/stores/flow';
|
import useFlowStore from 'src/stores/flow';
|
||||||
import { pageTabs, column } from './constants';
|
import { pageTabs, column, pageTabsReceive } from './constants';
|
||||||
|
import { isRoleInclude } from 'src/stores/utils';
|
||||||
|
import { PaginationResult } from 'src/types';
|
||||||
|
|
||||||
|
const taskOrderFormStore = useTaskOrderForm();
|
||||||
const navigatorStore = useNavigator();
|
const navigatorStore = useNavigator();
|
||||||
const flowStore = useFlowStore();
|
const flowStore = useFlowStore();
|
||||||
|
const taskOrderStore = useTaskOrderStore();
|
||||||
|
const { stats, pageMax, page, data, pageSize } = storeToRefs(taskOrderStore);
|
||||||
|
|
||||||
// NOTE: Variable
|
// NOTE: Variable
|
||||||
const pageState = reactive({
|
const pageState = reactive({
|
||||||
currentTab: 'title',
|
currentTab: 'Pending',
|
||||||
hideStat: false,
|
hideStat: false,
|
||||||
statusFilter: 'None',
|
statusFilter: 'None',
|
||||||
inputSearch: '',
|
inputSearch: '',
|
||||||
fieldSelected: [...column.map((v) => v.name)],
|
fieldSelected: [...column.map((v) => v.name)],
|
||||||
gridView: false,
|
gridView: false,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
isMessenger: isRoleInclude(['messenger']),
|
||||||
|
|
||||||
|
receiveDialog: false,
|
||||||
|
isReceiveScan: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const taskOrderList = ref<TaskOrder[]>([]);
|
||||||
|
|
||||||
const fieldSelectedOption = computed(() => {
|
const fieldSelectedOption = computed(() => {
|
||||||
return column.map((v) => ({
|
return column.map((v) => ({
|
||||||
label: v.label,
|
label: v.label,
|
||||||
|
|
@ -36,12 +57,118 @@ const fieldSelectedOption = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: Function
|
// NOTE: Function
|
||||||
|
async function fetchTaskOrderList(opts?: { page?: number; pageSize?: number }) {
|
||||||
|
let res: PaginationResult<TaskOrder> | null;
|
||||||
|
|
||||||
|
if (pageState.isMessenger) {
|
||||||
|
res = await taskOrderStore.getUserTaskOrderList({
|
||||||
|
page: opts?.page || page.value,
|
||||||
|
pageSize: opts?.pageSize || pageSize.value,
|
||||||
|
query: pageState.inputSearch === '' ? undefined : pageState.inputSearch,
|
||||||
|
userTaskStatus: pageState.currentTab as UserTaskStatus,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res = await taskOrderStore.getTaskOrderList({
|
||||||
|
page: opts?.page || page.value,
|
||||||
|
pageSize: opts?.pageSize || pageSize.value,
|
||||||
|
query: pageState.inputSearch === '' ? undefined : pageState.inputSearch,
|
||||||
|
taskOrderStatus: pageState.currentTab as TaskOrderStatus | undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (res) {
|
||||||
|
data.value = res.result;
|
||||||
|
pageState.total = res.total;
|
||||||
|
pageMax.value = Math.ceil(res.total / pageSize.value);
|
||||||
|
taskOrderList.value = res.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerTaskOrder(opts: {
|
||||||
|
statusDialog: 'info' | 'edit' | 'create';
|
||||||
|
id?: string;
|
||||||
|
}) {
|
||||||
|
const url = new URL(
|
||||||
|
`/task-order/order/${opts.statusDialog === 'create' ? 'add' : opts.id}`,
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
|
|
||||||
|
window.open(url.toString(), '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerTaskReceive(opts: {
|
||||||
|
statusDialog: 'info' | 'edit';
|
||||||
|
id?: string;
|
||||||
|
}) {
|
||||||
|
const url = new URL(`/task-order/receive/${opts.id}`, window.location.origin);
|
||||||
|
|
||||||
|
window.open(url.toString(), '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitReceiveTask(selectedTask: TaskOrder[]) {
|
||||||
|
const arrId = selectedTask.map((t) => t.id);
|
||||||
|
await taskOrderStore.acceptTaskOrder(arrId);
|
||||||
|
fetchTaskOrderList();
|
||||||
|
pageState.receiveDialog = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openReceiveDialog(scan?: boolean) {
|
||||||
|
if (scan) pageState.isReceiveScan = scan;
|
||||||
|
pageState.receiveDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
navigatorStore.current.title = 'taskOrder.title';
|
navigatorStore.current.title = 'taskOrder.title';
|
||||||
navigatorStore.current.path = [{ text: 'taskOrder.caption', i18n: true }];
|
navigatorStore.current.path = [{ text: 'taskOrder.caption', i18n: true }];
|
||||||
|
fetchTaskOrderList();
|
||||||
|
taskOrderStore.getTaskOrderStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
() => pageState.currentTab,
|
||||||
|
() => pageState.inputSearch,
|
||||||
|
() => pageSize.value,
|
||||||
|
() => pageState.statusFilter,
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
fetchTaskOrderList();
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
<FloatingActionButton
|
||||||
|
style="z-index: 999"
|
||||||
|
:hide-icon="!pageState.isMessenger"
|
||||||
|
@click.stop="
|
||||||
|
() => {
|
||||||
|
if (!pageState.isMessenger) {
|
||||||
|
triggerTaskOrder({ statusDialog: 'create' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<q-fab-action
|
||||||
|
id="add-customer-natural-person"
|
||||||
|
:label="$t('taskOrder.receiveCustom')"
|
||||||
|
external-label
|
||||||
|
label-position="left"
|
||||||
|
style="color: white; background-color: var(--brand-1)"
|
||||||
|
padding="xs"
|
||||||
|
icon="mdi-account-arrow-left-outline"
|
||||||
|
@click="openReceiveDialog()"
|
||||||
|
/>
|
||||||
|
<q-fab-action
|
||||||
|
id="add-customer-legal-entity"
|
||||||
|
style="color: white; background-color: var(--brand-1)"
|
||||||
|
padding="xs"
|
||||||
|
icon="mdi-barcode-scan"
|
||||||
|
:label="$t('taskOrder.receiveScan')"
|
||||||
|
external-label
|
||||||
|
label-position="left"
|
||||||
|
@click="openReceiveDialog(true)"
|
||||||
|
/>
|
||||||
|
</FloatingActionButton>
|
||||||
|
|
||||||
<div class="column full-height no-wrap">
|
<div class="column full-height no-wrap">
|
||||||
<!-- SEC: stat -->
|
<!-- SEC: stat -->
|
||||||
<section class="text-body-2 q-mb-xs flex items-center">
|
<section class="text-body-2 q-mb-xs flex items-center">
|
||||||
|
|
@ -54,9 +181,7 @@ onMounted(async () => {
|
||||||
color: hsl(var(--info-bg));
|
color: hsl(var(--info-bg));
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<!-- TODO: replace stat -->
|
{{ pageState.total }}
|
||||||
0
|
|
||||||
<!-- {{ Object.values(stats).reduce((a, c) => a + c, 0) }} -->
|
|
||||||
</q-badge>
|
</q-badge>
|
||||||
<q-btn
|
<q-btn
|
||||||
class="q-ml-sm"
|
class="q-ml-sm"
|
||||||
|
|
@ -75,43 +200,50 @@ onMounted(async () => {
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div v-if="!pageState.hideStat" class="scroll q-mb-md">
|
<div v-if="!pageState.hideStat" class="scroll q-mb-md">
|
||||||
<div style="display: inline-block">
|
<div style="display: inline-block">
|
||||||
<!-- TODO: replace count -->
|
|
||||||
<StatCardComponent
|
<StatCardComponent
|
||||||
|
v-if="!pageState.isMessenger"
|
||||||
labelI18n
|
labelI18n
|
||||||
:branch="[
|
:branch="[
|
||||||
{
|
{
|
||||||
icon: 'material-symbols-light:receipt-long',
|
icon: 'material-symbols-light:receipt-long',
|
||||||
count: 0,
|
count: stats[TaskOrderStatus.Pending],
|
||||||
label: 'taskOrder.title',
|
label: 'taskOrder.title',
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'material-symbols-light:receipt-long',
|
icon: 'material-symbols-light:receipt-long',
|
||||||
count: 0,
|
count: stats[TaskOrderStatus.InProgress],
|
||||||
label: 'taskOrder.receiveTaskOrder',
|
label: 'taskOrder.inProgress',
|
||||||
color: 'orange',
|
color: 'orange',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'mdi:email-fast-outline',
|
icon: 'mdi:email-fast-outline',
|
||||||
count: 0,
|
count: stats[TaskOrderStatus.Complete],
|
||||||
label: 'taskOrder.sendTaskOrder',
|
|
||||||
color: 'pink',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'tabler:cash-register',
|
|
||||||
count: 0,
|
|
||||||
label: 'taskOrder.payment',
|
|
||||||
color: 'purple',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'fluent:receipt-bag-24-regular',
|
|
||||||
count: 0,
|
|
||||||
label: 'taskOrder.goodReceipt',
|
label: 'taskOrder.goodReceipt',
|
||||||
color: 'light-green',
|
color: 'light-green',
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
:dark="$q.dark.isActive"
|
:dark="$q.dark.isActive"
|
||||||
/>
|
/>
|
||||||
|
<!-- TODO: stat count (messenger) -->
|
||||||
|
<StatCardComponent
|
||||||
|
v-else
|
||||||
|
labelI18n
|
||||||
|
:branch="[
|
||||||
|
{
|
||||||
|
icon: 'material-symbols-light:receipt-long',
|
||||||
|
count: pageState.total,
|
||||||
|
label:
|
||||||
|
pageState.currentTab === UserTaskStatus.Pending
|
||||||
|
? 'taskOrder.taskInCart'
|
||||||
|
: pageState.currentTab === UserTaskStatus.Accept
|
||||||
|
? 'taskOrder.receiveTask'
|
||||||
|
: 'taskOrder.sentTask',
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:dark="$q.dark.isActive"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
@ -142,12 +274,11 @@ onMounted(async () => {
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="row col-12 col-md-5 justify-end"
|
class="row col-12 col-md-3 justify-end"
|
||||||
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||||
style="white-space: nowrap"
|
style="white-space: nowrap"
|
||||||
>
|
>
|
||||||
<!-- TODO: replace status -->
|
<!-- <q-select
|
||||||
<q-select
|
|
||||||
v-model="pageState.statusFilter"
|
v-model="pageState.statusFilter"
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
|
|
@ -166,23 +297,23 @@ onMounted(async () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('requestList.status.Pending'),
|
label: $t('requestList.status.Pending'),
|
||||||
value: '',
|
value: 'Pending',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('requestList.status.InProgress'),
|
label: $t('requestList.status.InProgress'),
|
||||||
value: '',
|
value: 'InProgress',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('requestList.status.Completed'),
|
label: $t('requestList.status.Completed'),
|
||||||
value: '',
|
value: 'Complete',
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
/>
|
/> -->
|
||||||
<q-select
|
<q-select
|
||||||
v-if="!pageState.gridView"
|
v-if="!pageState.gridView"
|
||||||
id="select-field"
|
id="select-field"
|
||||||
for="select-field"
|
for="select-field"
|
||||||
class="col q-ml-sm"
|
class="col"
|
||||||
:options="
|
:options="
|
||||||
fieldSelectedOption.map((v) => ({
|
fieldSelectedOption.map((v) => ({
|
||||||
...v,
|
...v,
|
||||||
|
|
@ -260,7 +391,9 @@ onMounted(async () => {
|
||||||
active-color="info"
|
active-color="info"
|
||||||
>
|
>
|
||||||
<q-tab
|
<q-tab
|
||||||
v-for="tab in pageTabs"
|
v-for="tab in pageState.isMessenger
|
||||||
|
? pageTabsReceive
|
||||||
|
: pageTabs"
|
||||||
:name="tab.value"
|
:name="tab.value"
|
||||||
:key="tab.value"
|
:key="tab.value"
|
||||||
@click="
|
@click="
|
||||||
|
|
@ -289,19 +422,29 @@ onMounted(async () => {
|
||||||
|
|
||||||
<!-- SEC: body content -->
|
<!-- SEC: body content -->
|
||||||
<article
|
<article
|
||||||
v-if="true"
|
v-if="taskOrderList.length === 0"
|
||||||
class="col surface-2 flex items-center justify-center"
|
class="col surface-2 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<NoData :not-found="!!pageState.inputSearch" />
|
<NoData :not-found="!!pageState.inputSearch" />
|
||||||
</article>
|
</article>
|
||||||
<article
|
<article v-else class="col surface-2 full-width scroll q-pa-md">
|
||||||
v-else
|
<TableTaskOrder
|
||||||
class="col surface-2 full-width scroll q-pa-md"
|
:receive="pageState.isMessenger"
|
||||||
></article>
|
:rows="taskOrderList"
|
||||||
|
:columns="column"
|
||||||
|
:grid="pageState.gridView"
|
||||||
|
:visible-columns="pageState.fieldSelected"
|
||||||
|
@view="
|
||||||
|
(v) => {
|
||||||
|
pageState.isMessenger
|
||||||
|
? triggerTaskReceive({ statusDialog: 'info', id: v.id })
|
||||||
|
: triggerTaskOrder({ statusDialog: 'info', id: v.id });
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</article>
|
||||||
|
|
||||||
<!-- TODO: footer -->
|
<footer
|
||||||
<!-- SEC: footer content -->
|
|
||||||
<!-- <footer
|
|
||||||
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
||||||
v-if="pageMax > 0"
|
v-if="pageMax > 0"
|
||||||
>
|
>
|
||||||
|
|
@ -316,8 +459,10 @@ onMounted(async () => {
|
||||||
<div class="col-4 row justify-center app-text-muted">
|
<div class="col-4 row justify-center app-text-muted">
|
||||||
{{
|
{{
|
||||||
$t('general.recordsPage', {
|
$t('general.recordsPage', {
|
||||||
resultcurrentPage: data.length,
|
resultcurrentPage: taskOrderList.length,
|
||||||
total: pageState.inputSearch ? data.length : pageState.total,
|
total: pageState.inputSearch
|
||||||
|
? taskOrderList.length
|
||||||
|
: pageState.total,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -325,12 +470,18 @@ onMounted(async () => {
|
||||||
<PaginationComponent
|
<PaginationComponent
|
||||||
v-model:current-page="page"
|
v-model:current-page="page"
|
||||||
v-model:max-page="pageMax"
|
v-model:max-page="pageMax"
|
||||||
:fetch-data="() => fetchList({ rotateFlowId: true })"
|
:fetch-data="() => fetchTaskOrderList()"
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</footer> -->
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ReceiveDialog
|
||||||
|
@submit="submitReceiveTask"
|
||||||
|
v-model:open="pageState.receiveDialog"
|
||||||
|
:scan="pageState.isReceiveScan"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
|
||||||
302
src/pages/09_task-order/SelectReadyRequestWork.vue
Normal file
302
src/pages/09_task-order/SelectReadyRequestWork.vue
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, reactive, watch, onMounted } from 'vue';
|
||||||
|
import {
|
||||||
|
useRequestList,
|
||||||
|
RequestWork,
|
||||||
|
RequestWorkStatus,
|
||||||
|
} from 'src/stores/request-list';
|
||||||
|
import DialogHeader from 'src/components/dialog/DialogHeader.vue';
|
||||||
|
import CancelButton from 'src/components/button/CancelButton.vue';
|
||||||
|
import SaveButton from 'src/components/button/SaveButton.vue';
|
||||||
|
import DialogFormContainer from 'src/components/dialog/DialogFormContainer.vue';
|
||||||
|
import TableEmployee from './TableEmployee.vue';
|
||||||
|
import FormGroupHead from '../08_request-list/FormGroupHead.vue';
|
||||||
|
import NoData from 'src/components/NoData.vue';
|
||||||
|
|
||||||
|
import { baseUrl } from 'src/stores/utils';
|
||||||
|
import { Task } from 'src/stores/task-order/types';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'select', value: RequestWork[]): void;
|
||||||
|
(e: 'afterSubmit'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const requestListStore = useRequestList();
|
||||||
|
|
||||||
|
const taskList = defineModel<
|
||||||
|
{
|
||||||
|
step: number;
|
||||||
|
requestWorkId: string;
|
||||||
|
requestWorkStep?: Task;
|
||||||
|
}[]
|
||||||
|
>('taskList', {
|
||||||
|
default: [],
|
||||||
|
});
|
||||||
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
|
const selectedEmployee = ref<
|
||||||
|
(RequestWork & {
|
||||||
|
_template?: {
|
||||||
|
id: string;
|
||||||
|
templateName: string;
|
||||||
|
templateStepName: string;
|
||||||
|
step: number;
|
||||||
|
} | null;
|
||||||
|
})[]
|
||||||
|
>([]);
|
||||||
|
let data = ref<RequestWork[]>([]);
|
||||||
|
|
||||||
|
let group = computed(() =>
|
||||||
|
data.value.reduce<
|
||||||
|
{
|
||||||
|
product: RequestWork['productService']['product'];
|
||||||
|
list: RequestWork[];
|
||||||
|
}[]
|
||||||
|
>((acc, curr) => {
|
||||||
|
let exist = acc.find(
|
||||||
|
(item) => curr.productService.productId == item.product.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exist) exist.list.push(curr);
|
||||||
|
else acc.push({ product: curr.productService.product, list: [curr] });
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
|
||||||
|
let state = reactive({
|
||||||
|
search: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(getList);
|
||||||
|
watch(() => state.search, getList);
|
||||||
|
|
||||||
|
async function getList() {
|
||||||
|
let res = await requestListStore.getRequestWorkList({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 99999,
|
||||||
|
query: state.search,
|
||||||
|
readyToTask: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res) return;
|
||||||
|
|
||||||
|
data.value = res.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// function toggle(item: RequestWork) {
|
||||||
|
// switch (selected(item)) {
|
||||||
|
// case true:
|
||||||
|
// return deselect(item);
|
||||||
|
// case false:
|
||||||
|
// return select(item);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function select(item: RequestWork) {
|
||||||
|
// if (selected(item)) return;
|
||||||
|
// selectedEmployee.value = selectedEmployee.value
|
||||||
|
// ? selectedEmployee.value.concat(item)
|
||||||
|
// : [item];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function deselect(item: RequestWork) {
|
||||||
|
// const idx = selectedEmployee.value?.findIndex((v) => v.id === item.id);
|
||||||
|
// if (idx !== -1) selectedEmployee.value?.splice(idx, 1);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function selected(item: RequestWork): boolean {
|
||||||
|
// return !!selectedEmployee.value?.some((v) => v.id === item.id);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
|
||||||
|
function getStep(requestWork: RequestWork) {
|
||||||
|
const target = requestWork.stepStatus.find(
|
||||||
|
(v) => v.workStatus === RequestWorkStatus.Ready,
|
||||||
|
);
|
||||||
|
return target?.step || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTemplateData(requestWork: RequestWork) {
|
||||||
|
const target = getStep(requestWork);
|
||||||
|
if (!target) return null;
|
||||||
|
const flow = requestWork.productService.service?.workflow;
|
||||||
|
if (!flow) return null;
|
||||||
|
const step = flow.step.find((v) => v.order === target);
|
||||||
|
if (!step) return null;
|
||||||
|
return {
|
||||||
|
id: step.id,
|
||||||
|
step: step.order,
|
||||||
|
templateName: flow.name,
|
||||||
|
templateStepName: step.name || '-',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
let selected: {
|
||||||
|
step: number;
|
||||||
|
requestWorkId: string;
|
||||||
|
requestWorkStep?: Task;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
selectedEmployee.value.forEach((v, i) => {
|
||||||
|
const curr = v.stepStatus.find(
|
||||||
|
(s) => s.workStatus === RequestWorkStatus.Ready,
|
||||||
|
);
|
||||||
|
if (curr) {
|
||||||
|
const task: Task = {
|
||||||
|
...curr,
|
||||||
|
attributes: curr.attributes,
|
||||||
|
workStatus: RequestWorkStatus.Ready,
|
||||||
|
taskOrderId: '',
|
||||||
|
requestWork: selectedEmployee.value[i],
|
||||||
|
};
|
||||||
|
selected.push({
|
||||||
|
step: task.step,
|
||||||
|
requestWorkId: task.requestWorkId,
|
||||||
|
requestWorkStep: task,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
taskList.value = selected;
|
||||||
|
emit('afterSubmit');
|
||||||
|
}
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDialogOpen() {
|
||||||
|
selectedEmployee.value = [];
|
||||||
|
if (taskList.value.length === 0) return;
|
||||||
|
const matchingItems = group.value
|
||||||
|
.flatMap((g) => g.list)
|
||||||
|
.filter((l) =>
|
||||||
|
l.stepStatus.some((s) =>
|
||||||
|
taskList.value.some((t) => s.requestWorkId === t.requestWorkId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
selectedEmployee.value = JSON.parse(JSON.stringify(matchingItems));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogFormContainer v-model="open" v-on:open="onDialogOpen">
|
||||||
|
<template #header>
|
||||||
|
<DialogHeader
|
||||||
|
:title="$t('general.list', { msg: $t('productService.title') })"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<section class="col column full-width no-wrap surface-2 scroll">
|
||||||
|
<template v-if="group.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="{ product, list } in group"
|
||||||
|
:key="product.id"
|
||||||
|
class="bordered-b"
|
||||||
|
>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
class="overflow-hidden"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
:default-opened="
|
||||||
|
list.some((v) => selectedEmployee.some((s) => s.id === v.id))
|
||||||
|
"
|
||||||
|
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 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">
|
||||||
|
<TableEmployee
|
||||||
|
checkbox-on
|
||||||
|
step-on
|
||||||
|
:rows="
|
||||||
|
list.map((v) =>
|
||||||
|
Object.assign(v, { _template: getTemplateData(v) }),
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-model:selected-employee="selectedEmployee"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-expansion-item>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="row items-center justify-center full-height">
|
||||||
|
<NoData :text="$t('taskOrder.noRequestAvailable')" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<CancelButton class="q-ml-auto" outlined @click="close" />
|
||||||
|
<SaveButton
|
||||||
|
:label="$t('general.select')"
|
||||||
|
class="q-ml-sm"
|
||||||
|
icon="mdi-check"
|
||||||
|
solid
|
||||||
|
@click="submit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DialogFormContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon) {
|
||||||
|
color: hsl(var(--text-mute));
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(
|
||||||
|
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
|
||||||
|
) {
|
||||||
|
color: var(--brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
464
src/pages/09_task-order/TableEmployee.vue
Normal file
464
src/pages/09_task-order/TableEmployee.vue
Normal file
|
|
@ -0,0 +1,464 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { QTable, QTableProps, QTableSlots } from 'quasar';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||||
|
import ExpirationDate from 'src/components/03_customer-management/ExpirationDate.vue';
|
||||||
|
|
||||||
|
import { baseUrl } from 'src/stores/utils';
|
||||||
|
import { employeeColumn } from './constants';
|
||||||
|
import useOptionStore from 'src/stores/options';
|
||||||
|
import { RequestWork } from 'src/stores/request-list';
|
||||||
|
import { QuotationFull } from 'src/stores/quotations/types';
|
||||||
|
import { dateFormatJS, calculateAge } from 'src/utils/datetime';
|
||||||
|
import { TaskStatus } from 'src/stores/task-order/types';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
checkboxOn?: boolean;
|
||||||
|
checkAll?: boolean;
|
||||||
|
stepOn?: boolean;
|
||||||
|
rows: QTableProps['rows'];
|
||||||
|
grid?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
rows: () => [],
|
||||||
|
grid: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedEmployee = defineModel<
|
||||||
|
(RequestWork & {
|
||||||
|
_template?: {
|
||||||
|
id: string;
|
||||||
|
templateName: string;
|
||||||
|
templateStepName: string;
|
||||||
|
step: number;
|
||||||
|
} | null;
|
||||||
|
})[]
|
||||||
|
>('selectedEmployee', {
|
||||||
|
default: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(
|
||||||
|
e: 'changeAllStatus',
|
||||||
|
payload: {
|
||||||
|
data: (RequestWork & {
|
||||||
|
_template?: {
|
||||||
|
id: string;
|
||||||
|
templateName: string;
|
||||||
|
templateStepName: string;
|
||||||
|
step: number;
|
||||||
|
} | null;
|
||||||
|
})[];
|
||||||
|
status: TaskStatus;
|
||||||
|
},
|
||||||
|
): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function getEmployeeName(
|
||||||
|
record: RequestWork,
|
||||||
|
opts?: {
|
||||||
|
locale?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const employee = record.request.employee;
|
||||||
|
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
['eng']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstNameEN} ${employee?.lastNameEN}`,
|
||||||
|
['tha']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstName} ${employee?.lastName}`,
|
||||||
|
}[opts?.locale || 'eng'] || '-'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToQuotation(quotation: QuotationFull) {
|
||||||
|
const url = new URL('/quotation/view', window.location.origin);
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
'new-quotation',
|
||||||
|
JSON.stringify({
|
||||||
|
customerBranchId: quotation.customerBranchId,
|
||||||
|
agentPrice: quotation.agentPrice,
|
||||||
|
statusDialog: 'info',
|
||||||
|
quotationId: quotation.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
window.open(url.toString(), '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToRequestList(id: string) {
|
||||||
|
const url = new URL(`/request-list/${id}`, window.location.origin);
|
||||||
|
window.open(url.toString(), '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCheckAll() {
|
||||||
|
const arr = JSON.parse(JSON.stringify(props.rows));
|
||||||
|
const selectableTasks = arr.filter(
|
||||||
|
(
|
||||||
|
t: RequestWork & {
|
||||||
|
_template?: {
|
||||||
|
id: string;
|
||||||
|
templateName: string;
|
||||||
|
templateStepName: string;
|
||||||
|
step: number;
|
||||||
|
} | null;
|
||||||
|
taskStatus: TaskStatus;
|
||||||
|
},
|
||||||
|
) =>
|
||||||
|
t.taskStatus !== TaskStatus.Failed &&
|
||||||
|
t.taskStatus !== TaskStatus.Success &&
|
||||||
|
t.taskStatus !== TaskStatus.Complete &&
|
||||||
|
t.taskStatus !== TaskStatus.Redo,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedEmployee.value.length !== selectableTasks.length) {
|
||||||
|
selectedEmployee.value = selectableTasks;
|
||||||
|
} else {
|
||||||
|
selectedEmployee.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCheck(
|
||||||
|
row: RequestWork & {
|
||||||
|
_template?: {
|
||||||
|
id: string;
|
||||||
|
templateName: string;
|
||||||
|
templateStepName: string;
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
taskStatus: TaskStatus;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
row.taskStatus === TaskStatus.Failed ||
|
||||||
|
row.taskStatus === TaskStatus.Success ||
|
||||||
|
row.taskStatus === TaskStatus.Complete ||
|
||||||
|
row.taskStatus === TaskStatus.Redo
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const index = selectedEmployee.value.findIndex((data) => data.id === row.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
selectedEmployee.value.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
selectedEmployee.value.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
row-key="id"
|
||||||
|
v-bind="props"
|
||||||
|
hide-pagination
|
||||||
|
class="full-width"
|
||||||
|
:selection="!checkboxOn ? undefined : 'multiple'"
|
||||||
|
:columns="
|
||||||
|
stepOn
|
||||||
|
? [
|
||||||
|
...employeeColumn.slice(0, 2),
|
||||||
|
{
|
||||||
|
name: 'periodNo',
|
||||||
|
align: 'center',
|
||||||
|
label: 'flow.step',
|
||||||
|
field: (v) => v.product.code,
|
||||||
|
},
|
||||||
|
...employeeColumn.slice(2),
|
||||||
|
]
|
||||||
|
: employeeColumn
|
||||||
|
"
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
:no-data-label="$t('general.noDataTable')"
|
||||||
|
v-model:selected="selectedEmployee"
|
||||||
|
hide-selected-banner
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr
|
||||||
|
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
<q-th v-if="checkboxOn" class="relative-position">
|
||||||
|
<q-checkbox
|
||||||
|
v-if="checkAll"
|
||||||
|
:model-value="
|
||||||
|
selectedEmployee.length > 0 &&
|
||||||
|
selectedEmployee.length ===
|
||||||
|
rows.filter(
|
||||||
|
(t) =>
|
||||||
|
t.taskStatus !== TaskStatus.Complete &&
|
||||||
|
t.taskStatus !== TaskStatus.Success &&
|
||||||
|
t.taskStatus !== TaskStatus.Failed,
|
||||||
|
).length
|
||||||
|
"
|
||||||
|
@click="handleCheckAll"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<div v-if="checkAll" class="absolute-right row items-center">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
rounded
|
||||||
|
icon="mdi-chevron-down"
|
||||||
|
size="sm"
|
||||||
|
class=""
|
||||||
|
>
|
||||||
|
<q-menu :offset="[0, 4]">
|
||||||
|
<q-list dense>
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
class="items-center"
|
||||||
|
@click="
|
||||||
|
$emit('changeAllStatus', {
|
||||||
|
data: selectedEmployee,
|
||||||
|
status: TaskStatus.Success,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
style="color: hsl(var(--positive-bg))"
|
||||||
|
name="mdi-file-check-outline"
|
||||||
|
class="q-pr-sm"
|
||||||
|
size="xs"
|
||||||
|
></q-icon>
|
||||||
|
{{ $t(`taskOrder.status.Success`) }}
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
class="items-center"
|
||||||
|
@click="
|
||||||
|
$emit('changeAllStatus', {
|
||||||
|
data: selectedEmployee,
|
||||||
|
status: TaskStatus.Failed,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
style="color: hsl(var(--negative-bg))"
|
||||||
|
name="mdi-file-remove-outline"
|
||||||
|
class="q-pr-sm"
|
||||||
|
size="xs"
|
||||||
|
></q-icon>
|
||||||
|
{{ $t(`taskOrder.status.Failed`) }}
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-th>
|
||||||
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
{{ col.label && $t(col.label) }}
|
||||||
|
</q-th>
|
||||||
|
<q-th></q-th>
|
||||||
|
<q-th v-if="$slots.append"></q-th>
|
||||||
|
|
||||||
|
<q-th v-if="$slots.action"></q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-slot:body="props: {
|
||||||
|
row: RequestWork & {
|
||||||
|
_template?: {
|
||||||
|
id: string;
|
||||||
|
templateName: string;
|
||||||
|
templateStepName: string;
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
taskStatus: TaskStatus;
|
||||||
|
};
|
||||||
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||||
|
>
|
||||||
|
<q-tr
|
||||||
|
:class="{
|
||||||
|
urgent: props.row.request.quotation.urgent,
|
||||||
|
dark: $q.dark.isActive,
|
||||||
|
['disabled-row']:
|
||||||
|
selectedEmployee.length > 0 &&
|
||||||
|
selectedEmployee.some(
|
||||||
|
(v) => v._template?.id !== props.row._template?.id,
|
||||||
|
),
|
||||||
|
}"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<q-td v-if="checkboxOn">
|
||||||
|
<q-checkbox
|
||||||
|
:model-value="selectedEmployee.some((t) => t.id === props.row.id)"
|
||||||
|
@click="handleCheck(props.row)"
|
||||||
|
size="sm"
|
||||||
|
:disable="
|
||||||
|
props.row.taskStatus === TaskStatus.Failed ||
|
||||||
|
props.row.taskStatus === TaskStatus.Success ||
|
||||||
|
props.row.taskStatus === TaskStatus.Complete ||
|
||||||
|
props.row.taskStatus === TaskStatus.Redo ||
|
||||||
|
(selectedEmployee.length > 0 &&
|
||||||
|
selectedEmployee.some(
|
||||||
|
(v) => v._template?.id !== props.row._template?.id,
|
||||||
|
))
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
{{ props.rowIndex + 1 }}
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<span
|
||||||
|
class="cursor-pointer link"
|
||||||
|
@click="goToRequestList(props.row.request.id)"
|
||||||
|
>
|
||||||
|
{{ props.row.request.code }}
|
||||||
|
</span>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="stepOn" class="text-left">
|
||||||
|
<div v-if="props.row._template" class="column text-left">
|
||||||
|
<span>{{ props.row._template.templateName }}</span>
|
||||||
|
<span class="app-text-muted text-caption">
|
||||||
|
{{ $t('flow.stepNo', { msg: props.row._template.step }) }}
|
||||||
|
{{ props.row._template.templateStepName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<q-avatar class="q-mr-sm" size="md">
|
||||||
|
<q-img
|
||||||
|
class="text-center"
|
||||||
|
:ratio="1"
|
||||||
|
:src="`${baseUrl}/employee/${props.row.request.employee.id}/image/${props.row.request.employee.selectedImage}`"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<span class="full-width full-height">
|
||||||
|
<q-img src="/images/employee-avatar.png" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-img>
|
||||||
|
</q-avatar>
|
||||||
|
<div class="column text-left q-ml-sm">
|
||||||
|
<div>
|
||||||
|
{{ getEmployeeName(props.row, { locale: $i18n.locale }) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app-text-muted">
|
||||||
|
{{ props.row.request.employee.code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
class="q-ml-md"
|
||||||
|
:class="`app-text-${props.row.request.employee.gender}`"
|
||||||
|
:icon="`material-symbols:${props.row.request.employee.gender}`"
|
||||||
|
width="24px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td>{{ calculateAge(props.row.request.employee.dateOfBirth) }}</q-td>
|
||||||
|
<q-td>
|
||||||
|
{{
|
||||||
|
useOptionStore().mapOption(props.row.request.employee.nationality)
|
||||||
|
}}
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
{{
|
||||||
|
dateFormatJS({
|
||||||
|
date: props.row.request.quotation.dueDate,
|
||||||
|
locale: $i18n.locale,
|
||||||
|
dayStyle: '2-digit',
|
||||||
|
monthStyle: 'short',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<ExpirationDate
|
||||||
|
:expiration-date="new Date(props.row.request.quotation.dueDate)"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<span
|
||||||
|
class="cursor-pointer link"
|
||||||
|
@click="goToQuotation(props.row.request.quotation)"
|
||||||
|
>
|
||||||
|
{{ props.row.request.quotation.code }}
|
||||||
|
</span>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<BadgeComponent
|
||||||
|
v-if="props.row.request.quotation.urgent"
|
||||||
|
icon="mdi-fire"
|
||||||
|
:title="$t('general.urgent2')"
|
||||||
|
hsla-color="--gray-1-hsl"
|
||||||
|
hsla-background="--red-8-hsl"
|
||||||
|
solid
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="$slots.append">
|
||||||
|
<slot name="append" :props="props"></slot>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td v-if="$slots.action">
|
||||||
|
<slot name="action" :props="props"></slot>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- <template v-slot:item="props"></template> -->
|
||||||
|
</q-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(tr:nth-child(2n)) {
|
||||||
|
background: #f9fafc;
|
||||||
|
&.dark {
|
||||||
|
background: hsl(var(--gray-11-hsl) / 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-table tr.urgent {
|
||||||
|
background: hsla(var(--red-6-hsl) / 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-table tr.urgent td:first-child {
|
||||||
|
&::after {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 15%;
|
||||||
|
bottom: 15%;
|
||||||
|
background: var(--red-8);
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 99rem;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0% {
|
||||||
|
background: var(--red-8);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background: var(--red-3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background: var(--red-8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: hsl(var(--info-bg));
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-row {
|
||||||
|
opacity: 0.3;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.q-table tbody td:after) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
301
src/pages/09_task-order/TableTaskOrder.vue
Normal file
301
src/pages/09_task-order/TableTaskOrder.vue
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { QTableProps, QTableSlots } from 'quasar';
|
||||||
|
|
||||||
|
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
|
||||||
|
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||||
|
import KebabAction from 'src/components/shared/KebabAction.vue';
|
||||||
|
|
||||||
|
import useOptionStore from 'src/stores/options';
|
||||||
|
import { dateFormatJS, dateFormat } from 'src/utils/datetime';
|
||||||
|
import { TaskOrder, TaskOrderStatus } from 'src/stores/task-order/types';
|
||||||
|
import { column } from './constants';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const selectedTask = defineModel<TaskOrder[]>('selectedTask', { default: [] });
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
rows: QTableProps['rows'];
|
||||||
|
grid?: boolean;
|
||||||
|
visibleColumns?: string[];
|
||||||
|
selection?: 'single' | 'multiple' | 'none';
|
||||||
|
receive?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
rows: () => [],
|
||||||
|
grid: false,
|
||||||
|
receive: false,
|
||||||
|
selection: 'none',
|
||||||
|
visibleColumns: () => [
|
||||||
|
'createdAt',
|
||||||
|
'order',
|
||||||
|
'taskName',
|
||||||
|
'issueBranch',
|
||||||
|
'institution',
|
||||||
|
'createdBy',
|
||||||
|
'contactTel',
|
||||||
|
'contactName',
|
||||||
|
'taskStatus',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentBtnOpen = ref<boolean[]>([]);
|
||||||
|
|
||||||
|
function taskOrderStatus(value: TaskOrderStatus, type: 'status' | 'color') {
|
||||||
|
const mappings: Record<string, Record<'status' | 'color', string>> = {
|
||||||
|
Pending: {
|
||||||
|
status: props.receive ? 'taskOrder.taskInCart' : 'taskOrder.title',
|
||||||
|
color: '--blue-6-hsl',
|
||||||
|
},
|
||||||
|
InProgress: {
|
||||||
|
status: 'taskOrder.inProgress',
|
||||||
|
color: props.receive ? '--blue-6-hsl' : '--orange-5-hsl',
|
||||||
|
},
|
||||||
|
Validate: {
|
||||||
|
status: 'taskOrder.inProgress',
|
||||||
|
color: props.receive ? '--blue-6-hsl' : '--orange-5-hsl',
|
||||||
|
},
|
||||||
|
Complete: {
|
||||||
|
status: props.receive ? 'taskOrder.sentTask' : 'taskOrder.goodReceipt',
|
||||||
|
color: props.receive ? '--blue-6-hsl' : '--green-8-hsl',
|
||||||
|
},
|
||||||
|
Accept: {
|
||||||
|
status: 'taskOrder.receiveTask',
|
||||||
|
color: '--blue-6-hsl',
|
||||||
|
},
|
||||||
|
Submit: {
|
||||||
|
status: 'taskOrder.sentTask',
|
||||||
|
color: '--blue-6-hsl',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return mappings[value]?.[type] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCreatedByName(
|
||||||
|
record: TaskOrder,
|
||||||
|
opts?: {
|
||||||
|
locale?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const createdBy = record.createdBy;
|
||||||
|
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
['eng']: `${useOptionStore().mapOption(createdBy?.namePrefix) || ''} ${createdBy?.firstNameEN} ${createdBy?.lastNameEN}`,
|
||||||
|
['tha']: `${useOptionStore().mapOption(createdBy?.namePrefix) || ''} ${createdBy?.firstName} ${createdBy?.lastName}`,
|
||||||
|
}[opts?.locale || 'eng'] || '-'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openList(index: number, data: TaskOrder) {
|
||||||
|
if (!currentBtnOpen.value[index]) {
|
||||||
|
// currentBtnOpen.value.map((v, i) => {
|
||||||
|
// if (i !== index) {
|
||||||
|
// currentBtnOpen.value[i] = false;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
emit('clickSubRow', index, data);
|
||||||
|
}
|
||||||
|
currentBtnOpen.value[index] = !currentBtnOpen.value[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'view', data: TaskOrder): void;
|
||||||
|
(e: 'clickSubRow', index: number, data: TaskOrder): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-table
|
||||||
|
v-bind="props"
|
||||||
|
:columns="column"
|
||||||
|
bordered
|
||||||
|
flat
|
||||||
|
hide-pagination
|
||||||
|
card-container-class="q-col-gutter-sm"
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
class="full-width"
|
||||||
|
:no-data-label="$t('general.noDataTable')"
|
||||||
|
row-key="id"
|
||||||
|
v-model:selected="selectedTask"
|
||||||
|
hide-selected-banner
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr
|
||||||
|
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
<q-th v-if="selection !== 'none'">
|
||||||
|
<q-checkbox v-model="props.selected" size="sm" />
|
||||||
|
</q-th>
|
||||||
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
{{ col.label && $t(col.label) }}
|
||||||
|
</q-th>
|
||||||
|
<q-th></q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-slot:body="props: {
|
||||||
|
row: TaskOrder;
|
||||||
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||||
|
>
|
||||||
|
<q-tr class="text-center">
|
||||||
|
<q-td v-if="selection !== 'none'">
|
||||||
|
<q-checkbox v-model="props.selected" size="sm" />
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('order')">
|
||||||
|
{{ props.rowIndex + 1 }}
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('taskName')" class="text-left">
|
||||||
|
<div>
|
||||||
|
{{ props.row.taskName || '-' }}
|
||||||
|
<q-tooltip :delay="300">
|
||||||
|
{{ props.row.taskName || '-' }}
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption app-text-muted">
|
||||||
|
{{ props.row.code || '-' }}
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('issueBranch')">-</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('institution')">
|
||||||
|
{{
|
||||||
|
$i18n.locale === 'eng'
|
||||||
|
? props.row.institution.nameEN || '-'
|
||||||
|
: props.row.institution.name || '-'
|
||||||
|
}}
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('createdAt')">
|
||||||
|
{{
|
||||||
|
dateFormatJS({
|
||||||
|
date: props.row.createdAt,
|
||||||
|
dayStyle: '2-digit',
|
||||||
|
monthStyle: '2-digit',
|
||||||
|
}) || '-'
|
||||||
|
}}
|
||||||
|
{{ dateFormat(props.row.createdAt, false, true) }}
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('createdBy')" class="text-left">
|
||||||
|
{{ getCreatedByName(props.row, $i18n) }}
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('contactTel')">
|
||||||
|
{{ props.row.contactTel || '-' }}
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('contactName')" class="text-left">
|
||||||
|
{{ props.row.contactName || '-' }}
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('taskStatus')">
|
||||||
|
<BadgeComponent
|
||||||
|
hide-icon
|
||||||
|
:hsla-color="taskOrderStatus(props.row.taskOrderStatus, 'color')"
|
||||||
|
:title="$t(taskOrderStatus(props.row.taskOrderStatus, 'status'))"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="selection === 'none'">
|
||||||
|
<q-btn
|
||||||
|
:id="`btn-eye-${props.row.taskName}`"
|
||||||
|
icon="mdi-eye-outline"
|
||||||
|
size="sm"
|
||||||
|
dense
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
@click.stop="$emit('view', props.row)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<KebabAction
|
||||||
|
v-if="false"
|
||||||
|
:idName="`btn-kebab-${props.row.taskName}`"
|
||||||
|
status="'ACTIVE'"
|
||||||
|
hide-toggle
|
||||||
|
:hide-edit="true"
|
||||||
|
@view="$emit('view', props.row)"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-else>
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
class="rounded"
|
||||||
|
@click.stop="
|
||||||
|
() => {
|
||||||
|
openList(props.rowIndex, props.row);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<q-icon name="mdi-account-group-outline" />
|
||||||
|
<q-icon
|
||||||
|
class="btn-arrow-right"
|
||||||
|
:class="{
|
||||||
|
active: currentBtnOpen[props.rowIndex],
|
||||||
|
}"
|
||||||
|
size="xs"
|
||||||
|
:name="`mdi-chevron-${currentBtnOpen[props.rowIndex] ? 'down' : 'up'}`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
|
||||||
|
<q-tr v-show="currentBtnOpen[props.rowIndex]" :props="props">
|
||||||
|
<q-td colspan="100%" style="padding: 16px">
|
||||||
|
<slot name="subRow" :props="props"></slot>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item="props">
|
||||||
|
<div class="col-md-4 col-sm-6 col-12">
|
||||||
|
<!-- TODO: status -->
|
||||||
|
<QuotationCard
|
||||||
|
:status="$t(taskOrderStatus(props.row.taskOrderStatus, 'status'))"
|
||||||
|
:badge-color="taskOrderStatus(props.row.taskOrderStatus, 'color')"
|
||||||
|
hide-action
|
||||||
|
hidePreview
|
||||||
|
:code="props.row.code"
|
||||||
|
:title="props.row.taskName"
|
||||||
|
:custom-data="[
|
||||||
|
{
|
||||||
|
label: $t('taskOrder.issueBranch'),
|
||||||
|
value: props.row.issueBranch,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('general.agencies'),
|
||||||
|
value:
|
||||||
|
$i18n.locale === 'eng'
|
||||||
|
? props.row.institution.nameEN
|
||||||
|
: props.row.institution.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('taskOrder.issueDate'),
|
||||||
|
value: `${dateFormatJS({
|
||||||
|
date: props.row.createdAt,
|
||||||
|
dayStyle: '2-digit',
|
||||||
|
monthStyle: '2-digit',
|
||||||
|
})} ${dateFormat(props.row.createdAt, false, true)}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('taskOrder.madeBy'),
|
||||||
|
value: getCreatedByName(props.row, $i18n),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('general.telephone'),
|
||||||
|
value: props.row.contactTel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('taskOrder.contactName'),
|
||||||
|
value: props.row.contactName,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
@view="$emit('view', props.row)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
:deep(.q-table tbody td:after) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
176
src/pages/09_task-order/TaskStatusComponent.vue
Normal file
176
src/pages/09_task-order/TaskStatusComponent.vue
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { TaskStatus } from 'src/stores/task-order/types';
|
||||||
|
import { taskStatusReceiveToggle, taskStatusOrderToggle } from './constants';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
noAction?: boolean;
|
||||||
|
type?: 'order' | 'receive';
|
||||||
|
status: string | TaskStatus | undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'changeStatus', status: TaskStatus): void;
|
||||||
|
(e: 'clickFailed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const statusList = [
|
||||||
|
...taskStatusReceiveToggle,
|
||||||
|
...taskStatusOrderToggle,
|
||||||
|
{
|
||||||
|
value: TaskStatus.Validate,
|
||||||
|
icon: 'mdi-file-check-outline',
|
||||||
|
color: 'positive',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const currStatus = computed(() =>
|
||||||
|
statusList.find((v) => v.value === props.status),
|
||||||
|
);
|
||||||
|
|
||||||
|
function hideIcon() {
|
||||||
|
if (props.noAction) return props.noAction;
|
||||||
|
if (props.type === 'order') {
|
||||||
|
return (
|
||||||
|
currStatus.value?.value === TaskStatus.InProgress ||
|
||||||
|
currStatus.value?.value === TaskStatus.Redo ||
|
||||||
|
currStatus.value?.value === TaskStatus.Success ||
|
||||||
|
currStatus.value?.value === TaskStatus.Complete ||
|
||||||
|
currStatus.value?.value === TaskStatus.Canceled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.type === 'receive') {
|
||||||
|
return (
|
||||||
|
currStatus.value?.value === TaskStatus.Failed ||
|
||||||
|
currStatus.value?.value === TaskStatus.Success ||
|
||||||
|
currStatus.value?.value === TaskStatus.Complete ||
|
||||||
|
currStatus.value?.value === TaskStatus.Redo ||
|
||||||
|
currStatus.value?.value === TaskStatus.Validate ||
|
||||||
|
currStatus.value?.value === TaskStatus.Canceled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="readonly"
|
||||||
|
class="row rounded bordered surface-2 items-center justify-center q-pa-xs no-wrap"
|
||||||
|
:style="`color: hsl(var(--${currStatus?.color}-bg))`"
|
||||||
|
>
|
||||||
|
<q-icon :name="currStatus?.icon" class="q-pr-xs" size="xs" />
|
||||||
|
{{ $t(`taskOrder.status.${status}`) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="row items-center justify-center no-wrap">
|
||||||
|
<q-btn-dropdown
|
||||||
|
dense
|
||||||
|
unelevated
|
||||||
|
:label="
|
||||||
|
currStatus?.value === TaskStatus.Validate && type === 'order'
|
||||||
|
? $t('taskOrder.done')
|
||||||
|
: $t(`taskOrder.status.${status}`)
|
||||||
|
"
|
||||||
|
class="text-capitalize text-weight-regular product-status rounded"
|
||||||
|
:class="{
|
||||||
|
'hide-icon q-pr-md': hideIcon(),
|
||||||
|
warning:
|
||||||
|
currStatus?.value === TaskStatus.Pending ||
|
||||||
|
currStatus?.value === TaskStatus.InProgress,
|
||||||
|
positive:
|
||||||
|
currStatus?.value === TaskStatus.Complete ||
|
||||||
|
currStatus?.value === TaskStatus.Success ||
|
||||||
|
currStatus?.value === TaskStatus.Validate,
|
||||||
|
negative:
|
||||||
|
currStatus?.value === TaskStatus.Failed ||
|
||||||
|
currStatus?.value === TaskStatus.Redo ||
|
||||||
|
currStatus?.value === TaskStatus.Canceled,
|
||||||
|
'pointer-events-none': {
|
||||||
|
order: !['Success', 'Failed', 'Validate'].includes(status || ''),
|
||||||
|
receive: status !== TaskStatus.InProgress,
|
||||||
|
}[type ?? 'order'],
|
||||||
|
}"
|
||||||
|
:menu-offset="[0, 8]"
|
||||||
|
dropdown-icon="mdi-chevron-down"
|
||||||
|
content-class="bordered rounded"
|
||||||
|
@click.stop
|
||||||
|
:icon="currStatus?.icon"
|
||||||
|
>
|
||||||
|
<q-list v-if="!noAction" dense>
|
||||||
|
<q-item
|
||||||
|
v-for="(v, index) in type === 'order'
|
||||||
|
? {
|
||||||
|
Success: taskStatusOrderToggle.filter(
|
||||||
|
(v) => v.value === TaskStatus.Complete,
|
||||||
|
),
|
||||||
|
Failed: taskStatusOrderToggle.filter(
|
||||||
|
(v) => v.value === TaskStatus.Redo,
|
||||||
|
),
|
||||||
|
Validate: taskStatusOrderToggle.filter(
|
||||||
|
(v) => v.value !== TaskStatus.InProgress,
|
||||||
|
),
|
||||||
|
}[status ?? '']
|
||||||
|
: {
|
||||||
|
InProgress: taskStatusReceiveToggle.filter((v) =>
|
||||||
|
[TaskStatus.Success, TaskStatus.Failed].includes(v.value),
|
||||||
|
),
|
||||||
|
}[status ?? '']"
|
||||||
|
:key="index"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="$emit('changeStatus', v.value)"
|
||||||
|
class="items-center"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:style="`color: hsl(var(--${v.color}-bg))`"
|
||||||
|
:name="v.icon"
|
||||||
|
class="q-pr-sm"
|
||||||
|
size="xs"
|
||||||
|
></q-icon>
|
||||||
|
{{ $t(`taskOrder.status.${v.value}`) }}
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-if="currStatus?.value === TaskStatus.Failed"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
rounded
|
||||||
|
color="negative"
|
||||||
|
icon="mdi-shield-alert-outline"
|
||||||
|
@click="$emit('clickFailed')"
|
||||||
|
/>
|
||||||
|
<div style="width: 31.94px" v-else></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.product-status {
|
||||||
|
padding-left: 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: hsl(var(--_color));
|
||||||
|
background: hsla(var(--_color) / 0.15);
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
--_color: var(--warning-bg);
|
||||||
|
}
|
||||||
|
&.positive {
|
||||||
|
--_color: var(--positive-bg);
|
||||||
|
}
|
||||||
|
&.negative {
|
||||||
|
--_color: var(--negative-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer-events-none {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(
|
||||||
|
.hide-icon
|
||||||
|
i.q-icon.mdi.mdi-chevron-down.q-btn-dropdown__arrow.q-btn-dropdown__arrow-container
|
||||||
|
) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,11 +1,99 @@
|
||||||
import { QTableProps } from 'quasar';
|
import { QTableProps } from 'quasar';
|
||||||
|
import { TaskStatus } from 'src/stores/task-order/types';
|
||||||
|
|
||||||
|
export const taskStatusOpts = [
|
||||||
|
{
|
||||||
|
status: TaskStatus.Pending,
|
||||||
|
name: 'taskOrder.status.Pending',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: TaskStatus.InProgress,
|
||||||
|
name: 'taskOrder.status.InProgress',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: TaskStatus.Success,
|
||||||
|
name: 'taskOrder.status.Success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: TaskStatus.Failed,
|
||||||
|
name: 'taskOrder.status.Failed',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
status: TaskStatus.Redo,
|
||||||
|
name: 'taskOrder.status.Redo',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
status: TaskStatus.Validate,
|
||||||
|
name: 'taskOrder.status.Validate',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
status: TaskStatus.Complete,
|
||||||
|
name: 'taskOrder.status.Complete',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const pageTabs = [
|
export const pageTabs = [
|
||||||
{ label: 'title', value: 'title' },
|
{ label: 'title', value: 'Pending' },
|
||||||
{ label: 'receiveTaskOrder', value: 'receiveTaskOrder' },
|
{ label: 'inProgress', value: 'InProgress' },
|
||||||
{ label: 'sendTaskOrder', value: 'sendTaskOrder' },
|
{ label: 'goodReceipt', value: 'Complete' },
|
||||||
{ label: 'payment', value: 'payment' },
|
];
|
||||||
{ label: 'goodReceipt', value: 'goodReceipt' },
|
|
||||||
|
export const pageTabsReceive = [
|
||||||
|
{ label: 'taskInCart', value: 'Pending' },
|
||||||
|
{ label: 'receiveTask', value: 'Accept' },
|
||||||
|
{ label: 'sentTask', value: 'Submit' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export enum Status {
|
||||||
|
taskOrder = 'taskOrder',
|
||||||
|
receiveTaskOrder = 'receiveTaskOrder',
|
||||||
|
sendTaskOrder = 'sendTaskOrder',
|
||||||
|
payment = 'payment',
|
||||||
|
goodReceipt = 'goodReceipt',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const taskStatusReceiveToggle = [
|
||||||
|
{
|
||||||
|
value: TaskStatus.Pending,
|
||||||
|
icon: 'mdi-file-move-outline',
|
||||||
|
color: 'warning',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: TaskStatus.Success,
|
||||||
|
icon: 'mdi-file-check-outline',
|
||||||
|
color: 'positive',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: TaskStatus.Failed,
|
||||||
|
icon: 'mdi-file-remove-outline',
|
||||||
|
color: 'negative',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const taskStatusOrderToggle = [
|
||||||
|
{
|
||||||
|
value: TaskStatus.InProgress,
|
||||||
|
icon: 'mdi-file-move-outline',
|
||||||
|
color: 'positive',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: TaskStatus.Complete,
|
||||||
|
icon: 'mdi-file-check-outline',
|
||||||
|
color: 'positive',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: TaskStatus.Redo,
|
||||||
|
icon: 'mdi-file-remove-outline',
|
||||||
|
color: 'negative',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: TaskStatus.Canceled,
|
||||||
|
icon: 'mdi-file-remove-outline',
|
||||||
|
color: 'negative',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const column = [
|
export const column = [
|
||||||
|
|
@ -16,40 +104,40 @@ export const column = [
|
||||||
field: 'no',
|
field: 'no',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'taskOrder',
|
name: 'taskName',
|
||||||
align: 'left',
|
align: 'center',
|
||||||
label: 'taskOrder.title',
|
label: 'taskOrder.title',
|
||||||
field: 'taskOrder',
|
field: 'taskName',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'issueBranch',
|
name: 'issueBranch',
|
||||||
align: 'left',
|
align: 'center',
|
||||||
label: 'taskOrder.issueBranch',
|
label: 'taskOrder.issueBranch',
|
||||||
field: 'issueBranch',
|
field: 'issueBranch',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'agencies',
|
name: 'institution',
|
||||||
align: 'left',
|
align: 'center',
|
||||||
label: 'general.agencies',
|
label: 'general.agencies',
|
||||||
field: 'agencies',
|
field: 'institution',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'issueDate',
|
name: 'createdAt',
|
||||||
align: 'left',
|
align: 'center',
|
||||||
label: 'taskOrder.issueDate',
|
label: 'taskOrder.issueDate',
|
||||||
field: 'issueDate',
|
field: 'createdAt',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'madeBy',
|
name: 'createdBy',
|
||||||
align: 'left',
|
align: 'center',
|
||||||
label: 'taskOrder.madeBy',
|
label: 'taskOrder.madeBy',
|
||||||
field: 'madeBy',
|
field: 'createdBy',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'telephone',
|
name: 'contactTel',
|
||||||
align: 'left',
|
align: 'center',
|
||||||
label: 'general.telephone',
|
label: 'general.telephone',
|
||||||
field: 'telephone',
|
field: 'contactTel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'contactName',
|
name: 'contactName',
|
||||||
|
|
@ -58,9 +146,174 @@ export const column = [
|
||||||
field: 'contactName',
|
field: 'contactName',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'status',
|
name: 'taskStatus',
|
||||||
align: 'left',
|
align: 'center',
|
||||||
label: 'general.status',
|
label: 'general.status',
|
||||||
field: 'status',
|
field: 'taskStatus',
|
||||||
},
|
},
|
||||||
];
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
||||||
|
export const employeeColumn = [
|
||||||
|
{
|
||||||
|
name: 'order',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.order',
|
||||||
|
field: 'no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
align: 'center',
|
||||||
|
label: 'requestList.requestListCode',
|
||||||
|
field: 'code',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fullName',
|
||||||
|
align: 'center',
|
||||||
|
label: 'quotation.employeeName',
|
||||||
|
field: 'fullName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dateOfBirth',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.age',
|
||||||
|
field: 'dateOfBirth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nationality',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.nationality',
|
||||||
|
field: 'nationality',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dueDate',
|
||||||
|
align: 'center',
|
||||||
|
label: 'quotation.documentExpireDate',
|
||||||
|
field: 'dueDate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'day',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.numberOfDay',
|
||||||
|
field: 'day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quotationCode',
|
||||||
|
align: 'center',
|
||||||
|
label: 'requestList.quotationCode',
|
||||||
|
field: 'quotationCode',
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
||||||
|
export const productColumn = [
|
||||||
|
{
|
||||||
|
name: 'order',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.order',
|
||||||
|
field: 'no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
align: 'center',
|
||||||
|
label: 'productService.product.code',
|
||||||
|
field: 'code',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'productList',
|
||||||
|
align: 'center',
|
||||||
|
label: 'taskOrder.productList',
|
||||||
|
field: 'productList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amountOfEmployee',
|
||||||
|
align: 'center',
|
||||||
|
label: 'taskOrder.amountOfEmployee',
|
||||||
|
field: 'amountOfEmployee',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pricePerUnit',
|
||||||
|
align: 'center',
|
||||||
|
label: 'quotation.pricePerUnit',
|
||||||
|
field: 'pricePerUnit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'discount',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.discount',
|
||||||
|
field: 'discount',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'priceBeforeVat',
|
||||||
|
align: 'center',
|
||||||
|
label: 'quotation.priceBeforeVat',
|
||||||
|
field: 'priceBeforeVat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vat',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.vat',
|
||||||
|
field: 'vat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'totalPriceBaht',
|
||||||
|
align: 'center',
|
||||||
|
label: 'quotation.totalPriceBaht',
|
||||||
|
field: 'totalPriceBaht',
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
||||||
|
export const receiveProductColumn = [
|
||||||
|
{
|
||||||
|
name: 'order',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.order',
|
||||||
|
field: 'order',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'requestListCode',
|
||||||
|
align: 'center',
|
||||||
|
label: 'requestList.requestListCode',
|
||||||
|
field: 'requestListCode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'flow',
|
||||||
|
align: 'center',
|
||||||
|
label: 'flow.step',
|
||||||
|
field: 'flow',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quotation',
|
||||||
|
align: 'center',
|
||||||
|
label: 'quotation.employeeName',
|
||||||
|
field: 'quotation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'age',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.age',
|
||||||
|
field: 'age',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nationality',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.nationality',
|
||||||
|
field: 'nationality',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'documentExpireDate',
|
||||||
|
align: 'center',
|
||||||
|
label: 'quotation.documentExpireDate',
|
||||||
|
field: 'documentExpireDate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'numberOfDay',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.numberOfDay',
|
||||||
|
field: 'numberOfDay',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quotationCode',
|
||||||
|
align: 'center',
|
||||||
|
label: 'requestList.quotationCode',
|
||||||
|
field: 'quotationCode',
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
|
||||||
519
src/pages/09_task-order/document_view/MainPage.vue
Normal file
519
src/pages/09_task-order/document_view/MainPage.vue
Normal file
|
|
@ -0,0 +1,519 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { onMounted, nextTick, ref } from 'vue';
|
||||||
|
import ThaiBahtText from 'thai-baht-text';
|
||||||
|
|
||||||
|
// NOTE: Import stores
|
||||||
|
import { formatNumberDecimal } from 'stores/utils';
|
||||||
|
import { useConfigStore } from 'stores/config';
|
||||||
|
import { precisionRound } from 'src/utils/arithmetic';
|
||||||
|
|
||||||
|
// NOTE Import Types
|
||||||
|
import { Branch } from 'stores/branch/types';
|
||||||
|
|
||||||
|
// NOTE: Import Components
|
||||||
|
import ViewHeader from './ViewHeader.vue';
|
||||||
|
import ViewFooter from './ViewFooter.vue';
|
||||||
|
import PrintButton from 'src/components/button/PrintButton.vue';
|
||||||
|
import {
|
||||||
|
TaskOrder,
|
||||||
|
TaskOrderStatus,
|
||||||
|
TaskStatus,
|
||||||
|
} from 'src/stores/task-order/types';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useTaskOrderStore } from 'src/stores/task-order';
|
||||||
|
import { RequestWork } from 'src/stores/request-list';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const taskOrder = useTaskOrderStore();
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
const config = storeToRefs(configStore).data;
|
||||||
|
|
||||||
|
type Data = TaskOrder;
|
||||||
|
|
||||||
|
type ProductItem = {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
detail: string;
|
||||||
|
amount: number;
|
||||||
|
priceUnit: number;
|
||||||
|
discount: number;
|
||||||
|
vat: number;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SummaryPrice = {
|
||||||
|
totalPrice: number;
|
||||||
|
totalDiscount: number;
|
||||||
|
vat: number;
|
||||||
|
vatExcluded: number;
|
||||||
|
finalPrice: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const branch = ref<Branch>();
|
||||||
|
const product = ref<ProductItem[]>([]);
|
||||||
|
|
||||||
|
const elements = ref<HTMLElement[]>([]);
|
||||||
|
const chunks = ref<ProductItem[][]>([[]]);
|
||||||
|
const data = ref<Data>();
|
||||||
|
|
||||||
|
const summaryPrice = ref<SummaryPrice>({
|
||||||
|
totalPrice: 0,
|
||||||
|
totalDiscount: 0,
|
||||||
|
vat: 0,
|
||||||
|
vatExcluded: 0,
|
||||||
|
finalPrice: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function assignData() {
|
||||||
|
for (let i = 0; i < product.value.length; i++) {
|
||||||
|
let el = elements.value.at(-1);
|
||||||
|
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
if (getHeight(el) < 500) {
|
||||||
|
chunks.value.at(-1)?.push(product.value[i]);
|
||||||
|
} else {
|
||||||
|
chunks.value.push([]);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeight(el: HTMLElement) {
|
||||||
|
const shadow = document.createElement('div');
|
||||||
|
|
||||||
|
shadow.style.opacity = '0';
|
||||||
|
shadow.style.position = 'absolute';
|
||||||
|
shadow.style.top = '-999999px';
|
||||||
|
shadow.style.left = '-999999px';
|
||||||
|
shadow.style.pointerEvents = 'none';
|
||||||
|
|
||||||
|
document.body.appendChild(shadow);
|
||||||
|
|
||||||
|
shadow.appendChild(el.cloneNode(true));
|
||||||
|
|
||||||
|
const height = shadow.offsetHeight;
|
||||||
|
|
||||||
|
document.body.removeChild(shadow);
|
||||||
|
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'task-order-preview';
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (route.params['id'] && typeof route.params['id'] === 'string') {
|
||||||
|
const jsonObject = await taskOrder.getTaskOrderById(route.params['id']);
|
||||||
|
|
||||||
|
if (!jsonObject) return;
|
||||||
|
|
||||||
|
data.value = jsonObject;
|
||||||
|
branch.value = jsonObject.registeredBranch;
|
||||||
|
} else {
|
||||||
|
let jsonString: string | null;
|
||||||
|
|
||||||
|
jsonString = localStorage.getItem(STORAGE_KEY);
|
||||||
|
jsonString = jsonString || sessionStorage.getItem(STORAGE_KEY);
|
||||||
|
|
||||||
|
if (!jsonString) return;
|
||||||
|
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
|
||||||
|
const jsonObject: Data = JSON.parse(jsonString);
|
||||||
|
|
||||||
|
if (jsonObject) sessionStorage.setItem(STORAGE_KEY, jsonString);
|
||||||
|
|
||||||
|
data.value = jsonObject;
|
||||||
|
branch.value = jsonObject.registeredBranch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskListGroup = data.value?.taskList.reduce<
|
||||||
|
{
|
||||||
|
product: RequestWork['productService']['product'];
|
||||||
|
list: RequestWork[];
|
||||||
|
}[]
|
||||||
|
>((acc, curr) => {
|
||||||
|
if (
|
||||||
|
data.value?.taskOrderStatus === TaskOrderStatus.Complete &&
|
||||||
|
curr.taskStatus !== TaskStatus.Complete
|
||||||
|
) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = curr.requestWorkStep;
|
||||||
|
|
||||||
|
if (!task) return acc;
|
||||||
|
|
||||||
|
if (task.requestWork) {
|
||||||
|
let exist = acc.find(
|
||||||
|
(item) => task.requestWork.productService.productId == item.product.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exist) {
|
||||||
|
exist.list.push(task.requestWork);
|
||||||
|
} else {
|
||||||
|
acc.push({
|
||||||
|
product: task.requestWork.productService.product,
|
||||||
|
list: [task.requestWork],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
product.value = [];
|
||||||
|
summaryPrice.value = taskListGroup
|
||||||
|
.map((v) => ({
|
||||||
|
product: v.product,
|
||||||
|
pricePerUnit: v.product.serviceCharge,
|
||||||
|
discount:
|
||||||
|
data.value?.taskProduct.find(
|
||||||
|
({ productId }) => productId === v.product.id,
|
||||||
|
)?.discount || 0,
|
||||||
|
amount: v.list.length,
|
||||||
|
}))
|
||||||
|
.reduce(
|
||||||
|
(a, c) => {
|
||||||
|
const price = precisionRound(c.pricePerUnit * c.amount);
|
||||||
|
const vat =
|
||||||
|
precisionRound(
|
||||||
|
(c.pricePerUnit * (c.discount ? c.amount : 1) - c.discount) *
|
||||||
|
(config.value?.vat || 0.07),
|
||||||
|
) * (!c.discount ? c.amount : 1);
|
||||||
|
|
||||||
|
product.value.push({
|
||||||
|
id: c.product.id,
|
||||||
|
code: c.product.code,
|
||||||
|
detail: c.product.name,
|
||||||
|
priceUnit: c.pricePerUnit,
|
||||||
|
amount: c.amount,
|
||||||
|
discount: c.discount,
|
||||||
|
vat,
|
||||||
|
value: price * c.amount + vat,
|
||||||
|
});
|
||||||
|
|
||||||
|
a.totalPrice = precisionRound(a.totalPrice + price);
|
||||||
|
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assignData();
|
||||||
|
});
|
||||||
|
|
||||||
|
function print() {
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="toolbar">
|
||||||
|
<PrintButton solid @click="print" />
|
||||||
|
</div>
|
||||||
|
<div class="row justify-between container color-task">
|
||||||
|
<section class="content" v-for="chunk in chunks">
|
||||||
|
<ViewHeader
|
||||||
|
v-if="!!branch && data"
|
||||||
|
:branch="branch"
|
||||||
|
:institution="data.institution"
|
||||||
|
:details="{
|
||||||
|
code: data.code,
|
||||||
|
name: data.taskName,
|
||||||
|
contactName: data.contactName,
|
||||||
|
contactTel: data.contactTel,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="q-mb-sm q-mt-md"
|
||||||
|
style="
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--main);
|
||||||
|
display: block;
|
||||||
|
border-bottom: 2px solid var(--main);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ $t('taskOrder.goodReceipt') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<table ref="elements" class="q-mb-sm" cellpadding="0" style="width: 100%">
|
||||||
|
<tbody class="color-tr">
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('preview.rank') }}</th>
|
||||||
|
<th>{{ $t('preview.productCode') }}</th>
|
||||||
|
<th>{{ $t('general.detail') }}</th>
|
||||||
|
<th>{{ $t('general.amount') }}</th>
|
||||||
|
<th>{{ $t('preview.pricePerUnit') }}</th>
|
||||||
|
<th>{{ $t('preview.discount') }}</th>
|
||||||
|
<th>{{ $t('preview.vat') }}</th>
|
||||||
|
<th>{{ $t('preview.value') }}</th>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="(v, i) in chunk">
|
||||||
|
<td class="text-center">{{ i + 1 }}</td>
|
||||||
|
<td>{{ v.code }}</td>
|
||||||
|
<td>{{ v.detail }}</td>
|
||||||
|
<td style="text-align: right">{{ v.amount }}</td>
|
||||||
|
<td style="text-align: right">
|
||||||
|
{{ formatNumberDecimal(v.priceUnit, 2) }}
|
||||||
|
</td>
|
||||||
|
<td style="text-align: right">
|
||||||
|
{{ formatNumberDecimal(v.discount, 2) }}
|
||||||
|
</td>
|
||||||
|
<td style="text-align: right">
|
||||||
|
{{ formatNumberDecimal(v.vat, 2) }}
|
||||||
|
</td>
|
||||||
|
<td style="text-align: right">
|
||||||
|
{{ formatNumberDecimal(v.value, 2) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table
|
||||||
|
style="width: 40%; margin-left: auto"
|
||||||
|
class="q-mb-md"
|
||||||
|
cellpadding="0"
|
||||||
|
>
|
||||||
|
<tbody class="color-tr">
|
||||||
|
<tr>
|
||||||
|
<td>{{ $t('general.total') }}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{{ formatNumberDecimal(summaryPrice.totalPrice, 2) }}
|
||||||
|
฿
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>{{ $t('general.discount') }}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{{ formatNumberDecimal(summaryPrice.totalDiscount, 2) || 0 }} ฿
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ $t('general.totalAfterDiscount') }}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{{
|
||||||
|
formatNumberDecimal(
|
||||||
|
summaryPrice.totalPrice - summaryPrice.totalDiscount,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
฿
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>{{ $t('general.totalVatExcluded') }}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{{ formatNumberDecimal(summaryPrice.vatExcluded, 2) || 0 }}
|
||||||
|
฿
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ $t('general.vat', { msg: '7%' }) }}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{{ formatNumberDecimal(summaryPrice.vat, 2) }} ฿
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ $t('general.totalVatIncluded') }}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{{
|
||||||
|
formatNumberDecimal(
|
||||||
|
summaryPrice.totalPrice -
|
||||||
|
summaryPrice.totalDiscount +
|
||||||
|
summaryPrice.vat,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
฿
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="row justify-between q-mb-md" style="width: 100%">
|
||||||
|
<div
|
||||||
|
class="column set-width bg-color full-height"
|
||||||
|
style="padding: 12px"
|
||||||
|
>
|
||||||
|
({{ ThaiBahtText(summaryPrice.finalPrice) }})
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="row text-right border-5 items-center"
|
||||||
|
style="width: 40%; background: var(--main); padding: 8px"
|
||||||
|
>
|
||||||
|
<span style="color: white; font-weight: 600">ยอดรวมสุทธิ</span>
|
||||||
|
<span
|
||||||
|
class="border-5"
|
||||||
|
style="
|
||||||
|
width: 70%;
|
||||||
|
margin-left: auto;
|
||||||
|
background: white;
|
||||||
|
padding: 4px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
formatNumberDecimal(Math.max(summaryPrice.finalPrice, 0), 2) || 0
|
||||||
|
}}
|
||||||
|
฿
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="content" v-if="data">
|
||||||
|
<ViewHeader
|
||||||
|
v-if="!!branch"
|
||||||
|
:branch="branch"
|
||||||
|
:institution="data.institution"
|
||||||
|
:details="{
|
||||||
|
code: data.code,
|
||||||
|
name: data.taskName,
|
||||||
|
contactName: data.contactName,
|
||||||
|
contactTel: data.contactTel,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<article style="height: 5.8in"></article>
|
||||||
|
|
||||||
|
<ViewFooter
|
||||||
|
:data="{
|
||||||
|
name: '',
|
||||||
|
company: branch?.name || '',
|
||||||
|
buyer: '',
|
||||||
|
buyDate: '',
|
||||||
|
approveDate: '',
|
||||||
|
approver: '',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.color-task {
|
||||||
|
--main: var(--yellow-6);
|
||||||
|
--main-hsl: var(--yellow-6-hsl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
width: 100%;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: white;
|
||||||
|
border-bottom: 1px solid var(--gray-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: var(--main);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-5 {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.set-width {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-color {
|
||||||
|
background-color: hsla(var(--main-hsl) / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-tr > tr:nth-child(odd) {
|
||||||
|
background-color: hsla(var(--main-hsl) / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-inline: auto;
|
||||||
|
background: var(--gray-3);
|
||||||
|
width: calc(8.3in + 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container :deep(*) {
|
||||||
|
font-size: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5in;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
page-break-after: always;
|
||||||
|
break-after: page;
|
||||||
|
height: 11.7in;
|
||||||
|
max-height: 11.7in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-bottom {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-note {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
& > * {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--main);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.toolbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0;
|
||||||
|
gap: 0;
|
||||||
|
width: 100%;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 0;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
98
src/pages/09_task-order/document_view/ViewFooter.vue
Normal file
98
src/pages/09_task-order/document_view/ViewFooter.vue
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
data?: {
|
||||||
|
name: string;
|
||||||
|
buyer: string;
|
||||||
|
buyDate: string;
|
||||||
|
|
||||||
|
company: string;
|
||||||
|
approver: string;
|
||||||
|
approveDate: string;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="footer-container">
|
||||||
|
<div class="footer-top">
|
||||||
|
<div>ในนาม {{ data?.name || '-' }}</div>
|
||||||
|
<div>ในนาม {{ data?.company || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img src="/images/jws-stamp.png" alt="${0}" />
|
||||||
|
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<span class="data-placeholder"></span>
|
||||||
|
<span>ผู้สั่งซื้อสินค้า</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="data-placeholder"></span>
|
||||||
|
<span>วันที่</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<span class="data-placeholder"></span>
|
||||||
|
<span>ผู้อนุมัติ</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="data-placeholder"></span>
|
||||||
|
<span>วันที่</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer-container {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
justify-content: center;
|
||||||
|
height: 1.5in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-top {
|
||||||
|
position: absolute;
|
||||||
|
width: 100;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
width: 38%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.footer-bottom {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
display: flex;
|
||||||
|
width: 38%;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-placeholder {
|
||||||
|
display: block;
|
||||||
|
min-width: 1.2in;
|
||||||
|
border-bottom: 1px dotted black;
|
||||||
|
margin-bottom: 2mm;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
148
src/pages/09_task-order/document_view/ViewHeader.vue
Normal file
148
src/pages/09_task-order/document_view/ViewHeader.vue
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { dateFormat } from 'src/utils/datetime';
|
||||||
|
|
||||||
|
// NOTE: Import stores
|
||||||
|
import { formatAddress } from 'src/utils/address';
|
||||||
|
|
||||||
|
// NOTE Import Types
|
||||||
|
import { Branch } from 'src/stores/branch/types';
|
||||||
|
import { Institution } from 'src/stores/institution/types';
|
||||||
|
|
||||||
|
// NOTE: Import Components
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
branch: Branch;
|
||||||
|
institution: Institution;
|
||||||
|
details: {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
contactName: string;
|
||||||
|
contactTel: string;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row items-center q-mb-lg">
|
||||||
|
<div class="column" style="width: 50%">
|
||||||
|
<img src="/logo.png" width="192px" style="object-fit: scale-down" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="column"
|
||||||
|
style="text-align: center; width: 50%; font-weight: 800; font-size: 24px"
|
||||||
|
>
|
||||||
|
{{ $t('taskOrder.goodReceipt') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article class="detail-card">
|
||||||
|
<section class="detail-customer-info">
|
||||||
|
<article>
|
||||||
|
<b>
|
||||||
|
{{ !!branch.virtual ? '' : $t('general.company') }} {{ branch.name }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<span v-if="branch.province && branch.district && branch.subDistrict">
|
||||||
|
{{
|
||||||
|
formatAddress({
|
||||||
|
address: branch.address,
|
||||||
|
addressEN: branch.addressEN,
|
||||||
|
moo: branch.moo,
|
||||||
|
mooEN: branch.mooEN,
|
||||||
|
soi: branch.soi,
|
||||||
|
soiEN: branch.soiEN,
|
||||||
|
street: branch.street,
|
||||||
|
streetEN: branch.streetEN,
|
||||||
|
province: branch.province,
|
||||||
|
district: branch.district,
|
||||||
|
subDistrict: branch.subDistrict,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span>เลขประจำตัวผู้เสียภาษี {{ branch.taxNo }}</span>
|
||||||
|
<span>เบอร์โทร {{ branch.telephoneNo }}</span>
|
||||||
|
<span>{{ branch.webUrl }}</span>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<b>ลูกค้า</b>
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
formatAddress({
|
||||||
|
address: institution.address,
|
||||||
|
addressEN: institution.addressEN,
|
||||||
|
moo: institution.moo || '-',
|
||||||
|
mooEN: institution.mooEN || '-',
|
||||||
|
soi: institution.soi || '-',
|
||||||
|
soiEN: institution.soiEN || '-',
|
||||||
|
street: institution.street || '-',
|
||||||
|
streetEN: institution.streetEN || '-',
|
||||||
|
province: institution.province,
|
||||||
|
district: institution.district,
|
||||||
|
subDistrict: institution.subDistrict,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
<section class="detail-quotation-info">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{{ $t('general.itemNo', { msg: `${$t('preview.taskOrder')}` }) }}
|
||||||
|
</div>
|
||||||
|
<div>{{ details.code }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>ชื่องาน</div>
|
||||||
|
<div>{{ details.name }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>ผู้ติดต่อ</div>
|
||||||
|
<div>{{ details.contactName }}</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detail-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > :first-child {
|
||||||
|
max-width: 57.5%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-customer-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& > :first-child {
|
||||||
|
color: var(--main);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-quotation-info {
|
||||||
|
& > * {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
& > :first-child {
|
||||||
|
color: var(--main);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import UploadFileSection from 'src/components/upload-file/UploadFileSection.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
transformUrl?: (url: string) => string | Promise<string>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'fetchFileList'): void;
|
||||||
|
(e: 'upload', file: FileList): void;
|
||||||
|
(e: 'remove', name: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const fileData = defineModel<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
progress: number;
|
||||||
|
loaded: number;
|
||||||
|
total: number;
|
||||||
|
url?: string;
|
||||||
|
}[]
|
||||||
|
>('fileData', { default: [] });
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
class="overflow-hidden bordered full-width"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||||
|
@after-show="() => $emit('fetchFileList')"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('quotation.additionalFile') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main class="q-px-md q-py-sm surface-1">
|
||||||
|
<UploadFileSection
|
||||||
|
multiple
|
||||||
|
:layout="$q.screen.gt.sm ? 'column' : 'row'"
|
||||||
|
:readonly
|
||||||
|
:label="$t('general.upload', { msg: $t('general.attachment') })"
|
||||||
|
:transform-url="transformUrl"
|
||||||
|
v-model:file-data="fileData"
|
||||||
|
@update:file="(f) => $emit('upload', f as unknown as FileList)"
|
||||||
|
@close="(v) => $emit('remove', v)"
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
108
src/pages/09_task-order/expansion/DocumentExpansion.vue
Normal file
108
src/pages/09_task-order/expansion/DocumentExpansion.vue
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import SelectBranch from 'src/components/shared/select/SelectBranch.vue';
|
||||||
|
import SelectInstitution from 'src/components/shared/select/SelectInstitution.vue';
|
||||||
|
import DatePicker from 'src/components/shared/DatePicker.vue';
|
||||||
|
|
||||||
|
import { getName } from 'src/services/keycloak';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
taskListGroup: boolean;
|
||||||
|
institutionGroup: string[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const registeredBranchId = defineModel<string>('registeredBranchId');
|
||||||
|
const institutionId = defineModel<string>('institutionId');
|
||||||
|
const issueDate = defineModel<string>('issueDate');
|
||||||
|
const code = defineModel<string>('code');
|
||||||
|
|
||||||
|
const taskName = defineModel<string>('taskName');
|
||||||
|
const contactName = defineModel<string>('contactName');
|
||||||
|
const contactTel = defineModel<string>('contactTel');
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
default-opened
|
||||||
|
dense
|
||||||
|
class="overflow-hidden bordered full-width"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('general.information', { msg: $t('general.document') }) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main class="q-px-md q-py-sm surface-1 row q-col-gutter-sm">
|
||||||
|
<SelectBranch
|
||||||
|
:readonly
|
||||||
|
class="col-md-4 col-12"
|
||||||
|
:label="`${$t('taskOrder.issueBranch')}${$i18n.locale === 'tha' ? $t('taskOrder.title') : ''}`"
|
||||||
|
v-model:value="registeredBranchId"
|
||||||
|
/>
|
||||||
|
<SelectInstitution
|
||||||
|
:readonly
|
||||||
|
required
|
||||||
|
class="col-md-4 col-12"
|
||||||
|
:label="`${$t('general.agencies')}`"
|
||||||
|
v-model:value="institutionId"
|
||||||
|
:disabled="taskListGroup"
|
||||||
|
:params="{ payload: { group: institutionGroup } }"
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
:label="$t('taskOrder.issueDate')"
|
||||||
|
class="col-md-2 col-6"
|
||||||
|
:model-value="issueDate || new Date(Date.now())"
|
||||||
|
:readonly
|
||||||
|
:disabled="!readonly"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
:label="$t('taskOrder.code')"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
class="col-md-2 col-6"
|
||||||
|
:readonly
|
||||||
|
:disable="!readonly"
|
||||||
|
:model-value="code"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
:readonly
|
||||||
|
:label="$t('general.name', { msg: $t('taskOrder.title') })"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
class="col-md-4 col-6"
|
||||||
|
v-model="taskName"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
:readonly
|
||||||
|
:label="$t('taskOrder.contactName')"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
class="col-md col-6"
|
||||||
|
v-model="contactName"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
:readonly
|
||||||
|
:label="$t('general.telephone')"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
class="col"
|
||||||
|
v-model="contactTel"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
:readonly
|
||||||
|
:label="$t('taskOrder.madeBy')"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
class="col"
|
||||||
|
:disable="!readonly"
|
||||||
|
:model-value="getName()"
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
63
src/pages/09_task-order/expansion/PaymentExpansion.vue
Normal file
63
src/pages/09_task-order/expansion/PaymentExpansion.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import QuotationFormInfo from 'src/pages/05_quotation/QuotationFormInfo.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
complete?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const payType = defineModel<
|
||||||
|
| 'Full'
|
||||||
|
| 'Split'
|
||||||
|
| 'SplitCustom'
|
||||||
|
| 'BillFull'
|
||||||
|
| 'BillSplit'
|
||||||
|
| 'BillSplitCustom'
|
||||||
|
>('payType', { default: 'Full' });
|
||||||
|
const paySplit = defineModel<{ no: number; name?: string; amount: number }[]>(
|
||||||
|
'paySplit',
|
||||||
|
{ default: [] },
|
||||||
|
);
|
||||||
|
const summaryPrice = defineModel<{
|
||||||
|
totalPrice: number;
|
||||||
|
totalDiscount: number;
|
||||||
|
vat: number;
|
||||||
|
vatExcluded: number;
|
||||||
|
finalPrice: number;
|
||||||
|
}>('summaryPrice', {
|
||||||
|
required: true,
|
||||||
|
default: {
|
||||||
|
totalPrice: 0,
|
||||||
|
totalDiscount: 0,
|
||||||
|
vat: 0,
|
||||||
|
vatExcluded: 0,
|
||||||
|
finalPrice: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
class="overflow-hidden bordered full-width"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('general.payment') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main class="q-px-md q-py-sm surface-1">
|
||||||
|
<QuotationFormInfo
|
||||||
|
task-order
|
||||||
|
:task-order-complete="complete"
|
||||||
|
v-model:pay-type="payType"
|
||||||
|
v-model:pay-split="paySplit"
|
||||||
|
v-model:summary-price="summaryPrice"
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
310
src/pages/09_task-order/expansion/ProductExpansion.vue
Normal file
310
src/pages/09_task-order/expansion/ProductExpansion.vue
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { QTableSlots } from 'quasar';
|
||||||
|
|
||||||
|
import { AddButton } from 'src/components/button';
|
||||||
|
import TableEmployee from '../TableEmployee.vue';
|
||||||
|
|
||||||
|
import { RequestWork } from 'src/stores/request-list';
|
||||||
|
import { productColumn } from '../constants';
|
||||||
|
import { baseUrl, formatNumberDecimal, commaInput } from 'src/stores/utils';
|
||||||
|
import { precisionRound } from 'src/utils/arithmetic';
|
||||||
|
import { useConfigStore } from 'stores/config';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
const currentBtnOpen = ref<boolean[]>([]);
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
const { data: config } = storeToRefs(configStore);
|
||||||
|
|
||||||
|
// TODO: replace discount
|
||||||
|
const discount4Show = ref<string[]>([]);
|
||||||
|
|
||||||
|
const taskProduct = defineModel<{ productId: string; discount?: number }[]>(
|
||||||
|
'taskProduct',
|
||||||
|
{
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
taskList: {
|
||||||
|
product: RequestWork['productService']['product'];
|
||||||
|
list: RequestWork[];
|
||||||
|
}[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'addProduct'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
calcPricePerUnit,
|
||||||
|
});
|
||||||
|
|
||||||
|
function openList(index: number) {
|
||||||
|
if (!currentBtnOpen.value[index]) {
|
||||||
|
currentBtnOpen.value.map((v, i) => {
|
||||||
|
if (i !== index) {
|
||||||
|
currentBtnOpen.value[i] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
currentBtnOpen.value[index] = !currentBtnOpen.value[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcPricePerUnit(product: RequestWork['productService']['product']) {
|
||||||
|
return product.vatIncluded
|
||||||
|
? precisionRound(product.serviceCharge / (1 + (config.value?.vat || 0.07)))
|
||||||
|
: product.serviceCharge;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcPrice(
|
||||||
|
product: RequestWork['productService']['product'],
|
||||||
|
amount: number,
|
||||||
|
) {
|
||||||
|
const disc =
|
||||||
|
taskProduct.value.find((v) => v.productId === product.id)?.discount || 0;
|
||||||
|
const pricePerUnit = calcPricePerUnit(product);
|
||||||
|
|
||||||
|
return precisionRound(
|
||||||
|
pricePerUnit * amount -
|
||||||
|
disc +
|
||||||
|
precisionRound(
|
||||||
|
product.calcVat
|
||||||
|
? (pricePerUnit * (disc ? amount : 1) - disc) *
|
||||||
|
(config.value?.vat || 0.07)
|
||||||
|
: 0,
|
||||||
|
) *
|
||||||
|
(!disc ? amount : 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
class="overflow-hidden bordered full-width"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||||
|
default-opened
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span
|
||||||
|
class="row items-center justify-between full-width"
|
||||||
|
style="min-height: 31.01px"
|
||||||
|
>
|
||||||
|
{{ $t('general.information', { msg: $t('taskOrder.productList') }) }}
|
||||||
|
<AddButton
|
||||||
|
icon-only
|
||||||
|
@click.stop="$emit('addProduct')"
|
||||||
|
v-if="!readonly"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main class="q-px-md q-py-sm surface-1">
|
||||||
|
<q-table
|
||||||
|
:columns="productColumn"
|
||||||
|
:rows="taskList"
|
||||||
|
bordered
|
||||||
|
flat
|
||||||
|
hide-pagination
|
||||||
|
card-container-class="q-col-gutter-sm"
|
||||||
|
class="full-width"
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
:no-data-label="$t('general.noDataTable')"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr
|
||||||
|
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
{{ col.label && $t(col.label) }}
|
||||||
|
{{ col.label === 'quotation.vat' ? '%' : '' }}
|
||||||
|
</q-th>
|
||||||
|
<q-th></q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-slot:body="props: {
|
||||||
|
row: {
|
||||||
|
product: RequestWork['productService']['product'];
|
||||||
|
list: RequestWork[];
|
||||||
|
};
|
||||||
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||||
|
>
|
||||||
|
<q-tr class="text-center">
|
||||||
|
<q-td>
|
||||||
|
{{ props.rowIndex + 1 }}
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
{{ props.row.product.code }}
|
||||||
|
</q-td>
|
||||||
|
<q-td style="width: 100%" class="text-left">
|
||||||
|
<q-avatar class="q-mr-sm" size="md">
|
||||||
|
<q-img
|
||||||
|
class="text-center"
|
||||||
|
:ratio="1"
|
||||||
|
:src="`${baseUrl}/product/${props.row.product.id}/image/${props.row.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>
|
||||||
|
{{ props.row.product.name }}
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
{{ props.row.list.length }}
|
||||||
|
</q-td>
|
||||||
|
<q-td class="text-right">
|
||||||
|
{{
|
||||||
|
formatNumberDecimal(
|
||||||
|
calcPricePerUnit(props.row.product) +
|
||||||
|
(props.row.product.calcVat
|
||||||
|
? calcPricePerUnit(props.row.product) *
|
||||||
|
(config?.vat || 0.07)
|
||||||
|
: 0),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</q-td>
|
||||||
|
<!-- TODO: display price detail -->
|
||||||
|
<q-td align="center">
|
||||||
|
<q-input
|
||||||
|
:readonly
|
||||||
|
:bg-color="readonly ? 'transparent' : ''"
|
||||||
|
dense
|
||||||
|
min="0"
|
||||||
|
outlined
|
||||||
|
input-class="text-right"
|
||||||
|
style="width: 90px"
|
||||||
|
debounce="500"
|
||||||
|
:model-value="
|
||||||
|
commaInput(
|
||||||
|
taskProduct
|
||||||
|
.find((v) => v.productId === props.row.product.id)
|
||||||
|
?.discount?.toString() || '0',
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@update:model-value="
|
||||||
|
(v) => {
|
||||||
|
if (typeof v === 'string')
|
||||||
|
discount4Show[props.rowIndex] = commaInput(v);
|
||||||
|
const x = parseFloat(
|
||||||
|
discount4Show[props.rowIndex] &&
|
||||||
|
typeof discount4Show[props.rowIndex] === 'string'
|
||||||
|
? discount4Show[props.rowIndex].replace(/,/g, '')
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const product = taskProduct.find(
|
||||||
|
(v) => v.productId === props.row.product.id,
|
||||||
|
);
|
||||||
|
if (product) {
|
||||||
|
product.discount = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<!-- before vat -->
|
||||||
|
<q-td class="text-right">
|
||||||
|
{{ formatNumberDecimal(calcPricePerUnit(props.row.product), 2) }}
|
||||||
|
</q-td>
|
||||||
|
<!-- vat -->
|
||||||
|
<q-td class="text-right">
|
||||||
|
{{
|
||||||
|
formatNumberDecimal(
|
||||||
|
props.row.product.calcVat
|
||||||
|
? precisionRound(
|
||||||
|
(calcPricePerUnit(props.row.product) *
|
||||||
|
props.row.list.length -
|
||||||
|
(taskProduct.find(
|
||||||
|
(v) => v.productId === props.row.product.id,
|
||||||
|
)?.discount || 0)) *
|
||||||
|
(config?.vat || 0.07),
|
||||||
|
)
|
||||||
|
: 0,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</q-td>
|
||||||
|
<!-- total -->
|
||||||
|
<q-td class="text-right">
|
||||||
|
{{
|
||||||
|
formatNumberDecimal(
|
||||||
|
calcPrice(props.row.product, props.row.list.length),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
class="rounded"
|
||||||
|
@click.stop="openList(props.rowIndex)"
|
||||||
|
>
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<q-icon name="mdi-account-group-outline" />
|
||||||
|
<q-icon
|
||||||
|
class="btn-arrow-right"
|
||||||
|
:class="{
|
||||||
|
active: currentBtnOpen[props.rowIndex],
|
||||||
|
}"
|
||||||
|
size="xs"
|
||||||
|
:name="`mdi-chevron-${currentBtnOpen[props.rowIndex] ? 'down' : 'up'}`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
|
||||||
|
<q-tr v-show="currentBtnOpen[props.rowIndex]" :props="props">
|
||||||
|
<q-td colspan="100%" style="padding: 16px">
|
||||||
|
<TableEmployee step-on :rows="props.row.list" />
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
|
||||||
|
<div class="q-pt-md row items-center">
|
||||||
|
<span class="q-ml-auto q-mr-sm">
|
||||||
|
{{
|
||||||
|
$t('general.numberOf', {
|
||||||
|
msg: $t('productService.product.product'),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<div class="surface-3 q-px-sm rounded">{{ taskList.length }}</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.product-status {
|
||||||
|
padding-left: 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: hsl(var(--_color));
|
||||||
|
background: hsla(var(--_color) / 0.15);
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
--_color: var(--warning-bg);
|
||||||
|
}
|
||||||
|
&.positive {
|
||||||
|
--_color: var(--positive-bg);
|
||||||
|
}
|
||||||
|
&.negative {
|
||||||
|
--_color: var(--negative-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
67
src/pages/09_task-order/expansion/RemarkExpansion.vue
Normal file
67
src/pages/09_task-order/expansion/RemarkExpansion.vue
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const remark = defineModel<string>('remark', { default: '' });
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
class="overflow-hidden bordered full-width"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('general.remark') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main class="surface-1 q-pa-md full-width">
|
||||||
|
<q-editor
|
||||||
|
dense
|
||||||
|
:readonly="readonly"
|
||||||
|
:model-value="remark"
|
||||||
|
min-height="5rem"
|
||||||
|
class="full-width"
|
||||||
|
toolbar-bg="input-border"
|
||||||
|
style="cursor: auto; color: var(--foreground)"
|
||||||
|
:content-class="readonly ? 'q-mt-sm' : 'bordered q-mt-sm rounded'"
|
||||||
|
:flat="!readonly"
|
||||||
|
:style="`width: ${$q.screen.gt.xs ? '100%' : '63vw'}`"
|
||||||
|
:toolbar="[['left', 'center', 'justify'], ['clip']]"
|
||||||
|
:toolbar-toggle-color="readonly ? 'disabled' : 'primary'"
|
||||||
|
:toolbar-color="readonly ? 'disabled' : $q.dark.isActive ? 'white' : ''"
|
||||||
|
:definitions="{
|
||||||
|
clip: {
|
||||||
|
icon: 'mdi-paperclip',
|
||||||
|
tip: 'Upload',
|
||||||
|
disable: readonly,
|
||||||
|
handler: () => console.log('upload'),
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
@update:model-value="
|
||||||
|
(v) => {
|
||||||
|
remark = v;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
:deep(.q-editor__toolbar-group):nth-child(2) {
|
||||||
|
margin-left: auto !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>
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// NOTE: Import stores
|
||||||
|
import { dateFormat } from 'src/utils/datetime';
|
||||||
|
|
||||||
|
// NOTE Import Types
|
||||||
|
import {
|
||||||
|
TaskOrderStatus,
|
||||||
|
TaskStatus,
|
||||||
|
UserTaskStatus,
|
||||||
|
} from 'src/stores/task-order/types';
|
||||||
|
|
||||||
|
// NOTE: Import Components
|
||||||
|
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||||
|
import DataDisplay from 'src/components/08_request-list/DataDisplay.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
gender?: string;
|
||||||
|
contactName?: string;
|
||||||
|
contactTel?: string;
|
||||||
|
contactUrl?: string;
|
||||||
|
email?: string;
|
||||||
|
receivedDate?: Date | string;
|
||||||
|
deliveryDate?: Date | string;
|
||||||
|
status?: UserTaskStatus;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
default-opened
|
||||||
|
dense
|
||||||
|
class="overflow-hidden bordered full-width"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('general.information', { msg: $t('personnel.MESSENGER') }) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main
|
||||||
|
class="surface-1"
|
||||||
|
:class="{ muted: status === UserTaskStatus.Pending }"
|
||||||
|
>
|
||||||
|
<div class="row q-col-gutter-sm q-px-md q-py-sm">
|
||||||
|
<DataDisplay
|
||||||
|
class="col-md col-6"
|
||||||
|
:label="$t('taskOrder.recipientOrSender')"
|
||||||
|
>
|
||||||
|
<template #value>
|
||||||
|
<q-avatar size="md" class="q-mr-xs">
|
||||||
|
<q-img class="text-center" :ratio="1" :src="contactUrl">
|
||||||
|
<template #error>
|
||||||
|
<div
|
||||||
|
class="no-padding full-width full-height flex items-center justify-center"
|
||||||
|
:style="`${gender ? 'background: white' : 'background: linear-gradient(135deg,rgba(43, 137, 223, 1) 0%, rgba(230, 51, 81, 1) 100%);'}`"
|
||||||
|
>
|
||||||
|
<q-img
|
||||||
|
v-if="gender"
|
||||||
|
:src="
|
||||||
|
gender === 'male'
|
||||||
|
? '/no-img-man.png'
|
||||||
|
: '/no-img-female.png'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<q-icon
|
||||||
|
v-else
|
||||||
|
size="sm"
|
||||||
|
name="mdi-account-outline"
|
||||||
|
style="color: white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-img>
|
||||||
|
</q-avatar>
|
||||||
|
{{ contactName }}
|
||||||
|
</template>
|
||||||
|
</DataDisplay>
|
||||||
|
|
||||||
|
<DataDisplay
|
||||||
|
class="col-md col-6"
|
||||||
|
:label="$t('general.telephone')"
|
||||||
|
:value="contactTel || '-'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DataDisplay
|
||||||
|
class="col-md col-6"
|
||||||
|
:label="$t('form.email')"
|
||||||
|
:value="email || '-'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DataDisplay
|
||||||
|
class="col-md col-6"
|
||||||
|
:label="$t('taskOrder.workStartDate')"
|
||||||
|
>
|
||||||
|
<template #value>
|
||||||
|
{{ receivedDate ? dateFormat(receivedDate) : '-' }}
|
||||||
|
</template>
|
||||||
|
</DataDisplay>
|
||||||
|
|
||||||
|
<DataDisplay
|
||||||
|
class="col-md col-6"
|
||||||
|
:label="$t('taskOrder.workSubmissionDate')"
|
||||||
|
>
|
||||||
|
<template #value>
|
||||||
|
{{ deliveryDate ? dateFormat(deliveryDate) : '-' }}
|
||||||
|
</template>
|
||||||
|
</DataDisplay>
|
||||||
|
|
||||||
|
<DataDisplay class="col-md col-6" :label="$t('general.status')">
|
||||||
|
<template #value>
|
||||||
|
<BadgeComponent
|
||||||
|
v-if="status"
|
||||||
|
hide-icon
|
||||||
|
:title="
|
||||||
|
$t(
|
||||||
|
`taskOrder.${
|
||||||
|
{
|
||||||
|
Pending: 'waitReceive',
|
||||||
|
Accept: 'receiveTask',
|
||||||
|
Submit: 'sentTask',
|
||||||
|
}[status] ?? 'waitReceive'
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:hsla-color="
|
||||||
|
{
|
||||||
|
Pending: '--info-bg',
|
||||||
|
Accept: '--info-bg',
|
||||||
|
Submit: '--positive-bg',
|
||||||
|
}[status] ?? '--info-bg'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DataDisplay>
|
||||||
|
</div>
|
||||||
|
<slot name="product"></slot>
|
||||||
|
</main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.muted {
|
||||||
|
& .col-md {
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// NOTE: Import stores
|
||||||
|
|
||||||
|
// NOTE Import Types
|
||||||
|
import { ProductRelation } from 'src/stores/quotations/types';
|
||||||
|
|
||||||
|
// NOTE: Import Components
|
||||||
|
import FormGroupHead from 'src/pages/08_request-list/FormGroupHead.vue';
|
||||||
|
import DataDisplay from 'src/components/08_request-list/DataDisplay.vue';
|
||||||
|
import SelectBranch from 'src/components/shared/select/SelectBranch.vue';
|
||||||
|
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||||
|
import { TaskOrderStatus, UserTaskStatus } from 'src/stores/task-order/types';
|
||||||
|
|
||||||
|
// TODO: เอา รับค่าโดยใช้ defineProps
|
||||||
|
defineProps<{
|
||||||
|
code?: string;
|
||||||
|
taskName?: string;
|
||||||
|
issueBranch?: string;
|
||||||
|
agencies?: string;
|
||||||
|
madeBy?: string;
|
||||||
|
contactTel?: string;
|
||||||
|
contactName?: string;
|
||||||
|
userTaskStatus?: UserTaskStatus;
|
||||||
|
product?: ProductRelation;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function mapStatus(value: string, type: 'status' | 'color') {
|
||||||
|
const mappings: Record<string, Record<'status' | 'color', string>> = {
|
||||||
|
Pending: {
|
||||||
|
status: 'taskOrder.taskInCart',
|
||||||
|
color: '--blue-6-hsl',
|
||||||
|
},
|
||||||
|
Accept: {
|
||||||
|
status: 'taskOrder.inProgress',
|
||||||
|
color: '--blue-6-hsl',
|
||||||
|
},
|
||||||
|
Submit: {
|
||||||
|
status: 'taskOrder.sentTask',
|
||||||
|
color: '--blue-6-hsl',
|
||||||
|
},
|
||||||
|
|
||||||
|
InProgress: {
|
||||||
|
status: 'taskOrder.inProgress',
|
||||||
|
color: '--blue-6-hsl',
|
||||||
|
},
|
||||||
|
Validate: {
|
||||||
|
status: 'taskOrder.inProgress',
|
||||||
|
color: '--blue-6-hsl',
|
||||||
|
},
|
||||||
|
Complete: {
|
||||||
|
status: 'taskOrder.sentTask',
|
||||||
|
color: '--blue-6-hsl',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return mappings[value]?.[type] || '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
default-opened
|
||||||
|
dense
|
||||||
|
class="overflow-hidden bordered full-width"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('taskOrder.productList', { msg: $t('personnel.MESSENGER') }) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main class="surface-1">
|
||||||
|
<!-- TODO: เอา DataDisplay มาใส่ตรงนี้ -->
|
||||||
|
|
||||||
|
<div class="row q-col-gutter-sm q-px-md q-py-sm">
|
||||||
|
<DataDisplay
|
||||||
|
class="col-md-2 col-6"
|
||||||
|
:label="$t('taskOrder.workOrderCode')"
|
||||||
|
:value="code || '-'"
|
||||||
|
/>
|
||||||
|
<DataDisplay
|
||||||
|
class="col-md-2 col-6"
|
||||||
|
:label="$t('taskOrder.workOrderName')"
|
||||||
|
:value="taskName || '-'"
|
||||||
|
/>
|
||||||
|
<SelectBranch
|
||||||
|
class="col-md-2 col-6 fix-padding"
|
||||||
|
:label="$t('taskOrder.workOrderName')"
|
||||||
|
:value="issueBranch"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<DataDisplay
|
||||||
|
class="col-md-2 col-6"
|
||||||
|
:label="$t('general.agencies')"
|
||||||
|
:value="agencies || '-'"
|
||||||
|
/>
|
||||||
|
<DataDisplay
|
||||||
|
class="col-md-2 col-6"
|
||||||
|
:label="$t('taskOrder.madeBy')"
|
||||||
|
:value="madeBy || '-'"
|
||||||
|
/>
|
||||||
|
<DataDisplay
|
||||||
|
class="col-md-2 col-6"
|
||||||
|
:label="$t('taskOrder.telephone')"
|
||||||
|
:value="contactTel || '-'"
|
||||||
|
/>
|
||||||
|
<DataDisplay
|
||||||
|
class="col-md-2 col-6"
|
||||||
|
:label="$t('branch.form.contactName')"
|
||||||
|
:value="contactName || '-'"
|
||||||
|
/>
|
||||||
|
<DataDisplay class="col-md-2 col-6" :label="$t('general.status')">
|
||||||
|
<template #value>
|
||||||
|
<BadgeComponent
|
||||||
|
v-if="userTaskStatus"
|
||||||
|
hide-icon
|
||||||
|
:hsla-color="mapStatus(userTaskStatus, 'color')"
|
||||||
|
:title="$t(mapStatus(userTaskStatus, 'status'))"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DataDisplay>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormGroupHead>
|
||||||
|
{{ $t('productService.title') }}
|
||||||
|
</FormGroupHead>
|
||||||
|
|
||||||
|
<slot :product="product"></slot>
|
||||||
|
</main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
:deep(.fix-padding.q-field--outlined .q-field__control) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ProductRelation } from 'src/stores/quotations/types';
|
||||||
|
|
||||||
|
import { receiveProductColumn } from 'src/pages/09_task-order/constants';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { QTableSlots } from 'quasar';
|
||||||
|
|
||||||
|
const datarows = [
|
||||||
|
{
|
||||||
|
order: 1,
|
||||||
|
requestListCode: 'RL-20231201',
|
||||||
|
flow: 'Step 1: Review',
|
||||||
|
quotation: 'John Doe',
|
||||||
|
age: '30 ปี',
|
||||||
|
nationality: 'Thai',
|
||||||
|
documentExpireDate: '2024-12-31',
|
||||||
|
numberOfDay: '12',
|
||||||
|
quotationCode: 'QT-987654',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
class="overflow-hidden full-width"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||||
|
@click=""
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="row items-center">
|
||||||
|
<q-avatar class="q-mr-sm" size="md">
|
||||||
|
<q-icon
|
||||||
|
class="full-width full-height"
|
||||||
|
name="mdi-shopping"
|
||||||
|
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
|
||||||
|
/>
|
||||||
|
</q-avatar>
|
||||||
|
<div class="colum">
|
||||||
|
<div class="col">
|
||||||
|
<span>
|
||||||
|
ค่าบริการและค่าดำเนินงานยื่นขอบัญชีรายชื่อ (Name list)
|
||||||
|
สัญชาติเมียนมา
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col app-text-muted">AC103</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main><!-- TODO add table --></main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
:deep(tr:nth-child(2n)) {
|
||||||
|
background: #f9fafc;
|
||||||
|
&.dark {
|
||||||
|
background: hsl(var(--gray-11-hsl) / 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
background-color: hsla(var(--info-bg) / 0.1);
|
||||||
|
color: hsl(var(--info-bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-status {
|
||||||
|
padding-left: 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
&.await {
|
||||||
|
color: var(--yellow-6);
|
||||||
|
background: hsla(var(--yellow-6-hsl) / 0.15);
|
||||||
|
}
|
||||||
|
&.in-progress {
|
||||||
|
color: var(--orange-5);
|
||||||
|
background: hsla(var(--orange-5-hsl) / 0.15);
|
||||||
|
}
|
||||||
|
&.completed {
|
||||||
|
color: var(--green-5);
|
||||||
|
background: hsla(var(--green-5-hsl) / 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
164
src/pages/09_task-order/form.ts
Normal file
164
src/pages/09_task-order/form.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { dialog } from 'stores/utils';
|
||||||
|
|
||||||
|
// NOTE: Import types
|
||||||
|
import {
|
||||||
|
SetTaskStatusPayload,
|
||||||
|
TaskOrder,
|
||||||
|
TaskOrderPayload,
|
||||||
|
TaskStatus,
|
||||||
|
} from 'stores/task-order/types';
|
||||||
|
import { RequestWork } from 'src/stores/request-list';
|
||||||
|
|
||||||
|
// NOTE: Import stores
|
||||||
|
import { useTaskOrderStore } from 'src/stores/task-order';
|
||||||
|
|
||||||
|
export const useTaskOrderForm = defineStore('task-order-form', () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const taskOrderStore = useTaskOrderStore();
|
||||||
|
const DEFAULT_DATA: TaskOrderPayload = {
|
||||||
|
taskList: [],
|
||||||
|
institutionId: '',
|
||||||
|
contactTel: '',
|
||||||
|
contactName: '',
|
||||||
|
taskName: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = ref<{
|
||||||
|
mode: null | 'info' | 'create' | 'edit';
|
||||||
|
setTaskStatusList: SetTaskStatusPayload[];
|
||||||
|
}>({
|
||||||
|
mode: null,
|
||||||
|
setTaskStatusList: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
let resetFormData = structuredClone(DEFAULT_DATA);
|
||||||
|
|
||||||
|
const currentFormData = ref<TaskOrderPayload>(structuredClone(resetFormData));
|
||||||
|
const fullTaskOrder = ref<TaskOrder>();
|
||||||
|
|
||||||
|
function resetForm(clean = false) {
|
||||||
|
if (clean) {
|
||||||
|
currentFormData.value = structuredClone(DEFAULT_DATA);
|
||||||
|
resetFormData = structuredClone(DEFAULT_DATA);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentFormData.value = structuredClone(resetFormData);
|
||||||
|
|
||||||
|
state.value.mode = 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assignFormData(
|
||||||
|
id: string,
|
||||||
|
mode: 'info' | 'edit' | 'assign' = 'info',
|
||||||
|
userId?: string,
|
||||||
|
) {
|
||||||
|
const res = await taskOrderStore.getTaskOrderById(id, {
|
||||||
|
taskAssignedUserId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res) return;
|
||||||
|
|
||||||
|
fullTaskOrder.value = structuredClone(res);
|
||||||
|
resetFormData = Object.assign(res, {
|
||||||
|
requestWork: res.taskList.map((v) => ({
|
||||||
|
step: v.step,
|
||||||
|
requestWorkId: v.requestWorkId,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
currentFormData.value = structuredClone(resetFormData);
|
||||||
|
|
||||||
|
if (mode === 'assign') return;
|
||||||
|
|
||||||
|
state.value.mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitTask(opt?: {
|
||||||
|
taskProduct?: {
|
||||||
|
productId: string;
|
||||||
|
discount?: number;
|
||||||
|
}[];
|
||||||
|
}) {
|
||||||
|
let succeed = false;
|
||||||
|
|
||||||
|
if (state.value.mode === 'create') {
|
||||||
|
const mapTaskList = currentFormData.value.taskList.map((v) => ({
|
||||||
|
step: v.step,
|
||||||
|
requestWorkId: v.requestWorkId,
|
||||||
|
}));
|
||||||
|
const res = await taskOrderStore.createTaskOrder({
|
||||||
|
...currentFormData.value,
|
||||||
|
taskProduct: opt?.taskProduct,
|
||||||
|
taskList: mapTaskList,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
currentFormData.value.id = res.id;
|
||||||
|
succeed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.value.mode === 'edit' && !!currentFormData.value.id) {
|
||||||
|
const res = await taskOrderStore.editTaskOrder(currentFormData.value);
|
||||||
|
if (res) {
|
||||||
|
succeed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return succeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeStatus(
|
||||||
|
value: {
|
||||||
|
requestWorkId: string;
|
||||||
|
step: number;
|
||||||
|
failedComment?: string;
|
||||||
|
failedType?: string;
|
||||||
|
}[],
|
||||||
|
status: TaskStatus,
|
||||||
|
setRowStatus: () => void,
|
||||||
|
) {
|
||||||
|
dialog({
|
||||||
|
color: 'warning',
|
||||||
|
icon: 'mdi-alert',
|
||||||
|
title: t('dialog.title.confirmChangeStatus'),
|
||||||
|
actionText: t('general.confirm'),
|
||||||
|
persistent: true,
|
||||||
|
message: t('dialog.message.confirmChangeStatus'),
|
||||||
|
action: async () => {
|
||||||
|
if (!currentFormData.value.id) return;
|
||||||
|
|
||||||
|
const records = value.map((item) => ({
|
||||||
|
failedComment: item.failedComment,
|
||||||
|
failedType: item.failedType,
|
||||||
|
taskStatus: status,
|
||||||
|
requestWorkId: item.requestWorkId,
|
||||||
|
step: item.step,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const res = await taskOrderStore.changeTaskStatus(
|
||||||
|
currentFormData.value.id,
|
||||||
|
records,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res) setRowStatus();
|
||||||
|
},
|
||||||
|
cancel: () => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentFormData,
|
||||||
|
state,
|
||||||
|
fullTaskOrder,
|
||||||
|
|
||||||
|
resetForm,
|
||||||
|
assignFormData,
|
||||||
|
submitTask,
|
||||||
|
changeStatus,
|
||||||
|
};
|
||||||
|
});
|
||||||
1088
src/pages/09_task-order/order_view/MainPage.vue
Normal file
1088
src/pages/09_task-order/order_view/MainPage.vue
Normal file
File diff suppressed because it is too large
Load diff
299
src/pages/09_task-order/receive_view/FailRemarkDialog.vue
Normal file
299
src/pages/09_task-order/receive_view/FailRemarkDialog.vue
Normal 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>
|
||||||
774
src/pages/09_task-order/receive_view/MainPage.vue
Normal file
774
src/pages/09_task-order/receive_view/MainPage.vue
Normal 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>
|
||||||
250
src/pages/09_task-order/receive_view/ReceiveDialog.vue
Normal file
250
src/pages/09_task-order/receive_view/ReceiveDialog.vue
Normal 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>
|
||||||
|
|
@ -128,6 +128,27 @@ const routes: RouteRecordRaw[] = [
|
||||||
name: 'requestListView',
|
name: 'requestListView',
|
||||||
component: () => import('pages/08_request-list/RequestListView.vue'),
|
component: () => import('pages/08_request-list/RequestListView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/task-order/order/add',
|
||||||
|
name: 'TaskOrderAdd',
|
||||||
|
component: () => import('pages/09_task-order/order_view/MainPage.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/task-order/doc/:id',
|
||||||
|
name: 'TaskOrderAdd',
|
||||||
|
component: () => import('pages/09_task-order/document_view/MainPage.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/task-order/order/:id',
|
||||||
|
name: 'TaskOrderView',
|
||||||
|
component: () => import('pages/09_task-order/order_view/MainPage.vue'),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/task-order/receive/:id',
|
||||||
|
name: 'TaskReceiveView',
|
||||||
|
component: () => import('pages/09_task-order/receive_view/MainPage.vue'),
|
||||||
|
},
|
||||||
|
|
||||||
// Always leave this as last one,
|
// Always leave this as last one,
|
||||||
// but you can also remove it
|
// but you can also remove it
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ const useCustomerStore = defineStore('api-customer', () => {
|
||||||
query?: string;
|
query?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
|
activeRegisBranchOnly?: boolean;
|
||||||
},
|
},
|
||||||
Data extends Pagination<
|
Data extends Pagination<
|
||||||
(CustomerBranch &
|
(CustomerBranch &
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,29 @@ export const useInstitution = defineStore('institution-store', () => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getInstitutionList(params?: {
|
async function getInstitutionList(opts?: {
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
query?: string;
|
query?: string;
|
||||||
group?: string;
|
group?: string;
|
||||||
|
payload?: { group?: string[] };
|
||||||
}) {
|
}) {
|
||||||
const res = await api.get<PaginationResult<Institution>>('/institution', {
|
const { payload, ...params } = opts || {};
|
||||||
params,
|
|
||||||
});
|
console.log(params.query);
|
||||||
|
|
||||||
|
const res = payload
|
||||||
|
? await api.post<PaginationResult<Institution>>(
|
||||||
|
'/institution/list',
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: await api.get<PaginationResult<Institution>>(`/institution`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
if (res.status < 400) {
|
if (res.status < 400) {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { Status } from '../types';
|
import { Status } from '../types';
|
||||||
import { UpdatedBy, CreatedBy } from 'stores/types';
|
import { UpdatedBy, CreatedBy } from 'stores/types';
|
||||||
import { WorkFlowPayloadStep } from '../workflow-template/types';
|
import {
|
||||||
|
WorkFlowPayloadStep,
|
||||||
|
WorkflowTemplate,
|
||||||
|
} from '../workflow-template/types';
|
||||||
|
|
||||||
export interface TreeProduct {
|
export interface TreeProduct {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -27,6 +30,8 @@ export interface Service {
|
||||||
work: Work[];
|
work: Work[];
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
registeredBranchId: string;
|
registeredBranchId: string;
|
||||||
|
workflowId?: string;
|
||||||
|
workflow: WorkflowTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkCreate {
|
export interface WorkCreate {
|
||||||
|
|
@ -80,6 +85,8 @@ export interface Attributes {
|
||||||
workflowStep: (WorkFlowPayloadStep & { productsId: string[] })[];
|
workflowStep: (WorkFlowPayloadStep & { productsId: string[] })[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PropVariant = PropString | PropNumber | PropDate | PropOptions;
|
||||||
|
|
||||||
export type PropString = {
|
export type PropString = {
|
||||||
type: 'string';
|
type: 'string';
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { District, Province, SubDistrict } from '../address';
|
||||||
import { CreatedBy, Status, UpdatedBy } from '../types';
|
import { CreatedBy, Status, UpdatedBy } from '../types';
|
||||||
import { Invoice } from '../payment/types';
|
import { Invoice } from '../payment/types';
|
||||||
import { Employee } from '../employee/types';
|
import { Employee } from '../employee/types';
|
||||||
|
import { WorkflowTemplate } from '../workflow-template/types';
|
||||||
|
|
||||||
export type PayCondition =
|
export type PayCondition =
|
||||||
| 'Full'
|
| 'Full'
|
||||||
|
|
@ -184,6 +185,7 @@ type ServiceRelation = {
|
||||||
createdByUserId: string;
|
createdByUserId: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
updatedByUserId: string;
|
updatedByUserId: string;
|
||||||
|
workflow?: WorkflowTemplate;
|
||||||
|
|
||||||
work: (WorkRelation & {
|
work: (WorkRelation & {
|
||||||
productOnWork: {
|
productOnWork: {
|
||||||
|
|
@ -331,6 +333,8 @@ export type QuotationFull = {
|
||||||
updatedByUserId: string;
|
updatedByUserId: string;
|
||||||
updatedAt: string | Date;
|
updatedAt: string | Date;
|
||||||
updatedBy: UpdatedBy;
|
updatedBy: UpdatedBy;
|
||||||
|
|
||||||
|
agentPrice?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QuotationPayload = {
|
export type QuotationPayload = {
|
||||||
|
|
|
||||||
|
|
@ -215,9 +215,12 @@ export const useRequestList = defineStore('request-list', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRequestWorkList(params?: {
|
async function getRequestWorkList(params?: {
|
||||||
|
requestDataId?: string;
|
||||||
|
query?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
requestDataId?: string;
|
workStatus?: RequestWorkStatus;
|
||||||
|
readyToTask?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const res = await api.get<PaginationResult<RequestWork>>('/request-work', {
|
const res = await api.get<PaginationResult<RequestWork>>('/request-work', {
|
||||||
params,
|
params,
|
||||||
|
|
@ -290,3 +293,5 @@ export const useRequestList = defineStore('request-list', () => {
|
||||||
cancelRequest,
|
cancelRequest,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export * from './types.ts';
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export enum RequestDataStatus {
|
||||||
Pending = 'Pending',
|
Pending = 'Pending',
|
||||||
InProgress = 'InProgress',
|
InProgress = 'InProgress',
|
||||||
Completed = 'Completed',
|
Completed = 'Completed',
|
||||||
|
Canceled = 'Canceled',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RequestWorkStatus {
|
export enum RequestWorkStatus {
|
||||||
|
|
|
||||||
189
src/stores/task-order/index.ts
Normal file
189
src/stores/task-order/index.ts
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { api } from 'src/boot/axios';
|
||||||
|
|
||||||
|
// NOTE: Stores
|
||||||
|
|
||||||
|
// NOTE: Type
|
||||||
|
import {
|
||||||
|
SetTaskStatusPayload,
|
||||||
|
TaskOrder,
|
||||||
|
TaskOrderPayload,
|
||||||
|
TaskOrderStatus,
|
||||||
|
UserTaskStatus,
|
||||||
|
} from './types';
|
||||||
|
import { PaginationResult } from 'src/types';
|
||||||
|
import { manageAttachment } from '../utils';
|
||||||
|
|
||||||
|
export const useTaskOrderStore = defineStore('taskorder-store', () => {
|
||||||
|
const data = ref<TaskOrder[]>([]);
|
||||||
|
const page = ref<number>(1);
|
||||||
|
const pageMax = ref<number>(1);
|
||||||
|
const pageSize = ref<number>(30);
|
||||||
|
const stats = ref<Record<TaskOrderStatus, number>>({
|
||||||
|
[TaskOrderStatus.Pending]: 0,
|
||||||
|
[TaskOrderStatus.InProgress]: 0,
|
||||||
|
[TaskOrderStatus.Validate]: 0,
|
||||||
|
[TaskOrderStatus.Complete]: 0,
|
||||||
|
[TaskOrderStatus.Accept]: 0,
|
||||||
|
[TaskOrderStatus.Submit]: 0,
|
||||||
|
});
|
||||||
|
const fileManager = manageAttachment(api, 'task-order');
|
||||||
|
|
||||||
|
async function getTaskOrderStats() {
|
||||||
|
const res =
|
||||||
|
await api.get<Record<TaskOrderStatus, number>>('/task-order/stats');
|
||||||
|
|
||||||
|
if (res.status < 400) {
|
||||||
|
stats.value = res.data;
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTaskOrderList(params?: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
query?: string;
|
||||||
|
taskOrderStatus?: TaskOrderStatus;
|
||||||
|
assignedUserId?: boolean;
|
||||||
|
}) {
|
||||||
|
const res = await api.get<PaginationResult<TaskOrder>>('/task-order', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status < 400) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTaskOrderById(
|
||||||
|
id: string,
|
||||||
|
params?: { taskAssignedUserId?: string },
|
||||||
|
) {
|
||||||
|
const res = await api.get<TaskOrder>(`/task-order/${id}`, { params });
|
||||||
|
|
||||||
|
if (res.status < 400) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTaskOrder(body: TaskOrderPayload) {
|
||||||
|
const res = await api.post<TaskOrder>('/task-order', body);
|
||||||
|
|
||||||
|
if (res.status < 400) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeTaskStatus(id: string, body: SetTaskStatusPayload[]) {
|
||||||
|
const res = await api.post<TaskOrder>(
|
||||||
|
`/task-order/${id}/set-task-status`,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.status < 400) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acceptTaskOrder(id: string | string[]) {
|
||||||
|
const res = await api.post<TaskOrder>('/user-task-order/accept', {
|
||||||
|
taskOrderId: Array.isArray(id) ? id : [id],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status < 400) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editTaskOrder(body: TaskOrderPayload) {
|
||||||
|
const res = await api.put<TaskOrder>(`/task-order/${body.id}`, {
|
||||||
|
taskList: body.taskList.map((v) => ({
|
||||||
|
step: v.step,
|
||||||
|
requestWorkId: v.requestWorkId,
|
||||||
|
})),
|
||||||
|
institutionId: body.institutionId,
|
||||||
|
contactTel: body.contactTel,
|
||||||
|
contactName: body.contactName,
|
||||||
|
taskStatus: body.taskStatus,
|
||||||
|
taskName: body.taskName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status < 400) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTaskOrder(id: string) {
|
||||||
|
const res = await api.delete<TaskOrder>(`/task-order/${id}`);
|
||||||
|
|
||||||
|
if (res.status < 400) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitTaskOrder(id: string) {
|
||||||
|
const res = await api.post<TaskOrder>(`/task-order/${id}/submit`);
|
||||||
|
|
||||||
|
if (res.status < 400) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeTaskOrder(id: string) {
|
||||||
|
const res = await api.post<TaskOrder>(`/task-order/${id}/complete`);
|
||||||
|
|
||||||
|
if (res.status < 400) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserTaskOrderList(params?: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
query?: string;
|
||||||
|
userTaskStatus?: UserTaskStatus;
|
||||||
|
}) {
|
||||||
|
const res = await api.get<PaginationResult<TaskOrder>>('/user-task-order', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
if (res.status < 400) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
pageMax,
|
||||||
|
pageSize,
|
||||||
|
stats,
|
||||||
|
|
||||||
|
getTaskOrderStats,
|
||||||
|
getTaskOrderList,
|
||||||
|
getTaskOrderById,
|
||||||
|
createTaskOrder,
|
||||||
|
acceptTaskOrder,
|
||||||
|
editTaskOrder,
|
||||||
|
deleteTaskOrder,
|
||||||
|
changeTaskStatus,
|
||||||
|
submitTaskOrder,
|
||||||
|
completeTaskOrder,
|
||||||
|
|
||||||
|
// for receive user (messenger role)
|
||||||
|
getUserTaskOrderList,
|
||||||
|
|
||||||
|
...fileManager,
|
||||||
|
};
|
||||||
|
});
|
||||||
207
src/stores/task-order/types.ts
Normal file
207
src/stores/task-order/types.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
import { strict } from 'node:assert';
|
||||||
|
import { Branch } from '../branch/types';
|
||||||
|
import { Product, Service, Work } from '../product-service/types';
|
||||||
|
import { RequestWork } from '../request-list/types';
|
||||||
|
import { CreatedBy } from '../types';
|
||||||
|
import { User } from '../user';
|
||||||
|
|
||||||
|
export enum TaskOrderStatus {
|
||||||
|
Pending = 'Pending',
|
||||||
|
InProgress = 'InProgress',
|
||||||
|
Validate = 'Validate',
|
||||||
|
Complete = 'Complete',
|
||||||
|
Accept = 'Accept', // messenger only
|
||||||
|
Submit = 'Submit', // messenger only
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TaskStatus {
|
||||||
|
Pending = 'Pending',
|
||||||
|
InProgress = 'InProgress',
|
||||||
|
Success = 'Success',
|
||||||
|
Failed = 'Failed',
|
||||||
|
Redo = 'Redo',
|
||||||
|
Validate = 'Validate',
|
||||||
|
Complete = 'Complete',
|
||||||
|
Canceled = 'Canceled',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskOrder {
|
||||||
|
createdBy: CreatedBy;
|
||||||
|
registeredBranch: Branch;
|
||||||
|
taskProduct: {
|
||||||
|
productId: string;
|
||||||
|
discount?: number;
|
||||||
|
}[];
|
||||||
|
taskList: {
|
||||||
|
step: number;
|
||||||
|
requestWorkId: string;
|
||||||
|
requestWorkStep: Task;
|
||||||
|
taskStatus: TaskStatus;
|
||||||
|
taskOrderId: string;
|
||||||
|
id: string;
|
||||||
|
failedType?: string;
|
||||||
|
failedComment?: string;
|
||||||
|
}[];
|
||||||
|
institution: Institution;
|
||||||
|
createdByUserId: string;
|
||||||
|
createdAt: string;
|
||||||
|
registeredBranchId: string;
|
||||||
|
institutionId: string;
|
||||||
|
contactTel: string;
|
||||||
|
contactName: string;
|
||||||
|
taskOrderStatus: TaskOrderStatus;
|
||||||
|
taskName: string;
|
||||||
|
code: string;
|
||||||
|
id: string;
|
||||||
|
userTask: UserTask[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserTask {
|
||||||
|
id: string;
|
||||||
|
taskOrderId: string;
|
||||||
|
userId: string;
|
||||||
|
userTaskStatus: UserTaskStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Institution {
|
||||||
|
selectedImage: string;
|
||||||
|
subDistrictId: string;
|
||||||
|
districtId: string;
|
||||||
|
provinceId: string;
|
||||||
|
streetEN: string;
|
||||||
|
street: string;
|
||||||
|
mooEN: string;
|
||||||
|
moo: string;
|
||||||
|
soiEN: string;
|
||||||
|
soi: string;
|
||||||
|
addressEN: string;
|
||||||
|
address: string;
|
||||||
|
nameEN: string;
|
||||||
|
group: string;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AcceptedBy {
|
||||||
|
updatedByUserId: string;
|
||||||
|
updatedAt: string;
|
||||||
|
statusOrder: number;
|
||||||
|
status: string;
|
||||||
|
birthDate: string;
|
||||||
|
trainingPlace: string;
|
||||||
|
importNationality: string;
|
||||||
|
sourceNationality: string;
|
||||||
|
licenseExpireDate: string;
|
||||||
|
licenseIssueDate: string;
|
||||||
|
licenseNo: string;
|
||||||
|
discountCondition: string;
|
||||||
|
citizenExpire: string;
|
||||||
|
citizenIssue: string;
|
||||||
|
citizenId: string;
|
||||||
|
userRole: string;
|
||||||
|
userType: string;
|
||||||
|
checkpointEN: string;
|
||||||
|
checkpoint: string;
|
||||||
|
retireDate: string;
|
||||||
|
startDate: string;
|
||||||
|
registrationNo: string;
|
||||||
|
telephoneNo: string;
|
||||||
|
email: string;
|
||||||
|
gender: string;
|
||||||
|
username: string;
|
||||||
|
lastNameEN: string;
|
||||||
|
lastName: string;
|
||||||
|
middleNameEN: string;
|
||||||
|
middleName: string;
|
||||||
|
firstNameEN: string;
|
||||||
|
firstName: string;
|
||||||
|
namePrefix: string;
|
||||||
|
selectedImage: string;
|
||||||
|
subDistrictId: string;
|
||||||
|
districtId: string;
|
||||||
|
provinceId: string;
|
||||||
|
streetEN: string;
|
||||||
|
street: string;
|
||||||
|
mooEN: string;
|
||||||
|
moo: string;
|
||||||
|
soiEN: string;
|
||||||
|
soi: string;
|
||||||
|
addressEN: string;
|
||||||
|
address: string;
|
||||||
|
createdByUserId: string;
|
||||||
|
createdAt: string;
|
||||||
|
code: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
step: number;
|
||||||
|
workStatus: string;
|
||||||
|
requestWorkId: string;
|
||||||
|
attributes: any;
|
||||||
|
customerDuty?: boolean;
|
||||||
|
customerDutyCost?: number;
|
||||||
|
companyDuty?: boolean;
|
||||||
|
companyDutyCost?: number;
|
||||||
|
individualDuty?: boolean;
|
||||||
|
individualDutyCost?: number;
|
||||||
|
responsibleUserLocal?: boolean;
|
||||||
|
responsibleUserId?: string;
|
||||||
|
responsibleUser?: User;
|
||||||
|
taskOrderId: string;
|
||||||
|
requestWork: RequestWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskOrderPayload {
|
||||||
|
taskList: {
|
||||||
|
step: number;
|
||||||
|
requestWorkId: string;
|
||||||
|
requestWorkStep?: Task;
|
||||||
|
taskStatus?: TaskStatus;
|
||||||
|
}[];
|
||||||
|
taskProduct?: {
|
||||||
|
productId: string;
|
||||||
|
discount?: number;
|
||||||
|
}[];
|
||||||
|
institutionId: string;
|
||||||
|
contactTel: string;
|
||||||
|
contactName: string;
|
||||||
|
taskStatus?: TaskOrderStatus;
|
||||||
|
taskName: string;
|
||||||
|
registeredBranchId?: string;
|
||||||
|
id?: string;
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductService {
|
||||||
|
id: string;
|
||||||
|
quotationId: string;
|
||||||
|
order: number;
|
||||||
|
vat: number;
|
||||||
|
amount: number;
|
||||||
|
discount: number;
|
||||||
|
pricePerUnit: number;
|
||||||
|
installmentNo: number;
|
||||||
|
productId: string;
|
||||||
|
workId: string;
|
||||||
|
serviceId: string;
|
||||||
|
attributes: any;
|
||||||
|
product: Product;
|
||||||
|
service: Service;
|
||||||
|
work: Work;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserTaskStatus {
|
||||||
|
Pending = 'Pending',
|
||||||
|
Accept = 'Accept',
|
||||||
|
Submit = 'Submit',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetTaskStatusPayload {
|
||||||
|
failedComment?: string;
|
||||||
|
failedType?: string;
|
||||||
|
taskStatus: TaskStatus;
|
||||||
|
requestWorkId: string;
|
||||||
|
step: number;
|
||||||
|
}
|
||||||
|
|
@ -117,7 +117,7 @@ export function deleteItem(items: unknown[], index: number) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatNumberDecimal(num: number, point: number): string {
|
export function formatNumberDecimal(num: number, point: number = 2): string {
|
||||||
return num.toLocaleString('eng', {
|
return num.toLocaleString('eng', {
|
||||||
minimumFractionDigits: point,
|
minimumFractionDigits: point,
|
||||||
maximumFractionDigits: point,
|
maximumFractionDigits: point,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export type WorkflowStep = {
|
||||||
userId: string;
|
userId: string;
|
||||||
user: CreatedBy;
|
user: CreatedBy;
|
||||||
}[];
|
}[];
|
||||||
responsibleInstitution: string[];
|
responsibleInstitution: (string | { group: string })[];
|
||||||
attributes: WorkFlowAttributes;
|
attributes: WorkFlowAttributes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ export function formatAddress(opt: {
|
||||||
soiEN?: string;
|
soiEN?: string;
|
||||||
street?: string;
|
street?: string;
|
||||||
streetEN?: string;
|
streetEN?: string;
|
||||||
province: Province;
|
province?: Province | null;
|
||||||
district: District;
|
district?: District | null;
|
||||||
subDistrict: SubDistrict;
|
subDistrict?: SubDistrict | null;
|
||||||
en?: boolean;
|
en?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
@ -24,23 +24,29 @@ export function formatAddress(opt: {
|
||||||
if (opt.soiEN) addressParts.push(`Soi ${opt.soiEN},`);
|
if (opt.soiEN) addressParts.push(`Soi ${opt.soiEN},`);
|
||||||
if (opt.streetEN) addressParts.push(`${opt.streetEN} Rd.`);
|
if (opt.streetEN) addressParts.push(`${opt.streetEN} Rd.`);
|
||||||
|
|
||||||
addressParts.push(`${opt.subDistrict.nameEN} sub-district,`);
|
if (opt.subDistrict) {
|
||||||
addressParts.push(`${opt.district.nameEN} district,`);
|
addressParts.push(`${opt.subDistrict.nameEN} sub-district,`);
|
||||||
addressParts.push(`${opt.province.nameEN},`);
|
}
|
||||||
|
if (opt.district) addressParts.push(`${opt.district.nameEN} district,`);
|
||||||
|
if (opt.province) addressParts.push(`${opt.province.nameEN},`);
|
||||||
} else {
|
} else {
|
||||||
// th
|
// th
|
||||||
addressParts = [`${opt.address},`];
|
addressParts = [`${opt.address},`];
|
||||||
if (opt.moo) addressParts.push(`หมู่ ${opt.moo},`);
|
if (opt.moo) addressParts.push(`หมู่ ${opt.moo},`);
|
||||||
if (opt.soi) addressParts.push(`ซอย${opt.soi},`);
|
if (opt.soi) addressParts.push(`ซอย ${opt.soi},`);
|
||||||
if (opt.street) addressParts.push(`ถนน${opt.street},`);
|
if (opt.street) addressParts.push(`ถนน${opt.street},`);
|
||||||
|
|
||||||
addressParts.push(
|
if (opt.subDistrict) {
|
||||||
`${opt.province.id === '10' ? 'แขวง' : 'ตำบล'}${opt.subDistrict.name},`,
|
addressParts.push(
|
||||||
);
|
`${opt.province?.id === '10' ? 'แขวง' : 'ตำบล'}${opt.subDistrict.name},`,
|
||||||
addressParts.push(
|
);
|
||||||
`${opt.province.id === '10' ? 'เขต' : 'อำเภอ'}${opt.district.name},`,
|
}
|
||||||
);
|
if (opt.district) {
|
||||||
addressParts.push(`จังหวัด${opt.province.name},`);
|
addressParts.push(
|
||||||
|
`${opt.province?.id === '10' ? 'เขต' : 'อำเภอ'}${opt.district.name},`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (opt.province) addressParts.push(`จังหวัด${opt.province.name},`);
|
||||||
// addressParts.push(
|
// addressParts.push(
|
||||||
// `${opt.province.id === '10' ? t('address.subArea') : t('address.subDistrict')}${opt.subDistrict.name},`,
|
// `${opt.province.id === '10' ? t('address.subArea') : t('address.subDistrict')}${opt.subDistrict.name},`,
|
||||||
// );
|
// );
|
||||||
|
|
@ -52,7 +58,7 @@ export function formatAddress(opt: {
|
||||||
// );
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
addressParts.push(`${opt.subDistrict.zipCode}`);
|
if (opt.subDistrict) addressParts.push(`${opt.subDistrict.zipCode}`);
|
||||||
|
|
||||||
return addressParts.join(' ');
|
return addressParts.join(' ');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue