fix(04): service with workflow

This commit is contained in:
puriphatt 2024-10-29 17:17:32 +07:00
parent e2d2f526e8
commit aaa39fc72f
4 changed files with 994 additions and 557 deletions

View file

@ -74,7 +74,12 @@ async function addWork() {
workItems.value.push({ workItems.value.push({
id: '', id: '',
name: '', name: '',
attributes: { additional: [], showTotalPrice: false }, attributes: {
additional: [],
showTotalPrice: false,
stepProperties: [],
workflowId: '',
},
product: [], product: [],
}); });
await nextTick(); await nextTick();
@ -177,8 +182,16 @@ watch(
v-model:product-items="work.product" v-model:product-items="work.product"
v-model:attributes="work.attributes" v-model:attributes="work.attributes"
@add-product="$emit('addProduct', index)" @add-product="$emit('addProduct', index)"
@move-work-up="moveItemUp(workItems, index)" @move-work-up="
@move-work-down="moveItemDown(workItems, index)" () => {
moveItemUp(workItems, index);
}
"
@move-work-down="
() => {
moveItemDown(workItems, index);
}
"
@delete-work="confirmDelete(workItems, index)" @delete-work="confirmDelete(workItems, index)"
@move-product-up="moveItemUp" @move-product-up="moveItemUp"
@move-product-down="moveItemDown" @move-product-down="moveItemDown"

View file

@ -1,20 +1,35 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Option } from 'stores/options/types'; import { Option } from 'stores/options/types';
import { Attributes } from 'stores/product-service/types'; import { Attributes } from 'stores/product-service/types';
import { moveItemUp, moveItemDown, deleteItem, dialog } from 'stores/utils'; import { moveItemUp, moveItemDown, deleteItem, dialog } from 'stores/utils';
import { storeToRefs } from 'pinia';
import { QSelect } from 'quasar';
import { useWorkflowTemplate } from 'src/stores/workflow-template';
import { WorkflowTemplate } from 'src/stores/workflow-template/types';
import SelectInput from '../shared/SelectInput.vue';
import NoData from '../NoData.vue'; import NoData from '../NoData.vue';
const { t } = useI18n(); const { t } = useI18n();
const workflowStore = useWorkflowTemplate();
const { data: workflowData } = storeToRefs(workflowStore);
const propertiesOption = const propertiesOption =
defineModel<(Option & { type: Attributes['additional'][number]['type'] })[]>( defineModel<(Option & { type: Attributes['additional'][number]['type'] })[]>(
'propertiesOption', 'propertiesOption',
); );
const formServiceProperties = defineModel<Attributes>('formServiceProperties'); const formServiceProperties = defineModel<Attributes>('formServiceProperties', {
required: true,
default: {
workflowId: '',
stepProperties: [],
},
});
const currWorkflow = ref<WorkflowTemplate>();
const typeOption = ref([ const typeOption = ref([
{ {
label: 'Text', label: 'Text',
@ -43,21 +58,22 @@ const typeOption = ref([
]); ]);
function manageProperties( function manageProperties(
stepIndex: number,
property: string, property: string,
propertyType?: Attributes['additional'][number]['type'], propertyType?: Attributes['additional'][number]['type'],
) { ) {
if (property === 'all' && propertiesOption.value) { if (property === 'all' && propertiesOption.value) {
if ( if (
formServiceProperties.value?.additional.length === formServiceProperties.value.stepProperties[stepIndex].attributes
propertiesOption.value.length .length === propertiesOption.value.length
) { ) {
formServiceProperties.value.additional = []; formServiceProperties.value.stepProperties[stepIndex].attributes = [];
return; return;
} }
for (const ops of propertiesOption.value) { for (const ops of propertiesOption.value) {
if ( if (
formServiceProperties.value?.additional.some( formServiceProperties.value.stepProperties[stepIndex].attributes.some(
(prop) => prop.fieldName === ops.value, (prop) => prop.fieldName === ops.value,
) )
) { ) {
@ -65,22 +81,22 @@ function manageProperties(
} }
if (ops.type === 'date') { if (ops.type === 'date') {
formServiceProperties.value?.additional.push({ formServiceProperties.value.stepProperties[stepIndex].attributes.push({
type: ops.type, type: ops.type,
fieldName: ops.value, fieldName: ops.value,
}); });
} }
if (ops.type === 'array') { if (ops.type === 'array') {
formServiceProperties.value?.additional.push({ formServiceProperties.value.stepProperties[stepIndex].attributes.push({
type: ops.type, type: ops.type,
fieldName: ops.value, fieldName: ops.value,
options: [''], options: [],
}); });
} }
if (ops.type === 'string') { if (ops.type === 'string') {
formServiceProperties.value?.additional.push({ formServiceProperties.value.stepProperties[stepIndex].attributes.push({
type: ops.type, type: ops.type,
fieldName: ops.value, fieldName: ops.value,
isPhoneNumber: false, isPhoneNumber: false,
@ -89,7 +105,7 @@ function manageProperties(
} }
if (ops.type === 'number') { if (ops.type === 'number') {
formServiceProperties.value?.additional.push({ formServiceProperties.value.stepProperties[stepIndex].attributes.push({
type: ops.type, type: ops.type,
fieldName: ops.value, fieldName: ops.value,
comma: false, comma: false,
@ -101,31 +117,34 @@ function manageProperties(
return; return;
} }
if (formServiceProperties.value) { if (formServiceProperties.value.stepProperties[stepIndex].attributes) {
const propertyIndex = formServiceProperties.value.additional.findIndex( const propertyIndex = formServiceProperties.value.stepProperties[
(prop) => prop.fieldName === property, stepIndex
); ].attributes.findIndex((prop) => prop.fieldName === property);
if (propertyIndex !== -1) { if (propertyIndex !== -1) {
formServiceProperties.value.additional.splice(propertyIndex, 1); formServiceProperties.value.stepProperties[stepIndex].attributes.splice(
propertyIndex,
1,
);
} else { } else {
if (propertyType === 'date') { if (propertyType === 'date') {
formServiceProperties.value?.additional.push({ formServiceProperties.value.stepProperties[stepIndex].attributes.push({
type: propertyType, type: propertyType,
fieldName: property, fieldName: property,
}); });
} }
if (propertyType === 'array') { if (propertyType === 'array') {
formServiceProperties.value?.additional.push({ formServiceProperties.value.stepProperties[stepIndex].attributes.push({
type: propertyType, type: propertyType,
fieldName: property, fieldName: property,
options: [''], options: [],
}); });
} }
if (propertyType === 'string') { if (propertyType === 'string') {
formServiceProperties.value?.additional.push({ formServiceProperties.value.stepProperties[stepIndex].attributes.push({
type: propertyType, type: propertyType,
fieldName: property, fieldName: property,
isPhoneNumber: false, isPhoneNumber: false,
@ -134,7 +153,7 @@ function manageProperties(
} }
if (propertyType === 'number') { if (propertyType === 'number') {
formServiceProperties.value?.additional.push({ formServiceProperties.value.stepProperties[stepIndex].attributes.push({
type: propertyType, type: propertyType,
fieldName: property, fieldName: property,
comma: false, comma: false,
@ -146,16 +165,18 @@ function manageProperties(
} }
} }
function shouldShowItem(opt: Option) { function shouldShowItem(opt: Option, stepIndex: number) {
if (formServiceProperties.value) { if (formServiceProperties.value.stepProperties[stepIndex].attributes) {
const additionalFieldNames = new Set( const additionalFieldNames = new Set(
formServiceProperties.value.additional.map((o) => o.fieldName), formServiceProperties.value.stepProperties[stepIndex].attributes.map(
(o) => o.fieldName,
),
); );
return !!opt && !additionalFieldNames.has(opt.value); return !!opt && !additionalFieldNames.has(opt.value);
} }
} }
function changeType(fieldName: string) { function changeType(fieldName: string, stepIndex: number) {
if (!propertiesOption.value) return; if (!propertiesOption.value) return;
const defaultPropType = propertiesOption.value.find( const defaultPropType = propertiesOption.value.find(
@ -164,29 +185,29 @@ function changeType(fieldName: string) {
if (!defaultPropType) return; if (!defaultPropType) return;
const idx = formServiceProperties.value?.additional.findIndex( const idx = formServiceProperties.value.stepProperties[
(v) => v.fieldName === fieldName, stepIndex
); ].attributes.findIndex((v) => v.fieldName === fieldName);
if (!idx) return; if (!idx) return;
if (defaultPropType === 'date') { if (defaultPropType === 'date') {
formServiceProperties.value?.additional.push({ formServiceProperties.value.stepProperties[stepIndex].attributes.push({
type: defaultPropType, type: defaultPropType,
fieldName, fieldName,
}); });
} }
if (defaultPropType === 'array') { if (defaultPropType === 'array') {
formServiceProperties.value?.additional.push({ formServiceProperties.value.stepProperties[stepIndex].attributes.push({
type: defaultPropType, type: defaultPropType,
fieldName, fieldName,
options: [''], options: [],
}); });
} }
if (defaultPropType === 'string') { if (defaultPropType === 'string') {
formServiceProperties.value?.additional.push({ formServiceProperties.value.stepProperties[stepIndex].attributes.push({
type: defaultPropType, type: defaultPropType,
fieldName, fieldName,
isPhoneNumber: false, isPhoneNumber: false,
@ -195,7 +216,7 @@ function changeType(fieldName: string) {
} }
if (defaultPropType === 'number') { if (defaultPropType === 'number') {
formServiceProperties.value?.additional.push({ formServiceProperties.value.stepProperties[stepIndex].attributes.push({
type: defaultPropType, type: defaultPropType,
fieldName, fieldName,
comma: false, comma: false,
@ -218,32 +239,141 @@ function confirmDelete(items: unknown[], index: number) {
cancel: () => {}, cancel: () => {},
}); });
} }
async function filter(val: string, update: (...args: unknown[]) => void) {
update(
async () => {
await fetchWorkflowOption(val);
},
(ref: QSelect) => {
if (val !== '' && ref.options && ref.options?.length > 0) {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
},
);
}
async function fetchWorkflowOption(val?: string) {
const res = await workflowStore.getWorkflowTemplateList({
query: val,
pageSize: 30,
});
if (res) workflowData.value = res.result;
}
function mapStepName(id: string) {
const targetFlow = workflowData.value.find(
(w) => w.id === formServiceProperties.value.workflowId,
);
if (!targetFlow) return;
const name = targetFlow.step.find((s) => s.id === id)?.name;
return name || '-';
}
onMounted(async () => {
await fetchWorkflowOption();
});
watch(
() => formServiceProperties.value.workflowId,
() => {
if (!formServiceProperties.value.workflowId) return;
formServiceProperties.value.stepProperties = [];
currWorkflow.value = workflowData.value.find(
(s) => s.id === formServiceProperties.value.workflowId,
);
if (!currWorkflow.value) return;
currWorkflow.value.step.forEach((s) => {
formServiceProperties.value.stepProperties.push({
id: s.id,
productsId: [],
attributes: [],
});
});
},
);
</script> </script>
<template> <template>
<div class="full-width full-height column no-wrap"> <div
<div class="row"> class="bordered-b surface-3 row items-center no-wrap q-py-sm"
<q-btn-dropdown :class="{
id="btn-dropdow-properties" 'q-px-lg': $q.screen.gt.sm,
for="btn-dropdow-properties" 'q-px-md': !$q.screen.gt.sm,
dense }"
unelevated >
color="primary" {{ $t('flow.title') }}
:label="$t('productService.service.properties')" <SelectInput
class="q-px-sm q-mb-lg text-capitalize" v-model="formServiceProperties.workflowId"
menu-anchor="bottom end" id="select-workflow"
for="select-workflow"
incremental
:option="workflowData"
option-label="name"
option-value="id"
class="q-ml-md"
:label="$t('general.select', { msg: $t('flow.title') })"
@filter="(val: string, update) => filter(val, update)"
/>
</div>
<div class="col column no-wrap">
<div
v-if="!formServiceProperties?.workflowId"
class="bordered rounded surface-1 flex justify-center items-center col"
:class="{
'q-my-md q-mx-lg': $q.screen.gt.sm,
'q-my-sm q-mx-md ': !$q.screen.gt.sm,
}"
>
<NoData use-field />
</div>
<!-- attributes step -->
<div v-if="formServiceProperties?.workflowId" class="column">
<section
v-for="(step, stepIndex) in formServiceProperties.stepProperties"
:key="step.id"
class="column"
>
<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: stepIndex + 1 }) }}
<span class="app-text-muted">: {{ mapStepName(step.id) }}</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="
formServiceProperties?.stepProperties[stepIndex].attributes &&
propertiesOption
"
> >
<q-list dense v-if="formServiceProperties && propertiesOption">
<q-item <q-item
for="list-all" for="list-all"
id="list-all" id="list-all"
clickable clickable
@click="manageProperties('all')" @click="manageProperties(stepIndex, 'all')"
> >
<div class="full-width flex items-center"> <div class="full-width flex items-center">
<q-icon <q-icon
v-if=" v-if="
formServiceProperties.additional.length === formServiceProperties?.stepProperties[stepIndex]
propertiesOption.length .attributes.length === propertiesOption.length
" "
name="mdi-checkbox-marked" name="mdi-checkbox-marked"
size="xs" size="xs"
@ -265,17 +395,17 @@ function confirmDelete(items: unknown[], index: number) {
v-for="(ops, index) in propertiesOption" v-for="(ops, index) in propertiesOption"
clickable clickable
:key="index" :key="index"
@click="manageProperties(ops.value, ops.type)" @click="manageProperties(stepIndex, ops.value, ops.type)"
:for="`list-${ops.value}`" :for="`list-${ops.value}`"
:id="`list-${ops.value}`" :id="`list-${ops.value}`"
> >
<div class="full-width flex items-center no-wrap"> <div class="full-width flex items-center no-wrap">
<q-icon <q-icon
v-if=" v-if="
formServiceProperties && formServiceProperties?.stepProperties[stepIndex] &&
formServiceProperties.additional.some( formServiceProperties?.stepProperties[
(add) => add.fieldName === ops.value, stepIndex
) ].attributes.some((add) => add.fieldName === ops.value)
" "
name="mdi-checkbox-marked" name="mdi-checkbox-marked"
size="xs" size="xs"
@ -294,29 +424,34 @@ function confirmDelete(items: unknown[], index: number) {
</q-item> </q-item>
</q-list> </q-list>
</q-btn-dropdown> </q-btn-dropdown>
</div> </span>
<!-- step attribute -->
<div <div
v-if="formServiceProperties?.additional.length === 0" :class="{
class="bordered rounded surface-1 flex justify-center items-center col" 'q-px-lg': $q.screen.gt.sm,
> 'q-px-md': !$q.screen.gt.sm,
<NoData use-field /> }"
</div>
<div
v-if="
formServiceProperties?.additional &&
formServiceProperties.additional.length > 0
"
class="q-gutter-y-md"
> >
<div <div
v-for="(p, index) in formServiceProperties.additional" v-for="(p, index) in formServiceProperties?.stepProperties[
stepIndex
].attributes"
:key="index" :key="index"
class="bordered surface-1 rounded q-py-sm q-px-md row items-start" class="bordered surface-1 rounded q-py-sm q-px-md row items-start"
:class=" :class="{
index === formServiceProperties.additional.length - 1 && 'q-mb-lg' 'q-mt-md': index === 0,
" 'q-mb-sm':
index !==
formServiceProperties.stepProperties[stepIndex].attributes
.length -
1,
'q-mb-md':
index ===
formServiceProperties.stepProperties[stepIndex].attributes
.length -
1,
}"
> >
<div class="col-md col-12 row items-center"> <div class="col-md col-12 row items-center">
<q-btn <q-btn
@ -328,7 +463,12 @@ function confirmDelete(items: unknown[], index: number) {
:disable="index === 0" :disable="index === 0"
:size="$q.screen.xs ? 'xs' : ''" :size="$q.screen.xs ? 'xs' : ''"
style="color: hsl(var(--text-mute-2))" style="color: hsl(var(--text-mute-2))"
@click="moveItemUp(formServiceProperties.additional, index)" @click="
moveItemUp(
formServiceProperties.stepProperties[stepIndex].attributes,
index,
)
"
/> />
<q-btn <q-btn
id="btn-move-down-product" id="btn-move-down-product"
@ -337,9 +477,19 @@ function confirmDelete(items: unknown[], index: number) {
flat flat
round round
:size="$q.screen.xs ? 'xs' : ''" :size="$q.screen.xs ? 'xs' : ''"
:disable="index === formServiceProperties.additional.length - 1" :disable="
index ===
formServiceProperties.stepProperties[stepIndex].attributes
.length -
1
"
style="color: hsl(var(--text-mute-2))" style="color: hsl(var(--text-mute-2))"
@click="moveItemDown(formServiceProperties.additional, index)" @click="
moveItemDown(
formServiceProperties.stepProperties[stepIndex].attributes,
index,
)
"
/> />
<q-avatar <q-avatar
@ -365,11 +515,11 @@ function confirmDelete(items: unknown[], index: number) {
option-value="value" option-value="value"
:options="propertiesOption" :options="propertiesOption"
v-model="p.fieldName" v-model="p.fieldName"
@update:model-value="changeType" @update:model-value="(v) => changeType(v, stepIndex)"
> >
<template v-slot:option="scope"> <template v-slot:option="scope">
<q-item <q-item
v-if="scope.opt && shouldShowItem(scope.opt)" v-if="scope.opt && shouldShowItem(scope.opt, stepIndex)"
v-bind="scope.itemProps" v-bind="scope.itemProps"
class="row items-center col-12" class="row items-center col-12"
> >
@ -393,24 +543,34 @@ function confirmDelete(items: unknown[], index: number) {
option-value="value" option-value="value"
@update:model-value=" @update:model-value="
(t: 'string' | 'number' | 'date' | 'array') => { (t: 'string' | 'number' | 'date' | 'array') => {
if (!formServiceProperties) return; if (
!formServiceProperties.stepProperties[stepIndex]
.attributes
)
return;
if (t === 'date') { if (t === 'date') {
formServiceProperties.additional[index] = { formServiceProperties.stepProperties[
stepIndex
].attributes[index] = {
type: t, type: t,
fieldName: p.fieldName, fieldName: p.fieldName,
}; };
} }
if (t === 'array') { if (t === 'array') {
formServiceProperties.additional[index] = { formServiceProperties.stepProperties[
stepIndex
].attributes[index] = {
type: t, type: t,
fieldName: p.fieldName, fieldName: p.fieldName,
options: [''], options: [],
}; };
} }
if (t === 'string') { if (t === 'string') {
formServiceProperties.additional[index] = { formServiceProperties.stepProperties[
stepIndex
].attributes[index] = {
type: t, type: t,
fieldName: p.fieldName, fieldName: p.fieldName,
isPhoneNumber: false, isPhoneNumber: false,
@ -419,7 +579,9 @@ function confirmDelete(items: unknown[], index: number) {
} }
if (t === 'number') { if (t === 'number') {
formServiceProperties.additional[index] = { formServiceProperties.stepProperties[
stepIndex
].attributes[index] = {
type: t, type: t,
fieldName: p.fieldName, fieldName: p.fieldName,
comma: false, comma: false,
@ -560,7 +722,9 @@ function confirmDelete(items: unknown[], index: number) {
outlined outlined
input-class="text-caption" input-class="text-caption"
:label="$t('form.selection')" :label="$t('form.selection')"
:rules="[(val) => !!val || $t('form.error.required')]" :rules="[
(val) => !!val || $t('form.error.required'),
]"
hide-bottom-space hide-bottom-space
/> />
</div> </div>
@ -613,10 +777,17 @@ function confirmDelete(items: unknown[], index: number) {
round round
color="negative" color="negative"
class="q-ml-sm" class="q-ml-sm"
@click="confirmDelete(formServiceProperties.additional, index)" @click="
confirmDelete(
formServiceProperties.stepProperties[stepIndex].attributes,
index,
)
"
/> />
</div> </div>
</div> </div>
</section>
</div>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

View file

@ -1,21 +1,32 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Icon } from '@iconify/vue'; import { QSelect } from 'quasar';
import { formatNumberDecimal } from 'stores/utils';
import useProductServiceStore from 'stores/product-service';
import useOptionStore from 'stores/options';
import { Attributes, Product } from 'stores/product-service/types';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue'; import { Icon } from '@iconify/vue';
import { onMounted, ref, watch } from 'vue';
import useOptionStore from 'stores/options';
import useProductServiceStore from 'stores/product-service';
import { formatNumberDecimal } from 'stores/utils';
import { useWorkflowTemplate } from 'src/stores/workflow-template';
import { Attributes, Product } from 'stores/product-service/types';
import { WorkflowTemplate } from 'src/stores/workflow-template/types';
import SelectInput from '../shared/SelectInput.vue';
import NoData from '../NoData.vue';
import { AddButton } from '../button';
const baseUrl = ref<string>(import.meta.env.VITE_API_BASE_URL); const baseUrl = ref<string>(import.meta.env.VITE_API_BASE_URL);
const productServiceStore = useProductServiceStore(); const productServiceStore = useProductServiceStore();
const optionStore = useOptionStore(); const optionStore = useOptionStore();
const workflowStore = useWorkflowTemplate();
const { fetchListOfWork } = productServiceStore; // const { fetchListOfWork } = productServiceStore;
const { workNameItems } = storeToRefs(productServiceStore); const { splitPay } = storeToRefs(productServiceStore);
withDefaults( const { getWorkflowTemplateList } = workflowStore;
const { data: workflowData } = storeToRefs(workflowStore);
const props = withDefaults(
defineProps<{ defineProps<{
workIndex: number; workIndex: number;
length: number; length: number;
@ -46,7 +57,7 @@ const productItems = defineModel<(Product & { nameEn: string })[]>(
}, },
); );
const refMenu = ref(); // const refMenu = ref();
defineEmits<{ defineEmits<{
(e: 'moveWorkUp'): void; (e: 'moveWorkUp'): void;
@ -61,18 +72,94 @@ defineEmits<{
(e: 'workProperties'): void; (e: 'workProperties'): void;
}>(); }>();
watch( // watch(
() => workNameItems.value, // () => workNameItems.value,
(c, o) => { // (c, o) => {
const list = c.map((v: { name: string }) => v.name); // const list = c.map((v: { name: string }) => v.name);
const oldList = o.map((v: { name: string }) => v.name); // const oldList = o.map((v: { name: string }) => v.name);
const index = oldList.indexOf(workName.value); // const index = oldList.indexOf(workName.value);
if (list[index] !== oldList[index] && !list.includes(workName.value)) { // if (list[index] !== oldList[index] && !list.includes(workName.value)) {
if (list.length - 1 === index - 1) workName.value = list[index - 1]; // if (list.length - 1 === index - 1) workName.value = list[index - 1];
else workName.value = list[index]; // else workName.value = list[index];
// }
// },
// );
function mapFlowName(id: string): string {
const targetFlow = workflowData.value.find(
(w) => w.id === attributes.value.workflowId,
);
return targetFlow?.name || '';
}
function mapStepName(id: string) {
const targetFlow = workflowData.value.find(
(w) => w.id === attributes.value.workflowId,
);
if (!targetFlow) return;
const name = targetFlow.step.find((s) => s.id === id)?.name;
return name || '-';
}
function selectFlow(workflow: WorkflowTemplate) {
workName.value = workflow.name;
attributes.value.workflowId = workflow.id;
attributes.value.stepProperties = workflow.step.map((s) => ({
id: s.id,
attributes: [],
productsId: [],
}));
}
async function filter(val: string, update: (...args: unknown[]) => void) {
update(
async () => {
await fetchWorkflowOption(val);
},
(ref: QSelect) => {
if (val !== '' && ref.options && ref.options?.length > 0) {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
} }
}, },
);
}
async function fetchWorkflowOption(val?: string) {
const res = await workflowStore.getWorkflowTemplateList({
query: val,
pageSize: 30,
});
if (res) workflowData.value = res.result;
}
function toggleCheckProductInStep(id: string, stepIndex: number) {
const index =
attributes.value.stepProperties[stepIndex].productsId.indexOf(id);
if (!attributes.value.stepProperties[stepIndex].productsId.includes(id)) {
attributes.value.stepProperties[stepIndex].productsId.push(id);
} else {
attributes.value.stepProperties[stepIndex].productsId.splice(index, 1);
}
}
onMounted(async () => {
if (props.workIndex === 0) {
await fetchWorkflowOption();
}
});
watch(
() => productItems.value,
() => {
attributes.value.stepProperties.forEach((s) => {
s.productsId = productItems.value.map((p) => p.id);
});
},
{ deep: true },
); );
</script> </script>
<template> <template>
@ -84,7 +171,7 @@ watch(
switch-toggle-side switch-toggle-side
default-opened default-opened
expand-icon="mdi-chevron-down-circle" expand-icon="mdi-chevron-down-circle"
header-class="surface-2 expansion-rounded" header-class="expansion-rounded"
header-style="border-top-left-radius: var(--radius-2); border-top-right-radius: var(--radius-2)" header-style="border-top-left-radius: var(--radius-2); border-top-right-radius: var(--radius-2)"
> >
<template v-slot:header> <template v-slot:header>
@ -115,7 +202,7 @@ watch(
style="color: hsl(var(--text-mute-2))" style="color: hsl(var(--text-mute-2))"
@click.stop="$emit('moveWorkDown')" @click.stop="$emit('moveWorkDown')"
/> />
<div <!-- <div
:for="`select-work-name-${index + 1}`" :for="`select-work-name-${index + 1}`"
class="col q-py-sm q-px-md" class="col q-py-sm q-px-md"
style="background-color: var(--surface-1); z-index: 2" style="background-color: var(--surface-1); z-index: 2"
@ -184,7 +271,28 @@ watch(
</div> </div>
</q-item> </q-item>
</q-menu> </q-menu>
</div> </div> -->
<SelectInput
:readonly
incremental
:model-value="mapFlowName(attributes.workflowId)"
id="select-workflow-name"
for="select-workflow-name"
class="col"
option-label="name"
:option="workflowData"
:placeholder="$t('productService.service.workName')"
@update:model-value="(val: WorkflowTemplate) => selectFlow(val)"
@filter="(val: string, update) => filter(val, update)"
>
<template #prepend>
<span class="text-body2" style="color: var(--foreground)">
{{
$t('productService.service.workNo', { msg: workIndex + 1 })
}}:
</span>
</template>
</SelectInput>
<q-btn <q-btn
v-if="!readonly" v-if="!readonly"
id="btn-delete-work" id="btn-delete-work"
@ -203,60 +311,14 @@ watch(
</div> </div>
</template> </template>
<div class="surface-2"> <section
<!-- properties --> v-if="!workName && productItems.length === 0"
<div class="bordered-t"> class="surface-2 row items-center justify-center q-py-sm"
<div
class="q-py-xs text-weight-medium row justify-between items-center q-px-md"
style="background-color: hsla(var(--info-bg) / 0.1)"
> >
<span> <NoData />
{{ $t('productService.service.propertiesInWork') }} </section>
{{ workIndex + 1 }}
</span>
<q-btn
v-if="!readonly"
id="btn-add-work-product"
class="text-capitalize"
flat
dense
padding="0"
style="color: hsl(var(--info-bg))"
@click.stop="$emit('workProperties')"
>
<Icon
icon="basil:settings-adjust-solid"
width="24px"
class="q-mr-sm"
style="color: hsl(var(--info-bg))"
/>
<span v-if="$q.screen.gt.xs">
{{ $t('productService.service.properties') }}
</span>
</q-btn>
</div>
<div class="q-py-md q-px-md full-width">
<div
v-if="attributes.additional.length > 0"
class="row items-center full-width surface-1 q-pb-md q-pt-sm q-px-sm q-gutter-sm scroll"
:style="$q.screen.xs ? 'max-height: 100px' : ''"
>
<div
v-for="(p, index) in attributes.additional"
:key="index"
class="bordered q-px-sm surface-3"
style="border-radius: 6px"
>
{{ optionStore.mapOption(p.fieldName ?? '') }}
</div>
</div>
<div v-else class="app-text-muted">
{{ $t('productService.service.noProperties') }}
</div>
</div>
</div>
<section v-else class="surface-2">
<!-- product --> <!-- product -->
<div class="bordered-t"> <div class="bordered-t">
<div <div
@ -276,19 +338,11 @@ watch(
:disable="readonly" :disable="readonly"
/> />
</span> </span>
<q-btn <AddButton
v-if="!readonly" v-if="!readonly"
icon-only
id="btn-add-work-product" id="btn-add-work-product"
for="btn-add-work-product" for="btn-add-work-product"
flat
dense
icon="mdi-plus"
class="text-capitalize"
:label="
$q.screen.gt.xs ? $t('productService.product.addTitle') : ''
"
padding="0"
style="color: hsl(var(--info-bg))"
@click.stop="$emit('addProduct')" @click.stop="$emit('addProduct')"
/> />
</div> </div>
@ -297,15 +351,17 @@ watch(
v-if="productItems.length > 0" v-if="productItems.length > 0"
class="q-py-md q-px-md full-width q-gutter-y-sm" class="q-py-md q-px-md full-width q-gutter-y-sm"
> >
<div <section
v-for="(product, index) in productItems" v-for="(product, index) in productItems"
:key="product.id" :key="product.id"
class="full-width row items-center justify-between" class="full-width row items-center justify-between"
> >
<div <div
class="row col items-center justify-between full-width surface-1 q-py-md q-px-sm" class="row col items-center justify-between full-width surface-1 q-px-sm q-py-xs"
style="min-height: 70px"
> >
<div <!-- product detail -->
<section
class="row items-center col-md col-12 no-wrap" class="row items-center col-md col-12 no-wrap"
v-if="productItems" v-if="productItems"
> >
@ -316,6 +372,7 @@ watch(
dense dense
flat flat
round round
size="sm"
:disable="index === 0" :disable="index === 0"
style="color: hsl(var(--text-mute-2))" style="color: hsl(var(--text-mute-2))"
@click.stop="$emit('moveProductUp', productItems, index)" @click.stop="$emit('moveProductUp', productItems, index)"
@ -328,27 +385,27 @@ watch(
dense dense
flat flat
round round
class="q-mx-sm" size="sm"
:disable="index === productItems.length - 1" :disable="index === productItems.length - 1"
style="color: hsl(var(--text-mute-2))" style="color: hsl(var(--text-mute-2))"
@click.stop="$emit('moveProductDown', productItems, index)" @click.stop="$emit('moveProductDown', productItems, index)"
/> />
<q-avatar <q-avatar
size="md" size="sm"
:class="$q.screen.gt.xs ? 'q-mx-lg' : 'q-mr-lg'" class="q-mx-sm"
style="background-color: var(--surface-tab)" style="background-color: var(--surface-tab)"
> >
{{ index + 1 }} {{ index + 1 }}
</q-avatar> </q-avatar>
<div class="row no-wrap"> <div class="col row no-wrap items-center">
<div <div
v-if="$q.screen.gt.xs" v-if="$q.screen.gt.xs"
class="bordered q-mx-md col-3 image-box" class="bordered q-mx-md col-3 image-box"
> >
<q-img <q-img
:src="`${baseUrl}/product/${product?.id}/image`" :src="`${baseUrl}/product/${product.id}/image/${product.selectedImage}`"
style="object-fit: cover; width: 100%; height: 100%" style="object-fit: cover; width: 100%; height: 100%"
> >
<template #error> <template #error>
@ -360,35 +417,30 @@ watch(
</template> </template>
</q-img> </q-img>
</div> </div>
<div class="column col justify-between"> <article class="column col full-width justify-between">
<span <span
class="text-weight-bold ellipsis-2-lines" class="text-weight-medium ellipsis-2-lines full-width"
:style="`max-width: ${$q.screen.gt.sm ? (!readonly ? '10vw' : '25vw') : '20vw'}`"
> >
{{ product.name }} {{ product.name }}
<q-tooltip> <q-tooltip>
{{ product.name }} {{ product.name }}
</q-tooltip> </q-tooltip>
</span> </span>
<div <div class="text-caption">
class="bordered q-px-xs ellipsis" <span class="bordered q-px-xs rounded q-mr-sm">
style="border-radius: 6px; max-width: 100px"
>
{{ product.code }} {{ product.code }}
</div> </span>
</div>
</div>
</div>
<div <q-icon name="mdi-clock-outline" />
class="row justify-end text-right col-md-6 col-12" {{ product.process }} {{ $t('general.day') }}
:class="$q.screen.xs ? 'q-mt-sm text-caption' : 'q-pr-sm'" </div>
> </article>
<span </div>
class="col-12 row" </section>
:class="{ 'q-col-gutter-md': $q.screen.gt.xs }"
style="color: var(--teal-9)" <!-- product price -->
> <div class="row justify-end text-right col-md col-12">
<span class="col-12 row" style="color: var(--teal-9)">
<span <span
v-if="priceDisplay?.price" v-if="priceDisplay?.price"
class="col ellipsis price-orange text-weight-bold" class="col ellipsis price-orange text-weight-bold"
@ -437,28 +489,33 @@ watch(
฿{{ formatNumberDecimal(product.serviceCharge, 2) }} ฿{{ formatNumberDecimal(product.serviceCharge, 2) }}
</q-tooltip> </q-tooltip>
</span> </span>
<span class="col ellipsis text-weight-bold"> <span class="col ellipsis column text-weight-medium">
<div class="text-caption app-text-muted-2"> <div class="text-caption app-text-muted-2">
งวดทาย งวดทาย
</div> </div>
{{ !readonly ? '' : product.installmentNo }} {{ !readonly ? '' : product.installmentNo }}
<span class="row justify-end">
<q-input <q-input
v-if="!readonly && $q.screen.gt.xs" v-if="!readonly && $q.screen.gt.xs"
outlined outlined
:max="splitPay"
input-class="text-right no-padding"
for="input-bankbook" for="input-bankbook"
hide-bottom-space hide-bottom-space
class="col-2" class="installment-no col-10"
dense dense
type="number" type="number"
v-model="product.installmentNo" v-model="product.installmentNo"
min="0" min="0"
@update:model-value="
(v) => {
if (Number(v) > splitPay)
product.installmentNo = splitPay;
}
"
/> />
</span> </span>
</span> </span>
<span class="col-9 q-mt-sm text-caption app-text-muted-2">
{{ $t('productService.product.processingTime') }}
{{ product.process }} {{ $t('general.day') }}
</span> </span>
</div> </div>
</div> </div>
@ -478,19 +535,149 @@ watch(
> >
<q-tooltip>{{ $t('general.delete') }}</q-tooltip> <q-tooltip>{{ $t('general.delete') }}</q-tooltip>
</q-btn> </q-btn>
</section>
</div> </div>
</div> <div v-else class="app-text-muted q-py-md q-px-md">
<div v-else class="app-text-muted q-py-md q-px-lg">
{{ $t('productService.product.noProduct') }} {{ $t('productService.product.noProduct') }}
</div> </div>
</div> </div>
<!-- properties -->
<div class="bordered-t">
<div
class="q-py-xs text-weight-medium row justify-between items-center q-px-md"
style="background-color: hsla(var(--info-bg) / 0.1)"
>
<span>
{{ $t('flow.processStep') }}
</span>
<q-btn
v-if="!readonly"
id="btn-add-work-product"
class="text-capitalize rounded"
flat
dense
padding="4px 8px"
style="color: hsl(var(--info-bg))"
@click.stop="$emit('workProperties')"
>
<Icon
icon="basil:settings-adjust-solid"
width="20.08px"
style="color: hsl(var(--info-bg))"
/>
</q-btn>
</div> </div>
<div class="q-py-md q-px-md full-width column">
<span
v-if="
attributes.stepProperties?.length === 0 ||
!attributes.stepProperties
"
class="app-text-muted"
>
{{ $t('flow.noProcessStep') }}
</span>
<span
v-else
v-for="(step, stepIndex) in attributes.stepProperties"
:key="step.id"
>
<q-icon name="mdi-circle-medium" />
{{ $t('flow.stepNo', { msg: stepIndex + 1 }) }}:
{{ mapStepName(step.id) }}
<!-- step att -->
<section
class="col scroll q-pa-sm flex items-center surface-1 rounded"
>
<div
v-if="
attributes.stepProperties[stepIndex].attributes.length > 0
"
class="row q-gutter-sm"
>
<span
v-for="(att, i) in step.attributes"
:key="i"
class="surface-2 bordered rounded q-px-xs"
>
{{ optionStore.mapOption(att.fieldName ?? '') }}
</span>
</div>
<div v-else class="app-text-muted-2">
{{ $t('productService.service.noProperties') }}
</div>
</section>
<!-- step product -->
<section
class="q-pt-sm q-pl-lg column"
:class="{
'q-pb-sm': stepIndex !== attributes.stepProperties.length - 1,
}"
>
<span class="app-text-muted-2 text-caption">
{{
$t('general.select', {
msg: $t('productService.product.title'),
})
}}
</span>
<div
v-if="productItems.length > 0"
class="surface-1 rounded q-pa-xs"
>
<div v-for="product in productItems" :key="product.id">
<q-checkbox
:disable="readonly"
:model-value="
attributes.stepProperties[
stepIndex
].productsId.includes(product.id)
"
@click="toggleCheckProductInStep(product.id, stepIndex)"
size="xs"
/>
{{ product.name }}
</div>
</div>
<span v-else class="app-text-muted-2 surface-1 rounded q-pa-xs">
{{ $t('productService.product.noProduct') }}
</span>
</section>
</span>
</div>
<!-- <div class="q-py-md q-px-md full-width">
<div
v-if="attributes.additional.length > 0"
class="row items-center full-width surface-1 q-pb-md q-pt-sm q-px-sm q-gutter-sm scroll"
:style="$q.screen.xs ? 'max-height: 100px' : ''"
>
<div
v-for="(p, index) in attributes.additional"
:key="index"
class="bordered q-px-sm surface-3"
style="border-radius: 6px"
>
{{ optionStore.mapOption(p.fieldName ?? '') }}
</div>
</div>
<div v-else class="app-text-muted">
{{ $t('productService.service.noProperties') }}
</div>
</div> -->
</div>
</section>
</q-expansion-item> </q-expansion-item>
<div class="q-py-sm q-px-md bordered-t row items-center justify-between"> <div class="q-py-sm q-px-md bordered-t row items-center justify-between">
<div> <div>
{{ $t('productService.service.totalProductWork') }} {{ $t('productService.service.totalProductWork') }}
<span class="app-text-muted-2"> <span class="app-text-muted-2">
{{ workName }} {{ mapFlowName(attributes.workflowId) }}
</span> </span>
</div> </div>
<div> <div>
@ -502,8 +689,8 @@ watch(
<style lang="scss" scoped> <style lang="scss" scoped>
.image-box { .image-box {
height: 70px; height: 45px;
width: 70px; width: 45px;
border-color: var(--teal-9); border-color: var(--teal-9);
border-radius: 10px; border-radius: 10px;
background-color: var(--surface-3); background-color: var(--surface-3);
@ -540,4 +727,18 @@ watch(
.price-pink { .price-pink {
color: var(--pink-6); color: var(--pink-6);
} }
:deep(i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon) {
color: hsl(var(--text-mute));
}
:deep(
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
color: var(--brand-1);
}
:deep(.installment-no .q-field__control) {
height: 23px;
}
</style> </style>

View file

@ -63,6 +63,7 @@ import {
Attributes, Attributes,
} from 'stores/product-service/types'; } from 'stores/product-service/types';
import { computed } from 'vue'; import { computed } from 'vue';
import { WorkflowTemplate } from 'src/stores/workflow-template/types';
const flowStore = useFlowStore(); const flowStore = useFlowStore();
const utilsStore = useUtilsStore(); const utilsStore = useUtilsStore();
@ -94,7 +95,7 @@ const {
deleteWork, deleteWork,
} = productServiceStore; } = productServiceStore;
const { workNameItems } = storeToRefs(productServiceStore); const { workNameItems, splitPay } = storeToRefs(productServiceStore);
const readOnlybranchOption = ref<boolean>(false); const readOnlybranchOption = ref<boolean>(false);
const allStat = ref<{ mode: string; count: number }[]>([]); const allStat = ref<{ mode: string; count: number }[]>([]);
const stat = ref< const stat = ref<
@ -264,10 +265,14 @@ const formDataProduct = ref<ProductCreate>({
image: undefined, image: undefined,
}); });
const workflow = ref<WorkflowTemplate>();
const formDataProductService = ref<ServiceCreate>({ const formDataProductService = ref<ServiceCreate>({
work: [], work: [],
attributes: { attributes: {
showTotalPrice: false,
additional: [], additional: [],
workflowId: '',
stepProperties: [],
}, },
detail: '', detail: '',
name: '', name: '',
@ -892,6 +897,9 @@ async function assignFormDataGroup(data: ProductGroup) {
const prevService = ref<ServiceCreate>({ const prevService = ref<ServiceCreate>({
work: [], work: [],
attributes: { attributes: {
showTotalPrice: false,
workflowId: '',
stepProperties: [],
additional: [], additional: [],
}, },
detail: '', detail: '',
@ -941,6 +949,11 @@ async function assignFormDataProductService(id: string) {
formDataProductService.value.work = prevService.value.work; formDataProductService.value.work = prevService.value.work;
workItems.value = res.work.map((item) => { workItems.value = res.work.map((item) => {
splitPay.value = Math.max(
...item.productOnWork.map(
(productOnWorkItem) => productOnWorkItem.installmentNo,
),
);
return { return {
id: item.id, id: item.id,
name: item.name, name: item.name,
@ -1043,13 +1056,16 @@ function clearFormService() {
name: '', name: '',
detail: '', detail: '',
attributes: { attributes: {
workflowId: '',
stepProperties: [],
additional: [], additional: [],
showTotalPrice: false,
}, },
work: [], work: [],
status: undefined, status: undefined,
productGroupId: '', productGroupId: '',
}; };
splitPay.value = 0;
workItems.value = []; workItems.value = [];
selectProduct.value = []; selectProduct.value = [];
dialogService.value = false; dialogService.value = false;
@ -1229,6 +1245,9 @@ function triggerConfirmCloseWork() {
} }
const tempValueProperties = ref<Attributes>({ const tempValueProperties = ref<Attributes>({
showTotalPrice: false,
workflowId: '',
stepProperties: [],
additional: [], additional: [],
}); });
const currentPropertiesMode = ref<'service' | 'work'>('service'); const currentPropertiesMode = ref<'service' | 'work'>('service');
@ -3818,13 +3837,13 @@ watch(
<div <div
class="col surface-1 rounded bordered scroll row relative-position" class="col surface-1 rounded bordered scroll row relative-position"
:class="{ :class="{
'q-mb-lg q-mx-lg ': $q.screen.gt.sm, 'q-mb-md q-mx-lg ': $q.screen.gt.sm,
'q-mb-sm q-mx-md': !$q.screen.gt.sm, 'q-mb-sm q-mx-md': !$q.screen.gt.sm,
}" }"
id="service-form" id="service-form"
> >
<div <div
class="col" class="col column justify-between"
style="height: 100%; max-height: 100; overflow-y: auto" style="height: 100%; max-height: 100; overflow-y: auto"
v-if="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
> >
@ -3876,6 +3895,20 @@ watch(
" "
/> />
</div> </div>
<span
class="row items-center justify-center q-py-md text-caption no-wrap"
>
{{ $t('productService.service.splitPay') }}
<q-input
dense
outlined
class="col-3 split-pay q-mx-sm"
input-class="text-caption text-right"
type="number"
v-model="splitPay"
/>
{{ $t('quotation.receiptDialog.installments') }}
</span>
</div> </div>
<div <div
class="col-12 col-md-10" class="col-12 col-md-10"
@ -3973,7 +4006,7 @@ watch(
/> />
</div> </div>
</div> </div>
<div <!-- <div
class="col-2 surface-1 rounded bordered row" class="col-2 surface-1 rounded bordered row"
:class="{ :class="{
'q-mb-lg q-mx-lg ': $q.screen.gt.sm, 'q-mb-lg q-mx-lg ': $q.screen.gt.sm,
@ -3993,7 +4026,7 @@ watch(
} }
" "
/> />
</div> </div> -->
</DialogForm> </DialogForm>
<!-- service properties --> <!-- service properties -->
@ -4026,12 +4059,12 @@ watch(
} }
" "
> >
<div class="q-pa-lg full-width full-height"> <section class="col column">
<ServiceProperties <ServiceProperties
v-model:properties-option="propertiesOption" v-model:properties-option="propertiesOption"
v-model:form-service-properties="tempValueProperties" v-model:form-service-properties="tempValueProperties"
/> />
</div> </section>
</DialogForm> </DialogForm>
<!-- manage work name --> <!-- manage work name -->
@ -4241,7 +4274,7 @@ watch(
/> />
</div> </div>
<div <div
class="col" class="col column justify-between"
style="height: 100%; max-height: 100; overflow-y: auto" style="height: 100%; max-height: 100; overflow-y: auto"
v-if="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
> >
@ -4297,13 +4330,28 @@ watch(
}" }"
/> />
</div> </div>
<span
class="row items-center justify-center q-py-md text-caption no-wrap"
>
{{ $t('productService.service.splitPay') }}
<q-input
:readonly="!infoServiceEdit"
dense
outlined
class="col-3 split-pay q-mx-sm"
input-class="text-caption text-right"
type="number"
v-model="splitPay"
/>
{{ $t('quotation.receiptDialog.installments') }}
</span>
</div> </div>
<div <div
class="col-12 col-md-10" class="col-12 col-md-10"
id="customer-form-content" id="customer-form-content"
:class="{ :class="{
'q-py-md q-pr-md ': $q.screen.gt.sm, 'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-py-md q-px-lg': !$q.screen.gt.sm, 'q-py-sm q-px-lg': !$q.screen.gt.sm,
}" }"
style="height: 100%; max-height: 100%; overflow-y: auto" style="height: 100%; max-height: 100%; overflow-y: auto"
> >
@ -4354,7 +4402,7 @@ watch(
/> />
</div> </div>
</div> </div>
<div <!-- <div
class="col-2 surface-1 rounded bordered row" class="col-2 surface-1 rounded bordered row"
:class="{ :class="{
'q-mb-lg q-mx-lg ': $q.screen.gt.sm, 'q-mb-lg q-mx-lg ': $q.screen.gt.sm,
@ -4375,7 +4423,7 @@ watch(
} }
" "
/> />
</div> </div> -->
</DialogForm> </DialogForm>
<q-dialog v-model="holdDialog" position="bottom"> <q-dialog v-model="holdDialog" position="bottom">
@ -4751,4 +4799,8 @@ watch(
color: hsl(var(--info-bg)); color: hsl(var(--info-bg));
font-weight: 600; font-weight: 600;
} }
:deep(.split-pay .q-field__control) {
height: 23px;
}
</style> </style>