jws-frontend/src/components/04_flow-management/FormFlow.vue

742 lines
27 KiB
Vue
Raw Normal View History

2024-10-24 17:33:33 +07:00
<script lang="ts" setup>
2024-10-25 16:06:13 +07:00
import { onMounted, ref, watch } from 'vue';
2024-10-24 17:33:33 +07:00
import { QTableProps } from 'quasar';
import { moveItemUp, moveItemDown, deleteItem } from 'src/stores/utils';
2024-10-25 16:06:13 +07:00
import useUserStore from 'src/stores/user';
import useOptionStore from 'src/stores/options';
import { baseUrl } from 'stores/utils';
import { getRole } from 'src/services/keycloak';
2024-10-28 11:04:58 +07:00
import {
WorkflowUserInTable,
WorkflowTemplatePayload,
} from 'src/stores/workflow-template/types';
2024-10-25 16:06:13 +07:00
import { User } from 'src/stores/user/types';
import SelectMenuWithSearch from '../shared/SelectMenuWithSearch.vue';
2024-10-25 16:06:13 +07:00
import SelectInput from '../shared/SelectInput.vue';
2024-10-24 17:33:33 +07:00
import ToggleButton from 'src/components/button/ToggleButton.vue';
import NoData from '../NoData.vue';
2024-10-24 17:33:33 +07:00
defineProps<{
2024-10-28 11:04:58 +07:00
readonly?: boolean;
onDrawer?: boolean;
}>();
2024-10-25 16:06:13 +07:00
const userStore = useUserStore();
const optionStore = useOptionStore();
2024-11-04 15:41:05 +07:00
const modelByArea = ref<boolean>(false);
2024-11-04 14:44:12 +07:00
2024-10-28 11:04:58 +07:00
const userInTable = defineModel<WorkflowUserInTable[]>('userInTable', {
default: [],
});
2024-10-25 16:06:13 +07:00
const registerBranchId = defineModel('registerBranchId', { default: '' });
const flowData = defineModel<WorkflowTemplatePayload>('flowData', {
2024-10-24 17:33:33 +07:00
required: true,
default: {
2024-10-28 11:04:58 +07:00
status: 'CREATED',
2024-10-24 17:33:33 +07:00
name: '',
step: [],
},
});
const options = ref(optionStore.globalOption?.agenciesType);
2024-10-25 16:06:13 +07:00
const role = ref<string[]>([]);
const userList = ref<User[]>([]);
2024-10-24 17:33:33 +07:00
const responsiblePersonSearch = ref('');
const columns = [
{
name: 'detail',
align: 'center',
label: 'general.detail',
field: 'detail',
},
{
name: 'responsiblePerson',
align: 'center',
label: 'flow.responsiblePerson',
field: 'responsiblePerson',
},
{
name: 'responsiblePerson',
align: 'center',
label: 'general.agencies',
field: 'responsiblePerson',
},
2024-10-24 17:33:33 +07:00
{
name: 'action',
align: 'right',
label: '',
field: 'action',
},
] satisfies QTableProps['columns'];
2024-10-25 16:06:13 +07:00
async function getUserList(opts?: { query: string }) {
const resUser = await userStore.fetchList({
query: !!opts?.query ? opts.query : undefined,
});
if (resUser) userList.value = resUser.result;
}
2024-10-28 11:04:58 +07:00
// async function getUserById(responsiblePersonId: string) {
// const resUser = await userStore.fetchById(responsiblePersonId);
// if (resUser) userInTable.value.push(resUser);
// }
2024-10-25 16:06:13 +07:00
function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) {
2024-10-25 16:06:13 +07:00
const currStep = flowData.value.step[stepIndex];
const existPersonIndex = currStep.responsiblePersonId?.findIndex(
(p) => p === responsiblePerson.id,
);
if (existPersonIndex === -1) {
currStep.responsiblePersonId?.push(responsiblePerson.id);
2024-10-28 11:04:58 +07:00
if (!userInTable.value[stepIndex]) {
userInTable.value[stepIndex] = {
name: flowData.value.step[stepIndex].name,
responsiblePerson: [],
2024-10-28 11:04:58 +07:00
};
}
userInTable.value[stepIndex]?.responsiblePerson.push({
2024-10-28 11:04:58 +07:00
id: responsiblePerson.id,
selectedImage: responsiblePerson.selectedImage,
gender: responsiblePerson.gender,
namePrefix: responsiblePerson.namePrefix,
firstName: responsiblePerson.firstName,
lastName: responsiblePerson.lastName,
firstNameEN: responsiblePerson.firstNameEN,
lastNameEN: responsiblePerson.lastNameEN,
code: responsiblePerson.code,
});
2024-10-25 16:06:13 +07:00
} else {
currStep.responsiblePersonId?.splice(Number(existPersonIndex), 1);
userInTable.value[stepIndex]?.responsiblePerson.splice(
2024-10-28 11:04:58 +07:00
Number(existPersonIndex),
1,
);
2024-10-25 16:06:13 +07:00
}
}
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,
);
}
2024-10-24 17:33:33 +07:00
defineEmits<{
(e: 'moveUp'): void;
(e: 'moveDown'): void;
2024-10-28 11:04:58 +07:00
(e: 'changeStatus'): void;
2024-10-24 17:33:33 +07:00
}>();
2024-10-25 16:06:13 +07:00
watch(
responsiblePersonSearch,
async () => await getUserList({ query: responsiblePersonSearch.value }),
);
onMounted(async () => {
role.value = getRole() || [];
2024-10-28 11:04:58 +07:00
await getUserList();
2024-10-25 16:06:13 +07:00
await userStore.fetchHqOption();
});
2024-10-24 17:33:33 +07:00
</script>
<template>
<div class="row col-12">
<section
class="col-12 q-pb-sm text-weight-bold text-body1 row items-center"
>
<q-icon
flat
size="xs"
class="q-pa-sm rounded q-mr-xs"
color="info"
name="mdi-office-building-outline"
style="background-color: var(--surface-3)"
/>
{{ $t(`general.name`, { msg: $t('flow.title') }) }}
2024-10-28 11:04:58 +07:00
<span class="row q-ml-lg items-center text-weight-regular text-body2">
<ToggleButton
class="q-mr-sm"
two-way
:model-value="flowData.status !== 'INACTIVE'"
@click="
() => {
onDrawer
? $emit('changeStatus')
: flowData.status !== 'INACTIVE'
? (flowData.status = 'INACTIVE')
: (flowData.status = 'CREATED');
}
"
/>
2024-10-24 17:33:33 +07:00
{{ $t('status.title') }}
</span>
</section>
<section id="form-flow-template" class="col-12 row q-col-gutter-sm">
2024-10-25 16:06:13 +07:00
<SelectInput
v-if="role.includes('system')"
2024-10-28 11:04:58 +07:00
:readonly
2024-10-25 16:06:13 +07:00
v-model="registerBranchId"
class="col-2"
2024-10-25 16:06:13 +07:00
:option="userStore.userOption.hqOpts"
:label="$t('branch.form.code')"
:rules="[
(val: string) =>
(role.includes('system') && !!val) || $t('form.error.required'),
]"
2024-10-25 16:06:13 +07:00
/>
2024-10-24 17:33:33 +07:00
<q-input
2024-10-28 11:04:58 +07:00
:readonly
bg-color="transparent"
2024-10-24 17:33:33 +07:00
outlined
dense
class="col"
id="input-flow-template-name"
2024-10-24 17:33:33 +07:00
v-model="flowData.name"
hide-bottom-space
2024-10-24 17:33:33 +07:00
:label="$t(`general.name`, { msg: $t('flow.step') })"
:rules="[(val: string) => !!val || $t('form.error.required')]"
2024-10-25 16:06:13 +07:00
/>
2024-10-24 17:33:33 +07:00
</section>
<section
class="col-12 q-pb-sm q-pt-lg text-weight-bold text-body1 row items-center"
>
<q-icon
flat
size="xs"
class="q-pa-sm rounded q-mr-xs"
color="info"
name="mdi-office-building-outline"
style="background-color: var(--surface-3)"
/>
{{ $t(`flow.processStep`) }}
</section>
<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>
<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">
2024-10-28 11:04:58 +07:00
{{
workName
? workName
: $t('productService.service.workName')
2024-10-28 11:04:58 +07:00
}}
2024-10-25 16:06:13 +07:00
</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-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"
2024-10-24 17:33:33 +07:00
>
<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>
2024-10-24 17:33:33 +07:00
<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"
2024-10-25 16:06:13 +07:00
>
{{
$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"
>
2024-10-24 17:33:33 +07:00
<q-avatar class="q-ml-sm" size="md">
2024-10-25 16:06:13 +07:00
<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>
2024-10-24 17:33:33 +07:00
</q-avatar>
<div
class="column q-pl-md"
style="color: var(--foreground)"
>
2024-10-25 16:06:13 +07:00
<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>
2024-10-24 17:33:33 +07:00
</div>
</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"
>
<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>
2024-10-24 17:33:33 +07:00
<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>
2024-11-04 14:44:12 +07:00
<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"
>
2024-11-04 14:44:12 +07:00
<q-checkbox
:model-value="
step.responsibleInstitution.some(
(v: string) => v === opt.value,
)
"
class="q-pr-sm"
2024-11-04 14:44:12 +07:00
size="xs"
@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>
2024-10-24 17:33:33 +07:00
</section>
</div>
</template>
<style scoped>
:deep(.responsible-search .q-field__control) {
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;
}
2024-10-24 17:33:33 +07:00
</style>