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