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"
:label="$t('general.select', { msg: $t('quotation.title') })"
:params="{
cancelIncludeDebitNote: true,
hasCancel: true,
}"
/>

View file

@ -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',

View file

@ -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: 'เสร็จสิ้น',

View file

@ -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"

View file

@ -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>

View file

@ -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;

View file

@ -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>

View file

@ -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
});
}

View file

@ -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"

View file

@ -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 = [

View file

@ -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;

View file

@ -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()"

View file

@ -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) =>

View file

@ -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"

View file

@ -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)"
>

View file

@ -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 },
];

View file

@ -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;

View file

@ -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>

View file

@ -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;

View file

@ -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')"
/>

View file

@ -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,
};

View file

@ -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),

View file

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

View file

@ -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),
};

View file

@ -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,

View file

@ -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 = {

View file

@ -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>;