jws-frontend/src/pages/08_request-list/TableRequestList.vue
puriphatt 7679c076a7
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
feat: add disabled submit functionality to OcrDialog and enhance request list handling
2025-07-08 11:39:57 +07:00

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>