@@ -530,6 +532,10 @@ watch(
--_color: var(--pink-7-hsl);
}
+.debit-note-color {
+ --_color: var(--cyan-7-hsl);
+}
+
.bg-color {
color: white;
background: hsla(var(--_color));
diff --git a/src/pages/05_quotation/QuotationFormReceipt.vue b/src/pages/05_quotation/QuotationFormReceipt.vue
index 093661f6..e02d847e 100644
--- a/src/pages/05_quotation/QuotationFormReceipt.vue
+++ b/src/pages/05_quotation/QuotationFormReceipt.vue
@@ -1,6 +1,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('debitNote.title') }}
+
+
+ {{
+ $t('quotation.processOn', {
+ msg: dateFormatJS({
+ date: debitNoteData?.createdAt || new Date(Date.now()),
+ monthStyle: 'long',
+ }),
+ })
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
goToQuotation()"
+ />
+
+
+
+ {
+ if (!!debitNoteData) assignFormData(debitNoteData?.id);
+ }
+ "
+ />
+
+ (pageState.employeeModal = true)"
+ @delete="(i) => deleteItem(selectedWorker, i)"
+ />
+
+
+
+ (pageState.productServiceModal = true)"
+ @update-rows="
+ (v) => {
+ view === null && (productServiceList = v);
+ }
+ "
+ @delete="toggleDeleteProduct"
+ @view-file="viewProductFile"
+ />
+
+
+
+
+ {
+ 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);
+ }
+ "
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (quotationData?.payCondition !== 'Full') {
+ selectedInstallmentNo = [v.invoice.installments[0].no];
+ installmentAmount = v.invoice.amount;
+ }
+
+ view = null;
+ }
+ "
+ @example="() => exampleReceipt(v.id)"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ selectedWorker = v.worker;
+ }
+ "
+ />
+
+
+ {
+ 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 });
+ }
+ }
+ "
+ >
+
+
+
+
+
+
+
diff --git a/src/pages/12_debit-note/MainPage.vue b/src/pages/12_debit-note/MainPage.vue
new file mode 100644
index 00000000..2143ba59
--- /dev/null
+++ b/src/pages/12_debit-note/MainPage.vue
@@ -0,0 +1,499 @@
+
+
+
+ triggerCreateDebitNote()"
+ >
+
+
+
+
+ {{ $t('general.dataSum') }}
+
+ {{ pageState.total }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ navigateTo({ statusDialog: 'info', debitId: item.id })
+ "
+ @edit="
+ (item) => navigateTo({ statusDialog: 'edit', debitId: item.id })
+ "
+ @delete="(item) => triggerDelete(item.id)"
+ >
+
+
+
+ navigateTo({ statusDialog: 'info', debitId: item.row.id })
+ "
+ @edit="
+ () =>
+ navigateTo({ statusDialog: 'edit', debitId: item.row.id })
+ "
+ @delete="() => triggerDelete(item.row.id)"
+ :title="item.row.debitNoteQuotation?.workName"
+ :code="item.row.code"
+ :status="$t(`quotation.status.${item.row.quotationStatus}`)"
+ :badge-color="hslaColors[item.row.quotationStatus] || ''"
+ :custom-data="[
+ {
+ label: $t('branch.card.branchVirtual'),
+ value:
+ $i18n.locale === 'tha'
+ ? item.row.registeredBranch.name
+ : item.row.registeredBranch.nameEN,
+ },
+ {
+ label: $t('quotation.customer'),
+ value:
+ item.row.customerBranch.customer.customerType === 'CORP'
+ ? item.row.customerBranch.customerName
+ : $i18n.locale === 'tha'
+ ? `${item.row.customerBranch.firstName} ${item.row.customerBranch.lastName}`
+ : `${item.row.customerBranch.firstNameEN} ${item.row.customerBranch.lastNameEN}`,
+ },
+ {
+ label: $t('requestList.quotationCode'),
+ value: item.row.debitNoteQuotation?.code,
+ },
+ {
+ label: $t('debitNote.label.quotationPayment'),
+ value: $t(
+ `quotation.type.${item.row.debitNoteQuotation?.payCondition}`,
+ ),
+ },
+ ]"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ submit()"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/12_debit-note/TableDebitNote.vue b/src/pages/12_debit-note/TableDebitNote.vue
new file mode 100644
index 00000000..77d26d18
--- /dev/null
+++ b/src/pages/12_debit-note/TableDebitNote.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+ {{ $t(col.label) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ typeof col.field === 'string'
+ ? props.row[col.field as keyof DebitNote]
+ : col.field(props.row)
+ }}
+
+
+
+ {{ $t(`quotation.type.${col.field(props.row)}`) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/12_debit-note/constants.ts b/src/pages/12_debit-note/constants.ts
new file mode 100644
index 00000000..af2560cd
--- /dev/null
+++ b/src/pages/12_debit-note/constants.ts
@@ -0,0 +1,90 @@
+import { QTableProps } from 'quasar';
+import { DebitNoteStatus, DebitNote } from 'src/stores/debit-note';
+import { formatNumberDecimal } from 'src/stores/utils';
+
+export const taskStatusOpts = [
+ {
+ status: DebitNoteStatus.Expire,
+ name: `debitNote.status.${DebitNoteStatus.Expire}`,
+ },
+ {
+ status: DebitNoteStatus.Payment,
+ name: `debitNote.status.${DebitNoteStatus.Payment}`,
+ },
+ {
+ status: DebitNoteStatus.Receipt,
+ name: `debitNote.status.${DebitNoteStatus.Receipt}`,
+ },
+ {
+ status: DebitNoteStatus.Succeed,
+ name: `debitNote.status.${DebitNoteStatus.Succeed}`,
+ },
+];
+
+export const pageTabs = [
+ { label: 'Pending', value: DebitNoteStatus.Pending },
+ { label: 'Expire', value: DebitNoteStatus.Expire },
+ { label: 'Payment', value: DebitNoteStatus.Payment },
+ { label: 'Receipt', value: DebitNoteStatus.Receipt },
+ { label: 'Succeed', value: DebitNoteStatus.Succeed },
+];
+
+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: DebitNote & { _index: number; _page: number; _pageSize: number },
+ ) => (data._page - 1) * data._pageSize + data._index + 1,
+ },
+ {
+ name: 'code',
+ align: 'center',
+ label: 'debitNote.label.codeDebit',
+ field: (data: DebitNote) => data.code,
+ },
+ {
+ name: 'quotationCode',
+ align: 'center',
+ label: 'debitNote.label.codeQuotation',
+ field: (data: DebitNote) => data.debitNoteQuotation?.code,
+ },
+ {
+ name: 'quotationWorkName',
+ align: 'center',
+ label: 'debitNote.label.quotationWorkName',
+ field: (data: DebitNote) => data.workName,
+ },
+ {
+ name: 'quotationPayment',
+ align: 'center',
+ label: 'debitNote.label.quotationPayment',
+ field: (data: DebitNote) => data.debitNoteQuotation?.payCondition,
+ },
+ {
+ name: 'creditNoteValue',
+ align: 'center',
+ label: 'debitNote.label.value',
+ field: (data: DebitNote) => formatNumberDecimal(data.totalPrice),
+ },
+ {
+ 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/12_debit-note/document-view/BankComponents.vue b/src/pages/12_debit-note/document-view/BankComponents.vue
new file mode 100644
index 00000000..3f6396c4
--- /dev/null
+++ b/src/pages/12_debit-note/document-view/BankComponents.vue
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
diff --git a/src/pages/12_debit-note/document-view/MainPage.vue b/src/pages/12_debit-note/document-view/MainPage.vue
new file mode 100644
index 00000000..0e4432f4
--- /dev/null
+++ b/src/pages/12_debit-note/document-view/MainPage.vue
@@ -0,0 +1,654 @@
+
+
+
+
+
+
+
+
+
+ {{ $t('preview.productList') }}
+
+
+
+
+
+ | {{ $t('preview.rank') }} |
+ {{ $t('preview.productCode') }} |
+ {{ $t('general.detail') }} |
+ {{ $t('general.amount') }} |
+ {{ $t('preview.pricePerUnit') }} |
+ {{ $t('preview.discount') }} |
+ {{ $t('preview.vat') }} |
+ {{ $t('preview.value') }} |
+
+
+ | {{ i + 1 }} |
+ {{ v.code }} |
+ {{ v.detail }} |
+ {{ v.amount }} |
+
+ {{ formatNumberDecimal(v.priceUnit, 2) }}
+ |
+
+ {{ formatNumberDecimal(v.discount, 2) }}
+ |
+
+ {{ formatNumberDecimal(v.vat, 2) }}
+ |
+
+ {{ formatNumberDecimal(v.value, 2) }}
+ |
+
+
+
+
+
+
+
+ | {{ $t('general.total') }} |
+
+ {{ formatNumberDecimal(summaryPrice.totalPrice, 2) }}
+ ฿
+ |
+
+
+
+ | {{ $t('general.discount') }} |
+
+ {{ formatNumberDecimal(summaryPrice.totalDiscount, 2) || 0 }} ฿
+ |
+
+
+ | {{ $t('general.totalAfterDiscount') }} |
+
+ {{
+ formatNumberDecimal(
+ summaryPrice.totalPrice - summaryPrice.totalDiscount,
+ 2,
+ )
+ }}
+ ฿
+ |
+
+
+
+ | {{ $t('general.totalVatExcluded') }} |
+
+ {{ formatNumberDecimal(summaryPrice.vatExcluded, 2) || 0 }}
+ ฿
+ |
+
+
+ | {{ $t('general.vat', { msg: '7%' }) }} |
+
+ {{ formatNumberDecimal(summaryPrice.vat, 2) }} ฿
+ |
+
+
+ | {{ $t('general.totalVatIncluded') }} |
+
+ {{
+ formatNumberDecimal(
+ summaryPrice.totalPrice -
+ summaryPrice.totalDiscount +
+ summaryPrice.vat,
+ 2,
+ )
+ }}
+ ฿
+ |
+
+
+
+ | {{ $t('general.discountAfterVat') }} |
+
+ {{ formatNumberDecimal(data?.discount || 0, 2) }}
+ ฿
+ |
+
+
+
+
+
+
+ ({{ ThaiBahtText(summaryPrice.finalPrice) }})
+
+
+ ยอดรวมสุทธิ
+
+ {{
+ formatNumberDecimal(Math.max(summaryPrice.finalPrice, 0), 2) || 0
+ }}
+ ฿
+
+
+
+
+
+
+
+
+
+ {{ $t('general.remark') }}
+
+
+
+
+
+
+
+
+
+ {{ $t('preview.paymentMethods') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/12_debit-note/document-view/ViewFooter.vue b/src/pages/12_debit-note/document-view/ViewFooter.vue
new file mode 100644
index 00000000..fc9a34a5
--- /dev/null
+++ b/src/pages/12_debit-note/document-view/ViewFooter.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
diff --git a/src/pages/12_debit-note/document-view/ViewHeader.vue b/src/pages/12_debit-note/document-view/ViewHeader.vue
new file mode 100644
index 00000000..e9c4624d
--- /dev/null
+++ b/src/pages/12_debit-note/document-view/ViewHeader.vue
@@ -0,0 +1,192 @@
+
+
+
+
+
+

+
+
+ {{ $t(titleMode(view)) }}
+
+
+
+
+
+
+
+ {{ !!branch.virtual ? '' : $t('general.company') }} {{ branch.name }}
+
+
+
+ {{
+ formatAddress({
+ address: branch.address,
+ addressEN: branch.addressEN,
+ moo: branch.moo,
+ mooEN: branch.mooEN,
+ soi: branch.soi,
+ soiEN: branch.soiEN,
+ street: branch.street,
+ streetEN: branch.streetEN,
+ province: branch.province,
+ district: branch.district,
+ subDistrict: branch.subDistrict,
+ })
+ }}
+
+ เลขประจำตัวผู้เสียภาษี {{ branch.taxNo }}
+ เบอร์โทร {{ branch.telephoneNo }}
+ {{ branch.webUrl }}
+
+
+ ลูกค้า
+
+ {{
+ formatAddress({
+ address: customer.address,
+ addressEN: customer.addressEN,
+ moo: customer.moo,
+ mooEN: customer.mooEN,
+ soi: customer.soi,
+ soiEN: customer.soiEN,
+ street: customer.street,
+ streetEN: customer.streetEN,
+ province: customer.province,
+ district: customer.district,
+ subDistrict: customer.subDistrict,
+ })
+ }}
+
+ เลขประจำตัวผู้เสียภาษี {{ customer.citizenId }}
+ เบอร์โทร {{ customer.telephoneNo }}
+
+
+
+
+
{{ $t('general.itemNo', { msg: `${$t(titleMode(view))}` }) }}
+
{{ details.code }}
+
+
+
{{ $t('preview.dateAt', { msg: `${$t(titleMode(view))}` }) }}
+
{{ dateFormat(details.createdAt, true, false, true) }}
+
+
+
{{ $t('preview.seller') }}
+
{{ details.createdBy }}
+
+
+
{{ $t('quotation.paymentCondition') }}
+
+ {{
+ {
+ Full: $t('quotation.type.fullAmountCash'),
+ Split: $t('quotation.type.installmentsCash'),
+ BillFull: $t('quotation.type.fullAmountBill'),
+ BillSplit: $t('quotation.type.installmentsBill'),
+ }[details.payCondition]
+ }}
+
+
+
+
{{ $t('quotation.workName') }}
+
{{ details.workName }}
+
+
+
{{ $t('quotation.contactName') }}
+
{{ details.contactName }}
+
+
+
{{ $t('preview.dueDate') }}
+
{{ dateFormat(details.dueDate, true, false, true) }}
+
+
+
+
+
+
diff --git a/src/pages/12_debit-note/document-view/ViewPdf.vue b/src/pages/12_debit-note/document-view/ViewPdf.vue
new file mode 100644
index 00000000..f800e790
--- /dev/null
+++ b/src/pages/12_debit-note/document-view/ViewPdf.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/src/pages/12_debit-note/expansion/DebitNoteExpansion.vue b/src/pages/12_debit-note/expansion/DebitNoteExpansion.vue
new file mode 100644
index 00000000..a7649753
--- /dev/null
+++ b/src/pages/12_debit-note/expansion/DebitNoteExpansion.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+ {{ $t('debitNote.label.debitNoteInformation') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/12_debit-note/expansion/DocumentExpansion.vue b/src/pages/12_debit-note/expansion/DocumentExpansion.vue
new file mode 100644
index 00000000..0d38ac7b
--- /dev/null
+++ b/src/pages/12_debit-note/expansion/DocumentExpansion.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+ {{ $t('general.information', { msg: $t('general.document') }) }}
+
+
+
+
+
+
+
+ {
+ if (typeof v === 'string') dueDate = v;
+ }
+ "
+ :readonly
+ />
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/12_debit-note/expansion/PaymentExpansion.vue b/src/pages/12_debit-note/expansion/PaymentExpansion.vue
new file mode 100644
index 00000000..e1b5a7c5
--- /dev/null
+++ b/src/pages/12_debit-note/expansion/PaymentExpansion.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+ {{ $t('general.payment') }}
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/12_debit-note/expansion/ProductExpansion.vue b/src/pages/12_debit-note/expansion/ProductExpansion.vue
new file mode 100644
index 00000000..dca92aa2
--- /dev/null
+++ b/src/pages/12_debit-note/expansion/ProductExpansion.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+ {{ $t('general.information', { msg: $t('taskOrder.productList') }) }}
+
+
+
+
+
+ $emit('delete', v)"
+ @update:rows="(v) => $emit('updateRows', v)"
+ @update-table="(data, opt) => $emit('updateTable', data, opt)"
+ @view-file="(v) => $emit('viewFile', v)"
+ />
+
+
+
+
diff --git a/src/pages/12_debit-note/expansion/RemarkExpansion.vue b/src/pages/12_debit-note/expansion/RemarkExpansion.vue
new file mode 100644
index 00000000..d33fcf73
--- /dev/null
+++ b/src/pages/12_debit-note/expansion/RemarkExpansion.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+ {{ $t('general.remark') }}
+
+
+
+
+ {
+ remark = v;
+ }
+ "
+ />
+
+
+
+
diff --git a/src/pages/12_debit-note/expansion/WorkerItemExpansion.vue b/src/pages/12_debit-note/expansion/WorkerItemExpansion.vue
new file mode 100644
index 00000000..aa4ee499
--- /dev/null
+++ b/src/pages/12_debit-note/expansion/WorkerItemExpansion.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+ {{ $t('quotation.employeeList') }}
+
+
+
+ {{ toggleWorker ? $t('general.specify') : $t('general.noSpecify') }}
+
+
+
+
+
+
+
+ $emit('update:employeeAmount', v)"
+ :employee-amount
+ :readonly="readonly"
+ fallback-img="/images/employee-avatar.png"
+ :rows="rowWorker"
+ @delete="(i) => $emit('delete', i)"
+ />
+
+
+
+
+
diff --git a/src/pages/12_debit-note/form.ts b/src/pages/12_debit-note/form.ts
new file mode 100644
index 00000000..9f92c0ff
--- /dev/null
+++ b/src/pages/12_debit-note/form.ts
@@ -0,0 +1,64 @@
+import { dialog } from 'stores/utils';
+import { defineStore } from 'pinia';
+import { useI18n } from 'vue-i18n';
+import { ref } from 'vue';
+import { DebitNotePayload } from 'src/stores/debit-note';
+import { PayCondition } from 'src/stores/quotations';
+
+// NOTE: Import types
+
+// NOTE: Import stores
+
+const DEFAULT_DATA: DebitNotePayload = {
+ productServiceList: [],
+ debitNoteQuotationId: '',
+ worker: [],
+ payBillDate: new Date(),
+ paySplit: [],
+ paySplitCount: 0,
+ payCondition: PayCondition.Full,
+ dueDate: new Date(),
+ discount: 0,
+ status: 'CREATED',
+ remark: '#[quotation-labor]
#[quotation-payment]',
+ quotationId: '',
+ agentPrice: false,
+};
+
+export const useDebitNoteForm = defineStore('form-debit-note', () => {
+ const { t } = useI18n();
+
+ let resetFormData = structuredClone(DEFAULT_DATA);
+
+ const currentFormData = ref(structuredClone(resetFormData));
+
+ const currentFormState = ref<{
+ mode: null | 'info' | 'create' | 'edit';
+ }>({
+ mode: null,
+ });
+
+ function isFormDataDifferent() {
+ const { ...resetData } = resetFormData;
+ const { ...currData } = currentFormData.value;
+
+ return JSON.stringify(resetData) !== JSON.stringify(currData);
+ }
+
+ function resetForm(clean = false) {
+ if (clean) {
+ currentFormData.value = structuredClone(DEFAULT_DATA);
+ resetFormData = structuredClone(DEFAULT_DATA);
+ return;
+ }
+
+ currentFormData.value = structuredClone(resetFormData);
+
+ currentFormState.value.mode = 'info';
+ }
+
+ return {
+ isFormDataDifferent,
+ resetForm,
+ };
+});
diff --git a/src/router/routes.ts b/src/router/routes.ts
index dee71a15..785f6601 100644
--- a/src/router/routes.ts
+++ b/src/router/routes.ts
@@ -120,6 +120,11 @@ const routes: RouteRecordRaw[] = [
name: 'receipt',
component: () => import('pages/13_receipt/MainPage.vue'),
},
+ {
+ path: '/debit-note',
+ name: 'debitNote',
+ component: () => import('pages/12_debit-note/MainPage.vue'),
+ },
],
},
@@ -183,6 +188,22 @@ const routes: RouteRecordRaw[] = [
name: 'receiptform',
component: () => import('pages/13_receipt/MainPage.vue'),
},
+ {
+ path: '/debit-note/add',
+ name: 'DebitNoteNew',
+ component: () => import('pages/12_debit-note/FormPage.vue'),
+ },
+ {
+ path: '/debit-note/:id',
+ name: 'DebitNoteView',
+ component: () => import('pages/12_debit-note/FormPage.vue'),
+ },
+
+ {
+ path: '/debit-note/document-view',
+ name: 'DebitNoteDocumentView',
+ component: () => import('pages/12_debit-note/document-view/MainPage.vue'),
+ },
// Always leave this as last one,
// but you can also remove it
diff --git a/src/stores/debit-note/index.ts b/src/stores/debit-note/index.ts
new file mode 100644
index 00000000..7ef3b8b3
--- /dev/null
+++ b/src/stores/debit-note/index.ts
@@ -0,0 +1,94 @@
+import {
+ DebitNote as Data,
+ DebitNoteStatus as Status,
+ DebitNotePayload as Payload,
+} 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 = 'debit-note';
+
+export * from './types.ts';
+
+export async function getDebitNoteStats() {
+ const res = await api.get>(`/${ENDPOINT}/stats`);
+ if (res.status < 400) {
+ return res.data;
+ }
+ return null;
+}
+
+export async function getDebitNoteList(params?: {
+ page?: number;
+ pageSize?: number;
+ query?: string;
+ deebitNoteStatus?: Status;
+ includeRegisteredBranch?: boolean;
+}) {
+ const res = await api.get>(`/${ENDPOINT}`, {
+ params,
+ });
+ if (res.status < 400) return res.data;
+ return null;
+}
+
+export async function getDebitNote(id: string) {
+ const res = await api.get(`/${ENDPOINT}/${id}`);
+ if (res.status < 400) return res.data;
+ return null;
+}
+
+export async function createDebitNote(body: Payload) {
+ const res = await api.post(`/${ENDPOINT}`, body);
+ if (res.status < 400) return res.data;
+ return null;
+}
+
+export async function updateDebitNote(body: Payload) {
+ const { id, quotationId, ...payload } = body;
+ const res = await api.put(`/${ENDPOINT}/${id}`, payload);
+ if (res.status < 400) return res.data;
+ return null;
+}
+
+export async function deleteDebitNote(id: string) {
+ const res = await api.delete(`/${ENDPOINT}/${id}`);
+ if (res.status < 400) return res.data;
+ return null;
+}
+
+export const useDebitNote = defineStore('debit-note-store', () => {
+ const data = ref([]);
+ const page = ref(1);
+ const pageMax = ref(1);
+ const pageSize = ref(30);
+ const stats = ref>({
+ [Status.Pending]: 0,
+ [Status.Expire]: 0,
+ [Status.Payment]: 0,
+ [Status.Receipt]: 0,
+ [Status.Succeed]: 0,
+ });
+
+ return {
+ data,
+ page,
+ pageMax,
+ pageSize,
+ stats,
+
+ getDebitNoteStats,
+ getDebitNote,
+ getDebitNoteList,
+ createDebitNote,
+ updateDebitNote,
+ deleteDebitNote,
+
+ ...manageAttachment(api, ENDPOINT),
+ ...manageFile<'slip'>(api, ENDPOINT),
+ };
+});
diff --git a/src/stores/debit-note/types.ts b/src/stores/debit-note/types.ts
new file mode 100644
index 00000000..00f6b468
--- /dev/null
+++ b/src/stores/debit-note/types.ts
@@ -0,0 +1,94 @@
+import {
+ Quotation,
+ QuotationFull,
+ QuotationStatus,
+ QuotationPayload,
+ PayCondition,
+} from '../quotations';
+import { RequestWork } from '../request-list';
+import { CreatedBy, UpdatedBy } from '../types';
+import { CustomerBranch } from '../customer';
+import { Branch } from '../branch/types';
+
+export type DebitNotePayload = Omit<
+ QuotationPayload & {
+ id?: string;
+ quotationId: string;
+ debitNoteQuotationId?: string;
+ reason?: string;
+ detail?: string;
+ agentPrice: boolean;
+ },
+ | 'customerBranchId'
+ | 'registeredBranchId'
+ | 'contactName'
+ | 'contactTel'
+ | '_count'
+ | 'urgent'
+ | 'workName'
+ | 'workerMax'
+ | 'productServiceList'
+ | 'paySplit'
+> & {
+ productServiceList: {
+ installmentNo: number;
+ workerIndex: number[];
+ discount: number;
+ amount: number;
+ productId: string;
+ workId: string;
+ serviceId: string;
+ }[];
+};
+
+export type DebitNote = {
+ debitNoteQuotation?: QuotationFull;
+ updatedBy: UpdatedBy;
+ createdBy: CreatedBy;
+ productServiceList: QuotationFull['productServiceList'];
+ worker: QuotationFull['worker'];
+ paySplit: QuotationFull['paySplit'];
+ registeredBranch: Branch;
+ customerBranch: CustomerBranch;
+ _count: { worker: number };
+
+ updatedByUserId: string;
+ updatedAt: string;
+ createdByUserId: string;
+ createdAt: string;
+ debitNoteQuotationId: string;
+ isDebitNote: boolean;
+ finalPrice: number;
+ discount: number;
+ vatExcluded: number;
+ vat: number;
+ totalDiscount: number;
+ totalPrice: number;
+ agentPrice: boolean;
+ urgent: boolean;
+ workerMax: number;
+ payBillDate: string;
+ paySplitCount: number;
+ payCondition: PayCondition;
+ date: string;
+ dueDate: string;
+ contactTel: string;
+ contactName: string;
+ workName: string;
+ code: string;
+ remark: string;
+ quotationStatus: QuotationStatus;
+ statusOrder: number;
+ status: string;
+ customerBranchId: string;
+ registeredBranchId: string;
+ id: string;
+};
+
+export enum DebitNoteStatus {
+ Pending = 'Pending',
+ Expire = 'Expire',
+ Payment = 'Payment',
+ Receipt = 'Receipt',
+ Succeed = 'Succeed',
+}
diff --git a/src/stores/payment/index.ts b/src/stores/payment/index.ts
index e85de252..93c210fc 100644
--- a/src/stores/payment/index.ts
+++ b/src/stores/payment/index.ts
@@ -98,6 +98,9 @@ export const useReceipt = defineStore('receipt-store', () => {
pageSize?: number;
query?: string;
quotationId?: string;
+ debitNoteId?: string;
+ debitNoteOnly?: boolean;
+ quotationOnly?: boolean;
}) {
const res = await api.get>('/receipt', {
params: opts,
diff --git a/src/stores/quotations/index.ts b/src/stores/quotations/index.ts
index e985f1ce..775dc7ac 100644
--- a/src/stores/quotations/index.ts
+++ b/src/stores/quotations/index.ts
@@ -192,10 +192,15 @@ export const useQuotationStore = defineStore('quotation-store', () => {
* @deprecated Please use payment store instead.
*/
export const useQuotationPayment = defineStore('quotation-payment', () => {
- async function getQuotationPayment(quotationId: string) {
+ async function getQuotationPayment(params: {
+ quotationId?: string;
+ debitNoteId?: string;
+ debitNoteOnly?: boolean;
+ quotationOnly?: boolean;
+ }) {
const res = await api.get>(
'/payment',
- { params: { quotationId } },
+ { params },
);
if (res.status < 400) {
return res.data;
diff --git a/tests/institution.spec.ts b/tests/institution.spec.ts
new file mode 100644
index 00000000..906d571c
--- /dev/null
+++ b/tests/institution.spec.ts
@@ -0,0 +1,44 @@
+import { fakerEN, fakerTH } from '@faker-js/faker';
+import test, { expect, Page } from '@playwright/test';
+import { login } from './utils';
+
+test.describe.configure({ mode: 'serial' });
+
+let page: Page;
+
+test.beforeAll(async ({ browser }) => {
+ page = await browser.newPage();
+});
+
+test.afterAll(async () => {
+ await page.close();
+});
+
+test('JWS_INST_001 - Login', async () => {
+ await login(page);
+});
+
+test('JWS_INST_002 - Goto Institution', async () => {
+ await page.click('//span[text()="หน่วยงาน"]');
+ await expect(page).toHaveURL(/.*agencies-management/);
+ await page.waitForTimeout(5000);
+});
+
+test('JWS_INST_003 - Click New Institution', async () => {
+ await page.click('i.q-icon.mdi.mdi-plus');
+ await expect(page.locator('div.col.text-subtitle1')).toContainText(
+ 'เพิ่มหน่วยงาน',
+ );
+ await page.waitForTimeout(5000);
+});
+
+test('JWS_INST_004 - Fill Form', async () => {
+ await page
+ .getByRole('textbox', { name: 'ชื่อหน่วยงาน' })
+ .fill(fakerTH.company.name());
+ await page
+ .getByRole('textbox', { name: 'Agencies Name' })
+ .fill(fakerEN.company.name());
+ fakerEN.location.buildingNumber();
+ await page.waitForTimeout(5000);
+});
diff --git a/tests/utils/index.ts b/tests/utils/index.ts
new file mode 100644
index 00000000..3277a9aa
--- /dev/null
+++ b/tests/utils/index.ts
@@ -0,0 +1,19 @@
+import { expect, Page } from '@playwright/test';
+
+let isLoggedIn = false;
+
+export async function login(page: Page) {
+ if (!process.env.TEST_APP_URL) throw new Error('Expect TEST_APP_URL env.');
+
+ if (isLoggedIn) return;
+
+ await page.goto('/');
+ await expect(page).toHaveTitle(/^Sign in to /);
+ await page.fill("input[name='username']", 'admin');
+ await page.fill("input[name='password']", '1234');
+ await page.click('id=kc-login');
+ await page.waitForTimeout(2000);
+ await expect(page).toHaveURL(new RegExp('^' + process.env.TEST_APP_URL));
+
+ isLoggedIn = true;
+}