feat: new worker select

This commit is contained in:
Thanaphon Frappet 2024-12-20 09:46:19 +07:00
parent 928d07a3ba
commit cd1848c5fb
2 changed files with 355 additions and 155 deletions

View file

@ -70,6 +70,7 @@ import {
import QuotationFormReceipt from './QuotationFormReceipt.vue';
import QuotationFormProductSelect from './QuotationFormProductSelect.vue';
import QuotationFormInfo from './QuotationFormInfo.vue';
import QuotationFormWorkerSelect from './QuotationFormWorkerSelect.vue';
import QuotationFormWorkerAddDialog from './QuotationFormWorkerAddDialog.vue';
import ProfileBanner from 'components/ProfileBanner.vue';
import DialogForm from 'components/DialogForm.vue';
@ -161,6 +162,7 @@ const readonly = computed(() => {
quotationFormState.value.mode === 'edit'
);
});
const test = ref<boolean>(false);
const selectedWorker = ref<
(Employee & {
@ -721,10 +723,6 @@ function triggerCreateEmployee() {
async function triggerSelectEmployeeDialog() {
pageState.employeeModal = true;
await nextTick();
refSelectZoneEmployee.value?.assignSelect(
preSelectedWorker.value,
selectedWorker.value,
);
}
function triggerProductServiceDialog() {
@ -926,12 +924,12 @@ function convertToTable(nodes: Node[]) {
pageState.productServiceModal = false;
}
function convertEmployeeToTable() {
function convertEmployeeToTable(selected: Employee[]) {
productServiceList.value.forEach((v) => {
if (selectedWorker.value.length === 0 && v.amount === 1) v.amount -= 1;
v.amount = Math.max(
v.amount + preSelectedWorker.value.length - selectedWorker.value.length,
v.amount + selected.length - selectedWorker.value.length,
1,
);
@ -941,21 +939,17 @@ function convertEmployeeToTable() {
selectedWorker.value.forEach((item, i) => {
if (v.workerIndex.includes(i)) oldWorkerId.push(item.id);
});
preSelectedWorker.value.forEach((item, i) => {
selected.forEach((item, i) => {
if (selectedWorker.value.find((n) => item.id === n.id)) return;
newWorkerIndex.push(i);
});
v.workerIndex = oldWorkerId
.map((id) => preSelectedWorker.value.findIndex((item) => item.id === id))
.map((id) => selected.findIndex((item) => item.id === id))
.filter((idx) => idx !== -1)
.concat(newWorkerIndex);
});
refSelectZoneEmployee.value?.assignSelect(
selectedWorker.value,
preSelectedWorker.value,
);
pageState.employeeModal = false;
quotationFormData.value.workerMax = Math.max(
quotationFormData.value.workerMax || 1,
@ -1253,9 +1247,7 @@ async function getWorkerFromCriteria(
(a) => !selectedWorker.value.find((b) => a.id === b.id),
);
preSelectedWorker.value = [...deduplicate, ...selectedWorker.value];
convertEmployeeToTable();
convertEmployeeToTable([...deduplicate, ...selectedWorker.value]);
return true;
}
@ -2277,148 +2269,17 @@ watch(
<!-- SEC: Dialog -->
<!-- add employee quotation -->
<DialogForm
:title="$t('general.select', { msg: $t('quotation.employeeList') })"
v-model:modal="pageState.employeeModal"
:submit-label="$t('general.select', { msg: $t('quotation.employee') })"
submit-icon="mdi-check"
height="75vh"
:submit="() => convertEmployeeToTable()"
:close="
() => {
(preSelectedWorker = []), (pageState.employeeModal = false);
<QuotationFormWorkerSelect
:preselect-worker="selectedWorker"
v-model:open="pageState.employeeModal"
@success="
(v) => {
selectedWorker = v;
}
"
>
<template #top-append>
<q-btn
class="q-mr-sm"
flat
size="sm"
icon="mdi-dots-horizontal"
:id="`btn-kebab-action`"
@click.stop
:title="$t('general.additional')"
style="max-width: 20px"
>
<q-menu class="bordered" ref="refMenu">
<q-list>
<q-item
v-close-popup
dense
clickable
class="row items-center"
style="white-space: nowrap"
@click.stop="
() => {
triggerCreateEmployee();
}
"
>
<q-icon
size="xs"
class="q-mr-sm"
name="mdi-plus"
style="color: hsl(var(--info-bg))"
/>
<span>
{{ $t('quotation.addWorker') }}
</span>
</q-item>
<q-item
v-close-popup
dense
clickable
style="white-space: nowrap"
class="row items-center"
@click.stop="() => (pageState.importWorker = true)"
>
<q-icon
size="xs"
class="q-mr-sm"
name="mdi-import"
style="color: hsl(195 90% 50%)"
/>
<span>{{ $t('quotation.importWorker') }}</span>
</q-item>
</q-list>
</q-menu>
</q-btn>
</template>
<section class="col row scroll">
<SelectZone
ref="refSelectZoneEmployee"
v-model:selected-item="preSelectedWorker"
@search="
(v) => {
searchEmployee(v);
}
"
:items="workerList"
:new-items="newWorkerList"
>
<template #data="{ item }">
<PersonCard
noAction
prefixId="asda"
class="full-width"
:data="{
name:
$i18n.locale === 'eng'
? `${item.firstNameEN} ${item.lastNameEN}`
: `${item.firstName} ${item.lastName}`,
code: item.employeePassport?.at(0)?.number,
female: item.gender === 'female',
male: item.gender === 'male',
img: `${API_BASE_URL}/customer/${item.id}/image/${item.selectedImage}`,
fallbackImg: '/images/employee-avatar.png',
detail: [
{
icon: 'mdi-passport',
value: optionStore.mapOption(item.nationality),
},
{
icon: 'mdi-clock-outline',
value: calculateAge(item.dateOfBirth),
},
],
}"
></PersonCard>
</template>
<template #newData="{ item }">
<PersonCard
noAction
prefixId="asda"
class="full-width"
:data="{
name:
$i18n.locale === 'eng'
? `${item.firstNameEN} ${item.lastNameEN}`
: `${item.firstName} ${item.lastName}`,
code: item.code,
female: item.gender === 'female',
male: item.gender === 'male',
img: `${API_BASE_URL}/customer/${item.id}/image/${item.selectedImage}`,
fallbackImg: '/images/employee-avatar.png',
detail: [
{
icon: 'mdi-passport',
value: optionStore.mapOption(item.nationality),
},
{
icon: 'mdi-clock-outline',
value: calculateAge(item.dateOfBirth),
},
],
}"
></PersonCard>
</template>
</SelectZone>
</section>
</DialogForm>
@trigger-create-employee="() => triggerCreateEmployee()"
/>
<!-- add product service -->

View file

@ -0,0 +1,339 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { reactive, ref, watch } from 'vue';
import { calculateAge } from 'src/utils/datetime';
import useOptionStore from 'src/stores/options';
import useEmployeeStore from 'src/stores/employee';
import { useQuotationStore } from 'src/stores/quotations';
import { Employee } from 'src/stores/employee/types';
import { CancelButton, MainButton } from 'components/button';
import DialogContainer from 'components/dialog/DialogContainer.vue';
import DialogHeader from 'components/dialog/DialogHeader.vue';
import ImportWorker from './ImportWorker.vue';
import PersonCard from 'src/components/shared/PersonCard.vue';
import { QuotationFull, EmployeeWorker } from 'src/stores/quotations/types';
import { Lang } from 'src/utils/ui';
import NoData from 'src/components/NoData.vue';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
const { locale } = useI18n();
const props = defineProps<{
customerBranchId?: string;
disabledWorkerId?: string[];
preselectWorker?: Employee[];
}>();
const emits = defineEmits<{
(e: 'triggerCreateEmployee'): void;
(e: 'success', workerSelected: Employee[]): void;
}>();
const optionStore = useOptionStore();
const employeeStore = useEmployeeStore();
const open = defineModel<boolean>('open', { default: false });
const newWorkerList = defineModel<EmployeeWorker[]>('newWorkerList', {
default: [],
});
const workerSelected = ref<Employee[]>([]);
const workerList = ref<Employee[]>([]);
const importWorkerCriteria = ref<{
passport: string[] | null;
visa: string[] | null;
}>({
passport: [],
visa: [],
});
const state = reactive({
importWorker: false,
step: 1,
search: '',
});
function clean() {
workerList.value = [];
workerSelected.value = [];
open.value = false;
}
function selectedIndex(item: Employee) {
return workerSelected.value.findIndex((v) => v.id === item.id);
}
function toggleSelect(item: Employee) {
if (props.disabledWorkerId?.some((id) => id === item.id)) return;
const index = selectedIndex(item);
if (index === -1) {
workerSelected.value.push(item);
} else {
workerSelected.value.splice(index, 1);
}
}
function getEmployeeImageUrl(item: Employee) {
if (item.selectedImage) {
return `${API_BASE_URL}/employee/${item.id}/image/${item.selectedImage}`;
}
// NOTE: static image
return '/images/employee-avatar.png';
}
function init() {
if (props.preselectWorker) {
workerSelected.value = JSON.parse(JSON.stringify(props.preselectWorker));
}
getWorkerList();
}
async function getWorkerList() {
const ret = await employeeStore.fetchList({
pageSize: 100,
query: state.search,
passport: true,
customerBranchId: props.customerBranchId,
});
if (!ret) return false;
workerList.value = ret.result;
}
async function getWorkerFromCriteria(
payload: typeof importWorkerCriteria.value,
) {
const ret = await employeeStore.fetchList({
payload: {
passport: payload.passport || undefined,
},
pageSize: 99999, // must include all possible worker
page: 1,
passport: true,
customerBranchId: props.customerBranchId,
});
if (!ret) return false; // error, do not close dialog
workerSelected.value = ret.result.filter(
(lhs) => !props.disabledWorkerId?.find((rhs) => lhs.id === rhs),
);
if (workerSelected.value.length > 0) state.step = 2;
return true;
}
watch(() => state.search, getWorkerList);
</script>
<template>
<ImportWorker
v-model:open="state.importWorker"
v-model:data="importWorkerCriteria"
:import-worker="getWorkerFromCriteria"
/>
<DialogContainer v-model="open" @open="init" @close="clean">
<template #header>
<DialogHeader :title="$t('quotation.addWorker')">
<template #title-before>
<span class="q-mr-auto"></span>
</template>
<template #title-after>
<q-btn
class="q-ml-auto q-mr-xs"
flat
size="sm"
icon="mdi-dots-horizontal"
:id="`btn-kebab-action`"
@click.stop
:title="$t('general.additional')"
style="max-width: 20px"
>
<q-menu class="bordered" ref="refMenu">
<q-list>
<q-item
v-close-popup
dense
clickable
class="row items-center"
style="white-space: nowrap"
@click.stop="() => emits('triggerCreateEmployee')"
>
<q-icon
size="xs"
class="q-mr-sm"
name="mdi-plus"
style="color: hsl(var(--info-bg))"
/>
<span>
{{ $t('quotation.addWorker') }}
</span>
</q-item>
<q-item
v-close-popup
dense
clickable
style="white-space: nowrap"
class="row items-center"
@click.stop="() => (state.importWorker = true)"
>
<q-icon
size="xs"
class="q-mr-sm"
name="mdi-import"
style="color: hsl(195 90% 50%)"
/>
<span>{{ $t('quotation.importWorker') }}</span>
</q-item>
</q-list>
</q-menu>
</q-btn>
</template>
</DialogHeader>
</template>
<div class="column q-pa-md full-height">
<section class="row justify-end q-mb-md">
<q-input
for="input-search"
outlined
dense
:label="$t('general.search')"
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
v-model="state.search"
debounce="500"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
</section>
<!-- wrapper -->
<div class="col scroll">
<section
:class="{ ['items-center']: workerList.length === 0 }"
class="row q-col-gutter-md"
>
<div
style="display: inline-block; margin-inline: auto"
v-if="workerList.length === 0"
>
<NoData :not-found="!!state.search" />
</div>
<div
:key="emp.id"
v-for="(emp, index) in workerList.map((data) => ({
...data,
_selectedIndex: selectedIndex(data),
}))"
class="col-2"
>
<button
class="selectable-item full-width"
:class="{
['selectable-item__selected']: emp._selectedIndex !== -1,
['selectable-item__disabled']: disabledWorkerId?.some(
(id) => id === emp.id,
),
}"
@click="toggleSelect(emp)"
>
<span class="selectable-item__pos">
{{ emp._selectedIndex + 1 }}
</span>
<PersonCard
no-action
class="full-width"
:prefix-id="'employee-' + index"
:data="{
name:
locale === Lang.English
? `${emp.firstNameEN} ${emp.lastNameEN}`
: `${emp.firstName} ${emp.lastName}`,
code: emp.employeePassport?.at(0)?.number || '-',
female: emp.gender === 'female',
male: emp.gender === 'male',
img: getEmployeeImageUrl(emp),
fallbackImg: '/images/employee-avatar.png',
detail: [
{
icon: 'mdi-passport',
value: optionStore.mapOption(emp.nationality),
},
{
icon: 'mdi-clock-outline',
value: calculateAge(emp.dateOfBirth),
},
],
}"
/>
</button>
</div>
</section>
</div>
</div>
<template #footer>
<div class="q-gutter-x-xs q-ml-auto">
<CancelButton outlined @click="clean" />
<MainButton
icon="mdi-check"
color="207 96% 32%"
solid
@click="emits('success', workerSelected), (open = false)"
>
{{ $t('general.select', { msg: $t('quotation.employeeList') }) }}
</MainButton>
</div>
</template>
</DialogContainer>
</template>
<style scoped>
.selectable-item {
padding: 0;
appearance: none;
border: none;
background: transparent;
position: relative;
color: inherit;
& > .selectable-item__pos {
display: none;
}
}
.selectable-item__selected {
& > :deep(*) {
border: 1px solid var(--_color, var(--brand-1)) !important;
}
& > .selectable-item__pos {
display: block;
position: absolute;
margin: var(--size-2);
right: 0;
top: 0;
border-radius: 50%;
width: 20px;
height: 20px;
color: var(--surface-1);
background: var(--brand-1);
color: white;
}
}
.selectable-item__disabled {
filter: grayscale(1);
opacity: 0.5;
& :deep(*) {
cursor: not-allowed;
}
}
</style>