Merge branch 'develop'

This commit is contained in:
Methapon2001 2025-04-25 17:30:29 +07:00
commit 18844c70bc
80 changed files with 2772 additions and 869 deletions

View file

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -31,7 +31,7 @@ export default defineConfig((ctx) => {
devServer: {
host: '0.0.0.0',
open: false,
port: 5173,
port: 5174,
},
framework: {
config: {},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -65,6 +65,8 @@ onMounted(async () => {
}
await getSelectedOption();
valueOption.value = selectOptions.value.find((v) => v.id === value.value);
});
</script>
<template>

View file

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

View file

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

View file

@ -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: 'กำหนดช่วงวันที่เอง',
},
};

View file

@ -215,6 +215,10 @@ function initMenu() {
label: 'usage',
route: '/manual',
},
{
label: 'troubleshooting',
route: '/troubleshooting',
},
],
},
];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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