1543 lines
44 KiB
Vue
1543 lines
44 KiB
Vue
<script setup lang="ts">
|
|
import { useQuasar } from 'quasar';
|
|
import { ref, onMounted, reactive, computed } from 'vue';
|
|
import { api } from 'src/boot/axios';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { dateFormatJS, calculateAge } from 'src/utils/datetime';
|
|
import { initLang, initTheme, Lang } from 'src/utils/ui';
|
|
import { useQuotationStore } from 'src/stores/quotations';
|
|
import { useQuotationForm } from 'src/pages/05_quotation/form';
|
|
import { useDebitNoteForm } from './form';
|
|
import { useReceipt, usePayment } from 'stores/payment';
|
|
import useProductServiceStore from 'stores/product-service';
|
|
import {
|
|
DebitNote,
|
|
DebitNoteStatus,
|
|
DebitNotePayload,
|
|
useDebitNote,
|
|
} from 'src/stores/debit-note';
|
|
import {
|
|
QuotationStatus,
|
|
PayCondition,
|
|
QuotationPayload,
|
|
ProductRelation,
|
|
ProductServiceList,
|
|
} from 'src/stores/quotations/types';
|
|
import { useConfigStore } from 'src/stores/config';
|
|
import DocumentExpansion from './expansion/DocumentExpansion.vue';
|
|
import RemarkExpansion from '../12_debit-note/expansion/RemarkExpansion.vue';
|
|
import AdditionalFileExpansion from '../09_task-order/expansion/AdditionalFileExpansion.vue';
|
|
import PaymentExpansion from './expansion/PaymentExpansion.vue';
|
|
import DebitNoteExpansion from './expansion/DebitNoteExpansion.vue';
|
|
import StateButton from 'src/components/button/StateButton.vue';
|
|
import ProductExpansion from '../12_debit-note/expansion/ProductExpansion.vue';
|
|
import SelectReadyRequestWork from '../09_task-order/SelectReadyRequestWork.vue';
|
|
import QuotationFormReceipt from '../05_quotation/QuotationFormReceipt.vue';
|
|
import DialogViewFile from 'src/components/dialog/DialogViewFile.vue';
|
|
import WorkerItemExpansion from './expansion/WorkerItemExpansion.vue';
|
|
import PaymentForm from '../05_quotation/PaymentForm.vue';
|
|
import {
|
|
MainButton,
|
|
SaveButton,
|
|
UndoButton,
|
|
EditButton,
|
|
CancelButton,
|
|
} from 'src/components/button';
|
|
import { RequestWork } from 'src/stores/request-list/types';
|
|
import { storeToRefs } from 'pinia';
|
|
import useOptionStore from 'src/stores/options';
|
|
import {
|
|
canAccess,
|
|
deleteItem,
|
|
dialog,
|
|
dialogWarningClose,
|
|
isRoleInclude,
|
|
} from 'src/stores/utils';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { Employee } from 'src/stores/employee/types';
|
|
import QuotationFormWorkerSelect from '../05_quotation/QuotationFormWorkerSelect.vue';
|
|
import { watch } from 'vue';
|
|
import { ProductTree, quotationProductTree } from '../05_quotation/utils';
|
|
import TableRequest from '../05_quotation/TableRequest.vue';
|
|
import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
|
|
import { useRequestList } from 'src/stores/request-list';
|
|
import { Receipt } from 'src/stores/payment/types';
|
|
import {
|
|
Product,
|
|
ProductGroup,
|
|
Service,
|
|
} from 'src/stores/product-service/types';
|
|
import { precisionRound } from 'src/utils/arithmetic';
|
|
import QuotationFormProductSelect from '../05_quotation/QuotationFormProductSelect.vue';
|
|
import { getName } from 'src/services/keycloak';
|
|
|
|
const debitNoteForm = useDebitNoteForm();
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const debitNote = useDebitNote();
|
|
const configStore = useConfigStore();
|
|
const quotationStore = useQuotationStore();
|
|
const optionStore = useOptionStore();
|
|
const quotationForm = useQuotationForm();
|
|
const useReceiptStore = useReceipt();
|
|
const productServiceStore = useProductServiceStore();
|
|
const $q = useQuasar();
|
|
const paymentStore = usePayment();
|
|
const { data: config } = storeToRefs(configStore);
|
|
const { newWorkerList } = storeToRefs(quotationForm);
|
|
const { currentFormData } = storeToRefs(debitNoteForm);
|
|
const { t, locale } = useI18n();
|
|
const requestStore = useRequestList();
|
|
|
|
type ProductGroupId = string;
|
|
|
|
type Node = {
|
|
[key: string]: any;
|
|
opened?: boolean;
|
|
checked?: boolean;
|
|
bg?: string;
|
|
fg?: string;
|
|
icon?: string;
|
|
children?: Node[];
|
|
};
|
|
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
|
const tempTableProduct = ref<ProductServiceList[]>([]);
|
|
const productList = ref<Partial<Record<ProductGroupId, Product[]>>>({});
|
|
const serviceList = ref<Partial<Record<ProductGroupId, Service[]>>>({});
|
|
const selectedProductGroup = ref<string>('');
|
|
const selectedGroupSub = ref<'product' | 'service' | null>(null);
|
|
const productGroup = ref<ProductGroup[]>([]);
|
|
const agentPrice = ref(false);
|
|
const debitNoteData = ref<DebitNote>();
|
|
const quotationData = ref<DebitNote['debitNoteQuotation']>();
|
|
const view = ref<QuotationStatus>(QuotationStatus.Issued);
|
|
const fileList = ref<FileList>();
|
|
const receiptList = ref<Receipt[]>([]);
|
|
let previousValue: DebitNotePayload | undefined = undefined;
|
|
|
|
const rowsRequestList = ref<RequestData[]>([]);
|
|
|
|
const productServiceList = ref<
|
|
Required<QuotationPayload['productServiceList'][number]>[]
|
|
>([]);
|
|
const productServiceNodes = ref<ProductTree>([]);
|
|
|
|
const tempPaySplitCount = ref(0);
|
|
const tempPaySplit = ref<
|
|
{ no: number; amount: number; name?: string; invoice?: boolean }[]
|
|
>([]);
|
|
|
|
const formTaskList = ref<
|
|
{
|
|
requestWorkId: string;
|
|
requestWorkStep?: { requestWork: RequestWork };
|
|
}[]
|
|
>([]);
|
|
|
|
const attachmentList = ref<FileList>();
|
|
const fileData = ref<
|
|
{
|
|
name: string;
|
|
progress: number;
|
|
loaded: number;
|
|
total: number;
|
|
url?: string;
|
|
}[]
|
|
>([]);
|
|
const attachmentData = ref<
|
|
{
|
|
name: string;
|
|
progress: number;
|
|
loaded: number;
|
|
total: number;
|
|
url?: string;
|
|
placeholder?: boolean;
|
|
}[]
|
|
>([]);
|
|
|
|
const statusTabForm = ref<
|
|
{
|
|
title: string;
|
|
status: 'done' | 'doing' | 'waiting';
|
|
handler: () => void;
|
|
active?: () => boolean;
|
|
}[]
|
|
>([]);
|
|
|
|
const productService = computed(() => {
|
|
return selectedInstallmentNo.value.length > 0
|
|
? productServiceList.value.filter((v) => {
|
|
return (
|
|
v.installmentNo &&
|
|
selectedInstallmentNo.value.includes(v.installmentNo)
|
|
);
|
|
})
|
|
: productServiceList.value;
|
|
});
|
|
|
|
const summaryPrice = computed(() => getPrice(productServiceList.value));
|
|
|
|
const readonly = computed(
|
|
() =>
|
|
debitNoteData.value?.quotationStatus === QuotationStatus.PaymentSuccess ||
|
|
debitNoteData.value?.quotationStatus === QuotationStatus.ProcessComplete ||
|
|
pageState.mode === 'info',
|
|
);
|
|
|
|
const pageState = reactive({
|
|
mode: 'create' as 'create' | 'edit' | 'info',
|
|
employeeModal: false,
|
|
productDialog: false,
|
|
fileDialog: false,
|
|
productServiceModal: false,
|
|
inputSearchRequest: '',
|
|
statusFilterRequest: 'All' as 'All' | RequestDataStatus,
|
|
imageDialog: false,
|
|
imageDialogTitle: '',
|
|
imageDialogUrl: '',
|
|
});
|
|
|
|
const selectedWorker = ref<
|
|
(Employee & {
|
|
attachment?: {
|
|
name?: string;
|
|
group?: string;
|
|
url?: string;
|
|
file?: File;
|
|
_meta?: Record<string, any>;
|
|
}[];
|
|
})[]
|
|
>([]);
|
|
const selectedWorkerItem = ref([]);
|
|
|
|
const selectedInstallmentNo = ref<number[]>([]);
|
|
const installmentAmount = ref<number>(0);
|
|
|
|
const QUOTATION_STATUS = [
|
|
'Accepted',
|
|
'PaymentPending',
|
|
'PaymentSuccess',
|
|
'ProcessComplete',
|
|
];
|
|
|
|
function covertToNode() {
|
|
productServiceNodes.value = quotationProductTree(
|
|
productServiceList.value,
|
|
agentPrice.value,
|
|
config.value?.vat,
|
|
);
|
|
}
|
|
|
|
watch(
|
|
() => productServiceList.value,
|
|
() => {
|
|
covertToNode();
|
|
},
|
|
);
|
|
|
|
function toggleDeleteProduct(index: number) {
|
|
// product display
|
|
productServiceList.value.splice(index, 1);
|
|
productServiceList.value = [...productServiceList.value];
|
|
|
|
// find split count
|
|
tempPaySplitCount.value = Math.max(
|
|
...productServiceList.value.map((v) => v.installmentNo || 0),
|
|
);
|
|
currentFormData.value.paySplitCount = tempPaySplitCount.value;
|
|
|
|
// make split.length === split count
|
|
tempPaySplit.value.splice(currentFormData.value.paySplitCount);
|
|
}
|
|
|
|
async function getAllProduct(
|
|
groupId: string,
|
|
opts?: { force?: boolean; page?: number; pageSize?: number; query?: string },
|
|
) {
|
|
selectedGroupSub.value = 'product';
|
|
if (!opts?.force && productList.value[groupId] !== undefined) return;
|
|
const ret = await productServiceStore.fetchListProduct({
|
|
page: opts?.page ?? 1,
|
|
pageSize: opts?.pageSize ?? 9999,
|
|
query: opts?.query,
|
|
productGroupId: groupId,
|
|
activeOnly: true,
|
|
});
|
|
if (ret) productList.value[groupId] = ret.result;
|
|
}
|
|
|
|
async function getAllService(
|
|
groupId: string,
|
|
opts?: { force?: boolean; page?: number; pageSize?: number; query?: string },
|
|
) {
|
|
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,
|
|
query: opts?.query,
|
|
|
|
fullDetail: true,
|
|
});
|
|
if (ret) serviceList.value[groupId] = ret.result;
|
|
}
|
|
|
|
async function assignToProductServiceList() {
|
|
const ret = await productServiceStore.fetchProductGroup({
|
|
page: 1,
|
|
pageSize: 9999,
|
|
});
|
|
|
|
if (ret) {
|
|
productGroup.value = ret.result;
|
|
}
|
|
}
|
|
|
|
function undo() {
|
|
if (!previousValue) return;
|
|
|
|
currentFormData.value = structuredClone(previousValue);
|
|
assignProductServiceList();
|
|
assignSelectedWorker();
|
|
pageState.mode = 'info';
|
|
}
|
|
|
|
function assignProductServiceList() {
|
|
if (!debitNoteData.value) return;
|
|
productServiceList.value = debitNoteData.value.productServiceList.map(
|
|
(v, i) => ({
|
|
id: v.id!,
|
|
installmentNo: v.installmentNo || 0,
|
|
workerIndex: v.worker
|
|
.map((a) =>
|
|
debitNoteData.value!.worker.findIndex(
|
|
(b) => b.employeeId === a.employeeId,
|
|
),
|
|
)
|
|
.filter(
|
|
(index): index is number => index !== -1 && index !== undefined,
|
|
),
|
|
vat: v.vat || 0,
|
|
pricePerUnit: v.pricePerUnit || 0,
|
|
discount: v.discount || 0,
|
|
amount: v.amount || 0,
|
|
product: v.product,
|
|
work: v.work || null,
|
|
service: v.service || null,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function assignSelectedWorker() {
|
|
if (debitNoteData.value)
|
|
selectedWorkerItem.value = debitNoteData.value.worker.map((e) => {
|
|
return {
|
|
id: e.employee.id,
|
|
foreignRefNo: e.employee.employeePassport
|
|
? e.employee.employeePassport[0]?.number || '-'
|
|
: '-',
|
|
employeeName:
|
|
locale.value === Lang.English
|
|
? `${e.employee.firstNameEN} ${e.employee.lastNameEN}`
|
|
: `${e.employee.firstName || e.employee.firstNameEN} ${e.employee.lastName || e.employee.lastNameEN}`,
|
|
birthDate: dateFormatJS({ date: e.employee.dateOfBirth }),
|
|
gender: e.employee.gender,
|
|
age: calculateAge(e.employee.dateOfBirth),
|
|
nationality: optionStore.mapOption(e.employee.nationality),
|
|
documentExpireDate:
|
|
e.employee.employeePassport !== undefined &&
|
|
e.employee.employeePassport[0]?.expireDate !== undefined
|
|
? dateFormatJS({ date: e.employee.employeePassport[0]?.expireDate })
|
|
: '-',
|
|
imgUrl: e.employee.selectedImage
|
|
? `${API_BASE_URL}/employee/${e.id}/image/${e.employee.selectedImage}`
|
|
: '',
|
|
status: e.employee.status,
|
|
workerNew: false,
|
|
lastNameEN: e.employee.lastNameEN,
|
|
lastName: e.employee.lastName,
|
|
middleNameEN: e.employee.middleNameEN,
|
|
middleName: e.employee.middleName,
|
|
firstNameEN: e.employee.firstNameEN,
|
|
firstName: e.employee.firstName,
|
|
namePrefix: e.employee.namePrefix,
|
|
};
|
|
});
|
|
}
|
|
|
|
async function assignFormData(id: string) {
|
|
const data = await debitNote.getDebitNote(id);
|
|
if (!data) return;
|
|
|
|
debitNoteData.value = data;
|
|
|
|
selectedProductGroup.value =
|
|
data.productServiceList[0]?.product.productGroup?.id || '';
|
|
|
|
(previousValue = {
|
|
id: data.id || undefined,
|
|
debitNoteQuotationId: data.debitNoteQuotationId || undefined,
|
|
productServiceList: structuredClone(
|
|
data.productServiceList.map((v) => ({
|
|
workerIndex: v.worker.map((a) =>
|
|
data.worker.findIndex((b) => b.employeeId === a.employeeId),
|
|
),
|
|
installmentNo: v.installmentNo,
|
|
discount: v.discount,
|
|
amount: v.amount,
|
|
workId: v.workId ?? '',
|
|
serviceId: v.serviceId ?? '',
|
|
productId: v.productId,
|
|
})),
|
|
),
|
|
worker: data.worker.map((v) => v.employee),
|
|
payBillDate: new Date(data.payBillDate),
|
|
paySplitCount: data.paySplitCount,
|
|
payCondition: data.payCondition,
|
|
dueDate: new Date(data.dueDate),
|
|
discount: data.discount,
|
|
agentPrice: data.agentPrice,
|
|
quotationId: data.debitNoteQuotationId,
|
|
remark: data.remark || undefined,
|
|
}),
|
|
(currentFormData.value = structuredClone(previousValue));
|
|
|
|
assignProductServiceList();
|
|
assignSelectedWorker();
|
|
await getQuotation(data.debitNoteQuotation?.id);
|
|
await assignToProductServiceList();
|
|
await fetchRequest();
|
|
await fetchReceipt();
|
|
|
|
await initStatus();
|
|
}
|
|
|
|
async function getQuotation(id?: string) {
|
|
const quotationId =
|
|
typeof route.query['quotationId'] === 'string'
|
|
? route.query['quotationId']
|
|
: id;
|
|
|
|
if (!!quotationId) {
|
|
const data = await quotationStore.getQuotation(quotationId);
|
|
if (!!data) {
|
|
quotationData.value = data;
|
|
agentPrice.value = quotationData.value.agentPrice;
|
|
}
|
|
}
|
|
}
|
|
|
|
function getStatus(
|
|
status: typeof currentFormData.value.quotationStatus,
|
|
doneIndex: number,
|
|
doingIndex: number,
|
|
) {
|
|
return QUOTATION_STATUS.findIndex((v) => v === status) >= doneIndex
|
|
? 'done'
|
|
: QUOTATION_STATUS.findIndex((v) => v === status) >= doingIndex
|
|
? 'doing'
|
|
: 'waiting';
|
|
}
|
|
|
|
async function initStatus() {
|
|
if (debitNoteData.value?.quotationStatus === QuotationStatus.Issued)
|
|
view.value = QuotationStatus.Issued;
|
|
|
|
//TODO แก้สถานะแสดงแยกต่างหาก
|
|
statusTabForm.value = [
|
|
{
|
|
title: 'title',
|
|
status: debitNoteData.value?.id !== undefined ? 'done' : 'doing',
|
|
active: () => view.value === QuotationStatus.Issued,
|
|
handler: () => {
|
|
pageState.mode = 'info';
|
|
view.value = QuotationStatus.Issued;
|
|
},
|
|
},
|
|
{
|
|
title: 'accepted',
|
|
status:
|
|
debitNoteData.value?.id !== undefined
|
|
? getStatus(debitNoteData.value.quotationStatus, 1, -1)
|
|
: 'waiting',
|
|
active: () => view.value === QuotationStatus.Accepted,
|
|
handler: () => {
|
|
pageState.mode = 'info';
|
|
view.value = QuotationStatus.Accepted;
|
|
},
|
|
},
|
|
{
|
|
title: 'payment',
|
|
status:
|
|
debitNoteData.value?.id !== undefined
|
|
? getStatus(debitNoteData.value.quotationStatus, 2, 1)
|
|
: 'waiting',
|
|
active: () => view.value === QuotationStatus.PaymentPending,
|
|
handler: async () => {
|
|
view.value = QuotationStatus.PaymentPending;
|
|
},
|
|
},
|
|
|
|
{
|
|
title: 'receipt',
|
|
status: getStatus(debitNoteData.value?.quotationStatus, 2, 2),
|
|
active: () => view.value === QuotationStatus.PaymentSuccess,
|
|
handler: () => {
|
|
pageState.mode = 'info';
|
|
view.value = QuotationStatus.PaymentSuccess;
|
|
},
|
|
},
|
|
|
|
{
|
|
title: 'processComplete',
|
|
status: getStatus(debitNoteData.value?.quotationStatus, 3, 2),
|
|
active: () => view.value === QuotationStatus.ProcessComplete,
|
|
handler: () => {
|
|
pageState.mode = 'info';
|
|
view.value = QuotationStatus.ProcessComplete;
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
function goToQuotation() {
|
|
if (!quotationData.value) return;
|
|
|
|
const quotation = quotationData.value;
|
|
const url = new URL('/quotation/view', window.location.origin);
|
|
|
|
localStorage.setItem(
|
|
'new-quotation',
|
|
JSON.stringify({
|
|
customerBranchId: quotation.customerBranchId,
|
|
agentPrice: quotation.agentPrice,
|
|
statusDialog: 'info',
|
|
quotationId: quotation.id,
|
|
}),
|
|
);
|
|
|
|
window.open(url.toString(), '_blank');
|
|
}
|
|
|
|
function getPrice(
|
|
list: typeof productServiceList.value,
|
|
filterHook?: (
|
|
item: (typeof productServiceList.value)[number],
|
|
index?: number,
|
|
) => boolean,
|
|
) {
|
|
if (filterHook) list = list.filter(filterHook);
|
|
|
|
return list.reduce(
|
|
(a, c) => {
|
|
if (
|
|
selectedInstallmentNo.value.length > 0 &&
|
|
c.installmentNo &&
|
|
!selectedInstallmentNo.value.includes(c.installmentNo)
|
|
) {
|
|
return a;
|
|
}
|
|
|
|
const calcVat =
|
|
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
|
|
|
|
const vatFactor = calcVat ? (config.value?.vat ?? 0.07) : 0;
|
|
|
|
const pricePerUnit = precisionRound(
|
|
(c.pricePerUnit * (1 + vatFactor)) / (1 + vatFactor),
|
|
);
|
|
|
|
const price = precisionRound(
|
|
(pricePerUnit * c.amount * (1 + vatFactor) - c.discount) /
|
|
(1 + vatFactor),
|
|
);
|
|
const vat = price * vatFactor;
|
|
|
|
a.totalPrice = precisionRound(a.totalPrice + price + c.discount);
|
|
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
|
|
a.vat = precisionRound(a.vat + vat);
|
|
a.vatExcluded = c.product.calcVat
|
|
? a.vatExcluded
|
|
: precisionRound(a.vatExcluded + price);
|
|
a.finalPrice = precisionRound(a.totalPrice - a.totalDiscount + a.vat);
|
|
|
|
return a;
|
|
},
|
|
{
|
|
totalPrice: 0,
|
|
totalDiscount: 0,
|
|
vat: 0,
|
|
vatIncluded: 0,
|
|
vatExcluded: 0,
|
|
finalPrice: 0,
|
|
},
|
|
);
|
|
}
|
|
|
|
async function fetchRequest() {
|
|
const res = await requestStore.getRequestDataList({
|
|
query: pageState.inputSearchRequest,
|
|
page: 1,
|
|
pageSize: 999999,
|
|
requestDataStatus:
|
|
pageState.statusFilterRequest === 'All'
|
|
? undefined
|
|
: pageState.statusFilterRequest,
|
|
quotationId: currentFormData.value.id,
|
|
});
|
|
|
|
if (res) {
|
|
rowsRequestList.value = res.result;
|
|
}
|
|
}
|
|
|
|
async function fetchReceipt() {
|
|
const res = await useReceiptStore.getReceiptList({
|
|
debitNoteId: debitNoteData.value?.id || '',
|
|
debitNoteOnly: true,
|
|
quotationOnly: false,
|
|
});
|
|
|
|
if (res) {
|
|
receiptList.value = res.result;
|
|
}
|
|
}
|
|
|
|
function viewProductFile(data: ProductRelation) {
|
|
const base64 = data.detail.match(/src="([^"]+)"/);
|
|
|
|
pageState.imageDialog = true;
|
|
pageState.imageDialogTitle = data.name;
|
|
pageState.imageDialogUrl = base64 ? base64[1] : '';
|
|
}
|
|
|
|
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);
|
|
|
|
currentFormData.value.paySplitCount = Math.max(
|
|
...list.map((v) => v.installmentNo || 0),
|
|
);
|
|
tempPaySplitCount.value = currentFormData.value.paySplitCount;
|
|
|
|
list.forEach((v) => {
|
|
v.amount = Math.max(selectedWorkerItem.value.length, 1);
|
|
if (!v.workerIndex) v.workerIndex = [];
|
|
for (let i = 0; i < selectedWorkerItem.value.length; i++) {
|
|
v.workerIndex.push(i);
|
|
}
|
|
|
|
if (!v.installmentNo) {
|
|
v.installmentNo = currentFormData.value.paySplitCount;
|
|
}
|
|
});
|
|
|
|
productServiceList.value = list;
|
|
tempTableProduct.value = JSON.parse(JSON.stringify(list));
|
|
|
|
currentFormData.value.paySplitCount = 0;
|
|
currentFormData.value.payCondition = PayCondition.Full;
|
|
|
|
tempPaySplitCount.value = currentFormData.value.paySplitCount;
|
|
pageState.productServiceModal = false;
|
|
}
|
|
|
|
async function getFileList(debitNoteId: string, slip?: boolean) {
|
|
const list = slip
|
|
? await debitNote.listFile({
|
|
group: 'slip',
|
|
parentId: debitNoteId,
|
|
})
|
|
: await debitNote.listAttachment({
|
|
parentId: debitNoteId,
|
|
});
|
|
if (list)
|
|
slip
|
|
? (fileData.value = await Promise.all(
|
|
list.map(async (v) => {
|
|
const rse = await debitNote.headFile({
|
|
group: 'slip',
|
|
parentId: debitNoteId,
|
|
fileId: v,
|
|
});
|
|
|
|
let contentLength = 0;
|
|
if (rse) contentLength = Number(rse['content-length']);
|
|
|
|
return {
|
|
name: v,
|
|
progress: 1,
|
|
loaded: contentLength,
|
|
total: contentLength,
|
|
url: `/debit-note/${debitNoteId}/file-slip/${v}`,
|
|
};
|
|
}),
|
|
))
|
|
: (attachmentData.value = await Promise.all(
|
|
list.map(async (v) => {
|
|
const rse = await debitNote.headAttachment({
|
|
parentId: debitNoteId,
|
|
fileId: v,
|
|
});
|
|
|
|
let contentLength = 0;
|
|
if (rse) contentLength = Number(rse['content-length']);
|
|
|
|
return {
|
|
name: v,
|
|
progress: 1,
|
|
loaded: contentLength,
|
|
total: contentLength,
|
|
url: `/debit-note/${debitNoteId}/attachment/${v}`,
|
|
};
|
|
}),
|
|
));
|
|
}
|
|
|
|
async function uploadFile(debitNoteId: string, list: FileList, slip?: boolean) {
|
|
const promises: ReturnType<
|
|
typeof debitNote.putAttachment | typeof debitNote.putFile
|
|
>[] = [];
|
|
|
|
if (!slip) {
|
|
attachmentData.value = attachmentData.value.filter((v) => !v.placeholder);
|
|
}
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
const data = {
|
|
name: list[i].name,
|
|
progress: 1,
|
|
loaded: 0,
|
|
total: 0,
|
|
url: `/debit-note/${debitNoteId}/${slip ? 'file-slip' : 'attachment'}/${list[i].name}`,
|
|
};
|
|
promises.push(
|
|
slip
|
|
? debitNote.putFile({
|
|
group: 'slip',
|
|
parentId: debitNoteId,
|
|
fileId: list[i].name,
|
|
file: list[i],
|
|
uploadUrl: true,
|
|
onUploadProgress: (e) => {
|
|
const exists = fileData?.value.find((v) => v.name === data.name);
|
|
if (!exists) return fileData?.value.push(data);
|
|
exists.total = e.total || 0;
|
|
exists.progress = e.progress || 0;
|
|
exists.loaded = e.loaded;
|
|
},
|
|
})
|
|
: debitNote.putAttachment({
|
|
parentId: debitNoteId,
|
|
name: list[i].name,
|
|
file: list[i],
|
|
onUploadProgress: (e) => {
|
|
const exists = attachmentData?.value.find(
|
|
(v) => v.name === data.name,
|
|
);
|
|
if (!exists) return attachmentData?.value.push(data);
|
|
exists.total = e.total || 0;
|
|
exists.progress = e.progress || 0;
|
|
exists.loaded = e.loaded;
|
|
},
|
|
}),
|
|
);
|
|
slip ? fileData?.value.push(data) : attachmentData?.value.push(data);
|
|
}
|
|
fileList.value = undefined;
|
|
attachmentList.value = undefined;
|
|
|
|
const beforeUnloadHandler = (e: Event) => {
|
|
e.preventDefault();
|
|
};
|
|
|
|
window.addEventListener('beforeunload', beforeUnloadHandler);
|
|
|
|
return await Promise.all(promises).then((v) => {
|
|
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
|
return v;
|
|
});
|
|
}
|
|
|
|
async function remove(debitNoteId: string, n: string, slip?: boolean) {
|
|
dialogWarningClose(t, {
|
|
message: t('dialog.message.confirmDelete'),
|
|
actionText: t('dialog.action.ok'),
|
|
action: async () => {
|
|
const res = slip
|
|
? await debitNote.delFile({
|
|
group: 'slip',
|
|
parentId: debitNoteId,
|
|
fileId: n,
|
|
})
|
|
: await debitNote.delAttachment({
|
|
parentId: debitNoteId,
|
|
name: n,
|
|
});
|
|
if (res) {
|
|
getFileList(debitNoteId, slip);
|
|
}
|
|
},
|
|
cancel: () => {},
|
|
});
|
|
}
|
|
|
|
async function submit() {
|
|
const payload: DebitNotePayload = {
|
|
id: currentFormData.value.id || undefined,
|
|
productServiceList: productServiceList.value.map((v) => ({
|
|
installmentNo: v.installmentNo,
|
|
workerIndex: v.workerIndex,
|
|
discount: v.discount,
|
|
amount: v.amount,
|
|
productId: v.product.id,
|
|
workId: v.work?.id || '',
|
|
serviceId: v.service?.id || '',
|
|
})),
|
|
|
|
worker: JSON.parse(
|
|
JSON.stringify([
|
|
...selectedWorkerItem.value.map((v) => {
|
|
{
|
|
return v.id;
|
|
}
|
|
}),
|
|
]),
|
|
),
|
|
dueDate: currentFormData.value.dueDate,
|
|
payCondition: currentFormData.value.payCondition,
|
|
discount: currentFormData.value.discount,
|
|
remark: currentFormData.value.remark || undefined,
|
|
agentPrice: agentPrice.value,
|
|
quotationId: quotationData.value?.id || '',
|
|
};
|
|
|
|
const res =
|
|
pageState.mode === 'edit'
|
|
? await debitNote.updateDebitNote(payload)
|
|
: await debitNote.createDebitNote(payload);
|
|
|
|
if (res) {
|
|
newWorkerList.value = [];
|
|
await router.push(`/debit-note/${res.id}/?mode=info`);
|
|
|
|
if (attachmentList.value && pageState.mode === 'create') {
|
|
await uploadFile(res.id, attachmentList.value, false);
|
|
}
|
|
|
|
pageState.mode = 'info';
|
|
assignFormData(res.id);
|
|
|
|
initStatus();
|
|
}
|
|
}
|
|
|
|
function toggleMode(mode: 'create' | 'edit' | 'info') {
|
|
const currentMode = mode || route.query.mode;
|
|
const newMode = currentMode; // เปลี่ยนค่า 'edit' เป็น 'view' และกลับกัน
|
|
|
|
router.push({
|
|
path: route.path,
|
|
query: { ...route.query, mode: newMode },
|
|
});
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
function storeDataLocal() {
|
|
// quotationFormData.value.productServiceList = productServiceList.value;
|
|
//
|
|
|
|
localStorage.setItem(
|
|
'debit-note-preview',
|
|
JSON.stringify({
|
|
data: {
|
|
...currentFormData.value,
|
|
customerBranchId: quotationData.value?.customerBranchId,
|
|
registeredBranchId: quotationData.value?.registeredBranchId,
|
|
agentPrice: quotationData.value.agentPrice,
|
|
},
|
|
meta: {
|
|
source: {
|
|
code: pageState.mode === 'create' ? '-' : debitNoteData.value?.code,
|
|
createAt:
|
|
pageState.mode === 'create'
|
|
? Date.now()
|
|
: debitNoteData.value?.createdAt,
|
|
createBy: debitNoteData.value?.createdBy,
|
|
payCondition: currentFormData.value.payCondition,
|
|
contactName: quotationData.value?.contactName,
|
|
contactTel: quotationData.value?.contactTel,
|
|
workName: quotationData.value?.workName,
|
|
dueDate: currentFormData.value.dueDate,
|
|
},
|
|
productServicelist: productService.value,
|
|
selectedWorker: selectedWorkerItem.value,
|
|
createdBy: getName(),
|
|
},
|
|
}),
|
|
);
|
|
|
|
const url = new URL('/debit-note/document-view', window.location.origin);
|
|
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
async function closeTab() {
|
|
dialogWarningClose(t, {
|
|
message: t('dialog.message.close'),
|
|
action: () => {
|
|
window.close();
|
|
},
|
|
cancel: () => {},
|
|
});
|
|
}
|
|
|
|
function closeAble() {
|
|
return window.opener !== null;
|
|
}
|
|
|
|
function combineWorker(newWorker: any, oldWorker: any) {
|
|
selectedWorkerItem.value = [
|
|
...oldWorker.map((e) => ({
|
|
id: e.id,
|
|
foreignRefNo: e.employeePassport
|
|
? e.employeePassport[0]?.number || '-'
|
|
: '-',
|
|
employeeName:
|
|
locale.value === Lang.English
|
|
? `${e.firstNameEN} ${e.lastNameEN}`
|
|
: `${e.firstName || e.firstNameEN} ${e.lastName || e.lastNameEN}`,
|
|
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,
|
|
workerNew: false,
|
|
lastNameEN: e.lastNameEN,
|
|
lastName: e.lastName,
|
|
middleNameEN: e.middleNameEN,
|
|
middleName: e.middleName,
|
|
firstNameEN: e.firstNameEN,
|
|
firstName: e.firstName,
|
|
namePrefix: e.namePrefix,
|
|
})),
|
|
|
|
...newWorker.map((v: any) => ({
|
|
id: v.id,
|
|
foreignRefNo: v.passportNo || '-',
|
|
employeeName:
|
|
locale.value === Lang.English
|
|
? `${v.firstNameEN} ${v.lastNameEN}`
|
|
: `${v.firstName || v.firstNameEN} ${v.lastName || v.lastNameEN}`,
|
|
birthDate: dateFormatJS({ date: v.dateOfBirth }),
|
|
gender: v.gender,
|
|
age: calculateAge(v.dateOfBirth),
|
|
nationality: optionStore.mapOption(v.nationality),
|
|
documentExpireDate: '-',
|
|
imgUrl: '',
|
|
status: 'CREATED',
|
|
|
|
lastNameEN: v.lastNameEN,
|
|
lastName: v.lastName,
|
|
middleNameEN: v.middleNameEN,
|
|
middleName: v.middleName,
|
|
firstNameEN: v.firstNameEN,
|
|
firstName: v.firstName,
|
|
namePrefix: v.namePrefix,
|
|
|
|
dateOfBirth: v.dateOfBirth,
|
|
workerNew: true,
|
|
})),
|
|
];
|
|
}
|
|
|
|
watch(
|
|
() => pageState.mode,
|
|
() => toggleMode(pageState.mode),
|
|
);
|
|
|
|
watch(
|
|
[() => pageState.inputSearchRequest, () => pageState.statusFilterRequest],
|
|
() => {
|
|
fetchRequest();
|
|
},
|
|
);
|
|
|
|
onMounted(async () => {
|
|
initTheme();
|
|
initLang();
|
|
initStatus();
|
|
|
|
if (!!route.params['id'] && typeof route.params['id'] === 'string') {
|
|
await assignFormData(route.params['id']);
|
|
}
|
|
|
|
if (typeof route.query['quotationId'] === 'string') {
|
|
pageState.mode = 'create';
|
|
await getQuotation(route.query['quotationId']);
|
|
}
|
|
|
|
if (typeof route.query['mode'] === 'string') {
|
|
pageState.mode =
|
|
(route.query['mode'] as 'create' | 'edit' | 'info') || 'info';
|
|
}
|
|
|
|
if (typeof route.query['tab'] === 'string') {
|
|
view.value =
|
|
{
|
|
payment: QuotationStatus.PaymentPending,
|
|
receipt: QuotationStatus.PaymentSuccess,
|
|
}[route.query['tab']] || QuotationStatus.Issued;
|
|
}
|
|
|
|
if (
|
|
pageState.mode === 'edit' &&
|
|
debitNoteData.value?.quotationStatus !== QuotationStatus.Issued
|
|
) {
|
|
pageState.mode = 'info';
|
|
}
|
|
await useConfigStore().getConfig();
|
|
});
|
|
|
|
async function submitAccepted() {
|
|
dialog({
|
|
color: 'info',
|
|
icon: 'mdi-account-check',
|
|
title: t('dialog.title.confirmDebitNoteAccept'),
|
|
actionText: t('general.confirm'),
|
|
persistent: true,
|
|
message: t('dialog.message.quotationAccept'),
|
|
action: async () => {
|
|
if (!currentFormData.value.id) return;
|
|
|
|
const res = await debitNote.action.acceptDebitNote(
|
|
currentFormData.value.id,
|
|
);
|
|
|
|
if (res && typeof route.params['id'] === 'string') {
|
|
await assignFormData(route.params['id']);
|
|
}
|
|
},
|
|
cancel: () => {},
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="column surface-0 fullscreen">
|
|
<div class="color-bar" :class="{ ['dark']: $q.dark.isActive }">
|
|
<div :class="{ ['indigo-segment']: true }"></div>
|
|
<div :class="{ ['light-indigo-segment']: true }"></div>
|
|
<div class="white-segment"></div>
|
|
</div>
|
|
|
|
<!-- SEC: Header -->
|
|
<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="/debit-note">
|
|
<q-img src="/icons/favicon-512x512.png" width="3rem" />
|
|
</RouterLink>
|
|
<span class="column text-h6 text-bold q-ml-md">
|
|
{{ $t('debitNote.title') }}
|
|
<!-- {{ code || '' }} -->
|
|
<span class="text-caption text-regular app-text-muted">
|
|
{{
|
|
$t('quotation.processOn', {
|
|
msg: dateFormatJS({
|
|
date: debitNoteData?.createdAt || new Date(Date.now()),
|
|
monthStyle: 'long',
|
|
}),
|
|
})
|
|
}}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- SEC: Body -->
|
|
<article
|
|
class="col full-width q-pa-md"
|
|
style="flex-grow: 1; overflow-y: auto"
|
|
>
|
|
<section class="col-sm col-12">
|
|
<div class="col q-gutter-y-md">
|
|
<nav
|
|
class="surface-1 q-pa-sm row no-wrap full-width scroll rounded"
|
|
style="gap: 10px"
|
|
>
|
|
<StateButton
|
|
v-for="i in statusTabForm"
|
|
:key="i.title"
|
|
:label="
|
|
$t(
|
|
`debitNote${i.title === 'title' ? '' : '.viewMode'}.${i.title}`,
|
|
)
|
|
"
|
|
:status-active="i.active?.()"
|
|
:status-done="i.status === 'done'"
|
|
:status-waiting="i.status === 'waiting'"
|
|
@click="
|
|
() => {
|
|
if (pageState.mode !== 'create') i.handler();
|
|
}
|
|
"
|
|
/>
|
|
</nav>
|
|
<!-- #TODO add goToQuotation as @goto-quotation-->
|
|
|
|
<DocumentExpansion
|
|
v-if="
|
|
view === QuotationStatus.Issued ||
|
|
view === QuotationStatus.Accepted
|
|
"
|
|
:readonly
|
|
:registered-branch-id="quotationData?.registeredBranchId"
|
|
:customer-id="quotationData?.customerBranchId"
|
|
:quotation-code="quotationData?.code || '-'"
|
|
:quotation-work-name="quotationData?.workName || '-'"
|
|
:quotation-contact-name="quotationData?.contactName || '-'"
|
|
:quotation-contact-tel="quotationData?.contactTel || '-'"
|
|
v-model:due-date="currentFormData.dueDate"
|
|
@goto-quotation="() => goToQuotation()"
|
|
/>
|
|
|
|
<DebitNoteExpansion
|
|
v-if="false"
|
|
:readonly
|
|
v-model:reason="currentFormData.reason"
|
|
v-model:detail="currentFormData.detail"
|
|
/>
|
|
|
|
<PaymentForm
|
|
v-if="view === QuotationStatus.PaymentPending"
|
|
is-debit-note
|
|
:branch-id="quotationData?.registeredBranchId"
|
|
:readonly="isRoleInclude(['sale', 'head_of_sale'])"
|
|
:data="debitNoteData"
|
|
@fetch-status="
|
|
() => {
|
|
if (!!debitNoteData) assignFormData(debitNoteData?.id);
|
|
}
|
|
"
|
|
/>
|
|
|
|
<WorkerItemExpansion
|
|
v-if="
|
|
view === QuotationStatus.Issued ||
|
|
view === QuotationStatus.Accepted
|
|
"
|
|
:readonly
|
|
:hide-btn-add-worker="
|
|
!readonly &&
|
|
(!currentFormData || currentFormData.quotationStatus === 'Issued')
|
|
"
|
|
:row-worker="selectedWorkerItem"
|
|
@add-worker="() => (pageState.employeeModal = true)"
|
|
@delete="(i) => deleteItem(selectedWorkerItem, i)"
|
|
/>
|
|
|
|
<!-- #TODO add openProductDialog at @add-product-->
|
|
<ProductExpansion
|
|
v-if="
|
|
view === QuotationStatus.Issued ||
|
|
view === QuotationStatus.Accepted
|
|
"
|
|
:readonly
|
|
:installment-input="currentFormData.payCondition === 'SplitCustom'"
|
|
:max-installment="currentFormData.paySplitCount"
|
|
:agent-price="agentPrice"
|
|
:employee-rows="selectedWorkerItem"
|
|
:rows="productService"
|
|
@add-product="
|
|
() => {
|
|
pageState.productServiceModal = true;
|
|
covertToNode();
|
|
}
|
|
"
|
|
@update-rows="
|
|
(v) => {
|
|
view === null && (productServiceList = v);
|
|
}
|
|
"
|
|
@delete="toggleDeleteProduct"
|
|
@view-file="viewProductFile"
|
|
/>
|
|
|
|
<PaymentExpansion
|
|
v-if="
|
|
view === QuotationStatus.Issued ||
|
|
view === QuotationStatus.Accepted
|
|
"
|
|
:total-price="summaryPrice.finalPrice"
|
|
:pay-split="[]"
|
|
class="q-mb-md"
|
|
v-model:pay-type="currentFormData.payCondition"
|
|
v-model:pay-split-count="currentFormData.paySplitCount"
|
|
v-model:final-discount="currentFormData.discount"
|
|
v-model:pay-bill-date="currentFormData.payBillDate"
|
|
v-model:summary-price="summaryPrice"
|
|
/>
|
|
|
|
<!-- TODO: bind additional file -->
|
|
<AdditionalFileExpansion
|
|
v-if="
|
|
view === QuotationStatus.Issued ||
|
|
view === QuotationStatus.Accepted ||
|
|
view === QuotationStatus.PaymentPending
|
|
"
|
|
:readonly="isRoleInclude(['sale', 'head_of_sale'])"
|
|
v-model:file-data="attachmentData"
|
|
:transform-url="
|
|
async (url: string) => {
|
|
const result = await api.get<string>(url);
|
|
return result.data;
|
|
}
|
|
"
|
|
@fetch-file-list="
|
|
() => {
|
|
if (!debitNoteData) return;
|
|
getFileList(debitNoteData.id);
|
|
}
|
|
"
|
|
@upload="
|
|
async (f) => {
|
|
attachmentList = f;
|
|
attachmentData = [];
|
|
|
|
Array.from(f).forEach(({ name }) => {
|
|
attachmentData.push({
|
|
name: name,
|
|
progress: 1,
|
|
loaded: 0,
|
|
total: 0,
|
|
placeholder: true,
|
|
});
|
|
});
|
|
|
|
if (!debitNoteData) return;
|
|
|
|
await uploadFile(debitNoteData.id, f);
|
|
}
|
|
"
|
|
@remove="
|
|
async (n) => {
|
|
if (!debitNoteData) return;
|
|
await remove(debitNoteData.id, n);
|
|
}
|
|
"
|
|
/>
|
|
|
|
<RemarkExpansion
|
|
v-if="
|
|
view === QuotationStatus.Issued ||
|
|
view === QuotationStatus.Accepted ||
|
|
view === QuotationStatus.PaymentPending
|
|
"
|
|
:readonly="readonly"
|
|
:final-price="summaryPrice.finalPrice"
|
|
:selected-worker
|
|
v-model:remark="currentFormData.remark"
|
|
/>
|
|
|
|
<main
|
|
v-if="view === QuotationStatus.ProcessComplete"
|
|
class="surface-1 q-pa-md full-width q-gutter-y-md rounded"
|
|
>
|
|
<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="pageState.inputSearchRequest"
|
|
debounce="200"
|
|
>
|
|
<template #prepend>
|
|
<q-icon name="mdi-magnify" />
|
|
</template>
|
|
</q-input>
|
|
|
|
<q-select
|
|
v-model="pageState.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 :rows="rowsRequestList" />
|
|
</main>
|
|
|
|
<template v-if="view === QuotationStatus.PaymentSuccess">
|
|
<QuotationFormReceipt
|
|
hide-view-btn
|
|
v-for="(v, i) in receiptList"
|
|
:key="i"
|
|
:amount="v.amount"
|
|
:date="v.date"
|
|
:pay-type="quotationData?.payCondition"
|
|
:index="i"
|
|
:pay-split-count="quotationData?.paySplitCount || 0"
|
|
@view="
|
|
() => {
|
|
if (quotationData?.payCondition !== 'Full') {
|
|
selectedInstallmentNo = [v.invoice.installments[0].no];
|
|
installmentAmount = v.invoice.amount;
|
|
}
|
|
|
|
view = QuotationStatus.Issued;
|
|
}
|
|
"
|
|
@example="() => exampleReceipt(v.id)"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</section>
|
|
</article>
|
|
|
|
<!-- SEC: footer -->
|
|
<footer class="surface-1 q-pa-md full-width">
|
|
<nav class="row justify-end">
|
|
<!-- TODO: view example -->
|
|
<MainButton
|
|
class="q-mr-auto"
|
|
outlined
|
|
icon="mdi-play-box-outline"
|
|
color="207 96% 32%"
|
|
@click="storeDataLocal()"
|
|
>
|
|
{{ $t('general.view', { msg: $t('general.example') }) }}
|
|
</MainButton>
|
|
|
|
<CancelButton
|
|
outlined
|
|
id="btn-close"
|
|
@click="closeTab()"
|
|
:label="$t('dialog.action.close')"
|
|
v-if="
|
|
(pageState.mode === 'info' || pageState.mode === 'create') &&
|
|
closeAble()
|
|
"
|
|
/>
|
|
|
|
<div class="row q-gutter-x-sm q-ml-xs">
|
|
<UndoButton
|
|
outlined
|
|
v-if="pageState.mode === 'edit'"
|
|
@click="undo()"
|
|
/>
|
|
<SaveButton
|
|
v-if="pageState.mode !== 'info'"
|
|
:disabled="
|
|
selectedWorkerItem.length === 0 && productService.length === 0
|
|
"
|
|
@click="submit"
|
|
solid
|
|
/>
|
|
|
|
<template v-if="debitNoteData">
|
|
<EditButton
|
|
v-if="
|
|
pageState.mode === 'info' &&
|
|
view === QuotationStatus.Issued &&
|
|
debitNoteData.quotationStatus === QuotationStatus.Issued
|
|
"
|
|
class="no-print"
|
|
solid
|
|
@click="pageState.mode = 'edit'"
|
|
/>
|
|
|
|
<MainButton
|
|
v-if="
|
|
view === QuotationStatus.Accepted &&
|
|
debitNoteData.quotationStatus === QuotationStatus.Issued
|
|
"
|
|
solid
|
|
icon="mdi-account-multiple-check-outline"
|
|
color="207 96% 32%"
|
|
id="btn-submit-accepted"
|
|
@click="
|
|
() => {
|
|
submitAccepted();
|
|
}
|
|
"
|
|
>
|
|
{{ $t('quotation.customerAcceptance') }}
|
|
</MainButton>
|
|
</template>
|
|
</div>
|
|
</nav>
|
|
</footer>
|
|
</div>
|
|
|
|
<!-- SEC: Dialog -->
|
|
<!-- add employee quotation -->
|
|
|
|
<QuotationFormWorkerSelect
|
|
:preselect-worker="selectedWorkerItem"
|
|
:customerBranchId="quotationData?.customerBranchId"
|
|
v-model:open="pageState.employeeModal"
|
|
@success="
|
|
(v) => {
|
|
combineWorker(v.newWorker, v.worker);
|
|
}
|
|
"
|
|
/>
|
|
|
|
<!-- add product service -->
|
|
<QuotationFormProductSelect
|
|
v-model="pageState.productServiceModal"
|
|
v-model:nodes="productServiceNodes"
|
|
v-model:product-group="productGroup"
|
|
v-model:product-list="productList"
|
|
v-model:service-list="serviceList"
|
|
v-model:selected-product-group="selectedProductGroup"
|
|
:agent-price="agentPrice"
|
|
@submit="convertToTable"
|
|
@select-group="
|
|
async (id) => {
|
|
await getAllService(id);
|
|
await getAllProduct(id);
|
|
}
|
|
"
|
|
@search="
|
|
(id, text, mode) => {
|
|
if (mode === 'service') {
|
|
getAllService(id, { force: true, query: text, pageSize: 50 });
|
|
}
|
|
if (mode === 'product') {
|
|
getAllProduct(id, { force: true, query: text, pageSize: 50 });
|
|
}
|
|
}
|
|
"
|
|
></QuotationFormProductSelect>
|
|
|
|
<SelectReadyRequestWork
|
|
v-if="quotationData"
|
|
creditNote
|
|
:fetch-params="{ cancelOnly: true, quotationId: quotationData.id }"
|
|
v-model:open="pageState.productDialog"
|
|
v-model:task-list="formTaskList"
|
|
/>
|
|
|
|
<DialogViewFile
|
|
hide-tab
|
|
download
|
|
v-model="pageState.fileDialog"
|
|
:url="fileData[0]?.url"
|
|
:transform-url="
|
|
async (url: string) => {
|
|
const result = await api.get<string>(url);
|
|
return result.data;
|
|
}
|
|
"
|
|
/>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.color-bar {
|
|
width: 100%;
|
|
height: 1vh;
|
|
background: linear-gradient(
|
|
90deg,
|
|
rgb(47, 68, 173) 0%,
|
|
rgba(255, 255, 255, 1) 77%,
|
|
rgba(204, 204, 204, 1) 100%
|
|
);
|
|
display: flex;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.color-bar.dark {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.indigo-segment {
|
|
background-color: var(--cyan-7);
|
|
flex-grow: 4;
|
|
}
|
|
|
|
.light-indigo-segment {
|
|
background-color: hsla(var(--indigo-10-hsl) / 0.2);
|
|
flex-grow: 0.5;
|
|
}
|
|
|
|
.white-segment {
|
|
background-color: #ffffff;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.indigo-segment,
|
|
.light-indigo-segment,
|
|
.white-segment {
|
|
transform: skewX(-60deg);
|
|
}
|
|
|
|
.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 {
|
|
filter: invert(100%);
|
|
}
|
|
}
|
|
</style>
|