feat: new worker select
This commit is contained in:
parent
928d07a3ba
commit
cd1848c5fb
2 changed files with 355 additions and 155 deletions
339
src/pages/05_quotation/QuotationFormWorkerSelect.vue
Normal file
339
src/pages/05_quotation/QuotationFormWorkerSelect.vue
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue