feat: add workflow template fields (#63)
* feat: add type detail * feat: option => agencies type * feat: agencies i18n * fix: workflow => add institution type * refactor: map option specific category, key * feat: select menu with search components * feat: workflow => add field step description & agencies * fix: workflow => table page * refactor: workflow => floating dialog submit --------- Co-authored-by: Methapon Metanipat <methapon@frappet.com>
This commit is contained in:
parent
55f6a5f84e
commit
9708258e7a
9 changed files with 708 additions and 251 deletions
|
|
@ -958,6 +958,45 @@
|
|||
"label": "Processing Fee",
|
||||
"value": "processingFee"
|
||||
}
|
||||
],
|
||||
|
||||
"agenciesType": [
|
||||
{
|
||||
"label": "Agency",
|
||||
"value": "AGE"
|
||||
},
|
||||
{
|
||||
"label": "Department of Employment",
|
||||
"value": "DOE"
|
||||
},
|
||||
{
|
||||
"label": "Dhipaya Insurance",
|
||||
"value": "INS"
|
||||
},
|
||||
{
|
||||
"label": "Embassy",
|
||||
"value": "EMB"
|
||||
},
|
||||
{
|
||||
"label": "Government Agencies",
|
||||
"value": "GA"
|
||||
},
|
||||
{
|
||||
"label": "Hospital",
|
||||
"value": "HOS"
|
||||
},
|
||||
{
|
||||
"label": "Immigration Bureau",
|
||||
"value": "IMB"
|
||||
},
|
||||
{
|
||||
"label": "Immigration Checkpoint",
|
||||
"value": "IMC"
|
||||
},
|
||||
{
|
||||
"label": "Ministry of Labour",
|
||||
"value": "MOL"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
|
@ -1919,6 +1958,45 @@
|
|||
"label": "ค่าดำเนินงาน",
|
||||
"value": "processingFee"
|
||||
}
|
||||
],
|
||||
|
||||
"agenciesType": [
|
||||
{
|
||||
"label": "เอเจนซี่ / หน่วยงาน",
|
||||
"value": "AGE"
|
||||
},
|
||||
{
|
||||
"label": "จัดหางานพื้นที่",
|
||||
"value": "DOE"
|
||||
},
|
||||
{
|
||||
"label": "ทิพยประกันภัย",
|
||||
"value": "INS"
|
||||
},
|
||||
{
|
||||
"label": "สถานทูต",
|
||||
"value": "EMB"
|
||||
},
|
||||
{
|
||||
"label": "หน่วยงานราชการ",
|
||||
"value": "GA"
|
||||
},
|
||||
{
|
||||
"label": "โรงพยาบาล",
|
||||
"value": "HOS"
|
||||
},
|
||||
{
|
||||
"label": "สำนักงานตรวจคนเข้าเมือง",
|
||||
"value": "IMB"
|
||||
},
|
||||
{
|
||||
"label": "ด่านตรวจคนเข้าเมือง",
|
||||
"value": "IMC"
|
||||
},
|
||||
{
|
||||
"label": "กรมแรงงาน",
|
||||
"value": "MOL"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,18 +13,18 @@ import {
|
|||
} from 'src/stores/workflow-template/types';
|
||||
import { User } from 'src/stores/user/types';
|
||||
|
||||
import SelectMenuWithSearch from '../shared/SelectMenuWithSearch.vue';
|
||||
import SelectInput from '../shared/SelectInput.vue';
|
||||
import ToggleButton from 'src/components/button/ToggleButton.vue';
|
||||
import { DeleteButton } from '../button';
|
||||
import NoData from '../NoData.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
onDrawer?: boolean;
|
||||
}>();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const optionStore = useOptionStore();
|
||||
|
||||
const modelByArea = ref<boolean>(false);
|
||||
|
||||
const userInTable = defineModel<WorkflowUserInTable[]>('userInTable', {
|
||||
|
|
@ -40,6 +40,7 @@ const flowData = defineModel<WorkflowTemplatePayload>('flowData', {
|
|||
},
|
||||
});
|
||||
|
||||
const options = ref(optionStore.globalOption?.agenciesType);
|
||||
const role = ref<string[]>([]);
|
||||
const userList = ref<User[]>([]);
|
||||
const responsiblePersonSearch = ref('');
|
||||
|
|
@ -56,6 +57,12 @@ const columns = [
|
|||
label: 'flow.responsiblePerson',
|
||||
field: 'responsiblePerson',
|
||||
},
|
||||
{
|
||||
name: 'responsiblePerson',
|
||||
align: 'center',
|
||||
label: 'general.agencies',
|
||||
field: 'responsiblePerson',
|
||||
},
|
||||
{
|
||||
name: 'action',
|
||||
align: 'right',
|
||||
|
|
@ -112,6 +119,31 @@ function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) {
|
|||
}
|
||||
}
|
||||
|
||||
function selectItem(
|
||||
val: Record<string, unknown>,
|
||||
responsibleInstitution?: string[],
|
||||
) {
|
||||
if (!responsibleInstitution) return;
|
||||
const existIndex = responsibleInstitution.findIndex((p) => p === val.value);
|
||||
if (existIndex === -1) {
|
||||
responsibleInstitution.push(val.value as string);
|
||||
} else {
|
||||
responsibleInstitution.splice(Number(existIndex), 1);
|
||||
}
|
||||
}
|
||||
|
||||
function optionSearch(val: string | null) {
|
||||
if (val === '') {
|
||||
options.value = optionStore.globalOption?.agenciesType;
|
||||
return;
|
||||
}
|
||||
|
||||
const needle = val ? val.toLowerCase() : '';
|
||||
options.value = optionStore.globalOption?.agenciesType.filter(
|
||||
(v: { label: string }) => v.label.toLowerCase().indexOf(needle) > -1,
|
||||
);
|
||||
}
|
||||
|
||||
defineEmits<{
|
||||
(e: 'moveUp'): void;
|
||||
(e: 'moveDown'): void;
|
||||
|
|
@ -163,21 +195,25 @@ onMounted(async () => {
|
|||
</span>
|
||||
</section>
|
||||
|
||||
<section id="form-flow-template" class="col-12 row">
|
||||
<section id="form-flow-template" class="col-12 row q-col-gutter-sm">
|
||||
<SelectInput
|
||||
v-if="role.includes('system')"
|
||||
:readonly
|
||||
v-model="registerBranchId"
|
||||
v-if="role.includes('system')"
|
||||
class="col-12 q-pb-sm"
|
||||
class="col-2"
|
||||
:option="userStore.userOption.hqOpts"
|
||||
:label="$t('branch.form.code')"
|
||||
:rules="[
|
||||
(val: string) =>
|
||||
(role.includes('system') && !!val) || $t('form.error.required'),
|
||||
]"
|
||||
/>
|
||||
<q-input
|
||||
:readonly
|
||||
bg-color="transparent"
|
||||
outlined
|
||||
dense
|
||||
class="col-12"
|
||||
class="col"
|
||||
id="input-flow-template-name"
|
||||
v-model="flowData.name"
|
||||
hide-bottom-space
|
||||
|
|
@ -200,208 +236,220 @@ onMounted(async () => {
|
|||
{{ $t(`flow.processStep`) }}
|
||||
</section>
|
||||
|
||||
<section class="col-12">
|
||||
<q-table
|
||||
id="form-flow-step"
|
||||
flat
|
||||
bordered
|
||||
hide-pagination
|
||||
class="full-width"
|
||||
:columns
|
||||
:rows="flowData.step"
|
||||
:no-data-label="$t('general.noDataTable')"
|
||||
:pagination="{
|
||||
rowsPerPage: 0,
|
||||
}"
|
||||
>
|
||||
<template #header="{ cols }">
|
||||
<q-tr style="background-color: hsla(var(--info-bg) / 0.07)">
|
||||
<q-th v-for="v in cols" :key="v">
|
||||
{{ v.label && $t(v.label, { msg: $t('flow.step') }) }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<section
|
||||
v-if="flowData.step.length === 0"
|
||||
class="col-12 surface-2 rounded bordered column items-center justify-center q-pa-md"
|
||||
>
|
||||
<NoData class="col" />
|
||||
</section>
|
||||
|
||||
<template #body="props">
|
||||
<q-tr>
|
||||
<q-td style="width: 60%">
|
||||
<section class="row items-center">
|
||||
<q-btn
|
||||
:id="`btn-up-${props.rowIndex}`"
|
||||
:for="`btn-up-${props.rowIndex}`"
|
||||
icon="mdi-arrow-up"
|
||||
size="sm"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
:disable="props.rowIndex === 0"
|
||||
style="color: hsl(var(--text-mute-2))"
|
||||
@click.stop="
|
||||
moveItemUp(flowData.step, props.rowIndex);
|
||||
moveItemUp(userInTable, props.rowIndex);
|
||||
"
|
||||
/>
|
||||
<q-btn
|
||||
:id="`btn-down-${props.rowIndex}`"
|
||||
:for="`btn-down-${props.rowIndex}`"
|
||||
icon="mdi-arrow-down"
|
||||
size="sm"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
class="q-mx-sm"
|
||||
:disable="props.rowIndex === flowData.step.length - 1"
|
||||
style="color: hsl(var(--text-mute-2))"
|
||||
@click.stop="
|
||||
moveItemDown(flowData.step, props.rowIndex);
|
||||
moveItemDown(userInTable, props.rowIndex);
|
||||
"
|
||||
/>
|
||||
|
||||
<q-avatar
|
||||
size="md"
|
||||
class="q-mx-lg"
|
||||
style="background-color: var(--surface-tab)"
|
||||
>
|
||||
{{ props.rowIndex + 1 }}
|
||||
</q-avatar>
|
||||
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
:readonly
|
||||
:id="`input-flow-step-name-${props.rowIndex}`"
|
||||
:for="`input-flow-step-name-${props.rowIndex}`"
|
||||
class="col"
|
||||
:placeholder="$t('general.no', { msg: $t('flow.step') })"
|
||||
v-model="props.row.name"
|
||||
hide-bottom-space
|
||||
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||
/>
|
||||
</section>
|
||||
</q-td>
|
||||
|
||||
<q-td>
|
||||
<q-field @click.stop dense outlined :readonly>
|
||||
<span
|
||||
v-if="props.row.responsiblePersonId.length === 0"
|
||||
class="app-text-muted row items-center col"
|
||||
>
|
||||
{{ $t('general.no', { msg: $t('flow.responsiblePerson') }) }}
|
||||
<q-icon name="mdi-chevron-down" class="q-ml-auto" />
|
||||
</span>
|
||||
<div v-else>
|
||||
<div class="row items-center no-wrap">
|
||||
<q-avatar class="q-ml-sm" size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/user/${userInTable[props.rowIndex]?.responsiblePerson[0]?.id}/profile-image/${userInTable[props.rowIndex]?.responsiblePerson[0]?.selectedImage}`"
|
||||
>
|
||||
<template #error>
|
||||
<div
|
||||
class="no-padding full-width full-height flex items-center justify-center"
|
||||
:style="`${userInTable[props.rowIndex]?.responsiblePerson[0].gender ? 'background: white' : 'background: linear-gradient(135deg,rgba(43, 137, 223, 1) 0%, rgba(230, 51, 81, 1) 100%);'}`"
|
||||
<section v-else class="col-12 q-gutter-y-md">
|
||||
<template v-for="(step, index) in flowData.step" :key="index">
|
||||
<div class="bordered rounded">
|
||||
<q-expansion-item
|
||||
for="item-up"
|
||||
id="item-up"
|
||||
dense
|
||||
switch-toggle-side
|
||||
default-opened
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="expansion-rounded surface-2"
|
||||
header-style="border-top-left-radius: var(--radius-2); border-top-right-radius: var(--radius-2)"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<div class="column full-width">
|
||||
<div class="row items-center q-py-sm full-width" @click.stop>
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-work-up-product"
|
||||
for="btn-work-up-product"
|
||||
icon="mdi-arrow-up"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
:disable="index === 0"
|
||||
style="color: hsl(var(--text-mute-2))"
|
||||
@click.stop="
|
||||
moveItemUp(flowData.step, index);
|
||||
moveItemUp(userInTable, index);
|
||||
"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-work-down-product"
|
||||
for="btn-work-down-product"
|
||||
icon="mdi-arrow-down"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
class="q-mx-sm"
|
||||
:disable="index === flowData.step.length - 1"
|
||||
style="color: hsl(var(--text-mute-2))"
|
||||
@click.stop="
|
||||
moveItemDown(flowData.step, index);
|
||||
moveItemDown(userInTable, index);
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
:bg-color="readonly ? 'transparent' : ''"
|
||||
:prefix="`${$t('flow.stepNo')} ${index + 1}: `"
|
||||
dense
|
||||
outlined
|
||||
:readonly
|
||||
:id="`input-flow-step-name-${index}`"
|
||||
:for="`input-flow-step-name-${index}`"
|
||||
class="col q-ml-md"
|
||||
:placeholder="$t('general.no', { msg: $t('flow.step') })"
|
||||
v-model="step.name"
|
||||
hide-bottom-space
|
||||
:rules="[
|
||||
(val: string) => !!val || $t('form.error.required'),
|
||||
]"
|
||||
/>
|
||||
<!-- <div
|
||||
:for="`select-work-name-${index + 1}`"
|
||||
class="col q-py-sm q-px-md"
|
||||
style="background-color: var(--surface-1); z-index: 2"
|
||||
@click="() => (readonly ? '' : fetchListOfWork())"
|
||||
>
|
||||
<span class="text-body2" style="color: var(--foreground)">
|
||||
{{ $t('productService.service.work') }} {{ index + 1 }} :
|
||||
<span class="app-text-muted-2">
|
||||
{{
|
||||
workName
|
||||
? workName
|
||||
: $t('productService.service.workName')
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
<q-menu
|
||||
v-if="!readonly"
|
||||
fit
|
||||
ref="refMenu"
|
||||
self="top left"
|
||||
anchor="bottom left"
|
||||
>
|
||||
<q-item>
|
||||
<div
|
||||
class="full-width flex items-center justify-between"
|
||||
>
|
||||
{{ $t('productService.service.workName') }}
|
||||
<q-btn
|
||||
dense
|
||||
unelevated
|
||||
class="bordered q-px-sm text-capitalize"
|
||||
style="
|
||||
border-radius: var(--radius-2);
|
||||
color: hsl(var(--info-bg));
|
||||
"
|
||||
@click.stop="
|
||||
() => {
|
||||
refMenu.hide();
|
||||
$emit('manageWorkName');
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-img
|
||||
v-if="
|
||||
userInTable[props.rowIndex]
|
||||
?.responsiblePerson[0]?.gender
|
||||
"
|
||||
:src="
|
||||
userInTable[props.rowIndex]
|
||||
?.responsiblePerson[0]?.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(userInTable[props.rowIndex]?.responsiblePerson[0]?.namePrefix || '')} ${
|
||||
$i18n.locale === 'eng'
|
||||
? userInTable[props.rowIndex]
|
||||
?.responsiblePerson[0]?.firstNameEN
|
||||
: userInTable[props.rowIndex]
|
||||
?.responsiblePerson[0]?.firstName
|
||||
} ${
|
||||
$i18n.locale === 'eng'
|
||||
? userInTable[props.rowIndex]
|
||||
?.responsiblePerson[0]?.lastNameEN
|
||||
: userInTable[props.rowIndex]
|
||||
?.responsiblePerson[0]?.lastName
|
||||
}`
|
||||
}}
|
||||
</span>
|
||||
<span class="text-caption app-text-muted">
|
||||
{{
|
||||
userInTable[props.rowIndex]?.responsiblePerson[0]
|
||||
?.code
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="q-pl-md"
|
||||
v-if="props.row.responsiblePersonId.length > 1"
|
||||
>
|
||||
({{ props.row.responsiblePersonId.length }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<q-menu v-if="!readonly" :offset="[0, 4]">
|
||||
<q-list>
|
||||
<q-item>
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="col responsible-search"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="responsiblePersonSearch"
|
||||
debounce="200"
|
||||
<q-icon name="mdi-cog" size="xs" class="q-mr-sm" />
|
||||
{{ $t('general.manage') }}
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-item>
|
||||
<q-item
|
||||
@click="workName = item.name"
|
||||
clickable
|
||||
v-for="(item, index) in workNameItems"
|
||||
:key="index"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item>
|
||||
<span class="text-caption app-text-muted-2 q-px-md">
|
||||
{{ $t('general.people') }}
|
||||
</span>
|
||||
<div class="full-width flex items-center">
|
||||
<q-icon
|
||||
v-if="workName === item.name"
|
||||
name="mdi-checkbox-marked"
|
||||
size="xs"
|
||||
color="primary"
|
||||
class="q-mr-sm"
|
||||
/>
|
||||
<q-icon
|
||||
v-else
|
||||
name="mdi-checkbox-blank-outline"
|
||||
size="xs"
|
||||
style="color: hsl(var(--text-mute))"
|
||||
class="q-mr-sm"
|
||||
/>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</q-item>
|
||||
</q-menu>
|
||||
</div> -->
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-delete-work"
|
||||
icon="mdi-trash-can-outline"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
padding="0"
|
||||
class="q-ml-md"
|
||||
color="negative"
|
||||
@click="deleteItem(flowData.step, index)"
|
||||
>
|
||||
<q-tooltip>{{ $t('general.delete') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<q-item
|
||||
v-for="(person, i) in userList"
|
||||
:key="i"
|
||||
clickable
|
||||
class="column"
|
||||
@click.stop="
|
||||
selectResponsiblePerson(props.rowIndex, person)
|
||||
"
|
||||
<section class="q-px-md surface-2 q-py-sm">
|
||||
<div
|
||||
:class="{ 'surface-1 rounded bordered': readonly }"
|
||||
style="border: 1px solid transparent"
|
||||
>
|
||||
<div class="q-col-gutter-sm row">
|
||||
<q-input
|
||||
:bg-color="readonly ? 'transparent' : ''"
|
||||
:readonly
|
||||
v-model="step.detail"
|
||||
class="col-12"
|
||||
type="textarea"
|
||||
dense
|
||||
outlined
|
||||
:label="$t('general.detail')"
|
||||
/>
|
||||
|
||||
<q-field
|
||||
:bg-color="readonly ? 'transparent' : ''"
|
||||
v-if="step.responsiblePersonId"
|
||||
stack-label
|
||||
:label="
|
||||
step.responsiblePersonId.length > 0
|
||||
? $t('flow.responsiblePerson')
|
||||
: undefined
|
||||
"
|
||||
class="col-6"
|
||||
@click.stop
|
||||
dense
|
||||
outlined
|
||||
:readonly
|
||||
>
|
||||
<span
|
||||
v-if="step.responsiblePersonId.length === 0"
|
||||
class="app-text-muted row items-center col"
|
||||
>
|
||||
<div class="row items-center no-wrap">
|
||||
<q-checkbox
|
||||
size="xs"
|
||||
:model-value="
|
||||
props.row.responsiblePersonId.includes(person.id)
|
||||
"
|
||||
@click.stop="
|
||||
selectResponsiblePerson(props.rowIndex, person)
|
||||
"
|
||||
/>
|
||||
{{
|
||||
$t('general.no', { msg: $t('flow.responsiblePerson') })
|
||||
}}
|
||||
<q-icon
|
||||
v-if="!readonly"
|
||||
name="mdi-menu-down"
|
||||
size="sm"
|
||||
class="q-ml-auto"
|
||||
/>
|
||||
</span>
|
||||
<div v-else>
|
||||
<div
|
||||
class="row items-center no-wrap"
|
||||
v-for="person in userInTable[index]?.responsiblePerson"
|
||||
:key="person.id"
|
||||
>
|
||||
<q-avatar class="q-ml-sm" size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
|
|
@ -431,7 +479,10 @@ onMounted(async () => {
|
|||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
<div class="column q-pl-md">
|
||||
<div
|
||||
class="column q-pl-md"
|
||||
style="color: var(--foreground)"
|
||||
>
|
||||
<span>
|
||||
{{
|
||||
`${optionStore.mapOption(person.namePrefix || '')} ${
|
||||
|
|
@ -450,53 +501,217 @@ onMounted(async () => {
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-item>
|
||||
</div>
|
||||
<q-menu v-if="!readonly" :offset="[0, 4]">
|
||||
<q-list>
|
||||
<q-item>
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="col responsible-search"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="responsiblePersonSearch"
|
||||
debounce="200"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item>
|
||||
<span class="text-caption app-text-muted-2 q-px-md">
|
||||
{{ $t('general.people') }}
|
||||
</span>
|
||||
|
||||
<span class="text-caption app-text-muted-2 q-px-md">
|
||||
{{ $t('general.group') }}
|
||||
</span>
|
||||
<q-item clickable class="column">
|
||||
<div class="row items-center">
|
||||
<q-checkbox :model-value="false" size="xs"></q-checkbox>
|
||||
<q-avatar class="q-ml-sm" size="md">
|
||||
<q-img :src="`/images/employee-avatar.png`" />
|
||||
</q-avatar>
|
||||
<div class="column q-pl-md">
|
||||
<span>กลุ่มคาโมมายด์ (Mocking)</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-for="(person, i) in userList"
|
||||
dense
|
||||
:key="i"
|
||||
clickable
|
||||
class="column"
|
||||
@click.stop="selectResponsiblePerson(index, person)"
|
||||
>
|
||||
<div class="row items-center no-wrap">
|
||||
<q-checkbox
|
||||
size="xs"
|
||||
:model-value="
|
||||
step.responsiblePersonId.includes(person.id)
|
||||
"
|
||||
@click.stop="
|
||||
selectResponsiblePerson(index, person)
|
||||
"
|
||||
/>
|
||||
<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">
|
||||
<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>
|
||||
</q-item>
|
||||
|
||||
<span class="text-caption app-text-muted-2 q-px-md">
|
||||
{{ $t('general.area') }}
|
||||
</span>
|
||||
<q-item clickable class="column">
|
||||
<div class="row items-center">
|
||||
<span class="text-caption app-text-muted-2 q-px-md">
|
||||
{{ $t('general.group') }}
|
||||
</span>
|
||||
<q-item clickable class="column">
|
||||
<div class="row items-center">
|
||||
<q-checkbox
|
||||
:model-value="false"
|
||||
size="xs"
|
||||
></q-checkbox>
|
||||
<q-avatar class="q-ml-sm" size="md">
|
||||
<q-img :src="`/images/employee-avatar.png`" />
|
||||
</q-avatar>
|
||||
<div class="column q-pl-md">
|
||||
<span>กลุ่ม 1 (Mocking)</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-item>
|
||||
|
||||
<span class="text-caption app-text-muted-2 q-px-md">
|
||||
{{ $t('general.area') }}
|
||||
</span>
|
||||
<q-item
|
||||
clickable
|
||||
@click="modelByArea = !modelByArea"
|
||||
class="column"
|
||||
>
|
||||
<div class="row items-center">
|
||||
<q-checkbox
|
||||
v-model="modelByArea"
|
||||
size="xs"
|
||||
></q-checkbox>
|
||||
<div class="column q-pl-md">
|
||||
<span>{{ $t('general.byArea') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-field>
|
||||
|
||||
<q-select
|
||||
:bg-color="readonly ? 'transparent' : ''"
|
||||
:readonly
|
||||
outlined
|
||||
dense
|
||||
v-model="step.responsibleInstitution"
|
||||
multiple
|
||||
:options="options"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
emit-value
|
||||
:label="
|
||||
$t('general.select', { msg: $t('general.agencies') })
|
||||
"
|
||||
class="col-6"
|
||||
:hide-dropdown-icon="readonly"
|
||||
>
|
||||
<template v-slot:selected-item="scope">
|
||||
<q-chip
|
||||
dense
|
||||
removable
|
||||
@remove="scope.removeAtIndex(scope.index)"
|
||||
>
|
||||
<span class="text-caption">
|
||||
{{ optionStore.mapOption(scope.opt, 'agenciesType') }}
|
||||
</span>
|
||||
</q-chip>
|
||||
</template>
|
||||
<template v-slot:option></template>
|
||||
|
||||
<SelectMenuWithSearch
|
||||
v-if="!readonly"
|
||||
:title="
|
||||
$t('general.select', { msg: $t('general.agencies') })
|
||||
"
|
||||
:option="options"
|
||||
width="353.66px"
|
||||
@search="(v) => optionSearch(v as string)"
|
||||
@select="
|
||||
(v) => selectItem(v, step.responsibleInstitution)
|
||||
"
|
||||
@before-show="
|
||||
options = optionStore.globalOption?.agenciesType
|
||||
"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-separator></q-separator>
|
||||
</template>
|
||||
<template
|
||||
#option="{ opt }"
|
||||
v-if="step.responsibleInstitution"
|
||||
>
|
||||
<q-checkbox
|
||||
v-model="modelByArea"
|
||||
:model-value="
|
||||
step.responsibleInstitution.some(
|
||||
(v: string) => v === opt.value,
|
||||
)
|
||||
"
|
||||
class="q-pr-sm"
|
||||
size="xs"
|
||||
></q-checkbox>
|
||||
<div class="column q-pl-md">
|
||||
<span>{{ $t('general.byArea') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-field>
|
||||
</q-td>
|
||||
|
||||
<q-td style="width: 10%">
|
||||
<DeleteButton
|
||||
v-if="!readonly"
|
||||
icon-only
|
||||
class="q-ml-auto"
|
||||
@click="deleteItem(flowData.step, props.rowIndex)"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
@click="selectItem(opt, step.responsibleInstitution)"
|
||||
/>
|
||||
<span
|
||||
:class="{
|
||||
'app-text-info': step.responsibleInstitution.some(
|
||||
(v: string) => v === opt.value,
|
||||
),
|
||||
}"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</span>
|
||||
</template>
|
||||
</SelectMenuWithSearch>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</q-expansion-item>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -506,4 +721,21 @@ onMounted(async () => {
|
|||
height: 36px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon) {
|
||||
color: hsl(var(--text-mute));
|
||||
}
|
||||
|
||||
:deep(
|
||||
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
|
||||
) {
|
||||
color: var(--brand-1);
|
||||
}
|
||||
|
||||
:deep(
|
||||
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.expansion-rounded.surface-2
|
||||
.q-focus-helper
|
||||
) {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
105
src/components/shared/SelectMenuWithSearch.vue
Normal file
105
src/components/shared/SelectMenuWithSearch.vue
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
|
||||
title?: string;
|
||||
width?: string;
|
||||
offset?: number[];
|
||||
|
||||
option: Record<string, unknown>[];
|
||||
optionLabel?: string;
|
||||
}>(),
|
||||
{
|
||||
readonly: false,
|
||||
option: () => [],
|
||||
optionLabel: 'label',
|
||||
offset: () => [0, 12],
|
||||
},
|
||||
);
|
||||
|
||||
const inputSearch = ref('');
|
||||
|
||||
defineEmits<{
|
||||
(e: 'search', value: string | number | null): void;
|
||||
(e: 'select', value: Record<string, unknown>): void;
|
||||
(e: 'hide'): void;
|
||||
(e: 'show'): void;
|
||||
(e: 'beforeHide'): void;
|
||||
(e: 'beforeShow'): void;
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<q-menu
|
||||
v-if="!readonly"
|
||||
:offset
|
||||
class="bordered"
|
||||
:style="`width: ${width}`"
|
||||
@show="$emit('show')"
|
||||
@hide="$emit('hide')"
|
||||
@before-show="
|
||||
() => {
|
||||
inputSearch = '';
|
||||
$emit('beforeShow');
|
||||
}
|
||||
"
|
||||
@before-hide="$emit('beforeHide')"
|
||||
>
|
||||
<div
|
||||
v-if="title"
|
||||
class="column no-padding"
|
||||
style="padding: 16px !important"
|
||||
>
|
||||
<span class="text-weight-bold q-pb-sm">
|
||||
{{ title }}
|
||||
</span>
|
||||
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="col responsible-search"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="inputSearch"
|
||||
debounce="200"
|
||||
@update:model-value="(val) => $emit('search', val)"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<template v-if="$slots.prepend">
|
||||
<slot name="prepend"></slot>
|
||||
</template>
|
||||
|
||||
<span v-if="option.length === 0">
|
||||
<q-item dense class="flex items-center app-text-muted">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item>
|
||||
</span>
|
||||
<template v-if="option.length > 0">
|
||||
<q-item
|
||||
v-for="(opt, i) in option"
|
||||
dense
|
||||
:key="i"
|
||||
class="flex items-center"
|
||||
clickable
|
||||
@click.stop="$emit('select', opt)"
|
||||
>
|
||||
<template v-if="$slots.option">
|
||||
<slot name="option" :opt="opt"></slot>
|
||||
</template>
|
||||
|
||||
<span v-if="!$slots.option" class="row items-center">
|
||||
{{ opt.label }}
|
||||
</span>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-menu>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
|
|
@ -120,6 +120,7 @@ export default {
|
|||
area: 'Area by area',
|
||||
byArea: 'By area',
|
||||
company: 'Company',
|
||||
agencies: 'Agencies',
|
||||
form: 'Form',
|
||||
designForm: 'design a form',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ export default {
|
|||
area: 'เขตพื้นที่',
|
||||
byArea: 'ตามเขตพื้นที่',
|
||||
company: 'บริษัท',
|
||||
agencies: 'หน่วยงาน',
|
||||
form: 'แบบฟอร์ม',
|
||||
designForm: 'ออกแบบฟอร์ม',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -52,8 +52,10 @@ defineEmits<{
|
|||
|
||||
async function addStep() {
|
||||
flowData.value.step.push({
|
||||
responsibleInstitution: [],
|
||||
responsiblePersonId: [],
|
||||
value: [],
|
||||
detail: '',
|
||||
name: '',
|
||||
});
|
||||
|
||||
|
|
@ -71,6 +73,7 @@ async function addStep() {
|
|||
v-model:modal="model"
|
||||
:submit="() => $emit('submit')"
|
||||
:close="() => $emit('close')"
|
||||
hide-footer
|
||||
>
|
||||
<div
|
||||
class="col surface-1 rounded bordered scroll row relative-position"
|
||||
|
|
@ -79,6 +82,18 @@ async function addStep() {
|
|||
'q-mx-md q-my-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="rounded row"
|
||||
style="position: absolute; z-index: 999; right: 0; top: 0"
|
||||
:class="{
|
||||
'q-py-md q-px-lg': $q.screen.gt.sm,
|
||||
'q-py-sm q-px-lg': !$q.screen.gt.sm,
|
||||
}"
|
||||
>
|
||||
<div class="surface-1 row rounded">
|
||||
<SaveButton id="btn-info-basic-save" icon-only type="submit" />
|
||||
</div>
|
||||
</div>
|
||||
<section
|
||||
class="col"
|
||||
style="height: 100%; max-height: 100; overflow-y: auto"
|
||||
|
|
|
|||
|
|
@ -211,9 +211,10 @@ function assignFormData(workflowData: WorkflowTemplate) {
|
|||
return {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
// type: s.type,
|
||||
detail: s.detail,
|
||||
value: s.value || [],
|
||||
responsiblePersonId: s.responsiblePerson.map((p) => p.userId),
|
||||
responsibleInstitution: s.responsibleInstitution,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
|
@ -395,6 +396,7 @@ watch(() => pageState.inputSearch, fetchWorkflowList);
|
|||
hide-pagination
|
||||
:columns="columns"
|
||||
:rows="workflowData"
|
||||
:rows-per-page-options="[0]"
|
||||
>
|
||||
<template #header="{ cols }">
|
||||
<q-tr style="background-color: hsla(var(--info-bg) / 0.07)">
|
||||
|
|
@ -428,7 +430,6 @@ watch(() => pageState.inputSearch, fetchWorkflowList);
|
|||
'status-active': props.row.status !== 'INACTIVE',
|
||||
'status-inactive': props.row.status === 'INACTIVE',
|
||||
}"
|
||||
:props="props"
|
||||
:style="
|
||||
props.rowIndex % 2 !== 0
|
||||
? $q.dark.isActive
|
||||
|
|
@ -552,3 +553,16 @@ watch(() => pageState.inputSearch, fetchWorkflowList);
|
|||
v-model:register-branch-id="registeredBranchId"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.status-active {
|
||||
--_branch-status-color: var(--green-6-hsl);
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
--_branch-status-color: var(--stone-5-hsl);
|
||||
--_branch-badge-bg: var(--stone-5-hsl);
|
||||
filter: grayscale(0.5);
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,13 @@ const useOptionStore = defineStore('optionStore', () => {
|
|||
|
||||
const globalOption = ref();
|
||||
const rawOption = ref();
|
||||
function mapOption(value: string) {
|
||||
function mapOption(value: string, categoryKey?: string) {
|
||||
if (categoryKey) {
|
||||
const option = globalOption.value[categoryKey].find(
|
||||
(opt: { value: string }) => opt.value === value,
|
||||
);
|
||||
if (option) return option.label;
|
||||
}
|
||||
for (const category in globalOption.value) {
|
||||
if (globalOption.value.hasOwnProperty(category)) {
|
||||
const option = globalOption.value[category].find(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export type WorkflowStep = {
|
|||
id: string;
|
||||
order: number;
|
||||
name: string;
|
||||
detail?: string;
|
||||
type: string | null;
|
||||
|
||||
// NOTE: for list like type only
|
||||
|
|
@ -14,11 +15,13 @@ export type WorkflowStep = {
|
|||
userId: string;
|
||||
user: CreatedBy;
|
||||
}[];
|
||||
responsibleInstitution: string[];
|
||||
};
|
||||
|
||||
export type WorkflowTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
detail: string | null;
|
||||
registeredBranchId: string;
|
||||
step: WorkflowStep[];
|
||||
status: Status;
|
||||
|
|
@ -36,8 +39,10 @@ export type WorkflowTemplatePayload = {
|
|||
step: {
|
||||
name: string;
|
||||
type?: string | null;
|
||||
detail?: string | null;
|
||||
value?: string[];
|
||||
responsiblePersonId?: string[];
|
||||
responsibleInstitution?: string[];
|
||||
}[];
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue