419 lines
11 KiB
Vue
419 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
import { Lang } from 'src/utils/ui';
|
|
|
|
import { QTableSlots, QTableColumn } from 'quasar';
|
|
import { Product, Service } from 'src/stores/product-service/types';
|
|
|
|
import { calculateAge, dateFormatJS } from 'src/utils/datetime';
|
|
import useOptionStore from 'stores/options';
|
|
import { formatNumberDecimal, isRoleInclude } from 'src/stores/utils';
|
|
import { computed } from 'vue';
|
|
|
|
const selected = defineModel<unknown[]>('selected');
|
|
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
|
const { locale } = useI18n();
|
|
const optionStore = useOptionStore();
|
|
|
|
const priceDisplay = computed(() => ({
|
|
price: !isRoleInclude(['sale_agent']),
|
|
agentPrice: isRoleInclude([
|
|
'admin',
|
|
'head_of_admin',
|
|
'head_of_sale',
|
|
'system',
|
|
'owner',
|
|
'accountant',
|
|
'sale_agent',
|
|
]),
|
|
serviceCharge: isRoleInclude([
|
|
'admin',
|
|
'head_of_admin',
|
|
'system',
|
|
'owner',
|
|
'accountant',
|
|
]),
|
|
}));
|
|
|
|
defineEmits<{
|
|
(e: 'create'): void;
|
|
}>();
|
|
|
|
type ExclusiveProps = {
|
|
type: 'service' | 'product';
|
|
grid?: boolean;
|
|
disabledWorkerId?: string[];
|
|
disabledProductFields?: (typeof columnsProduct)[number]['name'][];
|
|
disabledServiceFields?: (typeof columnsService)[number]['name'][];
|
|
rows: Product[];
|
|
};
|
|
|
|
const columnsProduct = [
|
|
{
|
|
name: '#check',
|
|
align: 'left',
|
|
label: '',
|
|
field: (_) => '#check',
|
|
},
|
|
|
|
{
|
|
name: 'order',
|
|
align: 'center',
|
|
label: 'general.order',
|
|
field: (e: Product & { _index: number }) => e._index + 1,
|
|
},
|
|
|
|
{
|
|
name: '#productName',
|
|
align: 'left',
|
|
label: 'general.name',
|
|
field: (v: Product) => v.name,
|
|
},
|
|
|
|
{
|
|
name: 'productProcessingTime',
|
|
align: 'center',
|
|
label: 'productService.product.processingTimeDay',
|
|
field: (v: Product) => v.process,
|
|
},
|
|
|
|
{
|
|
name: '#priceInformation',
|
|
align: 'center',
|
|
label: 'productService.product.priceInformation',
|
|
field: (v: Product) => v,
|
|
},
|
|
] as const satisfies QTableColumn[];
|
|
|
|
const columnsService = [
|
|
{
|
|
name: '#check',
|
|
align: 'left',
|
|
label: '',
|
|
field: (_) => '#check',
|
|
},
|
|
{
|
|
name: 'order',
|
|
align: 'center',
|
|
label: 'general.order',
|
|
field: (e: Service & { _index: number }) => e._index + 1,
|
|
},
|
|
|
|
{
|
|
name: '#serviceName',
|
|
align: 'left',
|
|
label: 'general.name',
|
|
field: (v: Service) => v.name,
|
|
},
|
|
{
|
|
name: 'serviceDetail',
|
|
align: 'left',
|
|
label: 'general.detail',
|
|
field: (v: Service) => v.detail,
|
|
},
|
|
{
|
|
name: 'serviceWorkTotal',
|
|
align: 'left',
|
|
label: 'productService.service.totalWork',
|
|
field: (v: Service) => v.work.length,
|
|
},
|
|
{
|
|
name: 'createdAt',
|
|
align: 'left',
|
|
label: 'general.createdAt',
|
|
field: (v: Service) => dateFormatJS({ date: v.createdAt }),
|
|
},
|
|
] as const satisfies QTableColumn[];
|
|
|
|
const props = defineProps<ExclusiveProps>();
|
|
|
|
function getProductImageUrl(
|
|
item: (Product | Service) & { type: string; _index: number },
|
|
) {
|
|
if (item.selectedImage) {
|
|
return `${API_BASE_URL}/${item.type}/${item?.id}/image/${item?.selectedImage}`;
|
|
}
|
|
// NOTE: static image
|
|
return `/images/${item.type}-avatar.png`;
|
|
}
|
|
|
|
function handleUpdate() {
|
|
if (selected.value?.length === 0) {
|
|
selected.value = props.rows?.filter(
|
|
(v) => !props.disabledWorkerId?.includes(v.id),
|
|
);
|
|
} else {
|
|
selected.value = [];
|
|
}
|
|
}
|
|
|
|
function selectedIndex(item: any) {
|
|
return selected.value?.findIndex((v: any) => v.id === item.id);
|
|
}
|
|
</script>
|
|
<template>
|
|
<q-table
|
|
v-model:selected="selected"
|
|
:rows-per-page-options="[0]"
|
|
:rows="
|
|
rows.map((data, i) => ({
|
|
...data,
|
|
_index: i,
|
|
}))
|
|
"
|
|
:grid
|
|
:columns="type === 'product' ? columnsProduct : columnsService"
|
|
hide-bottom
|
|
bordered
|
|
flat
|
|
hide-pagination
|
|
selection="multiple"
|
|
card-container-class="q-col-gutter-sm"
|
|
class="full-width"
|
|
row-key="id"
|
|
>
|
|
<template v-slot:header="props">
|
|
<q-tr
|
|
style="background-color: hsla(var(--info-bg) / 0.07)"
|
|
:props="props"
|
|
>
|
|
<q-th
|
|
v-for="col in type === 'product'
|
|
? columnsProduct.filter((v) =>
|
|
disabledProductFields
|
|
? !disabledProductFields.includes(v.name)
|
|
: true,
|
|
)
|
|
: columnsService.filter((v) =>
|
|
disabledServiceFields
|
|
? !disabledServiceFields.includes(v.name)
|
|
: true,
|
|
)"
|
|
:auto-width="col.name === '#check'"
|
|
:key="col.name"
|
|
:props="props"
|
|
>
|
|
<template v-if="!col.name.startsWith('#')">
|
|
{{ $t(col.label) }}
|
|
</template>
|
|
|
|
<template
|
|
v-if="
|
|
col.name === '#serviceName' ||
|
|
col.name === '#productName' ||
|
|
col.name === '#priceInformation'
|
|
"
|
|
>
|
|
{{ $t(col.label) }}
|
|
</template>
|
|
|
|
<template v-if="col.name === '#check'">
|
|
<q-checkbox
|
|
v-model="props.selected"
|
|
@update:model-value="(v) => handleUpdate()"
|
|
size="sm"
|
|
/>
|
|
</template>
|
|
</q-th>
|
|
</q-tr>
|
|
</template>
|
|
|
|
<template
|
|
v-slot:body="props: {
|
|
row: (Product | Service) & { type: string; _index: number };
|
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
|
>
|
|
<q-tr
|
|
:class="{
|
|
dark: $q.dark.isActive,
|
|
'selectable-item__disabled': disabledWorkerId?.some(
|
|
(id) => id === props.row.id,
|
|
),
|
|
}"
|
|
class="text-center"
|
|
>
|
|
<q-td
|
|
v-for="col in type === 'product'
|
|
? columnsProduct.filter((v) =>
|
|
disabledProductFields
|
|
? !disabledProductFields.includes(v.name)
|
|
: true,
|
|
)
|
|
: columnsService.filter((v) =>
|
|
disabledServiceFields
|
|
? !disabledServiceFields.includes(v.name)
|
|
: true,
|
|
)"
|
|
:align="col.align"
|
|
:key="col.name"
|
|
>
|
|
<!-- NOTE: custom column will starts with # -->
|
|
<template v-if="!col.name.startsWith('#')">
|
|
<span v-if="col.name === 'serviceDetail'">
|
|
{{ props.row.detail.replace(/<\/?[^>]+(>|$)/g, '') || '-' }}
|
|
</span>
|
|
<span v-else>
|
|
{{
|
|
typeof col.field === 'string'
|
|
? props.row[col.field as keyof (Product | Service)]
|
|
: col.field(props.row as any)
|
|
}}
|
|
</span>
|
|
</template>
|
|
|
|
<template
|
|
v-if="col.name === '#productName' || col.name === '#serviceName'"
|
|
>
|
|
<div class="row full-height items-center">
|
|
<q-avatar size="md">
|
|
<q-img
|
|
:src="getProductImageUrl(props.row)"
|
|
:ratio="1"
|
|
class="text-center"
|
|
/>
|
|
</q-avatar>
|
|
<span class="column q-ml-sm">
|
|
<span class="col-6">
|
|
{{ props.row.name }}
|
|
</span>
|
|
<span class="col-6 app-text-muted">
|
|
{{ props.row.code }}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<template
|
|
v-if="
|
|
col.name === '#priceInformation' &&
|
|
!disabledWorkerId?.some((id) => id === props.row.id)
|
|
"
|
|
>
|
|
<div
|
|
class="row full-width q-gutter-x-md no-wrap items-center text-right"
|
|
>
|
|
<div
|
|
class="tags tags-color-orange col column ellipsis-2-lines"
|
|
:class="{
|
|
disable: props.row.status === 'INACTIVE',
|
|
}"
|
|
style="min-width: 50px"
|
|
v-if="priceDisplay.price"
|
|
>
|
|
<div class="col app-text-muted-2 text-caption">
|
|
{{ $t('productService.product.salePrice') }}
|
|
</div>
|
|
<div class="col text-weight-bold">
|
|
฿{{ formatNumberDecimal(props.row.price || 0, 2) }}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="tags tags-color-purple col column ellipsis-2-lines"
|
|
:class="{
|
|
disable: props.row.status === 'INACTIVE',
|
|
}"
|
|
style="min-width: 50px"
|
|
v-if="priceDisplay.agentPrice"
|
|
>
|
|
<div class="col app-text-muted-2 text-caption">
|
|
{{ $t('productService.product.agentPrice') }}
|
|
</div>
|
|
<div class="col text-weight-bold">
|
|
฿{{ formatNumberDecimal(props.row.agentPrice || 0, 2) }}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="tags tags-color-pink col column ellipsis-2-lines"
|
|
:class="{
|
|
disable: props.row.status === 'INACTIVE',
|
|
}"
|
|
style="min-width: 50px"
|
|
v-if="priceDisplay.serviceCharge"
|
|
>
|
|
<div class="col app-text-muted-2 text-caption">
|
|
{{ $t('productService.product.processingPrice') }}
|
|
</div>
|
|
<div class="col">
|
|
฿{{ formatNumberDecimal(props.row.serviceCharge || 0, 2) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template
|
|
v-if="
|
|
col.name === '#check' &&
|
|
!disabledWorkerId?.some((id) => id === props.row.id)
|
|
"
|
|
>
|
|
<q-checkbox v-model="props.selected" size="sm" />
|
|
</template>
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
<template v-slot:item="props: { row: any; rowIndex: number }">
|
|
<slot
|
|
name="grid"
|
|
:item="{
|
|
index: props.rowIndex,
|
|
...props.row,
|
|
_selectedIndex: selectedIndex(props.row),
|
|
}"
|
|
/>
|
|
</template>
|
|
</q-table>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.selectable-item__disabled {
|
|
filter: grayscale(1);
|
|
opacity: 0.5;
|
|
|
|
& :deep(*) {
|
|
cursor: not-allowed;
|
|
}
|
|
}
|
|
|
|
.tags {
|
|
display: inline-block;
|
|
color: hsla(var(--_color-tag) / 1);
|
|
background: hsla(var(--_color-tag) / 0.075);
|
|
border-radius: var(--radius-2);
|
|
padding-inline: var(--size-2);
|
|
|
|
&.disable {
|
|
filter: grayscale(100%);
|
|
opacity: 80%;
|
|
}
|
|
}
|
|
|
|
.tags-color-green {
|
|
--_color-tag: var(--teal-10-hsl);
|
|
}
|
|
|
|
.dark .tags-color-green {
|
|
--_color-tag: var(--teal-8-hsl);
|
|
}
|
|
|
|
.tags-color-orange {
|
|
--_color-tag: var(--orange-5-hsl);
|
|
}
|
|
|
|
.dark .tags-color-orange {
|
|
--_color-tag: var(--orange-6-hsl);
|
|
}
|
|
|
|
.tags-color-purple {
|
|
--_color-tag: var(--violet-11-hsl);
|
|
}
|
|
|
|
.dark .tags-color-purple {
|
|
--_color-tag: var(--violet-10-hsl);
|
|
}
|
|
|
|
.tags-color-pink {
|
|
--_color-tag: var(--pink-6-hsl);
|
|
}
|
|
</style>
|