jws-frontend/src/components/dialog/DialogProperties.vue
puriphatt 0ad05fa960
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
refactor: simplify property management logic and improve type definitions
2025-03-13 14:32:31 +07:00

811 lines
26 KiB
Vue

<script setup lang="ts">
import { ref, watch } from 'vue';
import { moveItemUp, moveItemDown, dialog, deleteItem } from 'stores/utils';
import { useI18n } from 'vue-i18n';
import useOptionStore from 'src/stores/options';
import { useWorkflowTemplate } from 'src/stores/workflow-template';
import {
WorkFlowPayloadStep,
WorkflowTemplate,
} from 'src/stores/workflow-template/types';
import SelectFlow from '../shared/select/SelectFlow.vue';
import NoData from '../NoData.vue';
import DialogForm from '../DialogForm.vue';
interface Option {
value: string;
label: string;
type: string;
}
const { t } = useI18n();
const { getWorkflowTemplate } = useWorkflowTemplate();
const optionStore = useOptionStore();
const props = defineProps<{
stepIndex?: number;
onEdit?: boolean;
selectFlow?: boolean;
}>();
const emit = defineEmits<{
(e: 'submit', currWorkflow: WorkflowTemplate): void;
(e: 'show'): void;
}>();
const model = defineModel<boolean>({ required: true, default: false });
const workflowId = defineModel<string>('workflowId', { default: '' });
const dataStep = defineModel<WorkFlowPayloadStep[]>('dataStep', {
default: [],
});
const tempStep = ref<WorkFlowPayloadStep[]>([]);
const tempWorkflowId = ref<string>('');
const propertiesOption = ref();
const currWorkflow = ref<WorkflowTemplate>();
const typeOption = [
{
label: 'Text',
value: 'string',
color: 'var(--pink-6-hsl)',
icon: 'mdi-alpha-t',
},
{
label: 'Number',
value: 'number',
color: 'var(--purple-11-hsl)',
icon: 'mdi-numeric',
},
{
label: 'Date',
value: 'date',
color: 'var(--green-9-hsl)',
icon: 'mdi-calendar-blank-outline',
},
{
label: 'Selection',
value: 'array',
color: 'var(--indigo-7-hsl)',
icon: 'mdi-code-array',
},
];
function submit() {
workflowId.value = tempWorkflowId.value;
dataStep.value = JSON.parse(JSON.stringify(tempStep.value));
model.value = false;
if (props.selectFlow && currWorkflow.value) {
emit('submit', currWorkflow.value);
}
}
function close() {
tempStep.value = [];
model.value = false;
}
function manageProperties(
stepIndex: number,
property: string,
propertyType?: 'date' | 'array' | 'string' | 'number',
) {
if (property === 'all' && propertiesOption.value) {
if (
tempStep.value[stepIndex].attributes.properties.filter((p) =>
propertiesOption.value.some((opt: Option) => opt.value === p.fieldName),
).length === propertiesOption.value.length
) {
tempStep.value[stepIndex].attributes.properties = [];
return;
}
for (const ops of propertiesOption.value) {
if (
tempStep.value[stepIndex].attributes.properties.some(
(prop) => prop.fieldName === ops.value,
)
) {
continue;
}
if (ops.type === 'date') {
tempStep.value[stepIndex].attributes.properties.push({
type: ops.type,
fieldName: ops.value,
});
}
if (ops.type === 'array') {
tempStep.value[stepIndex].attributes.properties.push({
type: ops.type,
fieldName: ops.value,
options: [],
});
}
if (ops.type === 'string') {
tempStep.value[stepIndex].attributes.properties.push({
type: ops.type,
fieldName: ops.value,
isPhoneNumber: false,
phoneNumberLength: 10,
});
}
if (ops.type === 'number') {
tempStep.value[stepIndex].attributes.properties.push({
type: ops.type,
fieldName: ops.value,
comma: false,
decimal: false,
decimalPlace: 2,
});
}
}
return;
}
if (tempStep.value[stepIndex].attributes.properties) {
const propStep = tempStep.value[stepIndex].attributes.properties.findIndex(
(prop) => prop.fieldName === property,
);
if (propStep !== -1) {
tempStep.value[stepIndex].attributes.properties.splice(propStep, 1);
} else {
if (propertyType === 'date') {
tempStep.value[stepIndex].attributes.properties.push({
type: propertyType,
fieldName: property,
});
}
if (propertyType === 'array') {
tempStep.value[stepIndex].attributes.properties.push({
type: propertyType,
fieldName: property,
options: [],
});
}
if (propertyType === 'string') {
tempStep.value[stepIndex].attributes.properties.push({
type: propertyType,
fieldName: property,
isPhoneNumber: false,
phoneNumberLength: 10,
});
}
if (propertyType === 'number') {
tempStep.value[stepIndex].attributes.properties.push({
type: propertyType,
fieldName: property,
comma: false,
decimal: false,
decimalPlace: 2,
});
}
}
}
}
function changeType(fieldName: string, stepIndex: number) {
if (!propertiesOption.value) return;
const defaultPropType = propertiesOption.value.find(
(op: { value: string }) => op.value === fieldName,
)?.type;
if (!defaultPropType) return;
const idx = tempStep.value[stepIndex].attributes.properties.findIndex(
(p) => p.fieldName === fieldName,
);
if (idx < 0) return;
if (defaultPropType === 'date') {
tempStep.value[stepIndex].attributes.properties[idx] = {
type: defaultPropType,
fieldName,
};
}
if (defaultPropType === 'array') {
tempStep.value[stepIndex].attributes.properties[idx] = {
type: defaultPropType,
fieldName,
options: [],
};
}
if (defaultPropType === 'string') {
tempStep.value[stepIndex].attributes.properties[idx] = {
type: defaultPropType,
fieldName,
isPhoneNumber: false,
phoneNumberLength: 10,
};
}
if (defaultPropType === 'number') {
tempStep.value[stepIndex].attributes.properties[idx] = {
type: defaultPropType,
fieldName,
comma: false,
decimal: false,
decimalPlace: 2,
};
}
}
function shouldShowItem(opt: Option, stepIndex: number) {
if (tempStep.value[stepIndex].attributes.properties) {
const properties = new Set(
tempStep.value[stepIndex].attributes.properties.map((p) => p.fieldName),
);
return !!opt && !properties.has(opt.value);
}
}
function confirmDelete(items: unknown[], index: number) {
dialog({
color: 'negative',
icon: 'mdi-alert',
title: t('dialog.title.confirmDelete'),
actionText: t('general.delete'),
message: t('dialog.message.confirmDelete'),
action: async () => {
deleteItem(items, index);
},
cancel: () => {},
});
}
async function assignTemp() {
propertiesOption.value = optionStore.globalOption?.propertiesField;
tempStep.value = JSON.parse(JSON.stringify(dataStep.value));
tempWorkflowId.value = workflowId.value;
}
watch(
() => tempWorkflowId.value,
async (a, b) => {
if (props.onEdit && workflowId.value === a) return;
if (props.selectFlow && a !== b && a) {
const ret = await getWorkflowTemplate(a);
if (ret) {
currWorkflow.value = JSON.parse(JSON.stringify(ret));
tempStep.value =
ret.step?.length > 0
? ret.step.map((s) => ({
name: s.name,
attributes: s.attributes,
}))
: [];
}
}
},
);
watch(
() => model.value,
() => {
if (model.value) {
assignTemp();
}
},
);
</script>
<template>
<DialogForm
no-address
no-app-box
height="60vh"
width="75%"
:title="$t('general.properties')"
v-model:modal="model"
:submit="submit"
:close="close"
:show="() => $emit('show')"
>
<div class="column full-height no-wrap">
<div
v-if="selectFlow"
class="bordered-b surface-3 row items-center no-wrap q-py-sm"
:class="{
'q-px-lg': $q.screen.gt.sm,
'q-px-md': !$q.screen.gt.sm,
}"
>
{{ $t('flow.title') }}
<SelectFlow
style="width: 18vw"
class="q-ml-sm"
v-model:value="tempWorkflowId"
:label="$t('flow.title')"
simple
/>
</div>
<template v-if="$slots.prepend">
<slot name="prepend"></slot>
</template>
<div
v-if="tempStep?.length === 0"
class="row surface-1 rounded bordered items-center justify-center col"
:class="{
'q-ma-lg': $q.screen.gt.sm,
'q-ma-md': !$q.screen.gt.sm,
}"
>
<NoData :text="$t('general.no', { msg: $t('flow.processStep') })" />
</div>
<section
v-for="(step, stepIndex) in tempStep"
:key="stepIndex"
class="column"
>
<template
v-if="
(props.stepIndex !== undefined && stepIndex === props.stepIndex) ||
props.stepIndex === undefined
"
>
<span
class="row items-center q-py-sm bordered-b"
:class="{
'q-px-lg': $q.screen.gt.sm,
'q-px-md ': !$q.screen.gt.sm,
}"
>
{{ $t('flow.stepNo', { msg: (props.stepIndex || stepIndex) + 1 }) }}
<span class="app-text-muted">: {{ step.name }}</span>
<q-btn-dropdown
for="select-step"
id="select-step"
unelevated
no-icon-animation
size="sm"
padding="0 0"
class="rounded q-ml-md"
dropdown-icon="mdi-plus"
style="color: hsl(var(--text-mute))"
>
<q-list dense v-if="propertiesOption">
<q-item
for="list-all"
id="list-all"
clickable
@click="manageProperties(stepIndex, 'all')"
>
<div class="full-width flex items-center">
<q-icon
v-if="
tempStep[stepIndex].attributes.properties?.filter((p) =>
propertiesOption.some(
(opt: Option) => opt.value === p.fieldName,
),
).length === propertiesOption.length
"
name="mdi-checkbox-marked"
size="xs"
class="q-mr-sm"
color="primary"
/>
<q-icon
v-else
name="mdi-checkbox-blank-outline"
size="xs"
class="q-mr-sm"
style="color: hsl(var(--text-mute))"
/>
{{ $t('general.selectAll') }}
</div>
</q-item>
<q-separator />
<q-item
v-for="(ops, index) in propertiesOption"
clickable
:class="{
'bordered-t': index === propertiesOption.length - 4,
}"
:key="index"
:for="`list-${ops.value}`"
:id="`list-${ops.value}`"
@click="manageProperties(stepIndex, ops.value, ops.type)"
>
<div class="full-width flex items-center no-wrap">
<q-icon
v-if="
tempStep[stepIndex].attributes.properties.some(
(p) => p.fieldName === ops.value,
)
"
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"
/>
{{ ops.label }}
</div>
</q-item>
</q-list>
</q-btn-dropdown>
</span>
<div
v-if="tempStep[stepIndex].attributes?.properties?.length === 0"
class="row surface-1 rounded bordered items-center justify-center col"
:class="{
'q-ma-lg': $q.screen.gt.sm,
'q-ma-md': !$q.screen.gt.sm,
}"
>
<NoData useField />
</div>
<div
v-if="tempStep[stepIndex].attributes?.properties?.length > 0"
class="q-py-sm"
:class="{
'q-px-lg': $q.screen.gt.sm,
'q-px-md': !$q.screen.gt.sm,
}"
>
<div
v-for="(prop, propIndex) in tempStep[stepIndex].attributes
.properties"
:key="propIndex"
class="bordered surface-1 rounded q-py-sm q-px-md row items-start q-my-sm"
>
<div class="col-md col-12 row items-center">
<q-btn
:id="`btn-move-up-product-${propIndex}`"
icon="mdi-arrow-up"
dense
flat
round
:disable="propIndex === 0"
:size="$q.screen.xs ? 'xs' : ''"
style="color: hsl(var(--text-mute-2))"
@click="
moveItemUp(
tempStep[stepIndex].attributes.properties,
propIndex,
)
"
/>
<q-btn
:id="`btn-move-down-product-${propIndex}`"
icon="mdi-arrow-down"
dense
flat
round
:size="$q.screen.xs ? 'xs' : ''"
:disable="
propIndex ===
tempStep[stepIndex].attributes.properties?.length - 1
"
style="color: hsl(var(--text-mute-2))"
@click="
moveItemDown(
tempStep[stepIndex].attributes.properties,
propIndex,
)
"
/>
<q-avatar
:size="$q.screen.xs ? 'sm' : 'md'"
class="q-mx-lg"
style="background-color: var(--surface-3)"
>
{{ propIndex + 1 }}
</q-avatar>
<!-- field name -->
<q-select
dense
outlined
emit-value
map-options
hide-bottom-space
:for="`input-properties-name-${propIndex}`"
class="col-md col-12 q-mr-md"
:class="{ 'q-my-sm': $q.screen.lt.md }"
:label="$t('productService.service.propertiesName')"
option-label="label"
option-value="value"
:options="propertiesOption"
v-model="prop.fieldName"
@update:model-value="(v) => changeType(v, stepIndex)"
>
<template v-slot:option="scope">
<q-item
v-if="scope.opt && shouldShowItem(scope.opt, stepIndex)"
v-bind="scope.itemProps"
class="row items-center col-12"
>
{{ scope.opt.label }}
</q-item>
</template>
</q-select>
</div>
<!-- type -->
<div
v-if="
prop.fieldName === 'documentCheck' ||
prop.fieldName === 'designForm' ||
prop.fieldName === 'messenger' ||
prop.fieldName === 'duty'
"
class="col-md col-12"
></div>
<div v-else class="col-md col-12">
<q-select
dense
outlined
emit-value
map-options
hide-bottom-space
:for="`input-properties-type-${propIndex}`"
:id="`input-properties-type-${propIndex}`"
:label="$t('general.type')"
option-value="value"
@update:model-value="
(t: 'string' | 'number' | 'date' | 'array') => {
if (!tempStep) return;
if (t === 'date') {
tempStep[stepIndex].attributes.properties[propIndex] = {
type: t,
fieldName: prop.fieldName,
};
}
if (t === 'array') {
tempStep[stepIndex].attributes.properties[propIndex] = {
type: t,
fieldName: prop.fieldName,
options: [],
};
}
if (t === 'string') {
tempStep[stepIndex].attributes.properties[propIndex] = {
type: t,
fieldName: prop.fieldName,
isPhoneNumber: false,
phoneNumberLength: 10,
};
}
if (t === 'number') {
tempStep[stepIndex].attributes.properties[propIndex] = {
type: t,
fieldName: prop.fieldName,
comma: false,
decimal: false,
decimalPlace: 2,
};
}
}
"
:options="typeOption"
v-model="prop.type"
>
<template v-slot:option="scope">
<q-item
v-if="scope.opt"
v-bind="scope.itemProps"
class="row items-center col-12"
:id="`type-${scope.itemProps}`"
>
<q-avatar
size="sm"
class="q-mr-md"
:style="`background-color: hsla(${scope.opt.color}/0.2)`"
>
<q-icon
size="20px"
:name="scope.opt.icon"
:style="`color: hsl(${scope.opt.color})`"
/>
</q-avatar>
{{ scope.opt.label }}
</q-item>
</template>
<template v-slot:selected-item="scope">
<div v-if="scope.opt" class="row items-center col-12">
<q-avatar
size="xs"
class="q-mr-sm"
:style="`background-color: hsla(${scope.opt.color}/0.2)`"
>
<q-icon
size="14px"
:name="scope.opt.icon"
:style="`color: hsl(${scope.opt.color})`"
/>
</q-avatar>
{{ scope.opt.label }}
</div>
</template>
</q-select>
<div v-if="prop.type !== 'date' && prop.type">
<q-item class="no-padding" style="font-size: 11px">
<q-item-section
class="column q-py-sm"
:style="{ 'padding-left: 12px': $q.screen.gt.xs }"
>
<span class="app-text-muted-2">
{{ $t('general.additional') }}
</span>
<div v-if="prop.type === 'string'" class="q-gutter-y-sm">
<div class="row items-center">
<div class="col-7 surface-3 rounded q-mr-sm q-py-xs">
<q-checkbox
:for="`checkbox-is-phone-number-${prop.fieldName}`"
v-if="prop.type === 'string'"
v-model="prop.isPhoneNumber"
size="xs"
/>
{{ $t('general.telephone') }}
</div>
<q-input
v-if="prop.type === 'string'"
:for="`input-max-length-${prop.fieldName}`"
v-model="prop.phoneNumberLength"
input-class="text-caption"
class="col additional-label"
dense
outlined
:label="$t('form.maxLength')"
/>
</div>
</div>
<div v-if="prop.type === 'number'" class="q-gutter-y-sm">
<div class="row items-center">
<div class="col-md-4 col-12 surface-3 rounded">
<q-checkbox
v-model="prop.comma"
size="xs"
class="q-py-xs"
:for="`checkbox-is-comma-${prop.fieldName}`"
/>
{{ $t('form.useComma') }}
</div>
<div
class="col-md-4 col-7 surface-3 rounded"
:class="{
'q-mx-sm': $q.screen.gt.sm,
'q-mr-sm q-mt-xs': $q.screen.lt.md,
}"
>
<q-checkbox
v-model="prop.decimal"
size="xs"
class="q-py-xs"
:for="`checkbox-is-decimal-${prop.fieldName}`"
/>
{{ $t('form.decimal') }}
</div>
<q-input
:for="`input-decimal-place-${prop.fieldName}`"
v-model="prop.decimalPlace"
class="col additional-label"
:class="{ 'q-mt-xs': $q.screen.lt.md }"
input-class="text-caption"
dense
outlined
:label="$t('form.decimalPlace')"
/>
</div>
</div>
<div v-if="prop.type === 'array'" class="q-gutter-y-sm">
<div
class="row items-center justify-between"
v-for="(_, i) in prop.options"
:key="i"
>
<div class="col rounded">
<q-input
v-model="prop.options[i]"
:for="`input-selection-${prop.fieldName}-${i}`"
class="col additional-label"
dense
outlined
input-class="text-caption"
:label="$t('form.selection')"
:rules="[
(val) => !!val || $t('form.error.required'),
]"
hide-bottom-space
/>
</div>
<div class="col-1 q-pl-sm">
<q-btn
:id="`btn-delete-selection-${prop.fieldName}-${i}`"
:for="`btn-delete-selection-${prop.fieldName}-${i}`"
@click="
() => {
prop.options.splice(i, 1);
}
"
dense
flat
icon="mdi-trash-can-outline"
class="bordered"
text-color="negative"
style="border-radius: 6px"
/>
</div>
</div>
<div class="row">
<q-btn
:for="`btn-add-selection-${prop.fieldName}`"
:id="`btn-add-selection-${prop.fieldName}`"
@click="
() => {
prop.options.push('');
}
"
dense
flat
icon="mdi-plus"
class="bordered col-11"
text-color="grey"
style="border-radius: 6px"
/>
</div>
</div>
</q-item-section>
</q-item>
</div>
</div>
<q-btn
id="btn-delete-work-product"
icon="mdi-trash-can-outline"
dense
flat
round
color="negative"
class="q-ml-sm"
@click="
confirmDelete(
tempStep[stepIndex].attributes.properties,
propIndex,
)
"
/>
</div>
</div>
</template>
</section>
</div>
</DialogForm>
</template>
<style scoped></style>