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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -46,6 +46,7 @@ type Options = { label: string; value: string };
(lhs: Options, rhs: Options) => lhs.value.localeCompare(rhs.value), (lhs: Options, rhs: Options) => lhs.value.localeCompare(rhs.value),
) )
" "
:rules="[(val: string) => !!val || $t('form.error.required')]"
v-model="group" v-model="group"
> >
<template v-slot:option="{ scope, opt }"> <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"> <section class="col-12 row q-col-gutter-sm">
<SelectQuotation <SelectQuotation
for="select-quotation" for="select-quotation"
required
class="col" class="col"
v-model:value="quotationId" v-model:value="quotationId"
:label="$t('general.select', { msg: $t('quotation.title') })" :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"> <section class="col-12 row q-col-gutter-sm">
<SelectQuotation <SelectQuotation
for="select-quotation" for="select-quotation"
required
class="col" class="col"
v-model:value="quotationId" v-model:value="quotationId"
:label="$t('general.select', { msg: $t('quotation.title') })" :label="$t('general.select', { msg: $t('quotation.title') })"

View file

@ -18,7 +18,12 @@ function handleClick() {
</script> </script>
<template> <template>
<label class="switch" @click.stop="handleClick"> <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> <div class="slider round" :class="{ disable: disable }"></div>
</label> </label>
</template> </template>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -147,6 +147,7 @@ export default {
ofPage: '{current} of {total}', ofPage: '{current} of {total}',
included: 'Included', included: 'Included',
notIncluded: 'Not Included', notIncluded: 'Not Included',
dueDate: 'Due date',
}, },
menu: { menu: {
@ -1057,65 +1058,102 @@ export default {
cancel: 'Cancel', cancel: 'Cancel',
}, },
backend: { 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.', productGroupAssociatedBadReq: 'Product group associated cannot be found.',
productTypeIsUsed: 'Product type is in used.',
productGroupBadReq: 'Product group cannot be found.', productGroupBadReq: 'Product group cannot be found.',
serviceNotFound: 'Service cannot be found.',
someProductBadReq: 'Some product not 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.', 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.', oneOrMoreUserBadReq: 'One or more user cannot be found.',
oneOrMoreBranchBadReq: 'One or more branch cannot be found.', oneOrMoreBranchBadReq: 'One or more branch cannot be found.',
customerBranchNotFound: 'Customer branch cannot be found.', employeeBadReq: 'Employee cannot be found.',
customerBranchIsUsed: 'Customer branch is in used.', parentMenuBadReq: 'Parent menu not found.',
customerNotFound: 'Customer cannot be found.', menuBadReq: 'Menu cannot be found.',
customerIsUsed: 'Customer is in used.',
oneOrMoreBranchMissing: oneOrMoreBranchMissing:
'One or more branch cannot be delete and is missing.', 'One or more branch cannot be delete and is missing.',
employeeCheckupNotFound: 'Employee checkup cannot be found.', cantMakeHQAndBranchSameTime:
provinceNotFound: 'Province cannot be found.', 'Cannot make this as headquaters and branch at the same time.',
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.',
unknowHowToVerify: 'Unknown how to verify identity.', unknowHowToVerify: 'Unknown how to verify identity.',
noPermission: noPermission:
'You do not have permission to access or perform with this resource.', 'You do not have permission to access or perform with this resource.',
noPermissionToAccess: noPermissionToAccess:
'You do not have permission to access or perform with this resource.', 'You do not have permission to access or perform with this resource.',
relationProvinceNotFound: 'Province cannot be found.', relationProvinceNotFound: 'Province cannot be found.',
relationDistrictNotFound: 'District cannot be found.', relationDistrictNotFound: 'District cannot be found.',
relationSubDistrictNotFound: 'Sub-district cannot be found.', relationSubDistrictNotFound: 'Sub-district cannot be found.',
relationHQNotFound: 'Headquarters cannot be found.', relationHQNotFound: 'Headquarters cannot be found.',
relationBranchNotFound: 'Branch cannot be found.',
relationCustomerNotFound: 'Customer cannot be found.', relationCustomerNotFound: 'Customer cannot be found.',
relationCustomerBranchNotFound: 'Customer Branch 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.', sameBranchCodeExists: 'This Head Office Abbreviation is already in use.',
productNameExists: productNameExists:
'Product with the same name already exists. If you want to create with this name please select another code.', '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', validateError: 'Validate Error',
codeMisMatch: 'Code Mismatch', codeMisMatch: 'Code Mismatch',
userExists: 'User already exits.',
crossCompanyNotPermit: 'Cannot move between different headoffice', crossCompanyNotPermit: 'Cannot move between different headoffice',
errorOccure: errorOccure:
'An error has occurred, causing the system to be unable to function. Please try again later.', '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. ', authFailed: 'Authentication Failed. Please try again later. ',
installmentsValidateFailed: installmentsValidateFailed:
'Validation failed. Each installment must include at least one product. Please review and update the installments accordingly.', '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.', 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: { stats: {
Pending: 'Debit Note', Issued: 'Debit Note',
Expire: 'Expired', Expired: 'Expired',
Payment: 'Payment', PaymentPending: 'Payment',
Receipt: 'Receipt', PaymentSuccess: 'Receipt',
Succeed: 'Completed', ProcessComplete: 'Completed',
}, },
viewMode: { viewMode: {

View file

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

View file

@ -83,292 +83,283 @@ onMounted(async () => {
}); });
</script> </script>
<template> <template>
<div> <q-btn
<q-btn rounded
rounded dense
dense flat
flat no-caps
no-caps color="dark"
color="dark" class="q-pa-none account-menu-down dropdown-menu"
class="q-pa-none account-menu-down dropdown-menu" >
> <div class="row items-center">
<div class="row items-center"> <div class="q-pa-none">
<div class="q-pa-none"> <q-avatar
<q-avatar class="surface-1 bordered"
class="surface-1 bordered" :size="$q.screen.lt.sm ? '30px' : '40px'"
: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)"
> >
<div class="text-caption column"> <q-img
<span :ratio="1"
v-if="isLoggedIn()" class="text-center"
class="text-weight-bold ellipsis" :src="userImage"
style="max-width: 9vw" 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() }} {{ getName() }}
<q-tooltip> </q-tooltip>
{{ getName() }} </span>
</q-tooltip> <span
</span> v-else
<span class="text-weight-bold q-pb-xs ellipsis"
v-else style="max-width: 9vw"
class="text-weight-bold q-pb-xs ellipsis" >
style="max-width: 9vw" {{ 'Guest' }}
> <q-tooltip>
{{ 'Guest' }} {{ 'Guest' }}
<q-tooltip> </q-tooltip>
{{ 'Guest' }} </span>
</q-tooltip>
</span>
<div style="font-size: 11px"> <div style="font-size: 11px">
{{ filterRole?.join(' | ') }} {{ filterRole?.join(' | ') }}
</div>
</div> </div>
</div> </div>
<div v-if="$q.screen.gt.sm" class="text-right">
<q-icon name="mdi-chevron-down" />
</div>
</div> </div>
<q-menu :offset="[5, 10]" max-width="300px"> <div v-if="$q.screen.gt.sm" class="text-right">
<div <q-icon name="mdi-chevron-down" />
no-padding </div>
class="row justify-center bordered rounded" </div>
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>
<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 <div
class="text-subtitle2 q-mb-xs text-center full-width ellipsis q-px-md" class="full-width account-cover"
style="margin-top: 58px" :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()"> <span v-if="isLoggedIn()">
{{ getName() }} {{ getName() }}
</span> </span>
<span v-else>{{ 'Guest' }}</span> <span v-else>{{ 'Guest' }}</span>
<q-tooltip> </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>
</div> </div>
<div
<div class="column col-12"> class="badge q-px-sm q-mb-md text-caption"
<q-separator /> :class="{ dark: $q.dark.isActive }"
<div class="column justify-center"> >
<q-list {{ filterRole?.join(' | ') }}
: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>
</div> </div>
</q-menu>
</q-btn> <div class="column col-12">
</div> <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> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.account-menu-down { .account-menu-down {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { QTableProps } from 'quasar'; import { QSelect, QTableProps } from 'quasar';
import { dialog } from 'src/stores/utils'; import { dialog } from 'src/stores/utils';
import { onMounted, reactive, ref, watch } from 'vue'; import { onMounted, reactive, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
@ -97,8 +97,11 @@ const blankFormData: InstitutionPayload = {
districtId: '', districtId: '',
provinceId: '', provinceId: '',
selectedImage: '', selectedImage: '',
status: 'CREATED',
}; };
const statusFilter = ref<'all' | 'statusACTIVE' | 'statusINACTIVE'>('all');
const refFilter = ref<InstanceType<typeof QSelect>>();
const refAgenciesDialog = ref(); const refAgenciesDialog = ref();
const formData = ref<InstitutionPayload>(structuredClone(blankFormData)); const formData = ref<InstitutionPayload>(structuredClone(blankFormData));
const currAgenciesData = ref<Institution>(); const currAgenciesData = ref<Institution>();
@ -109,6 +112,7 @@ const onCreateImageList = ref<{
function triggerDialog(type: 'add' | 'edit' | 'view') { function triggerDialog(type: 'add' | 'edit' | 'view') {
if (type === 'add') { if (type === 'add') {
formData.value = structuredClone(blankFormData);
pageState.addModal = true; pageState.addModal = true;
pageState.isDrawerEdit = true; pageState.isDrawerEdit = true;
} }
@ -155,6 +159,7 @@ function assignFormData(data: Institution) {
districtId: data.districtId, districtId: data.districtId,
provinceId: data.provinceId, provinceId: data.provinceId,
selectedImage: data.selectedImage, selectedImage: data.selectedImage,
status: data.status,
}; };
} }
@ -175,6 +180,7 @@ async function submit(opt?: { selectedImage: string }) {
subDistrictId: formData.value.subDistrictId, subDistrictId: formData.value.subDistrictId,
districtId: formData.value.districtId, districtId: formData.value.districtId,
provinceId: formData.value.provinceId, provinceId: formData.value.provinceId,
status: formData.value.status,
}; };
if ( if (
(pageState.isDrawerEdit && currAgenciesData.value?.id) || (pageState.isDrawerEdit && currAgenciesData.value?.id) ||
@ -182,6 +188,7 @@ async function submit(opt?: { selectedImage: string }) {
) { ) {
const ret = await institutionStore.editInstitution( const ret = await institutionStore.editInstitution(
Object.assign(payload, { Object.assign(payload, {
status: undefined,
id: currAgenciesData.value.id, id: currAgenciesData.value.id,
selectedImage: opt?.selectedImage || undefined, selectedImage: opt?.selectedImage || undefined,
}), }),
@ -238,6 +245,12 @@ async function fetchData(mobileFetch?: boolean) {
? data.value.length + (pageState.total === data.value.length ? 1 : 0) ? data.value.length + (pageState.total === data.value.length ? 1 : 0)
: pageSize.value, : pageSize.value,
query: pageState.inputSearch, query: pageState.inputSearch,
status:
statusFilter.value === 'all'
? undefined
: statusFilter.value === 'statusACTIVE'
? 'ACTIVE'
: 'INACTIVE',
}); });
if (ret) { 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 () => { onMounted(async () => {
navigatorStore.current.title = 'agencies.title'; navigatorStore.current.title = 'agencies.title';
navigatorStore.current.path = [{ text: 'agencies.caption', i18n: true }]; navigatorStore.current.path = [{ text: 'agencies.caption', i18n: true }];
@ -259,7 +320,7 @@ onMounted(async () => {
}); });
watch( watch(
() => pageState.inputSearch, () => [pageState.inputSearch, statusFilter.value],
() => { () => {
page.value = 1; page.value = 1;
data.value = []; data.value = [];
@ -342,10 +403,26 @@ watch(
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </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> </q-input>
<div class="row col-md-5 justify-end" style="white-space: nowrap"> <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" v-model="statusFilter"
outlined outlined
dense dense
@ -362,7 +439,7 @@ watch(
{ label: $t('general.active'), value: 'statusACTIVE' }, { label: $t('general.active'), value: 'statusACTIVE' },
{ label: $t('general.inactive'), value: 'statusINACTIVE' }, { label: $t('general.inactive'), value: 'statusINACTIVE' },
]" ]"
/> --> />
<q-select <q-select
v-if="!pageState.gridView" v-if="!pageState.gridView"
id="select-field" id="select-field"
@ -540,6 +617,18 @@ watch(
</div> </div>
</template> </template>
</q-img> </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> </q-avatar>
<span class="col q-pl-md"> <span class="col q-pl-md">
<div> <div>
@ -594,6 +683,7 @@ watch(
<q-td> <q-td>
<q-btn <q-btn
icon="mdi-eye-outline" icon="mdi-eye-outline"
:id="`btn-eye-${props.row.name}`"
size="sm" size="sm"
dense dense
round round
@ -606,8 +696,7 @@ watch(
" "
/> />
<KebabAction <KebabAction
hide-toggle :id-name="props.row.name"
:id-name="props.row.id"
:status="props.row.status" :status="props.row.status"
@view=" @view="
() => { () => {
@ -622,6 +711,7 @@ watch(
} }
" "
@delete="() => triggerDelete(props.row.id)" @delete="() => triggerDelete(props.row.id)"
@change-status="() => triggerChangeStatus(props.row)"
/> />
</q-td> </q-td>
</q-tr> </q-tr>
@ -649,6 +739,18 @@ watch(
</div> </div>
</template> </template>
</q-img> </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> </q-avatar>
<span class="text-weight-bold column q-pl-md"> <span class="text-weight-bold column q-pl-md">
{{ {{
@ -677,7 +779,6 @@ watch(
" "
/> />
<KebabAction <KebabAction
hide-toggle
:id-name="props.row.id" :id-name="props.row.id"
:status="props.row.status" :status="props.row.status"
@view=" @view="
@ -693,6 +794,7 @@ watch(
} }
" "
@delete="() => triggerDelete(props.row.id)" @delete="() => triggerDelete(props.row.id)"
@change-status="() => triggerChangeStatus(props.row)"
/> />
</nav> </nav>
</header> </header>
@ -814,6 +916,7 @@ watch(
if (v) await submit({ selectedImage: v }); if (v) await submit({ selectedImage: v });
} }
" "
@change-status="triggerChangeStatus"
:readonly="!pageState.isDrawerEdit" :readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit" :isEdit="pageState.isDrawerEdit"
v-model="pageState.addModal" v-model="pageState.addModal"

View file

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

View file

@ -320,6 +320,16 @@ function goToQuotation(
window.open(url.toString(), '_blank'); 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> </script>
<template> <template>
<div class="column surface-0 fullscreen" v-if="data"> <div class="column surface-0 fullscreen" v-if="data">
@ -503,7 +513,11 @@ function goToQuotation(
icon="mdi-file-document-outline" icon="mdi-file-document-outline"
:label="$t('requestList.quotationCode')" :label="$t('requestList.quotationCode')"
:value="data.quotation.code || '-'" :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 <DataDisplay
clickable clickable
@ -517,11 +531,17 @@ function goToQuotation(
@label-click=" @label-click="
(_: string, i: number) => { (_: string, i: number) => {
if (!data) return; if (!data) return;
goToQuotation(data.quotation, {
tab: 'invoice', data.quotation.isDebitNote
id: data.quotation.invoice?.[i]?.id, ? goToDebitNote({
amount: data.quotation.invoice?.[i]?.amount, 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 || [], (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 v-if="$q.screen.gt.sm" class="col"></div>
</div> </div>

View file

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

View file

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

View file

@ -165,35 +165,49 @@ function submit() {
requestWorkId: string; requestWorkId: string;
requestWorkStep?: Task; requestWorkStep?: Task;
}[] = []; }[] = [];
selectedEmployee.value.forEach((v, i) => { selectedEmployee.value.forEach((v, i) => {
const curr = v.stepStatus.find( if (v.stepStatus.length === 0) {
(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({ selected.push({
step: task.step, step: 0,
requestWorkId: task.requestWorkId, requestWorkId: v.id || '',
requestWorkStep: task, 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; if (taskList.value.length === 0) return;
const matchingItems = tempGroupEdit.value const matchingItems = tempGroupEdit.value
.flatMap((g) => g.list) .flatMap((g) => g.list)
.filter((l) => .filter((l) => {
l.stepStatus.some((s) => if (l.stepStatus.length === 0) {
taskList.value.some((t) => s.requestWorkId === t.requestWorkId), 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)); selectedEmployee.value = JSON.parse(JSON.stringify(matchingItems));
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,29 +4,24 @@ import { formatNumberDecimal } from 'src/stores/utils';
export const taskStatusOpts = [ export const taskStatusOpts = [
{ {
status: DebitNoteStatus.Expire, status: DebitNoteStatus.PaymentPending,
name: `debitNote.status.${DebitNoteStatus.Expire}`, name: `debitNote.status.${DebitNoteStatus.PaymentPending}`,
}, },
{ {
status: DebitNoteStatus.Payment, status: DebitNoteStatus.PaymentSuccess,
name: `debitNote.status.${DebitNoteStatus.Payment}`, name: `debitNote.status.${DebitNoteStatus.PaymentSuccess}`,
}, },
{ {
status: DebitNoteStatus.Receipt, status: DebitNoteStatus.PaymentSuccess,
name: `debitNote.status.${DebitNoteStatus.Receipt}`, name: `debitNote.status.${DebitNoteStatus.ProcessComplete}`,
},
{
status: DebitNoteStatus.Succeed,
name: `debitNote.status.${DebitNoteStatus.Succeed}`,
}, },
]; ];
export const pageTabs = [ export const pageTabs = [
{ label: 'Pending', value: DebitNoteStatus.Pending }, { label: 'Pending', value: DebitNoteStatus.Issued },
{ label: 'Expire', value: DebitNoteStatus.Expire }, { label: 'Payment', value: DebitNoteStatus.PaymentPending },
{ label: 'Payment', value: DebitNoteStatus.Payment }, { label: 'Receipt', value: DebitNoteStatus.PaymentSuccess },
{ label: 'Receipt', value: DebitNoteStatus.Receipt }, { label: 'Succeed', value: DebitNoteStatus.ProcessComplete },
{ label: 'Succeed', value: DebitNoteStatus.Succeed },
]; ];
export enum Status { export enum Status {
@ -85,6 +80,9 @@ export const columns = [
] as const satisfies QTableProps['columns']; ] as const satisfies QTableProps['columns'];
export const hslaColors: Record<string, string> = { export const hslaColors: Record<string, string> = {
Pending: '--blue-6-hsl', Issued: '--orange-6-hsl',
Success: '--red-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" :disabled="!readonly"
/> />
<DatePicker <DatePicker
:label="$t('general.createdAt')" :label="$t('general.dueDate')"
class="col-md-2 col-6" class="col-md-2 col-6"
:model-value="dueDate || new Date(Date.now())" :model-value="dueDate || new Date(Date.now())"
@update:model-value=" @update:model-value="
@ -69,6 +69,22 @@ const quotationCreatedBy = defineModel<string>('quotationCreatedBy');
if (typeof v === 'string') dueDate = v; 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 :readonly
/> />

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ export type RequestData = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
quotation: QuotationFull; quotation: QuotationFull & { isDebitNote: boolean };
quotationId: string; quotationId: string;
flow: Record<string, any>; 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 { export function formatNumberDecimal(num: number, point: number = 2): string {
return num.toLocaleString('eng', { return (num || 0).toLocaleString('eng', {
minimumFractionDigits: point, minimumFractionDigits: point,
maximumFractionDigits: point, maximumFractionDigits: point,
}); });
@ -609,3 +609,9 @@ export function getEmployeeName(
['tha']: `${typeof employee.namePrefix === 'string' ? useOptionStore().mapOption(employee.namePrefix) : ''} ${employee.firstName} ${employee.lastName}`, ['tha']: `${typeof employee.namePrefix === 'string' ? useOptionStore().mapOption(employee.namePrefix) : ''} ${employee.firstName} ${employee.lastName}`,
}[opts?.locale || 'eng']; }[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());
}