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,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));
}

View file

@ -8,6 +8,7 @@ type Menu = {
anchor: string;
name: string;
sub?: boolean;
hideSubIndex?: boolean;
tab?: string;
useBtn?: boolean;
};
@ -98,10 +99,10 @@ onUnmounted(() => {
class="row no-wrap items-center"
:class="{ 'app-text-muted': v.sub && activeMenu !== v.anchor }"
>
<div v-if="v.sub" class="circle-2"></div>
<div v-if="v.sub" class="circle-2 q-mr-md"></div>
<div
v-if="v.sub"
class="surface-tab circle flex justify-center q-mx-md"
v-if="v.sub && !v.hideSubIndex"
class="surface-tab circle flex justify-center q-mr-md"
>
{{ menu.filter((v) => v.sub === true).indexOf(v) + 1 }}
</div>

View file

@ -21,7 +21,8 @@ const props = withDefaults(
| 'blue'
| 'lime'
| 'light-purple'
| 'light-green';
| 'light-green'
| 'gray';
}[];
dark?: boolean;
textSize?: string;
@ -143,6 +144,10 @@ const props = withDefaults(
--_color: var(--blue-6-hsl);
}
.stat-card__gray {
--_color: var(--gray-6-hsl);
}
.dark .stat-card__purple {
--_color: var(--violet-10-hsl);
}

View file

@ -0,0 +1,688 @@
<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 { Option } from 'stores/options/types';
import NoData from '../NoData.vue';
import DialogForm from '../DialogForm.vue';
import { WorkFlowPayloadStep } from 'src/stores/workflow-template/types';
const { t } = useI18n();
const optionStore = useOptionStore();
const props = defineProps<{
stepIndex?: number;
}>();
const model = defineModel<boolean>({ required: true, default: false });
const dataStep = defineModel<WorkFlowPayloadStep[]>('dataStep', {
required: true,
default: [],
});
const tempStep = ref<WorkFlowPayloadStep[]>([]);
const propertiesOption = ref();
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() {
dataStep.value = JSON.parse(JSON.stringify(tempStep.value));
model.value = false;
}
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.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) return;
if (defaultPropType === 'date') {
tempStep.value[stepIndex].attributes.properties.push({
type: defaultPropType,
fieldName,
});
}
if (defaultPropType === 'array') {
tempStep.value[stepIndex].attributes.properties.push({
type: defaultPropType,
fieldName,
options: [],
});
}
if (defaultPropType === 'string') {
tempStep.value[stepIndex].attributes.properties.push({
type: defaultPropType,
fieldName,
isPhoneNumber: false,
phoneNumberLength: 10,
});
}
if (defaultPropType === 'number') {
tempStep.value[stepIndex].attributes.properties.push({
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: () => {},
});
}
watch(
() => model.value,
() => {
if (model.value) {
propertiesOption.value = optionStore.globalOption?.servicePropertiesField;
tempStep.value = JSON.parse(JSON.stringify(dataStep.value));
}
},
);
</script>
<template>
<DialogForm
no-address
no-app-box
height="60vh"
width="75%"
:title="$t('general.properties')"
v-model:modal="model"
:submit="submit"
:close="close"
>
<div class="column">
<section
v-for="(step, stepIndex) in dataStep"
: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
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.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
:key="index"
@click="manageProperties(stepIndex, ops.value, ops.type)"
:for="`list-${ops.value}`"
:id="`list-${ops.value}`"
>
<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"
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, propIndex)"
/>
<q-btn
id="btn-move-down-product"
icon="mdi-arrow-down"
dense
flat
round
:size="$q.screen.xs ? 'xs' : ''"
:disable="propIndex === tempStep.length - 1"
style="color: hsl(var(--text-mute-2))"
@click="moveItemDown(tempStep, 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"
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 class="col-md col-12">
<q-select
dense
outlined
emit-value
map-options
hide-bottom-space
for="input-properties-type"
id="input-properties-type"
: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, propIndex)"
/>
</div>
</div>
</template>
</section>
</div>
</DialogForm>
</template>
<style scoped></style>

View file

@ -11,12 +11,14 @@ const props = withDefaults(
option: Record<string, unknown>[];
optionLabel?: string;
separatorIndex?: number[];
}>(),
{
readonly: false,
option: () => [],
optionLabel: 'label',
offset: () => [0, 12],
separatorIndex: () => [],
},
);
@ -88,6 +90,7 @@ defineEmits<{
dense
:key="i"
class="flex items-center"
:class="{ 'bordered-t': separatorIndex.includes(i) }"
clickable
@click.stop="$emit('select', opt)"
>
@ -100,6 +103,10 @@ defineEmits<{
</span>
</q-item>
</template>
<template v-if="$slots.append">
<slot name="append"></slot>
</template>
</q-menu>
</template>
<style scoped></style>