554 lines
14 KiB
Vue
554 lines
14 KiB
Vue
<script setup lang="ts">
|
|
import { useI18n } from 'vue-i18n';
|
|
import { QTableColumn } from 'quasar';
|
|
import { EmployeeHistory, NewEmployeeHistory } from 'stores/employee/types';
|
|
import { dateFormat } from 'src/utils/datetime';
|
|
import NoData from '../NoData.vue';
|
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
import useOptionStore from 'stores/options';
|
|
|
|
const { t } = useI18n();
|
|
const optionStore = useOptionStore();
|
|
|
|
const historyList = defineModel<EmployeeHistory[]>('historyList', {
|
|
required: true,
|
|
});
|
|
|
|
const columns: QTableColumn[] = [
|
|
{
|
|
name: 'updatedAt',
|
|
label: t('general.time'),
|
|
field: 'updatedAt',
|
|
align: 'left',
|
|
headerStyle: 'font-weight: bold',
|
|
},
|
|
{
|
|
name: 'updatedBy',
|
|
align: 'center',
|
|
label: t('general.updatedBy'),
|
|
field: 'updatedBy',
|
|
headerStyle: 'font-weight: bold',
|
|
},
|
|
|
|
{
|
|
name: 'history',
|
|
align: 'center',
|
|
label: '',
|
|
field: 'history',
|
|
},
|
|
{
|
|
name: 'valueAfter',
|
|
align: 'center',
|
|
label: t('general.afterEdit'),
|
|
field: 'valueAfter',
|
|
headerStyle: 'font-weight: bold',
|
|
},
|
|
{
|
|
name: 'valueBefore',
|
|
align: 'center',
|
|
label: t('general.beforeEdit'),
|
|
field: 'valueBefore',
|
|
headerStyle: 'font-weight: bold',
|
|
},
|
|
];
|
|
|
|
const currentDate = ref();
|
|
const isHistoryData = ref(false);
|
|
const formatList = ref<NewEmployeeHistory[]>([]);
|
|
|
|
const fieldName = [
|
|
{
|
|
name: 'customerBranchId',
|
|
title: 'form.field.basicInformation',
|
|
i18n: 'customer.form.branchCode',
|
|
},
|
|
{
|
|
name: 'nrcNo',
|
|
title: 'form.field.basicInformation',
|
|
i18n: 'customerEmployee.form.nrcNo',
|
|
},
|
|
|
|
{
|
|
name: 'firstName',
|
|
title: 'customerEmployee.form.group.personalInfo',
|
|
i18n: 'form.firstName',
|
|
},
|
|
{
|
|
name: 'firstNameEN',
|
|
title: 'customerEmployee.form.group.personalInfo',
|
|
i18n: 'form.firstNameEN',
|
|
},
|
|
{
|
|
name: 'lastName',
|
|
title: 'customerEmployee.form.group.personalInfo',
|
|
i18n: 'form.lastName',
|
|
},
|
|
{
|
|
name: 'lastNameEN',
|
|
title: 'customerEmployee.form.group.personalInfo',
|
|
i18n: 'form.lastNameEN',
|
|
},
|
|
{
|
|
name: 'middleName',
|
|
title: 'customerEmployee.form.group.personalInfo',
|
|
i18n: 'form.middleName',
|
|
},
|
|
{
|
|
name: 'middleNameEN',
|
|
title: 'customerEmployee.form.group.personalInfo',
|
|
i18n: 'form.middleNameEN',
|
|
},
|
|
{
|
|
name: 'dateOfBirth',
|
|
title: 'customerEmployee.form.group.personalInfo',
|
|
i18n: 'form.birthDate',
|
|
},
|
|
{
|
|
name: 'gender',
|
|
title: 'customerEmployee.form.group.personalInfo',
|
|
i18n: 'form.gender',
|
|
},
|
|
{
|
|
name: 'nationality',
|
|
title: 'customerEmployee.form.group.personalInfo',
|
|
i18n: 'general.nationality',
|
|
},
|
|
|
|
{
|
|
name: 'address',
|
|
title: 'form.field.address',
|
|
i18n: 'form.addressNo',
|
|
},
|
|
{
|
|
name: 'addressEN',
|
|
title: 'form.field.address',
|
|
i18n: 'form.addressNo',
|
|
},
|
|
{
|
|
name: 'moo',
|
|
title: 'form.field.address',
|
|
i18n: 'form.moo',
|
|
},
|
|
{
|
|
name: 'mooEN',
|
|
title: 'form.field.address',
|
|
i18n: 'form.moo',
|
|
},
|
|
{
|
|
name: 'soi',
|
|
title: 'form.field.address',
|
|
i18n: 'form.soi',
|
|
},
|
|
{
|
|
name: 'soiEN',
|
|
title: 'form.field.address',
|
|
i18n: 'form.soi',
|
|
},
|
|
{
|
|
name: 'street',
|
|
title: 'form.field.address',
|
|
i18n: 'form.road',
|
|
},
|
|
{
|
|
name: 'streetEN',
|
|
title: 'form.field.address',
|
|
i18n: 'form.road',
|
|
},
|
|
{
|
|
name: 'provinceId',
|
|
title: 'form.field.address',
|
|
i18n: 'form.province',
|
|
},
|
|
{
|
|
name: 'districtId',
|
|
title: 'form.field.address',
|
|
i18n: 'form.district',
|
|
},
|
|
{
|
|
name: 'subDistrictId',
|
|
title: 'form.field.address',
|
|
i18n: 'form.subDistrict',
|
|
},
|
|
|
|
{
|
|
name: 'passportType',
|
|
title: 'customerEmployee.form.group.passport',
|
|
i18n: 'customerEmployee.form.passportType',
|
|
},
|
|
{
|
|
name: 'passportNumber',
|
|
title: 'customerEmployee.form.group.passport',
|
|
i18n: 'customerEmployee.form.passportNo',
|
|
},
|
|
{
|
|
name: 'previousPassportReference',
|
|
title: 'customerEmployee.form.group.passport',
|
|
i18n: 'customerEmployee.form.passportRef',
|
|
},
|
|
{
|
|
name: 'passportIssuingPlace',
|
|
title: 'customerEmployee.form.group.passport',
|
|
i18n: 'customerEmployee.form.passportPlace',
|
|
},
|
|
{
|
|
name: 'passportIssuingCountry',
|
|
title: 'customerEmployee.form.group.passport',
|
|
i18n: 'customerEmployee.form.passportIssuer',
|
|
},
|
|
{
|
|
name: 'passportIssueDate',
|
|
title: 'customerEmployee.form.group.passport',
|
|
i18n: 'customerEmployee.form.passportIssueDate',
|
|
},
|
|
{
|
|
name: 'passportExpiryDate',
|
|
title: 'customerEmployee.form.group.passport',
|
|
i18n: 'customerEmployee.form.passportExpireDate',
|
|
},
|
|
|
|
{
|
|
name: 'visaType',
|
|
title: 'customerEmployee.form.group.visa',
|
|
i18n: 'customerEmployee.form.visaType',
|
|
},
|
|
{
|
|
name: 'visaNumber',
|
|
title: 'customerEmployee.form.group.visa',
|
|
i18n: 'customerEmployee.form.visaNo',
|
|
},
|
|
{
|
|
name: 'visaIssueDate',
|
|
title: 'customerEmployee.form.group.visa',
|
|
i18n: 'customerEmployee.form.visaIssuance',
|
|
},
|
|
{
|
|
name: 'visaExpiryDate',
|
|
title: 'customerEmployee.form.group.visa',
|
|
i18n: 'customerEmployee.form.visaExpire',
|
|
},
|
|
{
|
|
name: 'visaIssuingPlace',
|
|
title: 'customerEmployee.form.group.visa',
|
|
i18n: 'customerEmployee.form.visaPlace',
|
|
},
|
|
{
|
|
name: 'visaStayUntilDate',
|
|
title: 'customerEmployee.form.group.visa',
|
|
i18n: 'customerEmployee.form.visaStayUntil',
|
|
},
|
|
{
|
|
name: 'tm6Number',
|
|
title: 'customerEmployee.form.group.visa',
|
|
i18n: 'customerEmployee.form.visaTM6',
|
|
},
|
|
{
|
|
name: 'entryDate',
|
|
title: 'customerEmployee.form.group.visa',
|
|
i18n: 'customerEmployee.form.visaEnter',
|
|
},
|
|
];
|
|
|
|
function nextDate(pre: boolean = false) {
|
|
const date = new Date(currentDate.value);
|
|
pre ? date.setDate(date.getDate() - 1) : date.setDate(date.getDate() + 1);
|
|
const dateKey = formatDate(date.toString());
|
|
currentDate.value = dateKey;
|
|
}
|
|
|
|
function formatDate(date: string) {
|
|
const updatedAt = new Date(date);
|
|
const dateKey = `${updatedAt.getFullYear()}-${updatedAt.getMonth() + 1}-${updatedAt.getDate()}`;
|
|
return dateKey;
|
|
}
|
|
|
|
function isValidDate(dateString: string): boolean {
|
|
if (typeof dateString !== 'string' || dateString.length < 24) {
|
|
return false;
|
|
}
|
|
|
|
const date = new Date(dateString);
|
|
|
|
return !isNaN(date.getTime()) && dateString === date.toISOString();
|
|
}
|
|
|
|
function mapName(field: string): { title: string; i18n: string } {
|
|
const fieldData = fieldName.find((item) => item.name === field);
|
|
if (fieldData) {
|
|
return { title: fieldData.title, i18n: fieldData.i18n };
|
|
}
|
|
return { title: '-', i18n: '-' };
|
|
}
|
|
|
|
function groupEmployeeHistory(
|
|
historyList: EmployeeHistory[],
|
|
): NewEmployeeHistory[] {
|
|
const grouped = historyList.reduce((acc, curr) => {
|
|
const updatedAt = new Date(curr.updatedAt);
|
|
const dateKey = `${updatedAt.getFullYear()}-${updatedAt.getMonth() + 1}-${updatedAt.getDate()}`;
|
|
|
|
const existingEntry = acc.find((entry) => entry.date === dateKey);
|
|
|
|
if (existingEntry) {
|
|
const existingData = existingEntry.data.find(
|
|
(data) => data.updatedAt.getTime() === updatedAt.getTime(),
|
|
);
|
|
|
|
if (existingData) {
|
|
existingData.history.push({
|
|
valueAfter: curr.valueAfter,
|
|
valueBefore: curr.valueBefore,
|
|
field: curr.field,
|
|
});
|
|
} else {
|
|
existingEntry.data.push({
|
|
masterId: curr.masterId,
|
|
updatedBy: curr.updatedBy,
|
|
updatedByUserId: curr.updatedByUserId,
|
|
updatedAt: updatedAt,
|
|
id: curr.id,
|
|
history: [
|
|
{
|
|
valueAfter: curr.valueAfter,
|
|
valueBefore: curr.valueBefore,
|
|
field: curr.field,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
} else {
|
|
acc.push({
|
|
date: dateKey,
|
|
data: [
|
|
{
|
|
masterId: curr.masterId,
|
|
updatedBy: curr.updatedBy,
|
|
updatedByUserId: curr.updatedByUserId,
|
|
updatedAt: updatedAt,
|
|
id: curr.id,
|
|
history: [
|
|
{
|
|
valueAfter: curr.valueAfter,
|
|
valueBefore: curr.valueBefore,
|
|
field: curr.field,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
return acc;
|
|
}, [] as NewEmployeeHistory[]);
|
|
|
|
return grouped;
|
|
}
|
|
|
|
onMounted(async () => {
|
|
formatList.value = groupEmployeeHistory(historyList.value);
|
|
currentDate.value =
|
|
formatList.value.at(0)?.date || formatDate(new Date().toISOString());
|
|
});
|
|
|
|
const currentData = computed(() => {
|
|
return formatList.value.find((v) => v.date === currentDate.value)?.data || [];
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="col-12">
|
|
<div
|
|
class="row full-width justify-center q-py-sm header-border"
|
|
style="background: hsla(var(--info-bg) / 0.1)"
|
|
>
|
|
<div class="surface-1 q-py-sm q-px-sm row items-center no-wrap">
|
|
<q-btn
|
|
flat
|
|
color="info"
|
|
padding="0"
|
|
icon="mdi-chevron-left"
|
|
@click="nextDate(true)"
|
|
/>
|
|
<VueDatePicker
|
|
:dark="$q.dark.isActive"
|
|
id="input-birth-date"
|
|
for="input-birth-date"
|
|
class="date-picker"
|
|
utc
|
|
autoApply
|
|
:teleport="true"
|
|
v-model="currentDate"
|
|
:locale="$i18n.locale === 'tha' ? 'th' : 'en'"
|
|
:enableTimePicker="false"
|
|
>
|
|
<template #year="{ value }">
|
|
{{ $i18n.locale === 'tha' ? value + 543 : value }}
|
|
</template>
|
|
<template #year-overlay-value="{ value }">
|
|
{{ $i18n.locale === 'tha' ? value + 543 : value }}
|
|
</template>
|
|
<template #trigger>
|
|
<span class="text-weight-medium q-px-xl">
|
|
{{ dateFormat(currentDate) }}
|
|
</span>
|
|
</template>
|
|
</VueDatePicker>
|
|
<q-btn
|
|
flat
|
|
color="info"
|
|
padding="0"
|
|
icon="mdi-chevron-right"
|
|
@click="nextDate()"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="currentData?.length > 0">
|
|
<q-table
|
|
flat
|
|
class="table-border"
|
|
table-header-class="surface-2"
|
|
:rows="currentData"
|
|
:columns="columns"
|
|
row-key="name"
|
|
>
|
|
<template v-slot:body="props">
|
|
<q-tr
|
|
:props="props"
|
|
:style="`background-color: ${props.rowIndex % 2 === 0 ? '' : 'var(--surface-2)'}`"
|
|
>
|
|
<q-td key="updatedAt" :props="props">
|
|
{{ dateFormat(props.row.updatedAt, false, true, true) }}
|
|
</q-td>
|
|
|
|
<q-td key="updatedBy" :props="props">
|
|
<div class="row items-center no-wrap">
|
|
<q-avatar class="surface-tab">
|
|
<img
|
|
v-if="false"
|
|
src="https://cdn.quasar.dev/img/avatar.png"
|
|
/>
|
|
<q-icon v-else name="mdi-account"></q-icon>
|
|
</q-avatar>
|
|
<div class="column q-pl-md items-start">
|
|
<span class="text-weight-bold">
|
|
{{
|
|
$i18n.locale === 'eng'
|
|
? `${props.row.updatedBy.firstNameEN} ${props.row.updatedBy.lastNameEN}`
|
|
: (`${props.row.updatedBy.firstName} ${props.row.updatedBy.lastName}` ??
|
|
'-')
|
|
}}
|
|
</span>
|
|
<!-- <span class="text-caption">นักบริหาร</span> -->
|
|
</div>
|
|
</div>
|
|
</q-td>
|
|
|
|
<q-td key="history" :props="props">
|
|
<q-stepper vertical flat>
|
|
<q-step
|
|
v-for="(item, index) in props.row.history"
|
|
:key="index"
|
|
:name="index"
|
|
:title="$t(mapName(item.field).title)"
|
|
:caption="$t(mapName(item.field).i18n)"
|
|
:icon="`mdi-numeric-${props.row.history.length - index}`"
|
|
></q-step>
|
|
</q-stepper>
|
|
</q-td>
|
|
|
|
<q-td key="valueBefore" :props="props">
|
|
<div
|
|
v-for="(i, index) in props.row.history"
|
|
:key="index"
|
|
class="q-py-md"
|
|
>
|
|
{{
|
|
isValidDate(i.valueBefore) === true
|
|
? dateFormat(i.valueBefore)
|
|
: optionStore.mapOption(i.valueBefore)
|
|
}}
|
|
</div>
|
|
</q-td>
|
|
|
|
<q-td key="valueAfter" :props="props">
|
|
<div
|
|
v-for="(i, index) in props.row.history"
|
|
:key="index"
|
|
class="q-py-md"
|
|
>
|
|
{{
|
|
isValidDate(i.valueAfter) === true
|
|
? dateFormat(i.valueAfter)
|
|
: optionStore.mapOption(i.valueAfter)
|
|
}}
|
|
</div>
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
</q-table>
|
|
</div>
|
|
|
|
<div v-else class="table-border flex items-center justify-center q-py-lg">
|
|
<NoData />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.header-border {
|
|
border: 1px solid var(--border-color);
|
|
border-top-left-radius: var(--radius-2);
|
|
border-top-right-radius: var(--radius-2);
|
|
}
|
|
|
|
.table-border {
|
|
border-bottom: 1px solid var(--border-color);
|
|
border-left: 1px solid var(--border-color);
|
|
border-right: 1px solid var(--border-color);
|
|
|
|
border-top-left-radius: 0 !important;
|
|
border-top-right-radius: 0 !important;
|
|
border-bottom-left-radius: var(--radius-2);
|
|
border-bottom-right-radius: var(--radius-2);
|
|
}
|
|
|
|
:deep(.q-stepper.q-stepper--vertical.q-stepper--flat) {
|
|
background-color: transparent;
|
|
padding: 0;
|
|
text-align: left;
|
|
}
|
|
|
|
:deep(.q-stepper__caption) {
|
|
color: hsl(var(--text-mute-2)) !important;
|
|
}
|
|
|
|
:deep(.q-stepper__title) {
|
|
color: hsl(var(--info-bg)) !important;
|
|
}
|
|
|
|
:deep(i.q-icon.mdi) {
|
|
font-size: 18px !important;
|
|
}
|
|
|
|
:deep(.q-stepper__dot.row.flex-center.q-stepper__line.relative-position) {
|
|
color: hsl(var(--info-bg)) !important;
|
|
}
|
|
|
|
.date-picker {
|
|
cursor: pointer;
|
|
font-family: 'Noto Sans Thai', sans-serif;
|
|
}
|
|
|
|
:deep(.q-stepper) {
|
|
counter-reset: css-counter;
|
|
}
|
|
|
|
:deep(.q-stepper__dot)span > * {
|
|
display: none;
|
|
}
|
|
|
|
:deep(.q-stepper__dot) span::before {
|
|
counter-increment: css-counter;
|
|
content: counter(css-counter);
|
|
}
|
|
</style>
|