feat: task order (#141)

* feat: task order => routes

* feat: Page

* refactor: pagination

* refactor: taskOrder => table, card and constants

* feat: add structure select request list comp

* fix: re-export type

* refactor: edit path route of task order

* feat: trigger task order

* refactor: edit type task statss

* feat: table select request list

* feat: i18n

* refactor: quasar expansion chevron color

* refactor: type

* refactor: state btn status done

* feat: task order => order view layout

* feat: task order => remark expansion

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

* feat: upload file section optional layout

* feat: task order => additional file expansion

* feat: task order => payment expansion

* feat: conditionally add urgent

* feat: send id together with link

* refactor: edit type

* feat: new form.ts

* refactor: edit url

* refactor: edit id trigger

* feat: select institution component

* feat: task order code i18n

* feat: task order => document expansion form

* feat: fallback address on null

* refactor: add type for table

* feat: add filter parameter

* refactor: edit name routes

* refactor: add type of task order payload

* refactor: by value form

* refactor: responsive quotation form info

* refactor: submit form

* refactor: add i18n

* refactor: status canceled

* refactor: handle task status

* refactor: handle mode view

* refactor: addtaskstatus

* refactor: i18n & constants

* refactor: table employee

* refactor: select ready request work

* refactor: handle save form

* refactor: edit layout btn

* feat: undo()

* cleanup delete import

* feat: closetab

* refactor: handle readonly

* fix: body edit

* refactor: handle readonly uploadfile

* feat: import manage attachment

* refactor: quotation/task-order => type

* refactor: select ready request work

* refactor: i18n & constants

* chore: clean duplicate i18n

* refactor: type according to backend relation

* refactor: edit base url

* feat: upload file

* feat: fetch file list

* feat: get url file

* refactor: set default opened

* refactor: type

* feat: removefile

* feat: task order => select product

* feat: add parameter only active branch is selectable

* refactor: add i18n

* feat: set layout

* feat: add info product expansion

* refactor: new info messenger

* refactor: add slot name value

* refactor: add i18n

* refactor: edit type task status

* refactor: use date format

* refactor: value can null

* refactor: add i18n

* cleanup

* feat: productlistinput

* refactor: edit i18n

* refactor: edit redo

* refactor: add slot

* feat: task order => i18n

* refactor: task order => constant

* refactor: taskOrder => status type and index

* feat: taskOrder => ReceiveDialog

* refactor: wording

* refactor: table employee due date

* refactor: receive task i18n

* feat: trigger receive & task stat in receive page

* refactor: receive dialog task in cart  i18n

* fix: remove task-order/receive/add

* feat: receivetabletaskorder

* refactor: fetch task on receive dialog

* feat: add separate api get user task

* refactor: receive fetch (messenger)

* refactor: edit layout table

* refactor: task order i18n & constant

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

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

* refactor: trigger receive view

* fix: add receive task condition

* feat: total count

* feat: prepare information

* fix: i18n error task order not found

* refacor: value

* feat: select worker

* refactor: status i18n & constant

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

* fix: order => select ready task

* refactor: order => toggle status

* refactor: receive => receive dialog

* feat: featch value

* refactor: task status display components

* refactor: status active can is null

* feat: update status tab

* refactor: data display

* refactor: i18n & fullTaskOrder variable

* refactor: task receive view

* refactor: add type responsible user

* refactor: set group messenger

* cleanup:

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

* refactor: receive view

* refactor: show info messenger

* refactor: handle flow step

* refactor: receive view => opacity when pending

* feat: add workflow template name and step name

* feat: display workflow data on table

* feat: add template step identifier

* fix: edit does not change workflow id if changed

* feat: detect if same template and step

* refactor: handle template

* refactor: add slot name product

* refactor: map step in list product

* refactor: bind data messenger list group

* refactor: change endpoint name

* chore: add helper package

* feat: changetaskstatus

* refactor: update type

* refactor: set color btn

* refactor: add step

* refactor: add resposible institution

* feat: disabled

* refactor: map responsible institution

* fix: order view => readonly

* chore: clean

* refactor: edit url api

* refactor: edit name type

* refactor: add slots action

* refactor: add type row

* refactor: add opts of task status

* refactor: add select status

* refactor: handle btn

* refactor: add btn change task status

* refactor: edit i18n redo th

* refactor: sort status opts

* feat: receive & order banner img

* refactor: fetch status after submit

* refactor: handle create only

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

* feat: receive messenger profile

* refactor: receive toggle status (display only)

* fix: document expansion readonly

* feat: confirmsendingbtn

* refactor: constant and task order status

* feat: receive task list count

* refactor: post or get

* refactor: define props institution group

* refactor: fetch status after submit

* refactor: handle create

* refactor: handle query

* refactor: update endpoint to support accept multiple order

* refactor: change function name

* feat: receive => functional accept task order

* feat: task status count

* feat: receive stat card count

* refactor: order messenger profile

* refactor: edit value to be task status

* refactor: handle status of type order

* refactor: use componet task status

* refactor: handle show btn saving status

* refactor: order => task status

* refactor: edit selectStatus => changeStatus

* refactor: edit @click btn confirmssending

* refactor: add i18n

* refactor: add function get template data

* refactor: add change status

* refactor: handle type receive

* feat: order => auto change tab by status

* refactor: fetch task after change status

* feat: fail remark dialog

* refactor: display step order (table employee)

* refactor: fail remark dialog

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

* refactor: task list type & change status param

* refactor: table task order, td background when selected

* refactor: order => change status param

* refactor: order => selectedEmployee variable type

* refactor: task status component => shield btn

* refactor: receive => change status

* refactor: order => step btn waiting

* fix: step btn waiting condition

* refactor: filter selectable task (Failed)

* refactor: find index condition on check

* refactor: no request list available

* refactor: fail btn no-wrap

* refactor: fail dialog readonly

* fix: reset state on open dialog

* fix: wrong title position

* refactor: hide task status drop down icon

* fix: handle check condition

* refactor: add userTask type and status

* feat: submit task order function

* refactor: table employee checkbox display condition

* refactor: main layout

* fix: task order validate i18n

* refactor: table task order add submit status

* refactor: status list

* refactor: info product => user task status

* feat: receive => submit task & step

* refactor: i18n

* feat: complete task oder function

* refactor: task status component no action props

* refactor: info messenger status

* refactor: receive and order view

* refactor: order complete view

* refactor: order => complete color and title

* refactor: calc price on table

* refactor: quotation table i18n + product image

* refactor: remove urgent checkbox

* refactor: task status color

* feat: calc summary price

* fix: data is not available

* feat: add doc view structure

* refactor: format address text

* feat: fetch document data from api

* fix: value is null

* fix: regression cannot edit package

* feat: add document view for task order

* feat: add view document button

* feat: update type add discount

* feat: readonly on cancel

* feat: add discount from relation

* refactor: add taskProduct on submit order

* refactor: order => task product discount

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

* refactor: receive date

* refactor: receive task status count

---------

Co-authored-by: puriphatt <puriphat@frappet.com>
Co-authored-by: nwpptrs <jay02499@gmail.com>
Co-authored-by: Thanaphon Frappet <thanaphon@frappet.com>
Co-authored-by: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Co-authored-by: oat_dev <nattapon@frappet.com>
This commit is contained in:
Methapon Metanipat 2024-12-25 11:59:49 +07:00 committed by GitHub
parent cd0831bac1
commit 9eff614dbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 6981 additions and 361 deletions

View file

@ -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
View file

@ -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)

View file

@ -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 +

View file

@ -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

View file

@ -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>

View file

@ -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="{

View file

@ -0,0 +1,7 @@
<script lang="ts" setup>
defineProps<{}>();
const emit = defineEmits<{}>();
</script>
<template></template>
<style scoped></style>

View file

@ -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>

View 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>

View file

@ -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,
}" }"
> >

View file

@ -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;
}

View file

@ -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.',
}, },
}, },

View file

@ -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: 'ไม่พบใบสั่งงาน',
}, },
}, },

View file

@ -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',
}) })
}} }}

View file

@ -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;

View file

@ -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));

View file

@ -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

View file

@ -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>

View file

@ -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"
/> />

View file

@ -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`
: '' : ''
" "

View file

@ -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>

View file

@ -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}`) || '-'

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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'];

View 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>

View 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>

View 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>

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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,
};
});

File diff suppressed because it is too large Load diff

View file

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

View file

@ -0,0 +1,774 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useRoute } from 'vue-router';
import { computed, onMounted, ref, watch } from 'vue';
import { getUserId } from 'src/services/keycloak';
// NOTE: Import Components
import { SaveButton } from 'src/components/button';
import { StateButton } from 'components/button';
import InfoMessengerExpansion from '../expansion/receive/InfoMessengerExpansion.vue';
import InfoProductExpansion from '../expansion/receive/InfoProductExpansion.vue';
import FormGroupHead from 'src/pages/08_request-list/FormGroupHead.vue';
import TableEmployee from '../TableEmployee.vue';
import TaskStatusComponent from '../TaskStatusComponent.vue';
import FailRemarkDialog from './FailRemarkDialog.vue';
// NOTE: Import Types and Store
import { dateFormatJS, dateFormat } from 'src/utils/datetime';
import { useTaskOrderForm } from '../form';
import { initLang, initTheme, Lang } from 'src/utils/ui';
import { RequestWork } from 'src/stores/request-list';
import { baseUrl, dialogWarningClose } from 'src/stores/utils';
import {
TaskOrder,
TaskOrderStatus,
TaskStatus,
UserTaskStatus,
} from 'src/stores/task-order/types';
import useOptionStore from 'src/stores/options';
import { useTaskOrderStore } from 'src/stores/task-order';
const route = useRoute();
const taskOrderFormStore = useTaskOrderForm();
const { currentFormData, state, fullTaskOrder } =
storeToRefs(taskOrderFormStore);
const statusTabForm = ref<
{
title: string;
status: 'done' | 'doing' | 'waiting';
handler: () => void;
active?: () => boolean;
}[]
>([
{
title: 'receive',
status: 'doing',
active: () => view.value === UserTaskStatus.Pending,
handler: () => (
(view.value = UserTaskStatus.Pending), console.log(view.value)
),
},
{
title: 'sendTaskOrder',
status: 'waiting',
active: () => view.value === UserTaskStatus.Submit,
handler: () => (
(view.value = UserTaskStatus.Submit), console.log(view.value)
),
},
]);
const failedDialog = ref(false);
const taskStatusRecords = ref<
{
requestWorkId: string;
step: number;
failedComment?: string;
failedType?: string;
code?: string;
}[]
>([]);
const selectedEmployee = ref<
(RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
} | null;
})[][]
>([]);
const view = ref<UserTaskStatus>(UserTaskStatus.Pending);
const TAB_STATUS = ['Pending', 'Accept', 'Submit'];
function getStatus(
status: UserTaskStatus,
doneIndex: number,
doingIndex: number,
) {
return TAB_STATUS.findIndex((v) => v === status) >= doneIndex
? 'done'
: TAB_STATUS.findIndex((v) => v === status) >= doingIndex
? 'doing'
: 'waiting';
}
async function fetchStatus() {
if (!fullTaskOrder.value) return;
statusTabForm.value = [
{
title: 'receive',
status: getStatus(
fullTaskOrder.value?.userTask[0]?.userTaskStatus ||
UserTaskStatus.Pending,
2,
-1,
),
active: () => view.value === UserTaskStatus.Pending,
handler: () => (
(view.value = UserTaskStatus.Pending), console.log(view.value)
),
},
{
title: 'sendTaskOrder',
status: getStatus(
fullTaskOrder.value?.userTask[0]?.userTaskStatus ||
UserTaskStatus.Pending,
2,
2,
),
active: () => view.value === UserTaskStatus.Submit,
handler: () => (
(view.value = UserTaskStatus.Submit), console.log(view.value)
),
},
];
}
function getTemplateData(
requestWork: RequestWork,
targetStep: number,
): {
id: string;
templateName: string;
templateStepName: string;
step: number;
} | null {
const flow = requestWork.productService.service?.workflow;
if (!flow) return null;
const step = flow.step.find((v) => v.order === targetStep);
if (!step) return null;
return {
id: step.id,
templateName: flow.name,
templateStepName: step.name || '-',
step: step.order,
};
}
let taskListGroup = computed(() => {
const cacheData = currentFormData.value.taskList.reduce<
{
product: RequestWork['productService']['product'];
list: (RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
} | null;
taskStatus?: TaskStatus;
})[];
}[]
>((acc, curr) => {
const taskStatus = curr.taskStatus;
const task = curr.requestWorkStep;
const step = curr.step;
if (!task) return acc;
if (task.requestWork) {
let exist = acc.find(
(item) => task.requestWork.productService.productId == item.product.id,
);
const record = Object.assign(task.requestWork, {
_template: getTemplateData(task.requestWork, step),
taskStatus: taskStatus ?? TaskStatus.Pending,
});
if (exist) {
exist.list.push(task.requestWork);
} else {
acc.push({
product: task.requestWork.productService.product,
list: [record],
});
if (selectedEmployee.value.length < acc.length) {
selectedEmployee.value.push([]);
}
}
}
return acc;
}, []);
return cacheData;
});
// NOTE: Function
async function sendTask() {
if (!fullTaskOrder.value) return;
if (
!fullTaskOrder.value.taskList.every(
(t) =>
t.taskStatus === TaskStatus.Success ||
t.taskStatus === TaskStatus.Failed,
)
)
return;
await useTaskOrderStore().submitTaskOrder(fullTaskOrder.value.id);
await taskOrderFormStore.assignFormData(
fullTaskOrder.value.id,
'info',
getUserId(),
);
await fetchStatus();
view.value = UserTaskStatus.Submit;
}
function getMessengerName(
record: TaskOrder,
opts?: {
gender?: boolean;
url?: boolean;
locale?: string;
},
) {
const user = record.taskList[0].requestWorkStep.responsibleUser;
if (!user) return '-';
const url = `${baseUrl}/user/${user.id}/profile-image/${user.selectedImage}`;
return opts?.gender
? user.gender
: opts?.url
? url
: {
[Lang.English]: `${useOptionStore().mapOption(user.namePrefix as string)} ${user.firstNameEN} ${user.lastNameEN}`,
[Lang.Thai]: `${useOptionStore().mapOption(user.namePrefix as string)} ${user.firstName} ${user.lastName}`,
}[opts?.locale || Lang.English] || '-';
}
function handleChangeStatus(
records: {
data: (RequestWork & {
_template?: {
id: string;
templateName: string;
templateStepName: string;
step: number;
} | null;
})[];
status: TaskStatus;
},
index: number,
) {
const { data, status } = records;
if (data.length === 0) return;
taskStatusRecords.value = data.map((v) => ({
code: `${v.productService.product.code}-${v.request.code}`,
requestWorkId: v.id || '',
step: v._template?.step || 0,
}));
if (status === TaskStatus.Failed) {
failedDialog.value = true;
return;
}
taskOrderFormStore.changeStatus(taskStatusRecords.value, status, () => {
if (!currentFormData.value.id) return;
selectedEmployee.value[index] = [];
taskOrderFormStore.assignFormData(
currentFormData.value.id,
'info',
getUserId(),
);
});
}
function taskStatusCount(index: number, id: string) {
const task = fullTaskOrder.value?.taskList.filter(
(t) => t.requestWorkStep.requestWork.productService.productId === id,
);
if (index === 1) {
return task?.filter((t) => t.taskStatus === TaskStatus.InProgress).length;
} else if (index === 2) {
return task?.filter(
(t) =>
t.taskStatus === TaskStatus.Success ||
t.taskStatus === TaskStatus.Complete ||
t.taskStatus === TaskStatus.Validate,
).length;
} else {
return task?.filter(
(t) =>
t.taskStatus === TaskStatus.Canceled ||
t.taskStatus === TaskStatus.Redo ||
t.taskStatus === TaskStatus.Failed,
).length;
}
}
onMounted(async () => {
initTheme();
initLang();
const currentId = route.params['id'];
state.value.mode = 'info';
if (currentId !== undefined && typeof currentId === 'string') {
await taskOrderFormStore.assignFormData(
currentId,
state.value.mode,
getUserId(),
);
await fetchStatus();
}
});
watch([currentFormData.value.taskStatus], () => {
fetchStatus();
});
</script>
<template>
<div v-if="fullTaskOrder" class="column surface-0 fullscreen">
<div class="color-bar" :class="{ dark: $q.dark.isActive }">
<div class="pink-segment"></div>
<div class="light-pink-segment"></div>
<div class="gray-segment"></div>
</div>
<!-- SEC: Header -->
<header
class="row q-px-md q-py-sm items-center full justify-between relative-position"
>
<section class="banner" :class="{ dark: $q.dark.isActive }"></section>
<div style="flex: 1" class="row items-center">
<RouterLink to="/task-order">
<q-img src="/icons/favicon-512x512.png" width="3rem" />
</RouterLink>
<span class="column text-h6 text-bold q-ml-md">
{{ $t('taskOrder.receive') }}
<!-- {{ code || '' }} -->
<span class="text-caption text-regular app-text-muted">
{{
$t('quotation.processOn', {
msg: dateFormatJS({
date: fullTaskOrder
? fullTaskOrder.createdAt
: new Date(Date.now()),
dayStyle: 'numeric',
monthStyle: 'long',
locale: $i18n.locale === 'tha' ? 'th-Th' : 'en-US',
}),
})
}}
{{
dateFormat(
fullTaskOrder ? fullTaskOrder.createdAt : new Date(Date.now()),
false,
true,
)
}}
</span>
</span>
</div>
</header>
<!-- SEC: Body -->
<article
class="col full-width q-pa-md"
style="flex-grow: 1; overflow-y: auto"
>
<section class="col-sm col-12">
<div class="col q-gutter-y-md">
<nav
class="surface-1 q-pa-sm row no-wrap full-width scroll rounded"
style="gap: 10px"
>
<!-- TODO: replace step and status -->
<StateButton
v-for="i in statusTabForm"
:key="i.title"
:label="$t(`taskOrder.${i.title}`)"
:statusActive="i.active?.()"
:statusDone="i.status === 'done'"
:status-waiting="i.status === 'waiting'"
@click="i.handler()"
/>
</nav>
<article
v-if="
fullTaskOrder.taskOrderStatus !== TaskOrderStatus.Pending &&
view !== UserTaskStatus.Submit
"
class="row items-center surface-1 q-pa-md rounded gradient-stat"
>
<span
class="row col rounded q-px-sm q-py-md info"
style="border: 1px solid hsl(var(--info-bg))"
>
{{ $t('taskOrder.allProduct') }}
<span class="q-ml-auto">{{ fullTaskOrder.taskList.length }}</span>
</span>
<span
class="row col rounded q-px-sm q-py-md q-mx-md positive"
style="border: 1px solid hsl(var(--positive-bg))"
>
{{ $t('taskOrder.alreadySentTask') }}
<span class="q-ml-auto">
{{
fullTaskOrder.taskList.filter(
(t) =>
t.taskStatus === TaskStatus.Complete ||
t.taskStatus === TaskStatus.Success ||
t.taskStatus === TaskStatus.Validate ||
t.taskStatus === TaskStatus.Redo ||
t.taskStatus === TaskStatus.Failed,
).length
}}
</span>
</span>
<span
class="row col rounded q-px-sm q-py-md warning"
style="border: 1px solid hsl(var(--warning-bg))"
>
{{ $t('taskOrder.status.Pending') }}
<span class="q-ml-auto">
{{
fullTaskOrder.taskList.filter(
(t) => t.taskStatus === TaskStatus.InProgress,
).length
}}
</span>
</span>
</article>
<InfoMessengerExpansion
:gender="getMessengerName(fullTaskOrder, { gender: true })"
:contact-url="getMessengerName(fullTaskOrder, { url: true })"
:contact-name="
getMessengerName(fullTaskOrder, { locale: $i18n.locale })
"
:contact-tel="
fullTaskOrder.taskList[0].requestWorkStep.responsibleUser
?.telephoneNo
"
:email="
fullTaskOrder.taskList[0].requestWorkStep.responsibleUser?.email
"
:status="
fullTaskOrder.userTask[0]?.userTaskStatus ||
UserTaskStatus.Pending
"
/>
<InfoProductExpansion
:code="currentFormData.code"
:task-name="currentFormData.taskName"
:issue-branch="currentFormData.registeredBranchId"
:agencies="
$i18n.locale === 'eng'
? fullTaskOrder.institution?.nameEN
: fullTaskOrder.institution?.name
"
:made-by="
$i18n.locale === 'eng'
? `${fullTaskOrder?.createdBy.firstNameEN} ${fullTaskOrder?.createdBy.lastNameEN}`
: `${fullTaskOrder?.createdBy.firstName} ${fullTaskOrder?.createdBy.lastName}`
"
:contact-tel="currentFormData.contactTel"
:contact-name="currentFormData.contactName"
:userTaskStatus="
fullTaskOrder?.userTask[0]?.userTaskStatus ||
UserTaskStatus.Pending
"
>
<div
v-for="({ product, list }, i) in taskListGroup"
:key="product.id"
class="bordered-b"
>
<q-expansion-item
dense
class="overflow-hidden"
switch-toggle-side
style="border-radius: var(--radius-2)"
expand-icon="mdi-chevron-down-circle"
header-class="q-py-sm text-medium text-body items-center rounded q-mx-md q-my-sm"
>
<template #header>
<q-avatar class="q-mr-md" size="md">
<q-img
class="text-center"
:ratio="1"
:src="`${baseUrl}/product/${product.id}/image/${product.selectedImage}`"
>
<template #error>
<q-icon
class="full-width full-height"
name="mdi-shopping-outline"
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
/>
</template>
</q-img>
</q-avatar>
<span>
{{ product.name }}
<div class="app-text-muted text-caption">
{{ product.code }}
</div>
</span>
<span
v-if="
fullTaskOrder.taskOrderStatus === TaskOrderStatus.Pending
"
class="q-ml-auto"
>
<div
class="rounded q-px-sm row items-center"
style="background: hsl(var(--text-mute) / 0.15)"
>
<q-icon
name="mdi-account-group-outline"
size="xs"
class="q-pr-sm"
/>
{{ list.length }}
</div>
</span>
<span v-else class="q-ml-auto row items-center q-gutter-x-sm">
<div
v-for="v in 3"
:key="v"
class="rounded q-px-sm row items-center"
style="background: hsl(var(--text-mute) / 0.1)"
:style="`color: hsl(var(--${
v === 1 ? 'warning' : v === 2 ? 'positive' : 'negative'
}-bg))`"
>
<q-icon
name="mdi-account-group-outline"
size="xs"
class="q-pr-sm"
/>
{{ taskStatusCount(v, product.id) }}
</div>
</span>
</template>
<div>
<FormGroupHead>
{{ $t('quotation.employeeList') }}
</FormGroupHead>
<div class="q-pa-md full-width">
<TableEmployee
:checkbox-on="
fullTaskOrder.taskOrderStatus !==
TaskOrderStatus.Pending &&
fullTaskOrder.userTask.every(
(v) => v.userTaskStatus !== UserTaskStatus.Submit,
)
"
:check-all="
fullTaskOrder.taskOrderStatus !==
TaskOrderStatus.Pending &&
fullTaskOrder.userTask.every(
(v) => v.userTaskStatus !== UserTaskStatus.Submit,
)
"
step-on
:rows="list"
@change-all-status="(v) => handleChangeStatus(v, i)"
v-model:selected-employee="selectedEmployee[i]"
>
<template #append="{ props: subProps }">
<TaskStatusComponent
type="receive"
:readonly="
fullTaskOrder.taskOrderStatus ===
TaskOrderStatus.Pending
"
:status="subProps.row.taskStatus"
@click-failed="
() => {
taskStatusRecords = [
{
code: `${subProps.row.productService.product.code}-${subProps.row.request.code}`,
requestWorkId: subProps.row.id || '',
step: subProps.row._template?.step || 0,
failedComment:
fullTaskOrder?.taskList[subProps.rowIndex]
.failedComment || '',
failedType:
fullTaskOrder?.taskList[subProps.rowIndex]
.failedType || '',
},
];
failedDialog = true;
}
"
@change-status="
(status) => {
handleChangeStatus(
{
data: [subProps.row],
status: status,
},
i,
);
}
"
/>
</template>
</TableEmployee>
</div>
</div>
</q-expansion-item>
<FailRemarkDialog
:fail-task-option="list"
v-model:open="failedDialog"
v-model:set-task-status-list="taskStatusRecords"
@submit="
async () => {
await taskOrderFormStore.changeStatus(
taskStatusRecords,
TaskStatus.Failed,
() => {
if (!currentFormData.id) return;
taskOrderFormStore.assignFormData(
currentFormData.id,
'info',
getUserId(),
);
},
);
selectedEmployee = [];
}
"
@close="
() => {
failedDialog = false;
taskStatusRecords = [];
}
"
/>
</div>
</InfoProductExpansion>
</div>
</section>
</article>
<!-- SEC: footer -->
<footer
v-if="fullTaskOrder.taskOrderStatus !== TaskOrderStatus.Pending"
class="surface-1 q-pa-md full-width"
>
<nav class="row justify-end">
<SaveButton
:disabled="
!fullTaskOrder.taskList.every(
(t) =>
t.taskStatus === TaskStatus.Success ||
t.taskStatus === TaskStatus.Failed,
)
"
@click="
dialogWarningClose($t, {
message: $t('dialog.message.confirmSending'),
action: async () => {
await sendTask();
},
cancel: () => {},
})
"
:label="$t('taskOrder.sentTask')"
icon="mdi-check"
solid
/>
</nav>
</footer>
</div>
</template>
<style scoped>
.color-bar {
width: 100%;
height: 1vh;
background: linear-gradient(
90deg,
rgb(214, 51, 108) 0%,
rgba(255, 255, 255, 1) 77%,
rgba(204, 204, 204, 1) 100%
);
display: flex;
overflow: hidden;
}
.color-bar.dark {
opacity: 0.7;
}
.pink-segment {
background-color: var(--pink-7);
flex-grow: 4;
}
.light-pink-segment {
background-color: hsla(var(--pink-7-hsl) / 0.2);
flex-grow: 0.5;
}
.gray-segment {
background-color: #ccc;
flex-grow: 1;
}
.pink-segment,
.light-pink-segment,
.gray-segment {
transform: skewX(-60deg);
}
.gradient-stat {
& .info {
background-image: linear-gradient(
to right,
hsl(var(--info-bg) / 0.15),
var(--surface-1)
);
}
& .positive {
background-image: linear-gradient(
to right,
hsl(var(--positive-bg) / 0.15),
var(--surface-1)
);
}
& .warning {
background-image: linear-gradient(
to right,
hsl(var(--warning-bg) / 0.15),
var(--surface-1)
);
}
}
.banner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url('/images/building-banner.png');
background-repeat: no-repeat;
background-size: cover;
z-index: -1;
&.dark {
filter: invert(100%);
}
}
</style>

View file

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

View file

@ -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

View file

@ -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 &

View file

@ -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;
} }

View file

@ -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;

View file

@ -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 = {

View file

@ -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';

View file

@ -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 {

View 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,
};
});

View 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;
}

View file

@ -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,

View file

@ -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;
}; };

View file

@ -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(' ');
} }