{
{
label: 'menu.account',
icon: 'mdi-bank-outline',
- disabled: true,
+ disabled: false,
children: [
- { label: 'uploadSlip', route: '' },
- { label: 'receipt', route: '' },
- { label: 'creditNote', route: '' },
- { label: 'debitNote', route: '' },
+ { label: 'uploadSlip', route: '', disabled: true },
+ { label: 'receipt', route: '', disabled: true },
+ { label: 'creditNote', route: '/credit-note' },
+ { label: 'debitNote', route: '', disabled: true },
],
},
diff --git a/src/pages/05_quotation/MainPage.vue b/src/pages/05_quotation/MainPage.vue
index 8c09854f..64b1b7c2 100644
--- a/src/pages/05_quotation/MainPage.vue
+++ b/src/pages/05_quotation/MainPage.vue
@@ -14,7 +14,7 @@ import { useQuotationForm } from './form';
import { hslaColors } from './constants';
// NOTE Import Types
-import { CustomerBranchCreate } from 'stores/customer/types';
+import { CustomerBranchCreate, CustomerType } from 'stores/customer/types';
// NOTE: Import Components
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
@@ -181,7 +181,7 @@ async function triggerDialogDeleteQuottaion(id: string) {
});
}
-function triggerCreateCustomerd(opts: { type: 'CORP' | 'PERS' }) {
+function triggerCreateCustomerd(opts: { type: CustomerType }) {
setDefaultCustomer();
customerFormState.value.dialogType = 'create';
customerFormData.value.customerType = opts?.type;
@@ -656,6 +656,8 @@ async function storeDataLocal(id: string) {
(),
- { payType: 'Full', amount: 0, date: () => new Date() },
+ { payType: 'Full', amount: 0, date: () => new Date(), index: 0 },
);
- {{ calculateAge(props.row.request.employee.dateOfBirth) }}
+ {{ calculateAge(props.row.request.employee?.dateOfBirth) }}
{{
- useOptionStore().mapOption(props.row.request.employee.nationality)
+ useOptionStore().mapOption(props.row.request.employee?.nationality)
}}
{{
dateFormatJS({
- date: props.row.request.quotation.dueDate,
+ date: props.row.request.quotation?.dueDate,
locale: $i18n.locale,
dayStyle: '2-digit',
monthStyle: 'short',
@@ -436,7 +453,7 @@ function handleCheck(
@@ -444,12 +461,12 @@ function handleCheck(
class="cursor-pointer link"
@click="goToQuotation(props.row.request.quotation)"
>
- {{ props.row.request.quotation.code }}
+ {{ props.row.request.quotation?.code }}
+
+
+
diff --git a/src/pages/09_task-order/expansion/AdditionalFileExpansion.vue b/src/pages/09_task-order/expansion/AdditionalFileExpansion.vue
index e6614f88..b14ee28a 100644
--- a/src/pages/09_task-order/expansion/AdditionalFileExpansion.vue
+++ b/src/pages/09_task-order/expansion/AdditionalFileExpansion.vue
@@ -19,6 +19,7 @@ const fileData = defineModel<
loaded: number;
total: number;
url?: string;
+ placeholder?: boolean;
}[]
>('fileData', { default: [] });
diff --git a/src/pages/09_task-order/expansion/ProductExpansion.vue b/src/pages/09_task-order/expansion/ProductExpansion.vue
index e5fe1d34..734f616f 100644
--- a/src/pages/09_task-order/expansion/ProductExpansion.vue
+++ b/src/pages/09_task-order/expansion/ProductExpansion.vue
@@ -26,12 +26,14 @@ const taskProduct = defineModel<{ productId: string; discount?: number }[]>(
},
);
-defineProps<{
+const props = defineProps<{
readonly?: boolean;
+ agentPrice?: boolean;
taskList: {
product: RequestWork['productService']['product'];
list: RequestWork[];
}[];
+ creditNote?: boolean;
}>();
defineEmits<{
@@ -55,15 +57,28 @@ function openList(index: number) {
function calcPricePerUnit(product: RequestWork['productService']['product']) {
return product.vatIncluded
- ? product.serviceCharge / (1 + (config.value?.vat || 0.07))
- : product.serviceCharge;
+ ? (props.creditNote
+ ? props.agentPrice
+ ? product.agentPrice
+ : product.price
+ : product.serviceCharge) /
+ (1 + (config.value?.vat || 0.07))
+ : props.creditNote
+ ? props.agentPrice
+ ? product.agentPrice
+ : product.price
+ : product.serviceCharge;
}
function calcPrice(
product: RequestWork['productService']['product'],
amount: number,
) {
- const pricePerUnit = product.serviceCharge;
+ const pricePerUnit = props.creditNote
+ ? props.agentPrice
+ ? product.agentPrice
+ : product.price
+ : product.serviceCharge;
const discount =
taskProduct.value.find((v) => v.productId === product.id)?.discount || 0;
const priceNoVat = product.vatIncluded
@@ -102,7 +117,16 @@ function calcPrice(
-
+
-
+
{{ formatNumberDecimal(calcPricePerUnit(props.row.product), 2) }}
-
+
{{
formatNumberDecimal(
props.row.product.calcVat
@@ -267,7 +291,11 @@ function calcPrice(
-
+
diff --git a/src/pages/09_task-order/order_view/MainPage.vue b/src/pages/09_task-order/order_view/MainPage.vue
index 677720f9..96896b04 100644
--- a/src/pages/09_task-order/order_view/MainPage.vue
+++ b/src/pages/09_task-order/order_view/MainPage.vue
@@ -1130,6 +1130,7 @@ watch([currentFormData.value.taskStatus], () => {
+import { ref, onMounted, reactive, computed } from 'vue';
+import { api } from 'src/boot/axios';
+import { useRoute, useRouter } from 'vue-router';
+import { dateFormatJS } from 'src/utils/datetime';
+import { initLang, initTheme } from 'src/utils/ui';
+import { useQuotationStore } from 'src/stores/quotations';
+import {
+ CreditNote,
+ CreditNotePaybackStatus,
+ CreditNotePayload,
+ CreditNoteStatus,
+ useCreditNote,
+} from 'src/stores/credit-note';
+import { useConfigStore } from 'src/stores/config';
+import DocumentExpansion from './expansion/DocumentExpansion.vue';
+import RemarkExpansion from '../09_task-order/expansion/RemarkExpansion.vue';
+import AdditionalFileExpansion from '../09_task-order/expansion/AdditionalFileExpansion.vue';
+import PaymentExpansion from './expansion/PaymentExpansion.vue';
+import CreditNoteExpansion from './expansion/CreditNoteExpansion.vue';
+import StateButton from 'src/components/button/StateButton.vue';
+import ProductExpansion from '../09_task-order/expansion/ProductExpansion.vue';
+import SelectReadyRequestWork from '../09_task-order/SelectReadyRequestWork.vue';
+import RefundInformation from './RefundInformation.vue';
+import QuotationFormReceipt from '../05_quotation/QuotationFormReceipt.vue';
+import DialogViewFile from 'src/components/dialog/DialogViewFile.vue';
+import { MainButton, SaveButton } from 'src/components/button';
+import { RequestWork } from 'src/stores/request-list/types';
+import { storeToRefs } from 'pinia';
+import useOptionStore from 'src/stores/options';
+import { dialogWarningClose } from 'src/stores/utils';
+import { useI18n } from 'vue-i18n';
+
+const route = useRoute();
+const router = useRouter();
+const creditNote = useCreditNote();
+const quotation = useQuotationStore();
+const configStore = useConfigStore();
+const { data: config } = storeToRefs(configStore);
+const { t } = useI18n();
+
+const creditNoteData = ref();
+const quotationData = ref();
+const view = ref(null);
+const fileList = ref();
+const attachmentList = ref();
+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 readonly = computed(
+ () =>
+ creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Pending ||
+ creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Success,
+);
+
+const pageState = reactive({
+ productDialog: false,
+ fileDialog: false,
+});
+
+const currentFormData = ref({
+ quotationId: '',
+ requestWorkId: [],
+ reason: '',
+ detail: '',
+ paybackType: 'Cash',
+ paybackBank: '',
+ paybackAccount: '',
+ paybackAccountName: '',
+});
+
+const formTaskList = ref<
+ {
+ requestWorkId: string;
+ requestWorkStep?: { requestWork: RequestWork };
+ }[]
+>([]);
+
+let taskListGroup = computed(() => {
+ const cacheData = formTaskList.value.reduce<
+ {
+ product: RequestWork['productService']['product'];
+ list: RequestWork[];
+ }[]
+ >((acc, curr) => {
+ const task = curr.requestWorkStep;
+
+ if (!task) return acc;
+
+ if (task.requestWork) {
+ let exist = acc.find(
+ (item) => task.requestWork.productService.productId == item.product.id,
+ );
+ const record = Object.assign(task.requestWork);
+
+ if (exist) {
+ exist.list.push(task.requestWork);
+ } else {
+ acc.push({
+ product: task.requestWork.productService.product,
+ list: [record],
+ });
+ }
+ }
+
+ return acc;
+ }, []);
+
+ return cacheData;
+});
+
+const summaryPrice = computed(() => getPrice(taskListGroup.value));
+
+async function initStatus() {
+ if (creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Pending)
+ view.value = CreditNoteStatus.Pending;
+ if (creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Success)
+ view.value = CreditNoteStatus.Success;
+
+ statusTabForm.value = [
+ {
+ title: 'title',
+ status: creditNoteData.value?.id !== undefined ? 'done' : 'doing',
+ active: () => view.value === null,
+ handler: () => {
+ view.value = null;
+ },
+ },
+ {
+ title: 'Pending',
+ status: creditNoteData.value?.id
+ ? creditNoteData.value.creditNoteStatus === CreditNoteStatus.Success
+ ? 'done'
+ : 'doing'
+ : 'waiting',
+ active: () => view.value === CreditNoteStatus.Pending,
+ handler: async () => {
+ view.value = CreditNoteStatus.Pending;
+ creditNoteData.value &&
+ (await getFileList(creditNoteData.value.id, true));
+ },
+ },
+ {
+ title: 'Success',
+ status:
+ creditNoteData.value?.id &&
+ creditNoteData.value.creditNoteStatus === CreditNoteStatus.Success
+ ? 'done'
+ : 'waiting',
+ active: () => view.value === CreditNoteStatus.Success,
+ handler: () => {
+ view.value = CreditNoteStatus.Success;
+ },
+ },
+ ];
+}
+
+function getPrice(
+ list: {
+ product: RequestWork['productService']['product'];
+ list: RequestWork[];
+ }[],
+) {
+ return list.reduce(
+ (a, c) => {
+ const pricePerUnit = quotationData.value?.agentPrice
+ ? c.product.agentPrice
+ : c.product.price;
+ const amount = c.list.length;
+ const discount = 0;
+ const priceNoVat = c.product.vatIncluded
+ ? pricePerUnit / (1 + (config.value?.vat || 0.07))
+ : pricePerUnit;
+ const priceDiscountNoVat = priceNoVat * amount - discount;
+
+ const rawVatTotal = priceDiscountNoVat * (config.value?.vat || 0.07);
+ // const rawVat = rawVatTotal / amount;
+
+ a.totalPrice = a.totalPrice + priceDiscountNoVat;
+ a.totalDiscount = a.totalDiscount + Number(discount);
+ a.vat = c.product.calcVat ? a.vat + rawVatTotal : a.vat;
+ a.vatExcluded = c.product.calcVat ? a.vatExcluded : a.vat + rawVatTotal;
+ a.finalPrice = a.totalPrice - a.totalDiscount + a.vat;
+ return a;
+ },
+ {
+ totalPrice: 0,
+ totalDiscount: 0,
+ vat: 0,
+ vatExcluded: 0,
+ finalPrice: 0,
+ },
+ );
+}
+
+function openProductDialog() {
+ pageState.productDialog = true;
+}
+
+async function getCreditNote() {
+ if (typeof route.params['id'] !== 'string') return;
+
+ const ret = await creditNote.getCreditNote(route.params['id']);
+
+ if (!ret) return;
+
+ creditNoteData.value = ret;
+ assignFormData();
+}
+
+function assignFormData() {
+ if (!creditNoteData.value) return;
+
+ const current = creditNoteData.value;
+
+ currentFormData.value = {
+ quotationId: creditNoteData.value.quotationId,
+ requestWorkId: creditNoteData.value.requestWork.map((v) => v.id || ''),
+ reason: creditNoteData.value.reason,
+ detail: creditNoteData.value.detail,
+ paybackType: creditNoteData.value.paybackType,
+ paybackBank: creditNoteData.value.paybackBank,
+ paybackAccount: creditNoteData.value.paybackAccount,
+ paybackAccountName: creditNoteData.value.paybackAccountName,
+ };
+
+ formTaskList.value = creditNoteData.value.requestWork.map((v) => ({
+ requestWorkId: v.id || '',
+ requestWorkStep: {
+ requestWork: {
+ ...v,
+ request: { ...v.request, quotation: current.quotation },
+ },
+ },
+ }));
+}
+
+function openSlipDialog() {
+ pageState.fileDialog = true;
+}
+
+async function getQuotation() {
+ if (creditNoteData.value) {
+ quotationData.value = creditNoteData.value.quotation;
+ return;
+ }
+
+ if (
+ route.name !== 'CreditNoteNew' ||
+ typeof route.query['quotationId'] !== 'string'
+ ) {
+ return;
+ }
+
+ const ret = await quotation.getQuotation(route.query['quotationId']);
+
+ if (!ret) return;
+
+ quotationData.value = ret;
+}
+
+async function submit() {
+ const payload = currentFormData.value;
+ payload.requestWorkId = formTaskList.value.map((v) => v.requestWorkId);
+ payload.quotationId =
+ typeof route.query['quotationId'] === 'string'
+ ? route.query['quotationId']
+ : '';
+ const res = await creditNote.createCreditNote(payload);
+
+ if (res) {
+ await router.push(`/credit-note/${res.id}`);
+
+ await getCreditNote();
+
+ if (attachmentList.value) {
+ await uploadFile(res.id, attachmentList.value, false);
+ }
+
+ initStatus();
+ }
+}
+
+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');
+}
+
+async function changePaybackStatus(status: CreditNotePaybackStatus) {
+ if (!creditNoteData.value) return;
+ const res = await creditNote.action.updatePaybackStatus(
+ creditNoteData.value.id,
+ status,
+ );
+ if (res) {
+ creditNoteData.value.paybackStatus = status;
+
+ if (status === CreditNotePaybackStatus.Done) {
+ creditNoteData.value.creditNoteStatus = CreditNoteStatus.Success;
+ initStatus();
+ }
+ }
+}
+
+async function uploadFile(
+ creditNoteId: string,
+ list: FileList,
+ slip?: boolean,
+) {
+ const promises: ReturnType<
+ typeof creditNote.putAttachment | typeof creditNote.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: `/credit-note/${creditNoteId}/${slip ? 'file-slip' : 'attachment'}/${list[i].name}`,
+ };
+ promises.push(
+ slip
+ ? creditNote.putFile({
+ group: 'slip',
+ parentId: creditNoteId,
+ 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;
+ },
+ })
+ : creditNote.putAttachment({
+ parentId: creditNoteId,
+ 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(creditNoteId: string, n: string, slip?: boolean) {
+ dialogWarningClose(t, {
+ message: t('dialog.message.confirmDelete'),
+ actionText: t('dialog.action.ok'),
+ action: async () => {
+ const res = slip
+ ? await creditNote.delFile({
+ group: 'slip',
+ parentId: creditNoteId,
+ fileId: n,
+ })
+ : await creditNote.delAttachment({
+ parentId: creditNoteId,
+ name: n,
+ });
+ if (res) {
+ getFileList(creditNoteId, slip);
+ }
+ },
+ cancel: () => {},
+ });
+}
+
+async function getFileList(creditNoteId: string, slip?: boolean) {
+ const list = slip
+ ? await creditNote.listFile({
+ group: 'slip',
+ parentId: creditNoteId,
+ })
+ : await creditNote.listAttachment({
+ parentId: creditNoteId,
+ });
+ if (list)
+ slip
+ ? (fileData.value = await Promise.all(
+ list.map(async (v) => {
+ const rse = await creditNote.headFile({
+ group: 'slip',
+ parentId: creditNoteId,
+ fileId: v,
+ });
+
+ let contentLength = 0;
+ if (rse) contentLength = Number(rse['content-length']);
+
+ return {
+ name: v,
+ progress: 1,
+ loaded: contentLength,
+ total: contentLength,
+ url: `/credit-note/${creditNoteId}/file-slip/${v}`,
+ };
+ }),
+ ))
+ : (attachmentData.value = await Promise.all(
+ list.map(async (v) => {
+ const rse = await creditNote.headAttachment({
+ parentId: creditNoteId,
+ fileId: v,
+ });
+
+ let contentLength = 0;
+ if (rse) contentLength = Number(rse['content-length']);
+
+ return {
+ name: v,
+ progress: 1,
+ loaded: contentLength,
+ total: contentLength,
+ url: `/credit-note/${creditNoteId}/attachment/${v}`,
+ };
+ }),
+ ));
+}
+
+onMounted(async () => {
+ initTheme();
+ initLang();
+ await useConfigStore().getConfig();
+ await getCreditNote();
+ await getQuotation();
+ creditNoteData.value && (await getFileList(creditNoteData.value.id, true));
+ initStatus();
+});
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('creditNote.title') }}
+
+
+ {{
+ $t('quotation.processOn', {
+ msg: dateFormatJS({
+ date: creditNoteData?.createdAt || new Date(Date.now()),
+ monthStyle: 'long',
+ }),
+ })
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
+ if (!creditNoteData) return;
+
+ fileList = f;
+
+ await uploadFile(creditNoteData.id, f, true);
+ }
+ "
+ @remove="
+ async (n) => {
+ if (!creditNoteData) return;
+ await remove(creditNoteData.id, n, true);
+ }
+ "
+ />
+
+
+ {
+ if (!creditNoteData) return;
+ getFileList(creditNoteData.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 (!creditNoteData) return;
+
+ await uploadFile(creditNoteData.id, f);
+ }
+ "
+ @remove="
+ async (n) => {
+ if (!creditNoteData) return;
+ await remove(creditNoteData.id, n);
+ }
+ "
+ />
+
+
+
+
+
+
+
+
+ {{ useOptionStore().mapOption(creditNoteData?.paybackBank) }}
+ {{ creditNoteData?.paybackAccount }}
+ {{
+ `${$t('creditNote.label.accountName')} ${creditNoteData?.paybackAccountName}`
+ }}
+
+ {{ $t('creditNote.label.Cash') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/11_credit-note/MainPage.vue b/src/pages/11_credit-note/MainPage.vue
new file mode 100644
index 00000000..1724cbd3
--- /dev/null
+++ b/src/pages/11_credit-note/MainPage.vue
@@ -0,0 +1,468 @@
+
+
+
+
+
+
+
+ {{ $t('general.dataSum') }}
+
+ {{ pageState.total }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ navigateTo({ statusDialog: 'info', creditId: v.id })"
+ @delete="(v) => triggerDelete(v.id)"
+ >
+
+
+
+ navigateTo({
+ statusDialog: 'info',
+ creditId: item.row.id,
+ })
+ "
+ @delete="() => triggerDelete(item.row.id)"
+ :title="item.row.quotation.workName"
+ :code="item.row.code"
+ :status="$t(`creditNote.status.${item.row.creditNoteStatus}`)"
+ :badge-color="hslaColors[item.row.creditNoteStatus] || ''"
+ :custom-data="[
+ {
+ label: $t('branch.card.branchVirtual'),
+ value:
+ $i18n.locale === 'tha'
+ ? item.row.quotation.registeredBranch.name
+ : item.row.quotation.registeredBranch.nameEN,
+ },
+ {
+ label: $t('quotation.customer'),
+ value:
+ item.row.quotation.customerBranch.customer
+ .customerType === 'CORP'
+ ? item.row.quotation.customerBranch.customerName
+ : $i18n.locale === 'tha'
+ ? `${item.row.quotation.customerBranch.firstName} ${item.row.quotation.customerBranch.lastName}`
+ : `${item.row.quotation.customerBranch.firstNameEN} ${item.row.quotation.customerBranch.lastNameEN}`,
+ },
+ {
+ label: $t('requestList.quotationCode'),
+ value: item.row.quotation.code,
+ },
+ {
+ label: $t('creditNote.label.quotationPayment'),
+ value: $t(
+ `quotation.type.${item.row.quotation.payCondition}`,
+ ),
+ },
+ ]"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/11_credit-note/RefundInformation.vue b/src/pages/11_credit-note/RefundInformation.vue
new file mode 100644
index 00000000..a6c69653
--- /dev/null
+++ b/src/pages/11_credit-note/RefundInformation.vue
@@ -0,0 +1,280 @@
+
+
+
+
+ {{ $t('general.information', { msg: $t('creditNote.label.refund') }) }}
+
+
+
+
+
+
+ {{ $t('creditNote.label.refundMethod') }}
+
+
+ {{ $t(`creditNote.label.${paybackType}`) }}
+
+
+
+
+ {{ $t('branch.form.bank') }}
+
+
+
+ {{ useOptionStore().mapOption(paybackBank) }}
+ {{ paybackAccount }}
+ {{ `${$t('creditNote.label.accountName')} ${paybackAccountName}` }}
+
+
+
+
+
+
+ {{ $t('creditNote.label.totalRefund') }}
+
+
+
+ {{ $t('creditNote.label.totalAmount') }}
+
+ {{ formatNumberDecimal(total) }}
+
+
+
+ {{ $t('creditNote.label.paid') }}
+
+ {{ formatNumberDecimal(paid) }}
+
+
+
+ {{ $t('creditNote.label.remain') }}
+
+ {{ formatNumberDecimal(remain) }}
+
+
+
+
+
+
+
+
+
+ {{ $t('creditNote.label.refund') }}
+
+
+
+
+ {{ $t(`creditNote.status.payback.${v.value}`) }}
+
+
+
+
+
+ $emit('upload', f as unknown as FileList)"
+ @close="(v) => $emit('remove', v)"
+ />
+
+
+
+
diff --git a/src/pages/11_credit-note/TableCreditNote.vue b/src/pages/11_credit-note/TableCreditNote.vue
new file mode 100644
index 00000000..a78ca3bb
--- /dev/null
+++ b/src/pages/11_credit-note/TableCreditNote.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+ {{ $t(col.label) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ typeof col.field === 'string'
+ ? props.row[col.field as keyof CreditNote]
+ : col.field(props.row)
+ }}
+
+
+
+ {{ $t(`quotation.type.${col.field(props.row)}`) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/11_credit-note/constants.ts b/src/pages/11_credit-note/constants.ts
new file mode 100644
index 00000000..78065525
--- /dev/null
+++ b/src/pages/11_credit-note/constants.ts
@@ -0,0 +1,78 @@
+import { QTableProps } from 'quasar';
+import { CreditNote, CreditNoteStatus } from 'src/stores/credit-note';
+import { formatNumberDecimal } from 'src/stores/utils';
+
+export const taskStatusOpts = [
+ {
+ status: CreditNoteStatus.Pending,
+ name: `creditNote.status.${CreditNoteStatus.Pending}`,
+ },
+ {
+ status: CreditNoteStatus.Success,
+ name: `creditNote.status.${CreditNoteStatus.Success}`,
+ },
+];
+
+export const pageTabs = [
+ { label: CreditNoteStatus.Pending, value: CreditNoteStatus.Pending },
+ { label: CreditNoteStatus.Success, value: CreditNoteStatus.Success },
+];
+
+export enum Status {
+ taskOrder = 'taskOrder',
+ receiveTaskOrder = 'receiveTaskOrder',
+ sendTaskOrder = 'sendTaskOrder',
+ payment = 'payment',
+ goodReceipt = 'goodReceipt',
+}
+
+export const columns = [
+ {
+ name: 'order',
+ align: 'center',
+ label: 'general.order',
+ field: (data: CreditNote & { _index: number; _page: number }) =>
+ data._page * (data._index + 1),
+ },
+ {
+ name: 'code',
+ align: 'center',
+ label: 'creditNote.label.code',
+ field: (data: CreditNote) => data.code,
+ },
+ {
+ name: 'quotationCode',
+ align: 'center',
+ label: 'creditNote.label.quotationCode',
+ field: (data: CreditNote) => data.quotation.code,
+ },
+ {
+ name: 'quotationWorkName',
+ align: 'center',
+ label: 'creditNote.label.quotationWorkName',
+ field: (data: CreditNote) => data.quotation.workName,
+ },
+ {
+ name: 'quotationPayment',
+ align: 'center',
+ label: 'creditNote.label.quotationPayment',
+ field: (data: CreditNote) => data.quotation.payCondition,
+ },
+ {
+ name: 'creditNoteValue',
+ align: 'center',
+ label: 'creditNote.label.value',
+ field: (data: CreditNote) => formatNumberDecimal(data.value),
+ },
+ {
+ name: '#action',
+ align: 'center',
+ label: '',
+ field: (_) => '#action',
+ },
+] as const satisfies QTableProps['columns'];
+
+export const hslaColors: Record = {
+ Pending: '--blue-6-hsl',
+ Success: '--red-6-hsl',
+};
diff --git a/src/pages/11_credit-note/expansion/CreditNoteExpansion.vue b/src/pages/11_credit-note/expansion/CreditNoteExpansion.vue
new file mode 100644
index 00000000..31da0211
--- /dev/null
+++ b/src/pages/11_credit-note/expansion/CreditNoteExpansion.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+ {{ $t('creditNote.label.creditNoteInformation') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/11_credit-note/expansion/DocumentExpansion.vue b/src/pages/11_credit-note/expansion/DocumentExpansion.vue
new file mode 100644
index 00000000..b500c2f9
--- /dev/null
+++ b/src/pages/11_credit-note/expansion/DocumentExpansion.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+ {{ $t('general.information', { msg: $t('general.document') }) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/11_credit-note/expansion/PaymentExpansion.vue b/src/pages/11_credit-note/expansion/PaymentExpansion.vue
new file mode 100644
index 00000000..2891ec5e
--- /dev/null
+++ b/src/pages/11_credit-note/expansion/PaymentExpansion.vue
@@ -0,0 +1,235 @@
+
+
+
+
+
+ {{ $t('general.payment') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('quotation.paymentCondition') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.opt.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('quotation.summary') }}
+
+
+
+
+
+ {{ $t('general.total') }}
+
+ {{ formatNumberDecimal(totalPrice) }}
+ ฿
+
+
+
+
+ {{ $t('quotation.totalPriceBaht') }}
+
+ {{ formatNumberDecimal(totalPrice) }}
+ ฿
+
+
+
+
+
+
+
+
diff --git a/src/router/routes.ts b/src/router/routes.ts
index 01be06f9..0a91c9fa 100644
--- a/src/router/routes.ts
+++ b/src/router/routes.ts
@@ -105,6 +105,11 @@ const routes: RouteRecordRaw[] = [
name: 'TaskOrder',
component: () => import('pages/09_task-order/MainPage.vue'),
},
+ {
+ path: '/credit-note',
+ name: 'CreditNote',
+ component: () => import('pages/11_credit-note/MainPage.vue'),
+ },
],
},
@@ -148,6 +153,16 @@ const routes: RouteRecordRaw[] = [
name: 'docOrder',
component: () => import('pages/09_task-order/document_view/MainPage.vue'),
},
+ {
+ path: '/credit-note/add',
+ name: 'CreditNoteNew',
+ component: () => import('pages/11_credit-note/FormPage.vue'),
+ },
+ {
+ path: '/credit-note/:id',
+ name: 'CreditNoteView',
+ component: () => import('pages/11_credit-note/FormPage.vue'),
+ },
// Always leave this as last one,
// but you can also remove it
diff --git a/src/stores/credit-note/index.ts b/src/stores/credit-note/index.ts
new file mode 100644
index 00000000..d057c4a4
--- /dev/null
+++ b/src/stores/credit-note/index.ts
@@ -0,0 +1,103 @@
+import {
+ CreditNote as Data,
+ CreditNoteStatus as Status,
+ CreditNotePayload as Payload,
+ CreditNotePaybackStatus,
+} from './types.ts';
+import { ref } from 'vue';
+import { defineStore } from 'pinia';
+
+import { api } from 'src/boot/axios.ts';
+import { PaginationResult } from 'src/types.ts';
+import { manageAttachment, manageFile } from '../utils/index.ts';
+
+const ENDPOINT = 'credit-note';
+
+export * from './types.ts';
+
+export async function getCreditNoteStats() {
+ const res = await api.get>(`/${ENDPOINT}/stats`);
+ if (res.status < 400) {
+ return res.data;
+ }
+ return null;
+}
+
+export async function getCreditNoteList(params?: {
+ page?: number;
+ pageSize?: number;
+ query?: string;
+ creditNoteStatus?: Status;
+}) {
+ const res = await api.get>(`/${ENDPOINT}`, {
+ params,
+ });
+ if (res.status < 400) return res.data;
+ return null;
+}
+
+export async function getCreditNote(id: string) {
+ const res = await api.get(`/${ENDPOINT}/${id}`);
+ if (res.status < 400) return res.data;
+ return null;
+}
+
+export async function createCreditNote(body: Payload) {
+ const res = await api.post(`/${ENDPOINT}`, body);
+ if (res.status < 400) return res.data;
+ return null;
+}
+
+export async function updateCreditNote(id: string, body: Payload) {
+ const res = await api.put(`/${ENDPOINT}/${id}`, body);
+ if (res.status < 400) return res.data;
+ return null;
+}
+
+export async function deleteCreditNote(id: string) {
+ const res = await api.delete(`/${ENDPOINT}/${id}`);
+ if (res.status < 400) return res.data;
+ return null;
+}
+
+export async function updatePaybackStatus(
+ creditNoteId: string,
+ status: CreditNotePaybackStatus,
+) {
+ const res = await api.post(`/${ENDPOINT}/${creditNoteId}/payback-status`, {
+ paybackStatus: status,
+ });
+ if (res.status < 400) return true;
+ return null;
+}
+
+export const useCreditNote = defineStore('credit-note-store', () => {
+ const data = ref([]);
+ const page = ref(1);
+ const pageMax = ref(1);
+ const pageSize = ref(30);
+ const stats = ref>({
+ [Status.Pending]: 0,
+ [Status.Success]: 0,
+ });
+
+ return {
+ data,
+ page,
+ pageMax,
+ pageSize,
+ stats,
+
+ getCreditNoteStats,
+ getCreditNote,
+ getCreditNoteList,
+ createCreditNote,
+ updateCreditNote,
+ deleteCreditNote,
+
+ ...manageAttachment(api, ENDPOINT),
+ ...manageFile<'slip'>(api, ENDPOINT),
+
+ action: { updatePaybackStatus },
+ };
+});
diff --git a/src/stores/credit-note/types.ts b/src/stores/credit-note/types.ts
new file mode 100644
index 00000000..12b982be
--- /dev/null
+++ b/src/stores/credit-note/types.ts
@@ -0,0 +1,51 @@
+import { QuotationFull } from '../quotations';
+import { RequestWork } from '../request-list';
+import { CreatedBy } from '../types';
+
+export type CreditNotePayload = {
+ quotationId: string;
+ requestWorkId: string[];
+ reason: string;
+ detail: string;
+ paybackType: 'BankTransfer' | 'Cash';
+ paybackBank: string;
+ paybackAccount: string;
+ paybackAccountName: string;
+};
+
+export type CreditNote = {
+ id: string;
+
+ code: string;
+
+ quotationId: string;
+ quotation: QuotationFull;
+ requestWork: RequestWork[];
+ reason: string;
+ detail: string;
+ paybackType: 'BankTransfer' | 'Cash';
+ paybackBank: string;
+ paybackAccount: string;
+ paybackAccountName: string;
+ paybackStatus: CreditNotePaybackStatus;
+ paybackDate?: string | null;
+
+ value: number;
+
+ createdAt: string;
+ createdBy?: CreatedBy;
+ createdByUserId?: string;
+
+ creditNoteStatus: CreditNoteStatus;
+};
+
+export enum CreditNoteStatus {
+ Pending = 'Pending',
+ Success = 'Success',
+}
+
+export enum CreditNotePaybackStatus {
+ Pending = 'Pending',
+ Verify = 'Verify',
+ Done = 'Done',
+}
diff --git a/src/stores/quotations/index.ts b/src/stores/quotations/index.ts
index 6668f48b..e985f1ce 100644
--- a/src/stores/quotations/index.ts
+++ b/src/stores/quotations/index.ts
@@ -69,6 +69,8 @@ export const useQuotationStore = defineStore('quotation-store', () => {
| 'Canceled';
urgentFirst?: boolean;
query?: string;
+ hasCancel?: boolean;
+ includeRegisteredBranch?: boolean;
}) {
const res = await api.get>('/quotation', {
params,
diff --git a/src/stores/quotations/types.ts b/src/stores/quotations/types.ts
index 91150071..d8a10a58 100644
--- a/src/stores/quotations/types.ts
+++ b/src/stores/quotations/types.ts
@@ -209,7 +209,7 @@ export type QuotationStats = {
};
export type Quotation = {
- _count: { worker: number };
+ _count: { worker: number; canceledWork: number };
id: string;
finalPrice: number;
vat: number;
@@ -253,6 +253,7 @@ export type Quotation = {
| 'canceled';
registeredBranchId: string;
+ registeredBranch?: { id: string; name: string; nameEN: string; code: string };
customerBranchId: string;
customerBranch: CustomerBranchRelation;
@@ -328,7 +329,7 @@ export type QuotationFull = {
customerBranchId: string;
customerBranch: CustomerBranchRelation;
registeredBranchId: string;
- registeredBranch: { id: string; name: string };
+ registeredBranch: { id: string; name: string; nameEN: string; code: string };
createdByUserId: string;
createdAt: string | Date;
diff --git a/src/stores/request-list/index.ts b/src/stores/request-list/index.ts
index 19254f93..12e63e32 100644
--- a/src/stores/request-list/index.ts
+++ b/src/stores/request-list/index.ts
@@ -223,6 +223,8 @@ export const useRequestList = defineStore('request-list', () => {
pageSize?: number;
workStatus?: RequestWorkStatus;
readyToTask?: boolean;
+ quotationId?: string;
+ cancelOnly?: boolean;
}) {
const res = await api.get>('/request-work', {
params,
diff --git a/src/stores/request-list/types.ts b/src/stores/request-list/types.ts
index 6f8ddfe1..6759ad5c 100644
--- a/src/stores/request-list/types.ts
+++ b/src/stores/request-list/types.ts
@@ -52,6 +52,8 @@ export type RequestWork = {
productServiceId: string;
request: RequestData;
attributes?: Attributes;
+ creditNoteId?: string;
+ processByUserId?: string;
};
export type RowDocument = {
diff --git a/src/stores/utils/index.ts b/src/stores/utils/index.ts
index 857fff50..1052c17a 100644
--- a/src/stores/utils/index.ts
+++ b/src/stores/utils/index.ts
@@ -355,13 +355,33 @@ export function manageFile(
parentId: string;
fileId: string;
file: File;
+ uploadUrl?: boolean;
onUploadProgress?: (e: AxiosProgressEvent) => void;
abortController?: AbortController;
}) => {
- const res = await api.put(
- `/${base}/${opts.parentId}/file-${opts.group}/${opts.fileId}`,
- opts.file,
- {
+ const res = opts.uploadUrl
+ ? await api.put(
+ `/${base}/${opts.parentId}/file-${opts.group}/${opts.fileId}`,
+ )
+ : await api.put(
+ `/${base}/${opts.parentId}/file-${opts.group}/${opts.fileId}`,
+ opts.file,
+ {
+ headers: { 'Content-Type': opts.file.type },
+ onUploadProgress: opts.onUploadProgress
+ ? opts.onUploadProgress
+ : option?.onUploadProgress
+ ? option.onUploadProgress
+ : (e) => console.log(e),
+ signal: opts.abortController?.signal,
+ },
+ );
+
+ if (res.status >= 400) return false;
+
+ if (opts.uploadUrl && typeof res.data === 'string') {
+ // NOTE: Must use axios instance or else CORS error.
+ const uploadRes = await axios.put(res.data, opts.file, {
headers: { 'Content-Type': opts.file.type },
onUploadProgress: opts.onUploadProgress
? opts.onUploadProgress
@@ -369,10 +389,12 @@ export function manageFile(
? option.onUploadProgress
: (e) => console.log(e),
signal: opts.abortController?.signal,
- },
- );
- if (res.status < 400) return true;
- return false;
+ });
+
+ if (uploadRes.status >= 400) return true;
+ }
+
+ return true;
},
delFile: async (opts: { group: T; parentId: string; fileId: string }) => {
const res = await api.delete(