591 lines
18 KiB
Vue
591 lines
18 KiB
Vue
<script setup lang="ts">
|
|
import { QTable, QTableProps, QTableSlots } from 'quasar';
|
|
import { baseUrl } from 'src/stores/utils';
|
|
|
|
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
|
|
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
|
import AvatarGroup from 'src/components/shared/AvatarGroup.vue';
|
|
|
|
import { RequestData } from 'src/stores/request-list/types';
|
|
import { RequestDataStatus } from 'src/stores/request-list/types';
|
|
import { QuotationFull } from 'src/stores/quotations/types';
|
|
import useOptionStore from 'src/stores/options';
|
|
|
|
import KebabAction from 'src/components/shared/KebabAction.vue';
|
|
import { CreatedBy } from 'src/stores/types';
|
|
import { dateFormatJS } from 'src/utils/datetime';
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
rows: QTableProps['rows'];
|
|
columns: QTableProps['columns'];
|
|
grid?: boolean;
|
|
visibleColumns?: string[];
|
|
hideAction?: boolean;
|
|
hideView?: boolean;
|
|
checkable?: boolean;
|
|
listSameArea?: string[];
|
|
noLink?: boolean;
|
|
}>(),
|
|
{
|
|
row: () => [],
|
|
column: () => [],
|
|
grid: false,
|
|
visibleColumns: () => [],
|
|
},
|
|
);
|
|
|
|
defineEmits<{
|
|
(e: 'view', data: RequestData): void;
|
|
(e: 'delete', data: RequestData): void;
|
|
(e: 'rejectCancel', data: RequestData): void;
|
|
}>();
|
|
|
|
const selected = defineModel<RequestData[]>('selected');
|
|
|
|
function responsiblePerson(quotation: QuotationFull) {
|
|
const productServiceList = quotation.productServiceList;
|
|
const tempPerson: CreatedBy[] = [];
|
|
const tempGroup: {
|
|
group: string;
|
|
id: string;
|
|
workflowTemplateStepId: string;
|
|
}[] = [];
|
|
|
|
const userIds = new Set<string>();
|
|
const groupKeys = new Set<string>();
|
|
|
|
for (const v of productServiceList) {
|
|
const tempStep = v.service?.workflow?.step;
|
|
|
|
if (tempStep) {
|
|
tempStep.forEach((lhs) => {
|
|
for (const rhs of lhs.responsiblePerson) {
|
|
if (!userIds.has(rhs.user.id)) {
|
|
userIds.add(rhs.user.id);
|
|
tempPerson.push(rhs.user);
|
|
}
|
|
}
|
|
});
|
|
|
|
tempStep.forEach((lhs) => {
|
|
const newGroup = lhs.responsibleGroup as unknown as {
|
|
group: string;
|
|
id: string;
|
|
workflowTemplateStepId: string;
|
|
}[];
|
|
|
|
for (const rhs of newGroup) {
|
|
const key = `${rhs.group}-${rhs.id}-${rhs.workflowTemplateStepId}`;
|
|
if (!groupKeys.has(key)) {
|
|
groupKeys.add(key);
|
|
tempGroup.push(rhs);
|
|
}
|
|
}
|
|
});
|
|
|
|
return { user: tempPerson, group: tempGroup };
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function getCustomerName(
|
|
record: RequestData,
|
|
opts?: {
|
|
locale?: string;
|
|
noCode?: boolean;
|
|
},
|
|
) {
|
|
const customer = record.quotation.customerBranch;
|
|
|
|
return (
|
|
{
|
|
['CORP']: {
|
|
['eng']: customer.registerNameEN,
|
|
['tha']: customer.registerName,
|
|
}[opts?.locale || 'eng'],
|
|
['PERS']:
|
|
{
|
|
['eng']: `${useOptionStore().mapOption(customer?.namePrefix || '')} ${customer?.firstNameEN} ${customer?.lastNameEN}`,
|
|
['tha']: `${useOptionStore().mapOption(customer?.namePrefix || '')} ${customer?.firstName} ${customer?.lastName}`,
|
|
}[opts?.locale || 'eng'] || '-',
|
|
}[customer.customer.customerType] +
|
|
(opts?.noCode ? '' : ' ' + `(${customer.code})`)
|
|
);
|
|
}
|
|
|
|
function getEmployeeName(
|
|
record: RequestData,
|
|
opts?: {
|
|
locale?: string;
|
|
},
|
|
) {
|
|
const employee = record.employee;
|
|
|
|
return (
|
|
{
|
|
['eng']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstNameEN} ${employee?.lastNameEN}`,
|
|
['tha']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstName || employee?.firstNameEN} ${employee?.lastName || employee?.lastNameEN}`,
|
|
}[opts?.locale || 'eng'] || '-'
|
|
);
|
|
}
|
|
|
|
function toCustomer(customer: RequestData['quotation']['customerBranch']) {
|
|
if (props.noLink) return;
|
|
const url = new URL(
|
|
`/customer-management?tab=customer&id=${customer.customerId}`,
|
|
window.location.origin,
|
|
);
|
|
|
|
window.open(url.toString(), '_blank');
|
|
}
|
|
|
|
function toEmployee(employee: RequestData['employee']) {
|
|
if (props.noLink) return;
|
|
const url = new URL(
|
|
`/customer-management?tab=employee&id=${employee.id}`,
|
|
window.location.origin,
|
|
);
|
|
|
|
window.open(url.toString(), '_blank');
|
|
}
|
|
|
|
function handleCheckAll() {
|
|
const filteredRows = props.rows.filter((row) =>
|
|
props.listSameArea?.includes(row.quotation.customerBranch.districtId),
|
|
);
|
|
if (selected.value.length === filteredRows.length) {
|
|
selected.value = [];
|
|
} else {
|
|
selected.value = filteredRows;
|
|
}
|
|
}
|
|
</script>
|
|
<template>
|
|
<q-table
|
|
v-bind="props"
|
|
bordered
|
|
flat
|
|
hide-pagination
|
|
card-container-class="q-col-gutter-sm"
|
|
:rows-per-page-options="[0]"
|
|
class="full-width"
|
|
selection="multiple"
|
|
v-model:selected="selected"
|
|
:selected-rows-label="
|
|
(n) =>
|
|
$t('general.selected', {
|
|
number: n,
|
|
msg: $t('general.list'),
|
|
})
|
|
"
|
|
:no-data-label="$t('general.noDataTable')"
|
|
>
|
|
<template v-slot:header="props">
|
|
<q-tr
|
|
style="background-color: hsla(var(--info-bg) / 0.07)"
|
|
:props="props"
|
|
>
|
|
<q-th v-if="checkable">
|
|
<q-checkbox
|
|
v-if="selected.length > 0"
|
|
:model-value="
|
|
selected.length ===
|
|
rows.filter((row) =>
|
|
listSameArea?.includes(row.quotation.customerBranch.districtId),
|
|
).length
|
|
"
|
|
size="sm"
|
|
@click="handleCheckAll"
|
|
/>
|
|
<div v-else style="width: 35px; height: 35px"></div>
|
|
</q-th>
|
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
{{ col.label && $t(col.label) }}
|
|
</q-th>
|
|
<q-th></q-th>
|
|
</q-tr>
|
|
</template>
|
|
|
|
<template
|
|
v-slot:body="props: {
|
|
row: RequestData;
|
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
|
>
|
|
<q-tr
|
|
:class="{
|
|
urgent: props.row.quotation.urgent,
|
|
dark: $q.dark.isActive,
|
|
'disabled-row':
|
|
selected &&
|
|
selected.length > 0 &&
|
|
!listSameArea.includes(
|
|
props.row.quotation.customerBranch.districtId,
|
|
),
|
|
}"
|
|
class="text-center"
|
|
>
|
|
<q-td v-if="checkable">
|
|
<q-checkbox
|
|
:disable="
|
|
selected.length > 0 &&
|
|
!listSameArea.includes(
|
|
props.row.quotation.customerBranch.districtId,
|
|
)
|
|
"
|
|
v-model="props.selected"
|
|
size="sm"
|
|
/>
|
|
</q-td>
|
|
<q-td v-if="visibleColumns.includes('order')">
|
|
{{ props.rowIndex + 1 }}
|
|
</q-td>
|
|
<q-td v-if="visibleColumns.includes('requestList')" class="text-left">
|
|
{{ props.row.quotation.workName || '-' }}
|
|
<div class="text-caption app-text-muted">
|
|
{{ props.row.code || '-' }}
|
|
</div>
|
|
</q-td>
|
|
<q-td v-if="visibleColumns.includes('employer')" class="text-left">
|
|
<span
|
|
:class="{ link: !noLink }"
|
|
@click="toCustomer(props.row.quotation.customerBranch)"
|
|
>
|
|
{{
|
|
getCustomerName(props.row, {
|
|
noCode: true,
|
|
locale: $i18n.locale,
|
|
}) || '-'
|
|
}}
|
|
</span>
|
|
</q-td>
|
|
<q-td v-if="visibleColumns.includes('employee')" class="text-left">
|
|
<span
|
|
:class="{ link: !noLink }"
|
|
@click="toEmployee(props.row.employee)"
|
|
>
|
|
{{ getEmployeeName(props.row, { locale: $i18n.locale }) || '-' }}
|
|
</span>
|
|
</q-td>
|
|
|
|
<q-td
|
|
v-if="visibleColumns.includes('employeePassport')"
|
|
class="text-left"
|
|
>
|
|
{{
|
|
props.row.employee.employeePassport.length !== 0
|
|
? props.row.employee.employeePassport[0].number
|
|
: '-'
|
|
}}
|
|
</q-td>
|
|
<q-td v-if="visibleColumns.includes('dataOffice')" class="text-left">
|
|
{{
|
|
$i18n.locale === 'eng'
|
|
? props.row.dataOffice.nameEN
|
|
: props.row.dataOffice.name
|
|
}}
|
|
</q-td>
|
|
<q-td v-if="visibleColumns.includes('createdAt')" class="text-left">
|
|
{{ dateFormatJS({ date: props.row.createdAt }) }}
|
|
</q-td>
|
|
|
|
<q-td v-if="visibleColumns.includes('quotationCode')">
|
|
{{ props.row.quotation.code || '-' }}
|
|
</q-td>
|
|
<q-td v-if="visibleColumns.includes('responsiblePerson')">
|
|
<!-- <AvatarGroup
|
|
:data="
|
|
responsiblePerson(props.row.quotation)?.map((v) => {
|
|
return {
|
|
name:
|
|
$i18n.locale === 'eng'
|
|
? `${v.firstNameEN} ${v.lastNameEN}`
|
|
: `${v.firstName} ${v.lastName}`,
|
|
imgUrl: !v.selectedImage
|
|
? v.gender === 'male'
|
|
? `/no-img-man.png`
|
|
: `/no-img-female.png`
|
|
: `${baseUrl}/user/${v.id}/profile-image/${v.selectedImage}`,
|
|
};
|
|
})
|
|
"
|
|
/> -->
|
|
<AvatarGroup
|
|
:data="[
|
|
...responsiblePerson(props.row.quotation).user.map((v) => ({
|
|
name:
|
|
$i18n.locale === 'eng'
|
|
? `${v.firstNameEN} ${v.lastNameEN}`
|
|
: `${v.firstName} ${v.lastName}`,
|
|
imgUrl: !v.selectedImage
|
|
? v.gender === 'male'
|
|
? `/no-img-man.png`
|
|
: `/no-img-female.png`
|
|
: `${baseUrl}/user/${v.id}/profile-image/${v.selectedImage}`,
|
|
})),
|
|
...responsiblePerson(props.row.quotation).group.map((g) => ({
|
|
name: `${$t('general.group')} ${g.group}`,
|
|
imgUrl: '/img-group.png',
|
|
})),
|
|
]"
|
|
></AvatarGroup>
|
|
</q-td>
|
|
<q-td v-if="visibleColumns.includes('status')">
|
|
<BadgeComponent
|
|
:hsla-color="
|
|
{
|
|
[RequestDataStatus.Pending]: '--orange-5-hsl',
|
|
[RequestDataStatus.Ready]: '--yellow-6-hsl',
|
|
[RequestDataStatus.InProgress]: '--blue-6-hsl',
|
|
[RequestDataStatus.Completed]: '--green-8-hsl',
|
|
[RequestDataStatus.Canceled]: '--red-5-hsl',
|
|
}[props.row.requestDataStatus]
|
|
"
|
|
:title="
|
|
$t(`requestList.status.${props.row.requestDataStatus}`) || '-'
|
|
"
|
|
/>
|
|
<BadgeComponent
|
|
v-if="
|
|
props.row.customerRequestCancel &&
|
|
props.row.requestDataStatus !== RequestDataStatus.Canceled
|
|
"
|
|
:hsla-color="
|
|
props.row.rejectRequestCancel ? '--blue-6-hsl' : '--red-5-hsl'
|
|
"
|
|
:title="
|
|
props.row.rejectRequestCancel
|
|
? $t('requestList.status.RejectedCancel') || '-'
|
|
: $t(`requestList.status.CancelRequested`) || '-'
|
|
"
|
|
class="q-ml-sm"
|
|
>
|
|
<template #append>
|
|
<q-tooltip>
|
|
{{
|
|
props.row.rejectRequestCancel
|
|
? props.row.rejectRequestCancelReason ||
|
|
$t('general.noReason')
|
|
: props.row.customerRequestCancelReason ||
|
|
$t('general.noReason')
|
|
}}
|
|
</q-tooltip>
|
|
</template>
|
|
</BadgeComponent>
|
|
</q-td>
|
|
<q-td class="text-right">
|
|
<q-btn
|
|
v-if="!hideView"
|
|
:id="`btn-eye-${props.row.code}`"
|
|
icon="mdi-eye-outline"
|
|
size="sm"
|
|
dense
|
|
round
|
|
flat
|
|
@click.stop="$emit('view', props.row)"
|
|
/>
|
|
|
|
<KebabAction
|
|
v-if="!hideAction"
|
|
:id-name="`btn-kebab-${props.row.code}`"
|
|
hide-edit
|
|
hide-toggle
|
|
hide-view
|
|
hide-delete
|
|
use-cancel
|
|
:use-reject-cancel="
|
|
props.row.customerRequestCancel && !props.row.rejectRequestCancel
|
|
"
|
|
:disable-cancel="
|
|
props.row.requestDataStatus === RequestDataStatus.Canceled ||
|
|
props.row.requestDataStatus === RequestDataStatus.Completed
|
|
"
|
|
@cancel="$emit('delete', props.row)"
|
|
@reject-cancel="$emit('rejectCancel', props.row)"
|
|
/>
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
|
|
<template
|
|
v-slot:item="props: {
|
|
row: RequestData;
|
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
|
>
|
|
<div class="col-md-4 col-sm-6 col-12">
|
|
<QuotationCard
|
|
hide-preview
|
|
hide-kebab-view
|
|
hide-kebab-edit
|
|
hide-kebab-delete
|
|
:use-cancel="!hideAction"
|
|
class="full-height"
|
|
:hide-action="hideAction"
|
|
:use-reject-cancel="
|
|
props.row.customerRequestCancel && !props.row.rejectRequestCancel
|
|
"
|
|
:disable-cancel="
|
|
props.row.requestDataStatus === RequestDataStatus.Canceled ||
|
|
props.row.requestDataStatus === RequestDataStatus.Completed
|
|
"
|
|
:badge-color="
|
|
{
|
|
[RequestDataStatus.Pending]: '--orange-5-hsl',
|
|
[RequestDataStatus.Ready]: '--yellow-6-hsl',
|
|
[RequestDataStatus.InProgress]: '--blue-6-hsl',
|
|
[RequestDataStatus.Completed]: '--green-8-hsl',
|
|
[RequestDataStatus.Canceled]: '--red-5-hsl',
|
|
}[props.row.requestDataStatus]
|
|
"
|
|
:urgent="props.row.quotation.urgent"
|
|
:code="props.row.code"
|
|
:title="props.row.quotation.workName"
|
|
:status="$t(`requestList.status.${props.row.requestDataStatus}`)"
|
|
:custom-data="[
|
|
{
|
|
label: $t('customer.employer'),
|
|
value:
|
|
getCustomerName(props.row, {
|
|
noCode: true,
|
|
locale: $i18n.locale,
|
|
}) || '-',
|
|
},
|
|
{
|
|
label: $t('customer.employee'),
|
|
value:
|
|
getEmployeeName(props.row, { locale: $i18n.locale }) || '-',
|
|
},
|
|
{
|
|
label: $t('requestList.quotationCode'),
|
|
value: props.row.quotation.code || '-',
|
|
},
|
|
{
|
|
label: $t('flow.responsiblePerson'),
|
|
value: '',
|
|
slotName: 'responsiblePerson',
|
|
},
|
|
]"
|
|
@view="$emit('view', props.row)"
|
|
@cancel="$emit('delete', props.row)"
|
|
@reject-cancel="$emit('rejectCancel', props.row)"
|
|
>
|
|
<template v-slot:responsiblePerson="{ props: subProps }">
|
|
<div class="col-4 app-text-muted q-pr-sm">
|
|
{{ subProps.label }}
|
|
</div>
|
|
<div class="col-8">
|
|
<AvatarGroup
|
|
v-if="
|
|
(responsiblePerson(props.row.quotation).user ?? []).length >
|
|
0 ||
|
|
(responsiblePerson(props.row.quotation).group ?? []).length >
|
|
0
|
|
"
|
|
:data="[
|
|
...responsiblePerson(props.row.quotation).user.map((v) => ({
|
|
name:
|
|
$i18n.locale === 'eng'
|
|
? `${v.firstNameEN} ${v.lastNameEN}`
|
|
: `${v.firstName} ${v.lastName}`,
|
|
imgUrl: !v.selectedImage
|
|
? v.gender === 'male'
|
|
? `/no-img-man.png`
|
|
: `/no-img-female.png`
|
|
: `${baseUrl}/user/${v.id}/profile-image/${v.selectedImage}`,
|
|
})),
|
|
...responsiblePerson(props.row.quotation).group.map((g) => ({
|
|
name: `${$t('general.group')} ${g.group}`,
|
|
imgUrl: '/img-group.png',
|
|
})),
|
|
]"
|
|
/>
|
|
<span v-else>-</span>
|
|
</div>
|
|
</template>
|
|
|
|
<template #badge>
|
|
<BadgeComponent
|
|
v-if="
|
|
props.row.customerRequestCancel &&
|
|
props.row.requestDataStatus !== RequestDataStatus.Canceled
|
|
"
|
|
:hsla-color="
|
|
props.row.rejectRequestCancel ? '--blue-6-hsl' : '--red-5-hsl'
|
|
"
|
|
:title="
|
|
props.row.rejectRequestCancel
|
|
? $t('requestList.status.RejectedCancel') || '-'
|
|
: $t(`requestList.status.CancelRequested`) || '-'
|
|
"
|
|
>
|
|
<template #append>
|
|
<q-tooltip>
|
|
{{
|
|
props.row.rejectRequestCancel
|
|
? props.row.rejectRequestCancelReason ||
|
|
$t('general.noReason')
|
|
: props.row.customerRequestCancelReason ||
|
|
$t('general.noReason')
|
|
}}
|
|
</q-tooltip>
|
|
</template>
|
|
</BadgeComponent>
|
|
</template>
|
|
</QuotationCard>
|
|
</div>
|
|
</template>
|
|
</q-table>
|
|
</template>
|
|
|
|
<style scoped>
|
|
:deep(tr:nth-child(2n)) {
|
|
background: #f9fafc;
|
|
&.dark {
|
|
background: hsl(var(--gray-11-hsl) / 0.2);
|
|
}
|
|
}
|
|
|
|
.q-table tr.urgent {
|
|
background: hsla(var(--red-6-hsl) / 0.03);
|
|
}
|
|
|
|
.q-table tr.urgent td:first-child {
|
|
&::after {
|
|
content: ' ';
|
|
display: block;
|
|
position: absolute;
|
|
left: 0;
|
|
top: 15%;
|
|
bottom: 15%;
|
|
background: var(--red-8);
|
|
width: 4px;
|
|
border-radius: 99rem;
|
|
animation: blink 1s infinite;
|
|
}
|
|
}
|
|
|
|
@keyframes blink {
|
|
0% {
|
|
background: var(--red-8);
|
|
}
|
|
50% {
|
|
background: var(--red-3);
|
|
}
|
|
100% {
|
|
background: var(--red-8);
|
|
}
|
|
}
|
|
|
|
.link {
|
|
color: hsl(var(--info-bg));
|
|
text-decoration: underline;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.disabled-row {
|
|
opacity: 0.3;
|
|
filter: grayscale(1);
|
|
}
|
|
</style>
|