feat: debit note (#172)
* feat: new file * feat: function api debit * feat: add route debit * feat: new form page * refactor: show menu debit * refactor: add type debit note status * feat: add i18n * feat: add constants * feat: add stores * feat: layout * feat: add function * refactor: change name value * feat: form select quotation * refactor: change name url * refactor: use form debit * refactor: change src import * refactor: move file form debit * refactor: add i18n * feat: add type debit note * refactor: add columns * refactor: bind value columns * refactor: change name Table * refactor: edit type * refactor: bind type debit note * refactor: bind value debit * refactor: chame name function * fix: calculate page * refactor: delete table * refactor: change name get list * refactor: change i18n * refactor: change name value * refactor: bind navigate and trigger delete * refactor: format number deciml * refactor: add i18n * feat: new page * refactor: add color debit * feat: Debit tab #178 * feat: TableRequest * refactor: edit type pay condition * refactor: add i18n btn submit * refactor: use type enum * feat: edit layout product expansion * refactor: bind function * refactor: show code * feat: add input search and select status * feat: paymentform * refactor: edit type * refactor: add manage file and edit end point * feat: add form.ts * refactor: send mode * refactor: edit v-model of due date * feat: submit create debit * fix: status * refactor: handle data not allow * fix: call updateDebitNote in edit mode and simplify payload handling * refactor: hide edit * refactor: handle pay condition only full * refactor: delete pay split * refactor: add query * refactor: handle is debit note * refactor: handle is quotation * refactor: add props hide * refactor: tap payment and receipt * refactor: add i18n * feat: view document * refactor: handle btn view doc * refactor: use my remark --------- Co-authored-by: Thanaphon Frappet <thanaphon@frappet.com> Co-authored-by: nwpptrs <jay02499@gmail.com> Co-authored-by: aif912752 <siripak@chamomind.com>
This commit is contained in:
parent
e3c781f857
commit
79240f53b0
32 changed files with 4172 additions and 12 deletions
39
src/components/12_debit-note/FormDebit.vue
Normal file
39
src/components/12_debit-note/FormDebit.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<script lang="ts" setup>
|
||||
import SelectQuotation from 'components/shared/select/SelectQuotation.vue';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
|
||||
const quotationId = defineModel<string>('quotationId', {
|
||||
required: true,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
<section
|
||||
:id="`form-credit`"
|
||||
class="col-12 q-pb-sm text-weight-bold text-body1 row items-center"
|
||||
>
|
||||
<q-icon
|
||||
flat
|
||||
size="xs"
|
||||
class="q-pa-sm rounded q-mr-xs"
|
||||
color="info"
|
||||
name="mdi-file-outline"
|
||||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`general.document`) }}
|
||||
</section>
|
||||
|
||||
<section class="col-12 row q-col-gutter-sm">
|
||||
<SelectQuotation
|
||||
for="select-quotation"
|
||||
class="col"
|
||||
v-model:value="quotationId"
|
||||
:label="$t('general.select', { msg: $t('quotation.title') })"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
|
|
@ -1125,6 +1125,8 @@ export default {
|
|||
},
|
||||
|
||||
preview: {
|
||||
dateAt: 'Date {msg}',
|
||||
seller: 'Seller',
|
||||
taskOrder: 'Work Order',
|
||||
doc: 'View Document',
|
||||
productList: 'Product List',
|
||||
|
|
@ -1135,15 +1137,17 @@ export default {
|
|||
vat: 'VAT',
|
||||
value: 'Value',
|
||||
netValue: 'Net Value',
|
||||
dueDate: 'Due Date',
|
||||
paymentMethods: 'Payment Methods',
|
||||
title: {
|
||||
creditNote: 'Credit Note',
|
||||
quotation: 'Quotation',
|
||||
invoice: 'Invoice',
|
||||
payment: 'Payment',
|
||||
receipt: 'Receipt',
|
||||
debitNote: 'Debit Note',
|
||||
},
|
||||
},
|
||||
|
||||
address: {
|
||||
subDistrict: 'Sub District',
|
||||
subArea: 'Sub Area',
|
||||
|
|
@ -1225,4 +1229,49 @@ export default {
|
|||
dataSum: 'Summary of all receipts/tax invoices',
|
||||
workSheetName: 'Worksheet name',
|
||||
},
|
||||
|
||||
debitNote: {
|
||||
title: 'Debit Note',
|
||||
caption: 'All Debit Notes',
|
||||
expire: 'Expired',
|
||||
payment: 'Payment',
|
||||
receipt: 'Receipt',
|
||||
succeed: 'Completed',
|
||||
downloadReceipt: 'Download Receipt',
|
||||
downloadTaxInvoice: 'Download Tax Invoice',
|
||||
|
||||
label: {
|
||||
additionalDetail: 'Additional Details',
|
||||
specifyReasonForDebit: 'Specify Reason for Debit',
|
||||
debitNoteInformation: 'Debit Note Information',
|
||||
codeDebit: 'Debit Note Number',
|
||||
codeQuotation: 'Quotation Number',
|
||||
quotationWorkName: 'Work Name',
|
||||
quotationPayment: 'Payment Method',
|
||||
value: 'Net Value',
|
||||
submit: 'Approve Debit Note',
|
||||
},
|
||||
|
||||
stats: {
|
||||
Pending: 'Debit Note',
|
||||
Expire: 'Expired',
|
||||
Payment: 'Payment',
|
||||
Receipt: 'Receipt',
|
||||
Succeed: 'Completed',
|
||||
},
|
||||
|
||||
viewMode: {
|
||||
payment: 'Payment',
|
||||
receipt: 'Receipt/Tax Invoice',
|
||||
processComplete: 'Completed',
|
||||
},
|
||||
|
||||
status: {
|
||||
Pending: 'Debit Note',
|
||||
Expire: 'Expired',
|
||||
Payment: 'Payment',
|
||||
Receipt: 'Receipt',
|
||||
Succeed: 'Completed',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1106,6 +1106,8 @@ export default {
|
|||
},
|
||||
|
||||
preview: {
|
||||
dateAt: 'วันที่{msg}',
|
||||
seller: 'ผู้ขาย',
|
||||
taskOrder: 'ใบสั่งงาน',
|
||||
doc: 'ดูเอกสาร',
|
||||
productList: 'รายการสินค้า',
|
||||
|
|
@ -1116,12 +1118,15 @@ export default {
|
|||
vat: 'ภาษี',
|
||||
value: 'มูลค่า',
|
||||
netValue: 'มูลค่าสุทธิ',
|
||||
dueDate: 'วันครบกำหนดชำระ',
|
||||
paymentMethods: 'ช่องทางชำระเงิน',
|
||||
title: {
|
||||
creditNote: 'ใบลดหนี้',
|
||||
quotation: 'ใบเสนอราคา',
|
||||
invoice: 'ใบแจ้งหนี้',
|
||||
payment: 'ชำระหนี้',
|
||||
receipt: 'ใบเสร็จรับเงิน',
|
||||
debitNote: 'ใบเพิ่มหนี้',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -1206,4 +1211,48 @@ export default {
|
|||
dataSum: 'สรุปใบเสร็จรับเงิน/กำกับภาษีทั้งหมด',
|
||||
workSheetName: 'ชื่อใบงาน',
|
||||
},
|
||||
debitNote: {
|
||||
title: 'ใบเพิ่มหนี้',
|
||||
caption: 'ใบเพิ่มหนี้ทั้งหมด',
|
||||
expire: 'พ้นกำหนด',
|
||||
payment: 'ชำระเงิน',
|
||||
receipt: 'ใบเสร็จรับเงิน',
|
||||
succeed: 'เสร็จสิ้น',
|
||||
downloadReceipt: 'ดาวน์โหลดใบเสร็จรับเงิน',
|
||||
downloadTaxInvoice: 'ดาวน์โหลดใบกำกับภาษี',
|
||||
|
||||
label: {
|
||||
additionalDetail: 'อธิบายเพิ่มเติม',
|
||||
specifyReasonForDebit: 'ระบุสาเหตุการเพิ่มหนี้',
|
||||
debitNoteInformation: 'ข้อมูลการเพิ่มหนี้',
|
||||
codeDebit: 'เลขที่ใบเพิ่มหนี้',
|
||||
codeQuotation: 'เลขที่ใบเสนอราคา',
|
||||
quotationWorkName: 'ชื่อใบงาน',
|
||||
quotationPayment: 'วิธีการชำระ',
|
||||
value: 'มูลค่าสุทธิ',
|
||||
submit: 'อนุมัติใบเพิ่มหนี้',
|
||||
},
|
||||
|
||||
stats: {
|
||||
Pending: 'ใบเพิ่มหนี้',
|
||||
Expire: 'พ้นกำหนด',
|
||||
Payment: 'ชำระเงิน',
|
||||
Receipt: 'ใบเสร็จรับเงิน',
|
||||
Succeed: 'เสร็จสิ้น',
|
||||
},
|
||||
|
||||
viewMode: {
|
||||
payment: 'ชำระเงิน',
|
||||
receipt: 'ใบเสร็จรับเงิน/ใบกำกับภาษี',
|
||||
processComplete: 'เสร็จสิ้น',
|
||||
},
|
||||
|
||||
status: {
|
||||
Pending: 'ใบเพิ่มหนี้',
|
||||
Expire: 'พ้นกำหนด',
|
||||
Payment: 'ชำระเงิน',
|
||||
Receipt: 'ใบเสร็จรับเงิน',
|
||||
Succeed: 'เสร็จสิ้น',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ onMounted(async () => {
|
|||
children: [
|
||||
{ label: 'receipt', route: '/receipt' },
|
||||
{ label: 'creditNote', route: '/credit-note', disabled: true },
|
||||
{ label: 'debitNote', route: '', disabled: true },
|
||||
{ label: 'debitNote', route: '/debit-note' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,16 @@ import { dateFormatJS } from 'src/utils/datetime';
|
|||
import { QFile, QMenu } from 'quasar';
|
||||
import UploadFileCard from 'src/components/upload-file/UploadFileCard.vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { DebitNote } from 'src/stores/debit-note';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const quotationPayment = useQuotationPayment();
|
||||
const { data: config } = storeToRefs(configStore);
|
||||
|
||||
const prop = defineProps<{ data?: Quotation | QuotationFull }>();
|
||||
const prop = defineProps<{
|
||||
data?: Quotation | QuotationFull | DebitNote;
|
||||
isDebitNote?: boolean;
|
||||
}>();
|
||||
|
||||
const refQFile = ref<InstanceType<typeof QFile>[]>([]);
|
||||
const refQMenu = ref<InstanceType<typeof QMenu>[]>([]);
|
||||
|
|
@ -194,7 +198,12 @@ async function triggerSubmit() {
|
|||
|
||||
onMounted(async () => {
|
||||
if (!prop.data) return;
|
||||
const ret = await quotationPayment.getQuotationPayment(prop.data.id);
|
||||
const ret = await quotationPayment.getQuotationPayment({
|
||||
quotationId: prop.isDebitNote === true ? undefined : prop.data.id,
|
||||
debitNoteId: prop.isDebitNote === true ? prop.data.id : undefined,
|
||||
quotationOnly: !!prop.isDebitNote ? false : true,
|
||||
debitNoteOnly: !!prop.isDebitNote ? true : false,
|
||||
});
|
||||
if (ret) {
|
||||
paymentData.value = ret.result;
|
||||
slipFile.value = paymentData.value.map((v) => ({
|
||||
|
|
|
|||
|
|
@ -443,6 +443,8 @@ async function fetchQuotation() {
|
|||
async function fetchReceipt() {
|
||||
const res = await useReceiptStore.getReceiptList({
|
||||
quotationId: quotationFormData.value.id,
|
||||
quotationOnly: true,
|
||||
debitNoteOnly: false,
|
||||
});
|
||||
|
||||
if (res) {
|
||||
|
|
@ -856,7 +858,7 @@ function convertToTable(nodes: Node[]) {
|
|||
} else {
|
||||
quotationFormData.value.paySplit = [];
|
||||
quotationFormData.value.paySplitCount = 0;
|
||||
quotationFormData.value.payCondition = 'Full';
|
||||
quotationFormData.value.payCondition = PayCondition.Full;
|
||||
}
|
||||
|
||||
tempPaySplit.value = JSON.parse(
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ defineProps<{
|
|||
};
|
||||
taskOrder?: boolean;
|
||||
taskOrderComplete?: boolean;
|
||||
debitNote?: boolean;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
@ -216,6 +217,7 @@ watch(
|
|||
'invoice-color': view === View.Invoice,
|
||||
'receipt-color': view === View.Receipt,
|
||||
'task-order-color': taskOrder && !taskOrderComplete,
|
||||
'debit-note-color': debitNote,
|
||||
}"
|
||||
>
|
||||
<div class="col bordered-r" v-if="!taskOrder">
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
import { dateFormatJS } from 'src/utils/datetime';
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
import { MainButton, ViewButton } from 'components/button';
|
||||
|
||||
|
|
@ -12,8 +13,10 @@ defineEmits<{
|
|||
withDefaults(
|
||||
defineProps<{
|
||||
title?: string;
|
||||
hideExampleBtn?: boolean;
|
||||
successLabel?: string;
|
||||
useBtnDownload?: boolean;
|
||||
hideViewBtn?: boolean;
|
||||
hideExampleBtn?: boolean;
|
||||
|
||||
payType?: string;
|
||||
amount?: number;
|
||||
|
|
@ -52,7 +55,7 @@ withDefaults(
|
|||
</aside>
|
||||
|
||||
<aside class="column col q-py-md text-right self-center q-px-md">
|
||||
<div class="q-gutter-x-xs">
|
||||
<div class="q-gutter-x-xs row justify-end">
|
||||
<MainButton
|
||||
v-if="!hideExampleBtn"
|
||||
icon="mdi-play-box-outline"
|
||||
|
|
@ -60,7 +63,40 @@ withDefaults(
|
|||
@click="() => $emit('example', index)"
|
||||
/>
|
||||
|
||||
<ViewButton icon-only @click="() => $emit('view', index)" />
|
||||
<ViewButton
|
||||
v-if="!hideViewBtn"
|
||||
icon-only
|
||||
@click="() => $emit('view', index)"
|
||||
/>
|
||||
|
||||
<q-btn-dropdown
|
||||
v-if="!!useBtnDownload"
|
||||
flat
|
||||
outline
|
||||
dropdown-icon="mdi-download"
|
||||
no-icon-animation
|
||||
padding="0 0"
|
||||
>
|
||||
<q-list>
|
||||
<q-item clickable>
|
||||
<q-item-section side>
|
||||
<Icon
|
||||
icon="fluent:receipt-money-24-regular"
|
||||
width="24px"
|
||||
height="24px"
|
||||
/>
|
||||
</q-item-section>
|
||||
{{ $t('debitNote.downloadReceipt') }}
|
||||
</q-item>
|
||||
|
||||
<q-item clickable>
|
||||
<q-item-section side>
|
||||
<Icon icon="hugeicons:invoice-03" width="24px" height="24px" />
|
||||
</q-item-section>
|
||||
{{ $t('debitNote.downloadTaxInvoice') }}
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</div>
|
||||
<span class="app-text-positive text-weight-bold text-body1">
|
||||
{{ successLabel || $t('quotation.receiptDialog.receiptIssued') }}
|
||||
|
|
|
|||
|
|
@ -64,12 +64,11 @@ function goToRequestList(id: string) {
|
|||
}))
|
||||
"
|
||||
:columns
|
||||
hide-bottom
|
||||
bordered
|
||||
flat
|
||||
hide-pagination
|
||||
selection="multiple"
|
||||
card-container-class="q-col-gutter-sm"
|
||||
:no-data-label="$t('general.noDataTable')"
|
||||
class="full-width"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
|
|
|
|||
1322
src/pages/12_debit-note/FormPage.vue
Normal file
1322
src/pages/12_debit-note/FormPage.vue
Normal file
File diff suppressed because it is too large
Load diff
499
src/pages/12_debit-note/MainPage.vue
Normal file
499
src/pages/12_debit-note/MainPage.vue
Normal file
|
|
@ -0,0 +1,499 @@
|
|||
<script lang="ts" setup>
|
||||
// NOTE: Library
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
// NOTE: Components
|
||||
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||
import NoData from 'src/components/NoData.vue';
|
||||
import PaginationComponent from 'src/components/PaginationComponent.vue';
|
||||
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||
import FloatingActionButton from 'components/FloatingActionButton.vue';
|
||||
import DialogFormContainer from 'src/components/dialog/DialogFormContainer.vue';
|
||||
import DialogHeader from 'src/components/dialog/DialogHeader.vue';
|
||||
import { CancelButton, SaveButton } from 'src/components/button';
|
||||
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
|
||||
import FormDebit from 'src/components/12_debit-note/FormDebit.vue';
|
||||
import TableDebitNote from './TableDebitNote.vue';
|
||||
|
||||
// NOTE: Stores & Type
|
||||
import { useNavigator } from 'src/stores/navigator';
|
||||
import useFlowStore from 'src/stores/flow';
|
||||
import { pageTabs, columns, hslaColors } from './constants';
|
||||
import { DebitNoteStatus, useDebitNote } from 'src/stores/debit-note';
|
||||
import { ref } from 'vue';
|
||||
import { dialogWarningClose } from 'src/stores/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
const flow = useFlowStore();
|
||||
const navigator = useNavigator();
|
||||
const debitNote = useDebitNote();
|
||||
const selectedQuotationId = ref<string>('');
|
||||
const { stats, pageMax, page, data, pageSize } = storeToRefs(debitNote);
|
||||
|
||||
// NOTE: Variable
|
||||
const pageState = reactive({
|
||||
quotationId: '',
|
||||
currentTab: DebitNoteStatus.Pending,
|
||||
hideStat: false,
|
||||
statusFilter: 'None',
|
||||
inputSearch: '',
|
||||
fieldSelected: columns
|
||||
.filter((v) => !v.name.startsWith('#'))
|
||||
.map((v) => v.name),
|
||||
gridView: false,
|
||||
total: 0,
|
||||
|
||||
debitDialog: false,
|
||||
});
|
||||
|
||||
const fieldSelectedOption = computed(() => {
|
||||
return columns
|
||||
.filter((v) => !v.name.startsWith('#'))
|
||||
.map((v) => ({
|
||||
label: v.label,
|
||||
value: v.name,
|
||||
}));
|
||||
});
|
||||
|
||||
// NOTE: Function
|
||||
|
||||
async function getList(opts?: { page?: number; pageSize?: number }) {
|
||||
const res = await debitNote.getDebitNoteList({
|
||||
page: opts?.page || page.value,
|
||||
pageSize: opts?.pageSize || pageSize.value,
|
||||
query: pageState.inputSearch === '' ? undefined : pageState.inputSearch,
|
||||
deebitNoteStatus: pageState.currentTab as DebitNoteStatus | undefined,
|
||||
includeRegisteredBranch: true,
|
||||
});
|
||||
|
||||
if (res) {
|
||||
data.value = res.result;
|
||||
pageState.total = res.total;
|
||||
pageMax.value = Math.ceil(res.total / pageSize.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerDelete(id: string) {
|
||||
dialogWarningClose(t, {
|
||||
message: t('dialog.message.confirmDelete'),
|
||||
actionText: t('dialog.action.ok'),
|
||||
action: async () => {
|
||||
const res = await debitNote.deleteDebitNote(id);
|
||||
if (!!res) {
|
||||
getList();
|
||||
}
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
async function triggerCreateDebitNote() {
|
||||
pageState.debitDialog = true;
|
||||
}
|
||||
|
||||
function navigateTo(opts: {
|
||||
statusDialog: 'info' | 'edit' | 'create';
|
||||
quotationId?: string;
|
||||
debitId?: string;
|
||||
}) {
|
||||
const url = new URL(
|
||||
`/debit-note/${opts.statusDialog === 'create' ? 'add' : opts.debitId}`,
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
if (opts.statusDialog === 'create') {
|
||||
url.searchParams.append('quotationId', opts.quotationId || '');
|
||||
}
|
||||
|
||||
url.searchParams.append('mode', opts.statusDialog);
|
||||
|
||||
window.open(url.toString(), '_blank');
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
navigateTo({ statusDialog: 'create', quotationId: pageState.quotationId });
|
||||
}
|
||||
|
||||
function close() {
|
||||
pageState.debitDialog = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
navigator.current.title = 'debitNote.title';
|
||||
navigator.current.path = [{ text: 'debitNote.caption', i18n: true }];
|
||||
|
||||
debitNote.getDebitNoteStats().then((res) => res && (stats.value = res));
|
||||
getList();
|
||||
});
|
||||
|
||||
watch(
|
||||
[
|
||||
() => pageState.currentTab,
|
||||
() => pageState.inputSearch,
|
||||
() => pageSize.value,
|
||||
() => pageState.statusFilter,
|
||||
],
|
||||
() => getList(),
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<!-- #TODO add trigger dialog add debit -->
|
||||
<FloatingActionButton
|
||||
style="z-index: 999"
|
||||
hide-icon
|
||||
@click.stop="() => triggerCreateDebitNote()"
|
||||
></FloatingActionButton>
|
||||
|
||||
<div class="column full-height no-wrap">
|
||||
<!-- SEC: stat -->
|
||||
<section class="text-body-2 q-mb-xs flex items-center">
|
||||
{{ $t('general.dataSum') }}
|
||||
<q-badge
|
||||
rounded
|
||||
class="q-ml-sm"
|
||||
style="
|
||||
background-color: hsla(var(--info-bg) / 0.15);
|
||||
color: hsl(var(--info-bg));
|
||||
"
|
||||
>
|
||||
{{ pageState.total }}
|
||||
</q-badge>
|
||||
<q-btn
|
||||
class="q-ml-sm"
|
||||
icon="mdi-pin-outline"
|
||||
color="primary"
|
||||
size="sm"
|
||||
flat
|
||||
dense
|
||||
rounded
|
||||
@click="pageState.hideStat = !pageState.hideStat"
|
||||
:style="pageState.hideStat ? 'rotate: 90deg' : ''"
|
||||
style="transition: 0.1s ease-in-out"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<transition name="slide">
|
||||
<div v-if="!pageState.hideStat" class="scroll q-mb-md">
|
||||
<div style="display: inline-block">
|
||||
<StatCardComponent
|
||||
labelI18n
|
||||
:branch="[
|
||||
{
|
||||
icon: 'material-symbols-light:receipt-long',
|
||||
count: stats[DebitNoteStatus.Pending],
|
||||
label: `debitNote.stats.${DebitNoteStatus.Pending}`,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-clock-alert-outline',
|
||||
count: stats[DebitNoteStatus.Expire],
|
||||
label: `debitNote.stats.${DebitNoteStatus.Expire}`,
|
||||
color: 'cyan',
|
||||
},
|
||||
{
|
||||
icon: 'tabler:cash-register',
|
||||
count: stats[DebitNoteStatus.Payment],
|
||||
label: `debitNote.stats.${DebitNoteStatus.Payment}`,
|
||||
color: 'dark-orange',
|
||||
},
|
||||
{
|
||||
icon: 'fluent:receipt-money-16-regular',
|
||||
count: stats[DebitNoteStatus.Receipt],
|
||||
label: `debitNote.stats.${DebitNoteStatus.Receipt}`,
|
||||
color: 'green',
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'mdi-check-decagram-outline',
|
||||
count: stats[DebitNoteStatus.Succeed],
|
||||
label: `debitNote.stats.${DebitNoteStatus.Succeed}`,
|
||||
color: 'blue',
|
||||
},
|
||||
]"
|
||||
:dark="$q.dark.isActive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<section class="col surface-1 rounded bordered overflow-hidden">
|
||||
<div class="column full-height">
|
||||
<!-- SEC: header content -->
|
||||
<header
|
||||
class="row surface-3 justify-between full-width items-center"
|
||||
style="z-index: 1"
|
||||
>
|
||||
<section
|
||||
class="row q-py-sm q-px-md bordered-b justify-between full-width"
|
||||
>
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="q-mr-md col-12 col-md-3"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="pageState.inputSearch"
|
||||
debounce="200"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div
|
||||
class="row col-12 col-md-3 justify-end"
|
||||
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||
style="white-space: nowrap"
|
||||
>
|
||||
<q-select
|
||||
v-if="!pageState.gridView"
|
||||
id="select-field"
|
||||
for="select-field"
|
||||
class="col"
|
||||
:options="
|
||||
fieldSelectedOption.map((v) => ({
|
||||
...v,
|
||||
label: v.label && $t(v.label),
|
||||
}))
|
||||
"
|
||||
:display-value="$t('general.displayField')"
|
||||
:hide-dropdown-icon="$q.screen.lt.sm"
|
||||
v-model="pageState.fieldSelected"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
map-options
|
||||
emit-value
|
||||
outlined
|
||||
multiple
|
||||
dense
|
||||
/>
|
||||
|
||||
<q-btn-toggle
|
||||
id="btn-mode"
|
||||
v-model="pageState.gridView"
|
||||
dense
|
||||
class="no-shadow bordered rounded surface-1 q-ml-sm"
|
||||
:toggle-color="$q.dark.isActive ? 'grey-9' : 'grey-2'"
|
||||
size="xs"
|
||||
:options="[
|
||||
{ value: true, slot: 'folder' },
|
||||
{ value: false, slot: 'list' },
|
||||
]"
|
||||
>
|
||||
<template v-slot:folder>
|
||||
<q-icon
|
||||
name="mdi-view-grid-outline"
|
||||
size="16px"
|
||||
class="q-px-sm q-py-xs rounded"
|
||||
:style="{
|
||||
color: $q.dark.isActive
|
||||
? pageState.gridView
|
||||
? '#C9D3DB '
|
||||
: '#787B7C'
|
||||
: pageState.gridView
|
||||
? '#787B7C'
|
||||
: '#C9D3DB',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:list>
|
||||
<q-icon
|
||||
name="mdi-format-list-bulleted"
|
||||
class="q-px-sm q-py-xs rounded"
|
||||
size="16px"
|
||||
:style="{
|
||||
color: $q.dark.isActive
|
||||
? pageState.gridView === false
|
||||
? '#C9D3DB'
|
||||
: '#787B7C'
|
||||
: pageState.gridView === false
|
||||
? '#787B7C'
|
||||
: '#C9D3DB',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
</q-btn-toggle>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav class="surface-2 bordered-b q-px-md full-width">
|
||||
<q-tabs
|
||||
inline-label
|
||||
mobile-arrows
|
||||
dense
|
||||
v-model="pageState.currentTab"
|
||||
align="left"
|
||||
class="full-width"
|
||||
active-color="info"
|
||||
>
|
||||
<q-tab
|
||||
v-for="tab in pageTabs"
|
||||
:name="tab.value"
|
||||
:key="tab.value"
|
||||
@click="
|
||||
() => {
|
||||
pageState.currentTab = tab.value;
|
||||
pageState.inputSearch = '';
|
||||
|
||||
flow.rotate();
|
||||
}
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="row text-capitalize"
|
||||
:class="
|
||||
pageState.currentTab === tab.value
|
||||
? 'text-bold'
|
||||
: 'app-text-muted'
|
||||
"
|
||||
>
|
||||
{{ $t(`debitNote.status.${tab.label}`) }}
|
||||
</div>
|
||||
</q-tab>
|
||||
</q-tabs>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- SEC: body content -->
|
||||
<article
|
||||
v-if="data.length === 0"
|
||||
class="col surface-2 flex items-center justify-center"
|
||||
>
|
||||
<NoData :not-found="!!pageState.inputSearch" />
|
||||
</article>
|
||||
<article v-else class="col surface-2 full-width scroll q-pa-md">
|
||||
<!-- #TODO change Table -->
|
||||
<TableDebitNote
|
||||
:grid="pageState.gridView"
|
||||
:visible-columns="pageState.fieldSelected"
|
||||
@view="
|
||||
(item) => navigateTo({ statusDialog: 'info', debitId: item.id })
|
||||
"
|
||||
@edit="
|
||||
(item) => navigateTo({ statusDialog: 'edit', debitId: item.id })
|
||||
"
|
||||
@delete="(item) => triggerDelete(item.id)"
|
||||
>
|
||||
<template #grid="{ item }">
|
||||
<div class="col-md-4 col-sm-6 col-12">
|
||||
<QuotationCard
|
||||
hide-kebab-edit
|
||||
@view="
|
||||
() =>
|
||||
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}`,
|
||||
),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</TableDebitNote>
|
||||
</article>
|
||||
|
||||
<footer
|
||||
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
||||
v-if="pageMax > 0"
|
||||
>
|
||||
<div class="col-4">
|
||||
<div class="row items-center no-wrap">
|
||||
<div class="app-text-muted q-mr-sm" v-if="$q.screen.gt.sm">
|
||||
{{ $t('general.recordPerPage') }}
|
||||
</div>
|
||||
<div><PaginationPageSize v-model="pageSize" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 row justify-center app-text-muted">
|
||||
{{
|
||||
$t('general.recordsPage', {
|
||||
resultcurrentPage: data.length,
|
||||
total: pageState.inputSearch ? data.length : pageState.total,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<nav class="col-4 row justify-end">
|
||||
<!-- #TODO add getList at fectch-data -->
|
||||
<PaginationComponent
|
||||
v-model:current-page="page"
|
||||
v-model:max-page="pageMax"
|
||||
:fetch-data="() => getList()"
|
||||
/>
|
||||
</nav>
|
||||
</footer>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- SEC: Dialog -->
|
||||
<!-- dialog create -->
|
||||
|
||||
<!-- #TODO add submit at submit -->
|
||||
<DialogFormContainer
|
||||
width="60vw"
|
||||
height="500px"
|
||||
v-model="pageState.debitDialog"
|
||||
@submit="() => submit()"
|
||||
>
|
||||
<template #header>
|
||||
<DialogHeader
|
||||
:title="$t(`general.add`, { text: $t('debitNote.title') })"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<section class="q-pa-md col full-width">
|
||||
<div class="surface-1 rounded bordered q-pa-md full-height full-width">
|
||||
<!-- TODO: bind quotation id -->
|
||||
<FormDebit v-model:quotation-id="pageState.quotationId" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<template #footer>
|
||||
<CancelButton class="q-ml-auto" outlined @click="close()" />
|
||||
<SaveButton
|
||||
:label="$t(`general.add`, { text: $t('debitNote.title') })"
|
||||
class="q-ml-sm"
|
||||
icon="mdi-check"
|
||||
solid
|
||||
type="submit"
|
||||
/>
|
||||
</template>
|
||||
</DialogFormContainer>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
95
src/pages/12_debit-note/TableDebitNote.vue
Normal file
95
src/pages/12_debit-note/TableDebitNote.vue
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { QTableSlots } from 'quasar';
|
||||
import { DebitNote, useDebitNote } from 'src/stores/debit-note';
|
||||
|
||||
import { columns } from './constants.ts';
|
||||
import KebabAction from 'src/components/shared/KebabAction.vue';
|
||||
|
||||
const debitNote = useDebitNote();
|
||||
const { data, page, pageSize } = storeToRefs(debitNote);
|
||||
|
||||
const prop = defineProps<{ grid: boolean; visibleColumns: string[] }>();
|
||||
defineEmits<{ (evt: 'view' | 'delete' | 'edit', val: DebitNote): void }>();
|
||||
|
||||
const visible = computed(() =>
|
||||
columns.filter(
|
||||
(v) => prop.visibleColumns.includes(v.name) || v.name === '#action',
|
||||
),
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<q-table
|
||||
:rows-per-page-options="[0]"
|
||||
:rows="
|
||||
data.map((item, i) => ({
|
||||
...item,
|
||||
_index: i,
|
||||
_page: page,
|
||||
_pageSize: pageSize,
|
||||
}))
|
||||
"
|
||||
:columns="visible"
|
||||
:grid
|
||||
hide-bottom
|
||||
bordered
|
||||
flat
|
||||
hide-pagination
|
||||
selection="multiple"
|
||||
card-container-class="q-col-gutter-sm"
|
||||
class="full-width"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||
:props="props"
|
||||
>
|
||||
<q-th v-for="col in visible" :key="col.name" :props="props">
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
{{ $t(col.label) }}
|
||||
</template>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-slot:body="props: {
|
||||
row: DebitNote & { _index: number; _page: number; _pageSize: number };
|
||||
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||
>
|
||||
<q-tr :class="{ dark: $q.dark.isActive }" class="text-center">
|
||||
<q-td v-for="col in visible" :align="col.align">
|
||||
<!-- NOTE: custom column will starts with # -->
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
<span v-if="col.name !== 'quotationPayment'">
|
||||
{{
|
||||
typeof col.field === 'string'
|
||||
? props.row[col.field as keyof DebitNote]
|
||||
: col.field(props.row)
|
||||
}}
|
||||
</span>
|
||||
|
||||
<span v-if="col.name === 'quotationPayment'">
|
||||
{{ $t(`quotation.type.${col.field(props.row)}`) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-if="col.name === '#action'">
|
||||
<KebabAction
|
||||
hide-toggle
|
||||
hide-edit
|
||||
@edit="$emit('edit', props.row)"
|
||||
@delete="$emit('delete', props.row)"
|
||||
@view="$emit('view', props.row)"
|
||||
/>
|
||||
</template>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item="props: { row: DebitNote }">
|
||||
<slot name="grid" :item="props" />
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
90
src/pages/12_debit-note/constants.ts
Normal file
90
src/pages/12_debit-note/constants.ts
Normal file
|
|
@ -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<string, string> = {
|
||||
Pending: '--blue-6-hsl',
|
||||
Success: '--red-6-hsl',
|
||||
};
|
||||
113
src/pages/12_debit-note/document-view/BankComponents.vue
Normal file
113
src/pages/12_debit-note/document-view/BankComponents.vue
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { QSelect } from 'quasar';
|
||||
|
||||
// NOTE: Import stores
|
||||
import { selectFilterOptionRefMod } from 'stores/utils';
|
||||
import useOptionStore from 'stores/options';
|
||||
|
||||
// NOTE Import Types
|
||||
import { BankBook } from 'stores/branch/types';
|
||||
|
||||
// NOTE: Import Components
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
|
||||
const bankBookOptions = ref<Record<string, unknown>[]>([]);
|
||||
let bankBookFilter: (
|
||||
value: string,
|
||||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
defineProps<{
|
||||
bankBook: BankBook;
|
||||
index: number;
|
||||
}>();
|
||||
|
||||
watch(
|
||||
() => optionStore.globalOption,
|
||||
() => {
|
||||
bankBookFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption.bankBook),
|
||||
bankBookOptions,
|
||||
'label',
|
||||
);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset class="rounded" style="border: 1px solid var(--gray-4)">
|
||||
<legend>ช่องทางที่ {{ index + 1 }}</legend>
|
||||
|
||||
<div class="border-5 full-width row" style="gap: var(--size-4)">
|
||||
<div class="column q-pa-sm" style="width: fit-content">
|
||||
<img
|
||||
:src="bankBook.bankUrl"
|
||||
class="rounded"
|
||||
style="
|
||||
border: 1px solid var(--gray-3);
|
||||
object-fit: scale-down;
|
||||
width: 1in;
|
||||
height: 1in;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="column col" style="gap: var(--size-3)">
|
||||
<div class="row" style="justify-content: space-between">
|
||||
<div class="text-with-label">
|
||||
<div>เลขบัญชีธนาคาร</div>
|
||||
<div class="row items-start">
|
||||
<img
|
||||
width="25px"
|
||||
height="25px"
|
||||
class="q-mr-xs"
|
||||
:src="`/img/bank/${bankBook.bankName}.png`"
|
||||
/>
|
||||
{{ bankBook.accountNumber }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-with-label">
|
||||
<div>เลขบัญชีธนาคาร</div>
|
||||
<div>{{ bankBook.accountNumber }}</div>
|
||||
</div>
|
||||
<div class="text-with-label">
|
||||
<div>ชื่อบัญชี</div>
|
||||
<div>{{ bankBook.accountName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="text-with-label">
|
||||
<div>สาขา</div>
|
||||
<div>{{ bankBook.bankBranch }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
legend {
|
||||
background-color: white;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.bordered-2 {
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.border-5 {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.text-with-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
& > :first-child {
|
||||
color: var(--gray-6);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
654
src/pages/12_debit-note/document-view/MainPage.vue
Normal file
654
src/pages/12_debit-note/document-view/MainPage.vue
Normal file
|
|
@ -0,0 +1,654 @@
|
|||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, nextTick, ref, watch } from 'vue';
|
||||
import { precisionRound } from 'src/utils/arithmetic';
|
||||
import ThaiBahtText from 'thai-baht-text';
|
||||
|
||||
// NOTE: Import stores
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
|
||||
import { useConfigStore } from 'stores/config';
|
||||
import useBranchStore from 'stores/branch';
|
||||
import { baseUrl } from 'stores/utils';
|
||||
import useCustomerStore from 'stores/customer';
|
||||
|
||||
import { DebitNotePayload, useDebitNote } from 'src/stores/debit-note';
|
||||
|
||||
// NOTE Import Types
|
||||
import { CustomerBranch } from 'stores/customer/types';
|
||||
import { BankBook, Branch } from 'stores/branch/types';
|
||||
import {
|
||||
CustomerBranchRelation,
|
||||
Details,
|
||||
ProductServiceList,
|
||||
} from 'src/stores/quotations/types';
|
||||
|
||||
// NOTE: Import Components
|
||||
import ViewPDF from './ViewPdf.vue';
|
||||
import ViewHeader from './ViewHeader.vue';
|
||||
import ViewFooter from './ViewFooter.vue';
|
||||
import BankComponents from './BankComponents.vue';
|
||||
import PrintButton from 'src/components/button/PrintButton.vue';
|
||||
import { convertTemplate } from 'src/utils/string-template';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const branchStore = useBranchStore();
|
||||
const customerStore = useCustomerStore();
|
||||
const debitNoteStore = useDebitNote();
|
||||
const { data: config } = storeToRefs(configStore);
|
||||
|
||||
type Product = {
|
||||
id: string;
|
||||
code: string;
|
||||
detail: string;
|
||||
amount: number;
|
||||
priceUnit: number;
|
||||
discount: number;
|
||||
vat: number;
|
||||
value: number;
|
||||
};
|
||||
|
||||
type SummaryPrice = {
|
||||
totalPrice: number;
|
||||
totalDiscount: number;
|
||||
vat: number;
|
||||
vatExcluded: number;
|
||||
finalPrice: number;
|
||||
};
|
||||
|
||||
const customer = ref<CustomerBranch>();
|
||||
const branch = ref<Branch>();
|
||||
const productList = ref<Product[]>([]);
|
||||
const bankList = ref<BankBook[]>([]);
|
||||
|
||||
const elements = ref<HTMLElement[]>([]);
|
||||
const chunks = ref<Product[][]>([[]]);
|
||||
const attachmentList = ref<
|
||||
{
|
||||
url: string;
|
||||
isImage?: boolean;
|
||||
isPDF?: boolean;
|
||||
}[]
|
||||
>([]);
|
||||
const data = ref<
|
||||
DebitNotePayload & {
|
||||
customerBranch: CustomerBranchRelation;
|
||||
customerBranchId: string;
|
||||
registeredBranchId: string;
|
||||
}
|
||||
>();
|
||||
|
||||
const productServiceList = ref<ProductServiceList[]>([]);
|
||||
|
||||
const summaryPrice = ref<SummaryPrice>({
|
||||
totalPrice: 0,
|
||||
totalDiscount: 0,
|
||||
vat: 0,
|
||||
vatExcluded: 0,
|
||||
finalPrice: 0,
|
||||
});
|
||||
|
||||
async function getAttachment(quotationId: string) {
|
||||
const attachment = await debitNoteStore.listAttachment({
|
||||
parentId: quotationId,
|
||||
});
|
||||
|
||||
if (attachment) {
|
||||
attachmentList.value = await Promise.all(
|
||||
attachment.map(async (v) => {
|
||||
const url = await debitNoteStore.getAttachment({
|
||||
parentId: quotationId,
|
||||
name: v,
|
||||
});
|
||||
const ft = v.substring(v.lastIndexOf('.') + 1);
|
||||
|
||||
return {
|
||||
url,
|
||||
isImage: ['png', 'jpg', 'jpeg'].includes(ft),
|
||||
isPDF: ft === 'pdf',
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function assignData() {
|
||||
for (let i = 0; i < productList.value.length; i++) {
|
||||
let el = elements.value.at(-1);
|
||||
|
||||
if (!el) return;
|
||||
|
||||
if (getHeight(el) < 500) {
|
||||
chunks.value.at(-1)?.push(productList.value[i]);
|
||||
} else {
|
||||
chunks.value.push([]);
|
||||
i--;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
|
||||
function getHeight(el: HTMLElement) {
|
||||
const shadow = document.createElement('div');
|
||||
|
||||
shadow.style.opacity = '0';
|
||||
shadow.style.position = 'absolute';
|
||||
shadow.style.top = '-999999px';
|
||||
shadow.style.left = '-999999px';
|
||||
shadow.style.pointerEvents = 'none';
|
||||
|
||||
document.body.appendChild(shadow);
|
||||
|
||||
shadow.appendChild(el.cloneNode(true));
|
||||
|
||||
const height = shadow.offsetHeight;
|
||||
|
||||
document.body.removeChild(shadow);
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
const details = ref<Details>();
|
||||
enum View {
|
||||
DebitNote,
|
||||
Invoice,
|
||||
Payment,
|
||||
Receipt,
|
||||
}
|
||||
const view = ref<View>(View.DebitNote);
|
||||
|
||||
onMounted(async () => {
|
||||
let str =
|
||||
localStorage.getItem('debit-note-preview') ||
|
||||
sessionStorage.getItem('debit-note-preview');
|
||||
|
||||
if (!str) return;
|
||||
|
||||
const obj: DebitNotePayload = JSON.parse(str);
|
||||
|
||||
if (obj) sessionStorage.setItem('debit-note-preview', JSON.stringify(obj));
|
||||
|
||||
delete localStorage['debit-note-preview'];
|
||||
|
||||
const parsed = JSON.parse(sessionStorage.getItem('debit-note-preview') || '');
|
||||
|
||||
data.value = 'data' in parsed ? parsed.data : undefined;
|
||||
|
||||
if (data.value) {
|
||||
if (data.value.id) {
|
||||
await getAttachment(data.value.id);
|
||||
}
|
||||
|
||||
const resCustomerBranch = await customerStore.getBranchById(
|
||||
data.value.customerBranchId,
|
||||
);
|
||||
|
||||
if (resCustomerBranch) {
|
||||
customer.value = resCustomerBranch;
|
||||
}
|
||||
|
||||
details.value = {
|
||||
code: parsed.meta.source.code,
|
||||
createdAt: parsed.meta.source.createdAt,
|
||||
createdBy: `${parsed.meta.createdBy} ${!parsed.meta.source.createdBy ? '' : parsed.meta.source.createdBy.telephoneNo}`,
|
||||
payCondition: parsed.meta.source.payCondition,
|
||||
contactName: parsed.meta.source.contactName,
|
||||
contactTel: parsed.meta.source.contactTel,
|
||||
workName: parsed.meta.source.workName,
|
||||
dueDate: parsed.meta.source.dueDate,
|
||||
worker: parsed.meta.selectedWorker,
|
||||
};
|
||||
|
||||
const resBranch = await branchStore.fetchById(
|
||||
data.value?.registeredBranchId,
|
||||
);
|
||||
|
||||
if (resBranch) {
|
||||
branch.value = resBranch;
|
||||
|
||||
bankList.value = resBranch.bank.map((v) => ({
|
||||
...v,
|
||||
bankUrl: `${baseUrl}/branch/${resBranch.id}/bank-qr/${v.id}?ts=${Date.now()}`,
|
||||
}));
|
||||
}
|
||||
|
||||
productServiceList.value = parsed.meta.productServicelist;
|
||||
|
||||
productList.value =
|
||||
productServiceList.value?.map((v) => ({
|
||||
id: v.product.id,
|
||||
code: v.product.code,
|
||||
detail: v.product.name,
|
||||
amount: v.amount || 0,
|
||||
priceUnit: v.pricePerUnit || 0,
|
||||
discount: v.discount || 0,
|
||||
vat: v.vat || 0,
|
||||
value: precisionRound(
|
||||
(v.pricePerUnit || 0) * v.amount -
|
||||
(v.discount || 0) +
|
||||
(v.product.calcVat
|
||||
? ((v.pricePerUnit || 0) * v.amount - (v.discount || 0)) *
|
||||
(config.value?.vat || 0.07)
|
||||
: 0),
|
||||
),
|
||||
})) || [];
|
||||
}
|
||||
|
||||
summaryPrice.value = (productServiceList.value || []).reduce(
|
||||
(a, c) => {
|
||||
const price = precisionRound((c.pricePerUnit || 0) * c.amount);
|
||||
const vat = precisionRound(
|
||||
((c.pricePerUnit || 0) * c.amount - (c.discount || 0)) *
|
||||
(config.value?.vat || 0.07),
|
||||
);
|
||||
|
||||
a.totalPrice = precisionRound(a.totalPrice + price);
|
||||
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
|
||||
a.vat = c.product.calcVat ? precisionRound(a.vat + vat) : a.vat;
|
||||
a.vatExcluded = c.product.calcVat
|
||||
? a.vatExcluded
|
||||
: precisionRound(a.vat + vat);
|
||||
a.finalPrice = precisionRound(
|
||||
a.totalPrice -
|
||||
a.totalDiscount +
|
||||
a.vat -
|
||||
Number(data.value?.discount || 0),
|
||||
);
|
||||
|
||||
return a;
|
||||
},
|
||||
{
|
||||
totalPrice: 0,
|
||||
totalDiscount: 0,
|
||||
vat: 0,
|
||||
vatExcluded: 0,
|
||||
finalPrice: 0,
|
||||
},
|
||||
);
|
||||
|
||||
assignData();
|
||||
});
|
||||
|
||||
watch(elements, () => {});
|
||||
|
||||
function print() {
|
||||
window.print();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toolbar">
|
||||
<PrintButton solid @click="print" />
|
||||
</div>
|
||||
<div class="row justify-between container color-debit-note">
|
||||
<section class="content" v-for="chunk in chunks">
|
||||
<ViewHeader
|
||||
v-if="!!branch && !!customer && !!details"
|
||||
:branch="branch"
|
||||
:customer="customer"
|
||||
:details="details"
|
||||
:view="view"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="q-mb-sm q-mt-md"
|
||||
style="
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
color: var(--main);
|
||||
display: block;
|
||||
border-bottom: 2px solid var(--main);
|
||||
"
|
||||
>
|
||||
{{ $t('preview.productList') }}
|
||||
</span>
|
||||
|
||||
<table ref="elements" class="q-mb-sm" cellpadding="0" style="width: 100%">
|
||||
<tbody class="color-tr">
|
||||
<tr>
|
||||
<th>{{ $t('preview.rank') }}</th>
|
||||
<th>{{ $t('preview.productCode') }}</th>
|
||||
<th>{{ $t('general.detail') }}</th>
|
||||
<th>{{ $t('general.amount') }}</th>
|
||||
<th>{{ $t('preview.pricePerUnit') }}</th>
|
||||
<th>{{ $t('preview.discount') }}</th>
|
||||
<th>{{ $t('preview.vat') }}</th>
|
||||
<th>{{ $t('preview.value') }}</th>
|
||||
</tr>
|
||||
<tr v-for="(v, i) in chunk">
|
||||
<td class="text-center">{{ i + 1 }}</td>
|
||||
<td>{{ v.code }}</td>
|
||||
<td>{{ v.detail }}</td>
|
||||
<td style="text-align: right">{{ v.amount }}</td>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumberDecimal(v.priceUnit, 2) }}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumberDecimal(v.discount, 2) }}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumberDecimal(v.vat, 2) }}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumberDecimal(v.value, 2) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table
|
||||
style="width: 40%; margin-left: auto"
|
||||
class="q-mb-md"
|
||||
cellpadding="0"
|
||||
>
|
||||
<tbody class="color-tr">
|
||||
<tr>
|
||||
<td>{{ $t('general.total') }}</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumberDecimal(summaryPrice.totalPrice, 2) }}
|
||||
฿
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ $t('general.discount') }}</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumberDecimal(summaryPrice.totalDiscount, 2) || 0 }} ฿
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('general.totalAfterDiscount') }}</td>
|
||||
<td class="text-right">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
summaryPrice.totalPrice - summaryPrice.totalDiscount,
|
||||
2,
|
||||
)
|
||||
}}
|
||||
฿
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ $t('general.totalVatExcluded') }}</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumberDecimal(summaryPrice.vatExcluded, 2) || 0 }}
|
||||
฿
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('general.vat', { msg: '7%' }) }}</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumberDecimal(summaryPrice.vat, 2) }} ฿
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('general.totalVatIncluded') }}</td>
|
||||
<td class="text-right">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
summaryPrice.totalPrice -
|
||||
summaryPrice.totalDiscount +
|
||||
summaryPrice.vat,
|
||||
2,
|
||||
)
|
||||
}}
|
||||
฿
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ $t('general.discountAfterVat') }}</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumberDecimal(data?.discount || 0, 2) }}
|
||||
฿
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="row justify-between q-mb-md" style="width: 100%">
|
||||
<div
|
||||
class="column set-width bg-color full-height"
|
||||
style="padding: 12px"
|
||||
>
|
||||
({{ ThaiBahtText(summaryPrice.finalPrice) }})
|
||||
</div>
|
||||
<div
|
||||
class="row text-right border-5 items-center"
|
||||
style="width: 40%; background: var(--main); padding: 8px"
|
||||
>
|
||||
<span style="color: white; font-weight: 600">ยอดรวมสุทธิ</span>
|
||||
<span
|
||||
class="border-5"
|
||||
style="
|
||||
width: 70%;
|
||||
margin-left: auto;
|
||||
background: white;
|
||||
padding: 4px;
|
||||
"
|
||||
>
|
||||
{{
|
||||
formatNumberDecimal(Math.max(summaryPrice.finalPrice, 0), 2) || 0
|
||||
}}
|
||||
฿
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content">
|
||||
<ViewHeader
|
||||
v-if="!!branch && !!customer && !!details"
|
||||
:branch="branch"
|
||||
:customer="customer"
|
||||
:details="details"
|
||||
:view="view"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="q-mb-sm q-mt-md"
|
||||
style="
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
color: var(--main);
|
||||
display: block;
|
||||
border-bottom: 2px solid var(--main);
|
||||
"
|
||||
>
|
||||
{{ $t('general.remark') }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="border-5 surface-0 detail-note q-mb-md"
|
||||
style="width: 100%; padding: 8px 16px; white-space: pre-wrap"
|
||||
>
|
||||
<div
|
||||
v-html="
|
||||
convertTemplate(data?.remark || '', {
|
||||
'quotation-payment': {
|
||||
paymentType: data?.payCondition || 'Full',
|
||||
amount: summaryPrice.finalPrice,
|
||||
installments: data?.paySplit,
|
||||
},
|
||||
'quotation-labor': {
|
||||
name:
|
||||
details?.worker.map(
|
||||
(v, i) =>
|
||||
`${i + 1}. ` +
|
||||
`${v.namePrefix}. ${v.firstNameEN} ${v.lastNameEN}`.toUpperCase(),
|
||||
) || [],
|
||||
},
|
||||
}) || '-'
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content">
|
||||
<ViewHeader
|
||||
v-if="!!branch && !!customer && !!details"
|
||||
:branch="branch"
|
||||
:customer="customer"
|
||||
:details="details"
|
||||
:view="view"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="q-mb-sm"
|
||||
style="
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
color: var(--main);
|
||||
display: block;
|
||||
border-bottom: 2px solid var(--main);
|
||||
"
|
||||
>
|
||||
{{ $t('preview.paymentMethods') }}
|
||||
</span>
|
||||
<article style="height: 5.8in">
|
||||
<BankComponents
|
||||
v-for="(bank, index) in bankList"
|
||||
:index="index"
|
||||
:bank-book="bank"
|
||||
:key="bank.id"
|
||||
/>
|
||||
</article>
|
||||
|
||||
<ViewFooter
|
||||
:data="{
|
||||
name: '',
|
||||
company: branch?.name || '',
|
||||
buyer: '',
|
||||
buyDate: '',
|
||||
approveDate: '',
|
||||
approver: '',
|
||||
}"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-for="item in attachmentList.filter((v) => v.isImage)"
|
||||
class="content"
|
||||
>
|
||||
<q-img :src="item.url" />
|
||||
</section>
|
||||
|
||||
<ViewPDF
|
||||
v-for="item in attachmentList.filter((v) => v.isPDF)"
|
||||
:url="item.url"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.color-debit-note {
|
||||
--main: var(--cyan-7);
|
||||
--main-hsl: var(--cyan-7-hsl);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--gray-3);
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--main);
|
||||
color: white;
|
||||
|
||||
padding: 4px;
|
||||
}
|
||||
td {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.border-5 {
|
||||
border-radius: 5px;
|
||||
}
|
||||
.set-width {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.bg-color {
|
||||
background-color: hsla(var(--main-hsl) / 0.1);
|
||||
}
|
||||
|
||||
.color-tr > tr:nth-child(odd) {
|
||||
background-color: hsla(var(--main-hsl) / 0.1);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-inline: auto;
|
||||
background: var(--gray-3);
|
||||
width: calc(8.3in + 1rem);
|
||||
}
|
||||
|
||||
.container :deep(*) {
|
||||
font-size: 95%;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
padding: 0.5in;
|
||||
align-items: center;
|
||||
background: white;
|
||||
page-break-after: always;
|
||||
break-after: page;
|
||||
height: 11.7in;
|
||||
max-height: 11.7in;
|
||||
}
|
||||
|
||||
.position-bottom {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.detail-note {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
& > * {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
border-style: solid;
|
||||
border-color: var(--main);
|
||||
}
|
||||
|
||||
@media print {
|
||||
.toolbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0;
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
98
src/pages/12_debit-note/document-view/ViewFooter.vue
Normal file
98
src/pages/12_debit-note/document-view/ViewFooter.vue
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
data?: {
|
||||
name: string;
|
||||
buyer: string;
|
||||
buyDate: string;
|
||||
|
||||
company: string;
|
||||
approver: string;
|
||||
approveDate: string;
|
||||
};
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<div class="footer-container">
|
||||
<div class="footer-top">
|
||||
<div>ในนาม {{ data?.name || '-' }}</div>
|
||||
<div>ในนาม {{ data?.company || '-' }}</div>
|
||||
</div>
|
||||
|
||||
<img src="/images/jws-stamp.png" alt="${0}" />
|
||||
|
||||
<div class="footer-bottom">
|
||||
<section>
|
||||
<div>
|
||||
<span class="data-placeholder"></span>
|
||||
<span>ผู้สั่งซื้อสินค้า</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="data-placeholder"></span>
|
||||
<span>วันที่</span>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div>
|
||||
<span class="data-placeholder"></span>
|
||||
<span>ผู้อนุมัติ</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="data-placeholder"></span>
|
||||
<span>วันที่</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.footer-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
height: 1.5in;
|
||||
}
|
||||
|
||||
.footer-top {
|
||||
position: absolute;
|
||||
width: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& > * {
|
||||
width: 38%;
|
||||
}
|
||||
}
|
||||
.footer-bottom {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& > * {
|
||||
display: flex;
|
||||
width: 38%;
|
||||
justify-content: space-around;
|
||||
|
||||
& > * {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-placeholder {
|
||||
display: block;
|
||||
min-width: 1.2in;
|
||||
border-bottom: 1px dotted black;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
</style>
|
||||
192
src/pages/12_debit-note/document-view/ViewHeader.vue
Normal file
192
src/pages/12_debit-note/document-view/ViewHeader.vue
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
<script lang="ts" setup>
|
||||
import { dateFormat } from 'src/utils/datetime';
|
||||
|
||||
// NOTE: Import stores
|
||||
import { formatAddress } from 'src/utils/address';
|
||||
|
||||
// NOTE Import Types
|
||||
import { Branch } from 'src/stores/branch/types';
|
||||
import { CustomerBranchRelation, Details } from 'src/stores/quotations/types';
|
||||
// NOTE: Import Components
|
||||
|
||||
enum View {
|
||||
DebitNote,
|
||||
Invoice,
|
||||
Payment,
|
||||
Receipt,
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
branch: Branch;
|
||||
customer: CustomerBranchRelation;
|
||||
details: Details;
|
||||
view: View;
|
||||
}>();
|
||||
|
||||
function titleMode(mode: View): string {
|
||||
if (mode === View.DebitNote) {
|
||||
return 'preview.title.debitNote';
|
||||
}
|
||||
if (mode === View.Invoice) {
|
||||
return 'preview.title.invoice';
|
||||
}
|
||||
if (mode === View.Payment) {
|
||||
return 'preview.title.payment';
|
||||
}
|
||||
if (mode === View.Receipt) {
|
||||
return 'preview.title.receipt';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row items-center q-mb-lg">
|
||||
<div class="column" style="width: 50%">
|
||||
<img src="/logo.png" width="192px" style="object-fit: scale-down" />
|
||||
</div>
|
||||
<div
|
||||
class="column"
|
||||
style="text-align: center; width: 50%; font-weight: 800; font-size: 24px"
|
||||
>
|
||||
{{ $t(titleMode(view)) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article class="detail-card">
|
||||
<section class="detail-customer-info">
|
||||
<article>
|
||||
<b>
|
||||
{{ !!branch.virtual ? '' : $t('general.company') }} {{ branch.name }}
|
||||
</b>
|
||||
|
||||
<span v-if="branch.province && branch.district && branch.subDistrict">
|
||||
{{
|
||||
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,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span>เลขประจำตัวผู้เสียภาษี {{ branch.taxNo }}</span>
|
||||
<span>เบอร์โทร {{ branch.telephoneNo }}</span>
|
||||
<span>{{ branch.webUrl }}</span>
|
||||
</article>
|
||||
<article>
|
||||
<b>ลูกค้า</b>
|
||||
<span>
|
||||
{{
|
||||
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,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span>เลขประจำตัวผู้เสียภาษี {{ customer.citizenId }}</span>
|
||||
<span>เบอร์โทร {{ customer.telephoneNo }}</span>
|
||||
</article>
|
||||
</section>
|
||||
<section class="detail-quotation-info">
|
||||
<div>
|
||||
<div>{{ $t('general.itemNo', { msg: `${$t(titleMode(view))}` }) }}</div>
|
||||
<div>{{ details.code }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ $t('preview.dateAt', { msg: `${$t(titleMode(view))}` }) }}</div>
|
||||
<div>{{ dateFormat(details.createdAt, true, false, true) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ $t('preview.seller') }}</div>
|
||||
<div>{{ details.createdBy }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ $t('quotation.paymentCondition') }}</div>
|
||||
<div>
|
||||
{{
|
||||
{
|
||||
Full: $t('quotation.type.fullAmountCash'),
|
||||
Split: $t('quotation.type.installmentsCash'),
|
||||
BillFull: $t('quotation.type.fullAmountBill'),
|
||||
BillSplit: $t('quotation.type.installmentsBill'),
|
||||
}[details.payCondition]
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ $t('quotation.workName') }}</div>
|
||||
<div>{{ details.workName }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ $t('quotation.contactName') }}</div>
|
||||
<div>{{ details.contactName }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ $t('preview.dueDate') }}</div>
|
||||
<div>{{ dateFormat(details.dueDate, true, false, true) }}</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.detail-card {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
& > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
& > :first-child {
|
||||
max-width: 57.5%;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-customer-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
& > * {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& > :first-child {
|
||||
color: var(--main);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-quotation-info {
|
||||
& > * {
|
||||
display: flex;
|
||||
|
||||
& > :first-child {
|
||||
color: var(--main);
|
||||
}
|
||||
|
||||
& > * {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
27
src/pages/12_debit-note/document-view/ViewPdf.vue
Normal file
27
src/pages/12_debit-note/document-view/ViewPdf.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import { VuePDF, usePDF } from '@tato30/vue-pdf';
|
||||
const props = defineProps<{
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
const { pdf, pages } = usePDF(props.url);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-for="page in pages" class="content">
|
||||
<VuePDF style="width: 100%" :pdf="pdf" :page="page" :scale="1.5" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.content :deep(canvas) {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.content :deep(canvas) {
|
||||
scale: 1.1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
src/pages/12_debit-note/expansion/DebitNoteExpansion.vue
Normal file
51
src/pages/12_debit-note/expansion/DebitNoteExpansion.vue
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<script setup lang="ts">
|
||||
import SelectInput from 'src/components/shared/SelectInput.vue';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
|
||||
const reason = defineModel<string>('reason');
|
||||
const detail = defineModel<string>('detail');
|
||||
</script>
|
||||
<template>
|
||||
<q-expansion-item
|
||||
dense
|
||||
:default-opened="true"
|
||||
class="overflow-hidden bordered full-width"
|
||||
switch-toggle-side
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||
>
|
||||
<template #header>
|
||||
<span>
|
||||
{{ $t('debitNote.label.debitNoteInformation') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<main class="q-px-md q-py-sm surface-1 row q-col-gutter-sm">
|
||||
<SelectInput
|
||||
:readonly
|
||||
for="select-debit-note-specify-reason"
|
||||
:label="$t('debitNote.label.specifyReasonForDebit')"
|
||||
class="col-md col-12"
|
||||
v-model="reason"
|
||||
:option="[
|
||||
{ label: $t('debitNote.label.reasonReturn'), value: 'Return' },
|
||||
{ label: $t('debitNote.label.reasonCanceled'), value: 'Canceled' },
|
||||
]"
|
||||
></SelectInput>
|
||||
<q-input
|
||||
:readonly
|
||||
for="input-debit-note-additional-detail"
|
||||
:label="$t('debitNote.label.additionalDetail')"
|
||||
outlined
|
||||
dense
|
||||
class="col"
|
||||
v-model="detail"
|
||||
></q-input>
|
||||
</main>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
120
src/pages/12_debit-note/expansion/DocumentExpansion.vue
Normal file
120
src/pages/12_debit-note/expansion/DocumentExpansion.vue
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<script lang="ts" setup>
|
||||
import SelectBranch from 'src/components/shared/select/SelectBranch.vue';
|
||||
import SelectCustomer from 'src/components/shared/select/SelectCustomer.vue';
|
||||
import DatePicker from 'src/components/shared/DatePicker.vue';
|
||||
import DataDisplay from 'src/components/08_request-list/DataDisplay.vue';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'gotoQuotation'): void;
|
||||
}>();
|
||||
|
||||
const registeredBranchId = defineModel<string>('registeredBranchId');
|
||||
const customerId = defineModel<string>('customerId');
|
||||
const issueDate = defineModel<string>('issueDate');
|
||||
const dueDate = defineModel<string | Date>('dueDate');
|
||||
|
||||
const quotationCode = defineModel<string>('quotationCode');
|
||||
const quotationWorkName = defineModel<string>('quotationWorkName');
|
||||
const quotationContactName = defineModel<string>('quotationContactName');
|
||||
const quotationContactTel = defineModel<string>('quotationContactTel');
|
||||
const quotationCreatedBy = defineModel<string>('quotationCreatedBy');
|
||||
</script>
|
||||
<template>
|
||||
<q-expansion-item
|
||||
default-opened
|
||||
dense
|
||||
class="overflow-hidden bordered full-width"
|
||||
switch-toggle-side
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||
>
|
||||
<template #header>
|
||||
<span>
|
||||
{{ $t('general.information', { msg: $t('general.document') }) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<main class="q-px-md q-py-sm surface-1 row q-col-gutter-sm">
|
||||
<SelectBranch
|
||||
readonly
|
||||
class="col-md-4 col-12"
|
||||
:label="`${$t('creditNote.label.quotationRegisteredBranch')}`"
|
||||
v-model:value="registeredBranchId"
|
||||
/>
|
||||
<SelectCustomer
|
||||
readonly
|
||||
simple
|
||||
class="col-md-4 col-12"
|
||||
:label="`${$t('creditNote.label.customer')}`"
|
||||
v-model:value="customerId"
|
||||
/>
|
||||
<DatePicker
|
||||
:label="$t('general.createdAt')"
|
||||
class="col-md-2 col-6"
|
||||
:model-value="issueDate || new Date(Date.now())"
|
||||
:readonly
|
||||
:disabled="!readonly"
|
||||
/>
|
||||
<DatePicker
|
||||
:label="$t('general.createdAt')"
|
||||
class="col-md-2 col-6"
|
||||
:model-value="dueDate || new Date(Date.now())"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (typeof v === 'string') dueDate = v;
|
||||
}
|
||||
"
|
||||
:readonly
|
||||
/>
|
||||
|
||||
<DataDisplay
|
||||
clickable
|
||||
class="col-md col-6"
|
||||
style="padding-inline: 20px"
|
||||
:label="$t('creditNote.label.quotationCode')"
|
||||
:value="quotationCode"
|
||||
@label-click="$emit('gotoQuotation')"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
readonly
|
||||
:label="$t('creditNote.label.quotationWorkName')"
|
||||
outlined
|
||||
dense
|
||||
class="col-md col-6"
|
||||
v-model="quotationWorkName"
|
||||
/>
|
||||
<q-input
|
||||
readonly
|
||||
:label="$t('quotation.contactName')"
|
||||
outlined
|
||||
dense
|
||||
class="col-md col-6"
|
||||
v-model="quotationContactName"
|
||||
/>
|
||||
<q-input
|
||||
readonly
|
||||
:label="$t('general.telephone')"
|
||||
outlined
|
||||
dense
|
||||
class="col-md col-6"
|
||||
v-model="quotationContactTel"
|
||||
/>
|
||||
<q-input
|
||||
:readonly
|
||||
:label="$t('creditNote.label.quotationCreatedBy')"
|
||||
outlined
|
||||
dense
|
||||
class="col-md col-6"
|
||||
:disable="!readonly"
|
||||
:model-value="quotationCreatedBy || '-'"
|
||||
/>
|
||||
</main>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
116
src/pages/12_debit-note/expansion/PaymentExpansion.vue
Normal file
116
src/pages/12_debit-note/expansion/PaymentExpansion.vue
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
<script lang="ts" setup>
|
||||
import useOptionStore from 'src/stores/options';
|
||||
import { watch } from 'vue';
|
||||
import QuotationFormInfo from 'src/pages/05_quotation/QuotationFormInfo.vue';
|
||||
import { PayCondition } from 'src/stores/quotations';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
installmentNo?: number[];
|
||||
installmentAmount?: number;
|
||||
}>(),
|
||||
{},
|
||||
);
|
||||
|
||||
const payBillDate = defineModel<Date | null | undefined>('payBillDate', {
|
||||
required: false,
|
||||
});
|
||||
const payType = defineModel<PayCondition>('payType', { required: true });
|
||||
const paySplitCount = defineModel<number | null>('paySplitCount', {
|
||||
default: 1,
|
||||
});
|
||||
const paySplit = defineModel<{ no: number; name?: string; amount: number }[]>(
|
||||
'paySplit',
|
||||
{ required: true },
|
||||
);
|
||||
const summaryPrice = defineModel<{
|
||||
totalPrice: number;
|
||||
totalDiscount: number;
|
||||
vat: number;
|
||||
vatExcluded: number;
|
||||
finalPrice: number;
|
||||
}>('summaryPrice', {
|
||||
required: true,
|
||||
default: {
|
||||
totalPrice: 0,
|
||||
totalDiscount: 0,
|
||||
vat: 0,
|
||||
vatExcluded: 0,
|
||||
finalPrice: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
</script>
|
||||
<template>
|
||||
<q-expansion-item
|
||||
dense
|
||||
class="overflow-hidden bordered full-width"
|
||||
switch-toggle-side
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||
>
|
||||
<template #header>
|
||||
<span>
|
||||
{{ $t('general.payment') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<main class="surface-1 q-pa-md full-width">
|
||||
<QuotationFormInfo
|
||||
v-bind="{ ...$props }"
|
||||
debit-note
|
||||
v-model:pay-type="payType"
|
||||
v-model:pay-split-count="paySplitCount"
|
||||
v-model:pay-split="paySplit"
|
||||
v-model:pay-bill-date="payBillDate"
|
||||
v-model:summary-price="summaryPrice"
|
||||
/>
|
||||
</main>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
<style scoped>
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 1/1;
|
||||
font-size: 1.5rem;
|
||||
padding: var(--size-1);
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
:deep(.price-tag .q-field__control) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 90px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.credit-note-color {
|
||||
--_color: var(--indigo-10-hsl);
|
||||
}
|
||||
|
||||
.bg-color {
|
||||
color: white;
|
||||
background: hsla(var(--_color));
|
||||
}
|
||||
|
||||
.dark .bg-color {
|
||||
--_color: var(--orange-6-hsl);
|
||||
}
|
||||
|
||||
.bg-color-light {
|
||||
background: hsla(var(--_color) / 0.1);
|
||||
}
|
||||
|
||||
.dark .bg-color-light {
|
||||
--_color: var(--orange-6-hsl / 0.2);
|
||||
}
|
||||
|
||||
.price-container > * {
|
||||
padding: var(--size-1);
|
||||
}
|
||||
</style>
|
||||
105
src/pages/12_debit-note/expansion/ProductExpansion.vue
Normal file
105
src/pages/12_debit-note/expansion/ProductExpansion.vue
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<script lang="ts" setup>
|
||||
import ProductItem from 'src/components/05_quotation/ProductItem.vue';
|
||||
import { AddButton } from 'components/button';
|
||||
import {
|
||||
ProductRelation,
|
||||
ProductServiceList,
|
||||
QuotationPayload,
|
||||
} from 'src/stores/quotations';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
agentPrice: boolean;
|
||||
installmentInput?: boolean;
|
||||
maxInstallment?: number | null;
|
||||
creditNote?: boolean;
|
||||
employeeRows: {
|
||||
foreignRefNo: string;
|
||||
employeeName: string;
|
||||
birthDate: string;
|
||||
gender: string;
|
||||
age: string;
|
||||
nationality: string;
|
||||
documentExpireDate: string;
|
||||
imgUrl: string;
|
||||
status: string;
|
||||
}[];
|
||||
}>();
|
||||
|
||||
const rows = defineModel<
|
||||
Required<QuotationPayload['productServiceList'][number]>[]
|
||||
>('rows', { required: true });
|
||||
|
||||
defineEmits<{
|
||||
(e: 'addProduct'): void;
|
||||
(e: 'viewFile', data: ProductRelation): void;
|
||||
(e: 'delete', index: number): void;
|
||||
(
|
||||
e: 'updateTable',
|
||||
v: QuotationPayload['productServiceList'][number],
|
||||
opt?: {
|
||||
newInstallmentNo: number;
|
||||
},
|
||||
): void;
|
||||
|
||||
(e: 'updateRows', v: Required<ProductServiceList>[]): void;
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<q-expansion-item
|
||||
dense
|
||||
class="overflow-hidden bordered full-width"
|
||||
switch-toggle-side
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||
default-opened
|
||||
>
|
||||
<template #header>
|
||||
<span
|
||||
class="row items-center justify-between full-width"
|
||||
style="min-height: 31.01px"
|
||||
>
|
||||
{{ $t('general.information', { msg: $t('taskOrder.productList') }) }}
|
||||
<AddButton
|
||||
icon-only
|
||||
@click.stop="$emit('addProduct')"
|
||||
v-if="!readonly"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<main class="q-px-md q-py-sm surface-1">
|
||||
<ProductItem
|
||||
:installment-input
|
||||
:max-installment
|
||||
:readonly
|
||||
:agent-price
|
||||
:employee-rows
|
||||
:rows="rows"
|
||||
@delete="(v) => $emit('delete', v)"
|
||||
@update:rows="(v) => $emit('updateRows', v)"
|
||||
@update-table="(data, opt) => $emit('updateTable', data, opt)"
|
||||
@view-file="(v) => $emit('viewFile', v)"
|
||||
/>
|
||||
</main>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
<style scoped>
|
||||
.product-status {
|
||||
padding-left: 8px;
|
||||
border-radius: 20px;
|
||||
color: hsl(var(--_color));
|
||||
background: hsla(var(--_color) / 0.15);
|
||||
|
||||
&.warning {
|
||||
--_color: var(--warning-bg);
|
||||
}
|
||||
&.positive {
|
||||
--_color: var(--positive-bg);
|
||||
}
|
||||
&.negative {
|
||||
--_color: var(--negative-bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
67
src/pages/12_debit-note/expansion/RemarkExpansion.vue
Normal file
67
src/pages/12_debit-note/expansion/RemarkExpansion.vue
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
|
||||
const remark = defineModel<string>('remark', { default: '' });
|
||||
</script>
|
||||
<template>
|
||||
<q-expansion-item
|
||||
dense
|
||||
class="overflow-hidden bordered full-width"
|
||||
switch-toggle-side
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||
>
|
||||
<template #header>
|
||||
<span>
|
||||
{{ $t('general.remark') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<main class="surface-1 q-pa-md full-width">
|
||||
<q-editor
|
||||
dense
|
||||
:readonly="readonly"
|
||||
:model-value="remark"
|
||||
min-height="5rem"
|
||||
class="full-width"
|
||||
toolbar-bg="input-border"
|
||||
style="cursor: auto; color: var(--foreground)"
|
||||
:content-class="readonly ? 'q-mt-sm' : 'bordered q-mt-sm rounded'"
|
||||
:flat="!readonly"
|
||||
:style="`width: ${$q.screen.gt.xs ? '100%' : '63vw'}`"
|
||||
:toolbar="[['left', 'center', 'justify'], ['clip']]"
|
||||
:toolbar-toggle-color="readonly ? 'disabled' : 'primary'"
|
||||
:toolbar-color="readonly ? 'disabled' : $q.dark.isActive ? 'white' : ''"
|
||||
:definitions="{
|
||||
clip: {
|
||||
icon: 'mdi-paperclip',
|
||||
tip: 'Upload',
|
||||
disable: readonly,
|
||||
handler: () => console.log('upload'),
|
||||
},
|
||||
}"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
remark = v;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</main>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
<style scoped>
|
||||
:deep(.q-editor__toolbar-group):nth-child(2) {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
:deep(.q-editor__toolbar.row.no-wrap.scroll-x) {
|
||||
background-color: var(--surface-2) !important;
|
||||
}
|
||||
|
||||
:deep(.q-editor__toolbar) {
|
||||
border-color: var(--surface-3) !important;
|
||||
}
|
||||
</style>
|
||||
78
src/pages/12_debit-note/expansion/WorkerItemExpansion.vue
Normal file
78
src/pages/12_debit-note/expansion/WorkerItemExpansion.vue
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<script setup lang="ts">
|
||||
import { AddButton } from 'components/button';
|
||||
import WorkerItem from 'src/components/05_quotation/WorkerItem.vue';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
hideBtnAddWorker?: boolean;
|
||||
employeeAmount?: number;
|
||||
rowWorker: {
|
||||
foreignRefNo: string;
|
||||
employeeName: string;
|
||||
birthDate: string;
|
||||
gender: string;
|
||||
age: string;
|
||||
nationality: string;
|
||||
documentExpireDate: string;
|
||||
imgUrl?: string;
|
||||
status: string;
|
||||
}[];
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'addWorker'): void;
|
||||
(e: 'edit'): void;
|
||||
(e: 'update:employeeAmount', number: number): void;
|
||||
(e: 'delete', index: number): void;
|
||||
}>();
|
||||
|
||||
const toggleWorker = defineModel<boolean>('toggleWorker');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-expansion-item
|
||||
for="item-up"
|
||||
id="item-up"
|
||||
dense
|
||||
class="overflow-hidden"
|
||||
switch-toggle-side
|
||||
default-opened
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<section class="row items-center full-width">
|
||||
<div class="row items-center q-pr-md q-py-sm">
|
||||
<span class="q-mr-md" style="font-size: 18px">
|
||||
{{ $t('quotation.employeeList') }}
|
||||
</span>
|
||||
<template v-if="!readonly">
|
||||
<ToggleButton class="q-mr-sm" v-model="toggleWorker" />
|
||||
{{ toggleWorker ? $t('general.specify') : $t('general.noSpecify') }}
|
||||
</template>
|
||||
</div>
|
||||
<nav class="q-ml-auto">
|
||||
<AddButton
|
||||
v-if="!hideBtnAddWorker"
|
||||
icon-only
|
||||
@click.stop="$emit('addWorker')"
|
||||
/>
|
||||
</nav>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<div class="surface-1 q-pa-md full-width">
|
||||
<WorkerItem
|
||||
@update:employee-amount="(v) => $emit('update:employeeAmount', v)"
|
||||
:employee-amount
|
||||
:readonly="readonly"
|
||||
fallback-img="/images/employee-avatar.png"
|
||||
:rows="rowWorker"
|
||||
@delete="(i) => $emit('delete', i)"
|
||||
/>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
64
src/pages/12_debit-note/form.ts
Normal file
64
src/pages/12_debit-note/form.ts
Normal file
|
|
@ -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]<br/><br/>#[quotation-payment]',
|
||||
quotationId: '',
|
||||
agentPrice: false,
|
||||
};
|
||||
|
||||
export const useDebitNoteForm = defineStore('form-debit-note', () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
let resetFormData = structuredClone(DEFAULT_DATA);
|
||||
|
||||
const currentFormData = ref<DebitNotePayload>(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,
|
||||
};
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
94
src/stores/debit-note/index.ts
Normal file
94
src/stores/debit-note/index.ts
Normal file
|
|
@ -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<Record<Status, number>>(`/${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<PaginationResult<Data>>(`/${ENDPOINT}`, {
|
||||
params,
|
||||
});
|
||||
if (res.status < 400) return res.data;
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getDebitNote(id: string) {
|
||||
const res = await api.get<Data>(`/${ENDPOINT}/${id}`);
|
||||
if (res.status < 400) return res.data;
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function createDebitNote(body: Payload) {
|
||||
const res = await api.post<Data>(`/${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<Data>(`/${ENDPOINT}/${id}`, payload);
|
||||
if (res.status < 400) return res.data;
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function deleteDebitNote(id: string) {
|
||||
const res = await api.delete<Data>(`/${ENDPOINT}/${id}`);
|
||||
if (res.status < 400) return res.data;
|
||||
return null;
|
||||
}
|
||||
|
||||
export const useDebitNote = defineStore('debit-note-store', () => {
|
||||
const data = ref<Data[]>([]);
|
||||
const page = ref<number>(1);
|
||||
const pageMax = ref<number>(1);
|
||||
const pageSize = ref<number>(30);
|
||||
const stats = ref<Record<Status, number>>({
|
||||
[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),
|
||||
};
|
||||
});
|
||||
94
src/stores/debit-note/types.ts
Normal file
94
src/stores/debit-note/types.ts
Normal file
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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<PaginationResult<Receipt>>('/receipt', {
|
||||
params: opts,
|
||||
|
|
|
|||
|
|
@ -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<PaginationResult<QuotationPaymentData>>(
|
||||
'/payment',
|
||||
{ params: { quotationId } },
|
||||
{ params },
|
||||
);
|
||||
if (res.status < 400) {
|
||||
return res.data;
|
||||
|
|
|
|||
44
tests/institution.spec.ts
Normal file
44
tests/institution.spec.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
19
tests/utils/index.ts
Normal file
19
tests/utils/index.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue