feat: workflow template properties (#70)
* feat: add i18n * refactor/feat: workflow attributes type * refactor: workflow => gray stat card * refactor: select menu with search => separator * feat: workflow => workflow step properties * fix: workflow type * fix: dialog properties component => model data * fix: form flow => prevent toggle expansion with keyboard * refactor: workflow step data & change status * fix: form flow properties btn * refactor: side menu => hide sub index * feat: workflow => avatar & status on table * refactor: workflow => drawer id and dialog id * feat: workflow => card * fix: agencies => format address
This commit is contained in:
parent
8a2a010776
commit
42e2f2b21d
12 changed files with 1257 additions and 225 deletions
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { QTableProps } from 'quasar';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { moveItemUp, moveItemDown, deleteItem } from 'src/stores/utils';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import useUserStore from 'src/stores/user';
|
||||
import useOptionStore from 'src/stores/options';
|
||||
|
|
@ -10,6 +11,7 @@ import { getRole } from 'src/services/keycloak';
|
|||
import {
|
||||
WorkflowUserInTable,
|
||||
WorkflowTemplatePayload,
|
||||
WorkFlowPayloadStep,
|
||||
} from 'src/stores/workflow-template/types';
|
||||
import { User } from 'src/stores/user/types';
|
||||
|
||||
|
|
@ -23,6 +25,7 @@ defineProps<{
|
|||
onDrawer?: boolean;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const optionStore = useOptionStore();
|
||||
const modelByArea = ref<boolean>(false);
|
||||
|
|
@ -40,36 +43,15 @@ const flowData = defineModel<WorkflowTemplatePayload>('flowData', {
|
|||
},
|
||||
});
|
||||
|
||||
const options = ref(optionStore.globalOption?.agenciesType);
|
||||
const objectOptions = [
|
||||
...(optionStore.globalOption?.agenciesType || []),
|
||||
{ label: t('flow.customer'), value: 'customer' },
|
||||
{ label: t('flow.officer'), value: 'officer' },
|
||||
];
|
||||
const options = ref(objectOptions);
|
||||
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: 'responsiblePerson',
|
||||
align: 'center',
|
||||
label: 'general.agencies',
|
||||
field: 'responsiblePerson',
|
||||
},
|
||||
{
|
||||
name: 'action',
|
||||
align: 'right',
|
||||
label: '',
|
||||
field: 'action',
|
||||
},
|
||||
] satisfies QTableProps['columns'];
|
||||
|
||||
async function getUserList(opts?: { query: string }) {
|
||||
const resUser = await userStore.fetchList({
|
||||
|
|
@ -134,12 +116,12 @@ function selectItem(
|
|||
|
||||
function optionSearch(val: string | null) {
|
||||
if (val === '') {
|
||||
options.value = optionStore.globalOption?.agenciesType;
|
||||
options.value = objectOptions;
|
||||
return;
|
||||
}
|
||||
|
||||
const needle = val ? val.toLowerCase() : '';
|
||||
options.value = optionStore.globalOption?.agenciesType.filter(
|
||||
options.value = objectOptions.filter(
|
||||
(v: { label: string }) => v.label.toLowerCase().indexOf(needle) > -1,
|
||||
);
|
||||
}
|
||||
|
|
@ -148,6 +130,7 @@ defineEmits<{
|
|||
(e: 'moveUp'): void;
|
||||
(e: 'moveDown'): void;
|
||||
(e: 'changeStatus'): void;
|
||||
(e: 'triggerProperties', step: WorkFlowPayloadStep): void;
|
||||
}>();
|
||||
|
||||
watch(
|
||||
|
|
@ -165,6 +148,7 @@ onMounted(async () => {
|
|||
<template>
|
||||
<div class="row col-12">
|
||||
<section
|
||||
:id="`form-flow-template-${onDrawer ? 'drawer' : 'dialog'}`"
|
||||
class="col-12 q-pb-sm text-weight-bold text-body1 row items-center"
|
||||
>
|
||||
<q-icon
|
||||
|
|
@ -172,7 +156,7 @@ onMounted(async () => {
|
|||
size="xs"
|
||||
class="q-pa-sm rounded q-mr-xs"
|
||||
color="info"
|
||||
name="mdi-office-building-outline"
|
||||
name="mdi-cogs"
|
||||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`general.name`, { msg: $t('flow.title') }) }}
|
||||
|
|
@ -195,7 +179,7 @@ onMounted(async () => {
|
|||
</span>
|
||||
</section>
|
||||
|
||||
<section id="form-flow-template" class="col-12 row q-col-gutter-sm">
|
||||
<section class="col-12 row q-col-gutter-sm">
|
||||
<SelectInput
|
||||
v-if="role.includes('system')"
|
||||
:readonly
|
||||
|
|
@ -222,15 +206,17 @@ onMounted(async () => {
|
|||
/>
|
||||
</section>
|
||||
|
||||
<!-- SEC: Step -->
|
||||
<section
|
||||
class="col-12 q-pb-sm q-pt-lg text-weight-bold text-body1 row items-center"
|
||||
:id="`form-flow-step-${onDrawer ? 'drawer' : 'dialog'}`"
|
||||
class="col-12 q-pb-sm q-pt-xl 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"
|
||||
name="mdi-note-edit-outline"
|
||||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`flow.processStep`) }}
|
||||
|
|
@ -238,7 +224,7 @@ onMounted(async () => {
|
|||
|
||||
<section
|
||||
v-if="flowData.step.length === 0"
|
||||
class="col-12 surface-2 rounded bordered column items-center justify-center q-pa-md"
|
||||
class="col-12 surface-2 rounded bordered column items-center justify-center q-pa-xl"
|
||||
>
|
||||
<NoData class="col" />
|
||||
</section>
|
||||
|
|
@ -257,8 +243,8 @@ onMounted(async () => {
|
|||
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>
|
||||
<div class="column full-width" @keyup.stop @click.stop>
|
||||
<div class="row items-center q-py-sm full-width">
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-work-up-product"
|
||||
|
|
@ -296,8 +282,8 @@ onMounted(async () => {
|
|||
dense
|
||||
outlined
|
||||
:readonly
|
||||
:id="`input-flow-step-name-${index}`"
|
||||
:for="`input-flow-step-name-${index}`"
|
||||
:id="`input-flow-step-name-${index}-${onDrawer ? 'drawer' : 'dialog'}`"
|
||||
:for="`input-flow-step-name-${index}-${onDrawer ? 'drawer' : 'dialog'}`"
|
||||
class="col q-ml-md"
|
||||
:placeholder="$t('general.no', { msg: $t('flow.step') })"
|
||||
v-model="step.name"
|
||||
|
|
@ -403,105 +389,151 @@ onMounted(async () => {
|
|||
:class="{ 'surface-1 rounded bordered': readonly }"
|
||||
style="border: 1px solid transparent"
|
||||
>
|
||||
<div class="q-col-gutter-sm row">
|
||||
<div class="row q-col-gutter-sm">
|
||||
<q-input
|
||||
:bg-color="readonly ? 'transparent' : ''"
|
||||
:readonly
|
||||
v-model="step.detail"
|
||||
class="col-12"
|
||||
class="col-6"
|
||||
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="col-6">
|
||||
<div
|
||||
class="surface-1 rounded bordered full-height"
|
||||
style="padding-inline: 12px"
|
||||
:style="readonly ? 'border: 1px solid transparent;' : ''"
|
||||
>
|
||||
{{
|
||||
$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"
|
||||
<section
|
||||
class="row items-center q-pt-xs justify-between relative-position"
|
||||
>
|
||||
<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)"
|
||||
class="app-text-muted-2"
|
||||
:style="
|
||||
step.attributes?.properties &&
|
||||
step.attributes?.properties.length > 0
|
||||
? 'font-size: 10px'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<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>
|
||||
{{ $t('general.properties') }}
|
||||
</div>
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-add-work-product"
|
||||
class="text-capitalize rounded absolute-top-right"
|
||||
flat
|
||||
dense
|
||||
padding="4px 8px"
|
||||
style="color: hsl(var(--info-bg)); top: 4px"
|
||||
@click.stop="$emit('triggerProperties', step)"
|
||||
>
|
||||
<Icon
|
||||
icon="basil:settings-adjust-solid"
|
||||
width="20.08px"
|
||||
style="color: hsl(var(--info-bg))"
|
||||
/>
|
||||
</q-btn>
|
||||
</section>
|
||||
<section class="row q-gutter-sm q-pb-sm scroll">
|
||||
<span
|
||||
v-for="(att, i) in step.attributes?.properties"
|
||||
:key="i"
|
||||
class="surface-2 bordered rounded q-px-xs"
|
||||
>
|
||||
{{ optionStore.mapOption(att.fieldName ?? '') }}
|
||||
</span>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RESPONSIBLE-PERSON -->
|
||||
<q-select
|
||||
v-if="step.responsiblePersonId"
|
||||
: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
|
||||
:label="$t('flow.responsiblePerson')"
|
||||
class="col-6"
|
||||
:hide-dropdown-icon="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)"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:option></template>
|
||||
<q-menu v-if="!readonly" :offset="[0, 4]">
|
||||
<q-list>
|
||||
<q-item>
|
||||
|
|
@ -524,6 +556,12 @@ onMounted(async () => {
|
|||
{{ $t('general.people') }}
|
||||
</span>
|
||||
|
||||
<q-item
|
||||
v-if="userList.length === 0"
|
||||
class="app-text-muted q-px-lg"
|
||||
>
|
||||
{{ $t('general.noData') }}
|
||||
</q-item>
|
||||
<q-item
|
||||
v-for="(person, i) in userList"
|
||||
dense
|
||||
|
|
@ -630,8 +668,9 @@ onMounted(async () => {
|
|||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-field>
|
||||
</q-select>
|
||||
|
||||
<!-- RESPONSIBLE-AGENCIES, RESPONSIBLE-INSTITUTION -->
|
||||
<q-select
|
||||
:bg-color="readonly ? 'transparent' : ''"
|
||||
:readonly
|
||||
|
|
@ -640,6 +679,7 @@ onMounted(async () => {
|
|||
v-model="step.responsibleInstitution"
|
||||
multiple
|
||||
:options="options"
|
||||
hide-bottom-space
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
emit-value
|
||||
|
|
@ -656,7 +696,16 @@ onMounted(async () => {
|
|||
@remove="scope.removeAtIndex(scope.index)"
|
||||
>
|
||||
<span class="text-caption">
|
||||
{{ optionStore.mapOption(scope.opt, 'agenciesType') }}
|
||||
{{
|
||||
scope.opt === 'customer'
|
||||
? $t('flow.customer')
|
||||
: scope.opt === 'officer'
|
||||
? $t('flow.officer')
|
||||
: optionStore.mapOption(
|
||||
scope.opt,
|
||||
'agenciesType',
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</q-chip>
|
||||
</template>
|
||||
|
|
@ -668,13 +717,21 @@ onMounted(async () => {
|
|||
$t('general.select', { msg: $t('general.agencies') })
|
||||
"
|
||||
:option="options"
|
||||
:separator-index="[9]"
|
||||
width="353.66px"
|
||||
@search="(v) => optionSearch(v as string)"
|
||||
@select="
|
||||
(v) => selectItem(v, step.responsibleInstitution)
|
||||
"
|
||||
@before-show="
|
||||
options = optionStore.globalOption?.agenciesType
|
||||
() => {
|
||||
objectOptions = [
|
||||
...(optionStore.globalOption?.agenciesType || []),
|
||||
{ label: t('flow.customer'), value: 'customer' },
|
||||
{ label: t('flow.officer'), value: 'officer' },
|
||||
];
|
||||
options = objectOptions;
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #prepend>
|
||||
|
|
@ -722,6 +779,15 @@ onMounted(async () => {
|
|||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(
|
||||
.q-item__section.column.q-item__section--side.justify-center.q-item__section--avatar.q-focusable.relative-position.cursor-pointer
|
||||
) {
|
||||
justify-content: start !important;
|
||||
padding-right: 8px !important;
|
||||
padding-top: 16px;
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
:deep(i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon) {
|
||||
color: hsl(var(--text-mute));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue