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:
puriphatt 2024-11-12 15:33:15 +07:00 committed by GitHub
parent 8a2a010776
commit 42e2f2b21d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1257 additions and 225 deletions

View file

@ -1,11 +1,13 @@
<script lang="ts" setup>
import { nextTick } from 'vue';
import { nextTick, reactive, ref } from 'vue';
import {
WorkflowUserInTable,
WorkflowTemplatePayload,
WorkFlowPayloadStep,
} from 'src/stores/workflow-template/types';
import FormFlow from 'src/components/04_flow-management/FormFlow.vue';
import DialogProperties from 'src/components/dialog/DialogProperties.vue';
import DialogForm from 'src/components/DialogForm.vue';
import SideMenu from 'src/components/SideMenu.vue';
import DrawerInfo from 'src/components/DrawerInfo.vue';
@ -33,6 +35,10 @@ const userInTable = defineModel<WorkflowUserInTable[]>('userInTable', {
default: [],
});
const pageState = reactive({ propertiesModal: false });
const currStep = ref<WorkFlowPayloadStep[]>([]);
withDefaults(
defineProps<{
readonly?: boolean;
@ -57,14 +63,21 @@ async function addStep() {
value: [],
detail: '',
name: '',
attributes: { properties: [] },
});
await nextTick();
const scrollTarget = document.getElementById(
`input-flow-step-name-${flowData.value.step.length - 1}`,
`input-flow-step-name-${flowData.value.step.length - 1}-${model.value ? 'dialog' : 'drawer'}`,
);
if (scrollTarget)
scrollTarget.scrollIntoView({ behavior: 'instant', inline: 'center' });
if (scrollTarget) {
scrollTarget.scrollIntoView({ behavior: 'instant', inline: 'start' });
}
}
function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
currStep.value = flowData.value.step.filter((s) => s === step);
pageState.propertiesModal = true;
}
</script>
<template>
@ -104,22 +117,28 @@ async function addStep() {
:menu="[
{
name: $t('general.name', { msg: $t('flow.template') }),
anchor: 'form-flow-template',
anchor: 'form-flow-template-dialog',
},
{
name: $t('flow.processStep'),
anchor: 'form-flow-step',
anchor: 'form-flow-step-dialog',
useBtn: true,
},
...flowData.step.map((_s, i) => ({
name: $t('flow.stepNo', { msg: i + 1 }),
anchor: `input-flow-step-name-${i}-dialog`,
sub: true,
hideSubIndex: true,
})),
]"
background="transparent"
:active="{
background: 'hsla(var(--blue-6-hsl) / .2)',
foreground: 'var(--blue-6)',
}"
scroll-element="#flow-form"
scroll-element="#flow-form-dialog"
>
<template v-slot:btn-form-flow-step>
<template v-slot:btn-form-flow-step-dialog>
<q-btn
dense
flat
@ -144,11 +163,12 @@ async function addStep() {
'q-py-md q-px-lg': !$q.screen.gt.sm,
}"
style="height: 100%; max-height: 100%; overflow-y: auto"
id="flow-form"
id="flow-form-dialog"
>
<FormFlow
v-model:flow-data="flowData"
v-model:register-branch-id="registerBranchId"
@trigger-properties="triggerPropertiesDialog"
/>
</section>
</div>
@ -241,22 +261,28 @@ async function addStep() {
:menu="[
{
name: $t('general.name', { msg: $t('flow.template') }),
anchor: 'form-flow-template',
anchor: 'form-flow-template-drawer',
},
{
name: $t('flow.processStep'),
anchor: 'form-flow-step',
anchor: 'form-flow-step-drawer',
useBtn: true,
},
...flowData.step.map((_s, i) => ({
name: $t('flow.stepNo', { msg: i + 1 }),
anchor: `input-flow-step-name-${i}-drawer`,
sub: true,
hideSubIndex: true,
})),
]"
background="transparent"
:active="{
background: 'hsla(var(--blue-6-hsl) / .2)',
foreground: 'var(--blue-6)',
}"
scroll-element="#flow-form"
scroll-element="#flow-form-drawer"
>
<template v-slot:btn-form-flow-step>
<template v-slot:btn-form-flow-step-drawer>
<q-btn
dense
flat
@ -281,20 +307,27 @@ async function addStep() {
'q-py-md q-px-lg': !$q.screen.gt.sm,
}"
style="height: 100%; max-height: 100%; overflow-y: auto"
id="flow-form"
id="flow-form-drawer"
>
<FormFlow
:readonly
@change-status="$emit('changeStatus')"
onDrawer
v-model:user-in-table="userInTable"
v-model:flow-data="flowData"
v-model:register-branch-id="registerBranchId"
@change-status="$emit('changeStatus')"
@trigger-properties="triggerPropertiesDialog"
/>
</section>
</div>
</div>
</div>
</DrawerInfo>
<DialogProperties
v-model="pageState.propertiesModal"
v-model:data-step="flowData.step"
:step-index="flowData.step.indexOf(currStep[0])"
/>
</template>
<style scoped></style>

View file

@ -43,6 +43,27 @@ const pageState = reactive({
isDrawerEdit: true,
});
const fieldSelected = ref<('order' | 'name' | 'step')[]>([
'order',
'name',
'step',
]);
const fieldSelectedOption = ref<{ label: string; value: string }[]>([
{
label: 'general.order',
value: 'order',
},
{
label: 'general.name',
value: 'name',
},
{
label: 'flow.processStep',
value: 'step',
},
]);
const currWorkflowData = ref<WorkflowTemplate>();
const formDataWorkflow = ref<WorkflowTemplatePayload>({
status: 'CREATED',
@ -68,15 +89,9 @@ const columns = [
{
name: 'step',
align: 'center',
label: 'general.numberOf',
label: 'flow.processStep',
field: 'step',
},
{
name: 'action',
align: 'center',
label: '',
field: 'action',
},
] satisfies QTableProps['columns'];
function triggerDialog(type: 'add' | 'edit' | 'view') {
@ -132,6 +147,7 @@ async function changeStatus(id?: string, regisBId?: string) {
if (currWorkflowData.value) {
currWorkflowData.value.status = res.data.status;
}
await fetchWorkflowList();
}
}
@ -189,35 +205,40 @@ async function submit() {
}
function assignFormData(workflowData: WorkflowTemplate) {
currWorkflowData.value = workflowData;
formDataWorkflow.value = {
status: workflowData.status,
name: workflowData.name,
step: workflowData.step.map((s, i) => {
userInTable.value[i] = { name: s.name, responsiblePerson: [] };
s.responsiblePerson.forEach((p) => {
userInTable.value[i].responsiblePerson.push({
id: p.user.id,
selectedImage: p.user.selectedImage,
gender: p.user.gender,
namePrefix: p.user.namePrefix,
firstName: p.user.firstName,
lastName: p.user.lastName,
firstNameEN: p.user.firstNameEN,
lastNameEN: p.user.lastNameEN,
code: p.user.code,
currWorkflowData.value = JSON.parse(JSON.stringify(workflowData));
formDataWorkflow.value = JSON.parse(
JSON.stringify({
status: workflowData.status,
name: workflowData.name,
step: workflowData.step.map((s, i) => {
userInTable.value[i] = { name: s.name, responsiblePerson: [] };
s.responsiblePerson.forEach((p) => {
userInTable.value[i].responsiblePerson.push({
id: p.user.id,
selectedImage: p.user.selectedImage,
gender: p.user.gender,
namePrefix: p.user.namePrefix,
firstName: p.user.firstName,
lastName: p.user.lastName,
firstNameEN: p.user.firstNameEN,
lastNameEN: p.user.lastNameEN,
code: p.user.code,
});
});
});
return {
id: s.id,
name: s.name,
detail: s.detail,
value: s.value || [],
responsiblePersonId: s.responsiblePerson.map((p) => p.userId),
responsibleInstitution: s.responsibleInstitution,
};
return {
id: s.id,
name: s.name,
detail: s.detail,
value: s.value.length > 0 ? JSON.parse(JSON.stringify(s.value)) : [],
responsiblePersonId: s.responsiblePerson.map((p) => p.userId),
responsibleInstitution: JSON.parse(
JSON.stringify(s.responsibleInstitution),
),
attributes: JSON.parse(JSON.stringify(s.attributes)),
};
}),
}),
};
);
registeredBranchId.value = workflowData.registeredBranchId;
}
@ -309,10 +330,10 @@ watch(() => pageState.inputSearch, fetchWorkflowList);
labelI18n
:branch="[
{
icon: 'mdi-folder-outline',
icon: 'mdi-cogs',
count: pageState.total,
label: 'flow.title',
color: 'red',
color: 'gray',
},
]"
:dark="$q.dark.isActive"
@ -345,7 +366,7 @@ watch(() => pageState.inputSearch, fetchWorkflowList);
</q-input>
<div
class="row col-12 col-md-3 justify-end"
class="row col-12 col-md-5 justify-end"
:class="{ 'q-pt-xs': $q.screen.lt.md }"
style="white-space: nowrap"
>
@ -356,7 +377,6 @@ watch(() => pageState.inputSearch, fetchWorkflowList);
option-value="value"
option-label="label"
class="col"
:class="{ 'offset-md-5': pageState.gridView }"
map-options
emit-value
:for="'field-select-status'"
@ -367,6 +387,73 @@ watch(() => pageState.inputSearch, fetchWorkflowList);
{ label: $t('general.inactive'), value: 'statusINACTIVE' },
]"
/>
<q-select
id="select-field"
for="select-field"
class="col q-ml-sm"
:options="
fieldSelectedOption.map((v) => ({
...v,
label: $t(v.label),
}))
"
:display-value="$t('general.displayField')"
:hide-dropdown-icon="$q.screen.lt.sm"
v-model="fieldSelected"
option-label="label"
option-value="value"
autocomplete="off"
map-options
emit-value
outlined
multiple
dense
/>
<q-btn-toggle
id="btn-mode"
v-model="pageState.gridView"
dense
class="no-shadow bordered rounded surface-1 q-ml-sm"
:toggle-color="$q.dark.isActive ? 'grey-9' : 'grey-2'"
size="xs"
:options="[
{ value: true, slot: 'folder' },
{ value: false, slot: 'list' },
]"
>
<template v-slot:folder>
<q-icon
name="mdi-view-grid-outline"
size="16px"
class="q-px-sm q-py-xs rounded"
:style="{
color: $q.dark.isActive
? pageState.gridView
? '#C9D3DB '
: '#787B7C'
: pageState.gridView
? '#787B7C'
: '#C9D3DB',
}"
/>
</template>
<template v-slot:list>
<q-icon
name="mdi-format-list-bulleted"
class="q-px-sm q-py-xs rounded"
size="16px"
:style="{
color: $q.dark.isActive
? pageState.gridView === false
? '#C9D3DB'
: '#787B7C'
: pageState.gridView === false
? '#787B7C'
: '#C9D3DB',
}"
/>
</template>
</q-btn-toggle>
</div>
</div>
</header>
@ -393,10 +480,15 @@ watch(() => pageState.inputSearch, fetchWorkflowList);
<q-table
flat
bordered
hide-pagination
:columns="columns"
:grid="pageState.gridView"
:rows="workflowData"
:columns="columns"
class="full-width"
card-container-class="q-col-gutter-md"
row-key="name"
:rows-per-page-options="[0]"
hide-pagination
:visible-columns="fieldSelected"
>
<template #header="{ cols }">
<q-tr style="background-color: hsla(var(--info-bg) / 0.07)">
@ -420,6 +512,7 @@ watch(() => pageState.inputSearch, fetchWorkflowList);
})
}}
</q-th>
<q-th auto-width />
</q-tr>
</template>
@ -438,25 +531,57 @@ watch(() => pageState.inputSearch, fetchWorkflowList);
: ''
"
>
<q-td class="text-center">{{ props.rowIndex + 1 }}</q-td>
<q-td>{{ props.row.name }}</q-td>
<q-td class="text-right">{{ props.row.step.length }}</q-td>
<q-td class="row items-center justify-end">
<div class="row">
<q-btn
icon="mdi-eye-outline"
size="sm"
dense
round
flat
@click.stop="
() => {
assignFormData(props.row);
triggerDialog('view');
}
<q-td
v-if="fieldSelected.includes('order')"
class="text-center"
>
{{ props.rowIndex + 1 }}
<!-- {{ (currentPage - 1) * pageSize + props.rowIndex + 1 }} -->
</q-td>
<q-td v-if="fieldSelected.includes('name')">
<section class="row items-center">
<q-avatar
class="q-mr-sm"
size="md"
style="
color: var(--gray-6);
background: hsla(var(--gray-6-hsl) / 0.1);
"
/>
</div>
>
<q-icon name="mdi-cogs" />
<q-badge
class="absolute-bottom-right no-padding"
style="
border-radius: 50%;
min-width: 8px;
min-height: 8px;
"
:style="{
background: `var(--${props.row.status === 'INACTIVE' ? 'stone-5' : 'green-6'})`,
}"
></q-badge>
</q-avatar>
{{ props.row.name }}
</section>
</q-td>
<q-td v-if="fieldSelected.includes('step')" class="text-right">
{{ props.row.step.length }}
</q-td>
<q-td style="width: 20%" class="text-right">
<q-btn
icon="mdi-eye-outline"
size="sm"
dense
round
flat
@click.stop="
() => {
assignFormData(props.row);
triggerDialog('view');
}
"
/>
<KebabAction
:id-name="props.row.id"
:status="props.row.status"
@ -478,6 +603,76 @@ watch(() => pageState.inputSearch, fetchWorkflowList);
</q-td>
</q-tr>
</template>
<template v-slot:item="props">
<section class="col-12 col-md-4 column">
<div
class="surface-1 rounded bordered row no-wrap col"
style="overflow: hidden"
>
<div
class="col-md-2 col-3 flex items-center justify-center"
style="
color: var(--gray-6);
background: hsla(var(--gray-6-hsl) / 0.1);
"
>
<q-icon name="mdi-cogs" size="md" />
</div>
<article class="row q-pa-sm q-gutter-y-sm col">
<div
v-if="fieldSelected.includes('name')"
class="text-weight-bold col-12 ellipsis-2-lines"
>
{{ props.row.name }}
<q-tooltip>
{{ props.row.name }}
</q-tooltip>
</div>
<div v-if="fieldSelected.includes('step')" class="self-end">
<div class="bordered rounded q-px-sm">
<q-icon name="mdi-note-edit-outline" class="q-pr-sm" />
{{ props.row.step.length }}
</div>
</div>
</article>
<nav class="q-pa-sm row items-center self-start">
<q-btn
icon="mdi-eye-outline"
size="sm"
dense
round
flat
@click.stop="
() => {
assignFormData(props.row);
triggerDialog('view');
}
"
/>
<KebabAction
:id-name="props.row.id"
:status="props.row.status"
@view="
() => {
assignFormData(props.row);
triggerDialog('view');
}
"
@edit="
() => {
assignFormData(props.row);
triggerDialog('edit');
}
"
@delete="() => deleteWorkflow(props.row.id)"
@change-status="() => triggerChangeStatus(props.row)"
/>
</nav>
</div>
</section>
</template>
</q-table>
</article>

View file

@ -72,7 +72,6 @@ const columns = [
const pageState = reactive({
hideStat: false,
inputSearch: '',
fieldSelected: [],
gridView: false,
total: 0,
@ -683,19 +682,37 @@ watch(
</span>
<span class="col">
{{
$i18n.locale === 'eng'
? `${props.row.addressEN}, ${props.row.mooEN && `${$t('form.moo')} ${props.row.mooEN},`} ${props.row.soiEN && `${$t('form.soi')} ${props.row.soiEN},`} ${props.row.moo && `${props.row.streetEN} Rd,`} ${props.row.subDistrict?.nameEN}, ${props.row.district?.nameEN}, ${props.row.province?.nameEN} ${props.row.subDistrict?.zipCode}` ||
'-'
: `${props.row.address}, ${props.row.moo && `${$t('form.moo')} ${props.row.moo},`} ${props.row.soi && `${$t('form.soi')} ${props.row.soi},`} ${props.row.street && `${$t('form.road')} ${props.row.street},`} ${props.row.subDistrict?.name}, ${props.row.district?.name}, ${props.row.province?.name} ${props.row.subDistrict?.zipCode}` ||
'-'
formatAddress({
address: props.row.address,
addressEN: props.row.addressEN,
moo: props.row.moo,
mooEN: props.row.mooEN,
soi: props.row.soi,
soiEN: props.row.soiEN,
street: props.row.street,
streetEN: props.row.streetEN,
province: props.row.province,
district: props.row.district,
subDistrict: props.row.subDistrict,
en: $i18n.locale === 'eng',
})
}}
<q-tooltip>
{{
$i18n.locale === 'eng'
? `${props.row.addressEN}, ${props.row.mooEN && `${$t('form.moo')} ${props.row.mooEN},`} ${props.row.soiEN && `${$t('form.soi')} ${props.row.soiEN},`} ${props.row.moo && `${props.row.streetEN} Rd,`} ${props.row.subDistrict?.nameEN}, ${props.row.district?.nameEN}, ${props.row.province?.nameEN} ${props.row.subDistrict?.zipCode}` ||
'-'
: `${props.row.address}, ${props.row.moo && `${$t('form.moo')} ${props.row.moo},`} ${props.row.soi && `${$t('form.soi')} ${props.row.soi},`} ${props.row.street && `${$t('form.road')} ${props.row.street},`} ${props.row.subDistrict?.name}, ${props.row.district?.name}, ${props.row.province?.name} ${props.row.subDistrict?.zipCode}` ||
'-'
formatAddress({
address: props.row.address,
addressEN: props.row.addressEN,
moo: props.row.moo,
mooEN: props.row.mooEN,
soi: props.row.soi,
soiEN: props.row.soiEN,
street: props.row.street,
streetEN: props.row.streetEN,
province: props.row.province,
district: props.row.district,
subDistrict: props.row.subDistrict,
en: $i18n.locale === 'eng',
})
}}
</q-tooltip>
</span>