feat: menu request list (#75)
* feat: i18n * feat: request list * refactor: hide stat transition on app.scss * feat: request list i18n * feat: request list => constants and main page * feat: add store * feat: add fetch data * feat: add utilities fn * feat: add store function / types * refactor: request list type * refactor: request list constants * refactor: quotation card => add customData and badge color props * feat: avatar group components * feat: request list group * refactor: request list => remove tab, add table data * feat: send search query * feat: add parameter * refactor: remove unused function * fix: rename component lits to list * feat: show stats from api * chore: cleanup * refactor: make it type safe * refactor: accept rotate flow id as parameter * feat: use page size component * feat: add component, data display & expansion product * feat: i18n * refactor: constants and request list table * refactor: type code, createdAt, updatedAt * refactor: utils function changThemeMode * feat: request list => view page * refactor: use type instead of infer from value * fix: function getEmployeeName att type * refactor: fetch work list * refactor: loop work list * feat: add i18n duty * feat: add form issue component * feat: add form issue section * fix: store error * refactor: edit by value * refactor: accept basic info from outside instead * feat: add status filter support on fetch * refactor: remove delete button * refactor: wording * feat/fix: request list i18n & constant * feat: document type * feat/refactor: request list => document expansion * refactor: doc expansion use FormGroupHead * refactor: fetch data based on id from route param * refactor: text area disable * feat: properties expansion display (mocking) * refactor: add document at product relation * refactor: edit get value product * feat: get workflow step to show on top * refactor: add type * refactor: add get attachment * refactor: add view attachment * refactor: edit file name * refactor: define props get hide icon * refactor: edit align row * refactor: by value table document * refactor: by value row table * feat: add independent ocr dialog * chore: clean up * refactor: accept more props and small adjustment * fix: error withDefault call * feat: accept default metadata when open * fix: typo * feat: add override hook when finish ocr * feat: reset state on open * feat: detect reader result is actually string * fix: variable name conflict * feat: properties to input component * feat: properties input in properties expansion * feat: properties expansion data (temporary) * refactor: add i18n status work * refactor: edit type work status and add step status * refactor: add edit status work * refactor: edit step work * refactor: properties data type * refactor: filter selected product & specific properties * feat: add emit event * refactor: change variable name for better understanding * refactor: hide step that no properties * refactor: work status type to validate * feat: work status color * refactor: key for filename * refactor: close expansion when change step * refactor: responsive meta data * refactor: product expansion responsive * fix: dark mode step text color * fix: document expansion table no data label * refactor: main page body bordered and overflow hidden * refactor: use utils function instead * refactor: add process * refactor: by value name * refactor: add upload file * refactor: upload file * refactor: by value * fix: option worker type * refactor: fetchRequestAttachment after edit * fix: metadata display * refactor: add class full-height * refactor: edit type * refactor: fetch file * refactor: by value visa * refactor: request list attributes type * fix: properties to input props (placeholder, readonly, disable) * feat: request list properties function * fix: error when no workflow * docs: update comment to fix indent * refactor: step type (attributes) * refactor: add attributes payload on editStatusRequestWork function * feat/refactor: functional form expansion/filter worklist * refactor: set attributes properties after submit * refactor: add request work ready status * feat: request list => form employee component * feat/refactor: form expansion select user/layout * fix: properties readonly --------- Co-authored-by: puriphatt <puriphat@frappet.com> Co-authored-by: Thanaphon Frappet <thanaphon@frappet.com>
This commit is contained in:
parent
9105dcf7fe
commit
972f6ba13e
36 changed files with 3653 additions and 57 deletions
|
|
@ -423,7 +423,7 @@ watch(
|
|||
class="col-12"
|
||||
:label="$t('customer.table.fullnameEN')"
|
||||
:disable="!readonly"
|
||||
:model-value="`${(prefixNameOptions.find((v) => v.value === namePrefix) || {}).value || ''} ${firstName || ''} ${lastName || ''}`"
|
||||
:model-value="`${(prefixNameOptions.find((v) => v.value === namePrefix) || {}).value || ''} ${firstNameEN || ''} ${lastNameEN || ''}`"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@ defineProps<{
|
|||
reporter?: string;
|
||||
totalPrice?: number;
|
||||
urgent?: boolean;
|
||||
hidePreview?: boolean;
|
||||
badgeColor?: string;
|
||||
|
||||
customData?: {
|
||||
label: string;
|
||||
value: string | number | unknown;
|
||||
slotName?: string;
|
||||
}[];
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
|
|
@ -52,13 +60,14 @@ const rand = Math.random();
|
|||
<div class="q-mr-sm" style="font-size: 90%">
|
||||
<BadgeComponent
|
||||
:title="status"
|
||||
hsla-color="--blue-6-hsl"
|
||||
:hsla-color="badgeColor || '--blue-6-hsl'"
|
||||
:border="urgent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav class="col text-right">
|
||||
<q-btn
|
||||
v-if="!hidePreview"
|
||||
flat
|
||||
dense
|
||||
rounded
|
||||
|
|
@ -111,7 +120,28 @@ const rand = Math.random();
|
|||
'surface-2': !urgent,
|
||||
}"
|
||||
>
|
||||
<article class="row q-py-sm">
|
||||
<article
|
||||
class="q-py-sm"
|
||||
:class="{
|
||||
row: $q.screen.gt.sm,
|
||||
column: $q.screen.lt.sm,
|
||||
}"
|
||||
v-if="customData && customData?.length > 0"
|
||||
>
|
||||
<template v-for="cData in customData" :key="cData.label">
|
||||
<template v-if="cData.slotName">
|
||||
<slot :name="cData.slotName" :props="cData" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="col-4 app-text-muted q-pr-sm">
|
||||
{{ cData.label || '-' }}
|
||||
</div>
|
||||
<div class="col-8">{{ cData.value || '-' }}</div>
|
||||
</template>
|
||||
</template>
|
||||
</article>
|
||||
|
||||
<article v-else class="row q-py-sm">
|
||||
<div class="col-4 app-text-muted q-pr-sm">
|
||||
{{ $t('quotation.customerName') }}
|
||||
</div>
|
||||
|
|
@ -125,6 +155,7 @@ const rand = Math.random();
|
|||
</div>
|
||||
<div class="col-8">
|
||||
<BadgeComponent
|
||||
:hsla-color="badgeColor"
|
||||
icon="mdi-account-multiple-outline"
|
||||
:title="[workerCount, workerMax].join(' / ')"
|
||||
/>
|
||||
|
|
|
|||
35
src/components/08_request-list/DataDisplay.vue
Normal file
35
src/components/08_request-list/DataDisplay.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: string;
|
||||
iconSize?: string;
|
||||
}>(),
|
||||
{
|
||||
label: '-',
|
||||
value: '-',
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="row items-center">
|
||||
<Icon
|
||||
v-if="icon"
|
||||
:icon
|
||||
class="app-text-muted q-pr-sm"
|
||||
:width="iconSize || '2rem'"
|
||||
/>
|
||||
<span class="column">
|
||||
<span class="app-text-muted-2 text-caption">
|
||||
{{ label }}
|
||||
</span>
|
||||
<span class="text-weight-medium">
|
||||
{{ value }}
|
||||
</span>
|
||||
</span>
|
||||
</article>
|
||||
</template>
|
||||
120
src/components/08_request-list/PropertiesToInput.vue
Normal file
120
src/components/08_request-list/PropertiesToInput.vue
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
PropDate,
|
||||
PropNumber,
|
||||
PropOptions,
|
||||
PropString,
|
||||
} from 'src/stores/product-service/types';
|
||||
|
||||
import SelectInput from '../shared/SelectInput.vue';
|
||||
import DatePicker from '../shared/DatePicker.vue';
|
||||
|
||||
defineProps<{
|
||||
prop: PropString | PropNumber | PropDate | PropOptions;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
disable?: boolean;
|
||||
}>();
|
||||
|
||||
const model = defineModel<string | number | null | undefined>();
|
||||
|
||||
function numberDisplay(prop: PropNumber, data: number): string {
|
||||
if (isNaN(data)) data = 0;
|
||||
|
||||
let formattedNumber: string;
|
||||
|
||||
if (prop.comma && prop.decimal) {
|
||||
formattedNumber = data.toLocaleString('en-US', {
|
||||
minimumFractionDigits: prop.decimalPlace,
|
||||
maximumFractionDigits: prop.decimalPlace,
|
||||
});
|
||||
} else if (prop.comma && !prop.decimal) {
|
||||
formattedNumber = data.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
} else if (!prop.comma && prop.decimal) {
|
||||
formattedNumber = data.toFixed(prop.decimalPlace);
|
||||
} else {
|
||||
formattedNumber = Math.round(data).toString();
|
||||
}
|
||||
|
||||
return formattedNumber;
|
||||
}
|
||||
|
||||
function numberParse(formatted: string, prop: PropNumber): number {
|
||||
let parsedNumber: number;
|
||||
|
||||
let cleanedFormatted = formatted.replace(/,/g, '');
|
||||
|
||||
if (prop.decimal) {
|
||||
parsedNumber = parseFloat(cleanedFormatted);
|
||||
} else {
|
||||
parsedNumber = parseInt(cleanedFormatted, 10);
|
||||
}
|
||||
|
||||
if (isNaN(parsedNumber)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parsedNumber;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<q-input
|
||||
v-if="prop.type === 'string'"
|
||||
:readonly
|
||||
:disable
|
||||
class="col-7"
|
||||
:model-value="readonly || disable ? model || '-' : model"
|
||||
dense
|
||||
outlined
|
||||
:placeholder
|
||||
:maxlength="prop.isPhoneNumber ? prop.phoneNumberLength : undefined"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
model = v;
|
||||
}
|
||||
"
|
||||
@focus="(e) => (e.target as HTMLInputElement).select()"
|
||||
/>
|
||||
<q-input
|
||||
v-if="prop.type === 'number'"
|
||||
:readonly
|
||||
:disable
|
||||
class="col-7"
|
||||
debounce="500"
|
||||
dense
|
||||
outlined
|
||||
:model-value="numberDisplay(prop, Number(model))"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
let cleanedFormatted = v?.toString().replace(/,/g, '');
|
||||
|
||||
const x = numberParse(
|
||||
numberDisplay(prop as PropNumber, Number(cleanedFormatted)),
|
||||
prop as PropNumber,
|
||||
);
|
||||
model = x;
|
||||
}
|
||||
"
|
||||
@focus="(e) => (e.target as HTMLInputElement).select()"
|
||||
/>
|
||||
<DatePicker
|
||||
v-if="prop.type === 'date'"
|
||||
:readonly
|
||||
:disabled="disable"
|
||||
class="col-7"
|
||||
v-model="model as string"
|
||||
/>
|
||||
<SelectInput
|
||||
v-if="prop.type === 'array'"
|
||||
:readonly
|
||||
:disable
|
||||
:label="$t('form.selection')"
|
||||
v-model="model as string"
|
||||
class="col-7"
|
||||
:option="prop.options.map((opt) => ({ label: opt, value: opt }))"
|
||||
/>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
|
|
@ -10,6 +10,7 @@ defineProps<{
|
|||
border?: boolean;
|
||||
solid?: boolean;
|
||||
transparency?: number;
|
||||
hideIcon?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
@ -24,8 +25,17 @@ defineProps<{
|
|||
: undefined,
|
||||
}"
|
||||
>
|
||||
<Icon :icon="icon || 'mdi-circle-medium'" style="margin-right: 0.25rem" />
|
||||
{{ title || (!!titleI18n ? $t(titleI18n) : '-') }}
|
||||
<Icon
|
||||
v-if="!hideIcon"
|
||||
:icon="icon || 'mdi-circle-medium'"
|
||||
style="margin-right: 0.25rem"
|
||||
/>
|
||||
|
||||
<slot name="label">
|
||||
{{ title || (!!titleI18n ? $t(titleI18n) : '-') }}
|
||||
</slot>
|
||||
|
||||
<slot name="append"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
107
src/components/shared/AvatarGroup.vue
Normal file
107
src/components/shared/AvatarGroup.vue
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
data?: Record<string, unknown>[];
|
||||
dataLabel?: string;
|
||||
dataUrl?: string;
|
||||
}>(),
|
||||
{
|
||||
dataLabel: 'name',
|
||||
dataUrl: 'imgUrl',
|
||||
data: () => [
|
||||
{
|
||||
name: 'Someone 1',
|
||||
imgUrl:
|
||||
'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=500&q=60',
|
||||
},
|
||||
{
|
||||
name: 'Someone 2',
|
||||
imgUrl:
|
||||
'https://images.unsplash.com/flagged/photo-1570612861542-284f4c12e75f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=500&q=60',
|
||||
},
|
||||
{
|
||||
name: 'Someone 3',
|
||||
imgUrl:
|
||||
'https://images.unsplash.com/photo-1547425260-76bcadfb4f2c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=500&q=60',
|
||||
},
|
||||
{
|
||||
name: 'Someone 4',
|
||||
imgUrl:
|
||||
'https://images.unsplash.com/photo-1499952127939-9bbf5af6c51c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTF8fHBlcnNvbnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=500&q=60',
|
||||
},
|
||||
{
|
||||
name: 'Someone 5',
|
||||
imgUrl:
|
||||
'https://images.unsplash.com/photo-1504593811423-6dd665756598?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTZ8fHBlcnNvbnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=500&q=60',
|
||||
},
|
||||
{
|
||||
name: 'Someone 6',
|
||||
imgUrl:
|
||||
'https://images.unsplash.com/photo-1504593811423-6dd665756598?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTZ8fHBlcnNvbnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=500&q=60',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="avatar-group">
|
||||
<div class="avatar" v-for="(person, i) in data.slice(0, 3)" :key="i">
|
||||
<q-tooltip>
|
||||
{{ person[dataLabel] }}
|
||||
</q-tooltip>
|
||||
<img
|
||||
:src="
|
||||
typeof person[dataUrl] === 'string' ? (person[dataUrl] as string) : ''
|
||||
"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="data.length > 3" class="avatar remaining-count">
|
||||
<q-tooltip>
|
||||
<div v-for="(person, i) in data.slice(3)" :key="i + 3">
|
||||
{{ person.name }}
|
||||
</div>
|
||||
</q-tooltip>
|
||||
<span>{{ `+${data.length - 3}` }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.avatar {
|
||||
position: relative;
|
||||
transition: 0.2s;
|
||||
}
|
||||
.avatar:not(:first-child) {
|
||||
margin-left: -0.75rem;
|
||||
}
|
||||
.avatar:hover {
|
||||
z-index: 1;
|
||||
transform: translateY(-0.5rem);
|
||||
}
|
||||
.avatar img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
||||
.remaining-count {
|
||||
color: hsl(var(--text-mute-2));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: var(--surface-2);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 50%;
|
||||
font-size: 0.8rem;
|
||||
margin-left: -0.75rem;
|
||||
}
|
||||
</style>
|
||||
140
src/components/upload-file/OcrDialog.vue
Normal file
140
src/components/upload-file/OcrDialog.vue
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import ShowAttachment from 'components/ShowAttachent.vue';
|
||||
import DialogForm from 'components/DialogForm.vue';
|
||||
|
||||
const isOpen = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const isRunning = ref(false);
|
||||
|
||||
const file = ref<File>();
|
||||
const url = ref<string>();
|
||||
const metadata = ref<Data>();
|
||||
|
||||
const splitRatio = ref(50);
|
||||
|
||||
type Data = Record<string, any>;
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
readonly?: boolean;
|
||||
autoSave?: boolean;
|
||||
data?: Data;
|
||||
};
|
||||
|
||||
type HandleProps = {
|
||||
ocr?: (file: File) => Promise<false | Data>;
|
||||
save?: (file: File, meta?: Data) => void | Promise<boolean>;
|
||||
override?: (before: Data, after: Data) => Data;
|
||||
};
|
||||
|
||||
defineEmits<{
|
||||
(e: 'submit', file: File, meta?: Data): void;
|
||||
}>();
|
||||
|
||||
defineExpose({ isRunning });
|
||||
|
||||
const props = withDefaults(defineProps<Props & HandleProps>(), { title: '' });
|
||||
|
||||
const input = (() => {
|
||||
const _element = document.createElement('input');
|
||||
_element.type = 'file';
|
||||
_element.accept = 'image/jpeg,image/png';
|
||||
_element.addEventListener('change', change);
|
||||
return _element;
|
||||
})();
|
||||
|
||||
async function change(e: Event) {
|
||||
const _element = e.target as HTMLInputElement | null;
|
||||
const _file = _element?.files?.[0];
|
||||
|
||||
if (!_file) return;
|
||||
|
||||
file.value = _file;
|
||||
|
||||
url.value = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(_file);
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
return resolve(reader.result);
|
||||
}
|
||||
return reject();
|
||||
};
|
||||
});
|
||||
|
||||
if (!props.ocr) return;
|
||||
|
||||
if (props.data) metadata.value = structuredClone(props.data);
|
||||
|
||||
isOpen.value = true;
|
||||
isRunning.value = true;
|
||||
const ocrResult = await props.ocr(_file);
|
||||
isRunning.value = false;
|
||||
|
||||
if (!ocrResult) return;
|
||||
|
||||
if (!props.override || !metadata.value) {
|
||||
return (metadata.value = ocrResult);
|
||||
}
|
||||
|
||||
if (Object.entries(metadata.value).some(([k, v]) => ocrResult[k] !== v)) {
|
||||
return (metadata.value = props.override(metadata.value, ocrResult));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot
|
||||
name="trigger"
|
||||
:browse="
|
||||
() => {
|
||||
file = undefined;
|
||||
url = undefined;
|
||||
metadata = undefined;
|
||||
input?.click();
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
<DialogForm
|
||||
v-if="file && url"
|
||||
v-model:modal="isOpen"
|
||||
style="position: absolute"
|
||||
height="100vh"
|
||||
weight="90%"
|
||||
hide-close-event
|
||||
hide-delete
|
||||
edit
|
||||
:title
|
||||
:is-edit
|
||||
:readonly
|
||||
:edit-data="() => (isEdit = true)"
|
||||
:undo="() => (isEdit = false)"
|
||||
:close="() => (isOpen = false)"
|
||||
:submit="
|
||||
() => {
|
||||
if (!file) return;
|
||||
$emit('submit', file, metadata);
|
||||
if (autoSave) save?.(file, metadata);
|
||||
isOpen = false;
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-splitter class="full-height" v-model="splitRatio">
|
||||
<template #before>
|
||||
<div class="full-height">
|
||||
<slot name="viewer" :url :file>
|
||||
<ShowAttachment :url :file />
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
<template #after>
|
||||
<div class="q-pa-md full-height">
|
||||
<slot name="body" :metadata :is-running :is-edit />
|
||||
</div>
|
||||
</template>
|
||||
</q-splitter>
|
||||
</DialogForm>
|
||||
</template>
|
||||
|
|
@ -105,7 +105,7 @@ async function change(e: Event) {
|
|||
const _file = _element?.files?.[0];
|
||||
|
||||
if (_file) {
|
||||
const newFileName = `${selectedMenu.value?.group}-${dateFormat(new Date().toISOString())}`;
|
||||
const newFileName = `${selectedMenu.value?.group}-${dateFormat(new Date().toISOString())}-${_file.name}`;
|
||||
const renamedFile = new File([_file], newFileName, { type: _file.type });
|
||||
|
||||
if (!obj.value[currentIndex.value] && selectedMenu.value) {
|
||||
|
|
@ -155,14 +155,12 @@ async function change(e: Event) {
|
|||
}
|
||||
|
||||
if (resOcr.group === 'passport') {
|
||||
const fullName = map['full_name'].split(' ');
|
||||
|
||||
obj.value[currentIndex.value]._meta = {
|
||||
type: map['doc_type'],
|
||||
number: map['doc_number'],
|
||||
gender: map['sex'],
|
||||
firstName: fullName[0],
|
||||
lastName: fullName[1],
|
||||
firstName: map['first_name'],
|
||||
lastName: map['last_name'],
|
||||
issueDate: map['issue_date'],
|
||||
expireDate: map['expire_date'],
|
||||
issuePlace: map['nationality'],
|
||||
|
|
@ -195,7 +193,7 @@ async function fileList() {
|
|||
.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
name: item.name?.split('-')[1],
|
||||
name: `${item.name?.split('-')[1]}-${item.name?.split('-')[2] || ''}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue