Merge branch 'develop'

This commit is contained in:
Methapon2001 2025-02-25 14:10:13 +07:00
commit 257790c1ce
31 changed files with 731 additions and 263 deletions

89
.github/workflows/gitea-local.yaml vendored Normal file
View file

@ -0,0 +1,89 @@
name: Gitea Action
run-name: Build ${{ github.actor }}
# Intended for local gitea instance only.
on:
workflow_dispatch:
env:
REGISTRY: ${{ vars.CONTAINER_REGISTRY }}
REGISTRY_USERNAME: ${{ vars.CONTAINER_REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
CONTAINER_IMAGE_NAME: ${{ vars.CONTAINER_REGISTRY }}/${{ vars.CONTAINER_IMAGE_OWNER }}/${{ vars.CONTAINER_IMAGE_NAME }}:latest
jobs:
gitea-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.REGISTRY_USERNAME }}
password: ${{ env.REGISTRY_PASSWORD }}
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config-inline: |
[registry."${{ env.REGISTRY }}"]
ca=["/etc/ssl/certs/ca-certificates.crt"]
- name: Build and Push Docker Image
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
tags: ${{ env.CONTAINER_IMAGE_NAME }}
push: true
- name: Remote Deploy Development
uses: appleboy/ssh-action@v1.2.1
with:
host: ${{ vars.SSH_DEVELOPMENT_HOST }}
port: ${{ vars.SSH_DEVELOPMENT_PORT }}
username: ${{ secrets.SSH_DEVELOPMENT_USER }}
password: ${{ secrets.SSH_DEVELOPMENT_PASSWORD }}
script: eval "${{ secrets.SSH_DEVELOPMENT_DEPLOY_CMD }}" & wait
- name: Remote Deploy Test
uses: appleboy/ssh-action@v1.2.1
with:
host: ${{ vars.SSH_TEST_HOST }}
port: ${{ vars.SSH_TEST_PORT }}
username: ${{ secrets.SSH_TEST_USER }}
password: ${{ secrets.SSH_TEST_PASSWORD }}
script: eval "${{ secrets.SSH_TEST_DEPLOY_CMD }}" & wait
- name: Notify Discord Success
if: success()
run: |
curl -H "Content-Type: application/json" -X POST \
-d '{
"embeds": [{
"title": "✅ Gitea Local Deployment Success!",
"description": "**Details:**\n- Image: `${{ env.CONTAINER_IMAGE_NAME }}`\n- Deployed by: `${{ github.actor }}`",
"color": 3066993,
"footer": {
"text": "Gitea Local Release Notification",
"icon_url": "https://example.com/success-icon.png"
},
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
}]
}' \
${{ secrets.DISCORD_WEBHOOK }}
- name: Notify Discord Failure
if: failure()
run: |
curl -H "Content-Type: application/json" -X POST \
-d '{
"embeds": [{
"title": "❌ Gitea Local Deployment Failed!",
"description": "**Details:**\n- Image: `${{ env.CONTAINER_IMAGE_NAME }}`\n- Attempted by: `${{ github.actor }}`",
"color": 15158332,
"footer": {
"text": "Gitea Local Release Notification",
"icon_url": "https://example.com/failure-icon.png"
},
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
}]
}' \
${{ secrets.DISCORD_WEBHOOK }}

View file

@ -1,31 +0,0 @@
name: local-build-release
# Intended for local network use.
# Remote access is possible if the host has a public IP address.
on:
workflow_dispatch:
env:
REGISTRY: ${{ vars.DOCKER_REGISTRY }}
jobs:
local-build-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config-inline: |
[registry."${{ env.REGISTRY }}"]
http = true
insecure = true
- name: Build and Push Docker Image
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/jws/jws-frontend:latest
allow: security.insecure

View file

@ -1,22 +0,0 @@
name: local-release-demo
# Intended for local network use.
# Remote access is possible if the host has a public IP address.
on:
workflow_dispatch:
jobs:
local-release-demo:
runs-on: ubuntu-latest
steps:
- name: Remote deploy internal chamomind server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
script: |
cd ~/repositories/jws-frontend
git pull
docker compose up -d --build

View file

@ -1,22 +0,0 @@
name: local-release-dev
# Intended for local network use.
# Remote access is possible if the host has a public IP address.
on:
workflow_dispatch:
jobs:
local-release-dev:
runs-on: ubuntu-latest
steps:
- name: Remote deploy internal chamomind server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
script: |
cd ~/repositories/jws-frontend
git pull
docker compose up -d --build

View file

@ -34,6 +34,7 @@ const quotationId = defineModel<string>('quotationId', {
v-model:value="quotationId" v-model:value="quotationId"
:label="$t('general.select', { msg: $t('quotation.title') })" :label="$t('general.select', { msg: $t('quotation.title') })"
:params="{ :params="{
cancelIncludeDebitNote: true,
hasCancel: true, hasCancel: true,
}" }"
/> />

View file

@ -779,6 +779,7 @@ export default {
specialCondition: 'Special Conditions', specialCondition: 'Special Conditions',
selectInvoice: 'Select Invoice', selectInvoice: 'Select Invoice',
approveInvoice: 'Approve the invoice', approveInvoice: 'Approve the invoice',
approveDebitNote: 'Approve Debit Note',
paymentCondition: 'Payment Terms', paymentCondition: 'Payment Terms',
payType: 'Payment Methods', payType: 'Payment Methods',
bank: 'Select Payment Account', bank: 'Select Payment Account',
@ -897,6 +898,8 @@ export default {
caption: 'All Request List', caption: 'All Request List',
quotationCode: 'Quotation Code', quotationCode: 'Quotation Code',
requestListCode: 'Request List Code', requestListCode: 'Request List Code',
referenceNo: 'Reference No.',
invoiceCode: 'Invoice Code', invoiceCode: 'Invoice Code',
receiptCode: 'Receipt Code', receiptCode: 'Receipt Code',
alienIdCard: 'Alien Identification Card"', alienIdCard: 'Alien Identification Card"',
@ -1022,6 +1025,7 @@ export default {
importWorker: 'Import Worker', importWorker: 'Import Worker',
confirmLogout: 'Confirm Logout', confirmLogout: 'Confirm Logout',
confirmQuotationAccept: 'Confirm acceptance of the quotation.', confirmQuotationAccept: 'Confirm acceptance of the quotation.',
confirmDebitNoteAccept: 'Confirm acceptance of the debit note.',
}, },
message: { message: {
quotationAccept: 'Once accepted, no further modifications can be made', quotationAccept: 'Once accepted, no further modifications can be made',
@ -1241,7 +1245,8 @@ export default {
'The customer returned all or part of the goods because they did not meet their requirements.', 'The customer returned all or part of the goods because they did not meet their requirements.',
reasonCanceled: reasonCanceled:
'The customer canceled certain items or services listed on the invoice.', 'The customer canceled certain items or services listed on the invoice.',
submit: 'Approve the credit note', request: 'Request Credit Note Approval',
submit: 'Approve Credit Note',
refund: 'Refund', refund: 'Refund',
refundMethod: 'Refund Method', refundMethod: 'Refund Method',
totalRefund: 'Total refund amount', totalRefund: 'Total refund amount',
@ -1252,6 +1257,7 @@ export default {
refundSuccess: 'Refund Success', refundSuccess: 'Refund Success',
}, },
status: { status: {
Waiting: 'Credit Note',
Pending: 'Pending Refund', Pending: 'Pending Refund',
Success: 'Refund Completed', Success: 'Refund Completed',
Canceled: 'Canceled', Canceled: 'Canceled',
@ -1261,10 +1267,6 @@ export default {
Done: 'Done', Done: 'Done',
}, },
}, },
stats: {
Pending: 'Pending Refund',
Success: 'Refund Completed',
},
}, },
invoice: { invoice: {
@ -1303,6 +1305,7 @@ export default {
quotationWorkName: 'Work Name', quotationWorkName: 'Work Name',
quotationPayment: 'Payment Method', quotationPayment: 'Payment Method',
value: 'Net Value', value: 'Net Value',
request: 'Request Debit Note Approval',
submit: 'Approve Debit Note', submit: 'Approve Debit Note',
}, },
@ -1315,6 +1318,7 @@ export default {
}, },
viewMode: { viewMode: {
accepted: 'Accept',
payment: 'Payment', payment: 'Payment',
receipt: 'Receipt/Tax Invoice', receipt: 'Receipt/Tax Invoice',
processComplete: 'Completed', processComplete: 'Completed',

View file

@ -1,5 +1,3 @@
import { title } from 'process';
export default { export default {
general: { general: {
ok: 'ตกลง', ok: 'ตกลง',
@ -761,7 +759,7 @@ export default {
branch: 'สาขาที่ออกใบเสนอราคา', branch: 'สาขาที่ออกใบเสนอราคา',
branchVirtual: 'จุดรับบริการที่ออกใบเสนอราคา', branchVirtual: 'จุดรับบริการที่ออกใบเสนอราคา',
customer: 'ลูกค้า', customer: 'ลูกค้า',
newCustomer: 'ลูกค้าใหม่', newCustomer: 'แรงงานใหม่',
employeeList: 'รายชื่อแรงงาน', employeeList: 'รายชื่อแรงงาน',
employee: 'แรงงาน', employee: 'แรงงาน',
employeeName: 'ชื่อ-นามสกุล แรงงาน', employeeName: 'ชื่อ-นามสกุล แรงงาน',
@ -772,6 +770,7 @@ export default {
specialCondition: 'เงื่อนไขพิเศษ', specialCondition: 'เงื่อนไขพิเศษ',
selectInvoice: 'เลือกใบแจ้งหนี้', selectInvoice: 'เลือกใบแจ้งหนี้',
approveInvoice: 'อนุมัติใบแจ้งหนี้', approveInvoice: 'อนุมัติใบแจ้งหนี้',
approveDebitNote: 'อนุมัติใบเพิ่มหนี้',
customerAcceptance: 'ลูกค้าตอบรับ', customerAcceptance: 'ลูกค้าตอบรับ',
additionalFile: 'ไฟล์เอกสารเพิ่มเติม', additionalFile: 'ไฟล์เอกสารเพิ่มเติม',
@ -890,6 +889,7 @@ export default {
caption: 'ใบรายการคำขอทั้งหมด', caption: 'ใบรายการคำขอทั้งหมด',
quotationCode: 'เลขที่ใบเสนอราคา', quotationCode: 'เลขที่ใบเสนอราคา',
requestListCode: 'เลขที่ใบรายการคำขอ', requestListCode: 'เลขที่ใบรายการคำขอ',
referenceNo: 'เลขที่ใบอ้างอิง',
invoiceCode: 'เลขที่ใบแจ้งหนี้', invoiceCode: 'เลขที่ใบแจ้งหนี้',
receiptCode: 'เลขที่ใบเสร็จ/กำกับภาษี', receiptCode: 'เลขที่ใบเสร็จ/กำกับภาษี',
alienIdCard: 'บัตรประจำตัวต่างด้าว', alienIdCard: 'บัตรประจำตัวต่างด้าว',
@ -1010,6 +1010,7 @@ export default {
importWorker: 'นำเข้าคนงาน', importWorker: 'นำเข้าคนงาน',
confirmLogout: 'ยืนยันการออกจากระบบ', confirmLogout: 'ยืนยันการออกจากระบบ',
confirmQuotationAccept: 'ยืนยันการตอบรับใบเสนอราคา', confirmQuotationAccept: 'ยืนยันการตอบรับใบเสนอราคา',
confirmDebitNoteAccept: 'ยืนยันการตอบรับใบเพิ่มหนี้',
}, },
message: { message: {
quotationAccept: 'เมื่อตอบรับเเล้วจะไม่สามารถแก้ไขได้อีก', quotationAccept: 'เมื่อตอบรับเเล้วจะไม่สามารถแก้ไขได้อีก',
@ -1224,6 +1225,7 @@ export default {
'ลูกค้าคืนสินค้าทั้งหมดหรือบางส่วน เนื่องจากสินค้าไม่ตรงตามความต้องการ', 'ลูกค้าคืนสินค้าทั้งหมดหรือบางส่วน เนื่องจากสินค้าไม่ตรงตามความต้องการ',
reasonCanceled: reasonCanceled:
'ลูกค้ายกเลิกคำสั่งซื้อบางรายการหรือบริการที่ระบุในใบแจ้งหนี้', 'ลูกค้ายกเลิกคำสั่งซื้อบางรายการหรือบริการที่ระบุในใบแจ้งหนี้',
request: 'ขออนุมัติใบลดหนี้',
submit: 'อนุมัติใบลดหนี้', submit: 'อนุมัติใบลดหนี้',
refund: 'การคืนเงิน', refund: 'การคืนเงิน',
refundMethod: 'วิธีการคืนเงิน', refundMethod: 'วิธีการคืนเงิน',
@ -1235,6 +1237,7 @@ export default {
refundSuccess: 'คืนเงินเสร็จเรียบร้อย', refundSuccess: 'คืนเงินเสร็จเรียบร้อย',
}, },
status: { status: {
Waiting: 'ใบลดหนี้',
Pending: 'รอคืนเงิน', Pending: 'รอคืนเงิน',
Success: 'คืนเงินเสร็จสิ้น', Success: 'คืนเงินเสร็จสิ้น',
Canceled: 'ยกเลิกรายการ', Canceled: 'ยกเลิกรายการ',
@ -1244,10 +1247,6 @@ export default {
Done: 'คืนเงินเรียบร้อย', Done: 'คืนเงินเรียบร้อย',
}, },
}, },
stats: {
Pending: 'รอคืนเงิน',
Success: 'คืนเงินเสร็จสิ้น',
},
}, },
invoice: { invoice: {
@ -1285,6 +1284,7 @@ export default {
quotationWorkName: 'ชื่อใบงาน', quotationWorkName: 'ชื่อใบงาน',
quotationPayment: 'วิธีการชำระ', quotationPayment: 'วิธีการชำระ',
value: 'มูลค่าสุทธิ', value: 'มูลค่าสุทธิ',
request: 'ขออนุมัติใบเพิ่มหนี้',
submit: 'อนุมัติใบเพิ่มหนี้', submit: 'อนุมัติใบเพิ่มหนี้',
}, },
@ -1297,6 +1297,7 @@ export default {
}, },
viewMode: { viewMode: {
accepted: 'ตอบรับ',
payment: 'ชำระเงิน', payment: 'ชำระเงิน',
receipt: 'ใบเสร็จรับเงิน/ใบกำกับภาษี', receipt: 'ใบเสร็จรับเงิน/ใบกำกับภาษี',
processComplete: 'เสร็จสิ้น', processComplete: 'เสร็จสิ้น',

View file

@ -54,7 +54,7 @@ import {
SaveButton, SaveButton,
EditButton, EditButton,
UndoButton, UndoButton,
CloseButton, CancelButton,
MainButton, MainButton,
} from 'components/button'; } from 'components/button';
import QuotationFormReceipt from './QuotationFormReceipt.vue'; import QuotationFormReceipt from './QuotationFormReceipt.vue';
@ -673,6 +673,7 @@ async function triggerSelectEmployeeDialog() {
} }
function triggerProductServiceDialog() { function triggerProductServiceDialog() {
covertToNode();
pageState.productServiceModal = true; pageState.productServiceModal = true;
} }
@ -1067,11 +1068,7 @@ const productServiceNodes = ref<ProductTree>([]);
watch( watch(
() => productServiceList.value, () => productServiceList.value,
() => { () => {
productServiceNodes.value = quotationProductTree( covertToNode();
productServiceList.value,
agentPrice.value,
config.value?.vat,
);
}, },
); );
@ -1302,6 +1299,14 @@ async function formDownload() {
a.click(); a.click();
a.remove(); a.remove();
} }
function covertToNode() {
productServiceNodes.value = quotationProductTree(
productServiceList.value,
agentPrice.value,
config.value?.vat,
);
}
</script> </script>
<template> <template>
@ -2233,14 +2238,9 @@ async function formDownload() {
</article> </article>
<footer class="surface-1 q-pa-md full-width"> <footer class="surface-1 q-pa-md full-width">
<div <div class="row full-width justify-end">
class="row full-width"
:class="{
'justify-between': view !== View.InvoicePre,
'justify-end': view === View.InvoicePre,
}"
>
<MainButton <MainButton
class="q-mr-auto"
v-if=" v-if="
view !== View.InvoicePre && view !== View.InvoicePre &&
view !== View.PaymentPre && view !== View.PaymentPre &&
@ -2255,7 +2255,16 @@ async function formDownload() {
{{ $t('general.view', { msg: $t('general.example') }) }} {{ $t('general.view', { msg: $t('general.example') }) }}
</MainButton> </MainButton>
<CancelButton
outlined
id="btn-close"
@click="closeTab()"
:label="$t('dialog.action.close')"
v-if="quotationFormState.mode === 'info' && closeAble()"
/>
<div <div
class="q-ml-sm"
v-if=" v-if="
view === View.Accepted && view === View.Accepted &&
quotationFormData.quotationStatus === 'Issued' quotationFormData.quotationStatus === 'Issued'
@ -2293,6 +2302,7 @@ async function formDownload() {
</template> </template>
<div <div
class="q-ml-sm"
v-if=" v-if="
view === View.Invoice && view === View.Invoice &&
((quotationFormData.quotationStatus !== 'PaymentPending' && ((quotationFormData.quotationStatus !== 'PaymentPending' &&
@ -2317,7 +2327,7 @@ async function formDownload() {
</div> </div>
<div <div
class="row" class="row q-ml-sm"
style="gap: var(--size-2)" style="gap: var(--size-2)"
v-if=" v-if="
(view === View.Quotation && (view === View.Quotation &&
@ -2332,12 +2342,6 @@ async function formDownload() {
id="btn-undo" id="btn-undo"
v-if="quotationFormState.mode === 'edit'" v-if="quotationFormState.mode === 'edit'"
/> />
<CloseButton
outlined
id="btn-close"
@click="closeTab()"
v-if="quotationFormState.mode === 'info' && closeAble()"
/>
<SaveButton <SaveButton
type="submit" type="submit"
id="btn-save" id="btn-save"

View file

@ -177,7 +177,7 @@ function setDefaultFormEmployee() {
namePrefix: '', namePrefix: '',
nationality: '', nationality: '',
gender: '', gender: '',
dateOfBirth: new Date(), dateOfBirth: null,
attachment: [], attachment: [],
}; };
@ -261,6 +261,49 @@ async function getWorkerFromCriteria(
} }
watch(() => state.search, getWorkerList); watch(() => state.search, getWorkerList);
watch(
() => formDataEmployee.value.dateOfBirth,
() => {
const age = calculateAge(formDataEmployee.value.dateOfBirth, 'year');
if (formDataEmployee.value.dateOfBirth && Number(age) < 15) {
dialog({
color: 'warning',
icon: 'mdi-alert',
title: t('dialog.title.youngWorker15'),
cancelText: t('general.edit'),
persistent: true,
message: t('dialog.message.youngWorker15'),
cancel: async () => {
formDataEmployee.value.dateOfBirth = null;
return;
},
});
}
if (
formDataEmployee.value.dateOfBirth &&
Number(age) > 15 &&
Number(age) <= 18
) {
dialog({
color: 'warning',
icon: 'mdi-alert',
title: t('dialog.title.youngWorker18'),
cancelText: t('general.cancel'),
actionText: t('general.confirm'),
persistent: true,
message: t('dialog.message.youngWorker18'),
action: () => {},
cancel: async () => {
formDataEmployee.value.dateOfBirth = null;
return;
},
});
}
},
);
</script> </script>
<template> <template>
@ -445,7 +488,7 @@ watch(() => state.search, getWorkerList);
class="text-weight-medium q-mr-md" class="text-weight-medium q-mr-md"
style="font-size: 18px" style="font-size: 18px"
> >
{{ $t('quotation.customer') }} {{ $t('quotation.employee') }}
</span> </span>
</div> </div>
</section> </section>

View file

@ -2,10 +2,11 @@
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { onMounted, nextTick, ref, watch } from 'vue'; import { onMounted, nextTick, ref, watch } from 'vue';
import { precisionRound } from 'src/utils/arithmetic'; import { precisionRound } from 'src/utils/arithmetic';
import { useI18n } from 'vue-i18n';
import ThaiBahtText from 'thai-baht-text'; import ThaiBahtText from 'thai-baht-text';
// NOTE: Import stores // NOTE: Import stores
import { formatNumberDecimal } from 'stores/utils'; import { dialogWarningClose, formatNumberDecimal } from 'stores/utils';
import { useConfigStore } from 'stores/config'; import { useConfigStore } from 'stores/config';
import useBranchStore from 'stores/branch'; import useBranchStore from 'stores/branch';
import { baseUrl } from 'stores/utils'; import { baseUrl } from 'stores/utils';
@ -28,12 +29,14 @@ import ViewHeader from './ViewHeader.vue';
import ViewFooter from './ViewFooter.vue'; import ViewFooter from './ViewFooter.vue';
import BankComponents from './BankComponents.vue'; import BankComponents from './BankComponents.vue';
import PrintButton from 'src/components/button/PrintButton.vue'; import PrintButton from 'src/components/button/PrintButton.vue';
import { CancelButton } from 'components/button';
import { convertTemplate } from 'src/utils/string-template'; import { convertTemplate } from 'src/utils/string-template';
const configStore = useConfigStore(); const configStore = useConfigStore();
const branchStore = useBranchStore(); const branchStore = useBranchStore();
const customerStore = useCustomerStore(); const customerStore = useCustomerStore();
const quotationStore = useQuotationStore(); const quotationStore = useQuotationStore();
const { t } = useI18n();
const { data: config } = storeToRefs(configStore); const { data: config } = storeToRefs(configStore);
type Product = { type Product = {
@ -323,6 +326,20 @@ function calcPrice(c: Product) {
return precisionRound(price + vat); return precisionRound(price + vat);
} }
async function closeTab() {
dialogWarningClose(t, {
message: t('dialog.message.close'),
action: () => {
window.close();
},
cancel: () => {},
});
}
function closeAble() {
return window.opener !== null;
}
watch(elements, () => {}); watch(elements, () => {});
function print() { function print() {
@ -333,7 +350,15 @@ function print() {
<template> <template>
<div class="toolbar"> <div class="toolbar">
<PrintButton solid @click="print" /> <PrintButton solid @click="print" />
<CancelButton
outlined
id="btn-close"
@click="closeTab()"
:label="$t('dialog.action.close')"
v-if="closeAble()"
/>
</div> </div>
<div <div
class="row justify-between container color-quotation" class="row justify-between container color-quotation"
:class="{ :class="{
@ -641,7 +666,7 @@ function print() {
position: sticky; position: sticky;
top: 0; top: 0;
display: flex; display: flex;
justify-content: center; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 1rem; margin-bottom: 1rem;
padding: 1rem; padding: 1rem;

View file

@ -11,6 +11,7 @@ import PropertiesExpansion from './PropertiesExpansion.vue';
import FormGroupHead from './FormGroupHead.vue'; import FormGroupHead from './FormGroupHead.vue';
import AvatarGroup from 'src/components/shared/AvatarGroup.vue'; import AvatarGroup from 'src/components/shared/AvatarGroup.vue';
import { StateButton } from 'components/button'; import { StateButton } from 'components/button';
import { CancelButton } from 'components/button';
import DutyExpansion from './DutyExpansion.vue'; import DutyExpansion from './DutyExpansion.vue';
import MessengerExpansion from './MessengerExpansion.vue'; import MessengerExpansion from './MessengerExpansion.vue';
@ -20,6 +21,7 @@ import {
dialog, dialog,
getEmployeeName, getEmployeeName,
getCustomerName, getCustomerName,
dialogWarningClose,
} from 'src/stores/utils'; } from 'src/stores/utils';
import { dateFormatJS } from 'src/utils/datetime'; import { dateFormatJS } from 'src/utils/datetime';
import { useRequestList } from 'src/stores/request-list'; import { useRequestList } from 'src/stores/request-list';
@ -37,7 +39,7 @@ import ProductExpansion from './ProductExpansion.vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useWorkflowTemplate } from 'src/stores/workflow-template'; import { useWorkflowTemplate } from 'src/stores/workflow-template';
import { WorkflowTemplate } from 'src/stores/workflow-template/types'; import { WorkflowTemplate } from 'src/stores/workflow-template/types';
import { initLang, initTheme, Lang } from 'src/utils/ui'; import { initLang, initTheme } from 'src/utils/ui';
import { import {
EmployeePassportPayload, EmployeePassportPayload,
EmployeeVisaPayload, EmployeeVisaPayload,
@ -314,7 +316,7 @@ function goToQuotation(
customerBranchId: quotation.customerBranchId, customerBranchId: quotation.customerBranchId,
agentPrice: quotation.agentPrice, agentPrice: quotation.agentPrice,
statusDialog: 'info', statusDialog: 'info',
quotationId: quotation.id, quotationId: opt && opt.id ? opt.id : quotation.id,
}), }),
); );
@ -330,6 +332,16 @@ function goToDebitNote(opt?: { tab?: string; id?: string }) {
window.open(url.toString(), '_blank'); window.open(url.toString(), '_blank');
} }
function closeTab() {
dialogWarningClose(t, {
message: t('dialog.message.close'),
action: () => {
window.close();
},
cancel: () => {},
});
}
</script> </script>
<template> <template>
<div class="column surface-0 fullscreen" v-if="data"> <div class="column surface-0 fullscreen" v-if="data">
@ -565,6 +577,21 @@ function goToDebitNote(opt?: { tab?: string; id?: string }) {
: goToQuotation(data.quotation, { tab: 'receipt' }) : goToQuotation(data.quotation, { tab: 'receipt' })
" "
/> />
<DataDisplay
v-if="data.quotation.isDebitNote"
clickable
class="col"
icon="mdi-file-document-outline"
:label="$t('requestList.referenceNo')"
:value="data.quotation.debitNoteQuotation.code || '-'"
@label-click="
goToQuotation(data.quotation, {
id: data.quotation.debitNoteQuotationId,
})
"
/>
<div v-if="$q.screen.gt.sm" class="col"></div> <div v-if="$q.screen.gt.sm" class="col"></div>
</div> </div>
<FormGroupHead class="col-12"> <FormGroupHead class="col-12">
@ -844,6 +871,15 @@ function goToDebitNote(opt?: { tab?: string; id?: string }) {
</div> </div>
</section> </section>
</main> </main>
<!-- SEC: Footer -->
<footer class="surface-1 q-pa-md full-width text-right">
<CancelButton
@click="closeTab()"
:label="$t('dialog.action.close')"
outlined
/>
</footer>
</div> </div>
</template> </template>
<style scoped></style> <style scoped></style>

View file

@ -23,7 +23,9 @@ const emit = defineEmits<{
const props = defineProps<{ const props = defineProps<{
taskListGroup?: { taskListGroup?: {
product: RequestWork['productService']['product']; product:
| RequestWork['productService']['product']
| RequestWork['productService'];
list: (RequestWork & { list: (RequestWork & {
_template?: { _template?: {
id: string; id: string;
@ -135,6 +137,7 @@ async function getList() {
} }
function getStep(requestWork: RequestWork) { function getStep(requestWork: RequestWork) {
if (!requestWork.stepStatus) return 0;
const target = requestWork.stepStatus.find( const target = requestWork.stepStatus.find(
(v) => (v) =>
v.workStatus === RequestWorkStatus.Ready || v.workStatus === RequestWorkStatus.Ready ||
@ -166,7 +169,7 @@ function submit() {
requestWorkStep?: Task; requestWorkStep?: Task;
}[] = []; }[] = [];
selectedEmployee.value.forEach((v, i) => { selectedEmployee.value.forEach((v, i) => {
if (v.stepStatus.length === 0) { if (!v.stepStatus || v.stepStatus.length === 0) {
selected.push({ selected.push({
step: 0, step: 0,
requestWorkId: v.id || '', requestWorkId: v.id || '',
@ -224,7 +227,7 @@ function close() {
function onDialogOpen() { function onDialogOpen() {
// assign selected to group // assign selected to group
!props.creditNote && assignTempGroup(); assignTempGroup();
// match group to check // match group to check
selectedEmployee.value = []; selectedEmployee.value = [];
@ -232,7 +235,7 @@ function onDialogOpen() {
const matchingItems = tempGroupEdit.value const matchingItems = tempGroupEdit.value
.flatMap((g) => g.list) .flatMap((g) => g.list)
.filter((l) => { .filter((l) => {
if (l.stepStatus.length === 0) { if (!l.stepStatus || l.stepStatus.length === 0) {
return taskList.value.some( return taskList.value.some(
(t) => t.requestWorkStep?.requestWork.id === l.id, (t) => t.requestWorkStep?.requestWork.id === l.id,
); );
@ -248,8 +251,12 @@ function onDialogOpen() {
function assignTempGroup() { function assignTempGroup() {
if (!props.taskListGroup) return; if (!props.taskListGroup) return;
props.taskListGroup.forEach((newGroup) => { props.taskListGroup.forEach((newGroup) => {
const productId = props.creditNote
? (newGroup.product as RequestWork['productService']).productId
: (newGroup.product as RequestWork['productService']['product']).id;
const existingGroup = tempGroupEdit.value.find( const existingGroup = tempGroupEdit.value.find(
(g) => g.product.id === newGroup.product.id, (g) => g.product.id === productId,
); );
if (existingGroup) { if (existingGroup) {
@ -260,7 +267,9 @@ function assignTempGroup() {
}); });
} else { } else {
tempGroupEdit.value.push({ tempGroupEdit.value.push({
...newGroup, product: props.creditNote
? (newGroup.product as RequestWork['productService']).product
: (newGroup.product as RequestWork['productService']['product']),
list: [...newGroup.list], // Ensure a new reference list: [...newGroup.list], // Ensure a new reference
}); });
} }

View file

@ -232,14 +232,14 @@ function disableCheckAll() {
:columns=" :columns="
stepOn stepOn
? [ ? [
...employeeColumn.slice(0, 2), ...employeeColumn.slice(0, 3),
{ {
name: 'periodNo', name: 'periodNo',
align: 'center', align: 'center',
label: 'flow.step', label: 'flow.step',
field: (v) => v.product.code, field: (v) => v.product.code,
}, },
...employeeColumn.slice(2), ...employeeColumn.slice(3),
] ]
: statusOn : statusOn
? [ ? [
@ -463,6 +463,16 @@ function disableCheckAll() {
{{ props.row.request.code }} {{ props.row.request.code }}
</span> </span>
</q-td> </q-td>
<q-td>
<span
class="cursor-pointer link"
@click="goToQuotation(props.row.request.quotation)"
>
{{ props.row.request.quotation?.code }}
</span>
</q-td>
<q-td v-if="stepOn" class="text-left"> <q-td v-if="stepOn" class="text-left">
<div v-if="props.row._template" class="column text-left"> <div v-if="props.row._template" class="column text-left">
<span>{{ props.row._template.templateName }}</span> <span>{{ props.row._template.templateName }}</span>
@ -528,14 +538,7 @@ function disableCheckAll() {
:expiration-date="new Date(props.row.request.quotation?.dueDate)" :expiration-date="new Date(props.row.request.quotation?.dueDate)"
/> />
</q-td> </q-td>
<q-td>
<span
class="cursor-pointer link"
@click="goToQuotation(props.row.request.quotation)"
>
{{ props.row.request.quotation?.code }}
</span>
</q-td>
<q-td> <q-td>
<BadgeComponent <BadgeComponent
v-if="props.row.request.quotation?.urgent" v-if="props.row.request.quotation?.urgent"

View file

@ -170,6 +170,12 @@ export const employeeColumn = [
label: 'requestList.requestListCode', label: 'requestList.requestListCode',
field: 'code', field: 'code',
}, },
{
name: 'quotationCode',
align: 'center',
label: 'requestList.quotationCode',
field: 'quotationCode',
},
{ {
name: 'fullName', name: 'fullName',
align: 'center', align: 'center',
@ -200,12 +206,6 @@ export const employeeColumn = [
label: 'general.numberOfDay', label: 'general.numberOfDay',
field: 'day', field: 'day',
}, },
{
name: 'quotationCode',
align: 'center',
label: 'requestList.quotationCode',
field: 'quotationCode',
},
] as const satisfies QTableProps['columns']; ] as const satisfies QTableProps['columns'];
export const productColumn = [ export const productColumn = [

View file

@ -1,10 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { onMounted, nextTick, ref } from 'vue'; import { onMounted, nextTick, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import ThaiBahtText from 'thai-baht-text'; import ThaiBahtText from 'thai-baht-text';
// NOTE: Import stores // NOTE: Import stores
import { formatNumberDecimal } from 'stores/utils'; import { dialogWarningClose, formatNumberDecimal } from 'stores/utils';
import { useConfigStore } from 'stores/config'; import { useConfigStore } from 'stores/config';
import { precisionRound } from 'src/utils/arithmetic'; import { precisionRound } from 'src/utils/arithmetic';
@ -12,6 +13,7 @@ import { precisionRound } from 'src/utils/arithmetic';
import { Branch } from 'stores/branch/types'; import { Branch } from 'stores/branch/types';
// NOTE: Import Components // NOTE: Import Components
import { CancelButton } from 'components/button';
import ViewHeader from './ViewHeader.vue'; import ViewHeader from './ViewHeader.vue';
import ViewFooter from './ViewFooter.vue'; import ViewFooter from './ViewFooter.vue';
import PrintButton from 'src/components/button/PrintButton.vue'; import PrintButton from 'src/components/button/PrintButton.vue';
@ -29,6 +31,7 @@ const route = useRoute();
const taskOrder = useTaskOrderStore(); const taskOrder = useTaskOrderStore();
const configStore = useConfigStore(); const configStore = useConfigStore();
const config = storeToRefs(configStore).data; const config = storeToRefs(configStore).data;
const { t } = useI18n();
const viewType = ref<'docOrder' | 'docReceive'>('docOrder'); const viewType = ref<'docOrder' | 'docReceive'>('docOrder');
type Data = TaskOrder; type Data = TaskOrder;
@ -250,11 +253,32 @@ onMounted(async () => {
function print() { function print() {
window.print(); window.print();
} }
async function closeTab() {
dialogWarningClose(t, {
message: t('dialog.message.close'),
action: () => {
window.close();
},
cancel: () => {},
});
}
function closeAble() {
return window.opener !== null;
}
</script> </script>
<template> <template>
<div class="toolbar"> <div class="toolbar">
<PrintButton solid @click="print" /> <PrintButton solid @click="print" />
<CancelButton
outlined
id="btn-close"
@click="closeTab()"
:label="$t('dialog.action.close')"
v-if="closeAble()"
/>
</div> </div>
<div <div
class="row justify-between container" class="row justify-between container"
@ -489,7 +513,7 @@ function print() {
position: sticky; position: sticky;
top: 0; top: 0;
display: flex; display: flex;
justify-content: center; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 1rem; margin-bottom: 1rem;
padding: 1rem; padding: 1rem;

View file

@ -1209,19 +1209,19 @@ watch(
{{ $t('general.view', { msg: $t('general.example') }) }} {{ $t('general.view', { msg: $t('general.example') }) }}
</MainButton> </MainButton>
<div class="row" style="gap: var(--size-2)"> <div class="row" style="gap: var(--size-2)">
<UndoButton outlined @click="undo()" v-if="state.mode === 'edit'" />
<CancelButton
v-if="state.mode !== 'edit'"
:label="$t('dialog.action.close')"
outlined
@click="closeTab()"
/>
<template <template
v-if=" v-if="
fullTaskOrder?.taskOrderStatus === TaskOrderStatus.Pending || fullTaskOrder?.taskOrderStatus === TaskOrderStatus.Pending ||
state.mode === 'create' state.mode === 'create'
" "
> >
<UndoButton outlined @click="undo()" v-if="state.mode === 'edit'" />
<CancelButton
@click="closeTab()"
:label="$t('dialog.action.close')"
v-if="state.mode === 'info'"
outlined
/>
<SaveButton <SaveButton
v-if="state.mode && ['create', 'edit'].includes(state.mode)" v-if="state.mode && ['create', 'edit'].includes(state.mode)"
@click="() => formDocument.submit()" @click="() => formDocument.submit()"

View file

@ -3,9 +3,10 @@ import { storeToRefs } from 'pinia';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { getUserId } from 'src/services/keycloak'; import { getUserId } from 'src/services/keycloak';
import { useI18n } from 'vue-i18n';
// NOTE: Import Components // NOTE: Import Components
import { SaveButton } from 'src/components/button'; import { SaveButton, CancelButton } from 'src/components/button';
import { StateButton } from 'components/button'; import { StateButton } from 'components/button';
import InfoMessengerExpansion from '../expansion/receive/InfoMessengerExpansion.vue'; import InfoMessengerExpansion from '../expansion/receive/InfoMessengerExpansion.vue';
import InfoProductExpansion from '../expansion/receive/InfoProductExpansion.vue'; import InfoProductExpansion from '../expansion/receive/InfoProductExpansion.vue';
@ -31,6 +32,7 @@ import { useTaskOrderStore } from 'src/stores/task-order';
const route = useRoute(); const route = useRoute();
const taskOrderFormStore = useTaskOrderForm(); const taskOrderFormStore = useTaskOrderForm();
const { t } = useI18n();
const { currentFormData, state, fullTaskOrder } = const { currentFormData, state, fullTaskOrder } =
storeToRefs(taskOrderFormStore); storeToRefs(taskOrderFormStore);
@ -342,6 +344,16 @@ function sortList(
}); });
} }
async function closeTab() {
dialogWarningClose(t, {
message: t('dialog.message.close'),
action: () => {
window.close();
},
cancel: () => {},
});
}
onMounted(async () => { onMounted(async () => {
initTheme(); initTheme();
initLang(); initLang();
@ -756,12 +768,15 @@ watch([currentFormData.value.taskStatus], () => {
<!-- SEC: footer --> <!-- SEC: footer -->
<footer <footer class="surface-1 q-pa-md full-width">
v-if="fullTaskOrder.taskOrderStatus !== TaskOrderStatus.Pending" <nav class="row justify-end" style="gap: var(--size-2)">
class="surface-1 q-pa-md full-width" <CancelButton
> :label="$t('dialog.action.close')"
<nav class="row justify-end"> outlined
@click="closeTab()"
/>
<SaveButton <SaveButton
v-if="fullTaskOrder.taskOrderStatus !== TaskOrderStatus.Pending"
:disabled=" :disabled="
fullTaskOrder.taskList.some( fullTaskOrder.taskList.some(
(t) => (t) =>

View file

@ -24,7 +24,13 @@ import SelectReadyRequestWork from '../09_task-order/SelectReadyRequestWork.vue'
import RefundInformation from './RefundInformation.vue'; import RefundInformation from './RefundInformation.vue';
import QuotationFormReceipt from '../05_quotation/QuotationFormReceipt.vue'; import QuotationFormReceipt from '../05_quotation/QuotationFormReceipt.vue';
import DialogViewFile from 'src/components/dialog/DialogViewFile.vue'; import DialogViewFile from 'src/components/dialog/DialogViewFile.vue';
import { MainButton, SaveButton } from 'src/components/button'; import {
MainButton,
SaveButton,
CancelButton,
EditButton,
UndoButton,
} from 'src/components/button';
import { RequestWork } from 'src/stores/request-list/types'; import { RequestWork } from 'src/stores/request-list/types';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import useOptionStore from 'src/stores/options'; import useOptionStore from 'src/stores/options';
@ -76,15 +82,16 @@ const statusTabForm = ref<
}[] }[]
>([]); >([]);
const readonly = computed( // const readonly = computed(
() => // () =>
creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Pending || // creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Pending ||
creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Success, // creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Success,
); // );
const pageState = reactive({ const pageState = reactive({
productDialog: false, productDialog: false,
fileDialog: false, fileDialog: false,
mode: 'view' as 'view' | 'edit' | 'info',
}); });
const defaultRemark = '#[quotation-labor]<br/><br/>#[quotation-payment]'; const defaultRemark = '#[quotation-labor]<br/><br/>#[quotation-payment]';
@ -161,9 +168,11 @@ async function initStatus() {
{ {
title: 'Pending', title: 'Pending',
status: creditNoteData.value?.id status: creditNoteData.value?.id
? creditNoteData.value.creditNoteStatus === CreditNoteStatus.Success ? creditNoteData.value.creditNoteStatus === CreditNoteStatus.Waiting
? 'done' ? 'waiting'
: 'doing' : creditNoteData.value.creditNoteStatus === CreditNoteStatus.Success
? 'done'
: 'doing'
: 'waiting', : 'waiting',
active: () => view.value === CreditNoteStatus.Pending, active: () => view.value === CreditNoteStatus.Pending,
handler: async () => { handler: async () => {
@ -253,8 +262,9 @@ function assignFormData() {
requestWorkStep: { requestWorkStep: {
requestWork: { requestWork: {
...v, ...v,
stepStatus: v.stepStatus || [],
request: { ...v.request, quotation: current.quotation }, request: { ...v.request, quotation: current.quotation },
}, } as RequestWork,
}, },
})); }));
} }
@ -285,12 +295,24 @@ async function getQuotation() {
async function submit() { async function submit() {
const payload = formData.value; const payload = formData.value;
payload.requestWorkId = formTaskList.value.map((v) => v.requestWorkId); payload.requestWorkId = formTaskList.value.map((v) => v.requestWorkId);
payload.quotationId = payload.quotationId =
typeof route.query['quotationId'] === 'string' (pageState.mode === 'edit'
? route.query['quotationId'] ? creditNoteData.value?.quotationId
: ''; : typeof route.query['quotationId'] === 'string'
const res = await creditNote.createCreditNote(payload); ? route.query['quotationId']
: '') || '';
const res =
pageState.mode === 'edit'
? await creditNote.updateCreditNote(
creditNoteData.value?.id || '',
payload,
)
: creditNoteData.value
? await creditNote.acceptCreditNote(creditNoteData.value.id)
: await creditNote.createCreditNote(payload);
if (res) { if (res) {
await router.push(`/credit-note/${res.id}`); await router.push(`/credit-note/${res.id}`);
@ -302,6 +324,7 @@ async function submit() {
} }
initStatus(); initStatus();
pageState.mode = 'info';
} }
} }
@ -526,13 +549,31 @@ function storeDataLocal() {
window.open(url, '_blank'); window.open(url, '_blank');
} }
function closeTab() {
dialogWarningClose(t, {
message: t('dialog.message.close'),
action: () => {
window.close();
},
cancel: () => {},
});
}
function undo() {
assignFormData();
pageState.mode = 'info';
}
onMounted(async () => { onMounted(async () => {
initTheme(); initTheme();
initLang(); initLang();
await useConfigStore().getConfig(); await useConfigStore().getConfig();
await getCreditNote(); await getCreditNote();
await getQuotation(); await getQuotation();
creditNoteData.value && (await getFileList(creditNoteData.value.id, true)); if (creditNoteData.value) {
pageState.mode = 'info';
await getFileList(creditNoteData.value.id, true);
}
initStatus(); initStatus();
}); });
</script> </script>
@ -587,7 +628,7 @@ onMounted(async () => {
:key="i.title" :key="i.title"
:label=" :label="
$t( $t(
`creditNote${i.title === 'title' ? '' : '.stats'}.${i.title}`, `creditNote${i.title === 'title' ? '' : '.status'}.${i.title}`,
) )
" "
:status-active="i.active?.()" :status-active="i.active?.()"
@ -616,7 +657,7 @@ onMounted(async () => {
> >
<CreditNoteExpansion <CreditNoteExpansion
v-if="view === null" v-if="view === null"
:readonly="readonly" :readonly="pageState.mode === 'info'"
v-model:reason="formData.reason" v-model:reason="formData.reason"
v-model:detail="formData.detail" v-model:detail="formData.detail"
/> />
@ -625,7 +666,7 @@ onMounted(async () => {
<ProductExpansion <ProductExpansion
v-if="view === null" v-if="view === null"
creditNote creditNote
:readonly="readonly" :readonly="pageState.mode === 'info'"
:agentPrice="quotationData?.agentPrice" :agentPrice="quotationData?.agentPrice"
:task-list="taskListGroup" :task-list="taskListGroup"
@add-product="openProductDialog" @add-product="openProductDialog"
@ -633,7 +674,7 @@ onMounted(async () => {
<PaymentExpansion <PaymentExpansion
v-if="view === null" v-if="view === null"
:readonly="readonly" :readonly="pageState.mode === 'info'"
:total-price="summaryPrice.finalPrice" :total-price="summaryPrice.finalPrice"
v-model:payback-type="formData.paybackType" v-model:payback-type="formData.paybackType"
v-model:payback-bank="formData.paybackBank" v-model:payback-bank="formData.paybackBank"
@ -746,7 +787,7 @@ onMounted(async () => {
v-model:remark="formData.remark" v-model:remark="formData.remark"
:default-remark="defaultRemark" :default-remark="defaultRemark"
:items="[]" :items="[]"
:readonly :readonly="pageState.mode === 'info'"
> >
<template #hint> <template #hint>
{{ $t('general.hintRemark') }} {{ $t('general.hintRemark') }}
@ -793,7 +834,7 @@ onMounted(async () => {
<!-- SEC: footer --> <!-- SEC: footer -->
<footer class="surface-1 q-pa-md full-width"> <footer class="surface-1 q-pa-md full-width">
<nav class="row justify-end"> <nav class="row justify-end" style="gap: var(--size-2)">
<!-- TODO: view example --> <!-- TODO: view example -->
<MainButton <MainButton
class="q-mr-auto" class="q-mr-auto"
@ -804,16 +845,45 @@ onMounted(async () => {
> >
{{ $t('general.view', { msg: $t('general.example') }) }} {{ $t('general.view', { msg: $t('general.example') }) }}
</MainButton> </MainButton>
<!-- @click="submit" -->
<UndoButton v-if="pageState.mode === 'edit'" outlined @click="undo()" />
<CancelButton
v-if="pageState.mode !== 'edit'"
@click="closeTab()"
:label="$t('dialog.action.close')"
outlined
/>
<SaveButton <SaveButton
v-if="!readonly" v-if="
:disabled="taskListGroup.length === 0" !creditNoteData ||
creditNoteData?.creditNoteStatus === CreditNoteStatus.Waiting
"
:disabled="taskListGroup.length === 0 || pageState.mode === 'edit'"
type="submit" type="submit"
@click.stop="(e) => refForm?.submit(e)" @click.stop="(e) => refForm?.submit(e)"
:label="$t('creditNote.label.submit')" :label="
$t(
`creditNote.label.${creditNoteData?.creditNoteStatus ? 'submit' : 'request'}`,
)
"
icon="mdi-account-multiple-check-outline" icon="mdi-account-multiple-check-outline"
solid solid
></SaveButton> />
<SaveButton
v-if="pageState.mode === 'edit'"
:disabled="taskListGroup.length === 0"
@click="(e) => refForm?.submit(e)"
solid
/>
<EditButton
v-if="
pageState.mode === 'info' &&
creditNoteData?.creditNoteStatus === CreditNoteStatus.Waiting
"
class="no-print"
@click="pageState.mode = 'edit'"
solid
/>
</nav> </nav>
</footer> </footer>
</div> </div>
@ -822,6 +892,7 @@ onMounted(async () => {
<SelectReadyRequestWork <SelectReadyRequestWork
v-if="quotationData" v-if="quotationData"
creditNote creditNote
:task-list-group="taskListGroup"
:fetch-params="{ cancelOnly: true, quotationId: quotationData.id }" :fetch-params="{ cancelOnly: true, quotationId: quotationData.id }"
v-model:open="pageState.productDialog" v-model:open="pageState.productDialog"
v-model:task-list="formTaskList" v-model:task-list="formTaskList"

View file

@ -35,7 +35,7 @@ const { stats, pageMax, page, data, pageSize } = storeToRefs(creditNote);
// NOTE: Variable // NOTE: Variable
const pageState = reactive({ const pageState = reactive({
quotationId: '', quotationId: '',
currentTab: CreditNoteStatus.Pending, currentTab: CreditNoteStatus.Waiting,
hideStat: false, hideStat: false,
statusFilter: 'None', statusFilter: 'None',
inputSearch: '', inputSearch: '',
@ -158,7 +158,7 @@ watch(
color: hsl(var(--info-bg)); color: hsl(var(--info-bg));
" "
> >
{{ stats.Pending + stats.Success || 0 }} {{ Object.values(stats).reduce((sum, value) => sum + value, 0) || 0 }}
</q-badge> </q-badge>
<q-btn <q-btn
class="q-ml-sm" class="q-ml-sm"
@ -180,16 +180,22 @@ watch(
<StatCardComponent <StatCardComponent
labelI18n labelI18n
:branch="[ :branch="[
{
icon: 'icon-park-outline:loading-one',
count: stats[CreditNoteStatus.Pending] || 0,
label: `creditNote.status.${CreditNoteStatus.Waiting}`,
color: 'light-yellow',
},
{ {
icon: 'material-symbols-light:receipt-long', icon: 'material-symbols-light:receipt-long',
count: stats[CreditNoteStatus.Pending] || 0, count: stats[CreditNoteStatus.Pending] || 0,
label: `creditNote.stats.${CreditNoteStatus.Pending}`, label: `creditNote.status.${CreditNoteStatus.Pending}`,
color: 'orange', color: 'orange',
}, },
{ {
icon: 'mdi-check-decagram-outline', icon: 'mdi-check-decagram-outline',
count: stats[CreditNoteStatus.Success] || 0, count: stats[CreditNoteStatus.Success] || 0,
label: `creditNote.stats.${CreditNoteStatus.Success}`, label: `creditNote.status.${CreditNoteStatus.Success}`,
color: 'blue', color: 'blue',
}, },
]" ]"
@ -345,7 +351,7 @@ watch(
<TableCreditNote <TableCreditNote
:grid="pageState.gridView" :grid="pageState.gridView"
:visible-columns="pageState.fieldSelected" :visible-columns="pageState.fieldSelected"
:hide-delete="pageState.currentTab === CreditNoteStatus.Success" :hide-delete="pageState.currentTab !== CreditNoteStatus.Waiting"
@view="(v) => navigateTo({ statusDialog: 'info', creditId: v.id })" @view="(v) => navigateTo({ statusDialog: 'info', creditId: v.id })"
@delete="(v) => triggerDelete(v.id)" @delete="(v) => triggerDelete(v.id)"
> >

View file

@ -14,6 +14,7 @@ export const taskStatusOpts = [
]; ];
export const pageTabs = [ export const pageTabs = [
{ label: CreditNoteStatus.Waiting, value: CreditNoteStatus.Waiting },
{ label: CreditNoteStatus.Pending, value: CreditNoteStatus.Pending }, { label: CreditNoteStatus.Pending, value: CreditNoteStatus.Pending },
{ label: CreditNoteStatus.Success, value: CreditNoteStatus.Success }, { label: CreditNoteStatus.Success, value: CreditNoteStatus.Success },
]; ];

View file

@ -3,9 +3,10 @@ import { storeToRefs } from 'pinia';
import { onMounted, nextTick, ref, watch } from 'vue'; import { onMounted, nextTick, ref, watch } from 'vue';
import { precisionRound } from 'src/utils/arithmetic'; import { precisionRound } from 'src/utils/arithmetic';
import ThaiBahtText from 'thai-baht-text'; import ThaiBahtText from 'thai-baht-text';
import { useI18n } from 'vue-i18n';
// NOTE: Import stores // NOTE: Import stores
import { formatNumberDecimal } from 'stores/utils'; import { dialogWarningClose, formatNumberDecimal } from 'stores/utils';
import { useConfigStore } from 'stores/config'; import { useConfigStore } from 'stores/config';
import useBranchStore from 'stores/branch'; import useBranchStore from 'stores/branch';
@ -28,12 +29,14 @@ import PrintButton from 'src/components/button/PrintButton.vue';
import { convertTemplate } from 'src/utils/string-template'; import { convertTemplate } from 'src/utils/string-template';
import { RequestWork } from 'src/stores/request-list'; import { RequestWork } from 'src/stores/request-list';
import { Employee } from 'src/stores/employee/types'; import { Employee } from 'src/stores/employee/types';
import { CancelButton } from 'components/button';
const configStore = useConfigStore(); const configStore = useConfigStore();
const branchStore = useBranchStore(); const branchStore = useBranchStore();
const customerStore = useCustomerStore(); const customerStore = useCustomerStore();
const creditNoteStore = useCreditNote(); const creditNoteStore = useCreditNote();
const { data: config } = storeToRefs(configStore); const { data: config } = storeToRefs(configStore);
const { t } = useI18n();
const agentPrice = ref<boolean>(false); const agentPrice = ref<boolean>(false);
@ -263,11 +266,32 @@ watch(elements, () => {});
function print() { function print() {
window.print(); window.print();
} }
async function closeTab() {
dialogWarningClose(t, {
message: t('dialog.message.close'),
action: () => {
window.close();
},
cancel: () => {},
});
}
function closeAble() {
return window.opener !== null;
}
</script> </script>
<template> <template>
<div class="toolbar"> <div class="toolbar">
<PrintButton solid @click="print" /> <PrintButton solid @click="print" />
<CancelButton
outlined
id="btn-close"
@click="closeTab()"
:label="$t('dialog.action.close')"
v-if="closeAble()"
/>
</div> </div>
<div class="row justify-between container color-debit-note"> <div class="row justify-between container color-debit-note">
<section class="content" v-for="(chunk, i) in chunks" :key="i"> <section class="content" v-for="(chunk, i) in chunks" :key="i">
@ -548,7 +572,7 @@ function print() {
position: sticky; position: sticky;
top: 0; top: 0;
display: flex; display: flex;
justify-content: center; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 1rem; margin-bottom: 1rem;
padding: 1rem; padding: 1rem;

View file

@ -7,6 +7,7 @@ import { dateFormatJS, calculateAge } from 'src/utils/datetime';
import { initLang, initTheme, Lang } from 'src/utils/ui'; import { initLang, initTheme, Lang } from 'src/utils/ui';
import { useQuotationStore } from 'src/stores/quotations'; import { useQuotationStore } from 'src/stores/quotations';
import { useQuotationForm } from 'src/pages/05_quotation/form'; import { useQuotationForm } from 'src/pages/05_quotation/form';
import { useDebitNoteForm } from './form';
import { useReceipt, usePayment } from 'stores/payment'; import { useReceipt, usePayment } from 'stores/payment';
import useProductServiceStore from 'stores/product-service'; import useProductServiceStore from 'stores/product-service';
import { import {
@ -40,16 +41,17 @@ import {
SaveButton, SaveButton,
UndoButton, UndoButton,
EditButton, EditButton,
CancelButton,
} from 'src/components/button'; } from 'src/components/button';
import { RequestWork } from 'src/stores/request-list/types'; import { RequestWork } from 'src/stores/request-list/types';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import useOptionStore from 'src/stores/options'; import useOptionStore from 'src/stores/options';
import { deleteItem, dialogWarningClose } from 'src/stores/utils'; import { deleteItem, dialog, dialogWarningClose } from 'src/stores/utils';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Employee } from 'src/stores/employee/types'; import { Employee } from 'src/stores/employee/types';
import QuotationFormWorkerSelect from '../05_quotation/QuotationFormWorkerSelect.vue'; import QuotationFormWorkerSelect from '../05_quotation/QuotationFormWorkerSelect.vue';
import { watch } from 'vue'; import { watch } from 'vue';
import { ProductTree } from '../05_quotation/utils'; import { ProductTree, quotationProductTree } from '../05_quotation/utils';
import TableRequest from '../05_quotation/TableRequest.vue'; import TableRequest from '../05_quotation/TableRequest.vue';
import { RequestData, RequestDataStatus } from 'src/stores/request-list/types'; import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
import { useRequestList } from 'src/stores/request-list'; import { useRequestList } from 'src/stores/request-list';
@ -63,6 +65,7 @@ import { precisionRound } from 'src/utils/arithmetic';
import QuotationFormProductSelect from '../05_quotation/QuotationFormProductSelect.vue'; import QuotationFormProductSelect from '../05_quotation/QuotationFormProductSelect.vue';
import { getName } from 'src/services/keycloak'; import { getName } from 'src/services/keycloak';
const debitNoteForm = useDebitNoteForm();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const debitNote = useDebitNote(); const debitNote = useDebitNote();
@ -76,6 +79,7 @@ const $q = useQuasar();
const paymentStore = usePayment(); const paymentStore = usePayment();
const { data: config } = storeToRefs(configStore); const { data: config } = storeToRefs(configStore);
const { newWorkerList } = storeToRefs(quotationForm); const { newWorkerList } = storeToRefs(quotationForm);
const { currentFormData } = storeToRefs(debitNoteForm);
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const requestStore = useRequestList(); const requestStore = useRequestList();
@ -101,9 +105,10 @@ const productGroup = ref<ProductGroup[]>([]);
const agentPrice = ref(false); const agentPrice = ref(false);
const debitNoteData = ref<DebitNote>(); const debitNoteData = ref<DebitNote>();
const quotationData = ref<DebitNote['debitNoteQuotation']>(); const quotationData = ref<DebitNote['debitNoteQuotation']>();
const view = ref<QuotationStatus | null>(null); const view = ref<QuotationStatus>(QuotationStatus.Issued);
const fileList = ref<FileList>(); const fileList = ref<FileList>();
const receiptList = ref<Receipt[]>([]); const receiptList = ref<Receipt[]>([]);
let previousValue: DebitNotePayload | undefined = undefined;
const rowsRequestList = ref<RequestData[]>([]); const rowsRequestList = ref<RequestData[]>([]);
@ -112,20 +117,6 @@ const productServiceList = ref<
>([]); >([]);
const productServiceNodes = ref<ProductTree>([]); const productServiceNodes = ref<ProductTree>([]);
const currentFormData = ref<DebitNotePayload>({
productServiceList: [],
debitNoteQuotationId: '',
worker: [],
payBillDate: new Date(),
paySplitCount: 0,
payCondition: PayCondition.Full,
dueDate: new Date(Date.now() + 86400000),
discount: 0,
status: 'CREATED',
remark: '#[quotation-labor]<br/><br/>#[quotation-payment]',
quotationId: '',
agentPrice: false,
});
const tempPaySplitCount = ref(0); const tempPaySplitCount = ref(0);
const tempPaySplit = ref< const tempPaySplit = ref<
{ no: number; amount: number; name?: string; invoice?: boolean }[] { no: number; amount: number; name?: string; invoice?: boolean }[]
@ -258,11 +249,27 @@ const selectedInstallmentNo = ref<number[]>([]);
const installmentAmount = ref<number>(0); const installmentAmount = ref<number>(0);
const QUOTATION_STATUS = [ const QUOTATION_STATUS = [
'Accepted',
'PaymentPending', 'PaymentPending',
'PaymentSuccess', 'PaymentSuccess',
'ProcessComplete', 'ProcessComplete',
]; ];
function covertToNode() {
productServiceNodes.value = quotationProductTree(
productServiceList.value,
agentPrice.value,
config.value?.vat,
);
}
watch(
() => productServiceList.value,
() => {
covertToNode();
},
);
function toggleDeleteProduct(index: number) { function toggleDeleteProduct(index: number) {
// product display // product display
productServiceList.value.splice(index, 1); productServiceList.value.splice(index, 1);
@ -324,6 +331,46 @@ async function assignToProductServiceList() {
} }
} }
function undo() {
if (!previousValue) return;
currentFormData.value = structuredClone(previousValue);
assignProductServiceList();
assignSelectedWorker();
pageState.mode = 'info';
}
function assignProductServiceList() {
if (!debitNoteData.value) return;
productServiceList.value = debitNoteData.value.productServiceList.map(
(v, i) => ({
id: v.id!,
installmentNo: v.installmentNo || 0,
workerIndex: v.worker
.map((a) =>
debitNoteData.value!.worker.findIndex(
(b) => b.employeeId === a.employeeId,
),
)
.filter(
(index): index is number => index !== -1 && index !== undefined,
),
vat: v.vat || 0,
pricePerUnit: v.pricePerUnit || 0,
discount: v.discount || 0,
amount: v.amount || 0,
product: v.product,
work: v.work || null,
service: v.service || null,
}),
);
}
function assignSelectedWorker() {
if (debitNoteData.value)
selectedWorker.value = debitNoteData.value.worker.map((v) => v.employee);
}
async function assignFormData(id: string) { async function assignFormData(id: string) {
const data = await debitNote.getDebitNote(id); const data = await debitNote.getDebitNote(id);
if (!data) return; if (!data) return;
@ -333,7 +380,7 @@ async function assignFormData(id: string) {
selectedProductGroup.value = selectedProductGroup.value =
data.productServiceList[0]?.product.productGroup?.id || ''; data.productServiceList[0]?.product.productGroup?.id || '';
currentFormData.value = { (previousValue = {
id: data.id || undefined, id: data.id || undefined,
debitNoteQuotationId: data.debitNoteQuotationId || undefined, debitNoteQuotationId: data.debitNoteQuotationId || undefined,
productServiceList: structuredClone( productServiceList: structuredClone(
@ -358,24 +405,11 @@ async function assignFormData(id: string) {
agentPrice: data.agentPrice, agentPrice: data.agentPrice,
quotationId: data.debitNoteQuotationId, quotationId: data.debitNoteQuotationId,
remark: data.remark || undefined, remark: data.remark || undefined,
}; }),
(currentFormData.value = structuredClone(previousValue));
productServiceList.value = data.productServiceList.map((v, i) => ({ assignProductServiceList();
id: v.id!, assignSelectedWorker();
installmentNo: v.installmentNo || 0,
workerIndex: v.worker.map((a) =>
data.worker.findIndex((b) => b.employeeId === a.employeeId),
) || [0],
vat: v.vat || 0,
pricePerUnit: v.pricePerUnit || 0,
discount: v.discount || 0,
amount: v.amount || 0,
product: v.product,
work: v.work || null,
service: v.service || null,
}));
selectedWorker.value = data.worker.map((v) => v.employee);
await getQuotation(data.debitNoteQuotation?.id); await getQuotation(data.debitNoteQuotation?.id);
await assignToProductServiceList(); await assignToProductServiceList();
await fetchRequest(); await fetchRequest();
@ -419,16 +453,29 @@ async function initStatus() {
{ {
title: 'title', title: 'title',
status: debitNoteData.value?.id !== undefined ? 'done' : 'doing', status: debitNoteData.value?.id !== undefined ? 'done' : 'doing',
active: () => view.value === null, active: () => view.value === QuotationStatus.Issued,
handler: () => { handler: () => {
view.value = null; pageState.mode = 'info';
view.value = QuotationStatus.Issued;
},
},
{
title: 'accepted',
status:
debitNoteData.value?.id !== undefined
? getStatus(debitNoteData.value.quotationStatus, 1, -1)
: 'waiting',
active: () => view.value === QuotationStatus.Accepted,
handler: () => {
pageState.mode = 'info';
view.value = QuotationStatus.Accepted;
}, },
}, },
{ {
title: 'payment', title: 'payment',
status: status:
debitNoteData.value?.id !== undefined debitNoteData.value?.id !== undefined
? getStatus(debitNoteData.value.quotationStatus, 1, -1) ? getStatus(debitNoteData.value.quotationStatus, 2, 1)
: 'waiting', : 'waiting',
active: () => view.value === QuotationStatus.PaymentPending, active: () => view.value === QuotationStatus.PaymentPending,
handler: async () => { handler: async () => {
@ -438,18 +485,20 @@ async function initStatus() {
{ {
title: 'receipt', title: 'receipt',
status: getStatus(debitNoteData.value?.quotationStatus, 1, 1), status: getStatus(debitNoteData.value?.quotationStatus, 2, 2),
active: () => view.value === QuotationStatus.PaymentSuccess, active: () => view.value === QuotationStatus.PaymentSuccess,
handler: () => { handler: () => {
pageState.mode = 'info';
view.value = QuotationStatus.PaymentSuccess; view.value = QuotationStatus.PaymentSuccess;
}, },
}, },
{ {
title: 'processComplete', title: 'processComplete',
status: getStatus(debitNoteData.value?.quotationStatus, 2, 1), status: getStatus(debitNoteData.value?.quotationStatus, 3, 2),
active: () => view.value === QuotationStatus.ProcessComplete, active: () => view.value === QuotationStatus.ProcessComplete,
handler: () => { handler: () => {
pageState.mode = 'info';
view.value = QuotationStatus.ProcessComplete; view.value = QuotationStatus.ProcessComplete;
}, },
}, },
@ -778,6 +827,7 @@ async function submit() {
: await debitNote.createDebitNote(payload); : await debitNote.createDebitNote(payload);
if (res) { if (res) {
newWorkerList.value = [];
await router.push(`/debit-note/${res.id}/?mode=info`); await router.push(`/debit-note/${res.id}/?mode=info`);
if (attachmentList.value && pageState.mode === 'create') { if (attachmentList.value && pageState.mode === 'create') {
@ -785,7 +835,6 @@ async function submit() {
} }
pageState.mode = 'info'; pageState.mode = 'info';
assignFormData(res.id); assignFormData(res.id);
initStatus(); initStatus();
@ -849,6 +898,20 @@ function storeDataLocal() {
window.open(url, '_blank'); window.open(url, '_blank');
} }
async function closeTab() {
dialogWarningClose(t, {
message: t('dialog.message.close'),
action: () => {
window.close();
},
cancel: () => {},
});
}
function closeAble() {
return window.opener !== null;
}
watch( watch(
() => pageState.mode, () => pageState.mode,
() => toggleMode(pageState.mode), () => toggleMode(pageState.mode),
@ -876,7 +939,8 @@ onMounted(async () => {
} }
if (typeof route.query['mode'] === 'string') { if (typeof route.query['mode'] === 'string') {
pageState.mode = route.query['mode'] as 'create' | 'edit' | 'info'; pageState.mode =
(route.query['mode'] as 'create' | 'edit' | 'info') || 'info';
} }
if (typeof route.query['tab'] === 'string') { if (typeof route.query['tab'] === 'string') {
@ -884,11 +948,40 @@ onMounted(async () => {
{ {
payment: QuotationStatus.PaymentPending, payment: QuotationStatus.PaymentPending,
receipt: QuotationStatus.PaymentSuccess, receipt: QuotationStatus.PaymentSuccess,
}[route.query['tab']] || null; }[route.query['tab']] || QuotationStatus.Issued;
} }
if (
pageState.mode === 'edit' &&
debitNoteData.value?.quotationStatus !== QuotationStatus.Issued
) {
pageState.mode = 'info';
}
await useConfigStore().getConfig(); await useConfigStore().getConfig();
}); });
async function submitAccepted() {
dialog({
color: 'info',
icon: 'mdi-account-check',
title: t('dialog.title.confirmDebitNoteAccept'),
actionText: t('general.confirm'),
persistent: true,
message: t('dialog.message.quotationAccept'),
action: async () => {
if (!currentFormData.value.id) return;
const res = await debitNote.action.acceptDebitNote(
currentFormData.value.id,
);
if (res && typeof route.params['id'] === 'string') {
await assignFormData(route.params['id']);
}
},
cancel: () => {},
});
}
</script> </script>
<template> <template>
@ -953,7 +1046,10 @@ onMounted(async () => {
<!-- #TODO add goToQuotation as @goto-quotation--> <!-- #TODO add goToQuotation as @goto-quotation-->
<DocumentExpansion <DocumentExpansion
v-if="view === null" v-if="
view === QuotationStatus.Issued ||
view === QuotationStatus.Accepted
"
:readonly :readonly
:registered-branch-id="quotationData?.registeredBranchId" :registered-branch-id="quotationData?.registeredBranchId"
:customer-id="quotationData?.customerBranchId" :customer-id="quotationData?.customerBranchId"
@ -984,7 +1080,10 @@ onMounted(async () => {
/> />
<WorkerItemExpansion <WorkerItemExpansion
v-if="view === null" v-if="
view === QuotationStatus.Issued ||
view === QuotationStatus.Accepted
"
:readonly :readonly
:hide-btn-add-worker=" :hide-btn-add-worker="
!readonly && !readonly &&
@ -997,14 +1096,22 @@ onMounted(async () => {
<!-- #TODO add openProductDialog at @add-product--> <!-- #TODO add openProductDialog at @add-product-->
<ProductExpansion <ProductExpansion
v-if="view === null" v-if="
view === QuotationStatus.Issued ||
view === QuotationStatus.Accepted
"
:readonly :readonly
:installment-input="currentFormData.payCondition === 'SplitCustom'" :installment-input="currentFormData.payCondition === 'SplitCustom'"
:max-installment="currentFormData.paySplitCount" :max-installment="currentFormData.paySplitCount"
:agent-price="agentPrice" :agent-price="agentPrice"
:employee-rows="selectedWorkerItem" :employee-rows="selectedWorkerItem"
:rows="productService" :rows="productService"
@add-product="() => (pageState.productServiceModal = true)" @add-product="
() => {
pageState.productServiceModal = true;
covertToNode();
}
"
@update-rows=" @update-rows="
(v) => { (v) => {
view === null && (productServiceList = v); view === null && (productServiceList = v);
@ -1015,7 +1122,10 @@ onMounted(async () => {
/> />
<PaymentExpansion <PaymentExpansion
v-if="view === null" v-if="
view === QuotationStatus.Issued ||
view === QuotationStatus.Accepted
"
readonly readonly
:total-price="summaryPrice.finalPrice" :total-price="summaryPrice.finalPrice"
class="q-mb-md" class="q-mb-md"
@ -1029,7 +1139,11 @@ onMounted(async () => {
<!-- TODO: bind additional file --> <!-- TODO: bind additional file -->
<AdditionalFileExpansion <AdditionalFileExpansion
v-if="view === null || view === QuotationStatus.PaymentPending" v-if="
view === QuotationStatus.Issued ||
view === QuotationStatus.Accepted ||
view === QuotationStatus.PaymentPending
"
:readonly :readonly
v-model:file-data="attachmentData" v-model:file-data="attachmentData"
:transform-url=" :transform-url="
@ -1074,7 +1188,11 @@ onMounted(async () => {
<!-- TODO: bind remark --> <!-- TODO: bind remark -->
<RemarkExpansion <RemarkExpansion
v-if="view === null || view === QuotationStatus.PaymentPending" v-if="
view === QuotationStatus.Issued ||
view === QuotationStatus.Accepted ||
view === QuotationStatus.PaymentPending
"
:readonly="readonly" :readonly="readonly"
v-model:remark="currentFormData.remark" v-model:remark="currentFormData.remark"
/> />
@ -1149,7 +1267,7 @@ onMounted(async () => {
installmentAmount = v.invoice.amount; installmentAmount = v.invoice.amount;
} }
view = null; view = QuotationStatus.Issued;
} }
" "
@example="() => exampleReceipt(v.id)" @example="() => exampleReceipt(v.id)"
@ -1173,38 +1291,59 @@ onMounted(async () => {
{{ $t('general.view', { msg: $t('general.example') }) }} {{ $t('general.view', { msg: $t('general.example') }) }}
</MainButton> </MainButton>
<div class="row q-gutter-x-sm"> <CancelButton
outlined
id="btn-close"
@click="closeTab()"
:label="$t('dialog.action.close')"
v-if="pageState.mode === 'info' && closeAble()"
/>
<div class="row q-gutter-x-sm q-ml-xs">
<UndoButton <UndoButton
outlined outlined
@click=" v-if="pageState.mode === 'edit'"
() => { @click="undo()"
pageState.mode = 'info';
}
"
v-if="false"
/> />
<SaveButton <SaveButton
v-if="!readonly && pageState.mode === 'create'" v-if="pageState.mode !== 'info'"
:disabled=" :disabled="
selectedWorkerItem.length === 0 && productService.length === 0 selectedWorkerItem.length === 0 && productService.length === 0
" "
@click="submit" @click="submit"
:label="true ? $t('debitNote.label.submit') : $t('general.save')"
:icon="
true
? 'mdi-account-multiple-check-outline'
: 'mdi-content-save-outline'
"
solid
></SaveButton>
<EditButton
v-if="false"
class="no-print"
@click="pageState.mode = 'edit'"
solid solid
/> />
<template v-if="debitNoteData">
<EditButton
v-if="
pageState.mode === 'info' &&
view === QuotationStatus.Issued &&
debitNoteData.quotationStatus === QuotationStatus.Issued
"
class="no-print"
solid
@click="pageState.mode = 'edit'"
/>
<MainButton
v-if="
view === QuotationStatus.Accepted &&
debitNoteData.quotationStatus === QuotationStatus.Issued
"
solid
icon="mdi-account-multiple-check-outline"
color="207 96% 32%"
id="btn-submit-accepted"
@click="
() => {
submitAccepted();
}
"
>
{{ $t('quotation.customerAcceptance') }}
</MainButton>
</template>
</div> </div>
</nav> </nav>
</footer> </footer>

View file

@ -3,9 +3,10 @@ import { storeToRefs } from 'pinia';
import { onMounted, nextTick, ref, watch } from 'vue'; import { onMounted, nextTick, ref, watch } from 'vue';
import { precisionRound } from 'src/utils/arithmetic'; import { precisionRound } from 'src/utils/arithmetic';
import ThaiBahtText from 'thai-baht-text'; import ThaiBahtText from 'thai-baht-text';
import { useI18n } from 'vue-i18n';
// NOTE: Import stores // NOTE: Import stores
import { formatNumberDecimal } from 'stores/utils'; import { dialogWarningClose, formatNumberDecimal } from 'stores/utils';
import { useConfigStore } from 'stores/config'; import { useConfigStore } from 'stores/config';
import useBranchStore from 'stores/branch'; import useBranchStore from 'stores/branch';
@ -29,6 +30,7 @@ import ViewHeader from './ViewHeader.vue';
import ViewFooter from './ViewFooter.vue'; import ViewFooter from './ViewFooter.vue';
import BankComponents from './BankComponents.vue'; import BankComponents from './BankComponents.vue';
import PrintButton from 'src/components/button/PrintButton.vue'; import PrintButton from 'src/components/button/PrintButton.vue';
import { CancelButton } from 'components/button';
import { convertTemplate } from 'src/utils/string-template'; import { convertTemplate } from 'src/utils/string-template';
const configStore = useConfigStore(); const configStore = useConfigStore();
@ -36,6 +38,7 @@ const branchStore = useBranchStore();
const customerStore = useCustomerStore(); const customerStore = useCustomerStore();
const debitNoteStore = useDebitNote(); const debitNoteStore = useDebitNote();
const { data: config } = storeToRefs(configStore); const { data: config } = storeToRefs(configStore);
const { t } = useI18n();
type Product = { type Product = {
id: string; id: string;
@ -272,6 +275,20 @@ onMounted(async () => {
watch(elements, () => {}); watch(elements, () => {});
async function closeTab() {
dialogWarningClose(t, {
message: t('dialog.message.close'),
action: () => {
window.close();
},
cancel: () => {},
});
}
function closeAble() {
return window.opener !== null;
}
function print() { function print() {
window.print(); window.print();
} }
@ -280,6 +297,13 @@ function print() {
<template> <template>
<div class="toolbar"> <div class="toolbar">
<PrintButton solid @click="print" /> <PrintButton solid @click="print" />
<CancelButton
outlined
id="btn-close"
@click="closeTab()"
:label="$t('dialog.action.close')"
v-if="closeAble()"
/>
</div> </div>
<div class="row justify-between container color-debit-note"> <div class="row justify-between container color-debit-note">
<section class="content" v-for="chunk in chunks"> <section class="content" v-for="chunk in chunks">
@ -553,7 +577,7 @@ function print() {
position: sticky; position: sticky;
top: 0; top: 0;
display: flex; display: flex;
justify-content: center; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 1rem; margin-bottom: 1rem;
padding: 1rem; padding: 1rem;

View file

@ -54,7 +54,7 @@ const toggleWorker = defineModel<boolean>('toggleWorker');
</div> </div>
<nav class="q-ml-auto"> <nav class="q-ml-auto">
<AddButton <AddButton
v-if="!hideBtnAddWorker" v-if="!readonly"
icon-only icon-only
@click.stop="$emit('addWorker')" @click.stop="$emit('addWorker')"
/> />

View file

@ -14,10 +14,9 @@ const DEFAULT_DATA: DebitNotePayload = {
debitNoteQuotationId: '', debitNoteQuotationId: '',
worker: [], worker: [],
payBillDate: new Date(), payBillDate: new Date(),
paySplit: [],
paySplitCount: 0, paySplitCount: 0,
payCondition: PayCondition.Full, payCondition: PayCondition.Full,
dueDate: new Date(), dueDate: new Date(Date.now() + 86400000),
discount: 0, discount: 0,
status: 'CREATED', status: 'CREATED',
remark: '#[quotation-labor]<br/><br/>#[quotation-payment]', remark: '#[quotation-labor]<br/><br/>#[quotation-payment]',
@ -58,6 +57,7 @@ export const useDebitNoteForm = defineStore('form-debit-note', () => {
} }
return { return {
currentFormData,
isFormDataDifferent, isFormDataDifferent,
resetForm, resetForm,
}; };

View file

@ -71,12 +71,19 @@ export async function updatePaybackStatus(
return null; return null;
} }
export async function acceptCreditNote(id: string) {
const res = await api.post<Data>(`/${ENDPOINT}/${id}/accept`);
if (res.status < 400) return res.data;
return null;
}
export const useCreditNote = defineStore('credit-note-store', () => { export const useCreditNote = defineStore('credit-note-store', () => {
const data = ref<Data[]>([]); const data = ref<Data[]>([]);
const page = ref<number>(1); const page = ref<number>(1);
const pageMax = ref<number>(1); const pageMax = ref<number>(1);
const pageSize = ref<number>(30); const pageSize = ref<number>(30);
const stats = ref<Record<Status, number>>({ const stats = ref<Record<Status, number>>({
[Status.Waiting]: 0,
[Status.Pending]: 0, [Status.Pending]: 0,
[Status.Success]: 0, [Status.Success]: 0,
}); });
@ -94,6 +101,7 @@ export const useCreditNote = defineStore('credit-note-store', () => {
createCreditNote, createCreditNote,
updateCreditNote, updateCreditNote,
deleteCreditNote, deleteCreditNote,
acceptCreditNote,
...manageAttachment(api, ENDPOINT), ...manageAttachment(api, ENDPOINT),
...manageFile<'slip'>(api, ENDPOINT), ...manageFile<'slip'>(api, ENDPOINT),

View file

@ -42,6 +42,7 @@ export type CreditNote = {
}; };
export enum CreditNoteStatus { export enum CreditNoteStatus {
Waiting = 'Waiting',
Pending = 'Pending', Pending = 'Pending',
Success = 'Success', Success = 'Success',
} }

View file

@ -61,6 +61,12 @@ export async function deleteDebitNote(id: string) {
return null; return null;
} }
export async function acceptDebitNote(id: string) {
const res = await api.post<Data>(`/${ENDPOINT}/${id}/accept`);
if (res.status < 400) return res.data;
return null;
}
export const useDebitNote = defineStore('debit-note-store', () => { export const useDebitNote = defineStore('debit-note-store', () => {
const data = ref<Data[]>([]); const data = ref<Data[]>([]);
const page = ref<number>(1); const page = ref<number>(1);
@ -87,6 +93,8 @@ export const useDebitNote = defineStore('debit-note-store', () => {
updateDebitNote, updateDebitNote,
deleteDebitNote, deleteDebitNote,
action: { acceptDebitNote },
...manageAttachment(api, ENDPOINT), ...manageAttachment(api, ENDPOINT),
...manageFile<'slip'>(api, ENDPOINT), ...manageFile<'slip'>(api, ENDPOINT),
}; };

View file

@ -72,6 +72,7 @@ export const useQuotationStore = defineStore('quotation-store', () => {
hasCancel?: boolean; hasCancel?: boolean;
includeRegisteredBranch?: boolean; includeRegisteredBranch?: boolean;
forDebitNote?: boolean; forDebitNote?: boolean;
cancelIncludeDebitNote?: boolean;
}) { }) {
const res = await api.get<PaginationResult<Quotation>>('/quotation', { const res = await api.get<PaginationResult<Quotation>>('/quotation', {
params, params,

View file

@ -343,6 +343,7 @@ export type QuotationFull = {
updatedBy: UpdatedBy; updatedBy: UpdatedBy;
agentPrice?: boolean; agentPrice?: boolean;
isDebitNote?: boolean;
}; };
export type QuotationPayload = { export type QuotationPayload = {
@ -387,7 +388,7 @@ export type EmployeeWorker = {
namePrefix: string; namePrefix: string;
nationality: string; nationality: string;
gender: string; gender: string;
dateOfBirth: Date; dateOfBirth: Date | null;
}; };
export type ProductGroup = { export type ProductGroup = {

View file

@ -9,7 +9,12 @@ export type RequestData = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
quotation: QuotationFull & { isDebitNote: boolean }; quotation: QuotationFull & {
debitNoteQuotationId: string;
isDebitNote: boolean;
debitNoteQuotation: { code: string };
};
quotationId: string; quotationId: string;
flow: Record<string, any>; flow: Record<string, any>;