feat: menu request list (#75)
* feat: i18n * feat: request list * refactor: hide stat transition on app.scss * feat: request list i18n * feat: request list => constants and main page * feat: add store * feat: add fetch data * feat: add utilities fn * feat: add store function / types * refactor: request list type * refactor: request list constants * refactor: quotation card => add customData and badge color props * feat: avatar group components * feat: request list group * refactor: request list => remove tab, add table data * feat: send search query * feat: add parameter * refactor: remove unused function * fix: rename component lits to list * feat: show stats from api * chore: cleanup * refactor: make it type safe * refactor: accept rotate flow id as parameter * feat: use page size component * feat: add component, data display & expansion product * feat: i18n * refactor: constants and request list table * refactor: type code, createdAt, updatedAt * refactor: utils function changThemeMode * feat: request list => view page * refactor: use type instead of infer from value * fix: function getEmployeeName att type * refactor: fetch work list * refactor: loop work list * feat: add i18n duty * feat: add form issue component * feat: add form issue section * fix: store error * refactor: edit by value * refactor: accept basic info from outside instead * feat: add status filter support on fetch * refactor: remove delete button * refactor: wording * feat/fix: request list i18n & constant * feat: document type * feat/refactor: request list => document expansion * refactor: doc expansion use FormGroupHead * refactor: fetch data based on id from route param * refactor: text area disable * feat: properties expansion display (mocking) * refactor: add document at product relation * refactor: edit get value product * feat: get workflow step to show on top * refactor: add type * refactor: add get attachment * refactor: add view attachment * refactor: edit file name * refactor: define props get hide icon * refactor: edit align row * refactor: by value table document * refactor: by value row table * feat: add independent ocr dialog * chore: clean up * refactor: accept more props and small adjustment * fix: error withDefault call * feat: accept default metadata when open * fix: typo * feat: add override hook when finish ocr * feat: reset state on open * feat: detect reader result is actually string * fix: variable name conflict * feat: properties to input component * feat: properties input in properties expansion * feat: properties expansion data (temporary) * refactor: add i18n status work * refactor: edit type work status and add step status * refactor: add edit status work * refactor: edit step work * refactor: properties data type * refactor: filter selected product & specific properties * feat: add emit event * refactor: change variable name for better understanding * refactor: hide step that no properties * refactor: work status type to validate * feat: work status color * refactor: key for filename * refactor: close expansion when change step * refactor: responsive meta data * refactor: product expansion responsive * fix: dark mode step text color * fix: document expansion table no data label * refactor: main page body bordered and overflow hidden * refactor: use utils function instead * refactor: add process * refactor: by value name * refactor: add upload file * refactor: upload file * refactor: by value * fix: option worker type * refactor: fetchRequestAttachment after edit * fix: metadata display * refactor: add class full-height * refactor: edit type * refactor: fetch file * refactor: by value visa * refactor: request list attributes type * fix: properties to input props (placeholder, readonly, disable) * feat: request list properties function * fix: error when no workflow * docs: update comment to fix indent * refactor: step type (attributes) * refactor: add attributes payload on editStatusRequestWork function * feat/refactor: functional form expansion/filter worklist * refactor: set attributes properties after submit * refactor: add request work ready status * feat: request list => form employee component * feat/refactor: form expansion select user/layout * fix: properties readonly --------- Co-authored-by: puriphatt <puriphat@frappet.com> Co-authored-by: Thanaphon Frappet <thanaphon@frappet.com>
This commit is contained in:
parent
9105dcf7fe
commit
972f6ba13e
36 changed files with 3653 additions and 57 deletions
|
|
@ -38,7 +38,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Nationality Verification Group",
|
"label": "Nationality Verification Group",
|
||||||
"value": "ืืnvg"
|
"value": "nvg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Border Pass Group",
|
"label": "Border Pass Group",
|
||||||
|
|
@ -1111,10 +1111,10 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "กลุ่ม พิสูจน์สัญชาติ",
|
"label": "กลุ่ม พิสูจน์สัญชาติ",
|
||||||
"value": "ืืnvg"
|
"value": "nvg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "กลุ่ม Border pass ",
|
"label": "กลุ่ม Border pass",
|
||||||
"value": "bp"
|
"value": "bp"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -423,7 +423,7 @@ watch(
|
||||||
class="col-12"
|
class="col-12"
|
||||||
:label="$t('customer.table.fullnameEN')"
|
:label="$t('customer.table.fullnameEN')"
|
||||||
:disable="!readonly"
|
:disable="!readonly"
|
||||||
:model-value="`${(prefixNameOptions.find((v) => v.value === namePrefix) || {}).value || ''} ${firstName || ''} ${lastName || ''}`"
|
:model-value="`${(prefixNameOptions.find((v) => v.value === namePrefix) || {}).value || ''} ${firstNameEN || ''} ${lastNameEN || ''}`"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-select
|
<q-select
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,14 @@ defineProps<{
|
||||||
reporter?: string;
|
reporter?: string;
|
||||||
totalPrice?: number;
|
totalPrice?: number;
|
||||||
urgent?: boolean;
|
urgent?: boolean;
|
||||||
|
hidePreview?: boolean;
|
||||||
|
badgeColor?: string;
|
||||||
|
|
||||||
|
customData?: {
|
||||||
|
label: string;
|
||||||
|
value: string | number | unknown;
|
||||||
|
slotName?: string;
|
||||||
|
}[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
|
|
@ -52,13 +60,14 @@ const rand = Math.random();
|
||||||
<div class="q-mr-sm" style="font-size: 90%">
|
<div class="q-mr-sm" style="font-size: 90%">
|
||||||
<BadgeComponent
|
<BadgeComponent
|
||||||
:title="status"
|
:title="status"
|
||||||
hsla-color="--blue-6-hsl"
|
:hsla-color="badgeColor || '--blue-6-hsl'"
|
||||||
:border="urgent"
|
:border="urgent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="col text-right">
|
<nav class="col text-right">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
v-if="!hidePreview"
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
rounded
|
rounded
|
||||||
|
|
@ -111,7 +120,28 @@ const rand = Math.random();
|
||||||
'surface-2': !urgent,
|
'surface-2': !urgent,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<article class="row q-py-sm">
|
<article
|
||||||
|
class="q-py-sm"
|
||||||
|
:class="{
|
||||||
|
row: $q.screen.gt.sm,
|
||||||
|
column: $q.screen.lt.sm,
|
||||||
|
}"
|
||||||
|
v-if="customData && customData?.length > 0"
|
||||||
|
>
|
||||||
|
<template v-for="cData in customData" :key="cData.label">
|
||||||
|
<template v-if="cData.slotName">
|
||||||
|
<slot :name="cData.slotName" :props="cData" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="col-4 app-text-muted q-pr-sm">
|
||||||
|
{{ cData.label || '-' }}
|
||||||
|
</div>
|
||||||
|
<div class="col-8">{{ cData.value || '-' }}</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article v-else class="row q-py-sm">
|
||||||
<div class="col-4 app-text-muted q-pr-sm">
|
<div class="col-4 app-text-muted q-pr-sm">
|
||||||
{{ $t('quotation.customerName') }}
|
{{ $t('quotation.customerName') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -125,6 +155,7 @@ const rand = Math.random();
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<BadgeComponent
|
<BadgeComponent
|
||||||
|
:hsla-color="badgeColor"
|
||||||
icon="mdi-account-multiple-outline"
|
icon="mdi-account-multiple-outline"
|
||||||
:title="[workerCount, workerMax].join(' / ')"
|
:title="[workerCount, workerMax].join(' / ')"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
35
src/components/08_request-list/DataDisplay.vue
Normal file
35
src/components/08_request-list/DataDisplay.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
icon?: string;
|
||||||
|
iconSize?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
label: '-',
|
||||||
|
value: '-',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="row items-center">
|
||||||
|
<Icon
|
||||||
|
v-if="icon"
|
||||||
|
:icon
|
||||||
|
class="app-text-muted q-pr-sm"
|
||||||
|
:width="iconSize || '2rem'"
|
||||||
|
/>
|
||||||
|
<span class="column">
|
||||||
|
<span class="app-text-muted-2 text-caption">
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
<span class="text-weight-medium">
|
||||||
|
{{ value }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
120
src/components/08_request-list/PropertiesToInput.vue
Normal file
120
src/components/08_request-list/PropertiesToInput.vue
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
PropDate,
|
||||||
|
PropNumber,
|
||||||
|
PropOptions,
|
||||||
|
PropString,
|
||||||
|
} from 'src/stores/product-service/types';
|
||||||
|
|
||||||
|
import SelectInput from '../shared/SelectInput.vue';
|
||||||
|
import DatePicker from '../shared/DatePicker.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
prop: PropString | PropNumber | PropDate | PropOptions;
|
||||||
|
placeholder?: string;
|
||||||
|
readonly?: boolean;
|
||||||
|
disable?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const model = defineModel<string | number | null | undefined>();
|
||||||
|
|
||||||
|
function numberDisplay(prop: PropNumber, data: number): string {
|
||||||
|
if (isNaN(data)) data = 0;
|
||||||
|
|
||||||
|
let formattedNumber: string;
|
||||||
|
|
||||||
|
if (prop.comma && prop.decimal) {
|
||||||
|
formattedNumber = data.toLocaleString('en-US', {
|
||||||
|
minimumFractionDigits: prop.decimalPlace,
|
||||||
|
maximumFractionDigits: prop.decimalPlace,
|
||||||
|
});
|
||||||
|
} else if (prop.comma && !prop.decimal) {
|
||||||
|
formattedNumber = data.toLocaleString('en-US', {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
});
|
||||||
|
} else if (!prop.comma && prop.decimal) {
|
||||||
|
formattedNumber = data.toFixed(prop.decimalPlace);
|
||||||
|
} else {
|
||||||
|
formattedNumber = Math.round(data).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberParse(formatted: string, prop: PropNumber): number {
|
||||||
|
let parsedNumber: number;
|
||||||
|
|
||||||
|
let cleanedFormatted = formatted.replace(/,/g, '');
|
||||||
|
|
||||||
|
if (prop.decimal) {
|
||||||
|
parsedNumber = parseFloat(cleanedFormatted);
|
||||||
|
} else {
|
||||||
|
parsedNumber = parseInt(cleanedFormatted, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(parsedNumber)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedNumber;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-input
|
||||||
|
v-if="prop.type === 'string'"
|
||||||
|
:readonly
|
||||||
|
:disable
|
||||||
|
class="col-7"
|
||||||
|
:model-value="readonly || disable ? model || '-' : model"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
:placeholder
|
||||||
|
:maxlength="prop.isPhoneNumber ? prop.phoneNumberLength : undefined"
|
||||||
|
@update:model-value="
|
||||||
|
(v) => {
|
||||||
|
model = v;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@focus="(e) => (e.target as HTMLInputElement).select()"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-if="prop.type === 'number'"
|
||||||
|
:readonly
|
||||||
|
:disable
|
||||||
|
class="col-7"
|
||||||
|
debounce="500"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
:model-value="numberDisplay(prop, Number(model))"
|
||||||
|
@update:model-value="
|
||||||
|
(v) => {
|
||||||
|
let cleanedFormatted = v?.toString().replace(/,/g, '');
|
||||||
|
|
||||||
|
const x = numberParse(
|
||||||
|
numberDisplay(prop as PropNumber, Number(cleanedFormatted)),
|
||||||
|
prop as PropNumber,
|
||||||
|
);
|
||||||
|
model = x;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@focus="(e) => (e.target as HTMLInputElement).select()"
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
v-if="prop.type === 'date'"
|
||||||
|
:readonly
|
||||||
|
:disabled="disable"
|
||||||
|
class="col-7"
|
||||||
|
v-model="model as string"
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
v-if="prop.type === 'array'"
|
||||||
|
:readonly
|
||||||
|
:disable
|
||||||
|
:label="$t('form.selection')"
|
||||||
|
v-model="model as string"
|
||||||
|
class="col-7"
|
||||||
|
:option="prop.options.map((opt) => ({ label: opt, value: opt }))"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -10,6 +10,7 @@ defineProps<{
|
||||||
border?: boolean;
|
border?: boolean;
|
||||||
solid?: boolean;
|
solid?: boolean;
|
||||||
transparency?: number;
|
transparency?: number;
|
||||||
|
hideIcon?: boolean;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -24,8 +25,17 @@ defineProps<{
|
||||||
: undefined,
|
: undefined,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Icon :icon="icon || 'mdi-circle-medium'" style="margin-right: 0.25rem" />
|
<Icon
|
||||||
{{ title || (!!titleI18n ? $t(titleI18n) : '-') }}
|
v-if="!hideIcon"
|
||||||
|
:icon="icon || 'mdi-circle-medium'"
|
||||||
|
style="margin-right: 0.25rem"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<slot name="label">
|
||||||
|
{{ title || (!!titleI18n ? $t(titleI18n) : '-') }}
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<slot name="append"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
107
src/components/shared/AvatarGroup.vue
Normal file
107
src/components/shared/AvatarGroup.vue
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
data?: Record<string, unknown>[];
|
||||||
|
dataLabel?: string;
|
||||||
|
dataUrl?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
dataLabel: 'name',
|
||||||
|
dataUrl: 'imgUrl',
|
||||||
|
data: () => [
|
||||||
|
{
|
||||||
|
name: 'Someone 1',
|
||||||
|
imgUrl:
|
||||||
|
'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=500&q=60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Someone 2',
|
||||||
|
imgUrl:
|
||||||
|
'https://images.unsplash.com/flagged/photo-1570612861542-284f4c12e75f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=500&q=60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Someone 3',
|
||||||
|
imgUrl:
|
||||||
|
'https://images.unsplash.com/photo-1547425260-76bcadfb4f2c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=500&q=60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Someone 4',
|
||||||
|
imgUrl:
|
||||||
|
'https://images.unsplash.com/photo-1499952127939-9bbf5af6c51c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTF8fHBlcnNvbnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=500&q=60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Someone 5',
|
||||||
|
imgUrl:
|
||||||
|
'https://images.unsplash.com/photo-1504593811423-6dd665756598?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTZ8fHBlcnNvbnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=500&q=60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Someone 6',
|
||||||
|
imgUrl:
|
||||||
|
'https://images.unsplash.com/photo-1504593811423-6dd665756598?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTZ8fHBlcnNvbnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=500&q=60',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="avatar-group">
|
||||||
|
<div class="avatar" v-for="(person, i) in data.slice(0, 3)" :key="i">
|
||||||
|
<q-tooltip>
|
||||||
|
{{ person[dataLabel] }}
|
||||||
|
</q-tooltip>
|
||||||
|
<img
|
||||||
|
:src="
|
||||||
|
typeof person[dataUrl] === 'string' ? (person[dataUrl] as string) : ''
|
||||||
|
"
|
||||||
|
alt="Image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="data.length > 3" class="avatar remaining-count">
|
||||||
|
<q-tooltip>
|
||||||
|
<div v-for="(person, i) in data.slice(3)" :key="i + 3">
|
||||||
|
{{ person.name }}
|
||||||
|
</div>
|
||||||
|
</q-tooltip>
|
||||||
|
<span>{{ `+${data.length - 3}` }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.avatar-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
position: relative;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
.avatar:not(:first-child) {
|
||||||
|
margin-left: -0.75rem;
|
||||||
|
}
|
||||||
|
.avatar:hover {
|
||||||
|
z-index: 1;
|
||||||
|
transform: translateY(-0.5rem);
|
||||||
|
}
|
||||||
|
.avatar img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
.remaining-count {
|
||||||
|
color: hsl(var(--text-mute-2));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background-color: var(--surface-2);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-left: -0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
140
src/components/upload-file/OcrDialog.vue
Normal file
140
src/components/upload-file/OcrDialog.vue
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import ShowAttachment from 'components/ShowAttachent.vue';
|
||||||
|
import DialogForm from 'components/DialogForm.vue';
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
const isRunning = ref(false);
|
||||||
|
|
||||||
|
const file = ref<File>();
|
||||||
|
const url = ref<string>();
|
||||||
|
const metadata = ref<Data>();
|
||||||
|
|
||||||
|
const splitRatio = ref(50);
|
||||||
|
|
||||||
|
type Data = Record<string, any>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string;
|
||||||
|
readonly?: boolean;
|
||||||
|
autoSave?: boolean;
|
||||||
|
data?: Data;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HandleProps = {
|
||||||
|
ocr?: (file: File) => Promise<false | Data>;
|
||||||
|
save?: (file: File, meta?: Data) => void | Promise<boolean>;
|
||||||
|
override?: (before: Data, after: Data) => Data;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'submit', file: File, meta?: Data): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineExpose({ isRunning });
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props & HandleProps>(), { title: '' });
|
||||||
|
|
||||||
|
const input = (() => {
|
||||||
|
const _element = document.createElement('input');
|
||||||
|
_element.type = 'file';
|
||||||
|
_element.accept = 'image/jpeg,image/png';
|
||||||
|
_element.addEventListener('change', change);
|
||||||
|
return _element;
|
||||||
|
})();
|
||||||
|
|
||||||
|
async function change(e: Event) {
|
||||||
|
const _element = e.target as HTMLInputElement | null;
|
||||||
|
const _file = _element?.files?.[0];
|
||||||
|
|
||||||
|
if (!_file) return;
|
||||||
|
|
||||||
|
file.value = _file;
|
||||||
|
|
||||||
|
url.value = await new Promise<string>((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(_file);
|
||||||
|
reader.onload = () => {
|
||||||
|
if (typeof reader.result === 'string') {
|
||||||
|
return resolve(reader.result);
|
||||||
|
}
|
||||||
|
return reject();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!props.ocr) return;
|
||||||
|
|
||||||
|
if (props.data) metadata.value = structuredClone(props.data);
|
||||||
|
|
||||||
|
isOpen.value = true;
|
||||||
|
isRunning.value = true;
|
||||||
|
const ocrResult = await props.ocr(_file);
|
||||||
|
isRunning.value = false;
|
||||||
|
|
||||||
|
if (!ocrResult) return;
|
||||||
|
|
||||||
|
if (!props.override || !metadata.value) {
|
||||||
|
return (metadata.value = ocrResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.entries(metadata.value).some(([k, v]) => ocrResult[k] !== v)) {
|
||||||
|
return (metadata.value = props.override(metadata.value, ocrResult));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<slot
|
||||||
|
name="trigger"
|
||||||
|
:browse="
|
||||||
|
() => {
|
||||||
|
file = undefined;
|
||||||
|
url = undefined;
|
||||||
|
metadata = undefined;
|
||||||
|
input?.click();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogForm
|
||||||
|
v-if="file && url"
|
||||||
|
v-model:modal="isOpen"
|
||||||
|
style="position: absolute"
|
||||||
|
height="100vh"
|
||||||
|
weight="90%"
|
||||||
|
hide-close-event
|
||||||
|
hide-delete
|
||||||
|
edit
|
||||||
|
:title
|
||||||
|
:is-edit
|
||||||
|
:readonly
|
||||||
|
:edit-data="() => (isEdit = true)"
|
||||||
|
:undo="() => (isEdit = false)"
|
||||||
|
:close="() => (isOpen = false)"
|
||||||
|
:submit="
|
||||||
|
() => {
|
||||||
|
if (!file) return;
|
||||||
|
$emit('submit', file, metadata);
|
||||||
|
if (autoSave) save?.(file, metadata);
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<q-splitter class="full-height" v-model="splitRatio">
|
||||||
|
<template #before>
|
||||||
|
<div class="full-height">
|
||||||
|
<slot name="viewer" :url :file>
|
||||||
|
<ShowAttachment :url :file />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #after>
|
||||||
|
<div class="q-pa-md full-height">
|
||||||
|
<slot name="body" :metadata :is-running :is-edit />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-splitter>
|
||||||
|
</DialogForm>
|
||||||
|
</template>
|
||||||
|
|
@ -105,7 +105,7 @@ async function change(e: Event) {
|
||||||
const _file = _element?.files?.[0];
|
const _file = _element?.files?.[0];
|
||||||
|
|
||||||
if (_file) {
|
if (_file) {
|
||||||
const newFileName = `${selectedMenu.value?.group}-${dateFormat(new Date().toISOString())}`;
|
const newFileName = `${selectedMenu.value?.group}-${dateFormat(new Date().toISOString())}-${_file.name}`;
|
||||||
const renamedFile = new File([_file], newFileName, { type: _file.type });
|
const renamedFile = new File([_file], newFileName, { type: _file.type });
|
||||||
|
|
||||||
if (!obj.value[currentIndex.value] && selectedMenu.value) {
|
if (!obj.value[currentIndex.value] && selectedMenu.value) {
|
||||||
|
|
@ -155,14 +155,12 @@ async function change(e: Event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resOcr.group === 'passport') {
|
if (resOcr.group === 'passport') {
|
||||||
const fullName = map['full_name'].split(' ');
|
|
||||||
|
|
||||||
obj.value[currentIndex.value]._meta = {
|
obj.value[currentIndex.value]._meta = {
|
||||||
type: map['doc_type'],
|
type: map['doc_type'],
|
||||||
number: map['doc_number'],
|
number: map['doc_number'],
|
||||||
gender: map['sex'],
|
gender: map['sex'],
|
||||||
firstName: fullName[0],
|
firstName: map['first_name'],
|
||||||
lastName: fullName[1],
|
lastName: map['last_name'],
|
||||||
issueDate: map['issue_date'],
|
issueDate: map['issue_date'],
|
||||||
expireDate: map['expire_date'],
|
expireDate: map['expire_date'],
|
||||||
issuePlace: map['nationality'],
|
issuePlace: map['nationality'],
|
||||||
|
|
@ -195,7 +193,7 @@ async function fileList() {
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
name: item.name?.split('-')[1],
|
name: `${item.name?.split('-')[1]}-${item.name?.split('-')[2] || ''}`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -248,3 +248,17 @@ html {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slide-enter-active {
|
||||||
|
transition: all 0.1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-leave-active {
|
||||||
|
transition: all 0.1s cubic-bezier(1, 0.5, 0.8, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-enter-from,
|
||||||
|
.slide-leave-to {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ $separator-dark-color: var(--border-color);
|
||||||
background: var(--surface-1);
|
background: var(--surface-1);
|
||||||
color: $primary;
|
color: $primary;
|
||||||
|
|
||||||
&:has(input:disabled) {
|
&:has(:where(input:disabled, textarea:disabled)) {
|
||||||
background: var(--surface-tab);
|
background: var(--gray-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,15 +149,11 @@ div.fullscreen.q-drawer__backdrop {
|
||||||
color: hsl(var(--text-mute));
|
color: hsl(var(--text-mute));
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-tree__node-header.relative-position.row.no-wrap.items-center:has(
|
.q-tree__node-header.relative-position.row.no-wrap.items-center:has(.clickable-node):hover {
|
||||||
.clickable-node
|
|
||||||
):hover {
|
|
||||||
background-color: hsla(var(--info-bg) / 0.1);
|
background-color: hsla(var(--info-bg) / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-tree__node-header.relative-position.row.no-wrap.items-center:has(
|
.q-tree__node-header.relative-position.row.no-wrap.items-center:has(.clickable-node.active-node) {
|
||||||
.clickable-node.active-node
|
|
||||||
) {
|
|
||||||
background-color: hsla(var(--info-bg) / 0.1);
|
background-color: hsla(var(--info-bg) / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,8 +161,7 @@ div.fullscreen.q-drawer__backdrop {
|
||||||
padding-left: 0px !important;
|
padding-left: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-tree__node.relative-position.q-tree__node--child:not(:first-child)
|
.q-tree__node.relative-position.q-tree__node--child:not(:first-child) .q-tree__node-header:before {
|
||||||
.q-tree__node-header:before {
|
|
||||||
top: -32px !important;
|
top: -32px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,8 @@ export default {
|
||||||
enterToAdd: 'Press enter to add.',
|
enterToAdd: 'Press enter to add.',
|
||||||
forExample: 'eg. {example}',
|
forExample: 'eg. {example}',
|
||||||
importFromFile: 'Import From File {suffix}',
|
importFromFile: 'Import From File {suffix}',
|
||||||
|
customer: 'Customer',
|
||||||
|
individual: 'Individual',
|
||||||
},
|
},
|
||||||
|
|
||||||
menu: {
|
menu: {
|
||||||
|
|
@ -203,7 +205,7 @@ export default {
|
||||||
debitNote: 'Debit Note',
|
debitNote: 'Debit Note',
|
||||||
},
|
},
|
||||||
|
|
||||||
managDocumente: {
|
manageDocument: {
|
||||||
title: 'Docs',
|
title: 'Docs',
|
||||||
document: 'Document',
|
document: 'Document',
|
||||||
},
|
},
|
||||||
|
|
@ -841,6 +843,48 @@ export default {
|
||||||
name: 'Agencies Name',
|
name: 'Agencies Name',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
requestList: {
|
||||||
|
title: 'Request List',
|
||||||
|
caption: 'All Request List',
|
||||||
|
quotationCode: 'Quotation Code',
|
||||||
|
requestListCode: 'Request List Code',
|
||||||
|
invoiceCode: 'Invoice Code',
|
||||||
|
receiptCode: 'Receipt Code',
|
||||||
|
alienIdCard: 'Alien Identification Card"',
|
||||||
|
relatedDoc: 'Related Documents',
|
||||||
|
attachment: 'Documents',
|
||||||
|
documentInSystem: 'Documents in the System',
|
||||||
|
noDocumentYet: 'Document Not Found',
|
||||||
|
companyEmployee: 'Company Employee',
|
||||||
|
localEmployee: 'Local Employee',
|
||||||
|
nonLocalEmployee: 'Non Local Employee',
|
||||||
|
status: {
|
||||||
|
work: {
|
||||||
|
Pending: 'Await for order',
|
||||||
|
Ready: 'Ready for order',
|
||||||
|
Waiting: 'Work assigned, awaiting action',
|
||||||
|
InProgress: 'In progress',
|
||||||
|
Validate: 'Awaiting validation',
|
||||||
|
Ended: 'Completed',
|
||||||
|
Completed: 'Work finished',
|
||||||
|
},
|
||||||
|
|
||||||
|
Pending: 'Pending',
|
||||||
|
InProgress: 'In Progress',
|
||||||
|
Completed: 'Completed',
|
||||||
|
|
||||||
|
AwaitOrder: 'Awaiting Order',
|
||||||
|
ReadyOrder: 'Ready for Order',
|
||||||
|
EndOrder: 'Order Completed',
|
||||||
|
|
||||||
|
AwaitReview: 'Awaiting Review',
|
||||||
|
UploadedAwaitReview: 'Uploaded, Awaiting Review',
|
||||||
|
ReviewedAwaitUpload: 'Reviewed, Awaiting Upload',
|
||||||
|
Reviewed: 'Reviewed',
|
||||||
|
ApprovedReview: 'Approved',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
dialog: {
|
dialog: {
|
||||||
title: {
|
title: {
|
||||||
incompleteDataEntry: 'Incomplete Data Entry',
|
incompleteDataEntry: 'Incomplete Data Entry',
|
||||||
|
|
@ -968,4 +1012,11 @@ export default {
|
||||||
district: 'District',
|
district: 'District',
|
||||||
province: 'Province',
|
province: 'Province',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
duty: {
|
||||||
|
text: '{subject} Duty',
|
||||||
|
notInclude: 'Not Include Duty',
|
||||||
|
include: 'Include Duty',
|
||||||
|
cost: 'Duty Cost (Baht)',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,8 @@ export default {
|
||||||
enterToAdd: 'กดปุ่ม Enter เพื่อเพิ่ม',
|
enterToAdd: 'กดปุ่ม Enter เพื่อเพิ่ม',
|
||||||
forExample: 'เช่น {example}',
|
forExample: 'เช่น {example}',
|
||||||
importFromFile: 'นำเข้าจากไฟล์ {suffix}',
|
importFromFile: 'นำเข้าจากไฟล์ {suffix}',
|
||||||
|
customer: 'ลูกค้า',
|
||||||
|
individual: 'รายบุคคล',
|
||||||
},
|
},
|
||||||
|
|
||||||
menu: {
|
menu: {
|
||||||
|
|
@ -203,7 +205,7 @@ export default {
|
||||||
debitNote: 'ใบเพิ่มหนี้',
|
debitNote: 'ใบเพิ่มหนี้',
|
||||||
},
|
},
|
||||||
|
|
||||||
managDocumente: {
|
manageDocument: {
|
||||||
title: 'คลังเอกสาร',
|
title: 'คลังเอกสาร',
|
||||||
document: 'เอกสาร',
|
document: 'เอกสาร',
|
||||||
},
|
},
|
||||||
|
|
@ -833,6 +835,47 @@ export default {
|
||||||
name: 'ชื่อหน่วยงาน',
|
name: 'ชื่อหน่วยงาน',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
requestList: {
|
||||||
|
title: 'ใบรายการคำขอ',
|
||||||
|
caption: 'ใบรายการคำขอทั้งหมด',
|
||||||
|
quotationCode: 'เลขที่ใบเสนอราคา',
|
||||||
|
requestListCode: 'เลขที่ใบรายการคำขอ',
|
||||||
|
invoiceCode: 'เลขที่ใบแจ้งหนี้',
|
||||||
|
receiptCode: 'เลขที่ใบเสร็จ/กำกับภาษี',
|
||||||
|
alienIdCard: 'บัตรประจำตัวต่างด้าว',
|
||||||
|
relatedDoc: 'เอกสารที่เกี่ยวข้อง',
|
||||||
|
attachment: 'ไฟล์เอกสาร',
|
||||||
|
documentInSystem: 'เอกสารในระบบ',
|
||||||
|
noDocumentYet: 'ยังไม่พบเอกสาร',
|
||||||
|
companyEmployee: 'พนักงานบริษัท',
|
||||||
|
localEmployee: 'พนักงานในพื้นที่',
|
||||||
|
nonLocalEmployee: 'พนักงานนอกพื้นที่',
|
||||||
|
status: {
|
||||||
|
work: {
|
||||||
|
Pending: 'รอสั่งงาน',
|
||||||
|
Ready: 'พร้อมสั่งงาน',
|
||||||
|
Waiting: 'สั่งงานเเล้วรอดำเนินการ',
|
||||||
|
InProgress: 'กำลังดำเนินการ',
|
||||||
|
Validate: 'รอตรวจสอบ',
|
||||||
|
Ended: 'เสร็จสิ้น',
|
||||||
|
Completed: 'จบงาน',
|
||||||
|
},
|
||||||
|
Pending: 'รอดำเนินการ',
|
||||||
|
InProgress: 'ดำเนินการ',
|
||||||
|
Completed: 'เสร็จสิ้น',
|
||||||
|
|
||||||
|
AwaitOrder: 'รอสั่งงาน',
|
||||||
|
ReadyOrder: 'พร้อมสั่งงาน',
|
||||||
|
EndOrder: 'จบงาน',
|
||||||
|
|
||||||
|
AwaitReview: 'รอตรวจสอบ',
|
||||||
|
UploadedAwaitReview: 'อัปโหลดแล้ว รอตรวจสอบ',
|
||||||
|
ReviewedAwaitUpload: 'ตรวจสอบแล้ว รอการอัปโหลด',
|
||||||
|
Reviewed: 'ตรวจสอบแล้ว',
|
||||||
|
ApprovedReview: 'ผ่านการตรวจสอบ',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
dialog: {
|
dialog: {
|
||||||
title: {
|
title: {
|
||||||
incompleteDataEntry: 'กรอกข้อมูลไม่ครบ',
|
incompleteDataEntry: 'กรอกข้อมูลไม่ครบ',
|
||||||
|
|
@ -955,4 +998,11 @@ export default {
|
||||||
district: 'อำเภอ',
|
district: 'อำเภอ',
|
||||||
province: 'จังหวัด',
|
province: 'จังหวัด',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
duty: {
|
||||||
|
text: 'อากร{subject}',
|
||||||
|
notInclude: 'ไม่ติดอากร',
|
||||||
|
include: 'ติดอากร',
|
||||||
|
cost: 'จำนวนเงินอากร (บาท)',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -142,14 +142,14 @@ onMounted(async () => {
|
||||||
{
|
{
|
||||||
label: 'menu.order',
|
label: 'menu.order',
|
||||||
icon: 'mdi-file-chart-outline',
|
icon: 'mdi-file-chart-outline',
|
||||||
disabled: true,
|
disabled: false,
|
||||||
children: [
|
children: [
|
||||||
{ label: 'requestList', route: '' },
|
{ label: 'requestList', route: '/request-list' },
|
||||||
{ label: 'documentCheck', route: '' },
|
{ label: 'documentCheck', route: '', hidden: true },
|
||||||
{ label: 'workOrder', route: '' },
|
{ label: 'workOrder', route: '', hidden: true },
|
||||||
{ label: 'goodReceipt', route: '' },
|
{ label: 'goodReceipt', route: '', hidden: true },
|
||||||
{ label: 'workReceipt', route: '' },
|
{ label: 'workReceipt', route: '', hidden: true },
|
||||||
{ label: 'workDelivery', route: '' },
|
{ label: 'workDelivery', route: '', hidden: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -165,8 +165,8 @@ onMounted(async () => {
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
label: 'menu.managDocumente',
|
label: 'menu.manageDocument',
|
||||||
icon: 'mdi-archive',
|
icon: 'mdi-archive-outline',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
label: 'document',
|
label: 'document',
|
||||||
|
|
|
||||||
|
|
@ -3082,19 +3082,6 @@ const emptyCreateDialog = ref(false);
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<template #form="{ mode, meta, isEdit }">
|
<template #form="{ mode, meta, isEdit }">
|
||||||
<FormCitizen
|
|
||||||
v-if="mode === 'citizen' && meta"
|
|
||||||
orc
|
|
||||||
ra
|
|
||||||
:readonly="!isEdit"
|
|
||||||
v-model:citizen-id="meta.citizenId"
|
|
||||||
v-model:birth-date="meta.birthDate"
|
|
||||||
v-model:first-name="meta.firstName"
|
|
||||||
v-model:first-name-en="meta.firstNameEN"
|
|
||||||
v-model:last-name="meta.lastName"
|
|
||||||
v-model:last-name-en="meta.lastNameEN"
|
|
||||||
v-model:address="meta.address"
|
|
||||||
/>
|
|
||||||
<FormEmployeePassport
|
<FormEmployeePassport
|
||||||
v-if="mode === 'passport' && meta"
|
v-if="mode === 'passport' && meta"
|
||||||
prefix-id="drawer-info-employee"
|
prefix-id="drawer-info-employee"
|
||||||
|
|
|
||||||
|
|
@ -193,14 +193,14 @@ export const columnsAttachment = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'createdAt',
|
name: 'createdAt',
|
||||||
align: 'center',
|
align: 'left',
|
||||||
label: 'general.uploadDate',
|
label: 'general.uploadDate',
|
||||||
field: 'attachmentName',
|
field: 'attachmentName',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'ัexpireDate',
|
name: 'ัexpireDate',
|
||||||
align: 'center',
|
align: 'left',
|
||||||
label: 'general.expirationDate',
|
label: 'general.expirationDate',
|
||||||
field: 'attachmentName',
|
field: 'attachmentName',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -888,8 +888,10 @@ async function assignFormDataProductService(id: string) {
|
||||||
|
|
||||||
currentService.value = JSON.parse(JSON.stringify(res));
|
currentService.value = JSON.parse(JSON.stringify(res));
|
||||||
|
|
||||||
const workflowRet = await getWorkflowTemplate(res.attributes.workflowId);
|
if (res.attributes && res.attributes.workflowId) {
|
||||||
if (workflowRet) currWorkflow.value = workflowRet;
|
const workflowRet = await getWorkflowTemplate(res.attributes.workflowId);
|
||||||
|
if (workflowRet) currWorkflow.value = workflowRet;
|
||||||
|
}
|
||||||
|
|
||||||
prevService.value = {
|
prevService.value = {
|
||||||
code: res.code,
|
code: res.code,
|
||||||
|
|
|
||||||
608
src/pages/08_request-list/DocumentExpansion.vue
Normal file
608
src/pages/08_request-list/DocumentExpansion.vue
Normal file
|
|
@ -0,0 +1,608 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import OcrDialog from 'src/components/upload-file/OcrDialog.vue';
|
||||||
|
import { parseResultMRZ, runOcr } from 'src/utils/ocr';
|
||||||
|
import { useRequestList } from 'src/stores/request-list';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DocStatus,
|
||||||
|
RowDocument,
|
||||||
|
Attributes,
|
||||||
|
} from 'src/stores/request-list/types';
|
||||||
|
import MainButton from 'src/components/button/MainButton.vue';
|
||||||
|
import FormGroupHead from './FormGroupHead.vue';
|
||||||
|
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||||
|
import FormEmployeePassport from 'components/03_customer-management/FormEmployeePassport.vue';
|
||||||
|
import FormEmployeeVisa from 'components/03_customer-management/FormEmployeeVisa.vue';
|
||||||
|
import { CancelButton, EditButton } from 'src/components/button';
|
||||||
|
|
||||||
|
import { docColumn } from './constants';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { QTableSlots } from 'quasar';
|
||||||
|
import {
|
||||||
|
EmployeePassportPayload,
|
||||||
|
EmployeeVisaPayload,
|
||||||
|
} from 'stores/employee/types';
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
type: 'customer' | 'employee';
|
||||||
|
data: RowDocument & { fileName: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'changeStatus', v: Partial<Data> & { status: DocStatus }): void;
|
||||||
|
(e: 'viewDoc', v: Data): void;
|
||||||
|
(
|
||||||
|
e: 'upload',
|
||||||
|
v: {
|
||||||
|
id: string;
|
||||||
|
file: File;
|
||||||
|
form?: EmployeePassportPayload | EmployeeVisaPayload;
|
||||||
|
group: string;
|
||||||
|
type: 'customer' | 'employee';
|
||||||
|
},
|
||||||
|
done: typeof fetchRequestAttachment,
|
||||||
|
): void;
|
||||||
|
(e: 'download', v: Data): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const group = ref('passport');
|
||||||
|
|
||||||
|
const requestListStore = useRequestList();
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
listDocument: string[];
|
||||||
|
currentId: { customer: string; employee: string };
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
listDocument: () => [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const attributes = defineModel<Attributes>('attributes', {
|
||||||
|
default: {
|
||||||
|
customer: {},
|
||||||
|
employee: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive<{
|
||||||
|
isEdit: boolean;
|
||||||
|
splitPercent: number;
|
||||||
|
tab: 'customer' | 'employee';
|
||||||
|
}>({
|
||||||
|
isEdit: false,
|
||||||
|
splitPercent: 15,
|
||||||
|
tab: 'customer',
|
||||||
|
});
|
||||||
|
|
||||||
|
const docStatus = [
|
||||||
|
DocStatus.AwaitReview,
|
||||||
|
DocStatus.UploadedAwaitReview,
|
||||||
|
DocStatus.ReviewedAwaitUpload,
|
||||||
|
DocStatus.Reviewed,
|
||||||
|
DocStatus.ApprovedReview,
|
||||||
|
];
|
||||||
|
|
||||||
|
const attachmentList = ref<
|
||||||
|
Awaited<ReturnType<typeof requestListStore.getAttachmentRequest>>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
async function ocr(file: File): Promise<Record<string, any> | false> {
|
||||||
|
const result = await ocrGroup(file);
|
||||||
|
if (result) ocrResultToVariable(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ocrGroup(file: File) {
|
||||||
|
switch (group.value) {
|
||||||
|
case 'passport':
|
||||||
|
return ocrPassport(file);
|
||||||
|
case 'visa':
|
||||||
|
return ocrPassport(file);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ocrPassport(file: File): Promise<Record<string, any> | false> {
|
||||||
|
const mrz = await runOcr(file, parseResultMRZ);
|
||||||
|
return mrz?.result || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ocrResultToVariable(
|
||||||
|
data: Record<string, any>,
|
||||||
|
): EmployeePassportPayload | EmployeeVisaPayload {
|
||||||
|
// TODO: assign ocr result to variable
|
||||||
|
let form: EmployeePassportPayload | EmployeeVisaPayload;
|
||||||
|
|
||||||
|
if (group.value === 'passport') {
|
||||||
|
form = {
|
||||||
|
birthCountry: data.birthCountry,
|
||||||
|
previousPassportRef: data.previousPassportRef,
|
||||||
|
issuePlace: data.nationality,
|
||||||
|
issueCountry: data.country,
|
||||||
|
issueDate: new Date(data.issue_date),
|
||||||
|
type: data.doc_type,
|
||||||
|
expireDate: new Date(data.expire_date),
|
||||||
|
birthDate: new Date(data.birthDate),
|
||||||
|
workerStatus: data.workerStatus,
|
||||||
|
nationality: data.nationality,
|
||||||
|
gender: data.sex,
|
||||||
|
lastNameEN: data.lastNameEN,
|
||||||
|
lastName: data.last_name,
|
||||||
|
middleNameEN: data.middleNameEN,
|
||||||
|
middleName: data.middleName,
|
||||||
|
firstNameEN: data.firstNameEN,
|
||||||
|
firstName: data.first_name,
|
||||||
|
namePrefix: data.namePrefix,
|
||||||
|
number: data.doc_number,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
form = {
|
||||||
|
arrivalAt: data.arrivalAt,
|
||||||
|
arrivalTMNo: data.arrivalTMNo,
|
||||||
|
arrivalTM: data.arrivalTM,
|
||||||
|
mrz: data.mrz,
|
||||||
|
entryCount: data.entryCount,
|
||||||
|
issuePlace: data.issue_place,
|
||||||
|
issueCountry: data.country,
|
||||||
|
issueDate: new Date(data.valid_until),
|
||||||
|
type: data.visa_type || '',
|
||||||
|
expireDate: new Date(data.expire_date),
|
||||||
|
remark: data.remark,
|
||||||
|
workerType: data.workerType,
|
||||||
|
number: data.doc_number,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
fetchRequestAttachment,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchRequestAttachment(type: 'customer' | 'employee') {
|
||||||
|
const res = await requestListStore.getAttachmentRequest(
|
||||||
|
{
|
||||||
|
customer: props.currentId.customer,
|
||||||
|
employee: props.currentId.employee,
|
||||||
|
}[type] || '',
|
||||||
|
type,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
attachmentList.value = res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentListDocument = computed(() => {
|
||||||
|
return props.listDocument.map((v, i) => {
|
||||||
|
const fileIds = attachmentList.value[v]
|
||||||
|
? Object.values(attachmentList.value[v])
|
||||||
|
: [];
|
||||||
|
const fileNames = fileIds.map((id) => String(id));
|
||||||
|
return {
|
||||||
|
no: i + 1,
|
||||||
|
documentType: v,
|
||||||
|
fileName: fileNames,
|
||||||
|
amount: fileIds.length,
|
||||||
|
documentInSystem: fileIds.length > 0,
|
||||||
|
status: attributes.value
|
||||||
|
? (attributes.value[state.tab]?.[v] ?? docStatus[0])
|
||||||
|
: docStatus[0],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function triggerCancel() {
|
||||||
|
state.isEdit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerEdit() {
|
||||||
|
state.isEdit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeCustomerTab(opts: { tab: 'customer' | 'employee' }) {
|
||||||
|
state.tab = opts.tab;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
class="overflow-hidden bordered"
|
||||||
|
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="fetchRequestAttachment(state.tab)"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('menu.order.documentCheck') }}
|
||||||
|
</span>
|
||||||
|
<nav class="q-ml-auto row">
|
||||||
|
<CancelButton
|
||||||
|
v-if="state.isEdit"
|
||||||
|
id="btn-info-basic-undo"
|
||||||
|
icon-only
|
||||||
|
type="button"
|
||||||
|
@click.stop="triggerCancel"
|
||||||
|
/>
|
||||||
|
<EditButton
|
||||||
|
v-if="!state.isEdit"
|
||||||
|
id="btn-info-basic-edit"
|
||||||
|
icon-only
|
||||||
|
@click.stop="triggerEdit"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormGroupHead>
|
||||||
|
{{ $t('requestList.relatedDoc') }}
|
||||||
|
</FormGroupHead>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<q-splitter
|
||||||
|
v-model="state.splitPercent"
|
||||||
|
:limits="[0, 100]"
|
||||||
|
style="width: 100%"
|
||||||
|
class="col"
|
||||||
|
before-class="overflow-hidden"
|
||||||
|
after-class="overflow-hidden"
|
||||||
|
>
|
||||||
|
<template v-slot:before>
|
||||||
|
<section class="q-px-md q-py-sm column" style="gap: var(--size-1)">
|
||||||
|
<q-item
|
||||||
|
v-for="tab in ['customer', 'employee'] as const"
|
||||||
|
v-close-popup
|
||||||
|
clickable
|
||||||
|
:key="tab"
|
||||||
|
dense
|
||||||
|
class="no-padding items-center rounded full-width"
|
||||||
|
active-class="active"
|
||||||
|
:active="state.tab === tab"
|
||||||
|
@click="
|
||||||
|
changeCustomerTab({ tab });
|
||||||
|
fetchRequestAttachment(tab);
|
||||||
|
"
|
||||||
|
:id="`btn-${tab}`"
|
||||||
|
>
|
||||||
|
<span class="q-px-md ellipsis">
|
||||||
|
{{
|
||||||
|
`${$t('general.document')}${tab === 'customer' ? $t('customer.employer') : $t('customer.employee')}`
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</q-item>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:after>
|
||||||
|
<section class="q-px-md q-py-sm">
|
||||||
|
<q-table
|
||||||
|
:columns="docColumn"
|
||||||
|
:rows="currentListDocument"
|
||||||
|
bordered
|
||||||
|
flat
|
||||||
|
hide-pagination
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
:no-data-label="$t('general.noDataTable')"
|
||||||
|
class="full-width"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
:class="`text-${col.align}`"
|
||||||
|
: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: (typeof currentListDocument.value)[number];
|
||||||
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||||
|
>
|
||||||
|
<q-tr
|
||||||
|
:class="{
|
||||||
|
dark: $q.dark.isActive,
|
||||||
|
}"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<q-td>
|
||||||
|
{{ props.rowIndex + 1 }}
|
||||||
|
</q-td>
|
||||||
|
<q-td class="text-left">
|
||||||
|
{{
|
||||||
|
$t(`customerEmployee.fileType.${props.row.documentType}`)
|
||||||
|
}}
|
||||||
|
</q-td>
|
||||||
|
<q-td
|
||||||
|
class="text-left q-gutter-xs"
|
||||||
|
style="
|
||||||
|
max-width: 400px;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="{
|
||||||
|
'app-text-muted': !props.row.fileName,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="q-gutter-y-xs">
|
||||||
|
<BadgeComponent
|
||||||
|
hide-icon
|
||||||
|
v-for="v in props.row.fileName"
|
||||||
|
:key="v"
|
||||||
|
:title="v ? v : $t('requestList.noDocumentYet')"
|
||||||
|
hsla-color="--gray-8-hsl"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer': props.row.fileName,
|
||||||
|
}"
|
||||||
|
@click.stop="
|
||||||
|
$emit('viewDoc', {
|
||||||
|
key: props.row.documentType,
|
||||||
|
data: { ...props.row, fileName: v },
|
||||||
|
id:
|
||||||
|
{
|
||||||
|
customer: currentId.customer,
|
||||||
|
employee: currentId.employee,
|
||||||
|
}[state.tab] || '',
|
||||||
|
type: state.tab,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ v ? $t(v) : $t('requestList.noDocumentYet') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<q-icon
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer': props.row.fileName,
|
||||||
|
}"
|
||||||
|
style="color: hsla(207% 96% 32%)"
|
||||||
|
name="mdi-tray-arrow-down"
|
||||||
|
size="xs"
|
||||||
|
class="app-text-muted q-ml-xs"
|
||||||
|
@click.stop="
|
||||||
|
$emit('download', {
|
||||||
|
key: props.row.documentType,
|
||||||
|
data: { ...props.row, fileName: v },
|
||||||
|
id:
|
||||||
|
{
|
||||||
|
customer: currentId.customer,
|
||||||
|
employee: currentId.employee,
|
||||||
|
}[state.tab] || '',
|
||||||
|
type: state.tab,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</BadgeComponent>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
{{ props.row.amount }}
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<q-icon
|
||||||
|
:name="`mdi-${props.row.documentInSystem ? 'check' : 'close'}-circle`"
|
||||||
|
size="sm"
|
||||||
|
:class="{
|
||||||
|
'app-text-positive': props.row.documentInSystem,
|
||||||
|
'app-text-negative': !props.row.documentInSystem,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<q-td class="text-left">
|
||||||
|
<q-btn-dropdown
|
||||||
|
dense
|
||||||
|
unelevated
|
||||||
|
:label="
|
||||||
|
$t(
|
||||||
|
`requestList.status.${props.row.status || 'AwaitReview'}`,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="text-capitalize text-weight-regular doc-status"
|
||||||
|
:class="{
|
||||||
|
await: props.row.status === DocStatus.AwaitReview,
|
||||||
|
'in-progress':
|
||||||
|
props.row.status === DocStatus.ReviewedAwaitUpload ||
|
||||||
|
props.row.status === DocStatus.UploadedAwaitReview,
|
||||||
|
completed:
|
||||||
|
props.row.status === DocStatus.Reviewed ||
|
||||||
|
props.row.status === DocStatus.ApprovedReview,
|
||||||
|
}"
|
||||||
|
:menu-offset="[0, 8]"
|
||||||
|
dropdown-icon="mdi-chevron-down"
|
||||||
|
content-class="bordered rounded"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<q-list dense>
|
||||||
|
<template v-for="s in docStatus" :key="s">
|
||||||
|
<q-item
|
||||||
|
v-if="s !== props.row.status"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="
|
||||||
|
$emit('changeStatus', {
|
||||||
|
key: props.row.documentType,
|
||||||
|
status: s,
|
||||||
|
type: state.tab,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ $t(`requestList.status.${s}`) }}
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</q-td>
|
||||||
|
<q-td>
|
||||||
|
<span class="row justify-end no-wrap">
|
||||||
|
<OcrDialog
|
||||||
|
@submit="
|
||||||
|
(file, meta) => {
|
||||||
|
$emit(
|
||||||
|
'upload',
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
customer: currentId.customer,
|
||||||
|
employee: currentId.employee,
|
||||||
|
}[state.tab] as string,
|
||||||
|
|
||||||
|
file,
|
||||||
|
form:
|
||||||
|
meta !== undefined
|
||||||
|
? ocrResultToVariable(meta)
|
||||||
|
: undefined,
|
||||||
|
group: props.row.documentType,
|
||||||
|
type: state.tab,
|
||||||
|
},
|
||||||
|
fetchRequestAttachment,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
:ocr
|
||||||
|
>
|
||||||
|
<template #trigger="{ browse }">
|
||||||
|
<MainButton
|
||||||
|
v-if="!!state.isEdit"
|
||||||
|
iconOnly
|
||||||
|
icon="mdi-tray-arrow-up"
|
||||||
|
color="var(--positive-bg)"
|
||||||
|
:title="$t('general.upload')"
|
||||||
|
@click.stop="
|
||||||
|
group = props.row.documentType;
|
||||||
|
browse();
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #body="{ metadata, isRunning }">
|
||||||
|
<FormEmployeePassport
|
||||||
|
v-if="group === 'passport' && metadata"
|
||||||
|
:title="$t('customerEmployee.form.group.passport')"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
separator
|
||||||
|
ocr
|
||||||
|
prefix-id="ocr"
|
||||||
|
v-model:birth-country="metadata.birthCountry"
|
||||||
|
v-model:previous-passportRef="
|
||||||
|
metadata.previousPassportRef
|
||||||
|
"
|
||||||
|
v-model:issue-place="metadata.nationality"
|
||||||
|
v-model:issue-country="metadata.country"
|
||||||
|
v-model:issue-date="metadata.issue_date"
|
||||||
|
v-model:type="metadata.doc_type"
|
||||||
|
v-model:expire-date="metadata.expire_date"
|
||||||
|
v-model:birth-date="metadata.birthDate"
|
||||||
|
v-model:worker-status="metadata.workerStatus"
|
||||||
|
v-model:nationality="metadata.nationality"
|
||||||
|
v-model:gender="metadata.sex"
|
||||||
|
v-model:last-name-en="metadata.lastNameEN"
|
||||||
|
v-model:last-name="metadata.last_name"
|
||||||
|
v-model:middle-name-en="metadata.middleNameEN"
|
||||||
|
v-model:middle-name="metadata.middleName"
|
||||||
|
v-model:first-name-en="metadata.firstNameEN"
|
||||||
|
v-model:first-name="metadata.first_name"
|
||||||
|
v-model:name-prefix="metadata.namePrefix"
|
||||||
|
v-model:passport-number="metadata.doc_number"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormEmployeeVisa
|
||||||
|
v-if="group === 'visa' && metadata"
|
||||||
|
:title="$t('customerEmployee.form.group.visa')"
|
||||||
|
ocr
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
prefix-id="ocr"
|
||||||
|
v-model:arrival-at="metadata.arrivalAt"
|
||||||
|
v-model:arrival-tm-no="metadata.arrivalTMNo"
|
||||||
|
v-model:arrival-tm="metadata.arrivalTM"
|
||||||
|
v-model:mrz="metadata.mrz"
|
||||||
|
v-model:entry-count="metadata.entryCount"
|
||||||
|
v-model:issue-place="metadata.issue_place"
|
||||||
|
v-model:issue-country="metadata.country"
|
||||||
|
v-model:visa-issue-date="metadata.valid_until"
|
||||||
|
v-model:visa-type="metadata.visa_type"
|
||||||
|
v-model:expire-date="metadata.expire_date"
|
||||||
|
v-model:visa-expiry-date="metadata.expireDate"
|
||||||
|
v-model:remark="metadata.remark"
|
||||||
|
v-model:worker-type="metadata.workerType"
|
||||||
|
v-model:visa-number="metadata.doc_number"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="isRunning"
|
||||||
|
class="full-height flex flex-center"
|
||||||
|
>
|
||||||
|
<q-circular-progress
|
||||||
|
indeterminate
|
||||||
|
rounded
|
||||||
|
size="50px"
|
||||||
|
color="light-blue"
|
||||||
|
class="q-ma-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</OcrDialog>
|
||||||
|
</span>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</q-splitter>
|
||||||
|
</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>
|
||||||
44
src/pages/08_request-list/FormDuty.vue
Normal file
44
src/pages/08_request-list/FormDuty.vue
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const duty = defineModel<boolean>('duty', { required: true });
|
||||||
|
const dutyCost = defineModel<number>('dutyCost', { required: false });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
cost?: boolean;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="row items-center q-px-md q-py-xs">
|
||||||
|
<q-radio
|
||||||
|
v-model="duty"
|
||||||
|
:val="false"
|
||||||
|
:label="$t('duty.notInclude')"
|
||||||
|
:disable="readonly"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
<q-radio
|
||||||
|
v-model="duty"
|
||||||
|
:val="true"
|
||||||
|
:label="$t('duty.include')"
|
||||||
|
:disable="readonly"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
:options="[30]"
|
||||||
|
:disable="!duty"
|
||||||
|
:label="$t('duty.cost')"
|
||||||
|
:readonly
|
||||||
|
:hide-dropdown-icon="readonly"
|
||||||
|
:model-value="dutyCost || 0"
|
||||||
|
@update:model-value="(value) => (dutyCost = value)"
|
||||||
|
v-if="dutyCost !== undefined || cost"
|
||||||
|
class="col"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
use-input
|
||||||
|
/>
|
||||||
|
<!-- NOTE: For spacing only -->
|
||||||
|
<div v-else class="col"></div>
|
||||||
|
<div class="offset-md-7"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
42
src/pages/08_request-list/FormEmployee.vue
Normal file
42
src/pages/08_request-list/FormEmployee.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SelectUser from 'src/components/shared/select/SelectUser.vue';
|
||||||
|
|
||||||
|
const localEmployee = defineModel<boolean>('localEmployee', { required: true });
|
||||||
|
const employeeId = defineModel<string>('employeeId', { required: false });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
cost?: boolean;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="row items-center q-px-md q-py-xs">
|
||||||
|
<div class="col-12 row">
|
||||||
|
<q-radio
|
||||||
|
v-model="localEmployee"
|
||||||
|
:val="true"
|
||||||
|
:label="$t('requestList.nonLocalEmployee')"
|
||||||
|
:disable="readonly"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
<q-radio
|
||||||
|
v-model="localEmployee"
|
||||||
|
:val="false"
|
||||||
|
:label="$t('requestList.localEmployee')"
|
||||||
|
:disable="readonly"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
<div class="col" />
|
||||||
|
<div class="offset-md-7"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 row">
|
||||||
|
<SelectUser
|
||||||
|
class="col-md-5 col-12"
|
||||||
|
v-model:value="employeeId"
|
||||||
|
:readonly
|
||||||
|
:label="$t('general.select', { msg: $t('personnel.MESSENGER') })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
166
src/pages/08_request-list/FormExpansion.vue
Normal file
166
src/pages/08_request-list/FormExpansion.vue
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import FormGroupHead from './FormGroupHead.vue';
|
||||||
|
import FormDuty from './FormDuty.vue';
|
||||||
|
import FormIssue from './FormIssue.vue';
|
||||||
|
import FormEmployee from './FormEmployee.vue';
|
||||||
|
|
||||||
|
import { UndoButton, SaveButton, EditButton } from 'src/components/button';
|
||||||
|
import { AttributesForm, Step } from 'src/stores/request-list/types';
|
||||||
|
import { useRequestList } from 'src/stores/request-list';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
step: Step;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const requestListStore = useRequestList();
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
isEdit: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultForm = {
|
||||||
|
customerDuty: false,
|
||||||
|
customerDutyCost: 30,
|
||||||
|
companyDuty: false,
|
||||||
|
companyDutyCost: 30,
|
||||||
|
individualDuty: false,
|
||||||
|
localEmployee: true,
|
||||||
|
employeeId: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const attributesForm = defineModel<AttributesForm>('attributesForm', {
|
||||||
|
default: {
|
||||||
|
customerDuty: false,
|
||||||
|
customerDutyCost: 30,
|
||||||
|
companyDuty: false,
|
||||||
|
companyDutyCost: 30,
|
||||||
|
individualDuty: false,
|
||||||
|
localEmployee: true,
|
||||||
|
employeeId: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formData = ref<AttributesForm>(defaultForm);
|
||||||
|
|
||||||
|
function triggerUndo() {
|
||||||
|
assignToForm();
|
||||||
|
state.isEdit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerSubmit() {
|
||||||
|
const payload = {
|
||||||
|
...props.step,
|
||||||
|
attributes: { form: formData.value },
|
||||||
|
};
|
||||||
|
const res = await requestListStore.editStatusRequestWork(payload);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
attributesForm.value = JSON.parse(JSON.stringify(formData.value));
|
||||||
|
state.isEdit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerEdit() {
|
||||||
|
state.isEdit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignToForm() {
|
||||||
|
formData.value = JSON.parse(JSON.stringify(attributesForm.value));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
class="overflow-hidden bordered q-my-sm"
|
||||||
|
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"
|
||||||
|
@before-show="assignToForm"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('general.designForm') }}
|
||||||
|
</span>
|
||||||
|
<nav class="q-ml-auto row">
|
||||||
|
<UndoButton
|
||||||
|
v-if="state.isEdit"
|
||||||
|
id="btn-info-basic-undo"
|
||||||
|
icon-only
|
||||||
|
type="button"
|
||||||
|
@click.stop="triggerUndo"
|
||||||
|
/>
|
||||||
|
<SaveButton
|
||||||
|
v-if="state.isEdit"
|
||||||
|
id="btn-info-basic-save"
|
||||||
|
icon-only
|
||||||
|
type="submit"
|
||||||
|
@click.stop="triggerSubmit"
|
||||||
|
/>
|
||||||
|
<EditButton
|
||||||
|
v-if="!state.isEdit"
|
||||||
|
id="btn-info-basic-edit"
|
||||||
|
icon-only
|
||||||
|
@click.stop="triggerEdit"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main class="row">
|
||||||
|
<section class="col-12">
|
||||||
|
<FormGroupHead>
|
||||||
|
{{ $t('duty.text', { subject: $t('general.customer') }) }}
|
||||||
|
</FormGroupHead>
|
||||||
|
<FormDuty
|
||||||
|
class="surface-1"
|
||||||
|
:readonly="!state.isEdit"
|
||||||
|
v-model:duty="formData.customerDuty"
|
||||||
|
v-model:duty-cost="formData.customerDutyCost"
|
||||||
|
cost
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section class="col-12">
|
||||||
|
<FormGroupHead>
|
||||||
|
{{ $t('duty.text', { subject: $t('general.company') }) }}
|
||||||
|
</FormGroupHead>
|
||||||
|
<FormDuty
|
||||||
|
class="surface-1"
|
||||||
|
:readonly="!state.isEdit"
|
||||||
|
v-model:duty="formData.companyDuty"
|
||||||
|
v-model:duty-cost="formData.companyDutyCost"
|
||||||
|
cost
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section class="col-12">
|
||||||
|
<FormGroupHead>
|
||||||
|
{{ $t('quotation.templateForm') }}
|
||||||
|
</FormGroupHead>
|
||||||
|
<FormIssue :readonly="!state.isEdit" />
|
||||||
|
</section>
|
||||||
|
<section class="col-12">
|
||||||
|
<FormGroupHead>
|
||||||
|
{{ $t('general.select', { msg: $t('requestList.companyEmployee') }) }}
|
||||||
|
</FormGroupHead>
|
||||||
|
<FormEmployee
|
||||||
|
:readonly="!state.isEdit"
|
||||||
|
class="surface-1"
|
||||||
|
v-model:local-employee="formData.localEmployee"
|
||||||
|
v-model:employee-id="formData.employeeId"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section class="col-12">
|
||||||
|
<FormGroupHead>
|
||||||
|
{{ $t('duty.text', { subject: $t('general.individual') }) }}
|
||||||
|
</FormGroupHead>
|
||||||
|
<FormDuty
|
||||||
|
:readonly="!state.isEdit"
|
||||||
|
class="surface-1"
|
||||||
|
v-model:duty="formData.individualDuty"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style></style>
|
||||||
12
src/pages/08_request-list/FormGroupHead.vue
Normal file
12
src/pages/08_request-list/FormGroupHead.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="surface-3 row items-center q-px-md q-py-xs"
|
||||||
|
style="color: hsla(var(--text-mute-2)); background: hsla(var(--gray-2-hsl))"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi-circle-medium" class="q-mr-xs" />
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
61
src/pages/08_request-list/FormIssue.vue
Normal file
61
src/pages/08_request-list/FormIssue.vue
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { MainButton } from 'components/button';
|
||||||
|
import SelectInput from 'src/components/shared/SelectInput.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
}>();
|
||||||
|
const templateForm = defineModel<string>();
|
||||||
|
const templateFormOption = ref<
|
||||||
|
{ label: string; labelEN: string; value: string }[]
|
||||||
|
>([]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="surface-1 q-pa-md"
|
||||||
|
:class="{
|
||||||
|
['template-container']: $q.screen.gt.sm,
|
||||||
|
['column']: !$q.screen.gt.sm,
|
||||||
|
}"
|
||||||
|
style="gap: var(--size-2)"
|
||||||
|
>
|
||||||
|
<SelectInput
|
||||||
|
id="quotation-branch"
|
||||||
|
style="grid-column: span 4"
|
||||||
|
incremental
|
||||||
|
v-model="templateForm"
|
||||||
|
class="full-width"
|
||||||
|
:readonly
|
||||||
|
:option="templateFormOption"
|
||||||
|
:label="$t('quotation.templateForm')"
|
||||||
|
:option-label="$i18n.locale === 'eng' ? 'labelEN' : 'label'"
|
||||||
|
/>
|
||||||
|
<MainButton
|
||||||
|
outlined
|
||||||
|
icon="mdi-play-box-outline"
|
||||||
|
color="207 96% 32%"
|
||||||
|
class="full-width"
|
||||||
|
style="grid-column: span 2"
|
||||||
|
>
|
||||||
|
{{ $t('general.view', { msg: $t('general.example') }) }}
|
||||||
|
</MainButton>
|
||||||
|
<MainButton
|
||||||
|
solid
|
||||||
|
icon="mdi-pencil-outline"
|
||||||
|
color="207 96% 32%"
|
||||||
|
class="full-width"
|
||||||
|
style="grid-column: span 2"
|
||||||
|
>
|
||||||
|
{{ $t('general.designForm') }}
|
||||||
|
</MainButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.template-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(12, 1fr);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
323
src/pages/08_request-list/MainPage.vue
Normal file
323
src/pages/08_request-list/MainPage.vue
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
// NOTE: Library
|
||||||
|
import { computed, onMounted, reactive, watch } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
// NOTE: Components
|
||||||
|
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||||
|
import NoData from 'src/components/NoData.vue';
|
||||||
|
import TableRequestList from './TableRequestList.vue';
|
||||||
|
import PaginationComponent from 'src/components/PaginationComponent.vue';
|
||||||
|
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||||
|
|
||||||
|
// NOTE: Stores & Type
|
||||||
|
import { useNavigator } from 'src/stores/navigator';
|
||||||
|
import { column } from './constants';
|
||||||
|
import useFlowStore from 'src/stores/flow';
|
||||||
|
import { useRequestList } from 'src/stores/request-list';
|
||||||
|
import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
|
||||||
|
|
||||||
|
const navigatorStore = useNavigator();
|
||||||
|
const flowStore = useFlowStore();
|
||||||
|
const requestListStore = useRequestList();
|
||||||
|
const { data, stats, page, pageMax, pageSize } = storeToRefs(requestListStore);
|
||||||
|
|
||||||
|
// NOTE: Variable
|
||||||
|
const pageState = reactive({
|
||||||
|
hideStat: false,
|
||||||
|
statusFilter: RequestDataStatus.Pending,
|
||||||
|
inputSearch: '',
|
||||||
|
fieldSelected: [...column.map((v) => v.name)],
|
||||||
|
gridView: false,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldSelectedOption = computed(() => {
|
||||||
|
return column.map((v) => ({
|
||||||
|
label: v.label,
|
||||||
|
value: v.name,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: Function
|
||||||
|
async function fetchList(opts?: { rotateFlowId?: boolean }) {
|
||||||
|
const ret = await requestListStore.getRequestDataList({
|
||||||
|
query: pageState.inputSearch,
|
||||||
|
page: page.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
requestDataStatus: pageState.statusFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ret) {
|
||||||
|
data.value = ret.result;
|
||||||
|
pageState.total = ret.total;
|
||||||
|
pageMax.value = Math.ceil(ret.total / pageSize.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.rotateFlowId) flowStore.rotate();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchStats() {
|
||||||
|
const ret = await requestListStore.getRequestDataStats();
|
||||||
|
if (ret) stats.value = ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerView(opts: { requestData: RequestData }) {
|
||||||
|
const url = new URL(
|
||||||
|
`/request-list/${opts.requestData.id}`,
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
|
window.open(url.toString(), '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
navigatorStore.current.title = 'requestList.title';
|
||||||
|
navigatorStore.current.path = [{ text: 'requestList.caption', i18n: true }];
|
||||||
|
|
||||||
|
await fetchStats();
|
||||||
|
await fetchList({ rotateFlowId: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([() => pageState.inputSearch, () => pageState.statusFilter], () =>
|
||||||
|
fetchList({ rotateFlowId: true }),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="column full-height no-wrap">
|
||||||
|
<!-- SEC: stat -->
|
||||||
|
<section class="text-body-2 q-mb-xs flex items-center">
|
||||||
|
{{ $t('general.dataSum') }}
|
||||||
|
<q-badge
|
||||||
|
rounded
|
||||||
|
class="q-ml-sm"
|
||||||
|
style="
|
||||||
|
background-color: hsla(var(--info-bg) / 0.15);
|
||||||
|
color: hsl(var(--info-bg));
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ 0 }}
|
||||||
|
</q-badge>
|
||||||
|
<q-btn
|
||||||
|
class="q-ml-sm"
|
||||||
|
icon="mdi-pin-outline"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
rounded
|
||||||
|
@click="pageState.hideStat = !pageState.hideStat"
|
||||||
|
:style="pageState.hideStat ? 'rotate: 90deg' : ''"
|
||||||
|
style="transition: 0.1s ease-in-out"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<transition name="slide">
|
||||||
|
<div v-if="!pageState.hideStat" class="scroll q-mb-md">
|
||||||
|
<div style="display: inline-block">
|
||||||
|
<StatCardComponent
|
||||||
|
labelI18n
|
||||||
|
:branch="[
|
||||||
|
{
|
||||||
|
icon: 'icon-park-outline:loading-one',
|
||||||
|
count: stats[RequestDataStatus.Pending],
|
||||||
|
label: 'requestList.status.Pending',
|
||||||
|
color: 'orange',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-timer-sand',
|
||||||
|
count: stats[RequestDataStatus.InProgress],
|
||||||
|
label: 'requestList.status.InProgress',
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
icon: 'mdi-check-decagram-outline',
|
||||||
|
count: stats[RequestDataStatus.Completed],
|
||||||
|
label: 'requestList.status.Completed',
|
||||||
|
color: 'light-green',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:dark="$q.dark.isActive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<section class="col surface-1 rounded bordered overflow-hidden">
|
||||||
|
<div class="column full-height">
|
||||||
|
<!-- SEC: header content -->
|
||||||
|
<header
|
||||||
|
class="row surface-3 justify-between full-width items-center bordered-b"
|
||||||
|
style="z-index: 1"
|
||||||
|
>
|
||||||
|
<div class="row q-py-sm q-px-md justify-between full-width">
|
||||||
|
<q-input
|
||||||
|
for="input-search"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
:label="$t('general.search')"
|
||||||
|
class="q-mr-md col-12 col-md-3"
|
||||||
|
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||||
|
v-model="pageState.inputSearch"
|
||||||
|
debounce="200"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-magnify" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="row col-12 col-md-5 justify-end"
|
||||||
|
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||||
|
style="white-space: nowrap"
|
||||||
|
>
|
||||||
|
<q-select
|
||||||
|
v-model="pageState.statusFilter"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
option-value="value"
|
||||||
|
option-label="label"
|
||||||
|
class="col"
|
||||||
|
:class="{ 'offset-md-5': pageState.gridView }"
|
||||||
|
map-options
|
||||||
|
emit-value
|
||||||
|
:for="'field-select-status'"
|
||||||
|
:hide-dropdown-icon="$q.screen.lt.sm"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
label: $t('requestList.status.Pending'),
|
||||||
|
value: RequestDataStatus.Pending,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('requestList.status.InProgress'),
|
||||||
|
value: RequestDataStatus.InProgress,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('requestList.status.Completed'),
|
||||||
|
value: RequestDataStatus.Completed,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-if="!pageState.gridView"
|
||||||
|
id="select-field"
|
||||||
|
for="select-field"
|
||||||
|
class="col q-ml-sm"
|
||||||
|
:options="
|
||||||
|
fieldSelectedOption.map((v) => ({
|
||||||
|
...v,
|
||||||
|
label: v.label && $t(v.label),
|
||||||
|
}))
|
||||||
|
"
|
||||||
|
:display-value="$t('general.displayField')"
|
||||||
|
:hide-dropdown-icon="$q.screen.lt.sm"
|
||||||
|
v-model="pageState.fieldSelected"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
map-options
|
||||||
|
emit-value
|
||||||
|
outlined
|
||||||
|
multiple
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn-toggle
|
||||||
|
id="btn-mode"
|
||||||
|
v-model="pageState.gridView"
|
||||||
|
dense
|
||||||
|
class="no-shadow bordered rounded surface-1 q-ml-sm"
|
||||||
|
:toggle-color="$q.dark.isActive ? 'grey-9' : 'grey-2'"
|
||||||
|
size="xs"
|
||||||
|
:options="[
|
||||||
|
{ value: true, slot: 'folder' },
|
||||||
|
{ value: false, slot: 'list' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<template v-slot:folder>
|
||||||
|
<q-icon
|
||||||
|
name="mdi-view-grid-outline"
|
||||||
|
size="16px"
|
||||||
|
class="q-px-sm q-py-xs rounded"
|
||||||
|
:style="{
|
||||||
|
color: $q.dark.isActive
|
||||||
|
? pageState.gridView
|
||||||
|
? '#C9D3DB '
|
||||||
|
: '#787B7C'
|
||||||
|
: pageState.gridView
|
||||||
|
? '#787B7C'
|
||||||
|
: '#C9D3DB',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:list>
|
||||||
|
<q-icon
|
||||||
|
name="mdi-format-list-bulleted"
|
||||||
|
class="q-px-sm q-py-xs rounded"
|
||||||
|
size="16px"
|
||||||
|
:style="{
|
||||||
|
color: $q.dark.isActive
|
||||||
|
? pageState.gridView === false
|
||||||
|
? '#C9D3DB'
|
||||||
|
: '#787B7C'
|
||||||
|
: pageState.gridView === false
|
||||||
|
? '#787B7C'
|
||||||
|
: '#C9D3DB',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-btn-toggle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- SEC: body content -->
|
||||||
|
<article
|
||||||
|
v-if="data.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">
|
||||||
|
<TableRequestList
|
||||||
|
:columns="column"
|
||||||
|
:rows="data"
|
||||||
|
:grid="pageState.gridView"
|
||||||
|
:visible-columns="pageState.fieldSelected"
|
||||||
|
@view="(data) => triggerView({ requestData: data })"
|
||||||
|
/>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- SEC: footer content -->
|
||||||
|
<footer
|
||||||
|
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
||||||
|
v-if="pageMax > 0"
|
||||||
|
>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="app-text-muted q-mr-sm" v-if="$q.screen.gt.sm">
|
||||||
|
{{ $t('general.recordPerPage') }}
|
||||||
|
</div>
|
||||||
|
<div><PaginationPageSize v-model="pageSize" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 row justify-center app-text-muted">
|
||||||
|
{{
|
||||||
|
$t('general.recordsPage', {
|
||||||
|
resultcurrentPage: data.length,
|
||||||
|
total: pageState.inputSearch ? data.length : pageState.total,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<nav class="col-4 row justify-end">
|
||||||
|
<PaginationComponent
|
||||||
|
v-model:current-page="page"
|
||||||
|
v-model:max-page="pageMax"
|
||||||
|
:fetch-data="() => fetchList({ rotateFlowId: true })"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style></style>
|
||||||
241
src/pages/08_request-list/ProductExpansion.vue
Normal file
241
src/pages/08_request-list/ProductExpansion.vue
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { baseUrl } from 'src/stores/utils';
|
||||||
|
|
||||||
|
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||||
|
|
||||||
|
import { ProductRelation } from 'src/stores/quotations/types';
|
||||||
|
import { Step, RequestWorkStatus } from 'src/stores/request-list/types';
|
||||||
|
|
||||||
|
const workStatus = [
|
||||||
|
RequestWorkStatus.Ready,
|
||||||
|
RequestWorkStatus.Waiting,
|
||||||
|
RequestWorkStatus.InProgress,
|
||||||
|
RequestWorkStatus.Validate,
|
||||||
|
RequestWorkStatus.Ended,
|
||||||
|
RequestWorkStatus.Completed,
|
||||||
|
];
|
||||||
|
|
||||||
|
const productId = defineModel<string>('productId', { required: true });
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(
|
||||||
|
e: 'changeStatus',
|
||||||
|
v: { step?: Step; requestWorkStatus: RequestWorkStatus },
|
||||||
|
): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
product: ProductRelation;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
status?: Step;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// NOTE: Function
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
class="overflow-hidden"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1"
|
||||||
|
>
|
||||||
|
<template v-slot:header>
|
||||||
|
<header class="row items-center q-py-sm no-wrap full-width">
|
||||||
|
<div
|
||||||
|
class="img-frame rounded q-mr-md"
|
||||||
|
:class="{ dark: $q.dark.isActive }"
|
||||||
|
style="width: 50px; height: 50px"
|
||||||
|
>
|
||||||
|
<q-img
|
||||||
|
:src="`${baseUrl}/product/${productId}/image/`"
|
||||||
|
style="object-fit: cover; width: 100%; height: 100%"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<span
|
||||||
|
class="flex items-center justify-center full-height full-width"
|
||||||
|
>
|
||||||
|
<q-img src="/shop-image.png" style="width: 80%"></q-img>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-img>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row col">
|
||||||
|
<span class="row items-center col-12 no-wrap">
|
||||||
|
<span class="ellipsis-2-lines">
|
||||||
|
{{ product?.name || name }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div class="rounded q-px-xs app-text-muted surface-3">
|
||||||
|
{{ product?.code || code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-ml-auto q-gutter-y-xs">
|
||||||
|
<div class="justify-end flex">
|
||||||
|
<q-btn-dropdown
|
||||||
|
:disable="
|
||||||
|
status?.workStatus === 'Waiting' ||
|
||||||
|
status?.workStatus === 'InProgress'
|
||||||
|
"
|
||||||
|
dense
|
||||||
|
unelevated
|
||||||
|
:label="
|
||||||
|
$q.screen.lt.sm
|
||||||
|
? undefined
|
||||||
|
: $t(
|
||||||
|
`requestList.status.work.${status?.workStatus ?? RequestWorkStatus.Pending}`,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="text-capitalize text-weight-regular product-status rounded"
|
||||||
|
:class="{
|
||||||
|
disable:
|
||||||
|
$q.screen.gt.xs &&
|
||||||
|
(status?.workStatus === RequestWorkStatus.Waiting ||
|
||||||
|
status?.workStatus === RequestWorkStatus.InProgress),
|
||||||
|
pending:
|
||||||
|
($q.screen.gt.xs && !status?.workStatus) ||
|
||||||
|
status?.workStatus === RequestWorkStatus.Pending ||
|
||||||
|
status?.workStatus === RequestWorkStatus.Ready,
|
||||||
|
progress:
|
||||||
|
$q.screen.gt.xs &&
|
||||||
|
status?.workStatus === RequestWorkStatus.Validate,
|
||||||
|
complete:
|
||||||
|
$q.screen.gt.xs &&
|
||||||
|
(status?.workStatus === RequestWorkStatus.Ended ||
|
||||||
|
status?.workStatus === RequestWorkStatus.Completed),
|
||||||
|
}"
|
||||||
|
:style="
|
||||||
|
$q.screen.xs &&
|
||||||
|
(status?.workStatus === RequestWorkStatus.Waiting ||
|
||||||
|
status?.workStatus === RequestWorkStatus.InProgress)
|
||||||
|
? `opacity: 30% !important`
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
:menu-offset="[0, 8]"
|
||||||
|
dropdown-icon="mdi-chevron-down"
|
||||||
|
content-class="bordered rounded"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<q-list dense>
|
||||||
|
<q-item
|
||||||
|
v-for="(value, index) in workStatus"
|
||||||
|
:key="index"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="
|
||||||
|
$emit('changeStatus', {
|
||||||
|
step: status,
|
||||||
|
requestWorkStatus: workStatus[index],
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ $t(`requestList.status.work.${value}`) }}
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption">
|
||||||
|
<span
|
||||||
|
class="q-pr-xs"
|
||||||
|
style="border-right: 1px solid #ccc"
|
||||||
|
v-if="$q.screen.gt.xs"
|
||||||
|
>
|
||||||
|
{{ $t('general.status') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="q-pl-xs product-status-text"
|
||||||
|
:class="{
|
||||||
|
pending:
|
||||||
|
!status?.workStatus ||
|
||||||
|
status.workStatus === RequestWorkStatus.Pending ||
|
||||||
|
status.workStatus === RequestWorkStatus.Ready,
|
||||||
|
wait: status?.workStatus === 'Waiting',
|
||||||
|
progress:
|
||||||
|
status?.workStatus === RequestWorkStatus.InProgress ||
|
||||||
|
status?.workStatus === RequestWorkStatus.Validate,
|
||||||
|
complete:
|
||||||
|
status?.workStatus === RequestWorkStatus.Ended ||
|
||||||
|
status?.workStatus === RequestWorkStatus.Completed,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t(`requestList.status.work.${status?.workStatus ?? 'Pending'}`)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<slot :product="product"></slot>
|
||||||
|
</q-expansion-item>
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-frame {
|
||||||
|
background: hsla(var(--teal-10-hsl) / 0.15);
|
||||||
|
&.dark {
|
||||||
|
background: hsla(var(--teal-8-hls) / 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-status {
|
||||||
|
padding-left: 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: hsl(var(--_color));
|
||||||
|
background: hsla(var(--_color) / 0.15);
|
||||||
|
&.disable {
|
||||||
|
--_color: var(--stone-7-hsl);
|
||||||
|
}
|
||||||
|
&.pending {
|
||||||
|
--_color: var(--yellow-6-hsl);
|
||||||
|
}
|
||||||
|
&.wait {
|
||||||
|
--_color: var(--blue-6-hsl);
|
||||||
|
}
|
||||||
|
&.progress {
|
||||||
|
--_color: var(--orange-5-hsl);
|
||||||
|
}
|
||||||
|
&.complete {
|
||||||
|
--_color: var(--green-5-hsl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-status-text {
|
||||||
|
color: hsl(var(--_color));
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
--_color: var(--yellow-6-hsl);
|
||||||
|
}
|
||||||
|
&.wait {
|
||||||
|
--_color: var(--blue-6-hsl);
|
||||||
|
}
|
||||||
|
&.progress {
|
||||||
|
--_color: var(--orange-5-hsl);
|
||||||
|
}
|
||||||
|
&.complete {
|
||||||
|
--_color: var(--green-5-hsl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
166
src/pages/08_request-list/PropertiesExpansion.vue
Normal file
166
src/pages/08_request-list/PropertiesExpansion.vue
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import PropertiesToInput from 'src/components/08_request-list/PropertiesToInput.vue';
|
||||||
|
import { UndoButton, SaveButton, EditButton } from 'src/components/button';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PropDate,
|
||||||
|
PropNumber,
|
||||||
|
PropOptions,
|
||||||
|
PropString,
|
||||||
|
} from 'src/stores/product-service/types';
|
||||||
|
|
||||||
|
import { Attributes } from 'src/stores/request-list/types';
|
||||||
|
|
||||||
|
import useOptionStore from 'src/stores/options';
|
||||||
|
import { useRequestList } from 'src/stores/request-list';
|
||||||
|
|
||||||
|
const optionStore = useOptionStore();
|
||||||
|
const requestListStore = useRequestList();
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
id: string;
|
||||||
|
propertiesToShow: (PropString | PropNumber | PropDate | PropOptions)[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
properties: () => [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const formRemark = ref<string>('');
|
||||||
|
const formData = ref<{
|
||||||
|
[field: string]: string | number | null | undefined;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
isEdit: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const attributes = defineModel<Attributes>('attributes', {
|
||||||
|
default: {
|
||||||
|
remark: '',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function triggerUndo() {
|
||||||
|
assignToForm();
|
||||||
|
state.isEdit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerSubmit() {
|
||||||
|
const res = await requestListStore.editRequestWork({
|
||||||
|
id: props.id,
|
||||||
|
attributes: {
|
||||||
|
...attributes.value,
|
||||||
|
properties: formData.value,
|
||||||
|
remark: formRemark.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
attributes.value.remark = formRemark.value || '';
|
||||||
|
attributes.value.properties = JSON.parse(
|
||||||
|
JSON.stringify(formData.value || {}),
|
||||||
|
);
|
||||||
|
state.isEdit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerEdit() {
|
||||||
|
state.isEdit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignToForm() {
|
||||||
|
formRemark.value = attributes.value?.remark || '';
|
||||||
|
formData.value = JSON.parse(
|
||||||
|
JSON.stringify(attributes.value?.properties || {}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'save', v: { [field: string]: string | number | null | undefined }): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
class="overflow-hidden bordered"
|
||||||
|
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"
|
||||||
|
@before-show="assignToForm"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('general.properties') }}
|
||||||
|
</span>
|
||||||
|
<nav class="q-ml-auto row">
|
||||||
|
<UndoButton
|
||||||
|
v-if="state.isEdit"
|
||||||
|
id="btn-info-basic-undo"
|
||||||
|
icon-only
|
||||||
|
type="button"
|
||||||
|
@click.stop="triggerUndo"
|
||||||
|
/>
|
||||||
|
<SaveButton
|
||||||
|
v-if="state.isEdit"
|
||||||
|
id="btn-info-basic-save"
|
||||||
|
icon-only
|
||||||
|
type="submit"
|
||||||
|
@click.stop="triggerSubmit"
|
||||||
|
/>
|
||||||
|
<EditButton
|
||||||
|
v-if="!state.isEdit"
|
||||||
|
id="btn-info-basic-edit"
|
||||||
|
icon-only
|
||||||
|
@click.stop="triggerEdit"
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main class="q-px-md q-py-sm" :class="{ row: $q.screen.gt.sm }">
|
||||||
|
<section class="col-7" :class="{ 'q-pr-sm': $q.screen.gt.sm }">
|
||||||
|
<span
|
||||||
|
v-for="(prop, i) in propertiesToShow"
|
||||||
|
:key="i"
|
||||||
|
class="row items-center q-pb-sm"
|
||||||
|
>
|
||||||
|
<article class="col-5">
|
||||||
|
{{ i + 1 }} {{ optionStore.mapOption(prop.fieldName) }}
|
||||||
|
</article>
|
||||||
|
<PropertiesToInput
|
||||||
|
:readonly="!state.isEdit"
|
||||||
|
:prop="prop"
|
||||||
|
:placeholder="optionStore.mapOption(prop.fieldName)"
|
||||||
|
v-model="formData[prop.fieldName]"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="col-5">
|
||||||
|
<q-input
|
||||||
|
class="full-height"
|
||||||
|
:model-value="formRemark || '-'"
|
||||||
|
dense
|
||||||
|
:readonly="!state.isEdit"
|
||||||
|
outlined
|
||||||
|
type="textarea"
|
||||||
|
:label="$t('general.remark')"
|
||||||
|
stack-label
|
||||||
|
@update:model-value="(v) => (formRemark = v as string)"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
:deep(.q-textarea .q-field__control) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
576
src/pages/08_request-list/RequestListView.vue
Normal file
576
src/pages/08_request-list/RequestListView.vue
Normal file
|
|
@ -0,0 +1,576 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
// NOTE: Library
|
||||||
|
import { onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
// NOTE: Components
|
||||||
|
import DataDisplay from 'src/components/08_request-list/DataDisplay.vue';
|
||||||
|
import DocumentExpansion from './DocumentExpansion.vue';
|
||||||
|
import FormExpansion from './FormExpansion.vue';
|
||||||
|
import PropertiesExpansion from './PropertiesExpansion.vue';
|
||||||
|
|
||||||
|
// NOTE: Store
|
||||||
|
import { dateFormatJS } from 'src/utils/datetime';
|
||||||
|
import { useRequestList } from 'src/stores/request-list';
|
||||||
|
import {
|
||||||
|
RequestData,
|
||||||
|
RequestWork,
|
||||||
|
Attributes,
|
||||||
|
DocStatus,
|
||||||
|
Step,
|
||||||
|
} from 'src/stores/request-list/types';
|
||||||
|
import useOptionStore from 'src/stores/options';
|
||||||
|
import ProductExpansion from './ProductExpansion.vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useWorkflowTemplate } from 'src/stores/workflow-template';
|
||||||
|
import { WorkflowTemplate } from 'src/stores/workflow-template/types';
|
||||||
|
import { initLang, initTheme, Lang } from 'src/utils/ui';
|
||||||
|
import {
|
||||||
|
EmployeePassportPayload,
|
||||||
|
EmployeeVisaPayload,
|
||||||
|
} from 'stores/employee/types';
|
||||||
|
|
||||||
|
const { locale } = useI18n();
|
||||||
|
|
||||||
|
// NOTE: Variable
|
||||||
|
const route = useRoute();
|
||||||
|
const optionStore = useOptionStore();
|
||||||
|
const requestListStore = useRequestList();
|
||||||
|
const flowTemplateStore = useWorkflowTemplate();
|
||||||
|
|
||||||
|
const workList = ref<RequestWork[]>();
|
||||||
|
const statusFile = ref<Attributes>({
|
||||||
|
customer: {},
|
||||||
|
employee: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const refDocumentExpansion = ref<InstanceType<typeof DocumentExpansion>[]>([]);
|
||||||
|
const data = ref<RequestData>();
|
||||||
|
const flow = ref<WorkflowTemplate>();
|
||||||
|
const pageState = reactive({
|
||||||
|
hideMetaData: false,
|
||||||
|
currentStep: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: Function
|
||||||
|
|
||||||
|
async function fetchRequestWorkList(opts: { requestDataId: string }) {
|
||||||
|
const res = await requestListStore.getRequestWorkList({
|
||||||
|
requestDataId: opts.requestDataId,
|
||||||
|
pageSize: 9999,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
workList.value = res.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCustomerName(
|
||||||
|
record: RequestData,
|
||||||
|
opts?: {
|
||||||
|
locale?: string;
|
||||||
|
noCode?: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const customer = record.quotation.customerBranch;
|
||||||
|
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
['CORP']: {
|
||||||
|
[Lang.English]: customer.registerNameEN,
|
||||||
|
[Lang.Thai]: customer.registerName,
|
||||||
|
}[opts?.locale || 'eng'],
|
||||||
|
['PERS']:
|
||||||
|
{
|
||||||
|
[Lang.English]: `${optionStore.mapOption(customer.namePrefix)} ${customer.firstNameEN} ${customer.lastNameEN}`,
|
||||||
|
[Lang.Thai]: `${optionStore.mapOption(customer.namePrefix)} ${customer.firstName} ${customer.lastName}`,
|
||||||
|
}[opts?.locale || Lang.English] || '-',
|
||||||
|
}[customer.customer.customerType] +
|
||||||
|
(opts?.noCode ? '' : ' ' + `(${customer.code})`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmployeeName(
|
||||||
|
record: RequestData,
|
||||||
|
opts?: {
|
||||||
|
locale?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const employee = record.employee;
|
||||||
|
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
[Lang.English]: `${optionStore.mapOption(employee.namePrefix)} ${employee.firstNameEN} ${employee.lastNameEN}`,
|
||||||
|
[Lang.Thai]: `${optionStore.mapOption(employee.namePrefix)} ${employee.firstName} ${employee.lastName}`,
|
||||||
|
}[opts?.locale || Lang.English] || '-'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
const current = route.params['requestListId'];
|
||||||
|
|
||||||
|
if (typeof current === 'string') {
|
||||||
|
const res = await requestListStore.getRequestData(current);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
data.value = res;
|
||||||
|
await fetchRequestWorkList({ requestDataId: current });
|
||||||
|
await getFlow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFlow() {
|
||||||
|
if (!workList.value) return;
|
||||||
|
|
||||||
|
const attr = workList.value.find((v) => !!v.productService.work?.attributes)
|
||||||
|
?.productService.work?.attributes;
|
||||||
|
|
||||||
|
if (attr && Object.hasOwn(attr, 'workflowId')) {
|
||||||
|
const workflowId = attr['workflowId'];
|
||||||
|
|
||||||
|
const res = await flowTemplateStore.getWorkflowTemplate(workflowId);
|
||||||
|
|
||||||
|
if (res) flow.value = res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
initTheme();
|
||||||
|
initLang();
|
||||||
|
|
||||||
|
// get data
|
||||||
|
await getData();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => route.params['requestListId'], getData);
|
||||||
|
|
||||||
|
async function triggerChangeStatusWork(step: Step) {
|
||||||
|
const res = await requestListStore.editStatusRequestWork(step);
|
||||||
|
if (res) {
|
||||||
|
const indexWork = workList.value?.findIndex(
|
||||||
|
(v) => v.id === step.requestWorkId,
|
||||||
|
);
|
||||||
|
if (indexWork === -1 || indexWork === undefined) return;
|
||||||
|
if (workList.value === undefined) return;
|
||||||
|
|
||||||
|
const indexStep = workList.value[indexWork].stepStatus.findIndex(
|
||||||
|
(v) => v.step === step.step,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (indexStep === -1) {
|
||||||
|
workList.value[indexWork].stepStatus.push(res);
|
||||||
|
}
|
||||||
|
if (indexStep !== -1) {
|
||||||
|
workList.value[indexWork].stepStatus[indexStep].workStatus =
|
||||||
|
res.workStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerChangeStatusFile(opt: {
|
||||||
|
index: number;
|
||||||
|
id: string;
|
||||||
|
documentType: string;
|
||||||
|
status: DocStatus;
|
||||||
|
type: 'customer' | 'employee';
|
||||||
|
}) {
|
||||||
|
if (!workList.value) return;
|
||||||
|
|
||||||
|
const workItem = workList.value[opt.index];
|
||||||
|
if (!workItem || !workItem.attributes) {
|
||||||
|
if (workItem) {
|
||||||
|
workItem.attributes = { customer: {}, employee: {} };
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = workItem.attributes;
|
||||||
|
|
||||||
|
statusFile.value = attributes;
|
||||||
|
if (!statusFile.value[opt.type]) {
|
||||||
|
statusFile.value[opt.type] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
statusFile.value[opt.type]![opt.documentType] = opt.status;
|
||||||
|
|
||||||
|
const res = await requestListStore.editRequestWork({
|
||||||
|
id: opt.id,
|
||||||
|
attributes: statusFile.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
workList.value[opt.index].attributes = res.attributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerUpload(opt: {
|
||||||
|
id: string;
|
||||||
|
type: 'customer' | 'employee';
|
||||||
|
group: string;
|
||||||
|
file: File;
|
||||||
|
form?: EmployeePassportPayload | EmployeeVisaPayload;
|
||||||
|
}) {
|
||||||
|
const newName = `${opt.group}-${Date.now()}-${opt.file.name}`;
|
||||||
|
|
||||||
|
const res = await requestListStore.uploadAttachmentRequest({
|
||||||
|
...opt,
|
||||||
|
name: newName,
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerViewFile(opt: {
|
||||||
|
id: string;
|
||||||
|
fileName: string;
|
||||||
|
type: 'customer' | 'employee';
|
||||||
|
group: string;
|
||||||
|
download?: boolean;
|
||||||
|
}) {
|
||||||
|
let url;
|
||||||
|
url = await requestListStore.viewAttachmentRequest({
|
||||||
|
id: opt.id,
|
||||||
|
name: opt.fileName,
|
||||||
|
type: opt.type,
|
||||||
|
group: opt.group,
|
||||||
|
download: opt.download,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!opt.download) window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="column surface-0 fullscreen" v-if="data">
|
||||||
|
<!-- SEC: Header -->
|
||||||
|
<header class="row q-px-md q-py-sm items-center full justify-between">
|
||||||
|
<div style="flex: 1" class="row items-center">
|
||||||
|
<RouterLink to="/quotation">
|
||||||
|
<q-img src="/icons/favicon-512x512.png" width="3rem" />
|
||||||
|
</RouterLink>
|
||||||
|
<span class="column text-h6 text-bold q-ml-md">
|
||||||
|
{{ $t('requestList.title') }}
|
||||||
|
{{ data.code || '' }}
|
||||||
|
<span class="text-caption text-regular app-text-muted">
|
||||||
|
{{
|
||||||
|
$t('quotation.processOn', {
|
||||||
|
msg: dateFormatJS({ date: data.createdAt }),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- SEC: Body -->
|
||||||
|
<main 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" :key="pageState.currentStep">
|
||||||
|
<!-- step -->
|
||||||
|
<nav
|
||||||
|
v-if="flow"
|
||||||
|
class="surface-1 q-pa-sm row no-wrap full-width scroll rounded"
|
||||||
|
style="gap: 10px"
|
||||||
|
>
|
||||||
|
<template v-for="(value, i) in flow.step" :key="value.id">
|
||||||
|
<button
|
||||||
|
v-if="
|
||||||
|
workList?.every(
|
||||||
|
(v) =>
|
||||||
|
v.productService.work?.attributes.workflowStep[i]
|
||||||
|
.attributes.properties.length > 0,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="status-color q-pa-sm bordered row items-center cursor-pointer no-wrap"
|
||||||
|
style="text-wrap: nowrap"
|
||||||
|
:class="{
|
||||||
|
[`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>
|
||||||
|
</template>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- meta data -->
|
||||||
|
<article class="surface-1 rounded q-pa-sm">
|
||||||
|
<div
|
||||||
|
class="text-weight-bold row items-center no-wrap"
|
||||||
|
style="gap: 16px"
|
||||||
|
>
|
||||||
|
<q-img src="/images/quotation-avatar.png" width="42px" />
|
||||||
|
<span class="ellipsis" style="font-size: 18px">
|
||||||
|
{{ data.quotation.workName || '-' }}
|
||||||
|
</span>
|
||||||
|
<q-btn
|
||||||
|
class="q-ml-sm"
|
||||||
|
icon="mdi-pin-outline"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
rounded
|
||||||
|
@click="pageState.hideMetaData = !pageState.hideMetaData"
|
||||||
|
:style="pageState.hideMetaData ? 'rotate: 90deg' : ''"
|
||||||
|
style="transition: 0.1s ease-in-out"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<transition name="slide">
|
||||||
|
<section
|
||||||
|
v-if="!pageState.hideMetaData"
|
||||||
|
class="q-pt-md"
|
||||||
|
:class="{ row: $q.screen.gt.sm, column: $q.screen.lt.md }"
|
||||||
|
>
|
||||||
|
<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.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"
|
||||||
|
:label="$t('requestList.quotationCode')"
|
||||||
|
: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.invoiceCode')"
|
||||||
|
:value="'-'"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
:label="$t('requestList.receiptCode')"
|
||||||
|
:value="'-'"
|
||||||
|
/>
|
||||||
|
<DataDisplay
|
||||||
|
class="col"
|
||||||
|
icon="mdi-passport"
|
||||||
|
:label="$t('customerEmployee.form.passportNo')"
|
||||||
|
:value="'-'"
|
||||||
|
/>
|
||||||
|
</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) =>
|
||||||
|
v.productService.work?.attributes.workflowStep[
|
||||||
|
pageState.currentStep - 1
|
||||||
|
].productsId.includes(v.productService.productId),
|
||||||
|
)"
|
||||||
|
:key="value"
|
||||||
|
>
|
||||||
|
<ProductExpansion
|
||||||
|
:status="
|
||||||
|
value.stepStatus.find((v) => v.step === pageState.currentStep)
|
||||||
|
"
|
||||||
|
v-model:product-id="value.productService.productId"
|
||||||
|
:name="value.productService.product.name"
|
||||||
|
:code="value.productService.product.code"
|
||||||
|
:product="value.productService.product"
|
||||||
|
@change-status="
|
||||||
|
(v) => {
|
||||||
|
triggerChangeStatusWork({
|
||||||
|
workStatus: v.requestWorkStatus,
|
||||||
|
step:
|
||||||
|
v.step === undefined
|
||||||
|
? pageState.currentStep
|
||||||
|
: v.step.step,
|
||||||
|
requestWorkId: value.id || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template v-slot="{ product }">
|
||||||
|
<section class="column surface-1 q-pa-sm bordered-t">
|
||||||
|
<DocumentExpansion
|
||||||
|
ref="refDocumentExpansion"
|
||||||
|
:attributes="value.attributes"
|
||||||
|
@change-status="
|
||||||
|
(opt) => {
|
||||||
|
triggerChangeStatusFile({
|
||||||
|
index,
|
||||||
|
id: value.id || '',
|
||||||
|
documentType: opt.key || '',
|
||||||
|
status: opt.status,
|
||||||
|
type: opt.type || 'customer',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@view-doc="
|
||||||
|
(opt) => {
|
||||||
|
triggerViewFile({
|
||||||
|
id: opt.id,
|
||||||
|
fileName: opt.data.fileName,
|
||||||
|
type: opt.type,
|
||||||
|
group: opt.data.documentType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@upload="
|
||||||
|
async (opt, done) => {
|
||||||
|
await triggerUpload({ ...opt });
|
||||||
|
await done(opt.type || 'customer');
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@download="
|
||||||
|
(opt) => {
|
||||||
|
console.log(opt);
|
||||||
|
|
||||||
|
triggerViewFile({
|
||||||
|
id: opt.id,
|
||||||
|
fileName: opt.data.fileName,
|
||||||
|
type: opt.type,
|
||||||
|
group: opt.data.documentType,
|
||||||
|
download: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"
|
||||||
|
:current-id="{
|
||||||
|
customer: value.request.quotation.customerBranchId,
|
||||||
|
employee: value.request.employeeId,
|
||||||
|
}"
|
||||||
|
:listDocument="product?.document"
|
||||||
|
/>
|
||||||
|
<FormExpansion
|
||||||
|
:step="{
|
||||||
|
step: pageState.currentStep,
|
||||||
|
requestWorkId: value.id || '',
|
||||||
|
}"
|
||||||
|
:id="value.id"
|
||||||
|
:attributes-form="
|
||||||
|
value.stepStatus[pageState.currentStep - 1]?.attributes
|
||||||
|
?.form
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<PropertiesExpansion
|
||||||
|
:id="value.id"
|
||||||
|
:properties-to-show="
|
||||||
|
value.productService.work?.attributes.workflowStep[
|
||||||
|
pageState.currentStep - 1
|
||||||
|
].attributes.properties
|
||||||
|
"
|
||||||
|
:attributes="value.attributes"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</ProductExpansion>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</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-done {
|
||||||
|
--_color: var(--green-5-hsl);
|
||||||
|
}
|
||||||
|
&.status-color-doing {
|
||||||
|
--_color: var(--blue-5-hsl);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
&.status-color-waiting {
|
||||||
|
--_color: var(--gray-4-hsl);
|
||||||
|
color: hsla(var(--_color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
270
src/pages/08_request-list/TableRequestList.vue
Normal file
270
src/pages/08_request-list/TableRequestList.vue
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { QTable, QTableProps, QTableSlots } from 'quasar';
|
||||||
|
|
||||||
|
import KebabAction from 'components/shared/KebabAction.vue';
|
||||||
|
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
|
||||||
|
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||||
|
import AvatarGroup from 'src/components/shared/AvatarGroup.vue';
|
||||||
|
|
||||||
|
import { RequestData } from 'src/stores/request-list/types';
|
||||||
|
import { RequestDataStatus } from 'src/stores/request-list/types';
|
||||||
|
import useOptionStore from 'src/stores/options';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
rows: QTableProps['rows'];
|
||||||
|
columns: QTableProps['columns'];
|
||||||
|
grid?: boolean;
|
||||||
|
visibleColumns?: string[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
row: () => [],
|
||||||
|
column: () => [],
|
||||||
|
grid: false,
|
||||||
|
visibleColumns: () => [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'view', data: RequestData): void;
|
||||||
|
(e: 'edit', id: string): void;
|
||||||
|
(e: 'delete', id: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function getCustomerName(
|
||||||
|
record: RequestData,
|
||||||
|
opts?: {
|
||||||
|
locale?: string;
|
||||||
|
noCode?: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const customer = record.quotation.customerBranch;
|
||||||
|
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
['CORP']: {
|
||||||
|
['eng']: customer.registerNameEN,
|
||||||
|
['tha']: customer.registerName,
|
||||||
|
}[opts?.locale || 'eng'],
|
||||||
|
['PERS']:
|
||||||
|
{
|
||||||
|
['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})`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmployeeName(
|
||||||
|
record: RequestData,
|
||||||
|
opts?: {
|
||||||
|
locale?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const employee = record.employee;
|
||||||
|
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
['eng']: `${useOptionStore().mapOption(employee.namePrefix)} ${employee.firstNameEN} ${employee.lastNameEN}`,
|
||||||
|
['tha']: `${useOptionStore().mapOption(employee.namePrefix)} ${employee.firstName} ${employee.lastName}`,
|
||||||
|
}[opts?.locale || 'eng'] || '-'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-table
|
||||||
|
v-bind="props"
|
||||||
|
bordered
|
||||||
|
flat
|
||||||
|
hide-pagination
|
||||||
|
card-container-class="q-col-gutter-sm"
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
class="full-width"
|
||||||
|
>
|
||||||
|
<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) }}
|
||||||
|
</q-th>
|
||||||
|
<q-th></q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-slot:body="props: {
|
||||||
|
row: RequestData;
|
||||||
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||||
|
>
|
||||||
|
<q-tr
|
||||||
|
:class="{ urgent: props.row.quotation.urgent, dark: $q.dark.isActive }"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<q-td v-if="visibleColumns.includes('order')">
|
||||||
|
{{ props.rowIndex + 1 }}
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('requestList')" class="text-left">
|
||||||
|
{{ props.row.quotation.workName || '-' }}
|
||||||
|
<div class="text-caption app-text-muted">
|
||||||
|
{{ props.row.code || '-' }}
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('employer')" class="text-left">
|
||||||
|
{{
|
||||||
|
getCustomerName(props.row, {
|
||||||
|
noCode: true,
|
||||||
|
locale: $i18n.locale,
|
||||||
|
}) || '-'
|
||||||
|
}}
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('employee')" class="text-left">
|
||||||
|
{{ getEmployeeName(props.row, { locale: $i18n.locale }) || '-' }}
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('quotationCode')">
|
||||||
|
{{ props.row.quotation.code || '-' }}
|
||||||
|
</q-td>
|
||||||
|
<q-td v-if="visibleColumns.includes('responsiblePerson')">
|
||||||
|
{{ '-' }}
|
||||||
|
</q-td>
|
||||||
|
<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'
|
||||||
|
: '--orange-5-hsl'
|
||||||
|
"
|
||||||
|
:title="
|
||||||
|
$t(`requestList.status.${props.row.requestDataStatus}`) || '-'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<q-td class="text-right">
|
||||||
|
<q-btn
|
||||||
|
:id="`btn-eye-${props.row.quotation.workName}`"
|
||||||
|
icon="mdi-eye-outline"
|
||||||
|
size="sm"
|
||||||
|
dense
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
@click.stop="$emit('view', props.row)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<KebabAction
|
||||||
|
:idName="`btn-kebab-${props.row.quotation.workName}`"
|
||||||
|
status="'ACTIVE'"
|
||||||
|
hide-toggle
|
||||||
|
@view="$emit('view', props.row)"
|
||||||
|
@edit="$emit('edit', props.row.id)"
|
||||||
|
@delete="$emit('delete', props.row.id)"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-slot:item="props: {
|
||||||
|
row: RequestData;
|
||||||
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||||
|
>
|
||||||
|
<div class="col-md-4 col-sm-6 col-12">
|
||||||
|
<QuotationCard
|
||||||
|
:badge-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'
|
||||||
|
: '--orange-5-hsl'
|
||||||
|
"
|
||||||
|
hidePreview
|
||||||
|
:urgent="props.row.quotation.urgent"
|
||||||
|
:code="props.row.code"
|
||||||
|
:title="props.row.quotation.workName"
|
||||||
|
:status="$t(`requestList.status.${props.row.requestDataStatus}`)"
|
||||||
|
:custom-data="[
|
||||||
|
{
|
||||||
|
label: $t('customer.employer'),
|
||||||
|
value:
|
||||||
|
getCustomerName(props.row, {
|
||||||
|
noCode: true,
|
||||||
|
locale: $i18n.locale,
|
||||||
|
}) || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('customer.employee'),
|
||||||
|
value:
|
||||||
|
getEmployeeName(props.row, { locale: $i18n.locale }) || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('requestList.quotationCode'),
|
||||||
|
value: props.row.quotation.code || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('flow.responsiblePerson'),
|
||||||
|
value: '',
|
||||||
|
slotName: 'responsiblePerson',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
@view="$emit('view', props.row)"
|
||||||
|
>
|
||||||
|
<template v-slot:responsiblePerson="{ props }">
|
||||||
|
<div class="col-4 app-text-muted q-pr-sm self-center">
|
||||||
|
{{ props.label }}
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<AvatarGroup />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</QuotationCard>
|
||||||
|
</div>
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
91
src/pages/08_request-list/constants.ts
Normal file
91
src/pages/08_request-list/constants.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { QTableProps } from 'quasar';
|
||||||
|
|
||||||
|
export const column = [
|
||||||
|
{
|
||||||
|
name: 'order',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.order',
|
||||||
|
field: 'no',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'requestList',
|
||||||
|
align: 'center',
|
||||||
|
label: 'requestList.title',
|
||||||
|
field: 'requestList',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'employer',
|
||||||
|
align: 'center',
|
||||||
|
label: 'customer.employer',
|
||||||
|
field: 'employer',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'employee',
|
||||||
|
align: 'center',
|
||||||
|
label: 'customer.employee',
|
||||||
|
field: 'employee',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'quotationCode',
|
||||||
|
align: 'center',
|
||||||
|
label: 'requestList.quotationCode',
|
||||||
|
field: 'quotationCode',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'responsiblePerson',
|
||||||
|
align: 'center',
|
||||||
|
label: 'flow.responsiblePerson',
|
||||||
|
field: 'responsiblePerson',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.status',
|
||||||
|
field: 'status',
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
||||||
|
export const docColumn = [
|
||||||
|
{
|
||||||
|
name: 'order',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.order',
|
||||||
|
field: 'no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document',
|
||||||
|
align: 'left',
|
||||||
|
label: 'general.document',
|
||||||
|
field: 'document',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'attachment',
|
||||||
|
align: 'left',
|
||||||
|
label: 'requestList.attachment',
|
||||||
|
field: 'attachment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.amount',
|
||||||
|
field: 'amount',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'documentInSystem',
|
||||||
|
align: 'center',
|
||||||
|
label: 'requestList.documentInSystem',
|
||||||
|
field: 'documentInSystem',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.status',
|
||||||
|
field: 'status',
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
17
src/pages/08_request-list/form.ts
Normal file
17
src/pages/08_request-list/form.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
// NOTE: Import types
|
||||||
|
import {
|
||||||
|
EmployeePassportPayload,
|
||||||
|
EmployeeVisaPayload,
|
||||||
|
} from 'stores/employee/types';
|
||||||
|
|
||||||
|
// NOTE: Import stores
|
||||||
|
const DEFAULT_DATA_META_OCR: EmployeePassportPayload | EmployeeVisaPayload =
|
||||||
|
{} as EmployeePassportPayload | EmployeeVisaPayload;
|
||||||
|
|
||||||
|
export const useRequestForm = defineStore('form-request', () => {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
@ -94,6 +94,11 @@ const routes: RouteRecordRaw[] = [
|
||||||
name: 'agencies-management',
|
name: 'agencies-management',
|
||||||
component: () => import('pages/07_agencies-management/MainPage.vue'),
|
component: () => import('pages/07_agencies-management/MainPage.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/request-list',
|
||||||
|
name: 'RequestList',
|
||||||
|
component: () => import('pages/08_request-list/MainPage.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -112,6 +117,11 @@ const routes: RouteRecordRaw[] = [
|
||||||
name: 'QuotationDocumentView',
|
name: 'QuotationDocumentView',
|
||||||
component: () => import('pages/05_quotation/preview/ViewForm.vue'),
|
component: () => import('pages/05_quotation/preview/ViewForm.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/request-list/:requestListId',
|
||||||
|
name: 'requestListView',
|
||||||
|
component: () => import('pages/08_request-list/RequestListView.vue'),
|
||||||
|
},
|
||||||
|
|
||||||
// Always leave this as last one,
|
// Always leave this as last one,
|
||||||
// but you can also remove it
|
// but you can also remove it
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ type EmployeeRelation = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProductRelation = {
|
export type ProductRelation = {
|
||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -144,6 +144,7 @@ type ProductRelation = {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
updatedByUserId: string;
|
updatedByUserId: string;
|
||||||
productGroup?: ProductGroup;
|
productGroup?: ProductGroup;
|
||||||
|
document?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type WorkRelation = {
|
type WorkRelation = {
|
||||||
|
|
|
||||||
273
src/stores/request-list/index.ts
Normal file
273
src/stores/request-list/index.ts
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import {
|
||||||
|
RequestData,
|
||||||
|
RequestDataStatus,
|
||||||
|
RequestWork,
|
||||||
|
RequestWorkStatus,
|
||||||
|
Step,
|
||||||
|
} from './types';
|
||||||
|
import { api } from 'src/boot/axios';
|
||||||
|
import { PaginationResult } from 'src/types';
|
||||||
|
import {
|
||||||
|
EmployeePassportPayload,
|
||||||
|
EmployeeVisaPayload,
|
||||||
|
} from 'stores/employee/types';
|
||||||
|
|
||||||
|
import { manageAttachment, manageFile, manageMeta } from '../utils';
|
||||||
|
|
||||||
|
export const useRequestList = defineStore('request-list', () => {
|
||||||
|
const data = ref<RequestData[]>([]);
|
||||||
|
const page = ref<number>(1);
|
||||||
|
const pageMax = ref<number>(1);
|
||||||
|
const pageSize = ref<number>(30);
|
||||||
|
const stats = ref<Record<RequestDataStatus, number>>({
|
||||||
|
[RequestDataStatus.Pending]: 0,
|
||||||
|
[RequestDataStatus.InProgress]: 0,
|
||||||
|
[RequestDataStatus.Completed]: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
type TypeFile =
|
||||||
|
| 'passport'
|
||||||
|
| 'visa'
|
||||||
|
| 'citizen'
|
||||||
|
| 'house-registration'
|
||||||
|
| 'commercial-registration'
|
||||||
|
| 'vat-registration'
|
||||||
|
| 'power-of-attorney';
|
||||||
|
|
||||||
|
async function uploadAttachmentRequest(opt: {
|
||||||
|
id: string;
|
||||||
|
type: 'customer' | 'employee';
|
||||||
|
group: string;
|
||||||
|
file: File;
|
||||||
|
form?: EmployeePassportPayload | EmployeeVisaPayload;
|
||||||
|
name?: string;
|
||||||
|
}) {
|
||||||
|
const base = { customer: 'customer-branch', employee: 'employee' }[
|
||||||
|
opt.type
|
||||||
|
];
|
||||||
|
const attachmentManag = manageAttachment(api, base);
|
||||||
|
const metaManager = manageMeta<TypeFile>(api, base);
|
||||||
|
|
||||||
|
let res;
|
||||||
|
const group = [
|
||||||
|
'passport',
|
||||||
|
'visa',
|
||||||
|
'citizen',
|
||||||
|
'house-registration',
|
||||||
|
'commercial-registration',
|
||||||
|
'vat-registration',
|
||||||
|
'power-of-attorney',
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(opt.group);
|
||||||
|
|
||||||
|
if (group.includes(opt.group)) {
|
||||||
|
res = await metaManager.postMeta({
|
||||||
|
group: opt.group as TypeFile,
|
||||||
|
parentId: opt.id,
|
||||||
|
meta: opt.form,
|
||||||
|
file: opt.file,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res = await attachmentManag.putAttachment({
|
||||||
|
parentId: opt.id,
|
||||||
|
name: opt.name || '',
|
||||||
|
file: opt.file,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function viewAttachmentRequest(opt: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: 'customer' | 'employee';
|
||||||
|
group: string;
|
||||||
|
download?: boolean;
|
||||||
|
}) {
|
||||||
|
const base = { customer: 'customer-branch', employee: 'employee' }[
|
||||||
|
opt.type
|
||||||
|
];
|
||||||
|
const attachmentManag = manageAttachment(api, base);
|
||||||
|
const fileManager = manageFile<TypeFile>(api, base);
|
||||||
|
|
||||||
|
let res;
|
||||||
|
const group = [
|
||||||
|
'passport',
|
||||||
|
'visa',
|
||||||
|
'citizen',
|
||||||
|
'house-registration',
|
||||||
|
'commercial-registration',
|
||||||
|
'vat-registration',
|
||||||
|
'power-of-attorney',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (group.includes(opt.group)) {
|
||||||
|
res = await fileManager.getFile({
|
||||||
|
parentId: opt.id,
|
||||||
|
group: opt.group as TypeFile,
|
||||||
|
fileId: opt.name,
|
||||||
|
download: opt.download,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!group.includes(opt.group)) {
|
||||||
|
res = await attachmentManag.getAttachment({
|
||||||
|
parentId: opt.id,
|
||||||
|
name: opt.name,
|
||||||
|
download: opt.download,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res) return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAttachmentRequest(
|
||||||
|
id: string,
|
||||||
|
type: 'customer' | 'employee',
|
||||||
|
) {
|
||||||
|
const base = { customer: 'customer-branch', employee: 'employee' }[type];
|
||||||
|
|
||||||
|
const attachmentManag = manageAttachment(api, base);
|
||||||
|
const fileManager = manageFile<TypeFile>(api, base);
|
||||||
|
|
||||||
|
const resFiles: Partial<Record<string, any>> = {};
|
||||||
|
|
||||||
|
if (type === 'employee') {
|
||||||
|
const resPassport = await fileManager.listFile({
|
||||||
|
group: 'passport',
|
||||||
|
parentId: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resVisa = await fileManager.listFile({
|
||||||
|
group: 'visa',
|
||||||
|
parentId: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
resFiles['passport'] = { ...resPassport };
|
||||||
|
resFiles['visa'] = { ...resVisa };
|
||||||
|
} else if (type === 'customer') {
|
||||||
|
const groups = [
|
||||||
|
'citizen',
|
||||||
|
'house-registration',
|
||||||
|
'commercial-registration',
|
||||||
|
'vat-registration',
|
||||||
|
'power-of-attorney',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const group of groups) {
|
||||||
|
const res = await fileManager.listFile({
|
||||||
|
group,
|
||||||
|
parentId: id,
|
||||||
|
});
|
||||||
|
resFiles[group] = { ...res };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resAttachment = await attachmentManag.listAttachment({
|
||||||
|
parentId: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resAttachment)
|
||||||
|
for (const item of resAttachment) {
|
||||||
|
const [key] = item.split('-').map((s) => s.trim());
|
||||||
|
if (key) {
|
||||||
|
if (!resFiles[key]) {
|
||||||
|
resFiles[key] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resFiles[key].includes(item)) {
|
||||||
|
resFiles[key].push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(resFiles);
|
||||||
|
|
||||||
|
return resFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRequestDataStats() {
|
||||||
|
const res = await api.get<typeof stats.value>('/request-data/stats');
|
||||||
|
if (res.status < 400) return res.data;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRequestData(id: string) {
|
||||||
|
const res = await api.get<RequestData>(`/request-data/${id}`);
|
||||||
|
if (res.status < 400) return res.data;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRequestDataList(params?: {
|
||||||
|
query?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
requestDataStatus?: RequestDataStatus;
|
||||||
|
}) {
|
||||||
|
const res = await api.get<PaginationResult<RequestData>>('/request-data', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
if (res.status < 400) return res.data;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRequestWorkList(params?: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
requestDataId?: string;
|
||||||
|
}) {
|
||||||
|
const res = await api.get<PaginationResult<RequestWork>>('/request-work', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
if (res.status < 400) return res.data;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editRequestWork(body: Partial<RequestWork>) {
|
||||||
|
const res = await api.put(`/request-work/${body.id}`, {
|
||||||
|
...body,
|
||||||
|
id: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status < 400) return res.data;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editStatusRequestWork(body: Step) {
|
||||||
|
const res = await api.put<Step>(
|
||||||
|
`/request-work/${body.requestWorkId}/step-status/${body.step}`,
|
||||||
|
{
|
||||||
|
attributes: body.attributes,
|
||||||
|
requestWorkStatus: body.workStatus,
|
||||||
|
requestWorkId: undefined,
|
||||||
|
step: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.status < 400) return res.data;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
pageMax,
|
||||||
|
pageSize,
|
||||||
|
stats,
|
||||||
|
|
||||||
|
viewAttachmentRequest,
|
||||||
|
getAttachmentRequest,
|
||||||
|
uploadAttachmentRequest,
|
||||||
|
|
||||||
|
getRequestDataStats,
|
||||||
|
getRequestData,
|
||||||
|
getRequestDataList,
|
||||||
|
getRequestWorkList,
|
||||||
|
|
||||||
|
editRequestWork,
|
||||||
|
editStatusRequestWork,
|
||||||
|
};
|
||||||
|
});
|
||||||
89
src/stores/request-list/types.ts
Normal file
89
src/stores/request-list/types.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { Employee } from '../employee/types';
|
||||||
|
import { QuotationFull, Quotation } from '../quotations/types';
|
||||||
|
|
||||||
|
export type RequestData = {
|
||||||
|
id: string;
|
||||||
|
employee: Employee;
|
||||||
|
employeeId: string;
|
||||||
|
code: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
|
||||||
|
quotation: QuotationFull;
|
||||||
|
quotationId: string;
|
||||||
|
|
||||||
|
flow: Record<string, any>;
|
||||||
|
|
||||||
|
requestWork: RequestWork[];
|
||||||
|
requestDataStatus: RequestDataStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum RequestDataStatus {
|
||||||
|
Pending = 'Pending',
|
||||||
|
InProgress = 'InProgress',
|
||||||
|
Completed = 'Completed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RequestWorkStatus {
|
||||||
|
Pending = 'Pending',
|
||||||
|
Ready = 'Ready',
|
||||||
|
Waiting = 'Waiting',
|
||||||
|
InProgress = 'InProgress',
|
||||||
|
Validate = 'Validate',
|
||||||
|
Ended = 'Ended',
|
||||||
|
Completed = 'Completed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DocStatus {
|
||||||
|
AwaitReview = 'AwaitReview',
|
||||||
|
UploadedAwaitReview = 'UploadedAwaitReview',
|
||||||
|
ReviewedAwaitUpload = 'ReviewedAwaitUpload',
|
||||||
|
Reviewed = 'Reviewed',
|
||||||
|
ApprovedReview = 'ApprovedReview',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestWork = {
|
||||||
|
id?: string;
|
||||||
|
stepStatus: Step[];
|
||||||
|
requestDataId: string;
|
||||||
|
productService: QuotationFull['productServiceList'][number];
|
||||||
|
productServiceId: string;
|
||||||
|
request: RequestData;
|
||||||
|
attributes?: Attributes;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RowDocument = {
|
||||||
|
no: number;
|
||||||
|
documentType: string;
|
||||||
|
amount: number;
|
||||||
|
documentInSystem: boolean;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Attributes = {
|
||||||
|
customer?: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
employee?: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
properties?: { [field: string]: string | number | null | undefined };
|
||||||
|
remark?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AttributesForm = {
|
||||||
|
customerDuty: boolean;
|
||||||
|
customerDutyCost?: number;
|
||||||
|
companyDuty: boolean;
|
||||||
|
companyDutyCost?: number;
|
||||||
|
individualDuty: boolean;
|
||||||
|
localEmployee: boolean;
|
||||||
|
employeeId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Step = {
|
||||||
|
attributes?: { form?: AttributesForm };
|
||||||
|
requestWorkId: string;
|
||||||
|
workStatus?: RequestWorkStatus;
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,11 @@
|
||||||
import { Dialog, QSelect, Notify, QNotifyCreateOptions } from 'quasar';
|
import {
|
||||||
|
Dialog,
|
||||||
|
QSelect,
|
||||||
|
Notify,
|
||||||
|
QNotifyCreateOptions,
|
||||||
|
QVueGlobals,
|
||||||
|
useQuasar,
|
||||||
|
} from 'quasar';
|
||||||
import { Ref, ref } from 'vue';
|
import { Ref, ref } from 'vue';
|
||||||
import axios, { AxiosInstance, AxiosProgressEvent } from 'axios';
|
import axios, { AxiosInstance, AxiosProgressEvent } from 'axios';
|
||||||
import { ComposerTranslation } from 'vue-i18n';
|
import { ComposerTranslation } from 'vue-i18n';
|
||||||
|
|
@ -498,3 +505,31 @@ export function createDataRefBase<T>(
|
||||||
pageSize: ref<number>(defaultPageSize),
|
pageSize: ref<number>(defaultPageSize),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function changeMode(mode: string) {
|
||||||
|
const $q = useQuasar();
|
||||||
|
if (mode === 'light') {
|
||||||
|
localStorage.setItem('currentTheme', 'light');
|
||||||
|
$q.dark.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'dark') {
|
||||||
|
localStorage.setItem('currentTheme', 'dark');
|
||||||
|
$q.dark.set(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'baseOnDevice') {
|
||||||
|
localStorage.setItem('currentTheme', 'baseOnDevice');
|
||||||
|
if (
|
||||||
|
window.matchMedia &&
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
) {
|
||||||
|
$q.dark.set(true);
|
||||||
|
} else {
|
||||||
|
$q.dark.set(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,26 @@ type MRZ = {
|
||||||
|
|
||||||
type Field = {
|
type Field = {
|
||||||
field: string;
|
field: string;
|
||||||
|
|
||||||
format?: (value: string, obj?: Record<string, string>) => string;
|
format?: (value: string, obj?: Record<string, string>) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post process value after format.
|
||||||
|
* Useful for extract one field to multiple fields .
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Convert fullname into firstname and lastname
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* (value) => [
|
||||||
|
* { field: 'first_name', value: value.split(' ').at(0) || '' },
|
||||||
|
* { field: 'last_name', value: value.split(' ').slice(1).join(' ') },
|
||||||
|
* ]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
process?: (value: string) => { field: string; value: string }[];
|
||||||
};
|
};
|
||||||
type FieldList = Record<string, Field>;
|
type FieldList = Record<string, Field | Field[]>;
|
||||||
|
|
||||||
const DEFAULT_FIELD = {
|
const DEFAULT_FIELD = {
|
||||||
documentType: { field: 'doc_type' },
|
documentType: { field: 'doc_type' },
|
||||||
|
|
@ -18,6 +35,10 @@ const DEFAULT_FIELD = {
|
||||||
name: {
|
name: {
|
||||||
field: 'full_name',
|
field: 'full_name',
|
||||||
format: (value, _) => value.replace(/0/, 'O'),
|
format: (value, _) => value.replace(/0/, 'O'),
|
||||||
|
process: (value) => [
|
||||||
|
{ field: 'first_name', value: value.split(' ').at(0) || '' },
|
||||||
|
{ field: 'last_name', value: value.split(' ').slice(1).join(' ') },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
country: {
|
country: {
|
||||||
field: 'country',
|
field: 'country',
|
||||||
|
|
@ -132,8 +153,18 @@ function mrzCleanResult(obj: Record<string, string>) {
|
||||||
|
|
||||||
const original = structuredClone(obj);
|
const original = structuredClone(obj);
|
||||||
for (const value of Object.values(DEFAULT_FIELD)) {
|
for (const value of Object.values(DEFAULT_FIELD)) {
|
||||||
if (obj[value.field] && 'format' in value) {
|
if (obj[value.field]) {
|
||||||
obj[value.field] = value.format(obj[value.field], original);
|
if ('format' in value) {
|
||||||
|
obj[value.field] = value.format(obj[value.field], original);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('process' in value) {
|
||||||
|
value.process(obj[value.field]).forEach((result) => {
|
||||||
|
obj[result.field] = result.value;
|
||||||
|
});
|
||||||
|
delete obj[value.field];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue