509 lines
18 KiB
Vue
509 lines
18 KiB
Vue
<script lang="ts" setup>
|
|
import { onMounted, ref, watch } from 'vue';
|
|
import { QTableProps } from 'quasar';
|
|
import { moveItemUp, moveItemDown, deleteItem } from 'src/stores/utils';
|
|
|
|
import useUserStore from 'src/stores/user';
|
|
import useOptionStore from 'src/stores/options';
|
|
import { baseUrl } from 'stores/utils';
|
|
import { getRole } from 'src/services/keycloak';
|
|
import {
|
|
WorkflowUserInTable,
|
|
WorkflowTemplatePayload,
|
|
} from 'src/stores/workflow-template/types';
|
|
import { User } from 'src/stores/user/types';
|
|
|
|
import SelectInput from '../shared/SelectInput.vue';
|
|
import ToggleButton from 'src/components/button/ToggleButton.vue';
|
|
import { DeleteButton } from '../button';
|
|
|
|
const props = defineProps<{
|
|
readonly?: boolean;
|
|
onDrawer?: boolean;
|
|
}>();
|
|
|
|
const userStore = useUserStore();
|
|
const optionStore = useOptionStore();
|
|
|
|
const modelByArea = ref<boolean>(true);
|
|
|
|
const userInTable = defineModel<WorkflowUserInTable[]>('userInTable', {
|
|
default: [],
|
|
});
|
|
const registerBranchId = defineModel('registerBranchId', { default: '' });
|
|
const flowData = defineModel<WorkflowTemplatePayload>('flowData', {
|
|
required: true,
|
|
default: {
|
|
status: 'CREATED',
|
|
name: '',
|
|
step: [],
|
|
},
|
|
});
|
|
|
|
const role = ref<string[]>([]);
|
|
const userList = ref<User[]>([]);
|
|
const responsiblePersonSearch = ref('');
|
|
const columns = [
|
|
{
|
|
name: 'detail',
|
|
align: 'center',
|
|
label: 'general.detail',
|
|
field: 'detail',
|
|
},
|
|
{
|
|
name: 'responsiblePerson',
|
|
align: 'center',
|
|
label: 'flow.responsiblePerson',
|
|
field: 'responsiblePerson',
|
|
},
|
|
{
|
|
name: 'action',
|
|
align: 'right',
|
|
label: '',
|
|
field: 'action',
|
|
},
|
|
] satisfies QTableProps['columns'];
|
|
|
|
async function getUserList(opts?: { query: string }) {
|
|
const resUser = await userStore.fetchList({
|
|
query: !!opts?.query ? opts.query : undefined,
|
|
});
|
|
if (resUser) userList.value = resUser.result;
|
|
}
|
|
|
|
// async function getUserById(responsiblePersonId: string) {
|
|
// const resUser = await userStore.fetchById(responsiblePersonId);
|
|
// if (resUser) userInTable.value.push(resUser);
|
|
// }
|
|
|
|
function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) {
|
|
const currStep = flowData.value.step[stepIndex];
|
|
const existPersonIndex = currStep.responsiblePersonId?.findIndex(
|
|
(p) => p === responsiblePerson.id,
|
|
);
|
|
|
|
if (existPersonIndex === -1) {
|
|
currStep.responsiblePersonId?.push(responsiblePerson.id);
|
|
|
|
if (!userInTable.value[stepIndex]) {
|
|
userInTable.value[stepIndex] = {
|
|
name: flowData.value.step[stepIndex].name,
|
|
responsiblePerson: [],
|
|
};
|
|
}
|
|
|
|
userInTable.value[stepIndex]?.responsiblePerson.push({
|
|
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,
|
|
});
|
|
} else {
|
|
currStep.responsiblePersonId?.splice(Number(existPersonIndex), 1);
|
|
userInTable.value[stepIndex]?.responsiblePerson.splice(
|
|
Number(existPersonIndex),
|
|
1,
|
|
);
|
|
}
|
|
}
|
|
|
|
defineEmits<{
|
|
(e: 'moveUp'): void;
|
|
(e: 'moveDown'): void;
|
|
(e: 'changeStatus'): void;
|
|
}>();
|
|
|
|
watch(
|
|
responsiblePersonSearch,
|
|
async () => await getUserList({ query: responsiblePersonSearch.value }),
|
|
);
|
|
|
|
onMounted(async () => {
|
|
role.value = getRole() || [];
|
|
await getUserList();
|
|
await userStore.fetchHqOption();
|
|
});
|
|
</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') }) }}
|
|
<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');
|
|
}
|
|
"
|
|
/>
|
|
{{ $t('status.title') }}
|
|
</span>
|
|
</section>
|
|
|
|
<section id="form-flow-template" class="col-12 row">
|
|
<SelectInput
|
|
:readonly
|
|
v-model="registerBranchId"
|
|
v-if="role.includes('system')"
|
|
class="col-12 q-pb-sm"
|
|
:option="userStore.userOption.hqOpts"
|
|
:label="$t('branch.form.code')"
|
|
/>
|
|
<q-input
|
|
:readonly
|
|
bg-color="transparent"
|
|
outlined
|
|
dense
|
|
class="col-12"
|
|
id="input-flow-template-name"
|
|
v-model="flowData.name"
|
|
hide-bottom-space
|
|
:label="$t(`general.name`, { msg: $t('flow.step') })"
|
|
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
|
/>
|
|
</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 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>
|
|
|
|
<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%);'}`"
|
|
>
|
|
<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"
|
|
>
|
|
<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>
|
|
|
|
<q-item
|
|
v-for="(person, i) in userList"
|
|
:key="i"
|
|
clickable
|
|
class="column"
|
|
@click.stop="
|
|
selectResponsiblePerson(props.rowIndex, person)
|
|
"
|
|
>
|
|
<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)
|
|
"
|
|
/>
|
|
<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.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>
|
|
|
|
<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">
|
|
<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-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>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
:deep(.responsible-search .q-field__control) {
|
|
height: 36px;
|
|
font-size: 12px;
|
|
}
|
|
</style>
|