642 lines
19 KiB
Vue
642 lines
19 KiB
Vue
<script lang="ts" setup>
|
|
import { useI18n } from 'vue-i18n';
|
|
import { storeToRefs } from 'pinia';
|
|
import { QSelect, useQuasar } from 'quasar';
|
|
import { nextTick, onMounted, reactive, ref } from 'vue';
|
|
|
|
import { baseUrl } from 'src/stores/utils';
|
|
import { setLocale, dateFormat, calculateAge } from 'src/utils/datetime';
|
|
|
|
import useOptionStore from 'stores/options';
|
|
import useEmployeeStore from 'src/stores/employee';
|
|
import { useQuotationForm } from './form';
|
|
import { Employee } from 'src/stores/employee/types';
|
|
import {
|
|
ProductGroup,
|
|
ProductList,
|
|
Service,
|
|
} from 'src/stores/product-service/types';
|
|
import { QuotationPayload } from 'src/stores/quotations/types';
|
|
|
|
import ProductServiceForm from './ProductServiceForm.vue';
|
|
import WorkerItem from 'components/05_quotation/WorkerItem.vue';
|
|
import QuotationFormInfo from './QuotationFormInfo.vue';
|
|
import ToggleButton from 'src/components/button/ToggleButton.vue';
|
|
import ProductItem from 'src/components/05_quotation/ProductItem.vue';
|
|
import FormAbout from 'src/components/05_quotation/FormAbout.vue';
|
|
import DialogForm from 'src/components/DialogForm.vue';
|
|
import SelectZone from 'src/components/shared/SelectZone.vue';
|
|
import PersonCard from 'src/components/shared/PersonCard.vue';
|
|
import { AddButton, SaveButton } from 'components/button';
|
|
import useProductServiceStore from 'src/stores/product-service';
|
|
import useCustomerStore from 'src/stores/customer';
|
|
|
|
type Node = {
|
|
[key: string]: any;
|
|
opened?: boolean;
|
|
checked?: boolean;
|
|
bg?: string;
|
|
fg?: string;
|
|
icon?: string;
|
|
children?: Node[];
|
|
};
|
|
|
|
defineProps<{
|
|
readonly?: boolean;
|
|
}>();
|
|
|
|
const optionStore = useOptionStore();
|
|
const customerStore = useCustomerStore();
|
|
const productServiceStore = useProductServiceStore();
|
|
const quotationForm = useQuotationForm();
|
|
const employeeStore = useEmployeeStore();
|
|
const { locale } = useI18n();
|
|
const { currentFormData: quotationFormData } = storeToRefs(quotationForm);
|
|
const $q = useQuasar();
|
|
|
|
const refSelectZoneEmployee = ref<InstanceType<typeof SelectZone>>();
|
|
const date = ref();
|
|
const rows = ref<Node[]>([]);
|
|
const selectedBranchIssuer = ref('');
|
|
const selectedCustomer = ref('');
|
|
const toggleWorker = ref(true);
|
|
const branchId = ref('');
|
|
|
|
const selectedWorker = ref<Employee[]>([]);
|
|
const preSelectedWorker = ref<Employee[]>([]);
|
|
const workerList = ref<Employee[]>([]);
|
|
|
|
const quotationNo = ref('');
|
|
const actor = ref('');
|
|
const workName = ref('');
|
|
const contactor = ref('');
|
|
const telephone = ref('');
|
|
const documentReceivePoint = ref('');
|
|
const dueDate = ref('');
|
|
const payType = ref('');
|
|
const paySplitCount = ref(1);
|
|
const payBank = ref('');
|
|
|
|
const pageState = reactive({
|
|
hideStat: false,
|
|
inputSearch: '',
|
|
statusFilter: 'all',
|
|
fieldSelected: [],
|
|
gridView: false,
|
|
|
|
currentTab: 'all',
|
|
addModal: false,
|
|
quotationModal: false,
|
|
employeeModal: false,
|
|
productServiceModal: false,
|
|
});
|
|
|
|
type ProductGroupId = string;
|
|
|
|
const productGroup = ref<ProductGroup[]>([]);
|
|
const productList = ref<Partial<Record<ProductGroupId, ProductList[]>>>({});
|
|
const serviceList = ref<Partial<Record<ProductGroupId, Service[]>>>({});
|
|
|
|
type Id = string;
|
|
const product = ref<Record<Id, ProductList>>({});
|
|
const service = ref<Record<Id, Service>>({});
|
|
|
|
const selectedGroup = ref<ProductGroup | null>(null);
|
|
const selectedGroupSub = ref<'product' | 'service' | null>(null);
|
|
const selectedProductServiceId = ref('');
|
|
|
|
const productServiceList = ref<
|
|
Required<QuotationPayload['productServiceList'][number]>[]
|
|
>([]);
|
|
|
|
async function getAllProduct(
|
|
groupId: string,
|
|
opts?: { force?: false; page?: number; pageSize?: number },
|
|
) {
|
|
selectedGroupSub.value = 'product';
|
|
if (!opts?.force && productList.value[groupId] !== undefined) return;
|
|
const ret = await productServiceStore.fetchListProduct({
|
|
page: opts?.page ?? 1,
|
|
pageSize: opts?.pageSize ?? 9999,
|
|
productGroupId: groupId,
|
|
});
|
|
if (ret) productList.value[groupId] = ret.result;
|
|
}
|
|
|
|
async function getAllService(
|
|
groupId: string,
|
|
opts?: { force?: false; page?: number; pageSize?: number },
|
|
) {
|
|
selectedGroupSub.value = 'service';
|
|
if (!opts?.force && serviceList.value[groupId] !== undefined) return;
|
|
const ret = await productServiceStore.fetchListService({
|
|
page: opts?.page ?? 1,
|
|
pageSize: opts?.pageSize ?? 9999,
|
|
productGroupId: groupId,
|
|
fullDetail: true,
|
|
});
|
|
if (ret) serviceList.value[groupId] = ret.result;
|
|
}
|
|
|
|
async function triggerSelectEmployeeDialog() {
|
|
pageState.employeeModal = true;
|
|
await nextTick();
|
|
refSelectZoneEmployee.value?.assignSelect(
|
|
preSelectedWorker.value,
|
|
selectedWorker.value,
|
|
);
|
|
}
|
|
|
|
function triggerProductServiceDialog() {
|
|
pageState.productServiceModal = true;
|
|
// TODO: form and state controll
|
|
}
|
|
|
|
function toggleDeleteProduct(index: number) {
|
|
console.log(index);
|
|
}
|
|
|
|
function convertToTable(nodes: Node[]) {
|
|
const _recursive = (v: Node): Node | Node[] => {
|
|
if (v.checked && v.children) return v.children.flatMap(_recursive);
|
|
if (v.checked) return v;
|
|
return [];
|
|
};
|
|
productServiceList.value = nodes.flatMap(_recursive).map((v) => v.value);
|
|
}
|
|
|
|
function convertEmployeeToTable() {
|
|
refSelectZoneEmployee.value?.assignSelect(
|
|
selectedWorker.value,
|
|
preSelectedWorker.value,
|
|
);
|
|
pageState.employeeModal = false;
|
|
}
|
|
|
|
function changeMode(mode: string) {
|
|
if (mode === 'light') {
|
|
localStorage.setItem('currentTheme', 'light');
|
|
$q.dark.set(false);
|
|
return;
|
|
}
|
|
|
|
if (mode === 'dark') {
|
|
localStorage.setItem('currentTheme', 'dark');
|
|
$q.dark.set(true);
|
|
return;
|
|
}
|
|
|
|
if (mode === 'baseOnDevice') {
|
|
localStorage.setItem('currentTheme', 'baseOnDevice');
|
|
if (
|
|
window.matchMedia &&
|
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
) {
|
|
$q.dark.set(true);
|
|
} else {
|
|
$q.dark.set(false);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
const ret = await productServiceStore.fetchListProductService({
|
|
page: 1,
|
|
pageSize: 9999,
|
|
});
|
|
if (ret) productGroup.value = ret.result;
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const queryDate = urlParams.get('date');
|
|
date.value = queryDate && new Date(Number(queryDate));
|
|
branchId.value = urlParams.get('branchId') || '';
|
|
quotationFormData.value.customerBranchId =
|
|
urlParams.get('customerBranchId') || '';
|
|
|
|
const resultOption = await fetch('/option/option.json');
|
|
const rawOption = await resultOption.json();
|
|
if (locale.value === 'eng') optionStore.globalOption = rawOption.eng;
|
|
if (locale.value === 'tha') optionStore.globalOption = rawOption.tha;
|
|
|
|
const retEmp = await customerStore.fetchBranchEmployee(
|
|
quotationFormData.value.customerBranchId,
|
|
);
|
|
if (retEmp) workerList.value = retEmp.data.result;
|
|
|
|
const getCurLang = localStorage.getItem('currentLanguage');
|
|
if (getCurLang === 'English') {
|
|
locale.value = 'eng';
|
|
setLocale('en-gb');
|
|
}
|
|
if (getCurLang === 'ไทย') {
|
|
locale.value = 'tha';
|
|
setLocale('th');
|
|
}
|
|
|
|
const getCurTheme = localStorage.getItem('currentTheme');
|
|
if (
|
|
getCurTheme === 'light' ||
|
|
getCurTheme === 'dark' ||
|
|
getCurTheme === 'baseOnDevice'
|
|
) {
|
|
changeMode(getCurTheme);
|
|
} else {
|
|
changeMode('light');
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="fullscreen column surface-0">
|
|
<div class="color-bar">
|
|
<div class="blue-segment"></div>
|
|
<div class="orange-segment"></div>
|
|
<div class="gray-segment"></div>
|
|
</div>
|
|
|
|
<header class="row q-pa-md items-center">
|
|
<div style="flex: 1" class="row items-center">
|
|
<q-img src="/icons/favicon-512x512.png" width="4rem" />
|
|
<span class="column text-h6 text-bold q-ml-md">
|
|
{{ $t('quotation.title') }}
|
|
<span class="text-caption text-regular app-text-muted">
|
|
{{
|
|
$t('quotation.processOn', {
|
|
msg: `${dateFormat(date, true)} ${dateFormat(date, true, true)}`,
|
|
})
|
|
}}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
<FormAbout
|
|
class="col-4"
|
|
input-only
|
|
v-model:branch-id="branchId"
|
|
v-model:customer-branch-id="quotationFormData.customerBranchId"
|
|
@add-customer="triggerSelectTypeCustomerd()"
|
|
/>
|
|
</header>
|
|
|
|
<article
|
|
class="row col"
|
|
style="flex-grow: 1; overflow-y: hidden"
|
|
:style="{
|
|
overflowY: $q.screen.gt.sm ? 'hidden' : 'auto',
|
|
}"
|
|
>
|
|
<section
|
|
class="col-12 col-sm-9 row"
|
|
style="padding: var(--size-3); overflow-y: auto"
|
|
:style="{
|
|
paddingRight: $q.screen.gt.sm ? 'var(--size-2)' : 'var(--size-3)',
|
|
maxHeight: $q.screen.gt.sm ? '100%' : undefined,
|
|
}"
|
|
>
|
|
<div class="col q-gutter-y-md">
|
|
<q-expansion-item
|
|
for="item-up"
|
|
id="item-up"
|
|
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-sm">
|
|
<span
|
|
class="text-weight-medium q-mr-md"
|
|
style="font-size: 18px"
|
|
>
|
|
{{ $t('quotation.employeeList') }}
|
|
</span>
|
|
<ToggleButton class="q-mr-sm" v-model="toggleWorker" />
|
|
{{
|
|
toggleWorker
|
|
? $t('general.specify')
|
|
: $t('general.noSpecify')
|
|
}}
|
|
</div>
|
|
<nav class="q-ml-auto">
|
|
<AddButton
|
|
icon-only
|
|
@click.stop="triggerSelectEmployeeDialog"
|
|
/>
|
|
</nav>
|
|
</section>
|
|
</template>
|
|
|
|
<div class="surface-1 q-pa-md full-width">
|
|
<WorkerItem
|
|
:employee-amount="selectedWorker.length"
|
|
fallback-img="/images/employee-avatar.png"
|
|
:rows="
|
|
selectedWorker.map((e: Employee) => ({
|
|
foreignRefNo: '123456789',
|
|
employeeName:
|
|
$i18n.locale === 'eng'
|
|
? `${e.firstNameEN} ${e.lastNameEN}`
|
|
: `${e.firstName} ${e.lastName}`,
|
|
birthDate: dateFormat(e.dateOfBirth),
|
|
gender: e.gender,
|
|
age: calculateAge(e.dateOfBirth),
|
|
nationality: optionStore.mapOption(e.nationality),
|
|
documentExpireDate: '1234',
|
|
imgUrl: `${baseUrl}/customer/${e.id}/image/${e.selectedImage}`,
|
|
status: e.status,
|
|
}))
|
|
"
|
|
/>
|
|
</div>
|
|
</q-expansion-item>
|
|
|
|
<q-expansion-item
|
|
for="item-up"
|
|
id="item-up"
|
|
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-sm">
|
|
<span class="text-weight-medium" style="font-size: 18px">
|
|
{{ $t('quotation.productList') }}
|
|
</span>
|
|
</div>
|
|
<nav class="q-ml-auto">
|
|
<AddButton
|
|
icon-only
|
|
class="q-ml-auto"
|
|
@click.stop="triggerProductServiceDialog"
|
|
/>
|
|
</nav>
|
|
</section>
|
|
</template>
|
|
<div class="surface-1 q-pa-md full-width">
|
|
<ProductItem
|
|
@delete="toggleDeleteProduct"
|
|
v-model:rows="productServiceList"
|
|
/>
|
|
</div>
|
|
</q-expansion-item>
|
|
|
|
<q-expansion-item
|
|
for="item-up"
|
|
id="item-up"
|
|
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>
|
|
<div class="row full-width items-center q-pr-md q-py-sm">
|
|
<span class="text-weight-medium" style="font-size: 18px">
|
|
{{ $t('general.remark') }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
<div class="surface-1 q-pa-md full-width">
|
|
<q-editor
|
|
dense
|
|
:model-value="''"
|
|
min-height="5rem"
|
|
class="full-width"
|
|
toolbar-bg="input-border"
|
|
style="cursor: auto; color: var(--foreground)"
|
|
:flat="!readonly"
|
|
:readonly="readonly"
|
|
:style="`width: ${$q.screen.gt.xs ? '100%' : '63vw'}`"
|
|
:toolbar="[['left', 'center', 'justify'], ['clip']]"
|
|
:toolbar-toggle-color="readonly ? 'disabled' : 'primary'"
|
|
:toolbar-color="
|
|
readonly ? 'disabled' : $q.dark.isActive ? 'white' : ''
|
|
"
|
|
:definitions="{
|
|
clip: {
|
|
icon: 'mdi-paperclip',
|
|
tip: 'Upload',
|
|
handler: () => console.log('upload'),
|
|
},
|
|
}"
|
|
/>
|
|
</div>
|
|
</q-expansion-item>
|
|
</div>
|
|
</section>
|
|
|
|
<div
|
|
class="col-12 col-sm-3"
|
|
style="padding: var(--size-3); overflow-y: auto"
|
|
:style="{
|
|
paddingLeft: $q.screen.gt.sm ? 'var(--size-1)' : 'var(--size-3)',
|
|
maxHeight: $q.screen.gt.sm ? '100%' : undefined,
|
|
}"
|
|
>
|
|
<QuotationFormInfo
|
|
v-model:quotation-no="quotationNo"
|
|
v-model:actor="actor"
|
|
v-model:work-name="workName"
|
|
v-model:contactor="contactor"
|
|
v-model:telephone="telephone"
|
|
v-model:document-receive-point="documentReceivePoint"
|
|
v-model:due-date="dueDate"
|
|
v-model:pay-type="payType"
|
|
v-model:pay-bank="payBank"
|
|
v-model:pay-split-count="paySplitCount"
|
|
:readonly
|
|
/>
|
|
</div>
|
|
</article>
|
|
|
|
<footer class="surface-1 q-pa-md">
|
|
<div class="row full-width justify-between">
|
|
<q-btn dense outline color="primary" class="rounded" padding="2px 8px">
|
|
<q-icon name="mdi-play-box-outline" size="xs" class="q-mr-xs" />
|
|
{{ $t('general.view', { msg: $t('general.example') }) }}
|
|
</q-btn>
|
|
<SaveButton solid />
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- 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);
|
|
}
|
|
"
|
|
>
|
|
<section class="col row scroll">
|
|
<SelectZone
|
|
ref="refSelectZoneEmployee"
|
|
v-model:selected-item="preSelectedWorker"
|
|
:items="workerList"
|
|
>
|
|
<template #top>
|
|
<AddButton
|
|
icon-only
|
|
@click="
|
|
() => {
|
|
triggerCreateEmployee();
|
|
}
|
|
"
|
|
/>
|
|
</template>
|
|
<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.code,
|
|
female: item.gender === 'female',
|
|
male: item.gender === 'male',
|
|
img: `${baseUrl}/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>
|
|
|
|
<!-- add product service -->
|
|
<ProductServiceForm
|
|
v-model="pageState.productServiceModal"
|
|
v-model:product-group="productGroup"
|
|
v-model:product-list="productList"
|
|
v-model:service-list="serviceList"
|
|
@submit="convertToTable"
|
|
@select-group="
|
|
async (id) => {
|
|
await getAllService(id);
|
|
await getAllProduct(id);
|
|
}
|
|
"
|
|
></ProductServiceForm>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.worker-list > :deep(*:not(:last-child)) {
|
|
margin-bottom: var(--size-2);
|
|
}
|
|
|
|
.icon-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
aspect-ratio: 1/1;
|
|
font-size: 1.5rem;
|
|
padding: var(--size-1);
|
|
border-radius: var(--radius-2);
|
|
}
|
|
|
|
.bg-color-orange {
|
|
--_color: var(--yellow-7-hsl);
|
|
color: white;
|
|
background: hsla(var(--_color));
|
|
}
|
|
|
|
.dark .bg-color-orange {
|
|
--_color: var(--orange-6-hsl);
|
|
}
|
|
|
|
.bg-color-orange-light {
|
|
--_color: var(--yellow-7-hsl);
|
|
background: hsla(var(--_color) / 0.2);
|
|
}
|
|
.dark .bg-color-orange {
|
|
--_color: var(--orange-6-hsl / 0.2);
|
|
}
|
|
|
|
.color-bar {
|
|
width: 100%;
|
|
height: 1vh;
|
|
background: linear-gradient(
|
|
90deg,
|
|
rgba(245, 159, 0, 1) 0%,
|
|
rgba(255, 255, 255, 1) 77%,
|
|
rgba(204, 204, 204, 1) 100%
|
|
);
|
|
display: flex;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.blue-segment {
|
|
background-color: var(--yellow-7);
|
|
flex-grow: 4;
|
|
}
|
|
|
|
.orange-segment {
|
|
background-color: hsla(var(--yellow-7-hsl) / 0.2);
|
|
flex-grow: 0.5;
|
|
}
|
|
|
|
.gray-segment {
|
|
background-color: #ccc;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.blue-segment,
|
|
.orange-segment,
|
|
.gray-segment {
|
|
transform: skewX(-60deg);
|
|
}
|
|
|
|
:deep(i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon) {
|
|
color: var(--brand-1);
|
|
}
|
|
|
|
:deep(.q-editor__toolbar-group):nth-child(2) {
|
|
margin-left: auto !important;
|
|
}
|
|
|
|
:deep(.q-editor__toolbar.row.no-wrap.scroll-x) {
|
|
background-color: var(--surface-2) !important;
|
|
}
|
|
|
|
:deep(.q-editor__toolbar) {
|
|
border-color: var(--surface-3) !important;
|
|
}
|
|
|
|
:deep(
|
|
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.surface-1
|
|
.q-focus-helper
|
|
) {
|
|
visibility: hidden;
|
|
}
|
|
</style>
|