Merge branch 'develop'
This commit is contained in:
commit
8021d91356
113 changed files with 9689 additions and 3248 deletions
|
|
@ -3,6 +3,7 @@ FROM node:20-slim AS build-stage
|
|||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
RUN npm i -g corepack@latest
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /app
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ defineEmits<{
|
|||
}>();
|
||||
|
||||
const bankBookOptions = ref<Record<string, unknown>[]>([]);
|
||||
let bankBoookFilter: (
|
||||
let bankBookFilter: (
|
||||
value: string,
|
||||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
|
@ -77,7 +77,7 @@ function change(e: Event) {
|
|||
|
||||
onMounted(() => {
|
||||
if (optionStore.globalOption) {
|
||||
bankBoookFilter = selectFilterOptionRefMod(
|
||||
bankBookFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption.bankBook),
|
||||
bankBookOptions,
|
||||
'label',
|
||||
|
|
@ -94,7 +94,7 @@ onMounted(() => {
|
|||
watch(
|
||||
() => optionStore.globalOption,
|
||||
() => {
|
||||
bankBoookFilter = selectFilterOptionRefMod(
|
||||
bankBookFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption.bankBook),
|
||||
bankBookOptions,
|
||||
'label',
|
||||
|
|
@ -131,8 +131,8 @@ watch(
|
|||
|
||||
<div
|
||||
v-for="(book, i) in bankBookList"
|
||||
class="col-12 row"
|
||||
:class="{ 'q-pt-lg': i !== 0 }"
|
||||
class="col-12"
|
||||
:class="{ 'q-pt-lg': i !== 0, row: $q.screen.gt.sm }"
|
||||
:key="i"
|
||||
>
|
||||
<q-separator
|
||||
|
|
@ -172,8 +172,8 @@ watch(
|
|||
</span>
|
||||
|
||||
<div
|
||||
class="bordered q-mr-sm rounded"
|
||||
:class="{ 'pointer-none': readonly }"
|
||||
class="bordered q-mr-sm rounded col text-center overflow-hidden"
|
||||
:class="{ 'pointer-none': readonly, 'q-my-sm': $q.screen.lt.md }"
|
||||
>
|
||||
<ImageHover
|
||||
:readonly="readonly"
|
||||
|
|
@ -214,7 +214,7 @@ watch(
|
|||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (book.bankName = v) : '')
|
||||
"
|
||||
@filter="bankBoookFilter"
|
||||
@filter="bankBookFilter"
|
||||
@clear="book.bankName = ''"
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ defineProps<{
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-6 col-md-4"
|
||||
class="col-12 col-md-4"
|
||||
:label="$t('form.telephone')"
|
||||
for="input-telephone-no"
|
||||
:model-value="readonly ? telephoneNo || '-' : telephoneNo"
|
||||
|
|
@ -116,7 +116,7 @@ defineProps<{
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-6 col-md-4"
|
||||
class="col-12 col-md-4"
|
||||
:label="$t('branch.form.contactTelephone')"
|
||||
for="input-contact"
|
||||
:model-value="readonly ? contact || '-' : contact"
|
||||
|
|
@ -139,7 +139,7 @@ defineProps<{
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-6 col-md-4"
|
||||
class="col-12 col-md-4"
|
||||
:label="$t('branch.form.webUrl')"
|
||||
for="input-web-url"
|
||||
:model-value="readonly ? webUrl || '-' : webUrl"
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-md-4 col-12"
|
||||
:label="$t('general.licenseNumber')"
|
||||
v-model="permitNo"
|
||||
:rules="[(val) => val && val.length > 0]"
|
||||
|
|
@ -212,7 +212,7 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
|
|||
/>
|
||||
|
||||
<DatePicker
|
||||
class="col-3"
|
||||
class="col-md-3 col-12"
|
||||
id="input-start-date"
|
||||
:readonly="readonly"
|
||||
:label="$t('general.dateOfIssue')"
|
||||
|
|
@ -221,7 +221,7 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
|
|||
/>
|
||||
|
||||
<DatePicker
|
||||
class="col-3"
|
||||
class="col-md-3 col-12"
|
||||
id="input-start-date"
|
||||
:readonly="readonly"
|
||||
:label="$t('general.expirationDate')"
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ watch(
|
|||
/>
|
||||
|
||||
<SelectOffice
|
||||
for="input-responsible-area"
|
||||
v-model:value="responsibleArea"
|
||||
v-if="userType === 'MESSENGER'"
|
||||
:readonly="readonly"
|
||||
|
|
|
|||
|
|
@ -221,7 +221,6 @@ watch(
|
|||
class="col"
|
||||
:label="$t('personnel.form.lastName')"
|
||||
v-model="lastName"
|
||||
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -289,9 +288,8 @@ watch(
|
|||
label="Surname"
|
||||
v-model="lastNameEN"
|
||||
:rules="[
|
||||
(val: string) => !!val || $t('form.error.required'),
|
||||
(val: string) =>
|
||||
/^[A-Za-z\s]+$/.test(val) || $t('form.error.letterOnly'),
|
||||
!val || /^[A-Za-z\s]+$/.test(val) || $t('form.error.letterOnly'),
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -408,7 +406,7 @@ watch(
|
|||
outlined
|
||||
readonly
|
||||
:label="$t('personnel.age')"
|
||||
class="col-md-2 col-12"
|
||||
class="col-md-2 col-6"
|
||||
:model-value="
|
||||
birthDate?.toString() === 'Invalid Date' ||
|
||||
birthDate?.toString() === undefined
|
||||
|
|
@ -472,7 +470,7 @@ watch(
|
|||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
class="col-2"
|
||||
class="col-md-2 col-6"
|
||||
:dense="dense"
|
||||
v-model="gender"
|
||||
:readonly="readonly"
|
||||
|
|
@ -506,7 +504,7 @@ watch(
|
|||
option-label="label"
|
||||
option-value="value"
|
||||
v-model="nationality"
|
||||
class="col-2"
|
||||
class="col-md-2 col-6"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="nationalityOptions"
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
|
|||
:for="`${prefixId}-input-hospital`"
|
||||
/>
|
||||
|
||||
<div class="col">
|
||||
<div class="col-md col-6">
|
||||
<DatePicker
|
||||
v-model="checkup.coverageStartDate"
|
||||
:id="`${prefixId}-input-coverage-start-date`"
|
||||
|
|
@ -301,7 +301,7 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
|
|||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
class="col-6"
|
||||
class="col-md-6 col-12"
|
||||
input-debounce="0"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
|
|||
:readonly="readonly || employeeOther.statusSave"
|
||||
hide-bottom-space
|
||||
:label="$t('customerEmployee.formFamily.citizenId')"
|
||||
class="col-4"
|
||||
class="col-md-4 col-12"
|
||||
v-model="employeeOther.citizenId"
|
||||
/>
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
|
|||
:readonly="readonly || employeeOther.statusSave"
|
||||
hide-bottom-space
|
||||
:label="$t('customerEmployee.formFamily.telephoneNo')"
|
||||
class="col-3"
|
||||
class="col-md-3 col-12"
|
||||
v-model="employeeOther.telephoneNo"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ watch(
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="q-col-gutter-sm" :class="{ row: $q.screen.gt.sm }">
|
||||
<div
|
||||
class="col row justify-center q-col-gutter-sml"
|
||||
style="max-height: 50%"
|
||||
|
|
@ -252,7 +252,7 @@ watch(
|
|||
input-debounce="0"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
class="col-6"
|
||||
class="col-md-6 col-12"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="workerStatusOptions"
|
||||
|
|
@ -283,7 +283,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-6"
|
||||
class="col-md-6 col-12"
|
||||
:label="$t('customerEmployee.form.previousPassportNumber')"
|
||||
v-model="previousPassportRef"
|
||||
:rules="[
|
||||
|
|
@ -306,7 +306,7 @@ watch(
|
|||
option-label="label"
|
||||
option-value="value"
|
||||
hide-dropdown-icon
|
||||
class="col-2"
|
||||
class="col-md-2 col-6"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="prefixNameOptions"
|
||||
|
|
@ -334,7 +334,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
class="col-md col-6"
|
||||
:label="$t('form.firstName')"
|
||||
:model-value="readonly ? firstName || '-' : firstName"
|
||||
@update:model-value="
|
||||
|
|
@ -388,7 +388,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-md-4 col-6"
|
||||
:label="$t('form.firstNameEN')"
|
||||
:model-value="readonly ? firstNameEN || '-' : firstNameEN"
|
||||
@update:model-value="
|
||||
|
|
@ -405,7 +405,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-md-4 col-6"
|
||||
:label="$t('form.middleNameEN')"
|
||||
:model-value="readonly ? middleNameEN || '-' : middleNameEN"
|
||||
@update:model-value="
|
||||
|
|
@ -419,7 +419,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-md-4 col-6"
|
||||
:label="$t('form.lastNameEN')"
|
||||
:model-value="readonly ? lastNameEN || '-' : lastNameEN"
|
||||
@update:model-value="
|
||||
|
|
@ -453,7 +453,7 @@ watch(
|
|||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
class="col-2"
|
||||
class="col-md-2 col-4"
|
||||
dense
|
||||
:readonly="readonly"
|
||||
:options="genderOptions"
|
||||
|
|
@ -482,7 +482,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-6 col-md-3"
|
||||
class="col-8 col-md-3"
|
||||
:label="$t('customerEmployee.form.passportNo')"
|
||||
v-model="passportNumber"
|
||||
:rules="[
|
||||
|
|
@ -494,7 +494,7 @@ watch(
|
|||
|
||||
<DatePicker
|
||||
v-model="birthDate"
|
||||
class="col-2"
|
||||
class="col-md-2 col-6"
|
||||
:id="`${prefixId}-input-birth-date`"
|
||||
:readonly="readonly"
|
||||
:label="$t('form.birthDate')"
|
||||
|
|
@ -513,7 +513,7 @@ watch(
|
|||
outlined
|
||||
readonly
|
||||
:label="$t('personnel.age')"
|
||||
class="col-2"
|
||||
class="col-md-2 col-6"
|
||||
:model-value="
|
||||
birthDate?.toString() === 'Invalid Date' ||
|
||||
birthDate?.toString() === undefined
|
||||
|
|
@ -534,7 +534,7 @@ watch(
|
|||
input-debounce="0"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
class="col"
|
||||
class="col-md col-6"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="nationalityOptions"
|
||||
|
|
@ -567,7 +567,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-md-4 col-6"
|
||||
:label="$t('customerEmployee.form.placeOfBirth')"
|
||||
:model-value="optionStore.mapOption(birthCountry || '')"
|
||||
@update:model-value="
|
||||
|
|
@ -621,7 +621,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-md-4 col-6"
|
||||
:label="$t('customerEmployee.form.passportPlace')"
|
||||
:model-value="
|
||||
readonly
|
||||
|
|
|
|||
|
|
@ -147,8 +147,13 @@ watch(
|
|||
<slot name="button"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div v-if="!ocr" class="col row justify-center" style="max-height: 50%">
|
||||
<div :class="{ row: $q.screen.gt.sm }">
|
||||
<div
|
||||
v-if="!ocr"
|
||||
class="col row justify-center"
|
||||
:class="{ 'q-mb-md': $q.screen.lt.md }"
|
||||
style="max-height: 50%"
|
||||
>
|
||||
<q-avatar
|
||||
style="border: 1px dashed; border-color: black"
|
||||
square
|
||||
|
|
@ -175,7 +180,7 @@ watch(
|
|||
input-debounce="0"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
:class="{ 'col-4': !ocr, 'col-6': ocr }"
|
||||
:class="{ 'col-md-4 col-6': !ocr, 'col-6': ocr }"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="workerTypeOptions"
|
||||
|
|
@ -207,7 +212,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
:class="{ 'col-4': !ocr, 'col-6': ocr }"
|
||||
:class="{ 'col-md-4 col-6': !ocr, 'col-6': ocr }"
|
||||
:label="$t('customerEmployee.form.visaNo')"
|
||||
:model-value="readonly ? number || '-' : number"
|
||||
@update:model-value="
|
||||
|
|
@ -221,7 +226,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
:class="{ 'col-4': !ocr, 'col-6': ocr }"
|
||||
:class="{ 'col-md-4 col-12': !ocr, 'col-6': ocr }"
|
||||
:label="$t('customerEmployee.form.visaPlace')"
|
||||
:model-value="readonly ? issuePlace || '-' : issuePlace"
|
||||
@update:model-value="
|
||||
|
|
@ -280,7 +285,7 @@ watch(
|
|||
input-debounce="0"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
:class="{ 'col-4': !ocr, 'col-6': ocr }"
|
||||
:class="{ 'col-md-4 col-6': !ocr, 'col-6': ocr }"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="visaTypeOptions"
|
||||
|
|
@ -303,7 +308,7 @@ watch(
|
|||
</template>
|
||||
</q-select>
|
||||
|
||||
<div class="col">
|
||||
<div class="col-md col-6">
|
||||
<DatePicker
|
||||
:id="`${prefixId}-date-picker-visa-issuance`"
|
||||
:readonly="readonly"
|
||||
|
|
@ -362,7 +367,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-md-4 col-12"
|
||||
:label="$t('customerEmployee.form.arrivalCardNo')"
|
||||
:model-value="readonly ? arrivalTMNo || '-' : arrivalTMNo"
|
||||
@update:model-value="
|
||||
|
|
@ -374,7 +379,7 @@ watch(
|
|||
/>
|
||||
|
||||
<DatePicker
|
||||
class="col-4"
|
||||
class="col-md-4 col-12"
|
||||
:id="`${prefixId}-date-picker-visa-enter`"
|
||||
:readonly="readonly"
|
||||
:label="$t('customerEmployee.form.visaEnter')"
|
||||
|
|
@ -391,7 +396,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-md-4 col-12"
|
||||
:label="$t('customerEmployee.form.visaCheckpoint')"
|
||||
:model-value="readonly ? arrivalAt || '-' : arrivalAt"
|
||||
@update:model-value="
|
||||
|
|
@ -414,7 +419,7 @@ watch(
|
|||
input-debounce="0"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
class="col-4"
|
||||
class="col-md-4 col-6"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="visaTypeOptions"
|
||||
|
|
@ -446,7 +451,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-md-4 col-6"
|
||||
:label="$t('customerEmployee.form.entryCount')"
|
||||
v-model="entryCount"
|
||||
type="number"
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ const workplaceFilter = selectFilterOptionRefMod(
|
|||
outlined
|
||||
:readonly="readonly || work.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-md-4 col-12"
|
||||
:label="$t('customerEmployee.formWorkHistory.identityNo')"
|
||||
v-model="work.identityNo"
|
||||
mask="#############"
|
||||
|
|
@ -256,7 +256,7 @@ const workplaceFilter = selectFilterOptionRefMod(
|
|||
outlined
|
||||
:readonly="readonly || work.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-md-4 col-6"
|
||||
:label="$t('customerEmployee.formWorkHistory.permitNo')"
|
||||
v-model="work.workPermitNo"
|
||||
/>
|
||||
|
|
@ -266,7 +266,7 @@ const workplaceFilter = selectFilterOptionRefMod(
|
|||
outlined
|
||||
:readonly="readonly || work.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-md-4 col-6"
|
||||
:label="$t('customerEmployee.formWorkHistory.permitIssuedAt')"
|
||||
v-model="work.workPermitIssueAt"
|
||||
/>
|
||||
|
|
@ -274,7 +274,7 @@ const workplaceFilter = selectFilterOptionRefMod(
|
|||
<DatePicker
|
||||
:label="$t('customerEmployee.formWorkHistory.permitIssueDate')"
|
||||
v-model="work.workPermitIssueDate"
|
||||
class="col-3"
|
||||
class="col-md-3 col-6"
|
||||
:id="`${prefixId}-date-picker-work-permit-issue-date`"
|
||||
:readonly="readonly || work.statusSave"
|
||||
clearable
|
||||
|
|
@ -282,7 +282,7 @@ const workplaceFilter = selectFilterOptionRefMod(
|
|||
<DatePicker
|
||||
:label="$t('customerEmployee.formWorkHistory.permitExpireDate')"
|
||||
v-model="work.workPermitExpireDate"
|
||||
class="col-3"
|
||||
class="col-md-3 col-6"
|
||||
:id="`${prefixId}-date-picker-work-permit-expire-date`"
|
||||
:readonly="readonly || work.statusSave"
|
||||
:disabled-dates="
|
||||
|
|
|
|||
|
|
@ -116,7 +116,11 @@ defineEmits<{
|
|||
"
|
||||
>
|
||||
<q-td class="text-center" v-if="fieldSelected.includes('orderNumber')">
|
||||
{{ (currentPage - 1) * pageSize + props.rowIndex + 1 }}
|
||||
{{
|
||||
$q.screen.xs
|
||||
? props.rowIndex + 1
|
||||
: (currentPage - 1) * pageSize + props.rowIndex + 1
|
||||
}}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('firstName')">
|
||||
|
|
@ -288,8 +292,8 @@ defineEmits<{
|
|||
: `${props.row.firstName} ${props.row.lastName} `.trim(),
|
||||
img:
|
||||
`${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}` ||
|
||||
'/images/employee-avatar.png',
|
||||
fallbackImg: '/images/employee-avatar.png',
|
||||
`/images/employee-avatar-${props.row.gender}.png`,
|
||||
fallbackImg: `/images/employee-avatar-${props.row.gender}.png`,
|
||||
male: props.row.gender === 'male',
|
||||
female: props.row.gender === 'female',
|
||||
detail: [
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ defineEmits<{
|
|||
hide-bottom-space
|
||||
:readonly="readonly"
|
||||
:disable="!readonly"
|
||||
class="col-3"
|
||||
class="col-md-3 col-12"
|
||||
:label="$t('customerEmployee.form.employeeCode')"
|
||||
v-model="code"
|
||||
/>
|
||||
|
|
@ -301,7 +301,7 @@ defineEmits<{
|
|||
outlined
|
||||
hide-bottom-space
|
||||
:readonly="readonly"
|
||||
class="col-6"
|
||||
class="col-md-6 col-12"
|
||||
:label="$t('customerEmployee.form.nrcNo')"
|
||||
:model-value="nrcNo"
|
||||
@update:model-value="(v) => (typeof v === 'string' ? (nrcNo = v) : '')"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import SelectMenuWithSearch from '../shared/SelectMenuWithSearch.vue';
|
|||
import ToggleButton from 'src/components/button/ToggleButton.vue';
|
||||
import NoData from '../NoData.vue';
|
||||
import SelectBranch from '../shared/select/SelectBranch.vue';
|
||||
import AddButton from '../button/AddButton.vue';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
|
|
@ -127,6 +128,7 @@ function optionSearch(val: string | null) {
|
|||
|
||||
defineEmits<{
|
||||
(e: 'moveUp'): void;
|
||||
(e: 'addStep'): void;
|
||||
(e: 'moveDown'): void;
|
||||
(e: 'changeStatus'): void;
|
||||
(e: 'triggerProperties', step: WorkFlowPayloadStep): void;
|
||||
|
|
@ -159,7 +161,10 @@ onMounted(async () => {
|
|||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`general.name`, { msg: $t('flow.title') }) }}
|
||||
<span class="row q-ml-lg items-center text-weight-regular text-body2">
|
||||
<span
|
||||
class="row items-center text-weight-regular text-body2"
|
||||
:class="{ 'q-ml-lg': $q.screen.gt.xs, 'q-mt-sm': $q.screen.lt.sm }"
|
||||
>
|
||||
<ToggleButton
|
||||
class="q-mr-sm"
|
||||
two-way
|
||||
|
|
@ -216,6 +221,13 @@ onMounted(async () => {
|
|||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`flow.processStep`) }}
|
||||
<AddButton
|
||||
v-if="!readonly && $q.screen.lt.md"
|
||||
id="btn-add-work"
|
||||
icon-only
|
||||
class="q-ml-sm"
|
||||
@click="$emit('addStep')"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ const detailEditorImageDrop = createEditorImageDrop(detail);
|
|||
"
|
||||
@drop="detailEditorImageDrop"
|
||||
min-height="5rem"
|
||||
class="q-mt-sm q-mb-xs"
|
||||
class="q-mt-sm q-mb-xs col"
|
||||
:flat="!readonly"
|
||||
:readonly="readonly"
|
||||
:toolbar-color="
|
||||
|
|
@ -275,4 +275,8 @@ const detailEditorImageDrop = createEditorImageDrop(detail);
|
|||
:deep(.q-editor__toolbar) {
|
||||
border-color: var(--surface-3) !important;
|
||||
}
|
||||
|
||||
:deep(.q-editor.q-editor--default) {
|
||||
width: 1px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ const detailEditorImageDrop = createEditorImageDrop(detail);
|
|||
/>
|
||||
|
||||
<q-field
|
||||
class="full-width"
|
||||
class="col-12"
|
||||
outlined
|
||||
for="input-service-description"
|
||||
id="input-service-description"
|
||||
|
|
@ -180,31 +180,34 @@ const detailEditorImageDrop = createEditorImageDrop(detail);
|
|||
>
|
||||
<q-editor
|
||||
dense
|
||||
class="q-mt-sm q-mb-xs col"
|
||||
:model-value="
|
||||
readonly ? serviceDescription || '-' : serviceDescription || ''
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (serviceDescription = v) : '')
|
||||
"
|
||||
@drop="detailEditorImageDrop"
|
||||
min-height="5rem"
|
||||
class="q-mt-sm q-mb-xs"
|
||||
:flat="!readonly"
|
||||
:readonly="readonly"
|
||||
:toolbar-toggle-color="readonly ? 'disabled' : 'primary'"
|
||||
:toolbar-color="
|
||||
readonly ? 'disabled' : $q.dark.isActive ? 'white' : ''
|
||||
"
|
||||
:toolbar-toggle-color="readonly ? 'disabled' : 'primary'"
|
||||
style="
|
||||
cursor: auto;
|
||||
color: var(--foreground);
|
||||
border-color: var(--surface-3);
|
||||
"
|
||||
:style="`width: ${$q.screen.gt.xs ? '100%' : '63vw'}`"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (serviceDescription = v) : '')
|
||||
"
|
||||
@drop="detailEditorImageDrop"
|
||||
/>
|
||||
</q-field>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
:deep(.q-editor.q-editor--default) {
|
||||
width: 1px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ function optionSearch(val: string | null) {
|
|||
|
||||
<div class="col-12 row q-col-gutter-sm">
|
||||
<q-select
|
||||
behavior="menu"
|
||||
:readonly
|
||||
outlined
|
||||
dense
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { formatNumberDecimal, commaInput } from 'stores/utils';
|
||||
import { QTableProps } from 'quasar';
|
||||
import { QTableProps, QTableSlots } from 'quasar';
|
||||
import { calculatePrice } from 'src/utils/arithmetic';
|
||||
|
||||
const serviceCharge = defineModel<number>('serviceCharge');
|
||||
|
|
@ -9,24 +9,48 @@ const agentPrice = defineModel<number>('agentPrice');
|
|||
const price = defineModel<number>('price');
|
||||
const vatIncluded = defineModel<boolean>('vatIncluded');
|
||||
const calcVat = defineModel<boolean>('calcVat');
|
||||
const agentPriceVatIncluded = defineModel<boolean>('agentPriceVatIncluded');
|
||||
const agentPriceCalcVat = defineModel<boolean>('agentPriceCalcVat');
|
||||
const serviceChargeVatIncluded = defineModel<boolean>(
|
||||
'serviceChargeVatIncluded',
|
||||
);
|
||||
const serviceChargeCalcVat = defineModel<boolean>('serviceChargeCalcVat');
|
||||
|
||||
const price4Show = ref<string>(commaInput(price.value?.toString() || '0'));
|
||||
const agentPrice4Show = ref<string>(
|
||||
const formattedPrice = ref<string>(commaInput(price.value?.toString() || '0'));
|
||||
const formattedAgentPrice = ref<string>(
|
||||
commaInput(agentPrice.value?.toString() || '0'),
|
||||
);
|
||||
const serviceCharge4Show = ref<string>(
|
||||
const formattedServiceCharge = ref<string>(
|
||||
commaInput(serviceCharge.value?.toString() || '0'),
|
||||
);
|
||||
|
||||
const column = [
|
||||
type RowData = {
|
||||
pricePerUnit: number;
|
||||
calcVat: boolean;
|
||||
vatIncluded: boolean;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'label',
|
||||
align: 'center',
|
||||
align: 'left',
|
||||
label: 'productService.product.priceInformation',
|
||||
field: 'label',
|
||||
},
|
||||
{
|
||||
name: 'pricePerUnit',
|
||||
name: '#calcVat',
|
||||
align: 'center',
|
||||
label: 'general.calculateVat',
|
||||
field: '#calcVat',
|
||||
},
|
||||
{
|
||||
name: '#vatIncluded',
|
||||
align: 'center',
|
||||
label: 'productService.product.vatIncluded',
|
||||
field: '#vatIncluded',
|
||||
},
|
||||
{
|
||||
name: '#pricePerUnit',
|
||||
align: 'center',
|
||||
label: 'quotation.pricePerUnit',
|
||||
field: 'pricePerUnit',
|
||||
|
|
@ -35,54 +59,77 @@ const column = [
|
|||
name: 'beforeVat',
|
||||
align: 'right',
|
||||
label: 'quotation.priceBeforeVat',
|
||||
field: 'beforeVat',
|
||||
field: (data: RowData) =>
|
||||
formatNumberDecimal(
|
||||
calculatePrice({
|
||||
output: 'beforeVat',
|
||||
vatIncluded: data.vatIncluded,
|
||||
price: data.pricePerUnit || 0,
|
||||
}),
|
||||
2,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'vat',
|
||||
align: 'right',
|
||||
label: 'general.vat',
|
||||
field: 'vat',
|
||||
field: (data: RowData) =>
|
||||
formatNumberDecimal(
|
||||
calculatePrice({
|
||||
output: 'vat',
|
||||
calcVat: data.calcVat,
|
||||
price: Number(
|
||||
formatNumberDecimal(
|
||||
calculatePrice({
|
||||
output: 'beforeVat',
|
||||
vatIncluded: data.vatIncluded,
|
||||
price: data.pricePerUnit,
|
||||
}),
|
||||
2,
|
||||
).replaceAll(',', ''),
|
||||
),
|
||||
}),
|
||||
2,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'total',
|
||||
align: 'right',
|
||||
label: 'quotation.sumPrice',
|
||||
field: 'total',
|
||||
field: (data: RowData) =>
|
||||
formatNumberDecimal(
|
||||
calculatePrice({
|
||||
output: 'total',
|
||||
vat: 0.07,
|
||||
price: data.pricePerUnit,
|
||||
}) +
|
||||
(!data.vatIncluded
|
||||
? calculatePrice({
|
||||
output: 'vat',
|
||||
calcVat: data.calcVat,
|
||||
price: data.pricePerUnit,
|
||||
})
|
||||
: 0),
|
||||
2,
|
||||
),
|
||||
},
|
||||
] as const satisfies QTableProps['columns'];
|
||||
] as QTableProps['columns'];
|
||||
|
||||
const row = [
|
||||
{
|
||||
label: 'productService.product.salePrice',
|
||||
beforeVat: 0,
|
||||
vat: 0,
|
||||
total: 0,
|
||||
},
|
||||
{
|
||||
label: 'productService.product.agentPrice',
|
||||
beforeVat: 0,
|
||||
vat: 0,
|
||||
total: 0,
|
||||
},
|
||||
{
|
||||
label: 'productService.product.processingPrice',
|
||||
beforeVat: 0,
|
||||
vat: 0,
|
||||
total: 0,
|
||||
},
|
||||
] as const satisfies QTableProps['rows'];
|
||||
|
||||
watch(calcVat, () => {
|
||||
if (calcVat.value === false) vatIncluded.value = false;
|
||||
watch([calcVat, agentPriceCalcVat, serviceChargeCalcVat], () => {
|
||||
if (calcVat.value === false) {
|
||||
vatIncluded.value = false;
|
||||
}
|
||||
if (agentPriceCalcVat.value === false) {
|
||||
agentPriceVatIncluded.value = false;
|
||||
}
|
||||
if (serviceChargeCalcVat.value === false) {
|
||||
serviceChargeVatIncluded.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
dense?: boolean;
|
||||
outlined?: boolean;
|
||||
readonly?: boolean;
|
||||
separator?: boolean;
|
||||
isType?: boolean;
|
||||
priceDisplay?: {
|
||||
price: boolean;
|
||||
agentPrice: boolean;
|
||||
|
|
@ -115,255 +162,239 @@ withDefaults(
|
|||
{{ $t('productService.product.priceInformation') }}
|
||||
</span>
|
||||
</div>
|
||||
<section class="q-pr-md">
|
||||
<input
|
||||
id="input-calc-vat"
|
||||
type="checkbox"
|
||||
v-model="calcVat"
|
||||
:disabled="readonly"
|
||||
/>
|
||||
<label
|
||||
class="q-pl-sm"
|
||||
for="input-calc-vat"
|
||||
:style="{ opacity: readonly ? '.5' : undefined }"
|
||||
>
|
||||
{{ $t('general.calculateVat') }}
|
||||
</label>
|
||||
</section>
|
||||
<div
|
||||
class="surface-3 q-px-sm q-py-xs row text-caption app-text-muted"
|
||||
style="border-radius: var(--radius-3)"
|
||||
v-if="calcVat"
|
||||
>
|
||||
<span
|
||||
id="btn-include-vat"
|
||||
for="btn-include-vat"
|
||||
class="q-px-sm q-mr-lg rounded cursor-pointer"
|
||||
:class="{
|
||||
dark: $q.dark.isActive,
|
||||
'active-addr': vatIncluded,
|
||||
'cursor-not-allowed': readonly,
|
||||
}"
|
||||
@click="readonly ? '' : (vatIncluded = true)"
|
||||
>
|
||||
{{ $t('productService.product.vatIncluded') }}
|
||||
</span>
|
||||
<span
|
||||
id="btn-no-include-vat"
|
||||
for="btn-no-include-vat"
|
||||
class="q-px-sm rounded cursor-pointer"
|
||||
:class="{
|
||||
dark: $q.dark.isActive,
|
||||
'active-addr': !vatIncluded,
|
||||
'cursor-not-allowed': readonly,
|
||||
}"
|
||||
@click="readonly ? '' : (vatIncluded = false)"
|
||||
>
|
||||
{{ $t('productService.product.vatExcluded') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="col-12 full-width">
|
||||
<q-table
|
||||
:columns="column"
|
||||
:rows="row"
|
||||
:rows-per-page-options="[0]"
|
||||
:rows="[
|
||||
{
|
||||
label: $t('productService.product.salePrice'),
|
||||
pricePerUnit: price,
|
||||
calcVat,
|
||||
vatIncluded,
|
||||
},
|
||||
{
|
||||
label: $t('productService.product.agentPrice'),
|
||||
calcVat: agentPriceCalcVat,
|
||||
vatIncluded: agentPriceVatIncluded,
|
||||
pricePerUnit: agentPrice,
|
||||
},
|
||||
{
|
||||
label: $t('productService.product.processingPrice'),
|
||||
calcVat: serviceChargeCalcVat,
|
||||
vatIncluded: serviceChargeVatIncluded,
|
||||
pricePerUnit: serviceCharge,
|
||||
},
|
||||
]"
|
||||
:columns
|
||||
hide-bottom
|
||||
bordered
|
||||
flat
|
||||
hide-pagination
|
||||
selection="multiple"
|
||||
class="full-width"
|
||||
:no-data-label="$t('general.noDataTable')"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||
:props="props"
|
||||
>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label && $t(col.label) }}
|
||||
<q-th v-for="col in columns" :key="col.name" :props="props">
|
||||
<template v-if="!!col.label">
|
||||
{{ $t(col.label) }}
|
||||
</template>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr>
|
||||
<q-td>{{ $t(props.row.label) }}</q-td>
|
||||
<q-td class="text-right" style="width: 15%">
|
||||
<q-input
|
||||
v-if="priceDisplay?.price && props.rowIndex === 0"
|
||||
id="input-price"
|
||||
for="input-price"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
:borderless="readonly"
|
||||
hide-bottom-space
|
||||
input-class="text-right"
|
||||
:model-value="price4Show"
|
||||
@blur="
|
||||
() => {
|
||||
price = Number(price4Show.replace(/,/g, ''));
|
||||
if (price % 1 === 0) {
|
||||
const [, dec] = price4Show.split('.');
|
||||
if (!dec) {
|
||||
price4Show += '.00';
|
||||
<template
|
||||
v-slot:body="props: {
|
||||
row: RowData;
|
||||
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||
>
|
||||
<q-tr :class="{ dark: $q.dark.isActive }" class="text-center">
|
||||
<q-td v-for="(col, i) in columns" :align="col.align" :key="i">
|
||||
<!-- NOTE: custom column will starts with # -->
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
<span
|
||||
v-if="col.name === 'total'"
|
||||
class="text-weight-bold"
|
||||
:class="{
|
||||
['tags-color-orange']: props.rowIndex === 0,
|
||||
['tags-color-purple']: props.rowIndex === 1,
|
||||
['tags-color-pink']: props.rowIndex === 2,
|
||||
['dark']: $q.dark.isActive,
|
||||
}"
|
||||
>
|
||||
{{
|
||||
typeof col.field === 'string'
|
||||
? props.row[col.field as keyof RowData]
|
||||
: col.field(props.row)
|
||||
}}
|
||||
</span>
|
||||
<template v-else>
|
||||
{{
|
||||
typeof col.field === 'string'
|
||||
? props.row[col.field as keyof RowData]
|
||||
: col.field(props.row)
|
||||
}}
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="col.name === '#calcVat'">
|
||||
<q-checkbox
|
||||
v-if="priceDisplay?.price && props.rowIndex === 0"
|
||||
v-model="calcVat"
|
||||
size="xs"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-if="priceDisplay?.agentPrice && props.rowIndex === 1"
|
||||
v-model="agentPriceCalcVat"
|
||||
size="xs"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-if="priceDisplay?.serviceCharge && props.rowIndex === 2"
|
||||
v-model="serviceChargeCalcVat"
|
||||
size="xs"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="col.name === '#vatIncluded'">
|
||||
<q-select
|
||||
:options="[
|
||||
{ label: $t('general.included'), value: true },
|
||||
{ label: $t('general.notIncluded'), value: false },
|
||||
]"
|
||||
v-if="priceDisplay?.price && props.rowIndex === 0"
|
||||
map-options
|
||||
emit-value
|
||||
flat
|
||||
outlined
|
||||
dense
|
||||
v-model="vatIncluded"
|
||||
></q-select>
|
||||
<q-select
|
||||
:options="[
|
||||
{ label: $t('general.included'), value: true },
|
||||
{ label: $t('general.notIncluded'), value: false },
|
||||
]"
|
||||
v-if="priceDisplay?.agentPrice && props.rowIndex === 1"
|
||||
map-options
|
||||
emit-value
|
||||
flat
|
||||
outlined
|
||||
dense
|
||||
v-model="agentPriceVatIncluded"
|
||||
></q-select>
|
||||
<q-select
|
||||
:options="[
|
||||
{ label: $t('general.included'), value: true },
|
||||
{ label: $t('general.notIncluded'), value: false },
|
||||
]"
|
||||
v-if="priceDisplay?.serviceCharge && props.rowIndex === 2"
|
||||
map-options
|
||||
emit-value
|
||||
flat
|
||||
outlined
|
||||
dense
|
||||
v-model="serviceChargeVatIncluded"
|
||||
></q-select>
|
||||
</template>
|
||||
<template v-if="col.name === '#pricePerUnit'">
|
||||
<q-input
|
||||
v-if="priceDisplay?.price && props.rowIndex === 0"
|
||||
id="input-price"
|
||||
for="input-price"
|
||||
dense
|
||||
outlined
|
||||
hide-bottom-space
|
||||
input-class="text-right"
|
||||
:readonly
|
||||
:borderless="readonly"
|
||||
:model-value="formattedPrice"
|
||||
@blur="
|
||||
() => {
|
||||
price = Number(formattedPrice.replace(/,/g, ''));
|
||||
if (price % 1 === 0 && !formattedPrice.split('.').at(1)) {
|
||||
formattedPrice += '.00';
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
price4Show = commaInput(v?.toString() || '0', 'string');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
v-if="priceDisplay?.agentPrice && props.rowIndex === 1"
|
||||
id="input-agent-price"
|
||||
for="input-agent-price"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
:borderless="readonly"
|
||||
hide-bottom-space
|
||||
input-class="text-right"
|
||||
:model-value="agentPrice4Show"
|
||||
@blur="
|
||||
() => {
|
||||
agentPrice = Number(agentPrice4Show.replace(/,/g, ''));
|
||||
if (agentPrice % 1 === 0) {
|
||||
const [, dec] = agentPrice4Show.split('.');
|
||||
if (!dec) {
|
||||
agentPrice4Show += '.00';
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
formattedPrice = commaInput(
|
||||
v?.toString() || '0',
|
||||
'string',
|
||||
);
|
||||
}
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
v-if="priceDisplay?.agentPrice && props.rowIndex === 1"
|
||||
id="input-agent-price"
|
||||
for="input-agent-price"
|
||||
dense
|
||||
outlined
|
||||
hide-bottom-space
|
||||
input-class="text-right"
|
||||
:readonly
|
||||
:borderless="readonly"
|
||||
:model-value="formattedAgentPrice"
|
||||
@blur="
|
||||
() => {
|
||||
agentPrice = Number(
|
||||
formattedAgentPrice.replace(/,/g, ''),
|
||||
);
|
||||
if (
|
||||
agentPrice % 1 === 0 &&
|
||||
!formattedAgentPrice.split('.').at(1)
|
||||
) {
|
||||
formattedAgentPrice += '.00';
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
agentPrice4Show = commaInput(
|
||||
v?.toString() || '0',
|
||||
'string',
|
||||
);
|
||||
}
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
v-if="priceDisplay?.serviceCharge && props.rowIndex === 2"
|
||||
id="input-service-charge"
|
||||
for="input-service-charge"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
:borderless="readonly"
|
||||
input-class="text-right"
|
||||
hide-bottom-space
|
||||
:model-value="serviceCharge4Show"
|
||||
@blur="
|
||||
() => {
|
||||
serviceCharge = Number(
|
||||
serviceCharge4Show.replace(/,/g, ''),
|
||||
);
|
||||
if (serviceCharge % 1 === 0) {
|
||||
const [, dec] = serviceCharge4Show.split('.');
|
||||
if (!dec) {
|
||||
serviceCharge4Show += '.00';
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
formattedAgentPrice = commaInput(
|
||||
v?.toString() || '0',
|
||||
'string',
|
||||
);
|
||||
}
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
v-if="priceDisplay?.serviceCharge && props.rowIndex === 2"
|
||||
id="input-service-charge"
|
||||
for="input-service-charge"
|
||||
dense
|
||||
outlined
|
||||
input-class="text-right"
|
||||
hide-bottom-space
|
||||
:readonly
|
||||
:borderless="readonly"
|
||||
:model-value="formattedServiceCharge"
|
||||
@blur="
|
||||
() => {
|
||||
serviceCharge = Number(
|
||||
formattedServiceCharge.replace(/,/g, ''),
|
||||
);
|
||||
if (
|
||||
serviceCharge % 1 === 0 &&
|
||||
!formattedServiceCharge.split('.').at(1)
|
||||
) {
|
||||
formattedServiceCharge += '.00';
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
serviceCharge4Show = commaInput(
|
||||
v?.toString() || '0',
|
||||
'string',
|
||||
);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</q-td>
|
||||
<q-td class="text-right" style="width: 15%">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
calculatePrice({
|
||||
output: 'beforeVat',
|
||||
vatIncluded: vatIncluded,
|
||||
price:
|
||||
(props.rowIndex === 0
|
||||
? price
|
||||
: props.rowIndex === 1
|
||||
? agentPrice
|
||||
: serviceCharge) || 0,
|
||||
}),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
</q-td>
|
||||
<q-td class="text-right" style="width: 15%">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
calculatePrice({
|
||||
output: 'vat',
|
||||
calcVat: calcVat,
|
||||
price: Number(
|
||||
formatNumberDecimal(
|
||||
calculatePrice({
|
||||
output: 'beforeVat',
|
||||
vatIncluded: vatIncluded,
|
||||
price:
|
||||
(props.rowIndex === 0
|
||||
? price
|
||||
: props.rowIndex === 1
|
||||
? agentPrice
|
||||
: serviceCharge) || 0,
|
||||
}),
|
||||
2,
|
||||
).replaceAll(',', ''),
|
||||
),
|
||||
}),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
</q-td>
|
||||
<q-td class="text-right" style="width: 15%">
|
||||
<span
|
||||
class="text-weight-bold"
|
||||
:class="{
|
||||
'tags-color-orange': props.rowIndex === 0,
|
||||
'tags-color-purple': props.rowIndex === 1,
|
||||
'tags-color-pink': props.rowIndex === 2,
|
||||
dark: $q.dark.isActive,
|
||||
}"
|
||||
>
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
calculatePrice({
|
||||
output: 'total',
|
||||
vat: 0.03,
|
||||
price:
|
||||
(props.rowIndex === 0
|
||||
? price
|
||||
: props.rowIndex === 1
|
||||
? agentPrice
|
||||
: serviceCharge) || 0,
|
||||
}) +
|
||||
(!vatIncluded
|
||||
? calculatePrice({
|
||||
output: 'vat',
|
||||
calcVat: calcVat,
|
||||
price:
|
||||
(props.rowIndex === 0
|
||||
? price
|
||||
: props.rowIndex === 1
|
||||
? agentPrice
|
||||
: serviceCharge) || 0,
|
||||
})
|
||||
: 0),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
formattedServiceCharge = commaInput(
|
||||
v?.toString() || '0',
|
||||
'string',
|
||||
);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
|
@ -373,16 +404,6 @@ withDefaults(
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
.active-addr {
|
||||
color: hsl(var(--info-bg));
|
||||
background-color: hsla(var(--info-bg) / 0.1);
|
||||
border-radius: var(--radius-3);
|
||||
|
||||
&.dark {
|
||||
background-color: var(--surface-1);
|
||||
}
|
||||
}
|
||||
|
||||
.tags-color-orange {
|
||||
color: var(--orange-5);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,20 +241,22 @@ watch(
|
|||
</span>
|
||||
</template>
|
||||
</SelectInput> -->
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-delete-work"
|
||||
icon="mdi-trash-can-outline"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
padding="0"
|
||||
class="q-ml-md"
|
||||
color="negative"
|
||||
@click.stop="$emit('deleteWork')"
|
||||
>
|
||||
<q-tooltip>{{ $t('general.delete') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<div :class="{ 'col-12 row justify-end q-mt-sm': $q.screen.lt.md }">
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-delete-work"
|
||||
icon="mdi-trash-can-outline"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
padding="0"
|
||||
class="q-ml-md"
|
||||
color="negative"
|
||||
@click.stop="$emit('deleteWork')"
|
||||
>
|
||||
<q-tooltip>{{ $t('general.delete') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -270,29 +272,29 @@ watch(
|
|||
<!-- product -->
|
||||
<div class="bordered-t">
|
||||
<div
|
||||
class="q-py-xs text-weight-medium row justify-between items-center q-px-md"
|
||||
class="q-py-xs text-weight-medium justify-between items-center q-px-md"
|
||||
:class="{ row: $q.screen.gt.xs }"
|
||||
style="background-color: hsla(var(--info-bg) / 0.1)"
|
||||
>
|
||||
<span>
|
||||
{{ $t('productService.service.productInWork') }}
|
||||
{{ workIndex + 1 }}
|
||||
{{ $t('productService.service.productInWork') }}
|
||||
{{ workIndex + 1 }}
|
||||
|
||||
<span class="row items-center justify-between">
|
||||
<q-checkbox
|
||||
size="xs"
|
||||
:class="$q.screen.gt.xs ? 'q-pl-lg' : 'q-pl-sm'"
|
||||
v-model="attributes.showTotalPrice"
|
||||
:label="$t('productService.service.showTotalPrice')"
|
||||
style="color: hsl(var(--text-mute-2))"
|
||||
:disable="readonly"
|
||||
/>
|
||||
<AddButton
|
||||
v-if="!readonly"
|
||||
icon-only
|
||||
id="btn-add-work-product"
|
||||
for="btn-add-work-product"
|
||||
@click.stop="$emit('addProduct')"
|
||||
/>
|
||||
</span>
|
||||
<AddButton
|
||||
v-if="!readonly"
|
||||
icon-only
|
||||
id="btn-add-work-product"
|
||||
for="btn-add-work-product"
|
||||
@click.stop="$emit('addProduct')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -302,19 +304,19 @@ watch(
|
|||
<section
|
||||
v-for="(product, index) in productItems"
|
||||
:key="product.id"
|
||||
class="full-width row items-center justify-between"
|
||||
class="full-width items-center justify-between row"
|
||||
>
|
||||
<div
|
||||
class="row col items-center justify-between full-width surface-1 q-px-sm q-py-xs"
|
||||
class="row col-md col-12 items-center justify-between full-width surface-1 q-px-sm q-py-xs rounded"
|
||||
style="min-height: 70px"
|
||||
>
|
||||
<!-- product detail -->
|
||||
<section
|
||||
class="row items-center col-md col-12 no-wrap"
|
||||
class="row items-center col-md col-12"
|
||||
v-if="productItems"
|
||||
>
|
||||
<q-btn
|
||||
v-if="!readonly && $q.screen.gt.xs"
|
||||
v-if="!readonly"
|
||||
id="btn-product-up"
|
||||
icon="mdi-arrow-up"
|
||||
dense
|
||||
|
|
@ -326,7 +328,7 @@ watch(
|
|||
@click.stop="$emit('moveProductUp', productItems, index)"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="!readonly && $q.screen.gt.xs"
|
||||
v-if="!readonly"
|
||||
id="btn-product-down"
|
||||
for="btn-product-down"
|
||||
icon="mdi-arrow-down"
|
||||
|
|
@ -347,25 +349,32 @@ watch(
|
|||
{{ index + 1 }}
|
||||
</q-avatar>
|
||||
|
||||
<div class="col row no-wrap items-center">
|
||||
<div
|
||||
class="col-md col-12 row items-center"
|
||||
:class="{ 'q-pt-sm': $q.screen.lt.md }"
|
||||
>
|
||||
<div
|
||||
v-if="$q.screen.gt.xs"
|
||||
class="bordered q-mx-md col-3 image-box"
|
||||
:class="{ 'col-12 flex justify-center': $q.screen.lt.md }"
|
||||
>
|
||||
<q-img
|
||||
:src="`${baseUrl}/product/${product.id}/image/${product.selectedImage}`"
|
||||
style="object-fit: cover; width: 100%; height: 100%"
|
||||
>
|
||||
<template #error>
|
||||
<div
|
||||
class="surface-3 full-width full-height no-padding"
|
||||
>
|
||||
<q-img src="blank-image.png"></q-img>
|
||||
</div>
|
||||
</template>
|
||||
</q-img>
|
||||
<div class="bordered q-mx-md col-md-3 image-box">
|
||||
<q-img
|
||||
:src="`${baseUrl}/product/${product.id}/image/${product.selectedImage}`"
|
||||
style="object-fit: cover; width: 100%; height: 100%"
|
||||
>
|
||||
<template #error>
|
||||
<div
|
||||
class="surface-3 full-width full-height no-padding"
|
||||
>
|
||||
<q-img src="blank-image.png"></q-img>
|
||||
</div>
|
||||
</template>
|
||||
</q-img>
|
||||
</div>
|
||||
</div>
|
||||
<article class="column col full-width justify-between">
|
||||
<article
|
||||
class="column col-md col-12 full-width justify-between"
|
||||
:class="{ 'q-py-sm': $q.screen.lt.md }"
|
||||
>
|
||||
<span
|
||||
class="text-weight-medium ellipsis-2-lines full-width"
|
||||
>
|
||||
|
|
@ -391,7 +400,8 @@ watch(
|
|||
<span class="col-12 row" style="color: var(--teal-9)">
|
||||
<span
|
||||
v-if="priceDisplay?.price"
|
||||
class="col ellipsis price-orange text-weight-bold"
|
||||
class="col-md col-12 ellipsis price-orange text-weight-bold"
|
||||
:class="{ 'row justify-between': $q.screen.lt.md }"
|
||||
>
|
||||
<div class="text-caption app-text-muted-2">
|
||||
{{ $t('productService.product.salePrice') }}
|
||||
|
|
@ -407,7 +417,8 @@ watch(
|
|||
</span>
|
||||
<span
|
||||
v-if="priceDisplay?.agentPrice"
|
||||
class="col ellipsis price-purple text-weight-bold"
|
||||
class="col-md col-12 ellipsis price-purple text-weight-bold"
|
||||
:class="{ 'row justify-between': $q.screen.lt.md }"
|
||||
>
|
||||
<div class="text-caption app-text-muted-2">
|
||||
{{ $t('productService.product.agentPrice') }}
|
||||
|
|
@ -423,7 +434,8 @@ watch(
|
|||
</span>
|
||||
<span
|
||||
v-if="priceDisplay?.serviceCharge"
|
||||
class="col ellipsis price-pink column"
|
||||
class="col-md col-12 ellipsis price-pink"
|
||||
:class="{ 'row justify-between': $q.screen.lt.md }"
|
||||
>
|
||||
<div class="text-caption app-text-muted-2">
|
||||
{{ $t('productService.product.processingPrice') }}
|
||||
|
|
@ -437,14 +449,16 @@ watch(
|
|||
฿{{ formatNumberDecimal(product.serviceCharge, 2) }}
|
||||
</q-tooltip>
|
||||
</span>
|
||||
<span class="col ellipsis column text-weight-medium">
|
||||
<span
|
||||
class="col-md col-12 ellipsis text-weight-medium"
|
||||
:class="{ 'row justify-between': $q.screen.lt.md }"
|
||||
>
|
||||
<div class="text-caption app-text-muted-2">
|
||||
{{ $t('productService.service.InstallmentsNo') }}
|
||||
</div>
|
||||
{{ !readonly ? '' : product.installmentNo }}
|
||||
<span class="row justify-end">
|
||||
<span v-if="!readonly" class="row justify-end">
|
||||
<q-input
|
||||
v-if="!readonly && $q.screen.gt.xs"
|
||||
outlined
|
||||
:max="installments"
|
||||
input-class="text-right no-padding"
|
||||
|
|
@ -462,21 +476,25 @@ watch(
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
class="q-ml-md"
|
||||
id="btn-delete-work-product"
|
||||
for="btn-delete-work-product"
|
||||
icon="mdi-trash-can-outline"
|
||||
padding="0"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
color="negative"
|
||||
@click.stop="$emit('deleteProduct', productItems, index)"
|
||||
<div
|
||||
:class="{ 'col-12 row justify-end q-mt-sm': $q.screen.lt.md }"
|
||||
>
|
||||
<q-tooltip>{{ $t('general.delete') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
class="q-ml-md"
|
||||
id="btn-delete-work-product"
|
||||
for="btn-delete-work-product"
|
||||
icon="mdi-trash-can-outline"
|
||||
padding="0"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
color="negative"
|
||||
@click.stop="$emit('deleteProduct', productItems, index)"
|
||||
>
|
||||
<q-tooltip>{{ $t('general.delete') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div v-else class="app-text-muted q-py-md q-px-md">
|
||||
|
|
@ -594,7 +612,11 @@ watch(
|
|||
v-if="productItems.length > 0"
|
||||
class="surface-1 rounded q-pa-xs"
|
||||
>
|
||||
<div v-for="product in productItems" :key="product.id">
|
||||
<div
|
||||
v-for="product in productItems"
|
||||
:key="product.id"
|
||||
class="ellipsis-2-lines"
|
||||
>
|
||||
<q-checkbox
|
||||
v-if="attributes.workflowStep[stepIndex].productsId"
|
||||
:disable="readonly"
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ defineEmits<{
|
|||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`general.about`) }}
|
||||
<div class="q-ml-md text-weight-regular">
|
||||
<div class="text-weight-regular" :class="{ 'q-ml-md ': $q.screen.gt.sm }">
|
||||
<q-checkbox
|
||||
:label="$t('productService.product.agentPrice')"
|
||||
size="xs"
|
||||
|
|
@ -54,11 +54,15 @@ defineEmits<{
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 row q-col-gutter-sm">
|
||||
<div class="col-12 row" :class="{ 'q-col-gutter-sm': !inputOnly }">
|
||||
<SelectBranch
|
||||
v-model:value="branchId"
|
||||
:label="$t('quotation.branchVirtual')"
|
||||
class="col-md-6 col-12"
|
||||
:class="{
|
||||
'field-one': inputOnly && $q.screen.gt.sm,
|
||||
'q-mb-sm': inputOnly && $q.screen.lt.md,
|
||||
}"
|
||||
simple
|
||||
required
|
||||
:readonly
|
||||
|
|
@ -71,6 +75,7 @@ defineEmits<{
|
|||
})})`"
|
||||
@create="$emit('addCustomer')"
|
||||
class="col-md-6 col-12"
|
||||
:class="{ 'field-two': inputOnly && $q.screen.gt.sm }"
|
||||
:creatable-disabled="!branchId"
|
||||
:creatable="!inputOnly"
|
||||
simple
|
||||
|
|
@ -80,3 +85,17 @@ defineEmits<{
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(
|
||||
label.q-field.row.no-wrap.items-start.q-field--outlined.q-select.field-one
|
||||
) {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
:deep(
|
||||
label.q-field.row.no-wrap.items-start.q-field--outlined.q-select.field-two
|
||||
) {
|
||||
padding-left: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ function calcPrice(c: (typeof rows.value)[number]) {
|
|||
const finalPriceNoVat = finalPriceWithVat / (1 + (config.value?.vat || 0.07));
|
||||
|
||||
const price = finalPriceNoVat * c.amount - c.discount;
|
||||
const vat = c.product.calcVat
|
||||
const vat = c.product[props.agentPrice ? 'agentPriceCalcVat' : 'calcVat']
|
||||
? (finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07)
|
||||
: 0;
|
||||
return precisionRound(price + vat);
|
||||
|
|
@ -234,7 +234,6 @@ watch(
|
|||
const incoming = current.filter(
|
||||
(lhs) =>
|
||||
!before.find((rhs) => {
|
||||
console.log(lhs, rhs);
|
||||
return JSON.stringify(lhs) === JSON.stringify(rhs);
|
||||
}),
|
||||
);
|
||||
|
|
@ -387,7 +386,9 @@ watch(
|
|||
{{
|
||||
formatNumberDecimal(
|
||||
props.row.pricePerUnit +
|
||||
(props.row.product.calcVat
|
||||
(props.row.product[
|
||||
agentPrice ? 'agentPriceCalcVat' : 'calcVat'
|
||||
]
|
||||
? props.row.pricePerUnit * (config?.vat || 0.07)
|
||||
: 0),
|
||||
2,
|
||||
|
|
@ -443,7 +444,9 @@ watch(
|
|||
<q-td align="right">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
props.row.product.calcVat
|
||||
props.row.product[
|
||||
agentPrice ? 'agentPriceCalcVat' : 'calcVat'
|
||||
]
|
||||
? precisionRound(
|
||||
(props.row.pricePerUnit * props.row.amount -
|
||||
props.row.discount) *
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { formatNumberDecimal } from 'src/stores/utils';
|
||||
import BadgeComponent from 'components/BadgeComponent.vue';
|
||||
import KebabAction from '../shared/KebabAction.vue';
|
||||
import MainButton from '../button/MainButton.vue';
|
||||
|
||||
defineProps<{
|
||||
title?: string;
|
||||
|
|
@ -48,7 +46,7 @@ const rand = Math.random();
|
|||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="surface-1 rounded q-pa-sm quo-card bordered"
|
||||
class="surface-1 rounded q-pa-sm quo-card bordered column"
|
||||
:class="{ 'urgent-card': urgent }"
|
||||
:style="{ '--animation-delay': rand + 's' }"
|
||||
>
|
||||
|
|
@ -71,7 +69,7 @@ const rand = Math.random();
|
|||
/>
|
||||
</div>
|
||||
|
||||
<nav class="col text-right">
|
||||
<nav class="col text-right no-wrap">
|
||||
<q-btn
|
||||
v-if="!hidePreview"
|
||||
flat
|
||||
|
|
@ -125,7 +123,7 @@ const rand = Math.random();
|
|||
|
||||
<!-- SEC: body -->
|
||||
<section
|
||||
class="rounded q-px-sm"
|
||||
class="rounded q-px-sm col"
|
||||
:class="{
|
||||
'surface-1': urgent,
|
||||
'surface-2': !urgent,
|
||||
|
|
|
|||
|
|
@ -16,12 +16,16 @@ const props = withDefaults(
|
|||
grid?: boolean;
|
||||
visibleColumns?: string[];
|
||||
hideEdit?: boolean;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}>(),
|
||||
{
|
||||
row: () => [],
|
||||
column: () => [],
|
||||
grid: false,
|
||||
visibleColumns: () => [],
|
||||
page: 1,
|
||||
pageSize: 30,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -64,7 +68,11 @@ defineEmits<{
|
|||
<template v-slot:body="props">
|
||||
<q-tr :class="{ urgent: props.row.urgent }">
|
||||
<q-td v-if="visibleColumns.includes('order')">
|
||||
{{ props.rowIndex + 1 }}
|
||||
{{
|
||||
$q.screen.xs
|
||||
? props.rowIndex + 1
|
||||
: (page - 1) * pageSize + props.rowIndex + 1
|
||||
}}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="visibleColumns.includes('workName')">
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ type Options = { label: string; value: string };
|
|||
|
||||
<div class="col-12 row q-col-gutter-sm">
|
||||
<SelectInput
|
||||
:class="{ col: $q.screen.lt.md }"
|
||||
:disable="!readonly && onDrawer"
|
||||
:readonly="readonly"
|
||||
for="input-agencies-code"
|
||||
|
|
@ -60,7 +61,7 @@ type Options = { label: string; value: string };
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
class="col-md col-12"
|
||||
:label="$t('agencies.name')"
|
||||
v-model="name"
|
||||
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||
|
|
@ -71,7 +72,7 @@ type Options = { label: string; value: string };
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
class="col-md col-12"
|
||||
:label="'Agencies Name'"
|
||||
v-model="nameEn"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ const quotationId = defineModel<string>('quotationId', {
|
|||
class="col"
|
||||
v-model:value="quotationId"
|
||||
:label="$t('general.select', { msg: $t('quotation.title') })"
|
||||
:params="{
|
||||
hasCancel: true,
|
||||
}"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
42
src/components/12_debit-note/FormDebit.vue
Normal file
42
src/components/12_debit-note/FormDebit.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<script lang="ts" setup>
|
||||
import SelectQuotation from 'components/shared/select/SelectQuotation.vue';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
|
||||
const quotationId = defineModel<string>('quotationId', {
|
||||
required: true,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
<section
|
||||
:id="`form-credit`"
|
||||
class="col-12 q-pb-sm text-weight-bold text-body1 row items-center"
|
||||
>
|
||||
<q-icon
|
||||
flat
|
||||
size="xs"
|
||||
class="q-pa-sm rounded q-mr-xs"
|
||||
color="info"
|
||||
name="mdi-file-outline"
|
||||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`general.document`) }}
|
||||
</section>
|
||||
|
||||
<section class="col-12 row q-col-gutter-sm">
|
||||
<SelectQuotation
|
||||
for="select-quotation"
|
||||
class="col"
|
||||
v-model:value="quotationId"
|
||||
:label="$t('general.select', { msg: $t('quotation.title') })"
|
||||
:params="{
|
||||
forDebitNote: true,
|
||||
}"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
|
|
@ -23,6 +23,7 @@ defineProps<{
|
|||
hideDelete?: boolean;
|
||||
hideBtn?: boolean;
|
||||
readonly?: boolean;
|
||||
disabledSubmit?: boolean;
|
||||
|
||||
saveAmount?: number;
|
||||
submitLabel?: string;
|
||||
|
|
@ -202,7 +203,7 @@ const currentTab = defineModel<string>('currentTab');
|
|||
</div>
|
||||
|
||||
<!-- center -->
|
||||
<div class="col column full-height">
|
||||
<div class="col column full-height no-wrap">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
|
|
@ -235,6 +236,7 @@ const currentTab = defineModel<string>('currentTab');
|
|||
id="btn-form-submit"
|
||||
type="submit"
|
||||
solid
|
||||
:disabled="disabledSubmit"
|
||||
:icon="submitIcon"
|
||||
:label="submitLabel"
|
||||
:amount="saveAmount"
|
||||
|
|
|
|||
|
|
@ -193,14 +193,21 @@ watch(
|
|||
</div>
|
||||
|
||||
<!-- body -->
|
||||
<div class="q-px-lg surface-2 col row full-width">
|
||||
<div
|
||||
class="image-dialog-body q-my-lg relative-position rounded surface-1 flex items-center"
|
||||
<section
|
||||
class="surface-2 col row full-width no-wrap q-pa-md items-center"
|
||||
:class="{
|
||||
row: $q.screen.gt.sm,
|
||||
column: $q.screen.lt.md,
|
||||
}"
|
||||
>
|
||||
<aside
|
||||
class="image-dialog-body relative-position rounded surface-1 flex items-center"
|
||||
:class="{ 'q-mr-md': $q.screen.gt.sm, 'q-mb-md': $q.screen.lt.md }"
|
||||
>
|
||||
<img
|
||||
:src="tempImage || fallbackUrl"
|
||||
v-if="tempImage || fallbackUrl"
|
||||
style="object-fit: contain; width: 100%"
|
||||
style="object-fit: contain; height: 100%; width: 100%"
|
||||
@error="
|
||||
() => {
|
||||
tempImage = '';
|
||||
|
|
@ -237,12 +244,12 @@ watch(
|
|||
style="color: hsla(var(--stone-0-hsl) / 0.7)"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div
|
||||
<aside
|
||||
v-if="!hiddenFooter"
|
||||
class="col q-ml-md q-pt-sm q-my-md"
|
||||
style="width: 40em; height: 30em; overflow: auto"
|
||||
class="self-start col scroll"
|
||||
style="height: 30em; width: 34em"
|
||||
>
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<div
|
||||
|
|
@ -254,6 +261,8 @@ watch(
|
|||
object-fit: cover;
|
||||
height: 5vw;
|
||||
width: 5vw;
|
||||
min-width: 70px;
|
||||
min-height: 70px;
|
||||
overflow: hidden;
|
||||
"
|
||||
@click="selectImg('')"
|
||||
|
|
@ -274,7 +283,12 @@ watch(
|
|||
? selectedImg === img
|
||||
: selectedImg === img.split('/').pop(),
|
||||
}"
|
||||
style="height: 5vw; width: 5vw"
|
||||
style="
|
||||
height: 5vw;
|
||||
width: 5vw;
|
||||
min-width: 70px;
|
||||
min-height: 70px;
|
||||
"
|
||||
@click="selectImg(img)"
|
||||
>
|
||||
<q-btn
|
||||
|
|
@ -339,8 +353,8 @@ watch(
|
|||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<!-- footer -->
|
||||
<div
|
||||
|
|
@ -410,6 +424,10 @@ watch(
|
|||
overflow: hidden;
|
||||
width: 30em;
|
||||
height: 30em;
|
||||
/* width: calc(400px - 5vw); */
|
||||
/* height: calc(400px - 5vw); */
|
||||
/* max-width: 400px;
|
||||
max-height: 400px; */
|
||||
}
|
||||
|
||||
.upload-image-btn {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,9 @@ onMounted(() => {
|
|||
:class="{ dark: $q.dark.isActive }"
|
||||
:style="`background-color: ${bgColor}`"
|
||||
>
|
||||
<div style="font-size: 14px">{{ $t(text) }}</div>
|
||||
<div style="font-size: 14px; color: var(--foreground)">
|
||||
{{ $t(text) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -154,15 +154,15 @@ const smallBanner = ref(false);
|
|||
class="absolute-bottom-right"
|
||||
style="
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 1.25vw;
|
||||
height: 1.25vw;
|
||||
z-index: 2;
|
||||
background: var(--surface-1);
|
||||
"
|
||||
>
|
||||
<q-badge
|
||||
class="absolute-center"
|
||||
style="border-radius: 50%; width: 14px; height: 14px"
|
||||
style="border-radius: 50%; width: 0.8vw; height: 0.8vw"
|
||||
:style="`background: hsl(var(${active ? '--positive-bg' : '--text-mute'}))`"
|
||||
></q-badge>
|
||||
</q-badge>
|
||||
|
|
@ -193,7 +193,7 @@ const smallBanner = ref(false);
|
|||
>
|
||||
<div
|
||||
class="row justify-between full-height"
|
||||
style="padding-left: 7.5vw"
|
||||
style="padding-left: calc(6vw + 50px)"
|
||||
>
|
||||
<div class="col column">
|
||||
<span
|
||||
|
|
@ -261,7 +261,7 @@ const smallBanner = ref(false);
|
|||
inline-label
|
||||
mobile-arrows
|
||||
v-model="currentTab"
|
||||
active-class="active-tab text-weight-bold"
|
||||
:active-class="`active-tab text-weight-bold ${$q.dark.isActive && 'dark'}`"
|
||||
class="app-text-muted full-width"
|
||||
align="left"
|
||||
v-if="typeof tabsList === 'object'"
|
||||
|
|
@ -271,6 +271,7 @@ const smallBanner = ref(false);
|
|||
:id="`${prefix}-tab-${tab.label}`"
|
||||
v-bind:key="tab.name"
|
||||
class="content-tab text-capitalize"
|
||||
:class="{ 'tab-label': currentTab !== tab.name }"
|
||||
:name="tab.name"
|
||||
:label="tab.label"
|
||||
/>
|
||||
|
|
@ -302,7 +303,11 @@ const smallBanner = ref(false);
|
|||
>
|
||||
<!-- profile -->
|
||||
<span class="row col items-center">
|
||||
<div class="flex items-center full-height q-pl-lg" style="z-index: 1">
|
||||
<div
|
||||
class="flex items-center full-height"
|
||||
:class="{ 'q-pl-lg': $q.screen.gt.sm, 'q-pl-sm': $q.screen.lt.md }"
|
||||
style="z-index: 1"
|
||||
>
|
||||
<div
|
||||
class="surface-1"
|
||||
style="border-radius: 50%; border: 2px solid var(--surface-1)"
|
||||
|
|
@ -338,7 +343,7 @@ const smallBanner = ref(false);
|
|||
>
|
||||
<template #error>
|
||||
<div
|
||||
class="full-width full-height flex items-center justify-center"
|
||||
class="full-width full-height flex items-center justify-center no-padding"
|
||||
:style="{
|
||||
background: `${bgColor || 'var(--brand-1)'}`,
|
||||
color: `${color || 'white'}`,
|
||||
|
|
@ -347,13 +352,14 @@ const smallBanner = ref(false);
|
|||
<Icon
|
||||
class="full-width full-height flex items-center justify-center"
|
||||
:icon="icon || 'mdi-account'"
|
||||
style="width: 25px !important"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</q-img>
|
||||
<div
|
||||
v-else
|
||||
class="full-width full-height flex items-center justify-center"
|
||||
class="full-width full-height flex items-center justify-center no-padding"
|
||||
:style="{
|
||||
background: `${bgColor || 'var(--brand-1)'}`,
|
||||
color: `${color || 'white'}`,
|
||||
|
|
@ -362,6 +368,7 @@ const smallBanner = ref(false);
|
|||
<Icon
|
||||
class="full-width full-height flex items-center justify-center"
|
||||
:icon="icon || 'mdi-account'"
|
||||
style="width: 25px !important"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -378,6 +385,7 @@ const smallBanner = ref(false);
|
|||
<Icon
|
||||
class="full-width full-height flex items-center justify-center"
|
||||
:icon="icon || 'mdi-account'"
|
||||
style="width: 25px !important"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -425,7 +433,7 @@ const smallBanner = ref(false);
|
|||
inline-label
|
||||
mobile-arrows
|
||||
v-model="currentTab"
|
||||
active-class="active-tab text-weight-bold"
|
||||
:active-class="`active-tab text-weight-bold ${$q.dark.isActive && 'dark'}`"
|
||||
class="app-text-muted full-width"
|
||||
align="left"
|
||||
v-if="typeof tabsList === 'object'"
|
||||
|
|
@ -435,6 +443,7 @@ const smallBanner = ref(false);
|
|||
:id="`${prefix}-tab-${tab.label}`"
|
||||
v-bind:key="tab.name"
|
||||
class="content-tab text-capitalize"
|
||||
:class="{ 'tab-label': currentTab !== tab.name }"
|
||||
:name="tab.name"
|
||||
:label="tab.label"
|
||||
/>
|
||||
|
|
@ -526,5 +535,13 @@ const smallBanner = ref(false);
|
|||
|
||||
.active-tab {
|
||||
color: var(--brand-1);
|
||||
&.dark {
|
||||
filter: brightness(1.3);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
color: var(--foreground);
|
||||
opacity: 75%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -57,13 +57,13 @@ async function downloadImage(url: string | null) {
|
|||
<main class="column full-height">
|
||||
<section
|
||||
v-if="!hideTab"
|
||||
style="background: var(--gray-3)"
|
||||
:style="`background: var(${$q.dark.isActive ? '--gray-8' : '--gray-3'})`"
|
||||
class="q-py-sm row justify-center"
|
||||
>
|
||||
<div class="surface-2 q-px-md q-py-sm rounded row no-wrap items-center">
|
||||
<MainButton
|
||||
icon="mdi-minus"
|
||||
color="0 0% 0%"
|
||||
:color="`var(--gray-${$q.dark.isActive ? '1' : '11'}-hsl)`"
|
||||
icon-only
|
||||
@click="
|
||||
() => {
|
||||
|
|
@ -90,7 +90,7 @@ async function downloadImage(url: string | null) {
|
|||
></q-input>
|
||||
<MainButton
|
||||
icon="mdi-plus"
|
||||
color="0 0% 0%"
|
||||
:color="`var(--gray-${$q.dark.isActive ? '1' : '11'}-hsl)`"
|
||||
icon-only
|
||||
@click="
|
||||
() => {
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ watchEffect(async () => {
|
|||
<q-input
|
||||
outlined
|
||||
hide-bottom-space
|
||||
class="col-3"
|
||||
class="col-md-3 col-12"
|
||||
v-model="homeCode"
|
||||
mask="###########"
|
||||
:dense="dense"
|
||||
|
|
@ -344,7 +344,7 @@ watchEffect(async () => {
|
|||
<q-input
|
||||
outlined
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
class="col-md col-12"
|
||||
:model-value="office"
|
||||
:dense="dense"
|
||||
:label="$t('customer.form.employmentOffice')"
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ const props = withDefaults(
|
|||
id?: string;
|
||||
label?: string;
|
||||
option: T[];
|
||||
optionLabel?: keyof T;
|
||||
optionValue?: keyof T;
|
||||
optionLabel?: keyof T | string;
|
||||
optionValue?: keyof T | string;
|
||||
placeholder?: string;
|
||||
|
||||
hideSelected?: boolean;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import TableProductAndService from './table/TableProductAndService.vue';
|
||||
import ToggleView from './ToggleView.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const viewMode = ref<boolean>(false);
|
||||
|
||||
const search = defineModel<string>('search');
|
||||
const selectedItem = defineModel<unknown[]>('selectedItem', { default: [] });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
items: any;
|
||||
newItems?: any;
|
||||
color?: string;
|
||||
|
|
@ -15,6 +19,7 @@ const props = withDefaults(
|
|||
noPadding?: boolean;
|
||||
noItemsIcon?: string;
|
||||
noItemsLabel?: string;
|
||||
type: 'service' | 'product';
|
||||
}>(),
|
||||
{
|
||||
items: () => [],
|
||||
|
|
@ -35,24 +40,24 @@ watch(search, () => {
|
|||
emit('search', search.value || '');
|
||||
});
|
||||
|
||||
function select(item?: unknown, all?: boolean) {
|
||||
function select(item?: any, all?: boolean) {
|
||||
if (all) {
|
||||
if (props.items.every((item) => selectedItem.value.includes(item))) {
|
||||
selectedItem.value = selectedItem.value.filter(
|
||||
(item) => !props.items.includes(item),
|
||||
);
|
||||
if (selectedItem.value.length !== 0) {
|
||||
selectedItem.value = [];
|
||||
} else {
|
||||
props.items.forEach((i) => {
|
||||
const productExists = selectedItem.value.some((item) => item === i);
|
||||
props.items.forEach((i: any) => {
|
||||
const productExists = selectedItem.value.find(
|
||||
(item: any) => item.id === i.id,
|
||||
);
|
||||
if (!productExists) {
|
||||
selectedItem.value.push(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (selectedItem.value.includes(item)) {
|
||||
const index = selectedItem.value.indexOf(item);
|
||||
selectedItem.value.splice(index, 1);
|
||||
const findIdex = selectedItem.value.findIndex((v: any) => v.id === item.id);
|
||||
if (findIdex !== -1) {
|
||||
selectedItem.value.splice(findIdex, 1);
|
||||
} else selectedItem.value.push(item);
|
||||
}
|
||||
}
|
||||
|
|
@ -74,78 +79,76 @@ function assignSelect(to: unknown[], from: unknown[]) {
|
|||
<template>
|
||||
<section class="full-width column">
|
||||
<header
|
||||
class="row items-center no-wrap q-px-md q-py-sm"
|
||||
class="row items-center q-px-md q-py-sm"
|
||||
:class="{ 'bordered surface-3 ': borderSearchSection }"
|
||||
>
|
||||
<div class="col"><slot name="top"></slot></div>
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="q-ml-auto"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="search"
|
||||
debounce="300"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
<div class="col-12 col-md"><slot name="top"></slot></div>
|
||||
|
||||
<div class="row items-stretch col-12 col-md-4 no-wrap">
|
||||
<ToggleView
|
||||
v-model="viewMode"
|
||||
class="q-mr-sm"
|
||||
style="margin-left: 0 !important"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
dense
|
||||
class="col"
|
||||
:label="$t('general.search')"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="search"
|
||||
debounce="300"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</header>
|
||||
<slot name="tab"></slot>
|
||||
|
||||
<section class="col q-ma-md">
|
||||
<div
|
||||
v-if="items.length > 0 || newItems.length > 0"
|
||||
class="column"
|
||||
class="column full-height relative-position"
|
||||
style="gap: var(--size-4)"
|
||||
>
|
||||
<!-- NOTE: START - item -->
|
||||
<div class="row q-col-gutter-md">
|
||||
<div v-for="(item, i) in items" :key="i" :class="`${itemClass}`">
|
||||
<div
|
||||
class="rounded cursor-pointer relative-position"
|
||||
:style="`border: 1px solid ${selectedItem.includes(item) ? color : 'transparent'}`"
|
||||
@click="() => select(item)"
|
||||
<div style="inset: 0" class="absolute scroll">
|
||||
<!-- NOTE: START - item -->
|
||||
<div class="row q-col-gutter-md">
|
||||
<TableProductAndService
|
||||
v-model:selected="selectedItem"
|
||||
:rows="items"
|
||||
:grid="viewMode"
|
||||
:type
|
||||
>
|
||||
<div
|
||||
v-if="selectedItem.includes(item)"
|
||||
class="badge absolute-top-right flex justify-center q-ma-sm"
|
||||
:style="`background-color: ${color}`"
|
||||
>
|
||||
{{ selectedItem.indexOf(item) + 1 }}
|
||||
</div>
|
||||
<slot name="data" :item="item"></slot>
|
||||
</div>
|
||||
<template #grid="{ item }">
|
||||
<div :key="item.index" :class="`${itemClass}`">
|
||||
<div
|
||||
class="rounded cursor-pointer relative-position"
|
||||
:style="`border: 1px solid ${
|
||||
item._selectedIndex !== -1 ? color : 'transparent'
|
||||
}`"
|
||||
@click="() => select(item)"
|
||||
>
|
||||
<div
|
||||
v-if="item._selectedIndex !== -1"
|
||||
class="badge absolute-top-right flex justify-center q-ma-sm"
|
||||
:style="`background-color: ${color}`"
|
||||
>
|
||||
{{ item._selectedIndex + 1 }}
|
||||
</div>
|
||||
<slot name="data" :item="item"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</TableProductAndService>
|
||||
</div>
|
||||
</div>
|
||||
<!-- NOTE: END - item -->
|
||||
|
||||
<q-separator v-if="newItems.length !== 0" dark inset />
|
||||
|
||||
<!-- NOTE: START - newItem -->
|
||||
<div v-if="newItems.length !== 0" class="row q-col-gutter-md">
|
||||
<div v-for="(item, i) in newItems" :key="i" :class="`${itemClass}`">
|
||||
<div
|
||||
class="rounded cursor-pointer relative-position"
|
||||
:style="`border: 1px solid ${selectedItem.includes(item) ? color : 'transparent'}`"
|
||||
@click="() => select(item)"
|
||||
>
|
||||
<div
|
||||
v-if="selectedItem.includes(item)"
|
||||
class="badge absolute-top-right flex justify-center q-ma-sm"
|
||||
:style="`background-color: ${color}`"
|
||||
>
|
||||
{{ selectedItem.indexOf(item) + 1 }}
|
||||
</div>
|
||||
<slot name="newData" :item="item"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- NOTE: END - newItem -->
|
||||
|
||||
<div v-else class="flex justify-center full-height">
|
||||
<span class="col column items-center justify-center app-text-muted">
|
||||
<q-avatar style="background: var(--surface-0)" class="q-mb-md">
|
||||
|
|
|
|||
51
src/components/shared/ToggleView.vue
Normal file
51
src/components/shared/ToggleView.vue
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<script setup lang="ts">
|
||||
const model = defineModel<boolean>('model');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-btn-toggle
|
||||
id="btn-mode"
|
||||
v-model="model"
|
||||
dense
|
||||
class="no-shadow bordered rounded surface-1 q-ml-sm"
|
||||
:toggle-color="$q.dark.isActive ? 'grey-9' : 'grey-2'"
|
||||
size="xs"
|
||||
:options="[
|
||||
{ value: true, slot: 'folder' },
|
||||
{ value: false, slot: 'list' },
|
||||
]"
|
||||
>
|
||||
<template v-slot:folder>
|
||||
<q-icon
|
||||
name="mdi-view-grid-outline"
|
||||
size="16px"
|
||||
class="q-px-sm q-py-xs rounded"
|
||||
:style="{
|
||||
color: $q.dark.isActive
|
||||
? model
|
||||
? '#C9D3DB '
|
||||
: '#787B7C'
|
||||
: model
|
||||
? '#787B7C'
|
||||
: '#C9D3DB',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:list>
|
||||
<q-icon
|
||||
name="mdi-format-list-bulleted"
|
||||
class="q-px-sm q-py-xs rounded"
|
||||
size="16px"
|
||||
:style="{
|
||||
color: $q.dark.isActive
|
||||
? model === false
|
||||
? '#C9D3DB'
|
||||
: '#787B7C'
|
||||
: model === false
|
||||
? '#787B7C'
|
||||
: '#C9D3DB',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
</q-btn-toggle>
|
||||
</template>
|
||||
|
|
@ -118,6 +118,9 @@ function visibleNode(text: string, node: Node, ancestor?: Node[]): boolean {
|
|||
<template v-for="(node, i) in nodes" :key="i">
|
||||
<div
|
||||
class="tree-item"
|
||||
:class="{
|
||||
'cursor-pointer': node.children ? true : undefined,
|
||||
}"
|
||||
v-if="filterText ? visibleNode(filterText, node, ancestorNode) : true"
|
||||
>
|
||||
<slot
|
||||
|
|
@ -205,19 +208,21 @@ function visibleNode(text: string, node: Node, ancestor?: Node[]): boolean {
|
|||
/>
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="item__icon flex items-center justify-center"
|
||||
:style="`background: ${node.bg || dec?.bg}; color: ${node.fg || dec?.fg}; height: ${iconSize}; width: ${iconSize}`"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="flex items-center justify-center"
|
||||
:style="`height: calc(${iconSize} - 40%); width: calc(${iconSize} - 40%)`"
|
||||
class="item__icon flex items-center justify-center"
|
||||
:style="`background: ${node.bg || dec?.bg}; color: ${node.fg || dec?.fg}; height: ${iconSize}; width: ${iconSize}`"
|
||||
>
|
||||
<Icon
|
||||
v-if="(node.icon && dec && dec.icon) || (dec && dec.icon)"
|
||||
:icon="node.icon || dec.icon"
|
||||
class="full-width full-height"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center justify-center"
|
||||
:style="`height: calc(${iconSize} - 40%); width: calc(${iconSize} - 40%)`"
|
||||
>
|
||||
<Icon
|
||||
v-if="(node.icon && dec && dec.icon) || (dec && dec.icon)"
|
||||
:icon="node.icon || dec.icon"
|
||||
class="full-width full-height"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ const { getOptions, setFirstValue, getSelectedOption, filter } =
|
|||
query,
|
||||
...props.params,
|
||||
pageSize: 30,
|
||||
hasCancel: true,
|
||||
includeRegisteredBranch: true,
|
||||
});
|
||||
if (ret) return ret.result;
|
||||
|
|
@ -151,6 +150,7 @@ function setDefaultValue() {
|
|||
class="surface-3 q-ml-sm"
|
||||
rounded
|
||||
style="color: var(--foreground)"
|
||||
v-if="props.params?.hasCancel"
|
||||
>
|
||||
{{ scope.opt._count.canceledWork || 0 }}
|
||||
</q-badge>
|
||||
|
|
@ -191,6 +191,7 @@ function setDefaultValue() {
|
|||
class="surface-3 q-ml-xs"
|
||||
rounded
|
||||
style="color: var(--foreground)"
|
||||
v-if="props.params?.hasCancel"
|
||||
>
|
||||
{{ scope.opt._count.canceledWork || 0 }}
|
||||
</q-badge>
|
||||
|
|
|
|||
380
src/components/shared/table/TableProductAndService.vue
Normal file
380
src/components/shared/table/TableProductAndService.vue
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { Lang } from 'src/utils/ui';
|
||||
|
||||
import { QTableSlots, QTableColumn } from 'quasar';
|
||||
import { Product, Service } from 'src/stores/product-service/types';
|
||||
|
||||
import { calculateAge, dateFormatJS } from 'src/utils/datetime';
|
||||
import useOptionStore from 'stores/options';
|
||||
import { formatNumberDecimal, isRoleInclude } from 'src/stores/utils';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const selected = defineModel<unknown[]>('selected');
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
const { locale } = useI18n();
|
||||
const optionStore = useOptionStore();
|
||||
|
||||
const priceDisplay = computed(() => ({
|
||||
price: !isRoleInclude(['sale_agent']),
|
||||
agentPrice: isRoleInclude([
|
||||
'admin',
|
||||
'head_of_admin',
|
||||
'head_of_sale',
|
||||
'system',
|
||||
'owner',
|
||||
'accountant',
|
||||
'sale_agent',
|
||||
]),
|
||||
serviceCharge: isRoleInclude([
|
||||
'admin',
|
||||
'head_of_admin',
|
||||
'system',
|
||||
'owner',
|
||||
'accountant',
|
||||
]),
|
||||
}));
|
||||
|
||||
defineEmits<{
|
||||
(e: 'create'): void;
|
||||
}>();
|
||||
|
||||
type ExclusiveProps = {
|
||||
type: 'service' | 'product';
|
||||
grid?: boolean;
|
||||
disabledWorkerId?: string[];
|
||||
rows: Product[];
|
||||
};
|
||||
|
||||
const columnsProduct = [
|
||||
{
|
||||
name: '#check',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: (_) => '#check',
|
||||
},
|
||||
|
||||
{
|
||||
name: '#productName',
|
||||
align: 'left',
|
||||
label: 'general.name',
|
||||
field: (v: Product) => v.name,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'productProcessingTime',
|
||||
align: 'center',
|
||||
label: 'productService.product.processingTimeDay',
|
||||
field: (v: Product) => v.process,
|
||||
},
|
||||
|
||||
{
|
||||
name: '#priceInformation',
|
||||
align: 'center',
|
||||
label: 'productService.product.priceInformation',
|
||||
field: (v: Product) => v,
|
||||
},
|
||||
] satisfies QTableColumn[];
|
||||
|
||||
const columnsService = [
|
||||
{
|
||||
name: '#check',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: (_) => '#check',
|
||||
},
|
||||
|
||||
{
|
||||
name: '#serviceName',
|
||||
align: 'left',
|
||||
label: 'general.name',
|
||||
field: (v: Service) => v.name,
|
||||
},
|
||||
{
|
||||
name: 'serviceDetail',
|
||||
align: 'left',
|
||||
label: 'general.detail',
|
||||
field: (v: Service) => v.detail,
|
||||
},
|
||||
{
|
||||
name: 'serviceWorkTotal',
|
||||
align: 'left',
|
||||
label: 'productService.service.totalWork',
|
||||
field: (v: Service) => v.work.length,
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
align: 'left',
|
||||
label: 'general.createdAt',
|
||||
field: (v: Service) => dateFormatJS({ date: v.createdAt }),
|
||||
},
|
||||
] satisfies QTableColumn[];
|
||||
|
||||
const props = defineProps<ExclusiveProps>();
|
||||
|
||||
function getProductImageUrl(
|
||||
item: (Product | Service) & { type: string; _index: number },
|
||||
) {
|
||||
if (item.selectedImage) {
|
||||
return `${API_BASE_URL}/${item.type}/${item?.id}/image/${item?.selectedImage}`;
|
||||
}
|
||||
// NOTE: static image
|
||||
return `/images/${item.type}-avatar.png`;
|
||||
}
|
||||
|
||||
function handleUpdate() {
|
||||
if (selected.value?.length === 0) {
|
||||
selected.value = props.rows?.filter(
|
||||
(v) => !props.disabledWorkerId?.includes(v.id),
|
||||
);
|
||||
} else {
|
||||
selected.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function selectedIndex(item: any) {
|
||||
return selected.value?.findIndex((v: any) => v.id === item.id);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<q-table
|
||||
v-model:selected="selected"
|
||||
:rows-per-page-options="[0]"
|
||||
:rows="
|
||||
rows.map((data, i) => ({
|
||||
...data,
|
||||
_index: i,
|
||||
}))
|
||||
"
|
||||
:grid
|
||||
:columns="type === 'product' ? columnsProduct : columnsService"
|
||||
hide-bottom
|
||||
bordered
|
||||
flat
|
||||
hide-pagination
|
||||
selection="multiple"
|
||||
card-container-class="q-col-gutter-sm"
|
||||
class="full-width"
|
||||
row-key="id"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||
:props="props"
|
||||
>
|
||||
<q-th
|
||||
v-for="col in type === 'product' ? columnsProduct : columnsService"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
{{ $t(col.label) }}
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="
|
||||
col.name === '#serviceName' ||
|
||||
col.name === '#productName' ||
|
||||
col.name === '#priceInformation'
|
||||
"
|
||||
>
|
||||
{{ $t(col.label) }}
|
||||
</template>
|
||||
|
||||
<template v-if="col.name === '#check'">
|
||||
<q-checkbox
|
||||
v-model="props.selected"
|
||||
@update:model-value="(v) => handleUpdate()"
|
||||
size="sm"
|
||||
/>
|
||||
</template>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-slot:body="props: {
|
||||
row: (Product | Service) & { type: string; _index: number };
|
||||
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||
>
|
||||
<q-tr
|
||||
:class="{
|
||||
dark: $q.dark.isActive,
|
||||
'selectable-item__disabled': disabledWorkerId?.some(
|
||||
(id) => id === props.row.id,
|
||||
),
|
||||
}"
|
||||
class="text-center"
|
||||
>
|
||||
<q-td
|
||||
v-for="col in type === 'product' ? columnsProduct : columnsService"
|
||||
:align="col.align"
|
||||
:key="col.name"
|
||||
>
|
||||
<!-- NOTE: custom column will starts with # -->
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
<span>
|
||||
{{
|
||||
typeof col.field === 'string'
|
||||
? props.row[col.field as keyof (Product | Service)]
|
||||
: col.field(props.row as any)
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="col.name === '#productName' || col.name === '#serviceName'"
|
||||
>
|
||||
<div class="row full-height items-center">
|
||||
<q-avatar size="md">
|
||||
<q-img
|
||||
:src="getProductImageUrl(props.row)"
|
||||
:ratio="1"
|
||||
class="text-center"
|
||||
/>
|
||||
</q-avatar>
|
||||
<span class="column q-ml-sm">
|
||||
<span class="col-6">
|
||||
{{ props.row.name }}
|
||||
</span>
|
||||
<span class="col-6 app-text-muted">
|
||||
{{ props.row.code }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="
|
||||
col.name === '#priceInformation' &&
|
||||
!disabledWorkerId?.some((id) => id === props.row.id)
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="row full-width q-gutter-x-md no-wrap items-center text-right"
|
||||
>
|
||||
<div
|
||||
class="tags tags-color-orange col column ellipsis-2-lines"
|
||||
:class="{
|
||||
disable: props.row.status === 'INACTIVE',
|
||||
}"
|
||||
style="min-width: 50px"
|
||||
v-if="priceDisplay.price"
|
||||
>
|
||||
<div class="col app-text-muted-2 text-caption">
|
||||
{{ $t('productService.product.salePrice') }}
|
||||
</div>
|
||||
<div class="col text-weight-bold">
|
||||
฿{{ formatNumberDecimal(props.row.price || 0, 2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tags tags-color-purple col column ellipsis-2-lines"
|
||||
:class="{
|
||||
disable: props.row.status === 'INACTIVE',
|
||||
}"
|
||||
style="min-width: 50px"
|
||||
v-if="priceDisplay.agentPrice"
|
||||
>
|
||||
<div class="col app-text-muted-2 text-caption">
|
||||
{{ $t('productService.product.agentPrice') }}
|
||||
</div>
|
||||
<div class="col text-weight-bold">
|
||||
฿{{ formatNumberDecimal(props.row.agentPrice || 0, 2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tags tags-color-pink col column ellipsis-2-lines"
|
||||
:class="{
|
||||
disable: props.row.status === 'INACTIVE',
|
||||
}"
|
||||
style="min-width: 50px"
|
||||
v-if="priceDisplay.serviceCharge"
|
||||
>
|
||||
<div class="col app-text-muted-2 text-caption">
|
||||
{{ $t('productService.product.processingPrice') }}
|
||||
</div>
|
||||
<div class="col">
|
||||
฿{{ formatNumberDecimal(props.row.serviceCharge || 0, 2) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="
|
||||
col.name === '#check' &&
|
||||
!disabledWorkerId?.some((id) => id === props.row.id)
|
||||
"
|
||||
>
|
||||
<q-checkbox v-model="props.selected" size="sm" />
|
||||
</template>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:item="props: { row: any; rowIndex: number }">
|
||||
<slot
|
||||
name="grid"
|
||||
:item="{
|
||||
index: props.rowIndex,
|
||||
...props.row,
|
||||
_selectedIndex: selectedIndex(props.row),
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.selectable-item__disabled {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.5;
|
||||
|
||||
& :deep(*) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: inline-block;
|
||||
color: hsla(var(--_color-tag) / 1);
|
||||
background: hsla(var(--_color-tag) / 0.075);
|
||||
border-radius: var(--radius-2);
|
||||
padding-inline: var(--size-2);
|
||||
|
||||
&.disable {
|
||||
filter: grayscale(100%);
|
||||
opacity: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.tags-color-green {
|
||||
--_color-tag: var(--teal-10-hsl);
|
||||
}
|
||||
|
||||
.dark .tags-color-green {
|
||||
--_color-tag: var(--teal-8-hsl);
|
||||
}
|
||||
|
||||
.tags-color-orange {
|
||||
--_color-tag: var(--orange-5-hsl);
|
||||
}
|
||||
|
||||
.dark .tags-color-orange {
|
||||
--_color-tag: var(--orange-6-hsl);
|
||||
}
|
||||
|
||||
.tags-color-purple {
|
||||
--_color-tag: var(--violet-11-hsl);
|
||||
}
|
||||
|
||||
.dark .tags-color-purple {
|
||||
--_color-tag: var(--violet-10-hsl);
|
||||
}
|
||||
|
||||
.tags-color-pink {
|
||||
--_color-tag: var(--pink-6-hsl);
|
||||
}
|
||||
</style>
|
||||
220
src/components/shared/table/TableWorker.vue
Normal file
220
src/components/shared/table/TableWorker.vue
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { Lang } from 'src/utils/ui';
|
||||
|
||||
import { QTableSlots, QTableColumn } from 'quasar';
|
||||
import { Employee } from 'src/stores/employee/types';
|
||||
|
||||
import { calculateAge, dateFormatJS } from 'src/utils/datetime';
|
||||
import useOptionStore from 'stores/options';
|
||||
|
||||
const selected = defineModel<Employee[]>('selected');
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
const { locale } = useI18n();
|
||||
const optionStore = useOptionStore();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'create'): void;
|
||||
}>();
|
||||
|
||||
type ExclusiveProps = {
|
||||
grid?: boolean;
|
||||
disabledWorkerId?: string[];
|
||||
rows: Employee[];
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: '#check',
|
||||
align: 'left',
|
||||
label: '',
|
||||
field: (_: any) => '#check',
|
||||
},
|
||||
{
|
||||
name: 'order',
|
||||
align: 'center',
|
||||
label: 'general.order',
|
||||
field: (e: Employee & { _index: number }) => e._index + 1,
|
||||
},
|
||||
{
|
||||
name: 'foreignRefNo',
|
||||
align: 'center',
|
||||
label: 'quotation.foreignRefNo',
|
||||
field: (v: Employee) =>
|
||||
v.employeePassport !== undefined
|
||||
? v.employeePassport[0]?.number || '-'
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
name: 'employeeName',
|
||||
align: 'left',
|
||||
label: 'quotation.employeeName',
|
||||
field: (v: Employee) =>
|
||||
locale.value === Lang.English
|
||||
? `${v.firstNameEN} ${v.lastNameEN}`
|
||||
: `${v.firstName} ${v.lastName}`,
|
||||
},
|
||||
{
|
||||
name: 'birthDate',
|
||||
align: 'left',
|
||||
label: 'general.birthDate',
|
||||
field: (v: Employee) => dateFormatJS({ date: v.dateOfBirth }),
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
align: 'left',
|
||||
label: 'general.age',
|
||||
field: (v: Employee) => calculateAge(v.dateOfBirth),
|
||||
},
|
||||
{
|
||||
name: 'nationality',
|
||||
align: 'left',
|
||||
label: 'general.nationality',
|
||||
field: (v: Employee) => optionStore.mapOption(v.nationality),
|
||||
},
|
||||
{
|
||||
name: 'documentExpireDate',
|
||||
align: 'left',
|
||||
label: 'quotation.documentExpireDate',
|
||||
field: (v: Employee) =>
|
||||
v.employeePassport !== undefined &&
|
||||
v.employeePassport[0]?.expireDate !== undefined
|
||||
? dateFormatJS({ date: v.employeePassport[0]?.expireDate })
|
||||
: '-',
|
||||
},
|
||||
] satisfies QTableColumn[];
|
||||
|
||||
const props = defineProps<ExclusiveProps>();
|
||||
|
||||
function getEmployeeImageUrl(item: Employee) {
|
||||
if (item.selectedImage) {
|
||||
return `${API_BASE_URL}/employee/${item.id}/image/${item.selectedImage}`;
|
||||
}
|
||||
// NOTE: static image
|
||||
return `/images/employee-avatar-${item.gender}.png`;
|
||||
}
|
||||
|
||||
function handleUpdate() {
|
||||
if (selected.value?.length === 0) {
|
||||
selected.value = props.rows?.filter(
|
||||
(v) => !props.disabledWorkerId?.includes(v.id),
|
||||
);
|
||||
} else {
|
||||
selected.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function selectedIndex(item: Employee) {
|
||||
return selected.value?.findIndex((v) => v.id === item.id);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<q-table
|
||||
v-model:selected="selected"
|
||||
:rows-per-page-options="[0]"
|
||||
:rows="
|
||||
rows.map((data, i) => ({
|
||||
...data,
|
||||
_index: i,
|
||||
}))
|
||||
"
|
||||
:grid
|
||||
:columns
|
||||
hide-bottom
|
||||
bordered
|
||||
flat
|
||||
hide-pagination
|
||||
selection="multiple"
|
||||
card-container-class="q-col-gutter-sm"
|
||||
class="full-width"
|
||||
row-key="id"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||
:props="props"
|
||||
>
|
||||
<q-th v-for="col in columns" :key="col.name" :props="props">
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
{{ $t(col.label) }}
|
||||
</template>
|
||||
<template v-if="col.name === '#check'">
|
||||
<q-checkbox
|
||||
v-model="props.selected"
|
||||
@update:model-value="(v) => handleUpdate()"
|
||||
size="sm"
|
||||
/>
|
||||
</template>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-slot:body="props: {
|
||||
row: Employee & { _index: number };
|
||||
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||
>
|
||||
<q-tr
|
||||
:class="{
|
||||
dark: $q.dark.isActive,
|
||||
'selectable-item__disabled': disabledWorkerId?.some(
|
||||
(id) => id === props.row.id,
|
||||
),
|
||||
}"
|
||||
class="text-center"
|
||||
>
|
||||
<q-td v-for="col in columns" :align="col.align" :key="col.name">
|
||||
<!-- NOTE: custom column will starts with # -->
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
<q-avatar
|
||||
v-if="col.name === 'employeeName'"
|
||||
class="q-mr-sm"
|
||||
size="md"
|
||||
>
|
||||
<q-img
|
||||
:src="getEmployeeImageUrl(props.row)"
|
||||
:ratio="1"
|
||||
class="text-center"
|
||||
/>
|
||||
</q-avatar>
|
||||
<span>
|
||||
{{
|
||||
typeof col.field === 'string'
|
||||
? props.row[col.field as keyof Employee]
|
||||
: col.field(props.row)
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<template
|
||||
v-if="
|
||||
col.name === '#check' &&
|
||||
!disabledWorkerId?.some((id) => id === props.row.id)
|
||||
"
|
||||
>
|
||||
<q-checkbox v-model="props.selected" size="sm" />
|
||||
</template>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:item="props: { row: Employee; rowIndex: number }">
|
||||
<slot
|
||||
name="grid"
|
||||
:index="props.rowIndex"
|
||||
:item="{ ...props.row, _selectedIndex: selectedIndex(props.row) }"
|
||||
/>
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.selectable-item__disabled {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.5;
|
||||
|
||||
& :deep(*) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -79,7 +79,7 @@ export default {
|
|||
calculateVat: 'Calculate VAT',
|
||||
discountAfterVat: 'Discount after vat',
|
||||
totalAfterDiscount: 'Total after discount',
|
||||
totalVatExcluded: 'Tax-exempt amoun',
|
||||
totalVatExcluded: 'Tax-exempt amount',
|
||||
totalVatIncluded: 'Taxable amount',
|
||||
vat: 'VAT {msg}',
|
||||
totalAmount: 'Total amount (Baht)',
|
||||
|
|
@ -140,6 +140,13 @@ export default {
|
|||
numberOfDay: 'Number of days',
|
||||
other: 'Other',
|
||||
agencyAddress: 'Agency Address',
|
||||
hintRemark: 'You can add tags such as ',
|
||||
quotationLabor: 'Used to display the list of employees',
|
||||
quotationPayment: 'Used to display payment details',
|
||||
orderDetail: 'To display the list of employees in that product',
|
||||
ofPage: '{current} of {total}',
|
||||
included: 'Included',
|
||||
notIncluded: 'Not Included',
|
||||
},
|
||||
|
||||
menu: {
|
||||
|
|
@ -1125,6 +1132,8 @@ export default {
|
|||
},
|
||||
|
||||
preview: {
|
||||
dateAt: 'Date {msg}',
|
||||
seller: 'Seller',
|
||||
taskOrder: 'Work Order',
|
||||
doc: 'View Document',
|
||||
productList: 'Product List',
|
||||
|
|
@ -1135,15 +1144,17 @@ export default {
|
|||
vat: 'VAT',
|
||||
value: 'Value',
|
||||
netValue: 'Net Value',
|
||||
dueDate: 'Due Date',
|
||||
paymentMethods: 'Payment Methods',
|
||||
title: {
|
||||
creditNote: 'Credit Note',
|
||||
quotation: 'Quotation',
|
||||
invoice: 'Invoice',
|
||||
payment: 'Payment',
|
||||
receipt: 'Receipt',
|
||||
debitNote: 'Debit Note',
|
||||
},
|
||||
},
|
||||
|
||||
address: {
|
||||
subDistrict: 'Sub District',
|
||||
subArea: 'Sub Area',
|
||||
|
|
@ -1225,4 +1236,49 @@ export default {
|
|||
dataSum: 'Summary of all receipts/tax invoices',
|
||||
workSheetName: 'Worksheet name',
|
||||
},
|
||||
|
||||
debitNote: {
|
||||
title: 'Debit Note',
|
||||
caption: 'All Debit Notes',
|
||||
expire: 'Expired',
|
||||
payment: 'Payment',
|
||||
receipt: 'Receipt',
|
||||
succeed: 'Completed',
|
||||
downloadReceipt: 'Download Receipt',
|
||||
downloadTaxInvoice: 'Download Tax Invoice',
|
||||
|
||||
label: {
|
||||
additionalDetail: 'Additional Details',
|
||||
specifyReasonForDebit: 'Specify Reason for Debit',
|
||||
debitNoteInformation: 'Debit Note Information',
|
||||
codeDebit: 'Debit Note Number',
|
||||
codeQuotation: 'Quotation Number',
|
||||
quotationWorkName: 'Work Name',
|
||||
quotationPayment: 'Payment Method',
|
||||
value: 'Net Value',
|
||||
submit: 'Approve Debit Note',
|
||||
},
|
||||
|
||||
stats: {
|
||||
Pending: 'Debit Note',
|
||||
Expire: 'Expired',
|
||||
Payment: 'Payment',
|
||||
Receipt: 'Receipt',
|
||||
Succeed: 'Completed',
|
||||
},
|
||||
|
||||
viewMode: {
|
||||
payment: 'Payment',
|
||||
receipt: 'Receipt/Tax Invoice',
|
||||
processComplete: 'Completed',
|
||||
},
|
||||
|
||||
status: {
|
||||
Pending: 'Debit Note',
|
||||
Expire: 'Expired',
|
||||
Payment: 'Payment',
|
||||
Receipt: 'Receipt',
|
||||
Succeed: 'Completed',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -142,6 +142,13 @@ export default {
|
|||
numberOfDay: 'จำนวนวัน',
|
||||
other: 'อื่นๆ',
|
||||
agencyAddress: 'ที่อยู่หน่วยงาน ',
|
||||
hintRemark: 'คุณสามารถใส่',
|
||||
quotationLabor: 'ใช้แสดงรายชื่อลูกจ้าง',
|
||||
quotationPayment: 'ใช้แสดงรายละเอียดในการชำระเงิน',
|
||||
orderDetail: 'เพื่อแสดงรายชื่อลูกจ้างในสินค้านั้นๆ',
|
||||
ofPage: '{current} จาก {total}',
|
||||
included: 'รวม',
|
||||
notIncluded: 'ไม่รวม',
|
||||
},
|
||||
|
||||
menu: {
|
||||
|
|
@ -1106,6 +1113,8 @@ export default {
|
|||
},
|
||||
|
||||
preview: {
|
||||
dateAt: 'วันที่{msg}',
|
||||
seller: 'ผู้ขาย',
|
||||
taskOrder: 'ใบสั่งงาน',
|
||||
doc: 'ดูเอกสาร',
|
||||
productList: 'รายการสินค้า',
|
||||
|
|
@ -1116,12 +1125,15 @@ export default {
|
|||
vat: 'ภาษี',
|
||||
value: 'มูลค่า',
|
||||
netValue: 'มูลค่าสุทธิ',
|
||||
dueDate: 'วันครบกำหนดชำระ',
|
||||
paymentMethods: 'ช่องทางชำระเงิน',
|
||||
title: {
|
||||
creditNote: 'ใบลดหนี้',
|
||||
quotation: 'ใบเสนอราคา',
|
||||
invoice: 'ใบแจ้งหนี้',
|
||||
payment: 'ชำระหนี้',
|
||||
receipt: 'ใบเสร็จรับเงิน',
|
||||
debitNote: 'ใบเพิ่มหนี้',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -1206,4 +1218,48 @@ export default {
|
|||
dataSum: 'สรุปใบเสร็จรับเงิน/กำกับภาษีทั้งหมด',
|
||||
workSheetName: 'ชื่อใบงาน',
|
||||
},
|
||||
debitNote: {
|
||||
title: 'ใบเพิ่มหนี้',
|
||||
caption: 'ใบเพิ่มหนี้ทั้งหมด',
|
||||
expire: 'พ้นกำหนด',
|
||||
payment: 'ชำระเงิน',
|
||||
receipt: 'ใบเสร็จรับเงิน',
|
||||
succeed: 'เสร็จสิ้น',
|
||||
downloadReceipt: 'ดาวน์โหลดใบเสร็จรับเงิน',
|
||||
downloadTaxInvoice: 'ดาวน์โหลดใบกำกับภาษี',
|
||||
|
||||
label: {
|
||||
additionalDetail: 'อธิบายเพิ่มเติม',
|
||||
specifyReasonForDebit: 'ระบุสาเหตุการเพิ่มหนี้',
|
||||
debitNoteInformation: 'ข้อมูลการเพิ่มหนี้',
|
||||
codeDebit: 'เลขที่ใบเพิ่มหนี้',
|
||||
codeQuotation: 'เลขที่ใบเสนอราคา',
|
||||
quotationWorkName: 'ชื่อใบงาน',
|
||||
quotationPayment: 'วิธีการชำระ',
|
||||
value: 'มูลค่าสุทธิ',
|
||||
submit: 'อนุมัติใบเพิ่มหนี้',
|
||||
},
|
||||
|
||||
stats: {
|
||||
Pending: 'ใบเพิ่มหนี้',
|
||||
Expire: 'พ้นกำหนด',
|
||||
Payment: 'ชำระเงิน',
|
||||
Receipt: 'ใบเสร็จรับเงิน',
|
||||
Succeed: 'เสร็จสิ้น',
|
||||
},
|
||||
|
||||
viewMode: {
|
||||
payment: 'ชำระเงิน',
|
||||
receipt: 'ใบเสร็จรับเงิน/ใบกำกับภาษี',
|
||||
processComplete: 'เสร็จสิ้น',
|
||||
},
|
||||
|
||||
status: {
|
||||
Pending: 'ใบเพิ่มหนี้',
|
||||
Expire: 'พ้นกำหนด',
|
||||
Payment: 'ชำระเงิน',
|
||||
Receipt: 'ใบเสร็จรับเงิน',
|
||||
Succeed: 'เสร็จสิ้น',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { storeToRefs } from 'pinia';
|
|||
import { Icon } from '@iconify/vue';
|
||||
import useMyBranch from 'stores/my-branch';
|
||||
import { getUserId, getRole } from 'src/services/keycloak';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
type Menu = {
|
||||
label: string;
|
||||
|
|
@ -17,6 +18,8 @@ type Menu = {
|
|||
};
|
||||
|
||||
const router = useRouter();
|
||||
const $q = useQuasar();
|
||||
|
||||
const userBranch = useMyBranch();
|
||||
const { currentMyBranch } = storeToRefs(userBranch);
|
||||
|
||||
|
|
@ -35,17 +38,6 @@ const currentPath = computed(() => {
|
|||
return router.currentRoute.value.path;
|
||||
});
|
||||
|
||||
// const labelMenu = ref<
|
||||
// {
|
||||
// label: string;
|
||||
// icon: string;
|
||||
// route: string;
|
||||
// hidden?: boolean;
|
||||
// disabled?: boolean;
|
||||
// isax?: boolean;
|
||||
// }[]
|
||||
// >([]);
|
||||
|
||||
function navigateTo(label: string, destination?: string) {
|
||||
if (!destination) return;
|
||||
router.push(`${destination}`);
|
||||
|
|
@ -57,6 +49,8 @@ function reActiveMenu() {
|
|||
);
|
||||
|
||||
const currMenuIndex = menuData.value.findIndex((m) => m === currMenu);
|
||||
|
||||
if ($q.screen.lt.sm) menuActive.value.fill(false);
|
||||
menuActive.value[currMenuIndex] = true;
|
||||
}
|
||||
|
||||
|
|
@ -158,8 +152,8 @@ onMounted(async () => {
|
|||
disabled: false,
|
||||
children: [
|
||||
{ label: 'receipt', route: '/receipt' },
|
||||
{ label: 'creditNote', route: '/credit-note', disabled: true },
|
||||
{ label: 'debitNote', route: '', disabled: true },
|
||||
{ label: 'creditNote', route: '/credit-note' },
|
||||
{ label: 'debitNote', route: '/debit-note' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -204,7 +198,6 @@ onMounted(async () => {
|
|||
:width="mini ? 80 : 256"
|
||||
show-if-above
|
||||
>
|
||||
<!-- :width="$q.screen.lt.sm ? $q.screen.width - 16 : 256" -->
|
||||
<section
|
||||
class="scroll"
|
||||
style="overflow-x: hidden; scrollbar-gutter: stable"
|
||||
|
|
@ -236,7 +229,6 @@ onMounted(async () => {
|
|||
:disable="menu.disabled"
|
||||
:header-class="{
|
||||
row: true,
|
||||
'justify-between': !mini,
|
||||
'no-padding justify-center': mini,
|
||||
'active-menu text-weight-bold': menuActive[i],
|
||||
'text-weight-medium': !menu.disabled,
|
||||
|
|
@ -254,7 +246,12 @@ onMounted(async () => {
|
|||
:class="`isax ${menu.icon}`"
|
||||
style="font-size: 24px"
|
||||
/>
|
||||
<Icon v-else :icon="menu.icon || ''" width="24px" />
|
||||
<Icon
|
||||
v-else
|
||||
:icon="menu.icon || ''"
|
||||
width="24px"
|
||||
:class="{ 'fix-icon': !menuActive[i] }"
|
||||
/>
|
||||
<span
|
||||
v-if="!mini"
|
||||
class="q-pl-sm"
|
||||
|
|
@ -514,4 +511,12 @@ onMounted(async () => {
|
|||
border-bottom-right-radius: var(--radius-2);
|
||||
}
|
||||
}
|
||||
|
||||
.fix-icon {
|
||||
color: var(--text-mute-2) !important;
|
||||
}
|
||||
|
||||
:deep(.q-item.q-item-type.row.no-wrap.q-item--dense.disabled) {
|
||||
opacity: 30% !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -208,6 +208,9 @@ onMounted(async () => {
|
|||
<div
|
||||
class="q-px-lg row items-center justify-start q-pb-md q-pt-lg"
|
||||
style="position: sticky; top: 0; z-index: 8"
|
||||
:style="`
|
||||
background: ${$q.screen.lt.md ? ($q.dark.isActive ? '#1c1d21' : '#ecedef') : 'transparent'};
|
||||
`"
|
||||
>
|
||||
<q-btn
|
||||
v-if="$q.screen.lt.sm"
|
||||
|
|
@ -530,7 +533,7 @@ onMounted(async () => {
|
|||
}
|
||||
}
|
||||
|
||||
.avartar-border {
|
||||
.avatar-border {
|
||||
margin-top: 24px;
|
||||
border: 5px solid var(--surface-1);
|
||||
border-radius: 50%;
|
||||
|
|
|
|||
|
|
@ -317,6 +317,7 @@ onMounted(async () => {
|
|||
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)">
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Icon } from '@iconify/vue';
|
|||
import { BranchContact } from 'stores/branch-contact/types';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { QTableProps, QTableSlots } from 'quasar';
|
||||
import type { QSelect, QTableProps, QTableSlots } from 'quasar';
|
||||
import { resetScrollBar } from 'src/stores/utils';
|
||||
import useBranchStore from 'stores/branch';
|
||||
import useFlowStore from 'stores/flow';
|
||||
|
|
@ -72,6 +72,7 @@ const typeBranchItem = [
|
|||
color: 'var(--blue-6-hsl)',
|
||||
},
|
||||
];
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
const holdDialog = ref(false);
|
||||
const isSubCreate = ref(false);
|
||||
const columns = [
|
||||
|
|
@ -302,7 +303,10 @@ const stats = ref<
|
|||
}[]
|
||||
>([]);
|
||||
|
||||
const splitterModel = ref(25);
|
||||
// const splitterModel = ref(25);
|
||||
const splitterModel = computed(() =>
|
||||
$q.screen.lt.md ? (currentHq.value.id ? 0 : 100) : 25,
|
||||
);
|
||||
|
||||
const defaultFormData = {
|
||||
headOfficeId: null,
|
||||
|
|
@ -1020,12 +1024,13 @@ watch(currentHq, () => {
|
|||
class="col"
|
||||
before-class="overflow-hidden"
|
||||
after-class="overflow-hidden"
|
||||
:disable="$q.screen.lt.sm"
|
||||
>
|
||||
<template v-slot:before>
|
||||
<div class="surface-1 column full-height">
|
||||
<div
|
||||
class="row no-wrap full-width bordered-b text-weight-bold surface-3 items-center q-px-md q-py-sm"
|
||||
:style="`min-height: ${$q.screen.gt.sm ? '57px' : '100.8px'}`"
|
||||
:style="`min-height: ${$q.screen.gt.sm ? '57px' : ''}`"
|
||||
>
|
||||
<div class="col ellipsis-2-lines">
|
||||
{{ $t('branch.allBranch') }}
|
||||
|
|
@ -1157,7 +1162,7 @@ watch(currentHq, () => {
|
|||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="q-mr-md col-12 col-md-4"
|
||||
class="col col-md-3"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="inputSearch"
|
||||
debounce="200"
|
||||
|
|
@ -1165,13 +1170,26 @@ watch(currentHq, () => {
|
|||
<template v-slot:prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div
|
||||
class="row col-12 col-md-6"
|
||||
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||
>
|
||||
<div class="row col-md-6 justify-end">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -1179,7 +1197,7 @@ watch(currentHq, () => {
|
|||
option-value="value"
|
||||
:hide-dropdown-icon="$q.screen.lt.sm"
|
||||
option-label="label"
|
||||
class="col"
|
||||
class="col-md-5"
|
||||
map-options
|
||||
:for="'field-select-status'"
|
||||
emit-value
|
||||
|
|
@ -1194,6 +1212,7 @@ watch(currentHq, () => {
|
|||
></q-select>
|
||||
|
||||
<q-select
|
||||
v-if="!modeView"
|
||||
id="select-field"
|
||||
for="select-field"
|
||||
:options="
|
||||
|
|
@ -1204,7 +1223,7 @@ watch(currentHq, () => {
|
|||
"
|
||||
:display-value="$t('general.displayField')"
|
||||
:hide-dropdown-icon="$q.screen.lt.sm"
|
||||
class="col q-mx-sm"
|
||||
class="col q-ml-sm"
|
||||
v-model="fieldSelected"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
|
|
@ -1220,7 +1239,7 @@ watch(currentHq, () => {
|
|||
id="btn-mode"
|
||||
v-model="modeView"
|
||||
dense
|
||||
class="no-shadow bordered rounded surface-1"
|
||||
class="no-shadow bordered rounded surface-1 q-ml-sm"
|
||||
:toggle-color="$q.dark.isActive ? 'grey-9' : 'grey-2'"
|
||||
size="xs"
|
||||
:options="[
|
||||
|
|
@ -1266,8 +1285,26 @@ watch(currentHq, () => {
|
|||
|
||||
<div
|
||||
v-if="
|
||||
inputSearch &&
|
||||
treeData.flatMap((v) => [v, ...v.branch]).length === 0
|
||||
(
|
||||
currentSubBranch ||
|
||||
(inputSearch !== ''
|
||||
? treeData.flatMap((v) => [v, ...v.branch])
|
||||
: treeData)
|
||||
).filter((v) => {
|
||||
if (
|
||||
statusFilter === 'statusACTIVE' &&
|
||||
v.status === 'INACTIVE'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
statusFilter === 'statusINACTIVE' &&
|
||||
v.status !== 'INACTIVE'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).length === 0
|
||||
"
|
||||
class="row items-center justify-center full-height"
|
||||
>
|
||||
|
|
@ -1788,7 +1825,7 @@ watch(currentHq, () => {
|
|||
v-if="currentTab === 'main'"
|
||||
:class="{
|
||||
'q-py-md q-px-lg': $q.screen.gt.sm,
|
||||
'q-py-sm q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
style="position: absolute; z-index: 99999; top: 0; right: 0"
|
||||
>
|
||||
|
|
@ -1886,7 +1923,7 @@ watch(currentHq, () => {
|
|||
class="col-12 col-md-10 full-height"
|
||||
:class="{
|
||||
'q-py-md q-pr-md ': $q.screen.gt.sm,
|
||||
'q-py-md q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
id="branch-form"
|
||||
style="overflow-y: auto"
|
||||
|
|
@ -2137,7 +2174,7 @@ watch(currentHq, () => {
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="col"
|
||||
class="col full-width"
|
||||
:class="{
|
||||
'q-px-lg q-pb-lg': $q.screen.gt.sm,
|
||||
'q-px-md q-pb-sm': !$q.screen.gt.sm,
|
||||
|
|
@ -2150,7 +2187,7 @@ watch(currentHq, () => {
|
|||
<div
|
||||
:class="{
|
||||
'q-py-md q-px-lg': $q.screen.gt.sm,
|
||||
'q-py-sm q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
style="position: absolute; z-index: 99999; top: 0; right: 0"
|
||||
>
|
||||
|
|
@ -2241,7 +2278,7 @@ watch(currentHq, () => {
|
|||
class="col-12 col-md-10 full-height"
|
||||
:class="{
|
||||
'q-py-md q-pr-md ': $q.screen.gt.sm,
|
||||
'q-py-md q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
id="branch-info"
|
||||
style="overflow-y: auto"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import useOptionStore from 'stores/options';
|
|||
import useAddressStore from 'stores/address';
|
||||
import useMyBranch from 'src/stores/my-branch';
|
||||
import { calculateAge } from 'src/utils/datetime';
|
||||
import { useQuasar, type QTableProps } from 'quasar';
|
||||
import { QSelect, useQuasar, type QTableProps } from 'quasar';
|
||||
import { dialog, baseUrl } from 'stores/utils';
|
||||
import { useNavigator } from 'src/stores/navigator';
|
||||
import { isRoleInclude, resetScrollBar } from 'src/stores/utils';
|
||||
|
|
@ -73,6 +73,8 @@ const isImageEdit = ref(false);
|
|||
const imageDialog = ref(false);
|
||||
const infoDrawerEdit = ref(false);
|
||||
const refreshImageState = ref(false);
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
const firstScroll = ref(false);
|
||||
|
||||
const inputSearch = ref('');
|
||||
const currentTab = ref<string>('ALL');
|
||||
|
|
@ -433,7 +435,7 @@ async function onSubmit(excludeDialog?: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
await fetchUserList();
|
||||
await fetchUserList($q.screen.xs);
|
||||
typeStats.value = await userStore.typeStats();
|
||||
const res = await branchStore.userStats(formData.value.userType);
|
||||
if (res) {
|
||||
|
|
@ -465,9 +467,14 @@ async function onSubmit(excludeDialog?: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
currentTab.value = formData.value.userType;
|
||||
if (
|
||||
currentTab.value === formData.value.userType &&
|
||||
currentTab.value !== 'ALL'
|
||||
) {
|
||||
await fetchUserList($q.screen.xs);
|
||||
}
|
||||
|
||||
await fetchUserList();
|
||||
currentTab.value = formData.value.userType;
|
||||
|
||||
typeStats.value = await userStore.typeStats();
|
||||
const res = await branchStore.userStats(formData.value.userType);
|
||||
|
|
@ -490,7 +497,7 @@ async function onDelete(id: string) {
|
|||
action: async () => {
|
||||
await userStore.deleteById(id);
|
||||
|
||||
await fetchUserList();
|
||||
await fetchUserList($q.screen.xs);
|
||||
typeStats.value = await userStore.typeStats();
|
||||
infoDrawer.value = false;
|
||||
flowStore.rotate();
|
||||
|
|
@ -637,11 +644,15 @@ async function fetchImageList(id: string, selectedName?: string) {
|
|||
return res;
|
||||
}
|
||||
|
||||
async function fetchUserList() {
|
||||
await userStore.fetchList({
|
||||
async function fetchUserList(mobileFetch?: boolean) {
|
||||
const userLength = userData.value?.result.length || 0;
|
||||
const total = typeStats.value?.[currentTab.value] ?? 0;
|
||||
const ret = await userStore.fetchList({
|
||||
includeBranch: true,
|
||||
pageSize: pageSize.value,
|
||||
page: currentPage.value,
|
||||
pageSize: mobileFetch
|
||||
? userLength + (total === userLength ? 1 : 0)
|
||||
: pageSize.value,
|
||||
page: mobileFetch ? 1 : currentPage.value,
|
||||
query: !!inputSearch.value ? inputSearch.value : undefined,
|
||||
userType: currentTab.value === 'ALL' ? undefined : currentTab.value,
|
||||
status:
|
||||
|
|
@ -651,6 +662,21 @@ async function fetchUserList() {
|
|||
? 'ACTIVE'
|
||||
: 'INACTIVE',
|
||||
});
|
||||
|
||||
if (ret) {
|
||||
if ($q.screen.xs && !mobileFetch) {
|
||||
if (!userData.value) {
|
||||
userData.value = ret;
|
||||
} else {
|
||||
userData.value.page = ret.page;
|
||||
userData.value.pageSize = ret.pageSize;
|
||||
userData.value.total = ret.total;
|
||||
userData.value?.result.push(...ret.result);
|
||||
}
|
||||
} else {
|
||||
userData.value = ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function noPersonnel() {
|
||||
|
|
@ -687,8 +713,11 @@ onMounted(async () => {
|
|||
watch(
|
||||
() => currentTab.value,
|
||||
async (label) => {
|
||||
firstScroll.value = true;
|
||||
mapUserType(label);
|
||||
await fetchUserList();
|
||||
if (userData.value) userData.value.result = [];
|
||||
currentPage.value = 1;
|
||||
if ($q.screen.gt.xs) await fetchUserList();
|
||||
const res = await branchStore.userStats(label);
|
||||
if (res) {
|
||||
userStats.value = res;
|
||||
|
|
@ -721,7 +750,12 @@ watch(
|
|||
},
|
||||
);
|
||||
|
||||
watch([inputSearch, statusFilter, pageSize], async () => await fetchUserList());
|
||||
watch([inputSearch, statusFilter, pageSize], async () => {
|
||||
if (userData.value) userData.value.result = [];
|
||||
currentPage.value = 1;
|
||||
|
||||
await fetchUserList();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => $q.screen.lt.md,
|
||||
|
|
@ -820,7 +854,7 @@ watch(
|
|||
class="col surface-2 rounded justify-between column no-wrap bordered full-height overflow-hidden"
|
||||
>
|
||||
<div class="column">
|
||||
<div
|
||||
<header
|
||||
class="row surface-3 justify-between full-width items-center bordered-b"
|
||||
style="z-index: 1"
|
||||
>
|
||||
|
|
@ -830,7 +864,7 @@ watch(
|
|||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="q-mr-md col-12 col-md-3"
|
||||
class="col col-md-3"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="inputSearch"
|
||||
debounce="200"
|
||||
|
|
@ -838,14 +872,26 @@ watch(
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div
|
||||
class="row col-12 col-md-5"
|
||||
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||
style="white-space: nowrap"
|
||||
>
|
||||
<div class="row col-md-5" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -936,7 +982,7 @@ watch(
|
|||
</q-btn-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="surface-2 bordered-b q-px-md full-width">
|
||||
<q-tabs
|
||||
|
|
@ -1053,11 +1099,27 @@ watch(
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
<article
|
||||
v-if="userData"
|
||||
class="flex full-width full-height q-pa-md surface-2 column col scroll"
|
||||
>
|
||||
<template v-if="userData && userData.total > 0">
|
||||
<q-infinite-scroll
|
||||
:key="currentTab"
|
||||
:offset="100"
|
||||
@load="
|
||||
(_, done) => {
|
||||
if ($q.screen.gt.xs) return;
|
||||
currentPage = firstScroll ? 1 : currentPage + 1;
|
||||
|
||||
fetchUserList().then(() => {
|
||||
firstScroll = false;
|
||||
done(currentPage >= currentMaxPage);
|
||||
});
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-table
|
||||
v-if="userData && userData.total > 0"
|
||||
flat
|
||||
bordered
|
||||
:grid="modeView"
|
||||
|
|
@ -1102,7 +1164,11 @@ watch(
|
|||
class="text-center"
|
||||
v-if="fieldSelected.includes('orderNumber')"
|
||||
>
|
||||
{{ (currentPage - 1) * pageSize + props.rowIndex + 1 }}
|
||||
{{
|
||||
$q.screen.xs
|
||||
? props.rowIndex + 1
|
||||
: (currentPage - 1) * pageSize + props.rowIndex + 1
|
||||
}}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('name')">
|
||||
|
|
@ -1362,7 +1428,15 @@ watch(
|
|||
</div>
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
<template v-slot:loading>
|
||||
<div
|
||||
v-if="$q.screen.lt.sm && currentPage !== currentMaxPage"
|
||||
class="row justify-center"
|
||||
>
|
||||
<q-spinner-dots color="primary" size="40px" />
|
||||
</div>
|
||||
</template>
|
||||
</q-infinite-scroll>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
|
|
@ -1397,11 +1471,11 @@ watch(
|
|||
>
|
||||
<NoData :not-found="!!inputSearch" />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div
|
||||
<footer
|
||||
class="row justify-between items-center q-px-md q-py-sm"
|
||||
v-if="currentMaxPage > 0"
|
||||
v-if="currentMaxPage > 0 && $q.screen.gt.xs"
|
||||
>
|
||||
<div class="col-4">
|
||||
<div class="row items-center no-wrap">
|
||||
|
|
@ -1414,10 +1488,15 @@ watch(
|
|||
|
||||
<div class="col-4 row justify-center app-text-muted">
|
||||
{{
|
||||
$t('general.recordsPage', {
|
||||
resultcurrentPage: userData?.result.length,
|
||||
total: userData?.total,
|
||||
})
|
||||
$q.screen.gt.sm
|
||||
? $t('general.recordsPage', {
|
||||
resultcurrentPage: userData?.result.length,
|
||||
total: userData?.total,
|
||||
})
|
||||
: $t('general.ofPage', {
|
||||
current: userData?.result.length,
|
||||
total: userData?.total,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div class="col-4 row justify-end">
|
||||
|
|
@ -1427,7 +1506,7 @@ watch(
|
|||
:fetch-data="async () => await fetchUserList()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1521,7 +1600,7 @@ watch(
|
|||
class="rounded row"
|
||||
:class="{
|
||||
'q-py-md q-px-lg': $q.screen.gt.sm,
|
||||
'q-py-sm q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
style="position: absolute; z-index: 999; top: 0; right: 0"
|
||||
>
|
||||
|
|
@ -1618,7 +1697,7 @@ watch(
|
|||
class="col-12 col-md-10 relative-position"
|
||||
:class="{
|
||||
'q-py-md q-pr-md ': $q.screen.gt.sm,
|
||||
'q-py-md q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
id="user-form-content"
|
||||
style="height: 100%; max-height: 100; overflow-y: auto"
|
||||
|
|
@ -1734,6 +1813,7 @@ watch(
|
|||
}"
|
||||
>
|
||||
<ProfileBanner
|
||||
:prefix="formData.firstName"
|
||||
active
|
||||
useToggle
|
||||
color="white"
|
||||
|
|
@ -1789,7 +1869,7 @@ watch(
|
|||
style="position: absolute; z-index: 999; right: 0; top: 0"
|
||||
:class="{
|
||||
'q-py-md q-px-lg': $q.screen.gt.sm,
|
||||
'q-py-sm q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
>
|
||||
<div class="surface-1 row rounded">
|
||||
|
|
@ -1837,7 +1917,7 @@ watch(
|
|||
class="col-md-10 col-12 full-height scroll"
|
||||
:class="{
|
||||
'q-py-md q-pr-md ': $q.screen.gt.sm,
|
||||
'q-py-md q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
>
|
||||
<FormInformation
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { ref, onMounted, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { QTableProps } from 'quasar';
|
||||
import { QSelect, QTableProps } from 'quasar';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { baseUrl } from 'src/stores/utils';
|
||||
|
|
@ -67,6 +67,7 @@ const currentCustomerUrlImage = defineModel<string | null>(
|
|||
|
||||
const { state: employeeFormState } = storeToRefs(employeeFormStore);
|
||||
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
const currentStatus = ref<Status | 'All'>('All');
|
||||
const currentBtnOpen = ref<boolean[]>([]);
|
||||
const totalBranch = ref(0);
|
||||
|
|
@ -334,11 +335,11 @@ watch(
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row items-center justify-end col-12 col-md q-py-sm no-wrap">
|
||||
<div class="row items-center justify-between col-12 col-md q-py-sm no-wrap">
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
class="col-6"
|
||||
class="col-md-6 col"
|
||||
:label="$t('general.search')"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="inputSearch"
|
||||
|
|
@ -347,9 +348,25 @@ watch(
|
|||
<template v-slot:prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
id="select-status"
|
||||
for="select-status"
|
||||
v-model="currentStatus"
|
||||
|
|
@ -657,7 +674,12 @@ watch(
|
|||
totalEmployee: props.row._count?.employee,
|
||||
}"
|
||||
:visible-columns="branchFieldSelected"
|
||||
@view-detail="$emit('viewDetail', props.row, props.rowIndex)"
|
||||
@view-detail="
|
||||
() => {
|
||||
customerBranchFormStore.initForm('info', props.row.id);
|
||||
customerBranchFormState.dialogModal = true;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -676,10 +698,15 @@ watch(
|
|||
|
||||
<div class="col-4 flex justify-center app-text-muted">
|
||||
{{
|
||||
$t('general.recordsPage', {
|
||||
resultcurrentPage: branch?.length,
|
||||
total: totalBranch,
|
||||
})
|
||||
$q.screen.gt.sm
|
||||
? $t('general.recordsPage', {
|
||||
resultcurrentPage: branch?.length,
|
||||
total: totalBranch,
|
||||
})
|
||||
: $t('general.ofPage', {
|
||||
current: branch?.length,
|
||||
total: totalBranch,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div class="col-4 flex justify-end">
|
||||
|
|
@ -868,7 +895,11 @@ watch(
|
|||
v-model:wage-rate="customerBranchFormData.wageRate"
|
||||
v-model:wage-rate-text="customerBranchFormData.wageRateText"
|
||||
/>
|
||||
<div class="row q-col-gutter-sm q-mb-sm" id="employer-branch-address">
|
||||
<div
|
||||
v-if="customerType === 'CORP'"
|
||||
class="row q-col-gutter-sm q-mb-sm"
|
||||
id="employer-branch-address"
|
||||
>
|
||||
<div class="col-12 text-weight-bold text-body1 row items-center">
|
||||
<q-icon
|
||||
flat
|
||||
|
|
@ -882,6 +913,7 @@ watch(
|
|||
</div>
|
||||
</div>
|
||||
<EmployerFormAuthorized
|
||||
v-if="customerType === 'CORP'"
|
||||
class="q-mb-xl"
|
||||
prefix-id="employer-branch"
|
||||
:readonly="customerBranchFormState.dialogType === 'info'"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -167,7 +167,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-6 col-md-5"
|
||||
class="col-12 col-md-5"
|
||||
:label="$t('customer.form.employerName')"
|
||||
for="input-legal-person-no"
|
||||
:model-value="customerName"
|
||||
|
|
@ -224,7 +224,7 @@ watch(
|
|||
<div class="col-12 row q-col-gutter-sm">
|
||||
<DatePicker
|
||||
v-model="registerDate"
|
||||
class="col-6 col-md-2"
|
||||
class="col-12 col-md-2"
|
||||
:id="`${prefixId}-input-register-date`"
|
||||
:label="$t('customer.form.registerDate')"
|
||||
:readonly="readonly"
|
||||
|
|
@ -264,7 +264,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-6 col-md-3"
|
||||
class="col-12 col-md-3"
|
||||
:label="$t('customer.form.headQuarters.telephoneNo')"
|
||||
for="input-telephone-no"
|
||||
:model-value="readonly ? telephoneNo || '-' : telephoneNo"
|
||||
|
|
@ -285,7 +285,7 @@ watch(
|
|||
</template>
|
||||
|
||||
<template v-if="customerType === 'PERS'">
|
||||
<div class="col-7 row q-col-gutter-sm">
|
||||
<div class="col-md-7 col-12 row q-col-gutter-sm">
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
|
|
@ -320,7 +320,7 @@ watch(
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-9 row q-col-gutter-sm">
|
||||
<div class="col-md-9 col-12 row q-col-gutter-sm">
|
||||
<q-select
|
||||
outlined
|
||||
use-input
|
||||
|
|
@ -360,7 +360,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
class="col-md col-12"
|
||||
:label="$t('personnel.form.firstName')"
|
||||
:model-value="firstName"
|
||||
@update:model-value="
|
||||
|
|
@ -384,7 +384,7 @@ watch(
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-9 row q-col-gutter-sm">
|
||||
<div class="col-md-9 col-12 row q-col-gutter-sm">
|
||||
<q-input
|
||||
:for="`${prefixId}-input-prefix-name`"
|
||||
dense
|
||||
|
|
@ -411,7 +411,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
class="col-md col-12"
|
||||
label="Name"
|
||||
:model-value="firstNameEN"
|
||||
@update:model-value="
|
||||
|
|
@ -443,13 +443,13 @@ watch(
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="row col-9 q-col-gutter-sm">
|
||||
<div class="row col-md-9 col-12 q-col-gutter-sm">
|
||||
<q-input
|
||||
:for="`${prefixId}-input-telephone`"
|
||||
dense
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
class="col-md col-6"
|
||||
class="col-md col-12"
|
||||
:label="$t('form.telephone')"
|
||||
:mask="readonly ? '' : '##########'"
|
||||
:model-value="readonly ? telephoneNo || '-' : telephoneNo"
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ const telephoneNo = defineModel<string>('telephoneNo', { default: '' });
|
|||
</div>
|
||||
|
||||
<div class="col-12 row q-col-gutter-sm">
|
||||
<div class="col-12 row q-col-gutter-sm">
|
||||
<div class="col-md-5 col-12">
|
||||
<SelectBranch
|
||||
:for="`${prefixId}-input-source-registered-branch`"
|
||||
class="col-md-6"
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ import {
|
|||
} from 'components/button';
|
||||
import { UploadFileGroup } from 'src/components/upload-file/';
|
||||
import { uploadFileListCustomer, columnsAttachment } from '../../constant';
|
||||
import { symOutlinedResume } from '@quasar/extras/material-symbols-outlined';
|
||||
import { group } from 'console';
|
||||
|
||||
const ocrStore = useOcrStore();
|
||||
const customerStore = useCustomerStore();
|
||||
|
|
@ -114,6 +112,7 @@ withDefaults(
|
|||
class="bordered-b"
|
||||
active-color="primary"
|
||||
no-caps
|
||||
mobile-arrows
|
||||
style="color: hsl(var(--text-mute))"
|
||||
>
|
||||
<q-tab name="main" :label="$t('customerBranch.tab.main')" />
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
|
|||
style="position: absolute; z-index: 999; right: 0; top: 0"
|
||||
:class="{
|
||||
'q-py-md q-px-lg': $q.screen.gt.sm,
|
||||
'q-py-sm q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
>
|
||||
<div class="surface-1 row rounded">
|
||||
|
|
@ -160,7 +160,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
|
|||
class="col-12 col-md-10"
|
||||
:class="{
|
||||
'q-py-md q-pr-md ': $q.screen.gt.sm,
|
||||
'q-py-md q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
style="height: 100%; max-height: 100%; overflow-y: auto"
|
||||
id="flow-form-dialog"
|
||||
|
|
@ -169,6 +169,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
|
|||
v-model:flow-data="flowData"
|
||||
v-model:register-branch-id="registerBranchId"
|
||||
@trigger-properties="triggerPropertiesDialog"
|
||||
@add-step="addStep"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -199,7 +200,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
|
|||
class="rounded row"
|
||||
:class="{
|
||||
'q-py-md q-px-lg': $q.screen.gt.sm,
|
||||
'q-py-sm q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
style="position: absolute; z-index: 999; top: 0; right: 0"
|
||||
>
|
||||
|
|
@ -304,7 +305,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
|
|||
class="col-12 col-md-10"
|
||||
:class="{
|
||||
'q-py-md q-pr-md ': $q.screen.gt.sm,
|
||||
'q-py-md q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
style="height: 100%; max-height: 100%; overflow-y: auto"
|
||||
id="flow-form-drawer"
|
||||
|
|
@ -317,6 +318,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
|
|||
v-model:register-branch-id="registerBranchId"
|
||||
@change-status="$emit('changeStatus')"
|
||||
@trigger-properties="triggerPropertiesDialog"
|
||||
@add-step="addStep"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { QTableProps } from 'quasar';
|
||||
import { QSelect, QTableProps } from 'quasar';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
|
|
@ -68,6 +68,7 @@ const fieldSelectedOption = ref<{ label: string; value: string }[]>([
|
|||
},
|
||||
]);
|
||||
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
const currWorkflowData = ref<WorkflowTemplate>();
|
||||
const formDataWorkflow = ref<WorkflowTemplatePayload>({
|
||||
status: 'CREATED',
|
||||
|
|
@ -126,7 +127,7 @@ async function deleteWorkflow(id?: string) {
|
|||
message: t('dialog.message.confirmDelete'),
|
||||
action: async () => {
|
||||
await workflowStore.deleteWorkflowTemplate(targetId);
|
||||
await fetchWorkflowList();
|
||||
await fetchWorkflowList($q.screen.xs);
|
||||
|
||||
resetForm();
|
||||
},
|
||||
|
|
@ -204,7 +205,7 @@ async function submit() {
|
|||
...formDataWorkflow.value,
|
||||
});
|
||||
}
|
||||
await fetchWorkflowList();
|
||||
await fetchWorkflowList($q.screen.xs);
|
||||
resetForm();
|
||||
}
|
||||
|
||||
|
|
@ -261,25 +262,30 @@ function resetForm() {
|
|||
};
|
||||
}
|
||||
|
||||
async function fetchWorkflowList() {
|
||||
{
|
||||
const res = await workflowStore.getWorkflowTemplateList({
|
||||
page: workflowPage.value,
|
||||
pageSize: workflowPageSize.value,
|
||||
query: pageState.inputSearch,
|
||||
status:
|
||||
statusFilter.value === 'all'
|
||||
? undefined
|
||||
: statusFilter.value === 'statusACTIVE'
|
||||
? 'ACTIVE'
|
||||
: 'INACTIVE',
|
||||
});
|
||||
if (res) {
|
||||
workflowData.value = res.result;
|
||||
workflowPageMax.value = Math.ceil(res.total / workflowPageSize.value);
|
||||
if (pageState.inputSearch || statusFilter.value !== 'all') return;
|
||||
pageState.total = res.total;
|
||||
}
|
||||
async function fetchWorkflowList(mobileFetch?: boolean) {
|
||||
const res = await workflowStore.getWorkflowTemplateList({
|
||||
page: mobileFetch ? 1 : workflowPage.value,
|
||||
pageSize: mobileFetch
|
||||
? workflowData.value.length +
|
||||
(pageState.total === workflowData.value.length ? 1 : 0)
|
||||
: workflowPageSize.value,
|
||||
query: pageState.inputSearch,
|
||||
status:
|
||||
statusFilter.value === 'all'
|
||||
? undefined
|
||||
: statusFilter.value === 'statusACTIVE'
|
||||
? 'ACTIVE'
|
||||
: 'INACTIVE',
|
||||
});
|
||||
if (res) {
|
||||
workflowData.value =
|
||||
$q.screen.xs && !mobileFetch
|
||||
? [...workflowData.value, ...res.result]
|
||||
: res.result;
|
||||
|
||||
workflowPageMax.value = Math.ceil(res.total / workflowPageSize.value);
|
||||
if (pageState.inputSearch || statusFilter.value !== 'all') return;
|
||||
pageState.total = res.total;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -291,8 +297,19 @@ onMounted(async () => {
|
|||
await fetchWorkflowList();
|
||||
});
|
||||
|
||||
watch(statusFilter, fetchWorkflowList);
|
||||
watch([() => pageState.inputSearch, workflowPageSize], fetchWorkflowList);
|
||||
watch(
|
||||
() => statusFilter.value,
|
||||
() => {
|
||||
workflowData.value = [];
|
||||
workflowPage.value = 1;
|
||||
fetchWorkflowList();
|
||||
},
|
||||
);
|
||||
watch([() => pageState.inputSearch, workflowPageSize], () => {
|
||||
workflowData.value = [];
|
||||
workflowPage.value = 1;
|
||||
fetchWorkflowList();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<FloatingActionButton
|
||||
|
|
@ -350,7 +367,7 @@ watch([() => pageState.inputSearch, workflowPageSize], fetchWorkflowList);
|
|||
|
||||
<!-- SEC: header content -->
|
||||
<section class="col surface-1 rounded bordered overflow-hidden">
|
||||
<div class="column full-height">
|
||||
<div class="column full-height no-wrap">
|
||||
<header
|
||||
class="row surface-3 justify-between full-width items-center bordered-b"
|
||||
style="z-index: 1"
|
||||
|
|
@ -361,7 +378,7 @@ watch([() => pageState.inputSearch, workflowPageSize], fetchWorkflowList);
|
|||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="q-mr-md col-12 col-md-3"
|
||||
class="col col-md-3"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="pageState.inputSearch"
|
||||
debounce="200"
|
||||
|
|
@ -369,14 +386,26 @@ watch([() => pageState.inputSearch, workflowPageSize], fetchWorkflowList);
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div
|
||||
class="row col-12 col-md-5 justify-end"
|
||||
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||
style="white-space: nowrap"
|
||||
>
|
||||
<div class="row col-md-5" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -394,6 +423,7 @@ watch([() => pageState.inputSearch, workflowPageSize], fetchWorkflowList);
|
|||
]"
|
||||
/>
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
id="select-field"
|
||||
for="select-field"
|
||||
class="col q-ml-sm"
|
||||
|
|
@ -486,173 +516,118 @@ watch([() => pageState.inputSearch, workflowPageSize], fetchWorkflowList);
|
|||
</article>
|
||||
|
||||
<article v-else class="col q-pa-md surface-2 scroll full-width">
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
:grid="pageState.gridView"
|
||||
:rows="workflowData"
|
||||
:columns="columns"
|
||||
class="full-width"
|
||||
card-container-class="q-col-gutter-md"
|
||||
row-key="name"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-pagination
|
||||
:visible-columns="fieldSelected"
|
||||
<q-infinite-scroll
|
||||
:offset="10"
|
||||
@load="
|
||||
(_, done) => {
|
||||
if ($q.screen.gt.xs || workflowPage === workflowPageMax) return;
|
||||
workflowPage = workflowPage + 1;
|
||||
fetchWorkflowList().then(() =>
|
||||
done(workflowPage >= workflowPageMax),
|
||||
);
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #header="{ cols }">
|
||||
<q-tr style="background-color: hsla(var(--info-bg) / 0.07)">
|
||||
<q-th
|
||||
v-for="v in cols"
|
||||
:key="v"
|
||||
:class="{
|
||||
'text-left': v.name === 'name',
|
||||
'text-right': v.name === 'step',
|
||||
}"
|
||||
>
|
||||
{{
|
||||
v.label &&
|
||||
$t(v.label, {
|
||||
msg:
|
||||
v.name === 'step'
|
||||
? $t('flow.step')
|
||||
: v.name === 'name'
|
||||
? $t('flow.title')
|
||||
: '',
|
||||
})
|
||||
}}
|
||||
</q-th>
|
||||
<q-th auto-width />
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template #body="props">
|
||||
<q-tr
|
||||
:class="{
|
||||
'app-text-muted': props.row.status === 'INACTIVE',
|
||||
'status-active': props.row.status !== 'INACTIVE',
|
||||
'status-inactive': props.row.status === 'INACTIVE',
|
||||
}"
|
||||
:style="
|
||||
props.rowIndex % 2 !== 0
|
||||
? $q.dark.isActive
|
||||
? 'background: hsl(var(--gray-11-hsl)/0.2)'
|
||||
: `background: #f9fafc`
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<q-td
|
||||
v-if="fieldSelected.includes('order')"
|
||||
class="text-center"
|
||||
>
|
||||
{{ props.rowIndex + 1 }}
|
||||
<!-- {{ (currentPage - 1) * pageSize + props.rowIndex + 1 }} -->
|
||||
</q-td>
|
||||
<q-td v-if="fieldSelected.includes('name')">
|
||||
<section class="row items-center no-wrap">
|
||||
<q-avatar
|
||||
class="q-mr-sm"
|
||||
size="md"
|
||||
style="
|
||||
color: var(--gray-6);
|
||||
background: hsla(var(--gray-6-hsl) / 0.1);
|
||||
"
|
||||
>
|
||||
<q-icon name="mdi-cogs" />
|
||||
|
||||
<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>
|
||||
{{ props.row.name }}
|
||||
</section>
|
||||
</q-td>
|
||||
<q-td v-if="fieldSelected.includes('step')" class="text-right">
|
||||
{{ props.row.step.length }}
|
||||
</q-td>
|
||||
<q-td style="width: 20%" class="text-right">
|
||||
<q-btn
|
||||
icon="mdi-eye-outline"
|
||||
size="sm"
|
||||
dense
|
||||
round
|
||||
flat
|
||||
@click.stop="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('view');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<KebabAction
|
||||
:id-name="props.row.id"
|
||||
:status="props.row.status"
|
||||
@view="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('view');
|
||||
}
|
||||
"
|
||||
@edit="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('edit');
|
||||
}
|
||||
"
|
||||
@delete="() => deleteWorkflow(props.row.id)"
|
||||
@change-status="() => triggerChangeStatus(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item="props">
|
||||
<section class="col-12 col-md-4 column">
|
||||
<div
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
:grid="pageState.gridView"
|
||||
:rows="workflowData"
|
||||
:columns="columns"
|
||||
class="full-width"
|
||||
card-container-class="q-col-gutter-md"
|
||||
row-key="name"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-pagination
|
||||
:visible-columns="fieldSelected"
|
||||
>
|
||||
<template #header="{ cols }">
|
||||
<q-tr style="background-color: hsla(var(--info-bg) / 0.07)">
|
||||
<q-th
|
||||
v-for="v in cols"
|
||||
:key="v"
|
||||
:class="{
|
||||
'text-left': v.name === 'name',
|
||||
'text-right': v.name === 'step',
|
||||
}"
|
||||
>
|
||||
{{
|
||||
v.label &&
|
||||
$t(v.label, {
|
||||
msg:
|
||||
v.name === 'step'
|
||||
? $t('flow.step')
|
||||
: v.name === 'name'
|
||||
? $t('flow.title')
|
||||
: '',
|
||||
})
|
||||
}}
|
||||
</q-th>
|
||||
<q-th auto-width />
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template #body="props">
|
||||
<q-tr
|
||||
:class="{
|
||||
'app-text-muted': props.row.status === 'INACTIVE',
|
||||
'status-active': props.row.status !== 'INACTIVE',
|
||||
'status-inactive': props.row.status === 'INACTIVE',
|
||||
}"
|
||||
class="surface-1 rounded bordered row no-wrap col"
|
||||
style="overflow: hidden"
|
||||
:style="
|
||||
props.rowIndex % 2 !== 0
|
||||
? $q.dark.isActive
|
||||
? 'background: hsl(var(--gray-11-hsl)/0.2)'
|
||||
: `background: #f9fafc`
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="col-md-2 col-3 flex items-center justify-center"
|
||||
style="
|
||||
color: var(--gray-6);
|
||||
background: hsla(var(--gray-6-hsl) / 0.1);
|
||||
"
|
||||
<q-td
|
||||
v-if="fieldSelected.includes('order')"
|
||||
class="text-center"
|
||||
>
|
||||
<q-icon name="mdi-cogs" size="md" />
|
||||
</div>
|
||||
<article class="row q-pa-sm q-gutter-y-sm col">
|
||||
<div
|
||||
v-if="fieldSelected.includes('name')"
|
||||
class="text-weight-bold col-12 ellipsis-2-lines"
|
||||
:class="{
|
||||
'app-text-muted': props.row.status === 'INACTIVE',
|
||||
}"
|
||||
>
|
||||
{{ props.row.name }}
|
||||
{{
|
||||
$q.screen.xs
|
||||
? props.rowIndex + 1
|
||||
: (workflowPage - 1) * workflowPageSize +
|
||||
props.rowIndex +
|
||||
1
|
||||
}}
|
||||
</q-td>
|
||||
<q-td v-if="fieldSelected.includes('name')">
|
||||
<section class="row items-center no-wrap">
|
||||
<q-avatar
|
||||
class="q-mr-sm"
|
||||
size="md"
|
||||
style="
|
||||
color: var(--gray-6);
|
||||
background: hsla(var(--gray-6-hsl) / 0.1);
|
||||
"
|
||||
>
|
||||
<q-icon name="mdi-cogs" />
|
||||
|
||||
<q-tooltip>
|
||||
{{ props.row.name }}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<div v-if="fieldSelected.includes('step')" class="self-end">
|
||||
<div class="bordered rounded q-px-sm">
|
||||
<q-icon name="mdi-note-edit-outline" class="q-pr-sm" />
|
||||
{{ props.row.step.length }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<nav class="q-pa-sm row items-center self-start">
|
||||
<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>
|
||||
{{ props.row.name }}
|
||||
</section>
|
||||
</q-td>
|
||||
<q-td
|
||||
v-if="fieldSelected.includes('step')"
|
||||
class="text-right"
|
||||
>
|
||||
{{ props.row.step.length }}
|
||||
</q-td>
|
||||
<q-td style="width: 20%" class="text-right">
|
||||
<q-btn
|
||||
icon="mdi-eye-outline"
|
||||
size="sm"
|
||||
|
|
@ -684,17 +659,108 @@ watch([() => pageState.inputSearch, workflowPageSize], fetchWorkflowList);
|
|||
@delete="() => deleteWorkflow(props.row.id)"
|
||||
@change-status="() => triggerChangeStatus(props.row)"
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item="props">
|
||||
<section class="col-12 col-md-4 column">
|
||||
<div
|
||||
:class="{
|
||||
'status-inactive': props.row.status === 'INACTIVE',
|
||||
}"
|
||||
class="surface-1 rounded bordered row no-wrap col"
|
||||
style="overflow: hidden"
|
||||
>
|
||||
<div
|
||||
class="col-md-2 col-3 flex items-center justify-center"
|
||||
style="
|
||||
color: var(--gray-6);
|
||||
background: hsla(var(--gray-6-hsl) / 0.1);
|
||||
"
|
||||
>
|
||||
<q-icon name="mdi-cogs" size="md" />
|
||||
</div>
|
||||
<article class="row q-pa-sm q-gutter-y-sm col">
|
||||
<div
|
||||
v-if="fieldSelected.includes('name')"
|
||||
class="text-weight-bold col-12 ellipsis-2-lines"
|
||||
:class="{
|
||||
'app-text-muted': props.row.status === 'INACTIVE',
|
||||
}"
|
||||
>
|
||||
{{ props.row.name }}
|
||||
|
||||
<q-tooltip>
|
||||
{{ props.row.name }}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<div
|
||||
v-if="fieldSelected.includes('step')"
|
||||
class="self-end"
|
||||
>
|
||||
<div class="bordered rounded q-px-sm">
|
||||
<q-icon
|
||||
name="mdi-note-edit-outline"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
{{ props.row.step.length }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<nav class="q-pa-sm row items-center self-start">
|
||||
<q-btn
|
||||
icon="mdi-eye-outline"
|
||||
size="sm"
|
||||
dense
|
||||
round
|
||||
flat
|
||||
@click.stop="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('view');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<KebabAction
|
||||
:id-name="props.row.id"
|
||||
:status="props.row.status"
|
||||
@view="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('view');
|
||||
}
|
||||
"
|
||||
@edit="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('edit');
|
||||
}
|
||||
"
|
||||
@delete="() => deleteWorkflow(props.row.id)"
|
||||
@change-status="() => triggerChangeStatus(props.row)"
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<template v-slot:loading>
|
||||
<div
|
||||
v-if="$q.screen.lt.sm && workflowPage !== workflowPageMax"
|
||||
class="row justify-center"
|
||||
>
|
||||
<q-spinner-dots color="primary" size="40px" />
|
||||
</div>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-infinite-scroll>
|
||||
</article>
|
||||
|
||||
<!-- SEC: footer content -->
|
||||
<footer
|
||||
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
||||
v-if="workflowPageMax > 0"
|
||||
v-if="workflowPageMax > 0 && $q.screen.gt.xs"
|
||||
>
|
||||
<div class="col-4">
|
||||
<div class="row items-center no-wrap">
|
||||
|
|
@ -706,19 +772,26 @@ watch([() => pageState.inputSearch, workflowPageSize], fetchWorkflowList);
|
|||
</div>
|
||||
<div class="col-4 row justify-center app-text-muted">
|
||||
{{
|
||||
$t('general.recordsPage', {
|
||||
resultcurrentPage: workflowData.length,
|
||||
total: pageState.inputSearch
|
||||
? workflowData.length
|
||||
: pageState.total,
|
||||
})
|
||||
$q.screen.gt.sm
|
||||
? $t('general.recordsPage', {
|
||||
resultcurrentPage: workflowData.length,
|
||||
total: pageState.inputSearch
|
||||
? workflowData.length
|
||||
: pageState.total,
|
||||
})
|
||||
: $t('general.ofPage', {
|
||||
current: workflowData.length,
|
||||
total: pageState.inputSearch
|
||||
? workflowData.length
|
||||
: pageState.total,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<nav class="col-4 row justify-end">
|
||||
<PaginationComponent
|
||||
v-model:current-page="workflowPage"
|
||||
v-model:max-page="workflowPageMax"
|
||||
:fetch-data="fetchWorkflowList"
|
||||
:fetch-data="() => fetchWorkflowList()"
|
||||
/>
|
||||
</nav>
|
||||
</footer>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { pageTabs, columnQuotation } from './constants';
|
||||
|
||||
import { onMounted, reactive, ref, watch, computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
// NOTE: Import stores
|
||||
import { useQuotationStore } from 'src/stores/quotations';
|
||||
|
|
@ -12,6 +11,7 @@ import useFlowStore from 'src/stores/flow';
|
|||
import useMyBranch from 'stores/my-branch';
|
||||
import { useQuotationForm } from './form';
|
||||
import { hslaColors } from './constants';
|
||||
import { pageTabs, columnQuotation } from './constants';
|
||||
|
||||
// NOTE Import Types
|
||||
import { CustomerBranchCreate, CustomerType } from 'stores/customer/types';
|
||||
|
|
@ -44,6 +44,7 @@ import { Quotation } from 'src/stores/quotations/types';
|
|||
import TableQuotation from 'src/components/05_quotation/TableQuotation.vue';
|
||||
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||
|
||||
const $q = useQuasar();
|
||||
const quotationFormStore = useQuotationForm();
|
||||
const customerFormStore = useCustomerForm();
|
||||
const flowStore = useFlowStore();
|
||||
|
|
@ -240,6 +241,7 @@ const {
|
|||
} = storeToRefs(quotationStore);
|
||||
|
||||
onMounted(async () => {
|
||||
pageState.gridView = $q.screen.lt.md ? true : false;
|
||||
navigatorStore.current.title = 'quotation.title';
|
||||
navigatorStore.current.path = [
|
||||
{
|
||||
|
|
@ -276,10 +278,10 @@ onMounted(async () => {
|
|||
flowStore.rotate();
|
||||
});
|
||||
|
||||
async function fetchQuotationList() {
|
||||
async function fetchQuotationList(mobileFetch?: boolean) {
|
||||
{
|
||||
const ret = await quotationStore.getQuotationList({
|
||||
page: quotationPage.value,
|
||||
page: mobileFetch ? 1 : quotationPage.value,
|
||||
pageSize: quotationPageSize.value,
|
||||
status:
|
||||
pageState.currentTab !== 'Issued'
|
||||
|
|
@ -299,7 +301,10 @@ async function fetchQuotationList() {
|
|||
});
|
||||
|
||||
if (ret) {
|
||||
quotationData.value = ret.result;
|
||||
quotationData.value =
|
||||
$q.screen.xs && !mobileFetch
|
||||
? [...quotationData.value, ...ret.result]
|
||||
: ret.result;
|
||||
quotationPageMax.value = Math.ceil(ret.total / quotationPageSize.value);
|
||||
pageState.total = ret.total;
|
||||
}
|
||||
|
|
@ -317,7 +322,11 @@ async function fetchQuotationList() {
|
|||
|
||||
watch(
|
||||
[() => pageState.currentTab, () => pageState.inputSearch, quotationPageSize],
|
||||
fetchQuotationList,
|
||||
() => {
|
||||
quotationPage.value = 1;
|
||||
quotationData.value = [];
|
||||
fetchQuotationList();
|
||||
},
|
||||
);
|
||||
|
||||
async function storeDataLocal(id: string) {
|
||||
|
|
@ -381,19 +390,11 @@ async function storeDataLocal(id: string) {
|
|||
"
|
||||
>
|
||||
{{
|
||||
pageState.currentTab === 'Issued'
|
||||
? quotationStats.issued
|
||||
: pageState.currentTab === 'Accepted'
|
||||
? quotationStats.accepted
|
||||
: pageState.currentTab === 'Expired'
|
||||
? quotationStats.expired
|
||||
: pageState.currentTab === 'Invoice'
|
||||
? quotationStats.paymentInProcess
|
||||
: pageState.currentTab === 'PaymentSuccess'
|
||||
? quotationStats.paymentSuccess
|
||||
: pageState.currentTab === 'ProcessComplete'
|
||||
? quotationStats.processComplete
|
||||
: 0
|
||||
quotationStats[
|
||||
pageState.currentTab === 'Invoice'
|
||||
? 'paymentInProcess'
|
||||
: (pageState.currentTab.toLowerCase() as keyof typeof quotationStats)
|
||||
]
|
||||
}}
|
||||
</q-badge>
|
||||
<q-btn
|
||||
|
|
@ -476,7 +477,7 @@ async function storeDataLocal(id: string) {
|
|||
<header class="col surface-1 rounded bordered overflow-hidden">
|
||||
<div class="column full-height">
|
||||
<section
|
||||
class="row surface-3 justify-between full-width items-center bordered-b"
|
||||
class="row surface-3 justify-between full-width bordered-b"
|
||||
style="z-index: 1"
|
||||
>
|
||||
<div class="row q-py-sm q-px-md justify-between full-width">
|
||||
|
|
@ -485,7 +486,7 @@ async function storeDataLocal(id: string) {
|
|||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="q-mr-md col-12 col-md-3"
|
||||
class="col col-md-3"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="pageState.inputSearch"
|
||||
debounce="200"
|
||||
|
|
@ -495,16 +496,12 @@ async function storeDataLocal(id: string) {
|
|||
</template>
|
||||
</q-input>
|
||||
|
||||
<div
|
||||
class="row col-12 col-md-3 justify-end"
|
||||
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||
style="white-space: nowrap"
|
||||
>
|
||||
<div class="row col-md-5 justify-end" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-if="!pageState.gridView"
|
||||
id="select-field"
|
||||
for="select-field"
|
||||
class="col q-ml-sm"
|
||||
class="col-md-5 q-ml-sm"
|
||||
:options="
|
||||
fieldSelectedOption.map((v) => ({
|
||||
...v,
|
||||
|
|
@ -627,105 +624,134 @@ async function storeDataLocal(id: string) {
|
|||
</article>
|
||||
<article v-else class="col surface-2 full-width scroll">
|
||||
<div class="q-pa-md">
|
||||
<TableQuotation
|
||||
:columns="columnQuotation"
|
||||
:rows="quotationData"
|
||||
:visible-columns="pageState.fieldSelected"
|
||||
:grid="pageState.gridView"
|
||||
:hide-edit="pageState.currentTab !== 'Issued'"
|
||||
@preview="(id: any) => storeDataLocal(id)"
|
||||
@view="
|
||||
(item) => {
|
||||
triggerQuotationDialog({
|
||||
statusDialog: 'info',
|
||||
quotationId: item.id,
|
||||
branchId: item.customerBranch.customer.registeredBranchId,
|
||||
<q-infinite-scroll
|
||||
:key="pageState.currentTab"
|
||||
:offset="100"
|
||||
@load="
|
||||
(_, done) => {
|
||||
if ($q.screen.gt.xs) return;
|
||||
quotationPage = quotationPage + 1;
|
||||
|
||||
fetchQuotationList().then(() => {
|
||||
done(quotationPage >= quotationPageMax);
|
||||
});
|
||||
}
|
||||
"
|
||||
@edit="
|
||||
(item) =>
|
||||
triggerQuotationDialog({
|
||||
statusDialog: 'edit',
|
||||
quotationId: item.id,
|
||||
branchId: item.customerBranch.customer.registeredBranchId,
|
||||
})
|
||||
"
|
||||
@delete="(id) => triggerDialogDeleteQuottaion(id)"
|
||||
>
|
||||
<template #grid="{ item }">
|
||||
<div class="col-md-4 col-sm-6 col-12">
|
||||
<QuotationCard
|
||||
hide-kebab-delete
|
||||
:hide-kebab-edit="!(pageState.currentTab === 'Issued')"
|
||||
:urgent="item.row.urgent"
|
||||
:code="item.row.code"
|
||||
:title="item.row.workName"
|
||||
:created-at="
|
||||
new Date(item.row.createdAt).toLocaleString('th-TH', {
|
||||
hour12: false,
|
||||
})
|
||||
"
|
||||
:valid-until="
|
||||
(() => {
|
||||
const date = new Date(item.row.dueDate);
|
||||
date.setHours(23, 59, 59, 0);
|
||||
return date.toLocaleString('th-TH', {
|
||||
<TableQuotation
|
||||
:page="quotationPage"
|
||||
:page-size="quotationPageSize"
|
||||
:columns="columnQuotation"
|
||||
:rows="quotationData"
|
||||
:visible-columns="pageState.fieldSelected"
|
||||
:grid="pageState.gridView"
|
||||
:hide-edit="pageState.currentTab !== 'Issued'"
|
||||
@preview="(id: any) => storeDataLocal(id)"
|
||||
@view="
|
||||
(item) => {
|
||||
triggerQuotationDialog({
|
||||
statusDialog: 'info',
|
||||
quotationId: item.id,
|
||||
branchId: item.customerBranch.customer.registeredBranchId,
|
||||
});
|
||||
}
|
||||
"
|
||||
@edit="
|
||||
(item) =>
|
||||
triggerQuotationDialog({
|
||||
statusDialog: 'edit',
|
||||
quotationId: item.id,
|
||||
branchId: item.customerBranch.customer.registeredBranchId,
|
||||
})
|
||||
"
|
||||
@delete="(id) => triggerDialogDeleteQuottaion(id)"
|
||||
>
|
||||
<template #grid="{ item }">
|
||||
<div class="col-md-4 col-sm-6 col-12 column">
|
||||
<QuotationCard
|
||||
class="col"
|
||||
hide-kebab-delete
|
||||
:hide-kebab-edit="!(pageState.currentTab === 'Issued')"
|
||||
:urgent="item.row.urgent"
|
||||
:code="item.row.code"
|
||||
:title="item.row.workName"
|
||||
:created-at="
|
||||
new Date(item.row.createdAt).toLocaleString('th-TH', {
|
||||
hour12: false,
|
||||
});
|
||||
})()
|
||||
"
|
||||
:status="$t(`quotation.status.${item.row.quotationStatus}`)"
|
||||
:worker-count="item.row._count.worker"
|
||||
:worker-max="item.row.workerMax || item.row._count.worker"
|
||||
:customer-name="
|
||||
item.row.customerBranch.registerName ||
|
||||
`${item.row.customerBranch.firstName || '-'} ${item.row.customerBranch.lastName || ''}`
|
||||
"
|
||||
:reporter="
|
||||
$i18n.locale === 'eng'
|
||||
? item.row.createdBy.firstNameEN +
|
||||
' ' +
|
||||
item.row.createdBy.lastNameEN
|
||||
: item.row.createdBy.firstName +
|
||||
' ' +
|
||||
item.row.createdBy.lastName
|
||||
"
|
||||
:total-price="item.row.finalPrice"
|
||||
:badge-color="hslaColors[item.row.quotationStatus] || ''"
|
||||
@preview="storeDataLocal(item.row.id)"
|
||||
@view="
|
||||
() => {
|
||||
})
|
||||
"
|
||||
:valid-until="
|
||||
(() => {
|
||||
const date = new Date(item.row.dueDate);
|
||||
date.setHours(23, 59, 59, 0);
|
||||
return date.toLocaleString('th-TH', {
|
||||
hour12: false,
|
||||
});
|
||||
})()
|
||||
"
|
||||
:status="
|
||||
$t(`quotation.status.${item.row.quotationStatus}`)
|
||||
"
|
||||
:worker-count="item.row._count.worker"
|
||||
:worker-max="item.row.workerMax || item.row._count.worker"
|
||||
:customer-name="
|
||||
item.row.customerBranch.registerName ||
|
||||
`${item.row.customerBranch.firstName || '-'} ${item.row.customerBranch.lastName || ''}`
|
||||
"
|
||||
:reporter="
|
||||
$i18n.locale === 'eng'
|
||||
? item.row.createdBy.firstNameEN +
|
||||
' ' +
|
||||
item.row.createdBy.lastNameEN
|
||||
: item.row.createdBy.firstName +
|
||||
' ' +
|
||||
item.row.createdBy.lastName
|
||||
"
|
||||
:total-price="item.row.finalPrice"
|
||||
:badge-color="hslaColors[item.row.quotationStatus] || ''"
|
||||
@preview="storeDataLocal(item.row.id)"
|
||||
@view="
|
||||
() => {
|
||||
triggerQuotationDialog({
|
||||
statusDialog: 'info',
|
||||
quotationId: item.row.id,
|
||||
branchId:
|
||||
item.row.customerBranch.customer
|
||||
.registeredBranchId,
|
||||
});
|
||||
}
|
||||
"
|
||||
@edit="
|
||||
triggerQuotationDialog({
|
||||
statusDialog: 'info',
|
||||
statusDialog: 'edit',
|
||||
quotationId: item.row.id,
|
||||
branchId:
|
||||
item.row.customerBranch.customer.registeredBranchId,
|
||||
});
|
||||
}
|
||||
"
|
||||
@edit="
|
||||
triggerQuotationDialog({
|
||||
statusDialog: 'edit',
|
||||
quotationId: item.row.id,
|
||||
branchId:
|
||||
item.row.customerBranch.customer.registeredBranchId,
|
||||
})
|
||||
"
|
||||
@link="triggerReceiptDialog(item.row)"
|
||||
@upload="console.log('upload')"
|
||||
@delete="triggerDialogDeleteQuottaion(item.row.id)"
|
||||
/>
|
||||
})
|
||||
"
|
||||
@link="triggerReceiptDialog(item.row)"
|
||||
@upload="console.log('upload')"
|
||||
@delete="triggerDialogDeleteQuottaion(item.row.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</TableQuotation>
|
||||
<template v-slot:loading>
|
||||
<div
|
||||
v-if="$q.screen.lt.sm && quotationPage !== quotationPageMax"
|
||||
class="row justify-center"
|
||||
>
|
||||
<q-spinner-dots color="primary" size="40px" />
|
||||
</div>
|
||||
</template>
|
||||
</TableQuotation>
|
||||
</q-infinite-scroll>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- SEC: footer content -->
|
||||
<footer
|
||||
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
||||
v-if="quotationPageMax > 0"
|
||||
v-if="quotationPageMax > 0 && $q.screen.gt.xs"
|
||||
>
|
||||
<div class="col-4">
|
||||
<div class="row items-center no-wrap">
|
||||
|
|
@ -737,19 +763,26 @@ async function storeDataLocal(id: string) {
|
|||
</div>
|
||||
<div class="col-4 row justify-center app-text-muted">
|
||||
{{
|
||||
$t('general.recordsPage', {
|
||||
resultcurrentPage: quotationData.length,
|
||||
total: pageState.inputSearch
|
||||
? quotationData.length
|
||||
: pageState.total,
|
||||
})
|
||||
$q.screen.gt.sm
|
||||
? $t('general.recordsPage', {
|
||||
resultcurrentPage: quotationData.length,
|
||||
total: pageState.inputSearch
|
||||
? quotationData.length
|
||||
: pageState.total,
|
||||
})
|
||||
: $t('general.ofPage', {
|
||||
current: quotationData.length,
|
||||
total: pageState.inputSearch
|
||||
? quotationData.length
|
||||
: pageState.total,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<nav class="col-4 row justify-end">
|
||||
<PaginationComponent
|
||||
v-model:current-page="quotationPage"
|
||||
v-model:max-page="quotationPageMax"
|
||||
:fetch-data="fetchQuotationList"
|
||||
:fetch-data="() => fetchQuotationList()"
|
||||
/>
|
||||
</nav>
|
||||
</footer>
|
||||
|
|
@ -788,7 +821,12 @@ async function storeDataLocal(id: string) {
|
|||
}
|
||||
"
|
||||
>
|
||||
<header class="q-mx-lg q-mt-lg">
|
||||
<header
|
||||
:class="{
|
||||
'q-mx-lg q-mt-md': $q.screen.gt.sm,
|
||||
'q-mx-md q-mt-sm': $q.screen.lt.md,
|
||||
}"
|
||||
>
|
||||
<ProfileBanner
|
||||
prefix="dialog"
|
||||
img="/images/quotation-bg-avatar.png"
|
||||
|
|
@ -801,9 +839,19 @@ async function storeDataLocal(id: string) {
|
|||
hideFade
|
||||
/>
|
||||
</header>
|
||||
<section class="col surface-1 q-ma-lg rounded bordered row scroll">
|
||||
<section
|
||||
class="col surface-1 rounded bordered row scroll"
|
||||
:class="{
|
||||
'q-mx-lg q-my-md': $q.screen.gt.sm,
|
||||
'q-mx-md q-my-sm': $q.screen.lt.md,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="col-12 q-px-md q-py-lg"
|
||||
class="col-12"
|
||||
:class="{
|
||||
'q-px-md q-py-lg': $q.screen.gt.sm,
|
||||
'q-pa-sm': $q.screen.lt.md,
|
||||
}"
|
||||
id="customer-form-content"
|
||||
style="height: 100%; max-height: 100%; overflow-y: auto"
|
||||
>
|
||||
|
|
@ -839,7 +887,7 @@ async function storeDataLocal(id: string) {
|
|||
>
|
||||
<div class="full-height row q-pa-md">
|
||||
<ItemCard
|
||||
class="col q-mx-sm full-height"
|
||||
class="col q-mx-sm full-height cursor-pointer"
|
||||
v-for="value in dialogCreateCustomerItem"
|
||||
:key="value.text"
|
||||
:icon="value.icon"
|
||||
|
|
@ -850,7 +898,9 @@ async function storeDataLocal(id: string) {
|
|||
() => {
|
||||
triggerCreateCustomerd({
|
||||
type:
|
||||
value.text === 'customer.employerLegalEntity' ? 'CORP' : 'PERS',
|
||||
value.text === 'customer.employerLegalEntity'
|
||||
? CustomerType.Corporate
|
||||
: CustomerType.Person,
|
||||
});
|
||||
emptyCreateDialog = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,16 @@ import { dateFormatJS } from 'src/utils/datetime';
|
|||
import { QFile, QMenu } from 'quasar';
|
||||
import UploadFileCard from 'src/components/upload-file/UploadFileCard.vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { DebitNote } from 'src/stores/debit-note';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const quotationPayment = useQuotationPayment();
|
||||
const { data: config } = storeToRefs(configStore);
|
||||
|
||||
const prop = defineProps<{ data?: Quotation | QuotationFull }>();
|
||||
const prop = defineProps<{
|
||||
data?: Quotation | QuotationFull | DebitNote;
|
||||
isDebitNote?: boolean;
|
||||
}>();
|
||||
|
||||
const refQFile = ref<InstanceType<typeof QFile>[]>([]);
|
||||
const refQMenu = ref<InstanceType<typeof QMenu>[]>([]);
|
||||
|
|
@ -194,7 +198,12 @@ async function triggerSubmit() {
|
|||
|
||||
onMounted(async () => {
|
||||
if (!prop.data) return;
|
||||
const ret = await quotationPayment.getQuotationPayment(prop.data.id);
|
||||
const ret = await quotationPayment.getQuotationPayment({
|
||||
quotationId: prop.isDebitNote === true ? undefined : prop.data.id,
|
||||
debitNoteId: prop.isDebitNote === true ? prop.data.id : undefined,
|
||||
quotationOnly: !!prop.isDebitNote ? false : true,
|
||||
debitNoteOnly: !!prop.isDebitNote ? true : false,
|
||||
});
|
||||
if (ret) {
|
||||
paymentData.value = ret.result;
|
||||
slipFile.value = paymentData.value.map((v) => ({
|
||||
|
|
@ -362,10 +371,11 @@ onMounted(async () => {
|
|||
"
|
||||
class="row items-center q-pb-sm"
|
||||
>
|
||||
<span class="app-text-muted-2">
|
||||
<span class="app-text-muted-2 col-12 col-md">
|
||||
{{ $t('quotation.paySplitCount') }}
|
||||
</span>
|
||||
<span class="q-ml-auto">
|
||||
|
||||
<span>
|
||||
{{ $t('quotation.receiptDialog.total') }}
|
||||
</span>
|
||||
<span class="bordered rounded surface-2 number-box q-mx-sm">
|
||||
|
|
@ -406,49 +416,58 @@ onMounted(async () => {
|
|||
<!-- summary total, paid, remain -->
|
||||
<div class="row items-center">
|
||||
<span
|
||||
class="row col rounded q-px-sm q-py-md"
|
||||
class="row col rounded q-px-sm q-py-md justify-end"
|
||||
style="border: 1px solid hsl(var(--info-bg))"
|
||||
>
|
||||
{{ $t('quotation.receiptDialog.totalAmount') }}
|
||||
<span class="q-ml-auto">
|
||||
{{ formatNumberDecimal(data.finalPrice, 2) }}
|
||||
<span
|
||||
class="col-sm col-12"
|
||||
:class="{ 'text-right': $q.screen.lt.sm }"
|
||||
>
|
||||
{{ $t('quotation.receiptDialog.totalAmount') }}
|
||||
</span>
|
||||
{{ formatNumberDecimal(data.finalPrice, 2) }}
|
||||
</span>
|
||||
<span
|
||||
class="row col rounded q-px-sm q-py-md q-mx-md"
|
||||
class="row col rounded q-px-sm q-py-md q-mx-md justify-end"
|
||||
style="border: 1px solid hsl(var(--positive-bg))"
|
||||
>
|
||||
{{ $t('quotation.receiptDialog.paid') }}
|
||||
<span class="q-ml-auto">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
paymentData.reduce(
|
||||
(c, i) =>
|
||||
i.paymentStatus === 'PaymentSuccess' ? c + i.amount : c,
|
||||
0,
|
||||
),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
<span
|
||||
class="col-sm col-12"
|
||||
:class="{ 'text-right': $q.screen.lt.sm }"
|
||||
>
|
||||
{{ $t('quotation.receiptDialog.paid') }}
|
||||
</span>
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
paymentData.reduce(
|
||||
(c, i) =>
|
||||
i.paymentStatus === 'PaymentSuccess' ? c + i.amount : c,
|
||||
0,
|
||||
),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="row col rounded q-px-sm q-py-md"
|
||||
class="row col rounded q-px-sm q-py-md justify-end"
|
||||
style="border: 1px solid hsl(var(--warning-bg))"
|
||||
>
|
||||
{{ $t('quotation.receiptDialog.remain') }}
|
||||
<span class="q-ml-auto">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
paymentData.reduce(
|
||||
(c, i) =>
|
||||
i.paymentStatus !== 'PaymentSuccess' ? c + i.amount : c,
|
||||
0,
|
||||
),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
<span
|
||||
class="col-sm col-12"
|
||||
:class="{ 'text-right': $q.screen.lt.sm }"
|
||||
>
|
||||
{{ $t('quotation.receiptDialog.remain') }}
|
||||
</span>
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
paymentData.reduce(
|
||||
(c, i) =>
|
||||
i.paymentStatus !== 'PaymentSuccess' ? c + i.amount : c,
|
||||
0,
|
||||
),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { QSelect, useQuasar } from 'quasar';
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import {
|
||||
baseUrl,
|
||||
|
|
@ -13,28 +13,26 @@ import { ProductTree, quotationProductTree } from './utils';
|
|||
|
||||
// NOTE: Import stores
|
||||
import { dateFormat, calculateAge, dateFormatJS } from 'src/utils/datetime';
|
||||
import { useEmployeeForm } from 'src/pages/03_customer-management/form';
|
||||
import { useQuotationStore } from 'src/stores/quotations';
|
||||
import useProductServiceStore from 'stores/product-service';
|
||||
import { waitAll, calculateDaysUntilExpire, dialog } from 'src/stores/utils';
|
||||
import { calculateDaysUntilExpire, dialog } from 'src/stores/utils';
|
||||
import useEmployeeStore from 'stores/employee';
|
||||
import { useInvoice, useReceipt, usePayment } from 'stores/payment';
|
||||
import useCustomerStore from 'stores/customer';
|
||||
import useOptionStore from 'stores/options';
|
||||
import { useQuotationForm } from './form';
|
||||
import { useQuotationForm, DEFAULT_DATA } from './form';
|
||||
import { deleteItem } from 'stores/utils';
|
||||
|
||||
// NOTE Import Types
|
||||
import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
|
||||
import { View } from './types.ts';
|
||||
import {
|
||||
EmployeeWorker,
|
||||
PayCondition,
|
||||
ProductRelation,
|
||||
ProductServiceList,
|
||||
QuotationPayload,
|
||||
} from 'src/stores/quotations/types';
|
||||
import { Employee, EmployeeWork } from 'src/stores/employee/types';
|
||||
import { Employee } from 'src/stores/employee/types';
|
||||
import { Receipt } from 'src/stores/payment/types';
|
||||
import {
|
||||
ProductGroup,
|
||||
|
|
@ -50,7 +48,6 @@ import ProductItem from 'components/05_quotation/ProductItem.vue';
|
|||
import WorkerItem from 'components/05_quotation/WorkerItem.vue';
|
||||
import ToggleButton from 'components/button/ToggleButton.vue';
|
||||
import FormAbout from 'components/05_quotation/FormAbout.vue';
|
||||
import SelectZone from 'components/shared/SelectZone.vue';
|
||||
import ImportWorker from './ImportWorker.vue';
|
||||
import {
|
||||
AddButton,
|
||||
|
|
@ -127,7 +124,7 @@ const {
|
|||
const { data: config } = storeToRefs(configStore);
|
||||
|
||||
const receiptList = ref<Receipt[]>([]);
|
||||
|
||||
const refStatusFilter = ref<InstanceType<typeof QSelect>>();
|
||||
const templateForm = ref<string>('');
|
||||
const templateFormOption = ref<{ label: string; value: string }[]>([]);
|
||||
|
||||
|
|
@ -254,10 +251,13 @@ function getPrice(
|
|||
const vat =
|
||||
(finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07);
|
||||
|
||||
const calcVat =
|
||||
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
|
||||
|
||||
a.totalPrice = precisionRound(a.totalPrice + price);
|
||||
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
|
||||
a.vat = c.product.calcVat ? precisionRound(a.vat + vat) : a.vat;
|
||||
a.vatExcluded = c.product.calcVat
|
||||
a.vat = calcVat ? precisionRound(a.vat + vat) : a.vat;
|
||||
a.vatExcluded = calcVat
|
||||
? a.vatExcluded
|
||||
: precisionRound(a.vatExcluded + price);
|
||||
a.finalPrice = precisionRound(
|
||||
|
|
@ -427,6 +427,7 @@ async function fetchQuotation() {
|
|||
const id = currentQuotationId.value || quotationFormData.value.id || '';
|
||||
|
||||
await quotationForm.assignFormData(id, quotationFormState.value.mode);
|
||||
|
||||
tempPaySplitCount.value = quotationFormData.value.paySplitCount || 0;
|
||||
tempPaySplit.value = JSON.parse(
|
||||
JSON.stringify(quotationFormData.value.paySplit),
|
||||
|
|
@ -443,6 +444,8 @@ async function fetchQuotation() {
|
|||
async function fetchReceipt() {
|
||||
const res = await useReceiptStore.getReceiptList({
|
||||
quotationId: quotationFormData.value.id,
|
||||
quotationOnly: true,
|
||||
debitNoteOnly: false,
|
||||
});
|
||||
|
||||
if (res) {
|
||||
|
|
@ -607,6 +610,7 @@ async function convertDataToFormSubmit() {
|
|||
status: quotationFormData.value.status,
|
||||
discount: quotationFormData.value.discount,
|
||||
remark: quotationFormData.value.remark || '',
|
||||
agentPrice: agentPrice.value,
|
||||
};
|
||||
|
||||
newWorkerList.value = [];
|
||||
|
|
@ -819,6 +823,7 @@ function convertToTable(nodes: Node[]) {
|
|||
return [];
|
||||
};
|
||||
const list = nodes.flatMap(_recursive).map((v) => v.value);
|
||||
if (list.length === 0) return;
|
||||
|
||||
quotationFormData.value.paySplitCount = Math.max(
|
||||
...list.map((v) => v.installmentNo || 0),
|
||||
|
|
@ -856,7 +861,7 @@ function convertToTable(nodes: Node[]) {
|
|||
} else {
|
||||
quotationFormData.value.paySplit = [];
|
||||
quotationFormData.value.paySplitCount = 0;
|
||||
quotationFormData.value.payCondition = 'Full';
|
||||
quotationFormData.value.payCondition = PayCondition.Full;
|
||||
}
|
||||
|
||||
tempPaySplit.value = JSON.parse(
|
||||
|
|
@ -1070,25 +1075,25 @@ watch(
|
|||
},
|
||||
);
|
||||
|
||||
async function searchEmployee(text: string) {
|
||||
let query: string | undefined = text;
|
||||
let pageSize = 50;
|
||||
// async function searchEmployee(text: string) {
|
||||
// let query: string | undefined = text;
|
||||
// let pageSize = 50;
|
||||
|
||||
if (!text) {
|
||||
query = undefined;
|
||||
pageSize = 9999;
|
||||
}
|
||||
// if (!text) {
|
||||
// query = undefined;
|
||||
// pageSize = 9999;
|
||||
// }
|
||||
|
||||
const retEmp = await customerStore.fetchBranchEmployee(
|
||||
quotationFormData.value.customerBranchId,
|
||||
{
|
||||
query: query,
|
||||
pageSize: pageSize,
|
||||
passport: true,
|
||||
},
|
||||
);
|
||||
if (retEmp) workerList.value = retEmp.data.result;
|
||||
}
|
||||
// const retEmp = await customerStore.fetchBranchEmployee(
|
||||
// quotationFormData.value.customerBranchId,
|
||||
// {
|
||||
// query: query,
|
||||
// pageSize: pageSize,
|
||||
// passport: true,
|
||||
// },
|
||||
// );
|
||||
// if (retEmp) workerList.value = retEmp.data.result;
|
||||
// }
|
||||
|
||||
function storeDataLocal() {
|
||||
quotationFormData.value.productServiceList = productServiceList.value;
|
||||
|
|
@ -1120,6 +1125,7 @@ function storeDataLocal() {
|
|||
},
|
||||
selectedWorker: selectedWorker.value,
|
||||
createdBy: quotationFormState.value.createdBy('tha'),
|
||||
agentPrice: agentPrice.value,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -1237,6 +1243,38 @@ function getInvoice() {
|
|||
}
|
||||
}
|
||||
|
||||
function handleWorkName() {
|
||||
const validService = productService.value.find(
|
||||
(item) => 'service' in item && item.service !== null,
|
||||
);
|
||||
|
||||
if (!validService || validService?.service === null) return;
|
||||
|
||||
const workName = validService?.service.name;
|
||||
|
||||
if (
|
||||
!!quotationFormData.value.workName &&
|
||||
!(quotationFormData.value.workName === workName)
|
||||
) {
|
||||
dialogCheckData({
|
||||
action: () => {
|
||||
quotationFormData.value.workName = workName;
|
||||
},
|
||||
checkData: () => {
|
||||
return {
|
||||
oldData: [
|
||||
{ nameField: 'workName', value: quotationFormData.value.workName },
|
||||
],
|
||||
newData: [{ nameField: 'workName', value: workName }],
|
||||
};
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
} else {
|
||||
quotationFormData.value.workName = workName;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[
|
||||
() => quotationFormState.value.statusFilterRequest,
|
||||
|
|
@ -1306,12 +1344,24 @@ async function formDownload() {
|
|||
|
||||
<div
|
||||
v-if="quotationFormState.mode !== 'create'"
|
||||
class="column col-2 q-ml-auto"
|
||||
class="column col-sm-2 col-12 q-ml-auto"
|
||||
style="gap: 10px"
|
||||
>
|
||||
<div class="row justify-end">
|
||||
<BadgeComponent :title-i18n="$t('general.laborIdentified')" />
|
||||
<div
|
||||
class="row"
|
||||
:class="{
|
||||
'justify-end': $q.screen.gt.xs,
|
||||
'q-pl-xl q-mt-sm': $q.screen.lt.sm,
|
||||
}"
|
||||
>
|
||||
<BadgeComponent
|
||||
:title-i18n="$t('general.laborIdentified')"
|
||||
:class="{
|
||||
'q-ml-md': $q.screen.lt.sm,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="row items-center justify-between surface-1 rounded q-pa-xs"
|
||||
style="height: 40px; border-radius: 40px"
|
||||
|
|
@ -1542,7 +1592,6 @@ async function formDownload() {
|
|||
quotationFormData.workerMax || selectedWorker.length
|
||||
"
|
||||
:readonly="readonly"
|
||||
fallback-img="/images/employee-avatar.png"
|
||||
:rows="selectedWorkerItem"
|
||||
@delete="(i) => deleteItem(selectedWorker, i)"
|
||||
/>
|
||||
|
|
@ -1774,6 +1823,7 @@ async function formDownload() {
|
|||
<div class="surface-1 q-pa-md full-width">
|
||||
<q-editor
|
||||
dense
|
||||
:hint="$t('general.enterToAdd')"
|
||||
:readonly="readonly || !pageState.remarkWrite"
|
||||
:model-value="
|
||||
!pageState.remarkWrite || readonly
|
||||
|
|
@ -1823,7 +1873,7 @@ async function formDownload() {
|
|||
"
|
||||
>
|
||||
<template v-if="!readonly" v-slot:toggle>
|
||||
<div class="text-caption row no-wrap q-px-sm">
|
||||
<div class="text-caption row no-wrap">
|
||||
<MainButton
|
||||
:disabled="readonly"
|
||||
:solid="!pageState.remarkWrite"
|
||||
|
|
@ -1857,6 +1907,15 @@ async function formDownload() {
|
|||
</div>
|
||||
</template>
|
||||
</q-editor>
|
||||
|
||||
<p class="app-text-muted text-caption">
|
||||
{{ $t('general.hintRemark') }}
|
||||
<code>#[quotation-labor]</code>
|
||||
{{ $t('general.quotationLabor') }}
|
||||
{{ $t('general.or') }}
|
||||
<code>#[quotation-payment]</code>
|
||||
{{ $t('general.quotationPayment') }}
|
||||
</p>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
|
|
@ -1894,9 +1953,25 @@ async function formDownload() {
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refStatusFilter?.showPopup"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refStatusFilter"
|
||||
v-model="quotationFormState.statusFilterRequest"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -2092,9 +2167,10 @@ async function formDownload() {
|
|||
</section>
|
||||
</template>
|
||||
|
||||
<div class="surface-1 q-pa-md flex" style="gap: var(--size-2)">
|
||||
<div class="surface-1 q-pa-md row" style="gap: var(--size-2)">
|
||||
<SelectInput
|
||||
class="q-mr-sm"
|
||||
class="q-mr-xl col-md-3 col-12"
|
||||
incremental
|
||||
v-model="templateForm"
|
||||
id="quotation-branch"
|
||||
:option="templateFormOption"
|
||||
|
|
@ -2294,7 +2370,12 @@ async function formDownload() {
|
|||
v-model:service-list="serviceList"
|
||||
v-model:selected-product-group="selectedProductGroup"
|
||||
:agent-price="agentPrice"
|
||||
@submit="convertToTable"
|
||||
@submit="
|
||||
(node) => {
|
||||
convertToTable(node);
|
||||
handleWorkName();
|
||||
}
|
||||
"
|
||||
@select-group="
|
||||
async (id) => {
|
||||
await getAllService(id);
|
||||
|
|
@ -2314,6 +2395,7 @@ async function formDownload() {
|
|||
></QuotationFormProductSelect>
|
||||
</div>
|
||||
|
||||
<!-- add Worker -->
|
||||
<QuotationFormWorkerAddDialog
|
||||
v-if="quotationFormState.source"
|
||||
:disabled-worker-id="selectedWorker.map((v) => v.id)"
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ defineProps<{
|
|||
};
|
||||
taskOrder?: boolean;
|
||||
taskOrderComplete?: boolean;
|
||||
debitNote?: boolean;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
@ -216,6 +217,7 @@ watch(
|
|||
'invoice-color': view === View.Invoice,
|
||||
'receipt-color': view === View.Receipt,
|
||||
'task-order-color': taskOrder && !taskOrderComplete,
|
||||
'debit-note-color': debitNote,
|
||||
}"
|
||||
>
|
||||
<div class="col bordered-r" v-if="!taskOrder">
|
||||
|
|
@ -444,9 +446,7 @@ watch(
|
|||
<span class="q-ml-auto">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
summaryPrice.totalPrice -
|
||||
summaryPrice.totalDiscount +
|
||||
summaryPrice.vat,
|
||||
summaryPrice.totalPrice - summaryPrice.totalDiscount,
|
||||
2,
|
||||
)
|
||||
}}
|
||||
|
|
@ -530,6 +530,10 @@ watch(
|
|||
--_color: var(--pink-7-hsl);
|
||||
}
|
||||
|
||||
.debit-note-color {
|
||||
--_color: var(--cyan-7-hsl);
|
||||
}
|
||||
|
||||
.bg-color {
|
||||
color: white;
|
||||
background: hsla(var(--_color));
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import useOptionStore from 'stores/options';
|
|||
import DialogForm from 'src/components/DialogForm.vue';
|
||||
import TreeView from 'src/components/shared/TreeView.vue';
|
||||
import SelectZone from 'src/components/shared/SelectZone.vue';
|
||||
import SelectInput from 'src/components/shared/SelectInput.vue';
|
||||
import SelectProductGroup from 'src/components/shared/select/SelectProductGroup.vue';
|
||||
import TotalProductCardComponent from 'src/components/04_product-service/TotalProductCardComponent.vue';
|
||||
import DeleteButton from 'src/components/button/DeleteButton.vue';
|
||||
|
|
@ -261,7 +260,11 @@ function mapNode() {
|
|||
const price = prop.agentPrice
|
||||
? p.product.agentPrice
|
||||
: p.product.price;
|
||||
const pricePerUnit = p.product.vatIncluded
|
||||
const pricePerUnit = (
|
||||
prop.agentPrice
|
||||
? p.product.agentPriceVatIncluded
|
||||
: p.product.vatIncluded
|
||||
)
|
||||
? precisionRound(price / (1 + (config.value?.vat || 0.07)))
|
||||
: price;
|
||||
productCount.value++;
|
||||
|
|
@ -304,7 +307,11 @@ function mapNode() {
|
|||
const price = prop.agentPrice
|
||||
? p.product.agentPrice
|
||||
: p.product.price;
|
||||
const pricePerUnit = p.product.vatIncluded
|
||||
const pricePerUnit = (
|
||||
prop.agentPrice
|
||||
? p.product.agentPriceVatIncluded
|
||||
: p.product.vatIncluded
|
||||
)
|
||||
? precisionRound(price / (1 + (config.value?.vat || 0.07)))
|
||||
: price;
|
||||
productCount.value++;
|
||||
|
|
@ -335,7 +342,9 @@ function mapNode() {
|
|||
};
|
||||
} else {
|
||||
const price = prop.agentPrice ? v.raw.agentPrice : v.raw.price;
|
||||
const pricePerUnit = v.raw.vatIncluded
|
||||
const pricePerUnit = (
|
||||
prop.agentPrice ? v.raw.agentPriceVatIncluded : v.raw.vatIncluded
|
||||
)
|
||||
? precisionRound(price / (1 + (config.value?.vat || 0.07)))
|
||||
: price;
|
||||
productCount.value++;
|
||||
|
|
@ -419,6 +428,7 @@ watch(
|
|||
<template>
|
||||
<div>
|
||||
<DialogForm
|
||||
:disabled-submit="countCheckedProducts(nodes) === 0"
|
||||
bg-color="var(--surface-2)"
|
||||
v-model:modal="model"
|
||||
:title="$t('general.list', { msg: $t('productService.title') })"
|
||||
|
|
@ -491,7 +501,7 @@ watch(
|
|||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row items-center q-gutter-x-sm">
|
||||
<div class="row items-center q-gutter-x-sm no-wrap">
|
||||
<q-btn
|
||||
color="primary"
|
||||
padding="4px"
|
||||
|
|
@ -499,7 +509,6 @@ watch(
|
|||
rounded
|
||||
icon="mdi-store-plus-outline"
|
||||
@click="triggerAddDialog"
|
||||
style="color: hsl(var(--text-mute))"
|
||||
/>
|
||||
<q-btn
|
||||
padding="4px"
|
||||
|
|
@ -507,6 +516,7 @@ watch(
|
|||
rounded
|
||||
icon="mdi-information-outline"
|
||||
@click="triggerInfo"
|
||||
:color="pageState.infoDrawer ? 'info' : ''"
|
||||
style="color: hsl(var(--text-mute))"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -634,7 +644,8 @@ watch(
|
|||
<div
|
||||
v-if="pageState.infoDrawer"
|
||||
class="column no-wrap surface-1"
|
||||
style="z-index: 1; width: 20vw; position: sticky"
|
||||
style="z-index: 1; position: sticky"
|
||||
:style="`width:${$q.screen.gt.sm ? '20vw' : '100%'}`"
|
||||
>
|
||||
<span
|
||||
v-if="selectedType === ''"
|
||||
|
|
@ -805,6 +816,7 @@ watch(
|
|||
border-search-section
|
||||
item-class="col-md-3 col-sm-6 col-12"
|
||||
v-model:selected-item="preSelectedItems"
|
||||
:type="pageState.productServiceTab === '1' ? 'service' : 'product'"
|
||||
@search="
|
||||
(value) => {
|
||||
$emit(
|
||||
|
|
@ -840,12 +852,14 @@ watch(
|
|||
>
|
||||
<template #top>
|
||||
<div class="row items-center app-text-muted">
|
||||
{{ $t('productService.group.title') }}
|
||||
<span class="q-pr-sm">
|
||||
{{ $t('productService.group.title') }}
|
||||
</span>
|
||||
|
||||
<SelectProductGroup
|
||||
class="q-pl-sm col-5"
|
||||
class="col-md-4 col-12"
|
||||
:class="{ 'q-mb-sm': $q.screen.lt.md }"
|
||||
id="product-group-select"
|
||||
style="min-height: 50px"
|
||||
clearable
|
||||
v-model:value="selectedProductGroup"
|
||||
:placeholder="
|
||||
|
|
@ -924,9 +938,11 @@ watch(
|
|||
|
||||
<template #data="{ item }">
|
||||
<TotalProductCardComponent
|
||||
:key="item.index"
|
||||
:priceDisplay="priceDisplay"
|
||||
:title="item.name"
|
||||
:data="item"
|
||||
no-Time-Img
|
||||
/>
|
||||
</template>
|
||||
</SelectZone>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
import { dateFormatJS } from 'src/utils/datetime';
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
import { MainButton, ViewButton } from 'components/button';
|
||||
|
||||
|
|
@ -12,8 +13,10 @@ defineEmits<{
|
|||
withDefaults(
|
||||
defineProps<{
|
||||
title?: string;
|
||||
hideExampleBtn?: boolean;
|
||||
successLabel?: string;
|
||||
useBtnDownload?: boolean;
|
||||
hideViewBtn?: boolean;
|
||||
hideExampleBtn?: boolean;
|
||||
|
||||
payType?: string;
|
||||
amount?: number;
|
||||
|
|
@ -25,8 +28,11 @@ withDefaults(
|
|||
);
|
||||
</script>
|
||||
<template>
|
||||
<section class="surface-1 rounded row">
|
||||
<aside class="column col bordered-r q-py-md q-pl-md">
|
||||
<section class="surface-1 rounded" :class="{ row: $q.screen.gt.xs }">
|
||||
<aside
|
||||
class="column col q-py-md q-pl-md"
|
||||
:class="{ 'bordered-r': $q.screen.gt.xs, ' bordered-b': $q.screen.lt.sm }"
|
||||
>
|
||||
<span class="text-weight-medium text-body1">
|
||||
{{ title || $t('quotation.receiptDialog.PaymentReceive') }}
|
||||
</span>
|
||||
|
|
@ -52,7 +58,7 @@ withDefaults(
|
|||
</aside>
|
||||
|
||||
<aside class="column col q-py-md text-right self-center q-px-md">
|
||||
<div class="q-gutter-x-xs">
|
||||
<div class="q-gutter-x-xs row justify-end">
|
||||
<MainButton
|
||||
v-if="!hideExampleBtn"
|
||||
icon="mdi-play-box-outline"
|
||||
|
|
@ -60,7 +66,40 @@ withDefaults(
|
|||
@click="() => $emit('example', index)"
|
||||
/>
|
||||
|
||||
<ViewButton icon-only @click="() => $emit('view', index)" />
|
||||
<ViewButton
|
||||
v-if="!hideViewBtn"
|
||||
icon-only
|
||||
@click="() => $emit('view', index)"
|
||||
/>
|
||||
|
||||
<q-btn-dropdown
|
||||
v-if="!!useBtnDownload"
|
||||
flat
|
||||
outline
|
||||
dropdown-icon="mdi-download"
|
||||
no-icon-animation
|
||||
padding="0 0"
|
||||
>
|
||||
<q-list>
|
||||
<q-item clickable>
|
||||
<q-item-section side>
|
||||
<Icon
|
||||
icon="fluent:receipt-money-24-regular"
|
||||
width="24px"
|
||||
height="24px"
|
||||
/>
|
||||
</q-item-section>
|
||||
{{ $t('debitNote.downloadReceipt') }}
|
||||
</q-item>
|
||||
|
||||
<q-item clickable>
|
||||
<q-item-section side>
|
||||
<Icon icon="hugeicons:invoice-03" width="24px" height="24px" />
|
||||
</q-item-section>
|
||||
{{ $t('debitNote.downloadTaxInvoice') }}
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</div>
|
||||
<span class="app-text-positive text-weight-bold text-body1">
|
||||
{{ successLabel || $t('quotation.receiptDialog.receiptIssued') }}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import PersonCard from 'src/components/shared/PersonCard.vue';
|
|||
import { QuotationFull } from 'src/stores/quotations/types';
|
||||
import { Lang } from 'src/utils/ui';
|
||||
import NoData from 'src/components/NoData.vue';
|
||||
import TableWorker from 'src/components/shared/table/TableWorker.vue';
|
||||
import ToggleView from 'src/components/shared/ToggleView.vue';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
|
|
@ -104,6 +106,7 @@ const employeeStore = useEmployeeStore();
|
|||
const quotationStore = useQuotationStore();
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
const viewMode = ref<boolean>(false);
|
||||
const workerSelected = ref<Employee[]>([]);
|
||||
const workerList = ref<Employee[]>([]);
|
||||
const importWorkerCriteria = ref<{
|
||||
|
|
@ -220,7 +223,7 @@ function getEmployeeImageUrl(item: Employee) {
|
|||
return `${API_BASE_URL}/employee/${item.id}/image/${item.selectedImage}`;
|
||||
}
|
||||
// NOTE: static image
|
||||
return '/images/employee-avatar.png';
|
||||
return `/images/employee-avatar-${item.gender}.png`;
|
||||
}
|
||||
|
||||
async function getWorkerList() {
|
||||
|
|
@ -305,7 +308,7 @@ watch(() => state.search, getWorkerList);
|
|||
</template>
|
||||
</DialogHeader>
|
||||
</template>
|
||||
<div class="col scroll">
|
||||
<div class="col full-width no-wrap scroll">
|
||||
<q-tab-panels
|
||||
class="surface-0 rounded full-height"
|
||||
v-model="state.step"
|
||||
|
|
@ -314,6 +317,8 @@ watch(() => state.search, getWorkerList);
|
|||
<q-tab-panel class="q-pa-none" :name="1">
|
||||
<div class="column q-pa-md full-height">
|
||||
<section class="row justify-end q-mb-md">
|
||||
<ToggleView v-model="viewMode" class="q-mr-sm" />
|
||||
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
|
|
@ -340,55 +345,59 @@ watch(() => state.search, getWorkerList);
|
|||
>
|
||||
<NoData :not-found="!!state.search" />
|
||||
</div>
|
||||
<div
|
||||
:key="emp.id"
|
||||
v-for="(emp, index) in workerList.map((data) => ({
|
||||
...data,
|
||||
_selectedIndex: selectedIndex(data),
|
||||
}))"
|
||||
class="col-2"
|
||||
|
||||
<TableWorker
|
||||
v-model:selected="workerSelected"
|
||||
:rows="workerList"
|
||||
:disabledWorkerId
|
||||
:grid="viewMode"
|
||||
>
|
||||
<button
|
||||
class="selectable-item full-width"
|
||||
:class="{
|
||||
['selectable-item__selected']: emp._selectedIndex !== -1,
|
||||
['selectable-item__disabled']: disabledWorkerId?.some(
|
||||
(id) => id === emp.id,
|
||||
),
|
||||
}"
|
||||
@click="toggleSelect(emp)"
|
||||
>
|
||||
<span class="selectable-item__pos">
|
||||
{{ emp._selectedIndex + 1 }}
|
||||
</span>
|
||||
<PersonCard
|
||||
no-action
|
||||
class="full-width"
|
||||
:prefix-id="'employee-' + index"
|
||||
:data="{
|
||||
name:
|
||||
locale === Lang.English
|
||||
? `${emp.firstNameEN} ${emp.lastNameEN}`
|
||||
: `${emp.firstName} ${emp.lastName}`,
|
||||
code: emp.employeePassport?.at(0)?.number || '-',
|
||||
female: emp.gender === 'female',
|
||||
male: emp.gender === 'male',
|
||||
img: getEmployeeImageUrl(emp),
|
||||
fallbackImg: '/images/employee-avatar.png',
|
||||
detail: [
|
||||
{
|
||||
icon: 'mdi-passport',
|
||||
value: optionStore.mapOption(emp.nationality),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-clock-outline',
|
||||
value: calculateAge(emp.dateOfBirth),
|
||||
},
|
||||
],
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<template #grid="{ item: emp, index }">
|
||||
<div :key="emp.id" class="col-md-2 col-sm-6 col-12">
|
||||
<button
|
||||
class="selectable-item full-width"
|
||||
:class="{
|
||||
['selectable-item__selected']:
|
||||
emp._selectedIndex !== -1,
|
||||
['selectable-item__disabled']: disabledWorkerId?.some(
|
||||
(id) => id === emp.id,
|
||||
),
|
||||
}"
|
||||
@click="toggleSelect(emp)"
|
||||
>
|
||||
<span class="selectable-item__pos">
|
||||
{{ (emp._selectedIndex || 0) + 1 }}
|
||||
</span>
|
||||
<PersonCard
|
||||
no-action
|
||||
class="full-width"
|
||||
:prefix-id="'employee-' + index"
|
||||
:data="{
|
||||
name:
|
||||
locale === Lang.English
|
||||
? `${emp.firstNameEN} ${emp.lastNameEN}`
|
||||
: `${emp.firstName} ${emp.lastName}`,
|
||||
code: emp.employeePassport?.at(0)?.number || '-',
|
||||
female: emp.gender === 'female',
|
||||
male: emp.gender === 'male',
|
||||
img: getEmployeeImageUrl(emp),
|
||||
fallbackImg: `/images/employee-avatar-${emp.gender}.png`,
|
||||
detail: [
|
||||
{
|
||||
icon: 'mdi-passport',
|
||||
value: optionStore.mapOption(emp.nationality),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-clock-outline',
|
||||
value: calculateAge(emp.dateOfBirth),
|
||||
},
|
||||
],
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</TableWorker>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -399,7 +408,10 @@ watch(() => state.search, getWorkerList);
|
|||
<BackButton icon-only @click="prev" />
|
||||
</section>
|
||||
<section class="full-height scroll col">
|
||||
<div class="rounded column" style="gap: var(--size-4)">
|
||||
<div
|
||||
class="rounded column full-width no-wrap"
|
||||
style="gap: var(--size-4)"
|
||||
>
|
||||
<q-expansion-item
|
||||
dense
|
||||
default-opened
|
||||
|
|
@ -408,6 +420,7 @@ watch(() => state.search, getWorkerList);
|
|||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="q-py-sm text-medium text-body items-center surface-1"
|
||||
v-for="{ id, amount, worker, product } in productServiceList"
|
||||
:key="id"
|
||||
>
|
||||
<template #header>
|
||||
<q-avatar class="q-mr-md" size="md">
|
||||
|
|
@ -453,82 +466,15 @@ watch(() => state.search, getWorkerList);
|
|||
</template>
|
||||
|
||||
<div class="q-pa-md surface-1">
|
||||
<q-table
|
||||
<TableWorker
|
||||
v-model:selected="productWorkerMap[id]"
|
||||
:rows-per-page-options="[0]"
|
||||
:rows="
|
||||
workerSelected.map((data, i) => ({
|
||||
...data,
|
||||
_index: i,
|
||||
}))
|
||||
"
|
||||
:columns
|
||||
hide-bottom
|
||||
bordered
|
||||
flat
|
||||
hide-pagination
|
||||
selection="multiple"
|
||||
card-container-class="q-col-gutter-sm"
|
||||
class="full-width"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||
:props="props"
|
||||
>
|
||||
<q-th
|
||||
v-for="col in columns"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
{{ $t(col.label) }}
|
||||
</template>
|
||||
<template v-if="col.name === '#check'">
|
||||
<q-checkbox v-model="props.selected" size="sm" />
|
||||
</template>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-slot:body="props: {
|
||||
row: Employee & { _index: number };
|
||||
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||
>
|
||||
<q-tr
|
||||
:class="{ dark: $q.dark.isActive }"
|
||||
class="text-center"
|
||||
>
|
||||
<q-td v-for="col in columns" :align="col.align">
|
||||
<!-- NOTE: custom column will starts with # -->
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
<q-avatar
|
||||
v-if="col.name === 'employeeName'"
|
||||
class="q-mr-sm"
|
||||
size="md"
|
||||
>
|
||||
<q-img
|
||||
:src="getEmployeeImageUrl(props.row)"
|
||||
:ratio="1"
|
||||
class="text-center"
|
||||
/>
|
||||
</q-avatar>
|
||||
<span>
|
||||
{{
|
||||
typeof col.field === 'string'
|
||||
? props.row[col.field as keyof Employee]
|
||||
: col.field(props.row)
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="col.name === '#check'">
|
||||
<q-checkbox v-model="props.selected" size="sm" />
|
||||
</template>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
/>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ import {
|
|||
columnsAttachment,
|
||||
} from 'src/pages/03_customer-management/constant';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import ToggleView from 'src/components/shared/ToggleView.vue';
|
||||
import TableWorker from 'src/components/shared/table/TableWorker.vue';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
|
|
@ -55,6 +57,8 @@ const quotationForm = useQuotationForm();
|
|||
const { locale } = useI18n();
|
||||
const ocrStore = useOcrStore();
|
||||
|
||||
const viewMode = ref<boolean>(false);
|
||||
|
||||
const { state: employeeFormState, currentFromDataEmployee } =
|
||||
storeToRefs(employeeFormStore);
|
||||
|
||||
|
|
@ -207,7 +211,7 @@ function getEmployeeImageUrl(item: Employee) {
|
|||
return `${API_BASE_URL}/employee/${item.id}/image/${item.selectedImage}`;
|
||||
}
|
||||
// NOTE: static image
|
||||
return '/images/employee-avatar.png';
|
||||
return `/images/employee-avatar-${item.gender}.png`;
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
|
@ -329,14 +333,15 @@ watch(() => state.search, getWorkerList);
|
|||
<div class="column full-height no-wrap surface-2">
|
||||
<section class="row q-mb-md">
|
||||
<header
|
||||
class="row items-center q-py-sm q-px-md justify-between full-width surface-3 bordered-b no-wrap"
|
||||
class="row items-center q-py-sm q-px-md justify-end full-width surface-3 bordered-b no-wrap"
|
||||
>
|
||||
<ToggleView v-model="viewMode" class="q-mr-sm full-height" />
|
||||
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="q-mr-md col-md-4"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="state.search"
|
||||
debounce="500"
|
||||
|
|
@ -400,7 +405,7 @@ watch(() => state.search, getWorkerList);
|
|||
: `${emp.firstName} ${emp.lastName}`,
|
||||
female: emp.gender === 'female',
|
||||
male: emp.gender === 'male',
|
||||
img: '/images/employee-avatar.png',
|
||||
img: `/images/employee-avatar-${emp.gender}.png`,
|
||||
index: index,
|
||||
detail: [
|
||||
{
|
||||
|
|
@ -447,7 +452,7 @@ watch(() => state.search, getWorkerList);
|
|||
<div class="full-width q-pt-md">
|
||||
<section
|
||||
:class="{ ['items-center']: workerList.length === 0 }"
|
||||
class="row q-col-gutter-md scroll"
|
||||
class="row scroll"
|
||||
>
|
||||
<div
|
||||
style="display: inline-block; margin-inline: auto"
|
||||
|
|
@ -455,52 +460,59 @@ watch(() => state.search, getWorkerList);
|
|||
>
|
||||
<NoData :not-found="!!state.search" />
|
||||
</div>
|
||||
<div
|
||||
:key="emp.id"
|
||||
v-for="(emp, index) in workerList.map((data) => ({
|
||||
...data,
|
||||
_selectedIndex: selectedIndex(data),
|
||||
}))"
|
||||
class="col-2"
|
||||
<TableWorker
|
||||
v-if="workerList.length !== 0"
|
||||
v-model:selected="workerSelected"
|
||||
:rows="workerList"
|
||||
:disabledWorkerId
|
||||
:grid="viewMode"
|
||||
>
|
||||
<button
|
||||
class="selectable-item full-width"
|
||||
:class="{
|
||||
['selectable-item__selected']: emp._selectedIndex !== -1,
|
||||
['selectable-item__disabled']: disabledWorkerId?.some(
|
||||
(id) => id === emp.id,
|
||||
),
|
||||
}"
|
||||
@click="toggleSelect(emp)"
|
||||
>
|
||||
<PersonCard
|
||||
no-action
|
||||
class="full-width"
|
||||
:prefix-id="'employee-' + index"
|
||||
:data="{
|
||||
name:
|
||||
locale === Lang.English
|
||||
? `${emp.firstNameEN} ${emp.lastNameEN}`
|
||||
: `${emp.firstName} ${emp.lastName}`,
|
||||
code: emp.employeePassport?.at(0)?.number || '-',
|
||||
female: emp.gender === 'female',
|
||||
male: emp.gender === 'male',
|
||||
img: getEmployeeImageUrl(emp),
|
||||
fallbackImg: '/images/employee-avatar.png',
|
||||
detail: [
|
||||
{
|
||||
icon: 'mdi-passport',
|
||||
value: optionStore.mapOption(emp.nationality),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-clock-outline',
|
||||
value: calculateAge(emp.dateOfBirth),
|
||||
},
|
||||
],
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<template #grid="{ item: emp, index }">
|
||||
<div :key="emp.id" class="col-md-2 col-sm-6 col-12">
|
||||
<button
|
||||
class="selectable-item full-width"
|
||||
:class="{
|
||||
['selectable-item__selected']:
|
||||
emp._selectedIndex !== -1,
|
||||
['selectable-item__disabled']: disabledWorkerId?.some(
|
||||
(id) => id === emp.id,
|
||||
),
|
||||
}"
|
||||
@click="toggleSelect(emp)"
|
||||
>
|
||||
<span class="selectable-item__pos">
|
||||
{{ (emp._selectedIndex || 0) + 1 }}
|
||||
</span>
|
||||
<PersonCard
|
||||
no-action
|
||||
class="full-width"
|
||||
:prefix-id="'employee-' + index"
|
||||
:data="{
|
||||
name:
|
||||
locale === Lang.English
|
||||
? `${emp.firstNameEN} ${emp.lastNameEN}`
|
||||
: `${emp.firstName} ${emp.lastName}`,
|
||||
code: emp.employeePassport?.at(0)?.number || '-',
|
||||
female: emp.gender === 'female',
|
||||
male: emp.gender === 'male',
|
||||
img: getEmployeeImageUrl(emp),
|
||||
fallbackImg: `/images/employee-avatar-${emp.gender}.png`,
|
||||
detail: [
|
||||
{
|
||||
icon: 'mdi-passport',
|
||||
value: optionStore.mapOption(emp.nationality),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-clock-outline',
|
||||
value: calculateAge(emp.dateOfBirth),
|
||||
},
|
||||
],
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</TableWorker>
|
||||
</section>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
|
|
@ -567,7 +579,10 @@ watch(() => state.search, getWorkerList);
|
|||
v-model:current-tab="employeeFormState.currentTab"
|
||||
v-model:toggle-status="currentFromDataEmployee.status"
|
||||
fallbackCover="/images/employee-banner.png"
|
||||
:img="employeeFormState.profileUrl || `/images/employee-avatar.png`"
|
||||
:img="
|
||||
employeeFormState.profileUrl ||
|
||||
`/images/employee-avatar-${currentFromDataEmployee.gender}.png`
|
||||
"
|
||||
:toggleTitle="$t('status.title')"
|
||||
hideFade
|
||||
@view="
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { QTableColumn, QTableSlots } from 'quasar';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
|
||||
import BadgeComponent from 'components/BadgeComponent.vue';
|
||||
|
||||
|
|
@ -46,8 +44,6 @@ const emits = defineEmits<{
|
|||
(e: 'success'): void;
|
||||
}>();
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
|
||||
function goToRequestList(id: string) {
|
||||
const url = new URL(`/request-list/${id}`, window.location.origin);
|
||||
window.open(url.toString(), '_blank');
|
||||
|
|
@ -64,13 +60,15 @@ function goToRequestList(id: string) {
|
|||
}))
|
||||
"
|
||||
:columns
|
||||
hide-bottom
|
||||
bordered
|
||||
flat
|
||||
hide-pagination
|
||||
selection="multiple"
|
||||
card-container-class="q-col-gutter-sm"
|
||||
:no-data-label="$t('general.noDataTable')"
|
||||
class="full-width"
|
||||
:pagination="{
|
||||
rowsPerPage: 0,
|
||||
}"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
|
|
@ -89,7 +87,7 @@ function goToRequestList(id: string) {
|
|||
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||
>
|
||||
<q-tr :class="{ dark: $q.dark.isActive }" class="text-center">
|
||||
<q-td v-for="col in columns" :align="col.align">
|
||||
<q-td v-for="col in columns" :align="col.align" :key="col.name">
|
||||
<!-- NOTE: custom column will starts with # -->
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
<span>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { useInvoice } from 'stores/payment';
|
|||
import useEmployeeStore from 'stores/employee';
|
||||
import { getName } from 'src/services/keycloak';
|
||||
|
||||
const DEFAULT_DATA: QuotationPayload = {
|
||||
export const DEFAULT_DATA: QuotationPayload = {
|
||||
productServiceList: [],
|
||||
urgent: false,
|
||||
customerBranchId: '',
|
||||
|
|
@ -39,6 +39,7 @@ const DEFAULT_DATA: QuotationPayload = {
|
|||
_count: { worker: 0 },
|
||||
status: 'CREATED',
|
||||
remark: '#[quotation-labor]<br/><br/>#[quotation-payment]',
|
||||
agentPrice: false,
|
||||
};
|
||||
|
||||
const DEFAULT_DATA_INVOICE: InvoicePayload = {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
QuotationPayload,
|
||||
Details,
|
||||
QuotationFull,
|
||||
ProductRelation,
|
||||
} from 'src/stores/quotations/types';
|
||||
|
||||
// NOTE: Import Components
|
||||
|
|
@ -40,11 +41,12 @@ type Product = {
|
|||
code: string;
|
||||
detail: string;
|
||||
amount: number;
|
||||
priceUnit: number;
|
||||
pricePerUnit: number;
|
||||
discount: number;
|
||||
vat: number;
|
||||
value: number;
|
||||
calcVat: boolean;
|
||||
product: ProductRelation;
|
||||
};
|
||||
|
||||
type SummaryPrice = {
|
||||
|
|
@ -60,6 +62,8 @@ const branch = ref<Branch>();
|
|||
const productList = ref<Product[]>([]);
|
||||
const bankList = ref<BankBook[]>([]);
|
||||
|
||||
const agentPrice = ref(false);
|
||||
|
||||
const elements = ref<HTMLElement[]>([]);
|
||||
const chunks = ref<Product[][]>([[]]);
|
||||
const attachmentList = ref<
|
||||
|
|
@ -194,6 +198,8 @@ onMounted(async () => {
|
|||
customer.value = resCustomerBranch;
|
||||
}
|
||||
|
||||
agentPrice.value = data.value.agentPrice || parsed?.meta?.agentPrice;
|
||||
|
||||
details.value = {
|
||||
code: parsed?.meta?.source?.code ?? data.value?.code,
|
||||
createdAt:
|
||||
|
|
@ -247,11 +253,12 @@ onMounted(async () => {
|
|||
code: v.product.code,
|
||||
detail: v.product.name,
|
||||
amount: v.amount || 0,
|
||||
priceUnit: v.pricePerUnit || 0,
|
||||
pricePerUnit: v.pricePerUnit || 0,
|
||||
discount: v.discount || 0,
|
||||
vat: v.vat || 0,
|
||||
value: precisionRound(price + (v.product.calcVat ? vat : 0)),
|
||||
calcVat: v.product.calcVat,
|
||||
product: v.product,
|
||||
};
|
||||
},
|
||||
) || [];
|
||||
|
|
@ -273,10 +280,13 @@ onMounted(async () => {
|
|||
const vat =
|
||||
(finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07);
|
||||
|
||||
const calcVat =
|
||||
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
|
||||
|
||||
a.totalPrice = precisionRound(a.totalPrice + price);
|
||||
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
|
||||
a.vat = c.product.calcVat ? precisionRound(a.vat + vat) : a.vat;
|
||||
a.vatExcluded = c.product.calcVat
|
||||
a.vat = calcVat ? precisionRound(a.vat + vat) : a.vat;
|
||||
a.vatExcluded = calcVat
|
||||
? a.vatExcluded
|
||||
: precisionRound(a.vatExcluded + price);
|
||||
a.finalPrice = precisionRound(
|
||||
|
|
@ -285,7 +295,6 @@ onMounted(async () => {
|
|||
a.vat -
|
||||
Number(data.value?.discount || 0),
|
||||
);
|
||||
|
||||
return a;
|
||||
},
|
||||
{
|
||||
|
|
@ -300,6 +309,20 @@ onMounted(async () => {
|
|||
assignData();
|
||||
});
|
||||
|
||||
function calcPrice(c: Product) {
|
||||
const originalPrice = c.pricePerUnit;
|
||||
const finalPriceWithVat = precisionRound(
|
||||
originalPrice + originalPrice * (config.value?.vat || 0.07),
|
||||
);
|
||||
const finalPriceNoVat = finalPriceWithVat / (1 + (config.value?.vat || 0.07));
|
||||
|
||||
const price = finalPriceNoVat * c.amount - c.discount;
|
||||
const vat = c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat']
|
||||
? (finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07)
|
||||
: 0;
|
||||
return precisionRound(price + vat);
|
||||
}
|
||||
|
||||
watch(elements, () => {});
|
||||
|
||||
function print() {
|
||||
|
|
@ -359,7 +382,15 @@ function print() {
|
|||
<td>{{ v.detail }}</td>
|
||||
<td style="text-align: right">{{ v.amount }}</td>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumberDecimal(v.priceUnit, 2) }}
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
v.pricePerUnit +
|
||||
(v.product[agentPrice ? 'agentPriceCalcVat' : 'calcVat']
|
||||
? v.pricePerUnit * (config?.vat || 0.07)
|
||||
: 0),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumberDecimal(v.discount, 2) }}
|
||||
|
|
@ -367,9 +398,9 @@ function print() {
|
|||
<td style="text-align: right">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
v.calcVat
|
||||
v.product[agentPrice ? 'agentPriceCalcVat' : 'calcVat']
|
||||
? precisionRound(
|
||||
(v.priceUnit * v.amount - v.discount) *
|
||||
(v.pricePerUnit * v.amount - v.discount) *
|
||||
(config?.vat || 0.07),
|
||||
)
|
||||
: 0,
|
||||
|
|
@ -378,7 +409,7 @@ function print() {
|
|||
}}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumberDecimal(v.value, 2) }}
|
||||
{{ formatNumberDecimal(calcPrice(v), 2) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
@ -435,9 +466,7 @@ function print() {
|
|||
<td class="text-right">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
summaryPrice.totalPrice -
|
||||
summaryPrice.totalDiscount +
|
||||
summaryPrice.vat,
|
||||
summaryPrice.totalPrice - summaryPrice.totalDiscount,
|
||||
2,
|
||||
)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -251,12 +251,16 @@ watch(
|
|||
id="agencies-form-content"
|
||||
:class="{
|
||||
'q-py-md q-pr-md ': $q.screen.gt.sm,
|
||||
'q-py-md q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
style="height: 100%; max-height: 100%; overflow-y: auto"
|
||||
>
|
||||
<div
|
||||
class="q-py-md q-px-lg"
|
||||
class="rounded"
|
||||
:class="{
|
||||
'q-py-md q-px-lg': $q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
style="position: absolute; z-index: 99999; top: 0; right: 0"
|
||||
>
|
||||
<div class="surface-1 row rounded">
|
||||
|
|
@ -346,7 +350,7 @@ watch(
|
|||
class="rounded row"
|
||||
:class="{
|
||||
'q-py-md q-px-lg': $q.screen.gt.sm,
|
||||
'q-py-sm q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
style="position: absolute; z-index: 999; top: 0; right: 0"
|
||||
>
|
||||
|
|
@ -425,7 +429,7 @@ watch(
|
|||
class="col-12 col-md-10 relative-position"
|
||||
:class="{
|
||||
'q-py-md q-pr-md ': $q.screen.gt.sm,
|
||||
'q-py-md q-px-lg': !$q.screen.gt.sm,
|
||||
'q-pa-sm': !$q.screen.gt.sm,
|
||||
}"
|
||||
id="user-form-content"
|
||||
style="height: 100%; max-height: 100; overflow-y: auto"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
import { QTableProps } from 'quasar';
|
||||
import { dialog } from 'src/stores/utils';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
import { baseUrl } from 'src/stores/utils';
|
||||
import { useNavigator } from 'src/stores/navigator';
|
||||
|
|
@ -12,6 +13,7 @@ import { useInstitution } from 'src/stores/institution';
|
|||
import { Institution, InstitutionPayload } from 'src/stores/institution/types';
|
||||
import { formatAddress } from 'src/utils/address';
|
||||
|
||||
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||
import PaginationComponent from 'src/components/PaginationComponent.vue';
|
||||
import KebabAction from 'src/components/shared/KebabAction.vue';
|
||||
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||
|
|
@ -19,9 +21,9 @@ import FloatingActionButton from 'src/components/FloatingActionButton.vue';
|
|||
import CreateButton from 'src/components/AddButton.vue';
|
||||
import NoData from 'src/components/NoData.vue';
|
||||
import AgenciesDialog from './AgenciesDialog.vue';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const $q = useQuasar();
|
||||
const navigatorStore = useNavigator();
|
||||
const institutionStore = useInstitution();
|
||||
|
||||
|
|
@ -189,7 +191,7 @@ async function submit(opt?: { selectedImage: string }) {
|
|||
pageState.isDrawerEdit = false;
|
||||
currAgenciesData.value = ret;
|
||||
formData.value.selectedImage = ret.selectedImage;
|
||||
await fetchData();
|
||||
await fetchData($q.screen.xs);
|
||||
|
||||
if (refAgenciesDialog.value && opt?.selectedImage) {
|
||||
refAgenciesDialog.value.clearImageState();
|
||||
|
|
@ -205,14 +207,12 @@ async function submit(opt?: { selectedImage: string }) {
|
|||
onCreateImageList.value,
|
||||
);
|
||||
|
||||
await fetchData();
|
||||
await fetchData($q.screen.xs);
|
||||
pageState.addModal = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerChangeStatus(data?: Institution) {}
|
||||
|
||||
async function triggerDelete(id?: string) {
|
||||
if (!id) return;
|
||||
dialog({
|
||||
|
|
@ -225,21 +225,26 @@ async function triggerDelete(id?: string) {
|
|||
action: async () => {
|
||||
await institutionStore.deleteInstitution(id);
|
||||
resetForm();
|
||||
await fetchData();
|
||||
await fetchData($q.screen.xs);
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
async function fetchData(mobileFetch?: boolean) {
|
||||
const ret = await institutionStore.getInstitutionList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
page: mobileFetch ? 1 : page.value,
|
||||
pageSize: mobileFetch
|
||||
? data.value.length + (pageState.total === data.value.length ? 1 : 0)
|
||||
: pageSize.value,
|
||||
query: pageState.inputSearch,
|
||||
});
|
||||
|
||||
if (ret) {
|
||||
data.value = ret.result;
|
||||
data.value =
|
||||
$q.screen.xs && !mobileFetch
|
||||
? [...data.value, ...ret.result]
|
||||
: ret.result;
|
||||
pageMax.value = Math.ceil(ret.total / pageSize.value);
|
||||
pageState.total = ret.total;
|
||||
}
|
||||
|
|
@ -248,13 +253,18 @@ async function fetchData() {
|
|||
onMounted(async () => {
|
||||
navigatorStore.current.title = 'agencies.title';
|
||||
navigatorStore.current.path = [{ text: 'agencies.caption', i18n: true }];
|
||||
pageState.gridView = $q.screen.lt.md ? true : false;
|
||||
|
||||
await fetchData();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => pageState.inputSearch,
|
||||
() => fetchData(),
|
||||
() => {
|
||||
page.value = 1;
|
||||
data.value = [];
|
||||
fetchData();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
|
|
@ -313,7 +323,7 @@ watch(
|
|||
|
||||
<!-- SEC: header content -->
|
||||
<section class="col surface-1 rounded bordered overflow-hidden">
|
||||
<div class="column full-height">
|
||||
<div class="column full-height no-wrap">
|
||||
<header
|
||||
class="row surface-3 justify-between full-width items-center bordered-b"
|
||||
style="z-index: 1"
|
||||
|
|
@ -324,7 +334,7 @@ watch(
|
|||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="q-mr-md col-12 col-md-3"
|
||||
class="col col-md-3"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="pageState.inputSearch"
|
||||
debounce="200"
|
||||
|
|
@ -334,11 +344,7 @@ watch(
|
|||
</template>
|
||||
</q-input>
|
||||
|
||||
<div
|
||||
class="row col-12 col-md-3 justify-end"
|
||||
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||
style="white-space: nowrap"
|
||||
>
|
||||
<div class="row col-md-5 justify-end" style="white-space: nowrap">
|
||||
<!-- <q-select
|
||||
v-model="statusFilter"
|
||||
outlined
|
||||
|
|
@ -361,7 +367,7 @@ watch(
|
|||
v-if="!pageState.gridView"
|
||||
id="select-field"
|
||||
for="select-field"
|
||||
class="col"
|
||||
class="col-md-5 q-ml-sm"
|
||||
:options="
|
||||
fieldSelectedOption.map((v) => ({
|
||||
...v,
|
||||
|
|
@ -436,12 +442,12 @@ watch(
|
|||
class="col surface-2 flex items-center justify-center"
|
||||
>
|
||||
<NoData
|
||||
v-if="pageState.total !== 0"
|
||||
v-if="pageState.inputSearch"
|
||||
:not-found="!!pageState.inputSearch"
|
||||
/>
|
||||
|
||||
<CreateButton
|
||||
v-if="pageState.total === 0"
|
||||
v-if="!pageState.inputSearch && pageState.total === 0"
|
||||
@click="triggerDialog('add')"
|
||||
label="general.add"
|
||||
:i18n-args="{ text: $t('agencies.title') }"
|
||||
|
|
@ -449,107 +455,107 @@ watch(
|
|||
</article>
|
||||
|
||||
<article v-else class="col q-pa-md surface-2 scroll full-width">
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
:grid="pageState.gridView"
|
||||
:rows="data"
|
||||
:columns="columns"
|
||||
class="full-width"
|
||||
card-container-class="q-col-gutter-md"
|
||||
row-key="name"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-pagination
|
||||
:visible-columns="fieldSelected"
|
||||
<q-infinite-scroll
|
||||
:offset="100"
|
||||
@load="
|
||||
(_, done) => {
|
||||
if ($q.screen.gt.xs || page === pageMax) return;
|
||||
page = page + 1;
|
||||
fetchData().then(() => done(page >= pageMax));
|
||||
}
|
||||
"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||
:props="props"
|
||||
>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ $t(col.label) }}
|
||||
</q-th>
|
||||
<q-th auto-width />
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr
|
||||
:class="{
|
||||
'app-text-muted': props.row.status === 'INACTIVE',
|
||||
'status-active': props.row.status !== 'INACTIVE',
|
||||
'status-inactive': props.row.status === 'INACTIVE',
|
||||
}"
|
||||
:props="props"
|
||||
:style="
|
||||
props.rowIndex % 2 !== 0
|
||||
? $q.dark.isActive
|
||||
? 'background: hsl(var(--gray-11-hsl)/0.2)'
|
||||
: `background: #f9fafc`
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<q-td
|
||||
class="text-center"
|
||||
v-if="fieldSelected.includes('orderNumber')"
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
:grid="pageState.gridView"
|
||||
:rows="data"
|
||||
:columns="columns"
|
||||
class="full-width"
|
||||
card-container-class="q-col-gutter-md"
|
||||
row-key="name"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-pagination
|
||||
:visible-columns="fieldSelected"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||
:props="props"
|
||||
>
|
||||
{{ props.rowIndex + 1 }}
|
||||
<!-- {{ (currentPage - 1) * pageSize + props.rowIndex + 1 }} -->
|
||||
</q-td>
|
||||
<q-td v-if="fieldSelected.includes('name')">
|
||||
<section class="row items-center no-wrap">
|
||||
<q-avatar size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/institution/${props.row.id}/image/${props.row.selectedImage}?ts=${Date.now()}`"
|
||||
>
|
||||
<template #error>
|
||||
<div
|
||||
class="no-padding full-width full-height flex items-center justify-center"
|
||||
style="
|
||||
background: hsla(var(--green-8-hsl) / 0.1);
|
||||
color: hsla(var(--green-8-hsl) / 1);
|
||||
"
|
||||
>
|
||||
<Icon icon="ph-building-office" />
|
||||
</div>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
<span class="col q-pl-md">
|
||||
<div>
|
||||
{{
|
||||
$i18n.locale === 'eng'
|
||||
? props.row.nameEN
|
||||
: props.row.name
|
||||
}}
|
||||
</div>
|
||||
<div class="app-text-muted">
|
||||
{{ props.row.code }}
|
||||
</div>
|
||||
</span>
|
||||
</section>
|
||||
</q-td>
|
||||
<q-td v-if="fieldSelected.includes('address')">
|
||||
{{
|
||||
formatAddress({
|
||||
address: props.row.address,
|
||||
addressEN: props.row.addressEN,
|
||||
moo: props.row.moo,
|
||||
mooEN: props.row.mooEN,
|
||||
soi: props.row.soi,
|
||||
soiEN: props.row.soiEN,
|
||||
street: props.row.street,
|
||||
streetEN: props.row.streetEN,
|
||||
province: props.row.province,
|
||||
district: props.row.district,
|
||||
subDistrict: props.row.subDistrict,
|
||||
en: $i18n.locale === 'eng',
|
||||
})
|
||||
}}
|
||||
<q-tooltip>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
{{ $t(col.label) }}
|
||||
</q-th>
|
||||
<q-th auto-width />
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr
|
||||
:class="{
|
||||
'app-text-muted': props.row.status === 'INACTIVE',
|
||||
'status-active': props.row.status !== 'INACTIVE',
|
||||
'status-inactive': props.row.status === 'INACTIVE',
|
||||
}"
|
||||
:props="props"
|
||||
:style="
|
||||
props.rowIndex % 2 !== 0
|
||||
? $q.dark.isActive
|
||||
? 'background: hsl(var(--gray-11-hsl)/0.2)'
|
||||
: `background: #f9fafc`
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<q-td
|
||||
class="text-center"
|
||||
v-if="fieldSelected.includes('orderNumber')"
|
||||
>
|
||||
{{
|
||||
$q.screen.xs
|
||||
? props.rowIndex + 1
|
||||
: (page - 1) * pageSize + props.rowIndex + 1
|
||||
}}
|
||||
</q-td>
|
||||
<q-td v-if="fieldSelected.includes('name')">
|
||||
<section class="row items-center no-wrap">
|
||||
<q-avatar size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/institution/${props.row.id}/image/${props.row.selectedImage}?ts=${Date.now()}`"
|
||||
>
|
||||
<template #error>
|
||||
<div
|
||||
class="no-padding full-width full-height flex items-center justify-center"
|
||||
style="
|
||||
background: hsla(var(--green-8-hsl) / 0.1);
|
||||
color: hsla(var(--green-8-hsl) / 1);
|
||||
"
|
||||
>
|
||||
<Icon icon="ph-building-office" />
|
||||
</div>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
<span class="col q-pl-md">
|
||||
<div>
|
||||
{{
|
||||
$i18n.locale === 'eng'
|
||||
? props.row.nameEN
|
||||
: props.row.name
|
||||
}}
|
||||
</div>
|
||||
<div class="app-text-muted">
|
||||
{{ props.row.code }}
|
||||
</div>
|
||||
</span>
|
||||
</section>
|
||||
</q-td>
|
||||
<q-td v-if="fieldSelected.includes('address')">
|
||||
{{
|
||||
formatAddress({
|
||||
address: props.row.address,
|
||||
|
|
@ -566,119 +572,7 @@ watch(
|
|||
en: $i18n.locale === 'eng',
|
||||
})
|
||||
}}
|
||||
</q-tooltip>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-btn
|
||||
icon="mdi-eye-outline"
|
||||
size="sm"
|
||||
dense
|
||||
round
|
||||
flat
|
||||
@click.stop="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('view');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<KebabAction
|
||||
hide-toggle
|
||||
:id-name="props.row.id"
|
||||
:status="props.row.status"
|
||||
@view="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('view');
|
||||
}
|
||||
"
|
||||
@edit="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('edit');
|
||||
}
|
||||
"
|
||||
@delete="() => triggerDelete(props.row.id)"
|
||||
@change-status="() => triggerChangeStatus(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item="props">
|
||||
<section class="column col-12 col-md-6">
|
||||
<div class="bordered col surface-1 rounded q-pa-md">
|
||||
<header class="row items-center">
|
||||
<q-avatar size="xl">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/institution/${props.row.id}/image/${props.row.selectedImage}?ts=${Date.now()}`"
|
||||
>
|
||||
<template #error>
|
||||
<div
|
||||
class="no-padding full-width full-height flex items-center justify-center"
|
||||
style="
|
||||
background: hsla(var(--green-8-hsl) / 0.1);
|
||||
color: hsla(var(--green-8-hsl) / 1);
|
||||
"
|
||||
>
|
||||
<Icon icon="ph-building-office" />
|
||||
</div>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
<span class="text-weight-bold column q-pl-md">
|
||||
{{
|
||||
$i18n.locale === 'eng'
|
||||
? props.row.nameEN
|
||||
: props.row.name
|
||||
}}
|
||||
<span class="text-caption app-text-muted-2">
|
||||
{{ props.row.code }}
|
||||
</span>
|
||||
</span>
|
||||
<nav class="row q-ml-auto items-center justify-end no-wrap">
|
||||
<q-btn
|
||||
icon="mdi-eye-outline"
|
||||
size="sm"
|
||||
dense
|
||||
round
|
||||
flat
|
||||
@click.stop="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('view');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<KebabAction
|
||||
hide-toggle
|
||||
:id-name="props.row.id"
|
||||
:status="props.row.status"
|
||||
@view="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('view');
|
||||
}
|
||||
"
|
||||
@edit="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('edit');
|
||||
}
|
||||
"
|
||||
@delete="() => triggerDelete(props.row.id)"
|
||||
@change-status="() => triggerChangeStatus(props.row)"
|
||||
/>
|
||||
</nav>
|
||||
</header>
|
||||
<q-separator spaced="lg" />
|
||||
<div class="row full-width">
|
||||
<span class="col-2 app-text-muted">
|
||||
{{ $t('general.address') }}
|
||||
</span>
|
||||
<span class="col">
|
||||
<q-tooltip>
|
||||
{{
|
||||
formatAddress({
|
||||
address: props.row.address,
|
||||
|
|
@ -695,7 +589,119 @@ watch(
|
|||
en: $i18n.locale === 'eng',
|
||||
})
|
||||
}}
|
||||
<q-tooltip>
|
||||
</q-tooltip>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-btn
|
||||
icon="mdi-eye-outline"
|
||||
size="sm"
|
||||
dense
|
||||
round
|
||||
flat
|
||||
@click.stop="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('view');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<KebabAction
|
||||
hide-toggle
|
||||
:id-name="props.row.id"
|
||||
:status="props.row.status"
|
||||
@view="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('view');
|
||||
}
|
||||
"
|
||||
@edit="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('edit');
|
||||
}
|
||||
"
|
||||
@delete="() => triggerDelete(props.row.id)"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item="props">
|
||||
<section class="column col-12 col-md-6">
|
||||
<div class="bordered col surface-1 rounded q-pa-md">
|
||||
<header class="row items-center">
|
||||
<q-avatar size="xl">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/institution/${props.row.id}/image/${props.row.selectedImage}?ts=${Date.now()}`"
|
||||
>
|
||||
<template #error>
|
||||
<div
|
||||
class="no-padding full-width full-height flex items-center justify-center"
|
||||
style="
|
||||
background: hsla(var(--green-8-hsl) / 0.1);
|
||||
color: hsla(var(--green-8-hsl) / 1);
|
||||
"
|
||||
>
|
||||
<Icon icon="ph-building-office" />
|
||||
</div>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
<span class="text-weight-bold column q-pl-md">
|
||||
{{
|
||||
$i18n.locale === 'eng'
|
||||
? props.row.nameEN
|
||||
: props.row.name
|
||||
}}
|
||||
<span class="text-caption app-text-muted-2">
|
||||
{{ props.row.code }}
|
||||
</span>
|
||||
</span>
|
||||
<nav
|
||||
class="row q-ml-auto items-center justify-end no-wrap"
|
||||
>
|
||||
<q-btn
|
||||
icon="mdi-eye-outline"
|
||||
size="sm"
|
||||
dense
|
||||
round
|
||||
flat
|
||||
@click.stop="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('view');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<KebabAction
|
||||
hide-toggle
|
||||
:id-name="props.row.id"
|
||||
:status="props.row.status"
|
||||
@view="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('view');
|
||||
}
|
||||
"
|
||||
@edit="
|
||||
() => {
|
||||
assignFormData(props.row);
|
||||
triggerDialog('edit');
|
||||
}
|
||||
"
|
||||
@delete="() => triggerDelete(props.row.id)"
|
||||
/>
|
||||
</nav>
|
||||
</header>
|
||||
<q-separator spaced="lg" />
|
||||
<div class="row full-width">
|
||||
<span class="col-2 app-text-muted">
|
||||
{{ $t('general.address') }}
|
||||
</span>
|
||||
<span class="col">
|
||||
{{
|
||||
formatAddress({
|
||||
address: props.row.address,
|
||||
|
|
@ -712,19 +718,45 @@ watch(
|
|||
en: $i18n.locale === 'eng',
|
||||
})
|
||||
}}
|
||||
</q-tooltip>
|
||||
</span>
|
||||
<q-tooltip>
|
||||
{{
|
||||
formatAddress({
|
||||
address: props.row.address,
|
||||
addressEN: props.row.addressEN,
|
||||
moo: props.row.moo,
|
||||
mooEN: props.row.mooEN,
|
||||
soi: props.row.soi,
|
||||
soiEN: props.row.soiEN,
|
||||
street: props.row.street,
|
||||
streetEN: props.row.streetEN,
|
||||
province: props.row.province,
|
||||
district: props.row.district,
|
||||
subDistrict: props.row.subDistrict,
|
||||
en: $i18n.locale === 'eng',
|
||||
})
|
||||
}}
|
||||
</q-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
</q-table>
|
||||
<template v-slot:loading>
|
||||
<div
|
||||
v-if="$q.screen.lt.sm && page !== pageMax"
|
||||
class="row justify-center"
|
||||
>
|
||||
<q-spinner-dots color="primary" size="40px" />
|
||||
</div>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-infinite-scroll>
|
||||
</article>
|
||||
|
||||
<!-- SEC: footer content -->
|
||||
<footer
|
||||
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
||||
v-if="pageMax > 0"
|
||||
v-if="pageMax > 0 && $q.screen.gt.xs"
|
||||
>
|
||||
<div class="col-4">
|
||||
<div class="row items-center no-wrap">
|
||||
|
|
@ -732,42 +764,32 @@ watch(
|
|||
{{ $t('general.recordPerPage') }}
|
||||
</div>
|
||||
<div>
|
||||
<q-btn-dropdown
|
||||
dense
|
||||
unelevated
|
||||
:label="pageSize"
|
||||
class="bordered q-pl-md"
|
||||
>
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="v in [10, 30, 50, 100, 500, 1000]"
|
||||
:key="v"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="pageSize = v"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ v }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
<PaginationPageSize v-model="pageSize" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 row justify-center app-text-muted">
|
||||
{{
|
||||
$t('general.recordsPage', {
|
||||
resultcurrentPage: data.length,
|
||||
total: pageState.inputSearch ? data.length : pageState.total,
|
||||
})
|
||||
$q.screen.gt.sm
|
||||
? $t('general.recordsPage', {
|
||||
resultcurrentPage: data.length,
|
||||
total: pageState.inputSearch
|
||||
? data.length
|
||||
: pageState.total,
|
||||
})
|
||||
: $t('general.ofPage', {
|
||||
current: data.length,
|
||||
total: pageState.inputSearch
|
||||
? data.length
|
||||
: pageState.total,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<nav class="col-4 row justify-end">
|
||||
<PaginationComponent
|
||||
v-model:current-page="page"
|
||||
v-model:max-page="pageMax"
|
||||
:fetch-data="fetchData"
|
||||
:fetch-data="() => fetchData()"
|
||||
/>
|
||||
</nav>
|
||||
</footer>
|
||||
|
|
@ -778,7 +800,6 @@ watch(
|
|||
<AgenciesDialog
|
||||
ref="refAgenciesDialog"
|
||||
:data-id="currAgenciesData && currAgenciesData.id"
|
||||
@change-status="triggerChangeStatus"
|
||||
@drawer-delete="
|
||||
() => {
|
||||
if (currAgenciesData) triggerDelete(currAgenciesData.id);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
// NOTE: Library
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { QSelect, useQuasar } from 'quasar';
|
||||
|
||||
// NOTE: Components
|
||||
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||
|
|
@ -19,12 +20,15 @@ import { useRequestList } from 'src/stores/request-list';
|
|||
import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
|
||||
import { dialogWarningClose } from 'src/stores/utils';
|
||||
|
||||
const $q = useQuasar();
|
||||
const navigatorStore = useNavigator();
|
||||
const flowStore = useFlowStore();
|
||||
const requestListStore = useRequestList();
|
||||
const { t } = useI18n();
|
||||
const { data, stats, page, pageMax, pageSize } = storeToRefs(requestListStore);
|
||||
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
|
||||
// NOTE: Variable
|
||||
const pageState = reactive({
|
||||
hideStat: false,
|
||||
|
|
@ -54,7 +58,8 @@ async function fetchList(opts?: { rotateFlowId?: boolean }) {
|
|||
});
|
||||
|
||||
if (ret) {
|
||||
data.value = ret.result;
|
||||
$q.screen.xs ? data.value.push(...ret.result) : (data.value = ret.result);
|
||||
|
||||
pageState.total = ret.total;
|
||||
pageMax.value = Math.ceil(ret.total / pageSize.value);
|
||||
}
|
||||
|
|
@ -89,6 +94,7 @@ function triggerView(opts: { requestData: RequestData }) {
|
|||
}
|
||||
|
||||
onMounted(async () => {
|
||||
pageState.gridView = $q.screen.lt.md ? true : false;
|
||||
navigatorStore.current.title = 'requestList.title';
|
||||
navigatorStore.current.path = [{ text: 'requestList.caption', i18n: true }];
|
||||
|
||||
|
|
@ -96,9 +102,11 @@ onMounted(async () => {
|
|||
await fetchList({ rotateFlowId: true });
|
||||
});
|
||||
|
||||
watch([() => pageState.inputSearch, () => pageState.statusFilter], () =>
|
||||
fetchList({ rotateFlowId: true }),
|
||||
);
|
||||
watch([() => pageState.inputSearch, () => pageState.statusFilter], () => {
|
||||
page.value = 1;
|
||||
data.value = [];
|
||||
fetchList({ rotateFlowId: true });
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="column full-height no-wrap">
|
||||
|
|
@ -185,7 +193,7 @@ watch([() => pageState.inputSearch, () => pageState.statusFilter], () =>
|
|||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="q-mr-md col-12 col-md-3"
|
||||
class="col col-md-3"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="pageState.inputSearch"
|
||||
debounce="200"
|
||||
|
|
@ -193,14 +201,26 @@ watch([() => pageState.inputSearch, () => pageState.statusFilter], () =>
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div
|
||||
class="row col-12 col-md-5 justify-end"
|
||||
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||
style="white-space: nowrap"
|
||||
>
|
||||
<div class="row col-md-5" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-model="pageState.statusFilter"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -315,20 +335,40 @@ watch([() => pageState.inputSearch, () => pageState.statusFilter], () =>
|
|||
<NoData :not-found="!!pageState.inputSearch" />
|
||||
</article>
|
||||
<article v-else class="col surface-2 full-width scroll q-pa-md">
|
||||
<TableRequestList
|
||||
:columns="column"
|
||||
:rows="data"
|
||||
:grid="pageState.gridView"
|
||||
:visible-columns="pageState.fieldSelected"
|
||||
@view="(data) => triggerView({ requestData: data })"
|
||||
@delete="(data) => triggerCancel(data.id)"
|
||||
/>
|
||||
<q-infinite-scroll
|
||||
@load="
|
||||
(_, done) => {
|
||||
if ($q.screen.gt.xs) return;
|
||||
page = page + 1;
|
||||
fetchList({ rotateFlowId: true }).then(() =>
|
||||
done(page >= pageMax),
|
||||
);
|
||||
}
|
||||
"
|
||||
>
|
||||
<TableRequestList
|
||||
:columns="column"
|
||||
:rows="data"
|
||||
:grid="pageState.gridView"
|
||||
:visible-columns="pageState.fieldSelected"
|
||||
@view="(data) => triggerView({ requestData: data })"
|
||||
@delete="(data) => triggerCancel(data.id)"
|
||||
/>
|
||||
<template v-slot:loading>
|
||||
<div
|
||||
v-if="$q.screen.lt.sm && page !== pageMax"
|
||||
class="row justify-center"
|
||||
>
|
||||
<q-spinner-dots color="primary" size="40px" />
|
||||
</div>
|
||||
</template>
|
||||
</q-infinite-scroll>
|
||||
</article>
|
||||
|
||||
<!-- SEC: footer content -->
|
||||
<footer
|
||||
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
||||
v-if="pageMax > 0"
|
||||
v-if="pageMax > 0 && $q.screen.gt.xs"
|
||||
>
|
||||
<div class="col-4">
|
||||
<div class="row items-center no-wrap">
|
||||
|
|
@ -340,10 +380,19 @@ watch([() => pageState.inputSearch, () => pageState.statusFilter], () =>
|
|||
</div>
|
||||
<div class="col-4 row justify-center app-text-muted">
|
||||
{{
|
||||
$t('general.recordsPage', {
|
||||
resultcurrentPage: data.length,
|
||||
total: pageState.inputSearch ? data.length : pageState.total,
|
||||
})
|
||||
$q.screen.gt.sm
|
||||
? $t('general.recordsPage', {
|
||||
resultcurrentPage: data.length,
|
||||
total: pageState.inputSearch
|
||||
? data.length
|
||||
: pageState.total,
|
||||
})
|
||||
: $t('general.ofPage', {
|
||||
current: data.length,
|
||||
total: pageState.inputSearch
|
||||
? data.length
|
||||
: pageState.total,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<nav class="col-4 row justify-end">
|
||||
|
|
|
|||
|
|
@ -265,11 +265,12 @@ function getEmployeeName(
|
|||
@cancel="$emit('delete', props.row)"
|
||||
>
|
||||
<template v-slot:responsiblePerson="{ props: subProps }">
|
||||
<div class="col-4 app-text-muted q-pr-sm self-center">
|
||||
<div class="col-4 app-text-muted q-pr-sm">
|
||||
{{ subProps.label }}
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<AvatarGroup
|
||||
v-if="(responsiblePerson(props.row.quotation) ?? []).length > 0"
|
||||
:data="
|
||||
responsiblePerson(props.row.quotation)?.map((v) => {
|
||||
return {
|
||||
|
|
@ -286,6 +287,7 @@ function getEmployeeName(
|
|||
})
|
||||
"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
</template>
|
||||
</QuotationCard>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// NOTE: Library
|
||||
import { computed, onMounted, reactive, watch, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
// NOTE: Components
|
||||
|
|
@ -21,14 +22,13 @@ import {
|
|||
} from 'src/stores/task-order/types';
|
||||
import { useNavigator } from 'src/stores/navigator';
|
||||
import { useTaskOrderStore } from 'src/stores/task-order';
|
||||
import { useTaskOrderForm } from './form';
|
||||
import useFlowStore from 'src/stores/flow';
|
||||
import { pageTabs, column, pageTabsReceive } from './constants';
|
||||
import { dialogWarningClose, isRoleInclude } from 'src/stores/utils';
|
||||
import { PaginationResult } from 'src/types';
|
||||
|
||||
const { t } = useI18n();
|
||||
const taskOrderFormStore = useTaskOrderForm();
|
||||
const $q = useQuasar();
|
||||
const navigatorStore = useNavigator();
|
||||
const flowStore = useFlowStore();
|
||||
const taskOrderStore = useTaskOrderStore();
|
||||
|
|
@ -138,6 +138,7 @@ async function deleteTaskOrder(id: string) {
|
|||
}
|
||||
|
||||
onMounted(async () => {
|
||||
pageState.gridView = $q.screen.lt.md ? true : false;
|
||||
navigatorStore.current.title = 'taskOrder.title';
|
||||
navigatorStore.current.path = [{ text: 'taskOrder.caption', i18n: true }];
|
||||
fetchTaskOrderList();
|
||||
|
|
@ -284,7 +285,7 @@ watch(
|
|||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="q-mr-md col-12 col-md-3"
|
||||
class="col col-md-3"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="pageState.inputSearch"
|
||||
debounce="200"
|
||||
|
|
@ -294,11 +295,7 @@ watch(
|
|||
</template>
|
||||
</q-input>
|
||||
|
||||
<div
|
||||
class="row col-12 col-md-3 justify-end"
|
||||
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||
style="white-space: nowrap"
|
||||
>
|
||||
<div class="row col-md-5 justify-end" style="white-space: nowrap">
|
||||
<!-- <q-select
|
||||
v-model="pageState.statusFilter"
|
||||
outlined
|
||||
|
|
@ -334,7 +331,7 @@ watch(
|
|||
v-if="!pageState.gridView"
|
||||
id="select-field"
|
||||
for="select-field"
|
||||
class="col"
|
||||
class="col-md-5 q-ml-sm"
|
||||
:options="
|
||||
fieldSelectedOption.map((v) => ({
|
||||
...v,
|
||||
|
|
@ -487,12 +484,19 @@ watch(
|
|||
</div>
|
||||
<div class="col-4 row justify-center app-text-muted">
|
||||
{{
|
||||
$t('general.recordsPage', {
|
||||
resultcurrentPage: taskOrderList.length,
|
||||
total: pageState.inputSearch
|
||||
? taskOrderList.length
|
||||
: pageState.total,
|
||||
})
|
||||
$q.screen.gt.sm
|
||||
? $t('general.recordsPage', {
|
||||
resultcurrentPage: taskOrderList.length,
|
||||
total: pageState.inputSearch
|
||||
? taskOrderList.length
|
||||
: pageState.total,
|
||||
})
|
||||
: $t('general.ofPage', {
|
||||
current: taskOrderList.length,
|
||||
total: pageState.inputSearch
|
||||
? taskOrderList.length
|
||||
: pageState.total,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<nav class="col-4 row justify-end">
|
||||
|
|
|
|||
|
|
@ -212,8 +212,10 @@ function handleCheck(
|
|||
|
||||
function disableCheckAll() {
|
||||
if (!props.selectReady) return false;
|
||||
const firstTemplate = props.rows[0]?._template.id;
|
||||
const hasDifferent = props.rows.some((r) => r._template.id !== firstTemplate);
|
||||
const firstTemplate = props.rows[0]?._template?.id;
|
||||
const hasDifferent = props.rows.some(
|
||||
(r) => r._template?.id !== firstTemplate,
|
||||
);
|
||||
|
||||
return hasDifferent && selectedEmployee.value.length === 0;
|
||||
}
|
||||
|
|
@ -277,7 +279,7 @@ function disableCheckAll() {
|
|||
(selectReady
|
||||
? rows.filter(
|
||||
(t) =>
|
||||
t._template.id ===
|
||||
t._template?.id ===
|
||||
selectedEmployeeInTable[0]?._template?.id,
|
||||
).length
|
||||
: validate
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ onMounted(async () => {
|
|||
})
|
||||
.reduce(
|
||||
(a, c) => {
|
||||
const priceNoVat = c.product.vatIncluded
|
||||
const priceNoVat = c.product.serviceChargeVatIncluded
|
||||
? c.pricePerUnit / (1 + (config.value?.vat || 0.07))
|
||||
: c.pricePerUnit;
|
||||
const adjustedPriceWithVat = precisionRound(
|
||||
|
|
@ -219,9 +219,10 @@ onMounted(async () => {
|
|||
priceUnit: precisionRound(priceNoVat),
|
||||
amount: c.amount,
|
||||
discount: c.discount,
|
||||
vat: c.product.calcVat ? precisionRound(rawVat) : 0,
|
||||
vat: c.product.serviceChargeCalcVat ? precisionRound(rawVat) : 0,
|
||||
value: precisionRound(
|
||||
priceNoVat * c.amount + (c.product.calcVat ? rawVatTotal : 0),
|
||||
priceNoVat * c.amount +
|
||||
(c.product.serviceChargeCalcVat ? rawVatTotal : 0),
|
||||
),
|
||||
});
|
||||
|
||||
|
|
@ -373,9 +374,7 @@ function print() {
|
|||
<td class="text-right">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
summaryPrice.totalPrice -
|
||||
summaryPrice.totalDiscount +
|
||||
summaryPrice.vat,
|
||||
summaryPrice.totalPrice - summaryPrice.totalDiscount,
|
||||
2,
|
||||
)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -56,18 +56,25 @@ function openList(index: number) {
|
|||
}
|
||||
|
||||
function calcPricePerUnit(product: RequestWork['productService']['product']) {
|
||||
return product.vatIncluded
|
||||
? (props.creditNote
|
||||
const val = props.creditNote
|
||||
? props.agentPrice
|
||||
? product.agentPrice
|
||||
: product.price
|
||||
: product.serviceCharge;
|
||||
|
||||
if (
|
||||
product[
|
||||
props.creditNote
|
||||
? props.agentPrice
|
||||
? product.agentPrice
|
||||
: product.price
|
||||
: product.serviceCharge) /
|
||||
(1 + (config.value?.vat || 0.07))
|
||||
: props.creditNote
|
||||
? props.agentPrice
|
||||
? product.agentPrice
|
||||
: product.price
|
||||
: product.serviceCharge;
|
||||
? 'agentPriceCalcVat'
|
||||
: 'calcVat'
|
||||
: 'serviceChargeCalcVat'
|
||||
]
|
||||
) {
|
||||
return val / (1 + (config.value?.vat || 0.07));
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
function calcPrice(
|
||||
|
|
@ -81,12 +88,24 @@ function calcPrice(
|
|||
: product.serviceCharge;
|
||||
const discount =
|
||||
taskProduct.value.find((v) => v.productId === product.id)?.discount || 0;
|
||||
const priceNoVat = product.vatIncluded
|
||||
const priceNoVat = product[
|
||||
props.creditNote
|
||||
? props.agentPrice
|
||||
? 'agentPriceVatIncluded'
|
||||
: 'vatIncluded'
|
||||
: 'serviceChargeVatIncluded'
|
||||
]
|
||||
? pricePerUnit / (1 + (config.value?.vat || 0.07))
|
||||
: pricePerUnit;
|
||||
const priceDiscountNoVat = priceNoVat * amount - discount;
|
||||
|
||||
const rawVatTotal = product.calcVat
|
||||
const rawVatTotal = product[
|
||||
props.creditNote
|
||||
? props.agentPrice
|
||||
? 'agentPriceCalcVat'
|
||||
: 'calcVat'
|
||||
: 'serviceChargeCalcVat'
|
||||
]
|
||||
? priceDiscountNoVat * (config.value?.vat || 0.07)
|
||||
: 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { ref } from 'vue';
|
|||
import { MainButton } from 'src/components/button';
|
||||
|
||||
const props = defineProps<{
|
||||
defaultRemark?: string;
|
||||
readonly?: boolean;
|
||||
itemsDiscount?: {
|
||||
productId: string;
|
||||
|
|
@ -115,6 +116,10 @@ const getToolbarConfig = computed(() => {
|
|||
</div>
|
||||
</template>
|
||||
</q-editor>
|
||||
|
||||
<p class="app-text-muted text-caption">
|
||||
<slot name="hint" />
|
||||
</p>
|
||||
</main>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -15,17 +15,18 @@ import { RequestWork } from 'src/stores/request-list';
|
|||
// NOTE: Import stores
|
||||
import { useTaskOrderStore } from 'src/stores/task-order';
|
||||
|
||||
export const DEFAULT_DATA: TaskOrderPayload = {
|
||||
taskList: [],
|
||||
institutionId: '',
|
||||
contactTel: '',
|
||||
contactName: '',
|
||||
taskName: '',
|
||||
remark: '#[order-detail]',
|
||||
};
|
||||
|
||||
export const useTaskOrderForm = defineStore('task-order-form', () => {
|
||||
const { t } = useI18n();
|
||||
const taskOrderStore = useTaskOrderStore();
|
||||
const DEFAULT_DATA: TaskOrderPayload = {
|
||||
taskList: [],
|
||||
institutionId: '',
|
||||
contactTel: '',
|
||||
contactName: '',
|
||||
taskName: '',
|
||||
remark: '#[order-detail]',
|
||||
};
|
||||
|
||||
const state = ref<{
|
||||
mode: null | 'info' | 'create' | 'edit';
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import FailRemarkDialog from '../receive_view/FailRemarkDialog.vue';
|
|||
import SelectReadyRequestWork from '../SelectReadyRequestWork.vue';
|
||||
import { dialogWarningClose } from 'stores/utils';
|
||||
import useOptionStore from 'src/stores/options';
|
||||
import { useTaskOrderForm } from '../form';
|
||||
import { useTaskOrderForm, DEFAULT_DATA } from '../form';
|
||||
import { useTaskOrderStore } from 'src/stores/task-order';
|
||||
import { dateFormatJS, dateFormat } from 'src/utils/datetime';
|
||||
import { initLang, initTheme } from 'src/utils/ui';
|
||||
|
|
@ -135,7 +135,7 @@ function getPrice(
|
|||
const discount =
|
||||
taskProduct.value.find((v) => v.productId === c.product.id)?.discount ||
|
||||
0;
|
||||
const priceNoVat = c.product.vatIncluded
|
||||
const priceNoVat = c.product.serviceChargeVatIncluded
|
||||
? pricePerUnit / (1 + (config.value?.vat || 0.07))
|
||||
: pricePerUnit;
|
||||
const adjustedPriceWithVat = precisionRound(
|
||||
|
|
@ -149,8 +149,8 @@ function getPrice(
|
|||
|
||||
a.totalPrice = a.totalPrice + priceDiscountNoVat;
|
||||
a.totalDiscount = a.totalDiscount + Number(discount);
|
||||
a.vat = c.product.calcVat ? a.vat + rawVatTotal : a.vat;
|
||||
a.vatExcluded = c.product.calcVat
|
||||
a.vat = c.product.serviceChargeCalcVat ? a.vat + rawVatTotal : a.vat;
|
||||
a.vatExcluded = c.product.serviceChargeCalcVat
|
||||
? a.vatExcluded
|
||||
: precisionRound(a.vatExcluded + priceDiscountNoVat);
|
||||
a.finalPrice = a.totalPrice - a.totalDiscount + a.vat;
|
||||
|
|
@ -982,7 +982,14 @@ watch(
|
|||
:readonly="!['create', 'edit'].includes(state.mode || '')"
|
||||
:items="taskListGroup"
|
||||
:items-discount="taskProduct"
|
||||
/>
|
||||
:default-remark="DEFAULT_DATA.remark"
|
||||
>
|
||||
<template #hint>
|
||||
{{ $t('general.hintRemark') }}
|
||||
<code>#[order-detail]</code>
|
||||
{{ $t('general.orderDetail') }}
|
||||
</template>
|
||||
</RemarkExpansion>
|
||||
|
||||
<template
|
||||
v-if="
|
||||
|
|
@ -1033,55 +1040,63 @@ watch(
|
|||
header-class="text-medium text-body items-center bordered-b "
|
||||
>
|
||||
<template #header>
|
||||
<q-avatar class="q-mr-md" size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/product/${product.id}/image/${product.selectedImage}`"
|
||||
<section class="row items-center full-width">
|
||||
<div class="flex items-center col-sm col-12 no-wrap">
|
||||
<q-avatar class="q-mr-md" size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/product/${product.id}/image/${product.selectedImage}`"
|
||||
>
|
||||
<template #error>
|
||||
<q-icon
|
||||
class="full-width full-height"
|
||||
name="mdi-shopping-outline"
|
||||
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
|
||||
/>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
<span>
|
||||
{{ product.name }}
|
||||
<div class="app-text-muted text-caption">
|
||||
{{ product.code }}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="row items-center q-gutter-x-sm"
|
||||
:class="{ 'q-py-xs': $q.screen.lt.sm }"
|
||||
>
|
||||
<template #error>
|
||||
<div
|
||||
v-for="taskStatus in 3"
|
||||
:key="taskStatus"
|
||||
class="rounded q-px-sm row items-center"
|
||||
style="background: hsl(var(--text-mute) / 0.1)"
|
||||
:style="`color: hsl(var(--${
|
||||
taskStatus === 1
|
||||
? 'warning'
|
||||
: taskStatus === 2
|
||||
? 'positive'
|
||||
: 'negative'
|
||||
}-bg))`"
|
||||
>
|
||||
<q-icon
|
||||
class="full-width full-height"
|
||||
name="mdi-shopping-outline"
|
||||
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
|
||||
name="mdi-account-group-outline"
|
||||
size="xs"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
<span>
|
||||
{{ product.name }}
|
||||
<div class="app-text-muted text-caption">
|
||||
{{ product.code }}
|
||||
</div>
|
||||
</span>
|
||||
<span class="q-ml-auto row items-center q-gutter-x-sm">
|
||||
<div
|
||||
v-for="taskStatus in 3"
|
||||
:key="taskStatus"
|
||||
class="rounded q-px-sm row items-center"
|
||||
style="background: hsl(var(--text-mute) / 0.1)"
|
||||
:style="`color: hsl(var(--${
|
||||
taskStatus === 1
|
||||
? 'warning'
|
||||
: taskStatus === 2
|
||||
? 'positive'
|
||||
: 'negative'
|
||||
}-bg))`"
|
||||
>
|
||||
<q-icon
|
||||
name="mdi-account-group-outline"
|
||||
size="xs"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
{{
|
||||
taskStatusCount(
|
||||
taskStatus,
|
||||
product.id,
|
||||
v.responsibleUser.id,
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</span>
|
||||
{{
|
||||
taskStatusCount(
|
||||
taskStatus,
|
||||
product.id,
|
||||
v.responsibleUser.id,
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</span>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import TaskStatusComponent from '../TaskStatusComponent.vue';
|
|||
import FailRemarkDialog from './FailRemarkDialog.vue';
|
||||
|
||||
// NOTE: Import Types and Store
|
||||
import { dateFormatJS, dateFormat } from 'src/utils/datetime';
|
||||
import { dateFormatJS } from 'src/utils/datetime';
|
||||
import { useTaskOrderForm } from '../form';
|
||||
import { initLang, initTheme, Lang } from 'src/utils/ui';
|
||||
import { RequestWork } from 'src/stores/request-list';
|
||||
|
|
@ -393,17 +393,10 @@ watch([currentFormData.value.taskStatus], () => {
|
|||
: new Date(Date.now()),
|
||||
dayStyle: 'numeric',
|
||||
monthStyle: 'long',
|
||||
locale: $i18n.locale === 'tha' ? 'th-Th' : 'en-US',
|
||||
withTime: true,
|
||||
}),
|
||||
})
|
||||
}}
|
||||
{{
|
||||
dateFormat(
|
||||
fullTaskOrder ? fullTaskOrder.createdAt : new Date(Date.now()),
|
||||
false,
|
||||
true,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -440,42 +433,55 @@ watch([currentFormData.value.taskStatus], () => {
|
|||
class="row items-center surface-1 q-pa-md rounded gradient-stat"
|
||||
>
|
||||
<span
|
||||
class="row col rounded q-px-sm q-py-md info"
|
||||
class="row col rounded q-px-sm q-py-md info justify-end"
|
||||
style="border: 1px solid hsl(var(--info-bg))"
|
||||
>
|
||||
{{ $t('taskOrder.allProduct') }}
|
||||
<span class="q-ml-auto">{{ fullTaskOrder.taskList.length }}</span>
|
||||
<span
|
||||
class="col-sm col-12"
|
||||
:class="{ 'text-right': $q.screen.lt.sm }"
|
||||
>
|
||||
{{ $t('taskOrder.allProduct') }}
|
||||
</span>
|
||||
{{ fullTaskOrder.taskList.length }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="row col rounded q-px-sm q-py-md q-mx-md positive"
|
||||
class="row col rounded q-px-sm q-py-md q-mx-md positive justify-end"
|
||||
style="border: 1px solid hsl(var(--positive-bg))"
|
||||
>
|
||||
{{ $t('taskOrder.alreadySentTask') }}
|
||||
<span class="q-ml-auto">
|
||||
{{
|
||||
fullTaskOrder.taskList.filter(
|
||||
(t) =>
|
||||
t.taskStatus === TaskStatus.Complete ||
|
||||
t.taskStatus === TaskStatus.Success ||
|
||||
t.taskStatus === TaskStatus.Validate ||
|
||||
t.taskStatus === TaskStatus.Redo ||
|
||||
t.taskStatus === TaskStatus.Failed,
|
||||
).length
|
||||
}}
|
||||
<span
|
||||
class="col-sm col-12"
|
||||
:class="{ 'text-right': $q.screen.lt.sm }"
|
||||
>
|
||||
{{ $t('taskOrder.alreadySentTask') }}
|
||||
</span>
|
||||
{{
|
||||
fullTaskOrder.taskList.filter(
|
||||
(t) =>
|
||||
t.taskStatus === TaskStatus.Complete ||
|
||||
t.taskStatus === TaskStatus.Success ||
|
||||
t.taskStatus === TaskStatus.Validate ||
|
||||
t.taskStatus === TaskStatus.Redo ||
|
||||
t.taskStatus === TaskStatus.Failed,
|
||||
).length
|
||||
}}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="row col rounded q-px-sm q-py-md warning"
|
||||
class="row col rounded q-px-sm q-py-md warning justify-end"
|
||||
style="border: 1px solid hsl(var(--warning-bg))"
|
||||
>
|
||||
{{ $t('taskOrder.status.Pending') }}
|
||||
<span class="q-ml-auto">
|
||||
{{
|
||||
fullTaskOrder.taskList.filter(
|
||||
(t) => t.taskStatus === TaskStatus.InProgress,
|
||||
).length
|
||||
}}
|
||||
<span
|
||||
class="col-sm col-12"
|
||||
:class="{ 'text-right': $q.screen.lt.sm }"
|
||||
>
|
||||
{{ $t('taskOrder.status.Pending') }}
|
||||
</span>
|
||||
{{
|
||||
fullTaskOrder.taskList.filter(
|
||||
(t) => t.taskStatus === TaskStatus.InProgress,
|
||||
).length
|
||||
}}
|
||||
</span>
|
||||
</article>
|
||||
|
||||
|
|
@ -553,64 +559,79 @@ watch([currentFormData.value.taskStatus], () => {
|
|||
header-class="q-py-sm text-medium text-body items-center rounded q-mx-md q-my-sm"
|
||||
>
|
||||
<template #header>
|
||||
<q-avatar class="q-mr-md" size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/product/${product.id}/image/${product.selectedImage}`"
|
||||
>
|
||||
<template #error>
|
||||
<q-icon
|
||||
class="full-width full-height"
|
||||
name="mdi-shopping-outline"
|
||||
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
|
||||
/>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
<span>
|
||||
{{ product.name }}
|
||||
<div class="app-text-muted text-caption">
|
||||
{{ product.code }}
|
||||
<section class="row items-center full-width">
|
||||
<div class="flex items-center col-sm col-12 no-wrap">
|
||||
<q-avatar class="q-mr-md" size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/product/${product.id}/image/${product.selectedImage}`"
|
||||
>
|
||||
<template #error>
|
||||
<q-icon
|
||||
class="full-width full-height"
|
||||
name="mdi-shopping-outline"
|
||||
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
|
||||
/>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
<span>
|
||||
{{ product.name }}
|
||||
<div class="app-text-muted text-caption">
|
||||
{{ product.code }}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
fullTaskOrder.taskOrderStatus === TaskOrderStatus.Pending
|
||||
"
|
||||
class="q-ml-auto"
|
||||
>
|
||||
<div
|
||||
class="rounded q-px-sm row items-center"
|
||||
style="background: hsl(var(--text-mute) / 0.15)"
|
||||
>
|
||||
<q-icon
|
||||
name="mdi-account-group-outline"
|
||||
size="xs"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
{{ list.length }}
|
||||
</div>
|
||||
</span>
|
||||
<span v-else class="q-ml-auto row items-center q-gutter-x-sm">
|
||||
<div
|
||||
v-for="v in 3"
|
||||
:key="v"
|
||||
class="rounded q-px-sm row items-center"
|
||||
style="background: hsl(var(--text-mute) / 0.1)"
|
||||
:style="`color: hsl(var(--${
|
||||
v === 1 ? 'warning' : v === 2 ? 'positive' : 'negative'
|
||||
}-bg))`"
|
||||
>
|
||||
<q-icon
|
||||
name="mdi-account-group-outline"
|
||||
size="xs"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
|
||||
{{ taskStatusCount(v, product.id) }}
|
||||
<div
|
||||
class="row items-center"
|
||||
:class="{ 'q-py-xs': $q.screen.lt.sm }"
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
fullTaskOrder.taskOrderStatus ===
|
||||
TaskOrderStatus.Pending
|
||||
"
|
||||
class="q-ml-auto"
|
||||
>
|
||||
<div
|
||||
class="rounded q-px-sm row items-center"
|
||||
style="background: hsl(var(--text-mute) / 0.15)"
|
||||
>
|
||||
<q-icon
|
||||
name="mdi-account-group-outline"
|
||||
size="xs"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
{{ list.length }}
|
||||
</div>
|
||||
</span>
|
||||
<span v-else class="row items-center q-gutter-x-sm">
|
||||
<div
|
||||
v-for="v in 3"
|
||||
:key="v"
|
||||
class="rounded q-px-sm row items-center"
|
||||
style="background: hsl(var(--text-mute) / 0.1)"
|
||||
:style="`color: hsl(var(--${
|
||||
v === 1
|
||||
? 'warning'
|
||||
: v === 2
|
||||
? 'positive'
|
||||
: 'negative'
|
||||
}-bg))`"
|
||||
>
|
||||
<q-icon
|
||||
name="mdi-account-group-outline"
|
||||
size="xs"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
|
||||
{{ taskStatusCount(v, product.id) }}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts" setup>
|
||||
// NOTE: Library
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { QSelect, useQuasar } from 'quasar';
|
||||
|
||||
// NOTE: Components
|
||||
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||
|
|
@ -15,15 +16,15 @@ import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
|
|||
import { useNavigator } from 'src/stores/navigator';
|
||||
import { columns, hslaColors } from './constants';
|
||||
import useFlowStore from 'src/stores/flow';
|
||||
import { useRequestList } from 'src/stores/request-list';
|
||||
import { useInvoice } from 'src/stores/payment';
|
||||
import { Invoice, PaymentDataStatus } from 'src/stores/payment/types';
|
||||
import { Quotation } from 'src/stores/quotations';
|
||||
|
||||
const $q = useQuasar();
|
||||
const navigatorStore = useNavigator();
|
||||
const flowStore = useFlowStore();
|
||||
const invoiceStore = useInvoice();
|
||||
const { data, stats, page, pageMax, pageSize } = storeToRefs(invoiceStore);
|
||||
const refFilter = ref<InstanceType<typeof QSelect>>();
|
||||
|
||||
// NOTE: Variable
|
||||
const pageState = reactive({
|
||||
|
|
@ -57,7 +58,7 @@ async function fetchList(opts?: { rotateFlowId?: boolean }) {
|
|||
debitNoteOnly: false,
|
||||
});
|
||||
if (ret) {
|
||||
data.value = ret.result;
|
||||
data.value = $q.screen.xs ? [...data.value, ...ret.result] : ret.result;
|
||||
pageState.total = ret.total;
|
||||
pageMax.value = Math.ceil(ret.total / pageSize.value);
|
||||
}
|
||||
|
|
@ -74,7 +75,7 @@ async function fetchStats() {
|
|||
}
|
||||
|
||||
function triggerView(opts: { quotationId: string }) {
|
||||
const url = new URL(`/quotation/view?tab=invoice`, window.location.origin);
|
||||
const url = new URL('/quotation/view?tab=invoice', window.location.origin);
|
||||
|
||||
localStorage.setItem(
|
||||
'new-quotation',
|
||||
|
|
@ -105,6 +106,8 @@ function viewDocExample(quotationId: string) {
|
|||
}
|
||||
|
||||
onMounted(async () => {
|
||||
pageState.gridView = $q.screen.lt.md ? true : false;
|
||||
|
||||
navigatorStore.current.title = 'invoice.title';
|
||||
navigatorStore.current.path = [{ text: 'invoice.caption', i18n: true }];
|
||||
|
||||
|
|
@ -117,9 +120,12 @@ watch(
|
|||
() => pageState.inputSearch,
|
||||
() => pageState.statusFilter,
|
||||
() => pageSize.value,
|
||||
() => page.value,
|
||||
],
|
||||
() => fetchList({ rotateFlowId: true }),
|
||||
() => {
|
||||
page.value = 1;
|
||||
data.value = [];
|
||||
fetchList({ rotateFlowId: true });
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
|
|
@ -189,7 +195,7 @@ watch(
|
|||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="q-mr-md col-12 col-md-3"
|
||||
class="col col-md-3"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="pageState.inputSearch"
|
||||
debounce="200"
|
||||
|
|
@ -197,14 +203,26 @@ watch(
|
|||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template v-if="$q.screen.lt.md" v-slot:append>
|
||||
<span class="row">
|
||||
<q-separator vertical />
|
||||
<q-btn
|
||||
icon="mdi-filter-variant"
|
||||
unelevated
|
||||
class="q-ml-sm"
|
||||
padding="4px"
|
||||
size="sm"
|
||||
rounded
|
||||
@click="refFilter?.showPopup"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div
|
||||
class="row col-12 col-md-5 justify-end"
|
||||
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||
style="white-space: nowrap"
|
||||
>
|
||||
<div class="row col-md-5" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-show="$q.screen.gt.sm"
|
||||
ref="refFilter"
|
||||
v-model="pageState.statusFilter"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -311,80 +329,101 @@ watch(
|
|||
<NoData :not-found="!!pageState.inputSearch" />
|
||||
</article>
|
||||
<article v-else class="col surface-2 full-width scroll q-pa-md">
|
||||
<TableInvoice
|
||||
:columns="columns"
|
||||
:rows="data"
|
||||
:grid="pageState.gridView"
|
||||
@view="(quotationId) => triggerView({ quotationId })"
|
||||
@preview="(quotationId) => viewDocExample(quotationId)"
|
||||
<q-infinite-scroll
|
||||
:offset="100"
|
||||
@load="
|
||||
(_, done) => {
|
||||
if ($q.screen.gt.xs || page === pageMax) return;
|
||||
page = page + 1;
|
||||
fetchList({ rotateFlowId: true }).then(() =>
|
||||
done(page >= pageMax),
|
||||
);
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #grid="{ item }">
|
||||
<div class="col-md-4 col-sm-6 col-12">
|
||||
<QuotationCard
|
||||
hide-action
|
||||
:code="item.row.code"
|
||||
:title="item.row.quotation.workName"
|
||||
:status="
|
||||
$t(`invoice.status.${item.row.payment.paymentStatus}`)
|
||||
"
|
||||
:badge-color="
|
||||
hslaColors[item.row.payment.paymentStatus] || ''
|
||||
"
|
||||
:custom-data="[
|
||||
{
|
||||
label: $t('general.customer'),
|
||||
value:
|
||||
item.row.quotation.customerBranch.registerName ||
|
||||
`${item.row.quotation.customerBranch?.firstName || '-'} ${item.row.quotation.customerBranch?.lastName || ''}`,
|
||||
},
|
||||
{
|
||||
label: $t('taskOrder.issueDate'),
|
||||
value: new Date(item.row.createdAt).toLocaleString(
|
||||
'th-TH',
|
||||
{
|
||||
hour12: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
label: $t('invoice.paymentDueDate'),
|
||||
value: new Date(
|
||||
item.row.quotation.dueDate,
|
||||
).toLocaleDateString('th-TH', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: 'numeric',
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: $t('quotation.payType'),
|
||||
value: $t(
|
||||
`quotation.type.${item.row.quotation.payCondition}`,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: $t('preview.netValue'),
|
||||
value: item.row.amount,
|
||||
},
|
||||
]"
|
||||
@view="
|
||||
() => triggerView({ quotationId: item.row.quotation.id })
|
||||
"
|
||||
@preview="
|
||||
() => {
|
||||
viewDocExample(item.row.quotation.id);
|
||||
}
|
||||
"
|
||||
/>
|
||||
<TableInvoice
|
||||
:columns="columns"
|
||||
:rows="data"
|
||||
:grid="pageState.gridView"
|
||||
@view="(quotationId) => triggerView({ quotationId })"
|
||||
@preview="(quotationId) => viewDocExample(quotationId)"
|
||||
>
|
||||
<template #grid="{ item }">
|
||||
<div class="col-md-4 col-sm-6 col-12">
|
||||
<QuotationCard
|
||||
hide-action
|
||||
:code="item.row.code"
|
||||
:title="item.row.quotation.workName"
|
||||
:status="
|
||||
$t(`invoice.status.${item.row.payment.paymentStatus}`)
|
||||
"
|
||||
:badge-color="
|
||||
hslaColors[item.row.payment.paymentStatus] || ''
|
||||
"
|
||||
:custom-data="[
|
||||
{
|
||||
label: $t('general.customer'),
|
||||
value:
|
||||
item.row.quotation.customerBranch.registerName ||
|
||||
`${item.row.quotation.customerBranch?.firstName || '-'} ${item.row.quotation.customerBranch?.lastName || ''}`,
|
||||
},
|
||||
{
|
||||
label: $t('taskOrder.issueDate'),
|
||||
value: new Date(item.row.createdAt).toLocaleString(
|
||||
'th-TH',
|
||||
{
|
||||
hour12: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
label: $t('invoice.paymentDueDate'),
|
||||
value: new Date(
|
||||
item.row.quotation.dueDate,
|
||||
).toLocaleDateString('th-TH', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: 'numeric',
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: $t('quotation.payType'),
|
||||
value: $t(
|
||||
`quotation.type.${item.row.quotation.payCondition}`,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: $t('preview.netValue'),
|
||||
value: item.row.amount,
|
||||
},
|
||||
]"
|
||||
@view="
|
||||
() => triggerView({ quotationId: item.row.quotation.id })
|
||||
"
|
||||
@preview="
|
||||
() => {
|
||||
viewDocExample(item.row.quotation.id);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</TableInvoice>
|
||||
<template v-slot:loading>
|
||||
<div
|
||||
v-if="$q.screen.lt.sm && page !== pageMax"
|
||||
class="row justify-center"
|
||||
>
|
||||
<q-spinner-dots color="primary" size="40px" />
|
||||
</div>
|
||||
</template>
|
||||
</TableInvoice>
|
||||
</q-infinite-scroll>
|
||||
</article>
|
||||
|
||||
<!-- SEC: footer content -->
|
||||
<footer
|
||||
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
||||
v-if="pageMax > 0"
|
||||
v-if="pageMax > 0 && $q.screen.gt.xs"
|
||||
>
|
||||
<div class="col-4">
|
||||
<div class="row items-center no-wrap">
|
||||
|
|
@ -396,10 +435,19 @@ watch(
|
|||
</div>
|
||||
<div class="col-4 row justify-center app-text-muted">
|
||||
{{
|
||||
$t('general.recordsPage', {
|
||||
resultcurrentPage: data.length,
|
||||
total: pageState.inputSearch ? data.length : pageState.total,
|
||||
})
|
||||
$q.screen.gt.sm
|
||||
? $t('general.recordsPage', {
|
||||
resultcurrentPage: data.length,
|
||||
total: pageState.inputSearch
|
||||
? data.length
|
||||
: pageState.total,
|
||||
})
|
||||
: $t('general.ofPage', {
|
||||
current: data.length,
|
||||
total: pageState.inputSearch
|
||||
? data.length
|
||||
: pageState.total,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<nav class="col-4 row justify-end">
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ defineEmits<{
|
|||
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||
>
|
||||
<q-tr :class="{ dark: $q.dark.isActive }" class="text-center">
|
||||
<q-td v-for="col in columns" :align="col.align">
|
||||
<q-td v-for="col in columns" :align="col.align" :key="col.name">
|
||||
<!-- NOTE: custom column will starts with # -->
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
<span>
|
||||
|
|
@ -92,8 +92,10 @@ defineEmits<{
|
|||
</template>
|
||||
<template v-if="col.name === '#order'">
|
||||
{{
|
||||
col.field(props.row) +
|
||||
(invoiceStore.page - 1) * invoiceStore.pageSize
|
||||
$q.screen.xs
|
||||
? props.rowIndex + 1
|
||||
: col.field(props.row) +
|
||||
(invoiceStore.page - 1) * invoiceStore.pageSize
|
||||
}}
|
||||
</template>
|
||||
<template v-if="col.name === '#status'">
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import AdditionalFileExpansion from '../09_task-order/expansion/AdditionalFileEx
|
|||
import PaymentExpansion from './expansion/PaymentExpansion.vue';
|
||||
import CreditNoteExpansion from './expansion/CreditNoteExpansion.vue';
|
||||
import StateButton from 'src/components/button/StateButton.vue';
|
||||
import ProductExpansion from '../09_task-order/expansion/ProductExpansion.vue';
|
||||
import ProductExpansion from './expansion/ProductExpansion.vue';
|
||||
import SelectReadyRequestWork from '../09_task-order/SelectReadyRequestWork.vue';
|
||||
import RefundInformation from './RefundInformation.vue';
|
||||
import QuotationFormReceipt from '../05_quotation/QuotationFormReceipt.vue';
|
||||
|
|
@ -87,6 +87,8 @@ const pageState = reactive({
|
|||
fileDialog: false,
|
||||
});
|
||||
|
||||
const defaultRemark = '#[quotation-labor]<br/><br/>#[quotation-payment]';
|
||||
|
||||
const formData = ref<CreditNotePayload>({
|
||||
quotationId: '',
|
||||
requestWorkId: [],
|
||||
|
|
@ -96,7 +98,7 @@ const formData = ref<CreditNotePayload>({
|
|||
paybackBank: '',
|
||||
paybackAccount: '',
|
||||
paybackAccountName: '',
|
||||
remark: '#[quotation-labor]<br/><br/>#[quotation-payment]',
|
||||
remark: defaultRemark,
|
||||
});
|
||||
|
||||
const formTaskList = ref<
|
||||
|
|
@ -109,7 +111,7 @@ const formTaskList = ref<
|
|||
let taskListGroup = computed(() => {
|
||||
const cacheData = formTaskList.value.reduce<
|
||||
{
|
||||
product: RequestWork['productService']['product'];
|
||||
product: RequestWork['productService'];
|
||||
list: RequestWork[];
|
||||
}[]
|
||||
>((acc, curr) => {
|
||||
|
|
@ -127,7 +129,7 @@ let taskListGroup = computed(() => {
|
|||
exist.list.push(task.requestWork);
|
||||
} else {
|
||||
acc.push({
|
||||
product: task.requestWork.productService.product,
|
||||
product: task.requestWork.productService,
|
||||
list: [record],
|
||||
});
|
||||
}
|
||||
|
|
@ -187,37 +189,28 @@ async function initStatus() {
|
|||
|
||||
function getPrice(
|
||||
list: {
|
||||
product: RequestWork['productService']['product'];
|
||||
product: RequestWork['productService'];
|
||||
list: RequestWork[];
|
||||
}[],
|
||||
) {
|
||||
return list.reduce(
|
||||
(a, c) => {
|
||||
const pricePerUnit = quotationData.value?.agentPrice
|
||||
? c.product.agentPrice
|
||||
: c.product.price;
|
||||
const pricePerUnit =
|
||||
c.product.pricePerUnit - c.product.discount / c.product.amount;
|
||||
const amount = c.list.length;
|
||||
const discount = 0;
|
||||
const priceNoVat = c.product.vatIncluded
|
||||
? pricePerUnit / (1 + (config.value?.vat || 0.07))
|
||||
: pricePerUnit;
|
||||
const priceDiscountNoVat = priceNoVat * amount - discount;
|
||||
const priceNoVat = pricePerUnit;
|
||||
const priceDiscountNoVat = priceNoVat * amount;
|
||||
|
||||
const rawVatTotal = priceDiscountNoVat * (config.value?.vat || 0.07);
|
||||
// const rawVat = rawVatTotal / amount;
|
||||
|
||||
a.totalPrice = a.totalPrice + priceDiscountNoVat;
|
||||
a.totalDiscount = a.totalDiscount + Number(discount);
|
||||
a.vat = c.product.calcVat ? a.vat + rawVatTotal : a.vat;
|
||||
a.vatExcluded = c.product.calcVat ? a.vatExcluded : a.vat + rawVatTotal;
|
||||
a.finalPrice = a.totalPrice - a.totalDiscount + a.vat;
|
||||
a.vat = c.product.vat !== 0 ? a.vat + rawVatTotal : a.vat;
|
||||
a.finalPrice = a.totalPrice + a.vat;
|
||||
return a;
|
||||
},
|
||||
{
|
||||
totalPrice: 0,
|
||||
totalDiscount: 0,
|
||||
vat: 0,
|
||||
vatExcluded: 0,
|
||||
finalPrice: 0,
|
||||
},
|
||||
);
|
||||
|
|
@ -747,12 +740,22 @@ onMounted(async () => {
|
|||
"
|
||||
/>
|
||||
|
||||
<!-- TODO: bind remark -->
|
||||
<RemarkExpansion
|
||||
v-if="view !== CreditNoteStatus.Success"
|
||||
:readonly="readonly"
|
||||
v-model:remark="formData.remark"
|
||||
/>
|
||||
:default-remark="defaultRemark"
|
||||
:items="[]"
|
||||
:readonly
|
||||
>
|
||||
<template #hint>
|
||||
{{ $t('general.hintRemark') }}
|
||||
<code>#[quotation-labor]</code>
|
||||
{{ $t('general.quotationLabor') }}
|
||||
{{ $t('general.or') }}
|
||||
<code>#[quotation-payment]</code>
|
||||
{{ $t('general.quotationPayment') }}
|
||||
</template>
|
||||
</RemarkExpansion>
|
||||
|
||||
<QuotationFormReceipt
|
||||
v-if="creditNoteData && view === CreditNoteStatus.Success"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
// NOTE: Components
|
||||
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||
|
|
@ -24,6 +25,7 @@ import { CreditNoteStatus, useCreditNote } from 'src/stores/credit-note';
|
|||
import TableCreditNote from './TableCreditNote.vue';
|
||||
import { dialogWarningClose } from 'src/stores/utils';
|
||||
|
||||
const $q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
const flow = useFlowStore();
|
||||
const navigator = useNavigator();
|
||||
|
|
@ -115,6 +117,7 @@ function close() {
|
|||
}
|
||||
|
||||
onMounted(async () => {
|
||||
pageState.gridView = $q.screen.lt.md ? true : false;
|
||||
navigator.current.title = 'creditNote.title';
|
||||
navigator.current.path = [{ text: 'creditNote.caption', i18n: true }];
|
||||
|
||||
|
|
@ -209,7 +212,7 @@ watch(
|
|||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="q-mr-md col-12 col-md-3"
|
||||
class="col col-md-3"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="pageState.inputSearch"
|
||||
debounce="200"
|
||||
|
|
@ -219,16 +222,12 @@ watch(
|
|||
</template>
|
||||
</q-input>
|
||||
|
||||
<div
|
||||
class="row col-12 col-md-3 justify-end"
|
||||
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||
style="white-space: nowrap"
|
||||
>
|
||||
<div class="row col-md-5 justify-end" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-if="!pageState.gridView"
|
||||
id="select-field"
|
||||
for="select-field"
|
||||
class="col"
|
||||
class="col-md-5 q-ml-sm"
|
||||
:options="
|
||||
fieldSelectedOption.map((v) => ({
|
||||
...v,
|
||||
|
|
@ -413,10 +412,19 @@ watch(
|
|||
</div>
|
||||
<div class="col-4 row justify-center app-text-muted">
|
||||
{{
|
||||
$t('general.recordsPage', {
|
||||
resultcurrentPage: data.length,
|
||||
total: pageState.inputSearch ? data.length : pageState.total,
|
||||
})
|
||||
$q.screen.gt.sm
|
||||
? $t('general.recordsPage', {
|
||||
resultcurrentPage: data.length,
|
||||
total: pageState.inputSearch
|
||||
? data.length
|
||||
: pageState.total,
|
||||
})
|
||||
: $t('general.ofPage', {
|
||||
current: data.length,
|
||||
total: pageState.inputSearch
|
||||
? data.length
|
||||
: pageState.total,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<nav class="col-4 row justify-end">
|
||||
|
|
|
|||
|
|
@ -125,31 +125,40 @@ const refundOpts = ref<
|
|||
class="row col-12 items-center surface-1 q-py-sm rounded gradient-stat"
|
||||
>
|
||||
<span
|
||||
class="row col rounded q-px-sm q-py-md info"
|
||||
class="row col rounded q-px-sm q-py-md info justify-end"
|
||||
style="border: 1px solid hsl(var(--info-bg))"
|
||||
>
|
||||
{{ $t('creditNote.label.totalAmount') }}
|
||||
<span class="q-ml-auto">
|
||||
{{ formatNumberDecimal(total) }}
|
||||
<span
|
||||
class="col-sm col-12"
|
||||
:class="{ 'text-right': $q.screen.lt.sm }"
|
||||
>
|
||||
{{ $t('creditNote.label.totalAmount') }}
|
||||
</span>
|
||||
{{ formatNumberDecimal(total) }}
|
||||
</span>
|
||||
<span
|
||||
class="row col rounded q-px-sm q-mx-md q-py-md positive"
|
||||
class="row col rounded q-px-sm q-mx-md q-py-md positive justify-end"
|
||||
style="border: 1px solid hsl(var(--positive-bg))"
|
||||
>
|
||||
{{ $t('creditNote.label.paid') }}
|
||||
<span class="q-ml-auto">
|
||||
{{ formatNumberDecimal(paid) }}
|
||||
<span
|
||||
class="col-sm col-12"
|
||||
:class="{ 'text-right': $q.screen.lt.sm }"
|
||||
>
|
||||
{{ $t('creditNote.label.paid') }}
|
||||
</span>
|
||||
{{ formatNumberDecimal(paid) }}
|
||||
</span>
|
||||
<span
|
||||
class="row col rounded q-px-sm q-py-md warning"
|
||||
class="row col rounded q-px-sm q-py-md warning justify-end"
|
||||
style="border: 1px solid hsl(var(--warning-bg))"
|
||||
>
|
||||
{{ $t('creditNote.label.remain') }}
|
||||
<span class="q-ml-auto">
|
||||
{{ formatNumberDecimal(remain) }}
|
||||
<span
|
||||
class="col-sm col-12"
|
||||
:class="{ 'text-right': $q.screen.lt.sm }"
|
||||
>
|
||||
{{ $t('creditNote.label.remain') }}
|
||||
</span>
|
||||
{{ formatNumberDecimal(remain) }}
|
||||
</span>
|
||||
</article>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -76,3 +76,60 @@ export const hslaColors: Record<string, string> = {
|
|||
Pending: '--orange-5-hsl',
|
||||
Success: '--blue-6-hsl',
|
||||
};
|
||||
|
||||
export const productColumn = [
|
||||
{
|
||||
name: 'order',
|
||||
align: 'center',
|
||||
label: 'general.order',
|
||||
field: 'no',
|
||||
},
|
||||
{
|
||||
name: 'code',
|
||||
align: 'center',
|
||||
label: 'productService.product.code',
|
||||
field: 'code',
|
||||
},
|
||||
{
|
||||
name: 'productList',
|
||||
align: 'center',
|
||||
label: 'taskOrder.productList',
|
||||
field: 'productList',
|
||||
},
|
||||
{
|
||||
name: 'amountOfEmployee',
|
||||
align: 'center',
|
||||
label: 'taskOrder.amountOfEmployee',
|
||||
field: 'amountOfEmployee',
|
||||
},
|
||||
{
|
||||
name: 'pricePerUnit',
|
||||
align: 'center',
|
||||
label: 'quotation.pricePerUnit',
|
||||
field: 'pricePerUnit',
|
||||
},
|
||||
{
|
||||
name: 'discount',
|
||||
align: 'center',
|
||||
label: 'general.discount',
|
||||
field: 'discount',
|
||||
},
|
||||
{
|
||||
name: 'priceBeforeVat',
|
||||
align: 'center',
|
||||
label: 'quotation.priceBeforeVat',
|
||||
field: 'priceBeforeVat',
|
||||
},
|
||||
{
|
||||
name: 'vat',
|
||||
align: 'center',
|
||||
label: 'general.vat',
|
||||
field: 'vat',
|
||||
},
|
||||
{
|
||||
name: 'totalPriceBaht',
|
||||
align: 'center',
|
||||
label: 'quotation.totalPriceBaht',
|
||||
field: 'totalPriceBaht',
|
||||
},
|
||||
] as const satisfies QTableProps['columns'];
|
||||
|
|
|
|||
232
src/pages/11_credit-note/expansion/ProductExpansion.vue
Normal file
232
src/pages/11_credit-note/expansion/ProductExpansion.vue
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { QTableSlots } from 'quasar';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { AddButton } from 'src/components/button';
|
||||
import TableEmployee from '../../09_task-order/TableEmployee.vue';
|
||||
|
||||
import { useConfigStore } from 'stores/config';
|
||||
import { RequestWork } from 'src/stores/request-list';
|
||||
import { baseUrl, formatNumberDecimal } from 'src/stores/utils';
|
||||
import { precisionRound } from 'src/utils/arithmetic';
|
||||
import { productColumn } from '../constants';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const { data: config } = storeToRefs(configStore);
|
||||
|
||||
const currentExpanded = ref<boolean[]>([]);
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
taskList: {
|
||||
product: RequestWork['productService'];
|
||||
list: RequestWork[];
|
||||
}[];
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'addProduct'): void;
|
||||
}>();
|
||||
|
||||
defineExpose({ calcPricePerUnit });
|
||||
|
||||
function openList(index: number) {
|
||||
if (!currentExpanded.value[index]) {
|
||||
currentExpanded.value.map((_, i) => {
|
||||
if (i !== index) currentExpanded.value[i] = false;
|
||||
});
|
||||
}
|
||||
currentExpanded.value[index] = !currentExpanded.value[index];
|
||||
}
|
||||
|
||||
function calcPricePerUnit(product: RequestWork['productService']) {
|
||||
return product.pricePerUnit - product.discount / product.amount;
|
||||
}
|
||||
|
||||
function calcPrice(c: RequestWork['productService'], amount: number) {
|
||||
const pricePerUnit = c.pricePerUnit - c.discount / c.amount;
|
||||
const priceNoVat = pricePerUnit;
|
||||
const priceDiscountNoVat = priceNoVat * amount;
|
||||
|
||||
const rawVatTotal =
|
||||
c.vat === 0 ? 0 : priceDiscountNoVat * (config.value?.vat || 0.07);
|
||||
|
||||
return precisionRound(priceNoVat * amount + rawVatTotal);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<q-expansion-item
|
||||
dense
|
||||
class="overflow-hidden bordered full-width"
|
||||
switch-toggle-side
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||
default-opened
|
||||
>
|
||||
<template #header>
|
||||
<span
|
||||
class="row items-center justify-between full-width"
|
||||
style="min-height: 31.01px"
|
||||
>
|
||||
{{ $t('general.information', { msg: $t('taskOrder.productList') }) }}
|
||||
<AddButton
|
||||
icon-only
|
||||
@click.stop="$emit('addProduct')"
|
||||
v-if="!readonly"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<main class="q-px-md q-py-sm surface-1">
|
||||
<q-table
|
||||
:columns="
|
||||
productColumn.filter(
|
||||
(v) =>
|
||||
v.name !== 'discount' &&
|
||||
v.name !== 'priceBeforeVat' &&
|
||||
v.name !== 'vat',
|
||||
)
|
||||
"
|
||||
:rows="taskList"
|
||||
bordered
|
||||
flat
|
||||
hide-pagination
|
||||
card-container-class="q-col-gutter-sm"
|
||||
class="full-width"
|
||||
:rows-per-page-options="[0]"
|
||||
:no-data-label="$t('general.noDataTable')"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||
:props="props"
|
||||
>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label && $t(col.label) }}
|
||||
{{ col.label === 'quotation.vat' ? '%' : '' }}
|
||||
</q-th>
|
||||
<q-th></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-slot:body="props: {
|
||||
row: {
|
||||
product: RequestWork['productService'];
|
||||
list: RequestWork[];
|
||||
};
|
||||
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||
>
|
||||
<q-tr class="text-center">
|
||||
<q-td>
|
||||
{{ props.rowIndex + 1 }}
|
||||
</q-td>
|
||||
<q-td>
|
||||
{{ props.row.product.product.code }}
|
||||
</q-td>
|
||||
<q-td style="width: 100%" class="text-left">
|
||||
<q-avatar class="q-mr-sm" size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/product/${props.row.product.id}/image/${props.row.product.product.selectedImage}`"
|
||||
>
|
||||
<template #error>
|
||||
<q-icon
|
||||
class="full-width full-height"
|
||||
name="mdi-shopping-outline"
|
||||
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
|
||||
/>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
{{ props.row.product.product.name }}
|
||||
</q-td>
|
||||
<q-td>
|
||||
{{ props.row.list.length }}
|
||||
</q-td>
|
||||
<q-td class="text-right">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
calcPricePerUnit(props.row.product) +
|
||||
(props.row.product.vat > 0
|
||||
? calcPricePerUnit(props.row.product) *
|
||||
(config?.vat || 0.07)
|
||||
: 0),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
</q-td>
|
||||
|
||||
<!-- total -->
|
||||
<q-td class="text-right">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
calcPrice(props.row.product, props.row.list.length),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
class="rounded"
|
||||
@click.stop="openList(props.rowIndex)"
|
||||
>
|
||||
<div class="row items-center no-wrap">
|
||||
<q-icon name="mdi-account-group-outline" />
|
||||
<q-icon
|
||||
class="btn-arrow-right"
|
||||
:class="{
|
||||
active: currentExpanded[props.rowIndex],
|
||||
}"
|
||||
size="xs"
|
||||
:name="`mdi-chevron-${currentExpanded[props.rowIndex] ? 'down' : 'up'}`"
|
||||
/>
|
||||
</div>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
|
||||
<q-tr v-show="currentExpanded[props.rowIndex]" :props="props">
|
||||
<q-td colspan="100%" style="padding: 16px">
|
||||
<TableEmployee :rows="props.row.list" />
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<div class="q-pt-md row items-center">
|
||||
<span class="q-ml-auto q-mr-sm">
|
||||
{{
|
||||
$t('general.numberOf', {
|
||||
msg: $t('productService.product.product'),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<div class="surface-3 q-px-sm rounded">{{ taskList.length }}</div>
|
||||
</div>
|
||||
</main>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
<style scoped>
|
||||
.product-status {
|
||||
padding-left: 8px;
|
||||
border-radius: 20px;
|
||||
color: hsl(var(--_color));
|
||||
background: hsla(var(--_color) / 0.15);
|
||||
|
||||
&.warning {
|
||||
--_color: var(--warning-bg);
|
||||
}
|
||||
&.positive {
|
||||
--_color: var(--positive-bg);
|
||||
}
|
||||
&.negative {
|
||||
--_color: var(--negative-bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1322
src/pages/12_debit-note/FormPage.vue
Normal file
1322
src/pages/12_debit-note/FormPage.vue
Normal file
File diff suppressed because it is too large
Load diff
505
src/pages/12_debit-note/MainPage.vue
Normal file
505
src/pages/12_debit-note/MainPage.vue
Normal file
|
|
@ -0,0 +1,505 @@
|
|||
<script lang="ts" setup>
|
||||
// NOTE: Library
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
// NOTE: Components
|
||||
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||
import NoData from 'src/components/NoData.vue';
|
||||
import PaginationComponent from 'src/components/PaginationComponent.vue';
|
||||
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||
import FloatingActionButton from 'components/FloatingActionButton.vue';
|
||||
import DialogFormContainer from 'src/components/dialog/DialogFormContainer.vue';
|
||||
import DialogHeader from 'src/components/dialog/DialogHeader.vue';
|
||||
import { CancelButton, SaveButton } from 'src/components/button';
|
||||
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
|
||||
import FormDebit from 'src/components/12_debit-note/FormDebit.vue';
|
||||
import TableDebitNote from './TableDebitNote.vue';
|
||||
|
||||
// NOTE: Stores & Type
|
||||
import { useNavigator } from 'src/stores/navigator';
|
||||
import useFlowStore from 'src/stores/flow';
|
||||
import { pageTabs, columns, hslaColors } from './constants';
|
||||
import { DebitNoteStatus, useDebitNote } from 'src/stores/debit-note';
|
||||
import { dialogWarningClose } from 'src/stores/utils';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
const $q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
const flow = useFlowStore();
|
||||
const navigator = useNavigator();
|
||||
const debitNote = useDebitNote();
|
||||
const { stats, pageMax, page, data, pageSize } = storeToRefs(debitNote);
|
||||
|
||||
// NOTE: Variable
|
||||
const pageState = reactive({
|
||||
quotationId: '',
|
||||
currentTab: DebitNoteStatus.Pending,
|
||||
hideStat: false,
|
||||
statusFilter: 'None',
|
||||
inputSearch: '',
|
||||
fieldSelected: columns
|
||||
.filter((v) => !v.name.startsWith('#'))
|
||||
.map((v) => v.name),
|
||||
gridView: false,
|
||||
total: 0,
|
||||
|
||||
debitDialog: false,
|
||||
});
|
||||
|
||||
const fieldSelectedOption = computed(() => {
|
||||
return columns
|
||||
.filter((v) => !v.name.startsWith('#'))
|
||||
.map((v) => ({
|
||||
label: v.label,
|
||||
value: v.name,
|
||||
}));
|
||||
});
|
||||
|
||||
// NOTE: Function
|
||||
|
||||
async function getList(opts?: { page?: number; pageSize?: number }) {
|
||||
const res = await debitNote.getDebitNoteList({
|
||||
page: opts?.page || page.value,
|
||||
pageSize: opts?.pageSize || pageSize.value,
|
||||
query: pageState.inputSearch === '' ? undefined : pageState.inputSearch,
|
||||
deebitNoteStatus: pageState.currentTab as DebitNoteStatus | undefined,
|
||||
includeRegisteredBranch: true,
|
||||
});
|
||||
|
||||
if (res) {
|
||||
data.value = res.result;
|
||||
pageState.total = res.total;
|
||||
pageMax.value = Math.ceil(res.total / pageSize.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerDelete(id: string) {
|
||||
dialogWarningClose(t, {
|
||||
message: t('dialog.message.confirmDelete'),
|
||||
actionText: t('dialog.action.ok'),
|
||||
action: async () => {
|
||||
const res = await debitNote.deleteDebitNote(id);
|
||||
if (!!res) {
|
||||
getList();
|
||||
}
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
async function triggerCreateDebitNote() {
|
||||
pageState.debitDialog = true;
|
||||
}
|
||||
|
||||
function navigateTo(opts: {
|
||||
statusDialog: 'info' | 'edit' | 'create';
|
||||
quotationId?: string;
|
||||
debitId?: string;
|
||||
}) {
|
||||
const url = new URL(
|
||||
`/debit-note/${opts.statusDialog === 'create' ? 'add' : opts.debitId}`,
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
if (opts.statusDialog === 'create') {
|
||||
url.searchParams.append('quotationId', opts.quotationId || '');
|
||||
}
|
||||
|
||||
url.searchParams.append('mode', opts.statusDialog);
|
||||
|
||||
window.open(url.toString(), '_blank');
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
navigateTo({ statusDialog: 'create', quotationId: pageState.quotationId });
|
||||
}
|
||||
|
||||
function close() {
|
||||
pageState.debitDialog = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
pageState.gridView = $q.screen.lt.md ? true : false;
|
||||
navigator.current.title = 'debitNote.title';
|
||||
navigator.current.path = [{ text: 'debitNote.caption', i18n: true }];
|
||||
|
||||
debitNote.getDebitNoteStats().then((res) => res && (stats.value = res));
|
||||
getList();
|
||||
});
|
||||
|
||||
watch(
|
||||
[
|
||||
() => pageState.currentTab,
|
||||
() => pageState.inputSearch,
|
||||
() => pageSize.value,
|
||||
() => pageState.statusFilter,
|
||||
],
|
||||
() => getList(),
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<!-- #TODO add trigger dialog add debit -->
|
||||
<FloatingActionButton
|
||||
style="z-index: 999"
|
||||
hide-icon
|
||||
@click.stop="() => triggerCreateDebitNote()"
|
||||
></FloatingActionButton>
|
||||
|
||||
<div class="column full-height no-wrap">
|
||||
<!-- SEC: stat -->
|
||||
<section class="text-body-2 q-mb-xs flex items-center">
|
||||
{{ $t('general.dataSum') }}
|
||||
<q-badge
|
||||
rounded
|
||||
class="q-ml-sm"
|
||||
style="
|
||||
background-color: hsla(var(--info-bg) / 0.15);
|
||||
color: hsl(var(--info-bg));
|
||||
"
|
||||
>
|
||||
{{ pageState.total }}
|
||||
</q-badge>
|
||||
<q-btn
|
||||
class="q-ml-sm"
|
||||
icon="mdi-pin-outline"
|
||||
color="primary"
|
||||
size="sm"
|
||||
flat
|
||||
dense
|
||||
rounded
|
||||
@click="pageState.hideStat = !pageState.hideStat"
|
||||
:style="pageState.hideStat ? 'rotate: 90deg' : ''"
|
||||
style="transition: 0.1s ease-in-out"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<transition name="slide">
|
||||
<div v-if="!pageState.hideStat" class="scroll q-mb-md">
|
||||
<div style="display: inline-block">
|
||||
<StatCardComponent
|
||||
labelI18n
|
||||
:branch="[
|
||||
{
|
||||
icon: 'material-symbols-light:receipt-long',
|
||||
count: stats[DebitNoteStatus.Pending] || 0,
|
||||
label: `debitNote.stats.${DebitNoteStatus.Pending}`,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-clock-alert-outline',
|
||||
count: stats[DebitNoteStatus.Expire] || 0,
|
||||
label: `debitNote.stats.${DebitNoteStatus.Expire}`,
|
||||
color: 'cyan',
|
||||
},
|
||||
{
|
||||
icon: 'tabler:cash-register',
|
||||
count: stats[DebitNoteStatus.Payment] || 0,
|
||||
label: `debitNote.stats.${DebitNoteStatus.Payment}`,
|
||||
color: 'dark-orange',
|
||||
},
|
||||
{
|
||||
icon: 'fluent:receipt-money-16-regular',
|
||||
count: stats[DebitNoteStatus.Receipt] || 0,
|
||||
label: `debitNote.stats.${DebitNoteStatus.Receipt}`,
|
||||
color: 'green',
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'mdi-check-decagram-outline',
|
||||
count: stats[DebitNoteStatus.Succeed] || 0,
|
||||
label: `debitNote.stats.${DebitNoteStatus.Succeed}`,
|
||||
color: 'blue',
|
||||
},
|
||||
]"
|
||||
:dark="$q.dark.isActive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<section class="col surface-1 rounded bordered overflow-hidden">
|
||||
<div class="column full-height">
|
||||
<!-- SEC: header content -->
|
||||
<header
|
||||
class="row surface-3 justify-between full-width items-center"
|
||||
style="z-index: 1"
|
||||
>
|
||||
<section
|
||||
class="row q-py-sm q-px-md bordered-b justify-between full-width"
|
||||
>
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="col col-md-3"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="pageState.inputSearch"
|
||||
debounce="200"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row col-md-5 justify-end" style="white-space: nowrap">
|
||||
<q-select
|
||||
v-if="!pageState.gridView"
|
||||
id="select-field"
|
||||
for="select-field"
|
||||
class="col-md-5 q-ml-sm"
|
||||
:options="
|
||||
fieldSelectedOption.map((v) => ({
|
||||
...v,
|
||||
label: v.label && $t(v.label),
|
||||
}))
|
||||
"
|
||||
:display-value="$t('general.displayField')"
|
||||
:hide-dropdown-icon="$q.screen.lt.sm"
|
||||
v-model="pageState.fieldSelected"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
map-options
|
||||
emit-value
|
||||
outlined
|
||||
multiple
|
||||
dense
|
||||
/>
|
||||
|
||||
<q-btn-toggle
|
||||
id="btn-mode"
|
||||
v-model="pageState.gridView"
|
||||
dense
|
||||
class="no-shadow bordered rounded surface-1 q-ml-sm"
|
||||
:toggle-color="$q.dark.isActive ? 'grey-9' : 'grey-2'"
|
||||
size="xs"
|
||||
:options="[
|
||||
{ value: true, slot: 'folder' },
|
||||
{ value: false, slot: 'list' },
|
||||
]"
|
||||
>
|
||||
<template v-slot:folder>
|
||||
<q-icon
|
||||
name="mdi-view-grid-outline"
|
||||
size="16px"
|
||||
class="q-px-sm q-py-xs rounded"
|
||||
:style="{
|
||||
color: $q.dark.isActive
|
||||
? pageState.gridView
|
||||
? '#C9D3DB '
|
||||
: '#787B7C'
|
||||
: pageState.gridView
|
||||
? '#787B7C'
|
||||
: '#C9D3DB',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:list>
|
||||
<q-icon
|
||||
name="mdi-format-list-bulleted"
|
||||
class="q-px-sm q-py-xs rounded"
|
||||
size="16px"
|
||||
:style="{
|
||||
color: $q.dark.isActive
|
||||
? pageState.gridView === false
|
||||
? '#C9D3DB'
|
||||
: '#787B7C'
|
||||
: pageState.gridView === false
|
||||
? '#787B7C'
|
||||
: '#C9D3DB',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
</q-btn-toggle>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav class="surface-2 bordered-b q-px-md full-width">
|
||||
<q-tabs
|
||||
inline-label
|
||||
mobile-arrows
|
||||
dense
|
||||
v-model="pageState.currentTab"
|
||||
align="left"
|
||||
class="full-width"
|
||||
active-color="info"
|
||||
>
|
||||
<q-tab
|
||||
v-for="tab in pageTabs"
|
||||
:name="tab.value"
|
||||
:key="tab.value"
|
||||
@click="
|
||||
() => {
|
||||
pageState.currentTab = tab.value;
|
||||
pageState.inputSearch = '';
|
||||
|
||||
flow.rotate();
|
||||
}
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="row text-capitalize"
|
||||
:class="
|
||||
pageState.currentTab === tab.value
|
||||
? 'text-bold'
|
||||
: 'app-text-muted'
|
||||
"
|
||||
>
|
||||
{{ $t(`debitNote.status.${tab.label}`) }}
|
||||
</div>
|
||||
</q-tab>
|
||||
</q-tabs>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- SEC: body content -->
|
||||
<article
|
||||
v-if="data.length === 0"
|
||||
class="col surface-2 flex items-center justify-center"
|
||||
>
|
||||
<NoData :not-found="!!pageState.inputSearch" />
|
||||
</article>
|
||||
<article v-else class="col surface-2 full-width scroll q-pa-md">
|
||||
<!-- #TODO change Table -->
|
||||
<TableDebitNote
|
||||
:grid="pageState.gridView"
|
||||
:visible-columns="pageState.fieldSelected"
|
||||
@view="
|
||||
(item) => navigateTo({ statusDialog: 'info', debitId: item.id })
|
||||
"
|
||||
@edit="
|
||||
(item) => navigateTo({ statusDialog: 'edit', debitId: item.id })
|
||||
"
|
||||
@delete="(item) => triggerDelete(item.id)"
|
||||
>
|
||||
<template #grid="{ item }">
|
||||
<div class="col-md-4 col-sm-6 col-12">
|
||||
<QuotationCard
|
||||
hide-kebab-edit
|
||||
@view="
|
||||
() =>
|
||||
navigateTo({ statusDialog: 'info', debitId: item.row.id })
|
||||
"
|
||||
@edit="
|
||||
() =>
|
||||
navigateTo({ statusDialog: 'edit', debitId: item.row.id })
|
||||
"
|
||||
@delete="() => triggerDelete(item.row.id)"
|
||||
:title="item.row.debitNoteQuotation?.workName"
|
||||
:code="item.row.code"
|
||||
:status="$t(`quotation.status.${item.row.quotationStatus}`)"
|
||||
:badge-color="hslaColors[item.row.quotationStatus] || ''"
|
||||
:custom-data="[
|
||||
{
|
||||
label: $t('branch.card.branchVirtual'),
|
||||
value:
|
||||
$i18n.locale === 'tha'
|
||||
? item.row.registeredBranch.name
|
||||
: item.row.registeredBranch.nameEN,
|
||||
},
|
||||
{
|
||||
label: $t('quotation.customer'),
|
||||
value:
|
||||
item.row.customerBranch.customer.customerType === 'CORP'
|
||||
? item.row.customerBranch.customerName
|
||||
: $i18n.locale === 'tha'
|
||||
? `${item.row.customerBranch.firstName} ${item.row.customerBranch.lastName}`
|
||||
: `${item.row.customerBranch.firstNameEN} ${item.row.customerBranch.lastNameEN}`,
|
||||
},
|
||||
{
|
||||
label: $t('requestList.quotationCode'),
|
||||
value: item.row.debitNoteQuotation?.code,
|
||||
},
|
||||
{
|
||||
label: $t('debitNote.label.quotationPayment'),
|
||||
value: $t(
|
||||
`quotation.type.${item.row.debitNoteQuotation?.payCondition}`,
|
||||
),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</TableDebitNote>
|
||||
</article>
|
||||
|
||||
<footer
|
||||
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
||||
v-if="pageMax > 0"
|
||||
>
|
||||
<div class="col-4">
|
||||
<div class="row items-center no-wrap">
|
||||
<div class="app-text-muted q-mr-sm" v-if="$q.screen.gt.sm">
|
||||
{{ $t('general.recordPerPage') }}
|
||||
</div>
|
||||
<div><PaginationPageSize v-model="pageSize" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 row justify-center app-text-muted">
|
||||
{{
|
||||
$q.screen.gt.sm
|
||||
? $t('general.recordsPage', {
|
||||
resultcurrentPage: data.length,
|
||||
total: pageState.inputSearch
|
||||
? data.length
|
||||
: pageState.total,
|
||||
})
|
||||
: $t('general.ofPage', {
|
||||
current: data.length,
|
||||
total: pageState.inputSearch
|
||||
? data.length
|
||||
: pageState.total,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<nav class="col-4 row justify-end">
|
||||
<!-- #TODO add getList at fectch-data -->
|
||||
<PaginationComponent
|
||||
v-model:current-page="page"
|
||||
v-model:max-page="pageMax"
|
||||
:fetch-data="() => getList()"
|
||||
/>
|
||||
</nav>
|
||||
</footer>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- SEC: Dialog -->
|
||||
<!-- dialog create -->
|
||||
|
||||
<!-- #TODO add submit at submit -->
|
||||
<DialogFormContainer
|
||||
width="60vw"
|
||||
height="500px"
|
||||
v-model="pageState.debitDialog"
|
||||
@submit="() => submit()"
|
||||
>
|
||||
<template #header>
|
||||
<DialogHeader
|
||||
:title="$t(`general.add`, { text: $t('debitNote.title') })"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<section class="q-pa-md col full-width">
|
||||
<div class="surface-1 rounded bordered q-pa-md full-height full-width">
|
||||
<!-- TODO: bind quotation id -->
|
||||
<FormDebit v-model:quotation-id="pageState.quotationId" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<template #footer>
|
||||
<CancelButton class="q-ml-auto" outlined @click="close()" />
|
||||
<SaveButton
|
||||
:label="$t(`general.add`, { text: $t('debitNote.title') })"
|
||||
class="q-ml-sm"
|
||||
icon="mdi-check"
|
||||
solid
|
||||
type="submit"
|
||||
/>
|
||||
</template>
|
||||
</DialogFormContainer>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
95
src/pages/12_debit-note/TableDebitNote.vue
Normal file
95
src/pages/12_debit-note/TableDebitNote.vue
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { QTableSlots } from 'quasar';
|
||||
import { DebitNote, useDebitNote } from 'src/stores/debit-note';
|
||||
|
||||
import { columns } from './constants.ts';
|
||||
import KebabAction from 'src/components/shared/KebabAction.vue';
|
||||
|
||||
const debitNote = useDebitNote();
|
||||
const { data, page, pageSize } = storeToRefs(debitNote);
|
||||
|
||||
const prop = defineProps<{ grid: boolean; visibleColumns: string[] }>();
|
||||
defineEmits<{ (evt: 'view' | 'delete' | 'edit', val: DebitNote): void }>();
|
||||
|
||||
const visible = computed(() =>
|
||||
columns.filter(
|
||||
(v) => prop.visibleColumns.includes(v.name) || v.name === '#action',
|
||||
),
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<q-table
|
||||
:rows-per-page-options="[0]"
|
||||
:rows="
|
||||
data.map((item, i) => ({
|
||||
...item,
|
||||
_index: i,
|
||||
_page: page,
|
||||
_pageSize: pageSize,
|
||||
}))
|
||||
"
|
||||
:columns="visible"
|
||||
:grid
|
||||
hide-bottom
|
||||
bordered
|
||||
flat
|
||||
hide-pagination
|
||||
selection="multiple"
|
||||
card-container-class="q-col-gutter-sm"
|
||||
class="full-width"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||
:props="props"
|
||||
>
|
||||
<q-th v-for="col in visible" :key="col.name" :props="props">
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
{{ $t(col.label) }}
|
||||
</template>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-slot:body="props: {
|
||||
row: DebitNote & { _index: number; _page: number; _pageSize: number };
|
||||
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||
>
|
||||
<q-tr :class="{ dark: $q.dark.isActive }" class="text-center">
|
||||
<q-td v-for="col in visible" :align="col.align">
|
||||
<!-- NOTE: custom column will starts with # -->
|
||||
<template v-if="!col.name.startsWith('#')">
|
||||
<span v-if="col.name !== 'quotationPayment'">
|
||||
{{
|
||||
typeof col.field === 'string'
|
||||
? props.row[col.field as keyof DebitNote]
|
||||
: col.field(props.row)
|
||||
}}
|
||||
</span>
|
||||
|
||||
<span v-if="col.name === 'quotationPayment'">
|
||||
{{ $t(`quotation.type.${col.field(props.row)}`) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-if="col.name === '#action'">
|
||||
<KebabAction
|
||||
hide-toggle
|
||||
hide-edit
|
||||
@edit="$emit('edit', props.row)"
|
||||
@delete="$emit('delete', props.row)"
|
||||
@view="$emit('view', props.row)"
|
||||
/>
|
||||
</template>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item="props: { row: DebitNote }">
|
||||
<slot name="grid" :item="props" />
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
90
src/pages/12_debit-note/constants.ts
Normal file
90
src/pages/12_debit-note/constants.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { QTableProps } from 'quasar';
|
||||
import { DebitNoteStatus, DebitNote } from 'src/stores/debit-note';
|
||||
import { formatNumberDecimal } from 'src/stores/utils';
|
||||
|
||||
export const taskStatusOpts = [
|
||||
{
|
||||
status: DebitNoteStatus.Expire,
|
||||
name: `debitNote.status.${DebitNoteStatus.Expire}`,
|
||||
},
|
||||
{
|
||||
status: DebitNoteStatus.Payment,
|
||||
name: `debitNote.status.${DebitNoteStatus.Payment}`,
|
||||
},
|
||||
{
|
||||
status: DebitNoteStatus.Receipt,
|
||||
name: `debitNote.status.${DebitNoteStatus.Receipt}`,
|
||||
},
|
||||
{
|
||||
status: DebitNoteStatus.Succeed,
|
||||
name: `debitNote.status.${DebitNoteStatus.Succeed}`,
|
||||
},
|
||||
];
|
||||
|
||||
export const pageTabs = [
|
||||
{ label: 'Pending', value: DebitNoteStatus.Pending },
|
||||
{ label: 'Expire', value: DebitNoteStatus.Expire },
|
||||
{ label: 'Payment', value: DebitNoteStatus.Payment },
|
||||
{ label: 'Receipt', value: DebitNoteStatus.Receipt },
|
||||
{ label: 'Succeed', value: DebitNoteStatus.Succeed },
|
||||
];
|
||||
|
||||
export enum Status {
|
||||
taskOrder = 'taskOrder',
|
||||
receiveTaskOrder = 'receiveTaskOrder',
|
||||
sendTaskOrder = 'sendTaskOrder',
|
||||
payment = 'payment',
|
||||
goodReceipt = 'goodReceipt',
|
||||
}
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
name: 'order',
|
||||
align: 'center',
|
||||
label: 'general.order',
|
||||
field: (
|
||||
data: DebitNote & { _index: number; _page: number; _pageSize: number },
|
||||
) => (data._page - 1) * data._pageSize + data._index + 1,
|
||||
},
|
||||
{
|
||||
name: 'code',
|
||||
align: 'center',
|
||||
label: 'debitNote.label.codeDebit',
|
||||
field: (data: DebitNote) => data.code,
|
||||
},
|
||||
{
|
||||
name: 'quotationCode',
|
||||
align: 'center',
|
||||
label: 'debitNote.label.codeQuotation',
|
||||
field: (data: DebitNote) => data.debitNoteQuotation?.code,
|
||||
},
|
||||
{
|
||||
name: 'quotationWorkName',
|
||||
align: 'center',
|
||||
label: 'debitNote.label.quotationWorkName',
|
||||
field: (data: DebitNote) => data.workName,
|
||||
},
|
||||
{
|
||||
name: 'quotationPayment',
|
||||
align: 'center',
|
||||
label: 'debitNote.label.quotationPayment',
|
||||
field: (data: DebitNote) => data.debitNoteQuotation?.payCondition,
|
||||
},
|
||||
{
|
||||
name: 'creditNoteValue',
|
||||
align: 'center',
|
||||
label: 'debitNote.label.value',
|
||||
field: (data: DebitNote) => formatNumberDecimal(data.totalPrice),
|
||||
},
|
||||
{
|
||||
name: '#action',
|
||||
align: 'center',
|
||||
label: '',
|
||||
field: (_) => '#action',
|
||||
},
|
||||
] as const satisfies QTableProps['columns'];
|
||||
|
||||
export const hslaColors: Record<string, string> = {
|
||||
Pending: '--blue-6-hsl',
|
||||
Success: '--red-6-hsl',
|
||||
};
|
||||
113
src/pages/12_debit-note/document-view/BankComponents.vue
Normal file
113
src/pages/12_debit-note/document-view/BankComponents.vue
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { QSelect } from 'quasar';
|
||||
|
||||
// NOTE: Import stores
|
||||
import { selectFilterOptionRefMod } from 'stores/utils';
|
||||
import useOptionStore from 'stores/options';
|
||||
|
||||
// NOTE Import Types
|
||||
import { BankBook } from 'stores/branch/types';
|
||||
|
||||
// NOTE: Import Components
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
|
||||
const bankBookOptions = ref<Record<string, unknown>[]>([]);
|
||||
let bankBookFilter: (
|
||||
value: string,
|
||||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
defineProps<{
|
||||
bankBook: BankBook;
|
||||
index: number;
|
||||
}>();
|
||||
|
||||
watch(
|
||||
() => optionStore.globalOption,
|
||||
() => {
|
||||
bankBookFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption.bankBook),
|
||||
bankBookOptions,
|
||||
'label',
|
||||
);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset class="rounded" style="border: 1px solid var(--gray-4)">
|
||||
<legend>ช่องทางที่ {{ index + 1 }}</legend>
|
||||
|
||||
<div class="border-5 full-width row" style="gap: var(--size-4)">
|
||||
<div class="column q-pa-sm" style="width: fit-content">
|
||||
<img
|
||||
:src="bankBook.bankUrl"
|
||||
class="rounded"
|
||||
style="
|
||||
border: 1px solid var(--gray-3);
|
||||
object-fit: scale-down;
|
||||
width: 1in;
|
||||
height: 1in;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="column col" style="gap: var(--size-3)">
|
||||
<div class="row" style="justify-content: space-between">
|
||||
<div class="text-with-label">
|
||||
<div>เลขบัญชีธนาคาร</div>
|
||||
<div class="row items-start">
|
||||
<img
|
||||
width="25px"
|
||||
height="25px"
|
||||
class="q-mr-xs"
|
||||
:src="`/img/bank/${bankBook.bankName}.png`"
|
||||
/>
|
||||
{{ bankBook.accountNumber }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-with-label">
|
||||
<div>เลขบัญชีธนาคาร</div>
|
||||
<div>{{ bankBook.accountNumber }}</div>
|
||||
</div>
|
||||
<div class="text-with-label">
|
||||
<div>ชื่อบัญชี</div>
|
||||
<div>{{ bankBook.accountName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="text-with-label">
|
||||
<div>สาขา</div>
|
||||
<div>{{ bankBook.bankBranch }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
legend {
|
||||
background-color: white;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.bordered-2 {
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.border-5 {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.text-with-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
& > :first-child {
|
||||
color: var(--gray-6);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
654
src/pages/12_debit-note/document-view/MainPage.vue
Normal file
654
src/pages/12_debit-note/document-view/MainPage.vue
Normal file
|
|
@ -0,0 +1,654 @@
|
|||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, nextTick, ref, watch } from 'vue';
|
||||
import { precisionRound } from 'src/utils/arithmetic';
|
||||
import ThaiBahtText from 'thai-baht-text';
|
||||
|
||||
// NOTE: Import stores
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
|
||||
import { useConfigStore } from 'stores/config';
|
||||
import useBranchStore from 'stores/branch';
|
||||
import { baseUrl } from 'stores/utils';
|
||||
import useCustomerStore from 'stores/customer';
|
||||
|
||||
import { DebitNotePayload, useDebitNote } from 'src/stores/debit-note';
|
||||
|
||||
// NOTE Import Types
|
||||
import { CustomerBranch } from 'stores/customer/types';
|
||||
import { BankBook, Branch } from 'stores/branch/types';
|
||||
import {
|
||||
CustomerBranchRelation,
|
||||
Details,
|
||||
ProductServiceList,
|
||||
} from 'src/stores/quotations/types';
|
||||
|
||||
// NOTE: Import Components
|
||||
import ViewPDF from './ViewPdf.vue';
|
||||
import ViewHeader from './ViewHeader.vue';
|
||||
import ViewFooter from './ViewFooter.vue';
|
||||
import BankComponents from './BankComponents.vue';
|
||||
import PrintButton from 'src/components/button/PrintButton.vue';
|
||||
import { convertTemplate } from 'src/utils/string-template';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const branchStore = useBranchStore();
|
||||
const customerStore = useCustomerStore();
|
||||
const debitNoteStore = useDebitNote();
|
||||
const { data: config } = storeToRefs(configStore);
|
||||
|
||||
type Product = {
|
||||
id: string;
|
||||
code: string;
|
||||
detail: string;
|
||||
amount: number;
|
||||
priceUnit: number;
|
||||
discount: number;
|
||||
vat: number;
|
||||
value: number;
|
||||
};
|
||||
|
||||
type SummaryPrice = {
|
||||
totalPrice: number;
|
||||
totalDiscount: number;
|
||||
vat: number;
|
||||
vatExcluded: number;
|
||||
finalPrice: number;
|
||||
};
|
||||
|
||||
const customer = ref<CustomerBranch>();
|
||||
const branch = ref<Branch>();
|
||||
const productList = ref<Product[]>([]);
|
||||
const bankList = ref<BankBook[]>([]);
|
||||
|
||||
const elements = ref<HTMLElement[]>([]);
|
||||
const chunks = ref<Product[][]>([[]]);
|
||||
const attachmentList = ref<
|
||||
{
|
||||
url: string;
|
||||
isImage?: boolean;
|
||||
isPDF?: boolean;
|
||||
}[]
|
||||
>([]);
|
||||
const data = ref<
|
||||
DebitNotePayload & {
|
||||
customerBranch: CustomerBranchRelation;
|
||||
customerBranchId: string;
|
||||
registeredBranchId: string;
|
||||
}
|
||||
>();
|
||||
|
||||
const productServiceList = ref<ProductServiceList[]>([]);
|
||||
|
||||
const summaryPrice = ref<SummaryPrice>({
|
||||
totalPrice: 0,
|
||||
totalDiscount: 0,
|
||||
vat: 0,
|
||||
vatExcluded: 0,
|
||||
finalPrice: 0,
|
||||
});
|
||||
|
||||
async function getAttachment(quotationId: string) {
|
||||
const attachment = await debitNoteStore.listAttachment({
|
||||
parentId: quotationId,
|
||||
});
|
||||
|
||||
if (attachment) {
|
||||
attachmentList.value = await Promise.all(
|
||||
attachment.map(async (v) => {
|
||||
const url = await debitNoteStore.getAttachment({
|
||||
parentId: quotationId,
|
||||
name: v,
|
||||
});
|
||||
const ft = v.substring(v.lastIndexOf('.') + 1);
|
||||
|
||||
return {
|
||||
url,
|
||||
isImage: ['png', 'jpg', 'jpeg'].includes(ft),
|
||||
isPDF: ft === 'pdf',
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function assignData() {
|
||||
for (let i = 0; i < productList.value.length; i++) {
|
||||
let el = elements.value.at(-1);
|
||||
|
||||
if (!el) return;
|
||||
|
||||
if (getHeight(el) < 500) {
|
||||
chunks.value.at(-1)?.push(productList.value[i]);
|
||||
} else {
|
||||
chunks.value.push([]);
|
||||
i--;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
|
||||
function getHeight(el: HTMLElement) {
|
||||
const shadow = document.createElement('div');
|
||||
|
||||
shadow.style.opacity = '0';
|
||||
shadow.style.position = 'absolute';
|
||||
shadow.style.top = '-999999px';
|
||||
shadow.style.left = '-999999px';
|
||||
shadow.style.pointerEvents = 'none';
|
||||
|
||||
document.body.appendChild(shadow);
|
||||
|
||||
shadow.appendChild(el.cloneNode(true));
|
||||
|
||||
const height = shadow.offsetHeight;
|
||||
|
||||
document.body.removeChild(shadow);
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
const details = ref<Details>();
|
||||
enum View {
|
||||
DebitNote,
|
||||
Invoice,
|
||||
Payment,
|
||||
Receipt,
|
||||
}
|
||||
const view = ref<View>(View.DebitNote);
|
||||
|
||||
onMounted(async () => {
|
||||
let str =
|
||||
localStorage.getItem('debit-note-preview') ||
|
||||
sessionStorage.getItem('debit-note-preview');
|
||||
|
||||
if (!str) return;
|
||||
|
||||
const obj: DebitNotePayload = JSON.parse(str);
|
||||
|
||||
if (obj) sessionStorage.setItem('debit-note-preview', JSON.stringify(obj));
|
||||
|
||||
delete localStorage['debit-note-preview'];
|
||||
|
||||
const parsed = JSON.parse(sessionStorage.getItem('debit-note-preview') || '');
|
||||
|
||||
data.value = 'data' in parsed ? parsed.data : undefined;
|
||||
|
||||
if (data.value) {
|
||||
if (data.value.id) {
|
||||
await getAttachment(data.value.id);
|
||||
}
|
||||
|
||||
const resCustomerBranch = await customerStore.getBranchById(
|
||||
data.value.customerBranchId,
|
||||
);
|
||||
|
||||
if (resCustomerBranch) {
|
||||
customer.value = resCustomerBranch;
|
||||
}
|
||||
|
||||
details.value = {
|
||||
code: parsed.meta.source.code,
|
||||
createdAt: parsed.meta.source.createdAt,
|
||||
createdBy: `${parsed.meta.createdBy} ${!parsed.meta.source.createdBy ? '' : parsed.meta.source.createdBy.telephoneNo}`,
|
||||
payCondition: parsed.meta.source.payCondition,
|
||||
contactName: parsed.meta.source.contactName,
|
||||
contactTel: parsed.meta.source.contactTel,
|
||||
workName: parsed.meta.source.workName,
|
||||
dueDate: parsed.meta.source.dueDate,
|
||||
worker: parsed.meta.selectedWorker,
|
||||
};
|
||||
|
||||
const resBranch = await branchStore.fetchById(
|
||||
data.value?.registeredBranchId,
|
||||
);
|
||||
|
||||
if (resBranch) {
|
||||
branch.value = resBranch;
|
||||
|
||||
bankList.value = resBranch.bank.map((v) => ({
|
||||
...v,
|
||||
bankUrl: `${baseUrl}/branch/${resBranch.id}/bank-qr/${v.id}?ts=${Date.now()}`,
|
||||
}));
|
||||
}
|
||||
|
||||
productServiceList.value = parsed.meta.productServicelist;
|
||||
|
||||
productList.value =
|
||||
productServiceList.value?.map((v) => ({
|
||||
id: v.product.id,
|
||||
code: v.product.code,
|
||||
detail: v.product.name,
|
||||
amount: v.amount || 0,
|
||||
priceUnit: v.pricePerUnit || 0,
|
||||
discount: v.discount || 0,
|
||||
vat: v.vat || 0,
|
||||
value: precisionRound(
|
||||
(v.pricePerUnit || 0) * v.amount -
|
||||
(v.discount || 0) +
|
||||
(v.product.calcVat
|
||||
? ((v.pricePerUnit || 0) * v.amount - (v.discount || 0)) *
|
||||
(config.value?.vat || 0.07)
|
||||
: 0),
|
||||
),
|
||||
})) || [];
|
||||
}
|
||||
|
||||
summaryPrice.value = (productServiceList.value || []).reduce(
|
||||
(a, c) => {
|
||||
const price = precisionRound((c.pricePerUnit || 0) * c.amount);
|
||||
const vat = precisionRound(
|
||||
((c.pricePerUnit || 0) * c.amount - (c.discount || 0)) *
|
||||
(config.value?.vat || 0.07),
|
||||
);
|
||||
|
||||
a.totalPrice = precisionRound(a.totalPrice + price);
|
||||
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
|
||||
a.vat = c.product.calcVat ? precisionRound(a.vat + vat) : a.vat;
|
||||
a.vatExcluded = c.product.calcVat
|
||||
? a.vatExcluded
|
||||
: precisionRound(a.vat + vat);
|
||||
a.finalPrice = precisionRound(
|
||||
a.totalPrice -
|
||||
a.totalDiscount +
|
||||
a.vat -
|
||||
Number(data.value?.discount || 0),
|
||||
);
|
||||
|
||||
return a;
|
||||
},
|
||||
{
|
||||
totalPrice: 0,
|
||||
totalDiscount: 0,
|
||||
vat: 0,
|
||||
vatExcluded: 0,
|
||||
finalPrice: 0,
|
||||
},
|
||||
);
|
||||
|
||||
assignData();
|
||||
});
|
||||
|
||||
watch(elements, () => {});
|
||||
|
||||
function print() {
|
||||
window.print();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toolbar">
|
||||
<PrintButton solid @click="print" />
|
||||
</div>
|
||||
<div class="row justify-between container color-debit-note">
|
||||
<section class="content" v-for="chunk in chunks">
|
||||
<ViewHeader
|
||||
v-if="!!branch && !!customer && !!details"
|
||||
:branch="branch"
|
||||
:customer="customer"
|
||||
:details="details"
|
||||
:view="view"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="q-mb-sm q-mt-md"
|
||||
style="
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
color: var(--main);
|
||||
display: block;
|
||||
border-bottom: 2px solid var(--main);
|
||||
"
|
||||
>
|
||||
{{ $t('preview.productList') }}
|
||||
</span>
|
||||
|
||||
<table ref="elements" class="q-mb-sm" cellpadding="0" style="width: 100%">
|
||||
<tbody class="color-tr">
|
||||
<tr>
|
||||
<th>{{ $t('preview.rank') }}</th>
|
||||
<th>{{ $t('preview.productCode') }}</th>
|
||||
<th>{{ $t('general.detail') }}</th>
|
||||
<th>{{ $t('general.amount') }}</th>
|
||||
<th>{{ $t('preview.pricePerUnit') }}</th>
|
||||
<th>{{ $t('preview.discount') }}</th>
|
||||
<th>{{ $t('preview.vat') }}</th>
|
||||
<th>{{ $t('preview.value') }}</th>
|
||||
</tr>
|
||||
<tr v-for="(v, i) in chunk">
|
||||
<td class="text-center">{{ i + 1 }}</td>
|
||||
<td>{{ v.code }}</td>
|
||||
<td>{{ v.detail }}</td>
|
||||
<td style="text-align: right">{{ v.amount }}</td>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumberDecimal(v.priceUnit, 2) }}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumberDecimal(v.discount, 2) }}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumberDecimal(v.vat, 2) }}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumberDecimal(v.value, 2) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table
|
||||
style="width: 40%; margin-left: auto"
|
||||
class="q-mb-md"
|
||||
cellpadding="0"
|
||||
>
|
||||
<tbody class="color-tr">
|
||||
<tr>
|
||||
<td>{{ $t('general.total') }}</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumberDecimal(summaryPrice.totalPrice, 2) }}
|
||||
฿
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ $t('general.discount') }}</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumberDecimal(summaryPrice.totalDiscount, 2) || 0 }} ฿
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('general.totalAfterDiscount') }}</td>
|
||||
<td class="text-right">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
summaryPrice.totalPrice - summaryPrice.totalDiscount,
|
||||
2,
|
||||
)
|
||||
}}
|
||||
฿
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ $t('general.totalVatExcluded') }}</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumberDecimal(summaryPrice.vatExcluded, 2) || 0 }}
|
||||
฿
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('general.vat', { msg: '7%' }) }}</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumberDecimal(summaryPrice.vat, 2) }} ฿
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('general.totalVatIncluded') }}</td>
|
||||
<td class="text-right">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
summaryPrice.totalPrice -
|
||||
summaryPrice.totalDiscount +
|
||||
summaryPrice.vat,
|
||||
2,
|
||||
)
|
||||
}}
|
||||
฿
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ $t('general.discountAfterVat') }}</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumberDecimal(data?.discount || 0, 2) }}
|
||||
฿
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="row justify-between q-mb-md" style="width: 100%">
|
||||
<div
|
||||
class="column set-width bg-color full-height"
|
||||
style="padding: 12px"
|
||||
>
|
||||
({{ ThaiBahtText(summaryPrice.finalPrice) }})
|
||||
</div>
|
||||
<div
|
||||
class="row text-right border-5 items-center"
|
||||
style="width: 40%; background: var(--main); padding: 8px"
|
||||
>
|
||||
<span style="color: white; font-weight: 600">ยอดรวมสุทธิ</span>
|
||||
<span
|
||||
class="border-5"
|
||||
style="
|
||||
width: 70%;
|
||||
margin-left: auto;
|
||||
background: white;
|
||||
padding: 4px;
|
||||
"
|
||||
>
|
||||
{{
|
||||
formatNumberDecimal(Math.max(summaryPrice.finalPrice, 0), 2) || 0
|
||||
}}
|
||||
฿
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content">
|
||||
<ViewHeader
|
||||
v-if="!!branch && !!customer && !!details"
|
||||
:branch="branch"
|
||||
:customer="customer"
|
||||
:details="details"
|
||||
:view="view"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="q-mb-sm q-mt-md"
|
||||
style="
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
color: var(--main);
|
||||
display: block;
|
||||
border-bottom: 2px solid var(--main);
|
||||
"
|
||||
>
|
||||
{{ $t('general.remark') }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="border-5 surface-0 detail-note q-mb-md"
|
||||
style="width: 100%; padding: 8px 16px; white-space: pre-wrap"
|
||||
>
|
||||
<div
|
||||
v-html="
|
||||
convertTemplate(data?.remark || '', {
|
||||
'quotation-payment': {
|
||||
paymentType: data?.payCondition || 'Full',
|
||||
amount: summaryPrice.finalPrice,
|
||||
installments: data?.paySplit,
|
||||
},
|
||||
'quotation-labor': {
|
||||
name:
|
||||
details?.worker.map(
|
||||
(v, i) =>
|
||||
`${i + 1}. ` +
|
||||
`${v.namePrefix}. ${v.firstNameEN} ${v.lastNameEN}`.toUpperCase(),
|
||||
) || [],
|
||||
},
|
||||
}) || '-'
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content">
|
||||
<ViewHeader
|
||||
v-if="!!branch && !!customer && !!details"
|
||||
:branch="branch"
|
||||
:customer="customer"
|
||||
:details="details"
|
||||
:view="view"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="q-mb-sm"
|
||||
style="
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
color: var(--main);
|
||||
display: block;
|
||||
border-bottom: 2px solid var(--main);
|
||||
"
|
||||
>
|
||||
{{ $t('preview.paymentMethods') }}
|
||||
</span>
|
||||
<article style="height: 5.8in">
|
||||
<BankComponents
|
||||
v-for="(bank, index) in bankList"
|
||||
:index="index"
|
||||
:bank-book="bank"
|
||||
:key="bank.id"
|
||||
/>
|
||||
</article>
|
||||
|
||||
<ViewFooter
|
||||
:data="{
|
||||
name: '',
|
||||
company: branch?.name || '',
|
||||
buyer: '',
|
||||
buyDate: '',
|
||||
approveDate: '',
|
||||
approver: '',
|
||||
}"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-for="item in attachmentList.filter((v) => v.isImage)"
|
||||
class="content"
|
||||
>
|
||||
<q-img :src="item.url" />
|
||||
</section>
|
||||
|
||||
<ViewPDF
|
||||
v-for="item in attachmentList.filter((v) => v.isPDF)"
|
||||
:url="item.url"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.color-debit-note {
|
||||
--main: var(--cyan-7);
|
||||
--main-hsl: var(--cyan-7-hsl);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--gray-3);
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--main);
|
||||
color: white;
|
||||
|
||||
padding: 4px;
|
||||
}
|
||||
td {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.border-5 {
|
||||
border-radius: 5px;
|
||||
}
|
||||
.set-width {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.bg-color {
|
||||
background-color: hsla(var(--main-hsl) / 0.1);
|
||||
}
|
||||
|
||||
.color-tr > tr:nth-child(odd) {
|
||||
background-color: hsla(var(--main-hsl) / 0.1);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-inline: auto;
|
||||
background: var(--gray-3);
|
||||
width: calc(8.3in + 1rem);
|
||||
}
|
||||
|
||||
.container :deep(*) {
|
||||
font-size: 95%;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
padding: 0.5in;
|
||||
align-items: center;
|
||||
background: white;
|
||||
page-break-after: always;
|
||||
break-after: page;
|
||||
height: 11.7in;
|
||||
max-height: 11.7in;
|
||||
}
|
||||
|
||||
.position-bottom {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.detail-note {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
& > * {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
border-style: solid;
|
||||
border-color: var(--main);
|
||||
}
|
||||
|
||||
@media print {
|
||||
.toolbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0;
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
98
src/pages/12_debit-note/document-view/ViewFooter.vue
Normal file
98
src/pages/12_debit-note/document-view/ViewFooter.vue
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
data?: {
|
||||
name: string;
|
||||
buyer: string;
|
||||
buyDate: string;
|
||||
|
||||
company: string;
|
||||
approver: string;
|
||||
approveDate: string;
|
||||
};
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<div class="footer-container">
|
||||
<div class="footer-top">
|
||||
<div>ในนาม {{ data?.name || '-' }}</div>
|
||||
<div>ในนาม {{ data?.company || '-' }}</div>
|
||||
</div>
|
||||
|
||||
<img src="/images/jws-stamp.png" alt="${0}" />
|
||||
|
||||
<div class="footer-bottom">
|
||||
<section>
|
||||
<div>
|
||||
<span class="data-placeholder"></span>
|
||||
<span>ผู้สั่งซื้อสินค้า</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="data-placeholder"></span>
|
||||
<span>วันที่</span>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div>
|
||||
<span class="data-placeholder"></span>
|
||||
<span>ผู้อนุมัติ</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="data-placeholder"></span>
|
||||
<span>วันที่</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.footer-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
height: 1.5in;
|
||||
}
|
||||
|
||||
.footer-top {
|
||||
position: absolute;
|
||||
width: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& > * {
|
||||
width: 38%;
|
||||
}
|
||||
}
|
||||
.footer-bottom {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& > * {
|
||||
display: flex;
|
||||
width: 38%;
|
||||
justify-content: space-around;
|
||||
|
||||
& > * {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-placeholder {
|
||||
display: block;
|
||||
min-width: 1.2in;
|
||||
border-bottom: 1px dotted black;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
</style>
|
||||
192
src/pages/12_debit-note/document-view/ViewHeader.vue
Normal file
192
src/pages/12_debit-note/document-view/ViewHeader.vue
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
<script lang="ts" setup>
|
||||
import { dateFormat } from 'src/utils/datetime';
|
||||
|
||||
// NOTE: Import stores
|
||||
import { formatAddress } from 'src/utils/address';
|
||||
|
||||
// NOTE Import Types
|
||||
import { Branch } from 'src/stores/branch/types';
|
||||
import { CustomerBranchRelation, Details } from 'src/stores/quotations/types';
|
||||
// NOTE: Import Components
|
||||
|
||||
enum View {
|
||||
DebitNote,
|
||||
Invoice,
|
||||
Payment,
|
||||
Receipt,
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
branch: Branch;
|
||||
customer: CustomerBranchRelation;
|
||||
details: Details;
|
||||
view: View;
|
||||
}>();
|
||||
|
||||
function titleMode(mode: View): string {
|
||||
if (mode === View.DebitNote) {
|
||||
return 'preview.title.debitNote';
|
||||
}
|
||||
if (mode === View.Invoice) {
|
||||
return 'preview.title.invoice';
|
||||
}
|
||||
if (mode === View.Payment) {
|
||||
return 'preview.title.payment';
|
||||
}
|
||||
if (mode === View.Receipt) {
|
||||
return 'preview.title.receipt';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row items-center q-mb-lg">
|
||||
<div class="column" style="width: 50%">
|
||||
<img src="/logo.png" width="192px" style="object-fit: scale-down" />
|
||||
</div>
|
||||
<div
|
||||
class="column"
|
||||
style="text-align: center; width: 50%; font-weight: 800; font-size: 24px"
|
||||
>
|
||||
{{ $t(titleMode(view)) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article class="detail-card">
|
||||
<section class="detail-customer-info">
|
||||
<article>
|
||||
<b>
|
||||
{{ !!branch.virtual ? '' : $t('general.company') }} {{ branch.name }}
|
||||
</b>
|
||||
|
||||
<span v-if="branch.province && branch.district && branch.subDistrict">
|
||||
{{
|
||||
formatAddress({
|
||||
address: branch.address,
|
||||
addressEN: branch.addressEN,
|
||||
moo: branch.moo,
|
||||
mooEN: branch.mooEN,
|
||||
soi: branch.soi,
|
||||
soiEN: branch.soiEN,
|
||||
street: branch.street,
|
||||
streetEN: branch.streetEN,
|
||||
province: branch.province,
|
||||
district: branch.district,
|
||||
subDistrict: branch.subDistrict,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span>เลขประจำตัวผู้เสียภาษี {{ branch.taxNo }}</span>
|
||||
<span>เบอร์โทร {{ branch.telephoneNo }}</span>
|
||||
<span>{{ branch.webUrl }}</span>
|
||||
</article>
|
||||
<article>
|
||||
<b>ลูกค้า</b>
|
||||
<span>
|
||||
{{
|
||||
formatAddress({
|
||||
address: customer.address,
|
||||
addressEN: customer.addressEN,
|
||||
moo: customer.moo,
|
||||
mooEN: customer.mooEN,
|
||||
soi: customer.soi,
|
||||
soiEN: customer.soiEN,
|
||||
street: customer.street,
|
||||
streetEN: customer.streetEN,
|
||||
province: customer.province,
|
||||
district: customer.district,
|
||||
subDistrict: customer.subDistrict,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span>เลขประจำตัวผู้เสียภาษี {{ customer.citizenId }}</span>
|
||||
<span>เบอร์โทร {{ customer.telephoneNo }}</span>
|
||||
</article>
|
||||
</section>
|
||||
<section class="detail-quotation-info">
|
||||
<div>
|
||||
<div>{{ $t('general.itemNo', { msg: `${$t(titleMode(view))}` }) }}</div>
|
||||
<div>{{ details.code }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ $t('preview.dateAt', { msg: `${$t(titleMode(view))}` }) }}</div>
|
||||
<div>{{ dateFormat(details.createdAt, true, false, true) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ $t('preview.seller') }}</div>
|
||||
<div>{{ details.createdBy }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ $t('quotation.paymentCondition') }}</div>
|
||||
<div>
|
||||
{{
|
||||
{
|
||||
Full: $t('quotation.type.fullAmountCash'),
|
||||
Split: $t('quotation.type.installmentsCash'),
|
||||
BillFull: $t('quotation.type.fullAmountBill'),
|
||||
BillSplit: $t('quotation.type.installmentsBill'),
|
||||
}[details.payCondition]
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ $t('quotation.workName') }}</div>
|
||||
<div>{{ details.workName }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ $t('quotation.contactName') }}</div>
|
||||
<div>{{ details.contactName }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ $t('preview.dueDate') }}</div>
|
||||
<div>{{ dateFormat(details.dueDate, true, false, true) }}</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.detail-card {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
& > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
& > :first-child {
|
||||
max-width: 57.5%;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-customer-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
& > * {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& > :first-child {
|
||||
color: var(--main);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-quotation-info {
|
||||
& > * {
|
||||
display: flex;
|
||||
|
||||
& > :first-child {
|
||||
color: var(--main);
|
||||
}
|
||||
|
||||
& > * {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
27
src/pages/12_debit-note/document-view/ViewPdf.vue
Normal file
27
src/pages/12_debit-note/document-view/ViewPdf.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import { VuePDF, usePDF } from '@tato30/vue-pdf';
|
||||
const props = defineProps<{
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
const { pdf, pages } = usePDF(props.url);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-for="page in pages" class="content">
|
||||
<VuePDF style="width: 100%" :pdf="pdf" :page="page" :scale="1.5" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.content :deep(canvas) {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.content :deep(canvas) {
|
||||
scale: 1.1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
src/pages/12_debit-note/expansion/DebitNoteExpansion.vue
Normal file
51
src/pages/12_debit-note/expansion/DebitNoteExpansion.vue
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<script setup lang="ts">
|
||||
import SelectInput from 'src/components/shared/SelectInput.vue';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
|
||||
const reason = defineModel<string>('reason');
|
||||
const detail = defineModel<string>('detail');
|
||||
</script>
|
||||
<template>
|
||||
<q-expansion-item
|
||||
dense
|
||||
:default-opened="true"
|
||||
class="overflow-hidden bordered full-width"
|
||||
switch-toggle-side
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||
>
|
||||
<template #header>
|
||||
<span>
|
||||
{{ $t('debitNote.label.debitNoteInformation') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<main class="q-px-md q-py-sm surface-1 row q-col-gutter-sm">
|
||||
<SelectInput
|
||||
:readonly
|
||||
for="select-debit-note-specify-reason"
|
||||
:label="$t('debitNote.label.specifyReasonForDebit')"
|
||||
class="col-md col-12"
|
||||
v-model="reason"
|
||||
:option="[
|
||||
{ label: $t('debitNote.label.reasonReturn'), value: 'Return' },
|
||||
{ label: $t('debitNote.label.reasonCanceled'), value: 'Canceled' },
|
||||
]"
|
||||
></SelectInput>
|
||||
<q-input
|
||||
:readonly
|
||||
for="input-debit-note-additional-detail"
|
||||
:label="$t('debitNote.label.additionalDetail')"
|
||||
outlined
|
||||
dense
|
||||
class="col"
|
||||
v-model="detail"
|
||||
></q-input>
|
||||
</main>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
120
src/pages/12_debit-note/expansion/DocumentExpansion.vue
Normal file
120
src/pages/12_debit-note/expansion/DocumentExpansion.vue
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<script lang="ts" setup>
|
||||
import SelectBranch from 'src/components/shared/select/SelectBranch.vue';
|
||||
import SelectCustomer from 'src/components/shared/select/SelectCustomer.vue';
|
||||
import DatePicker from 'src/components/shared/DatePicker.vue';
|
||||
import DataDisplay from 'src/components/08_request-list/DataDisplay.vue';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'gotoQuotation'): void;
|
||||
}>();
|
||||
|
||||
const registeredBranchId = defineModel<string>('registeredBranchId');
|
||||
const customerId = defineModel<string>('customerId');
|
||||
const issueDate = defineModel<string>('issueDate');
|
||||
const dueDate = defineModel<string | Date>('dueDate');
|
||||
|
||||
const quotationCode = defineModel<string>('quotationCode');
|
||||
const quotationWorkName = defineModel<string>('quotationWorkName');
|
||||
const quotationContactName = defineModel<string>('quotationContactName');
|
||||
const quotationContactTel = defineModel<string>('quotationContactTel');
|
||||
const quotationCreatedBy = defineModel<string>('quotationCreatedBy');
|
||||
</script>
|
||||
<template>
|
||||
<q-expansion-item
|
||||
default-opened
|
||||
dense
|
||||
class="overflow-hidden bordered full-width"
|
||||
switch-toggle-side
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||
>
|
||||
<template #header>
|
||||
<span>
|
||||
{{ $t('general.information', { msg: $t('general.document') }) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<main class="q-px-md q-py-sm surface-1 row q-col-gutter-sm">
|
||||
<SelectBranch
|
||||
readonly
|
||||
class="col-md-4 col-12"
|
||||
:label="`${$t('creditNote.label.quotationRegisteredBranch')}`"
|
||||
v-model:value="registeredBranchId"
|
||||
/>
|
||||
<SelectCustomer
|
||||
readonly
|
||||
simple
|
||||
class="col-md-4 col-12"
|
||||
:label="`${$t('creditNote.label.customer')}`"
|
||||
v-model:value="customerId"
|
||||
/>
|
||||
<DatePicker
|
||||
:label="$t('general.createdAt')"
|
||||
class="col-md-2 col-6"
|
||||
:model-value="issueDate || new Date(Date.now())"
|
||||
:readonly
|
||||
:disabled="!readonly"
|
||||
/>
|
||||
<DatePicker
|
||||
:label="$t('general.createdAt')"
|
||||
class="col-md-2 col-6"
|
||||
:model-value="dueDate || new Date(Date.now())"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (typeof v === 'string') dueDate = v;
|
||||
}
|
||||
"
|
||||
:readonly
|
||||
/>
|
||||
|
||||
<DataDisplay
|
||||
clickable
|
||||
class="col-md col-6"
|
||||
style="padding-inline: 20px"
|
||||
:label="$t('creditNote.label.quotationCode')"
|
||||
:value="quotationCode"
|
||||
@label-click="$emit('gotoQuotation')"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
readonly
|
||||
:label="$t('creditNote.label.quotationWorkName')"
|
||||
outlined
|
||||
dense
|
||||
class="col-md col-6"
|
||||
v-model="quotationWorkName"
|
||||
/>
|
||||
<q-input
|
||||
readonly
|
||||
:label="$t('quotation.contactName')"
|
||||
outlined
|
||||
dense
|
||||
class="col-md col-6"
|
||||
v-model="quotationContactName"
|
||||
/>
|
||||
<q-input
|
||||
readonly
|
||||
:label="$t('general.telephone')"
|
||||
outlined
|
||||
dense
|
||||
class="col-md col-6"
|
||||
v-model="quotationContactTel"
|
||||
/>
|
||||
<q-input
|
||||
:readonly
|
||||
:label="$t('creditNote.label.quotationCreatedBy')"
|
||||
outlined
|
||||
dense
|
||||
class="col-md col-6"
|
||||
:disable="!readonly"
|
||||
:model-value="quotationCreatedBy || '-'"
|
||||
/>
|
||||
</main>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
116
src/pages/12_debit-note/expansion/PaymentExpansion.vue
Normal file
116
src/pages/12_debit-note/expansion/PaymentExpansion.vue
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
<script lang="ts" setup>
|
||||
import useOptionStore from 'src/stores/options';
|
||||
import { watch } from 'vue';
|
||||
import QuotationFormInfo from 'src/pages/05_quotation/QuotationFormInfo.vue';
|
||||
import { PayCondition } from 'src/stores/quotations';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
installmentNo?: number[];
|
||||
installmentAmount?: number;
|
||||
}>(),
|
||||
{},
|
||||
);
|
||||
|
||||
const payBillDate = defineModel<Date | null | undefined>('payBillDate', {
|
||||
required: false,
|
||||
});
|
||||
const payType = defineModel<PayCondition>('payType', { required: true });
|
||||
const paySplitCount = defineModel<number | null>('paySplitCount', {
|
||||
default: 1,
|
||||
});
|
||||
const paySplit = defineModel<{ no: number; name?: string; amount: number }[]>(
|
||||
'paySplit',
|
||||
{ required: true },
|
||||
);
|
||||
const summaryPrice = defineModel<{
|
||||
totalPrice: number;
|
||||
totalDiscount: number;
|
||||
vat: number;
|
||||
vatExcluded: number;
|
||||
finalPrice: number;
|
||||
}>('summaryPrice', {
|
||||
required: true,
|
||||
default: {
|
||||
totalPrice: 0,
|
||||
totalDiscount: 0,
|
||||
vat: 0,
|
||||
vatExcluded: 0,
|
||||
finalPrice: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
</script>
|
||||
<template>
|
||||
<q-expansion-item
|
||||
dense
|
||||
class="overflow-hidden bordered full-width"
|
||||
switch-toggle-side
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||
>
|
||||
<template #header>
|
||||
<span>
|
||||
{{ $t('general.payment') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<main class="surface-1 q-pa-md full-width">
|
||||
<QuotationFormInfo
|
||||
v-bind="{ ...$props }"
|
||||
debit-note
|
||||
v-model:pay-type="payType"
|
||||
v-model:pay-split-count="paySplitCount"
|
||||
v-model:pay-split="paySplit"
|
||||
v-model:pay-bill-date="payBillDate"
|
||||
v-model:summary-price="summaryPrice"
|
||||
/>
|
||||
</main>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
<style scoped>
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 1/1;
|
||||
font-size: 1.5rem;
|
||||
padding: var(--size-1);
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
:deep(.price-tag .q-field__control) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 90px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.credit-note-color {
|
||||
--_color: var(--indigo-10-hsl);
|
||||
}
|
||||
|
||||
.bg-color {
|
||||
color: white;
|
||||
background: hsla(var(--_color));
|
||||
}
|
||||
|
||||
.dark .bg-color {
|
||||
--_color: var(--orange-6-hsl);
|
||||
}
|
||||
|
||||
.bg-color-light {
|
||||
background: hsla(var(--_color) / 0.1);
|
||||
}
|
||||
|
||||
.dark .bg-color-light {
|
||||
--_color: var(--orange-6-hsl / 0.2);
|
||||
}
|
||||
|
||||
.price-container > * {
|
||||
padding: var(--size-1);
|
||||
}
|
||||
</style>
|
||||
105
src/pages/12_debit-note/expansion/ProductExpansion.vue
Normal file
105
src/pages/12_debit-note/expansion/ProductExpansion.vue
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<script lang="ts" setup>
|
||||
import ProductItem from 'src/components/05_quotation/ProductItem.vue';
|
||||
import { AddButton } from 'components/button';
|
||||
import {
|
||||
ProductRelation,
|
||||
ProductServiceList,
|
||||
QuotationPayload,
|
||||
} from 'src/stores/quotations';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
agentPrice: boolean;
|
||||
installmentInput?: boolean;
|
||||
maxInstallment?: number | null;
|
||||
creditNote?: boolean;
|
||||
employeeRows: {
|
||||
foreignRefNo: string;
|
||||
employeeName: string;
|
||||
birthDate: string;
|
||||
gender: string;
|
||||
age: string;
|
||||
nationality: string;
|
||||
documentExpireDate: string;
|
||||
imgUrl: string;
|
||||
status: string;
|
||||
}[];
|
||||
}>();
|
||||
|
||||
const rows = defineModel<
|
||||
Required<QuotationPayload['productServiceList'][number]>[]
|
||||
>('rows', { required: true });
|
||||
|
||||
defineEmits<{
|
||||
(e: 'addProduct'): void;
|
||||
(e: 'viewFile', data: ProductRelation): void;
|
||||
(e: 'delete', index: number): void;
|
||||
(
|
||||
e: 'updateTable',
|
||||
v: QuotationPayload['productServiceList'][number],
|
||||
opt?: {
|
||||
newInstallmentNo: number;
|
||||
},
|
||||
): void;
|
||||
|
||||
(e: 'updateRows', v: Required<ProductServiceList>[]): void;
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<q-expansion-item
|
||||
dense
|
||||
class="overflow-hidden bordered full-width"
|
||||
switch-toggle-side
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||
default-opened
|
||||
>
|
||||
<template #header>
|
||||
<span
|
||||
class="row items-center justify-between full-width"
|
||||
style="min-height: 31.01px"
|
||||
>
|
||||
{{ $t('general.information', { msg: $t('taskOrder.productList') }) }}
|
||||
<AddButton
|
||||
icon-only
|
||||
@click.stop="$emit('addProduct')"
|
||||
v-if="!readonly"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<main class="q-px-md q-py-sm surface-1">
|
||||
<ProductItem
|
||||
:installment-input
|
||||
:max-installment
|
||||
:readonly
|
||||
:agent-price
|
||||
:employee-rows
|
||||
:rows="rows"
|
||||
@delete="(v) => $emit('delete', v)"
|
||||
@update:rows="(v) => $emit('updateRows', v)"
|
||||
@update-table="(data, opt) => $emit('updateTable', data, opt)"
|
||||
@view-file="(v) => $emit('viewFile', v)"
|
||||
/>
|
||||
</main>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
<style scoped>
|
||||
.product-status {
|
||||
padding-left: 8px;
|
||||
border-radius: 20px;
|
||||
color: hsl(var(--_color));
|
||||
background: hsla(var(--_color) / 0.15);
|
||||
|
||||
&.warning {
|
||||
--_color: var(--warning-bg);
|
||||
}
|
||||
&.positive {
|
||||
--_color: var(--positive-bg);
|
||||
}
|
||||
&.negative {
|
||||
--_color: var(--negative-bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
67
src/pages/12_debit-note/expansion/RemarkExpansion.vue
Normal file
67
src/pages/12_debit-note/expansion/RemarkExpansion.vue
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
|
||||
const remark = defineModel<string>('remark', { default: '' });
|
||||
</script>
|
||||
<template>
|
||||
<q-expansion-item
|
||||
dense
|
||||
class="overflow-hidden bordered full-width"
|
||||
switch-toggle-side
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||
>
|
||||
<template #header>
|
||||
<span>
|
||||
{{ $t('general.remark') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<main class="surface-1 q-pa-md full-width">
|
||||
<q-editor
|
||||
dense
|
||||
:readonly="readonly"
|
||||
:model-value="remark"
|
||||
min-height="5rem"
|
||||
class="full-width"
|
||||
toolbar-bg="input-border"
|
||||
style="cursor: auto; color: var(--foreground)"
|
||||
:content-class="readonly ? 'q-mt-sm' : 'bordered q-mt-sm rounded'"
|
||||
:flat="!readonly"
|
||||
:style="`width: ${$q.screen.gt.xs ? '100%' : '63vw'}`"
|
||||
:toolbar="[['left', 'center', 'justify'], ['clip']]"
|
||||
:toolbar-toggle-color="readonly ? 'disabled' : 'primary'"
|
||||
:toolbar-color="readonly ? 'disabled' : $q.dark.isActive ? 'white' : ''"
|
||||
:definitions="{
|
||||
clip: {
|
||||
icon: 'mdi-paperclip',
|
||||
tip: 'Upload',
|
||||
disable: readonly,
|
||||
handler: () => console.log('upload'),
|
||||
},
|
||||
}"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
remark = v;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</main>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
<style scoped>
|
||||
:deep(.q-editor__toolbar-group):nth-child(2) {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
:deep(.q-editor__toolbar.row.no-wrap.scroll-x) {
|
||||
background-color: var(--surface-2) !important;
|
||||
}
|
||||
|
||||
:deep(.q-editor__toolbar) {
|
||||
border-color: var(--surface-3) !important;
|
||||
}
|
||||
</style>
|
||||
77
src/pages/12_debit-note/expansion/WorkerItemExpansion.vue
Normal file
77
src/pages/12_debit-note/expansion/WorkerItemExpansion.vue
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<script setup lang="ts">
|
||||
import { AddButton } from 'components/button';
|
||||
import WorkerItem from 'src/components/05_quotation/WorkerItem.vue';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
hideBtnAddWorker?: boolean;
|
||||
employeeAmount?: number;
|
||||
rowWorker: {
|
||||
foreignRefNo: string;
|
||||
employeeName: string;
|
||||
birthDate: string;
|
||||
gender: string;
|
||||
age: string;
|
||||
nationality: string;
|
||||
documentExpireDate: string;
|
||||
imgUrl?: string;
|
||||
status: string;
|
||||
}[];
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'addWorker'): void;
|
||||
(e: 'edit'): void;
|
||||
(e: 'update:employeeAmount', number: number): void;
|
||||
(e: 'delete', index: number): void;
|
||||
}>();
|
||||
|
||||
const toggleWorker = defineModel<boolean>('toggleWorker');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-expansion-item
|
||||
for="item-up"
|
||||
id="item-up"
|
||||
dense
|
||||
class="overflow-hidden"
|
||||
switch-toggle-side
|
||||
default-opened
|
||||
style="border-radius: var(--radius-2)"
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-1"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<section class="row items-center full-width">
|
||||
<div class="row items-center q-pr-md q-py-sm">
|
||||
<span class="q-mr-md" style="font-size: 18px">
|
||||
{{ $t('quotation.employeeList') }}
|
||||
</span>
|
||||
<template v-if="!readonly">
|
||||
<ToggleButton class="q-mr-sm" v-model="toggleWorker" />
|
||||
{{ toggleWorker ? $t('general.specify') : $t('general.noSpecify') }}
|
||||
</template>
|
||||
</div>
|
||||
<nav class="q-ml-auto">
|
||||
<AddButton
|
||||
v-if="!hideBtnAddWorker"
|
||||
icon-only
|
||||
@click.stop="$emit('addWorker')"
|
||||
/>
|
||||
</nav>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<div class="surface-1 q-pa-md full-width">
|
||||
<WorkerItem
|
||||
@update:employee-amount="(v) => $emit('update:employeeAmount', v)"
|
||||
:employee-amount
|
||||
:readonly="readonly"
|
||||
:rows="rowWorker"
|
||||
@delete="(i) => $emit('delete', i)"
|
||||
/>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
64
src/pages/12_debit-note/form.ts
Normal file
64
src/pages/12_debit-note/form.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { dialog } from 'stores/utils';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
import { DebitNotePayload } from 'src/stores/debit-note';
|
||||
import { PayCondition } from 'src/stores/quotations';
|
||||
|
||||
// NOTE: Import types
|
||||
|
||||
// NOTE: Import stores
|
||||
|
||||
const DEFAULT_DATA: DebitNotePayload = {
|
||||
productServiceList: [],
|
||||
debitNoteQuotationId: '',
|
||||
worker: [],
|
||||
payBillDate: new Date(),
|
||||
paySplit: [],
|
||||
paySplitCount: 0,
|
||||
payCondition: PayCondition.Full,
|
||||
dueDate: new Date(),
|
||||
discount: 0,
|
||||
status: 'CREATED',
|
||||
remark: '#[quotation-labor]<br/><br/>#[quotation-payment]',
|
||||
quotationId: '',
|
||||
agentPrice: false,
|
||||
};
|
||||
|
||||
export const useDebitNoteForm = defineStore('form-debit-note', () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
let resetFormData = structuredClone(DEFAULT_DATA);
|
||||
|
||||
const currentFormData = ref<DebitNotePayload>(structuredClone(resetFormData));
|
||||
|
||||
const currentFormState = ref<{
|
||||
mode: null | 'info' | 'create' | 'edit';
|
||||
}>({
|
||||
mode: null,
|
||||
});
|
||||
|
||||
function isFormDataDifferent() {
|
||||
const { ...resetData } = resetFormData;
|
||||
const { ...currData } = currentFormData.value;
|
||||
|
||||
return JSON.stringify(resetData) !== JSON.stringify(currData);
|
||||
}
|
||||
|
||||
function resetForm(clean = false) {
|
||||
if (clean) {
|
||||
currentFormData.value = structuredClone(DEFAULT_DATA);
|
||||
resetFormData = structuredClone(DEFAULT_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
currentFormData.value = structuredClone(resetFormData);
|
||||
|
||||
currentFormState.value.mode = 'info';
|
||||
}
|
||||
|
||||
return {
|
||||
isFormDataDifferent,
|
||||
resetForm,
|
||||
};
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue