Merge branch 'develop'

This commit is contained in:
Thanaphon Frappet 2025-02-21 16:06:09 +07:00
commit 5a0663b94b
53 changed files with 932 additions and 541 deletions

View file

@ -199,7 +199,7 @@ onMounted(async () => {
outlined
dense
class="col"
id="input-flow-template-name"
for="input-flow-template-name"
v-model="flowData.name"
hide-bottom-space
:label="$t(`general.name`, { msg: $t('flow.step') })"
@ -255,8 +255,8 @@ onMounted(async () => {
<div class="row items-center q-py-sm full-width">
<q-btn
v-if="!readonly"
id="btn-work-up-product"
for="btn-work-up-product"
:id="`btn-work-up-product-${step.name || index}-${onDrawer ? 'drawer' : 'dialog'}`"
:for="`btn-work-up-product-${step.name || index}-${onDrawer ? 'drawer' : 'dialog'}`"
icon="mdi-arrow-up"
dense
flat
@ -270,8 +270,8 @@ onMounted(async () => {
/>
<q-btn
v-if="!readonly"
id="btn-work-down-product"
for="btn-work-down-product"
:id="`btn-work-down-product-${step.name || index}-${onDrawer ? 'drawer' : 'dialog'}`"
:for="`btn-work-down-product-${step.name || index}-${onDrawer ? 'drawer' : 'dialog'}`"
icon="mdi-arrow-down"
dense
flat
@ -376,7 +376,7 @@ onMounted(async () => {
</div> -->
<q-btn
v-if="!readonly"
id="btn-delete-work"
:id="`btn-delete-work-${step.name || index}-${onDrawer ? 'drawer' : 'dialog'}`"
icon="mdi-trash-can-outline"
dense
flat
@ -399,6 +399,7 @@ onMounted(async () => {
>
<div class="row q-col-gutter-sm">
<q-input
:for="`textarea-detail-${index}-${onDrawer ? 'drawer' : 'dialog'}`"
:bg-color="readonly ? 'transparent' : ''"
:readonly
class="col-md-6 col-12"
@ -411,7 +412,6 @@ onMounted(async () => {
(v) => (step.detail = v?.toString() || '')
"
/>
<div class="col-md-6 col-12">
<div
class="surface-1 rounded bordered full-height"
@ -434,7 +434,7 @@ onMounted(async () => {
</div>
<q-btn
v-if="!readonly"
id="btn-add-work-product"
:id="`btn-add-work-product-${index}-${onDrawer ? 'drawer' : 'dialog'}`"
class="text-capitalize rounded absolute-top-right"
flat
dense
@ -465,6 +465,7 @@ onMounted(async () => {
<q-select
v-if="step.responsiblePersonId"
behavior="menu"
:for="`select-responsible-person-${index}-${onDrawer ? 'drawer' : 'dialog'}`"
:bg-color="readonly ? 'transparent' : ''"
:readonly
outlined
@ -550,7 +551,7 @@ onMounted(async () => {
<q-list>
<q-item>
<q-input
for="input-search"
:for="`input-search-${index}-${onDrawer ? 'drawer' : 'dialog'}`"
outlined
dense
:label="$t('general.search')"
@ -582,7 +583,10 @@ onMounted(async () => {
class="column"
@click.stop="selectResponsiblePerson(index, person)"
>
<div class="row items-center no-wrap">
<div
class="row items-center no-wrap"
:id="`select-responsible-person-${index}-${person.firstName || person.firstNameEN}-${onDrawer ? 'drawer' : 'dialog'}`"
>
<q-checkbox
size="xs"
:model-value="
@ -652,6 +656,7 @@ onMounted(async () => {
>
<div class="row items-center">
<q-checkbox
:id="`select-area-${index}`"
v-model="step.messengerByArea"
size="xs"
></q-checkbox>
@ -667,6 +672,7 @@ onMounted(async () => {
<!-- RESPONSIBLE-AGENCIES, RESPONSIBLE-INSTITUTION -->
<q-select
behavior="menu"
:for="`select-responsible-institution-${index}-${onDrawer ? 'drawer' : 'dialog'}`"
:bg-color="readonly ? 'transparent' : ''"
:readonly
outlined
@ -737,6 +743,7 @@ onMounted(async () => {
v-if="step.responsibleInstitution"
>
<q-checkbox
:id="`select-responsible-institution-${index}-${opt.value}-${onDrawer ? 'drawer' : 'dialog'}`"
:model-value="
step.responsibleInstitution.some(
(v: string) => v === opt.value,

View file

@ -222,6 +222,7 @@ const detailEditorImageDrop = createEditorImageDrop(detail);
dense
>
<q-editor
id="input-detail"
dense
:model-value="readonly ? detail || '-' : detail"
@update:model-value="

View file

@ -106,6 +106,7 @@ function optionSearch(val: string | null) {
<div class="col-12 row q-col-gutter-sm">
<q-select
for="select-attachment"
behavior="menu"
:readonly
outlined
@ -150,6 +151,7 @@ function optionSearch(val: string | null) {
{{ $t('general.add', { text: $t('general.attachment') }) }}
</q-item> -->
<q-item
for="select-all"
dense
clickable
class="flex items-center"
@ -172,6 +174,7 @@ function optionSearch(val: string | null) {
</template>
<template #option="{ opt }">
<q-checkbox
:id="`select-${opt.label}`"
:model-value="attachment.some((v) => v === opt.value)"
class="q-pr-sm"
size="xs"

View file

@ -260,6 +260,7 @@ withDefaults(
</template>
<template v-if="col.name === '#vatIncluded'">
<q-select
for="select-vat-included"
:options="[
{ label: $t('general.included'), value: true },
{ label: $t('general.notIncluded'), value: false },
@ -278,6 +279,7 @@ withDefaults(
{ label: $t('general.notIncluded'), value: false },
]"
v-if="priceDisplay?.agentPrice && props.rowIndex === 1"
for="select-vat-included"
map-options
emit-value
flat
@ -291,6 +293,7 @@ withDefaults(
{ label: $t('general.notIncluded'), value: false },
]"
v-if="priceDisplay?.serviceCharge && props.rowIndex === 2"
for="select-vat-included"
map-options
emit-value
flat

View file

@ -127,8 +127,8 @@ watch(
<div class="row items-center q-py-sm full-width" @click.stop>
<q-btn
v-if="!readonly"
id="btn-work-up-product"
for="btn-work-up-product"
:id="`btn-work-up-product-${workIndex}`"
:for="`btn-work-up-product-${workIndex}`"
icon="mdi-arrow-up"
dense
flat
@ -139,8 +139,8 @@ watch(
/>
<q-btn
v-if="!readonly"
id="btn-work-down-product"
for="btn-work-down-product"
:id="`btn-work-down-product-${workIndex}`"
:for="`btn-work-down-product-${workIndex}`"
icon="mdi-arrow-down"
dense
flat
@ -244,7 +244,7 @@ watch(
<div :class="{ 'col-12 row justify-end q-mt-sm': $q.screen.lt.md }">
<q-btn
v-if="!readonly"
id="btn-delete-work"
:id="`btn-delete-work-${workIndex}`"
icon="mdi-trash-can-outline"
dense
flat
@ -290,8 +290,8 @@ watch(
<AddButton
v-if="!readonly"
icon-only
id="btn-add-work-product"
for="btn-add-work-product"
:id="`btn-add-work-product-${workIndex}`"
:for="`btn-add-work-product-${workIndex}`"
@click.stop="$emit('addProduct')"
/>
</span>
@ -317,7 +317,7 @@ watch(
>
<q-btn
v-if="!readonly"
id="btn-product-up"
:id="`btn-product-up-${index}`"
icon="mdi-arrow-up"
dense
flat
@ -329,8 +329,8 @@ watch(
/>
<q-btn
v-if="!readonly"
id="btn-product-down"
for="btn-product-down"
:id="`btn-product-down-${index}`"
:for="`btn-product-down-${index}`"
icon="mdi-arrow-down"
dense
flat
@ -482,8 +482,8 @@ watch(
<q-btn
v-if="!readonly"
class="q-ml-md"
id="btn-delete-work-product"
for="btn-delete-work-product"
:id="`btn-delete-work-product-${index}`"
:for="`btn-delete-work-product-${index}`"
icon="mdi-trash-can-outline"
padding="0"
dense
@ -521,7 +521,7 @@ watch(
</span>
<q-btn
v-if="!readonly"
id="btn-add-work-product"
:id="`btn-add-work-product-${workIndex}`"
class="text-capitalize rounded"
flat
dense

View file

@ -127,7 +127,7 @@ defineEmits<{
<q-td class="text-right">
<q-btn
:id="`btn-eye-${props.row.firstName}`"
:id="`btn-preview-${props.row.workName}`"
icon="mdi-play-box-outline"
size="sm"
dense
@ -137,7 +137,7 @@ defineEmits<{
/>
<q-btn
:id="`btn-eye-${props.row.firstName}`"
:id="`btn-eye-${props.row.workName}`"
icon="mdi-eye-outline"
size="sm"
dense
@ -147,7 +147,7 @@ defineEmits<{
/>
<KebabAction
:idName="`btn-kebab-${props.row.firstName}`"
:idName="`btn-kebab-${props.row.workName}`"
status="'ACTIVE'"
hide-toggle
hide-delete

View file

@ -46,6 +46,7 @@ type Options = { label: string; value: string };
(lhs: Options, rhs: Options) => lhs.value.localeCompare(rhs.value),
)
"
:rules="[(val: string) => !!val || $t('form.error.required')]"
v-model="group"
>
<template v-slot:option="{ scope, opt }">

View file

@ -29,6 +29,7 @@ const quotationId = defineModel<string>('quotationId', {
<section class="col-12 row q-col-gutter-sm">
<SelectQuotation
for="select-quotation"
required
class="col"
v-model:value="quotationId"
:label="$t('general.select', { msg: $t('quotation.title') })"

View file

@ -29,6 +29,7 @@ const quotationId = defineModel<string>('quotationId', {
<section class="col-12 row q-col-gutter-sm">
<SelectQuotation
for="select-quotation"
required
class="col"
v-model:value="quotationId"
:label="$t('general.select', { msg: $t('quotation.title') })"

View file

@ -18,7 +18,12 @@ function handleClick() {
</script>
<template>
<label class="switch" @click.stop="handleClick">
<input type="checkbox" :disabled="twoWay || disable" v-model="model" />
<input
id="btn-toggle"
type="checkbox"
:disabled="twoWay || disable"
v-model="model"
/>
<div class="slider round" :class="{ disable: disable }"></div>
</label>
</template>

View file

@ -365,6 +365,8 @@ watch(
{{ $t('flow.stepNo', { msg: (props.stepIndex || stepIndex) + 1 }) }}
<span class="app-text-muted">: {{ step.name }}</span>
<q-btn-dropdown
for="select-step"
id="select-step"
unelevated
no-icon-animation
size="sm"
@ -463,7 +465,7 @@ watch(
>
<div class="col-md col-12 row items-center">
<q-btn
id="btn-move-up-product"
:id="`btn-move-up-product-${propIndex}`"
icon="mdi-arrow-up"
dense
flat
@ -479,7 +481,7 @@ watch(
"
/>
<q-btn
id="btn-move-down-product"
:id="`btn-move-down-product-${propIndex}`"
icon="mdi-arrow-down"
dense
flat
@ -513,7 +515,7 @@ watch(
emit-value
map-options
hide-bottom-space
for="input-properties-name"
:for="`input-properties-name-${propIndex}`"
class="col-md col-12 q-mr-md"
:class="{ 'q-my-sm': $q.screen.lt.md }"
:label="$t('productService.service.propertiesName')"
@ -552,8 +554,8 @@ watch(
emit-value
map-options
hide-bottom-space
for="input-properties-type"
id="input-properties-type"
:for="`input-properties-type-${propIndex}`"
:id="`input-properties-type-${propIndex}`"
:label="$t('general.type')"
option-value="value"
@update:model-value="

View file

@ -27,6 +27,7 @@ const props = withDefaults(
fillInput?: boolean;
disable?: boolean;
multiple?: boolean;
hideInput?: boolean;
rules?: ((value: string) => string | true)[];
}>(),
@ -73,7 +74,7 @@ watch(
outlined
:clearable
:disable
use-input
:use-input="!hideInput"
emit-value
map-options
:multiple

View file

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

View file

@ -72,6 +72,7 @@ onMounted(async () => {
v-model="value"
option-value="id"
incremental
for="select-customer"
:label
:placeholder
:readonly
@ -129,6 +130,8 @@ onMounted(async () => {
clickable
v-close-popup
@click.stop="$emit('create')"
for="select-customer-add-new"
id="select-customer-add-new"
>
<q-item-section>
<span class="row items-center">

View file

@ -73,6 +73,7 @@ function setDefaultValue() {
</script>
<template>
<SelectInput
for="select-work-flow-template"
v-model="value"
incremental
:label

View file

@ -108,6 +108,7 @@ function setDefaultValue() {
:label
:placeholder
:readonly
:hide-input="value !== ''"
:disable="disabled"
:option="selectOptions"
:hide-selected="false"

View file

@ -142,6 +142,7 @@ function selectedIndex(item: Employee) {
</template>
<template v-if="col.name === '#check'">
<q-checkbox
id="select-worker-all"
v-model="props.selected"
@update:model-value="(v) => handleUpdate()"
size="sm"
@ -193,7 +194,11 @@ function selectedIndex(item: Employee) {
!disabledWorkerId?.some((id) => id === props.row.id)
"
>
<q-checkbox v-model="props.selected" size="sm" />
<q-checkbox
v-model="props.selected"
size="sm"
:id="`select-worker-${props.row.firstName}`"
/>
</template>
</q-td>
</q-tr>

View file

@ -147,6 +147,7 @@ export default {
ofPage: '{current} of {total}',
included: 'Included',
notIncluded: 'Not Included',
dueDate: 'Due date',
},
menu: {
@ -1057,65 +1058,102 @@ export default {
cancel: 'Cancel',
},
backend: {
productGroupNotFound: 'Product group cannot be found.',
productGroupIsUsed: 'Product group is in used.',
productNotFound: 'Product cannot be found.',
productIsUsed: 'Product is in used.',
productTypeNotFound: 'Product type cannot be found.',
productGroupAssociatedBadReq: 'Product group associated cannot be found.',
productTypeIsUsed: 'Product type is in used.',
productGroupBadReq: 'Product group cannot be found.',
serviceNotFound: 'Service cannot be found.',
someProductBadReq: 'Some product not found.',
serviceIsUsed: 'Service is in used.',
workNotFound: 'Work cannot be found.',
workIsUsed: 'Work is in used.',
branchContactNotFound: 'Branch contact cannot be found.',
branchBadReq: 'Branch cannot be found.',
branchNotFound: 'Branch cannot be found.',
cantMakeHQAndBranchSameTime:
'Cannot make this as headquaters and branch at the same time.',
branchIsUsed: 'Branch is in used.',
oneOrMoreUserBadReq: 'One or more user cannot be found.',
oneOrMoreBranchBadReq: 'One or more branch cannot be found.',
customerBranchNotFound: 'Customer branch cannot be found.',
customerBranchIsUsed: 'Customer branch is in used.',
customerNotFound: 'Customer cannot be found.',
customerIsUsed: 'Customer is in used.',
employeeBadReq: 'Employee cannot be found.',
parentMenuBadReq: 'Parent menu not found.',
menuBadReq: 'Menu cannot be found.',
oneOrMoreBranchMissing:
'One or more branch cannot be delete and is missing.',
employeeCheckupNotFound: 'Employee checkup cannot be found.',
provinceNotFound: 'Province cannot be found.',
employeeNotFound: 'Employee cannot be found.',
employeeBadReq: 'Employee cannot be found.',
employeeIsUsed: 'Employee is in used.',
someProvinceNotFound: 'Some province cannot be found.',
employeeOtherNotFound: 'Employee other info cannot be found.',
employeeWorkNotFound: 'Employee work cannot be found.',
parentMenuBadReq: 'Parent menu not found.',
menuNotFound: 'Menu cannot be found.',
menuBadReq: 'Menu cannot be found.',
menuComponentNotFound: 'Menu component cannot be found.',
roleMenuNotFound: 'Role menu cannot be found.',
userNotFound: 'User cannot be found.',
userIsUsed: 'User is in used.',
cantMakeHQAndBranchSameTime:
'Cannot make this as headquaters and branch at the same time.',
unknowHowToVerify: 'Unknown how to verify identity.',
noPermission:
'You do not have permission to access or perform with this resource.',
noPermissionToAccess:
'You do not have permission to access or perform with this resource.',
relationProvinceNotFound: 'Province cannot be found.',
relationDistrictNotFound: 'District cannot be found.',
relationSubDistrictNotFound: 'Sub-district cannot be found.',
relationHQNotFound: 'Headquarters cannot be found.',
relationBranchNotFound: 'Branch cannot be found.',
relationCustomerNotFound: 'Customer cannot be found.',
relationCustomerBranchNotFound: 'Customer Branch cannot be found.',
relationUserNotFound: 'User cannot be found.',
relationProductGroupNotFound: 'Product group cannot be found.',
relationProductTypeNotFound: 'Product type cannot be found.',
relationServiceNotFound: 'Service cannot be found.',
relationProductNotFound: 'Product cannot be found.',
relationQuotationNotFound: 'Quotation cannot be found.',
relationWorkerNotFound: 'Worker cannot be found.',
relationWorkNotFound: 'Work cannot be found.',
dataNotFound: 'Data not found.',
employmentOfficeNotFound: 'Employment office not found.',
branchNotFound: 'Branch cannot be found.',
productGroupNotFound: 'Product group cannot be found.',
productNotFound: 'Product cannot be found.',
productTypeNotFound: 'Product type cannot be found.',
serviceNotFound: 'Service cannot be found.',
workNotFound: 'Work cannot be found.',
branchContactNotFound: 'Branch contact cannot be found.',
customerBranchNotFound: 'Customer branch cannot be found.',
customerNotFound: 'Customer cannot be found.',
employeeCheckupNotFound: 'Employee checkup cannot be found.',
provinceNotFound: 'Province cannot be found.',
employeeNotFound: 'Employee cannot be found.',
someProvinceNotFound: 'Some province cannot be found.',
employeeOtherNotFound: 'Employee other info cannot be found.',
employeeWorkNotFound: 'Employee work cannot be found.',
menuNotFound: 'Menu cannot be found.',
menuComponentNotFound: 'Menu component cannot be found.',
roleMenuNotFound: 'Role menu cannot be found.',
userNotFound: 'User cannot be found.',
flowTemplateNotFound: 'Workflow template cannot be found.',
taskOrderNotFound: 'Task order cannot be found.',
quotationNotFound: 'Quotation cannot be found.',
citizenNotFound: 'Citizen cannot be found.',
employeeOtherInfoNotFound: 'Employee other info cannot be found.',
passportNotFound: 'Passport cannot be found.',
visaNotFound: 'Visa cannot be found.',
workflowNotFound: 'Workflow cannot be found.',
institutionNotFound: 'Agencies cannot be found.',
invoiceNotFound: 'Invoice cannot be found.',
receiptNotFound: 'Receipt cannot be found.',
paymentNotFound: 'Payment cannot be found.',
requestDataNotFound: 'Request data cannot be found.',
requestWorkNotFound: 'Request work cannot be found.',
taskListNotFound: 'Task list cannot be found.',
creditNoteNotFound: 'Credit note cannot be found.',
debitNoteNotFound: 'Debit note cannot be found.',
productGroupIsUsed: 'Product group is in used.',
productIsUsed: 'Product is in used.',
productTypeIsUsed: 'Product type is in used.',
serviceIsUsed: 'Service is in used.',
workIsUsed: 'Work is in used.',
branchIsUsed: 'Branch is in used.',
customerBranchIsUsed: 'Customer branch is in used.',
customerIsUsed: 'Customer is in used.',
employeeIsUsed: 'Employee is in used.',
userIsUsed: 'User is in used.',
institutionIsUsed: 'Agencies is in used.',
quotationIsUsed: 'Quotation is in used.',
debitNoteIsUsed: 'Debit note is in used.',
sameBranchCodeExists: 'This Head Office Abbreviation is already in use.',
productNameExists:
'Product with the same name already exists. If you want to create with this name please select another code.',
userExists: 'User already exits.',
sameNameExists: 'Same name exists.',
validateError: 'Validate Error',
codeMisMatch: 'Code Mismatch',
userExists: 'User already exits.',
crossCompanyNotPermit: 'Cannot move between different headoffice',
errorOccure:
'An error has occurred, causing the system to be unable to function. Please try again later.',
@ -1123,11 +1161,20 @@ export default {
authFailed: 'Authentication Failed. Please try again later. ',
installmentsValidateFailed:
'Validation failed. Each installment must include at least one product. Please review and update the installments accordingly.',
flowTemplateNotFound: 'Workflow template cannot be found.',
taskOrderNotFound: 'Task order cannot be found.',
quotationNotFound: 'Quotation cannot be found.',
sameNameExists: 'Same name exists.',
requestWorkMustReady: 'Request work must ready.',
notValidImage: 'Not a valid image.',
minimumBranchNotMet: 'Require at least one branch for a user.',
requireOneMinBranch: 'Require at least one branch as headoffice.',
reqMinAffilatedBranch:
'You must be affilated with at least one branch or specify branch to be registered (System permission required).',
paymentNotSuccess: 'Payment not success.',
flowAccountError: 'FlowAccount error.',
InvoiceReceiptIssueFailed: 'Failed to issue invoice/receipt document.',
QuotationWorkerExceed: 'Worker exceed current quotation max worker.',
taskListNotPending: 'One or more task is not pending.',
reqNotMet: 'Not Match',
systemError: 'A system error occurred.',
},
},
@ -1260,11 +1307,11 @@ export default {
},
stats: {
Pending: 'Debit Note',
Expire: 'Expired',
Payment: 'Payment',
Receipt: 'Receipt',
Succeed: 'Completed',
Issued: 'Debit Note',
Expired: 'Expired',
PaymentPending: 'Payment',
PaymentSuccess: 'Receipt',
ProcessComplete: 'Completed',
},
viewMode: {

View file

@ -149,6 +149,7 @@ export default {
ofPage: '{current} จาก {total}',
included: 'รวม',
notIncluded: 'ไม่รวม',
dueDate: 'วันครบกำหนด',
},
menu: {
@ -712,7 +713,7 @@ export default {
product: {
title: 'สินค้าและบริการ',
code: 'รหัสสินค้าและบริการ',
name: 'ชื่อกลุ่มสินค้าและบริการ',
name: 'ชื่อสินค้าและบริการ',
registeredBranch: 'สาขาที่ลงทะเบียน',
noProduct: 'ยังไม่มีสินค้าและบริการ',
allProduct: 'สินค้าและบริการทั้งหมด',
@ -1041,62 +1042,99 @@ export default {
cancel: 'ยกเลิก',
},
backend: {
productGroupNotFound: 'ไม่พบกลุ่มสินค้าและบริการ',
productGroupIsUsed: 'กลุ่มสินค้าและบริการที่ใช้งานอยู่',
productNotFound: 'ไม่พบสินค้าและบริการ',
productIsUsed: 'สินค้าและบริการใช้งานอยู่',
productTypeNotFound: 'ไม่พบประเภทสินค้าและบริการ',
productGroupAssociatedBadReq: 'ไม่พบกลุ่มสินค้าและบริการที่เกี่ยวข้อง',
productTypeIsUsed: 'ประเภทสินค้าและบริการใช้งานอยู่',
productGroupBadReq: 'ไม่พบกลุ่มสินค้าและบริการ',
serviceNotFound: 'ไม่พบบริการ',
someProductBadReq: 'ไม่พบสินค้าและบริการบางส่วน',
serviceIsUsed: 'บริการใช้งานอยู่',
workNotFound: 'ไม่พบงาน',
workIsUsed: 'งานที่ใช้งานอยู่',
branchContactNotFound: 'ไม่พบผู้ติดต่อสาขา',
branchBadReq: 'ไม่พบสาขา',
branchNotFound: 'ไม่พบสาขา',
cantMakeHQAndBranchSameTime:
'ไม่สามารถทำให้เป็นสำนักงานใหญ่และสาขาพร้อมกันได้',
branchIsUsed: 'สาขาใช้งานอยู่',
oneOrMoreUserBadReq: 'ไม่พบผู้ใช้หนึ่งคนหรือมากกว่า',
oneOrMoreBranchBadReq: 'ไม่พบสาขาหนึ่งสาขาหรือมากกว่า',
customerBranchNotFound: 'ไม่พบสาขาลูกค้า',
customerBranchIsUsed: 'สาขาลูกค้าที่ใช้งานอยู่',
customerNotFound: 'ไม่พบลูกค้า',
customerIsUsed: 'ลูกค้าใช้งานอยู่',
oneOrMoreBranchMissing: 'ไม่สามารถลบสาขาหนึ่งหรือมากกว่าได้และสูญหาย',
employeeCheckupNotFound: 'ไม่พบการตรวจสอบพนักงาน',
provinceNotFound: 'ไม่พบจังหวัด',
employeeNotFound: 'ไม่พบพนักงาน',
employeeBadReq: 'ไม่พบพนักงาน',
employeeIsUsed: 'พนักงานใช้งานอยู่',
someProvinceNotFound: 'ไม่พบจังหวัดบางส่วน',
employeeOtherNotFound: 'ไม่พบข้อมูลอื่นของพนักงาน',
employeeWorkNotFound: 'ไม่พบงานของพนักงาน',
parentMenuBadReq: 'ไม่พบเมนูหลัก',
menuNotFound: 'ไม่พบเมนู',
menuBadReq: 'ไม่พบเมนู',
menuComponentNotFound: 'ไม่พบส่วนประกอบเมนู',
roleMenuNotFound: 'ไม่พบเมนูบทบาท',
userNotFound: 'ไม่พบผู้ใช้',
userIsUsed: 'ผู้ใช้ใช้งานอยู่',
oneOrMoreBranchMissing: 'ไม่สามารถลบสาขาหนึ่งหรือมากกว่าได้และสูญหาย',
cantMakeHQAndBranchSameTime:
'ไม่สามารถทำให้เป็นสำนักงานใหญ่และสาขาพร้อมกันได้',
unknowHowToVerify: 'ไม่ทราบวิธียืนยันตัวตน',
noPermission: 'คุณไม่มีสิทธิในการเข้าถึงหรือดำเนินการใดๆ กับข้อมูลนี้',
noPermissionToAccess: 'คุณไม่มีสิทธิในการเข้าถึงข้อมูลนี้',
relationProvinceNotFound: 'ไม่พบจังหวัด',
relationDistrictNotFound: 'ไม่พบอำเภอ',
relationSubDistrictNotFound: 'ไม่พบตำบล',
relationHQNotFound: 'ไม่พบสำนักงานใหญ่',
relationBranchNotFound: 'ไม่พบสาขา',
relationCustomerNotFound: 'ไม่พบลูกค้า',
relationCustomerBranchNotFound: 'ไม่พบสาขาลูกค้า',
relationUserNotFound: 'ไม่พบผู้ใช้',
relationProductGroupNotFound: 'ไม่พบกลุ่มสินค้า',
relationProductTypeNotFound: 'ไม่พบประเภทสินค้า',
relationServiceNotFound: 'ไม่พบบริการ',
relationProductNotFound: 'ไม่พบสินค้า',
relationQuotationNotFound: 'ไม่พบใบเสนอราคา',
relationWorkerNotFound: 'ไม่พบแรงงาน',
relationWorkNotFound: 'ไม่พบงาน',
dataNotFound: 'ไม่พบข้อมูล',
employmentOfficeNotFound: 'ไม่พบสำนักงานจัดหางาน',
branchNotFound: 'ไม่พบสาขา',
productGroupNotFound: 'ไม่พบกลุ่มสินค้าและบริการ',
productNotFound: 'ไม่พบสินค้าและบริการ',
productTypeNotFound: 'ไม่พบประเภทสินค้าและบริการ',
serviceNotFound: 'ไม่พบบริการ',
workNotFound: 'ไม่พบงาน',
branchContactNotFound: 'ไม่พบผู้ติดต่อสาขา',
customerBranchNotFound: 'ไม่พบสาขาลูกค้า',
customerNotFound: 'ไม่พบลูกค้า',
employeeCheckupNotFound: 'ไม่พบการตรวจสอบพนักงาน',
provinceNotFound: 'ไม่พบจังหวัด',
employeeNotFound: 'ไม่พบพนักงาน',
someProvinceNotFound: 'ไม่พบจังหวัดบางส่วน',
employeeOtherNotFound: 'ไม่พบข้อมูลอื่นของพนักงาน',
employeeWorkNotFound: 'ไม่พบงานของพนักงาน',
menuNotFound: 'ไม่พบเมนู',
menuComponentNotFound: 'ไม่พบส่วนประกอบเมนู',
roleMenuNotFound: 'ไม่พบเมนูสิทธิ์',
userNotFound: 'ไม่พบผู้ใช้',
flowTemplateNotFound: 'ไม่พบขั้นตอนการทำงาน',
taskOrderNotFound: 'ไม่พบใบสั่งงาน',
quotationNotFound: 'ไม่พบใบเสนอราคา',
citizenNotFound: 'ไม่พบข้อมูลพลเมือง',
employeeOtherInfoNotFound: 'ไม่พบข้อมูลอื่น ๆ ของพนักงาน',
passportNotFound: 'ไม่พบหนังสือเดินทาง',
visaNotFound: 'ไม่พบวีซ่า',
workflowNotFound: 'ไม่พบขั้นตอนการทำงาน',
institutionNotFound: 'ไม่พบหน่วยงาน',
invoiceNotFound: 'ไม่พบใบแจ้งหนี้',
receiptNotFound: 'ไม่พบใบเสร็จรับเงิน',
paymentNotFound: 'ไม่พบการชำระเงิน',
requestDataNotFound: 'ไม่พบใบรายการคำขอ',
requestWorkNotFound: 'ไม่พบรายการคำขอ',
taskListNotFound: 'ไม่พบใบสั่งงาน',
creditNoteNotFound: 'ไม่พบใบลดหนี้',
debitNoteNotFound: 'ไม่พบใบเพิ่มหนี้',
productGroupIsUsed: 'กลุ่มสินค้าและบริการที่ใช้งานอยู่',
productIsUsed: 'สินค้าและบริการใช้งานอยู่',
productTypeIsUsed: 'ประเภทสินค้าและบริการใช้งานอยู่',
serviceIsUsed: 'บริการใช้งานอยู่',
workIsUsed: 'งานที่ใช้งานอยู่',
branchIsUsed: 'สาขาใช้งานอยู่',
customerBranchIsUsed: 'สาขาลูกค้าที่ใช้งานอยู่',
customerIsUsed: 'ลูกค้าใช้งานอยู่',
employeeIsUsed: 'พนักงานใช้งานอยู่',
userIsUsed: 'ผู้ใช้ใช้งานอยู่',
institutionIsUsed: 'หน่วยงานใช้งานอยู่.',
quotationIsUsed: 'ใบเสนอราคาใช้งานอยู่.',
debitNoteIsUsed: 'ใบเพิ่มหนี้ใช้งานอยู่.',
sameBranchCodeExists: 'ตัวย่อสำนักงานใหญ่นี้ถูกใช้แล้ว',
productNameExists:
'สินค้าที่มีชื่อเดียวกันมีในระบบแล้ว หากคุณต้องการสร้างด้วยชื่อนี้โปรดเลือกรหัสอื่น',
userExists: 'ชื่อผู้ใช้นี้มีอยู่ในระบบอยู่แล้ว',
sameNameExists: 'ชื่อนี้ถูกใช้ไปแล้ว',
validateError: 'เกิดข้อผิดพลาดจากการตรวจสอบ',
codeMisMatch: 'รหัสไม่ตรงกัน',
userExists: 'ชื่อผู้ใช้นี้มีอยู่ในระบบอยู่แล้ว',
crossCompanyNotPermit: 'ไม่สามารถดำเนินการระหว่างสำนักงานใหญ่อื่นได้',
errorOccure:
'เกิดข้อผิดพลาดทำให้ระบบไม่สามารถทำงานได้ กรุณาลองใหม่ในภายหลัง',
@ -1104,11 +1142,21 @@ export default {
authFailed: 'การยืนยันตัวตนล้มเหลว กรุณาลองใหม่ในภายหลัง',
installmentsValidateFailed:
'ข้อมูลงวดไม่ถูกต้อง กรุณาตรวจสอบและยืนยันว่าแต่ละงวดมีสินค้าอย่างน้อยหนึ่งรายการ',
flowTemplateNotFound: 'ไม่พบขั้นตอนการทำงาน',
taskOrderNotFound: 'ไม่พบใบสั่งงาน',
quotationNotFound: 'ไม่พบใบเสนอราคา',
sameNameExists: 'ชื่อนี้ถูกใช้ไปแล้ว',
requestWorkMustReady: 'ใบรายการคำขอต้องพร้อมดำเนินการ',
notValidImage: 'รูปภาพไม่ถูกต้อง',
minimumBranchNotMet: 'ต้องมีอย่างน้อยหนึ่งสาขาสำหรับผู้ใช้',
requireOneMinBranch: 'ต้องมีอย่างน้อยหนึ่งสาขาเป็นสำนักงานใหญ่',
reqMinAffilatedBranch:
'คุณต้องมีสาขาที่เกี่ยวข้องอย่างน้อยหนึ่งสาขา หรือระบุสาขาที่จะลงทะเบียน (ต้องมีสิทธิ์ระบบ)',
paymentNotSuccess: 'การชำระเงินไม่สำเร็จ',
flowAccountError: 'เกิดข้อผิดพลาดใน FlowAccount',
InvoiceReceiptIssueFailed: 'ไม่สามารถออกใบแจ้งหนี้/ใบเสร็จรับเงินได้',
QuotationWorkerExceed: 'จำนวนพนักงานเกินขีดจำกัดสูงสุดของใบเสนอราคา',
taskListNotPending:
'มีงานหนึ่งงานหรือมากกว่าที่ไม่อยู่ในสถานะรอดำเนินการ',
reqNotMet: 'ไม่ตรงกัน',
systemError: 'ระบบเกิดข้อผิดพลาด',
},
},
@ -1241,11 +1289,11 @@ export default {
},
stats: {
Pending: 'ใบเพิ่มหนี้',
Expire: 'พ้นกำหนด',
Payment: 'ชำระเงิน',
Receipt: 'ใบเสร็จรับเงิน',
Succeed: 'เสร็จสิ้น',
Issued: 'ใบเพิ่มหนี้',
Expired: 'พ้นกำหนด',
PaymentPending: 'ชำระเงิน',
PaymentSuccess: 'ใบเสร็จรับเงิน',
ProcessComplete: 'เสร็จสิ้น',
},
viewMode: {

View file

@ -83,292 +83,283 @@ onMounted(async () => {
});
</script>
<template>
<div>
<q-btn
rounded
dense
flat
no-caps
color="dark"
class="q-pa-none account-menu-down dropdown-menu"
>
<div class="row items-center">
<div class="q-pa-none">
<q-avatar
class="surface-1 bordered"
:size="$q.screen.lt.sm ? '30px' : '40px'"
>
<q-img
:ratio="1"
class="text-center"
:src="userImage"
v-if="userImage"
>
<template #error>
<div
class="no-padding full-width full-height flex items-center justify-center"
style="
background: linear-gradient(
135deg,
rgba(43, 137, 223, 1) 0%,
rgba(230, 51, 81, 1) 100%
);
"
>
<q-img
v-if="gender"
:ratio="1"
:src="`/no-img-${gender === 'female' ? 'female' : 'man'}.png`"
></q-img>
<q-icon
v-else
name="mdi-account-outline"
color="white"
:size="$q.screen.lt.sm ? 'xs' : 'sm'"
/>
</div>
</template>
</q-img>
<q-icon
v-else
name="mdi-account-outline"
:size="$q.screen.lt.sm ? 'xs' : 'sm'"
/>
</q-avatar>
</div>
<div
class="text-left q-px-md"
v-if="$q.screen.gt.sm"
style="color: var(--foreground)"
<q-btn
rounded
dense
flat
no-caps
color="dark"
class="q-pa-none account-menu-down dropdown-menu"
>
<div class="row items-center">
<div class="q-pa-none">
<q-avatar
class="surface-1 bordered"
:size="$q.screen.lt.sm ? '30px' : '40px'"
>
<div class="text-caption column">
<span
v-if="isLoggedIn()"
class="text-weight-bold ellipsis"
style="max-width: 9vw"
>
<q-img
:ratio="1"
class="text-center"
:src="userImage"
v-if="userImage"
>
<template #error>
<div
class="no-padding full-width full-height flex items-center justify-center"
style="
background: linear-gradient(
135deg,
rgba(43, 137, 223, 1) 0%,
rgba(230, 51, 81, 1) 100%
);
"
>
<q-img
v-if="gender"
:ratio="1"
:src="`/no-img-${gender === 'female' ? 'female' : 'man'}.png`"
></q-img>
<q-icon
v-else
name="mdi-account-outline"
color="white"
:size="$q.screen.lt.sm ? 'xs' : 'sm'"
/>
</div>
</template>
</q-img>
<q-icon
v-else
name="mdi-account-outline"
:size="$q.screen.lt.sm ? 'xs' : 'sm'"
/>
</q-avatar>
</div>
<div
class="text-left q-px-md"
v-if="$q.screen.gt.sm"
style="color: var(--foreground)"
>
<div class="text-caption column">
<span
v-if="isLoggedIn()"
class="text-weight-bold ellipsis"
style="max-width: 9vw"
>
{{ getName() }}
<q-tooltip>
{{ getName() }}
<q-tooltip>
{{ getName() }}
</q-tooltip>
</span>
<span
v-else
class="text-weight-bold q-pb-xs ellipsis"
style="max-width: 9vw"
>
</q-tooltip>
</span>
<span
v-else
class="text-weight-bold q-pb-xs ellipsis"
style="max-width: 9vw"
>
{{ 'Guest' }}
<q-tooltip>
{{ 'Guest' }}
<q-tooltip>
{{ 'Guest' }}
</q-tooltip>
</span>
</q-tooltip>
</span>
<div style="font-size: 11px">
{{ filterRole?.join(' | ') }}
</div>
<div style="font-size: 11px">
{{ filterRole?.join(' | ') }}
</div>
</div>
<div v-if="$q.screen.gt.sm" class="text-right">
<q-icon name="mdi-chevron-down" />
</div>
</div>
<q-menu :offset="[5, 10]" max-width="300px">
<div
no-padding
class="row justify-center bordered rounded"
style="overflow: hidden"
>
<div class="column col-12 items-center">
<div
class="full-width row justify-center"
style="position: relative"
>
<div
class="full-width account-cover"
:class="{ dark: $q.dark.isActive }"
></div>
<div class="avartar-border">
<q-avatar
id="changeAvatar"
size="72px"
class="surface-1 bordered"
:class="{ 'hover-profile': isLoggedIn() }"
@click="
() => {
isLoggedIn() ? inputFile.click() : '';
}
"
>
<q-img
:ratio="1"
class="text-center"
:src="userImage"
v-if="userImage"
>
<template #error>
<div
class="no-padding full-width full-height flex items-center justify-center"
style="
background: linear-gradient(
135deg,
rgba(43, 137, 223, 1) 0%,
rgba(230, 51, 81, 1) 100%
);
"
>
<q-img
v-if="gender"
:ratio="1"
:src="`/no-img-${gender === 'female' ? 'female' : 'man'}.png`"
></q-img>
<q-icon
v-else
name="mdi-account-outline"
color="white"
/>
</div>
</template>
</q-img>
<q-icon name="mdi-account-outline" v-else />
</q-avatar>
</div>
</div>
<div v-if="$q.screen.gt.sm" class="text-right">
<q-icon name="mdi-chevron-down" />
</div>
</div>
<q-menu :offset="[5, 10]" max-width="300px">
<div
no-padding
class="row justify-center bordered rounded"
style="overflow: hidden"
>
<div class="column col-12 items-center">
<div class="full-width row justify-center" style="position: relative">
<div
class="text-subtitle2 q-mb-xs text-center full-width ellipsis q-px-md"
style="margin-top: 58px"
>
class="full-width account-cover"
:class="{ dark: $q.dark.isActive }"
></div>
<div class="avartar-border">
<q-avatar
id="changeAvatar"
size="72px"
class="surface-1 bordered"
:class="{ 'hover-profile': isLoggedIn() }"
@click="
() => {
isLoggedIn() ? inputFile.click() : '';
}
"
>
<q-img
:ratio="1"
class="text-center"
:src="userImage"
v-if="userImage"
>
<template #error>
<div
class="no-padding full-width full-height flex items-center justify-center"
style="
background: linear-gradient(
135deg,
rgba(43, 137, 223, 1) 0%,
rgba(230, 51, 81, 1) 100%
);
"
>
<q-img
v-if="gender"
:ratio="1"
:src="`/no-img-${gender === 'female' ? 'female' : 'man'}.png`"
></q-img>
<q-icon v-else name="mdi-account-outline" color="white" />
</div>
</template>
</q-img>
<q-icon name="mdi-account-outline" v-else />
</q-avatar>
</div>
</div>
<div
class="text-subtitle2 q-mb-xs text-center full-width ellipsis q-px-md"
style="margin-top: 58px"
>
<span v-if="isLoggedIn()">
{{ getName() }}
</span>
<span v-else>{{ 'Guest' }}</span>
<q-tooltip>
<span v-if="isLoggedIn()">
{{ getName() }}
</span>
<span v-else>{{ 'Guest' }}</span>
<q-tooltip>
<span v-if="isLoggedIn()">
{{ getName() }}
</span>
<span v-else>{{ 'Guest' }}</span>
</q-tooltip>
</div>
<div
class="badge q-px-sm q-mb-md text-caption"
:class="{ dark: $q.dark.isActive }"
>
{{ filterRole?.join(' | ') }}
</div>
</q-tooltip>
</div>
<div class="column col-12">
<q-separator />
<div class="column justify-center">
<q-list
:dense="true"
v-for="op in options"
:key="op.value"
:id="op.value"
>
<q-item
v-if="op.value !== 'mode'"
clickable
:disable="op.disabled"
:id="`btn-${op.value}`"
@click="$emit(op.value)"
>
<q-item-section avatar>
<q-icon :name="op.icon" :color="op.color" size="20px" />
</q-item-section>
<q-item-section class="q-py-sm">
{{ $t(op.label) }}
</q-item-section>
</q-item>
<q-separator v-if="op.value === 'mode'" />
<q-item
v-if="op.value === 'mode'"
clickable
:id="`btn-${op.value}`"
@click="$emit(op.value)"
>
<q-item-section avatar>
<q-icon :name="op.icon" :color="op.color" size="20px" />
</q-item-section>
<q-item-section class="q-py-sm">
<div class="row justify-between">
<span>
{{ $t(op.label) }}
</span>
<span class="app-text-muted-2">
{{
$t(
`general.${theme === Theme.Auto ? 'baseOnDevice' : theme}`,
)
}}
<q-icon name="mdi-chevron-right" />
</span>
</div>
</q-item-section>
<q-menu
class="bordered rounded"
anchor="top right"
self="top left"
max-width="200"
:offset="[10, 0]"
style="width: 160px"
:touch-position="$q.screen.lt.sm"
>
<div v-for="(mode, index) in themeMode" :key="index">
<q-item clickable @click="theme = setTheme(mode.value)">
<q-item-section>
<div class="row justify-between">
<span>
{{ $t(`general.${mode.label}`) }}
</span>
<q-icon
v-if="mode.value === theme"
name="mdi-check"
/>
</div>
</q-item-section>
</q-item>
</div>
</q-menu>
</q-item>
</q-list>
</div>
<q-separator />
<q-btn
v-if="isLoggedIn()"
no-caps
dense
unelevated
class="q-ma-md app-text-negative"
:class="{ dark: $q.dark.isActive }"
:label="$t('general.logout')"
@click="$emit('logout')"
id="btn-logout"
style="background-color: hsla(var(--negative-bg) / 0.1)"
v-close-popup
/>
<q-btn
v-else
no-caps
dense
color="primary"
unelevated
class="q-ma-md app-text-negative"
:label="$t('general.login')"
@click="$emit('login')"
id="btn-login"
v-close-popup
/>
<div
class="badge q-px-sm q-mb-md text-caption"
:class="{ dark: $q.dark.isActive }"
>
{{ filterRole?.join(' | ') }}
</div>
</div>
</q-menu>
</q-btn>
</div>
<div class="column col-12">
<q-separator />
<div class="column justify-center">
<q-list
:dense="true"
v-for="op in options"
:key="op.value"
:id="op.value"
>
<q-item
v-if="op.value !== 'mode'"
clickable
:disable="op.disabled"
:id="`btn-${op.value}`"
@click="$emit(op.value)"
>
<q-item-section avatar>
<q-icon :name="op.icon" :color="op.color" size="20px" />
</q-item-section>
<q-item-section class="q-py-sm">
{{ $t(op.label) }}
</q-item-section>
</q-item>
<q-separator v-if="op.value === 'mode'" />
<q-item
v-if="op.value === 'mode'"
clickable
:id="`btn-${op.value}`"
@click="$emit(op.value)"
>
<q-item-section avatar>
<q-icon :name="op.icon" :color="op.color" size="20px" />
</q-item-section>
<q-item-section class="q-py-sm">
<div class="row justify-between">
<span>
{{ $t(op.label) }}
</span>
<span class="app-text-muted-2">
{{
$t(
`general.${theme === Theme.Auto ? 'baseOnDevice' : theme}`,
)
}}
<q-icon name="mdi-chevron-right" />
</span>
</div>
</q-item-section>
<q-menu
class="bordered rounded"
anchor="top right"
self="top left"
max-width="200"
:offset="[10, 0]"
style="width: 160px"
:touch-position="$q.screen.lt.sm"
>
<div v-for="(mode, index) in themeMode" :key="index">
<q-item clickable @click="theme = setTheme(mode.value)">
<q-item-section>
<div class="row justify-between">
<span>
{{ $t(`general.${mode.label}`) }}
</span>
<q-icon
v-if="mode.value === theme"
name="mdi-check"
/>
</div>
</q-item-section>
</q-item>
</div>
</q-menu>
</q-item>
</q-list>
</div>
<q-separator />
<q-btn
v-if="isLoggedIn()"
no-caps
dense
unelevated
class="q-ma-md app-text-negative"
:class="{ dark: $q.dark.isActive }"
:label="$t('general.logout')"
@click="$emit('logout')"
id="btn-logout"
style="background-color: hsla(var(--negative-bg) / 0.1)"
v-close-popup
/>
<q-btn
v-else
no-caps
dense
color="primary"
unelevated
class="q-ma-md app-text-negative"
:label="$t('general.login')"
@click="$emit('login')"
id="btn-login"
v-close-popup
/>
</div>
</div>
</q-menu>
</q-btn>
</template>
<style scoped lang="scss">
.account-menu-down {

View file

@ -2649,7 +2649,7 @@ const emptyCreateDialog = ref(false);
},
{
name: $t('general.uploadFile'),
anchor: 'drawer-info-file-upload',
anchor: 'form-info-file-upload',
tab: 'personalInfo',
},
@ -2672,14 +2672,14 @@ const emptyCreateDialog = ref(false);
},
...(currentFromDataEmployee.employeePassport?.map((v, i) => ({
name: dateFormat(v.expireDate),
anchor: `drawer-employee-employeePassport-${i}`,
anchor: `form-employee-employeePassport-${i}`,
tab: 'passport',
sub: true,
})) || []),
{
name: $t('customerEmployee.form.group.visa'),
anchor: 'drawer-visa',
anchor: 'form-visa',
tab: 'visa',
useBtn:
currentFromDataEmployee.employeeVisa?.filter((item) => {
@ -2695,7 +2695,7 @@ const emptyCreateDialog = ref(false);
...(currentFromDataEmployee.employeeVisa?.map((v, i) => ({
name: dateFormat(v.expireDate),
anchor: `drawer-employee-visa-${i}`,
anchor: `form-employee-visa-${i}`,
tab: 'visa',
sub: true,
})) || []),
@ -2774,7 +2774,7 @@ const emptyCreateDialog = ref(false);
/>
</template>
<template v-slot:btn-drawer-visa>
<template v-slot:btn-form-visa>
<q-btn
id="form-add-visa"
dense
@ -2787,7 +2787,7 @@ const emptyCreateDialog = ref(false);
/>
</template>
<template v-slot:btn-drawer-employee-checkup>
<template v-slot:btn-form-employee-checkup>
<q-btn
id="form-add-checkup"
dense
@ -2800,7 +2800,7 @@ const emptyCreateDialog = ref(false);
/>
</template>
<template v-slot:btn-drawer-employee-work-history>
<template v-slot:btn-form-employee-work-history>
<q-btn
id="form-add-work-history"
dense

View file

@ -101,6 +101,12 @@ const columns = [
function triggerDialog(type: 'add' | 'edit' | 'view') {
if (type === 'add') {
registeredBranchId.value = '';
formDataWorkflow.value = {
status: 'CREATED',
name: '',
step: [],
};
pageState.addModal = true;
pageState.isDrawerEdit = true;
}
@ -630,6 +636,7 @@ watch([() => pageState.inputSearch, workflowPageSize], () => {
<q-td style="width: 20%" class="text-right">
<q-btn
icon="mdi-eye-outline"
:id="`btn-eye-${props.row.name}`"
size="sm"
dense
round
@ -642,7 +649,7 @@ watch([() => pageState.inputSearch, workflowPageSize], () => {
"
/>
<KebabAction
:id-name="props.row.id"
:id-name="props.row.name"
:status="props.row.status"
@view="
() => {

View file

@ -1262,6 +1262,11 @@ async function submitService(notClose = false) {
onCreateImageList.value,
);
if (res) {
const group = productGroup.value?.find(
(g) => g.id === currentIdGroup.value,
);
if (group) group.status = 'ACTIVE';
allStat.value[1].count = allStat.value[1].count + 1;
stat.value[1].count = stat.value[1].count + 1;
} else {
@ -1306,6 +1311,11 @@ async function submitProduct(notClose = false) {
);
if (res) {
const group = productGroup.value?.find(
(g) => g.id === currentIdGroup.value,
);
if (group) group.status = 'ACTIVE';
allStat.value[2].count = allStat.value[2].count + 1;
stat.value[2].count = stat.value[2].count + 1;
} else {
@ -4337,6 +4347,7 @@ watch(
class="split-pay q-mx-sm"
input-class="text-caption text-right"
type="number"
for="dialog-input-installments"
v-model="formService.installments"
/>
{{ $t('quotation.receiptDialog.installments') }}

View file

@ -12,6 +12,7 @@ import useMyBranch from 'stores/my-branch';
import { useQuotationForm } from './form';
import { hslaColors } from './constants';
import { pageTabs, columnQuotation } from './constants';
import { toCamelCase } from 'stores/utils';
// NOTE Import Types
import { CustomerBranchCreate, CustomerType } from 'stores/customer/types';
@ -389,13 +390,7 @@ async function storeDataLocal(id: string) {
color: hsl(var(--info-bg));
"
>
{{
quotationStats[
pageState.currentTab === 'Invoice'
? 'paymentInProcess'
: (pageState.currentTab.toLowerCase() as keyof typeof quotationStats)
]
}}
{{ Object.values(quotationStats).reduce((a, c) => a + c, 0) }}
</q-badge>
<q-btn
class="q-ml-sm"
@ -894,6 +889,7 @@ async function storeDataLocal(id: string) {
:text="value.text"
:icon-color="value.iconColor"
:bg-color="value.color"
:id="`btn-add-new-${value.text}`"
@trigger="
() => {
triggerCreateCustomerd({

View file

@ -514,6 +514,7 @@ onMounted(async () => {
<div class="q-ml-auto row" style="gap: 10px">
<q-btn
id="btn-payment"
@click.stop
unelevated
padding="4px 8px"
@ -552,6 +553,7 @@ onMounted(async () => {
:key="opts.status"
>
<q-item
:id="`btn-payment-${opts.status}`"
v-if="
(p.paymentStatus === 'PaymentWait' &&
opts.status !== 'PaymentRetry') ||
@ -591,6 +593,7 @@ onMounted(async () => {
flat
rounded
padding="0"
id="btn-show-file"
:icon="`mdi-chevron-${state.payExpansion[i] ? 'down' : 'up'}`"
@click.stop="
state.payExpansion[i] = !state.payExpansion[i]
@ -625,6 +628,7 @@ onMounted(async () => {
}}
<q-btn
unelevated
id="btn-upload-file"
:label="$t('general.upload')"
rounded
class="app-bg-info q-mt-sm"

View file

@ -1421,6 +1421,7 @@ async function formDownload() {
>
<button
v-for="value in statusQuotationForm"
:id="`btn-status-${value.title}`"
:key="value.title"
class="q-pa-sm bordered status-color row items-center"
:class="{
@ -1553,6 +1554,8 @@ async function formDownload() {
</div>
<nav class="q-ml-auto">
<AddButton
id="btn-add-worker"
for="btn-add-worker"
v-if="
!readonly &&
(!quotationFormState.source ||
@ -1565,6 +1568,8 @@ async function formDownload() {
@click.stop="triggerSelectEmployeeDialog"
/>
<AddButton
id="btn-add-worker"
for="btn-add-worker"
v-if="
!!quotationFormState.source &&
quotationFormState.source.quotationStatus !==
@ -1621,6 +1626,7 @@ async function formDownload() {
v-if="!readonly"
icon-only
class="q-ml-auto"
id="trigger-product-service-dialog"
@click.stop="triggerProductServiceDialog"
/>
</nav>
@ -2243,6 +2249,7 @@ async function formDownload() {
outlined
icon="mdi-play-box-outline"
color="207 96% 32%"
id="btn-view-example"
@click="storeDataLocal"
>
{{ $t('general.view', { msg: $t('general.example') }) }}
@ -2258,6 +2265,7 @@ async function formDownload() {
solid
icon="mdi-account-multiple-check-outline"
color="207 96% 32%"
id="btn-submit-accepted"
@click="
() => {
submitAccepted();
@ -2273,6 +2281,7 @@ async function formDownload() {
solid
icon="mdi-account-multiple-check-outline"
color="207 96% 32%"
id="btn-select-invoice"
@click="
() => {
view = View.Invoice;
@ -2296,6 +2305,7 @@ async function formDownload() {
solid
icon="mdi-account-multiple-check-outline"
color="207 96% 32%"
id="btn-approve-invoice"
@click="
() => {
convertInvoiceToSubmit();
@ -2319,15 +2329,18 @@ async function formDownload() {
<UndoButton
outlined
@click="closeTab()"
id="btn-undo"
v-if="quotationFormState.mode === 'edit'"
/>
<CloseButton
outlined
id="btn-close"
@click="closeTab()"
v-if="quotationFormState.mode === 'info' && closeAble()"
/>
<SaveButton
type="submit"
id="btn-save"
v-if="
quotationFormState.mode === 'create' ||
quotationFormState.mode === 'edit'
@ -2338,6 +2351,7 @@ async function formDownload() {
<EditButton
v-else
class="no-print"
id="btn-edit"
@click="quotationFormState.mode = 'edit'"
solid
/>

View file

@ -508,6 +508,7 @@ watch(
flat
rounded
icon="mdi-store-plus-outline"
id="trigger-add-dialog"
@click="triggerAddDialog"
/>
<q-btn
@ -515,6 +516,7 @@ watch(
flat
rounded
icon="mdi-information-outline"
id="trigger-info"
@click="triggerInfo"
:color="pageState.infoDrawer ? 'info' : ''"
style="color: hsl(var(--text-mute))"

View file

@ -289,6 +289,7 @@ watch(() => state.search, getWorkerList);
<q-menu class="bordered" ref="refMenu">
<q-list>
<q-item
id="trigger-create-employee"
v-close-popup
dense
clickable
@ -308,6 +309,7 @@ watch(() => state.search, getWorkerList);
</q-item>
<q-item
id="import-worker"
v-close-popup
dense
clickable
@ -470,6 +472,7 @@ watch(() => state.search, getWorkerList);
<template #grid="{ item: emp, index }">
<div :key="emp.id" class="col-md-2 col-sm-6 col-12">
<button
:id="`select-worker-${emp.firstName}`"
class="selectable-item full-width"
:class="{
['selectable-item__selected']:
@ -520,11 +523,12 @@ watch(() => state.search, getWorkerList);
</div>
<template #footer>
<div class="q-gutter-x-xs q-ml-auto">
<CancelButton outlined @click="clean" />
<CancelButton id="btn-cancel" outlined @click="clean" />
<MainButton
icon="mdi-check"
color="207 96% 32%"
solid
id="btn-success"
@click="
emits('success', {
worker: workerSelected,

View file

@ -102,6 +102,7 @@ function goToRequestList(id: string) {
<template v-if="col.name === '#codeRequest'">
<span
class="cursor-pointer link"
:id="`go-to-request-list-${props.row.code}`"
@click="goToRequestList(props.row.id)"
>
{{ props.row.code }}

View file

@ -30,7 +30,7 @@ export const DEFAULT_DATA: QuotationPayload = {
paySplit: [],
paySplitCount: 0,
payCondition: 'Full',
dueDate: new Date(),
dueDate: new Date(Date.now() + 86400000),
discount: 0,
contactTel: '',
contactName: '',

View file

@ -199,16 +199,23 @@ watch(
<ProfileBanner
prefix="dialog"
active
use-toggle
hide-fade
hide-active
:toggle-title="$t('status.title')"
:icon="'ph-building-office'"
:img="imageState.imageUrl || null"
:title="data.name"
:caption="data.code"
:color="`hsla(var(--green-8-hsl)/1)`"
:bg-color="`hsla(var(--green-8-hsl)/0.1)`"
v-model:toggle-status="data.status"
@view="viewImage"
@edit="editImage"
@update:toggle-status="
() => {
data.status = data.status === 'CREATED' ? 'INACTIVE' : 'CREATED';
}
"
/>
</div>
@ -317,9 +324,10 @@ watch(
>
<ProfileBanner
:prefix="data.name"
active
hide-fade
hide-active
use-toggle
:active="data.status !== 'INACTIVE'"
:toggle-title="$t('status.title')"
:icon="'ph-building-office'"
:title="data.name"
:caption="data.code"
@ -330,8 +338,10 @@ watch(
imageState.refreshImageState ? `?ts=${Date.now()}` : '',
) || null
"
v-model:toggle-status="data.status"
@view="viewImage"
@edit="editImage"
@update:toggle-status="$emit('changeStatus')"
/>
</div>
@ -354,7 +364,10 @@ watch(
}"
style="position: absolute; z-index: 999; top: 0; right: 0"
>
<div v-if="true" class="surface-1 row rounded">
<div
v-if="data.status !== 'INACTIVE'"
class="surface-1 row rounded"
>
<UndoButton
v-if="isEdit"
id="btn-info-basic-undo"

View file

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { QTableProps } from 'quasar';
import { QSelect, QTableProps } from 'quasar';
import { dialog } from 'src/stores/utils';
import { onMounted, reactive, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
@ -97,8 +97,11 @@ const blankFormData: InstitutionPayload = {
districtId: '',
provinceId: '',
selectedImage: '',
status: 'CREATED',
};
const statusFilter = ref<'all' | 'statusACTIVE' | 'statusINACTIVE'>('all');
const refFilter = ref<InstanceType<typeof QSelect>>();
const refAgenciesDialog = ref();
const formData = ref<InstitutionPayload>(structuredClone(blankFormData));
const currAgenciesData = ref<Institution>();
@ -109,6 +112,7 @@ const onCreateImageList = ref<{
function triggerDialog(type: 'add' | 'edit' | 'view') {
if (type === 'add') {
formData.value = structuredClone(blankFormData);
pageState.addModal = true;
pageState.isDrawerEdit = true;
}
@ -155,6 +159,7 @@ function assignFormData(data: Institution) {
districtId: data.districtId,
provinceId: data.provinceId,
selectedImage: data.selectedImage,
status: data.status,
};
}
@ -175,6 +180,7 @@ async function submit(opt?: { selectedImage: string }) {
subDistrictId: formData.value.subDistrictId,
districtId: formData.value.districtId,
provinceId: formData.value.provinceId,
status: formData.value.status,
};
if (
(pageState.isDrawerEdit && currAgenciesData.value?.id) ||
@ -182,6 +188,7 @@ async function submit(opt?: { selectedImage: string }) {
) {
const ret = await institutionStore.editInstitution(
Object.assign(payload, {
status: undefined,
id: currAgenciesData.value.id,
selectedImage: opt?.selectedImage || undefined,
}),
@ -238,6 +245,12 @@ async function fetchData(mobileFetch?: boolean) {
? data.value.length + (pageState.total === data.value.length ? 1 : 0)
: pageSize.value,
query: pageState.inputSearch,
status:
statusFilter.value === 'all'
? undefined
: statusFilter.value === 'statusACTIVE'
? 'ACTIVE'
: 'INACTIVE',
});
if (ret) {
@ -250,6 +263,54 @@ async function fetchData(mobileFetch?: boolean) {
}
}
async function triggerChangeStatus(data?: Institution) {
const targetId = (data && data.id) || currAgenciesData.value?.id;
const targetStatus = (data && data.status) || currAgenciesData.value?.status;
if (data) assignFormData(data);
if (targetId === undefined || targetStatus === undefined) return;
return await new Promise((resolve, reject) => {
dialog({
color: targetStatus !== 'INACTIVE' ? 'warning' : 'info',
icon:
targetStatus !== 'INACTIVE'
? 'mdi-alert'
: 'mdi-message-processing-outline',
title: t('dialog.title.confirmChangeStatus'),
actionText:
targetStatus !== 'INACTIVE' ? t('general.close') : t('general.open'),
message:
targetStatus !== 'INACTIVE'
? t('dialog.message.confirmChangeStatusOff')
: t('dialog.message.confirmChangeStatusOn'),
action: async () => {
await changeStatus(targetId).then(resolve).catch(reject);
},
cancel: () => {},
});
});
}
async function changeStatus(id?: string) {
const targetId = id || currAgenciesData.value?.id;
if (targetId === undefined) return;
formData.value.status =
formData.value.status !== 'INACTIVE' ? 'INACTIVE' : 'ACTIVE';
const res = await institutionStore.editInstitution({
id: targetId,
...formData.value,
});
if (res) {
formData.value.status = res.status;
if (currAgenciesData.value) {
currAgenciesData.value.status = res.status;
}
await fetchData();
}
}
onMounted(async () => {
navigatorStore.current.title = 'agencies.title';
navigatorStore.current.path = [{ text: 'agencies.caption', i18n: true }];
@ -259,7 +320,7 @@ onMounted(async () => {
});
watch(
() => pageState.inputSearch,
() => [pageState.inputSearch, statusFilter.value],
() => {
page.value = 1;
data.value = [];
@ -342,10 +403,26 @@ watch(
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
<template v-if="$q.screen.lt.md" v-slot:append>
<span class="row">
<q-separator vertical />
<q-btn
icon="mdi-filter-variant"
unelevated
class="q-ml-sm"
padding="4px"
size="sm"
rounded
@click="refFilter?.showPopup"
/>
</span>
</template>
</q-input>
<div class="row col-md-5 justify-end" style="white-space: nowrap">
<!-- <q-select
<q-select
v-show="$q.screen.gt.sm"
ref="refFilter"
v-model="statusFilter"
outlined
dense
@ -362,7 +439,7 @@ watch(
{ label: $t('general.active'), value: 'statusACTIVE' },
{ label: $t('general.inactive'), value: 'statusINACTIVE' },
]"
/> -->
/>
<q-select
v-if="!pageState.gridView"
id="select-field"
@ -540,6 +617,18 @@ watch(
</div>
</template>
</q-img>
<q-badge
class="absolute-bottom-right no-padding"
style="
border-radius: 50%;
min-width: 8px;
min-height: 8px;
"
:style="{
background: `var(--${props.row.status === 'INACTIVE' ? 'stone-5' : 'green-6'})`,
}"
></q-badge>
</q-avatar>
<span class="col q-pl-md">
<div>
@ -594,6 +683,7 @@ watch(
<q-td>
<q-btn
icon="mdi-eye-outline"
:id="`btn-eye-${props.row.name}`"
size="sm"
dense
round
@ -606,8 +696,7 @@ watch(
"
/>
<KebabAction
hide-toggle
:id-name="props.row.id"
:id-name="props.row.name"
:status="props.row.status"
@view="
() => {
@ -622,6 +711,7 @@ watch(
}
"
@delete="() => triggerDelete(props.row.id)"
@change-status="() => triggerChangeStatus(props.row)"
/>
</q-td>
</q-tr>
@ -649,6 +739,18 @@ watch(
</div>
</template>
</q-img>
<q-badge
class="absolute-bottom-right no-padding"
style="
border-radius: 50%;
min-width: 10px;
min-height: 10px;
"
:style="{
background: `var(--${props.row.status === 'INACTIVE' ? 'stone-5' : 'green-6'})`,
}"
></q-badge>
</q-avatar>
<span class="text-weight-bold column q-pl-md">
{{
@ -677,7 +779,6 @@ watch(
"
/>
<KebabAction
hide-toggle
:id-name="props.row.id"
:status="props.row.status"
@view="
@ -693,6 +794,7 @@ watch(
}
"
@delete="() => triggerDelete(props.row.id)"
@change-status="() => triggerChangeStatus(props.row)"
/>
</nav>
</header>
@ -814,6 +916,7 @@ watch(
if (v) await submit({ selectedImage: v });
}
"
@change-status="triggerChangeStatus"
:readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit"
v-model="pageState.addModal"

View file

@ -241,6 +241,10 @@ watch([() => pageState.inputSearch, () => pageState.statusFilter], () => {
label: $t('requestList.status.Pending'),
value: RequestDataStatus.Pending,
},
{
label: $t('requestList.status.Ready'),
value: RequestDataStatus.Ready,
},
{
label: $t('requestList.status.InProgress'),
value: RequestDataStatus.InProgress,

View file

@ -320,6 +320,16 @@ function goToQuotation(
window.open(url.toString(), '_blank');
}
function goToDebitNote(opt?: { tab?: string; id?: string }) {
const { tab, id } = opt || {};
const url = new URL(
`/debit-note/${id}?mode=info&tab=${tab}`,
window.location.origin,
);
window.open(url.toString(), '_blank');
}
</script>
<template>
<div class="column surface-0 fullscreen" v-if="data">
@ -503,7 +513,11 @@ function goToQuotation(
icon="mdi-file-document-outline"
:label="$t('requestList.quotationCode')"
:value="data.quotation.code || '-'"
@label-click="goToQuotation(data.quotation)"
@label-click="
data.quotation.isDebitNote
? goToDebitNote({ id: data.quotation.id, tab: 'title' })
: goToQuotation(data.quotation)
"
/>
<DataDisplay
clickable
@ -517,11 +531,17 @@ function goToQuotation(
@label-click="
(_: string, i: number) => {
if (!data) return;
goToQuotation(data.quotation, {
tab: 'invoice',
id: data.quotation.invoice?.[i]?.id,
amount: data.quotation.invoice?.[i]?.amount,
});
data.quotation.isDebitNote
? goToDebitNote({
id: data.quotation.id,
tab: 'payment',
})
: goToQuotation(data.quotation, {
tab: 'invoice',
id: data.quotation.invoice?.[i]?.id,
amount: data.quotation.invoice?.[i]?.amount,
});
}
"
/>
@ -536,7 +556,14 @@ function goToQuotation(
(i: Invoice) => i.payment?.code || [],
)
"
@click="goToQuotation(data.quotation, { tab: 'receipt' })"
@click="
data.quotation.isDebitNote
? goToDebitNote({
id: data.quotation.id,
tab: 'receipt',
})
: goToQuotation(data.quotation, { tab: 'receipt' })
"
/>
<div v-if="$q.screen.gt.sm" class="col"></div>
</div>

View file

@ -186,7 +186,7 @@ function getEmployeeName(
</q-td>
<q-td class="text-right">
<q-btn
:id="`btn-eye-${props.row.quotation.workName}`"
:id="`btn-eye-${props.row.code}`"
icon="mdi-eye-outline"
size="sm"
dense
@ -196,6 +196,7 @@ function getEmployeeName(
/>
<KebabAction
:id-name="`btn-kebab-${props.row.code}`"
hide-edit
hide-toggle
hide-view
@ -223,15 +224,13 @@ function getEmployeeName(
hide-kebab-delete
use-cancel
:badge-color="
props.row.requestDataStatus === RequestDataStatus.Pending
? '--orange-5-hsl'
: props.row.requestDataStatus === RequestDataStatus.Canceled
? '--red-5-hsl'
: props.row.requestDataStatus === RequestDataStatus.InProgress
? '--blue-6-hsl'
: props.row.requestDataStatus === RequestDataStatus.Completed
? '--green-8-hsl'
: '--orange-5-hsl'
{
[RequestDataStatus.Pending]: '--orange-5-hsl',
[RequestDataStatus.Ready]: '--yellow-6-hsl',
[RequestDataStatus.InProgress]: '--blue-6-hsl',
[RequestDataStatus.Completed]: '--green-8-hsl',
[RequestDataStatus.Canceled]: '--red-5-hsl',
}[props.row.requestDataStatus]
"
:urgent="props.row.quotation.urgent"
:code="props.row.code"

View file

@ -203,7 +203,7 @@ watch(
color: hsl(var(--info-bg));
"
>
{{ pageState.total }}
{{ Object.values(stats).reduce((s, v) => s + v, 0) }}
</q-badge>
<q-btn
class="q-ml-sm"

View file

@ -165,35 +165,49 @@ function submit() {
requestWorkId: string;
requestWorkStep?: Task;
}[] = [];
selectedEmployee.value.forEach((v, i) => {
const curr = v.stepStatus.find(
(s) =>
s.workStatus ===
(props.creditNote
? RequestWorkStatus.Canceled
: RequestWorkStatus.Ready) ||
s.workStatus ===
(props.creditNote
? RequestWorkStatus.Canceled
: RequestWorkStatus.InProgress),
);
if (curr) {
const task: Task = {
...curr,
attributes: curr.attributes,
workStatus:
curr.workStatus || props.creditNote
? RequestWorkStatus.Ready
: RequestWorkStatus.Canceled,
taskOrderId: '',
requestWork: selectedEmployee.value[i],
};
if (v.stepStatus.length === 0) {
selected.push({
step: task.step,
requestWorkId: task.requestWorkId,
requestWorkStep: task,
step: 0,
requestWorkId: v.id || '',
requestWorkStep: {
taskOrderId: '',
requestWork: v,
step: 0,
workStatus: '',
requestWorkId: '',
attributes: undefined,
},
});
} else {
const curr = v.stepStatus.find(
(s) =>
s.workStatus ===
(props.creditNote
? RequestWorkStatus.Canceled
: RequestWorkStatus.Ready) ||
s.workStatus ===
(props.creditNote
? RequestWorkStatus.Canceled
: RequestWorkStatus.InProgress),
);
if (curr) {
const task: Task = {
...curr,
attributes: curr.attributes,
workStatus:
curr.workStatus || props.creditNote
? RequestWorkStatus.Ready
: RequestWorkStatus.Canceled,
taskOrderId: '',
requestWork: selectedEmployee.value[i],
};
selected.push({
step: task.step,
requestWorkId: task.requestWorkId,
requestWorkStep: task,
});
}
}
});
@ -217,11 +231,17 @@ function onDialogOpen() {
if (taskList.value.length === 0) return;
const matchingItems = tempGroupEdit.value
.flatMap((g) => g.list)
.filter((l) =>
l.stepStatus.some((s) =>
taskList.value.some((t) => s.requestWorkId === t.requestWorkId),
),
);
.filter((l) => {
if (l.stepStatus.length === 0) {
return taskList.value.some(
(t) => t.requestWorkStep?.requestWork.id === l.id,
);
} else {
return l.stepStatus.some((s) =>
taskList.value.some((t) => s.requestWorkId === t.requestWorkId),
);
}
});
selectedEmployee.value = JSON.parse(JSON.stringify(matchingItems));
}

View file

@ -630,6 +630,7 @@ onMounted(async () => {
:task-list="taskListGroup"
@add-product="openProductDialog"
/>
<PaymentExpansion
v-if="view === null"
:readonly="readonly"
@ -806,6 +807,7 @@ onMounted(async () => {
<!-- @click="submit" -->
<SaveButton
v-if="!readonly"
:disabled="taskListGroup.length === 0"
type="submit"
@click.stop="(e) => refForm?.submit(e)"
:label="$t('creditNote.label.submit')"

View file

@ -88,6 +88,7 @@ async function triggerDelete(id: string) {
}
async function triggerCreateCreditNote() {
pageState.quotationId = '';
pageState.creditDialog = true;
}
@ -110,6 +111,7 @@ function navigateTo(opts: {
async function submit() {
navigateTo({ statusDialog: 'create', quotationId: pageState.quotationId });
close();
}
function close() {
@ -156,7 +158,7 @@ watch(
color: hsl(var(--info-bg));
"
>
{{ pageState.total }}
{{ stats.Pending + stats.Success || 0 }}
</q-badge>
<q-btn
class="q-ml-sm"
@ -180,13 +182,13 @@ watch(
:branch="[
{
icon: 'material-symbols-light:receipt-long',
count: stats[CreditNoteStatus.Pending],
count: stats[CreditNoteStatus.Pending] || 0,
label: `creditNote.stats.${CreditNoteStatus.Pending}`,
color: 'orange',
},
{
icon: 'mdi-check-decagram-outline',
count: stats[CreditNoteStatus.Success],
count: stats[CreditNoteStatus.Success] || 0,
label: `creditNote.stats.${CreditNoteStatus.Success}`,
color: 'blue',
},
@ -455,7 +457,6 @@ watch(
<section class="q-pa-md col full-width">
<div class="surface-1 rounded bordered q-pa-md full-height full-width">
<!-- TODO: bind quotation id -->
<FormCredit v-model:quotation-id="pageState.quotationId" />
</div>
</section>

View file

@ -74,6 +74,7 @@ const visible = computed(() =>
<template v-if="col.name === '#action'">
<KebabAction
:id-name="`btn-kebab-${props.row.quotation.workName}`"
hide-edit
hide-toggle
:hide-delete

View file

@ -51,7 +51,7 @@ const bankList = ref<BankBook[]>([]);
const worker = ref<Employee[]>([]);
const taskListGroup = ref<
{
product: RequestWork['productService']['product'];
product: RequestWork['productService'];
list: RequestWork[];
}[]
>([]);
@ -59,7 +59,7 @@ const taskListGroup = ref<
const elements = ref<HTMLElement[]>([]);
const chunks = ref<
{
product: RequestWork['productService']['product'];
product: RequestWork['productService'];
list: RequestWork[];
}[][]
>([[]]);
@ -112,6 +112,7 @@ async function getAttachment(quotationId: string) {
}
async function assignData() {
console.log(taskListGroup.value);
for (let i = 0; i < taskListGroup.value.length; i++) {
let el = elements.value.at(-1);
@ -269,7 +270,7 @@ function print() {
<PrintButton solid @click="print" />
</div>
<div class="row justify-between container color-debit-note">
<section class="content" v-for="chunk in chunks">
<section class="content" v-for="(chunk, i) in chunks" :key="i">
<ViewHeader
v-if="!!branch && !!customer && !!details"
:branch="branch"
@ -300,23 +301,31 @@ function print() {
<th>{{ $t('preview.pricePerUnit') }}</th>
<th>{{ $t('preview.value') }}</th>
</tr>
<tr v-for="(v, i) in chunk">
{{ 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.code }}</td>
<td>{{ v.product.name }}</td>
<td>{{ v.product.product.code }}</td>
<td>{{ v.product.product.name }}</td>
<td style="text-align: center">
{{
formatNumberDecimal(
calcPricePerUnit(v.product) +
(v.product.calcVat
? calcPricePerUnit(v.product) * (config?.vat || 0.07)
calcPricePerUnit(v.product.product) +
(v.product.product.calcVat
? calcPricePerUnit(v.product.product) *
(config?.vat || 0.07)
: 0),
2,
)
}}
</td>
<td style="text-align: center">
{{ formatNumberDecimal(calcPrice(v.product, v.list.length), 2) }}
{{
formatNumberDecimal(
calcPrice(v.product.product, v.list.length),
2,
)
}}
</td>
</tr>
</tbody>
@ -513,14 +522,16 @@ function print() {
</section>
<section
v-for="item in attachmentList.filter((v) => v.isImage)"
v-for="(item, i) in attachmentList.filter((v) => v.isImage)"
:key="i"
class="content"
>
<q-img :src="item.url" />
</section>
<ViewPDF
v-for="item in attachmentList.filter((v) => v.isPDF)"
v-for="(item, i) in attachmentList.filter((v) => v.isPDF)"
:key="i"
:url="item.url"
/>
</div>

View file

@ -44,8 +44,8 @@ const detail = defineModel<string>('detail');
outlined
dense
class="col"
v-model="detail"
:rules="[(val: string) => !!val || $t('form.error.required')]"
:model-value="readonly ? detail || '-' : detail"
@update:model-value="(v) => (detail = v?.toString())"
></q-input>
</main>
</q-expansion-item>

View file

@ -119,7 +119,7 @@ const currentFormData = ref<DebitNotePayload>({
payBillDate: new Date(),
paySplitCount: 0,
payCondition: PayCondition.Full,
dueDate: new Date(),
dueDate: new Date(Date.now() + 86400000),
discount: 0,
status: 'CREATED',
remark: '#[quotation-labor]<br/><br/>#[quotation-payment]',
@ -879,6 +879,14 @@ onMounted(async () => {
pageState.mode = route.query['mode'] as 'create' | 'edit' | 'info';
}
if (typeof route.query['tab'] === 'string') {
view.value =
{
payment: QuotationStatus.PaymentPending,
receipt: QuotationStatus.PaymentSuccess,
}[route.query['tab']] || null;
}
await useConfigStore().getConfig();
});
</script>
@ -943,6 +951,7 @@ onMounted(async () => {
/>
</nav>
<!-- #TODO add goToQuotation as @goto-quotation-->
<DocumentExpansion
v-if="view === null"
:readonly
@ -957,7 +966,7 @@ onMounted(async () => {
/>
<DebitNoteExpansion
v-if="view === null"
v-if="false"
:readonly
v-model:reason="currentFormData.reason"
v-model:detail="currentFormData.detail"
@ -987,7 +996,6 @@ onMounted(async () => {
/>
<!-- #TODO add openProductDialog at @add-product-->
<ProductExpansion
v-if="view === null"
:readonly
@ -1156,7 +1164,6 @@ onMounted(async () => {
<nav class="row justify-end">
<!-- TODO: view example -->
<MainButton
v-if="view === null"
class="q-mr-auto"
outlined
icon="mdi-play-box-outline"
@ -1179,6 +1186,9 @@ onMounted(async () => {
<SaveButton
v-if="!readonly && pageState.mode === 'create'"
:disabled="
selectedWorkerItem.length === 0 && productService.length === 0
"
@click="submit"
:label="true ? $t('debitNote.label.submit') : $t('general.save')"
:icon="

View file

@ -35,7 +35,7 @@ const { stats, pageMax, page, data, pageSize } = storeToRefs(debitNote);
// NOTE: Variable
const pageState = reactive({
quotationId: '',
currentTab: DebitNoteStatus.Pending,
currentTab: DebitNoteStatus.Issued,
hideStat: false,
statusFilter: 'None',
inputSearch: '',
@ -64,7 +64,9 @@ async function getList(opts?: { page?: number; pageSize?: number }) {
page: opts?.page || page.value,
pageSize: opts?.pageSize || pageSize.value,
query: pageState.inputSearch === '' ? undefined : pageState.inputSearch,
deebitNoteStatus: pageState.currentTab as DebitNoteStatus | undefined,
status: (pageState.currentTab === DebitNoteStatus.Issued
? undefined
: pageState.currentTab) as DebitNoteStatus,
includeRegisteredBranch: true,
});
@ -90,6 +92,7 @@ async function triggerDelete(id: string) {
}
async function triggerCreateDebitNote() {
pageState.quotationId = '';
pageState.debitDialog = true;
}
@ -114,6 +117,7 @@ function navigateTo(opts: {
async function submit() {
navigateTo({ statusDialog: 'create', quotationId: pageState.quotationId });
close();
}
function close() {
@ -125,7 +129,17 @@ onMounted(async () => {
navigator.current.title = 'debitNote.title';
navigator.current.path = [{ text: 'debitNote.caption', i18n: true }];
debitNote.getDebitNoteStats().then((res) => res && (stats.value = res));
await debitNote.getDebitNoteStats().then((res) => {
if (res) {
stats.value = res;
stats.value['issued'] = Object.values(res).reduce(
(sum, value) => sum + value,
0,
);
}
});
getList();
});
@ -159,7 +173,12 @@ watch(
color: hsl(var(--info-bg));
"
>
{{ pageState.total }}
{{
Object.entries(stats).reduce(
(sum, [key, value]) => (key === 'canceled' ? sum : sum + value),
0,
)
}}
</q-badge>
<q-btn
class="q-ml-sm"
@ -183,33 +202,28 @@ watch(
:branch="[
{
icon: 'material-symbols-light:receipt-long',
count: stats[DebitNoteStatus.Pending] || 0,
label: `debitNote.stats.${DebitNoteStatus.Pending}`,
count: stats['issued'] || 0,
label: `debitNote.stats.${DebitNoteStatus.Issued}`,
color: 'orange',
},
{
icon: 'mdi-clock-alert-outline',
count: stats[DebitNoteStatus.Expire] || 0,
label: `debitNote.stats.${DebitNoteStatus.Expire}`,
color: 'cyan',
},
{
icon: 'tabler:cash-register',
count: stats[DebitNoteStatus.Payment] || 0,
label: `debitNote.stats.${DebitNoteStatus.Payment}`,
count: stats['paymentPending'] || 0,
label: `debitNote.stats.${DebitNoteStatus.PaymentPending}`,
color: 'dark-orange',
},
{
icon: 'fluent:receipt-money-16-regular',
count: stats[DebitNoteStatus.Receipt] || 0,
label: `debitNote.stats.${DebitNoteStatus.Receipt}`,
count: stats['paymentSuccess'] || 0,
label: `debitNote.stats.${DebitNoteStatus.PaymentSuccess}`,
color: 'green',
},
{
icon: 'mdi-check-decagram-outline',
count: stats[DebitNoteStatus.Succeed] || 0,
label: `debitNote.stats.${DebitNoteStatus.Succeed}`,
count: stats['processComplete'] || 0,
label: `debitNote.stats.${DebitNoteStatus.ProcessComplete}`,
color: 'blue',
},
]"
@ -485,7 +499,6 @@ watch(
<section class="q-pa-md col full-width">
<div class="surface-1 rounded bordered q-pa-md full-height full-width">
<!-- TODO: bind quotation id -->
<FormDebit v-model:quotation-id="pageState.quotationId" />
</div>
</section>

View file

@ -77,6 +77,7 @@ const visible = computed(() =>
<template v-if="col.name === '#action'">
<KebabAction
:id-name="`btn-kebab-${props.row.workName}`"
hide-toggle
hide-edit
@edit="$emit('edit', props.row)"

View file

@ -4,29 +4,24 @@ import { formatNumberDecimal } from 'src/stores/utils';
export const taskStatusOpts = [
{
status: DebitNoteStatus.Expire,
name: `debitNote.status.${DebitNoteStatus.Expire}`,
status: DebitNoteStatus.PaymentPending,
name: `debitNote.status.${DebitNoteStatus.PaymentPending}`,
},
{
status: DebitNoteStatus.Payment,
name: `debitNote.status.${DebitNoteStatus.Payment}`,
status: DebitNoteStatus.PaymentSuccess,
name: `debitNote.status.${DebitNoteStatus.PaymentSuccess}`,
},
{
status: DebitNoteStatus.Receipt,
name: `debitNote.status.${DebitNoteStatus.Receipt}`,
},
{
status: DebitNoteStatus.Succeed,
name: `debitNote.status.${DebitNoteStatus.Succeed}`,
status: DebitNoteStatus.PaymentSuccess,
name: `debitNote.status.${DebitNoteStatus.ProcessComplete}`,
},
];
export const pageTabs = [
{ label: 'Pending', value: DebitNoteStatus.Pending },
{ label: 'Expire', value: DebitNoteStatus.Expire },
{ label: 'Payment', value: DebitNoteStatus.Payment },
{ label: 'Receipt', value: DebitNoteStatus.Receipt },
{ label: 'Succeed', value: DebitNoteStatus.Succeed },
{ label: 'Pending', value: DebitNoteStatus.Issued },
{ label: 'Payment', value: DebitNoteStatus.PaymentPending },
{ label: 'Receipt', value: DebitNoteStatus.PaymentSuccess },
{ label: 'Succeed', value: DebitNoteStatus.ProcessComplete },
];
export enum Status {
@ -85,6 +80,9 @@ export const columns = [
] as const satisfies QTableProps['columns'];
export const hslaColors: Record<string, string> = {
Pending: '--blue-6-hsl',
Success: '--red-6-hsl',
Issued: '--orange-6-hsl',
PaymentPending: '--orange-10-hsl',
PaymentSuccess: '--green-8-hsl',
ProcessComplete: '--blue-6-hsl',
Canceled: '--red-6-hsl',
};

View file

@ -61,7 +61,7 @@ const quotationCreatedBy = defineModel<string>('quotationCreatedBy');
:disabled="!readonly"
/>
<DatePicker
:label="$t('general.createdAt')"
:label="$t('general.dueDate')"
class="col-md-2 col-6"
:model-value="dueDate || new Date(Date.now())"
@update:model-value="
@ -69,6 +69,22 @@ const quotationCreatedBy = defineModel<string>('quotationCreatedBy');
if (typeof v === 'string') dueDate = v;
}
"
:rules="[
() => {
if (!dueDate) return $t('form.error.required');
const currentDate = new Date(dueDate);
const toDate = new Date();
if (
!readonly &&
(currentDate.getTime() === toDate.getTime() ||
currentDate.getTime() < toDate.getTime())
)
return $t('quotation.validateDueDate');
return true;
},
]"
:disabled-dates="(date: Date) => date.getTime() <= Date.now()"
:readonly
/>

View file

@ -26,7 +26,7 @@ export async function getDebitNoteList(params?: {
page?: number;
pageSize?: number;
query?: string;
deebitNoteStatus?: Status;
status?: Status;
includeRegisteredBranch?: boolean;
}) {
const res = await api.get<PaginationResult<Data>>(`/${ENDPOINT}`, {
@ -66,12 +66,11 @@ export const useDebitNote = defineStore('debit-note-store', () => {
const page = ref<number>(1);
const pageMax = ref<number>(1);
const pageSize = ref<number>(30);
const stats = ref<Record<Status, number>>({
[Status.Pending]: 0,
[Status.Expire]: 0,
[Status.Payment]: 0,
[Status.Receipt]: 0,
[Status.Succeed]: 0,
const stats = ref<Record<string, number>>({
['issued']: 0,
['paymentPending']: 0,
['paymentSuccess']: 0,
['processComplete']: 0,
});
return {

View file

@ -86,9 +86,9 @@ export type DebitNote = {
};
export enum DebitNoteStatus {
Pending = 'Pending',
Expire = 'Expire',
Payment = 'Payment',
Receipt = 'Receipt',
Succeed = 'Succeed',
Issued = 'Issued',
Expired = 'Expired',
PaymentPending = 'PaymentPending',
PaymentSuccess = 'PaymentSuccess',
ProcessComplete = 'ProcessComplete',
}

View file

@ -4,6 +4,7 @@ import { Institution, InstitutionPayload } from './types';
import { api } from 'src/boot/axios';
import { PaginationResult } from 'src/types';
import useFlowStore from '../flow';
import { Status } from '../types';
export const useInstitution = defineStore('institution-store', () => {
const flowStore = useFlowStore();
@ -26,6 +27,7 @@ export const useInstitution = defineStore('institution-store', () => {
pageSize?: number;
query?: string;
group?: string;
status?: Status;
payload?: { group?: string[] };
}) {
const { payload, ...params } = opts || {};
@ -40,7 +42,7 @@ export const useInstitution = defineStore('institution-store', () => {
params,
},
)
: await api.get<PaginationResult<Institution>>(`/institution`, {
: await api.get<PaginationResult<Institution>>('/institution', {
params,
});
@ -80,6 +82,7 @@ export const useInstitution = defineStore('institution-store', () => {
const res = await api.put(`/institution/${data.id}`, {
...data,
id: undefined,
group: undefined,
});
if (res.status < 400) {
return res.data;

View file

@ -1,4 +1,5 @@
import { District, Province, SubDistrict } from '../address';
import { Status } from '../types';
export type Institution = {
id: string;
@ -23,6 +24,7 @@ export type Institution = {
subDistrictId: string;
districtId: string;
provinceId: string;
status: Status;
};
export type InstitutionPayload = {
@ -44,4 +46,5 @@ export type InstitutionPayload = {
subDistrictId: string;
districtId: string;
provinceId: string;
status?: Status;
};

View file

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

View file

@ -124,7 +124,7 @@ export function deleteItem(items: unknown[], index: number) {
}
export function formatNumberDecimal(num: number, point: number = 2): string {
return num.toLocaleString('eng', {
return (num || 0).toLocaleString('eng', {
minimumFractionDigits: point,
maximumFractionDigits: point,
});
@ -609,3 +609,9 @@ export function getEmployeeName(
['tha']: `${typeof employee.namePrefix === 'string' ? useOptionStore().mapOption(employee.namePrefix) : ''} ${employee.firstName} ${employee.lastName}`,
}[opts?.locale || 'eng'];
}
export function toCamelCase(text: string): string {
return text
.replace(/[^a-zA-Z0-9]+(.)/g, (match, chr) => chr.toUpperCase())
.replace(/^[A-Z]/, (match) => match.toLowerCase());
}