Merge branch 'develop'
This commit is contained in:
commit
257790c1ce
31 changed files with 731 additions and 263 deletions
89
.github/workflows/gitea-local.yaml
vendored
Normal file
89
.github/workflows/gitea-local.yaml
vendored
Normal 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 }}
|
||||
31
.github/workflows/local-build-release.yaml
vendored
31
.github/workflows/local-build-release.yaml
vendored
|
|
@ -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
|
||||
22
.github/workflows/local-release-demo.yml
vendored
22
.github/workflows/local-release-demo.yml
vendored
|
|
@ -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
|
||||
22
.github/workflows/local-release-dev.yml
vendored
22
.github/workflows/local-release-dev.yml
vendored
|
|
@ -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
|
||||
|
|
@ -34,6 +34,7 @@ const quotationId = defineModel<string>('quotationId', {
|
|||
v-model:value="quotationId"
|
||||
:label="$t('general.select', { msg: $t('quotation.title') })"
|
||||
:params="{
|
||||
cancelIncludeDebitNote: true,
|
||||
hasCancel: true,
|
||||
}"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -779,6 +779,7 @@ export default {
|
|||
specialCondition: 'Special Conditions',
|
||||
selectInvoice: 'Select Invoice',
|
||||
approveInvoice: 'Approve the invoice',
|
||||
approveDebitNote: 'Approve Debit Note',
|
||||
paymentCondition: 'Payment Terms',
|
||||
payType: 'Payment Methods',
|
||||
bank: 'Select Payment Account',
|
||||
|
|
@ -897,6 +898,8 @@ export default {
|
|||
caption: 'All Request List',
|
||||
quotationCode: 'Quotation Code',
|
||||
requestListCode: 'Request List Code',
|
||||
|
||||
referenceNo: 'Reference No.',
|
||||
invoiceCode: 'Invoice Code',
|
||||
receiptCode: 'Receipt Code',
|
||||
alienIdCard: 'Alien Identification Card"',
|
||||
|
|
@ -1022,6 +1025,7 @@ export default {
|
|||
importWorker: 'Import Worker',
|
||||
confirmLogout: 'Confirm Logout',
|
||||
confirmQuotationAccept: 'Confirm acceptance of the quotation.',
|
||||
confirmDebitNoteAccept: 'Confirm acceptance of the debit note.',
|
||||
},
|
||||
message: {
|
||||
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.',
|
||||
reasonCanceled:
|
||||
'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',
|
||||
refundMethod: 'Refund Method',
|
||||
totalRefund: 'Total refund amount',
|
||||
|
|
@ -1252,6 +1257,7 @@ export default {
|
|||
refundSuccess: 'Refund Success',
|
||||
},
|
||||
status: {
|
||||
Waiting: 'Credit Note',
|
||||
Pending: 'Pending Refund',
|
||||
Success: 'Refund Completed',
|
||||
Canceled: 'Canceled',
|
||||
|
|
@ -1261,10 +1267,6 @@ export default {
|
|||
Done: 'Done',
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
Pending: 'Pending Refund',
|
||||
Success: 'Refund Completed',
|
||||
},
|
||||
},
|
||||
|
||||
invoice: {
|
||||
|
|
@ -1303,6 +1305,7 @@ export default {
|
|||
quotationWorkName: 'Work Name',
|
||||
quotationPayment: 'Payment Method',
|
||||
value: 'Net Value',
|
||||
request: 'Request Debit Note Approval',
|
||||
submit: 'Approve Debit Note',
|
||||
},
|
||||
|
||||
|
|
@ -1315,6 +1318,7 @@ export default {
|
|||
},
|
||||
|
||||
viewMode: {
|
||||
accepted: 'Accept',
|
||||
payment: 'Payment',
|
||||
receipt: 'Receipt/Tax Invoice',
|
||||
processComplete: 'Completed',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { title } from 'process';
|
||||
|
||||
export default {
|
||||
general: {
|
||||
ok: 'ตกลง',
|
||||
|
|
@ -761,7 +759,7 @@ export default {
|
|||
branch: 'สาขาที่ออกใบเสนอราคา',
|
||||
branchVirtual: 'จุดรับบริการที่ออกใบเสนอราคา',
|
||||
customer: 'ลูกค้า',
|
||||
newCustomer: 'ลูกค้าใหม่',
|
||||
newCustomer: 'แรงงานใหม่',
|
||||
employeeList: 'รายชื่อแรงงาน',
|
||||
employee: 'แรงงาน',
|
||||
employeeName: 'ชื่อ-นามสกุล แรงงาน',
|
||||
|
|
@ -772,6 +770,7 @@ export default {
|
|||
specialCondition: 'เงื่อนไขพิเศษ',
|
||||
selectInvoice: 'เลือกใบแจ้งหนี้',
|
||||
approveInvoice: 'อนุมัติใบแจ้งหนี้',
|
||||
approveDebitNote: 'อนุมัติใบเพิ่มหนี้',
|
||||
customerAcceptance: 'ลูกค้าตอบรับ',
|
||||
additionalFile: 'ไฟล์เอกสารเพิ่มเติม',
|
||||
|
||||
|
|
@ -890,6 +889,7 @@ export default {
|
|||
caption: 'ใบรายการคำขอทั้งหมด',
|
||||
quotationCode: 'เลขที่ใบเสนอราคา',
|
||||
requestListCode: 'เลขที่ใบรายการคำขอ',
|
||||
referenceNo: 'เลขที่ใบอ้างอิง',
|
||||
invoiceCode: 'เลขที่ใบแจ้งหนี้',
|
||||
receiptCode: 'เลขที่ใบเสร็จ/กำกับภาษี',
|
||||
alienIdCard: 'บัตรประจำตัวต่างด้าว',
|
||||
|
|
@ -1010,6 +1010,7 @@ export default {
|
|||
importWorker: 'นำเข้าคนงาน',
|
||||
confirmLogout: 'ยืนยันการออกจากระบบ',
|
||||
confirmQuotationAccept: 'ยืนยันการตอบรับใบเสนอราคา',
|
||||
confirmDebitNoteAccept: 'ยืนยันการตอบรับใบเพิ่มหนี้',
|
||||
},
|
||||
message: {
|
||||
quotationAccept: 'เมื่อตอบรับเเล้วจะไม่สามารถแก้ไขได้อีก',
|
||||
|
|
@ -1224,6 +1225,7 @@ export default {
|
|||
'ลูกค้าคืนสินค้าทั้งหมดหรือบางส่วน เนื่องจากสินค้าไม่ตรงตามความต้องการ',
|
||||
reasonCanceled:
|
||||
'ลูกค้ายกเลิกคำสั่งซื้อบางรายการหรือบริการที่ระบุในใบแจ้งหนี้',
|
||||
request: 'ขออนุมัติใบลดหนี้',
|
||||
submit: 'อนุมัติใบลดหนี้',
|
||||
refund: 'การคืนเงิน',
|
||||
refundMethod: 'วิธีการคืนเงิน',
|
||||
|
|
@ -1235,6 +1237,7 @@ export default {
|
|||
refundSuccess: 'คืนเงินเสร็จเรียบร้อย',
|
||||
},
|
||||
status: {
|
||||
Waiting: 'ใบลดหนี้',
|
||||
Pending: 'รอคืนเงิน',
|
||||
Success: 'คืนเงินเสร็จสิ้น',
|
||||
Canceled: 'ยกเลิกรายการ',
|
||||
|
|
@ -1244,10 +1247,6 @@ export default {
|
|||
Done: 'คืนเงินเรียบร้อย',
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
Pending: 'รอคืนเงิน',
|
||||
Success: 'คืนเงินเสร็จสิ้น',
|
||||
},
|
||||
},
|
||||
|
||||
invoice: {
|
||||
|
|
@ -1285,6 +1284,7 @@ export default {
|
|||
quotationWorkName: 'ชื่อใบงาน',
|
||||
quotationPayment: 'วิธีการชำระ',
|
||||
value: 'มูลค่าสุทธิ',
|
||||
request: 'ขออนุมัติใบเพิ่มหนี้',
|
||||
submit: 'อนุมัติใบเพิ่มหนี้',
|
||||
},
|
||||
|
||||
|
|
@ -1297,6 +1297,7 @@ export default {
|
|||
},
|
||||
|
||||
viewMode: {
|
||||
accepted: 'ตอบรับ',
|
||||
payment: 'ชำระเงิน',
|
||||
receipt: 'ใบเสร็จรับเงิน/ใบกำกับภาษี',
|
||||
processComplete: 'เสร็จสิ้น',
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ import {
|
|||
SaveButton,
|
||||
EditButton,
|
||||
UndoButton,
|
||||
CloseButton,
|
||||
CancelButton,
|
||||
MainButton,
|
||||
} from 'components/button';
|
||||
import QuotationFormReceipt from './QuotationFormReceipt.vue';
|
||||
|
|
@ -673,6 +673,7 @@ async function triggerSelectEmployeeDialog() {
|
|||
}
|
||||
|
||||
function triggerProductServiceDialog() {
|
||||
covertToNode();
|
||||
pageState.productServiceModal = true;
|
||||
}
|
||||
|
||||
|
|
@ -1067,11 +1068,7 @@ const productServiceNodes = ref<ProductTree>([]);
|
|||
watch(
|
||||
() => productServiceList.value,
|
||||
() => {
|
||||
productServiceNodes.value = quotationProductTree(
|
||||
productServiceList.value,
|
||||
agentPrice.value,
|
||||
config.value?.vat,
|
||||
);
|
||||
covertToNode();
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1302,6 +1299,14 @@ async function formDownload() {
|
|||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
function covertToNode() {
|
||||
productServiceNodes.value = quotationProductTree(
|
||||
productServiceList.value,
|
||||
agentPrice.value,
|
||||
config.value?.vat,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -2233,14 +2238,9 @@ async function formDownload() {
|
|||
</article>
|
||||
|
||||
<footer class="surface-1 q-pa-md full-width">
|
||||
<div
|
||||
class="row full-width"
|
||||
:class="{
|
||||
'justify-between': view !== View.InvoicePre,
|
||||
'justify-end': view === View.InvoicePre,
|
||||
}"
|
||||
>
|
||||
<div class="row full-width justify-end">
|
||||
<MainButton
|
||||
class="q-mr-auto"
|
||||
v-if="
|
||||
view !== View.InvoicePre &&
|
||||
view !== View.PaymentPre &&
|
||||
|
|
@ -2255,7 +2255,16 @@ async function formDownload() {
|
|||
{{ $t('general.view', { msg: $t('general.example') }) }}
|
||||
</MainButton>
|
||||
|
||||
<CancelButton
|
||||
outlined
|
||||
id="btn-close"
|
||||
@click="closeTab()"
|
||||
:label="$t('dialog.action.close')"
|
||||
v-if="quotationFormState.mode === 'info' && closeAble()"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="q-ml-sm"
|
||||
v-if="
|
||||
view === View.Accepted &&
|
||||
quotationFormData.quotationStatus === 'Issued'
|
||||
|
|
@ -2293,6 +2302,7 @@ async function formDownload() {
|
|||
</template>
|
||||
|
||||
<div
|
||||
class="q-ml-sm"
|
||||
v-if="
|
||||
view === View.Invoice &&
|
||||
((quotationFormData.quotationStatus !== 'PaymentPending' &&
|
||||
|
|
@ -2317,7 +2327,7 @@ async function formDownload() {
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="row"
|
||||
class="row q-ml-sm"
|
||||
style="gap: var(--size-2)"
|
||||
v-if="
|
||||
(view === View.Quotation &&
|
||||
|
|
@ -2332,12 +2342,6 @@ async function formDownload() {
|
|||
id="btn-undo"
|
||||
v-if="quotationFormState.mode === 'edit'"
|
||||
/>
|
||||
<CloseButton
|
||||
outlined
|
||||
id="btn-close"
|
||||
@click="closeTab()"
|
||||
v-if="quotationFormState.mode === 'info' && closeAble()"
|
||||
/>
|
||||
<SaveButton
|
||||
type="submit"
|
||||
id="btn-save"
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ function setDefaultFormEmployee() {
|
|||
namePrefix: '',
|
||||
nationality: '',
|
||||
gender: '',
|
||||
dateOfBirth: new Date(),
|
||||
dateOfBirth: null,
|
||||
attachment: [],
|
||||
};
|
||||
|
||||
|
|
@ -261,6 +261,49 @@ async function getWorkerFromCriteria(
|
|||
}
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -445,7 +488,7 @@ watch(() => state.search, getWorkerList);
|
|||
class="text-weight-medium q-mr-md"
|
||||
style="font-size: 18px"
|
||||
>
|
||||
{{ $t('quotation.customer') }}
|
||||
{{ $t('quotation.employee') }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, nextTick, ref, watch } from 'vue';
|
||||
import { precisionRound } from 'src/utils/arithmetic';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import ThaiBahtText from 'thai-baht-text';
|
||||
|
||||
// NOTE: Import stores
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
import { dialogWarningClose, formatNumberDecimal } from 'stores/utils';
|
||||
import { useConfigStore } from 'stores/config';
|
||||
import useBranchStore from 'stores/branch';
|
||||
import { baseUrl } from 'stores/utils';
|
||||
|
|
@ -28,12 +29,14 @@ import ViewHeader from './ViewHeader.vue';
|
|||
import ViewFooter from './ViewFooter.vue';
|
||||
import BankComponents from './BankComponents.vue';
|
||||
import PrintButton from 'src/components/button/PrintButton.vue';
|
||||
import { CancelButton } from 'components/button';
|
||||
import { convertTemplate } from 'src/utils/string-template';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const branchStore = useBranchStore();
|
||||
const customerStore = useCustomerStore();
|
||||
const quotationStore = useQuotationStore();
|
||||
const { t } = useI18n();
|
||||
const { data: config } = storeToRefs(configStore);
|
||||
|
||||
type Product = {
|
||||
|
|
@ -323,6 +326,20 @@ function calcPrice(c: Product) {
|
|||
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, () => {});
|
||||
|
||||
function print() {
|
||||
|
|
@ -333,7 +350,15 @@ function print() {
|
|||
<template>
|
||||
<div class="toolbar">
|
||||
<PrintButton solid @click="print" />
|
||||
<CancelButton
|
||||
outlined
|
||||
id="btn-close"
|
||||
@click="closeTab()"
|
||||
:label="$t('dialog.action.close')"
|
||||
v-if="closeAble()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="row justify-between container color-quotation"
|
||||
:class="{
|
||||
|
|
@ -641,7 +666,7 @@ function print() {
|
|||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import PropertiesExpansion from './PropertiesExpansion.vue';
|
|||
import FormGroupHead from './FormGroupHead.vue';
|
||||
import AvatarGroup from 'src/components/shared/AvatarGroup.vue';
|
||||
import { StateButton } from 'components/button';
|
||||
import { CancelButton } from 'components/button';
|
||||
import DutyExpansion from './DutyExpansion.vue';
|
||||
import MessengerExpansion from './MessengerExpansion.vue';
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ import {
|
|||
dialog,
|
||||
getEmployeeName,
|
||||
getCustomerName,
|
||||
dialogWarningClose,
|
||||
} from 'src/stores/utils';
|
||||
import { dateFormatJS } from 'src/utils/datetime';
|
||||
import { useRequestList } from 'src/stores/request-list';
|
||||
|
|
@ -37,7 +39,7 @@ import ProductExpansion from './ProductExpansion.vue';
|
|||
import { useRoute } from 'vue-router';
|
||||
import { useWorkflowTemplate } from 'src/stores/workflow-template';
|
||||
import { WorkflowTemplate } from 'src/stores/workflow-template/types';
|
||||
import { initLang, initTheme, Lang } from 'src/utils/ui';
|
||||
import { initLang, initTheme } from 'src/utils/ui';
|
||||
import {
|
||||
EmployeePassportPayload,
|
||||
EmployeeVisaPayload,
|
||||
|
|
@ -314,7 +316,7 @@ function goToQuotation(
|
|||
customerBranchId: quotation.customerBranchId,
|
||||
agentPrice: quotation.agentPrice,
|
||||
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');
|
||||
}
|
||||
|
||||
function closeTab() {
|
||||
dialogWarningClose(t, {
|
||||
message: t('dialog.message.close'),
|
||||
action: () => {
|
||||
window.close();
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<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' })
|
||||
"
|
||||
/>
|
||||
|
||||
<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>
|
||||
<FormGroupHead class="col-12">
|
||||
|
|
@ -844,6 +871,15 @@ function goToDebitNote(opt?: { tab?: string; id?: string }) {
|
|||
</div>
|
||||
</section>
|
||||
</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>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ const emit = defineEmits<{
|
|||
|
||||
const props = defineProps<{
|
||||
taskListGroup?: {
|
||||
product: RequestWork['productService']['product'];
|
||||
product:
|
||||
| RequestWork['productService']['product']
|
||||
| RequestWork['productService'];
|
||||
list: (RequestWork & {
|
||||
_template?: {
|
||||
id: string;
|
||||
|
|
@ -135,6 +137,7 @@ async function getList() {
|
|||
}
|
||||
|
||||
function getStep(requestWork: RequestWork) {
|
||||
if (!requestWork.stepStatus) return 0;
|
||||
const target = requestWork.stepStatus.find(
|
||||
(v) =>
|
||||
v.workStatus === RequestWorkStatus.Ready ||
|
||||
|
|
@ -166,7 +169,7 @@ function submit() {
|
|||
requestWorkStep?: Task;
|
||||
}[] = [];
|
||||
selectedEmployee.value.forEach((v, i) => {
|
||||
if (v.stepStatus.length === 0) {
|
||||
if (!v.stepStatus || v.stepStatus.length === 0) {
|
||||
selected.push({
|
||||
step: 0,
|
||||
requestWorkId: v.id || '',
|
||||
|
|
@ -224,7 +227,7 @@ function close() {
|
|||
|
||||
function onDialogOpen() {
|
||||
// assign selected to group
|
||||
!props.creditNote && assignTempGroup();
|
||||
assignTempGroup();
|
||||
|
||||
// match group to check
|
||||
selectedEmployee.value = [];
|
||||
|
|
@ -232,7 +235,7 @@ function onDialogOpen() {
|
|||
const matchingItems = tempGroupEdit.value
|
||||
.flatMap((g) => g.list)
|
||||
.filter((l) => {
|
||||
if (l.stepStatus.length === 0) {
|
||||
if (!l.stepStatus || l.stepStatus.length === 0) {
|
||||
return taskList.value.some(
|
||||
(t) => t.requestWorkStep?.requestWork.id === l.id,
|
||||
);
|
||||
|
|
@ -248,8 +251,12 @@ function onDialogOpen() {
|
|||
function assignTempGroup() {
|
||||
if (!props.taskListGroup) return;
|
||||
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(
|
||||
(g) => g.product.id === newGroup.product.id,
|
||||
(g) => g.product.id === productId,
|
||||
);
|
||||
|
||||
if (existingGroup) {
|
||||
|
|
@ -260,7 +267,9 @@ function assignTempGroup() {
|
|||
});
|
||||
} else {
|
||||
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
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,14 +232,14 @@ function disableCheckAll() {
|
|||
:columns="
|
||||
stepOn
|
||||
? [
|
||||
...employeeColumn.slice(0, 2),
|
||||
...employeeColumn.slice(0, 3),
|
||||
{
|
||||
name: 'periodNo',
|
||||
align: 'center',
|
||||
label: 'flow.step',
|
||||
field: (v) => v.product.code,
|
||||
},
|
||||
...employeeColumn.slice(2),
|
||||
...employeeColumn.slice(3),
|
||||
]
|
||||
: statusOn
|
||||
? [
|
||||
|
|
@ -463,6 +463,16 @@ function disableCheckAll() {
|
|||
{{ props.row.request.code }}
|
||||
</span>
|
||||
</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">
|
||||
<div v-if="props.row._template" class="column text-left">
|
||||
<span>{{ props.row._template.templateName }}</span>
|
||||
|
|
@ -528,14 +538,7 @@ function disableCheckAll() {
|
|||
:expiration-date="new Date(props.row.request.quotation?.dueDate)"
|
||||
/>
|
||||
</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>
|
||||
<BadgeComponent
|
||||
v-if="props.row.request.quotation?.urgent"
|
||||
|
|
|
|||
|
|
@ -170,6 +170,12 @@ export const employeeColumn = [
|
|||
label: 'requestList.requestListCode',
|
||||
field: 'code',
|
||||
},
|
||||
{
|
||||
name: 'quotationCode',
|
||||
align: 'center',
|
||||
label: 'requestList.quotationCode',
|
||||
field: 'quotationCode',
|
||||
},
|
||||
{
|
||||
name: 'fullName',
|
||||
align: 'center',
|
||||
|
|
@ -200,12 +206,6 @@ export const employeeColumn = [
|
|||
label: 'general.numberOfDay',
|
||||
field: 'day',
|
||||
},
|
||||
{
|
||||
name: 'quotationCode',
|
||||
align: 'center',
|
||||
label: 'requestList.quotationCode',
|
||||
field: 'quotationCode',
|
||||
},
|
||||
] as const satisfies QTableProps['columns'];
|
||||
|
||||
export const productColumn = [
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, nextTick, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import ThaiBahtText from 'thai-baht-text';
|
||||
|
||||
// NOTE: Import stores
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
import { dialogWarningClose, formatNumberDecimal } from 'stores/utils';
|
||||
import { useConfigStore } from 'stores/config';
|
||||
import { precisionRound } from 'src/utils/arithmetic';
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ import { precisionRound } from 'src/utils/arithmetic';
|
|||
import { Branch } from 'stores/branch/types';
|
||||
|
||||
// NOTE: Import Components
|
||||
import { CancelButton } from 'components/button';
|
||||
import ViewHeader from './ViewHeader.vue';
|
||||
import ViewFooter from './ViewFooter.vue';
|
||||
import PrintButton from 'src/components/button/PrintButton.vue';
|
||||
|
|
@ -29,6 +31,7 @@ const route = useRoute();
|
|||
const taskOrder = useTaskOrderStore();
|
||||
const configStore = useConfigStore();
|
||||
const config = storeToRefs(configStore).data;
|
||||
const { t } = useI18n();
|
||||
const viewType = ref<'docOrder' | 'docReceive'>('docOrder');
|
||||
|
||||
type Data = TaskOrder;
|
||||
|
|
@ -250,11 +253,32 @@ onMounted(async () => {
|
|||
function print() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
async function closeTab() {
|
||||
dialogWarningClose(t, {
|
||||
message: t('dialog.message.close'),
|
||||
action: () => {
|
||||
window.close();
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
function closeAble() {
|
||||
return window.opener !== null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toolbar">
|
||||
<PrintButton solid @click="print" />
|
||||
<CancelButton
|
||||
outlined
|
||||
id="btn-close"
|
||||
@click="closeTab()"
|
||||
:label="$t('dialog.action.close')"
|
||||
v-if="closeAble()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="row justify-between container"
|
||||
|
|
@ -489,7 +513,7 @@ function print() {
|
|||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
|
|
|
|||
|
|
@ -1209,19 +1209,19 @@ watch(
|
|||
{{ $t('general.view', { msg: $t('general.example') }) }}
|
||||
</MainButton>
|
||||
<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
|
||||
v-if="
|
||||
fullTaskOrder?.taskOrderStatus === TaskOrderStatus.Pending ||
|
||||
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
|
||||
v-if="state.mode && ['create', 'edit'].includes(state.mode)"
|
||||
@click="() => formDocument.submit()"
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ import { storeToRefs } from 'pinia';
|
|||
import { useRoute } from 'vue-router';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { getUserId } from 'src/services/keycloak';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
// NOTE: Import Components
|
||||
import { SaveButton } from 'src/components/button';
|
||||
import { SaveButton, CancelButton } from 'src/components/button';
|
||||
import { StateButton } from 'components/button';
|
||||
import InfoMessengerExpansion from '../expansion/receive/InfoMessengerExpansion.vue';
|
||||
import InfoProductExpansion from '../expansion/receive/InfoProductExpansion.vue';
|
||||
|
|
@ -31,6 +32,7 @@ import { useTaskOrderStore } from 'src/stores/task-order';
|
|||
|
||||
const route = useRoute();
|
||||
const taskOrderFormStore = useTaskOrderForm();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { currentFormData, state, fullTaskOrder } =
|
||||
storeToRefs(taskOrderFormStore);
|
||||
|
|
@ -342,6 +344,16 @@ function sortList(
|
|||
});
|
||||
}
|
||||
|
||||
async function closeTab() {
|
||||
dialogWarningClose(t, {
|
||||
message: t('dialog.message.close'),
|
||||
action: () => {
|
||||
window.close();
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
initTheme();
|
||||
initLang();
|
||||
|
|
@ -756,12 +768,15 @@ watch([currentFormData.value.taskStatus], () => {
|
|||
|
||||
<!-- SEC: footer -->
|
||||
|
||||
<footer
|
||||
v-if="fullTaskOrder.taskOrderStatus !== TaskOrderStatus.Pending"
|
||||
class="surface-1 q-pa-md full-width"
|
||||
>
|
||||
<nav class="row justify-end">
|
||||
<footer class="surface-1 q-pa-md full-width">
|
||||
<nav class="row justify-end" style="gap: var(--size-2)">
|
||||
<CancelButton
|
||||
:label="$t('dialog.action.close')"
|
||||
outlined
|
||||
@click="closeTab()"
|
||||
/>
|
||||
<SaveButton
|
||||
v-if="fullTaskOrder.taskOrderStatus !== TaskOrderStatus.Pending"
|
||||
:disabled="
|
||||
fullTaskOrder.taskList.some(
|
||||
(t) =>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,13 @@ import SelectReadyRequestWork from '../09_task-order/SelectReadyRequestWork.vue'
|
|||
import RefundInformation from './RefundInformation.vue';
|
||||
import QuotationFormReceipt from '../05_quotation/QuotationFormReceipt.vue';
|
||||
import DialogViewFile from 'src/components/dialog/DialogViewFile.vue';
|
||||
import { MainButton, SaveButton } from 'src/components/button';
|
||||
import {
|
||||
MainButton,
|
||||
SaveButton,
|
||||
CancelButton,
|
||||
EditButton,
|
||||
UndoButton,
|
||||
} from 'src/components/button';
|
||||
import { RequestWork } from 'src/stores/request-list/types';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import useOptionStore from 'src/stores/options';
|
||||
|
|
@ -76,15 +82,16 @@ const statusTabForm = ref<
|
|||
}[]
|
||||
>([]);
|
||||
|
||||
const readonly = computed(
|
||||
() =>
|
||||
creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Pending ||
|
||||
creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Success,
|
||||
);
|
||||
// const readonly = computed(
|
||||
// () =>
|
||||
// creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Pending ||
|
||||
// creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Success,
|
||||
// );
|
||||
|
||||
const pageState = reactive({
|
||||
productDialog: false,
|
||||
fileDialog: false,
|
||||
mode: 'view' as 'view' | 'edit' | 'info',
|
||||
});
|
||||
|
||||
const defaultRemark = '#[quotation-labor]<br/><br/>#[quotation-payment]';
|
||||
|
|
@ -161,9 +168,11 @@ async function initStatus() {
|
|||
{
|
||||
title: 'Pending',
|
||||
status: creditNoteData.value?.id
|
||||
? creditNoteData.value.creditNoteStatus === CreditNoteStatus.Success
|
||||
? 'done'
|
||||
: 'doing'
|
||||
? creditNoteData.value.creditNoteStatus === CreditNoteStatus.Waiting
|
||||
? 'waiting'
|
||||
: creditNoteData.value.creditNoteStatus === CreditNoteStatus.Success
|
||||
? 'done'
|
||||
: 'doing'
|
||||
: 'waiting',
|
||||
active: () => view.value === CreditNoteStatus.Pending,
|
||||
handler: async () => {
|
||||
|
|
@ -253,8 +262,9 @@ function assignFormData() {
|
|||
requestWorkStep: {
|
||||
requestWork: {
|
||||
...v,
|
||||
stepStatus: v.stepStatus || [],
|
||||
request: { ...v.request, quotation: current.quotation },
|
||||
},
|
||||
} as RequestWork,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
|
@ -285,12 +295,24 @@ async function getQuotation() {
|
|||
|
||||
async function submit() {
|
||||
const payload = formData.value;
|
||||
|
||||
payload.requestWorkId = formTaskList.value.map((v) => v.requestWorkId);
|
||||
payload.quotationId =
|
||||
typeof route.query['quotationId'] === 'string'
|
||||
? route.query['quotationId']
|
||||
: '';
|
||||
const res = await creditNote.createCreditNote(payload);
|
||||
(pageState.mode === 'edit'
|
||||
? creditNoteData.value?.quotationId
|
||||
: typeof route.query['quotationId'] === 'string'
|
||||
? 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) {
|
||||
await router.push(`/credit-note/${res.id}`);
|
||||
|
|
@ -302,6 +324,7 @@ async function submit() {
|
|||
}
|
||||
|
||||
initStatus();
|
||||
pageState.mode = 'info';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -526,13 +549,31 @@ function storeDataLocal() {
|
|||
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 () => {
|
||||
initTheme();
|
||||
initLang();
|
||||
await useConfigStore().getConfig();
|
||||
await getCreditNote();
|
||||
await getQuotation();
|
||||
creditNoteData.value && (await getFileList(creditNoteData.value.id, true));
|
||||
if (creditNoteData.value) {
|
||||
pageState.mode = 'info';
|
||||
await getFileList(creditNoteData.value.id, true);
|
||||
}
|
||||
initStatus();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -587,7 +628,7 @@ onMounted(async () => {
|
|||
:key="i.title"
|
||||
:label="
|
||||
$t(
|
||||
`creditNote${i.title === 'title' ? '' : '.stats'}.${i.title}`,
|
||||
`creditNote${i.title === 'title' ? '' : '.status'}.${i.title}`,
|
||||
)
|
||||
"
|
||||
:status-active="i.active?.()"
|
||||
|
|
@ -616,7 +657,7 @@ onMounted(async () => {
|
|||
>
|
||||
<CreditNoteExpansion
|
||||
v-if="view === null"
|
||||
:readonly="readonly"
|
||||
:readonly="pageState.mode === 'info'"
|
||||
v-model:reason="formData.reason"
|
||||
v-model:detail="formData.detail"
|
||||
/>
|
||||
|
|
@ -625,7 +666,7 @@ onMounted(async () => {
|
|||
<ProductExpansion
|
||||
v-if="view === null"
|
||||
creditNote
|
||||
:readonly="readonly"
|
||||
:readonly="pageState.mode === 'info'"
|
||||
:agentPrice="quotationData?.agentPrice"
|
||||
:task-list="taskListGroup"
|
||||
@add-product="openProductDialog"
|
||||
|
|
@ -633,7 +674,7 @@ onMounted(async () => {
|
|||
|
||||
<PaymentExpansion
|
||||
v-if="view === null"
|
||||
:readonly="readonly"
|
||||
:readonly="pageState.mode === 'info'"
|
||||
:total-price="summaryPrice.finalPrice"
|
||||
v-model:payback-type="formData.paybackType"
|
||||
v-model:payback-bank="formData.paybackBank"
|
||||
|
|
@ -746,7 +787,7 @@ onMounted(async () => {
|
|||
v-model:remark="formData.remark"
|
||||
:default-remark="defaultRemark"
|
||||
:items="[]"
|
||||
:readonly
|
||||
:readonly="pageState.mode === 'info'"
|
||||
>
|
||||
<template #hint>
|
||||
{{ $t('general.hintRemark') }}
|
||||
|
|
@ -793,7 +834,7 @@ onMounted(async () => {
|
|||
|
||||
<!-- SEC: footer -->
|
||||
<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 -->
|
||||
<MainButton
|
||||
class="q-mr-auto"
|
||||
|
|
@ -804,16 +845,45 @@ onMounted(async () => {
|
|||
>
|
||||
{{ $t('general.view', { msg: $t('general.example') }) }}
|
||||
</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
|
||||
v-if="!readonly"
|
||||
:disabled="taskListGroup.length === 0"
|
||||
v-if="
|
||||
!creditNoteData ||
|
||||
creditNoteData?.creditNoteStatus === CreditNoteStatus.Waiting
|
||||
"
|
||||
:disabled="taskListGroup.length === 0 || pageState.mode === 'edit'"
|
||||
type="submit"
|
||||
@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"
|
||||
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>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
@ -822,6 +892,7 @@ onMounted(async () => {
|
|||
<SelectReadyRequestWork
|
||||
v-if="quotationData"
|
||||
creditNote
|
||||
:task-list-group="taskListGroup"
|
||||
:fetch-params="{ cancelOnly: true, quotationId: quotationData.id }"
|
||||
v-model:open="pageState.productDialog"
|
||||
v-model:task-list="formTaskList"
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const { stats, pageMax, page, data, pageSize } = storeToRefs(creditNote);
|
|||
// NOTE: Variable
|
||||
const pageState = reactive({
|
||||
quotationId: '',
|
||||
currentTab: CreditNoteStatus.Pending,
|
||||
currentTab: CreditNoteStatus.Waiting,
|
||||
hideStat: false,
|
||||
statusFilter: 'None',
|
||||
inputSearch: '',
|
||||
|
|
@ -158,7 +158,7 @@ watch(
|
|||
color: hsl(var(--info-bg));
|
||||
"
|
||||
>
|
||||
{{ stats.Pending + stats.Success || 0 }}
|
||||
{{ Object.values(stats).reduce((sum, value) => sum + value, 0) || 0 }}
|
||||
</q-badge>
|
||||
<q-btn
|
||||
class="q-ml-sm"
|
||||
|
|
@ -180,16 +180,22 @@ watch(
|
|||
<StatCardComponent
|
||||
labelI18n
|
||||
: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',
|
||||
count: stats[CreditNoteStatus.Pending] || 0,
|
||||
label: `creditNote.stats.${CreditNoteStatus.Pending}`,
|
||||
label: `creditNote.status.${CreditNoteStatus.Pending}`,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-check-decagram-outline',
|
||||
count: stats[CreditNoteStatus.Success] || 0,
|
||||
label: `creditNote.stats.${CreditNoteStatus.Success}`,
|
||||
label: `creditNote.status.${CreditNoteStatus.Success}`,
|
||||
color: 'blue',
|
||||
},
|
||||
]"
|
||||
|
|
@ -345,7 +351,7 @@ watch(
|
|||
<TableCreditNote
|
||||
:grid="pageState.gridView"
|
||||
:visible-columns="pageState.fieldSelected"
|
||||
:hide-delete="pageState.currentTab === CreditNoteStatus.Success"
|
||||
:hide-delete="pageState.currentTab !== CreditNoteStatus.Waiting"
|
||||
@view="(v) => navigateTo({ statusDialog: 'info', creditId: v.id })"
|
||||
@delete="(v) => triggerDelete(v.id)"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const taskStatusOpts = [
|
|||
];
|
||||
|
||||
export const pageTabs = [
|
||||
{ label: CreditNoteStatus.Waiting, value: CreditNoteStatus.Waiting },
|
||||
{ label: CreditNoteStatus.Pending, value: CreditNoteStatus.Pending },
|
||||
{ label: CreditNoteStatus.Success, value: CreditNoteStatus.Success },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ import { storeToRefs } from 'pinia';
|
|||
import { onMounted, nextTick, ref, watch } from 'vue';
|
||||
import { precisionRound } from 'src/utils/arithmetic';
|
||||
import ThaiBahtText from 'thai-baht-text';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
// NOTE: Import stores
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
import { dialogWarningClose, formatNumberDecimal } from 'stores/utils';
|
||||
|
||||
import { useConfigStore } from 'stores/config';
|
||||
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 { RequestWork } from 'src/stores/request-list';
|
||||
import { Employee } from 'src/stores/employee/types';
|
||||
import { CancelButton } from 'components/button';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const branchStore = useBranchStore();
|
||||
const customerStore = useCustomerStore();
|
||||
const creditNoteStore = useCreditNote();
|
||||
const { data: config } = storeToRefs(configStore);
|
||||
const { t } = useI18n();
|
||||
|
||||
const agentPrice = ref<boolean>(false);
|
||||
|
||||
|
|
@ -263,11 +266,32 @@ watch(elements, () => {});
|
|||
function print() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
async function closeTab() {
|
||||
dialogWarningClose(t, {
|
||||
message: t('dialog.message.close'),
|
||||
action: () => {
|
||||
window.close();
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
function closeAble() {
|
||||
return window.opener !== null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toolbar">
|
||||
<PrintButton solid @click="print" />
|
||||
<CancelButton
|
||||
outlined
|
||||
id="btn-close"
|
||||
@click="closeTab()"
|
||||
:label="$t('dialog.action.close')"
|
||||
v-if="closeAble()"
|
||||
/>
|
||||
</div>
|
||||
<div class="row justify-between container color-debit-note">
|
||||
<section class="content" v-for="(chunk, i) in chunks" :key="i">
|
||||
|
|
@ -548,7 +572,7 @@ function print() {
|
|||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { dateFormatJS, calculateAge } from 'src/utils/datetime';
|
|||
import { initLang, initTheme, Lang } from 'src/utils/ui';
|
||||
import { useQuotationStore } from 'src/stores/quotations';
|
||||
import { useQuotationForm } from 'src/pages/05_quotation/form';
|
||||
import { useDebitNoteForm } from './form';
|
||||
import { useReceipt, usePayment } from 'stores/payment';
|
||||
import useProductServiceStore from 'stores/product-service';
|
||||
import {
|
||||
|
|
@ -40,16 +41,17 @@ import {
|
|||
SaveButton,
|
||||
UndoButton,
|
||||
EditButton,
|
||||
CancelButton,
|
||||
} from 'src/components/button';
|
||||
import { RequestWork } from 'src/stores/request-list/types';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import useOptionStore from 'src/stores/options';
|
||||
import { deleteItem, dialogWarningClose } from 'src/stores/utils';
|
||||
import { deleteItem, dialog, dialogWarningClose } from 'src/stores/utils';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Employee } from 'src/stores/employee/types';
|
||||
import QuotationFormWorkerSelect from '../05_quotation/QuotationFormWorkerSelect.vue';
|
||||
import { watch } from 'vue';
|
||||
import { ProductTree } from '../05_quotation/utils';
|
||||
import { ProductTree, quotationProductTree } from '../05_quotation/utils';
|
||||
import TableRequest from '../05_quotation/TableRequest.vue';
|
||||
import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
|
||||
import { useRequestList } from 'src/stores/request-list';
|
||||
|
|
@ -63,6 +65,7 @@ import { precisionRound } from 'src/utils/arithmetic';
|
|||
import QuotationFormProductSelect from '../05_quotation/QuotationFormProductSelect.vue';
|
||||
import { getName } from 'src/services/keycloak';
|
||||
|
||||
const debitNoteForm = useDebitNoteForm();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const debitNote = useDebitNote();
|
||||
|
|
@ -76,6 +79,7 @@ const $q = useQuasar();
|
|||
const paymentStore = usePayment();
|
||||
const { data: config } = storeToRefs(configStore);
|
||||
const { newWorkerList } = storeToRefs(quotationForm);
|
||||
const { currentFormData } = storeToRefs(debitNoteForm);
|
||||
const { t, locale } = useI18n();
|
||||
const requestStore = useRequestList();
|
||||
|
||||
|
|
@ -101,9 +105,10 @@ const productGroup = ref<ProductGroup[]>([]);
|
|||
const agentPrice = ref(false);
|
||||
const debitNoteData = ref<DebitNote>();
|
||||
const quotationData = ref<DebitNote['debitNoteQuotation']>();
|
||||
const view = ref<QuotationStatus | null>(null);
|
||||
const view = ref<QuotationStatus>(QuotationStatus.Issued);
|
||||
const fileList = ref<FileList>();
|
||||
const receiptList = ref<Receipt[]>([]);
|
||||
let previousValue: DebitNotePayload | undefined = undefined;
|
||||
|
||||
const rowsRequestList = ref<RequestData[]>([]);
|
||||
|
||||
|
|
@ -112,20 +117,6 @@ const productServiceList = ref<
|
|||
>([]);
|
||||
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 tempPaySplit = ref<
|
||||
{ no: number; amount: number; name?: string; invoice?: boolean }[]
|
||||
|
|
@ -258,11 +249,27 @@ const selectedInstallmentNo = ref<number[]>([]);
|
|||
const installmentAmount = ref<number>(0);
|
||||
|
||||
const QUOTATION_STATUS = [
|
||||
'Accepted',
|
||||
'PaymentPending',
|
||||
'PaymentSuccess',
|
||||
'ProcessComplete',
|
||||
];
|
||||
|
||||
function covertToNode() {
|
||||
productServiceNodes.value = quotationProductTree(
|
||||
productServiceList.value,
|
||||
agentPrice.value,
|
||||
config.value?.vat,
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => productServiceList.value,
|
||||
() => {
|
||||
covertToNode();
|
||||
},
|
||||
);
|
||||
|
||||
function toggleDeleteProduct(index: number) {
|
||||
// product display
|
||||
productServiceList.value.splice(index, 1);
|
||||
|
|
@ -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) {
|
||||
const data = await debitNote.getDebitNote(id);
|
||||
if (!data) return;
|
||||
|
|
@ -333,7 +380,7 @@ async function assignFormData(id: string) {
|
|||
selectedProductGroup.value =
|
||||
data.productServiceList[0]?.product.productGroup?.id || '';
|
||||
|
||||
currentFormData.value = {
|
||||
(previousValue = {
|
||||
id: data.id || undefined,
|
||||
debitNoteQuotationId: data.debitNoteQuotationId || undefined,
|
||||
productServiceList: structuredClone(
|
||||
|
|
@ -358,24 +405,11 @@ async function assignFormData(id: string) {
|
|||
agentPrice: data.agentPrice,
|
||||
quotationId: data.debitNoteQuotationId,
|
||||
remark: data.remark || undefined,
|
||||
};
|
||||
}),
|
||||
(currentFormData.value = structuredClone(previousValue));
|
||||
|
||||
productServiceList.value = data.productServiceList.map((v, i) => ({
|
||||
id: v.id!,
|
||||
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);
|
||||
assignProductServiceList();
|
||||
assignSelectedWorker();
|
||||
await getQuotation(data.debitNoteQuotation?.id);
|
||||
await assignToProductServiceList();
|
||||
await fetchRequest();
|
||||
|
|
@ -419,16 +453,29 @@ async function initStatus() {
|
|||
{
|
||||
title: 'title',
|
||||
status: debitNoteData.value?.id !== undefined ? 'done' : 'doing',
|
||||
active: () => view.value === null,
|
||||
active: () => view.value === QuotationStatus.Issued,
|
||||
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',
|
||||
status:
|
||||
debitNoteData.value?.id !== undefined
|
||||
? getStatus(debitNoteData.value.quotationStatus, 1, -1)
|
||||
? getStatus(debitNoteData.value.quotationStatus, 2, 1)
|
||||
: 'waiting',
|
||||
active: () => view.value === QuotationStatus.PaymentPending,
|
||||
handler: async () => {
|
||||
|
|
@ -438,18 +485,20 @@ async function initStatus() {
|
|||
|
||||
{
|
||||
title: 'receipt',
|
||||
status: getStatus(debitNoteData.value?.quotationStatus, 1, 1),
|
||||
status: getStatus(debitNoteData.value?.quotationStatus, 2, 2),
|
||||
active: () => view.value === QuotationStatus.PaymentSuccess,
|
||||
handler: () => {
|
||||
pageState.mode = 'info';
|
||||
view.value = QuotationStatus.PaymentSuccess;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: 'processComplete',
|
||||
status: getStatus(debitNoteData.value?.quotationStatus, 2, 1),
|
||||
status: getStatus(debitNoteData.value?.quotationStatus, 3, 2),
|
||||
active: () => view.value === QuotationStatus.ProcessComplete,
|
||||
handler: () => {
|
||||
pageState.mode = 'info';
|
||||
view.value = QuotationStatus.ProcessComplete;
|
||||
},
|
||||
},
|
||||
|
|
@ -778,6 +827,7 @@ async function submit() {
|
|||
: await debitNote.createDebitNote(payload);
|
||||
|
||||
if (res) {
|
||||
newWorkerList.value = [];
|
||||
await router.push(`/debit-note/${res.id}/?mode=info`);
|
||||
|
||||
if (attachmentList.value && pageState.mode === 'create') {
|
||||
|
|
@ -785,7 +835,6 @@ async function submit() {
|
|||
}
|
||||
|
||||
pageState.mode = 'info';
|
||||
|
||||
assignFormData(res.id);
|
||||
|
||||
initStatus();
|
||||
|
|
@ -849,6 +898,20 @@ function storeDataLocal() {
|
|||
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(
|
||||
() => pageState.mode,
|
||||
() => toggleMode(pageState.mode),
|
||||
|
|
@ -876,7 +939,8 @@ onMounted(async () => {
|
|||
}
|
||||
|
||||
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') {
|
||||
|
|
@ -884,11 +948,40 @@ onMounted(async () => {
|
|||
{
|
||||
payment: QuotationStatus.PaymentPending,
|
||||
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();
|
||||
});
|
||||
|
||||
async function submitAccepted() {
|
||||
dialog({
|
||||
color: 'info',
|
||||
icon: 'mdi-account-check',
|
||||
title: t('dialog.title.confirmDebitNoteAccept'),
|
||||
actionText: t('general.confirm'),
|
||||
persistent: true,
|
||||
message: t('dialog.message.quotationAccept'),
|
||||
action: async () => {
|
||||
if (!currentFormData.value.id) return;
|
||||
|
||||
const res = await debitNote.action.acceptDebitNote(
|
||||
currentFormData.value.id,
|
||||
);
|
||||
|
||||
if (res && typeof route.params['id'] === 'string') {
|
||||
await assignFormData(route.params['id']);
|
||||
}
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -953,7 +1046,10 @@ onMounted(async () => {
|
|||
<!-- #TODO add goToQuotation as @goto-quotation-->
|
||||
|
||||
<DocumentExpansion
|
||||
v-if="view === null"
|
||||
v-if="
|
||||
view === QuotationStatus.Issued ||
|
||||
view === QuotationStatus.Accepted
|
||||
"
|
||||
:readonly
|
||||
:registered-branch-id="quotationData?.registeredBranchId"
|
||||
:customer-id="quotationData?.customerBranchId"
|
||||
|
|
@ -984,7 +1080,10 @@ onMounted(async () => {
|
|||
/>
|
||||
|
||||
<WorkerItemExpansion
|
||||
v-if="view === null"
|
||||
v-if="
|
||||
view === QuotationStatus.Issued ||
|
||||
view === QuotationStatus.Accepted
|
||||
"
|
||||
:readonly
|
||||
:hide-btn-add-worker="
|
||||
!readonly &&
|
||||
|
|
@ -997,14 +1096,22 @@ onMounted(async () => {
|
|||
|
||||
<!-- #TODO add openProductDialog at @add-product-->
|
||||
<ProductExpansion
|
||||
v-if="view === null"
|
||||
v-if="
|
||||
view === QuotationStatus.Issued ||
|
||||
view === QuotationStatus.Accepted
|
||||
"
|
||||
:readonly
|
||||
:installment-input="currentFormData.payCondition === 'SplitCustom'"
|
||||
:max-installment="currentFormData.paySplitCount"
|
||||
:agent-price="agentPrice"
|
||||
:employee-rows="selectedWorkerItem"
|
||||
:rows="productService"
|
||||
@add-product="() => (pageState.productServiceModal = true)"
|
||||
@add-product="
|
||||
() => {
|
||||
pageState.productServiceModal = true;
|
||||
covertToNode();
|
||||
}
|
||||
"
|
||||
@update-rows="
|
||||
(v) => {
|
||||
view === null && (productServiceList = v);
|
||||
|
|
@ -1015,7 +1122,10 @@ onMounted(async () => {
|
|||
/>
|
||||
|
||||
<PaymentExpansion
|
||||
v-if="view === null"
|
||||
v-if="
|
||||
view === QuotationStatus.Issued ||
|
||||
view === QuotationStatus.Accepted
|
||||
"
|
||||
readonly
|
||||
:total-price="summaryPrice.finalPrice"
|
||||
class="q-mb-md"
|
||||
|
|
@ -1029,7 +1139,11 @@ onMounted(async () => {
|
|||
|
||||
<!-- TODO: bind additional file -->
|
||||
<AdditionalFileExpansion
|
||||
v-if="view === null || view === QuotationStatus.PaymentPending"
|
||||
v-if="
|
||||
view === QuotationStatus.Issued ||
|
||||
view === QuotationStatus.Accepted ||
|
||||
view === QuotationStatus.PaymentPending
|
||||
"
|
||||
:readonly
|
||||
v-model:file-data="attachmentData"
|
||||
:transform-url="
|
||||
|
|
@ -1074,7 +1188,11 @@ onMounted(async () => {
|
|||
|
||||
<!-- TODO: bind remark -->
|
||||
<RemarkExpansion
|
||||
v-if="view === null || view === QuotationStatus.PaymentPending"
|
||||
v-if="
|
||||
view === QuotationStatus.Issued ||
|
||||
view === QuotationStatus.Accepted ||
|
||||
view === QuotationStatus.PaymentPending
|
||||
"
|
||||
:readonly="readonly"
|
||||
v-model:remark="currentFormData.remark"
|
||||
/>
|
||||
|
|
@ -1149,7 +1267,7 @@ onMounted(async () => {
|
|||
installmentAmount = v.invoice.amount;
|
||||
}
|
||||
|
||||
view = null;
|
||||
view = QuotationStatus.Issued;
|
||||
}
|
||||
"
|
||||
@example="() => exampleReceipt(v.id)"
|
||||
|
|
@ -1173,38 +1291,59 @@ onMounted(async () => {
|
|||
{{ $t('general.view', { msg: $t('general.example') }) }}
|
||||
</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
|
||||
outlined
|
||||
@click="
|
||||
() => {
|
||||
pageState.mode = 'info';
|
||||
}
|
||||
"
|
||||
v-if="false"
|
||||
v-if="pageState.mode === 'edit'"
|
||||
@click="undo()"
|
||||
/>
|
||||
|
||||
<SaveButton
|
||||
v-if="!readonly && pageState.mode === 'create'"
|
||||
v-if="pageState.mode !== 'info'"
|
||||
:disabled="
|
||||
selectedWorkerItem.length === 0 && productService.length === 0
|
||||
"
|
||||
@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
|
||||
/>
|
||||
|
||||
<template v-if="debitNoteData">
|
||||
<EditButton
|
||||
v-if="
|
||||
pageState.mode === 'info' &&
|
||||
view === QuotationStatus.Issued &&
|
||||
debitNoteData.quotationStatus === QuotationStatus.Issued
|
||||
"
|
||||
class="no-print"
|
||||
solid
|
||||
@click="pageState.mode = 'edit'"
|
||||
/>
|
||||
|
||||
<MainButton
|
||||
v-if="
|
||||
view === QuotationStatus.Accepted &&
|
||||
debitNoteData.quotationStatus === QuotationStatus.Issued
|
||||
"
|
||||
solid
|
||||
icon="mdi-account-multiple-check-outline"
|
||||
color="207 96% 32%"
|
||||
id="btn-submit-accepted"
|
||||
@click="
|
||||
() => {
|
||||
submitAccepted();
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ $t('quotation.customerAcceptance') }}
|
||||
</MainButton>
|
||||
</template>
|
||||
</div>
|
||||
</nav>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ import { storeToRefs } from 'pinia';
|
|||
import { onMounted, nextTick, ref, watch } from 'vue';
|
||||
import { precisionRound } from 'src/utils/arithmetic';
|
||||
import ThaiBahtText from 'thai-baht-text';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
// NOTE: Import stores
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
import { dialogWarningClose, formatNumberDecimal } from 'stores/utils';
|
||||
|
||||
import { useConfigStore } from 'stores/config';
|
||||
import useBranchStore from 'stores/branch';
|
||||
|
|
@ -29,6 +30,7 @@ import ViewHeader from './ViewHeader.vue';
|
|||
import ViewFooter from './ViewFooter.vue';
|
||||
import BankComponents from './BankComponents.vue';
|
||||
import PrintButton from 'src/components/button/PrintButton.vue';
|
||||
import { CancelButton } from 'components/button';
|
||||
import { convertTemplate } from 'src/utils/string-template';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
|
|
@ -36,6 +38,7 @@ const branchStore = useBranchStore();
|
|||
const customerStore = useCustomerStore();
|
||||
const debitNoteStore = useDebitNote();
|
||||
const { data: config } = storeToRefs(configStore);
|
||||
const { t } = useI18n();
|
||||
|
||||
type Product = {
|
||||
id: string;
|
||||
|
|
@ -272,6 +275,20 @@ onMounted(async () => {
|
|||
|
||||
watch(elements, () => {});
|
||||
|
||||
async function closeTab() {
|
||||
dialogWarningClose(t, {
|
||||
message: t('dialog.message.close'),
|
||||
action: () => {
|
||||
window.close();
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
function closeAble() {
|
||||
return window.opener !== null;
|
||||
}
|
||||
|
||||
function print() {
|
||||
window.print();
|
||||
}
|
||||
|
|
@ -280,6 +297,13 @@ function print() {
|
|||
<template>
|
||||
<div class="toolbar">
|
||||
<PrintButton solid @click="print" />
|
||||
<CancelButton
|
||||
outlined
|
||||
id="btn-close"
|
||||
@click="closeTab()"
|
||||
:label="$t('dialog.action.close')"
|
||||
v-if="closeAble()"
|
||||
/>
|
||||
</div>
|
||||
<div class="row justify-between container color-debit-note">
|
||||
<section class="content" v-for="chunk in chunks">
|
||||
|
|
@ -553,7 +577,7 @@ function print() {
|
|||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const toggleWorker = defineModel<boolean>('toggleWorker');
|
|||
</div>
|
||||
<nav class="q-ml-auto">
|
||||
<AddButton
|
||||
v-if="!hideBtnAddWorker"
|
||||
v-if="!readonly"
|
||||
icon-only
|
||||
@click.stop="$emit('addWorker')"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -14,10 +14,9 @@ const DEFAULT_DATA: DebitNotePayload = {
|
|||
debitNoteQuotationId: '',
|
||||
worker: [],
|
||||
payBillDate: new Date(),
|
||||
paySplit: [],
|
||||
paySplitCount: 0,
|
||||
payCondition: PayCondition.Full,
|
||||
dueDate: new Date(),
|
||||
dueDate: new Date(Date.now() + 86400000),
|
||||
discount: 0,
|
||||
status: 'CREATED',
|
||||
remark: '#[quotation-labor]<br/><br/>#[quotation-payment]',
|
||||
|
|
@ -58,6 +57,7 @@ export const useDebitNoteForm = defineStore('form-debit-note', () => {
|
|||
}
|
||||
|
||||
return {
|
||||
currentFormData,
|
||||
isFormDataDifferent,
|
||||
resetForm,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -71,12 +71,19 @@ export async function updatePaybackStatus(
|
|||
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', () => {
|
||||
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.Waiting]: 0,
|
||||
[Status.Pending]: 0,
|
||||
[Status.Success]: 0,
|
||||
});
|
||||
|
|
@ -94,6 +101,7 @@ export const useCreditNote = defineStore('credit-note-store', () => {
|
|||
createCreditNote,
|
||||
updateCreditNote,
|
||||
deleteCreditNote,
|
||||
acceptCreditNote,
|
||||
|
||||
...manageAttachment(api, ENDPOINT),
|
||||
...manageFile<'slip'>(api, ENDPOINT),
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export type CreditNote = {
|
|||
};
|
||||
|
||||
export enum CreditNoteStatus {
|
||||
Waiting = 'Waiting',
|
||||
Pending = 'Pending',
|
||||
Success = 'Success',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,12 @@ export async function deleteDebitNote(id: string) {
|
|||
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', () => {
|
||||
const data = ref<Data[]>([]);
|
||||
const page = ref<number>(1);
|
||||
|
|
@ -87,6 +93,8 @@ export const useDebitNote = defineStore('debit-note-store', () => {
|
|||
updateDebitNote,
|
||||
deleteDebitNote,
|
||||
|
||||
action: { acceptDebitNote },
|
||||
|
||||
...manageAttachment(api, ENDPOINT),
|
||||
...manageFile<'slip'>(api, ENDPOINT),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export const useQuotationStore = defineStore('quotation-store', () => {
|
|||
hasCancel?: boolean;
|
||||
includeRegisteredBranch?: boolean;
|
||||
forDebitNote?: boolean;
|
||||
cancelIncludeDebitNote?: boolean;
|
||||
}) {
|
||||
const res = await api.get<PaginationResult<Quotation>>('/quotation', {
|
||||
params,
|
||||
|
|
|
|||
|
|
@ -343,6 +343,7 @@ export type QuotationFull = {
|
|||
updatedBy: UpdatedBy;
|
||||
|
||||
agentPrice?: boolean;
|
||||
isDebitNote?: boolean;
|
||||
};
|
||||
|
||||
export type QuotationPayload = {
|
||||
|
|
@ -387,7 +388,7 @@ export type EmployeeWorker = {
|
|||
namePrefix: string;
|
||||
nationality: string;
|
||||
gender: string;
|
||||
dateOfBirth: Date;
|
||||
dateOfBirth: Date | null;
|
||||
};
|
||||
|
||||
export type ProductGroup = {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,12 @@ export type RequestData = {
|
|||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
quotation: QuotationFull & { isDebitNote: boolean };
|
||||
quotation: QuotationFull & {
|
||||
debitNoteQuotationId: string;
|
||||
isDebitNote: boolean;
|
||||
debitNoteQuotation: { code: string };
|
||||
};
|
||||
|
||||
quotationId: string;
|
||||
|
||||
flow: Record<string, any>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue