jws-frontend/src/pages/05_quotation/QuotationForm.vue

2428 lines
74 KiB
Vue
Raw Normal View History

2024-06-19 15:43:58 +07:00
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
2024-10-04 14:51:31 +07:00
import { useQuasar } from 'quasar';
2024-10-11 11:54:22 +07:00
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
2024-10-29 11:56:02 +07:00
import {
dialogCheckData,
dialogWarningClose,
formatNumberDecimal,
} from 'stores/utils';
import { ProductTree, quotationProductTree } from './utils';
2024-10-04 14:51:31 +07:00
// NOTE: Import stores
import { dateFormat, calculateAge, dateFormatJS } from 'src/utils/datetime';
2024-10-04 17:01:24 +07:00
import { useEmployeeForm } from 'src/pages/03_customer-management/form';
import { useQuotationStore } from 'src/stores/quotations';
2024-10-04 14:51:31 +07:00
import useProductServiceStore from 'stores/product-service';
2024-12-17 14:38:38 +07:00
import { waitAll, calculateDaysUntilExpire, dialog } from 'src/stores/utils';
2024-11-12 16:01:04 +07:00
import useEmployeeStore from 'stores/employee';
import { useInvoice, useReceipt, usePayment } from 'stores/payment';
2024-10-04 14:51:31 +07:00
import useCustomerStore from 'stores/customer';
import useOptionStore from 'stores/options';
import { useQuotationForm } from './form';
import { deleteItem } from 'stores/utils';
2024-10-04 14:51:31 +07:00
// NOTE Import Types
import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
2024-11-08 10:42:27 +07:00
import { View } from './types.ts';
import {
2024-12-20 15:56:23 +07:00
EmployeeWorker,
PayCondition,
ProductServiceList,
QuotationPayload,
} from 'src/stores/quotations/types';
2024-12-20 15:56:23 +07:00
import { Employee, EmployeeWork } from 'src/stores/employee/types';
import { Receipt } from 'src/stores/payment/types';
import {
ProductGroup,
2024-10-04 14:57:08 +07:00
Product,
Service,
} from 'src/stores/product-service/types';
2024-06-19 15:43:58 +07:00
2024-10-04 14:51:31 +07:00
// NOTE: Import Components
2024-12-18 17:55:45 +07:00
import TableRequest from './TableRequest.vue';
2024-11-06 17:39:02 +07:00
import SelectInput from 'components/shared/SelectInput.vue';
2024-10-28 10:33:17 +07:00
import SwitchItem from 'components/shared/SwitchItem.vue';
2024-10-04 14:51:31 +07:00
import ProductItem from 'components/05_quotation/ProductItem.vue';
2024-07-25 09:27:58 +07:00
import WorkerItem from 'components/05_quotation/WorkerItem.vue';
2024-10-04 14:51:31 +07:00
import ToggleButton from 'components/button/ToggleButton.vue';
import FormAbout from 'components/05_quotation/FormAbout.vue';
import SelectZone from 'components/shared/SelectZone.vue';
import ImportWorker from './ImportWorker.vue';
2024-10-10 17:49:28 +07:00
import {
AddButton,
SaveButton,
EditButton,
UndoButton,
2024-10-18 13:43:20 +07:00
CloseButton,
2024-10-18 09:37:34 +07:00
MainButton,
2024-10-10 17:49:28 +07:00
} from 'components/button';
import QuotationFormReceipt from './QuotationFormReceipt.vue';
2024-10-30 08:59:20 +07:00
import QuotationFormProductSelect from './QuotationFormProductSelect.vue';
2024-10-04 14:51:31 +07:00
import QuotationFormInfo from './QuotationFormInfo.vue';
2024-12-20 09:46:19 +07:00
import QuotationFormWorkerSelect from './QuotationFormWorkerSelect.vue';
import QuotationFormWorkerAddDialog from './QuotationFormWorkerAddDialog.vue';
import UploadFileSection from 'src/components/upload-file/UploadFileSection.vue';
2024-10-29 11:06:13 +07:00
import { columnPaySplit } from './constants';
2024-10-10 09:24:02 +07:00
import { precisionRound } from 'src/utils/arithmetic';
import { useConfigStore } from 'src/stores/config';
import { useRequestList } from 'src/stores/request-list';
2024-10-21 15:06:14 +07:00
import QuotationFormMetadata from './QuotationFormMetadata.vue';
2024-10-28 10:17:12 +07:00
import BadgeComponent from 'src/components/BadgeComponent.vue';
2024-10-28 16:44:14 +07:00
import PaymentForm from './PaymentForm.vue';
import { api } from 'src/boot/axios';
import { RouterLink, useRoute } from 'vue-router';
import { initLang, initTheme, Lang } from 'src/utils/ui';
2024-11-27 14:27:02 +07:00
import { convertTemplate } from 'src/utils/string-template';
2024-10-03 11:14:12 +07:00
type Node = {
[key: string]: any;
opened?: boolean;
checked?: boolean;
bg?: string;
fg?: string;
icon?: string;
children?: Node[];
};
2024-09-18 15:38:50 +07:00
2024-10-04 14:51:31 +07:00
type ProductGroupId = string;
2024-06-19 15:43:58 +07:00
2024-12-17 14:38:38 +07:00
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
2024-11-12 16:01:04 +07:00
const employeeStore = useEmployeeStore();
const route = useRoute();
const useReceiptStore = useReceipt();
2024-10-10 09:24:02 +07:00
const configStore = useConfigStore();
2024-10-03 11:14:12 +07:00
const productServiceStore = useProductServiceStore();
2024-10-04 14:51:31 +07:00
const customerStore = useCustomerStore();
2024-10-03 11:14:12 +07:00
const quotationForm = useQuotationForm();
const quotationStore = useQuotationStore();
const invoiceStore = useInvoice();
const paymentStore = usePayment();
2024-10-04 14:51:31 +07:00
const optionStore = useOptionStore();
const requestStore = useRequestList();
2024-10-18 13:57:26 +07:00
const { t, locale } = useI18n();
2024-09-30 16:36:18 +07:00
const $q = useQuasar();
const openQuotation = ref<boolean>(false);
2024-12-20 17:42:50 +07:00
const formMetadata = ref();
2024-09-30 16:36:18 +07:00
const rowsRequestList = ref<RequestData[]>([]);
2024-10-07 16:57:44 +07:00
const {
currentFormData: quotationFormData,
currentFormState: quotationFormState,
2024-10-29 18:02:20 +07:00
invoicePayload: invoiceFormData,
2024-10-08 13:45:06 +07:00
newWorkerList,
fileItemNewWorker,
quotationFull,
2024-10-07 16:57:44 +07:00
} = storeToRefs(quotationForm);
2024-10-04 14:51:31 +07:00
2024-10-10 09:24:02 +07:00
const { data: config } = storeToRefs(configStore);
const receiptList = ref<Receipt[]>([]);
2024-11-06 17:39:02 +07:00
const templateForm = ref<string>('');
const templateFormOption = ref<{ label: string; value: string }[]>([]);
2024-06-19 15:43:58 +07:00
const toggleWorker = ref(true);
const tempTableProduct = ref<ProductServiceList[]>([]);
2024-12-19 09:55:47 +07:00
const tempPaySplitCount = ref(0);
const tempPaySplit = ref<
{ no: number; amount: number; name?: string; invoice?: boolean }[]
>([]);
const currentQuotationId = ref<string | undefined>(undefined);
2024-10-04 14:51:31 +07:00
const date = ref();
2024-10-10 13:31:46 +07:00
const readonly = computed(() => {
return !(
quotationFormState.value.mode === 'create' ||
quotationFormState.value.mode === 'edit'
);
});
2024-10-08 13:45:06 +07:00
const selectedWorker = ref<
(Employee & {
attachment?: {
name?: string;
group?: string;
url?: string;
file?: File;
_meta?: Record<string, any>;
}[];
})[]
>([]);
2024-12-20 15:56:23 +07:00
const selectedWorkerItem = computed(() => {
return [
...selectedWorker.value.map((e) => ({
foreignRefNo: e.employeePassport
? e.employeePassport[0]?.number || '-'
: '-',
2024-12-20 15:56:23 +07:00
employeeName:
locale.value === Lang.English
? `${e.firstNameEN} ${e.lastNameEN}`
: `${e.firstName} ${e.lastName}`,
birthDate: dateFormatJS({ date: e.dateOfBirth }),
gender: e.gender,
age: calculateAge(e.dateOfBirth),
nationality: optionStore.mapOption(e.nationality),
documentExpireDate:
e.employeePassport !== undefined &&
e.employeePassport[0]?.expireDate !== undefined
? dateFormatJS({ date: e.employeePassport[0]?.expireDate })
: '-',
imgUrl: e.selectedImage
? `${API_BASE_URL}/employee/${e.id}/image/${e.selectedImage}`
: '',
status: e.status,
})),
...newWorkerList.value.map((v: any) => ({
foreignRefNo: v.passportNo,
employeeName:
locale.value === Lang.English
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName} ${v.lastName}`,
birthDate: dateFormatJS({ date: v.dateOfBirth }),
gender: v.gender,
age: calculateAge(v.dateOfBirth),
nationality: optionStore.mapOption(v.nationality),
documentExpireDate: '-',
imgUrl: '',
status: 'CREATED',
})),
];
});
2024-10-07 12:55:39 +07:00
const workerList = ref<Employee[]>([]);
2024-12-17 13:20:19 +07:00
2024-10-08 16:38:15 +07:00
const selectedProductGroup = ref('');
2024-10-28 15:12:07 +07:00
const selectedInstallmentNo = ref<number[]>([]);
const installmentAmount = ref<number>(0);
2024-10-29 18:02:20 +07:00
const selectedInstallment = ref();
const agentPrice = ref(false);
const attachmentData = ref<
{
name: string;
progress: number;
loaded: number;
total: number;
url?: string;
}[]
>([]);
2024-11-27 15:52:04 +07:00
const getToolbarConfig = computed(() => {
const toolbar = [['left', 'center', 'justify'], ['toggle'], ['clip']];
if (readonly.value) {
return toolbar.filter((item) => !item.includes('toggle'));
}
return toolbar;
});
function getPrice(
list: typeof productServiceList.value,
filterHook?: (
item: (typeof productServiceList.value)[number],
index?: number,
) => boolean,
) {
if (filterHook) list = list.filter(filterHook);
return list.reduce(
2024-10-10 09:24:02 +07:00
(a, c) => {
2024-10-28 15:12:07 +07:00
if (
selectedInstallmentNo.value.length > 0 &&
c.installmentNo &&
!selectedInstallmentNo.value.includes(c.installmentNo)
) {
return a;
}
2024-10-10 09:24:02 +07:00
const price = precisionRound(c.pricePerUnit * c.amount);
const vat =
precisionRound(
(c.pricePerUnit * (c.discount ? c.amount : 1) - c.discount) *
(config.value?.vat || 0.07),
) * (!c.discount ? c.amount : 1);
2024-10-10 09:24:02 +07:00
a.totalPrice = precisionRound(a.totalPrice + price);
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
2024-10-17 15:50:26 +07:00
a.vat = c.product.calcVat ? precisionRound(a.vat + vat) : a.vat;
a.vatExcluded = c.product.calcVat
? a.vatExcluded
: precisionRound(a.vat + vat);
2024-10-10 09:24:02 +07:00
a.finalPrice = precisionRound(
2024-10-10 15:05:15 +07:00
a.totalPrice -
a.totalDiscount +
a.vat -
Number(quotationFormData.value.discount || 0),
2024-10-10 09:24:02 +07:00
);
return a;
},
{
totalPrice: 0,
totalDiscount: 0,
vat: 0,
2024-10-17 15:50:26 +07:00
vatExcluded: 0,
2024-10-10 09:24:02 +07:00
finalPrice: 0,
},
);
}
const summaryPrice = computed(() => getPrice(productServiceList.value));
2024-09-18 15:38:50 +07:00
const payBank = ref('');
2024-09-30 16:36:18 +07:00
2024-10-03 11:14:12 +07:00
const pageState = reactive({
hideStat: false,
inputSearch: '',
statusFilter: 'all',
fieldSelected: [],
gridView: false,
isLoaded: false,
importWorker: false,
2024-10-03 11:14:12 +07:00
currentTab: 'all',
addModal: false,
quotationModal: false,
employeeModal: false,
productServiceModal: false,
2024-11-27 15:02:35 +07:00
remarkWrite: true,
2024-10-03 11:14:12 +07:00
});
2024-10-04 14:57:08 +07:00
const productList = ref<Partial<Record<ProductGroupId, Product[]>>>({});
2024-10-03 11:14:12 +07:00
const serviceList = ref<Partial<Record<ProductGroupId, Service[]>>>({});
2024-10-04 14:51:31 +07:00
const productGroup = ref<ProductGroup[]>([]);
2024-10-03 11:14:12 +07:00
const selectedGroupSub = ref<'product' | 'service' | null>(null);
2024-10-04 17:01:24 +07:00
const productServiceList = ref<
Required<QuotationPayload['productServiceList'][number]>[]
>([]);
2024-10-30 11:54:49 +07:00
const productService = computed(() => {
return selectedInstallmentNo.value.length > 0
? productServiceList.value.filter((v) => {
return (
v.installmentNo &&
selectedInstallmentNo.value.includes(v.installmentNo)
);
})
: productServiceList.value;
});
2024-11-26 14:01:09 +07:00
function isIssueInvoice() {
return quotationFormData.value.paySplit.some(
2024-11-27 10:46:15 +07:00
(value) =>
selectedInstallmentNo.value.includes(value.no) &&
value.invoiceId !== undefined,
2024-11-26 14:01:09 +07:00
);
}
2024-10-29 18:02:20 +07:00
async function fetchStatus() {
statusQuotationForm.value = [
{
2024-11-07 15:12:06 +07:00
title: 'Issued',
2024-11-01 10:55:20 +07:00
status: getStatus(quotationFormData.value.quotationStatus, 0, -1),
active: () => view.value === View.Quotation,
handler: () => (
(view.value = View.Quotation),
(code.value = ''),
(selectedInstallmentNo.value = []),
(selectedInstallment.value = [])
),
2024-10-29 18:02:20 +07:00
},
{
2024-11-07 15:12:06 +07:00
title: 'Accepted',
2024-11-01 10:55:20 +07:00
status: getStatus(quotationFormData.value.quotationStatus, 1, 0),
active: () => view.value === View.Accepted,
handler: () => ((view.value = View.Accepted), (code.value = '')),
2024-10-29 18:02:20 +07:00
},
{
2024-11-07 15:12:06 +07:00
title: 'Invoice',
2024-11-01 17:36:13 +07:00
status:
quotationFormData.value.payCondition === 'Full'
? getStatus(quotationFormData.value.quotationStatus, 3, 1)
: getStatus(quotationFormData.value.quotationStatus, 4, 1),
active: () =>
view.value === View.Invoice || view.value === View.InvoicePre,
2024-10-29 18:02:20 +07:00
handler: () => {
2024-11-27 11:49:28 +07:00
selectedInstallmentNo.value = [];
selectedInstallment.value = [];
2024-10-29 18:02:20 +07:00
view.value =
quotationFormData.value.payCondition === 'Full' ||
quotationFormData.value.payCondition === 'BillFull'
? View.Invoice
: View.InvoicePre;
if (
quotationFormData.value.payCondition === 'Full' ||
quotationFormData.value.payCondition === 'BillFull'
) {
getInvoiceCodeFullPay();
}
2024-10-29 18:02:20 +07:00
},
},
{
2024-11-07 15:12:06 +07:00
title: 'PaymentInProcess',
2024-10-29 18:02:20 +07:00
status: getStatus(quotationFormData.value.quotationStatus, 4, 1),
active: () =>
view.value === View.Payment || view.value === View.PaymentPre,
2024-10-29 18:02:20 +07:00
handler: () => {
view.value =
quotationFormData.value.payCondition === 'Full' ||
quotationFormData.value.payCondition === 'BillFull'
? View.Payment
: View.PaymentPre;
},
},
{
2024-11-07 15:12:06 +07:00
title: 'Receipt',
2024-10-29 18:02:20 +07:00
status: getStatus(quotationFormData.value.quotationStatus, 4, 1),
active: () => view.value === View.Receipt,
2024-10-29 18:02:20 +07:00
handler: () => {
fetchReceipt();
2024-11-01 09:12:27 +07:00
view.value = View.Receipt;
code.value = '';
2024-10-29 18:02:20 +07:00
},
},
{
2024-11-07 15:12:06 +07:00
title: 'ProcessComplete',
2024-10-29 18:02:20 +07:00
status: getStatus(quotationFormData.value.quotationStatus, 5, 4),
active: () => view.value === View.Complete,
handler: async () => {
2024-10-29 18:02:20 +07:00
view.value = View.Complete;
await fetchRequest();
2024-10-29 18:02:20 +07:00
},
},
];
}
async function fetchRequest() {
const res = await requestStore.getRequestDataList({
query: quotationFormState.value.inputSearchRequest,
page: 1,
pageSize: 999999,
requestDataStatus:
quotationFormState.value.statusFilterRequest === 'All'
? undefined
: quotationFormState.value.statusFilterRequest,
quotationId: quotationFormData.value.id,
});
if (res) {
rowsRequestList.value = res.result;
}
}
2024-10-29 18:02:20 +07:00
async function fetchQuotation() {
if (
2024-10-30 10:24:24 +07:00
(!!currentQuotationId.value || !!quotationFormData.value.id) &&
2024-10-29 18:02:20 +07:00
quotationFormState.value.mode &&
quotationFormState.value.mode !== 'create'
) {
2024-10-30 10:24:24 +07:00
const id = currentQuotationId.value || quotationFormData.value.id || '';
await quotationForm.assignFormData(id, quotationFormState.value.mode);
2024-12-19 09:55:47 +07:00
tempPaySplitCount.value = quotationFormData.value.paySplitCount || 0;
tempPaySplit.value = JSON.parse(
JSON.stringify(quotationFormData.value.paySplit),
);
2024-10-29 18:02:20 +07:00
}
assignWorkerToSelectedWorker();
2024-10-29 18:02:20 +07:00
await assignToProductServiceList();
await fetchStatus();
}
async function fetchReceipt() {
const res = await useReceiptStore.getReceiptList({
quotationId: quotationFormData.value.id,
});
if (res) {
receiptList.value = res.result;
}
}
2024-10-18 14:33:27 +07:00
async function closeTab() {
2024-10-18 13:30:40 +07:00
if (quotationFormState.value.mode === 'edit') {
quotationForm.resetForm();
2024-10-18 14:33:27 +07:00
await assignToProductServiceList();
2024-10-18 13:30:40 +07:00
} else {
2024-10-18 13:57:26 +07:00
dialogWarningClose(t, {
message: t('dialog.message.close'),
action: () => {
window.close();
},
cancel: () => {},
});
2024-10-18 13:30:40 +07:00
}
2024-10-18 09:10:33 +07:00
}
2024-10-18 13:34:45 +07:00
function closeAble() {
return window.opener !== null;
}
async function assignToProductServiceList() {
2024-10-15 16:13:00 +07:00
const ret = await productServiceStore.fetchProductGroup({
page: 1,
pageSize: 9999,
});
2024-10-15 17:16:36 +07:00
if (ret) {
productGroup.value = ret.result;
productServiceList.value = quotationFormData.value.productServiceList.map(
(v) => ({
id: v.id!,
2024-10-28 10:33:17 +07:00
installmentNo: v.installmentNo || 0,
workerIndex: v.workerIndex || [0],
vat: v.vat || 0,
pricePerUnit: v.pricePerUnit || 0,
discount: v.discount || 0,
2024-10-11 11:54:22 +07:00
amount: v.amount || 0,
product: v.product,
work: v.work || null,
service: v.service || null,
}),
);
selectedProductGroup.value =
2024-10-10 17:49:28 +07:00
quotationFormData.value.productServiceList[0]?.product.productGroup?.id ||
'';
}
}
2024-10-28 09:43:33 +07:00
async function submitAccepted() {
2024-10-28 09:05:26 +07:00
dialog({
color: 'info',
icon: 'mdi-account-check',
title: t('dialog.title.confirmQuotationAccept'),
actionText: t('general.confirm'),
persistent: true,
message: t('dialog.message.quotationAccept'),
action: async () => {
if (!quotationFormData.value.id) return;
2024-10-29 18:02:20 +07:00
const res = await quotationForm.accepted(quotationFormData.value.id);
if (res) {
await fetchQuotation();
}
2024-10-28 09:05:26 +07:00
},
cancel: () => {},
});
}
2024-10-29 18:02:20 +07:00
async function convertInvoiceToSubmit() {
if (quotationFormData.value.id) {
invoiceFormData.value = {
installmentNo: selectedInstallmentNo.value,
amount:
quotationFormData.value.payCondition === 'SplitCustom'
? installmentAmount.value
: summaryPrice.value.finalPrice,
2024-10-29 18:02:20 +07:00
quotationId: quotationFormData.value.id,
};
const res = await quotationForm.submitInvoice();
2024-10-30 10:24:24 +07:00
if (res) {
await fetchQuotation();
view.value = View.Payment;
}
2024-10-29 18:02:20 +07:00
}
}
async function convertDataToFormSubmit() {
2024-10-07 16:57:44 +07:00
quotationFormData.value.productServiceList = JSON.parse(
JSON.stringify(
2024-10-08 13:45:06 +07:00
productServiceList.value.map((v) => ({
2024-10-29 18:02:20 +07:00
installmentNo: v.installmentNo,
2024-10-10 18:16:04 +07:00
workerIndex: v.workerIndex,
discount: v.discount,
amount: v.amount,
2024-10-07 16:57:44 +07:00
product: v.product,
work: v.work,
service: v.service,
})),
),
);
2024-10-08 13:45:06 +07:00
selectedWorker.value.forEach((v, i) => {
if (v.attachment !== undefined) {
v.attachment.forEach((value) => {
fileItemNewWorker.value.push({
employeeIndex: i,
name: value.name,
group: value.group as 'passport' | 'visa' | 'in-country-notice',
url: value.url,
file: value.file,
_meta: value._meta,
});
});
}
});
2024-10-07 16:57:44 +07:00
quotationFormData.value.worker = JSON.parse(
2024-12-20 15:56:23 +07:00
JSON.stringify([
...selectedWorker.value.map((v) => {
{
2024-10-07 16:57:44 +07:00
return v.id;
}
}),
2024-12-20 15:56:23 +07:00
...newWorkerList.value.map((v) => {
const { attachment, ...payload } = v;
return payload;
}),
]),
2024-10-07 16:57:44 +07:00
);
2024-10-17 14:51:42 +07:00
quotationFormData.value.paySplit = JSON.parse(
JSON.stringify(
quotationFormData.value.paySplit.map((p) => ({ ...p, no: undefined })),
),
);
quotationFormData.value = {
id: quotationFormData.value.id,
workerMax: quotationFormData.value.workerMax,
productServiceList: quotationFormData.value.productServiceList,
urgent: quotationFormData.value.urgent,
customerBranchId: quotationFormData.value.customerBranchId,
2024-10-15 17:16:36 +07:00
registeredBranchId: quotationFormData.value.registeredBranchId,
worker: quotationFormData.value.worker,
payBillDate: quotationFormData.value.payBillDate,
paySplit: quotationFormData.value.paySplit,
paySplitCount: quotationFormData.value.paySplitCount,
payCondition: quotationFormData.value.payCondition,
dueDate: quotationFormData.value.dueDate,
contactTel: quotationFormData.value.contactTel,
contactName: quotationFormData.value.contactName,
workName: quotationFormData.value.workName,
_count: quotationFormData.value._count,
status: quotationFormData.value.status,
2024-10-18 10:40:34 +07:00
discount: quotationFormData.value.discount,
2024-10-18 14:10:03 +07:00
remark: quotationFormData.value.remark || '',
};
2024-12-20 15:56:23 +07:00
newWorkerList.value = [];
const res = await quotationForm.submitQuotation();
if (res === true) {
quotationFormState.value.mode = 'info';
localStorage.setItem(
'new-quotation',
JSON.stringify({
quotationId: quotationFormData.value.id,
customerBranchId: quotationFormData.value.customerBranchId,
branchId: quotationFormData.value.registeredBranchId,
agentPrice: agentPrice.value,
statusDialog: 'info',
}),
);
2024-10-29 18:02:20 +07:00
await fetchQuotation();
}
2024-10-07 16:57:44 +07:00
}
2024-10-03 11:14:12 +07:00
async function getAllProduct(
groupId: string,
2024-10-15 18:01:11 +07:00
opts?: { force?: boolean; page?: number; pageSize?: number; query?: string },
2024-10-03 11:14:12 +07:00
) {
selectedGroupSub.value = 'product';
if (!opts?.force && productList.value[groupId] !== undefined) return;
const ret = await productServiceStore.fetchListProduct({
page: opts?.page ?? 1,
pageSize: opts?.pageSize ?? 9999,
2024-10-15 17:16:36 +07:00
query: opts?.query,
2024-10-03 11:14:12 +07:00
productGroupId: groupId,
activeOnly: true,
2024-10-03 11:14:12 +07:00
});
if (ret) productList.value[groupId] = ret.result;
}
async function getAllService(
groupId: string,
2024-10-15 18:01:11 +07:00
opts?: { force?: boolean; page?: number; pageSize?: number; query?: string },
2024-10-03 11:14:12 +07:00
) {
selectedGroupSub.value = 'service';
2024-10-15 18:01:11 +07:00
2024-10-03 11:14:12 +07:00
if (!opts?.force && serviceList.value[groupId] !== undefined) return;
2024-10-15 18:01:11 +07:00
2024-10-03 11:14:12 +07:00
const ret = await productServiceStore.fetchListService({
page: opts?.page ?? 1,
pageSize: opts?.pageSize ?? 9999,
productGroupId: groupId,
2024-10-15 17:16:36 +07:00
query: opts?.query,
2024-10-03 11:14:12 +07:00
fullDetail: true,
});
if (ret) serviceList.value[groupId] = ret.result;
}
async function triggerSelectEmployeeDialog() {
2024-10-03 11:14:12 +07:00
pageState.employeeModal = true;
await nextTick();
2024-10-03 11:14:12 +07:00
}
function triggerProductServiceDialog() {
pageState.productServiceModal = true;
}
function handleChangePayType(type: PayCondition) {
if (type === 'Full') {
quotationFormData.value.paySplitCount = 0;
}
if (
type === 'Split' &&
2024-12-19 09:55:47 +07:00
tempPaySplitCount.value !== undefined &&
tempPaySplit.value &&
tempTableProduct.value
) {
quotationFormData.value.paySplitCount = tempPaySplitCount.value;
quotationFormData.value.paySplit = JSON.parse(
JSON.stringify(tempPaySplit.value),
);
productServiceList.value = productServiceList.value.map((item, index) => ({
...item,
installmentNo: tempTableProduct.value[index].installmentNo || 0,
}));
}
2024-12-19 09:55:47 +07:00
if (type === 'SplitCustom') {
2024-12-23 11:28:04 +07:00
quotationFormData.value.paySplitCount = tempPaySplitCount.value || 1;
if (tempPaySplit.value.length === 0) {
quotationFormData.value.paySplit = [];
quotationFormData.value.paySplit.push({ no: Number(1), amount: 0 });
}
2024-12-19 09:55:47 +07:00
}
}
function handleUpdateProductTable(
data: QuotationPayload['productServiceList'][number],
opt?: {
newInstallmentNo: number;
},
) {
// calc price
const calc = (c: QuotationPayload['productServiceList'][number]) => {
const pricePerUnit = c.pricePerUnit || 0;
const discount = c.discount || 0;
return precisionRound(
pricePerUnit * c.amount -
discount +
(c.product.calcVat
? (pricePerUnit * c.amount - discount) * (config.value?.vat || 0.07)
: 0),
);
};
// installment
if (opt && opt.newInstallmentNo) {
const oldInstallmentNo = JSON.parse(JSON.stringify(data.installmentNo));
const oldPaySplit = quotationFormData.value.paySplit.find(
(v) => v.no === oldInstallmentNo,
);
const newPaySplit = quotationFormData.value.paySplit.find(
(v) => v.no === opt.newInstallmentNo,
);
if (oldPaySplit) oldPaySplit.amount = oldPaySplit.amount - calc(data);
if (newPaySplit) newPaySplit.amount = newPaySplit.amount + calc(data);
return;
}
// re calc price
const currTempPaySplit = tempPaySplit.value.find(
(v) => v.no === data.installmentNo,
);
const currPaySplit = quotationFormData.value.paySplit.find(
(v) => v.no === data.installmentNo,
);
const targetPaySplit = productService.value.filter(
(v) => v.installmentNo === data.installmentNo,
);
let targetPaySplitAmount: number[] = [];
targetPaySplitAmount = targetPaySplit.map((c) => {
return calc(c);
});
const totalSum = targetPaySplitAmount.reduce((sum, value) => sum + value, 0);
if (currTempPaySplit && currPaySplit) {
currTempPaySplit.amount = totalSum;
currPaySplit.amount = totalSum;
}
}
function toggleDeleteProduct(index: number) {
const currProduct = productServiceList.value[index];
let currPaySplit = quotationFormData.value.paySplit.find(
(v) => v.no === currProduct.installmentNo,
);
let currTempPaySplit = tempPaySplit.value.find(
(v) => v.no === currProduct.installmentNo,
);
// cal curr amount
if (currPaySplit && currTempPaySplit) {
const price = agentPrice.value
? currProduct.product.agentPrice
: currProduct.product.price;
const pricePerUnit = currProduct.product.vatIncluded
? precisionRound(price / (1 + (config.value?.vat || 0.07)))
: price;
const vat = precisionRound(
(pricePerUnit * currProduct.amount - currProduct.discount) *
(config.value?.vat || 0.07),
);
const finalPrice = precisionRound(
pricePerUnit * currProduct.amount +
vat -
Number(currProduct.discount || 0),
);
currTempPaySplit.amount = currPaySplit.amount - finalPrice;
currPaySplit.amount = currTempPaySplit.amount;
}
// product display
productServiceList.value.splice(index, 1);
productServiceList.value = [...productServiceList.value];
// find split count
tempPaySplitCount.value = Math.max(
2024-12-19 09:55:47 +07:00
...productServiceList.value.map((v) => v.installmentNo || 0),
);
quotationFormData.value.paySplitCount = tempPaySplitCount.value;
// make split.length === split count
tempPaySplit.value.splice(quotationFormData.value.paySplitCount);
quotationFormData.value.paySplit.splice(
quotationFormData.value.paySplitCount,
);
}
2024-11-28 14:50:22 +07:00
async function assignWorkerToSelectedWorker() {
2024-12-02 17:47:53 +07:00
selectedWorker.value = quotationFormData.value.worker;
}
2024-10-03 11:14:12 +07:00
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 [];
};
const list = nodes.flatMap(_recursive).map((v) => v.value);
2024-10-29 18:02:20 +07:00
quotationFormData.value.paySplitCount = Math.max(
2024-12-19 09:55:47 +07:00
...list.map((v) => v.installmentNo || 0),
2024-10-29 18:02:20 +07:00
);
2024-10-31 13:05:10 +07:00
tempPaySplitCount.value = quotationFormData.value.paySplitCount;
2024-10-29 18:02:20 +07:00
list.forEach((v) => {
v.amount = Math.max(selectedWorker.value.length, 1);
2024-10-18 17:38:33 +07:00
if (!v.workerIndex) v.workerIndex = [];
2024-10-10 17:49:28 +07:00
for (let i = 0; i < selectedWorker.value.length; i++) {
v.workerIndex.push(i);
}
2024-10-29 18:02:20 +07:00
if (!v.installmentNo) {
v.installmentNo = quotationFormData.value.paySplitCount;
}
});
productServiceList.value = list;
tempTableProduct.value = JSON.parse(JSON.stringify(list));
if (nodes.length > 0) {
quotationFormData.value.paySplit = Array.apply(
null,
new Array(quotationFormData.value.paySplitCount),
).map((_, i) => ({
no: i + 1,
amount: getPrice(list, (v) => {
return (
v.installmentNo === i + 1 ||
(i + 1 === quotationFormData.value.paySplitCount && !v.installmentNo)
);
}).finalPrice,
}));
} else {
quotationFormData.value.paySplit = [];
2024-12-19 09:55:47 +07:00
quotationFormData.value.paySplitCount = 0;
quotationFormData.value.payCondition = 'Full';
}
tempPaySplit.value = JSON.parse(
JSON.stringify(quotationFormData.value.paySplit),
);
tempPaySplitCount.value = quotationFormData.value.paySplitCount;
2024-10-04 09:16:40 +07:00
pageState.productServiceModal = false;
2024-10-03 11:14:12 +07:00
}
2024-12-20 09:46:19 +07:00
function convertEmployeeToTable(selected: Employee[]) {
productServiceList.value.forEach((v) => {
if (selectedWorker.value.length === 0 && v.amount === 1) v.amount -= 1;
v.amount = Math.max(
2024-12-20 09:46:19 +07:00
v.amount + selected.length - selectedWorker.value.length,
1,
);
2024-10-10 17:49:28 +07:00
const oldWorkerId: string[] = [];
const newWorkerIndex: number[] = [];
selectedWorker.value.forEach((item, i) => {
if (v.workerIndex.includes(i)) oldWorkerId.push(item.id);
});
2024-12-20 09:46:19 +07:00
selected.forEach((item, i) => {
if (selectedWorker.value.find((n) => item.id === n.id)) return;
newWorkerIndex.push(i);
});
v.workerIndex = oldWorkerId
2024-12-20 09:46:19 +07:00
.map((id) => selected.findIndex((item) => item.id === id))
.filter((idx) => idx !== -1)
.concat(newWorkerIndex);
});
pageState.employeeModal = false;
quotationFormData.value.workerMax = Math.max(
quotationFormData.value.workerMax || 1,
selectedWorker.value.length,
);
}
async function triggerDelete(name: string) {
await quotationStore.delAttachment({
parentId: quotationFormData.value.id || '',
name,
});
await getAttachment();
}
async function getAttachment() {
const attachment = await quotationStore.listAttachment({
parentId: quotationFormData.value.id || '',
});
if (attachment && attachment.length > 0) {
attachmentData.value = attachmentData.value.filter((item) =>
attachment.includes(item.name),
);
attachment.forEach((v) => {
const exists = attachmentData.value.some((item) => item.name === v);
if (!exists) {
attachmentData.value.push({
name: v,
progress: 1,
loaded: 0,
total: 0,
url: `quotation/${quotationFormData.value.id || ''}/attachment/${v}`,
});
}
});
} else attachmentData.value = [];
}
async function uploadAttachment(file?: File) {
if (!file) return;
if (!quotationFormData.value) return;
attachmentData.value.push({
name: file.name,
progress: 0,
loaded: 0,
total: 0,
});
const ret = await quotationStore.putAttachment({
parentId: quotationFormData.value.id || '',
name: file.name,
file: file,
onUploadProgress: (e) => {
attachmentData.value[attachmentData.value.length - 1] = {
name: file.name,
progress: e.progress || 0,
loaded: e.loaded,
total: e.total || 0,
url: `quotation/${quotationFormData.value.id || ''}/attachment/${file.name}`,
};
},
});
if (ret) await getAttachment();
}
2024-10-21 11:13:34 +07:00
const sessionData = ref<Record<string, any>>();
2024-09-30 16:36:18 +07:00
onMounted(async () => {
initTheme();
initLang();
2024-09-30 16:36:18 +07:00
await configStore.getConfig();
2024-10-21 11:13:34 +07:00
sessionStorage.setItem(
'new-quotation',
localStorage.getItem('new-quotation') ||
sessionStorage.getItem('new-quotation') ||
'',
);
2024-10-15 17:16:36 +07:00
2024-10-21 11:13:34 +07:00
localStorage.removeItem('new-quotation');
const payload = sessionStorage.getItem('new-quotation');
if (!!payload) {
const parsed = JSON.parse(payload);
date.value = Date.now();
quotationFormState.value.mode = parsed.statusDialog;
quotationFormData.value.registeredBranchId = parsed.branchId;
quotationFormData.value.customerBranchId = parsed.customerBranchId;
currentQuotationId.value = parsed.quotationId;
agentPrice.value = parsed.agentPrice;
2024-10-29 18:02:20 +07:00
await fetchQuotation();
2024-11-01 11:18:25 +07:00
await assignWorkerToSelectedWorker();
2024-10-21 11:13:34 +07:00
sessionData.value = parsed;
}
2024-10-29 18:02:20 +07:00
await fetchStatus();
if (quotationFormState.value.mode !== 'create') {
await getAttachment();
}
2024-10-28 14:44:56 +07:00
pageState.isLoaded = true;
if (route.query['tab'] === 'invoice') {
view.value =
quotationFormData.value.payCondition === 'Full' ||
quotationFormData.value.payCondition === 'BillFull'
? View.Invoice
: View.InvoicePre;
}
if (route.query['tab'] === 'receipt') {
view.value = View.Receipt;
}
2024-09-30 16:36:18 +07:00
});
2024-10-29 18:02:20 +07:00
watch(
2024-10-30 10:24:24 +07:00
() => quotationFormData.value.quotationStatus,
2024-10-29 18:02:20 +07:00
() => {
fetchStatus();
},
);
watch(
() => quotationFormData.value.customerBranchId,
async (v) => {
if (!v) return;
2024-10-21 11:13:34 +07:00
const retEmp = await customerStore.fetchBranchEmployee(v, {
passport: true,
});
2024-10-21 11:13:34 +07:00
if (retEmp) workerList.value = retEmp.data.result;
},
);
watch(
2024-10-15 17:16:36 +07:00
() => quotationFormData.value.registeredBranchId,
async (v) => {
if (!pageState.isLoaded) return;
const url = new URL(window.location.href);
url.searchParams.set('branchId', v);
history.pushState({}, '', url.toString());
},
);
2024-10-15 17:16:36 +07:00
const productServiceNodes = ref<ProductTree>([]);
watch(
() => productServiceList.value,
() => {
productServiceNodes.value = quotationProductTree(
productServiceList.value,
agentPrice.value,
config.value?.vat,
);
},
);
2024-10-15 17:16:36 +07:00
async function searchEmployee(text: string) {
let query: string | undefined = text;
let pageSize = 50;
if (!text) {
query = undefined;
pageSize = 9999;
}
const retEmp = await customerStore.fetchBranchEmployee(
quotationFormData.value.customerBranchId,
{
query: query,
pageSize: pageSize,
2024-11-26 16:51:15 +07:00
passport: true,
2024-10-15 17:16:36 +07:00
},
);
if (retEmp) workerList.value = retEmp.data.result;
}
2024-10-18 09:27:56 +07:00
function storeDataLocal() {
2024-10-21 12:06:50 +07:00
quotationFormData.value.productServiceList = productServiceList.value;
2024-10-18 09:27:56 +07:00
localStorage.setItem(
'quotation-preview',
2024-10-18 12:32:55 +07:00
JSON.stringify({
2024-10-30 11:54:49 +07:00
data: {
...quotationFormData.value,
productServiceList: productService.value,
},
2024-10-18 12:32:55 +07:00
meta: {
2024-10-21 12:06:50 +07:00
source: {
...quotationFormState.value.source,
code:
quotationFormState.value.mode === 'create'
? '-'
: quotationFormState.value?.source?.code,
createAt:
quotationFormState.value.mode === 'create'
? Date.now()
: quotationFormState.value?.source?.createdAt,
createBy: quotationFormState.value?.source?.createdBy,
payCondition: quotationFormData.value.payCondition,
contactName: quotationFormData.value.contactName,
contactTel: quotationFormData.value.contactTel,
workName: quotationFormData.value.workName,
2024-10-22 13:35:34 +07:00
dueDate: quotationFormData.value.dueDate,
2024-10-21 12:06:50 +07:00
},
2024-11-27 14:27:02 +07:00
selectedWorker: selectedWorker.value,
2024-10-18 12:32:55 +07:00
createdBy: quotationFormState.value.createdBy('tha'),
},
}),
2024-10-18 09:27:56 +07:00
);
2024-10-18 10:42:35 +07:00
2024-11-08 10:42:27 +07:00
const url = new URL('/quotation/document-view', window.location.origin);
url.searchParams.append('type', view.value);
2024-10-30 11:54:49 +07:00
2024-11-08 10:42:27 +07:00
window.open(url, '_blank');
2024-10-18 09:27:56 +07:00
}
2024-10-28 11:40:29 +07:00
const QUOTATION_STATUS = [
'Issued',
'Accepted',
'PaymentPending',
'PaymentInProcess',
'PaymentSuccess',
'ProcessComplete',
'Canceled',
];
function getStatus(
status: typeof quotationFormData.value.quotationStatus,
doneIndex: number,
doingIndex: number,
) {
2024-11-01 10:55:20 +07:00
return QUOTATION_STATUS.findIndex((v) => v === status) >= doneIndex
2024-10-28 11:40:29 +07:00
? 'done'
2024-11-01 10:55:20 +07:00
: QUOTATION_STATUS.findIndex((v) => v === status) >= doingIndex
2024-10-28 11:40:29 +07:00
? 'doing'
: 'waiting';
}
2024-10-25 11:39:38 +07:00
const statusQuotationForm = ref<
{
title: string;
status: 'done' | 'doing' | 'waiting';
handler: () => void;
active?: () => boolean;
}[]
2024-10-28 14:44:56 +07:00
>([]);
2024-10-28 10:33:17 +07:00
const view = ref<View>(View.Quotation);
const code = ref<string>('');
async function getInvoiceCode(invoiceId: string) {
const ret = await invoiceStore.getInvoice(invoiceId);
if (ret) code.value = ret.code;
}
async function getInvoiceCodeFullPay() {
2024-11-14 11:46:00 +07:00
const ret = await invoiceStore.getInvoiceList({
quotationId: quotationFormData.value.id,
});
if (ret) code.value = ret.result.at(0)?.code || '';
}
const importWorkerCriteria = ref<{
passport: string[] | null;
visa: string[] | null;
}>({
passport: [],
visa: [],
});
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: quotationFormData.value.customerBranchId,
activeOnly: true,
});
if (!ret) return false; // error, do not close dialog
const deduplicate = ret.result.filter(
(a) => !selectedWorker.value.find((b) => a.id === b.id),
);
2024-12-20 09:46:19 +07:00
convertEmployeeToTable([...deduplicate, ...selectedWorker.value]);
return true;
}
async function exampleReceipt(id: string) {
$q.loading.show();
const url = await paymentStore.getFlowAccount(id);
if (url) {
$q.loading.hide();
window.open(url.data.link, '_blank');
}
}
watch(
[
() => quotationFormState.value.statusFilterRequest,
() => quotationFormState.value.inputSearchRequest,
],
() => {
fetchRequest();
},
);
2024-06-19 15:43:58 +07:00
</script>
<template>
<ImportWorker
v-model:open="pageState.importWorker"
v-model:data="importWorkerCriteria"
:import-worker="getWorkerFromCriteria"
/>
2024-10-16 14:24:54 +07:00
<div class="column surface-0 fullscreen">
2024-11-26 09:56:55 +07:00
<div class="color-bar" :class="{ dark: $q.dark.isActive }">
2024-09-27 15:45:24 +07:00
<div class="orange-segment"></div>
2024-10-16 11:13:34 +07:00
<div class="yellow-segment"></div>
2024-09-27 15:45:24 +07:00
<div class="gray-segment"></div>
</div>
2024-12-16 10:54:12 +07:00
<header
class="row q-px-md q-py-sm items-center justify-between relative-position"
>
<section class="banner" :class="{ dark: $q.dark.isActive }"></section>
<div style="flex: 1" class="row items-center">
<RouterLink to="/quotation">
<q-img src="/icons/favicon-512x512.png" width="3rem" />
</RouterLink>
2024-09-27 15:45:24 +07:00
<span class="column text-h6 text-bold q-ml-md">
{{ $t('quotation.title') }}
{{ code || quotationFormState.source?.code || '' }}
2024-09-27 15:45:24 +07:00
<span class="text-caption text-regular app-text-muted">
2024-10-03 11:14:12 +07:00
{{
$t('quotation.processOn', {
2024-10-11 11:28:06 +07:00
msg:
quotationFormState.mode === 'create'
? `${dateFormat(date, true)} ${dateFormat(date, true, true)}`
: `${dateFormat(quotationFull?.createdAt, true)} ${dateFormat(quotationFull?.createdAt, true, true)}`,
2024-10-03 11:14:12 +07:00
})
}}
2024-09-27 15:45:24 +07:00
</span>
</span>
</div>
2024-10-25 16:28:51 +07:00
2024-10-25 17:14:30 +07:00
<div
v-if="quotationFormState.mode !== 'create'"
class="column col-2 q-ml-auto"
2024-10-25 17:14:30 +07:00
style="gap: 10px"
>
2024-10-25 16:28:51 +07:00
<div class="row justify-end">
2024-10-28 10:33:17 +07:00
<BadgeComponent :title-i18n="$t('general.laborIdentified')" />
2024-10-25 16:28:51 +07:00
</div>
<div
class="row items-center justify-between surface-1 rounded q-pa-xs"
style="height: 40px; border-radius: 40px"
2024-10-29 10:47:59 +07:00
v-if="quotationFormData.quotationStatus === 'Issued'"
2024-10-25 16:28:51 +07:00
>
<div class="full-height full-width row">
<div
class="col-6 flex flex-center"
style="
color: white;
background: linear-gradient(to right, #035aa1 0%, #14ab35 100%);
border-radius: 20px;
gap: 10px;
"
>
<q-icon name="mdi-calendar-range" />
2024-10-25 17:08:19 +07:00
{{ $t('quotation.receiptDialog.remain') }}
</div>
<div class="col-6 flex flex-center">
<span
v-if="calculateDaysUntilExpire(quotationFormData.dueDate) <= 0"
>
{{
$t(
`general.${
calculateDaysUntilExpire(quotationFormData.dueDate) > 0
? 'beDue'
: calculateDaysUntilExpire(
quotationFormData.dueDate,
) === 0
? 'due'
: 'overDue'
}`,
)
}}
</span>
<span v-else>
{{ calculateDaysUntilExpire(quotationFormData.dueDate) }}
{{ $t('general.day') }}
</span>
2024-10-25 16:28:51 +07:00
</div>
</div>
</div>
</div>
2024-09-27 15:45:24 +07:00
</header>
2024-06-19 15:43:58 +07:00
2024-09-30 13:13:08 +07:00
<article
2024-10-21 15:06:14 +07:00
class="col full-width q-pa-md"
2024-10-22 09:55:10 +07:00
style="flex-grow: 1; overflow-y: auto"
2024-06-19 15:43:58 +07:00
>
2024-10-22 09:55:10 +07:00
<section class="col-sm col-12">
2024-09-27 15:45:24 +07:00
<div class="col q-gutter-y-md">
2024-10-25 11:39:38 +07:00
<div
2024-12-04 15:42:09 +07:00
class="surface-1 q-pa-sm row no-wrap full-width scroll rounded"
style="gap: 10px"
2024-10-25 11:39:38 +07:00
>
2024-10-28 11:40:29 +07:00
<button
2024-10-25 11:39:38 +07:00
v-for="value in statusQuotationForm"
:key="value.title"
class="q-pa-sm bordered status-color row items-center"
:class="{
[`status-color-${value.status}`]: true,
'quotation-status-active': value.active?.(),
}"
2024-10-28 11:40:29 +07:00
@click="value.status !== 'waiting' && value.handler()"
2024-12-04 15:42:09 +07:00
style="min-width: 120px; cursor: pointer; text-wrap: nowrap"
2024-10-25 11:39:38 +07:00
>
<div class="q-px-sm">
<q-icon
class="icon-color quotation-status"
style="border-radius: 50%"
:name="`${value.status === 'done' ? 'mdi-check-circle' : 'mdi-checkbox-blank-circle-outline'}`"
/>
</div>
2024-11-07 15:12:06 +07:00
<div class="col text-left">
{{ $t(`quotation.status.${value.title}`) }}
</div>
2024-10-28 11:40:29 +07:00
</button>
2024-10-25 11:39:38 +07:00
</div>
2024-11-06 17:39:02 +07:00
<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('general.document') }}
</span>
</div>
<q-checkbox
v-model="quotationFormData.urgent"
class="q-ml-auto"
size="xs"
:label="$t('general.urgent')"
:disable="readonly"
/>
</section>
</template>
<div class="surface-1 q-pa-md full-width">
2024-12-20 17:42:50 +07:00
<q-form
ref="formMetadata"
@submit="
() => {
convertDataToFormSubmit();
}
2024-12-20 17:24:43 +07:00
"
2024-11-06 17:39:02 +07:00
>
2024-12-20 17:42:50 +07:00
<QuotationFormMetadata
class="q-mb-md"
:readonly
:actor="quotationFormState.createdBy?.($i18n.locale) || ''"
:quotation-no="(quotationFull && quotationFull.code) || ''"
:quotation-status="
quotationFormState.source?.quotationStatus === 'Expired'
"
v-model:urgent="quotationFormData.urgent"
v-model:work-name="quotationFormData.workName"
v-model:contactor="quotationFormData.contactName"
v-model:telephone="quotationFormData.contactTel"
v-model:due-date="quotationFormData.dueDate"
>
<template #issue-info>
<FormAbout
hide-add
class="col-12 col-md-8"
input-only
v-model:branch-id="quotationFormData.registeredBranchId"
v-model:customer-branch-id="
quotationFormData.customerBranchId
"
:readonly="readonly"
/>
</template>
</QuotationFormMetadata>
</q-form>
2024-11-06 17:39:02 +07:00
</div>
</q-expansion-item>
2024-10-28 16:44:14 +07:00
<template
v-if="
view === View.Quotation ||
view === View.Accepted ||
view === View.Invoice
2024-10-28 16:44:14 +07:00
"
>
2024-10-28 10:33:17 +07:00
<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>
<template v-if="!readonly">
<ToggleButton class="q-mr-sm" v-model="toggleWorker" />
{{
toggleWorker
? $t('general.specify')
: $t('general.noSpecify')
}}
</template>
</div>
<nav class="q-ml-auto">
<AddButton
v-if="
!readonly &&
(!quotationFormState.source ||
quotationFormState.source?.quotationStatus ===
'Issued' ||
quotationFormState.source?.quotationStatus ===
'Expired')
"
2024-10-28 10:33:17 +07:00
icon-only
@click.stop="triggerSelectEmployeeDialog"
/>
<AddButton
v-if="
!!quotationFormState.source &&
quotationFormState.source.quotationStatus !==
'Issued' &&
quotationFormState.source?.quotationStatus !== 'Expired'
"
@click.stop="openQuotation = !openQuotation"
icon-only
class="q-ml-auto"
/>
2024-10-28 10:33:17 +07:00
</nav>
</section>
</template>
<div class="surface-1 q-pa-md full-width">
<WorkerItem
@update:employee-amount="
(v) =>
(quotationFormData.workerMax = Math.max(
v,
selectedWorker.length,
))
"
:employee-amount="
quotationFormData.workerMax || selectedWorker.length
"
2024-10-28 10:33:17 +07:00
:readonly="readonly"
fallback-img="/images/employee-avatar.png"
:rows="selectedWorkerItem"
2024-10-28 10:33:17 +07:00
@delete="(i) => deleteItem(selectedWorker, i)"
/>
</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
v-if="!readonly"
icon-only
class="q-ml-auto"
@click.stop="triggerProductServiceDialog"
/>
</nav>
</section>
</template>
<div class="surface-1 q-pa-md full-width">
<ProductItem
2024-10-31 13:05:10 +07:00
:installment-input="
quotationFormData.payCondition === 'SplitCustom'
"
:max-installment="quotationFormData.paySplitCount"
2024-10-28 10:33:17 +07:00
:readonly="readonly"
:agent-price="agentPrice"
:employee-rows="selectedWorkerItem"
2024-10-28 10:33:17 +07:00
@delete="toggleDeleteProduct"
2024-10-30 11:54:49 +07:00
:rows="productService"
2024-10-28 15:12:07 +07:00
@update:rows="
(v) => {
view === View.Quotation && (productServiceList = v);
}
2024-10-28 15:12:07 +07:00
"
@update-table="handleUpdateProductTable"
2024-10-28 10:33:17 +07:00
/>
</div>
</q-expansion-item>
2024-10-28 10:33:17 +07:00
<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('general.payment') }}
</span>
</div>
</section>
</template>
<div class="surface-1 q-pa-md full-width">
2024-10-29 18:02:20 +07:00
<template v-if="true">
<QuotationFormInfo
2024-10-30 14:10:59 +07:00
:view="view"
:installment-amount="installmentAmount"
:installment-no="selectedInstallmentNo"
2024-10-29 18:02:20 +07:00
v-model:pay-type="quotationFormData.payCondition"
v-model:pay-bank="payBank"
v-model:pay-split-count="quotationFormData.paySplitCount"
v-model:pay-split="quotationFormData.paySplit"
:readonly
v-model:final-discount="quotationFormData.discount"
v-model:pay-bill-date="quotationFormData.payBillDate"
v-model:summary-price="summaryPrice"
class="q-mb-md"
@change-pay-type="handleChangePayType"
2024-10-29 18:02:20 +07:00
/>
</template>
2024-10-28 10:33:17 +07:00
</div>
</q-expansion-item>
2024-10-28 10:33:17 +07:00
<q-expansion-item
v-if="quotationFormState.mode !== 'create'"
2024-10-28 10:33:17 +07:00
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.additionalFile') }}
</span>
</div>
</section>
</template>
<div class="surface-1 q-pa-md full-width">
<UploadFileSection
2024-11-25 15:50:49 +07:00
:readonly="
2024-11-27 11:20:55 +07:00
{
quotation: quotationFormState.mode !== 'edit',
invoice: false,
accepted: true,
}[view]
2024-11-25 15:50:49 +07:00
"
v-model:file-data="attachmentData"
:label="
$t('general.upload', { msg: $t('general.attachment') })
"
:transform-url="
async (url: string) => {
const result = await api.get<string>(url);
return result.data;
}
"
@update:file="(f) => uploadAttachment(f)"
@close="(v) => triggerDelete(v)"
/>
</div>
</q-expansion-item>
2024-12-17 13:16:11 +07:00
<q-expansion-item
v-if="view === View.Accepted"
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.letterOfAcceptance') }}
</span>
</div>
</section>
</template>
<div class="surface-1 q-pa-md flex" style="gap: var(--size-2)">
2024-12-17 13:20:19 +07:00
<SelectInput
style="max-width: 250px"
class=""
incremental
v-model="templateForm"
id="quotation-branch"
:option="templateFormOption"
:label="$t('quotation.templateForm')"
:option-label="locale === 'eng' ? 'labelEN' : 'label'"
2024-12-17 13:16:11 +07:00
/>
<MainButton
outlined
icon="mdi-play-box-outline"
color="207 96% 32%"
2024-12-17 13:20:19 +07:00
@click="() => {}"
2024-12-17 13:16:11 +07:00
>
{{ $t('general.view', { msg: $t('general.example') }) }}
</MainButton>
<MainButton
solid
2024-12-17 13:20:19 +07:00
icon="mdi-pencil-outline"
2024-12-17 13:16:11 +07:00
color="207 96% 32%"
2024-12-17 13:20:19 +07:00
@click="() => {}"
2024-12-17 13:16:11 +07:00
>
2024-12-17 13:20:19 +07:00
{{ $t('general.designForm') }}
2024-12-17 13:16:11 +07:00
</MainButton>
</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"
2024-10-28 10:33:17 +07:00
header-class="surface-1"
>
<template v-slot:header>
2024-11-06 17:39:02 +07:00
<section class="row items-ceenhancement nter full-width">
2024-10-28 10:33:17 +07:00
<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.remark') }}
</span>
</div>
</section>
</template>
<div class="surface-1 q-pa-md full-width">
<q-editor
dense
2024-11-27 14:27:02 +07:00
:readonly="readonly || !pageState.remarkWrite"
:model-value="
2024-11-27 15:11:09 +07:00
!pageState.remarkWrite || readonly
2024-11-27 14:27:02 +07:00
? convertTemplate(quotationFormData.remark || '', {
'quotation-payment': {
paymentType: quotationFormData.payCondition,
amount: getPrice(productServiceList).finalPrice,
installments: quotationFormData.paySplit,
},
'quotation-labor': {
name: selectedWorker.map(
(v, i) =>
`${i + 1}. ` +
`${v.namePrefix}. ${v.firstNameEN} ${v.lastNameEN}`.toUpperCase(),
),
},
})
: quotationFormData.remark || ''
"
2024-10-28 10:33:17 +07:00
min-height="5rem"
class="full-width"
2024-11-27 13:55:59 +07:00
:content-class="{
'q-my-sm rounded': true,
2024-11-27 15:15:25 +07:00
bordered: !readonly && pageState.remarkWrite,
2024-11-27 13:55:59 +07:00
}"
2024-10-28 10:33:17 +07:00
toolbar-bg="input-border"
style="cursor: auto; color: var(--foreground)"
:flat="!readonly"
:style="`width: ${$q.screen.gt.xs ? '100%' : '63vw'}`"
2024-11-27 15:52:04 +07:00
:toolbar="getToolbarConfig"
2024-10-28 10:33:17 +07:00
:toolbar-toggle-color="readonly ? 'disabled' : 'primary'"
:toolbar-color="
readonly ? 'disabled' : $q.dark.isActive ? 'white' : ''
"
:definitions="{
clip: {
icon: 'mdi-paperclip',
tip: 'Upload',
2024-11-27 15:02:35 +07:00
disable: readonly,
2024-10-28 10:33:17 +07:00
handler: () => console.log('upload'),
},
}"
@update:model-value="
(v) => {
quotationFormData.remark = v;
}
"
2024-11-27 15:02:35 +07:00
>
2024-11-27 15:52:04 +07:00
<template v-if="!readonly" v-slot:toggle>
<div class="text-caption row no-wrap q-px-sm">
2024-11-27 15:02:35 +07:00
<MainButton
:disabled="readonly"
:solid="!pageState.remarkWrite"
icon="mdi-eye-outline"
color="0 0% 40%"
@click="pageState.remarkWrite = false"
style="padding: 0 var(--size-2)"
:style="{
color: pageState.remarkWrite
? 'hsl(0 0% 40%)'
: undefined,
}"
>
{{ $t('general.view', { msg: $t('general.example') }) }}
</MainButton>
<MainButton
:disabled="readonly"
:solid="pageState.remarkWrite"
icon="mdi-pencil-outline"
color="0 0% 40%"
@click="pageState.remarkWrite = true"
style="padding: 0 var(--size-2)"
:style="{
color: !pageState.remarkWrite
? 'hsl(0 0% 40%)'
: undefined,
}"
>
{{ $t('general.edit') }}
</MainButton>
</div>
</template>
</q-editor>
2024-10-28 10:33:17 +07:00
</div>
</q-expansion-item>
</template>
2024-11-06 17:39:02 +07:00
2024-10-28 16:44:14 +07:00
<template v-else>
2024-10-30 14:10:59 +07:00
<PaymentForm
2024-12-18 17:55:45 +07:00
v-if="
view !== View.InvoicePre &&
view !== View.Receipt &&
view !== View.Complete
"
2024-10-30 14:10:59 +07:00
:data="quotationFormState.source"
2024-11-01 17:36:13 +07:00
@fetch-status="
() => {
fetchQuotation();
}
"
2024-10-30 14:10:59 +07:00
/>
<div
v-if="view === View.Complete"
class="surface-1 q-pa-md full-width q-gutter-y-md"
>
<div class="row justify-between items-center">
<q-input
for="input-search"
outlined
dense
:label="$t('general.search')"
class="col-12 col-md-3"
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
v-model="quotationFormState.inputSearchRequest"
debounce="200"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
<q-select
v-model="quotationFormState.statusFilterRequest"
outlined
dense
option-value="value"
option-label="label"
map-options
emit-value
:for="'field-select-status'"
:hide-dropdown-icon="$q.screen.lt.sm"
:options="[
{
label: $t('general.all'),
value: 'All',
},
{
label: $t('requestList.status.Pending'),
value: RequestDataStatus.Pending,
},
{
label: $t('requestList.status.InProgress'),
value: RequestDataStatus.InProgress,
},
{
label: $t('requestList.status.Completed'),
value: RequestDataStatus.Completed,
},
]"
></q-select>
</div>
<TableRequest
v-if="view === View.Complete"
:rows="rowsRequestList"
/>
</div>
2024-10-29 18:02:20 +07:00
<q-expansion-item
v-if="view === View.InvoicePre"
2024-10-29 18:02:20 +07:00
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"
>
<div class="surface-1 q-pa-md full-width">
<SwitchItem :value="view">
<template #[View.InvoicePre]>
<q-table
flat
bordered
:columns="columnPaySplit"
:rows="quotationFormData.paySplit"
selection="multiple"
v-model:selected="selectedInstallment"
@update:selected="
(v) => {
selectedInstallment = v.filter(
(value) => !value.invoiceId,
);
selectedInstallmentNo = selectedInstallment.map(
(value: any) => value.no,
);
installmentAmount = selectedInstallment.reduce(
(total: number, value: any) => total + value.amount,
0,
);
2024-10-29 18:02:20 +07:00
}
"
row-key="no"
:no-data-label="$t('general.noDataTable')"
:rows-per-page-options="[0]"
hide-pagination
:selected-rows-label="
(n) =>
$t('general.selected', {
number: n,
msg: $t('productService.product.product'),
})
"
2024-10-29 18:02:20 +07:00
>
<template v-slot:header="props">
<q-tr
:props="props"
:style="`background-color:hsla(var(--info-bg) / 0.07)`"
>
<q-th auto-width>
<q-checkbox v-model="props.selected" />
</q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ $t(col.label) }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr
:class="{ 'cursor-pointer': props.row.invoiceId }"
:props="props"
@click="
async () => {
if (props.row.invoiceId) {
await getInvoiceCode(props.row.invoiceId);
selectedInstallmentNo =
quotationFormState.source?.paySplit
.filter(
(v) =>
v.invoiceId === props.row.invoiceId,
)
.map((v) => v.no) || [];
installmentAmount = props.row.amount;
view = View.Invoice;
}
}
"
>
2024-10-29 18:02:20 +07:00
<q-td auto-width>
2024-10-30 14:10:59 +07:00
<q-checkbox
v-model="props.selected"
v-if="!props.row.invoiceId"
2024-10-30 14:10:59 +07:00
/>
2024-10-29 18:02:20 +07:00
</q-td>
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
>
<template v-if="col.name === 'installmentNo'">
{{ col.value }}
</template>
<template v-if="col.name === 'amount'">
{{ formatNumberDecimal(col.value, 2) }}
</template>
<template v-if="col.name === 'status'">
<BadgeComponent
:title="
!!col.value
? 'สร้างใบแจ้งหนี้เเล้ว'
: 'ยังไม่สร้างใบแจ้งหนี้'
"
:hsla-color="!!col.value ? '' : '--red-7-hsl'"
/>
</template>
</q-td>
</q-tr>
</template>
</q-table>
</template>
</SwitchItem>
</div>
</q-expansion-item>
2024-10-28 16:44:14 +07:00
</template>
2024-11-06 17:39:02 +07:00
<template v-if="view === View.Quotation || view === View.Payment">
<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('general.form') }}
</span>
</div>
</section>
</template>
<div class="surface-1 q-pa-md flex" style="gap: var(--size-2)">
<SelectInput
style="max-width: 250px"
class="q-mr-xl"
incremental
v-model="templateForm"
id="quotation-branch"
:option="templateFormOption"
:label="$t('quotation.templateForm')"
:option-label="locale === 'eng' ? 'labelEN' : 'label'"
/>
<MainButton
outlined
icon="mdi-play-box-outline"
color="207 96% 32%"
@click="storeDataLocal"
>
{{ $t('general.view', { msg: $t('general.example') }) }}
</MainButton>
<MainButton
solid
icon="mdi-pencil-outline"
color="207 96% 32%"
@click="storeDataLocal"
>
{{ $t('general.designForm') }}
</MainButton>
</div>
</q-expansion-item>
</template>
<template v-if="view === View.Receipt">
<QuotationFormReceipt
v-for="(v, i) in receiptList"
:key="i"
:amount="v.amount"
:date="v.date"
:pay-type="quotationFormData.payCondition"
:index="i"
2024-12-19 09:55:47 +07:00
:pay-split-count="quotationFormData.paySplitCount || 0"
@view="
() => {
selectedInstallmentNo = [v.invoice.installments[0].no];
installmentAmount = v.invoice.amount;
view = View.Quotation;
}
"
@example="
() => {
exampleReceipt(v.id);
}
"
/>
</template>
2024-09-18 15:38:50 +07:00
</div>
2024-09-27 15:45:24 +07:00
</section>
2024-09-30 13:13:08 +07:00
</article>
2024-06-19 15:43:58 +07:00
2024-10-09 09:56:15 +07:00
<footer class="surface-1 q-pa-md full-width">
2024-10-30 10:24:24 +07:00
<div
class="row full-width"
:class="{
'justify-between': view !== View.InvoicePre,
'justify-end': view === View.InvoicePre,
}"
>
2024-10-18 09:37:34 +07:00
<MainButton
v-if="view !== View.InvoicePre && view !== View.PaymentPre"
2024-10-18 09:37:34 +07:00
outlined
icon="mdi-play-box-outline"
color="207 96% 32%"
2024-10-18 09:27:56 +07:00
@click="storeDataLocal"
>
2024-09-27 15:45:24 +07:00
{{ $t('general.view', { msg: $t('general.example') }) }}
2024-10-18 09:37:34 +07:00
</MainButton>
2024-10-29 18:02:20 +07:00
2024-10-30 11:54:49 +07:00
<div
v-if="
view === View.Accepted &&
quotationFormData.quotationStatus === 'Issued'
"
>
2024-10-29 18:02:20 +07:00
<MainButton
2024-10-30 10:24:24 +07:00
solid
icon="mdi-account-multiple-check-outline"
2024-10-29 18:02:20 +07:00
color="207 96% 32%"
2024-10-30 10:24:24 +07:00
@click="
() => {
submitAccepted();
}
"
2024-10-29 18:02:20 +07:00
>
2024-10-30 10:24:24 +07:00
{{ $t('quotation.customerAcceptance') }}
2024-10-29 18:02:20 +07:00
</MainButton>
</div>
2024-10-30 10:24:24 +07:00
<template v-if="view === View.InvoicePre">
2024-10-29 18:02:20 +07:00
<MainButton
solid
icon="mdi-account-multiple-check-outline"
color="207 96% 32%"
@click="
() => {
view = View.Invoice;
}
"
>
{{ $t('quotation.selectInvoice') }}
</MainButton>
2024-10-30 10:24:24 +07:00
</template>
2024-10-29 18:02:20 +07:00
<div
v-if="
view === View.Invoice &&
2024-11-01 17:36:13 +07:00
((quotationFormData.quotationStatus !== 'PaymentPending' &&
quotationFormData.payCondition !== 'Full') ||
2024-11-26 14:01:09 +07:00
quotationFormData.quotationStatus === 'Accepted') &&
!isIssueInvoice()
"
>
2024-10-29 18:02:20 +07:00
<MainButton
solid
icon="mdi-account-multiple-check-outline"
color="207 96% 32%"
@click="
() => {
convertInvoiceToSubmit();
}
"
>
{{ $t('quotation.approveInvoice') }}
</MainButton>
</div>
2024-10-29 09:20:53 +07:00
<div
class="row"
style="gap: var(--size-2)"
v-if="
2024-10-30 10:31:14 +07:00
(view === View.Quotation &&
(quotationFormData.quotationStatus === 'Issued' ||
quotationFormData.quotationStatus === 'Expired')) ||
2024-10-30 10:31:14 +07:00
!quotationFormData.quotationStatus
2024-10-29 09:20:53 +07:00
"
>
2024-10-18 13:30:40 +07:00
<UndoButton
2024-10-18 13:46:37 +07:00
outlined
2024-10-18 13:30:40 +07:00
@click="closeTab()"
v-if="quotationFormState.mode === 'edit'"
/>
2024-10-18 13:43:20 +07:00
<CloseButton
2024-10-18 13:46:37 +07:00
outlined
2024-10-18 13:34:45 +07:00
@click="closeTab()"
v-if="quotationFormState.mode === 'info' && closeAble()"
/>
2024-10-18 09:10:33 +07:00
<SaveButton
2024-12-20 17:42:50 +07:00
type="submit"
2024-10-18 09:10:33 +07:00
v-if="
quotationFormState.mode === 'create' ||
quotationFormState.mode === 'edit'
"
solid
2024-12-20 17:42:50 +07:00
@click="() => formMetadata.submit()"
2024-10-18 09:10:33 +07:00
/>
<EditButton
v-else
class="no-print"
@click="quotationFormState.mode = 'edit'"
solid
/>
</div>
2024-07-26 10:55:30 +07:00
</div>
2024-09-27 15:45:24 +07:00
</footer>
2024-10-03 11:14:12 +07:00
<!-- SEC: Dialog -->
<!-- add employee quotation -->
2024-12-20 09:46:19 +07:00
<QuotationFormWorkerSelect
:preselect-worker="selectedWorker"
v-model:open="pageState.employeeModal"
2024-12-20 16:46:32 +07:00
v-model:new-worker-list="newWorkerList"
2024-12-20 09:46:19 +07:00
@success="
(v) => {
2024-12-20 15:56:23 +07:00
selectedWorker = v.worker;
}
"
2024-12-20 09:46:19 +07:00
/>
2024-10-03 11:14:12 +07:00
<!-- add product service -->
2024-10-30 08:59:20 +07:00
<QuotationFormProductSelect
2024-10-03 11:14:12 +07:00
v-model="pageState.productServiceModal"
v-model:nodes="productServiceNodes"
2024-10-03 11:14:12 +07:00
v-model:product-group="productGroup"
v-model:product-list="productList"
v-model:service-list="serviceList"
2024-10-08 16:38:15 +07:00
v-model:selected-product-group="selectedProductGroup"
2024-10-07 15:14:33 +07:00
:agent-price="agentPrice"
@submit="convertToTable"
2024-10-03 11:14:12 +07:00
@select-group="
async (id) => {
await getAllService(id);
await getAllProduct(id);
}
2024-10-15 17:16:36 +07:00
"
@search="
(id, text, mode) => {
if (mode === 'service') {
2024-10-15 18:01:11 +07:00
getAllService(id, { force: true, query: text, pageSize: 50 });
2024-10-15 17:16:36 +07:00
}
if (mode === 'product') {
2024-10-15 18:01:11 +07:00
getAllProduct(id, { force: true, query: text, pageSize: 50 });
2024-10-15 17:16:36 +07:00
}
}
2024-10-03 11:14:12 +07:00
"
2024-10-30 08:59:20 +07:00
></QuotationFormProductSelect>
2024-09-27 15:45:24 +07:00
</div>
2024-10-04 17:01:24 +07:00
<QuotationFormWorkerAddDialog
v-if="quotationFormState.source"
:disabled-worker-id="selectedWorker.map((v) => v.id)"
:product-service-list="quotationFormState.source.productServiceList"
:quotation-id="quotationFormState.source.id"
:customer-branch-id="quotationFormState.source.customerBranchId"
v-model:open="openQuotation"
@success="
openQuotation = !openQuotation;
fetchQuotation();
"
/>
2024-06-19 15:43:58 +07:00
</template>
<style scoped>
.worker-list > :deep(*:not(:last-child)) {
margin-bottom: var(--size-2);
}
2024-09-18 15:38:50 +07:00
.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);
}
2024-09-27 15:45:24 +07:00
.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;
}
2024-11-26 09:56:55 +07:00
.color-bar.dark {
opacity: 0.7;
}
2024-10-16 11:13:34 +07:00
.orange-segment {
2024-09-27 15:45:24 +07:00
background-color: var(--yellow-7);
flex-grow: 4;
}
2024-10-16 11:13:34 +07:00
.yellow-segment {
2024-09-27 15:45:24 +07:00
background-color: hsla(var(--yellow-7-hsl) / 0.2);
flex-grow: 0.5;
}
.gray-segment {
background-color: #ccc;
flex-grow: 1;
}
.orange-segment,
2024-10-16 11:13:34 +07:00
.yellow-segment,
2024-09-27 15:45:24 +07:00
.gray-segment {
transform: skewX(-60deg);
}
:deep(i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon) {
2024-10-11 10:01:29 +07:00
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
) {
2024-09-27 15:45:24 +07:00
color: var(--brand-1);
}
:deep(.q-editor__toolbar-group):nth-child(2) {
margin-left: auto !important;
}
2024-09-30 16:36:18 +07:00
: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;
}
2024-10-25 11:39:38 +07:00
.status-color {
--_color: var(--gray-0);
border-color: hsla(var(--_color));
background: hsla(var(--_color) / 0.05);
border-radius: 4px;
.icon-color {
color: hsla(var(--_color));
}
&.status-color-done {
--_color: var(--green-5-hsl);
2024-11-26 09:03:18 +07:00
color: var(--foreground);
2024-10-25 11:39:38 +07:00
}
&.status-color-doing {
--_color: var(--blue-5-hsl);
2024-11-26 09:03:18 +07:00
color: var(--foreground);
2024-10-25 11:39:38 +07:00
}
&.status-color-waiting {
--_color: var(--gray-4-hsl);
2024-11-26 09:03:18 +07:00
color: var(--foreground);
2024-10-25 11:39:38 +07:00
}
}
.quotation-status-active {
opacity: 1;
font-weight: 600;
transition: 1s box-shadow ease-in-out;
animation: status 1s infinite;
}
@keyframes status {
0% {
box-shadow: 0x 0px 0px hsla(var(--_color) / 0.5);
}
50% {
box-shadow: 0px 0px 1px 4px hsla(var(--_color) / 0.2);
}
100% {
box-shadow: 0px 0px 4px 7px hsla(var(--_color) / 0);
}
}
2024-12-16 10:54:12 +07:00
.banner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url('/images/building-banner.png');
background-repeat: no-repeat;
background-size: cover;
z-index: -1;
&.dark {
2024-12-16 11:03:56 +07:00
filter: invert(100%);
2024-12-16 10:54:12 +07:00
}
}
2024-06-19 15:43:58 +07:00
</style>