Compare commits

...

91 commits

Author SHA1 Message Date
HAM
65dcd138db fix: creditNote column number wrong running
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2026-01-12 15:25:32 +07:00
net
e6d06b39da refactor: id missing test
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-12-16 17:07:28 +07:00
net
3cab6cc0e5 refactor: handle btn save
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-12-16 10:28:24 +07:00
JakkrapartXD
9994366c74 feat: Add multi-language user name display to the profile menu.
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-12-04 16:44:09 +07:00
Aif
f4db5ad855 feat: required branch
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-11-12 11:34:21 +07:00
Aif
15a812b50e feat: Refresh quotation list on window focus
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-11-12 11:27:55 +07:00
Aif
f7a8416e7a feat: Refresh task order list on window focus
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-11-12 11:10:21 +07:00
Aif
79d6482caa Add unique id and for attributes to status elements
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
Introduces dynamic id and for attributes to readonly status, dropdown, menu items, and failed remark button for improved accessibility and testability in TaskStatusComponent.vue.
2025-11-11 15:02:36 +07:00
Aif
75d5c7dfe8 feat: unique id attributes to UI components
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-11-11 11:01:36 +07:00
Aif
637eeab3c2 feat: unique IDs to UI components for testing
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-11-11 10:02:13 +07:00
net
2b1e3b12a4 refacotr: add id
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-11-07 16:24:10 +07:00
Aif
a1ed625d32 feat: for
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-20 11:24:28 +07:00
Aif
59a3f964c4 fix: Adjust the badge count to reflect the current tab
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-10-20 10:11:24 +07:00
Aif
2afb5ea7e9 feat: fetch data
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-17 10:11:25 +07:00
net
aaf776639d refactor: hide btn
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-16 14:17:58 +07:00
net
04c463a717 refactor: handle i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-15 10:24:07 +07:00
net
8e65a1c5a2 refactor: handle i18n #236
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-10-15 10:04:56 +07:00
net
21fc2d5d96 refacotr: bind value
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 18:03:59 +07:00
net
73f43c2a29 refactor: date en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-14 17:57:05 +07:00
net
16ea66484d refactor: add i18n #229
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-14 17:51:05 +07:00
net
90f31a0c87 refactor: update stats #234
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 17:25:51 +07:00
net
d1785faed2 refactor: update stats
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 17:20:10 +07:00
net
f68e8cf675 refactor: add i18n error
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 17:16:35 +07:00
net
cd4b087fec refactor: hide filter
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-14 16:50:14 +07:00
Aif
8a0340f588 feat: i18n payment condition en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-14 16:04:33 +07:00
net
2b9c8aa613 refacotr: clear value #230
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 15:47:53 +07:00
Aif
eebd585554 feat: i18n branch name en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 15:28:17 +07:00
Aif
db5262da42 feat: i18n register name en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 15:17:13 +07:00
Aif
72b0e89642 feat: i18n register name en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 14:57:58 +07:00
Aif
2320883cb6 feat: i18n register name en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 14:50:18 +07:00
Aif
0e6bee7b62 feat: i18n registerNameEN
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 14:46:12 +07:00
net
5c867a496d fix: #242
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-14 14:45:41 +07:00
Aif
d06c26c3c8 feat: i18n registerName
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 14:40:21 +07:00
Aif
c4f088c5cb feat: i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-14 14:30:08 +07:00
net
6cf8cf28aa refactor: #245 handle i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 13:32:30 +07:00
puriphatt
1249f67a0f Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-25 10:18:48 +07:00
puriphatt
05d38b1ab3 chore: remove duplicate 2025-09-25 10:18:31 +07:00
Thanaphon Frappet
d09484a52a refactor: get contact number and name 2025-09-25 10:15:12 +07:00
puriphatt
d8d02a679d fix: quotation payment date
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-25 10:14:22 +07:00
Methapon2001
11047e569d Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-09-22 08:56:46 +07:00
puriphatt
f3b5b25bf3 refactor: quotation payment method select bank from register branch
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-19 14:43:15 +07:00
Methapon2001
e817e8fd05 Merge branch 'develop' 2025-09-19 10:02:10 +07:00
puriphatt
18e5517325 fix: submit payment method
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-09-19 09:55:21 +07:00
Methapon2001
5e13864d4a Merge branch 'develop' 2025-09-19 09:13:06 +07:00
net
61dca12e5a fix: no data
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-18 17:34:23 +07:00
net
aa908f0c3d fix: show summary price
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-18 16:41:41 +07:00
net
80056f8e0b fix: search date product
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-18 15:39:25 +07:00
Methapon2001
02bb682150 Merge branch 'develop' 2025-09-18 14:27:56 +07:00
net
d2acd6ba4c refactor: export product
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-09-18 13:21:11 +07:00
net
d53eb15a88 fix: new worker
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-18 11:13:35 +07:00
Methapon2001
35e23aa291 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-09-18 10:29:45 +07:00
puriphatt
00f9b5f4c4 fix: customer filter and export
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-18 10:25:23 +07:00
Methapon2001
7846950802 Merge branch 'develop' 2025-09-18 10:09:37 +07:00
puriphatt
ef8e294ae4 Merge remote-tracking branch 'forgejo/refactor/customer' into develop
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-18 09:59:59 +07:00
net
016a54e45e fix: label 2025-09-18 09:59:59 +07:00
puriphatt
4e887fdff8 refactor: customer, employee upload profile after created 2025-09-18 09:59:59 +07:00
net
ded56d103b refactor: handle show discount 2025-09-18 09:59:59 +07:00
puriphatt
0d708405f6 refactor: drawer employee name 2025-09-18 09:59:59 +07:00
net
2099031fa8 refactor: add icon 2025-09-18 09:59:59 +07:00
puriphatt
fd12f32ab0 fix: customer & employee image list 2025-09-18 09:59:59 +07:00
net
f10103b5d0 fix: label 2025-09-18 09:59:59 +07:00
puriphatt
d6e366f788 feat: add customer export by status 2025-09-18 09:59:59 +07:00
net
52c384f0fb fix: create sub branch 2025-09-18 09:59:59 +07:00
puriphatt
fcafaeebc0 fix: change employee status with drawer alert 2025-09-18 09:59:59 +07:00
Methapon2001
0e57a3daf6 fix: .01 error 2025-09-18 09:59:59 +07:00
puriphatt
0ad017309f fix: drawer before close and employee edit state 2025-09-18 09:59:59 +07:00
net
b9cfb6274b fix: label 2025-09-18 09:59:59 +07:00
puriphatt
5becbae369 feat: filter customer by business type, address 2025-09-18 09:59:59 +07:00
net
7c3a9818c2 fix: calc price order 2025-09-18 09:59:59 +07:00
net
56f0a86845 fix: create sub branch 2025-09-18 09:59:59 +07:00
net
492f341e68 fix: current branch missing 2025-09-18 09:59:59 +07:00
puriphatt
49897ff007 fix: image selection 2025-09-18 09:59:59 +07:00
net
c430b6082e fix: calc price 2025-09-18 09:59:59 +07:00
puriphatt
e6f8870cdf refactor: drawer employee 2025-09-18 09:59:59 +07:00
Methapon2001
67cde37e34 fix: price calc on update installments 2025-09-18 09:59:59 +07:00
puriphatt
763ac07be7 refactor: customer & employee 2025-09-18 09:59:59 +07:00
Methapon2001
c07efa7318 fix: type error 2025-09-18 09:59:59 +07:00
puriphatt
507141dca5 fix: component 2025-09-18 09:59:59 +07:00
net
73b2d52fb0 fix: price calc 2025-09-18 09:59:59 +07:00
puriphatt
e6ecd39d24 feat: resize img 2025-09-18 09:59:59 +07:00
Methapon2001
c0a2d3769d fix: margin error .01 2025-09-18 09:59:59 +07:00
Methapon2001
05f7c886d6 refactor: use switch case instead 2025-09-18 09:59:59 +07:00
Methapon2001
93c54c0dd1 fix: remark 2025-09-18 09:59:59 +07:00
Methapon2001
ef4c84341c fix: price calc 2025-09-18 09:58:29 +07:00
net
09b51d601e refactor: can create new type 2025-09-18 09:58:29 +07:00
net
c29e1d4ec5 fix: select customer 2025-09-18 09:58:29 +07:00
puriphatt
d89925dee9 feat: quotation payment method 2025-09-18 09:58:29 +07:00
Thanaphon Frappet
3e24a46f66 feat: add customer export 2025-09-18 09:58:29 +07:00
Thanaphon Frappet
764d9bab3f fix: get contact number and name 2025-09-18 09:58:29 +07:00
Thanaphon Frappet
e5b2114984 fix: show remark
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-22 11:45:25 +07:00
Thanaphon Frappet
37c9f5fcd5 fix: edit label
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-22 11:25:51 +07:00
104 changed files with 8628 additions and 6903 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Before After
Before After

View file

@ -293,15 +293,11 @@ watch(
:readonly="readonly"
:label="$t('form.birthDate')"
:disabled-dates="disabledAfterToday"
:rules="
employee
? []
: [
(val: string) =>
!!val ||
$t('form.error.selectField', { field: $t('form.birthDate') }),
]
"
:rules="[
(val: string) =>
!!val ||
$t('form.error.selectField', { field: $t('form.birthDate') }),
]"
/>
<q-input

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -20,8 +20,8 @@ const issuePlace = defineModel<string>('issuePlace');
const issueCountry = defineModel<string>('issueCountry');
const issueDate = defineModel<Date | null | string>('issueDate');
const type = defineModel<string>('type');
const expireDate = defineModel<Date>('expireDate');
const birthDate = defineModel<Date>('birthDate');
const expireDate = defineModel<Date | string>('expireDate');
const birthDate = defineModel<Date | string>('birthDate');
const workerStatus = defineModel<string>('workerStatus');
const nationality = defineModel<string>('nationality');
const gender = defineModel<string>('gender');

View file

@ -28,12 +28,12 @@ const arrivalAt = defineModel<string>('arrivalAt');
const arrivalTMNo = defineModel<string>('arrivalTmNo');
const arrivalTM = defineModel<string>('arrivalTm');
const mrz = defineModel<string>('mrz');
const entryCount = defineModel<number>('entryCount');
const entryCount = defineModel<number | string>('entryCount');
const issuePlace = defineModel<string>('issuePlace');
const issueCountry = defineModel<string>('issueCountry');
const issueDate = defineModel<Date | null | string>('visaIssueDate');
const type = defineModel<string>('type');
const expireDate = defineModel<Date>('expireDate');
const expireDate = defineModel<Date | string>('expireDate');
const remark = defineModel<string>('remark');
const workerType = defineModel<string>('workerType');
const number = defineModel<string>('number');

View file

@ -141,8 +141,9 @@ defineEmits<{
<q-avatar size="md">
<q-img
:src="
`${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}` ||
`/images/employee-avatar-${props.row.gender}.png`
props.row.selectedImage
? `${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}`
: `/images/employee-avatar-${props.row.gender}.png`
"
class="text-center"
:ratio="1"
@ -295,9 +296,9 @@ defineEmits<{
$i18n.locale === 'eng'
? `${props.row.firstNameEN} ${props.row.lastNameEN} `.trim()
: `${props.row.firstName} ${props.row.lastName} `.trim(),
img:
`${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}` ||
`/images/employee-avatar-${props.row.gender}.png`,
img: props.row.selectedImage
? `${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}`
: `/images/employee-avatar-${props.row.gender}.png`,
fallbackImg: `/images/employee-avatar-${props.row.gender}.png`,
male: props.row.gender === 'male',
female: props.row.gender === 'female',

View file

@ -2,11 +2,18 @@
import SelectCustomer from '../shared/select/SelectCustomer.vue';
import SelectBranch from '../shared/select/SelectBranch.vue';
import { CustomerBranch } from 'src/stores/customer';
import { ref } from 'vue';
const branchId = defineModel<string>('branchId');
const customerBranchId = defineModel<string>('customerBranchId');
const agentPrice = defineModel<boolean>('agentPrice');
const special = defineModel<boolean>('special');
const customerBranchOption = defineModel<CustomerBranch>(
'customerBranchOption',
);
defineProps<{
outlined?: boolean;
readonly?: boolean;
@ -67,10 +74,12 @@ defineEmits<{
required
:readonly
/>
<SelectCustomer
id="about-select-customer-branch-id"
for="about-select-customer-branch-id"
v-model:value="customerBranchId"
v-model:value-option="customerBranchOption"
:label="$t('quotation.customer')"
:creatable-disabled-text="`(${$t('form.error.selectField', {
field: $t('quotation.branchVirtual'),

View file

@ -58,16 +58,15 @@ const currentBtnOpen = ref<{ title: string; opened: boolean[] }[]>([
function calcPrice(c: (typeof rows.value)[number]) {
const originalPrice = c.pricePerUnit;
const finalPriceWithVat = precisionRound(
originalPrice + originalPrice * (config.value?.vat || 0.07),
const finalPricePerUnit = precisionRound(
originalPrice +
(c.product[props.agentPrice ? 'agentPriceCalcVat' : 'calcVat']
? originalPrice * (config.value?.vat || 0.07)
: 0),
);
const finalPriceNoVat = finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const price = finalPricePerUnit * c.amount - c.discount;
const price = finalPriceNoVat * c.amount - c.discount;
const vat = c.product[props.agentPrice ? 'agentPriceCalcVat' : 'calcVat']
? (finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07)
: 0;
return precisionRound(price + vat);
return precisionRound(price);
}
const discount4Show = ref<string[]>([]);
@ -435,8 +434,20 @@ watch(
<q-td align="right">
{{
formatNumberDecimal(
props.row.pricePerUnit * props.row.amount -
props.row.discount,
props.row.product[
agentPrice ? 'agentPriceCalcVat' : 'calcVat'
]
? precisionRound(
(props.row.pricePerUnit *
(1 + (config?.vat || 0.07)) *
props.row.amount -
props.row.discount) /
(1 + (config?.vat || 0.07)),
)
: precisionRound(
props.row.pricePerUnit * props.row.amount -
props.row.discount,
),
2,
)
}}
@ -448,9 +459,12 @@ watch(
agentPrice ? 'agentPriceCalcVat' : 'calcVat'
]
? precisionRound(
(props.row.pricePerUnit * props.row.amount -
props.row.discount) *
(config?.vat || 0.07),
((props.row.pricePerUnit *
(1 + (config?.vat || 0.07)) *
props.row.amount -
props.row.discount) /
(1 + (config?.vat || 0.07))) *
0.07,
)
: 0,
2,

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { QTableProps } from 'quasar';
import { dateFormat } from 'src/utils/datetime';
import { dateFormat, dateFormatJS } from 'src/utils/datetime';
import { formatNumberDecimal } from 'stores/utils';
@ -86,11 +86,11 @@ defineEmits<{
</q-td>
<q-td v-if="visibleColumns.includes('createdAt')">
{{ dateFormat(props.row.createdAt) }}
{{ dateFormatJS({ date: props.row.createdAt }) }}
</q-td>
<q-td v-if="visibleColumns.includes('dueDate')">
{{ dateFormat(props.row.dueDate) }}
{{ dateFormatJS({ date: props.row.dueDate }) }}
</q-td>
<q-td v-if="visibleColumns.includes('contactName')">

View file

@ -27,26 +27,38 @@ withDefaults(
class="app-text-muted q-pr-sm"
:width="iconSize || '2rem'"
/>
<span class="row col">
<span class="col-12 app-text-muted-2" style="font-size: 10px">
<span :id="`dd-wrapper-${label}`" class="row col">
<span
:id="`dd-label-${label}`"
class="col-12 app-text-muted-2"
style="font-size: 10px"
>
{{ label }}
</span>
<span class="col-12 ellipsis">
<span :id="`dd-value-wrapper-${label}`" class="col-12 ellipsis">
<slot name="value">
<span
:class="{ 'link cursor-pointer': clickable }"
v-if="typeof value === 'string'"
@click="clickable ? $emit('labelClick', value, null) : undefined"
:id="`link-${value}`"
:for="`link-${value}`"
>
{{ value }}
<q-tooltip v-if="tooltip" :delay="500">{{ value }}</q-tooltip>
</span>
<span v-else :class="{ 'link cursor-pointer': clickable }">
<span
:id="`dd-value-${label}`"
v-else
:class="{ 'link cursor-pointer': clickable }"
>
<span
v-for="(item, index) in value"
:key="index"
@click="$emit('labelClick', item, index)"
class="link cursor-pointer"
:id="`link-${item}`"
:for="`link-${item}`"
>
{{ item }}
<span v-if="index < value.length - 1">,&nbsp;</span>

View file

@ -8,7 +8,7 @@ import {
UndoButton,
} from 'components/button';
withDefaults(
const props = withDefaults(
defineProps<{
title: string;
category?: string;
@ -42,6 +42,11 @@ const drawerOpen = defineModel<boolean>('drawerOpen', {
const myForm = ref();
function reset() {
if (props.beforeClose) {
drawerOpen.value = props.beforeClose
? props.beforeClose()
: !drawerOpen.value;
}
if (myForm.value) {
myForm.value.resetValidation();
}
@ -62,7 +67,6 @@ async function onValidationError(ref: any) {
@show="show"
@before-hide="reset"
@hide="close"
@update:model-value="(v) => (drawerOpen = beforeClose ? beforeClose() : v)"
:width="$q.screen.gt.xs ? windowSize * 0.85 : windowSize"
v-model="drawerOpen"
behavior="mobile"

View file

@ -96,7 +96,9 @@ defineEmits<{
expandedTree[expandedTree.length - 1] === node.id,
}"
>
{{ node.name }}
{{
$i18n.locale === 'eng' ? node.nameEN || node.name : node.name
}}
</span>
<span class="app-text-muted text-caption ellipsis">
{{ node.code }}

View file

@ -5,6 +5,7 @@ defineEmits<{
(e: 'click', v: MouseEvent): void;
}>();
defineProps<{
id?: string;
icon?: string;
color: string;
iconOnly?: boolean;
@ -18,6 +19,7 @@ defineProps<{
<template>
<button
:id="id"
@click="(e) => $emit('click', e)"
class="main-btn"
:class="{

View file

@ -10,6 +10,7 @@ defineProps<{
outlined?: boolean;
disabled?: boolean;
dark?: boolean;
color?: string;
label?: string;
icon?: string;
@ -23,7 +24,7 @@ defineProps<{
@click="(e) => $emit('click', e)"
v-bind="{ ...$props, ...$attrs }"
:icon="icon || 'mdi-content-save-outline'"
color="207 96% 32%"
:color="color || '207 96% 32%'"
:title="iconOnly ? $t('general.save') : undefined"
>
{{ label || $t('general.save') }}

View file

@ -16,3 +16,4 @@ export { default as SideMenu } from './SideMenu.vue';
export { default as StatCardComponent } from './StatCardComponent.vue';
export { default as TooltipComponent } from './TooltipComponent.vue';
export { default as TreeComponent } from './TreeComponent.vue';
export { default as PaginationPageSize } from './PaginationPageSize.vue';

View file

@ -13,6 +13,7 @@ let defaultFilter: (
const props = withDefaults(
defineProps<{
prefix?: string;
id?: string;
label?: string;
option: T[];
@ -71,6 +72,7 @@ watch(
</script>
<template>
<q-select
:id="id"
:placeholder="placeholder"
outlined
:clearable

View file

@ -75,9 +75,9 @@ function setDefaultValue() {
</script>
<template>
<SelectInput
for="select-hq-id"
v-model="value"
incremental
id="select-hq-id"
:label
:placeholder
:readonly

View file

@ -158,6 +158,39 @@ function setDefaultValue() {
<q-separator class="q-mx-sm" />
</template>
<template #before-options v-if="creatable">
<q-item
:disable="creatableDisabled"
clickable
v-close-popup
@click.stop="$emit('create')"
for="select-business-type-add-new"
id="select-business-type-add-new"
>
<q-item-section>
<span class="row items-center">
<q-icon
name="mdi-plus-circle-outline"
class="q-mr-sm"
style="color: hsl(var(--positive-bg))"
/>
<b>
{{ $t('general.add', { text: $t('menu.manage.businessType') }) }}
</b>
<span
v-if="creatableDisabled && creatableDisabledText"
class="app-text-muted q-pl-xs"
style="font-size: 80%"
>
{{ creatableDisabledText }}
</span>
</span>
</q-item-section>
</q-item>
<q-separator class="q-mx-sm" />
</template>
<template #option="{ opt, scope }">
<q-item v-bind="scope.itemProps">
<span class="row items-center">

View file

@ -30,6 +30,7 @@ defineEmits<{
type ExclusiveProps = {
simple?: boolean;
simpleBranchNo?: boolean;
selectFirstValue?: boolean;
};
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
@ -64,10 +65,14 @@ onMounted(async () => {
setFirstValue();
}
await getSelectedOption();
valueOption.value = selectOptions.value.find((v) => v.id === value.value);
if (props.selectFirstValue) {
setDefaultValue();
} else await getSelectedOption();
});
function setDefaultValue() {
setFirstValue();
}
</script>
<template>
<SelectInput
@ -160,11 +165,9 @@ onMounted(async () => {
</template>
<template #option="{ opt, scope }">
<q-item @click="valueOption = opt" v-bind="scope.itemProps">
<q-item v-bind="scope.itemProps" class="q-mx-sm bodrder">
<SelectCustomerItem :data="opt" :simple :simple-branch-no />
</q-item>
<q-separator class="q-mx-sm" />
</template>
<template #append v-if="clearable">
@ -177,3 +180,11 @@ onMounted(async () => {
</template>
</SelectInput>
</template>
<style scoped>
.bodrder {
border-bottom: solid;
border-bottom-width: 1px;
border-color: var(--border-color);
}
</style>

View file

@ -26,6 +26,7 @@ defineEmits<{
type ExclusiveProps = {
selectFirstValue?: boolean;
prefix?: string;
};
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
@ -71,6 +72,7 @@ function setDefaultValue() {
<SelectInput
v-model="value"
incremental
:id="`${prefix || 'nome'}-select-user`"
:label
:placeholder
:readonly

View file

@ -145,6 +145,7 @@ function selectedIndex(item: Employee) {
<template v-if="col.name === '#check'">
<q-checkbox
id="select-worker-all"
for="select-worker-all"
v-model="props.selected"
@update:model-value="(v) => handleUpdate()"
size="sm"
@ -200,6 +201,7 @@ function selectedIndex(item: Employee) {
v-model="props.selected"
size="sm"
:id="`select-worker-${props.row.firstName}`"
:for="`select-worker-${props.row.firstName}`"
/>
</template>
</q-td>

View file

@ -54,10 +54,11 @@ onMounted(() => {
@click="$emit('click')"
>
<q-icon :name="icon" size="lg" :style="`color: ${color}`" />
<article class="col column q-pl-md">
<span class="ellipsis full-width">
<div class="col column q-pl-md">
<div class="ellipsis full-width" style="max-width: 65vw !important">
{{ name }}
</span>
</div>
<span class="text-caption app-text-muted-2">
{{
uploading.loaded
@ -79,7 +80,7 @@ onMounted(() => {
/>
{{ idle ? `Pending` : progress !== 1 ? `Uploading...` : 'Completed' }}
</span>
</article>
</div>
<q-btn
v-if="closeable"
icon="mdi-close"

View file

@ -45,9 +45,9 @@ const props = withDefaults(
readonly?: boolean;
showTitle?: boolean;
ocr?: (
group: any,
group: string,
file: File,
) => void | Promise<{
) => Promise<{
status: boolean;
group: string;
meta: { name: string; value: string }[];

View file

@ -198,3 +198,10 @@ i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-i
.q-focus-helper {
visibility: hidden;
}
.clear-btn {
opacity: 0.6;
&:hover {
opacity: 1;
}
}

View file

@ -161,6 +161,7 @@ export default {
documentStatus: 'Document Status',
advanceSearch: 'Advance Search',
totalPeople: '{meg} people',
price: 'Price {price} Baht',
},
menu: {
@ -507,6 +508,7 @@ export default {
miss: 'MISS.',
},
taxpayyerNo: 'Taxpayer Identification Number',
citizenId: 'Citizen ID',
religion: 'Religion',
issueDate: 'Issue Date',
@ -777,6 +779,8 @@ export default {
seller: 'Seller',
paymentChannels: 'Payment Channels',
channelsThat: 'Channels That',
refNo: 'Reference Number',
bankAccount: 'Bank Account',
bankAccountNumber: 'Bank Account Number',
bankAccountName: 'Bank Account Name',
inTheNameOf: 'In The Name Of',
@ -1230,6 +1234,9 @@ export default {
taskListNotPending: 'One or more task is not pending.',
reqNotMet: 'Not Match',
systemError: 'A system error occurred.',
taskOrderInvalid: 'Please select the product and the organization.',
flowAccountProductIdNotFound: 'Product not found in flow account',
},
},

View file

@ -161,6 +161,7 @@ export default {
documentStatus: 'สถานะเอกสาร',
advanceSearch: 'ค้นหาขั้นสูง',
totalPeople: '{meg} คน',
price: 'ราคา {price} บาท',
},
menu: {
@ -508,6 +509,7 @@ export default {
religion: 'ศาสนา',
issueDate: 'วันที่ออกหนังสือ',
passportExpiryDate: 'วันหiมดอายุหนังสือเดินทาง',
taxpayyerNo: 'เลขที่ประจำตัวผู้เสียภาษี',
ownerName: 'ชื่อนายจ้าง',
firstName: 'ชื่อ ',
@ -775,6 +777,8 @@ export default {
seller: 'ผู้ขาย',
paymentChannels: 'ช่องทางชำระเงิน',
channelsThat: 'ช่องทางที่',
refNo: 'เลขที่อ้างอิง',
bankAccount: 'บัญชีธนาคาร',
bankAccountNumber: 'เลขบัญชีธนาคาร',
bankAccountName: 'ชื่อบัญชี',
inTheNameOf: 'ในนาม',
@ -1216,6 +1220,8 @@ export default {
'มีงานหนึ่งงานหรือมากกว่าที่ไม่อยู่ในสถานะรอดำเนินการ',
reqNotMet: 'ไม่ตรงกัน',
systemError: 'ระบบเกิดข้อผิดพลาด',
taskOrderInvalid: 'โปรดเลือก สินค้าเเละสาขา',
flowAccountProductIdNotFound: 'ไม่พบสินค้าใน flow account',
},
},

View file

@ -2,7 +2,7 @@
import { ref, onMounted, computed, reactive } from 'vue';
import { storeToRefs } from 'pinia';
import { useQuasar } from 'quasar';
import { getUserId, getUsername, logout, getRole } from 'src/services/keycloak';
import { getUserId, getUsername, getName, logout, getRole } from 'src/services/keycloak';
import { Icon } from '@iconify/vue';
import { useI18n } from 'vue-i18n';
import moment from 'moment';
@ -39,7 +39,7 @@ const configStore = useConfigStore();
const { data: notificationData } = storeToRefs(notificationStore);
const { visible } = storeToRefs(loaderStore);
const { t } = useI18n({ useScope: 'global' });
const { t, locale } = useI18n({ useScope: 'global' });
const userStore = useUserStore();
const canvasModal = ref(false);
@ -52,8 +52,14 @@ const unread = computed<number>(
);
const userImage = ref<string>();
const userGender = ref('');
const userName = ref({ th: '', en: '' });
const canvasRef = ref();
const displayName = computed(() => {
if (!userName.value.th && !userName.value.en) return getName() || 'Guest';
return locale.value === 'eng' ? userName.value.en : userName.value.th;
});
const language: {
value: Lang;
label: string;
@ -161,9 +167,14 @@ onMounted(async () => {
if (user === 'admin') return;
if (uid) {
const res = await userStore.fetchById(uid);
if (res && res.gender) {
userGender.value = res.gender;
userImage.value = `${baseUrl}/user/${uid}/profile-image/${res.selectedImage}`;
if (res) {
if (res.gender) {
userGender.value = res.gender;
userImage.value = `${baseUrl}/user/${uid}/profile-image/${res.selectedImage}`;
}
//
userName.value.th = `${res.firstName || ''} ${res.lastName || ''}`.trim();
userName.value.en = `${res.firstNameEN || ''} ${res.lastNameEN || ''}`.trim();
}
}
});
@ -484,6 +495,7 @@ onMounted(async () => {
<!-- User -->
<ProfileMenu
id="btn-profile-menu"
:user-name="displayName"
@logout="doLogout"
@edit-personal-info="console.log('edit')"
@signature="

View file

@ -12,6 +12,7 @@ const filterRole = ref<string[]>();
defineProps<{
userImage?: string;
gender?: string;
userName?: string;
}>();
const inputFile = document.createElement('input');
@ -147,9 +148,9 @@ onMounted(async () => {
class="text-weight-bold ellipsis"
style="max-width: 9vw"
>
{{ getName() }}
{{ userName || getName() }}
<q-tooltip>
{{ getName() }}
{{ userName || getName() }}
</q-tooltip>
</span>
<span
@ -234,12 +235,12 @@ onMounted(async () => {
style="margin-top: 58px"
>
<span v-if="isLoggedIn()">
{{ getName() }}
{{ userName || getName() }}
</span>
<span v-else>{{ 'Guest' }}</span>
<q-tooltip>
<span v-if="isLoggedIn()">
{{ getName() }}
{{ userName || getName() }}
</span>
<span v-else>{{ 'Guest' }}</span>
</q-tooltip>

View file

@ -18,6 +18,8 @@ import { CustomerBranch, CustomerType } from 'stores/customer/types';
import { columnsEmployee } from './constant';
import { useCustomerBranchForm, useEmployeeForm } from './form';
import DialogEmployee from 'src/components/03_customer-management/DialogEmployee.vue';
import DrawerEmployee from 'src/components/03_customer-management/DrawerEmployee.vue';
import EmployerFormAuthorized from './components/employer/EmployerFormAuthorized.vue';
import FloatingActionButton from 'components/FloatingActionButton.vue';
import SideMenu from 'components/SideMenu.vue';
@ -89,6 +91,11 @@ const prop = withDefaults(
currentCitizenId?: string;
gender: string;
selectedImage: string;
fetchImageList: (
id: string,
selectedName: string,
type: 'customer' | 'employee',
) => Promise<void>;
}>(),
{
color: 'green',
@ -96,7 +103,6 @@ const prop = withDefaults(
);
const currentBranchEmployee = ref<string>('');
const listEmployee = ref<Employee[]>([]);
const customerId = defineModel<string>('customerId', { required: true });
defineEmits<{
@ -106,16 +112,6 @@ defineEmits<{
(e: 'dialog'): void;
}>();
onMounted(async () => {
customerBranchFormState.value.currentCustomerId = route.params
.customerId as string;
await fetchList();
branch.value?.forEach((v) => {
currentBtnOpen.value.push(false);
});
});
const columns = [
{
name: 'branchName',
@ -257,10 +253,6 @@ async function fetchEmployee(opts: { branchId: string; pageSize?: number }) {
}
}
onMounted(async () => {
await fetchList();
});
watch([customerId, inputSearch, currentStatus, pageSizeBranch], async () => {
await fetchList();
});
@ -280,6 +272,16 @@ watch(
}
},
);
onMounted(async () => {
customerBranchFormState.value.currentCustomerId = route.params
.customerId as string;
await fetchList();
branch.value?.forEach((v) => {
currentBtnOpen.value.push(false);
});
});
</script>
<template>
@ -473,7 +475,6 @@ watch(
<q-tr
:class="{
'app-text-muted': props.row.status === 'INACTIVE',
'cursor-pointer': props.row._count?.branch !== 0,
}"
:props="props"
@click="$emit('viewDetail', props.row, props.rowIndex)"
@ -547,7 +548,13 @@ watch(
v-if="branchFieldSelected.includes('businessTypePure')"
class="text-left"
>
{{ useOptionStore().mapOption(props.row.businessType) || '-' }}
{{
props.row.businessType
? props.row.businessType[
$i18n.locale === 'eng' ? 'nameEN' : 'name'
]
: '-'
}}
</q-td>
<q-td
v-if="branchFieldSelected.includes('totalEmployee')"
@ -566,8 +573,6 @@ watch(
await fetchEmployee({
branchId: currentBranchEmployee,
pageSize: 999,
passport: true,
visa: true,
});
currentBtnOpen.map((v, i) => {
@ -638,10 +643,15 @@ watch(
"
@history="(item) => {}"
@view="
(item) => {
async (item) => {
employeeFormState.drawerModal = true;
//employeeFormState.isEmployeeEdit = true;
employeeFormStore.assignFormDataEmployee(item.id);
await fetchImageList(
item.id,
item.selectedImage || '',
'employee',
);
}
"
/>
@ -668,9 +678,11 @@ watch(
? `${props.row.addressEN || ''} ${props.row.subDistrict?.nameEN || ''} ${props.row.district?.nameEN || ''} ${props.row.province?.nameEN || ''}`
: `${props.row.address || ''} ${props.row.subDistrict?.name || ''} ${props.row.district?.name || ''} ${props.row.province?.name || ''}`,
telephone: props.row.telephoneNo,
businessTypePure: useOptionStore().mapOption(
props.row.businessType,
),
businessTypePure: props.row.businessType
? props.row.businessType[
$i18n.locale === 'eng' ? 'nameEN' : 'name'
]
: '-',
totalEmployee: props.row._count?.employee,
}"
:visible-columns="branchFieldSelected"
@ -885,15 +897,14 @@ watch(
</div>
<EmployerFormBusiness
dense
class="q-mb-xl"
outlined
prefix-id="employer-branch"
:readonly="customerBranchFormState.dialogType === 'info'"
v-model:bussiness-type="customerBranchFormData.businessType"
v-model:business-type-id="customerBranchFormData.businessTypeId"
v-model:job-position="customerBranchFormData.jobPosition"
v-model:job-description="customerBranchFormData.jobDescription"
v-model:pay-date="customerBranchFormData.payDate"
v-model:pay-date-e-n="customerBranchFormData.payDateEN"
v-model:pay-date-en="customerBranchFormData.payDateEN"
v-model:wage-rate="customerBranchFormData.wageRate"
v-model:wage-rate-text="customerBranchFormData.wageRateText"
/>
@ -984,7 +995,7 @@ watch(
v-model:email="customerBranchFormData.email"
v-model:contact-tel="customerBranchFormData.contactTel"
v-model:office-tel="customerBranchFormData.officeTel"
v-model:agent="customerBranchFormData.agent"
v-model:agent-user-id="customerBranchFormData.agentUserId"
/>
</div>
</div>
@ -1000,6 +1011,18 @@ watch(
/>
</template>
</DialogFormContainer>
<DialogEmployee
:fetch-list-employee="fetchEmployee"
:fetch-image-list="fetchImageList"
current-tab="employer"
/>
<DrawerEmployee
:fetch-list-employee="fetchEmployee"
:fetch-image-list="fetchImageList"
current-tab="employer"
/>
</template>
<style scoped>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,514 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import {
PaginationComponent,
PaginationPageSize,
ImageUploadDialog,
DialogForm,
NoData,
} from 'src/components';
import DialogEmployee from 'src/components/03_customer-management/DialogEmployee.vue';
import HistoryEditComponent from 'src/components/03_customer-management/HistoryEditComponent.vue';
import TableEmpoloyee from 'src/components/03_customer-management/TableEmpoloyee.vue';
import DrawerEmployee from 'src/components/03_customer-management/DrawerEmployee.vue';
import useFlowStore from 'src/stores/flow';
import useEmployeeStore from 'src/stores/employee';
import { Status } from 'src/stores/types';
import { Employee, EmployeeHistory } from 'src/stores/employee/types';
import { baseUrl, dialog, canAccess } from 'src/stores/utils';
import { calculateAge, toISOStringWithTimezone } from 'src/utils/datetime';
import { useCustomerForm, useEmployeeForm } from './form';
import { columnsEmployee } from './constant';
const $q = useQuasar();
const flowStore = useFlowStore();
const employeeStore = useEmployeeStore();
const employeeFormStore = useEmployeeForm();
const customerFormStore = useCustomerForm();
const { t } = useI18n();
const { state: employeeFormState, currentFromDataEmployee } =
storeToRefs(employeeFormStore);
const { deleteEmployeeById } = employeeFormStore;
const { state: customerFormState } = storeToRefs(customerFormStore);
const employeeStats = defineModel('employeeStats', { default: 0 });
const props = defineProps<{
currentTab: 'employee' | 'employer';
currentStatus: Status | 'All';
gridView: boolean;
inputSearch: string;
searchDate: string[];
fieldSelected: string[];
fetchImageList: (
id: string,
selectedName: string,
type: 'customer' | 'employee',
) => Promise<void>;
triggerChangeStatus: (
id: string,
status: string,
employeeName?: string,
) => void;
}>();
defineExpose({
openSpecificEmployee,
fetchListEmployee,
toggleStatusEmployee,
});
const listEmployee = ref<Employee[]>([]);
const currentPageEmployee = ref<number>(1);
const maxPageEmployee = ref<number>(1);
const pageSize = ref<number>(30);
const employeeHistoryDialog = ref(false);
const employeeHistory = ref<EmployeeHistory[]>();
const splitPercent = computed(() => ($q.screen.lt.md ? 0 : 15));
async function fetchListEmployee(opt?: {
fetchStats?: boolean;
page?: number;
pageSize?: number;
customerId?: string;
mobileFetch?: boolean;
}) {
const resultListEmployee = await employeeStore.fetchList({
customerId: opt?.customerId,
page: opt
? opt.mobileFetch
? 1
: opt.page || currentPageEmployee.value
: currentPageEmployee.value,
pageSize: opt
? opt.mobileFetch
? listEmployee.value.length +
(employeeStats.value === listEmployee.value.length ? 1 : 0)
: opt.pageSize || pageSize.value
: pageSize.value,
status:
props.currentStatus === 'All'
? undefined
: props.currentStatus === 'ACTIVE'
? 'ACTIVE'
: 'INACTIVE',
query: props.inputSearch,
passport: true,
visa: true,
startDate: props.searchDate[0],
endDate: props.searchDate[1],
});
if (resultListEmployee) {
maxPageEmployee.value = Math.ceil(
resultListEmployee.total / pageSize.value,
);
$q.screen.xs && !(opt && opt.mobileFetch)
? listEmployee.value.push(...resultListEmployee.result)
: (listEmployee.value = resultListEmployee.result);
}
if (opt && opt.fetchStats)
employeeStats.value = await employeeStore.getStatsEmployee();
}
async function openHistory(id: string) {
const res = await employeeStore.getEditHistory(id);
employeeHistory.value = res.reverse();
employeeHistoryDialog.value = true;
}
async function editEmployeeFormPersonal(id: string) {
await employeeFormStore.assignFormDataEmployee(id);
await props.fetchImageList(
id,
currentFromDataEmployee.value.selectedImage || '',
'employee',
);
employeeFormState.value.isEmployeeEdit = true;
employeeFormState.value.dialogType = 'edit';
employeeFormState.value.drawerModal = true;
}
async function openSpecificEmployee(id: string) {
await employeeFormStore.assignFormDataEmployee(id);
await props.fetchImageList(
id,
currentFromDataEmployee.value.selectedImage || '',
'employee',
);
employeeFormState.value.dialogType = 'info';
employeeFormState.value.drawerModal = true;
}
async function toggleStatusEmployee(
id: string,
status: boolean,
employeeName: string,
) {
const res = await employeeStore.editById(id, {
status: !status ? 'ACTIVE' : 'INACTIVE',
firstNameEN: employeeName,
});
if (res && employeeFormState.value.drawerModal) {
currentFromDataEmployee.value.status = res.status;
}
await employeeFormStore.assignFormDataEmployee(id);
await fetchListEmployee({ mobileFetch: $q.screen.xs });
flowStore.rotate();
}
watch(
() => [
props.inputSearch,
props.searchDate,
props.currentStatus,
pageSize.value,
],
async () => {
currentPageEmployee.value = 1;
listEmployee.value = [];
await fetchListEmployee({ fetchStats: true });
customerFormState.value.currentCustomerId = undefined;
flowStore.rotate();
},
);
watch(
() => employeeFormState.value.currentCustomerBranch,
(e) => {
if (!e) return;
if (employeeFormState.value.formDataEmployeeSameAddr) {
currentFromDataEmployee.value.address = e.address;
currentFromDataEmployee.value.addressEN = e.addressEN;
currentFromDataEmployee.value.provinceId = e.provinceId;
currentFromDataEmployee.value.districtId = e.districtId;
currentFromDataEmployee.value.subDistrictId = e.subDistrictId;
}
currentFromDataEmployee.value.customerBranchId = e.id;
},
);
watch(
() => employeeFormState.value.formDataEmployeeSameAddr,
(isSame) => {
if (!employeeFormState.value.currentCustomerBranch) return;
if (isSame) {
currentFromDataEmployee.value.address =
employeeFormState.value.currentCustomerBranch.address;
currentFromDataEmployee.value.addressEN =
employeeFormState.value.currentCustomerBranch.addressEN;
currentFromDataEmployee.value.provinceId =
employeeFormState.value.currentCustomerBranch.provinceId;
currentFromDataEmployee.value.districtId =
employeeFormState.value.currentCustomerBranch.districtId;
currentFromDataEmployee.value.subDistrictId =
employeeFormState.value.currentCustomerBranch.subDistrictId;
}
currentFromDataEmployee.value.customerBranchId =
employeeFormState.value.currentCustomerBranch.id;
},
);
watch(
() => currentFromDataEmployee.value.dateOfBirth,
(v) => {
const isEdit =
employeeFormState.value.drawerModal &&
employeeFormState.value.isEmployeeEdit;
let currentFormDate = v && toISOStringWithTimezone(new Date(v));
let currentDate: string = '';
if (isEdit && employeeFormState.value.currentEmployee) {
currentDate = toISOStringWithTimezone(
new Date(employeeFormState.value.currentEmployee.dateOfBirth),
);
}
if (
employeeFormState.value.dialogModal ||
(isEdit && currentFormDate !== currentDate)
) {
const age = calculateAge(
currentFromDataEmployee.value.dateOfBirth,
'year',
);
if (currentFromDataEmployee.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 () => {
currentFromDataEmployee.value.dateOfBirth = null;
return;
},
});
}
if (
currentFromDataEmployee.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 () => {
currentFromDataEmployee.value.dateOfBirth = null;
return;
},
});
}
}
},
);
watch(
() => currentFromDataEmployee.value.image,
() => {
if (currentFromDataEmployee.value.image !== null)
employeeFormState.value.isImageEdit = true;
},
);
onMounted(async () => {
currentPageEmployee.value = 1;
listEmployee.value = [];
await fetchListEmployee({ fetchStats: true });
});
</script>
<template>
<q-splitter
v-model="splitPercent"
:limits="[0, 100]"
class="col full-width"
before-class="overflow-hidden"
after-class="overflow-hidden"
:disable="$q.screen.lt.sm"
>
<template v-slot:before>
<div
class="column q-pa-md surface-1 full-height full-width"
style="gap: var(--size-1)"
>
<q-item
active
dense
active-class="employer-active"
class="no-padding items-center rounded full-width"
v-close-popup
clickable
>
<span class="q-px-md ellipsis">
{{ $t('general.all') }}
</span>
</q-item>
</div>
</template>
<template v-slot:after>
<div class="column full-height no-wrap">
<!-- employee -->
<template
v-if="listEmployee && employeeStats > 0 && currentTab === 'employee'"
>
<div
v-if="listEmployee.length === 0"
class="row col full-width items-center justify-center"
style="min-height: 250px"
>
<NoData :not-found="!!inputSearch" />
</div>
<article
v-if="listEmployee.length !== 0"
class="column scroll q-pa-md col"
>
<q-infinite-scroll
:offset="10"
@load="
(_, done) => {
if (
$q.screen.gt.xs ||
currentPageEmployee === maxPageEmployee
)
return;
currentPageEmployee = currentPageEmployee + 1;
fetchListEmployee().then(() =>
done(currentPageEmployee >= maxPageEmployee),
);
}
"
>
<TableEmpoloyee
:hide-delete="!canAccess('customer', 'edit')"
v-model:page-size="pageSize"
v-model:current-page="currentPageEmployee"
:grid-view="gridView"
:list-employee="listEmployee"
:columns-employee="columnsEmployee"
:field-selected="fieldSelected"
@history="
(item: any) => {
openHistory(item.id);
}
"
@view="
async (item: any) => {
employeeFormState.drawerModal = true;
employeeFormState.isEmployeeEdit = false;
employeeFormStore.assignFormDataEmployee(item.id);
await fetchImageList(
item.id,
item.selectedImage || '',
'employee',
);
}
"
@edit="(item: any) => editEmployeeFormPersonal(item.id)"
@delete="
(item: any) => {
deleteEmployeeById({
id: item.id,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId: customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
});
}
"
@toggle-status="
async (item: any) => {
triggerChangeStatus(item.id, item.status, item.firstNameEN);
}
"
/>
<template v-slot:loading>
<div
v-if="
$q.screen.lt.sm && currentPageEmployee !== maxPageEmployee
"
class="row justify-center"
>
<q-spinner-dots color="primary" size="40px" />
</div>
</template>
</q-infinite-scroll>
</article>
<footer
v-if="listEmployee.length !== 0 && $q.screen.gt.xs"
class="row justify-between items-center q-px-md q-py-sm"
>
<div class="row col-4 items-center">
<div
class="app-text-muted"
style="width: 80px"
v-if="$q.screen.gt.sm"
>
{{ $t('general.recordPerPage') }}
</div>
<div><PaginationPageSize v-model="pageSize" /></div>
</div>
<div class="col-4 flex justify-center app-text-muted">
{{
$q.screen.gt.sm
? $t('general.recordsPage', {
resultcurrentPage: listEmployee.length,
total: employeeStats,
})
: $t('general.ofPage', {
current: listEmployee.length,
total: employeeStats,
})
}}
</div>
<div class="col-4 flex justify-end">
<PaginationComponent
v-model:current-page="currentPageEmployee"
v-model:max-page="maxPageEmployee"
:fetch-data="
async () => {
await fetchListEmployee();
flowStore.rotate();
}
"
/>
</div>
</footer>
</template>
</div>
</template>
</q-splitter>
<!-- add employee -->
<DialogEmployee
:fetch-list-employee="fetchListEmployee"
:fetch-image-list="fetchImageList"
:current-tab="currentTab"
/>
<!-- กจาง edit employee -->
<DrawerEmployee
:fetch-list-employee="fetchListEmployee"
:fetch-image-list="fetchImageList"
:current-tab="currentTab"
@change-status="
(s) =>
triggerChangeStatus(
currentFromDataEmployee.id,
s,
currentFromDataEmployee.firstNameEN,
)
"
/>
<DialogForm
:title="$t('general.historyEdit')"
hide-footer
v-model:modal="employeeHistoryDialog"
>
<div class="q-pa-md">
<HistoryEditComponent
v-if="employeeHistory"
v-model:history-list="employeeHistory"
/>
</div>
</DialogForm>
</template>
<style scoped>
.employer-active {
background-color: hsla(var(--info-bg) / 0.1);
color: hsl(var(--info-bg));
}
</style>

View file

@ -336,6 +336,7 @@ watch(
(v) => (typeof v === 'string' ? (prefixName = v) : '')
"
@clear="prefixName = ''"
:rules="[(val: string) => !!val || $t('form.error.required')]"
>
<template v-slot:no-option>
<q-item>

View file

@ -7,6 +7,8 @@ import { CustomerCreate } from 'stores/customer/types';
import EmployerFormAbout from './EmployerFormAbout.vue';
import EmployerFormAuthorized from './EmployerFormAuthorized.vue';
import { waitAll } from 'src/stores/utils';
import FormEmployeePassport from 'src/components/03_customer-management/FormEmployeePassport.vue';
import FormEmployeeVisa from 'src/components/03_customer-management/FormEmployeeVisa.vue';
import {
FormCitizen,
CorpFormBusinessRegistration,

View file

@ -10,10 +10,25 @@ import { useI18n } from 'vue-i18n';
import ThaiBahtText from 'thai-baht-text';
import SelectBusinessType from 'src/components/shared/select/SelectBusinessType.vue';
import BusinessTypeDialog from 'src/pages/16_business-type-management/BusinessTypeDialog.vue';
import useBusinessTypeStore from 'src/stores/business-type';
const { locale } = useI18n({ useScope: 'global' });
const rawOption = ref();
const businessTypeDialog = ref<boolean>(false);
const formDataBusinessType = ref<{
name: string;
nameEN: string;
}>({
name: '',
nameEN: '',
});
const useBusinessType = useBusinessTypeStore();
const businessTypeId = defineModel<string>('businessTypeId');
const jobPosition = defineModel<string>('jobPosition');
const jobDescription = defineModel<string>('jobDescription');
@ -29,6 +44,8 @@ const typeBusinessENOption = ref([]);
const jobPositionOption = ref([]);
const jobPositionENOption = ref([]);
const keySelect = ref<number>(0);
defineProps<{
title?: string;
dense?: boolean;
@ -38,6 +55,20 @@ defineProps<{
showTitle?: boolean;
}>();
function resetFormBusinessType() {
businessTypeDialog.value = false;
formDataBusinessType.value = { name: '', nameEN: '' };
}
async function submitBusinessType() {
const res = await useBusinessType.create(formDataBusinessType.value);
if (res) {
businessTypeId.value = res.id;
resetFormBusinessType();
keySelect.value++;
}
}
onMounted(async () => {
const resultOption = await fetch('/option/option.json');
rawOption.value = await resultOption.json();
@ -121,16 +152,24 @@ let jobPositionENFilter = selectFilterOptionRefMod(
</div>
<SelectBusinessType
:key="keySelect"
class="col-md-6 col-12"
v-model:value="businessTypeId"
:readonly
creatable
lang="tha"
:rules="[(val: string) => !!val || $t('form.error.required')]"
@create="() => (businessTypeDialog = true)"
/>
<SelectBusinessType
:key="keySelect"
class="col-md-6 col-12"
v-model:value="businessTypeId"
lang="eng"
:readonly
creatable
lang="eng"
:rules="[(val: string) => !!val || $t('form.error.required')]"
@create="() => (businessTypeDialog = true)"
/>
<q-select
@ -282,4 +321,12 @@ let jobPositionENFilter = selectFilterOptionRefMod(
"
/>
</div>
<BusinessTypeDialog
ref="refBusinessTypeDialog"
@close="resetFormBusinessType()"
@submit="submitBusinessType()"
v-model="businessTypeDialog"
v-model:data="formDataBusinessType"
/>
</template>

View file

@ -9,8 +9,6 @@ const contactName = defineModel<string>('contactName');
const email = defineModel<string>('email');
const contactTel = defineModel<string>('contactTel');
const officeTel = defineModel<string>('officeTel');
const agent = defineModel<string>('agent');
const agentUserId = defineModel<string>('agentUserId');
</script>
@ -109,7 +107,6 @@ const agentUserId = defineModel<string>('agentUserId');
/>
</template>
</q-input>
<SelectAgent
:label="$t('customer.form.agent')"
v-model:value="agentUserId"

View file

@ -22,6 +22,10 @@ import { useRoute } from 'vue-router';
export const useCustomerForm = defineStore('form-customer', () => {
const customerStore = useCustomerStore();
const onCreateImageList = ref<{
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}>({ selectedImage: '', list: [] });
const { t } = useI18n();
const flowStore = useFlowStore();
@ -30,6 +34,8 @@ export const useCustomerForm = defineStore('form-customer', () => {
const registerAbleBranchOption = ref<{ id: string; name: string }[]>();
const currentBranchRootId = ref<string>('');
const tabFieldRequired = ref<{
[key: string]: (keyof CustomerBranchCreate)[];
}>({
@ -82,6 +88,7 @@ export const useCustomerForm = defineStore('form-customer', () => {
formDataOcr: Record<string, any>;
isImageEdit: boolean;
currentCustomerId?: string;
imageList: { list: string[]; selectedImage: string };
}>({
dialogType: 'info',
dialogOpen: false,
@ -98,6 +105,7 @@ export const useCustomerForm = defineStore('form-customer', () => {
treeFile: [],
formDataOcr: {},
isImageEdit: false,
imageList: { list: [], selectedImage: '' },
});
watch(
@ -160,6 +168,7 @@ export const useCustomerForm = defineStore('form-customer', () => {
state.value.editCustomerCode = data.code;
state.value.customerImageUrl = `${baseUrl}/customer/${id}/image/${data.selectedImage}`;
state.value.defaultCustomerImageUrl = `${baseUrl}/customer/${id}/image/${data.selectedImage}`;
currentBranchRootId.value = data.branch[0].id || '';
resetFormData.registeredBranchId = data.registeredBranchId;
resetFormData.status = data.status;
@ -255,7 +264,6 @@ export const useCustomerForm = defineStore('form-customer', () => {
async function addCurrentCustomerBranch() {
if (currentFormData.value.customerBranch?.some((v) => !v.id)) return;
currentFormData.value.customerBranch?.push({
id: '',
customerId: '',
branchCode:
currentFormData.value.customerBranch.length !== 0
@ -495,11 +503,14 @@ export const useCustomerForm = defineStore('form-customer', () => {
}
return {
onCreateImageList,
tabFieldRequired,
registerAbleBranchOption,
state,
resetFormData,
currentFormData,
currentBranchRootId,
isFormDataDifferent,
resetForm,
assignFormData,
@ -779,6 +790,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
}
| undefined;
ocr: boolean;
imageList: { list: string[]; selectedImage: string };
}>({
currentBranchId: '',
isImageEdit: false,
@ -803,6 +815,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
infoEmployeePersonCard: [],
formDataEmployeeOwner: undefined,
ocr: false,
imageList: { list: [], selectedImage: '' },
});
const defaultFormData: EmployeeCreate & { image?: File } = {
@ -955,6 +968,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
state.value.currentIndexVisa = -1;
state.value.currentIndexCheckup = -1;
state.value.currentIndexWorkHistory = -1;
state.value.imageList = { list: [], selectedImage: '' };
// state.value.currentTab = 'personalInfo';
if (clean) {
state.value.formDataEmployeeOwner = undefined;
@ -1384,12 +1398,10 @@ export const useEmployeeForm = defineStore('form-employee', () => {
statusSave: true,
})),
),
employeeOtherInfo: structuredClone(
{
...payload.employeeOtherInfo,
statusSave: !!payload.employeeOtherInfo?.id ? true : false,
} || {},
),
employeeOtherInfo: structuredClone({
...(payload.employeeOtherInfo ?? {}),
statusSave: true,
}),
employeeWork: structuredClone(
payload.employeeWork?.length === 0
? state.value.dialogModal
@ -1663,6 +1675,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
state,
currentFromDataEmployee,
resetEmployeeData,
addPassport,
addVisa,
addCheckup,

View file

@ -102,6 +102,8 @@ const {
deleteWork,
importProduct,
productExport,
} = productServiceStore;
const { workNameItems } = storeToRefs(productServiceStore);
@ -1169,6 +1171,7 @@ function clearFormService() {
profileSubmit.value = false;
imageProduct.value = undefined;
profileFileImg.value = null;
serviceTab.value = 1;
}
function sameFormService() {
@ -1896,6 +1899,25 @@ async function submitWorkName(
else await editWork(workId, data);
}
async function triggerExport() {
productExport({
pageSize: 100_000,
productGroupId: currentIdGroup.value,
query: !!inputSearchProductAndService.value
? inputSearchProductAndService.value
: undefined,
status:
currentStatus.value === 'INACTIVE'
? 'INACTIVE'
: currentStatus.value === 'ACTIVE'
? 'ACTIVE'
: undefined,
startDate: searchDate.value[0] ? new Date(searchDate.value[0]) : undefined,
endDate: searchDate.value[1] ? new Date(searchDate.value[1]) : undefined,
});
}
watch(
() => formService.value.attributes.workflowId,
async (a, b) => {
@ -2229,7 +2251,6 @@ watch(
</AdvanceSearch>
</template>
</q-input>
<div class="row col-md-6" style="white-space: nowrap">
<q-select
v-show="$q.screen.gt.sm"
@ -2780,6 +2801,13 @@ watch(
}
"
/>
<SaveButton
icon-only
:icon="'material-symbols:download'"
color="var(--info-bg)"
@click.stop="triggerExport()"
/>
</div>
<div class="row col-md-6" style="white-space: nowrap">
@ -4357,6 +4385,7 @@ watch(
<!-- add service -->
<DialogForm
v-if="dialogService"
hide-footer
no-address
no-app-box
@ -4722,6 +4751,7 @@ watch(
<!-- edit service edit package-->
<!-- :edit="!(formDataProductService.status === 'INACTIVE')" -->
<DialogForm
v-if="dialogServiceEdit"
hide-footer
no-address
height="95vh"

View file

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { onMounted, reactive, ref, watch, computed } from 'vue';
import { onMounted, onUnmounted, reactive, ref, watch, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
@ -60,7 +60,6 @@ const flowStore = useFlowStore();
const userBranch = useMyBranch();
const navigatorStore = useNavigator();
const customerStore = useCustomerStore();
const {
fetchListOfOptionBranch,
customerFormUndo,
@ -76,6 +75,7 @@ const {
const {
state: customerFormState,
currentFormData: customerFormData,
currentBranchRootId,
registerAbleBranchOption,
tabFieldRequired,
} = storeToRefs(customerFormStore);
@ -89,6 +89,8 @@ const fieldSelectedOption = computed(() => {
value: v.name,
}));
});
const keyAddDialog = ref<number>(0);
const special = ref(false);
const branchId = ref('');
const agentPrice = ref<boolean>(false);
@ -273,6 +275,10 @@ const customerNameInfo = computed(() => {
return name || '-';
});
function handleWindowFocus() {
fetchQuotationList();
}
onMounted(async () => {
pageState.gridView = $q.screen.lt.md ? true : false;
navigatorStore.current.title = 'quotation.title';
@ -310,6 +316,12 @@ onMounted(async () => {
}
flowStore.rotate();
window.addEventListener('focus', handleWindowFocus);
});
onUnmounted(() => {
window.removeEventListener('focus', handleWindowFocus);
});
async function fetchQuotationList(mobileFetch?: boolean) {
@ -765,8 +777,13 @@ async function filterBySellerId() {
:worker-count="item.row._count.worker"
:worker-max="item.row.workerMax || item.row._count.worker"
:customer-name="
item.row.customerBranch.registerName ||
`${item.row.customerBranch.firstName || '-'} ${item.row.customerBranch.lastName || ''}`
item.row.customerBranch.customer.customerType === 'CORP'
? $i18n.locale === 'tha'
? item.row.customerBranch.registerName
: item.row.customerBranch.registerNameEN
: $i18n.locale === 'tha'
? `${item.row.customerBranch.firstName || '-'} ${item.row.customerBranch.lastName || ''}`
: `${item.row.customerBranch.firstNameEN || '-'} ${item.row.customerBranch.lastNameEN || ''}`
"
:reporter="
$i18n.locale === 'eng'
@ -865,6 +882,7 @@ async function filterBySellerId() {
<!-- NOTE: START - Quotation Form, Add Quotation -->
<DialogForm
:key="keyAddDialog"
:title="$t('general.add', { text: $t('quotation.title') })"
v-model:modal="pageState.addModal"
:submit-label="$t('general.add', { text: $t('quotation.title') })"
@ -874,13 +892,14 @@ async function filterBySellerId() {
:submit="
() => {
quotationFormState.mode = 'create';
quotationFormData.customerBranchId = currentBranchRootId;
triggerQuotationDialog({ statusDialog: 'create' });
}
"
:close="
() => {
branchId = '';
quotationFormData.customerBranchId = '';
currentBranchRootId = '';
}
"
:beforeClose="
@ -930,7 +949,7 @@ async function filterBySellerId() {
v-model:agent-price="agentPrice"
v-model:branch-id="branchId"
v-model:special="special"
v-model:customer-branch-id="quotationFormData.customerBranchId"
v-model:customer-branch-id="currentBranchRootId"
@add-customer="triggerSelectTypeCustomerd()"
/>
</div>
@ -999,6 +1018,7 @@ async function filterBySellerId() {
() => {
customerFormState.dialogModal = false;
onCreateImageList = { selectedImage: '', list: [] };
keyAddDialog++;
}
"
>
@ -1190,7 +1210,7 @@ async function filterBySellerId() {
customerFormData.customerBranch[0].legalPersonNo
"
v-model:business-type="
customerFormData.customerBranch[0].businessType
customerFormData.customerBranch[0].businessTypeId
"
v-model:job-position="
customerFormData.customerBranch[0].jobPosition
@ -1271,7 +1291,6 @@ async function filterBySellerId() {
res = await customerStore.createBranch({
...customerFormData.customerBranch[idx],
customerId: customerFormState.editCustomerId || '',
id: undefined,
});
}
if (res) {

File diff suppressed because it is too large Load diff

View file

@ -80,6 +80,8 @@ import { RouterLink, useRoute } from 'vue-router';
import { initLang, initTheme, Lang } from 'src/utils/ui';
import { convertTemplate } from 'src/utils/string-template';
import { CustomerBranch } from 'src/stores/customer';
type Node = {
[key: string]: any;
opened?: boolean;
@ -94,6 +96,8 @@ type ProductGroupId = string;
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
const customerBranchOption = ref<CustomerBranch>();
const employeeStore = useEmployeeStore();
const route = useRoute();
const useReceiptStore = useReceipt();
@ -157,49 +161,7 @@ const selectedWorker = ref<
}[];
})[]
>([]);
const selectedWorkerItem = computed(() => {
return [
...selectedWorker.value.map((e) => ({
foreignRefNo: e.employeePassport
? e.employeePassport[0]?.number || '-'
: '-',
employeeName:
locale.value === Lang.English
? `${e.firstNameEN} ${e.lastNameEN}`
: e.firstName
? `${e.firstName} ${e.lastName}`
: `${e.firstNameEN} ${e.lastNameEN}`,
birthDate: dateFormatJS({ date: e.dateOfBirth }),
gender: e.gender,
age: calculateAge(e.dateOfBirth),
nationality: optionStore.mapOption(e.nationality),
documentExpireDate:
e.employeePassport !== undefined &&
e.employeePassport[0]?.expireDate !== undefined
? dateFormatJS({ date: e.employeePassport[0]?.expireDate })
: '-',
imgUrl: e.selectedImage
? `${API_BASE_URL}/employee/${e.id}/image/${e.selectedImage}`
: '',
status: e.status,
})),
...newWorkerList.value.map((v: any) => ({
foreignRefNo: v.passportNo,
employeeName:
locale.value === Lang.English
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName} ${v.lastName}`,
birthDate: dateFormatJS({ date: v.dateOfBirth }),
gender: v.gender,
age: calculateAge(v.dateOfBirth),
nationality: optionStore.mapOption(v.nationality),
documentExpireDate: '-',
imgUrl: '',
status: 'CREATED',
})),
];
});
const selectedWorkerItem = ref([]);
const firstCodePayment = ref('');
const selectedProductGroup = ref('');
const selectedInstallmentNo = ref<number[]>([]);
@ -234,7 +196,7 @@ function getPrice(
) {
if (filterHook) list = list.filter(filterHook);
return list.reduce(
const value = list.reduce(
(a, c) => {
if (
selectedInstallmentNo.value.length > 0 &&
@ -244,32 +206,25 @@ function getPrice(
return a;
}
const originalPrice = c.pricePerUnit;
const finalPriceWithVat = precisionRound(
originalPrice * (1 + (config.value?.vat || 0.07)),
);
const finalPriceNoVat =
finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const price = finalPriceNoVat * c.amount;
const vat =
(finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07);
const calcVat =
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
const vatFactor = calcVat ? (config.value?.vat ?? 0.07) : 0;
a.totalPrice = precisionRound(a.totalPrice + price);
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
a.vat = calcVat ? precisionRound(a.vat + vat) : a.vat;
const pricePerUnit =
precisionRound(c.pricePerUnit * (1 + vatFactor)) / (1 + vatFactor);
const price =
(pricePerUnit * c.amount * (1 + vatFactor) - c.discount) /
(1 + vatFactor);
const vat = price * vatFactor;
a.totalPrice = precisionRound(a.totalPrice + price + c.discount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + vat);
a.vatExcluded = calcVat
? a.vatExcluded
: precisionRound(a.vatExcluded + price);
a.finalPrice = precisionRound(
a.totalPrice -
a.totalDiscount +
a.vat -
Number(quotationFormData.value.discount || 0),
);
a.finalPrice = precisionRound(a.totalPrice - a.totalDiscount + a.vat);
return a;
},
@ -281,6 +236,8 @@ function getPrice(
finalPrice: 0,
},
);
return value;
}
const summaryPrice = computed(() => getPrice(productServiceList.value));
@ -559,7 +516,7 @@ async function convertDataToFormSubmit() {
),
);
selectedWorker.value.forEach((v, i) => {
selectedWorkerItem.value.forEach((v, i) => {
if (v.attachment !== undefined) {
v.attachment.forEach((value) => {
fileItemNewWorker.value.push({
@ -576,7 +533,7 @@ async function convertDataToFormSubmit() {
quotationFormData.value.worker = JSON.parse(
JSON.stringify([
...selectedWorker.value.map((v) => {
...selectedWorkerItem.value.map((v) => {
{
return v.id;
}
@ -719,19 +676,13 @@ function handleUpdateProductTable(
// handleChangePayType(quotationFormData.value.payCondition);
// calc price
const calc = (c: QuotationPayload['productServiceList'][number]) => {
const originalPrice = c.pricePerUnit || 0;
const finalPriceWithVat = precisionRound(
originalPrice * (1 + (config.value?.vat || 0.07)),
);
const finalPriceNoVat =
finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const price = finalPriceNoVat * c.amount;
const vat = c.product.calcVat
? (finalPriceNoVat * c.amount - (c.discount || 0)) *
(config.value?.vat || 0.07)
: 0;
return precisionRound(price + vat);
const calcVat =
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
const vatFactor = calcVat ? (config.value?.vat ?? 0.07) : 0;
const pricePerUnit =
precisionRound(c.pricePerUnit * (1 + vatFactor)) / (1 + vatFactor);
const price = pricePerUnit * c.amount * (1 + vatFactor) - c.discount;
return precisionRound(price);
};
// installment
@ -784,21 +735,16 @@ function toggleDeleteProduct(index: number) {
// cal curr amount
if (currPaySplit && currTempPaySplit) {
const price = agentPrice.value
? currProduct.product.agentPrice
: currProduct.product.price;
const pricePerUnit = currProduct.product.vatIncluded
? price / (1 + (config.value?.vat || 0.07))
: price;
const vat =
(pricePerUnit * currProduct.amount - currProduct.discount) *
(config.value?.vat || 0.07);
const finalPrice =
pricePerUnit * currProduct.amount +
vat -
Number(currProduct.discount || 0);
const calcVat =
currProduct.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
const vatFactor = calcVat ? (config.value?.vat ?? 0.07) : 0;
currTempPaySplit.amount = currPaySplit.amount - finalPrice;
const price = precisionRound(
currProduct.pricePerUnit * currProduct.amount * (1 + vatFactor) -
currProduct.discount,
);
currTempPaySplit.amount = currPaySplit.amount - price;
currPaySplit.amount = currTempPaySplit.amount;
}
@ -820,7 +766,40 @@ function toggleDeleteProduct(index: number) {
}
async function assignWorkerToSelectedWorker() {
selectedWorker.value = quotationFormData.value.worker;
selectedWorkerItem.value = quotationFormData.value.worker.map((e) => {
return {
id: e.id,
foreignRefNo: e.employeePassport
? e.employeePassport[0]?.number || '-'
: '-',
employeeName:
locale.value === Lang.English
? `${e.firstNameEN} ${e.lastNameEN}`
: `${e.firstName || e.firstNameEN} ${e.lastName || e.lastNameEN}`,
birthDate: dateFormatJS({ date: e.dateOfBirth }),
gender: e.gender,
age: calculateAge(e.dateOfBirth),
nationality: optionStore.mapOption(e.nationality),
documentExpireDate:
e.employeePassport !== undefined &&
e.employeePassport[0]?.expireDate !== undefined
? dateFormatJS({ date: e.employeePassport[0]?.expireDate })
: '-',
imgUrl: e.selectedImage
? `${API_BASE_URL}/employee/${e.id}/image/${e.selectedImage}`
: '',
employeePassport: e.employeePassport,
status: e.status,
workerNew: false,
lastNameEN: e.lastNameEN,
lastName: e.lastName,
middleNameEN: e.middleNameEN,
middleName: e.middleName,
firstNameEN: e.firstNameEN,
firstName: e.firstName,
namePrefix: e.namePrefix,
};
});
}
function convertToTable(nodes: Node[]) {
@ -853,8 +832,7 @@ function convertToTable(nodes: Node[]) {
tempTableProduct.value = JSON.parse(JSON.stringify(list));
if (nodes.length > 0) {
quotationFormData.value.paySplit = Array.apply(
null,
quotationFormData.value.paySplit = Array.from(
new Array(quotationFormData.value.paySplitCount),
).map((_, i) => ({
no: i + 1,
@ -880,21 +858,21 @@ function convertToTable(nodes: Node[]) {
function convertEmployeeToTable(selected: Employee[]) {
productServiceList.value.forEach((v) => {
if (selectedWorker.value.length === 0 && v.amount === 1) v.amount -= 1;
if (selectedWorkerItem.value.length === 0 && v.amount === 1) v.amount -= 1;
v.amount = Math.max(
v.amount + selected.length - selectedWorker.value.length,
v.amount + selected.length - selectedWorkerItem.value.length,
1,
);
const oldWorkerId: string[] = [];
const newWorkerIndex: number[] = [];
selectedWorker.value.forEach((item, i) => {
selectedWorkerItem.value.forEach((item, i) => {
if (v.workerIndex.includes(i)) oldWorkerId.push(item.id);
});
selected.forEach((item, i) => {
if (selectedWorker.value.find((n) => item.id === n.id)) return;
if (selectedWorkerItem.value.find((n) => item.id === n.id)) return;
newWorkerIndex.push(i);
});
@ -907,7 +885,7 @@ function convertEmployeeToTable(selected: Employee[]) {
pageState.employeeModal = false;
quotationFormData.value.workerMax = Math.max(
quotationFormData.value.workerMax || 1,
selectedWorker.value.length,
selectedWorkerItem.value.length,
);
}
@ -980,6 +958,71 @@ function viewProductFile(data: ProductRelation) {
pageState.imageDialogUrl = base64 ? base64[1] : '';
}
function combineWorker(newWorker: any, oldWorker: any) {
selectedWorkerItem.value = [
...oldWorker.map((e) => ({
id: e.id,
foreignRefNo: e.employeePassport
? e.employeePassport[0]?.number || '-'
: '-',
employeeName:
locale.value === Lang.English
? `${e.firstNameEN} ${e.lastNameEN}`
: `${e.firstName || e.firstNameEN} ${e.lastName || e.lastNameEN}`,
birthDate: dateFormatJS({ date: e.dateOfBirth }),
gender: e.gender,
age: calculateAge(e.dateOfBirth),
nationality: optionStore.mapOption(e.nationality),
documentExpireDate:
e.employeePassport !== undefined &&
e.employeePassport[0]?.expireDate !== undefined
? dateFormatJS({ date: e.employeePassport[0]?.expireDate })
: '-',
imgUrl: e.selectedImage
? `${API_BASE_URL}/employee/${e.id}/image/${e.selectedImage}`
: '',
employeePassport: e.employeePassport,
status: e.status,
workerNew: false,
lastNameEN: e.lastNameEN,
lastName: e.lastName,
middleNameEN: e.middleNameEN,
middleName: e.middleName,
firstNameEN: e.firstNameEN,
firstName: e.firstName,
namePrefix: e.namePrefix,
})),
...newWorker.map((v: any) => ({
id: v.id,
foreignRefNo: v.passportNo || '-',
employeeName:
locale.value === Lang.English
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName || v.firstNameEN} ${v.lastName || v.lastNameEN}`,
birthDate: dateFormatJS({ date: v.dateOfBirth }),
gender: v.gender,
age: calculateAge(v.dateOfBirth),
nationality: optionStore.mapOption(v.nationality),
documentExpireDate: '-',
imgUrl: '',
status: 'CREATED',
lastNameEN: v.lastNameEN,
lastName: v.lastName,
middleNameEN: v.middleNameEN,
middleName: v.middleName,
firstNameEN: v.firstNameEN,
firstName: v.firstName,
namePrefix: v.namePrefix,
dateOfBirth: v.dateOfBirth,
workerNew: true,
})),
];
}
const sessionData = ref<Record<string, any>>();
onMounted(async () => {
@ -1051,7 +1094,7 @@ watch(
() => quotationFormData.value.customerBranchId,
async (v) => {
if (!v) return;
selectedWorker.value = [];
selectedWorkerItem.value = [];
},
);
@ -1067,6 +1110,15 @@ watch(
const productServiceNodes = ref<ProductTree>([]);
watch(customerBranchOption, () => {
if (!customerBranchOption.value) return;
quotationFormData.value.contactName =
customerBranchOption.value.contactName || '';
quotationFormData.value.contactTel =
customerBranchOption.value.contactTel || '';
});
watch(
() => productServiceList.value,
() => {
@ -1074,6 +1126,13 @@ watch(
},
);
watch(customerBranchOption, () => {
if (!customerBranchOption.value) return;
quotationFormData.value.contactName = customerBranchOption.value.contactName;
quotationFormData.value.contactTel = customerBranchOption.value.contactTel;
});
// async function searchEmployee(text: string) {
// let query: string | undefined = text;
// let pageSize = 50;
@ -1095,7 +1154,19 @@ watch(
// }
function storeDataLocal() {
quotationFormData.value.productServiceList = productService.value;
const tempProductService = productService.value.map((v) => {
return {
...v,
vat: v.product[agentPrice ? 'agentPriceCalcVat' : 'calcVat']
? precisionRound(
((v.pricePerUnit * (1 + (config?.value.vat || 0.07)) * v.amount -
v.discount) /
(1 + (config?.value.vat || 0.07))) *
0.07,
)
: 0,
};
});
localStorage.setItem(
'quotation-preview',
@ -1104,7 +1175,7 @@ function storeDataLocal() {
codeInvoice: code.value,
codePayment: firstCodePayment.value,
...quotationFormData.value,
productServiceList: productService.value,
productServiceList: tempProductService,
},
meta: {
source: {
@ -1124,7 +1195,7 @@ function storeDataLocal() {
workName: quotationFormData.value.workName,
dueDate: quotationFormData.value.dueDate,
},
selectedWorker: selectedWorker.value,
selectedWorker: selectedWorkerItem.value,
createdBy: quotationFormState.value.createdBy('tha'),
agentPrice: agentPrice.value,
},
@ -1209,10 +1280,10 @@ async function getWorkerFromCriteria(
if (!ret) return false; // error, do not close dialog
const deduplicate = ret.result.filter(
(a) => !selectedWorker.value.find((b) => a.id === b.id),
(a) => !selectedWorkerItem.value.find((b) => a.id === b.id),
);
convertEmployeeToTable([...deduplicate, ...selectedWorker.value]);
convertEmployeeToTable([...deduplicate, ...selectedWorkerItem.value]);
return true;
}
@ -1519,6 +1590,7 @@ function covertToNode() {
v-model:customer-branch-id="
quotationFormData.customerBranchId
"
v-model:customer-branch-option="customerBranchOption"
:readonly="readonly"
/>
</template>
@ -1601,15 +1673,15 @@ function covertToNode() {
(v) =>
(quotationFormData.workerMax = Math.max(
v,
selectedWorker.length,
selectedWorkerItem.length,
))
"
:employee-amount="
quotationFormData.workerMax || selectedWorker.length
quotationFormData.workerMax || selectedWorkerItem.length
"
:readonly="readonly"
:rows="selectedWorkerItem"
@delete="(i) => deleteItem(selectedWorker, i)"
@delete="(i) => deleteItem(selectedWorkerItem, i)"
/>
</div>
</q-expansion-item>
@ -1853,10 +1925,10 @@ function covertToNode() {
installments: quotationFormData.paySplit,
},
'quotation-labor': {
name: selectedWorker.map(
name: selectedWorkerItem.map(
(v, i) =>
`${i + 1}. ` +
`${v.employeePassport.length !== 0 ? v.employeePassport[0].number + '_' : ''} ${v.namePrefix}.${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
`${v.employeePassport.length !== 0 ? v.employeePassport[0].number + '_' : ''}${v.namePrefix}.${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
),
},
})
@ -1946,6 +2018,7 @@ function covertToNode() {
view !== View.Receipt &&
view !== View.Complete
"
:branch-id="quotationFull.registeredBranchId"
:readonly="
isRoleInclude(['sale', 'head_of_sale']) ||
!canAccess('quotation', 'edit')
@ -2396,13 +2469,12 @@ function covertToNode() {
<!-- add employee quotation -->
<QuotationFormWorkerSelect
:preselect-worker="selectedWorker"
:preselect-worker="selectedWorkerItem"
:customerBranchId="quotationFormData.customerBranchId"
v-model:open="pageState.employeeModal"
v-model:new-worker-list="newWorkerList"
@success="
(v) => {
selectedWorker = v.worker;
combineWorker(v.newWorker, v.worker);
}
"
/>
@ -2445,7 +2517,7 @@ function covertToNode() {
<!-- add Worker -->
<QuotationFormWorkerAddDialog
v-if="quotationFormState.source"
:disabled-worker-id="selectedWorker.map((v) => v.id)"
:disabled-worker-id="selectedWorkerItem.map((v) => v.id)"
:product-service-list="quotationFormState.source.productServiceList"
:quotation-id="quotationFormState.source.id"
:customer-branch-id="quotationFormState.source.customerBranchId"

View file

@ -234,6 +234,7 @@ watch(
<section class="row q-col-gutter-sm col-12 items-center">
<SelectInput
class="col-md-6 col-12"
id="select-pay-type"
:label="$t('quotation.payType')"
:option="
taskOrder
@ -241,7 +242,6 @@ watch(
: payTypeOption
"
:readonly="readonly || debitNote"
id="pay-type"
:model-value="payType"
@update:model-value="
(v) => {
@ -275,6 +275,7 @@ watch(
</div>
<q-input
v-model="paySplitCount"
id="input-pay-split-count"
:readonly="readonly || payType === 'Split'"
class="col-3"
type="number"
@ -311,6 +312,7 @@ watch(
<q-input
:readonly="readonly"
:label="$t('general.name')"
:id="`input-period-name-${i}`"
v-if="payType === 'SplitCustom'"
v-model="period.name"
class="col q-mx-sm"
@ -320,6 +322,7 @@ watch(
<q-input
:readonly="readonly || payType === 'Split'"
class="col q-mx-sm"
:id="`input-period-amount-${i}`"
:label="$t('quotation.amount')"
:model-value="
amount4Show[i] || commaInput(period.amount.toString())
@ -377,6 +380,7 @@ watch(
<DatePicker
v-if="payType === 'BillFull'"
id="datepicker-bill-date"
:readonly
class="col-12"
:label="$t('quotation.callDueDate')"
@ -446,7 +450,9 @@ watch(
<span class="q-ml-auto">
{{
formatNumberDecimal(
summaryPrice.totalPrice - summaryPrice.totalDiscount,
summaryPrice.totalPrice -
summaryPrice.totalDiscount -
summaryPrice.vatExcluded,
2,
)
}}
@ -482,7 +488,11 @@ watch(
<div class="q-pa-sm row surface-2 items-center text-weight-bold">
{{ $t('quotation.totalPriceBaht') }}
<span class="q-ml-auto" style="color: var(--brand-1)">
<span
class="q-ml-auto"
style="color: var(--brand-1)"
id="value-final-price"
>
{{
payType === 'SplitCustom' && view === View.Invoice
? formatNumberDecimal(Math.max(installmentAmount || 0, 0), 2) || 0

View file

@ -341,12 +341,13 @@ watch(() => state.search, getWorkerList);
>
<div
style="display: inline-block; margin-inline: auto"
v-if="workerList.length === 0"
v-if="workerList.length === 0 && state.search"
>
<NoData :not-found="!!state.search" />
</div>
<TableWorker
v-else
v-model:selected="workerSelected"
:rows="workerList"
:disabledWorkerId

View file

@ -113,7 +113,7 @@ const props = withDefaults(
defineProps<{
customerBranchId?: string;
disabledWorkerId?: string[];
preselectWorker?: Employee[];
preselectWorker?: (Employee & { workerNew: boolean })[];
}>(),
{},
);
@ -133,7 +133,7 @@ const optionStore = useOptionStore();
const employeeStore = useEmployeeStore();
const open = defineModel<boolean>('open', { default: false });
const newWorkerList = defineModel<
const newWorkerList = ref<
(EmployeeWorker & {
attachment?: {
name?: string;
@ -143,7 +143,7 @@ const newWorkerList = defineModel<
_meta?: Record<string, any>;
}[];
})[]
>('newWorkerList', { default: [] });
>([]);
const workerSelected = ref<Employee[]>([]);
const workerList = ref<Employee[]>([]);
const importWorkerCriteria = ref<{
@ -208,7 +208,13 @@ function getEmployeeImageUrl(item: Employee) {
function init() {
if (props.preselectWorker) {
workerSelected.value = JSON.parse(JSON.stringify(props.preselectWorker));
workerSelected.value = JSON.parse(
JSON.stringify(props.preselectWorker.filter((v) => !v.workerNew)),
);
newWorkerList.value = JSON.parse(
JSON.stringify(props.preselectWorker.filter((v) => v.workerNew)),
);
}
getWorkerList();
}
@ -608,11 +614,14 @@ watch(
solid
id="btn-success"
@click="
(emits('success', {
worker: workerSelected,
newWorker: newWorkerList,
}),
(open = false))
() => {
$emit('success', {
worker: workerSelected,
newWorker: newWorkerList,
});
open = false;
}
"
>
{{ $t('general.select', { msg: $t('quotation.employeeList') }) }}
@ -634,9 +643,11 @@ watch(
if (employeeFormState.currentTab === 'personalInfo') {
const currentEmployeeId =
await employeeFormStore.submitPersonal(onCreateImageList);
quotationForm.injectNewEmployee({
data: { ...currentFromDataEmployee, id: currentEmployeeId },
});
newWorkerList.push(
quotationForm.injectNewEmployee({
data: { ...currentFromDataEmployee, id: currentEmployeeId },
}),
);
employeeFormState.isEmployeeEdit = false;
employeeFormState.dialogType = 'info';
}

View file

@ -69,6 +69,7 @@ export const useQuotationForm = defineStore('form-quotation', () => {
file?: File;
_meta?: Record<string, any>;
}[];
workerNew: boolean;
})[]
>([]);
@ -220,7 +221,7 @@ export const useQuotationForm = defineStore('form-quotation', () => {
},
callback?: () => void,
) {
newWorkerList.value.push({
const temp = {
//passportNo: obj.data.passportNo,
//documentExpireDate: obj.data.documentExpireDate,
id: obj.data.id,
@ -235,9 +236,12 @@ export const useQuotationForm = defineStore('form-quotation', () => {
gender: obj.data.gender,
dateOfBirth: obj.data.dateOfBirth,
attachment: obj.data.attachment,
});
workerNew: true,
};
callback?.();
return temp;
}
function dialogDelete(callback: () => void) {

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { onMounted, nextTick, ref, watch } from 'vue';
import { onMounted, nextTick, ref, watch, toRaw } from 'vue';
import { precisionRound } from 'src/utils/arithmetic';
import { useI18n } from 'vue-i18n';
import ThaiBahtText from 'thai-baht-text';
@ -175,6 +175,8 @@ enum View {
const view = ref<View>(View.Quotation);
onMounted(async () => {
await configStore.getConfig();
const currentDocumentType = new URL(window.location.href).searchParams.get(
'type',
);
@ -259,18 +261,6 @@ onMounted(async () => {
productList.value =
(data?.value?.productServiceList ?? data.value?.productServiceList).map(
(v) => {
const originalPrice = v.pricePerUnit;
const finalPriceWithVat = precisionRound(
originalPrice * (1 + (config.value?.vat || 0.07)),
);
const finalPriceNoVat =
finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const price = finalPriceNoVat * v.amount - v.discount;
const vat =
(finalPriceNoVat * v.amount - v.discount) *
(config.value?.vat || 0.07);
return {
id: v.product.id,
code: v.product.code,
@ -279,8 +269,8 @@ onMounted(async () => {
pricePerUnit: v.pricePerUnit || 0,
discount: v.discount || 0,
vat: v.vat || 0,
value: precisionRound(price + (v.product.calcVat ? vat : 0)),
calcVat: v.product.calcVat,
value: 0,
calcVat: v.vat > 0,
product: v.product,
};
},
@ -292,23 +282,17 @@ onMounted(async () => {
[]
).reduce(
(a, c) => {
const originalPrice = c.pricePerUnit;
const finalPriceWithVat = precisionRound(
originalPrice * (1 + (config.value?.vat || 0.07)),
);
const finalPriceNoVat =
finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const calcVat = c.vat > 0;
const vatFactor = calcVat ? (config.value?.vat ?? 0.07) : 0;
const pricePerUnit =
precisionRound(c.pricePerUnit * (1 + vatFactor)) / (1 + vatFactor);
const price =
(pricePerUnit * c.amount * (1 + vatFactor) - c.discount) /
(1 + vatFactor);
const price = finalPriceNoVat * c.amount;
const vat =
(finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07);
const calcVat =
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
a.totalPrice = precisionRound(a.totalPrice + price);
a.totalPrice = precisionRound(a.totalPrice + price + c.discount);
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
a.vat = calcVat ? precisionRound(a.vat + vat) : a.vat;
a.vat = calcVat ? precisionRound(a.vat + c.vat) : a.vat;
a.vatExcluded = calcVat
? a.vatExcluded
: precisionRound(a.vatExcluded + price);
@ -334,16 +318,12 @@ onMounted(async () => {
function calcPrice(c: Product) {
const originalPrice = c.pricePerUnit;
const finalPriceWithVat = precisionRound(
originalPrice + originalPrice * (config.value?.vat || 0.07),
const finalPricePerUnit = precisionRound(
originalPrice +
(c.calcVat ? originalPrice * (config.value?.vat || 0.07) : 0),
);
const finalPriceNoVat = finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const price = finalPriceNoVat * c.amount - c.discount;
const vat = c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat']
? (finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07)
: 0;
return precisionRound(price + vat);
const price = finalPricePerUnit * c.amount - c.discount;
return precisionRound(price);
}
async function closeTab() {
@ -427,31 +407,15 @@ function print() {
<td>{{ v.detail }}</td>
<td style="text-align: right">{{ v.amount }}</td>
<td style="text-align: right">
{{
formatNumberDecimal(
v.pricePerUnit +
(v.product[agentPrice ? 'agentPriceCalcVat' : 'calcVat']
? v.pricePerUnit * (config?.vat || 0.07)
: 0),
2,
)
}}
{{ formatNumberDecimal(v.pricePerUnit, 2) }}
</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.discount, 2) }}
<template v-if="v.discount !== 0">
{{ formatNumberDecimal(v.discount, 2) }} ฿
</template>
</td>
<td style="text-align: right">
{{
formatNumberDecimal(
v.product[agentPrice ? 'agentPriceCalcVat' : 'calcVat']
? precisionRound(
(v.pricePerUnit * v.amount - v.discount) *
(config?.vat || 0.07),
)
: 0,
2,
)
}}
{{ Math.round((v.vat > 0 ? config?.vat || 0.07 : 0) * 100) }}%
</td>
<td style="text-align: right">
{{ formatNumberDecimal(calcPrice(v), 2) }}
@ -511,7 +475,9 @@ function print() {
<td class="text-right">
{{
formatNumberDecimal(
summaryPrice.totalPrice - summaryPrice.totalDiscount,
summaryPrice.totalPrice -
summaryPrice.totalDiscount -
summaryPrice.vatExcluded,
2,
)
}}
@ -600,7 +566,7 @@ function print() {
details?.worker.map(
(v, i) =>
`${i + 1}. ` +
`${v.namePrefix}. ${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
`${v.employeePassport.length !== 0 ? v.employeePassport[0].number + '_' : ''}${v.namePrefix}. ${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
) || [],
},
}) || '-'

View file

@ -79,8 +79,8 @@ function titleMode(mode: View): string {
})
}}
</span>
<span>เลขประจำตวผเสยภาษ {{ branch.taxNo }}</span>
<span>เบอรโทร {{ branch.telephoneNo }}</span>
<span>{{ $t('branch.form.taxNo') }} {{ branch.taxNo }}</span>
<span>{{ $t('taskOrder.telephone') }} {{ branch.telephoneNo }}</span>
<span>{{ branch.webUrl }}</span>
</article>
<article>
@ -105,8 +105,18 @@ function titleMode(mode: View): string {
})
}}
</span>
<span>เลขประจำตวผเสยภาษ {{ customer.citizenId }}</span>
<span>เบอรโทร {{ customer.telephoneNo }}</span>
<span>
{{
customer.customer.customerType === 'CORP'
? `${$t('customer.form.legalPersonNo')} `
: `${$t('customer.form.taxpayyerNo')} `
}}{{
customer.customer.customerType === 'CORP'
? customer.codeCustomer
: customer.citizenId
}}
</span>
<span>{{ $t('taskOrder.telephone') }} {{ customer.telephoneNo }}</span>
</article>
</section>
<section class="detail-quotation-info">

View file

@ -61,6 +61,7 @@ const props = withDefaults(
readonly?: boolean;
listDocument: string[];
currentId: { customer: string; employee: string };
prefix?: string;
}>(),
{
listDocument: () => [],
@ -244,14 +245,14 @@ function changeCustomerTab(opts: { tab: 'customer' | 'employee' }) {
<nav class="q-ml-auto row" v-if="!readonly">
<CancelButton
v-if="state.isEdit"
id="btn-info-basic-undo"
:id="`btn-docs-${props.prefix || 'nome'}-info-basic-undo`"
icon-only
type="button"
@click.stop="triggerCancel"
/>
<EditButton
v-if="!state.isEdit"
id="btn-info-basic-edit"
:id="`btn-docs-${props.prefix || 'nome'}-info-basic-edit`"
icon-only
@click.stop="triggerEdit"
type="button"

View file

@ -11,6 +11,7 @@ import { useRequestList } from 'src/stores/request-list';
const props = defineProps<{
readonly?: boolean;
step: Step;
prefix?: string;
}>();
const requestListStore = useRequestList();
@ -100,21 +101,21 @@ function assignToForm() {
<nav class="q-ml-auto row" v-if="!readonly">
<UndoButton
v-if="state.isEdit"
id="btn-info-basic-undo"
:id="`btn-duty-${props.prefix || 'nome'}-info-basic-undo`"
icon-only
type="button"
@click.stop="triggerUndo"
/>
<SaveButton
v-if="state.isEdit"
id="btn-info-basic-save"
:id="`btn-duty-${props.prefix || 'nome'}-info-basic-save`"
icon-only
type="submit"
@click.stop="triggerSubmit"
/>
<EditButton
v-if="!state.isEdit"
id="btn-info-basic-edit"
:id="`btn-duty-${props.prefix || 'nome'}-info-basic-edit`"
icon-only
@click.stop="triggerEdit"
type="button"

View file

@ -12,6 +12,7 @@ const props = defineProps<{
readonly?: boolean;
step: Step;
requestWorkId: string;
prefix?: string;
}>();
const requestListStore = useRequestList();
@ -90,21 +91,21 @@ function assignToForm() {
<nav class="q-ml-auto row" v-if="!readonly">
<UndoButton
v-if="state.isEdit"
id="btn-info-basic-undo"
:id="`btn-form-${props.prefix || 'nome'}-info-basic-undo`"
icon-only
type="button"
@click.stop="triggerUndo"
/>
<SaveButton
v-if="state.isEdit"
id="btn-info-basic-save"
:id="`btn-form-${props.prefix || 'nome'}-info-basic-save`"
icon-only
type="submit"
@click.stop="triggerSubmit"
/>
<EditButton
v-if="!state.isEdit"
id="btn-info-basic-edit"
:id="`btn-form-${props.prefix || 'nome'}-info-basic-edit`"
icon-only
@click.stop="triggerEdit"
type="button"

View file

@ -12,6 +12,7 @@ const responsibleUserId = defineModel<string>('responsibleUserId', {
defineProps<{
readonly?: boolean;
districtId?: string;
prefix?: string;
}>();
watch(responsibleUserLocal, (lhs, rhs) => {
@ -27,6 +28,8 @@ watch(responsibleUserLocal, (lhs, rhs) => {
:label="$t('requestList.localEmployee')"
:disable="readonly"
class="col"
:id="`${prefix || 'nome'}-radio-local-employee`"
:for="`${prefix || 'nome'}-radio-local-employee`"
/>
<q-radio
v-model="responsibleUserLocal"
@ -34,6 +37,8 @@ watch(responsibleUserLocal, (lhs, rhs) => {
:label="$t('requestList.nonLocalEmployee')"
:disable="readonly"
class="col"
:id="`${prefix || 'nome'}-radio-non-local-employee`"
:for="`${prefix || 'nome'}-radio-non-local-employee`"
/>
<div class="col" />
<div class="offset-md-7"></div>
@ -52,6 +57,8 @@ watch(responsibleUserLocal, (lhs, rhs) => {
}"
:readonly
:label="$t('general.select', { msg: $t('personnel.MESSENGER') })"
:id="`${prefix || 'nome'}-select-user`"
:for="`${prefix || 'nome'}-select-user`"
/>
</div>
</div>

View file

@ -95,6 +95,7 @@ function triggerCancel(id: string) {
const res = await requestListStore.cancelRequest(id);
if (res) {
fetchList();
fetchStats();
}
},
});

View file

@ -14,6 +14,7 @@ const props = defineProps<{
step: Step;
responsibleAreaDistrictId?: string;
defaultMessenger?: string;
prefix?: string;
}>();
const emit = defineEmits<{
@ -116,21 +117,21 @@ function assignToForm() {
<nav class="q-ml-auto row" v-if="!readonly">
<UndoButton
v-if="state.isEdit"
id="btn-info-basic-undo"
:id="`btn-messenger-${props.prefix || 'nome'}-info-basic-undo`"
icon-only
type="button"
@click.stop="triggerUndo"
/>
<SaveButton
v-if="state.isEdit"
id="btn-info-basic-save"
:id="`btn-messenger-${props.prefix || 'nome'}-info-basic-save`"
icon-only
type="submit"
@click.stop="(e) => refForm?.submit(e)"
/>
<EditButton
v-if="!state.isEdit"
id="btn-info-basic-edit"
:id="`btn-messenger-${props.prefix || 'nome'}-info-basic-edit`"
icon-only
@click.stop="triggerEdit"
type="button"
@ -157,6 +158,7 @@ function assignToForm() {
v-model:responsible-user-local="formData.responsibleUserLocal"
v-model:responsible-user-id="formData.responsibleUserId"
:district-id="responsibleAreaDistrictId"
:prefix
/>
</q-form>
</section>

View file

@ -83,6 +83,8 @@ function changeableStatus(currentStatus?: RequestWorkStatus) {
</script>
<template>
<q-expansion-item
:id="`expansion-${product?.name || name}`"
:for="`expansion-${product?.name || name}`"
dense
:class="{ 'status-unpaid': !paySuccess }"
class="overflow-hidden"
@ -146,6 +148,8 @@ function changeableStatus(currentStatus?: RequestWorkStatus) {
<div class="q-ml-auto q-gutter-y-xs">
<div class="justify-end flex">
<q-btn-dropdown
:id="`btn-dropdown-${product?.name || name}`"
:for="`btn-dropdown-${product?.name || name}`"
:disable="
readonly || changeableStatus(status?.workStatus).length === 0
"
@ -197,6 +201,8 @@ function changeableStatus(currentStatus?: RequestWorkStatus) {
<q-list dense>
<q-item
v-for="(value, index) in changeableStatus(status?.workStatus)"
:id="`btn-dropdown-${product?.name || name}-${value}`"
:for="`btn-dropdown-${product?.name || name}-${value}`"
:key="index"
clickable
v-close-popup
@ -271,15 +277,15 @@ function changeableStatus(currentStatus?: RequestWorkStatus) {
}
:deep(
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
color: var(--brand-1);
}
:deep(
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.surface-1
.q-focus-helper
) {
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.surface-1
.q-focus-helper
) {
visibility: hidden;
}

View file

@ -28,6 +28,7 @@ const props = withDefaults(
readonly?: boolean;
propertiesToShow: (PropString | PropNumber | PropDate | PropOptions)[];
requestListData: RequestData;
prefix?: string;
}>(),
{
id: '',
@ -128,7 +129,7 @@ defineEmits<{
<nav class="q-ml-auto row" v-if="!readonly">
<UndoButton
v-if="state.isEdit"
id="btn-info-basic-undo"
:id="`btn-properties-${props.prefix || 'nome'}-info-basic-undo`"
icon-only
type="button"
@click.stop="triggerUndo"
@ -136,13 +137,14 @@ defineEmits<{
<SaveButton
v-if="state.isEdit"
id="btn-info-basic-save"
:id="`btn-properties-${props.prefix || 'nome'}-info-basic-save`"
icon-only
type="submit"
@click.stop="triggerSubmit"
/>
<EditButton
v-if="!state.isEdit"
id="btn-info-basic-edit"
:id="`btn-properties-${props.prefix || 'nome'}-info-basic-edit`"
icon-only
@click.stop="triggerEdit"
type="button"

View file

@ -881,6 +881,7 @@ function toEmployee(employee: RequestData['employee']) {
:readonly="value._readonly"
ref="refDocumentExpansion"
:attributes="value.attributes"
:prefix="value.productService.product.name"
@change-status="
(opt) => {
triggerChangeStatusFile({
@ -928,6 +929,7 @@ function toEmployee(employee: RequestData['employee']) {
<MessengerExpansion
v-if="value._messengerExpansion"
:readonly="value._readonly"
:prefix="value.productService.product.name"
:default-messenger="
value.stepStatus[pageState.currentStep - 1]
? undefined
@ -953,6 +955,7 @@ function toEmployee(employee: RequestData['employee']) {
<DutyExpansion
v-if="value._dutyExpansion"
:readonly="value._readonly"
:prefix="value.productService.product.name"
:step="{
step: pageState.currentStep,
requestWorkId: value.id || '',
@ -966,6 +969,7 @@ function toEmployee(employee: RequestData['employee']) {
v-if="value._formExpansion"
:request-work-id="value.id"
:readonly="value._readonly"
:prefix="value.productService.product.name"
:step="{
step: pageState.currentStep,
requestWorkId: value.id || '',
@ -979,6 +983,7 @@ function toEmployee(employee: RequestData['employee']) {
:request-list-data="data"
:id="value.id"
:readonly="value._readonly"
:prefix="value.productService.product.name"
:properties-to-show="
value.productService.work?.attributes.workflowStep[
pageState.currentStep - 1

View file

@ -190,6 +190,8 @@ function handleCheckAll() {
>
<q-th v-if="checkable">
<q-checkbox
:for="`list-checkbox-all`"
:id="`list-checkbox-all`"
v-if="selected.length > 0"
:model-value="
selected.length ===
@ -229,6 +231,8 @@ function handleCheckAll() {
>
<q-td v-if="checkable">
<q-checkbox
:for="`list-checkbox-${props.rowIndex + 1}`"
:id="`list-checkbox-${props.rowIndex + 1}`"
:disable="
selected.length > 0 &&
!listSameArea.includes(

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
// NOTE: Library
import { computed, onMounted, reactive, watch, ref } from 'vue';
import { computed, onMounted, onUnmounted, reactive, watch, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
@ -86,6 +86,7 @@ async function fetchTaskOrderList(opts?: { page?: number; pageSize?: number }) {
});
}
if (res) {
taskOrderStore.getTaskOrderStats();
data.value = res.result;
pageState.total = res.total;
pageMax.value = Math.ceil(res.total / pageSize.value);
@ -146,6 +147,10 @@ async function deleteTaskOrder(id: string) {
});
}
function handleWindowFocus() {
fetchTaskOrderList();
}
onMounted(async () => {
pageState.gridView = $q.screen.lt.md ? true : false;
navigatorStore.current.title = 'taskOrder.title';
@ -156,6 +161,12 @@ onMounted(async () => {
if (route.query['tab'] && typeof route.query['tab'] === 'string') {
pageState.currentTab = route.query['tab'];
}
window.addEventListener('focus', handleWindowFocus);
});
onUnmounted(() => {
window.removeEventListener('focus', handleWindowFocus);
});
watch(
@ -218,7 +229,11 @@ watch(
color: hsl(var(--info-bg));
"
>
{{ Object.values(stats).reduce((s, v) => s + v, 0) }}
{{
pageState.isMessenger
? pageState.total
: stats[pageState.currentTab as TaskOrderStatus]
}}
</q-badge>
<q-btn
class="q-ml-sm"

View file

@ -295,6 +295,7 @@ function assignTempGroup() {
class="bordered-b"
>
<q-expansion-item
:id="`expansion-product-${product.code}`"
dense
class="overflow-hidden"
switch-toggle-side
@ -348,6 +349,7 @@ function assignTempGroup() {
</FormGroupHead>
<div class="q-pa-md full-width">
<TableEmployee
:id="`table-employee-${product.code}`"
checkbox-on
check-all
select-ready
@ -371,9 +373,15 @@ function assignTempGroup() {
</section>
<template #footer>
<CancelButton class="q-ml-auto" outlined @click="close" />
<CancelButton
id="btn-dialog-cancel"
class="q-ml-auto"
outlined
@click="close"
/>
<SaveButton
:label="$t('general.select')"
id="btn-dialog-select"
class="q-ml-sm"
icon="mdi-check"
solid

View file

@ -225,6 +225,7 @@ function disableCheckAll() {
<template>
<q-table
flat
id="table-employee"
bordered
row-key="id"
v-bind="props"
@ -273,6 +274,7 @@ function disableCheckAll() {
>
<q-th v-if="checkboxOn" class="relative-position">
<q-checkbox
id="checkbox-check-all"
v-if="checkAll"
:disable="disableCheckAll()"
:model-value="
@ -305,6 +307,7 @@ function disableCheckAll() {
class="absolute-right row items-center"
>
<q-btn
id="btn-change-all-status"
flat
dense
rounded
@ -339,6 +342,7 @@ function disableCheckAll() {
{{ $t(`taskOrder.status.Complete`) }}
</q-item>
<q-item
id="menu-item-redo"
clickable
v-close-popup
class="items-center"
@ -358,6 +362,7 @@ function disableCheckAll() {
{{ $t(`taskOrder.status.Redo`) }}
</q-item>
<q-item
id="menu-item-restart"
clickable
v-close-popup
class="items-center"
@ -379,6 +384,7 @@ function disableCheckAll() {
</q-list>
<q-list v-if="!validate" dense>
<q-item
id="menu-item-success"
clickable
v-close-popup
class="items-center"
@ -398,6 +404,7 @@ function disableCheckAll() {
{{ $t(`taskOrder.status.Success`) }}
</q-item>
<q-item
id="menu-item-failed"
clickable
v-close-popup
class="items-center"
@ -480,6 +487,7 @@ function disableCheckAll() {
<q-td>
<span
class="cursor-pointer link"
:id="`link-request-list-${props.row.request.code}`"
@click="goToRequestList(props.row.request.id)"
>
{{ props.row.request.code }}
@ -489,6 +497,7 @@ function disableCheckAll() {
<q-td>
<span
class="cursor-pointer link"
:id="`link-quotation-${props.row.request.quotation?.code}`"
@click="goToQuotation(props.row.request.quotation)"
>
{{ props.row.request.quotation?.code }}
@ -496,7 +505,11 @@ function disableCheckAll() {
</q-td>
<q-td v-if="stepOn" class="text-left">
<div v-if="props.row._template" class="column text-left">
<div
v-if="props.row._template"
class="column text-left"
:id="`template-step-${props.row.request.code}`"
>
<span>{{ props.row._template.templateName }}</span>
<span class="app-text-muted text-caption">
{{ $t('flow.stepNo', { msg: props.row._template.step }) }}
@ -522,7 +535,10 @@ function disableCheckAll() {
</template>
</q-img>
</q-avatar>
<div class="column text-left q-ml-sm">
<div
class="column text-left q-ml-sm"
:id="`employee-name-${props.row.request.employee?.code}`"
>
<div>
{{ getEmployeeName(props.row, { locale: $i18n.locale }) }}
</div>
@ -532,6 +548,7 @@ function disableCheckAll() {
</div>
</div>
<Icon
:id="`icon-gender-${props.row.request.employee?.code}`"
class="q-ml-md"
:class="`app-text-${props.row.request.employee?.gender}`"
:icon="`material-symbols:${props.row.request.employee?.gender}`"
@ -539,13 +556,18 @@ function disableCheckAll() {
/>
</div>
</q-td>
<q-td>{{ calculateAge(props.row.request.employee?.dateOfBirth) }}</q-td>
<q-td>
<q-td :id="`employee-age-${props.row.request.employee?.code}`">
{{ calculateAge(props.row.request.employee?.dateOfBirth) }}
</q-td>
<q-td :id="`employee-nationality-${props.row.request.employee?.code}`">
{{
useOptionStore().mapOption(props.row.request.employee?.nationality)
}}
</q-td>
<q-td>
<q-td
:id="`quotation-due-date-${props.row.request.quotation?.code}`"
v-if="!statusOn"
>
{{
dateFormatJS({
date: props.row.request.quotation?.dueDate,
@ -555,13 +577,16 @@ function disableCheckAll() {
})
}}
</q-td>
<q-td>
<q-td
:id="`expiration-date-${props.row.request.quotation?.code}`"
v-if="!statusOn"
>
<ExpirationDate
:expiration-date="new Date(props.row.request.quotation?.dueDate)"
/>
</q-td>
<q-td>
<q-td v-if="!statusOn">
<BadgeComponent
v-if="props.row.request.quotation?.urgent"
icon="mdi-fire"

View file

@ -114,6 +114,7 @@ const emit = defineEmits<{
</script>
<template>
<q-table
id="table-task-order"
v-bind="props"
:columns="column"
bordered
@ -133,7 +134,11 @@ const emit = defineEmits<{
:props="props"
>
<q-th v-if="selection !== 'none'">
<q-checkbox v-model="props.selected" size="sm" />
<q-checkbox
id="checkbox-select-all-task"
v-model="props.selected"
size="sm"
/>
</q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label && $t(col.label) }}
@ -149,13 +154,21 @@ const emit = defineEmits<{
>
<q-tr class="text-center" :class="{ urgent: props.row.urgent }">
<q-td v-if="selection !== 'none'">
<q-checkbox v-model="props.selected" size="sm" />
<q-checkbox
:id="`checkbox-task-${props.row.code}`"
v-model="props.selected"
size="sm"
/>
</q-td>
<q-td v-if="visibleColumns.includes('order')">
{{ props.rowIndex + 1 }}
</q-td>
<q-td v-if="visibleColumns.includes('taskName')" class="text-left">
<div>
<q-td
v-if="visibleColumns.includes('taskName')"
class="text-left"
:id="`task-name-${props.row.code}`"
>
<div :id="`task-name-div-${props.row.code}`">
{{ props.row.taskName || '-' }}
<q-tooltip :delay="300">
{{ props.row.taskName || '-' }}
@ -170,31 +183,51 @@ const emit = defineEmits<{
}}
</div>
</q-td>
<q-td v-if="visibleColumns.includes('issueBranch')">
<q-td
v-if="visibleColumns.includes('issueBranch')"
:id="`task-issue-branch-${props.row.code}`"
>
{{
$i18n.locale === 'eng'
? props.row.registeredBranch.nameEN || '-'
: props.row.registeredBranch.name || '-'
}}
</q-td>
<q-td v-if="visibleColumns.includes('institution')">
<q-td
v-if="visibleColumns.includes('institution')"
:id="`task-institution-${props.row.code}`"
>
{{
$i18n.locale === 'eng'
? props.row.institution.nameEN || '-'
: props.row.institution.name || '-'
}}
</q-td>
<q-td v-if="visibleColumns.includes('createdAt')">
<q-td
v-if="visibleColumns.includes('createdAt')"
:id="`task-created-at-${props.row.code}`"
>
{{ dateFormatJS({ date: props.row.createdAt }) || '-' }}
{{ dateFormatJS({ date: props.row.createdAt, timeOnly: true }) }}
</q-td>
<q-td v-if="visibleColumns.includes('createdBy')" class="text-left">
<q-td
v-if="visibleColumns.includes('createdBy')"
class="text-left"
:id="`task-created-by-${props.row.code}`"
>
{{ getCreatedByName(props.row, $i18n) }}
</q-td>
<q-td v-if="visibleColumns.includes('contactTel')">
<q-td
v-if="visibleColumns.includes('contactTel')"
:id="`task-contact-tel-${props.row.code}`"
>
{{ props.row.contactTel || '-' }}
</q-td>
<q-td v-if="visibleColumns.includes('contactName')" class="text-left">
<q-td
v-if="visibleColumns.includes('contactName')"
class="text-left"
:id="`task-contact-name-${props.row.code}`"
>
{{ props.row.contactName || '-' }}
</q-td>
<q-td v-if="visibleColumns.includes('taskStatus')">
@ -202,11 +235,12 @@ const emit = defineEmits<{
hide-icon
:hsla-color="taskOrderStatus(props.row.taskOrderStatus, 'color')"
:title="$t(taskOrderStatus(props.row.taskOrderStatus, 'status'))"
:id="`badge-task-status-${props.row.code}`"
/>
</q-td>
<q-td v-if="selection === 'none'">
<q-btn
:id="`btn-eye-${props.row.taskName}`"
:id="`btn-view-task-${props.row.code}`"
icon="mdi-eye-outline"
size="sm"
dense
@ -221,7 +255,7 @@ const emit = defineEmits<{
canAccess('taskOrder', 'edit')
"
:hide-delete="!canAccess('taskOrder', 'create')"
:idName="`btn-kebab-${props.row.taskName}`"
:idName="`btn-kebab-${props.row.code}`"
status="'ACTIVE'"
hide-toggle
@view="$emit('view', props.row)"
@ -233,6 +267,7 @@ const emit = defineEmits<{
<q-btn
dense
flat
:id="`btn-sub-row-${props.row.code}`"
class="rounded"
@click.stop="
() => {

View file

@ -60,16 +60,25 @@ function inactiveCheck() {
</script>
<template>
<div
:id="`readonly-status-${status}`"
:for="`readonly-status-${status}`"
v-if="readonly"
class="row rounded bordered surface-2 items-center justify-center q-pa-xs no-wrap"
:style="`color: hsl(var(--${currStatus?.color}-bg))`"
>
<q-icon :name="currStatus?.icon" class="q-pr-xs" size="xs" />
{{ $t(`taskOrder.status.${status}`) }}
<q-icon
:id="`readonly-status-icon-${status}`"
:name="currStatus?.icon"
class="q-pr-xs"
size="xs"
/>
<span :id="`readonly-status-label-${status}`">{{ $t(`taskOrder.status.${status}`) }}</span>
</div>
<div v-else class="row items-center justify-center no-wrap">
<q-btn-dropdown
:id="`btn-dropdown-status-${status}`"
:for="`btn-dropdown-status-${status}`"
dense
unelevated
:label="
@ -110,6 +119,8 @@ function inactiveCheck() {
>
<q-list v-if="!noAction" dense>
<q-item
:for="`menu-item-status-${v.value}`"
:id="`menu-item-status-${v.value}`"
v-for="(v, index) in type === 'order'
? {
Success: taskStatusOrderToggle.filter(
@ -147,6 +158,8 @@ function inactiveCheck() {
</q-btn-dropdown>
<q-btn
:id="`btn-failed-remark-${status}`"
:for="`btn-failed-remark-${status}`"
v-if="currStatus?.value === TaskStatus.Failed"
flat
dense
@ -186,9 +199,9 @@ function inactiveCheck() {
}
:deep(
.hide-icon
i.q-icon.mdi.mdi-chevron-down.q-btn-dropdown__arrow.q-btn-dropdown__arrow-container
) {
.hide-icon
i.q-icon.mdi.mdi-chevron-down.q-btn-dropdown__arrow.q-btn-dropdown__arrow-container
) {
display: none;
}
</style>

View file

@ -202,40 +202,44 @@ onMounted(async () => {
})
.reduce(
(a, c) => {
const priceNoVat = c.product.serviceChargeVatIncluded
? c.pricePerUnit / (1 + (config.value?.vat || 0.07))
: c.pricePerUnit;
const adjustedPriceWithVat = precisionRound(
priceNoVat * (1 + (config.value?.vat || 0.07)),
);
const adjustedPriceNoVat =
adjustedPriceWithVat / (1 + (config.value?.vat || 0.07));
const priceDiscountNoVat = adjustedPriceNoVat * c.amount - c.discount;
const vatFactor = c.product.serviceChargeCalcVat
? (config.value?.vat ?? 0.07)
: 0;
const rawVatTotal = priceDiscountNoVat * (config.value?.vat || 0.07);
const rawVat = rawVatTotal / c.amount;
const pricePerUnit =
precisionRound(c.product.serviceCharge * (1 + vatFactor)) /
(1 + vatFactor);
const amount = c.amount;
const discount = c.discount || 0;
const price =
(pricePerUnit * amount * (1 + vatFactor) - discount) /
(1 + vatFactor);
const vat = price * vatFactor;
product.value.push({
id: c.product.id,
code: c.product.code,
detail: c.product.name,
priceUnit: precisionRound(priceNoVat),
priceUnit: precisionRound(pricePerUnit),
amount: c.amount,
discount: c.discount,
vat: c.product.serviceChargeCalcVat ? precisionRound(rawVat) : 0,
vat: c.product.serviceChargeCalcVat ? precisionRound(vat) : 0,
value: precisionRound(
priceNoVat * c.amount +
(c.product.serviceChargeCalcVat ? rawVatTotal : 0),
pricePerUnit * c.amount +
(c.product.serviceChargeCalcVat ? vat : 0),
),
});
a.totalPrice = a.totalPrice + priceDiscountNoVat;
a.totalDiscount = a.totalDiscount + Number(c.discount);
a.vat = c.product.calcVat ? a.vat + rawVatTotal : a.vat;
a.vatExcluded = c.product.calcVat
a.totalPrice = precisionRound(a.totalPrice + price + discount);
a.totalDiscount = precisionRound(a.totalDiscount + discount);
a.vat = precisionRound(a.vat + vat);
a.vatExcluded = c.product.serviceChargeCalcVat
? a.vatExcluded
: precisionRound(a.vatExcluded + priceDiscountNoVat);
a.finalPrice = a.totalPrice - a.totalDiscount + a.vat;
: precisionRound(a.vatExcluded + price);
a.finalPrice = precisionRound(a.totalPrice - a.totalDiscount + a.vat);
return a;
},
{
@ -311,7 +315,7 @@ function closeAble() {
border-bottom: 2px solid var(--main);
"
>
{{ $t('taskOrder.goodReceipt') }}
{{ $t('preview.productList') }}
</span>
<table ref="elements" class="q-mb-sm" cellpadding="0" style="width: 100%">
@ -335,7 +339,9 @@ function closeAble() {
{{ formatNumberDecimal(v.priceUnit, 2) }}
</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.discount, 2) }}
<template v-if="v.discount !== 0">
{{ formatNumberDecimal(v.discount, 2) }} ฿
</template>
</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.vat, 2) }}
@ -346,7 +352,6 @@ function closeAble() {
</tr>
</tbody>
</table>
<table
style="width: 40%; margin-left: auto"
class="q-mb-md"
@ -398,7 +403,9 @@ function closeAble() {
<td class="text-right">
{{
formatNumberDecimal(
summaryPrice.totalPrice - summaryPrice.totalDiscount,
summaryPrice.totalPrice -
summaryPrice.totalDiscount -
summaryPrice.vatExcluded,
2,
)
}}

View file

@ -64,8 +64,8 @@ defineProps<{
})
}}
</span>
<span>เลขประจำตวผเสยภาษ {{ branch.taxNo }}</span>
<span>เบอรโทร {{ branch.telephoneNo }}</span>
<span>{{ $t('branch.form.taxNo') }} {{ branch.taxNo }}</span>
<span>{{ $t('taskOrder.telephone') }} {{ branch.telephoneNo }}</span>
<span>{{ branch.webUrl }}</span>
</article>
<article>

View file

@ -25,6 +25,7 @@ const fileData = defineModel<
</script>
<template>
<q-expansion-item
id="expansion-additional-file"
dense
class="overflow-hidden bordered full-width"
switch-toggle-side
@ -41,6 +42,7 @@ const fileData = defineModel<
<main class="q-px-md q-py-sm surface-1">
<UploadFileSection
id="upload-additional-file"
multiple
:layout="$q.screen.gt.sm ? 'column' : 'row'"
:readonly

View file

@ -22,6 +22,7 @@ const contactTel = defineModel<string>('contactTel');
</script>
<template>
<q-expansion-item
id="expansion-document"
default-opened
dense
class="overflow-hidden bordered full-width"
@ -38,13 +39,16 @@ const contactTel = defineModel<string>('contactTel');
<main class="q-px-md q-py-sm surface-1 row q-col-gutter-sm">
<SelectBranch
id="select-issue-branch"
:readonly
required
class="col-md-4 col-12"
:label="`${$t('taskOrder.issueBranch')}${$i18n.locale === 'tha' ? $t('taskOrder.title') : ''}`"
v-model:value="registeredBranchId"
auto-select-on-single
/>
<SelectInstitution
id="select-agencies"
:readonly
required
class="col-md-4 col-12"
@ -55,6 +59,7 @@ const contactTel = defineModel<string>('contactTel');
auto-select-on-single
/>
<DatePicker
id="datepicker-issue-date"
:label="$t('taskOrder.issueDate')"
class="col-md-2 col-6"
:model-value="issueDate || new Date(Date.now())"
@ -62,6 +67,7 @@ const contactTel = defineModel<string>('contactTel');
:disabled="!readonly"
/>
<q-input
id="input-task-code"
:label="$t('taskOrder.code')"
outlined
dense
@ -72,6 +78,7 @@ const contactTel = defineModel<string>('contactTel');
/>
<q-input
id="input-task-name"
:readonly
:label="$t('general.name', { msg: $t('taskOrder.title') })"
outlined
@ -80,6 +87,7 @@ const contactTel = defineModel<string>('contactTel');
v-model="taskName"
/>
<q-input
id="input-contact-name"
:readonly
:label="$t('taskOrder.contactName')"
outlined
@ -88,6 +96,7 @@ const contactTel = defineModel<string>('contactTel');
v-model="contactName"
/>
<q-input
id="input-contact-tel"
:readonly
:label="$t('general.telephone')"
outlined
@ -96,6 +105,7 @@ const contactTel = defineModel<string>('contactTel');
v-model="contactTel"
/>
<q-input
id="input-made-by"
:readonly
:label="$t('taskOrder.madeBy')"
outlined

View file

@ -32,6 +32,7 @@ const summaryPrice = defineModel<{
</script>
<template>
<q-expansion-item
id="expansion-payment"
dense
class="overflow-hidden bordered full-width"
switch-toggle-side
@ -47,6 +48,7 @@ const summaryPrice = defineModel<{
<main class="q-px-md q-py-sm surface-1">
<QuotationFormInfo
id="form-info-payment"
task-order
:task-order-complete="complete"
v-model:pay-type="payType"

View file

@ -14,6 +14,10 @@ import { storeToRefs } from 'pinia';
import BadgeComponent from 'src/components/BadgeComponent.vue';
import { TaskStatus } from 'src/stores/task-order/types';
import { useTaskOrderForm } from '../form';
const taskOrderFormStore = useTaskOrderForm();
const currentBtnOpen = ref<boolean[]>([]);
const configStore = useConfigStore();
const { data: config } = storeToRefs(configStore);
@ -61,60 +65,28 @@ function openList(index: number) {
}
function calcPricePerUnit(product: RequestWork['productService']['product']) {
const val = props.creditNote
? props.agentPrice
? product.agentPrice
: product.price
: product.serviceCharge;
if (
product[
props.creditNote
? props.agentPrice
? 'agentPriceCalcVat'
: 'calcVat'
: 'serviceChargeCalcVat'
]
) {
return val / (1 + (config.value?.vat || 0.07));
} else {
return val;
}
return product.serviceCharge;
}
function calcPrice(
product: RequestWork['productService']['product'],
amount: number,
) {
const pricePerUnit = props.creditNote
? props.agentPrice
? product.agentPrice
: product.price
: product.serviceCharge;
const discount =
taskProduct.value.find((v) => v.productId === product.id)?.discount || 0;
const priceNoVat = product[
props.creditNote
? props.agentPrice
? 'agentPriceVatIncluded'
: 'vatIncluded'
: 'serviceChargeVatIncluded'
]
? pricePerUnit / (1 + (config.value?.vat || 0.07))
: pricePerUnit;
const priceDiscountNoVat = priceNoVat * amount - discount;
const rawVatTotal = product[
props.creditNote
? props.agentPrice
? 'agentPriceCalcVat'
: 'calcVat'
: 'serviceChargeCalcVat'
]
? priceDiscountNoVat * (config.value?.vat || 0.07)
const vatFactor = product.serviceChargeCalcVat
? (config.value?.vat ?? 0.07)
: 0;
return precisionRound(priceNoVat * amount + rawVatTotal);
const discount =
taskProduct.value.find((v) => v.productId === product.id)?.discount || 0;
const pricePerUnit = precisionRound(product.serviceCharge);
const price =
(pricePerUnit * amount * (1 + vatFactor) - discount) / (1 + vatFactor);
const vat = price * vatFactor;
return price + vat;
}
function taskOrderStatus(value: TaskStatus) {
@ -139,6 +111,7 @@ function taskOrderStatus(value: TaskStatus) {
</script>
<template>
<q-expansion-item
id="expansion-product-list"
dense
class="overflow-hidden bordered full-width"
switch-toggle-side
@ -151,9 +124,11 @@ function taskOrderStatus(value: TaskStatus) {
<span
class="row items-center justify-between full-width"
style="min-height: 31.01px"
id="header-product-list"
>
{{ $t('general.information', { msg: $t('taskOrder.productList') }) }}
<AddButton
id="btn-add-product"
icon-only
@click.stop="$emit('addProduct')"
v-if="!readonly"
@ -163,6 +138,7 @@ function taskOrderStatus(value: TaskStatus) {
<main class="q-px-md q-py-sm surface-1">
<q-table
id="table-product-list"
:columns="
creditNote
? productColumn.filter(
@ -208,7 +184,7 @@ function taskOrderStatus(value: TaskStatus) {
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
>
<q-tr class="text-center">
<q-td>
<q-td :id="`product-order-${props.rowIndex}`">
{{ props.rowIndex + 1 }}
</q-td>
<q-td>
@ -234,20 +210,21 @@ function taskOrderStatus(value: TaskStatus) {
</q-td>
<q-td class="text-left" v-if="!creditNote">
<BadgeComponent
:id="`badge-status-${props.row.product.code}`"
hide-icon
:hsla-color="taskOrderStatus(props.row.product.taskStatus)"
:title="`${$t(`taskOrder.status.${props.row.product.taskStatus}`)} ${!!props.row.product.totalNotStatusComplete ? $t('general.totalPeople', { meg: props.row.product.totalNotStatusComplete }) : ''}`"
/>
</q-td>
<q-td>
<q-td :id="`product-amount-${props.row.product.code}`">
{{ props.row.list.length }}
</q-td>
<q-td class="text-right">
<q-td :id="`product-price-per-unit-${props.row.product.code}`" class="text-right">
{{
formatNumberDecimal(
calcPricePerUnit(props.row.product) +
(props.row.product.calcVat
(props.row.product.serviceChargeCalcVat
? calcPricePerUnit(props.row.product) *
(config?.vat || 0.07)
: 0),
@ -258,6 +235,7 @@ function taskOrderStatus(value: TaskStatus) {
<!-- TODO: display price detail -->
<q-td align="center" v-if="!creditNote">
<q-input
:id="`input-discount-${props.row.product.code}`"
:readonly
:bg-color="readonly ? 'transparent' : ''"
dense
@ -295,37 +273,49 @@ function taskOrderStatus(value: TaskStatus) {
/>
</q-td>
<!-- before vat -->
<q-td class="text-right" v-if="!creditNote">
<q-td
:id="`product-price-before-vat-${props.row.product.code}`"
class="text-right"
v-if="!creditNote"
>
{{
formatNumberDecimal(
calcPricePerUnit(props.row.product) * props.row.list.length -
(taskProduct.find(
(v) => v.productId === props.row.product.id,
)?.discount || 0),
2,
props.row.product.serviceChargeCalcVat
? (calcPricePerUnit(props.row.product) *
props.row.list.length *
(1 + (config?.vat || 0.07))) /
(1 + (config?.vat || 0.07))
: calcPricePerUnit(props.row.product) *
props.row.list.length -
taskProduct.find(
(v) => v.productId === props.row.product.id,
)?.discount || 0,
)
}}
</q-td>
<!-- vat -->
<q-td class="text-right" v-if="!creditNote">
<q-td
:id="`product-vat-${props.row.product.code}`"
class="text-right"
v-if="!creditNote"
>
{{
formatNumberDecimal(
props.row.product.calcVat
? precisionRound(
(calcPricePerUnit(props.row.product) *
props.row.list.length -
(taskProduct.find(
(v) => v.productId === props.row.product.id,
)?.discount || 0)) *
(config?.vat || 0.07),
)
props.row.product.serviceChargeCalcVat
? ((calcPricePerUnit(props.row.product) *
props.row.list.length *
(1 + (config?.vat || 0.07)) -
taskProduct.find(
(v) => v.productId === props.row.product.id,
)?.discount || 0) /
(1 + (config?.vat || 0.07))) *
0.07
: 0,
2,
)
}}
</q-td>
<!-- total -->
<q-td class="text-right">
<q-td :id="`product-total-price-${props.row.product.code}`" class="text-right">
{{
formatNumberDecimal(
calcPrice(props.row.product, props.row.list.length),
@ -335,6 +325,7 @@ function taskOrderStatus(value: TaskStatus) {
</q-td>
<q-td>
<q-btn
:id="`btn-toggle-employee-${props.row.product.code}`"
dense
flat
class="rounded"
@ -358,6 +349,7 @@ function taskOrderStatus(value: TaskStatus) {
<q-tr v-show="currentBtnOpen[props.rowIndex]" :props="props">
<q-td colspan="100%" style="padding: 16px">
<TableEmployee
:id="`table-employee-in-product-${props.row.product.code}`"
:step-on="!creditNote"
:status-on="creditNote"
:rows="props.row.list"
@ -375,7 +367,9 @@ function taskOrderStatus(value: TaskStatus) {
})
}}
</span>
<div class="surface-3 q-px-sm rounded">{{ taskList.length }}</div>
<div class="surface-3 q-px-sm rounded" id="total-product-count">
{{ taskList.length }}
</div>
</div>
</main>
</q-expansion-item>

View file

@ -33,6 +33,7 @@ const getToolbarConfig = computed(() => {
</script>
<template>
<q-expansion-item
id="expansion-remark"
dense
class="overflow-hidden bordered full-width"
switch-toggle-side
@ -48,6 +49,7 @@ const getToolbarConfig = computed(() => {
<main class="surface-1 q-pa-md full-width">
<q-editor
id="editor-remark"
dense
:readonly="readonly || !remarkWrite"
:model-value="
@ -90,6 +92,7 @@ const getToolbarConfig = computed(() => {
<template v-if="!readonly" v-slot:toggle>
<div class="text-caption row no-wrap q-px-sm">
<MainButton
id="btn-remark-view"
:solid="!remarkWrite"
icon="mdi-eye-outline"
color="0 0% 40%"
@ -102,6 +105,7 @@ const getToolbarConfig = computed(() => {
{{ $t('general.view', { msg: $t('general.example') }) }}
</MainButton>
<MainButton
id="btn-remark-edit"
:solid="remarkWrite"
icon="mdi-pencil-outline"
color="0 0% 40%"

View file

@ -47,11 +47,17 @@ defineProps<{
<div class="row q-col-gutter-sm q-px-md q-py-sm">
<DataDisplay
class="col-md col-6"
id="dd-recipient"
:label="$t('taskOrder.recipientOrSender')"
>
<template #value>
<q-avatar size="md" class="q-mr-xs">
<q-img class="text-center" :ratio="1" :src="contactUrl">
<q-img
:id="`img-avatar-${contactName}`"
class="text-center"
:ratio="1"
:src="contactUrl"
>
<template #error>
<div
class="no-padding full-width full-height flex items-center justify-center"
@ -59,6 +65,7 @@ defineProps<{
>
<q-img
v-if="gender"
:id="`img-gender-${contactName}`"
:src="
gender === 'male'
? '/no-img-man.png'
@ -67,6 +74,7 @@ defineProps<{
/>
<q-icon
v-else
:id="`icon-avatar-${contactName}`"
size="sm"
name="mdi-account-outline"
style="color: white"
@ -81,18 +89,21 @@ defineProps<{
<DataDisplay
class="col-md col-6"
id="dd-telephone"
:label="$t('general.telephone')"
:value="contactTel || '-'"
/>
<DataDisplay
class="col-md col-6"
id="dd-email"
:label="$t('form.email')"
:value="email || '-'"
/>
<DataDisplay
class="col-md col-6"
id="dd-work-start-date"
:label="$t('taskOrder.workStartDate')"
>
<template #value>
@ -106,6 +117,7 @@ defineProps<{
<DataDisplay
class="col-md col-6"
id="dd-work-submission-date"
:label="$t('taskOrder.workSubmissionDate')"
>
<template #value>
@ -117,9 +129,14 @@ defineProps<{
</template>
</DataDisplay>
<DataDisplay class="col-md col-6" :label="$t('general.status')">
<DataDisplay
id="dd-status"
class="col-md col-6"
:label="$t('general.status')"
>
<template #value>
<BadgeComponent
:id="`badge-status-${contactName}`"
v-if="status"
hide-icon
:title="

View file

@ -128,32 +128,33 @@ function getPrice(
})[];
}[],
) {
return list.reduce(
const value = list.reduce(
(a, c) => {
const pricePerUnit = c.product.serviceCharge;
const vatFactor = c.product.serviceChargeCalcVat
? (config.value?.vat ?? 0.07)
: 0;
const pricePerUnit =
precisionRound(c.product.serviceCharge * (1 + vatFactor)) /
(1 + vatFactor);
const amount = c.list.length;
const discount =
taskProduct.value.find((v) => v.productId === c.product.id)?.discount ||
0;
const priceNoVat = c.product.serviceChargeVatIncluded
? pricePerUnit / (1 + (config.value?.vat || 0.07))
: pricePerUnit;
const adjustedPriceWithVat = precisionRound(
priceNoVat * (1 + (config.value?.vat || 0.07)),
);
const adjustedPriceNoVat =
adjustedPriceWithVat / (1 + (config.value?.vat || 0.07));
const priceDiscountNoVat = adjustedPriceNoVat * amount - discount;
const rawVatTotal = priceDiscountNoVat * (config.value?.vat || 0.07);
const price =
(pricePerUnit * amount * (1 + vatFactor) - discount) / (1 + vatFactor);
a.totalPrice = a.totalPrice + priceDiscountNoVat;
a.totalDiscount = a.totalDiscount + Number(discount);
a.vat = c.product.serviceChargeCalcVat ? a.vat + rawVatTotal : a.vat;
const vat = price * vatFactor;
a.totalPrice = precisionRound(a.totalPrice + price + discount);
a.totalDiscount = precisionRound(a.totalDiscount + discount);
a.vat = precisionRound(a.vat + vat);
a.vatExcluded = c.product.serviceChargeCalcVat
? a.vatExcluded
: precisionRound(a.vatExcluded + priceDiscountNoVat);
a.finalPrice = a.totalPrice - a.totalDiscount + a.vat;
: precisionRound(a.vatExcluded + price);
a.finalPrice = precisionRound(a.totalPrice - a.totalDiscount + a.vat);
return a;
},
{
@ -164,6 +165,8 @@ function getPrice(
finalPrice: 0,
},
);
return value;
}
function getTemplateData(
@ -811,7 +814,7 @@ watch(
>
<section class="banner" :class="{ dark: $q.dark.isActive }"></section>
<div style="flex: 1" class="row items-center">
<RouterLink to="/task-order">
<RouterLink to="/task-order" id="link-task-order">
<q-img src="/icons/favicon-512x512.png" width="3rem" />
</RouterLink>
<span class="column text-h6 text-bold q-ml-md">
@ -860,6 +863,7 @@ watch(
<!-- TODO: replace step and status -->
<StateButton
v-for="i in statusTabForm"
:id="`btn-status-${i.title}`"
:key="i.title"
:label="$t(`taskOrder.${i.title}`)"
:status-active="i.active?.()"
@ -913,6 +917,7 @@ watch(
"
>
<DocumentExpansion
id="expansion-document"
:readonly="!['create', 'edit'].includes(state.mode || '')"
v-model:registered-branch-id="currentFormData.registeredBranchId"
v-model:institution-id="currentFormData.institutionId"
@ -932,6 +937,7 @@ watch(
/>
</q-form>
<ProductExpansion
id="expansion-product"
ref="refProductExpansion"
v-if="
view === TaskOrderStatus.Pending ||
@ -944,6 +950,7 @@ watch(
/>
<PaymentExpansion
id="expansion-payment"
v-model:summary-price="summaryPrice"
:complete="view === TaskOrderStatus.Complete"
v-if="
@ -953,6 +960,7 @@ watch(
/>
<AdditionalFileExpansion
id="expansion-additional-file"
:readonly="!['create', 'edit'].includes(state.mode || '')"
v-if="
view === TaskOrderStatus.Pending ||
@ -1011,6 +1019,7 @@ watch(
/>
<!-- TODO: blind remark, urgent -->
<RemarkExpansion
id="expansion-remark"
v-if="
view === TaskOrderStatus.Pending ||
view === TaskOrderStatus.Complete
@ -1035,6 +1044,7 @@ watch(
"
>
<InfoMessengerExpansion
:id="`expansion-messenger-${messengerIndex}`"
v-for="(v, messengerIndex) in messengerListGroup"
:key="messengerIndex"
:gender="getMessengerName(v.responsibleUser, { gender: true })"
@ -1070,6 +1080,7 @@ watch(
class="bordered-b"
>
<q-expansion-item
:id="`expansion-product-${productIndex}`"
dense
class="overflow-hidden"
switch-toggle-side
@ -1243,6 +1254,7 @@ watch(
<nav class="row justify-end">
<MainButton
class="q-mr-auto"
id="btn-view-example"
v-if="currentFormData.id"
outlined
icon="mdi-play-box-outline"
@ -1257,10 +1269,16 @@ 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'" />
<UndoButton
id="btn-undo"
outlined
@click="undo()"
v-if="state.mode === 'edit'"
/>
<CancelButton
v-if="state.mode !== 'edit'"
:label="$t('dialog.action.close')"
id="btn-close"
outlined
@click="closeTab()"
/>
@ -1272,12 +1290,14 @@ watch(
"
>
<SaveButton
id="btn-save"
v-if="state.mode && ['create', 'edit'].includes(state.mode)"
@click="() => formDocument.submit()"
solid
/>
<EditButton
v-else
id="btn-edit"
class="no-print"
@click="state.mode = 'edit'"
solid
@ -1285,6 +1305,7 @@ watch(
</template>
<SaveButton
v-if="
canAccess('taskOrder', 'edit') &&
state.mode !== 'create' &&
view === TaskOrderStatus.Validate &&
fullTaskOrder?.taskOrderStatus !== TaskOrderStatus.Pending &&
@ -1323,6 +1344,7 @@ watch(
"
:label="$t('taskOrder.confirmEndWork')"
icon="mdi-check"
id="btn-confirm-end-work"
solid
></SaveButton>
</div>
@ -1332,6 +1354,7 @@ watch(
<!-- SEC: Dialog -->
<SelectReadyRequestWork
id="dialog-select-request-work"
:task-list-group="taskListGroup"
:fetch-params="{ readyToTask: true }"
v-model:open="pageState.productDialog"

View file

@ -98,6 +98,7 @@ function tooltip(id: string) {
<main class="q-py-sm q-px-md scroll">
<q-select
:readonly
id="select-fail-task"
dense
outlined
multiple
@ -131,6 +132,7 @@ function tooltip(id: string) {
>
<template #selected-item="scope">
<q-chip
:id="`chip-fail-task-${taskStatusList[scope.index].code}`"
dense
:removable="!readonly"
@remove="scope.removeAtIndex(scope.index)"
@ -170,6 +172,7 @@ function tooltip(id: string) {
<template #option="scope">
<q-item
:id="`option-fail-task-${scope.opt.request.code}`"
clickable
v-bind="scope.itemProps"
class="row items-start col-12 no-padding"
@ -230,6 +233,7 @@ function tooltip(id: string) {
<SelectInput
:readonly
id="select-fail-type"
:option="[
{
label: $t('taskOrder.documentSubmitFailed'),
@ -260,6 +264,7 @@ function tooltip(id: string) {
<div v-if="failedType === 'other'" class="q-mt-sm rounded">
<q-editor
id="editor-fail-comment"
dense
flat
v-model="failedComment"
@ -279,8 +284,18 @@ function tooltip(id: string) {
</main>
<template #footer v-if="!readonly">
<CancelButton class="q-ml-auto" outlined @click="$emit('close')" />
<SaveButton class="q-ml-sm" solid @click="submit" />
<CancelButton
id="btn-fail-remark-cancel"
class="q-ml-auto"
outlined
@click="$emit('close')"
/>
<SaveButton
id="btn-fail-remark-save"
class="q-ml-sm"
solid
@click="submit"
/>
</template>
</DialogFormContainer>
</template>

View file

@ -394,8 +394,14 @@ watch(
{
label: $t('general.customer'),
value:
item.row.quotation.customerBranch.registerName ||
`${item.row.quotation.customerBranch?.firstName || '-'} ${item.row.quotation.customerBranch?.lastName || ''}`,
item.row.quotation.customerBranch.customer
.customerType === 'CORP'
? $i18n.locale === 'tha'
? item.row.quotation.customerBranch.registerName
: item.row.quotation.customerBranch.registerNameEN
: $i18n.locale === 'tha'
? `${item.row.quotation.customerBranch?.firstName || '-'} ${item.row.quotation.customerBranch?.lastName || ''}`
: `${item.row.quotation.customerBranch?.firstNameEN || '-'} ${item.row.quotation.customerBranch?.lastNameEN || ''}`,
},
{
label: $t('taskOrder.issueDate'),

View file

@ -83,11 +83,16 @@ defineEmits<{
<!-- NOTE: custom column will starts with # -->
<template v-if="!col.name.startsWith('#')">
<span>
{{
typeof col.field === 'string'
? props.row[col.field as keyof Invoice]
: col.field(props.row)
}}
<template v-if="typeof col.field === 'function'">
{{
typeof col.field(props.row) === 'object'
? col.field(props.row)[$i18n.locale]
: col.field(props.row)
}}
</template>
<template v-else>
{{ props.row[col.field as keyof Invoice] }}
</template>
</span>
</template>
<template v-if="col.name === '#order'">

View file

@ -29,7 +29,16 @@ export const columns = [
name: 'customer',
align: 'center',
label: 'general.customer',
field: (v: Invoice) => v.quotation.customerBranch.registerName,
field: (v: Invoice) =>
v.quotation.customerBranch.customer.customerType === 'CORP'
? {
tha: v.quotation.customerBranch.registerName,
eng: v.quotation.customerBranch.registerNameEN,
}
: {
tha: `${v.quotation.customerBranch.firstName || ''} ${v.quotation.customerBranch.lastName || ''}`,
eng: `${v.quotation.customerBranch.firstNameEN || ''} ${v.quotation.customerBranch.lastNameEN || ''}`,
},
},
{

View file

@ -31,6 +31,7 @@ import {
EditButton,
UndoButton,
} from 'src/components/button';
import { precisionRound } from 'src/utils/arithmetic';
import { DebitNote, useDebitNote } from 'src/stores/debit-note';
import { RequestWork } from 'src/stores/request-list/types';
import { storeToRefs } from 'pinia';
@ -40,6 +41,8 @@ import { useI18n } from 'vue-i18n';
import { QForm } from 'quasar';
import { getName } from 'src/services/keycloak';
import { RequestWorkStatus } from 'src/stores/request-list/types';
const route = useRoute();
const router = useRouter();
const creditNote = useCreditNote();
@ -49,6 +52,7 @@ const configStore = useConfigStore();
const { data: config } = storeToRefs(configStore);
const { t } = useI18n();
const agentPrice = ref<boolean>(false);
const refForm = ref<InstanceType<typeof QForm>>();
const creditNoteData = ref<CreditNote>();
const quotationData = ref<DebitNote | QuotationFull>();
@ -206,22 +210,23 @@ function getPrice(
) {
return list.reduce(
(a, c) => {
const pricePerUnit =
c.product.pricePerUnit - c.product.discount / c.product.amount;
const amount = c.list.length;
const priceNoVat = pricePerUnit;
const priceDiscountNoVat = priceNoVat * amount;
const value = creditNote.getfinalPriceCredit(c.list || []);
const rawVatTotal = priceDiscountNoVat * (config.value?.vat || 0.07);
a.totalPrice = precisionRound(a.totalPrice + value.totalPrice);
a.totalDiscount = precisionRound(a.totalDiscount + value.totalDiscount);
a.vat = precisionRound(a.vat + value.vat);
a.vatIncluded = precisionRound(a.vatIncluded + value.vatIncluded);
a.vatExcluded = precisionRound(a.vatExcluded + value.vatExcluded);
a.finalPrice = precisionRound(a.finalPrice + value.finalPrice);
a.totalPrice = a.totalPrice + priceDiscountNoVat;
a.vat = c.product.vat !== 0 ? a.vat + rawVatTotal : a.vat;
a.finalPrice = a.totalPrice + a.vat;
return a;
},
{
totalPrice: 0,
totalDiscount: 0,
vat: 0,
vatIncluded: 0,
vatExcluded: 0,
finalPrice: 0,
},
);
@ -296,6 +301,8 @@ async function getQuotation() {
if (!ret) return;
quotationData.value = ret;
agentPrice.value = quotationData.value.agentPrice;
}
async function submit() {

View file

@ -134,6 +134,11 @@ onMounted(async () => {
creditNote.getCreditNoteStats().then((res) => res && (stats.value = res));
getList();
window.addEventListener('focus', () => {
creditNote.getCreditNoteStats().then((res) => res && (stats.value = res));
getList();
});
});
watch(
@ -406,10 +411,12 @@ watch(
value:
item.row.quotation.customerBranch.customer
.customerType === 'CORP'
? item.row.quotation.customerBranch.registerName
? $i18n.locale === 'tha'
? item.row.quotation.customerBranch.registerName
: item.row.quotation.customerBranch.registerNameEN
: $i18n.locale === 'tha'
? `${item.row.quotation.customerBranch.firstName} ${item.row.quotation.customerBranch.lastName}`
: `${item.row.quotation.customerBranch.firstNameEN} ${item.row.quotation.customerBranch.lastNameEN}`,
? `${item.row.quotation.customerBranch.firstName || ''} ${item.row.quotation.customerBranch.lastName || ''}`
: `${item.row.quotation.customerBranch.firstNameEN || ''} ${item.row.quotation.customerBranch.lastNameEN || ''}`,
},
{
label: $t('requestList.quotationCode'),

View file

@ -8,7 +8,7 @@ import { columns } from './constants';
import KebabAction from 'src/components/shared/KebabAction.vue';
const creditNote = useCreditNote();
const { data, page } = storeToRefs(creditNote);
const { data, page, pageSize } = storeToRefs(creditNote);
const prop = defineProps<{
grid: boolean;
@ -26,7 +26,14 @@ const visible = computed(() =>
<template>
<q-table
:rows-per-page-options="[0]"
:rows="data.map((item, i) => ({ ...item, _index: i, _page: page }))"
:rows="
data.map((item, i) => ({
...item,
_index: i,
_page: page,
_pageSize: pageSize,
}))
"
:columns="visible"
:grid
hide-bottom
@ -52,7 +59,7 @@ const visible = computed(() =>
<template
v-slot:body="props: {
row: CreditNote & { _index: number; _page: number };
row: CreditNote & { _index: number; _page: number; _pageSize: number };
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
>
<q-tr :class="{ dark: $q.dark.isActive }" class="text-center">

View file

@ -32,8 +32,9 @@ export const columns = [
name: 'order',
align: 'center',
label: 'general.order',
field: (data: CreditNote & { _index: number; _page: number }) =>
data._page * (data._index + 1),
field: (
data: CreditNote & { _index: number; _page: number; _pageSize: number },
) => (data._page - 1) * data._pageSize + (data._index + 1),
},
{
name: 'code',

View file

@ -237,30 +237,12 @@ onMounted(async () => {
});
function calcPricePerUnit(product: RequestWork['productService']['product']) {
return product.vatIncluded
? (agentPrice.value ? product.agentPrice : product.price) /
(1 + (config.value?.vat || 0.07))
: agentPrice.value
? product.agentPrice
: product.price;
return agentPrice.value ? product.agentPrice : product.price;
}
function calcPrice(
product: RequestWork['productService']['product'],
amount: number,
vat: number = 0,
) {
const pricePerUnit = agentPrice.value ? product.agentPrice : product.price;
const priceNoVat = product.vatIncluded
? pricePerUnit / (1 + (config.value?.vat || 0.07))
: pricePerUnit;
const priceDiscountNoVat = priceNoVat * amount - 0;
const rawVatTotal =
vat === 0 ? 0 : priceDiscountNoVat * (config.value?.vat || 0.07);
return precisionRound(priceNoVat * amount + rawVatTotal);
function calcPrice(c: RequestWork[]): number {
const price = creditNoteStore.getfinalPriceCredit(c);
return price.finalPrice;
}
watch(elements, () => {});
@ -327,31 +309,15 @@ function closeAble() {
<th>{{ $t('preview.pricePerUnit') }}</th>
<th>{{ $t('preview.value') }}</th>
</tr>
{{ console.log(chunks) }}
{{ console.log(chunk) }}
<tr v-for="(v, i) in chunk" :key="i">
<td class="text-center">{{ i + 1 }}</td>
<td>{{ v.product.product.code }}</td>
<td>{{ v.product.product.name }}</td>
<td style="text-align: center">
{{
formatNumberDecimal(
calcPricePerUnit(v.product.product) +
(v.product.product.calcVat
? calcPricePerUnit(v.product.product) *
(config?.vat || 0.07)
: 0),
2,
)
}}
<td style="text-align: right">
{{ formatNumberDecimal(calcPricePerUnit(v.product.product), 2) }}
</td>
<td style="text-align: center">
{{
formatNumberDecimal(
calcPrice(v.product.product, v.list.length, v.product.vat),
2,
)
}}
<td style="text-align: right">
{{ formatNumberDecimal(calcPrice(v.list), 2) }}
</td>
</tr>
</tbody>
@ -409,8 +375,8 @@ function closeAble() {
{{
formatNumberDecimal(
summaryPrice.totalPrice -
summaryPrice.totalDiscount +
summaryPrice.vat,
summaryPrice.totalDiscount -
summaryPrice.vatExcluded,
2,
)
}}

View file

@ -79,8 +79,8 @@ function titleMode(mode: View): string {
})
}}
</span>
<span>เลขประจำตวผเสยภาษ {{ branch.taxNo }}</span>
<span>เบอรโทร {{ branch.telephoneNo }}</span>
<span>{{ $t('branch.form.taxNo') }} {{ branch.taxNo }}</span>
<span>{{ $t('taskOrder.telephone') }} {{ branch.telephoneNo }}</span>
<span>{{ branch.webUrl }}</span>
</article>
<article>
@ -105,8 +105,18 @@ function titleMode(mode: View): string {
})
}}
</span>
<span>เลขประจำตวผเสยภาษ {{ customer.citizenId }}</span>
<span>เบอรโทร {{ customer.telephoneNo }}</span>
<span>
{{
customer.customer.customerType === 'CORP'
? `${$t('customer.form.legalPersonNo')} `
: `${$t('customer.form.taxpayyerNo')} `
}}{{
customer.customer.customerType === 'CORP'
? customer.codeCustomer
: customer.citizenId
}}
</span>
<span>{{ $t('taskOrder.telephone') }} {{ customer.telephoneNo }}</span>
</article>
</section>
<section class="detail-quotation-info">

View file

@ -12,10 +12,13 @@ import { baseUrl, formatNumberDecimal } from 'src/stores/utils';
import { precisionRound } from 'src/utils/arithmetic';
import { productColumn } from '../constants';
import { useCreditNote } from 'src/stores/credit-note';
const configStore = useConfigStore();
const { data: config } = storeToRefs(configStore);
const currentExpanded = ref<boolean[]>([]);
const creditNote = useCreditNote();
defineProps<{
readonly?: boolean;
@ -44,15 +47,22 @@ function calcPricePerUnit(product: RequestWork['productService']) {
return product.pricePerUnit - product.discount / product.amount;
}
function calcPrice(c: RequestWork['productService'], amount: number) {
const pricePerUnit = c.pricePerUnit - c.discount / c.amount;
const priceNoVat = pricePerUnit;
const priceDiscountNoVat = priceNoVat * amount;
function calcVat(c: RequestWork['productService']) {
const vatFactor = c.product.serviceChargeCalcVat
? (config.value?.vat ?? 0.07)
: 0;
const rawVatTotal =
c.vat === 0 ? 0 : priceDiscountNoVat * (config.value?.vat || 0.07);
const price = precisionRound(
c.pricePerUnit * (1 + vatFactor) - c.discount / (1 + vatFactor),
);
return precisionRound(priceNoVat * amount + rawVatTotal);
const vat = (price / (1 + vatFactor)) * vatFactor;
return vat;
}
function calcPrice(c: RequestWork[]) {
const price = creditNote.getfinalPriceCredit(c);
return price.finalPrice;
}
</script>
<template>
@ -162,12 +172,7 @@ function calcPrice(c: RequestWork['productService'], amount: number) {
<!-- total -->
<q-td class="text-right">
{{
formatNumberDecimal(
calcPrice(props.row.product, props.row.list.length),
2,
)
}}
{{ formatNumberDecimal(calcPrice(props.row.list), 2) }}
</q-td>
<q-td>
<q-btn

View file

@ -209,47 +209,7 @@ const selectedWorker = ref<
}[];
})[]
>([]);
const selectedWorkerItem = computed(() => {
return [
...selectedWorker.value.map((e) => ({
foreignRefNo: e.employeePassport
? e.employeePassport[0]?.number || '-'
: '-',
employeeName:
locale.value === Lang.English
? `${e.firstNameEN} ${e.lastNameEN}`
: `${e.firstName || e.firstNameEN} ${e.lastName || e.lastNameEN}`,
birthDate: dateFormatJS({ date: e.dateOfBirth }),
gender: e.gender,
age: calculateAge(e.dateOfBirth),
nationality: optionStore.mapOption(e.nationality),
documentExpireDate:
e.employeePassport !== undefined &&
e.employeePassport[0]?.expireDate !== undefined
? dateFormatJS({ date: e.employeePassport[0]?.expireDate })
: '-',
imgUrl: e.selectedImage
? `${API_BASE_URL}/employee/${e.id}/image/${e.selectedImage}`
: '',
status: e.status,
})),
...newWorkerList.value.map((v: any) => ({
foreignRefNo: v.passportNo || '-',
employeeName:
locale.value === Lang.English
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName || v.firstNameEN} ${v.lastName || v.lastNameEN}`,
birthDate: dateFormatJS({ date: v.dateOfBirth }),
gender: v.gender,
age: calculateAge(v.dateOfBirth),
nationality: optionStore.mapOption(v.nationality),
documentExpireDate: '-',
imgUrl: '',
status: 'CREATED',
})),
];
});
const selectedWorkerItem = ref([]);
const selectedInstallmentNo = ref<number[]>([]);
const installmentAmount = ref<number>(0);
@ -374,7 +334,39 @@ function assignProductServiceList() {
function assignSelectedWorker() {
if (debitNoteData.value)
selectedWorker.value = debitNoteData.value.worker.map((v) => v.employee);
selectedWorkerItem.value = debitNoteData.value.worker.map((e) => {
return {
id: e.employee.id,
foreignRefNo: e.employee.employeePassport
? e.employee.employeePassport[0]?.number || '-'
: '-',
employeeName:
locale.value === Lang.English
? `${e.employee.firstNameEN} ${e.employee.lastNameEN}`
: `${e.employee.firstName || e.employee.firstNameEN} ${e.employee.lastName || e.employee.lastNameEN}`,
birthDate: dateFormatJS({ date: e.employee.dateOfBirth }),
gender: e.employee.gender,
age: calculateAge(e.employee.dateOfBirth),
nationality: optionStore.mapOption(e.employee.nationality),
documentExpireDate:
e.employee.employeePassport !== undefined &&
e.employee.employeePassport[0]?.expireDate !== undefined
? dateFormatJS({ date: e.employee.employeePassport[0]?.expireDate })
: '-',
imgUrl: e.employee.selectedImage
? `${API_BASE_URL}/employee/${e.id}/image/${e.employee.selectedImage}`
: '',
status: e.employee.status,
workerNew: false,
lastNameEN: e.employee.lastNameEN,
lastName: e.employee.lastName,
middleNameEN: e.employee.middleNameEN,
middleName: e.employee.middleName,
firstNameEN: e.employee.firstNameEN,
firstName: e.employee.firstName,
namePrefix: e.employee.namePrefix,
};
});
}
async function assignFormData(id: string) {
@ -434,6 +426,7 @@ async function getQuotation(id?: string) {
const data = await quotationStore.getQuotation(quotationId);
if (!!data) {
quotationData.value = data;
agentPrice.value = quotationData.value.agentPrice;
}
}
}
@ -549,25 +542,28 @@ function getPrice(
return a;
}
const price = precisionRound(c.pricePerUnit * c.amount);
const vat =
precisionRound(
(c.pricePerUnit * (c.discount ? c.amount : 1) - c.discount) *
(config.value?.vat || 0.07),
) * (!c.discount ? c.amount : 1);
const calcVat =
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
a.totalPrice = precisionRound(a.totalPrice + price);
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
a.vat = c.product.calcVat ? precisionRound(a.vat + vat) : a.vat;
const vatFactor = calcVat ? (config.value?.vat ?? 0.07) : 0;
const pricePerUnit = precisionRound(
(c.pricePerUnit * (1 + vatFactor)) / (1 + vatFactor),
);
const price = precisionRound(
(pricePerUnit * c.amount * (1 + vatFactor) - c.discount) /
(1 + vatFactor),
);
const vat = price * vatFactor;
a.totalPrice = precisionRound(a.totalPrice + price + c.discount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + vat);
a.vatExcluded = c.product.calcVat
? a.vatExcluded
: precisionRound(a.vat + vat);
a.finalPrice = precisionRound(
a.totalPrice -
a.totalDiscount +
a.vat -
Number(currentFormData.value.discount || 0),
);
: precisionRound(a.vatExcluded + price);
a.finalPrice = precisionRound(a.totalPrice - a.totalDiscount + a.vat);
return a;
},
@ -575,6 +571,7 @@ function getPrice(
totalPrice: 0,
totalDiscount: 0,
vat: 0,
vatIncluded: 0,
vatExcluded: 0,
finalPrice: 0,
},
@ -808,15 +805,11 @@ async function submit() {
worker: JSON.parse(
JSON.stringify([
...selectedWorker.value.map((v) => {
...selectedWorkerItem.value.map((v) => {
{
return v.id;
}
}),
...newWorkerList.value.map((v) => {
const { attachment, ...payload } = v;
return pageState.mode === 'edit' ? payload.id : payload;
}),
]),
),
dueDate: currentFormData.value.dueDate,
@ -869,6 +862,7 @@ async function exampleReceipt(id: string) {
function storeDataLocal() {
// quotationFormData.value.productServiceList = productServiceList.value;
//
localStorage.setItem(
'debit-note-preview',
@ -877,6 +871,7 @@ function storeDataLocal() {
...currentFormData.value,
customerBranchId: quotationData.value?.customerBranchId,
registeredBranchId: quotationData.value?.registeredBranchId,
agentPrice: quotationData.value.agentPrice,
},
meta: {
source: {
@ -893,7 +888,7 @@ function storeDataLocal() {
dueDate: currentFormData.value.dueDate,
},
productServicelist: productService.value,
selectedWorker: selectedWorker.value,
selectedWorker: selectedWorkerItem.value,
createdBy: getName(),
},
}),
@ -918,6 +913,69 @@ function closeAble() {
return window.opener !== null;
}
function combineWorker(newWorker: any, oldWorker: any) {
selectedWorkerItem.value = [
...oldWorker.map((e) => ({
id: e.id,
foreignRefNo: e.employeePassport
? e.employeePassport[0]?.number || '-'
: '-',
employeeName:
locale.value === Lang.English
? `${e.firstNameEN} ${e.lastNameEN}`
: `${e.firstName || e.firstNameEN} ${e.lastName || e.lastNameEN}`,
birthDate: dateFormatJS({ date: e.dateOfBirth }),
gender: e.gender,
age: calculateAge(e.dateOfBirth),
nationality: optionStore.mapOption(e.nationality),
documentExpireDate:
e.employeePassport !== undefined &&
e.employeePassport[0]?.expireDate !== undefined
? dateFormatJS({ date: e.employeePassport[0]?.expireDate })
: '-',
imgUrl: e.selectedImage
? `${API_BASE_URL}/employee/${e.id}/image/${e.selectedImage}`
: '',
status: e.status,
workerNew: false,
lastNameEN: e.lastNameEN,
lastName: e.lastName,
middleNameEN: e.middleNameEN,
middleName: e.middleName,
firstNameEN: e.firstNameEN,
firstName: e.firstName,
namePrefix: e.namePrefix,
})),
...newWorker.map((v: any) => ({
id: v.id,
foreignRefNo: v.passportNo || '-',
employeeName:
locale.value === Lang.English
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName || v.firstNameEN} ${v.lastName || v.lastNameEN}`,
birthDate: dateFormatJS({ date: v.dateOfBirth }),
gender: v.gender,
age: calculateAge(v.dateOfBirth),
nationality: optionStore.mapOption(v.nationality),
documentExpireDate: '-',
imgUrl: '',
status: 'CREATED',
lastNameEN: v.lastNameEN,
lastName: v.lastName,
middleNameEN: v.middleNameEN,
middleName: v.middleName,
firstNameEN: v.firstNameEN,
firstName: v.firstName,
namePrefix: v.namePrefix,
dateOfBirth: v.dateOfBirth,
workerNew: true,
})),
];
}
watch(
() => pageState.mode,
() => toggleMode(pageState.mode),
@ -1081,6 +1139,7 @@ async function submitAccepted() {
<PaymentForm
v-if="view === QuotationStatus.PaymentPending"
is-debit-note
:branch-id="quotationData?.registeredBranchId"
:readonly="isRoleInclude(['sale', 'head_of_sale'])"
:data="debitNoteData"
@fetch-status="
@ -1102,7 +1161,7 @@ async function submitAccepted() {
"
:row-worker="selectedWorkerItem"
@add-worker="() => (pageState.employeeModal = true)"
@delete="(i) => deleteItem(selectedWorker, i)"
@delete="(i) => deleteItem(selectedWorkerItem, i)"
/>
<!-- #TODO add openProductDialog at @add-product-->
@ -1322,7 +1381,7 @@ async function submitAccepted() {
<SaveButton
v-if="pageState.mode !== 'info'"
:disabled="
selectedWorkerItem.length === 0 && productService.length === 0
selectedWorkerItem.length === 0 || productService.length === 0
"
@click="submit"
solid
@ -1367,13 +1426,12 @@ async function submitAccepted() {
<!-- add employee quotation -->
<QuotationFormWorkerSelect
:preselect-worker="selectedWorker"
:preselect-worker="selectedWorkerItem"
:customerBranchId="quotationData?.customerBranchId"
v-model:open="pageState.employeeModal"
v-model:new-worker-list="newWorkerList"
@success="
(v) => {
selectedWorker = v.worker;
combineWorker(v.newWorker, v.worker);
}
"
/>

View file

@ -143,6 +143,15 @@ onMounted(async () => {
});
getList();
window.addEventListener('focus', () => {
debitNote.getDebitNoteStats().then((res) => {
if (res) {
stats.value = res;
}
});
getList();
});
});
watch(
@ -411,7 +420,7 @@ watch(
@delete="() => triggerDelete(item.row.id)"
:title="item.row.debitNoteQuotation?.workName"
:code="item.row.code"
:status="$t(`quotation.status.${item.row.quotationStatus}`)"
:status="$t(`debitNote.stats.${item.row.quotationStatus}`)"
:badge-color="hslaColors[item.row.quotationStatus] || ''"
:custom-data="[
{
@ -425,10 +434,12 @@ watch(
label: $t('quotation.customer'),
value:
item.row.customerBranch.customer.customerType === 'CORP'
? item.row.customerBranch.registerName
? $i18n.locale === 'tha'
? item.row.customerBranch.registerName
: item.row.customerBranch.registerNameEN
: $i18n.locale === 'tha'
? `${item.row.customerBranch.firstName} ${item.row.customerBranch.lastName}`
: `${item.row.customerBranch.firstNameEN} ${item.row.customerBranch.lastNameEN}`,
? `${item.row.customerBranch.firstName || ''} ${item.row.customerBranch.lastName || ''}`
: `${item.row.customerBranch.firstNameEN || ''} ${item.row.customerBranch.lastNameEN || ''}`,
},
{
label: $t('requestList.quotationCode'),

View file

@ -82,6 +82,8 @@ const data = ref<
>();
const productServiceList = ref<ProductServiceList[]>([]);
const selectedInstallmentNo = ref<number[]>([]);
const agentPrice = ref<boolean>(false);
const summaryPrice = ref<SummaryPrice>({
totalPrice: 0,
@ -217,6 +219,8 @@ onMounted(async () => {
}
productServiceList.value = parsed.meta.productServicelist;
selectedInstallmentNo.value = parsed.meta.selectedInstallmentNo;
agentPrice.value = parsed.meta.agentPrice;
productList.value =
productServiceList.value?.map((v) => ({
@ -224,40 +228,56 @@ onMounted(async () => {
code: v.product.code,
detail: v.product.name,
amount: v.amount || 0,
priceUnit: v.pricePerUnit || 0,
priceUnit:
v.pricePerUnit +
(v.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat']
? v.pricePerUnit * (config.value?.vat || 0.07)
: 0),
discount: v.discount || 0,
vat: v.vat || 0,
value: precisionRound(
(v.pricePerUnit || 0) * v.amount -
(v.discount || 0) +
(v.product.calcVat
? ((v.pricePerUnit || 0) * v.amount - (v.discount || 0)) *
(config.value?.vat || 0.07)
: 0),
(v.pricePerUnit +
(v.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat']
? v.pricePerUnit * (config.value?.vat || 0.07)
: 0)) *
v.amount -
v.discount,
),
})) || [];
}
summaryPrice.value = (productServiceList.value || []).reduce(
(a, c) => {
const price = precisionRound((c.pricePerUnit || 0) * c.amount);
const vat = precisionRound(
((c.pricePerUnit || 0) * c.amount - (c.discount || 0)) *
(config.value?.vat || 0.07),
if (
selectedInstallmentNo.value?.length > 0 &&
c.installmentNo &&
!selectedInstallmentNo.value?.includes(c.installmentNo)
) {
return a;
}
const calcVat =
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
const vatFactor = calcVat ? (config.value?.vat ?? 0.07) : 0;
const pricePerUnit = precisionRound(
(c.pricePerUnit * (1 + vatFactor)) / (1 + vatFactor),
);
a.totalPrice = precisionRound(a.totalPrice + price);
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
a.vat = c.product.calcVat ? precisionRound(a.vat + vat) : a.vat;
const price = precisionRound(
(pricePerUnit * c.amount * (1 + vatFactor) - c.discount) /
(1 + vatFactor),
);
const vat = price * vatFactor;
a.totalPrice = precisionRound(a.totalPrice + price + c.discount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + vat);
a.vatExcluded = c.product.calcVat
? a.vatExcluded
: precisionRound(a.vat + vat);
a.finalPrice = precisionRound(
a.totalPrice -
a.totalDiscount +
a.vat -
Number(data.value?.discount || 0),
);
: precisionRound(a.vatExcluded + price);
a.finalPrice = precisionRound(a.totalPrice - a.totalDiscount + a.vat);
return a;
},
@ -349,7 +369,9 @@ function print() {
{{ formatNumberDecimal(v.priceUnit, 2) }}
</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.discount, 2) }}
<template v-if="v.discount !== 0">
{{ formatNumberDecimal(v.discount, 2) }} ฿
</template>
</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.vat, 2) }}
@ -413,8 +435,8 @@ function print() {
{{
formatNumberDecimal(
summaryPrice.totalPrice -
summaryPrice.totalDiscount +
summaryPrice.vat,
summaryPrice.totalDiscount -
summaryPrice.vatExcluded,
2,
)
}}

View file

@ -79,8 +79,8 @@ function titleMode(mode: View): string {
})
}}
</span>
<span>เลขประจำตวผเสยภาษ {{ branch.taxNo }}</span>
<span>เบอรโทร {{ branch.telephoneNo }}</span>
<span>{{ $t('branch.form.taxNo') }} {{ branch.taxNo }}</span>
<span>{{ $t('taskOrder.telephone') }} {{ branch.telephoneNo }}</span>
<span>{{ branch.webUrl }}</span>
</article>
<article>
@ -105,8 +105,18 @@ function titleMode(mode: View): string {
})
}}
</span>
<span>เลขประจำตวผเสยภาษ {{ customer.citizenId }}</span>
<span>เบอรโทร {{ customer.telephoneNo }}</span>
<span>
{{
customer.customer.customerType === 'CORP'
? `${$t('customer.form.legalPersonNo')} `
: `${$t('customer.form.taxpayyerNo')} `
}}{{
customer.customer.customerType === 'CORP'
? customer.codeCustomer
: customer.citizenId
}}
</span>
<span>{{ $t('taskOrder.telephone') }} {{ customer.telephoneNo }}</span>
</article>
</section>
<section class="detail-quotation-info">

View file

@ -343,9 +343,16 @@ watch(
{
label: $t('general.customer'),
value:
item.row.invoice.quotation.customerBranch
.registerName ||
`${item.row.invoice.quotation.customerBranch?.firstName || '-'} ${item.row.invoice.quotation.customerBranch?.lastName || ''}`,
item.row.invoice.quotation.customerBranch.customer
.customerType === 'CORP'
? $i18n.locale === 'tha'
? item.row.invoice.quotation.customerBranch
.registerName
: item.row.invoice.quotation.customerBranch
.registerNameEN
: $i18n.locale === 'tha'
? `${item.row.invoice.quotation.customerBranch?.firstName || '-'} ${item.row.invoice.quotation.customerBranch?.lastName || ''}`
: `${item.row.invoice.quotation.customerBranch?.firstNameEN || '-'} ${item.row.invoice.quotation.customerBranch?.lastNameEN || ''}`,
},
{
label: $t('taskOrder.issueDate'),

View file

@ -45,7 +45,7 @@ const fieldSelected = ref<('no' | 'name' | 'nameEN')[]>([
const fieldSelectedOption = ref<{ label: string; value: string }[]>([
{
label: 'general.order',
value: 'orderNumber',
value: 'no',
},
{
@ -312,25 +312,6 @@ watch(
</q-input>
<div class="row col-md-5 justify-end" style="white-space: nowrap">
<q-select
v-if="$q.screen.gt.sm"
v-model="statusFilter"
outlined
dense
option-value="value"
option-label="label"
class="col"
:class="{ 'offset-md-5': pageState.gridView }"
map-options
emit-value
:for="'field-select-status'"
:hide-dropdown-icon="$q.screen.lt.sm"
:options="[
{ label: $t('general.all'), value: 'all' },
{ label: $t('general.active'), value: 'statusACTIVE' },
{ label: $t('general.inactive'), value: 'statusINACTIVE' },
]"
/>
<q-select
v-if="!pageState.gridView"
id="select-field"
@ -595,10 +576,18 @@ watch(
></q-badge>
</q-avatar>
<span class="text-weight-bold column q-pl-md">
{{ props.row.name }}
{{
$i18n.locale === 'tha'
? props.row.name
: props.row.nameEN
}}
<span class="text-caption app-text-muted-2">
{{ props.row.nameEN }}
{{
$i18n.locale === 'tha'
? props.row.nameEN
: props.row.name
}}
</span>
</span>
<nav

View file

@ -447,6 +447,17 @@ const useBranchStore = defineStore('api-branch', () => {
return false;
}
async function fetchListBankByBranch(branchId: string) {
const res = await api.get(`/branch/${branchId}/bank`, {
headers: { 'X-Rtid': flowStore.rtid },
});
if (!res) return false;
if (res.status === 200) return res.data;
return false;
}
return {
data,
map,
@ -475,6 +486,8 @@ const useBranchStore = defineStore('api-branch', () => {
fetchByIdAttachment,
putAttachment,
deleteByIdAttachment,
fetchListBankByBranch,
};
});

View file

@ -9,6 +9,10 @@ import { defineStore } from 'pinia';
import { api } from 'src/boot/axios';
import { PaginationResult } from 'src/types';
import { RequestWork, RequestWorkStatus } from 'src/stores/request-list/types';
import { precisionRound } from 'src/utils/arithmetic';
import { useConfigStore } from 'src/stores/config';
import { manageAttachment, manageFile } from '../utils';
const ENDPOINT = 'credit-note';
@ -80,6 +84,7 @@ export async function acceptCreditNote(id: string) {
}
export const useCreditNote = defineStore('credit-note-store', () => {
const configStore = useConfigStore();
const data = ref<Data[]>([]);
const page = ref<number>(1);
const pageMax = ref<number>(1);
@ -90,6 +95,75 @@ export const useCreditNote = defineStore('credit-note-store', () => {
[Status.Success]: 0,
});
function getfinalPriceCredit(c: RequestWork[]): {
totalPrice: number;
totalDiscount: number;
vat: number;
vatIncluded: number;
vatExcluded: number;
finalPrice: number;
} {
const price = c.reduce(
(acc, crr) => {
const servicesChargeStepCount =
crr.productService.work?.productOnWork?.find(
(item) => item.productId === crr.productService.productId,
).stepCount;
const successCount = crr.stepStatus.filter(
(item) => item.workStatus === RequestWorkStatus.Completed,
).length;
const vatFactor =
crr.productService.vat > 0 ? (configStore.data?.vat ?? 0.07) : 0;
const price = precisionRound(
crr.productService.pricePerUnit * (1 + vatFactor) -
crr.productService.discount,
);
const vat = crr.productService.product.calcVat
? (price / (1 + vatFactor)) * vatFactor
: 0;
acc.totalPrice = precisionRound(
acc.totalPrice + price + crr.productService.discount,
);
acc.totalDiscount = precisionRound(
acc.totalDiscount + crr.productService.discount,
);
acc.vat = precisionRound(acc.vat + vat);
acc.vatExcluded = crr.productService.product.agentPriceCalcVat
? acc.vatExcluded
: precisionRound(acc.vatExcluded + price / (1 + vatFactor));
if (servicesChargeStepCount && successCount) {
acc.finalPrice = precisionRound(
acc.finalPrice +
price -
crr.productService.product.serviceCharge * successCount,
);
return acc;
}
acc.finalPrice = precisionRound(acc.finalPrice + price);
return acc;
},
{
totalPrice: 0,
totalDiscount: 0,
vat: 0,
vatIncluded: 0,
vatExcluded: 0,
finalPrice: 0,
},
);
return price;
}
return {
data,
page,
@ -97,6 +171,8 @@ export const useCreditNote = defineStore('credit-note-store', () => {
pageSize,
stats,
getfinalPriceCredit,
getCreditNoteStats,
getCreditNote,
getCreditNoteList,

View file

@ -13,6 +13,7 @@ import {
CitizenPayload,
} from './types';
import { Employee } from '../employee/types';
import { getToken } from 'src/services/keycloak';
import { baseUrl, manageAttachment, manageFile, manageMeta } from '../utils';
const useCustomerStore = defineStore('api-customer', () => {
@ -114,6 +115,10 @@ const useCustomerStore = defineStore('api-customer', () => {
customerType?: CustomerType;
startDate?: string;
endDate?: string;
businessTypeId?: string;
provinceId?: string;
districtId?: string;
subDistrictId?: string;
},
Data extends Pagination<
(Customer &
@ -472,6 +477,56 @@ const useCustomerStore = defineStore('api-customer', () => {
return false;
}
async function customerExport(params: {
customerType?: CustomerType;
query?: string;
status?: 'CREATED' | 'ACTIVE' | 'INACTIVE';
page?: number;
pageSize?: number;
includeBranch?: boolean;
company?: boolean;
activeBranchOnly?: boolean;
startDate?: string | Date;
endDate?: string | Date;
businessTypeId?: string;
provinceId?: string;
districtId?: string;
subDistrictId?: string;
}) {
let url = baseUrl + '/' + 'customer-export';
const queryParams = new URLSearchParams(
Object.keys(params).reduce((acc: Record<string, string>, key) => {
const value = params[key as keyof typeof params];
if (value !== undefined && value !== null && value !== '') {
const stringValue =
typeof value === 'boolean' || typeof value === 'number'
? String(value)
: value instanceof Date
? value.toISOString()
: String(value);
acc[key] = stringValue;
}
return acc;
}, {}),
);
url += '?' + queryParams.toString();
const res = await fetch(url, {
headers: { ['Authorization']: 'Bearer ' + (await getToken()) },
});
const text = await res.json();
const blob = new Blob([text], { type: 'text/csv' });
if (res.ok && blob) {
const a = document.createElement('a');
a.download = 'customer-report' + '.csv';
a.href = window.URL.createObjectURL(blob);
a.click();
a.remove();
}
}
return {
data,
@ -498,6 +553,8 @@ const useCustomerStore = defineStore('api-customer', () => {
fetchBranchEmployee,
deleteAttachment,
customerExport,
...attachmentManager,
...fileManager,
...metaManager,

View file

@ -15,6 +15,7 @@ import {
EmployeeVisaPayload,
} from './types';
import { CustomerBranch } from '../customer/types';
import { getToken } from 'src/services/keycloak';
import { baseUrl, manageAttachment, manageFile, manageMeta } from '../utils';
const useEmployeeStore = defineStore('api-employee', () => {
@ -469,6 +470,55 @@ const useEmployeeStore = defineStore('api-employee', () => {
return false;
}
async function employeeExport(params: {
zipCode?: string;
gender?: string;
status?: Status;
visa?: boolean;
passport?: boolean;
customerId?: string;
customerBranchId?: string;
query?: string;
page?: number;
pageSize?: number;
activeOnly?: boolean;
startDate?: string | Date;
endDate?: string | Date;
}) {
let url = baseUrl + '/' + 'employee-export';
const queryParams = new URLSearchParams(
Object.keys(params).reduce((acc: Record<string, string>, key) => {
const value = params[key as keyof typeof params];
if (value !== undefined && value !== null && value !== '') {
const stringValue =
typeof value === 'boolean' || typeof value === 'number'
? String(value)
: value instanceof Date
? value.toISOString()
: String(value);
acc[key] = stringValue;
}
return acc;
}, {}),
);
url += '?' + queryParams.toString();
const res = await fetch(url, {
headers: { ['Authorization']: 'Bearer ' + (await getToken()) },
});
const text = await res.json();
const blob = new Blob([text], { type: 'text/csv' });
if (res.ok && blob) {
const a = document.createElement('a');
a.download = 'employee-report' + '.csv';
a.href = window.URL.createObjectURL(blob);
a.click();
a.remove();
}
}
return {
data,
globalOption,
@ -512,6 +562,8 @@ const useEmployeeStore = defineStore('api-employee', () => {
...attachmentManager,
...fileManager,
...metaManager,
employeeExport,
};
});

Some files were not shown because too many files have changed in this diff Show more