Merge branch 'develop'
This commit is contained in:
commit
18844c70bc
80 changed files with 2772 additions and 869 deletions
|
|
@ -22,6 +22,7 @@
|
|||
"apexcharts": "^4.5.0",
|
||||
"axios": "^1.8.4",
|
||||
"cropperjs": "^1.6.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"highlight.js": "^11.11.1",
|
||||
"keycloak-js": "^25.0.6",
|
||||
"markdown-it": "^14.1.0",
|
||||
|
|
|
|||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
|
@ -29,6 +29,9 @@ importers:
|
|||
cropperjs:
|
||||
specifier: ^1.6.2
|
||||
version: 1.6.2
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
highlight.js:
|
||||
specifier: ^11.11.1
|
||||
version: 11.11.1
|
||||
|
|
@ -1473,6 +1476,9 @@ packages:
|
|||
date-fns@3.6.0:
|
||||
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
|
||||
|
||||
dayjs@1.11.13:
|
||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||
|
||||
de-indent@1.0.2:
|
||||
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
||||
|
||||
|
|
@ -5272,6 +5278,8 @@ snapshots:
|
|||
|
||||
date-fns@3.6.0: {}
|
||||
|
||||
dayjs@1.11.13: {}
|
||||
|
||||
de-indent@1.0.2: {}
|
||||
|
||||
debug@2.6.9:
|
||||
|
|
|
|||
BIN
public/img-group.png
Normal file
BIN
public/img-group.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
|
|
@ -31,7 +31,7 @@ export default defineConfig((ctx) => {
|
|||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
open: false,
|
||||
port: 5173,
|
||||
port: 5174,
|
||||
},
|
||||
framework: {
|
||||
config: {},
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ watch(
|
|||
|
||||
<div
|
||||
v-if="!single"
|
||||
class="bordered q-mr-sm rounded col text-center overflow-hidden"
|
||||
class="bordered q-mr-sm rounded col-4 text-center overflow-hidden"
|
||||
:class="{ 'pointer-none': readonly, 'q-my-sm': $q.screen.lt.md }"
|
||||
>
|
||||
<ImageHover
|
||||
|
|
|
|||
|
|
@ -29,19 +29,18 @@ const discountCondition = defineModel<string | null | undefined>(
|
|||
const sourceNationality = defineModel<string | null | undefined>(
|
||||
'sourceNationality',
|
||||
);
|
||||
const importNationality = defineModel<string | null | undefined>(
|
||||
const importNationality = defineModel<string[] | null | undefined>(
|
||||
'importNationality',
|
||||
);
|
||||
const trainingPlace = defineModel<string | null | undefined>('trainingPlace');
|
||||
const checkpoint = defineModel<string | null | undefined>('checkpoint');
|
||||
const agencyFile = defineModel<File[]>('agencyFile');
|
||||
const agencyFileList =
|
||||
defineModel<{ name: string; url: string }[]>('agencyFileList');
|
||||
const userFile = defineModel<File[]>('userFile');
|
||||
const userFileList =
|
||||
defineModel<{ name: string; url: string }[]>('userFileList');
|
||||
const remark = defineModel<string | null | undefined>('remark');
|
||||
const agencyStatus = defineModel<string | null | undefined>('agencyStatus');
|
||||
|
||||
const attachmentRef = ref();
|
||||
const checkpointENOption = ref([]);
|
||||
|
||||
defineProps<{
|
||||
dense?: boolean;
|
||||
|
|
@ -71,18 +70,12 @@ function deleteFile(name: string) {
|
|||
userStore.deleteAttachment(userId.value, payload);
|
||||
const result = await userStore.fetchAttachment(userId.value);
|
||||
if (result) {
|
||||
agencyFileList.value = result;
|
||||
userFileList.value = result;
|
||||
}
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const resultOption = await fetch('/option/option.json');
|
||||
const rawOption = await resultOption.json();
|
||||
checkpointENOption.value = rawOption.eng.border;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
|
|
@ -140,11 +133,12 @@ onMounted(async () => {
|
|||
/>
|
||||
|
||||
<SelectOffice
|
||||
v-if="userType === 'MESSENGER'"
|
||||
for="input-responsible-area"
|
||||
v-model:value="responsibleArea"
|
||||
v-if="userType === 'MESSENGER'"
|
||||
:readonly="readonly"
|
||||
:label="$t('personnel.form.responsibleArea')"
|
||||
class="col"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -185,38 +179,27 @@ onMounted(async () => {
|
|||
(v) => (typeof v === 'string' ? (sourceNationality = v) : '')
|
||||
"
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
:model-value="readonly ? importNationality || '-' : importNationality"
|
||||
v-model="importNationality"
|
||||
id="input-import-nationality"
|
||||
for="input-import-nationality"
|
||||
:option="optionStore.globalOption.nationality"
|
||||
class="col-md-3 col-6"
|
||||
:readonly
|
||||
multiple
|
||||
:hideSelected="false"
|
||||
clearable
|
||||
fillInput
|
||||
:label="$t('personnel.form.importNationality')"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (importNationality = v) : '')
|
||||
"
|
||||
/>
|
||||
<SelectInput
|
||||
:model-value="readonly ? trainingPlace || '-' : trainingPlace"
|
||||
id="select-trainig-place"
|
||||
for="select-trainig-place"
|
||||
:option="optionStore.globalOption.training"
|
||||
class="col-md-6 col-12"
|
||||
:readonly
|
||||
:label="$t('personnel.form.trainingPlace')"
|
||||
clearable
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (trainingPlace = v) : '')
|
||||
"
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
:model-value="readonly ? checkpoint || '-' : checkpoint"
|
||||
id="select-checkpoint"
|
||||
for="select-checkpoint"
|
||||
:option="optionStore.globalOption.border"
|
||||
class="col-6"
|
||||
class="col-md-6 col-12"
|
||||
:readonly
|
||||
:label="$t('personnel.form.checkpoint')"
|
||||
clearable
|
||||
|
|
@ -224,17 +207,18 @@ onMounted(async () => {
|
|||
(v) => (typeof v === 'string' ? (checkpoint = v) : '')
|
||||
"
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
:model-value="readonly ? checkpoint || '-' : checkpoint"
|
||||
id="select-checkpoint-en"
|
||||
for="select-checkpoint-en"
|
||||
:option="checkpointENOption"
|
||||
class="col-6"
|
||||
:model-value="readonly ? trainingPlace || '-' : trainingPlace"
|
||||
id="select-trainig-place"
|
||||
for="select-trainig-place"
|
||||
:option="optionStore.globalOption.training"
|
||||
class="col-md-8 col-12"
|
||||
:readonly
|
||||
:label="$t('personnel.form.checkpointEN')"
|
||||
:label="$t('personnel.form.trainingPlace')"
|
||||
clearable
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (checkpoint = v) : '')
|
||||
(v) => (typeof v === 'string' ? (trainingPlace = v) : '')
|
||||
"
|
||||
/>
|
||||
|
||||
|
|
@ -253,7 +237,7 @@ onMounted(async () => {
|
|||
value: AgencyStatus.Blacklist,
|
||||
},
|
||||
]"
|
||||
class="col-md-6 col-12"
|
||||
class="col-md-4 col-12"
|
||||
:readonly
|
||||
:label="$t('personnel.form.agencyStatus')"
|
||||
clearable
|
||||
|
|
@ -275,76 +259,78 @@ onMounted(async () => {
|
|||
"
|
||||
@clear="remark = ''"
|
||||
/>
|
||||
<q-file
|
||||
ref="attachmentRef"
|
||||
for="input-attachment"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
multiple
|
||||
append
|
||||
:label="$t('personnel.form.attachment')"
|
||||
class="col"
|
||||
v-model="agencyFile"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<Icon
|
||||
icon="material-symbols:attach-file"
|
||||
width="20px"
|
||||
style="color: var(--brand-1)"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:file="file">
|
||||
<div class="row full-width items-center">
|
||||
<span class="col ellipsis">
|
||||
{{ file.file.name }}
|
||||
</span>
|
||||
<q-btn
|
||||
dense
|
||||
rounded
|
||||
flat
|
||||
padding="2 2"
|
||||
class="app-text-muted"
|
||||
icon="mdi-close-circle"
|
||||
@click.stop="attachmentRef.removeAtIndex(file.index)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</q-file>
|
||||
</div>
|
||||
|
||||
<div v-if="agencyFileList && agencyFileList?.length > 0" class="col-12">
|
||||
<q-list bordered separator class="rounded" style="padding: 0">
|
||||
<q-item
|
||||
id="attachment-file"
|
||||
for="attachment-file"
|
||||
v-for="item in agencyFileList"
|
||||
clickable
|
||||
:key="item.url"
|
||||
class="items-center row"
|
||||
@click="() => openNewTab(item.url)"
|
||||
>
|
||||
<q-item-section>
|
||||
<div class="row items-center justify-between">
|
||||
<div class="col">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<q-btn
|
||||
id="delete-file"
|
||||
v-if="!readonly && userId"
|
||||
rounded
|
||||
flat
|
||||
dense
|
||||
unelevated
|
||||
size="md"
|
||||
icon="mdi-trash-can-outline"
|
||||
class="app-text-negative"
|
||||
@click.stop="deleteFile(item.name)"
|
||||
/>
|
||||
<q-file
|
||||
v-if="userType"
|
||||
ref="attachmentRef"
|
||||
for="input-attachment"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
multiple
|
||||
append
|
||||
:label="$t('personnel.form.attachment')"
|
||||
class="col"
|
||||
v-model="userFile"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<Icon
|
||||
icon="material-symbols:attach-file"
|
||||
width="20px"
|
||||
style="color: var(--brand-1)"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:file="file">
|
||||
<div class="row full-width items-center">
|
||||
<span class="col ellipsis">
|
||||
{{ file.file.name }}
|
||||
</span>
|
||||
<q-btn
|
||||
dense
|
||||
rounded
|
||||
flat
|
||||
padding="2 2"
|
||||
class="app-text-muted"
|
||||
icon="mdi-close-circle"
|
||||
@click.stop="attachmentRef.removeAtIndex(file.index)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</q-file>
|
||||
|
||||
<div v-if="userFileList && userFileList?.length > 0" class="col-12">
|
||||
<q-list bordered separator class="rounded" style="padding: 0">
|
||||
<q-item
|
||||
id="attachment-file"
|
||||
for="attachment-file"
|
||||
v-for="item in userFileList"
|
||||
clickable
|
||||
:key="item.url"
|
||||
class="items-center row"
|
||||
@click="() => openNewTab(item.url)"
|
||||
>
|
||||
<q-item-section>
|
||||
<div class="row items-center justify-between">
|
||||
<div class="col">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<q-btn
|
||||
id="delete-file"
|
||||
v-if="!readonly && userId"
|
||||
rounded
|
||||
flat
|
||||
dense
|
||||
unelevated
|
||||
size="md"
|
||||
icon="mdi-trash-can-outline"
|
||||
class="app-text-negative"
|
||||
@click.stop="deleteFile(item.name)"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { QSelect } from 'quasar';
|
||||
import useOptionStore from 'stores/options';
|
||||
import { selectFilterOptionRefMod } from 'stores/utils';
|
||||
import { calculateAge, disabledAfterToday } from 'src/utils/datetime';
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { capitalize } from 'vue';
|
||||
import { watch } from 'vue';
|
||||
import SelectInput from '../shared/SelectInput.vue';
|
||||
import DatePicker from '../shared/DatePicker.vue';
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
|
|
@ -23,6 +21,8 @@ const midNameEN = defineModel<string | null>('midNameEn');
|
|||
const citizenId = defineModel<string>('citizenId');
|
||||
const citizenIssue = defineModel<Date | null>('citizenIssue');
|
||||
const citizenExpire = defineModel<Date | null>('citizenExpire');
|
||||
const contactName = defineModel<string>('contactName');
|
||||
const contactTel = defineModel<string>('contactTel');
|
||||
|
||||
const props = defineProps<{
|
||||
dense?: boolean;
|
||||
|
|
@ -30,94 +30,19 @@ const props = defineProps<{
|
|||
readonly?: boolean;
|
||||
separator?: boolean;
|
||||
employee?: boolean;
|
||||
agency?: boolean;
|
||||
title?: string;
|
||||
prefixId: string;
|
||||
hideNameEn?: boolean;
|
||||
}>();
|
||||
|
||||
const prefixNameOptions = ref<Record<string, unknown>[]>([]);
|
||||
let prefixNameFilter: (
|
||||
value: string,
|
||||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
const prefixNameOptionsEn = ref<Record<string, unknown>[]>([]);
|
||||
let prefixNameFilterEn: (
|
||||
value: string,
|
||||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
const genderOptions = ref<Record<string, unknown>[]>([]);
|
||||
let genderFilter: (
|
||||
value: string,
|
||||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
const nationalityOptions = ref<Record<string, unknown>[]>([]);
|
||||
let nationalityFilter: (
|
||||
value: string,
|
||||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
function matPreFixName() {
|
||||
function matchPreFixName() {
|
||||
if (gender.value === 'male') prefixName.value = 'mr';
|
||||
if (gender.value === 'female' && prefixName.value === 'mr') {
|
||||
prefixName.value = 'mrs';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
prefixNameFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.prefix),
|
||||
prefixNameOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
prefixNameFilterEn = selectFilterOptionRefMod(
|
||||
ref(optionStore.rawOption?.eng.prefix),
|
||||
prefixNameOptionsEn,
|
||||
'label',
|
||||
);
|
||||
|
||||
genderFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.gender),
|
||||
genderOptions,
|
||||
'label',
|
||||
);
|
||||
nationalityFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.nationality),
|
||||
nationalityOptions,
|
||||
'label',
|
||||
);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => optionStore.globalOption,
|
||||
() => {
|
||||
prefixNameFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption.prefix),
|
||||
prefixNameOptions,
|
||||
'label',
|
||||
);
|
||||
genderFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption.gender),
|
||||
genderOptions,
|
||||
'label',
|
||||
);
|
||||
nationalityFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption.nationality),
|
||||
nationalityOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
prefixNameFilterEn = selectFilterOptionRefMod(
|
||||
ref(optionStore.rawOption?.eng.prefix),
|
||||
prefixNameOptionsEn,
|
||||
'label',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => prefixName.value,
|
||||
(v) => {
|
||||
|
|
@ -131,7 +56,7 @@ watch(
|
|||
() => gender.value,
|
||||
() => {
|
||||
if (props.readonly) return;
|
||||
matPreFixName();
|
||||
matchPreFixName();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
|
@ -171,41 +96,19 @@ watch(
|
|||
for="input-citizen-id"
|
||||
/>
|
||||
<div class="col-12 row" style="display: flex; gap: var(--size-2)">
|
||||
<q-select
|
||||
outlined
|
||||
use-input
|
||||
fill-input
|
||||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
<SelectInput
|
||||
hide-dropdown-icon
|
||||
autocomplete="off"
|
||||
class="col-md-1 col-6"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="prefixNameOptions"
|
||||
:readonly
|
||||
:option="optionStore.globalOption?.prefix"
|
||||
:id="`${prefixId}-select-prefix-name`"
|
||||
:for="`${prefixId}-select-prefix-name`"
|
||||
:label="$t('personnel.form.prefixName')"
|
||||
@filter="prefixNameFilter"
|
||||
:model-value="readonly ? prefixName || '-' : prefixName"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (prefixName = v) : '')
|
||||
:rules="
|
||||
agency ? [] : [(val: string) => !!val || $t('form.error.required')]
|
||||
"
|
||||
@clear="prefixName = ''"
|
||||
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
:label="$t('personnel.form.prefixName')"
|
||||
class="col-md-1 col-6"
|
||||
v-model="prefixName"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-first-name`"
|
||||
|
|
@ -217,7 +120,7 @@ watch(
|
|||
:label="$t('personnel.form.firstName')"
|
||||
v-model="firstName"
|
||||
:rules="
|
||||
employee
|
||||
employee || agency
|
||||
? []
|
||||
: [(val: string) => !!val || $t('form.error.required')]
|
||||
"
|
||||
|
|
@ -255,41 +158,17 @@ watch(
|
|||
class="col-12 row"
|
||||
style="display: flex; gap: var(--size-2)"
|
||||
>
|
||||
<q-select
|
||||
outlined
|
||||
use-input
|
||||
fill-input
|
||||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
<SelectInput
|
||||
hide-dropdown-icon
|
||||
autocomplete="off"
|
||||
class="col-md-1 col-6"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="prefixNameOptionsEn"
|
||||
:readonly
|
||||
:option="optionStore.rawOption?.eng.prefix"
|
||||
:id="`${prefixId}-select-prefix-name-en`"
|
||||
:for="`${prefixId}-select-prefix-name-en`"
|
||||
label="Prefix"
|
||||
@filter="prefixNameFilter"
|
||||
:model-value="readonly ? prefixName || '-' : prefixName"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (prefixName = v) : '')
|
||||
"
|
||||
@clear="prefixName = ''"
|
||||
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
:label="$t('personnel.form.prefixName')"
|
||||
class="col-md-1 col-6"
|
||||
v-model="prefixName"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-first-name-en`"
|
||||
|
|
@ -331,13 +210,7 @@ watch(
|
|||
v-model="lastNameEN"
|
||||
:rules="
|
||||
employee
|
||||
? [
|
||||
(val: string) => !!val || $t('form.error.required'),
|
||||
(val: string) =>
|
||||
!val ||
|
||||
/^[A-Za-z\s]+$/.test(val) ||
|
||||
$t('form.error.letterOnly'),
|
||||
]
|
||||
? []
|
||||
: [
|
||||
(val: string) =>
|
||||
!val ||
|
||||
|
|
@ -405,39 +278,16 @@ watch(
|
|||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-select
|
||||
<SelectInput
|
||||
v-if="!employee"
|
||||
outlined
|
||||
use-input
|
||||
fill-input
|
||||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
autocomplete="off"
|
||||
class="col-md-2 col-6"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="genderOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:readonly
|
||||
:option="optionStore.globalOption?.gender"
|
||||
:id="`${prefixId}-select-gender`"
|
||||
:for="`${prefixId}-select-gender`"
|
||||
:label="$t('form.gender')"
|
||||
@filter="genderFilter"
|
||||
:model-value="readonly ? gender || '-' : gender"
|
||||
@update:model-value="(v) => (typeof v === 'string' ? (gender = v) : '')"
|
||||
@clear="gender = ''"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
class="col-md-2 col-6"
|
||||
v-model="gender"
|
||||
/>
|
||||
|
||||
<DatePicker
|
||||
v-model="birthDate"
|
||||
|
|
@ -514,70 +364,67 @@ watch(
|
|||
"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
<SelectInput
|
||||
v-if="employee"
|
||||
outlined
|
||||
clearable
|
||||
use-input
|
||||
fill-input
|
||||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
autocomplete="off"
|
||||
hide-bottom-space
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
class="col-md-2 col-6"
|
||||
:dense="dense"
|
||||
v-model="gender"
|
||||
:readonly="readonly"
|
||||
:options="genderOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:readonly
|
||||
:option="optionStore.globalOption?.gender"
|
||||
:id="`${prefixId}-select-gender`"
|
||||
:for="`${prefixId}-select-gender`"
|
||||
:label="$t('form.gender')"
|
||||
@filter="genderFilter"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
<q-select
|
||||
v-if="employee"
|
||||
outlined
|
||||
clearable
|
||||
use-input
|
||||
fill-input
|
||||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
autocomplete="off"
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
v-model="nationality"
|
||||
class="col-md-2 col-6"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="nationalityOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
v-model="gender"
|
||||
/>
|
||||
<SelectInput
|
||||
v-if="employee"
|
||||
:readonly
|
||||
:option="optionStore.globalOption.nationality"
|
||||
:id="`${prefixId}-select-nationality`"
|
||||
:for="`${prefixId}-select-nationality`"
|
||||
:label="$t('general.nationality')"
|
||||
@filter="nationalityFilter"
|
||||
class="col-md-2 col-6"
|
||||
v-model="nationality"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-if="agency"
|
||||
for="input-agencies-contact-name"
|
||||
dense
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-md-4 col-12"
|
||||
:label="$t('personnel.form.contactName')"
|
||||
:model-value="readonly ? contactName || '-' : contactName"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (contactName = v) : '')
|
||||
"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-if="agency"
|
||||
for="input-agencies-contact-tel"
|
||||
id="input-agencies-contact-tel"
|
||||
dense
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-md-4 col-12"
|
||||
:label="$t('personnel.form.contactTel')"
|
||||
:model-value="readonly ? contactTel || '-' : contactTel"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (contactTel = v) : '')
|
||||
"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<template #prepend>
|
||||
<q-icon
|
||||
size="xs"
|
||||
name="mdi-phone-outline"
|
||||
class="cursor-pointer"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
</q-select>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -32,11 +32,11 @@ const entryCount = defineModel<number>('entryCount');
|
|||
const issuePlace = defineModel<string>('issuePlace');
|
||||
const issueCountry = defineModel<string>('issueCountry');
|
||||
const issueDate = defineModel<Date | null | string>('visaIssueDate');
|
||||
const type = defineModel<string>('visaType');
|
||||
const type = defineModel<string>('type');
|
||||
const expireDate = defineModel<Date>('expireDate');
|
||||
const remark = defineModel<string>('remark');
|
||||
const workerType = defineModel<string>('workerType');
|
||||
const number = defineModel<string>('visaNumber');
|
||||
const number = defineModel<string>('number');
|
||||
|
||||
const calculatedVisaDate = computed(() => {
|
||||
if (!issueDate.value) return undefined;
|
||||
|
|
@ -78,6 +78,12 @@ onMounted(async () => {
|
|||
await fetchProvince();
|
||||
});
|
||||
|
||||
const visaIssueCountryOptions = ref<Record<string, unknown>[]>([]);
|
||||
let visaIssueCountryFilter: (
|
||||
value: string,
|
||||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
const visaTypeOptions = ref<Record<string, unknown>[]>([]);
|
||||
let visaTypeFilter: (
|
||||
value: string,
|
||||
|
|
@ -97,6 +103,12 @@ onMounted(() => {
|
|||
'label',
|
||||
);
|
||||
|
||||
visaIssueCountryFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.nationality),
|
||||
visaIssueCountryOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
workerTypeFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.workerType),
|
||||
workerTypeOptions,
|
||||
|
|
@ -107,8 +119,14 @@ onMounted(() => {
|
|||
watch(
|
||||
() => optionStore.globalOption,
|
||||
() => {
|
||||
visaIssueCountryFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.nationality),
|
||||
visaIssueCountryOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
visaTypeFilter = selectFilterOptionRefMod(
|
||||
optionStore.globalOption.nationality,
|
||||
optionStore.globalOption.visaType,
|
||||
visaTypeOptions,
|
||||
'label',
|
||||
);
|
||||
|
|
@ -422,11 +440,11 @@ watch(
|
|||
class="col-md-4 col-6"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="visaTypeOptions"
|
||||
:options="visaIssueCountryOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:for="`${prefixId}-select-issue-country`"
|
||||
:label="$t('customerEmployee.form.issueCountry')"
|
||||
@filter="visaTypeFilter"
|
||||
@filter="visaIssueCountryFilter"
|
||||
:model-value="readonly ? issueCountry || '-' : issueCountry"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (issueCountry = v) : '')
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import { useI18n } from 'vue-i18n';
|
|||
|
||||
import useUserStore from 'src/stores/user';
|
||||
import useOptionStore from 'src/stores/options';
|
||||
import { useWorkflowTemplate } from 'src/stores/workflow-template';
|
||||
import { baseUrl } from 'stores/utils';
|
||||
import { getRole } from 'src/services/keycloak';
|
||||
import {
|
||||
WorkflowUserInTable,
|
||||
WorkflowTemplatePayload,
|
||||
WorkFlowPayloadStep,
|
||||
Group,
|
||||
} from 'src/stores/workflow-template/types';
|
||||
import { User } from 'src/stores/user/types';
|
||||
|
||||
|
|
@ -20,6 +22,7 @@ import ToggleButton from 'src/components/button/ToggleButton.vue';
|
|||
import NoData from '../NoData.vue';
|
||||
import SelectBranch from '../shared/select/SelectBranch.vue';
|
||||
import AddButton from '../button/AddButton.vue';
|
||||
import { QField } from 'quasar';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
|
|
@ -29,6 +32,7 @@ defineProps<{
|
|||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const optionStore = useOptionStore();
|
||||
const workflowStore = useWorkflowTemplate();
|
||||
|
||||
const userInTable = defineModel<WorkflowUserInTable[]>('userInTable', {
|
||||
default: [],
|
||||
|
|
@ -51,7 +55,9 @@ let objectOptions = [
|
|||
const options = ref(objectOptions);
|
||||
const role = ref<string[]>([]);
|
||||
const userList = ref<User[]>([]);
|
||||
const groupList = ref<Group[]>([]);
|
||||
const responsiblePersonSearch = ref('');
|
||||
const responsibleMenu = ref(false);
|
||||
|
||||
async function getUserList(opts?: { query: string }) {
|
||||
const resUser = await userStore.fetchList({
|
||||
|
|
@ -60,10 +66,10 @@ async function getUserList(opts?: { query: string }) {
|
|||
if (resUser) userList.value = resUser.result;
|
||||
}
|
||||
|
||||
// async function getUserById(responsiblePersonId: string) {
|
||||
// const resUser = await userStore.fetchById(responsiblePersonId);
|
||||
// if (resUser) userInTable.value.push(resUser);
|
||||
// }
|
||||
async function getGroupList() {
|
||||
const resGroup = await workflowStore.getGroupList();
|
||||
if (resGroup) groupList.value = resGroup;
|
||||
}
|
||||
|
||||
function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) {
|
||||
const currStep = flowData.value.step[stepIndex];
|
||||
|
|
@ -78,6 +84,7 @@ function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) {
|
|||
userInTable.value[stepIndex] = {
|
||||
name: flowData.value.step[stepIndex].name,
|
||||
responsiblePerson: [],
|
||||
responsibleGroup: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +108,33 @@ function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) {
|
|||
}
|
||||
}
|
||||
|
||||
function selectResponsibleGroup(stepIndex: number, responsibleGroup: string) {
|
||||
const currStep = flowData.value.step[stepIndex];
|
||||
const existGroupIndex = currStep.responsibleGroup?.findIndex(
|
||||
(p) => p === responsibleGroup,
|
||||
);
|
||||
|
||||
if (existGroupIndex === -1) {
|
||||
currStep.responsibleGroup?.push(responsibleGroup);
|
||||
|
||||
if (!userInTable.value[stepIndex]) {
|
||||
userInTable.value[stepIndex] = {
|
||||
name: flowData.value.step[stepIndex].name,
|
||||
responsiblePerson: [],
|
||||
responsibleGroup: [],
|
||||
};
|
||||
}
|
||||
|
||||
userInTable.value[stepIndex]?.responsibleGroup.push(responsibleGroup);
|
||||
} else {
|
||||
currStep.responsibleGroup?.splice(Number(existGroupIndex), 1);
|
||||
userInTable.value[stepIndex]?.responsibleGroup.splice(
|
||||
Number(existGroupIndex),
|
||||
1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function selectItem(
|
||||
val: Record<string, unknown>,
|
||||
responsibleInstitution?: string[],
|
||||
|
|
@ -142,6 +176,7 @@ watch(
|
|||
onMounted(async () => {
|
||||
role.value = getRole() || [];
|
||||
await getUserList();
|
||||
await getGroupList();
|
||||
await userStore.fetchHqOption();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -467,92 +502,128 @@ onMounted(async () => {
|
|||
</div>
|
||||
|
||||
<!-- RESPONSIBLE-PERSON -->
|
||||
<q-select
|
||||
<q-field
|
||||
v-if="step.responsiblePersonId"
|
||||
behavior="menu"
|
||||
:for="`select-responsible-person-${index}-${onDrawer ? 'drawer' : 'dialog'}`"
|
||||
:bg-color="readonly ? 'transparent' : ''"
|
||||
:readonly
|
||||
outlined
|
||||
dense
|
||||
v-model="step.responsiblePersonId"
|
||||
multiple
|
||||
:options="[1, 2, 3]"
|
||||
hide-bottom-space
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
emit-value
|
||||
:stack-label="
|
||||
userInTable[index]?.responsiblePerson.length > 0 ||
|
||||
userInTable[index]?.responsibleGroup.length > 0
|
||||
"
|
||||
:label="$t('flow.responsiblePerson')"
|
||||
dense
|
||||
class="col-md-6 col-12"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:class="{ 'cursor-pointer': !readonly }"
|
||||
>
|
||||
<template v-slot:selected-item="scope">
|
||||
<div class="column full-width">
|
||||
<div
|
||||
class="row items-center no-wrap"
|
||||
v-for="person in userInTable[
|
||||
index
|
||||
]?.responsiblePerson.filter(
|
||||
(p) => p.id === scope.opt,
|
||||
)"
|
||||
:key="person.id"
|
||||
>
|
||||
<q-avatar class="q-ml-sm" size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/user/${person.id}/profile-image/${person.selectedImage}`"
|
||||
>
|
||||
<template #error>
|
||||
<div
|
||||
class="no-padding full-width full-height flex items-center justify-center"
|
||||
:style="`${person.gender ? 'background: white' : 'background: linear-gradient(135deg,rgba(43, 137, 223, 1) 0%, rgba(230, 51, 81, 1) 100%);'}`"
|
||||
>
|
||||
<q-img
|
||||
v-if="person.gender"
|
||||
:src="
|
||||
person.gender === 'male'
|
||||
? '/no-img-man.png'
|
||||
: '/no-img-female.png'
|
||||
"
|
||||
/>
|
||||
<q-icon
|
||||
v-else
|
||||
size="sm"
|
||||
name="mdi-account-outline"
|
||||
style="color: white"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
<div
|
||||
class="column q-pl-md"
|
||||
style="color: var(--foreground)"
|
||||
<template #control>
|
||||
<q-item
|
||||
dense
|
||||
class="items-center full-width no-padding"
|
||||
v-for="person in userInTable[
|
||||
index
|
||||
]?.responsiblePerson.filter((p) =>
|
||||
step.responsiblePersonId.includes(p.id),
|
||||
)"
|
||||
:key="person.id"
|
||||
>
|
||||
<q-avatar class="q-ml-sm" size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/user/${person.id}/profile-image/${person.selectedImage}`"
|
||||
>
|
||||
<span>
|
||||
{{
|
||||
`${optionStore.mapOption(person.namePrefix || '')} ${
|
||||
$i18n.locale === 'eng'
|
||||
? person.firstNameEN
|
||||
: person.firstName
|
||||
} ${
|
||||
$i18n.locale === 'eng'
|
||||
? person.lastNameEN
|
||||
: person.lastName
|
||||
}`
|
||||
}}
|
||||
</span>
|
||||
<span class="text-caption app-text-muted">
|
||||
{{ person.code }}
|
||||
</span>
|
||||
</div>
|
||||
<template #error>
|
||||
<div
|
||||
class="no-padding full-width full-height flex items-center justify-center"
|
||||
:style="`${person.gender ? 'background: white' : 'background: linear-gradient(135deg,rgba(43, 137, 223, 1) 0%, rgba(230, 51, 81, 1) 100%);'}`"
|
||||
>
|
||||
<q-img
|
||||
v-if="person.gender"
|
||||
:src="
|
||||
person.gender === 'male'
|
||||
? '/no-img-man.png'
|
||||
: '/no-img-female.png'
|
||||
"
|
||||
/>
|
||||
<q-icon
|
||||
v-else
|
||||
size="sm"
|
||||
name="mdi-account-outline"
|
||||
style="color: white"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
<div
|
||||
class="column q-pl-md"
|
||||
style="color: var(--foreground)"
|
||||
>
|
||||
<span>
|
||||
{{
|
||||
`${optionStore.mapOption(person.namePrefix || '')} ${
|
||||
$i18n.locale === 'eng'
|
||||
? person.firstNameEN
|
||||
: person.firstName
|
||||
} ${
|
||||
$i18n.locale === 'eng'
|
||||
? person.lastNameEN
|
||||
: person.lastName
|
||||
}`
|
||||
}}
|
||||
</span>
|
||||
<span class="text-caption app-text-muted">
|
||||
{{ person.code }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</q-item>
|
||||
|
||||
<template v-slot:option></template>
|
||||
<q-menu v-if="!readonly" :offset="[0, 4]">
|
||||
<div
|
||||
v-if="step.responsibleGroup.length > 0"
|
||||
class="full-width app-text-muted text-weight-medium"
|
||||
style="font-size: 10px"
|
||||
>
|
||||
{{ $t('general.group') }}
|
||||
</div>
|
||||
<q-item
|
||||
class="items-center full-width no-padding"
|
||||
v-for="group in userInTable[
|
||||
index
|
||||
]?.responsibleGroup.filter((g) =>
|
||||
step.responsibleGroup.includes(g),
|
||||
)"
|
||||
:key="group"
|
||||
dense
|
||||
>
|
||||
<q-avatar class="q-ml-sm" size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`/img-group.png`"
|
||||
/>
|
||||
</q-avatar>
|
||||
<span class="q-pl-md">
|
||||
{{ group }}
|
||||
</span>
|
||||
</q-item>
|
||||
</template>
|
||||
<template #append>
|
||||
<q-icon
|
||||
name="mdi-menu-down"
|
||||
:class="{ rotated: responsibleMenu }"
|
||||
class="transition-rotate"
|
||||
/>
|
||||
</template>
|
||||
<q-menu
|
||||
v-if="!readonly"
|
||||
no-focus
|
||||
no-refocus
|
||||
:offset="[0, 4]"
|
||||
@before-show="() => (responsibleMenu = true)"
|
||||
@before-hide="() => (responsibleMenu = false)"
|
||||
>
|
||||
<q-list>
|
||||
<q-item>
|
||||
<q-input
|
||||
|
|
@ -581,6 +652,7 @@ onMounted(async () => {
|
|||
{{ $t('general.noData') }}
|
||||
</q-item>
|
||||
<q-item
|
||||
v-else
|
||||
v-for="(person, i) in userList"
|
||||
dense
|
||||
:key="i"
|
||||
|
|
@ -655,6 +727,7 @@ onMounted(async () => {
|
|||
{{ $t('personnel.MESSENGER') }}
|
||||
</span>
|
||||
<q-item
|
||||
dense
|
||||
clickable
|
||||
@click="step.messengerByArea = !step.messengerByArea"
|
||||
class="column"
|
||||
|
|
@ -670,9 +743,49 @@ onMounted(async () => {
|
|||
</div>
|
||||
</div>
|
||||
</q-item>
|
||||
|
||||
<span class="text-caption app-text-muted-2 q-px-md">
|
||||
{{ $t('general.group') }}
|
||||
</span>
|
||||
<q-item
|
||||
v-if="groupList.length === 0"
|
||||
class="app-text-muted q-px-lg"
|
||||
>
|
||||
{{ $t('general.noData') }}
|
||||
</q-item>
|
||||
<q-item
|
||||
v-else
|
||||
v-for="(group, i) in groupList"
|
||||
dense
|
||||
clickable
|
||||
@click="selectResponsibleGroup(index, group.name)"
|
||||
class="column"
|
||||
>
|
||||
<div class="row items-center">
|
||||
<q-checkbox
|
||||
size="xs"
|
||||
:model-value="
|
||||
step.responsibleGroup.includes(group.name)
|
||||
"
|
||||
@click.stop="
|
||||
selectResponsibleGroup(index, group.name)
|
||||
"
|
||||
/>
|
||||
<q-avatar class="q-ml-sm" size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`/img-group.png`"
|
||||
/>
|
||||
</q-avatar>
|
||||
<div class="column q-pl-md">
|
||||
<span>{{ group.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-select>
|
||||
</q-field>
|
||||
|
||||
<!-- RESPONSIBLE-AGENCIES, RESPONSIBLE-INSTITUTION -->
|
||||
<q-select
|
||||
|
|
@ -815,4 +928,11 @@ onMounted(async () => {
|
|||
:deep(.q-dialog.fullscreen.no-pointer-events.q-dialog--modal) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.transition-rotate {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -244,16 +244,19 @@ withDefaults(
|
|||
<template v-if="col.name === '#calcVat'">
|
||||
<q-checkbox
|
||||
v-if="priceDisplay?.price && props.rowIndex === 0"
|
||||
:disable="readonly"
|
||||
v-model="calcVat"
|
||||
size="xs"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-if="priceDisplay?.agentPrice && props.rowIndex === 1"
|
||||
:disable="readonly"
|
||||
v-model="agentPriceCalcVat"
|
||||
size="xs"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-if="priceDisplay?.serviceCharge && props.rowIndex === 2"
|
||||
:disable="readonly"
|
||||
v-model="serviceChargeCalcVat"
|
||||
size="xs"
|
||||
/>
|
||||
|
|
@ -271,6 +274,8 @@ withDefaults(
|
|||
flat
|
||||
outlined
|
||||
dense
|
||||
:readonly
|
||||
:hide-dropdown-icon="readonly"
|
||||
v-model="vatIncluded"
|
||||
></q-select>
|
||||
<q-select
|
||||
|
|
@ -285,6 +290,8 @@ withDefaults(
|
|||
flat
|
||||
outlined
|
||||
dense
|
||||
:readonly
|
||||
:hide-dropdown-icon="readonly"
|
||||
v-model="agentPriceVatIncluded"
|
||||
></q-select>
|
||||
<q-select
|
||||
|
|
@ -299,6 +306,8 @@ withDefaults(
|
|||
flat
|
||||
outlined
|
||||
dense
|
||||
:readonly
|
||||
:hide-dropdown-icon="readonly"
|
||||
v-model="serviceChargeVatIncluded"
|
||||
></q-select>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ type Options = { label: string; value: string };
|
|||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-md-4 col-12"
|
||||
:label="$t('form.telephone')"
|
||||
:label="$t('agencies.contactTel')"
|
||||
:model-value="readonly ? contactTel || '-' : contactTel"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (contactTel = v) : '')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
defineProps<{
|
||||
hideIcon?: boolean;
|
||||
icon?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
@ -10,10 +13,13 @@ defineProps<{
|
|||
v-if="!hideIcon"
|
||||
id="btn-add"
|
||||
padding="sm"
|
||||
icon="mdi-plus"
|
||||
:icon="icon ? undefined : 'mdi-plus'"
|
||||
direction="up"
|
||||
class="color-btn"
|
||||
>
|
||||
<template #icon v-if="icon">
|
||||
<Icon :icon width="24" />
|
||||
</template>
|
||||
<slot>
|
||||
<q-fab-action
|
||||
padding="xs"
|
||||
|
|
@ -29,10 +35,12 @@ defineProps<{
|
|||
fab
|
||||
id="btn-add"
|
||||
padding="sm"
|
||||
icon="mdi-plus"
|
||||
:icon="icon ? undefined : 'mdi-plus'"
|
||||
direction="up"
|
||||
class="color-btn"
|
||||
/>
|
||||
>
|
||||
<Icon v-if="icon" :icon width="24" />
|
||||
</q-btn>
|
||||
</q-page-sticky>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import MainButton from './MainButton.vue';
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', v: MouseEvent): void;
|
||||
(e: 'fileSelected', v: File[]): void;
|
||||
}>();
|
||||
defineProps<{
|
||||
iconOnly?: boolean;
|
||||
|
|
@ -10,15 +12,29 @@ defineProps<{
|
|||
outlined?: boolean;
|
||||
disabled?: boolean;
|
||||
dark?: boolean;
|
||||
importFile?: boolean;
|
||||
|
||||
label?: string;
|
||||
icon?: string;
|
||||
}>();
|
||||
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
function triggerFileInput() {
|
||||
inputRef.value?.click();
|
||||
}
|
||||
|
||||
function handleFileChange(event: Event) {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
if (files && files.length > 0) {
|
||||
emit('fileSelected', Array.from(files));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MainButton
|
||||
@click="(e) => $emit('click', e)"
|
||||
@click="(e) => (importFile ? triggerFileInput() : $emit('click', e))"
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
:icon="icon || 'mdi-import'"
|
||||
color="var(--info-bg)"
|
||||
|
|
@ -26,4 +42,13 @@ defineProps<{
|
|||
>
|
||||
{{ label || $t('general.import') }}
|
||||
</MainButton>
|
||||
|
||||
<input
|
||||
ref="inputRef"
|
||||
type="file"
|
||||
@change="(e) => handleFileChange(e)"
|
||||
hidden
|
||||
accept=".xls, .xlsx , .csv"
|
||||
multiple
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
32
src/components/button/PasteButton.vue
Normal file
32
src/components/button/PasteButton.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts" setup>
|
||||
import MainButton from './MainButton.vue';
|
||||
|
||||
defineEmits<{
|
||||
(e: 'click', v: MouseEvent): void;
|
||||
}>();
|
||||
defineProps<{
|
||||
iconOnly?: boolean;
|
||||
solid?: boolean;
|
||||
outlined?: boolean;
|
||||
disabled?: boolean;
|
||||
dark?: boolean;
|
||||
|
||||
label?: string;
|
||||
icon?: string;
|
||||
|
||||
amount?: number;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MainButton
|
||||
@click="(e) => $emit('click', e)"
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
:icon="icon || 'mdi-file-replace'"
|
||||
color="207 96% 32%"
|
||||
:title="iconOnly ? $t('general.paste') : undefined"
|
||||
>
|
||||
{{ label || $t('general.paste') }}
|
||||
{{ amount && amount > 0 ? `(${amount})` : '' }}
|
||||
</MainButton>
|
||||
</template>
|
||||
|
|
@ -14,3 +14,4 @@ export { default as PrintButton } from './PrintButton.vue';
|
|||
export { default as StateButton } from './StateButton.vue';
|
||||
export { default as NextButton } from './NextButton.vue';
|
||||
export { default as ImportButton } from './ImportButton.vue';
|
||||
export { default as PasteButton } from './PasteButton.vue';
|
||||
|
|
|
|||
189
src/components/shared/AdvanceSearch.vue
Normal file
189
src/components/shared/AdvanceSearch.vue
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { dateFormatJS } from 'src/utils/datetime';
|
||||
import SelectInput from './SelectInput.vue';
|
||||
import VueDatePicker from '@vuepic/vue-datepicker';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
defineProps<{
|
||||
active?: boolean;
|
||||
}>();
|
||||
|
||||
const date = defineModel<string[]>();
|
||||
|
||||
const dateRange = ref<string>('');
|
||||
const isDateSelect = ref(false);
|
||||
|
||||
function mapDateRange(val: string) {
|
||||
const today = dayjs();
|
||||
let start: dayjs.Dayjs, end: dayjs.Dayjs;
|
||||
|
||||
switch (val) {
|
||||
case 'toDay':
|
||||
start = today.startOf('day');
|
||||
end = today.endOf('day');
|
||||
break;
|
||||
case 'yesterday':
|
||||
start = today.subtract(1, 'day').startOf('day');
|
||||
end = today.subtract(1, 'day').endOf('day');
|
||||
break;
|
||||
case 'thisWeek':
|
||||
start = today.startOf('week');
|
||||
end = today.endOf('week');
|
||||
break;
|
||||
case 'lastWeek':
|
||||
start = today.subtract(1, 'week').startOf('week');
|
||||
end = today.subtract(1, 'week').endOf('week');
|
||||
break;
|
||||
case 'thisMonth':
|
||||
start = today.startOf('month');
|
||||
end = today.endOf('month');
|
||||
break;
|
||||
case 'lastMonth':
|
||||
start = today.subtract(1, 'month').startOf('month');
|
||||
end = today.subtract(1, 'month').endOf('month');
|
||||
break;
|
||||
case 'thisYear':
|
||||
start = today.startOf('year');
|
||||
end = today.endOf('year');
|
||||
break;
|
||||
case 'lastYear':
|
||||
start = today.subtract(1, 'year').startOf('year');
|
||||
end = today.subtract(1, 'year').endOf('year');
|
||||
break;
|
||||
case 'last7Days':
|
||||
start = today.subtract(6, 'day').startOf('day');
|
||||
end = today.endOf('day');
|
||||
break;
|
||||
case 'last30Days':
|
||||
start = today.subtract(29, 'day').startOf('day');
|
||||
end = today.endOf('day');
|
||||
break;
|
||||
case 'last90Days':
|
||||
start = today.subtract(89, 'day').startOf('day');
|
||||
end = today.endOf('day');
|
||||
break;
|
||||
case 'customDateRange':
|
||||
start = today.startOf('day');
|
||||
end = today.endOf('day');
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
return [start.toDate().toISOString(), end.toDate().toISOString()];
|
||||
}
|
||||
|
||||
watch(
|
||||
() => dateRange.value,
|
||||
() => {
|
||||
if (!dateRange.value) return;
|
||||
date.value = mapDateRange(dateRange.value);
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => date.value,
|
||||
() => {
|
||||
if (date.value && date.value.length === 0) dateRange.value = '';
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<q-btn
|
||||
size="xs"
|
||||
round
|
||||
dense
|
||||
unelevated
|
||||
icon="mdi-tune-variant"
|
||||
:flat="active ? false : !dateRange"
|
||||
:color="active || dateRange ? 'info' : undefined"
|
||||
>
|
||||
<q-menu
|
||||
:offset="[5, 10]"
|
||||
max-width="300px"
|
||||
class="bordered"
|
||||
:persistent="isDateSelect"
|
||||
>
|
||||
<div class="q-pa-sm">
|
||||
<div class="text-weight-medium">
|
||||
{{ $t('general.advanceSearch') }}
|
||||
</div>
|
||||
<SelectInput
|
||||
v-model="dateRange"
|
||||
:label="$t('general.period')"
|
||||
:option="[
|
||||
{ label: $t('dateRange.today'), value: 'toDay' },
|
||||
{ label: $t('dateRange.yesterday'), value: 'yesterday' },
|
||||
{ label: $t('dateRange.thisWeek'), value: 'thisWeek' },
|
||||
{ label: $t('dateRange.lastWeek'), value: 'lastWeek' },
|
||||
{ label: $t('dateRange.thisMonth'), value: 'thisMonth' },
|
||||
{ label: $t('dateRange.lastMonth'), value: 'lastMonth' },
|
||||
{ label: $t('dateRange.thisYear'), value: 'thisYear' },
|
||||
{ label: $t('dateRange.lastYear'), value: 'lastYear' },
|
||||
{ label: $t('dateRange.last7Days'), value: 'last7Days' },
|
||||
{ label: $t('dateRange.last30Days'), value: 'last30Days' },
|
||||
{ label: $t('dateRange.last90Days'), value: 'last90Days' },
|
||||
{
|
||||
label: $t('dateRange.customDateRange'),
|
||||
value: 'customDateRange',
|
||||
},
|
||||
]"
|
||||
clearable
|
||||
@clear="() => (date = [])"
|
||||
/>
|
||||
|
||||
<VueDatePicker
|
||||
v-if="dateRange === 'customDateRange'"
|
||||
utc
|
||||
range
|
||||
teleport
|
||||
auto-apply
|
||||
for="select-date-range"
|
||||
class="q-mt-sm"
|
||||
v-model="date"
|
||||
:dark="$q.dark.isActive"
|
||||
:locale="$i18n.locale === 'tha' ? 'th' : 'en'"
|
||||
@open="() => (isDateSelect = true)"
|
||||
@closed="() => (isDateSelect = false)"
|
||||
>
|
||||
<template #trigger>
|
||||
<q-input
|
||||
placeholder="DD/MM/YYYY"
|
||||
hide-bottom-space
|
||||
dense
|
||||
outlined
|
||||
for="select-date-range"
|
||||
:model-value="
|
||||
date
|
||||
? dateFormatJS({ date: date[0] }) +
|
||||
' - ' +
|
||||
dateFormatJS({ date: date[1] })
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-calendar-outline" class="app-text-muted" />
|
||||
</template>
|
||||
<q-tooltip>
|
||||
{{
|
||||
date
|
||||
? dateFormatJS({ date: date[0] }) +
|
||||
' - ' +
|
||||
dateFormatJS({ date: date[1] })
|
||||
: ''
|
||||
}}
|
||||
</q-tooltip>
|
||||
</q-input>
|
||||
</template>
|
||||
</VueDatePicker>
|
||||
|
||||
<slot></slot>
|
||||
<!-- <SelectInput :label="$t('general.documentStatus')" :option="[]" /> -->
|
||||
</div>
|
||||
</q-menu>
|
||||
<q-tooltip v-if="$q.screen.gt.sm">
|
||||
{{ $t('general.advanceSearch') }}
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
|
|
@ -25,7 +25,11 @@ withDefaults(
|
|||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="data.length > 3" class="avatar remaining-count">
|
||||
<div
|
||||
v-if="data.length > 3"
|
||||
class="avatar remaining-count"
|
||||
style="cursor: default"
|
||||
>
|
||||
<q-tooltip>
|
||||
<div v-for="(person, i) in data.slice(3)" :key="i + 3">
|
||||
{{ person.name }}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const props = withDefaults(
|
|||
useUpload?: boolean;
|
||||
useCancel?: boolean;
|
||||
useRejectCancel?: boolean;
|
||||
useCopy?: boolean;
|
||||
disableCancel?: boolean;
|
||||
disableDelete?: boolean;
|
||||
}>(),
|
||||
|
|
@ -31,6 +32,7 @@ defineEmits<{
|
|||
(e: 'link'): void;
|
||||
(e: 'upload'): void;
|
||||
(e: 'delete'): void;
|
||||
(e: 'copy'): void;
|
||||
(e: 'cancel'): void;
|
||||
(e: 'rejectCancel'): void;
|
||||
(e: 'changeStatus'): void;
|
||||
|
|
@ -172,6 +174,27 @@ watch(
|
|||
</span>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
v-if="useCopy"
|
||||
v-close-popup
|
||||
dense
|
||||
clickable
|
||||
class="row q-py-sm"
|
||||
style="white-space: nowrap"
|
||||
:id="`btn-kebab-copy-${idName}`"
|
||||
@click.stop="() => $emit('copy')"
|
||||
>
|
||||
<q-icon
|
||||
size="xs"
|
||||
class="col-3"
|
||||
name="mdi-content-copy"
|
||||
style="color: hsl(var(--teal-5-hsl))"
|
||||
/>
|
||||
<span class="col-9 q-px-md flex items-center">
|
||||
{{ $t('general.copy') }}
|
||||
</span>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
v-if="useCancel"
|
||||
v-close-popup
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const props = withDefaults(
|
|||
disable?: boolean;
|
||||
multiple?: boolean;
|
||||
hideInput?: boolean;
|
||||
hideDropdownIcon?: boolean;
|
||||
|
||||
rules?: ((value: string) => string | true)[];
|
||||
}>(),
|
||||
|
|
@ -82,7 +83,7 @@ watch(
|
|||
:hide-selected
|
||||
hide-bottom-space
|
||||
:fill-input="fillInput && !!model"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:hide-dropdown-icon="readonly || hideDropdownIcon"
|
||||
input-debounce="500"
|
||||
:option-value="
|
||||
typeof props.optionValue === 'string' ? props.optionValue : 'value'
|
||||
|
|
@ -103,6 +104,11 @@ watch(
|
|||
}
|
||||
"
|
||||
:rules
|
||||
@clear="
|
||||
() => {
|
||||
multiple ? (model = []) : (model = '');
|
||||
}
|
||||
"
|
||||
>
|
||||
<template v-if="$slots.prepend" v-slot:prepend>
|
||||
<slot name="prepend"></slot>
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ onMounted(async () => {
|
|||
}
|
||||
|
||||
await getSelectedOption();
|
||||
|
||||
valueOption.value = selectOptions.value.find((v) => v.id === value.value);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const { getOptions, setFirstValue, getSelectedOption, filter } =
|
|||
const ret = await getList({
|
||||
query: query === '' ? undefined : query,
|
||||
...props.params,
|
||||
activeOnly: true,
|
||||
});
|
||||
if (ret) return ret.result;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export default {
|
|||
branchStatus: 'Branch Status',
|
||||
success: 'Success',
|
||||
taxNo: 'Legal Person',
|
||||
contactName: 'Contact Name',
|
||||
contactName: 'Contact Person',
|
||||
image: 'Image of ',
|
||||
apply: 'Apply',
|
||||
licenseNumber: 'License number',
|
||||
|
|
@ -154,6 +154,12 @@ export default {
|
|||
draw: 'Draw',
|
||||
newUpload: 'New Upload',
|
||||
nativeLanguage: '{msg} Native Language',
|
||||
copy: 'Copy',
|
||||
paste: 'Paste',
|
||||
period: 'Period',
|
||||
documentStatus: 'Document Status',
|
||||
advanceSearch: 'Advance Search',
|
||||
totalPeople: '{meg} people',
|
||||
},
|
||||
|
||||
menu: {
|
||||
|
|
@ -248,7 +254,8 @@ export default {
|
|||
|
||||
manual: {
|
||||
title: 'Manual',
|
||||
usage: 'การใช้งาน',
|
||||
usage: 'Usage',
|
||||
troubleshooting: 'Troubleshooting',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -377,7 +384,7 @@ export default {
|
|||
branchLabel: 'Branch',
|
||||
branchHQLabel: 'Headoffice',
|
||||
taxNo: 'Legal Person',
|
||||
contactName: 'Contact Name',
|
||||
contactName: 'Contact Person',
|
||||
},
|
||||
page: {
|
||||
captionManage: 'Manage',
|
||||
|
|
@ -398,8 +405,8 @@ export default {
|
|||
code: 'Headoffice Code',
|
||||
codeBranch: 'Branch Code',
|
||||
taxNo: 'Tax Identification Number',
|
||||
contactName: 'Contact Name',
|
||||
contactTelephone: 'Contact Telephone',
|
||||
contactName: 'Contact Person',
|
||||
contactTelephone: 'Contact Number',
|
||||
branchName: 'Branch Name',
|
||||
branchNameEN: 'Branch Name (EN)',
|
||||
servicePointName: 'Service Point Name',
|
||||
|
|
@ -464,6 +471,8 @@ export default {
|
|||
normal: 'Normal',
|
||||
canceled: 'Canceled',
|
||||
blacklist: 'Black list',
|
||||
contactName: 'Contact Person',
|
||||
contactTel: 'Contact Number',
|
||||
},
|
||||
},
|
||||
customer: {
|
||||
|
|
@ -477,7 +486,6 @@ export default {
|
|||
powerOfAttorney: 'Power of Attorney',
|
||||
others: 'Others',
|
||||
},
|
||||
|
||||
employer: 'Employer',
|
||||
employerLegalEntity: 'Legal Entity',
|
||||
employerNaturalPerson: 'Natrual Person',
|
||||
|
|
@ -499,15 +507,12 @@ export default {
|
|||
religion: 'Religion',
|
||||
issueDate: 'Issue Date',
|
||||
passportExpiryDate: 'Passport Expiry Date',
|
||||
|
||||
ownerName: 'Customer Name',
|
||||
firstName: 'First Name ',
|
||||
lastName: 'Last Name ',
|
||||
firstNameEN: 'First Name in English',
|
||||
lastNameEN: 'Last Name in English',
|
||||
|
||||
cardNumber: 'ID Card Number',
|
||||
|
||||
prefixName: 'Prefix',
|
||||
legalPersonNo: 'Legal Entity Registration Number',
|
||||
registerName: 'Company Name',
|
||||
|
|
@ -515,7 +520,6 @@ export default {
|
|||
registerDate: 'Registered On',
|
||||
registerCompanyName: 'Registered Name',
|
||||
authorizedCapital: 'Authorized Capital',
|
||||
|
||||
workplace: 'Workplace',
|
||||
workplaceEN: 'Workplace (EN)',
|
||||
address: 'Address',
|
||||
|
|
@ -523,7 +527,6 @@ export default {
|
|||
branchCode: 'Branch Code',
|
||||
customerCode: 'Employer Code',
|
||||
legalPersonCode: 'Legal Entity Code',
|
||||
|
||||
codeAbbrev: 'Company Abbreviation',
|
||||
codeNumber: 'Company Number',
|
||||
registeredBranch: 'Registered Branch',
|
||||
|
|
@ -561,7 +564,7 @@ export default {
|
|||
jobPosition: 'Job Position',
|
||||
address: 'Address',
|
||||
workPlace: 'Workplace',
|
||||
contactName: 'Contact Name',
|
||||
contactName: 'Contact Person',
|
||||
contactPhone: 'Contact Phone',
|
||||
totalEmployee: 'Total Employee',
|
||||
officeTel: 'Headoffice Telephone',
|
||||
|
|
@ -799,7 +802,7 @@ export default {
|
|||
employee: 'Employee',
|
||||
employeeName: 'Full Name',
|
||||
workName: 'Work Name',
|
||||
contactName: 'Contact Name',
|
||||
contactName: 'Contact Person',
|
||||
documentReceivePoint: 'Document Drop-Off Point"',
|
||||
dueDate: 'Quotation Due Date',
|
||||
specialCondition: 'Special Conditions',
|
||||
|
|
@ -917,8 +920,8 @@ export default {
|
|||
code: 'Agencies Code',
|
||||
group: 'Agencies Group',
|
||||
name: 'Agencies Name',
|
||||
contactName: 'Contact Name',
|
||||
contactTel: 'Contact Telephone',
|
||||
contactName: 'Contact Person',
|
||||
contactTel: 'Contact Number',
|
||||
bankInfo: 'Bank Information',
|
||||
},
|
||||
|
||||
|
|
@ -941,8 +944,9 @@ export default {
|
|||
localEmployee: 'Local Employee',
|
||||
nonLocalEmployee: 'Non Local Employee',
|
||||
noWorkflowTemplate: 'A workflow template has not been selected.',
|
||||
|
||||
salesRepresentative: 'Sales Representative',
|
||||
|
||||
dataOffice: 'Employment Office District',
|
||||
ref: 'Reference',
|
||||
action: {
|
||||
title: 'Action',
|
||||
|
|
@ -1006,7 +1010,7 @@ export default {
|
|||
issueBranch: 'Issue Branch',
|
||||
issueDate: 'Issue Date',
|
||||
madeBy: 'Made By',
|
||||
contactName: 'Contact Name',
|
||||
contactName: 'Contact Person',
|
||||
workOrderCode: 'Work Order Code',
|
||||
workOrderName: 'Work Order Name',
|
||||
telephone: 'Telephone',
|
||||
|
|
@ -1065,6 +1069,10 @@ export default {
|
|||
confirmDebitNoteAccept: 'Confirm acceptance of the debit note.',
|
||||
},
|
||||
message: {
|
||||
copy: 'Copy',
|
||||
warningPaste:
|
||||
'Do you want to replace the data with the newly copied information?',
|
||||
warningCopyEmpty: 'You have not copied any data yet',
|
||||
quotationAccept: 'Once accepted, no further modifications can be made',
|
||||
beingUse: '"{msg}" is being used.',
|
||||
incompleteDataEntry: 'Incomplete data entry on {tap} page',
|
||||
|
|
@ -1485,4 +1493,19 @@ export default {
|
|||
type: 'Type',
|
||||
},
|
||||
},
|
||||
|
||||
dateRange: {
|
||||
today: 'Today',
|
||||
yesterday: 'Yesterday',
|
||||
thisWeek: 'This Week',
|
||||
lastWeek: 'Last Week',
|
||||
thisMonth: 'This Month',
|
||||
lastMonth: 'Last Month',
|
||||
thisYear: 'This Year',
|
||||
lastYear: 'Last Year',
|
||||
last7Days: 'Last 7 Days',
|
||||
last30Days: 'Last 30 Days',
|
||||
last90Days: 'Last 90 Days',
|
||||
customDateRange: 'Custom Date Range',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -154,6 +154,12 @@ export default {
|
|||
draw: 'วาด',
|
||||
newUpload: 'อัปโหลดใหม่',
|
||||
nativeLanguage: '{msg} ภาษาต้นทาง',
|
||||
copy: 'คัดลอก',
|
||||
paste: 'วาง',
|
||||
period: 'ช่วงเวลา',
|
||||
documentStatus: 'สถานะเอกสาร',
|
||||
advanceSearch: 'ค้นหาขั้นสูง',
|
||||
totalPeople: '{meg} คน',
|
||||
},
|
||||
|
||||
menu: {
|
||||
|
|
@ -249,6 +255,7 @@ export default {
|
|||
manual: {
|
||||
title: 'คู่มือ',
|
||||
usage: 'การใช้งาน',
|
||||
troubleshooting: 'การแก้ปัญหา',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -456,10 +463,12 @@ export default {
|
|||
citizenId: 'เลขที่บัตรประชาชน',
|
||||
citizenIssue: 'วันที่ออกบัตร',
|
||||
citizenExpire: 'วันที่หมดอายุ',
|
||||
agencyStatus: 'สถานะการเป็นเอเจนซี่',
|
||||
agencyStatus: 'สถานะเอเจนซี่',
|
||||
normal: 'ปกติ',
|
||||
canceled: 'ยกเลิก',
|
||||
blacklist: 'แบล็คลิสต์',
|
||||
contactName: 'ชื่อผู้ติดต่อ',
|
||||
contactTel: 'เบอร์โทรศัพท์ผู้ติดต่อ',
|
||||
},
|
||||
},
|
||||
customer: {
|
||||
|
|
@ -932,6 +941,7 @@ export default {
|
|||
nonLocalEmployee: 'พนักงานนอกพื้นที่',
|
||||
noWorkflowTemplate: 'คุณไม่ได้เลือกแม่แบบขั้นตอนการทำงาน',
|
||||
salesRepresentative: 'พนักงานขาย',
|
||||
dataOffice: 'สำนักงานเขตจัดหางาน',
|
||||
ref: 'อ้างอิง',
|
||||
action: {
|
||||
title: 'จัดการ',
|
||||
|
|
@ -1050,6 +1060,9 @@ export default {
|
|||
confirmDebitNoteAccept: 'ยืนยันการตอบรับใบเพิ่มหนี้',
|
||||
},
|
||||
message: {
|
||||
copy: 'คัดลอก',
|
||||
warningPaste: 'คุณต้องการที่จะเเทนที่ข้อมูลที่คัดลอกมาใหม่ใช่หรือไม่',
|
||||
warningCopyEmpty: 'คุณยังไม่ได้คัดลอกข้อมูล',
|
||||
quotationAccept: 'เมื่อตอบรับเเล้วจะไม่สามารถแก้ไขได้อีก',
|
||||
beingUse: '"{msg}" มีการใช้งานอยู่',
|
||||
incompleteDataEntry: 'กรอกข้อมูลไม่ครบในหน้า {tap}',
|
||||
|
|
@ -1468,4 +1481,19 @@ export default {
|
|||
type: 'ประเภท',
|
||||
},
|
||||
},
|
||||
|
||||
dateRange: {
|
||||
today: 'วันนี้',
|
||||
yesterday: 'เมื่อวานนี้',
|
||||
thisWeek: 'สัปดาห์นี้',
|
||||
lastWeek: 'สัปดาห์ที่แล้ว',
|
||||
thisMonth: 'เดือนนี้',
|
||||
lastMonth: 'เดือนที่แล้ว',
|
||||
thisYear: 'ปีนี้',
|
||||
lastYear: 'ปีที่แล้ว',
|
||||
last7Days: '7 วันที่ผ่านมา',
|
||||
last30Days: '30 วันที่ผ่านมา',
|
||||
last90Days: '90 วันที่ผ่านมา',
|
||||
customDateRange: 'กำหนดช่วงวันที่เอง',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -215,6 +215,10 @@ function initMenu() {
|
|||
label: 'usage',
|
||||
route: '/manual',
|
||||
},
|
||||
{
|
||||
label: 'troubleshooting',
|
||||
route: '/troubleshooting',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
// NOTE: Library
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted } from 'vue';
|
||||
import { onMounted, watch } from 'vue';
|
||||
|
||||
// NOTE: Components
|
||||
|
||||
|
|
@ -10,22 +10,33 @@ import { onMounted } from 'vue';
|
|||
import { useManualStore } from 'src/stores/manual';
|
||||
import { useNavigator } from 'src/stores/navigator';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
// NOTE: Variable
|
||||
const route = useRoute();
|
||||
const manualStore = useManualStore();
|
||||
const navigatorStore = useNavigator();
|
||||
const { dataManual } = storeToRefs(manualStore);
|
||||
|
||||
async function fetchManual() {
|
||||
const res = await manualStore.getManual();
|
||||
dataManual.value = res ? res : [];
|
||||
}
|
||||
const { dataManual, dataTroubleshooting } = storeToRefs(manualStore);
|
||||
|
||||
onMounted(async () => {
|
||||
navigatorStore.current.title = 'menu.manual.title';
|
||||
navigatorStore.current.path = [{ text: '' }];
|
||||
await fetchManual();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.name,
|
||||
async () => {
|
||||
if (route.name === 'Manual') {
|
||||
const res = await manualStore.getManual();
|
||||
dataManual.value = res ? res : [];
|
||||
}
|
||||
if (route.name === 'Troubleshooting') {
|
||||
const res = await manualStore.getTroubleshooting();
|
||||
dataTroubleshooting.value = res ? res : [];
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -34,7 +45,7 @@ onMounted(async () => {
|
|||
>
|
||||
<section class="scroll q-gutter-y-sm">
|
||||
<q-expansion-item
|
||||
v-for="v in dataManual"
|
||||
v-for="v in $route.name === 'Manual' ? dataManual : dataTroubleshooting"
|
||||
:key="v.labelEN"
|
||||
:content-inset-level="0.5"
|
||||
class="rounded overflow-hidden bordered"
|
||||
|
|
@ -58,7 +69,11 @@ onMounted(async () => {
|
|||
clickable
|
||||
dense
|
||||
class="dot items-center rounded q-my-xs"
|
||||
:to="`/manual/${v.category}/${x.name}`"
|
||||
:to="
|
||||
$route.name === 'Manual'
|
||||
? `/manual/${v.category}/${x.name}`
|
||||
: `/troubleshooting/${v.category}/${x.name}`
|
||||
"
|
||||
>
|
||||
<Icon
|
||||
v-if="!!x.icon"
|
||||
|
|
|
|||
|
|
@ -56,14 +56,28 @@ onUnmounted(() => {
|
|||
|
||||
async function getContent() {
|
||||
if (!category.value || !page.value) return;
|
||||
const res = await manualStore.getManualByPage({
|
||||
category: category.value,
|
||||
pageName: page.value,
|
||||
});
|
||||
if (res && res.ok) {
|
||||
const text = await res.text();
|
||||
content.value = text;
|
||||
contentParsed.value = md.parse(text, {});
|
||||
|
||||
if (ROUTE.name === 'ManualView') {
|
||||
const res = await manualStore.getManualByPage({
|
||||
category: category.value,
|
||||
pageName: page.value,
|
||||
});
|
||||
if (res && res.ok) {
|
||||
const text = await res.text();
|
||||
content.value = text;
|
||||
contentParsed.value = md.parse(text, {});
|
||||
}
|
||||
}
|
||||
if (ROUTE.name === 'TroubleshootingView') {
|
||||
const res = await manualStore.getTroubleshootingByPage({
|
||||
category: category.value,
|
||||
pageName: page.value,
|
||||
});
|
||||
if (res && res.ok) {
|
||||
const text = await res.text();
|
||||
content.value = text;
|
||||
contentParsed.value = md.parse(text, {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,7 +198,9 @@ async function scrollTo(id: string) {
|
|||
md.render(
|
||||
content.replaceAll(
|
||||
'assets/',
|
||||
`${baseUrl}/manual/${category}/assets/`,
|
||||
$route.name === 'ManualView'
|
||||
? `${baseUrl}/manual/${category}/assets/`
|
||||
: `${baseUrl}/troubleshooting/${category}/assets/`,
|
||||
),
|
||||
)
|
||||
"
|
||||
|
|
@ -315,4 +331,20 @@ async function scrollTo(id: string) {
|
|||
.markdown :deep(video) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown :deep(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.markdown :deep(:where(table th)) {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.markdown :deep(:where(table td, table th)) {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 0.25rem 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Icon } from '@iconify/vue';
|
|||
import { BranchContact } from 'stores/branch-contact/types';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { QSelect, QTableProps, QTableSlots } from 'quasar';
|
||||
import type { QTableProps, QTableSlots } from 'quasar';
|
||||
import { resetScrollBar } from 'src/stores/utils';
|
||||
import useBranchStore from 'stores/branch';
|
||||
import useFlowStore from 'stores/flow';
|
||||
|
|
@ -52,6 +52,7 @@ import {
|
|||
UndoButton,
|
||||
} from 'components/button';
|
||||
import { useNavigator } from 'src/stores/navigator';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const $q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
|
@ -72,7 +73,6 @@ const typeBranchItem = [
|
|||
color: 'var(--blue-6-hsl)',
|
||||
},
|
||||
];
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
const holdDialog = ref(false);
|
||||
const isSubCreate = ref(false);
|
||||
const columns = [
|
||||
|
|
@ -175,6 +175,8 @@ const qrCodeDialog = ref(false);
|
|||
const qrCodeimageUrl = ref<string>('');
|
||||
const formLastSubBranch = ref<number>(0);
|
||||
|
||||
const searchDate = ref<string[]>([]);
|
||||
|
||||
const branchStore = useBranchStore();
|
||||
const flowStore = useFlowStore();
|
||||
const { locale } = useI18n();
|
||||
|
|
@ -715,12 +717,20 @@ async function fetchList(opts: {
|
|||
tree?: boolean;
|
||||
withHead?: boolean;
|
||||
filter?: 'head' | 'sub';
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
await branchStore.fetchList(opts);
|
||||
}
|
||||
|
||||
watch(inputSearch, () => {
|
||||
fetchList({ tree: true, query: inputSearch.value, withHead: true });
|
||||
watch([inputSearch, searchDate], () => {
|
||||
fetchList({
|
||||
tree: true,
|
||||
query: inputSearch.value,
|
||||
withHead: true,
|
||||
startDate: searchDate.value[0],
|
||||
endDate: searchDate.value[1],
|
||||
});
|
||||
currentSubBranch.value = undefined;
|
||||
});
|
||||
|
||||
|
|
@ -1170,26 +1180,49 @@ watch(currentHq, () => {
|
|||
<template v-slot:prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
|
||||
<AdvanceSearch
|
||||
v-model="searchDate"
|
||||
:active="$q.screen.lt.md && statusFilter !== 'all'"
|
||||
>
|
||||
<div
|
||||
v-if="$q.screen.lt.md"
|
||||
class="q-mt-sm text-weight-medium"
|
||||
>
|
||||
{{ $t('general.status') }}
|
||||
</div>
|
||||
<q-select
|
||||
v-if="$q.screen.lt.md"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
autocomplete="off"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
map-options
|
||||
emit-value
|
||||
:for="'field-select-status'"
|
||||
:options="[
|
||||
{ label: $t('general.all'), value: 'all' },
|
||||
{
|
||||
label: $t('status.ACTIVE'),
|
||||
value: 'statusACTIVE',
|
||||
},
|
||||
{
|
||||
label: $t('status.INACTIVE'),
|
||||
value: 'statusINACTIVE',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
</AdvanceSearch>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-6 justify-end">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-if="$q.screen.gt.sm"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import useOptionStore from 'stores/options';
|
|||
import useAddressStore from 'stores/address';
|
||||
import useMyBranch from 'src/stores/my-branch';
|
||||
import { calculateAge } from 'src/utils/datetime';
|
||||
import { QSelect, useQuasar, type QTableProps } from 'quasar';
|
||||
import { dialog, baseUrl } from 'stores/utils';
|
||||
import { useQuasar, type QTableProps } from 'quasar';
|
||||
import { dialog, baseUrl, setPrefixName } from 'stores/utils';
|
||||
import { useNavigator } from 'src/stores/navigator';
|
||||
import { isRoleInclude, resetScrollBar } from 'src/stores/utils';
|
||||
import { BranchUserStats } from 'stores/branch/types';
|
||||
|
|
@ -49,6 +49,7 @@ import FormPerson from 'components/02_personnel-management/FormPerson.vue';
|
|||
import FormByType from 'components/02_personnel-management/FormByType.vue';
|
||||
import FormInformation from 'components/02_personnel-management/FormInformation.vue';
|
||||
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const { locale, t } = useI18n();
|
||||
const $q = useQuasar();
|
||||
|
|
@ -73,7 +74,6 @@ const isImageEdit = ref(false);
|
|||
const imageDialog = ref(false);
|
||||
const infoDrawerEdit = ref(false);
|
||||
const refreshImageState = ref(false);
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
const firstScroll = ref(false);
|
||||
|
||||
const inputSearch = ref('');
|
||||
|
|
@ -93,12 +93,14 @@ const currentUser = ref<User>();
|
|||
const userCode = ref<string>();
|
||||
|
||||
const statusToggle = ref(true);
|
||||
const agencyFile = ref<File[]>([]);
|
||||
const agencyFileList = ref<{ name: string; url: string }[]>([]);
|
||||
const userFile = ref<File[]>([]);
|
||||
const userFileList = ref<{ name: string; url: string }[]>([]);
|
||||
|
||||
const typeStats = ref<UserTypeStats>();
|
||||
const userStats = ref<BranchUserStats[]>();
|
||||
|
||||
const searchDate = ref<[]>([]);
|
||||
|
||||
const urlProfile = ref<string>();
|
||||
const profileFileImg = ref<File | null>(null);
|
||||
const imageList = ref<{ selectedImage: string; list: string[] }>();
|
||||
|
|
@ -124,7 +126,7 @@ const defaultFormData = {
|
|||
streetEN: '',
|
||||
street: '',
|
||||
trainingPlace: null,
|
||||
importNationality: null,
|
||||
importNationality: [],
|
||||
sourceNationality: null,
|
||||
licenseExpireDate: null,
|
||||
licenseIssueDate: null,
|
||||
|
|
@ -151,8 +153,10 @@ const defaultFormData = {
|
|||
citizenExpire: null,
|
||||
citizenIssue: null,
|
||||
citizenId: '',
|
||||
contactName: '',
|
||||
contactTel: '',
|
||||
remark: '',
|
||||
agencyStatus: null,
|
||||
agencyStatus: '',
|
||||
};
|
||||
|
||||
const formData = ref<UserCreate>({
|
||||
|
|
@ -174,7 +178,7 @@ const formData = ref<UserCreate>({
|
|||
streetEN: '',
|
||||
street: '',
|
||||
trainingPlace: null,
|
||||
importNationality: null,
|
||||
importNationality: [],
|
||||
sourceNationality: null,
|
||||
licenseExpireDate: null,
|
||||
licenseIssueDate: null,
|
||||
|
|
@ -201,8 +205,10 @@ const formData = ref<UserCreate>({
|
|||
citizenExpire: null,
|
||||
citizenIssue: null,
|
||||
citizenId: '',
|
||||
contactName: '',
|
||||
contactTel: '',
|
||||
remark: '',
|
||||
agencyStatus: null,
|
||||
agencyStatus: '',
|
||||
});
|
||||
|
||||
const fieldSelectedOption = ref<{ label: string; value: string }[]>([
|
||||
|
|
@ -331,7 +337,7 @@ function onClose(excludeDialog?: boolean) {
|
|||
urlProfile.value = '';
|
||||
profileFileImg.value = null;
|
||||
infoDrawerEdit.value = false;
|
||||
agencyFile.value = [];
|
||||
userFile.value = [];
|
||||
isEdit.value = false;
|
||||
statusToggle.value = true;
|
||||
isImageEdit.value = false;
|
||||
|
|
@ -340,6 +346,8 @@ function onClose(excludeDialog?: boolean) {
|
|||
mapUserType(currentTab.value);
|
||||
imageList.value = { selectedImage: '', list: [] };
|
||||
onCreateImageList.value = { selectedImage: '', list: [] };
|
||||
userFileList.value = [];
|
||||
userFile.value = [];
|
||||
flowStore.rotate();
|
||||
}
|
||||
|
||||
|
|
@ -360,12 +368,10 @@ async function openDialog(
|
|||
isEdit.value = true;
|
||||
await assignFormData(id);
|
||||
|
||||
if (formData.value.userType === 'AGENCY') {
|
||||
const result = await userStore.fetchAttachment(id);
|
||||
const result = await userStore.fetchAttachment(id);
|
||||
|
||||
if (result) {
|
||||
agencyFileList.value = result;
|
||||
}
|
||||
if (result) {
|
||||
userFileList.value = result;
|
||||
}
|
||||
}
|
||||
if (userStore.userOption.hqOpts.length !== 0 && !id) {
|
||||
|
|
@ -429,10 +435,9 @@ async function onSubmit(excludeDialog?: boolean) {
|
|||
|
||||
await userStore.editById(currentUser.value.id, formDataEdit);
|
||||
|
||||
if (currentUser.value.id && formDataEdit.userType === 'AGENCY') {
|
||||
if (!agencyFile.value) return;
|
||||
if (userFile.value) {
|
||||
const payload: UserAttachmentCreate = {
|
||||
file: agencyFile.value,
|
||||
file: userFile.value,
|
||||
};
|
||||
|
||||
if (payload?.file) {
|
||||
|
|
@ -461,11 +466,9 @@ async function onSubmit(excludeDialog?: boolean) {
|
|||
onCreateImageList.value,
|
||||
);
|
||||
|
||||
if (result && formData.value.userType === 'AGENCY') {
|
||||
if (!agencyFile.value) return;
|
||||
|
||||
if (userFile.value && result) {
|
||||
const payload: UserAttachmentCreate = {
|
||||
file: agencyFile.value,
|
||||
file: userFile.value,
|
||||
};
|
||||
|
||||
if (payload?.file) {
|
||||
|
|
@ -552,6 +555,7 @@ async function triggerChangeStatus(id: string, status: string) {
|
|||
async function assignFormData(idEdit: string) {
|
||||
if (!userData.value) return;
|
||||
const foundUser = userData.value.result.find((user) => user.id === idEdit);
|
||||
console.log(foundUser);
|
||||
|
||||
if (foundUser) {
|
||||
currentUser.value = foundUser;
|
||||
|
|
@ -573,7 +577,10 @@ async function assignFormData(idEdit: string) {
|
|||
street: foundUser.street,
|
||||
streetEN: foundUser.streetEN,
|
||||
trainingPlace: foundUser.trainingPlace,
|
||||
importNationality: foundUser.importNationality,
|
||||
importNationality:
|
||||
typeof foundUser.importNationality === 'string'
|
||||
? [foundUser.importNationality]
|
||||
: foundUser.importNationality,
|
||||
sourceNationality: foundUser.sourceNationality,
|
||||
licenseNo: foundUser.licenseNo,
|
||||
discountCondition: foundUser.discountCondition,
|
||||
|
|
@ -593,6 +600,8 @@ async function assignFormData(idEdit: string) {
|
|||
responsibleArea: foundUser.responsibleArea,
|
||||
status: foundUser.status,
|
||||
selectedImage: foundUser.selectedImage,
|
||||
contactName: foundUser.contactName,
|
||||
contactTel: foundUser.contactTel,
|
||||
licenseExpireDate:
|
||||
(foundUser.licenseExpireDate &&
|
||||
new Date(foundUser.licenseExpireDate)) ||
|
||||
|
|
@ -669,6 +678,8 @@ async function fetchUserList(mobileFetch?: boolean) {
|
|||
: statusFilter.value === 'statusACTIVE'
|
||||
? 'ACTIVE'
|
||||
: 'INACTIVE',
|
||||
startDate: searchDate.value[0],
|
||||
endDate: searchDate.value[1],
|
||||
});
|
||||
|
||||
if (ret) {
|
||||
|
|
@ -743,11 +754,11 @@ watch(
|
|||
formData.value.responsibleArea = null;
|
||||
formData.value.discountCondition = null;
|
||||
formData.value.sourceNationality = null;
|
||||
formData.value.importNationality = null;
|
||||
formData.value.importNationality = [];
|
||||
formData.value.trainingPlace = null;
|
||||
formData.value.checkpoint = null;
|
||||
formData.value.checkpointEN = null;
|
||||
agencyFile.value = [];
|
||||
userFile.value = [];
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -758,7 +769,7 @@ watch(
|
|||
},
|
||||
);
|
||||
|
||||
watch([inputSearch, statusFilter, pageSize], async () => {
|
||||
watch([inputSearch, statusFilter, pageSize, searchDate], async () => {
|
||||
if (userData.value) userData.value.result = [];
|
||||
currentPage.value = 1;
|
||||
|
||||
|
|
@ -880,26 +891,45 @@ watch(
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch
|
||||
v-model="searchDate"
|
||||
:active="$q.screen.lt.md && statusFilter !== 'all'"
|
||||
>
|
||||
<div
|
||||
v-if="$q.screen.lt.md"
|
||||
class="q-mt-sm text-weight-medium"
|
||||
>
|
||||
{{ $t('general.status') }}
|
||||
</div>
|
||||
<q-select
|
||||
v-if="$q.screen.lt.md"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
map-options
|
||||
emit-value
|
||||
autocomplete="off"
|
||||
:for="'field-select-status'"
|
||||
:options="[
|
||||
{ label: $t('general.all'), value: 'all' },
|
||||
{ label: $t('general.active'), value: 'statusACTIVE' },
|
||||
{
|
||||
label: $t('general.inactive'),
|
||||
value: 'statusINACTIVE',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
</AdvanceSearch>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-if="$q.screen.gt.sm"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -1559,7 +1589,18 @@ watch(
|
|||
v-model:toggle-status="formData.status"
|
||||
hideFade
|
||||
:toggle-title="$t('status.title')"
|
||||
:title="`${locale === 'eng' ? `${formData.firstNameEN} ${formData.lastNameEN}` : `${formData.firstName} ${formData.lastName}`}`"
|
||||
:title="
|
||||
setPrefixName(
|
||||
{
|
||||
namePrefix: formData.namePrefix,
|
||||
firstName: formData.firstName,
|
||||
lastName: formData.lastName,
|
||||
firstNameEN: formData.firstNameEN,
|
||||
lastNameEN: formData.lastNameEN,
|
||||
},
|
||||
{ locale },
|
||||
)
|
||||
"
|
||||
:caption="userCode"
|
||||
:img="
|
||||
`${baseUrl}/user/${currentUser.id}/profile-image/${formData.selectedImage}`.concat(
|
||||
|
|
@ -1744,12 +1785,15 @@ watch(
|
|||
v-model:citizen-id="formData.citizenId"
|
||||
v-model:citizen-issue="formData.citizenIssue"
|
||||
v-model:citizen-expire="formData.citizenExpire"
|
||||
v-model:contact-name="formData.contactName"
|
||||
v-model:contact-tel="formData.contactTel"
|
||||
:title="'personnel.form.personalInformation'"
|
||||
prefix-id="drawer-info-personnel"
|
||||
dense
|
||||
outlined
|
||||
separator
|
||||
:readonly="!infoDrawerEdit"
|
||||
:agency="formData.userType === 'AGENCY'"
|
||||
class="q-mb-xl"
|
||||
/>
|
||||
|
||||
|
|
@ -1789,8 +1833,8 @@ watch(
|
|||
v-model:import-nationality="formData.importNationality"
|
||||
v-model:training-place="formData.trainingPlace"
|
||||
v-model:checkpoint="formData.checkpoint"
|
||||
v-model:agency-file="agencyFile"
|
||||
v-model:agency-file-list="agencyFileList"
|
||||
v-model:user-file="userFile"
|
||||
v-model:user-file-list="userFileList"
|
||||
v-model:user-id="currentUser.id"
|
||||
v-model:remark="formData.remark"
|
||||
v-model:agency-status="formData.agencyStatus"
|
||||
|
|
@ -1837,7 +1881,18 @@ watch(
|
|||
}[formData.gender]
|
||||
"
|
||||
:toggleTitle="$t('status.title')"
|
||||
:title="`${locale === 'eng' ? `${formData.firstNameEN} ${formData.lastNameEN}` : `${formData.firstName} ${formData.lastName}`}`"
|
||||
:title="
|
||||
setPrefixName(
|
||||
{
|
||||
namePrefix: formData.namePrefix,
|
||||
firstName: formData.firstName,
|
||||
lastName: formData.lastName,
|
||||
firstNameEN: formData.firstNameEN,
|
||||
lastNameEN: formData.lastNameEN,
|
||||
},
|
||||
{ locale },
|
||||
)
|
||||
"
|
||||
:fallbackImg="
|
||||
{
|
||||
male: '/no-img-man.png',
|
||||
|
|
@ -1948,6 +2003,7 @@ watch(
|
|||
id="dialog-form-personal"
|
||||
prefix-id="form-dialog-personnel"
|
||||
dense
|
||||
:agency="formData.userType === 'AGENCY'"
|
||||
outlined
|
||||
separator
|
||||
:title="'personnel.form.personalInformation'"
|
||||
|
|
@ -1966,6 +2022,8 @@ watch(
|
|||
v-model:citizen-id="formData.citizenId"
|
||||
v-model:citizen-issue="formData.citizenIssue"
|
||||
v-model:citizen-expire="formData.citizenExpire"
|
||||
v-model:contact-name="formData.contactName"
|
||||
v-model:contact-tel="formData.contactTel"
|
||||
class="q-mb-xl"
|
||||
/>
|
||||
<AddressForm
|
||||
|
|
@ -2003,7 +2061,8 @@ watch(
|
|||
v-model:checkpoint="formData.checkpoint"
|
||||
v-model:agency-status="formData.agencyStatus"
|
||||
v-model:remark="formData.remark"
|
||||
v-model:agency-file="agencyFile"
|
||||
v-model:user-file="userFile"
|
||||
v-model:user-file-list="userFileList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { QSelect, useQuasar } from 'quasar';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getUserId, getRole } from 'src/services/keycloak';
|
||||
import { baseUrl, waitAll } from 'src/stores/utils';
|
||||
import { baseUrl, setPrefixName, waitAll } from 'src/stores/utils';
|
||||
import { dateFormat } from 'src/utils/datetime';
|
||||
import { dialogCheckData } from 'stores/utils';
|
||||
|
||||
|
|
@ -86,6 +86,7 @@ import { nextTick } from 'vue';
|
|||
import FormEmployeeVisa from 'components/03_customer-management/FormEmployeeVisa.vue';
|
||||
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||
import { AddButton } from 'components/button';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const $q = useQuasar();
|
||||
|
|
@ -101,7 +102,6 @@ const employeeFormStore = useEmployeeForm();
|
|||
const optionStore = useOptionStore();
|
||||
const ocrStore = useOcrStore();
|
||||
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
const mrz = ref<Awaited<ReturnType<typeof parseResultMRZ>>>();
|
||||
|
||||
const {
|
||||
|
|
@ -142,6 +142,14 @@ async function init() {
|
|||
|
||||
gridView.value = $q.screen.lt.md ? true : false;
|
||||
|
||||
if (route.query.tab === 'customer') {
|
||||
currentTab.value = 'employer';
|
||||
if (route.query.id) openSpecificCustomer(route.query.id as string);
|
||||
} else if (route.query.tab === 'employee') {
|
||||
currentTab.value = 'employee';
|
||||
if (route.query.id) openSpecificEmployee(route.query.id as string);
|
||||
}
|
||||
|
||||
if (route.name === 'CustomerManagement') await fetchListCustomer(true);
|
||||
|
||||
if (
|
||||
|
|
@ -181,6 +189,7 @@ const statsCustomerType = ref<CustomerStats>({
|
|||
});
|
||||
|
||||
// NOTE: Page State
|
||||
const searchDate = ref<string[]>([]);
|
||||
const currentTab = ref<'employer' | 'employee'>('employer');
|
||||
const inputSearch = ref('');
|
||||
const currentStatus = ref<Status | 'All'>('All');
|
||||
|
|
@ -218,8 +227,16 @@ const dialogEmployeeImageUpload = ref<InstanceType<typeof ImageUploadDialog>>();
|
|||
const imageList = ref<{ selectedImage: string; list: string[] }>();
|
||||
watch(() => route.name, init);
|
||||
watch(
|
||||
[currentTab, currentStatus, inputSearch, customerTypeSelected, pageSize],
|
||||
async ([tabName]) => {
|
||||
[
|
||||
currentTab,
|
||||
currentStatus,
|
||||
inputSearch,
|
||||
customerTypeSelected,
|
||||
pageSize,
|
||||
searchDate,
|
||||
],
|
||||
async ([tabName], [oldTabName]) => {
|
||||
// if (tabName !== oldTabName) searchDate.value = [];
|
||||
if (tabName === 'employer') {
|
||||
currentPageCustomer.value = 1;
|
||||
currentBtnOpen.value = [];
|
||||
|
|
@ -309,6 +326,8 @@ async function fetchListCustomer(fetchStats = false, mobileFetch?: boolean) {
|
|||
? 'ACTIVE'
|
||||
: 'INACTIVE',
|
||||
query: inputSearch.value,
|
||||
startDate: searchDate.value[0],
|
||||
endDate: searchDate.value[1],
|
||||
customerType: (
|
||||
{
|
||||
all: undefined,
|
||||
|
|
@ -362,6 +381,8 @@ async function fetchListEmployee(opt?: {
|
|||
query: inputSearch.value,
|
||||
passport: true,
|
||||
visa: true,
|
||||
startDate: searchDate.value[0],
|
||||
endDate: searchDate.value[1],
|
||||
});
|
||||
if (resultListEmployee) {
|
||||
maxPageEmployee.value = Math.ceil(
|
||||
|
|
@ -490,6 +511,30 @@ async function fetchImageList(
|
|||
return res;
|
||||
}
|
||||
|
||||
async function openSpecificCustomer(id: string) {
|
||||
await customerFormStore.assignFormData(id);
|
||||
await fetchImageList(
|
||||
id,
|
||||
customerFormData.value.selectedImage || '',
|
||||
'customer',
|
||||
);
|
||||
customerFormState.value.branchIndex = -1;
|
||||
customerFormState.value.drawerModal = true;
|
||||
customerFormState.value.editCustomerId = id;
|
||||
customerFormState.value.dialogType = 'info';
|
||||
}
|
||||
|
||||
async function openSpecificEmployee(id: string) {
|
||||
await employeeFormStore.assignFormDataEmployee(id);
|
||||
await fetchImageList(
|
||||
id,
|
||||
currentFromDataEmployee.value.selectedImage || '',
|
||||
'employee',
|
||||
);
|
||||
employeeFormState.value.dialogType = 'info';
|
||||
employeeFormState.value.drawerModal = true;
|
||||
}
|
||||
|
||||
// TODO: When in employee form, if select address same as customer then auto fill
|
||||
|
||||
watch(
|
||||
|
|
@ -743,26 +788,43 @@ const emptyCreateDialog = ref(false);
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch
|
||||
v-model="searchDate"
|
||||
:active="$q.screen.lt.md && currentStatus !== 'All'"
|
||||
>
|
||||
<div
|
||||
v-if="$q.screen.lt.md"
|
||||
class="q-mt-sm text-weight-medium"
|
||||
>
|
||||
{{ $t('general.status') }}
|
||||
</div>
|
||||
<q-select
|
||||
v-if="$q.screen.lt.md"
|
||||
id="select-status"
|
||||
for="select-status"
|
||||
v-model="currentStatus"
|
||||
outlined
|
||||
dense
|
||||
autocomplete="off"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
map-options
|
||||
emit-value
|
||||
:options="[
|
||||
{ label: $t('general.all'), value: 'All' },
|
||||
{ label: $t('status.ACTIVE'), value: 'ACTIVE' },
|
||||
{ label: $t('status.INACTIVE'), value: 'INACTIVE' },
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
</AdvanceSearch>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-if="$q.screen.gt.sm"
|
||||
id="select-status"
|
||||
for="select-status"
|
||||
v-model="currentStatus"
|
||||
|
|
@ -2004,7 +2066,16 @@ const emptyCreateDialog = ref(false);
|
|||
"
|
||||
:title="
|
||||
customerFormData.customerType === 'PERS'
|
||||
? `${customerFormData.customerBranch[0]?.firstName} ${customerFormData.customerBranch[0]?.lastName}`
|
||||
? setPrefixName(
|
||||
{
|
||||
namePrefix: customerFormData.customerBranch[0]?.namePrefix,
|
||||
firstName: customerFormData.customerBranch[0]?.firstName,
|
||||
lastName: customerFormData.customerBranch[0]?.lastName,
|
||||
firstNameEN: customerFormData.customerBranch[0]?.firstNameEN,
|
||||
lastNameEN: customerFormData.customerBranch[0]?.lastNameEN,
|
||||
},
|
||||
{ locale },
|
||||
)
|
||||
: customerFormData.customerBranch[0]?.registerName
|
||||
"
|
||||
:caption="
|
||||
|
|
@ -2113,16 +2184,16 @@ const emptyCreateDialog = ref(false);
|
|||
id="form-basic-info-customer"
|
||||
:onCreate="customerFormState.dialogType === 'create'"
|
||||
@edit="
|
||||
(customerFormState.dialogType = 'edit'),
|
||||
(customerFormState.readonly = false)
|
||||
((customerFormState.dialogType = 'edit'),
|
||||
(customerFormState.readonly = false))
|
||||
"
|
||||
@cancel="() => customerFormUndo(false)"
|
||||
@delete="
|
||||
customerFormState.editCustomerId &&
|
||||
deleteCustomerById(
|
||||
customerFormState.editCustomerId,
|
||||
async () => await fetchListCustomer(true, $q.screen.xs),
|
||||
)
|
||||
deleteCustomerById(
|
||||
customerFormState.editCustomerId,
|
||||
async () => await fetchListCustomer(true, $q.screen.xs),
|
||||
)
|
||||
"
|
||||
:customer-type="customerFormData.customerType"
|
||||
v-model:registered-branch-id="customerFormData.registeredBranchId"
|
||||
|
|
@ -2452,6 +2523,20 @@ const emptyCreateDialog = ref(false);
|
|||
"
|
||||
:toggleTitle="$t('status.title')"
|
||||
hideFade
|
||||
:title="
|
||||
currentFromDataEmployee
|
||||
? setPrefixName(
|
||||
{
|
||||
namePrefix: currentFromDataEmployee.namePrefix,
|
||||
firstName: currentFromDataEmployee.firstName,
|
||||
lastName: currentFromDataEmployee.lastName,
|
||||
firstNameEN: currentFromDataEmployee.firstNameEN,
|
||||
lastNameEN: currentFromDataEmployee.lastNameEN,
|
||||
},
|
||||
{ locale },
|
||||
)
|
||||
: '-'
|
||||
"
|
||||
@view="
|
||||
() => {
|
||||
employeeFormState.imageDialog = true;
|
||||
|
|
@ -4052,7 +4137,17 @@ const emptyCreateDialog = ref(false);
|
|||
"
|
||||
:title="
|
||||
customerFormData.customerType === 'PERS'
|
||||
? `${customerFormData.customerBranch[0]?.firstName} ${customerFormData.customerBranch[0]?.lastName}`
|
||||
? setPrefixName(
|
||||
{
|
||||
namePrefix: customerFormData.customerBranch[0]?.namePrefix,
|
||||
firstName: customerFormData.customerBranch[0]?.firstName,
|
||||
lastName: customerFormData.customerBranch[0]?.lastName,
|
||||
firstNameEN:
|
||||
customerFormData.customerBranch[0]?.firstNameEN,
|
||||
lastNameEN: customerFormData.customerBranch[0]?.lastNameEN,
|
||||
},
|
||||
{ locale },
|
||||
)
|
||||
: customerFormData.customerBranch[0]?.registerName
|
||||
"
|
||||
:caption="
|
||||
|
|
@ -4166,16 +4261,16 @@ const emptyCreateDialog = ref(false);
|
|||
id="form-basic-info-customer"
|
||||
:onCreate="customerFormState.dialogType === 'create'"
|
||||
@edit="
|
||||
(customerFormState.dialogType = 'edit'),
|
||||
(customerFormState.readonly = false)
|
||||
((customerFormState.dialogType = 'edit'),
|
||||
(customerFormState.readonly = false))
|
||||
"
|
||||
@cancel="() => customerFormUndo(false)"
|
||||
@delete="
|
||||
customerFormState.editCustomerId &&
|
||||
deleteCustomerById(
|
||||
customerFormState.editCustomerId,
|
||||
async () => await fetchListCustomer(true, $q.screen.xs),
|
||||
)
|
||||
deleteCustomerById(
|
||||
customerFormState.editCustomerId,
|
||||
async () => await fetchListCustomer(true, $q.screen.xs),
|
||||
)
|
||||
"
|
||||
:customer-type="customerFormData.customerType"
|
||||
v-model:registered-branch-id="customerFormData.registeredBranchId"
|
||||
|
|
@ -4475,9 +4570,16 @@ const emptyCreateDialog = ref(false);
|
|||
fallback-cover="/images/employee-banner.png"
|
||||
:title="
|
||||
employeeFormState.currentEmployee
|
||||
? $i18n.locale === 'eng'
|
||||
? `${employeeFormState.currentEmployee.firstNameEN} ${employeeFormState.currentEmployee.lastNameEN}`
|
||||
: `${employeeFormState.currentEmployee.firstName} ${employeeFormState.currentEmployee.lastName}`
|
||||
? setPrefixName(
|
||||
{
|
||||
namePrefix: employeeFormState.currentEmployee.namePrefix,
|
||||
firstName: employeeFormState.currentEmployee.firstName,
|
||||
lastName: employeeFormState.currentEmployee.lastName,
|
||||
firstNameEN: employeeFormState.currentEmployee.firstNameEN,
|
||||
lastNameEN: employeeFormState.currentEmployee.lastNameEN,
|
||||
},
|
||||
{ locale },
|
||||
)
|
||||
: '-'
|
||||
"
|
||||
:caption="currentFromDataEmployee.code"
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ async function addStep() {
|
|||
flowData.value.step.push({
|
||||
responsibleInstitution: [],
|
||||
responsiblePersonId: [],
|
||||
responsibleGroup: [],
|
||||
value: [],
|
||||
detail: '',
|
||||
name: '',
|
||||
|
|
@ -166,6 +167,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
|
|||
id="flow-form-dialog"
|
||||
>
|
||||
<FormFlow
|
||||
v-model:user-in-table="userInTable"
|
||||
v-model:flow-data="flowData"
|
||||
v-model:register-branch-id="registerBranchId"
|
||||
@trigger-properties="triggerPropertiesDialog"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { QSelect, QTableProps } from 'quasar';
|
||||
import { QTableProps } from 'quasar';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
|
|
@ -22,6 +22,7 @@ import NoData from 'src/components/NoData.vue';
|
|||
import KebabAction from 'src/components/shared/KebabAction.vue';
|
||||
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const workflowStore = useWorkflowTemplate();
|
||||
|
|
@ -45,6 +46,7 @@ const pageState = reactive({
|
|||
addModal: false,
|
||||
viewDrawer: false,
|
||||
isDrawerEdit: true,
|
||||
searchDate: [],
|
||||
});
|
||||
|
||||
const fieldSelected = ref<('order' | 'name' | 'step')[]>([
|
||||
|
|
@ -68,7 +70,6 @@ const fieldSelectedOption = ref<{ label: string; value: string }[]>([
|
|||
},
|
||||
]);
|
||||
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
const currWorkflowData = ref<WorkflowTemplate>();
|
||||
const formDataWorkflow = ref<WorkflowTemplatePayload>({
|
||||
status: 'CREATED',
|
||||
|
|
@ -102,6 +103,7 @@ const columns = [
|
|||
function triggerDialog(type: 'add' | 'edit' | 'view') {
|
||||
if (type === 'add') {
|
||||
registeredBranchId.value = '';
|
||||
userInTable.value = [];
|
||||
formDataWorkflow.value = {
|
||||
status: 'CREATED',
|
||||
name: '',
|
||||
|
|
@ -206,7 +208,7 @@ async function submit() {
|
|||
...formDataWorkflow.value,
|
||||
});
|
||||
} else {
|
||||
await workflowStore.creatWorkflowTemplate({
|
||||
await workflowStore.createWorkflowTemplate({
|
||||
registeredBranchId: registeredBranchId.value,
|
||||
...formDataWorkflow.value,
|
||||
});
|
||||
|
|
@ -222,7 +224,11 @@ function assignFormData(workflowData: WorkflowTemplate) {
|
|||
status: workflowData.status,
|
||||
name: workflowData.name,
|
||||
step: workflowData.step.map((s, i) => {
|
||||
userInTable.value[i] = { name: s.name, responsiblePerson: [] };
|
||||
userInTable.value[i] = {
|
||||
name: s.name,
|
||||
responsiblePerson: [],
|
||||
responsibleGroup: [],
|
||||
};
|
||||
s.responsiblePerson.forEach((p) => {
|
||||
userInTable.value[i].responsiblePerson.push({
|
||||
id: p.user.id,
|
||||
|
|
@ -236,12 +242,16 @@ function assignFormData(workflowData: WorkflowTemplate) {
|
|||
code: p.user.code,
|
||||
});
|
||||
});
|
||||
s.responsibleGroup.forEach((g) => {
|
||||
userInTable.value[i].responsibleGroup.push(g);
|
||||
});
|
||||
return {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
detail: s.detail,
|
||||
messengerByArea: s.messengerByArea || false,
|
||||
value: s.value.length > 0 ? JSON.parse(JSON.stringify(s.value)) : [],
|
||||
responsibleGroup: s.responsibleGroup.map((g) => g),
|
||||
responsiblePersonId: s.responsiblePerson.map((p) => p.userId),
|
||||
responsibleInstitution: JSON.parse(
|
||||
JSON.stringify(s.responsibleInstitution),
|
||||
|
|
@ -282,6 +292,8 @@ async function fetchWorkflowList(mobileFetch?: boolean) {
|
|||
: statusFilter.value === 'statusACTIVE'
|
||||
? 'ACTIVE'
|
||||
: 'INACTIVE',
|
||||
startDate: pageState.searchDate[0],
|
||||
endDate: pageState.searchDate[1],
|
||||
});
|
||||
if (res) {
|
||||
workflowData.value =
|
||||
|
|
@ -311,11 +323,14 @@ watch(
|
|||
fetchWorkflowList();
|
||||
},
|
||||
);
|
||||
watch([() => pageState.inputSearch, workflowPageSize], () => {
|
||||
workflowData.value = [];
|
||||
workflowPage.value = 1;
|
||||
fetchWorkflowList();
|
||||
});
|
||||
watch(
|
||||
[() => pageState.inputSearch, workflowPageSize, () => pageState.searchDate],
|
||||
() => {
|
||||
workflowData.value = [];
|
||||
workflowPage.value = 1;
|
||||
fetchWorkflowList();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<FloatingActionButton
|
||||
|
|
@ -392,26 +407,44 @@ watch([() => pageState.inputSearch, workflowPageSize], () => {
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch
|
||||
v-model="pageState.searchDate"
|
||||
:active="$q.screen.lt.md && statusFilter !== 'all'"
|
||||
>
|
||||
<div
|
||||
v-if="$q.screen.lt.md"
|
||||
class="q-mt-sm text-weight-medium"
|
||||
>
|
||||
{{ $t('general.status') }}
|
||||
</div>
|
||||
<q-select
|
||||
v-if="$q.screen.lt.md"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
map-options
|
||||
emit-value
|
||||
:for="'field-select-status'"
|
||||
:options="[
|
||||
{ label: $t('general.all'), value: 'all' },
|
||||
{ label: $t('general.active'), value: 'statusACTIVE' },
|
||||
{
|
||||
label: $t('general.inactive'),
|
||||
value: 'statusINACTIVE',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
</AdvanceSearch>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-if="$q.screen.gt.sm"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -509,12 +542,12 @@ watch([() => pageState.inputSearch, workflowPageSize], () => {
|
|||
class="col surface-2 flex items-center justify-center"
|
||||
>
|
||||
<NoData
|
||||
v-if="pageState.total !== 0"
|
||||
v-if="pageState.total !== 0 || pageState.searchDate.length > 0"
|
||||
:not-found="!!pageState.inputSearch"
|
||||
/>
|
||||
|
||||
<CreateButton
|
||||
v-if="pageState.total === 0"
|
||||
v-if="pageState.total === 0 && pageState.searchDate.length === 0"
|
||||
@click="triggerDialog('add')"
|
||||
label="general.add"
|
||||
:i18n-args="{ text: $t('flow.title') }"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { nextTick, ref, watch, reactive } from 'vue';
|
|||
import { useI18n } from 'vue-i18n';
|
||||
import { onMounted } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { QSelect, useQuasar, type QTableProps } from 'quasar';
|
||||
import { useQuasar, type QTableProps } from 'quasar';
|
||||
|
||||
import DialogProperties from 'src/components/dialog/DialogProperties.vue';
|
||||
import ProductCardComponent from 'components/04_product-service/ProductCardComponent.vue';
|
||||
|
|
@ -33,6 +33,7 @@ import {
|
|||
SaveButton,
|
||||
UndoButton,
|
||||
ToggleButton,
|
||||
ImportButton,
|
||||
} from 'components/button';
|
||||
import TableProduct from 'src/components/04_product-service/TableProduct.vue';
|
||||
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||
|
|
@ -40,7 +41,7 @@ import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
|||
import useFlowStore from 'stores/flow';
|
||||
|
||||
import { dateFormat } from 'src/utils/datetime';
|
||||
import { formatNumberDecimal, isRoleInclude } from 'stores/utils';
|
||||
import { formatNumberDecimal, isRoleInclude, notify } from 'stores/utils';
|
||||
const { getWorkflowTemplate } = useWorkflowTemplate();
|
||||
|
||||
import { Status } from 'stores/types';
|
||||
|
|
@ -67,6 +68,8 @@ import {
|
|||
} from 'src/stores/workflow-template/types';
|
||||
import { useWorkflowTemplate } from 'src/stores/workflow-template';
|
||||
import { deepEquals } from 'src/utils/arr';
|
||||
import { toRaw } from 'vue';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const flowStore = useFlowStore();
|
||||
const navigatorStore = useNavigator();
|
||||
|
|
@ -96,10 +99,13 @@ const {
|
|||
createWork,
|
||||
editWork,
|
||||
deleteWork,
|
||||
|
||||
importProduct,
|
||||
} = productServiceStore;
|
||||
|
||||
const { workNameItems } = storeToRefs(productServiceStore);
|
||||
const allStat = ref<{ mode: string; count: number }[]>([]);
|
||||
|
||||
const stat = ref<
|
||||
{
|
||||
icon: string;
|
||||
|
|
@ -161,8 +167,6 @@ const splitterModel = computed(() =>
|
|||
$q.screen.lt.md ? (productMode.value !== 'group' ? 0 : 100) : 25,
|
||||
);
|
||||
|
||||
const refFilterGroup = ref<InstanceType<typeof QSelect>>();
|
||||
const refFilterProductService = ref<InstanceType<typeof QSelect>>();
|
||||
const holdDialog = ref(false);
|
||||
const imageDialog = ref(false);
|
||||
const currentNode = ref<ProductGroup & { type: string }>();
|
||||
|
|
@ -520,6 +524,7 @@ const currentStatusGroupType = ref<Status>('CREATED');
|
|||
const currentIdGroupType = ref('');
|
||||
|
||||
const currentStatus = ref<Status | 'All'>('All');
|
||||
const searchDate = ref<string[]>([]);
|
||||
|
||||
// img
|
||||
const isImageEdit = ref<boolean>(false);
|
||||
|
|
@ -615,6 +620,8 @@ async function fetchListGroups(mobileFetch?: boolean) {
|
|||
: currentStatus.value === 'ACTIVE'
|
||||
? 'ACTIVE'
|
||||
: 'INACTIVE',
|
||||
startDate: searchDate.value[0],
|
||||
endDate: searchDate.value[1],
|
||||
});
|
||||
|
||||
if (res) {
|
||||
|
|
@ -675,6 +682,8 @@ async function fetchListOfProduct(mobileFetch?: boolean) {
|
|||
? 'ACTIVE'
|
||||
: undefined,
|
||||
productGroupId: currentIdGroup.value,
|
||||
startDate: searchDate.value[0],
|
||||
endDate: searchDate.value[1],
|
||||
});
|
||||
|
||||
if (res) {
|
||||
|
|
@ -720,6 +729,8 @@ async function fetchListOfService(mobileFetch?: boolean) {
|
|||
? 'ACTIVE'
|
||||
: undefined,
|
||||
productGroupId: currentIdGroup.value,
|
||||
startDate: searchDate.value[0],
|
||||
endDate: searchDate.value[1],
|
||||
});
|
||||
|
||||
if (res) {
|
||||
|
|
@ -1590,6 +1601,7 @@ async function enterNext(type: 'service' | 'product') {
|
|||
inputSearchProductAndService.value = '';
|
||||
currentStatus.value = 'All';
|
||||
filterStat.value = [];
|
||||
searchDate.value = [];
|
||||
|
||||
if (
|
||||
expandedTree.value.length > 1 &&
|
||||
|
|
@ -1745,7 +1757,7 @@ watch(currentStatus, async () => {
|
|||
flowStore.rotate();
|
||||
});
|
||||
|
||||
watch(inputSearch, async () => {
|
||||
watch([inputSearch, () => searchDate.value], async () => {
|
||||
if (productMode.value === 'group') {
|
||||
productGroup.value = [];
|
||||
currentPageGroup.value = 1;
|
||||
|
|
@ -1754,7 +1766,7 @@ watch(inputSearch, async () => {
|
|||
}
|
||||
});
|
||||
|
||||
watch(inputSearchProductAndService, async () => {
|
||||
watch([inputSearchProductAndService, () => searchDate.value], async () => {
|
||||
product.value = [];
|
||||
service.value = [];
|
||||
currentPageServiceAndProduct.value = 1;
|
||||
|
|
@ -1831,6 +1843,51 @@ function handleSubmitSameWorkflow() {
|
|||
);
|
||||
}
|
||||
|
||||
async function copy(id: string) {
|
||||
{
|
||||
const res = await fetchListServiceById(id);
|
||||
if (res) {
|
||||
formService.value = {
|
||||
code: res.code.slice(0, -3),
|
||||
name: res.name,
|
||||
detail: res.detail,
|
||||
attributes: res.attributes,
|
||||
work: res.work.map((v) => ({
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
attributes: v.attributes,
|
||||
product: v.productOnWork.map((productOnWorkItem) => ({
|
||||
id: productOnWorkItem.product.id,
|
||||
installmentNo: productOnWorkItem.installmentNo,
|
||||
stepCount: productOnWorkItem.stepCount,
|
||||
})),
|
||||
})),
|
||||
status: res.status,
|
||||
productGroupId: res.productGroupId,
|
||||
selectedImage: res.selectedImage,
|
||||
installments: res.installments,
|
||||
};
|
||||
|
||||
workItems.value = res.work.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
attributes: item.attributes,
|
||||
product: item.productOnWork.map((productOnWorkItem) => {
|
||||
return {
|
||||
...productOnWorkItem.product,
|
||||
nameEn: productOnWorkItem.product.name,
|
||||
installmentNo: productOnWorkItem.installmentNo,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dialogService.value = true;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formService.value.attributes.workflowId,
|
||||
async (a, b) => {
|
||||
|
|
@ -1948,19 +2005,34 @@ watch(
|
|||
<template v-slot:prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilterGroup?.showPopup"
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch
|
||||
v-model="searchDate"
|
||||
:active="$q.screen.lt.md && currentStatus !== 'All'"
|
||||
>
|
||||
<div class="q-mt-sm text-weight-medium">
|
||||
{{ $t('general.status') }}
|
||||
</div>
|
||||
<q-select
|
||||
v-model="currentStatus"
|
||||
for="select-status"
|
||||
outlined
|
||||
dense
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
map-options
|
||||
emit-value
|
||||
:options="[
|
||||
{ label: $t('general.all'), value: 'All' },
|
||||
{ label: $t('general.active'), value: 'ACTIVE' },
|
||||
{
|
||||
label: $t('general.inactive'),
|
||||
value: 'INACTIVE',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
</AdvanceSearch>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
|
@ -2115,26 +2187,44 @@ watch(
|
|||
<template v-slot:prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilterGroup?.showPopup"
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch
|
||||
v-model="searchDate"
|
||||
:active="$q.screen.lt.md && currentStatus !== 'All'"
|
||||
>
|
||||
<div
|
||||
v-if="$q.screen.lt.md"
|
||||
class="q-mt-sm text-weight-medium"
|
||||
>
|
||||
{{ $t('general.status') }}
|
||||
</div>
|
||||
<q-select
|
||||
v-if="$q.screen.lt.md"
|
||||
v-model="currentStatus"
|
||||
for="select-status"
|
||||
outlined
|
||||
dense
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
map-options
|
||||
emit-value
|
||||
:options="[
|
||||
{ label: $t('general.all'), value: 'All' },
|
||||
{ label: $t('general.active'), value: 'ACTIVE' },
|
||||
{
|
||||
label: $t('general.inactive'),
|
||||
value: 'INACTIVE',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
</AdvanceSearch>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-6" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilterGroup"
|
||||
v-model="currentStatus"
|
||||
for="select-status"
|
||||
outlined
|
||||
|
|
@ -2155,7 +2245,6 @@ watch(
|
|||
},
|
||||
]"
|
||||
></q-select>
|
||||
|
||||
<q-select
|
||||
v-if="modeView === false"
|
||||
id="select-field"
|
||||
|
|
@ -2178,7 +2267,6 @@ watch(
|
|||
multiple
|
||||
dense
|
||||
/>
|
||||
|
||||
<q-btn-toggle
|
||||
v-model="modeView"
|
||||
id="btn-mode"
|
||||
|
|
@ -2618,26 +2706,65 @@ watch(
|
|||
<template v-slot:prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilterProductService?.showPopup"
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch
|
||||
v-model="searchDate"
|
||||
:active="$q.screen.lt.md && currentStatus !== 'All'"
|
||||
>
|
||||
<div
|
||||
v-if="$q.screen.lt.md"
|
||||
class="q-mt-sm text-weight-medium"
|
||||
>
|
||||
{{ $t('general.status') }}
|
||||
</div>
|
||||
<q-select
|
||||
v-if="$q.screen.lt.md"
|
||||
:for="'field-select-status'"
|
||||
v-model="currentStatus"
|
||||
outlined
|
||||
dense
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
map-options
|
||||
emit-value
|
||||
:options="[
|
||||
{ label: $t('general.all'), value: 'All' },
|
||||
{ label: $t('general.active'), value: 'ACTIVE' },
|
||||
{
|
||||
label: $t('general.inactive'),
|
||||
value: 'INACTIVE',
|
||||
},
|
||||
]"
|
||||
@update:model-value="fetchStatus()"
|
||||
/>
|
||||
</span>
|
||||
</AdvanceSearch>
|
||||
</template>
|
||||
</q-input>
|
||||
<div
|
||||
class="flex q-mr-auto q-pl-sm"
|
||||
v-if="productAndServiceTab === 'product'"
|
||||
>
|
||||
<input ref="fileImport" type="file" hidden />
|
||||
<ImportButton
|
||||
type="file"
|
||||
import-file
|
||||
icon-only
|
||||
@file-selected="
|
||||
(file) => {
|
||||
importProduct(
|
||||
currentIdGroup,
|
||||
file,
|
||||
async () => await fetchListOfProduct(),
|
||||
);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row col-md-6" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilterProductService"
|
||||
:for="'field-select-status'"
|
||||
v-model="currentStatus"
|
||||
outlined
|
||||
|
|
@ -2659,7 +2786,6 @@ watch(
|
|||
]"
|
||||
@update:model-value="fetchStatus()"
|
||||
></q-select>
|
||||
|
||||
<q-select
|
||||
v-if="modeView === false"
|
||||
:hide-dropdown-icon="$q.screen.lt.sm"
|
||||
|
|
@ -2694,7 +2820,6 @@ watch(
|
|||
multiple
|
||||
dense
|
||||
/>
|
||||
|
||||
<q-btn-toggle
|
||||
v-model="modeView"
|
||||
id="btn-mode"
|
||||
|
|
@ -3127,8 +3252,14 @@ watch(
|
|||
"
|
||||
/>
|
||||
<KebabAction
|
||||
:use-copy="productAndServiceTab === 'service'"
|
||||
:status="props.row.status"
|
||||
:id-name="props.row.name"
|
||||
@copy="
|
||||
() => {
|
||||
copy(props.row.id);
|
||||
}
|
||||
"
|
||||
@view="
|
||||
async () => {
|
||||
if (props.row.type === 'product') {
|
||||
|
|
@ -4422,6 +4553,7 @@ watch(
|
|||
@click="serviceTreeView = false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SaveButton id="btn-info-basic-save" icon-only type="submit" />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { QSelect, QTableProps } from 'quasar';
|
||||
import { QTableProps } from 'quasar';
|
||||
import { useNavigator } from 'src/stores/navigator';
|
||||
import { useProperty } from 'src/stores/property';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
|
@ -17,6 +17,7 @@ import { Property } from 'src/stores/property/types';
|
|||
import { dialog, toCamelCase } from 'src/stores/utils';
|
||||
import CreateButton from 'src/components/AddButton.vue';
|
||||
import useOptionStore from 'stores/options';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const $q = useQuasar();
|
||||
|
|
@ -38,7 +39,6 @@ const formProperty = ref<Property>({
|
|||
type: {},
|
||||
});
|
||||
const statusFilter = ref<'all' | 'statusACTIVE' | 'statusINACTIVE'>('all');
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
const fieldSelected = ref<('order' | 'name' | 'type')[]>([
|
||||
'order',
|
||||
'name',
|
||||
|
|
@ -118,6 +118,7 @@ const pageState = reactive({
|
|||
addModal: false,
|
||||
viewDrawer: false,
|
||||
isDrawerEdit: true,
|
||||
searchDate: [],
|
||||
});
|
||||
|
||||
async function fetchPropertyList(mobileFetch?: boolean) {
|
||||
|
|
@ -134,6 +135,8 @@ async function fetchPropertyList(mobileFetch?: boolean) {
|
|||
: statusFilter.value === 'statusACTIVE'
|
||||
? 'ACTIVE'
|
||||
: 'INACTIVE',
|
||||
startDate: pageState.searchDate[0],
|
||||
endDate: pageState.searchDate[1],
|
||||
});
|
||||
if (res) {
|
||||
propertyData.value =
|
||||
|
|
@ -317,11 +320,14 @@ watch(
|
|||
fetchPropertyList();
|
||||
},
|
||||
);
|
||||
watch([() => pageState.inputSearch, propertyPageSize], () => {
|
||||
propertyData.value = [];
|
||||
propertyPage.value = 1;
|
||||
fetchPropertyList();
|
||||
});
|
||||
watch(
|
||||
[() => pageState.inputSearch, propertyPageSize, () => pageState.searchDate],
|
||||
() => {
|
||||
propertyData.value = [];
|
||||
propertyPage.value = 1;
|
||||
fetchPropertyList();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<FloatingActionButton
|
||||
|
|
@ -398,26 +404,44 @@ watch([() => pageState.inputSearch, propertyPageSize], () => {
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch
|
||||
v-model="pageState.searchDate"
|
||||
:active="$q.screen.lt.md && statusFilter !== 'all'"
|
||||
>
|
||||
<div
|
||||
v-if="$q.screen.lt.md"
|
||||
class="q-mt-sm text-weight-medium"
|
||||
>
|
||||
{{ $t('general.status') }}
|
||||
</div>
|
||||
<q-select
|
||||
v-if="$q.screen.lt.md"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
map-options
|
||||
emit-value
|
||||
:for="'field-select-status'"
|
||||
:options="[
|
||||
{ label: $t('general.all'), value: 'all' },
|
||||
{ label: $t('general.active'), value: 'statusACTIVE' },
|
||||
{
|
||||
label: $t('general.inactive'),
|
||||
value: 'statusINACTIVE',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
</AdvanceSearch>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-if="$q.screen.gt.sm"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -512,11 +536,11 @@ watch([() => pageState.inputSearch, propertyPageSize], () => {
|
|||
class="col surface-2 flex items-center justify-center"
|
||||
>
|
||||
<NoData
|
||||
v-if="pageState.total !== 0"
|
||||
v-if="pageState.total !== 0 || pageState.searchDate.length > 0"
|
||||
:not-found="!!pageState.inputSearch"
|
||||
/>
|
||||
<CreateButton
|
||||
v-if="pageState.total === 0"
|
||||
v-if="pageState.total === 0 && pageState.searchDate.length === 0"
|
||||
@click="triggerDialog('add')"
|
||||
label="general.add"
|
||||
:i18n-args="{ text: $t('flow.title') }"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { useI18n } from 'vue-i18n';
|
|||
// NOTE: Import stores
|
||||
import useCustomerStore from 'stores/customer';
|
||||
import { useQuotationStore } from 'src/stores/quotations';
|
||||
import { dialog, isRoleInclude, notify } from 'stores/utils';
|
||||
import { dialog, isRoleInclude, notify, setPrefixName } from 'stores/utils';
|
||||
import { useNavigator } from 'src/stores/navigator';
|
||||
import useFlowStore from 'src/stores/flow';
|
||||
import useMyBranch from 'stores/my-branch';
|
||||
|
|
@ -49,6 +49,7 @@ import { Quotation } from 'src/stores/quotations/types';
|
|||
import TableQuotation from 'src/components/05_quotation/TableQuotation.vue';
|
||||
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||
import { DialogContainer, DialogHeader } from 'src/components/dialog';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const $q = useQuasar();
|
||||
|
|
@ -109,6 +110,7 @@ const pageState = reactive({
|
|||
quotationModal: false,
|
||||
employeeModal: false,
|
||||
receiptModal: false,
|
||||
searchDate: [],
|
||||
});
|
||||
|
||||
pageState.fieldSelected = [...columnQuotation.map((v) => v.name)];
|
||||
|
|
@ -327,6 +329,8 @@ async function fetchQuotationList(mobileFetch?: boolean) {
|
|||
: 'Issued',
|
||||
query: pageState.inputSearch,
|
||||
urgentFirst: true,
|
||||
startDate: pageState.searchDate[0],
|
||||
endDate: pageState.searchDate[1],
|
||||
});
|
||||
|
||||
if (ret) {
|
||||
|
|
@ -350,7 +354,12 @@ async function fetchQuotationList(mobileFetch?: boolean) {
|
|||
}
|
||||
|
||||
watch(
|
||||
[() => pageState.currentTab, () => pageState.inputSearch, quotationPageSize],
|
||||
[
|
||||
() => pageState.currentTab,
|
||||
() => pageState.inputSearch,
|
||||
() => pageState.searchDate,
|
||||
quotationPageSize,
|
||||
],
|
||||
() => {
|
||||
quotationPage.value = 1;
|
||||
quotationData.value = [];
|
||||
|
|
@ -517,6 +526,10 @@ async function storeDataLocal(id: string) {
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch v-model="pageState.searchDate" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5 justify-end" style="white-space: nowrap">
|
||||
|
|
@ -1008,7 +1021,16 @@ async function storeDataLocal(id: string) {
|
|||
"
|
||||
:title="
|
||||
customerFormData.customerType === 'PERS'
|
||||
? `${customerFormData.customerBranch[0]?.firstName} ${customerFormData.customerBranch[0]?.lastName}`
|
||||
? setPrefixName(
|
||||
{
|
||||
namePrefix: customerFormData.customerBranch[0]?.namePrefix,
|
||||
firstName: customerFormData.customerBranch[0]?.firstName,
|
||||
lastName: customerFormData.customerBranch[0]?.lastName,
|
||||
firstNameEN: customerFormData.customerBranch[0]?.firstNameEN,
|
||||
lastNameEN: customerFormData.customerBranch[0]?.lastNameEN,
|
||||
},
|
||||
{ locale },
|
||||
)
|
||||
: customerFormData.customerBranch[0]?.registerName
|
||||
"
|
||||
:caption="
|
||||
|
|
@ -1117,13 +1139,13 @@ async function storeDataLocal(id: string) {
|
|||
id="form-basic-info-customer"
|
||||
:onCreate="customerFormState.dialogType === 'create'"
|
||||
@edit="
|
||||
(customerFormState.dialogType = 'edit'),
|
||||
(customerFormState.readonly = false)
|
||||
((customerFormState.dialogType = 'edit'),
|
||||
(customerFormState.readonly = false))
|
||||
"
|
||||
@cancel="() => customerFormUndo(false)"
|
||||
@delete="
|
||||
customerFormState.editCustomerId &&
|
||||
deleteCustomerById(customerFormState.editCustomerId)
|
||||
deleteCustomerById(customerFormState.editCustomerId)
|
||||
"
|
||||
:customer-type="customerFormData.customerType"
|
||||
v-model:registered-branch-id="customerFormData.registeredBranchId"
|
||||
|
|
|
|||
|
|
@ -1865,7 +1865,7 @@ function covertToNode() {
|
|||
name: selectedWorker.map(
|
||||
(v, i) =>
|
||||
`${i + 1}. ` +
|
||||
`${v.namePrefix}. ${v.firstNameEN} ${v.lastNameEN}`.toUpperCase(),
|
||||
`${v.employeePassport.length !== 0 ? v.employeePassport[0].number + '_' : ''} ${v.namePrefix}.${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
|
||||
),
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { reactive, ref, watch, onMounted } from 'vue';
|
||||
import { baseUrl, notify } from 'src/stores/utils';
|
||||
import { baseUrl, notify, setPrefixName } from 'src/stores/utils';
|
||||
|
||||
// NOTE: Import stores
|
||||
import { dialog } from 'stores/utils';
|
||||
|
|
@ -296,6 +296,42 @@ watch(
|
|||
function setCurrentBranchId() {
|
||||
employeeFormState.value.currentBranchId = props.customerBranchId;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => employeeFormState.value.currentCustomerBranch,
|
||||
(e) => {
|
||||
if (!e) return;
|
||||
if (employeeFormState.value.formDataEmployeeSameAddr) {
|
||||
currentFromDataEmployee.value.address = e.address;
|
||||
currentFromDataEmployee.value.addressEN = e.addressEN;
|
||||
currentFromDataEmployee.value.provinceId = e.provinceId;
|
||||
currentFromDataEmployee.value.districtId = e.districtId;
|
||||
currentFromDataEmployee.value.subDistrictId = e.subDistrictId;
|
||||
}
|
||||
currentFromDataEmployee.value.customerBranchId = e.id;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => employeeFormState.value.formDataEmployeeSameAddr,
|
||||
(isSame) => {
|
||||
if (!employeeFormState.value.currentCustomerBranch) return;
|
||||
if (isSame) {
|
||||
currentFromDataEmployee.value.address =
|
||||
employeeFormState.value.currentCustomerBranch.address;
|
||||
currentFromDataEmployee.value.addressEN =
|
||||
employeeFormState.value.currentCustomerBranch.addressEN;
|
||||
currentFromDataEmployee.value.provinceId =
|
||||
employeeFormState.value.currentCustomerBranch.provinceId;
|
||||
currentFromDataEmployee.value.districtId =
|
||||
employeeFormState.value.currentCustomerBranch.districtId;
|
||||
currentFromDataEmployee.value.subDistrictId =
|
||||
employeeFormState.value.currentCustomerBranch.subDistrictId;
|
||||
}
|
||||
currentFromDataEmployee.value.customerBranchId =
|
||||
employeeFormState.value.currentCustomerBranch.id;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -700,6 +736,20 @@ function setCurrentBranchId() {
|
|||
"
|
||||
:toggleTitle="$t('status.title')"
|
||||
hideFade
|
||||
:title="
|
||||
currentFromDataEmployee
|
||||
? setPrefixName(
|
||||
{
|
||||
namePrefix: currentFromDataEmployee.namePrefix,
|
||||
firstName: currentFromDataEmployee.firstName,
|
||||
lastName: currentFromDataEmployee.lastName,
|
||||
firstNameEN: currentFromDataEmployee.firstNameEN,
|
||||
lastNameEN: currentFromDataEmployee.lastNameEN,
|
||||
},
|
||||
{ locale },
|
||||
)
|
||||
: '-'
|
||||
"
|
||||
@view="
|
||||
() => {
|
||||
employeeFormState.imageDialog = true;
|
||||
|
|
@ -996,6 +1046,9 @@ function setCurrentBranchId() {
|
|||
title="form.field.basicInformation"
|
||||
:readonly="!employeeFormState.isEmployeeEdit"
|
||||
v-model:customer-branch-id="employeeFormState.currentBranchId"
|
||||
v-model:current-customer-branch="
|
||||
employeeFormState.currentCustomerBranch
|
||||
"
|
||||
v-model:employee-id="employeeFormState.currentEmployeeCode"
|
||||
v-model:nrc-no="currentFromDataEmployee.nrcNo"
|
||||
v-model:code="currentFromDataEmployee.code"
|
||||
|
|
|
|||
|
|
@ -600,7 +600,7 @@ function print() {
|
|||
details?.worker.map(
|
||||
(v, i) =>
|
||||
`${i + 1}. ` +
|
||||
`${v.namePrefix}. ${v.firstNameEN} ${v.lastNameEN}`.toUpperCase(),
|
||||
`${v.namePrefix}. ${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
|
||||
) || [],
|
||||
},
|
||||
}) || '-'
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
import AddressForm from 'src/components/form/AddressForm.vue';
|
||||
import ImageUploadDialog from 'src/components/ImageUploadDialog.vue';
|
||||
import { BankBook } from 'src/stores/branch/types';
|
||||
import QrCodeUploadDialog from 'src/components/QrCodeUploadDialog.vue';
|
||||
|
||||
const institutionStore = useInstitution();
|
||||
|
||||
|
|
@ -29,10 +30,14 @@ const drawerModel = defineModel<boolean>('drawerModel', {
|
|||
required: true,
|
||||
default: false,
|
||||
});
|
||||
const onCreateImageList = defineModel<{
|
||||
const imageListOnCreate = defineModel<{
|
||||
selectedImage: string;
|
||||
list: { url: string; imgFile: File | null; name: string }[];
|
||||
}>('onCreateImageList', { default: { selectedImage: '', list: [] } });
|
||||
}>('imageListOnCreate', { default: { selectedImage: '', list: [] } });
|
||||
const deletesStatusQrCodeBankImag = defineModel<number[]>(
|
||||
'deletesStatusQrCodeBankImag',
|
||||
{ default: [] },
|
||||
);
|
||||
|
||||
const imageState = reactive({
|
||||
imageDialog: false,
|
||||
|
|
@ -45,6 +50,14 @@ const imageState = reactive({
|
|||
const imageFile = ref<File | null>(null);
|
||||
const imageList = ref<{ selectedImage: string; list: string[] }>();
|
||||
|
||||
const qrCodeDialog = ref(false);
|
||||
const qrCodeImageUrl = ref<string>('');
|
||||
const currentIndexQrCodeBank = ref<number>(-1);
|
||||
const statusQrCodeFile = ref<File | undefined>(undefined);
|
||||
const refQrCodeUpload = ref();
|
||||
const statusQrCodeUrl = ref<string>('');
|
||||
const statusDeletesQrCode = ref<boolean>(false);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
|
|
@ -159,10 +172,39 @@ async function submitImage(name: string) {
|
|||
function clearImageState() {
|
||||
imageState.imageDialog = false;
|
||||
imageFile.value = null;
|
||||
onCreateImageList.value = { selectedImage: '', list: [] };
|
||||
imageListOnCreate.value = { selectedImage: '', list: [] };
|
||||
imageState.refreshImageState = false;
|
||||
}
|
||||
|
||||
function triggerEditQrCodeBank(opts?: { save?: boolean }) {
|
||||
if (opts?.save === undefined) {
|
||||
qrCodeDialog.value = true;
|
||||
statusDeletesQrCode.value = false;
|
||||
statusQrCodeUrl.value =
|
||||
formBankBook.value[currentIndexQrCodeBank.value].bankUrl || '';
|
||||
statusDeletesQrCode.value = false;
|
||||
} else {
|
||||
formBankBook.value[currentIndexQrCodeBank.value].bankUrl =
|
||||
statusQrCodeUrl.value;
|
||||
|
||||
formBankBook.value[currentIndexQrCodeBank.value].bankQr =
|
||||
statusQrCodeFile.value;
|
||||
|
||||
if (statusDeletesQrCode.value === true) {
|
||||
deletesStatusQrCodeBankImag.value.push(currentIndexQrCodeBank.value);
|
||||
}
|
||||
if (statusDeletesQrCode.value === false) {
|
||||
deletesStatusQrCodeBankImag.value =
|
||||
deletesStatusQrCodeBankImag.value.filter(
|
||||
(item) => item !== currentIndexQrCodeBank.value,
|
||||
);
|
||||
}
|
||||
|
||||
currentIndexQrCodeBank.value = -1;
|
||||
statusDeletesQrCode.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => imageFile.value,
|
||||
() => {
|
||||
|
|
@ -177,7 +219,6 @@ watch(
|
|||
imageList.value
|
||||
? (imageList.value.selectedImage = data.value.selectedImage || '')
|
||||
: '';
|
||||
console.log(imageState.imageUrl);
|
||||
imageState.refreshImageState = false;
|
||||
},
|
||||
);
|
||||
|
|
@ -327,8 +368,19 @@ watch(
|
|||
title="agencies.bankInfo"
|
||||
class="q-pt-xl"
|
||||
dense
|
||||
single
|
||||
v-model:bank-book-list="formBankBook"
|
||||
@view-qr="
|
||||
(i) => {
|
||||
currentIndexQrCodeBank = i;
|
||||
triggerEditQrCodeBank();
|
||||
}
|
||||
"
|
||||
@edit-qr="
|
||||
(i) => {
|
||||
currentIndexQrCodeBank = i;
|
||||
refQrCodeUpload && refQrCodeUpload.browse();
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -518,9 +570,20 @@ watch(
|
|||
title="agencies.bankInfo"
|
||||
class="q-pt-xl"
|
||||
dense
|
||||
single
|
||||
:readonly
|
||||
v-model:bank-book-list="formBankBook"
|
||||
@view-qr="
|
||||
(i) => {
|
||||
currentIndexQrCodeBank = i;
|
||||
triggerEditQrCodeBank();
|
||||
}
|
||||
"
|
||||
@edit-qr="
|
||||
(i) => {
|
||||
currentIndexQrCodeBank = i;
|
||||
refQrCodeUpload && refQrCodeUpload.browse();
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -531,7 +594,7 @@ watch(
|
|||
<ImageUploadDialog
|
||||
v-model:dialog-state="imageState.imageDialog"
|
||||
v-model:file="imageFile"
|
||||
v-model:on-create-data-list="onCreateImageList"
|
||||
v-model:on-create-data-list="imageListOnCreate"
|
||||
v-model:image-url="imageState.imageUrl"
|
||||
v-model:data-list="imageList"
|
||||
:on-create="model"
|
||||
|
|
@ -561,5 +624,31 @@ watch(
|
|||
</div>
|
||||
</template>
|
||||
</ImageUploadDialog>
|
||||
|
||||
<QrCodeUploadDialog
|
||||
ref="refQrCodeUpload"
|
||||
v-model:dialog-state="qrCodeDialog"
|
||||
v-model:file="statusQrCodeFile as File"
|
||||
v-model:image-url="statusQrCodeUrl"
|
||||
@save="
|
||||
(_file) => {
|
||||
qrCodeDialog = false;
|
||||
if (currentIndexQrCodeBank !== -1) {
|
||||
triggerEditQrCodeBank({ save: true });
|
||||
}
|
||||
}
|
||||
"
|
||||
@clear="statusDeletesQrCode = true"
|
||||
clearButton
|
||||
>
|
||||
<template #error>
|
||||
<div
|
||||
class="full-width full-height flex items-center justify-center"
|
||||
style="color: gray"
|
||||
>
|
||||
<q-icon size="15rem" name="mdi-qrcode" />
|
||||
</div>
|
||||
</template>
|
||||
</QrCodeUploadDialog>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import { QSelect, QTableProps } from 'quasar';
|
||||
import { QTableProps } from 'quasar';
|
||||
import { dialog } from 'src/stores/utils';
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
|
@ -21,6 +21,7 @@ import FloatingActionButton from 'src/components/FloatingActionButton.vue';
|
|||
import CreateButton from 'src/components/AddButton.vue';
|
||||
import NoData from 'src/components/NoData.vue';
|
||||
import AgenciesDialog from './AgenciesDialog.vue';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const $q = useQuasar();
|
||||
|
|
@ -78,8 +79,9 @@ const pageState = reactive({
|
|||
addModal: false,
|
||||
viewDrawer: false,
|
||||
isDrawerEdit: true,
|
||||
searchDate: [],
|
||||
});
|
||||
|
||||
const deletesStatusQrCodeBankImag = ref<number[]>([]);
|
||||
const blankFormData: InstitutionPayload = {
|
||||
group: '',
|
||||
code: '',
|
||||
|
|
@ -114,11 +116,10 @@ const blankFormData: InstitutionPayload = {
|
|||
};
|
||||
|
||||
const statusFilter = ref<'all' | 'statusACTIVE' | 'statusINACTIVE'>('all');
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
const refAgenciesDialog = ref();
|
||||
const formData = ref<InstitutionPayload>(structuredClone(blankFormData));
|
||||
const currAgenciesData = ref<Institution>();
|
||||
const onCreateImageList = ref<{
|
||||
const imageListOnCreate = ref<{
|
||||
selectedImage: string;
|
||||
list: { url: string; imgFile: File | null; name: string }[];
|
||||
}>({ selectedImage: '', list: [] });
|
||||
|
|
@ -176,16 +177,15 @@ function assignFormData(data: Institution) {
|
|||
contactEmail: data.contactEmail,
|
||||
contactName: data.contactName,
|
||||
contactTel: data.contactTel,
|
||||
bank: [
|
||||
{
|
||||
bankName: data.bank[0]?.bankName,
|
||||
accountNumber: data.bank[0]?.accountNumber,
|
||||
bankBranch: data.bank[0]?.bankBranch,
|
||||
accountName: data.bank[0]?.accountName,
|
||||
accountType: data.bank[0]?.accountType,
|
||||
currentlyUse: data.bank[0]?.currentlyUse,
|
||||
},
|
||||
],
|
||||
bank: data.bank.map((v) => ({
|
||||
bankName: v.bankName,
|
||||
accountNumber: v.accountNumber,
|
||||
bankBranch: v.bankBranch,
|
||||
accountName: v.accountName,
|
||||
accountType: v.accountType,
|
||||
currentlyUse: v.currentlyUse,
|
||||
bankUrl: `${baseUrl}/institution/${data.id}/bank-qr/${v.id}?ts=${Date.now()}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -211,14 +211,10 @@ async function submit(opt?: { selectedImage: string }) {
|
|||
provinceId: formData.value.provinceId,
|
||||
status: formData.value.status,
|
||||
bank: formData.value.bank.map((v) => ({
|
||||
bankName: v.bankName,
|
||||
accountNumber: v.accountNumber,
|
||||
bankBranch: v.bankBranch,
|
||||
accountName: v.accountName,
|
||||
accountType: v.accountType,
|
||||
currentlyUse: v.currentlyUse,
|
||||
...v,
|
||||
})),
|
||||
};
|
||||
console.log('payload', payload);
|
||||
if (
|
||||
(pageState.isDrawerEdit && currAgenciesData.value?.id) ||
|
||||
(opt?.selectedImage && currAgenciesData.value?.id)
|
||||
|
|
@ -229,6 +225,7 @@ async function submit(opt?: { selectedImage: string }) {
|
|||
id: currAgenciesData.value.id,
|
||||
selectedImage: opt?.selectedImage || undefined,
|
||||
}),
|
||||
{ indexDeleteQrCodeBank: deletesStatusQrCodeBankImag.value },
|
||||
);
|
||||
|
||||
if (ret) {
|
||||
|
|
@ -248,7 +245,7 @@ async function submit(opt?: { selectedImage: string }) {
|
|||
...payload,
|
||||
code: formData.value.group || '',
|
||||
},
|
||||
onCreateImageList.value,
|
||||
imageListOnCreate.value,
|
||||
);
|
||||
|
||||
await fetchData($q.screen.xs);
|
||||
|
|
@ -288,6 +285,8 @@ async function fetchData(mobileFetch?: boolean) {
|
|||
: statusFilter.value === 'statusACTIVE'
|
||||
? 'ACTIVE'
|
||||
: 'INACTIVE',
|
||||
startDate: pageState.searchDate[0],
|
||||
endDate: pageState.searchDate[1],
|
||||
});
|
||||
|
||||
if (ret) {
|
||||
|
|
@ -357,7 +356,7 @@ onMounted(async () => {
|
|||
});
|
||||
|
||||
watch(
|
||||
() => [pageState.inputSearch, statusFilter.value],
|
||||
() => [pageState.inputSearch, statusFilter.value, pageState.searchDate],
|
||||
() => {
|
||||
page.value = 1;
|
||||
data.value = [];
|
||||
|
|
@ -440,26 +439,44 @@ watch(
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch
|
||||
v-model="pageState.searchDate"
|
||||
:active="$q.screen.lt.md && statusFilter !== 'all'"
|
||||
>
|
||||
<div
|
||||
v-if="$q.screen.lt.md"
|
||||
class="q-mt-sm text-weight-medium"
|
||||
>
|
||||
{{ $t('general.status') }}
|
||||
</div>
|
||||
<q-select
|
||||
v-if="$q.screen.lt.md"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
map-options
|
||||
emit-value
|
||||
:for="'field-select-status'"
|
||||
:options="[
|
||||
{ label: $t('general.all'), value: 'all' },
|
||||
{ label: $t('general.active'), value: 'statusACTIVE' },
|
||||
{
|
||||
label: $t('general.inactive'),
|
||||
value: 'statusINACTIVE',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
</AdvanceSearch>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5 justify-end" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-if="$q.screen.gt.sm"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -960,7 +977,8 @@ watch(
|
|||
v-model:drawer-model="pageState.viewDrawer"
|
||||
v-model:data="formData"
|
||||
v-model:form-bank-book="formData.bank"
|
||||
v-model:on-create-image-list="onCreateImageList"
|
||||
v-model:image-list-on-create="imageListOnCreate"
|
||||
v-model:deletes-status-qr-code-bank-imag="deletesStatusQrCodeBankImag"
|
||||
/>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { QSelect, useQuasar } from 'quasar';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
// NOTE: Components
|
||||
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||
|
|
@ -24,6 +24,9 @@ import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
|
|||
import { dialogWarningClose } from 'src/stores/utils';
|
||||
import { CancelButton, SaveButton } from 'src/components/button';
|
||||
import { getRole } from 'src/services/keycloak';
|
||||
import FloatingActionButton from 'src/components/FloatingActionButton.vue';
|
||||
import RequestListAction from './RequestListAction .vue';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const $q = useQuasar();
|
||||
const navigatorStore = useNavigator();
|
||||
|
|
@ -32,7 +35,7 @@ const requestListStore = useRequestList();
|
|||
const { t } = useI18n();
|
||||
const { data, stats, page, pageMax, pageSize } = storeToRefs(requestListStore);
|
||||
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
const requestListActionData = ref<RequestData[]>();
|
||||
|
||||
// NOTE: Variable
|
||||
const pageState = reactive({
|
||||
|
|
@ -45,6 +48,8 @@ const pageState = reactive({
|
|||
rejectCancelDialog: false,
|
||||
rejectCancelReason: '',
|
||||
requestId: '',
|
||||
requestListActionDialog: false,
|
||||
searchDate: [],
|
||||
});
|
||||
|
||||
const fieldSelectedOption = computed(() => {
|
||||
|
|
@ -60,6 +65,8 @@ async function fetchList(opts?: { rotateFlowId?: boolean }) {
|
|||
query: pageState.inputSearch,
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
startDate: pageState.searchDate[0],
|
||||
endDate: pageState.searchDate[1],
|
||||
requestDataStatus:
|
||||
pageState.statusFilter === 'None' ? undefined : pageState.statusFilter,
|
||||
// responsibleOnly: true,
|
||||
|
|
@ -131,6 +138,32 @@ async function submitRejectCancel() {
|
|||
}
|
||||
}
|
||||
|
||||
async function openRequestListDialog() {
|
||||
const ret = await requestListStore.getRequestDataList({
|
||||
page: 1,
|
||||
pageSize: 999,
|
||||
incomplete: true,
|
||||
});
|
||||
|
||||
if (ret) {
|
||||
requestListActionData.value = ret.result;
|
||||
}
|
||||
|
||||
pageState.requestListActionDialog = true;
|
||||
}
|
||||
|
||||
async function submitRequestListAction(data: {
|
||||
form: { responsibleUserLocal: boolean; responsibleUserId: string };
|
||||
selected: RequestData[];
|
||||
}) {
|
||||
const res = await requestListStore.updateMessenger(
|
||||
data.selected.map((v) => v.id),
|
||||
data.form.responsibleUserId,
|
||||
);
|
||||
|
||||
if (res) pageState.requestListActionDialog = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
pageState.gridView = $q.screen.lt.md ? true : false;
|
||||
navigatorStore.current.title = 'requestList.title';
|
||||
|
|
@ -140,13 +173,27 @@ onMounted(async () => {
|
|||
await fetchList({ rotateFlowId: true });
|
||||
});
|
||||
|
||||
watch([() => pageState.inputSearch, () => pageState.statusFilter], () => {
|
||||
page.value = 1;
|
||||
data.value = [];
|
||||
fetchList({ rotateFlowId: true });
|
||||
});
|
||||
watch(
|
||||
[
|
||||
() => pageState.inputSearch,
|
||||
() => pageState.statusFilter,
|
||||
() => pageState.searchDate,
|
||||
],
|
||||
() => {
|
||||
page.value = 1;
|
||||
data.value = [];
|
||||
fetchList({ rotateFlowId: true });
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<FloatingActionButton
|
||||
hide-icon
|
||||
style="z-index: 999"
|
||||
icon="mdi-account-outline"
|
||||
@click.stop="openRequestListDialog"
|
||||
></FloatingActionButton>
|
||||
|
||||
<div class="column full-height no-wrap">
|
||||
<!-- SEC: stat -->
|
||||
<section class="text-body-2 q-mb-xs flex items-center">
|
||||
|
|
@ -239,26 +286,62 @@ watch([() => pageState.inputSearch, () => pageState.statusFilter], () => {
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch
|
||||
v-model="pageState.searchDate"
|
||||
:active="$q.screen.lt.md && pageState.statusFilter !== 'None'"
|
||||
>
|
||||
<div
|
||||
v-if="$q.screen.lt.md"
|
||||
class="q-mt-sm text-weight-medium"
|
||||
>
|
||||
{{ $t('general.status') }}
|
||||
</div>
|
||||
<q-select
|
||||
v-if="$q.screen.lt.md"
|
||||
v-model="pageState.statusFilter"
|
||||
outlined
|
||||
dense
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
map-options
|
||||
emit-value
|
||||
:for="'field-select-status'"
|
||||
:options="[
|
||||
{
|
||||
label: $t('general.all'),
|
||||
value: 'None',
|
||||
},
|
||||
{
|
||||
label: $t('requestList.status.Pending'),
|
||||
value: RequestDataStatus.Pending,
|
||||
},
|
||||
{
|
||||
label: $t('requestList.status.Ready'),
|
||||
value: RequestDataStatus.Ready,
|
||||
},
|
||||
{
|
||||
label: $t('requestList.status.InProgress'),
|
||||
value: RequestDataStatus.InProgress,
|
||||
},
|
||||
{
|
||||
label: $t('requestList.status.Completed'),
|
||||
value: RequestDataStatus.Completed,
|
||||
},
|
||||
{
|
||||
label: $t('requestList.status.Canceled'),
|
||||
value: RequestDataStatus.Canceled,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
</AdvanceSearch>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-if="$q.screen.gt.sm"
|
||||
v-model="pageState.statusFilter"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -485,6 +568,13 @@ watch([() => pageState.inputSearch, () => pageState.statusFilter], () => {
|
|||
/>
|
||||
</template>
|
||||
</DialogFormContainer>
|
||||
|
||||
<RequestListAction
|
||||
v-if="requestListActionData"
|
||||
v-model="pageState.requestListActionDialog"
|
||||
:request-list="requestListActionData"
|
||||
@submit="submitRequestListAction"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style></style>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const props = defineProps<{
|
|||
readonly?: boolean;
|
||||
step: Step;
|
||||
responsibleAreaDistrictId?: string;
|
||||
defaultMessenger?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -85,7 +86,8 @@ function assignToForm() {
|
|||
companyDuty: attributesForm.value.companyDuty ?? false,
|
||||
companyDutyCost: attributesForm.value.companyDutyCost ?? 30,
|
||||
responsibleUserLocal: attributesForm.value.responsibleUserLocal ?? true,
|
||||
responsibleUserId: attributesForm.value.responsibleUserId ?? '',
|
||||
responsibleUserId:
|
||||
attributesForm.value.responsibleUserId || props.defaultMessenger,
|
||||
individualDuty: attributesForm.value.individualDuty ?? false,
|
||||
individualDutyCost: attributesForm.value.individualDutyCost ?? 10,
|
||||
}),
|
||||
|
|
|
|||
238
src/pages/08_request-list/RequestListAction .vue
Normal file
238
src/pages/08_request-list/RequestListAction .vue
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { RequestData } from 'src/stores/request-list';
|
||||
import { DialogContainer, DialogHeader } from 'src/components/dialog';
|
||||
import {
|
||||
BackButton,
|
||||
CancelButton,
|
||||
MainButton,
|
||||
SaveButton,
|
||||
} from 'src/components/button';
|
||||
import FormResponsibleUser from './FormResponsibleUser.vue';
|
||||
import FormGroupHead from './FormGroupHead.vue';
|
||||
import TableRequestList from './TableRequestList.vue';
|
||||
import { column } from './constants';
|
||||
import useAddressStore from 'src/stores/address';
|
||||
|
||||
defineProps<{
|
||||
requestList: RequestData[];
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(
|
||||
e: 'submit',
|
||||
data: {
|
||||
form: { responsibleUserLocal: boolean; responsibleUserId: string };
|
||||
selected: RequestData[];
|
||||
},
|
||||
): void;
|
||||
}>();
|
||||
|
||||
enum Step {
|
||||
RequestList = 1,
|
||||
Configure = 2,
|
||||
}
|
||||
|
||||
const open = defineModel<boolean>({ default: false });
|
||||
const step = ref<Step>(Step.RequestList);
|
||||
const selected = ref<RequestData[]>([]);
|
||||
const listSameArea = ref<string[]>([]);
|
||||
const form = reactive({
|
||||
responsibleUserLocal: false,
|
||||
responsibleUserId: '',
|
||||
});
|
||||
|
||||
function reset() {
|
||||
step.value = Step.RequestList;
|
||||
selected.value = [];
|
||||
form.responsibleUserLocal = false;
|
||||
form.responsibleUserId = '';
|
||||
}
|
||||
|
||||
function prev() {
|
||||
step.value = Step.RequestList;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => selected.value,
|
||||
async () => {
|
||||
if (selected.value.length === 1) {
|
||||
const districtId = selected.value[0].quotation.customerBranch.districtId;
|
||||
const ret = await useAddressStore().listSameOfficeArea(districtId);
|
||||
if (ret) listSameArea.value = ret;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<DialogContainer v-model="open" :onOpen="reset">
|
||||
<template #header>
|
||||
<DialogHeader :title="$t('requestList.action.title')" />
|
||||
</template>
|
||||
|
||||
<div class="surface-0 q-pa-md">
|
||||
<div class="stepper-wrapper">
|
||||
<div class="stepper">
|
||||
<template
|
||||
v-for="(label, i) in [
|
||||
$t('menu.product'),
|
||||
$t('requestList.action.configure'),
|
||||
]"
|
||||
:key="i"
|
||||
>
|
||||
<span class="step" :class="{ ['step__active']: step > i }">
|
||||
<span class="step-outer"><span class="step-inner" /></span>
|
||||
<span class="step-label">{{ label }}</span>
|
||||
</span>
|
||||
<span
|
||||
class="step-connector"
|
||||
:class="{ ['step-connector__active']: step > i + 1 }"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="surface-1 q-pa-md col full-width scroll">
|
||||
<TableRequestList
|
||||
v-if="step === Step.RequestList"
|
||||
v-model:selected="selected"
|
||||
hide-action
|
||||
hide-view
|
||||
checkable
|
||||
:list-same-area="listSameArea"
|
||||
:columns="column"
|
||||
:rows="requestList"
|
||||
:visible-columns="[...column.map((col) => col.name)]"
|
||||
/>
|
||||
|
||||
<template v-if="step === Step.Configure">
|
||||
<q-expansion-item
|
||||
dense
|
||||
class="overflow-hidden bordered full-width"
|
||||
switch-toggle-side
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||
default-opened
|
||||
>
|
||||
<template #header>
|
||||
<span>
|
||||
{{ $t('requestList.employeeMessenger') }}
|
||||
</span>
|
||||
</template>
|
||||
<FormGroupHead>
|
||||
{{
|
||||
$t('general.select', { msg: $t('requestList.employeeMessenger') })
|
||||
}}
|
||||
</FormGroupHead>
|
||||
|
||||
<FormResponsibleUser
|
||||
:district-id="listSameArea[0]"
|
||||
v-model:responsible-user-id="form.responsibleUserId"
|
||||
v-model:responsible-user-local="form.responsibleUserLocal"
|
||||
/>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="q-gutter-x-xs q-ml-auto">
|
||||
<CancelButton
|
||||
v-if="step === Step.RequestList"
|
||||
id="btn-cancel"
|
||||
outlined
|
||||
@click="
|
||||
reset();
|
||||
open = false;
|
||||
"
|
||||
/>
|
||||
<BackButton
|
||||
v-if="step === Step.Configure"
|
||||
id="btn-back"
|
||||
outlined
|
||||
@click="prev"
|
||||
/>
|
||||
<MainButton
|
||||
icon="mdi-check"
|
||||
color="207 96% 32%"
|
||||
solid
|
||||
id="btn-next"
|
||||
v-if="step === Step.RequestList"
|
||||
@click="step = Step.Configure"
|
||||
>
|
||||
{{ $t('general.next') }}
|
||||
</MainButton>
|
||||
<SaveButton
|
||||
v-if="step === Step.Configure"
|
||||
id="btn-save"
|
||||
solid
|
||||
@click="$emit('submit', { form, selected })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</DialogContainer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-inline: 25%;
|
||||
|
||||
& > .step {
|
||||
--__color: var(--gray-5);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
gap: 0.25rem;
|
||||
|
||||
&.step__active {
|
||||
--__color: var(--brand-1);
|
||||
}
|
||||
|
||||
& > .step-label {
|
||||
position: absolute;
|
||||
font-weight: 600;
|
||||
color: var(--__color);
|
||||
white-space: nowrap;
|
||||
top: 2rem;
|
||||
}
|
||||
|
||||
& > .step-outer {
|
||||
display: inline-flex;
|
||||
border: 2px solid var(--__color);
|
||||
border-radius: 50%;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > .step-inner {
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
background-color: var(--__color);
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .step-connector {
|
||||
display: block;
|
||||
border-bottom: 2px solid var(--gray-5);
|
||||
flex-grow: 1;
|
||||
|
||||
&.step-connector__active {
|
||||
border-color: var(--brand-1);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -54,6 +54,7 @@ import { Invoice } from 'src/stores/payment/types';
|
|||
import { CreatedBy } from 'src/stores/types';
|
||||
import { getUserId } from 'src/services/keycloak';
|
||||
import { QuotationFull } from 'src/stores/quotations/types';
|
||||
import useUserStore from 'src/stores/user';
|
||||
|
||||
const { locale, t } = useI18n();
|
||||
|
||||
|
|
@ -62,7 +63,9 @@ const route = useRoute();
|
|||
const optionStore = useOptionStore();
|
||||
const requestListStore = useRequestList();
|
||||
const flowTemplateStore = useWorkflowTemplate();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const currentUserGroup = ref<string[]>([]);
|
||||
const workList = ref<RequestWork[]>([]);
|
||||
const statusFile = ref<Attributes>({
|
||||
customer: {},
|
||||
|
|
@ -158,6 +161,10 @@ onMounted(async () => {
|
|||
initTheme();
|
||||
initLang();
|
||||
|
||||
const result = await userStore.fetchUserGroup();
|
||||
|
||||
currentUserGroup.value = result.map((v) => v.name);
|
||||
|
||||
// get data
|
||||
await getData();
|
||||
});
|
||||
|
|
@ -283,26 +290,38 @@ async function triggerViewFile(opt: {
|
|||
if (!opt.download) window.open(url, '_blank');
|
||||
}
|
||||
|
||||
const responsiblePersonList = computed(() => {
|
||||
const temp = workList.value?.reduce<Record<string, CreatedBy[]>>(
|
||||
(acc, curr: RequestWork) => {
|
||||
curr.productService.service?.workflow?.step.forEach((v) => {
|
||||
const key = v.order.toString();
|
||||
const responsibleList = computed(() => {
|
||||
const temp = workList.value?.reduce<
|
||||
Record<string, { user: CreatedBy[]; group: string[] }>
|
||||
>((acc, curr: RequestWork) => {
|
||||
curr.productService.service?.workflow?.step.forEach((v) => {
|
||||
const key = v.order.toString();
|
||||
const responsibleGroup = (
|
||||
v.responsibleGroup as unknown as { group: string }[]
|
||||
).map((v) => v.group);
|
||||
|
||||
if (!acc[key]) acc[key] = v.responsiblePerson.map((v) => v.user);
|
||||
if (!acc[key]) {
|
||||
acc[key] = {
|
||||
user: v.responsiblePerson.map((v) => v.user),
|
||||
group: responsibleGroup,
|
||||
};
|
||||
}
|
||||
|
||||
const current = acc[key];
|
||||
const current = acc[key];
|
||||
|
||||
v.responsiblePerson.forEach((lhs) => {
|
||||
if (current.find((rhs) => rhs.id === lhs.userId)) return;
|
||||
current.push(lhs.user);
|
||||
});
|
||||
v.responsiblePerson.forEach((lhs) => {
|
||||
if (current.user.find((rhs) => rhs.id === lhs.userId)) return;
|
||||
current.user.push(lhs.user);
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
responsibleGroup.forEach((lhs) => {
|
||||
if (current.group.find((rhs) => rhs === lhs)) return;
|
||||
current.group.push(lhs);
|
||||
});
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return temp || {};
|
||||
});
|
||||
|
|
@ -438,6 +457,24 @@ async function submitRejectCancel() {
|
|||
pageState.rejectCancelDialog = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toCustomer(customer: RequestData['quotation']['customerBranch']) {
|
||||
const url = new URL(
|
||||
`/customer-management?tab=customer&id=${customer.customerId}`,
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
window.open(url.toString(), '_blank');
|
||||
}
|
||||
|
||||
function toEmployee(employee: RequestData['employee']) {
|
||||
const url = new URL(
|
||||
`/customer-management?tab=employee&id=${employee.id}`,
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
window.open(url.toString(), '_blank');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="column surface-0 fullscreen" v-if="data">
|
||||
|
|
@ -478,11 +515,11 @@ async function submitRejectCancel() {
|
|||
<span class="app-text-muted">{{ $t('flow.responsiblePerson') }}</span>
|
||||
<span>
|
||||
<template
|
||||
v-if="responsiblePersonList[pageState.currentStep]?.length"
|
||||
v-if="responsibleList[pageState.currentStep]?.user.length"
|
||||
>
|
||||
<AvatarGroup
|
||||
:data="
|
||||
(responsiblePersonList[pageState.currentStep] || []).map(
|
||||
:data="[
|
||||
...(responsibleList[pageState.currentStep].user || []).map(
|
||||
(v) => ({
|
||||
name:
|
||||
$i18n.locale === 'eng'
|
||||
|
|
@ -494,8 +531,12 @@ async function submitRejectCancel() {
|
|||
: `/no-img-female.png`
|
||||
: `${baseUrl}/user/${v.id}/profile-image/${v.selectedImage}`,
|
||||
}),
|
||||
)
|
||||
"
|
||||
),
|
||||
...responsibleList[pageState.currentStep].group.map((g) => ({
|
||||
name: `${$t('general.group')} ${g}`,
|
||||
imgUrl: '/img-group.png',
|
||||
})),
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
|
|
@ -701,6 +742,7 @@ async function submitRejectCancel() {
|
|||
}"
|
||||
>
|
||||
<DataDisplay
|
||||
clickable
|
||||
class="col"
|
||||
icon="mdi-account-settings-outline"
|
||||
:label="$t('customer.employer')"
|
||||
|
|
@ -710,8 +752,10 @@ async function submitRejectCancel() {
|
|||
noCode: true,
|
||||
}) || '-'
|
||||
"
|
||||
@label-click="toCustomer(data.quotation.customerBranch)"
|
||||
/>
|
||||
<DataDisplay
|
||||
clickable
|
||||
class="col"
|
||||
icon="mdi-account-settings-outline"
|
||||
:label="$t('customer.employee')"
|
||||
|
|
@ -720,6 +764,7 @@ async function submitRejectCancel() {
|
|||
locale: $i18n.locale,
|
||||
}) || '-'
|
||||
"
|
||||
@label-click="toEmployee(data.employee)"
|
||||
/>
|
||||
<DataDisplay
|
||||
class="col"
|
||||
|
|
@ -776,11 +821,15 @@ async function submitRejectCancel() {
|
|||
:cancel="data.requestDataStatus === RequestDataStatus.Canceled"
|
||||
:readonly="
|
||||
data.requestDataStatus === RequestDataStatus.Canceled ||
|
||||
(responsiblePersonList &&
|
||||
!!responsiblePersonList[pageState.currentStep]?.length &&
|
||||
!responsiblePersonList[pageState.currentStep]?.find(
|
||||
(responsibleList &&
|
||||
!responsibleList[pageState.currentStep]?.user.find(
|
||||
(v) => v.id === getUserId(),
|
||||
))
|
||||
) &&
|
||||
!responsibleList[pageState.currentStep]?.group.some((v) =>
|
||||
currentUserGroup.includes(v),
|
||||
)) ||
|
||||
(!!responsibleList[pageState.currentStep]?.user?.length &&
|
||||
!!responsibleList[pageState.currentStep]?.user?.length)
|
||||
"
|
||||
:order-able="value._messengerExpansion"
|
||||
:installment-info="getInstallmentInfo()"
|
||||
|
|
@ -873,6 +922,11 @@ async function submitRejectCancel() {
|
|||
:readonly="
|
||||
data.requestDataStatus === RequestDataStatus.Canceled
|
||||
"
|
||||
:default-messenger="
|
||||
value.stepStatus[pageState.currentStep - 1]
|
||||
? undefined
|
||||
: data.defaultMessengerId
|
||||
"
|
||||
:step="{
|
||||
step: pageState.currentStep,
|
||||
requestWorkId: value.id || '',
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import useOptionStore from 'src/stores/options';
|
|||
|
||||
import KebabAction from 'src/components/shared/KebabAction.vue';
|
||||
import { CreatedBy } from 'src/stores/types';
|
||||
import { dateFormatJS } from 'src/utils/datetime';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
|
@ -21,6 +22,9 @@ const props = withDefaults(
|
|||
grid?: boolean;
|
||||
visibleColumns?: string[];
|
||||
hideAction?: boolean;
|
||||
hideView?: boolean;
|
||||
checkable?: boolean;
|
||||
listSameArea?: string[];
|
||||
}>(),
|
||||
{
|
||||
row: () => [],
|
||||
|
|
@ -36,9 +40,16 @@ defineEmits<{
|
|||
(e: 'rejectCancel', data: RequestData): void;
|
||||
}>();
|
||||
|
||||
function responsiblePerson(quotation: QuotationFull): CreatedBy[] | undefined {
|
||||
const selected = defineModel<RequestData[]>('selected');
|
||||
|
||||
function responsiblePerson(quotation: QuotationFull) {
|
||||
const productServiceList = quotation.productServiceList;
|
||||
const tempPerson: CreatedBy[] = [];
|
||||
const tempGroup: {
|
||||
group: string;
|
||||
id: string;
|
||||
workflowTemplateStepId: string;
|
||||
}[] = [];
|
||||
|
||||
for (const v of productServiceList) {
|
||||
const tempStep = v.service?.workflow?.step;
|
||||
|
|
@ -49,7 +60,17 @@ function responsiblePerson(quotation: QuotationFull): CreatedBy[] | undefined {
|
|||
tempPerson.push(rhs.user);
|
||||
}
|
||||
});
|
||||
return tempPerson;
|
||||
tempStep.forEach((lhs) => {
|
||||
const newGroup = lhs.responsibleGroup as unknown as {
|
||||
group: string;
|
||||
id: string;
|
||||
workflowTemplateStepId: string;
|
||||
}[];
|
||||
for (const rhs of newGroup) {
|
||||
tempGroup.push(rhs);
|
||||
}
|
||||
});
|
||||
return { user: tempPerson, group: tempGroup };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,10 +113,39 @@ function getEmployeeName(
|
|||
return (
|
||||
{
|
||||
['eng']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstNameEN} ${employee?.lastNameEN}`,
|
||||
['tha']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstName} ${employee?.lastName}`,
|
||||
['tha']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstName || employee?.firstNameEN} ${employee?.lastName || employee?.lastNameEN}`,
|
||||
}[opts?.locale || 'eng'] || '-'
|
||||
);
|
||||
}
|
||||
|
||||
function toCustomer(customer: RequestData['quotation']['customerBranch']) {
|
||||
const url = new URL(
|
||||
`/customer-management?tab=customer&id=${customer.customerId}`,
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
window.open(url.toString(), '_blank');
|
||||
}
|
||||
|
||||
function toEmployee(employee: RequestData['employee']) {
|
||||
const url = new URL(
|
||||
`/customer-management?tab=employee&id=${employee.id}`,
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
window.open(url.toString(), '_blank');
|
||||
}
|
||||
|
||||
function handleCheckAll() {
|
||||
const filteredRows = props.rows.filter((row) =>
|
||||
props.listSameArea?.includes(row.quotation.customerBranch.districtId),
|
||||
);
|
||||
if (selected.value.length === filteredRows.length) {
|
||||
selected.value = [];
|
||||
} else {
|
||||
selected.value = filteredRows;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<q-table
|
||||
|
|
@ -106,12 +156,36 @@ function getEmployeeName(
|
|||
card-container-class="q-col-gutter-sm"
|
||||
:rows-per-page-options="[0]"
|
||||
class="full-width"
|
||||
selection="multiple"
|
||||
v-model:selected="selected"
|
||||
:selected-rows-label="
|
||||
(n) =>
|
||||
$t('general.selected', {
|
||||
number: n,
|
||||
msg: $t('general.list'),
|
||||
})
|
||||
"
|
||||
:no-data-label="$t('general.noDataTable')"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||
:props="props"
|
||||
>
|
||||
<q-th v-if="checkable">
|
||||
<q-checkbox
|
||||
v-if="selected.length > 0"
|
||||
:model-value="
|
||||
selected.length ===
|
||||
rows.filter((row) =>
|
||||
listSameArea?.includes(row.quotation.customerBranch.districtId),
|
||||
).length
|
||||
"
|
||||
size="sm"
|
||||
@click="handleCheckAll"
|
||||
/>
|
||||
<div v-else style="width: 35px; height: 35px"></div>
|
||||
</q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label && $t(col.label) }}
|
||||
</q-th>
|
||||
|
|
@ -125,9 +199,30 @@ function getEmployeeName(
|
|||
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||
>
|
||||
<q-tr
|
||||
:class="{ urgent: props.row.quotation.urgent, dark: $q.dark.isActive }"
|
||||
:class="{
|
||||
urgent: props.row.quotation.urgent,
|
||||
dark: $q.dark.isActive,
|
||||
'disabled-row':
|
||||
selected &&
|
||||
selected.length > 0 &&
|
||||
!listSameArea.includes(
|
||||
props.row.quotation.customerBranch.districtId,
|
||||
),
|
||||
}"
|
||||
class="text-center"
|
||||
>
|
||||
<q-td v-if="checkable">
|
||||
<q-checkbox
|
||||
:disable="
|
||||
selected.length > 0 &&
|
||||
!listSameArea.includes(
|
||||
props.row.quotation.customerBranch.districtId,
|
||||
)
|
||||
"
|
||||
v-model="props.selected"
|
||||
size="sm"
|
||||
/>
|
||||
</q-td>
|
||||
<q-td v-if="visibleColumns.includes('order')">
|
||||
{{ props.rowIndex + 1 }}
|
||||
</q-td>
|
||||
|
|
@ -138,21 +233,50 @@ function getEmployeeName(
|
|||
</div>
|
||||
</q-td>
|
||||
<q-td v-if="visibleColumns.includes('employer')" class="text-left">
|
||||
{{
|
||||
getCustomerName(props.row, {
|
||||
noCode: true,
|
||||
locale: $i18n.locale,
|
||||
}) || '-'
|
||||
}}
|
||||
<span
|
||||
class="link"
|
||||
@click="toCustomer(props.row.quotation.customerBranch)"
|
||||
>
|
||||
{{
|
||||
getCustomerName(props.row, {
|
||||
noCode: true,
|
||||
locale: $i18n.locale,
|
||||
}) || '-'
|
||||
}}
|
||||
</span>
|
||||
</q-td>
|
||||
<q-td v-if="visibleColumns.includes('employee')" class="text-left">
|
||||
{{ getEmployeeName(props.row, { locale: $i18n.locale }) || '-' }}
|
||||
<span class="link" @click="toEmployee(props.row.employee)">
|
||||
{{ getEmployeeName(props.row, { locale: $i18n.locale }) || '-' }}
|
||||
</span>
|
||||
</q-td>
|
||||
|
||||
<q-td
|
||||
v-if="visibleColumns.includes('employeePassport')"
|
||||
class="text-left"
|
||||
>
|
||||
{{
|
||||
props.row.employee.employeePassport.length !== 0
|
||||
? props.row.employee.employeePassport[0].number
|
||||
: '-'
|
||||
}}
|
||||
</q-td>
|
||||
<q-td v-if="visibleColumns.includes('dataOffice')" class="text-left">
|
||||
{{
|
||||
$i18n.locale === 'eng'
|
||||
? props.row.dataOffice.nameEN
|
||||
: props.row.dataOffice.name
|
||||
}}
|
||||
</q-td>
|
||||
<q-td v-if="visibleColumns.includes('createdAt')" class="text-left">
|
||||
{{ dateFormatJS({ date: props.row.createdAt }) }}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="visibleColumns.includes('quotationCode')">
|
||||
{{ props.row.quotation.code || '-' }}
|
||||
</q-td>
|
||||
<q-td v-if="visibleColumns.includes('responsiblePerson')">
|
||||
<AvatarGroup
|
||||
<!-- <AvatarGroup
|
||||
:data="
|
||||
responsiblePerson(props.row.quotation)?.map((v) => {
|
||||
return {
|
||||
|
|
@ -168,7 +292,26 @@ function getEmployeeName(
|
|||
};
|
||||
})
|
||||
"
|
||||
/>
|
||||
/> -->
|
||||
<AvatarGroup
|
||||
:data="[
|
||||
...responsiblePerson(props.row.quotation).user.map((v) => ({
|
||||
name:
|
||||
$i18n.locale === 'eng'
|
||||
? `${v.firstNameEN} ${v.lastNameEN}`
|
||||
: `${v.firstName} ${v.lastName}`,
|
||||
imgUrl: !v.selectedImage
|
||||
? v.gender === 'male'
|
||||
? `/no-img-man.png`
|
||||
: `/no-img-female.png`
|
||||
: `${baseUrl}/user/${v.id}/profile-image/${v.selectedImage}`,
|
||||
})),
|
||||
...responsiblePerson(props.row.quotation).group.map((g) => ({
|
||||
name: `${$t('general.group')} ${g.group}`,
|
||||
imgUrl: '/img-group.png',
|
||||
})),
|
||||
]"
|
||||
></AvatarGroup>
|
||||
</q-td>
|
||||
<q-td v-if="visibleColumns.includes('status')">
|
||||
<BadgeComponent
|
||||
|
|
@ -215,6 +358,7 @@ function getEmployeeName(
|
|||
</q-td>
|
||||
<q-td class="text-right">
|
||||
<q-btn
|
||||
v-if="!hideView"
|
||||
:id="`btn-eye-${props.row.code}`"
|
||||
icon="mdi-eye-outline"
|
||||
size="sm"
|
||||
|
|
@ -313,22 +457,29 @@ function getEmployeeName(
|
|||
</div>
|
||||
<div class="col-8">
|
||||
<AvatarGroup
|
||||
v-if="(responsiblePerson(props.row.quotation) ?? []).length > 0"
|
||||
:data="
|
||||
responsiblePerson(props.row.quotation)?.map((v) => {
|
||||
return {
|
||||
name:
|
||||
$i18n.locale === 'eng'
|
||||
? `${v.firstNameEN} ${v.lastNameEN}`
|
||||
: `${v.firstName} ${v.lastName}`,
|
||||
imgUrl: !v.selectedImage
|
||||
? v.gender === 'male'
|
||||
? `/no-img-man.png`
|
||||
: `/no-img-female.png`
|
||||
: `${baseUrl}/user/${v.id}/profile-image/${v.selectedImage}`,
|
||||
};
|
||||
})
|
||||
v-if="
|
||||
(responsiblePerson(props.row.quotation).user ?? []).length >
|
||||
0 ||
|
||||
(responsiblePerson(props.row.quotation).group ?? []).length >
|
||||
0
|
||||
"
|
||||
:data="[
|
||||
...responsiblePerson(props.row.quotation).user.map((v) => ({
|
||||
name:
|
||||
$i18n.locale === 'eng'
|
||||
? `${v.firstNameEN} ${v.lastNameEN}`
|
||||
: `${v.firstName} ${v.lastName}`,
|
||||
imgUrl: !v.selectedImage
|
||||
? v.gender === 'male'
|
||||
? `/no-img-man.png`
|
||||
: `/no-img-female.png`
|
||||
: `${baseUrl}/user/${v.id}/profile-image/${v.selectedImage}`,
|
||||
})),
|
||||
...responsiblePerson(props.row.quotation).group.map((g) => ({
|
||||
name: `${$t('general.group')} ${g.group}`,
|
||||
imgUrl: '/img-group.png',
|
||||
})),
|
||||
]"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
|
|
@ -406,4 +557,15 @@ function getEmployeeName(
|
|||
background: var(--red-8);
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: hsl(var(--info-bg));
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled-row {
|
||||
opacity: 0.3;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,24 @@ export const column = [
|
|||
label: 'customer.employee',
|
||||
field: 'employee',
|
||||
},
|
||||
{
|
||||
name: 'employeePassport',
|
||||
align: 'center',
|
||||
label: 'customerEmployee.form.passportNo',
|
||||
field: 'employeePassport',
|
||||
},
|
||||
{
|
||||
name: 'dataOffice',
|
||||
align: 'center',
|
||||
label: 'requestList.dataOffice',
|
||||
field: 'dataOffice',
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
align: 'center',
|
||||
label: 'general.createdAt',
|
||||
field: 'createdAt',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'quotationCode',
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import useFlowStore from 'src/stores/flow';
|
|||
import { pageTabs, column, pageTabsReceive } from './constants';
|
||||
import { dialogWarningClose, isRoleInclude } from 'src/stores/utils';
|
||||
import { PaginationResult } from 'src/types';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const $q = useQuasar();
|
||||
|
|
@ -48,6 +49,7 @@ const pageState = reactive({
|
|||
isMessenger: isRoleInclude(['messenger']),
|
||||
receiveDialog: false,
|
||||
isReceiveScan: false,
|
||||
searchDate: [],
|
||||
});
|
||||
|
||||
const taskOrderList = ref<TaskOrder[]>([]);
|
||||
|
|
@ -69,6 +71,8 @@ async function fetchTaskOrderList(opts?: { page?: number; pageSize?: number }) {
|
|||
pageSize: opts?.pageSize || pageSize.value,
|
||||
query: pageState.inputSearch === '' ? undefined : pageState.inputSearch,
|
||||
userTaskStatus: pageState.currentTab as UserTaskStatus,
|
||||
startDate: pageState.searchDate[0],
|
||||
endDate: pageState.searchDate[1],
|
||||
});
|
||||
} else {
|
||||
res = await taskOrderStore.getTaskOrderList({
|
||||
|
|
@ -76,6 +80,8 @@ async function fetchTaskOrderList(opts?: { page?: number; pageSize?: number }) {
|
|||
pageSize: opts?.pageSize || pageSize.value,
|
||||
query: pageState.inputSearch === '' ? undefined : pageState.inputSearch,
|
||||
taskOrderStatus: pageState.currentTab as TaskOrderStatus | undefined,
|
||||
startDate: pageState.searchDate[0],
|
||||
endDate: pageState.searchDate[1],
|
||||
});
|
||||
}
|
||||
if (res) {
|
||||
|
|
@ -157,6 +163,7 @@ watch(
|
|||
() => pageState.inputSearch,
|
||||
() => pageSize.value,
|
||||
() => pageState.statusFilter,
|
||||
() => pageState.searchDate,
|
||||
],
|
||||
() => {
|
||||
fetchTaskOrderList();
|
||||
|
|
@ -299,6 +306,10 @@ watch(
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch v-model="pageState.searchDate" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5 justify-end" style="white-space: nowrap">
|
||||
|
|
|
|||
|
|
@ -160,7 +160,12 @@ const emit = defineEmits<{
|
|||
</q-tooltip>
|
||||
</div>
|
||||
<div class="text-caption app-text-muted">
|
||||
{{ props.row.code || '-' }}
|
||||
{{
|
||||
(props.row.taskOrderStatus === TaskOrderStatus.Complete &&
|
||||
props.row.codeProductReceived
|
||||
? props.row.codeProductReceived
|
||||
: props.row.code) || '-'
|
||||
}}
|
||||
</div>
|
||||
</q-td>
|
||||
<q-td v-if="visibleColumns.includes('issueBranch')">
|
||||
|
|
|
|||
|
|
@ -227,6 +227,12 @@ export const productColumn = [
|
|||
label: 'taskOrder.productList',
|
||||
field: 'productList',
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
align: 'center',
|
||||
label: 'general.status',
|
||||
field: 'status',
|
||||
},
|
||||
{
|
||||
name: 'amountOfEmployee',
|
||||
align: 'center',
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ function closeAble() {
|
|||
:branch="branch"
|
||||
:institution="data.institution"
|
||||
:details="{
|
||||
code: data.code,
|
||||
code: data.codeProductReceived ?? data.code,
|
||||
name: data.taskName,
|
||||
contactName: data.contactName,
|
||||
contactTel: data.contactTel,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { baseUrl, formatNumberDecimal, commaInput } from 'src/stores/utils';
|
|||
import { precisionRound } from 'src/utils/arithmetic';
|
||||
import { useConfigStore } from 'stores/config';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||
import { TaskStatus } from 'src/stores/task-order/types';
|
||||
|
||||
const currentBtnOpen = ref<boolean[]>([]);
|
||||
const configStore = useConfigStore();
|
||||
|
|
@ -30,7 +32,10 @@ const props = defineProps<{
|
|||
readonly?: boolean;
|
||||
agentPrice?: boolean;
|
||||
taskList: {
|
||||
product: RequestWork['productService']['product'];
|
||||
product: RequestWork['productService']['product'] & {
|
||||
taskStatus?: TaskStatus;
|
||||
totalNotStatusComplete?: number;
|
||||
};
|
||||
list: RequestWork[];
|
||||
}[];
|
||||
creditNote?: boolean;
|
||||
|
|
@ -111,6 +116,26 @@ function calcPrice(
|
|||
|
||||
return precisionRound(priceNoVat * amount + rawVatTotal);
|
||||
}
|
||||
|
||||
function taskOrderStatus(value: TaskStatus) {
|
||||
if ([TaskStatus.Pending].includes(value)) {
|
||||
return '--blue-6-hsl';
|
||||
}
|
||||
if ([TaskStatus.InProgress, TaskStatus.Validate].includes(value)) {
|
||||
return '--orange-5-hsl';
|
||||
}
|
||||
if (
|
||||
[
|
||||
TaskStatus.Canceled,
|
||||
TaskStatus.Restart,
|
||||
TaskStatus.Redo,
|
||||
TaskStatus.Failed,
|
||||
].includes(value)
|
||||
) {
|
||||
return '--red-5-hsl';
|
||||
}
|
||||
return '--green-8-hsl';
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<q-expansion-item
|
||||
|
|
@ -144,7 +169,8 @@ function calcPrice(
|
|||
(v) =>
|
||||
v.name !== 'discount' &&
|
||||
v.name !== 'priceBeforeVat' &&
|
||||
v.name !== 'vat',
|
||||
v.name !== 'vat' &&
|
||||
v.name !== 'status',
|
||||
)
|
||||
: productColumn
|
||||
"
|
||||
|
|
@ -173,7 +199,10 @@ function calcPrice(
|
|||
<template
|
||||
v-slot:body="props: {
|
||||
row: {
|
||||
product: RequestWork['productService']['product'];
|
||||
product: RequestWork['productService']['product'] & {
|
||||
taskStatus?: TaskStatus;
|
||||
totalNotStatusComplete?: number;
|
||||
};
|
||||
list: RequestWork[];
|
||||
};
|
||||
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||
|
|
@ -203,6 +232,14 @@ function calcPrice(
|
|||
</q-avatar>
|
||||
{{ props.row.product.name }}
|
||||
</q-td>
|
||||
<q-td class="text-left" v-if="!creditNote">
|
||||
<BadgeComponent
|
||||
hide-icon
|
||||
:hsla-color="taskOrderStatus(props.row.product.taskStatus)"
|
||||
:title="`${$t(`taskOrder.status.${props.row.product.taskStatus}`)} ${!!props.row.product.totalNotStatusComplete ? $t('general.totalPeople', { meg: props.row.product.totalNotStatusComplete }) : ''}`"
|
||||
/>
|
||||
</q-td>
|
||||
|
||||
<q-td>
|
||||
{{ props.row.list.length }}
|
||||
</q-td>
|
||||
|
|
|
|||
|
|
@ -280,7 +280,10 @@ let taskListGroup = computed(() => {
|
|||
|
||||
const cacheData = currentFormData.value.taskList.reduce<
|
||||
{
|
||||
product: RequestWork['productService']['product'];
|
||||
product: RequestWork['productService']['product'] & {
|
||||
taskStatus?: TaskStatus;
|
||||
totalNotStatusComplete?: number;
|
||||
};
|
||||
list: (RequestWork & {
|
||||
_template?: {
|
||||
id: string;
|
||||
|
|
@ -289,15 +292,15 @@ let taskListGroup = computed(() => {
|
|||
step: number;
|
||||
responsibleInstitution: (string | { group: string })[];
|
||||
} | null;
|
||||
taskStatus?: TaskStatus;
|
||||
failedComment?: string;
|
||||
failedType?: string;
|
||||
})[];
|
||||
}[]
|
||||
>((acc, curr) => {
|
||||
if (
|
||||
const isNotComplete =
|
||||
fullTaskOrder.value?.taskOrderStatus === TaskOrderStatus.Complete &&
|
||||
curr.taskStatus !== TaskStatus.Complete
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
curr.taskStatus !== TaskStatus.Complete;
|
||||
|
||||
const task = curr.requestWorkStep;
|
||||
const step = curr.step;
|
||||
|
|
@ -308,9 +311,18 @@ let taskListGroup = computed(() => {
|
|||
let exist = acc.find(
|
||||
(item) => task.requestWork.productService.productId == item.product.id,
|
||||
);
|
||||
const record = Object.assign(task.requestWork, {
|
||||
_template: getTemplateData(task.requestWork, step),
|
||||
});
|
||||
|
||||
const record = Object.assign(
|
||||
{
|
||||
...task.requestWork,
|
||||
taskStatus: curr.taskStatus,
|
||||
failedComment: curr.failedComment || '',
|
||||
failedType: curr.failedType || '',
|
||||
},
|
||||
{
|
||||
_template: getTemplateData(task.requestWork, step),
|
||||
},
|
||||
);
|
||||
|
||||
const template = getTemplateData(task.requestWork, step);
|
||||
|
||||
|
|
@ -323,10 +335,18 @@ let taskListGroup = computed(() => {
|
|||
}
|
||||
|
||||
if (exist) {
|
||||
exist.list.push(task.requestWork);
|
||||
exist.list.push(record);
|
||||
if (isNotComplete) {
|
||||
exist.product.totalNotStatusComplete =
|
||||
(exist.product.totalNotStatusComplete || undefined) + 1;
|
||||
}
|
||||
} else {
|
||||
acc.push({
|
||||
product: task.requestWork.productService.product,
|
||||
product: {
|
||||
...task.requestWork.productService.product,
|
||||
taskStatus: curr.taskStatus || TaskStatus.Pending,
|
||||
totalNotStatusComplete: isNotComplete ? 1 : undefined,
|
||||
},
|
||||
list: [record],
|
||||
});
|
||||
}
|
||||
|
|
@ -897,9 +917,14 @@ watch(
|
|||
v-model:registered-branch-id="currentFormData.registeredBranchId"
|
||||
v-model:institution-id="currentFormData.institutionId"
|
||||
v-model:task-name="currentFormData.taskName"
|
||||
v-model:code="currentFormData.code"
|
||||
v-model:contact-name="currentFormData.contactName"
|
||||
v-model:contact-tel="currentFormData.contactTel"
|
||||
:code="
|
||||
view === TaskOrderStatus.Complete &&
|
||||
currentFormData.codeProductReceived
|
||||
? currentFormData.codeProductReceived
|
||||
: currentFormData.code
|
||||
"
|
||||
:task-list-group="
|
||||
taskListGroup.length === 0 && state.mode === 'create'
|
||||
"
|
||||
|
|
@ -985,6 +1010,7 @@ watch(
|
|||
"
|
||||
/>
|
||||
<!-- TODO: blind remark, urgent -->
|
||||
{{ console.log(taskListGroup) }}
|
||||
<RemarkExpansion
|
||||
v-if="
|
||||
view === TaskOrderStatus.Pending ||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// NOTE: Library
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { QSelect, useQuasar } from 'quasar';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
// NOTE: Components
|
||||
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||
|
|
@ -18,13 +18,13 @@ import { columns, hslaColors } from './constants';
|
|||
import useFlowStore from 'src/stores/flow';
|
||||
import { useInvoice } from 'src/stores/payment';
|
||||
import { Invoice, PaymentDataStatus } from 'src/stores/payment/types';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const $q = useQuasar();
|
||||
const navigatorStore = useNavigator();
|
||||
const flowStore = useFlowStore();
|
||||
const invoiceStore = useInvoice();
|
||||
const { data, stats, page, pageMax, pageSize } = storeToRefs(invoiceStore);
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
|
||||
// NOTE: Variable
|
||||
const pageState = reactive({
|
||||
|
|
@ -34,6 +34,7 @@ const pageState = reactive({
|
|||
fieldSelected: [...columns.map((v) => v.name)],
|
||||
gridView: false,
|
||||
total: 0,
|
||||
searchDate: [],
|
||||
});
|
||||
|
||||
const fieldSelectedOption = computed(() => {
|
||||
|
|
@ -56,6 +57,8 @@ async function fetchList(opts?: { rotateFlowId?: boolean }) {
|
|||
: undefined,
|
||||
quotationOnly: true,
|
||||
debitNoteOnly: false,
|
||||
startDate: pageState.searchDate[0],
|
||||
endDate: pageState.searchDate[1],
|
||||
});
|
||||
if (ret) {
|
||||
data.value = $q.screen.xs ? [...data.value, ...ret.result] : ret.result;
|
||||
|
|
@ -89,8 +92,6 @@ function triggerView(opts: { quotationId: string }) {
|
|||
}
|
||||
|
||||
function viewDocExample(quotationId: string, codeInvoice: string) {
|
||||
console.log(codeInvoice);
|
||||
|
||||
localStorage.setItem(
|
||||
'quotation-preview',
|
||||
JSON.stringify({
|
||||
|
|
@ -124,6 +125,7 @@ watch(
|
|||
() => pageState.inputSearch,
|
||||
() => pageState.statusFilter,
|
||||
() => pageSize.value,
|
||||
() => pageState.searchDate,
|
||||
],
|
||||
() => {
|
||||
page.value = 1;
|
||||
|
|
@ -207,26 +209,50 @@ watch(
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch
|
||||
v-model="pageState.searchDate"
|
||||
:active="$q.screen.lt.md && pageState.statusFilter !== 'None'"
|
||||
>
|
||||
<div
|
||||
v-if="$q.screen.lt.md"
|
||||
class="q-mt-sm text-weight-medium"
|
||||
>
|
||||
{{ $t('general.status') }}
|
||||
</div>
|
||||
<q-select
|
||||
v-if="$q.screen.lt.md"
|
||||
v-model="pageState.statusFilter"
|
||||
outlined
|
||||
dense
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
map-options
|
||||
emit-value
|
||||
:for="'field-select-status'"
|
||||
:options="[
|
||||
{
|
||||
label: $t('general.all'),
|
||||
value: 'None',
|
||||
},
|
||||
{
|
||||
label: $t('invoice.status.PaymentWait'),
|
||||
value: PaymentDataStatus.Wait,
|
||||
},
|
||||
{
|
||||
label: $t('invoice.status.PaymentSuccess'),
|
||||
value: PaymentDataStatus.Success,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
</AdvanceSearch>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-if="$q.screen.gt.sm"
|
||||
v-model="pageState.statusFilter"
|
||||
outlined
|
||||
dense
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { pageTabs, columns, hslaColors } from './constants';
|
|||
import { CreditNoteStatus, useCreditNote } from 'src/stores/credit-note';
|
||||
import TableCreditNote from './TableCreditNote.vue';
|
||||
import { dialogWarningClose } from 'src/stores/utils';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const $q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
|
@ -46,6 +47,7 @@ const pageState = reactive({
|
|||
total: 0,
|
||||
|
||||
creditDialog: false,
|
||||
searchDate: [],
|
||||
});
|
||||
|
||||
const fieldSelectedOption = computed(() => {
|
||||
|
|
@ -64,6 +66,8 @@ async function getList(opts?: { page?: number; pageSize?: number }) {
|
|||
pageSize: opts?.pageSize || pageSize.value,
|
||||
query: pageState.inputSearch === '' ? undefined : pageState.inputSearch,
|
||||
creditNoteStatus: pageState.currentTab as CreditNoteStatus | undefined,
|
||||
startDate: pageState.searchDate[0],
|
||||
endDate: pageState.searchDate[1],
|
||||
});
|
||||
|
||||
if (res) {
|
||||
|
|
@ -133,6 +137,7 @@ watch(
|
|||
() => pageState.inputSearch,
|
||||
() => pageSize.value,
|
||||
() => pageState.statusFilter,
|
||||
() => pageState.searchDate,
|
||||
],
|
||||
() => {
|
||||
getList();
|
||||
|
|
@ -228,6 +233,10 @@ watch(
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch v-model="pageState.searchDate" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5 justify-end" style="white-space: nowrap">
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ function calcPricePerUnit(product: RequestWork['productService']['product']) {
|
|||
function calcPrice(
|
||||
product: RequestWork['productService']['product'],
|
||||
amount: number,
|
||||
vat: number = 0,
|
||||
) {
|
||||
const pricePerUnit = agentPrice.value ? product.agentPrice : product.price;
|
||||
|
||||
|
|
@ -256,7 +257,8 @@ function calcPrice(
|
|||
: pricePerUnit;
|
||||
const priceDiscountNoVat = priceNoVat * amount - 0;
|
||||
|
||||
const rawVatTotal = priceDiscountNoVat * (config.value?.vat || 0.07);
|
||||
const rawVatTotal =
|
||||
vat === 0 ? 0 : priceDiscountNoVat * (config.value?.vat || 0.07);
|
||||
|
||||
return precisionRound(priceNoVat * amount + rawVatTotal);
|
||||
}
|
||||
|
|
@ -346,7 +348,7 @@ function closeAble() {
|
|||
<td style="text-align: center">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
calcPrice(v.product.product, v.list.length),
|
||||
calcPrice(v.product.product, v.list.length, v.product.vat),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
|
|
@ -431,7 +433,7 @@ function closeAble() {
|
|||
class="column set-width bg-color full-height"
|
||||
style="padding: 12px"
|
||||
>
|
||||
({{ ThaiBahtText(summaryPrice.finalPrice) }})
|
||||
({{ ThaiBahtText(precisionRound(summaryPrice.finalPrice)) }})
|
||||
</div>
|
||||
<div
|
||||
class="row text-right border-5 items-center"
|
||||
|
|
@ -494,7 +496,7 @@ function closeAble() {
|
|||
details?.worker.map(
|
||||
(v, i) =>
|
||||
`${i + 1}. ` +
|
||||
`${v.namePrefix}. ${v.firstNameEN} ${v.lastNameEN}`.toUpperCase(),
|
||||
`${v.namePrefix}. ${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
|
||||
) || [],
|
||||
},
|
||||
}) || '-'
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { pageTabs, columns, hslaColors } from './constants';
|
|||
import { DebitNoteStatus, useDebitNote } from 'src/stores/debit-note';
|
||||
import { dialogWarningClose } from 'src/stores/utils';
|
||||
import { useQuasar } from 'quasar';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const $q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
|
@ -46,6 +47,7 @@ const pageState = reactive({
|
|||
total: 0,
|
||||
|
||||
debitDialog: false,
|
||||
searchDate: [],
|
||||
});
|
||||
|
||||
const fieldSelectedOption = computed(() => {
|
||||
|
|
@ -68,6 +70,8 @@ async function getList(opts?: { page?: number; pageSize?: number }) {
|
|||
? undefined
|
||||
: pageState.currentTab) as DebitNoteStatus,
|
||||
includeRegisteredBranch: true,
|
||||
startDate: pageState.searchDate[0],
|
||||
endDate: pageState.searchDate[1],
|
||||
});
|
||||
|
||||
if (res) {
|
||||
|
|
@ -149,6 +153,7 @@ watch(
|
|||
() => pageState.inputSearch,
|
||||
() => pageSize.value,
|
||||
() => pageState.statusFilter,
|
||||
() => pageState.searchDate,
|
||||
],
|
||||
() => getList(),
|
||||
);
|
||||
|
|
@ -256,6 +261,10 @@ watch(
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch v-model="pageState.searchDate" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5 justify-end" style="white-space: nowrap">
|
||||
|
|
|
|||
|
|
@ -501,7 +501,7 @@ function print() {
|
|||
details?.worker.map(
|
||||
(v, i) =>
|
||||
`${i + 1}. ` +
|
||||
`${v.namePrefix}. ${v.firstNameEN} ${v.lastNameEN}`.toUpperCase(),
|
||||
`${v.namePrefix}. ${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
|
||||
) || [],
|
||||
},
|
||||
}) || '-'
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ import { columns, hslaColors } from './constants';
|
|||
import useFlowStore from 'src/stores/flow';
|
||||
import { usePayment, useReceipt } from 'src/stores/payment';
|
||||
import { PaymentDataStatus } from 'src/stores/payment/types';
|
||||
import { QSelect, useQuasar } from 'quasar';
|
||||
import { useQuasar } from 'quasar';
|
||||
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
|
||||
|
||||
const $q = useQuasar();
|
||||
const navigatorStore = useNavigator();
|
||||
|
|
@ -26,7 +27,6 @@ const receiptStore = useReceipt();
|
|||
const { data, page, pageMax, pageSize } = storeToRefs(receiptStore);
|
||||
|
||||
// NOTE: Variable
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
|
||||
const pageState = reactive({
|
||||
hideStat: false,
|
||||
|
|
@ -35,6 +35,7 @@ const pageState = reactive({
|
|||
fieldSelected: [...columns.map((v) => v.name)],
|
||||
gridView: false,
|
||||
total: 0,
|
||||
searchDate: [],
|
||||
});
|
||||
|
||||
const fieldSelectedOption = computed(() => {
|
||||
|
|
@ -49,6 +50,8 @@ async function fetchList(opts?: { rotateFlowId?: boolean }) {
|
|||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
query: pageState.inputSearch,
|
||||
startDate: pageState.searchDate[0],
|
||||
endDate: pageState.searchDate[1],
|
||||
});
|
||||
if (ret) {
|
||||
data.value = $q.screen.xs ? [...data.value, ...ret.result] : ret.result;
|
||||
|
|
@ -95,6 +98,7 @@ watch(
|
|||
() => pageState.inputSearch,
|
||||
() => pageState.statusFilter,
|
||||
() => pageSize.value,
|
||||
() => pageState.searchDate,
|
||||
],
|
||||
() => {
|
||||
page.value = 1;
|
||||
|
|
@ -172,25 +176,43 @@ watch(
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
<template v-slot:append>
|
||||
<q-separator vertical inset class="q-mr-xs" />
|
||||
<AdvanceSearch
|
||||
v-model="pageState.searchDate"
|
||||
:active="$q.screen.lt.md && pageState.statusFilter !== 'None'"
|
||||
>
|
||||
<div
|
||||
v-if="$q.screen.lt.md"
|
||||
class="q-mt-sm text-weight-medium"
|
||||
>
|
||||
{{ $t('general.status') }}
|
||||
</div>
|
||||
<q-select
|
||||
v-if="$q.screen.lt.md"
|
||||
ref="refFilter"
|
||||
v-model="pageState.statusFilter"
|
||||
outlined
|
||||
dense
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
map-options
|
||||
emit-value
|
||||
:for="'field-select-status'"
|
||||
:options="[
|
||||
{
|
||||
label: $t('general.all'),
|
||||
value: 'None',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
</AdvanceSearch>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
v-if="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-model="pageState.statusFilter"
|
||||
outlined
|
||||
|
|
|
|||
|
|
@ -155,6 +155,16 @@ const routes: RouteRecordRaw[] = [
|
|||
name: 'ManualView',
|
||||
component: () => import('pages/00_manual/ViewPage.vue'),
|
||||
},
|
||||
{
|
||||
path: '/troubleshooting',
|
||||
name: 'Troubleshooting',
|
||||
component: () => import('pages/00_manual/MainPage.vue'),
|
||||
},
|
||||
{
|
||||
path: '/troubleshooting/:category/:page',
|
||||
name: 'TroubleshootingView',
|
||||
component: () => import('pages/00_manual/ViewPage.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -102,12 +102,25 @@ const useAddressStore = defineStore('api-address', () => {
|
|||
return subDistrict.value[districtId];
|
||||
}
|
||||
|
||||
async function listSameOfficeArea(districtId: string) {
|
||||
const res = await api.post<string[]>(
|
||||
`/employment-office/list-same-office-area`,
|
||||
{ districtId: districtId },
|
||||
);
|
||||
|
||||
if (!res) return false;
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
||||
return {
|
||||
fetchOffice,
|
||||
fetchOfficeById,
|
||||
fetchProvince,
|
||||
fetchDistrictByProvinceId,
|
||||
fetchSubDistrictByProvinceId,
|
||||
|
||||
listSameOfficeArea,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ const useBranchStore = defineStore('api-branch', () => {
|
|||
withHead?: boolean;
|
||||
activeOnly?: boolean;
|
||||
headOfficeId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
},
|
||||
Data extends Pagination<Branch[]>,
|
||||
>(opts?: Options): Promise<Data | false> {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ export async function getCreditNoteList(params?: {
|
|||
pageSize?: number;
|
||||
query?: string;
|
||||
creditNoteStatus?: Status;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const res = await api.get<PaginationResult<Data>>(`/${ENDPOINT}`, {
|
||||
params,
|
||||
|
|
|
|||
|
|
@ -113,6 +113,8 @@ const useCustomerStore = defineStore('api-customer', () => {
|
|||
includeBranch?: boolean;
|
||||
status?: 'CREATED' | 'ACTIVE' | 'INACTIVE';
|
||||
customerType?: CustomerType;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
},
|
||||
Data extends Pagination<
|
||||
(Customer &
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ export async function getDebitNoteList(params?: {
|
|||
query?: string;
|
||||
status?: Status;
|
||||
includeRegisteredBranch?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const res = await api.get<PaginationResult<Data>>(`/${ENDPOINT}`, {
|
||||
params,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ const useEmployeeStore = defineStore('api-employee', () => {
|
|||
customerId?: string;
|
||||
customerBranchId?: string;
|
||||
activeOnly?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
payload?: { passport?: string[] };
|
||||
}) {
|
||||
const { payload, ...params } = opts || {};
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ export const useInstitution = defineStore('institution-store', () => {
|
|||
group?: string;
|
||||
status?: Status;
|
||||
payload?: { group?: string[] };
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
activeOnly?: boolean;
|
||||
}) {
|
||||
const { payload, ...params } = opts || {};
|
||||
|
||||
|
|
@ -72,18 +75,67 @@ export const useInstitution = defineStore('institution-store', () => {
|
|||
}
|
||||
}
|
||||
|
||||
if (res.data.bank && data.bank.length > 0) {
|
||||
for (let i = 0; i < data.bank?.length; i++) {
|
||||
if (data.bank[i].bankQr) {
|
||||
await api
|
||||
.put(
|
||||
`/institution/${res.data.id}/bank-qr/${res.data.bank[i].id}`,
|
||||
data.bank[i].bankQr,
|
||||
{
|
||||
headers: { 'Content-Type': data.bank[i].bankQr?.type },
|
||||
onUploadProgress: (e) => console.log(e),
|
||||
},
|
||||
)
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (res.status < 400) {
|
||||
return res.data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function editInstitution(data: InstitutionPayload & { id: string }) {
|
||||
async function editInstitution(
|
||||
data: InstitutionPayload & { id: string },
|
||||
opts?: { indexDeleteQrCodeBank?: number[] },
|
||||
) {
|
||||
const res = await api.put(`/institution/${data.id}`, {
|
||||
...data,
|
||||
id: undefined,
|
||||
group: undefined,
|
||||
});
|
||||
|
||||
if (!!res.data.bank && !!data.bank.length) {
|
||||
for (let i = 0; i < data.bank?.length; i++) {
|
||||
if (data.bank[i].bankQr) {
|
||||
console.log(i);
|
||||
console.log(data.bank[i].bankQr);
|
||||
await api
|
||||
.put(
|
||||
`/institution/${res.data.id}/bank-qr/${res.data.bank[i].id}`,
|
||||
data.bank[i].bankQr,
|
||||
{
|
||||
headers: { 'Content-Type': data.bank[i].bankQr?.type },
|
||||
onUploadProgress: (e) => console.log(e),
|
||||
},
|
||||
)
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.indexDeleteQrCodeBank && opts.indexDeleteQrCodeBank.length > 0) {
|
||||
console.log('delete');
|
||||
opts.indexDeleteQrCodeBank.forEach(async (i) => {
|
||||
await api
|
||||
.delete(`/institution/${res.data.id}/bank-qr/${res.data.bank[i].id}`)
|
||||
.catch((e) => console.error(e));
|
||||
});
|
||||
}
|
||||
|
||||
if (res.status < 400) {
|
||||
return res.data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ import { getToken } from 'src/services/keycloak';
|
|||
import { Manual } from './types';
|
||||
import { baseUrl } from '../utils';
|
||||
|
||||
const ENDPOINT = 'manual';
|
||||
const MANUAL_ENDPOINT = 'manual';
|
||||
const TROUBLESHOOTING_ENDPOINT = 'troubleshooting';
|
||||
|
||||
export async function getManual() {
|
||||
const res = await api.get<Manual[]>(`/${ENDPOINT}`);
|
||||
const res = await api.get<Manual[]>(`/${MANUAL_ENDPOINT}`);
|
||||
if (res.status < 400) {
|
||||
return res.data;
|
||||
}
|
||||
|
|
@ -20,7 +21,28 @@ export async function getManualByPage(opt: {
|
|||
pageName: string;
|
||||
}) {
|
||||
const res = await fetch(
|
||||
`${baseUrl}/${ENDPOINT}/${opt.category}/page/${opt.pageName}`,
|
||||
`${baseUrl}/${MANUAL_ENDPOINT}/${opt.category}/page/${opt.pageName}`,
|
||||
);
|
||||
if (res.status < 400) {
|
||||
return res;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getTroubleshooting() {
|
||||
const res = await api.get<Manual[]>(`/${TROUBLESHOOTING_ENDPOINT}`);
|
||||
if (res.status < 400) {
|
||||
return res.data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getTroubleshootingByPage(opt: {
|
||||
category: string;
|
||||
pageName: string;
|
||||
}) {
|
||||
const res = await fetch(
|
||||
`${baseUrl}/${TROUBLESHOOTING_ENDPOINT}/${opt.category}/page/${opt.pageName}`,
|
||||
);
|
||||
if (res.status < 400) {
|
||||
return res;
|
||||
|
|
@ -30,11 +52,15 @@ export async function getManualByPage(opt: {
|
|||
|
||||
export const useManualStore = defineStore('manual-store', () => {
|
||||
const dataManual = ref<Manual[]>([]);
|
||||
const dataTroubleshooting = ref<Manual[]>([]);
|
||||
|
||||
return {
|
||||
getManual,
|
||||
getManualByPage,
|
||||
getTroubleshooting,
|
||||
getTroubleshootingByPage,
|
||||
|
||||
dataManual,
|
||||
dataTroubleshooting,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ export const useReceipt = defineStore('receipt-store', () => {
|
|||
debitNoteId?: string;
|
||||
debitNoteOnly?: boolean;
|
||||
quotationOnly?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const res = await api.get<PaginationResult<Receipt>>('/receipt', {
|
||||
params: opts,
|
||||
|
|
@ -162,6 +164,8 @@ export const useInvoice = defineStore('invoice-store', () => {
|
|||
debitNoteOnly?: boolean;
|
||||
quotationId?: string;
|
||||
debitNoteId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const res = await api.get<PaginationResult<Invoice>>('/invoice', {
|
||||
params,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ const useProductServiceStore = defineStore('api-product-service', () => {
|
|||
query?: string;
|
||||
status?: 'CREATED' | 'ACTIVE' | 'INACTIVE';
|
||||
activeOnly?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
|
|
@ -142,6 +144,8 @@ const useProductServiceStore = defineStore('api-product-service', () => {
|
|||
orderField?: string;
|
||||
activeOnly?: boolean;
|
||||
orderBy?: 'asc' | 'desc';
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
|
|
@ -190,6 +194,23 @@ const useProductServiceStore = defineStore('api-product-service', () => {
|
|||
return false;
|
||||
}
|
||||
|
||||
async function importProduct(
|
||||
productGroupId: string,
|
||||
files: File[],
|
||||
fetch: (...args: unknown[]) => void,
|
||||
) {
|
||||
const importTasks = files.map((f) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', f);
|
||||
return api.post('/product/import-product', formData, {
|
||||
params: { productGroupId },
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(importTasks);
|
||||
fetch?.();
|
||||
}
|
||||
|
||||
async function fetchListProductById(productId: string) {
|
||||
const res = await api.get<Product>(`/product/${productId}`);
|
||||
|
||||
|
|
@ -249,6 +270,8 @@ const useProductServiceStore = defineStore('api-product-service', () => {
|
|||
productGroupId?: string;
|
||||
status?: string;
|
||||
fullDetail?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
|
|
@ -543,6 +566,8 @@ const useProductServiceStore = defineStore('api-product-service', () => {
|
|||
fetchImageListById,
|
||||
addImageList,
|
||||
deleteImageByName,
|
||||
|
||||
importProduct,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ export const useProperty = defineStore('property-store', () => {
|
|||
query?: string;
|
||||
status?: Status;
|
||||
activeOnly?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const res = await api.get<PaginationResult<Property>>('/property', {
|
||||
params,
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ export const useQuotationStore = defineStore('quotation-store', () => {
|
|||
includeRegisteredBranch?: boolean;
|
||||
forDebitNote?: boolean;
|
||||
cancelIncludeDebitNote?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const res = await api.get<PaginationResult<Quotation>>('/quotation', {
|
||||
params,
|
||||
|
|
|
|||
|
|
@ -209,6 +209,9 @@ export const useRequestList = defineStore('request-list', () => {
|
|||
requestDataStatus?: RequestDataStatus;
|
||||
responsibleOnly?: boolean;
|
||||
quotationId?: string;
|
||||
incomplete?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const res = await api.get<PaginationResult<RequestData>>('/request-data', {
|
||||
params,
|
||||
|
|
@ -325,6 +328,20 @@ export const useRequestList = defineStore('request-list', () => {
|
|||
return false;
|
||||
}
|
||||
|
||||
async function updateMessenger(
|
||||
requestDataId: string[],
|
||||
defaultMessengerId: string,
|
||||
) {
|
||||
const res = await api.post('/request-data/update-messenger', {
|
||||
requestDataId,
|
||||
defaultMessengerId,
|
||||
});
|
||||
|
||||
if (res.status < 400) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
page,
|
||||
|
|
@ -350,6 +367,8 @@ export const useRequestList = defineStore('request-list', () => {
|
|||
|
||||
rejectRequest,
|
||||
rejectRequestWork,
|
||||
|
||||
updateMessenger,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export type RequestData = {
|
|||
rejectRequestCancel?: boolean;
|
||||
rejectRequestCancelReason?: string;
|
||||
|
||||
defaultMessengerId?: string;
|
||||
|
||||
quotation: QuotationFull & {
|
||||
debitNoteQuotationId: string;
|
||||
isDebitNote: boolean;
|
||||
|
|
@ -26,6 +28,7 @@ export type RequestData = {
|
|||
|
||||
requestWork: RequestWork[];
|
||||
requestDataStatus: RequestDataStatus;
|
||||
dataOffice: { name: string; nameEN: string };
|
||||
};
|
||||
|
||||
export enum RequestDataStatus {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ export const useTaskOrderStore = defineStore('taskorder-store', () => {
|
|||
query?: string;
|
||||
taskOrderStatus?: TaskOrderStatus;
|
||||
assignedUserId?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const res = await api.get<PaginationResult<TaskOrder>>('/task-order', {
|
||||
params,
|
||||
|
|
@ -161,6 +163,8 @@ export const useTaskOrderStore = defineStore('taskorder-store', () => {
|
|||
pageSize?: number;
|
||||
query?: string;
|
||||
userTaskStatus?: UserTaskStatus;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const res = await api.get<PaginationResult<TaskOrder>>('/user-task-order', {
|
||||
params,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export interface TaskOrder {
|
|||
contactName: string;
|
||||
taskOrderStatus: TaskOrderStatus;
|
||||
taskName: string;
|
||||
codeProductReceived?: string;
|
||||
code: string;
|
||||
id: string;
|
||||
userTask: UserTask[];
|
||||
|
|
@ -164,6 +165,8 @@ export interface TaskOrderPayload {
|
|||
requestWorkId: string;
|
||||
requestWorkStep?: Task;
|
||||
taskStatus?: TaskStatus;
|
||||
failedType?: string;
|
||||
failedComment?: string;
|
||||
}[];
|
||||
taskProduct?: {
|
||||
productId: string;
|
||||
|
|
@ -177,6 +180,7 @@ export interface TaskOrderPayload {
|
|||
registeredBranchId?: string;
|
||||
id?: string;
|
||||
code?: string;
|
||||
codeProductReceived?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import axios from 'axios';
|
|||
import useBranchStore from '../branch';
|
||||
import { Branch } from '../branch/types';
|
||||
import { getSignature, setSignature } from './signature';
|
||||
import { getUserId } from 'src/services/keycloak';
|
||||
|
||||
const branchStore = useBranchStore();
|
||||
|
||||
|
|
@ -38,6 +39,14 @@ const useUserStore = defineStore('api-user', () => {
|
|||
|
||||
const data = ref<Pagination<User[]>>();
|
||||
|
||||
async function fetchUserGroup(id?: string) {
|
||||
return await api
|
||||
.get<
|
||||
{ id: string; name: string; path: string }[]
|
||||
>(`/user/${id || getUserId()}/group`)
|
||||
.then((res) => res.data);
|
||||
}
|
||||
|
||||
async function fetchHqOption() {
|
||||
if (userOption.value.hqOpts.length === 0) {
|
||||
const res = await branchStore.fetchList({
|
||||
|
|
@ -168,6 +177,8 @@ const useUserStore = defineStore('api-user', () => {
|
|||
status?: Status;
|
||||
responsibleDistrictId?: string;
|
||||
activeBranchOnly?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const res = await api.get<Pagination<User[]>>('/user', { params: opts });
|
||||
|
||||
|
|
@ -332,6 +343,8 @@ const useUserStore = defineStore('api-user', () => {
|
|||
setSignature,
|
||||
|
||||
typeStats,
|
||||
|
||||
fetchUserGroup,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export type User = {
|
|||
createdBy: string;
|
||||
status: Status;
|
||||
trainingPlace: string | null;
|
||||
importNationality: string | null;
|
||||
importNationality: [] | null;
|
||||
sourceNationality: string | null;
|
||||
licenseExpireDate: Date | null;
|
||||
licenseIssueDate: Date | null;
|
||||
|
|
@ -57,7 +57,8 @@ export type User = {
|
|||
citizenIssue?: Date | null;
|
||||
citizenId: string;
|
||||
branch: Branch[];
|
||||
|
||||
contactName?: string;
|
||||
contactTel?: string;
|
||||
remark?: string;
|
||||
agencyStatus?: AgencyStatus;
|
||||
};
|
||||
|
|
@ -81,7 +82,7 @@ export type UserCreate = {
|
|||
streetEN: string;
|
||||
street: string;
|
||||
trainingPlace?: string | null;
|
||||
importNationality?: string | null;
|
||||
importNationality?: string[] | null;
|
||||
sourceNationality?: string | null;
|
||||
licenseExpireDate?: Date | null;
|
||||
licenseIssueDate?: Date | null;
|
||||
|
|
@ -108,7 +109,8 @@ export type UserCreate = {
|
|||
citizenExpire?: Date | null;
|
||||
citizenIssue?: Date | null;
|
||||
citizenId: string;
|
||||
|
||||
contactName?: string;
|
||||
contactTel?: string;
|
||||
remark?: string;
|
||||
agencyStatus?: AgencyStatus | string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -610,6 +610,26 @@ export function getEmployeeName(
|
|||
}[opts?.locale || 'eng'];
|
||||
}
|
||||
|
||||
export function setPrefixName(
|
||||
record: {
|
||||
namePrefix: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
firstNameEN: string;
|
||||
lastNameEN: string;
|
||||
},
|
||||
opts?: {
|
||||
locale?: string;
|
||||
},
|
||||
) {
|
||||
const data = record;
|
||||
|
||||
return {
|
||||
['eng']: `${typeof data.namePrefix === 'string' ? useOptionStore().mapOption(data.namePrefix) : ''} ${data.firstNameEN} ${data.lastNameEN}`,
|
||||
['tha']: `${typeof data.namePrefix === 'string' ? useOptionStore().mapOption(data.namePrefix) : ''} ${data.firstName} ${data.lastName}`,
|
||||
}[opts?.locale || 'eng'];
|
||||
}
|
||||
|
||||
export function toCamelCase(text: string): string {
|
||||
return text
|
||||
.replace(/[^a-zA-Z0-9]+(.)/g, (match, chr) => chr.toUpperCase())
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { ref } from 'vue';
|
|||
import { defineStore } from 'pinia';
|
||||
import { api } from 'src/boot/axios';
|
||||
import { PaginationResult } from 'src/types';
|
||||
import { WorkflowTemplate, WorkflowTemplatePayload } from './types';
|
||||
import { Group, WorkflowTemplate, WorkflowTemplatePayload } from './types';
|
||||
import { Status } from '../types';
|
||||
|
||||
export const useWorkflowTemplate = defineStore('workflow-store', () => {
|
||||
|
|
@ -25,6 +25,8 @@ export const useWorkflowTemplate = defineStore('workflow-store', () => {
|
|||
query?: string;
|
||||
status?: Status;
|
||||
activeOnly?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const res = await api.get<PaginationResult<WorkflowTemplate>>(
|
||||
'/workflow-template',
|
||||
|
|
@ -36,7 +38,7 @@ export const useWorkflowTemplate = defineStore('workflow-store', () => {
|
|||
return res.data;
|
||||
}
|
||||
|
||||
async function creatWorkflowTemplate(data: WorkflowTemplatePayload) {
|
||||
async function createWorkflowTemplate(data: WorkflowTemplatePayload) {
|
||||
const res = await api.post<WorkflowTemplate>('/workflow-template', data);
|
||||
if (res.status >= 400) return null;
|
||||
return res;
|
||||
|
|
@ -62,6 +64,12 @@ export const useWorkflowTemplate = defineStore('workflow-store', () => {
|
|||
return res;
|
||||
}
|
||||
|
||||
async function getGroupList() {
|
||||
const res = await api.get<Group[]>('/keycloak/group');
|
||||
if (res.status >= 400) return null;
|
||||
return res.data;
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
page,
|
||||
|
|
@ -70,8 +78,9 @@ export const useWorkflowTemplate = defineStore('workflow-store', () => {
|
|||
|
||||
getWorkflowTemplate,
|
||||
getWorkflowTemplateList,
|
||||
creatWorkflowTemplate,
|
||||
createWorkflowTemplate,
|
||||
editWorkflowTemplate,
|
||||
deleteWorkflowTemplate,
|
||||
getGroupList,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export type WorkflowStep = {
|
|||
user: CreatedBy;
|
||||
}[];
|
||||
responsibleInstitution: (string | { group: string })[];
|
||||
responsibleGroup: string[];
|
||||
attributes: WorkFlowAttributes;
|
||||
};
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ export type WorkflowTemplatePayload = {
|
|||
|
||||
export type WorkflowUserInTable = {
|
||||
name: string;
|
||||
responsibleGroup: string[];
|
||||
responsiblePerson: {
|
||||
id: string;
|
||||
selectedImage?: string;
|
||||
|
|
@ -73,6 +75,15 @@ export type WorkFlowPayloadStep = {
|
|||
value?: string[];
|
||||
responsiblePersonId?: string[];
|
||||
responsibleInstitution?: string[];
|
||||
responsibleGroup?: string[];
|
||||
attributes: WorkFlowAttributes;
|
||||
messengerByArea?: boolean;
|
||||
};
|
||||
|
||||
export type Group = {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
subGroupCount: number;
|
||||
subGroups: Group[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { RequestWork } from 'src/stores/request-list';
|
||||
import { TaskStatus } from 'src/stores/task-order/types';
|
||||
import { formatNumberDecimal } from 'src/stores/utils';
|
||||
import { i18n } from 'src/boot/i18n';
|
||||
|
||||
const templates = {
|
||||
'quotation-labor': {
|
||||
|
|
@ -46,7 +48,12 @@ const templates = {
|
|||
converter: (context?: {
|
||||
items?: {
|
||||
product: RequestWork['productService']['product'];
|
||||
list: RequestWork[];
|
||||
list: (RequestWork & {
|
||||
taskStatus?: TaskStatus;
|
||||
failedComment?: string;
|
||||
failedType?: string;
|
||||
codeRequest?: string;
|
||||
})[];
|
||||
}[];
|
||||
itemsDiscount?: {
|
||||
productId: string;
|
||||
|
|
@ -67,8 +74,9 @@ const templates = {
|
|||
const branch = v.request.quotation.customerBranch;
|
||||
return (
|
||||
`${i + 1}. ` +
|
||||
` ${v.request.code}_${branch.customer.customerType === 'PERS' ? `นายจ้าง ${branch.namePrefix}. ${branch.firstNameEN} ${branch.lastNameEN} `.toUpperCase() : branch.registerName}_` +
|
||||
`${employee.namePrefix}. ${employee.firstNameEN} ${employee.lastNameEN} `.toUpperCase() +
|
||||
`(${branch.customer.customerType === 'PERS' ? `นายจ้าง ${branch.namePrefix}. ${branch.firstNameEN} ${branch.lastNameEN} `.toUpperCase() : branch.registerName})`
|
||||
`${!!v.failedType && v.failedType !== 'other' ? `${i18n.global.t(`taskOrder.${v.failedType}`)}` : !!v.failedComment ? v.failedComment : ''}`
|
||||
);
|
||||
});
|
||||
return [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue