jws-frontend/src/pages/05_quotation/ImportWorker.vue
Methapon Metanipat 202e8c2e6f
feat: import worker with multiple criteria (#72)
* feat: add i18n import from file

* fix: background color

* feat: add import worker component

* feat: support newer passport

* refactor: update validator

* fix: wrong endpoint

* refactor: change to display passport

* feat: add component and state

* fix: wrong validator

* refactor: add i18n

* refactor: add slont top-append

* refactor: use v-model

* refactor: impurt workder

* fix: criteria

* refactor: add customer branch id

---------

Co-authored-by: Thanaphon Frappet <thanaphon@frappet.com>
2024-11-13 11:53:25 +07:00

232 lines
6.6 KiB
Vue

<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { csvToData } from 'src/utils/file';
import UploadFileArea from 'components/upload-file/UploadFileArea.vue';
import DialogHeader from 'components/dialog/DialogHeader.vue';
import { SaveButton, CancelButton } from 'components/button';
import DialogFormContainer from 'src/components/dialog/DialogFormContainer.vue';
const open = defineModel<boolean>('open', { required: true });
const payload = defineModel<{
passport: string[] | null;
visa: string[] | null;
}>('data', { required: true });
defineEmits<{
(e: 'importWorker', v: typeof payload.value): void;
}>();
defineProps<{
importWorker?: (v: typeof payload.value) => boolean | Promise<boolean>;
}>();
const passportValidator = /[a-zA-Z]{1}[a-zA-Z0-9]{1}[0-9]{5,7}$/;
async function handleFileSelect(file: File[] | File) {
const promise = [file]
.flat()
.map((v) => csvToData<{ passport: string; visa: string }>(v));
const data = await Promise.all(promise);
payload.value.passport = (payload.value.passport ?? []).concat(
data
.flat()
.flatMap((v) =>
v?.passport &&
!payload.value.passport?.includes(v.passport) &&
passportValidator.test(v.passport)
? v.passport
: [],
),
);
}
const SELECT_PROPS = {
clearable: true,
dense: true,
multiple: true,
outlined: true,
useInput: true,
useChips: true,
hideDropdownIcon: true,
stackLabel: true,
};
</script>
<template>
<DialogFormContainer
v-model="open"
@submit="
async () => {
if (importWorker) open = !(await importWorker(payload));
$emit('importWorker', payload);
}
"
>
<template #header>
<DialogHeader :title="$t('dialog.title.importWorker')" />
</template>
<template #footer>
<CancelButton type="button" class="q-ml-auto" outlined v-close-popup />
<SaveButton
:label="$t('dialog.title.importWorker')"
icon="mdi-plus"
class="q-ml-sm"
type="submit"
solid
/>
</template>
<div class="q-pa-md">
<UploadFileArea
:label="$t('general.importFromFile', { suffix: '(.csv)' })"
type="button"
accept=".csv"
multiple
@file="handleFileSelect"
/>
</div>
<div style="flex: 1; width: 100%; overflow-y: auto" class="q-px-md q-mb-md">
<div class="content-container">
<q-expansion-item
dense
class="overflow-hidden"
switch-toggle-side
default-opened
style="border-radius: var(--radius-2)"
expand-icon="mdi-chevron-down-circle"
header-class="surface-1"
>
<template v-slot:header>
<section class="row items-center full-width">
<div class="row items-center q-pr-md q-py-xs">
<span
class="text-weight-medium q-mr-md"
style="font-size: 18px"
>
{{ $t('general.passport') }}
</span>
</div>
</section>
</template>
<div class="surface-1 q-pa-md">
<q-select
class="fixed-height"
v-model="payload.passport"
v-bind="SELECT_PROPS"
lazy-rules
:placeholder="`${$t('general.passportNo')} ${$t('general.forExample', { example: 'G420316, P897546' })}`"
:hint="$t('general.enterToAdd')"
@new-value="
(value, done) => {
value = value.trim();
if (passportValidator.test(value)) {
return done(value.toUpperCase(), 'add-unique');
}
}
"
:rules="[
(v: string[]) =>
(!!v && v.length > 0) || $t('form.error.required'),
]"
new-value-mode="add-unique"
autocomplete="off"
>
<template #selected-item="scope">
<q-chip
removable
dense
@remove="scope.removeAtIndex(scope.index)"
:tabindex="scope.tabindex"
class="q-ml-none q-pa-md"
>
<Icon
icon="mdi-passport"
class="q-mr-xs"
style="font-size: 150%"
color="hsla(var(--blue-9-hsl) / 1)"
/>
{{ scope.opt }}
</q-chip>
</template>
</q-select>
</div>
</q-expansion-item>
<q-expansion-item
dense
v-if="false"
class="overflow-hidden"
switch-toggle-side
default-opened
style="border-radius: var(--radius-2)"
expand-icon="mdi-chevron-down-circle"
header-class="surface-1"
>
<template v-slot:header>
<section class="row items-center full-width">
<div class="row items-center q-pr-md q-py-sm">
<span
class="text-weight-medium q-mr-md"
style="font-size: 18px"
>
{{ $t('general.visa') }}
</span>
</div>
</section>
</template>
<div class="surface-1 q-pa-md">
<q-select
class="fixed-height"
v-model="payload.visa"
v-bind="SELECT_PROPS"
:placeholder="`${$t('general.visaNo')} ${$t('general.forExample', { example: 'G420316, V897546' })}`"
@new-value="
(value, done) => {
value = value.trim();
if (passportValidator.test(value)) {
return done(value.toUpperCase(), 'add-unique');
}
}
"
:hint="$t('general.enterToAdd')"
autocomplete="off"
/>
</div>
</q-expansion-item>
</div>
</div>
</DialogFormContainer>
</template>
<style scoped>
.content-container {
display: flex;
flex-direction: column;
gap: var(--size-4);
}
.fixed-height {
--_height: 10rem;
}
.fixed-height :deep(.q-field__control) {
min-height: var(--_height) !important;
align-items: start;
}
.fixed-height :deep(.q-field__append) {
min-height: var(--_height) !important;
align-self: center;
}
:deep(i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon) {
color: hsl(var(--text-mute));
}
:deep(
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
color: var(--brand-1);
}
</style>