Merge branch 'develop'

This commit is contained in:
Methapon Metanipat 2024-09-25 12:50:36 +07:00
commit cc35d9c53c
181 changed files with 22636 additions and 6120 deletions

31
.github/workflows/local-build-dev.yaml vendored Normal file
View file

@ -0,0 +1,31 @@
name: local-build-dev
# Intended for local network use.
# Remote access is possible if the host has a public IP address.
on:
workflow_dispatch:
env:
REGISTRY: ${{ vars.DOCKER_REGISTRY }}
jobs:
local-build-dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config-inline: |
[registry."${{ env.REGISTRY }}"]
http = true
insecure = true
- name: Build and Push Docker Image
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/jws/jws-frontend:dev
allow: security.insecure

View file

@ -0,0 +1,31 @@
name: local-build-release
# Intended for local network use.
# Remote access is possible if the host has a public IP address.
on:
workflow_dispatch:
env:
REGISTRY: ${{ vars.DOCKER_REGISTRY }}
jobs:
local-build-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
with:
config-inline: |
[registry."${{ env.REGISTRY }}"]
http = true
insecure = true
- name: Build and Push Docker Image
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/jws/jws-frontend:latest
allow: security.insecure

View file

@ -1,4 +1,4 @@
FROM node:20-slim as build-stage
FROM node:20-slim AS build-stage
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
@ -19,7 +19,7 @@ ENV VITE_API_BASE_URL_OCR=ENV_API_BASE_URL_OCR
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run build
FROM alpine as production-stage
FROM alpine AS production-stage
WORKDIR /app

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -854,6 +854,21 @@
"label": "Certificate of Identity",
"value": "ci"
}
],
"expenseType": [
{
"label": "Service Fee",
"value": "serviceFee"
},
{
"label": "Fee",
"value": "fee"
},
{
"label": "Processing Fee",
"value": "processingFee"
}
]
},
@ -1712,6 +1727,21 @@
"label": "เอกสารสำคัญประจำตัวเพื่อใช้แทนหนังสือเดินทาง",
"value": "ci"
}
],
"expenseType": [
{
"label": "ค่าบริการ",
"value": "serviceFee"
},
{
"label": "ค่าธรรมเนียม",
"value": "fee"
},
{
"label": "ค่าดำเนินการ",
"value": "processingFee"
}
]
}
}

167
reports.json Normal file
View file

@ -0,0 +1,167 @@
{
"config": {
"configFile": "/Users/linping/Desktop/Chamomind&FrappeT/JWS_TestScript/playwright.config.ts",
"rootDir": "/Users/linping/Desktop/Chamomind&FrappeT/JWS_TestScript/tests",
"forbidOnly": false,
"fullyParallel": true,
"globalSetup": null,
"globalTeardown": null,
"globalTimeout": 0,
"grep": {},
"grepInvert": null,
"maxFailures": 0,
"metadata": {
"actualWorkers": 1
},
"preserveOutput": "always",
"reporter": [
[
"json",
{
"outputFile": "reports.json"
}
]
],
"reportSlowTests": {
"max": 5,
"threshold": 15000
},
"quiet": false,
"projects": [
{
"outputDir": "/Users/linping/Desktop/Chamomind&FrappeT/JWS_TestScript/test-results",
"repeatEach": 1,
"retries": 0,
"metadata": {},
"id": "chromium",
"name": "chromium",
"testDir": "/Users/linping/Desktop/Chamomind&FrappeT/JWS_TestScript/tests",
"testIgnore": [],
"testMatch": [
"**/*.@(spec|test).?(c|m)[jt]s?(x)"
],
"timeout": 30000
}
],
"shard": null,
"updateSnapshots": "missing",
"version": "1.44.1",
"workers": 1,
"webServer": null
},
"suites": [
{
"title": "01-Admin-BranchManagement/JWS_BM_001_CreateHeadquarters.spec.ts",
"file": "01-Admin-BranchManagement/JWS_BM_001_CreateHeadquarters.spec.ts",
"column": 0,
"line": 0,
"specs": [
{
"title": "Login",
"ok": true,
"tags": [],
"tests": [
{
"timeout": 30000,
"annotations": [],
"expectedStatus": "passed",
"projectId": "chromium",
"projectName": "chromium",
"results": [
{
"workerIndex": 4,
"status": "passed",
"duration": 3024,
"errors": [],
"stdout": [],
"stderr": [],
"retry": 0,
"startTime": "2024-07-30T02:59:00.817Z",
"attachments": []
}
],
"status": "expected"
}
],
"id": "8c5091bd59605f227965-8109f0f4a59e27330a76",
"file": "01-Admin-BranchManagement/JWS_BM_001_CreateHeadquarters.spec.ts",
"line": 16,
"column": 1
},
{
"title": "Create Branch Managenment",
"ok": true,
"tags": [],
"tests": [
{
"timeout": 30000,
"annotations": [],
"expectedStatus": "passed",
"projectId": "chromium",
"projectName": "chromium",
"results": [
{
"workerIndex": 4,
"status": "passed",
"duration": 5091,
"errors": [],
"stdout": [],
"stderr": [],
"retry": 0,
"startTime": "2024-07-30T02:59:05.659Z",
"attachments": []
}
],
"status": "expected"
}
],
"id": "8c5091bd59605f227965-5a0d70f27623401a3479",
"file": "01-Admin-BranchManagement/JWS_BM_001_CreateHeadquarters.spec.ts",
"line": 27,
"column": 1
},
{
"title": "Create Branch Managenment Second",
"ok": true,
"tags": [],
"tests": [
{
"timeout": 30000,
"annotations": [],
"expectedStatus": "passed",
"projectId": "chromium",
"projectName": "chromium",
"results": [
{
"workerIndex": 4,
"status": "passed",
"duration": 5029,
"errors": [],
"stdout": [],
"stderr": [],
"retry": 0,
"startTime": "2024-07-30T02:59:10.755Z",
"attachments": []
}
],
"status": "expected"
}
],
"id": "8c5091bd59605f227965-d619bd2184e7f07d4970",
"file": "01-Admin-BranchManagement/JWS_BM_001_CreateHeadquarters.spec.ts",
"line": 52,
"column": 1
}
]
}
],
"errors": [],
"stats": {
"startTime": "2024-07-30T02:59:00.334Z",
"duration": 15556.794999999925,
"expected": 3,
"skipped": 0,
"unexpected": 0,
"flaky": 0
}
}

View file

@ -3,6 +3,7 @@ import { boot } from 'quasar/wrappers';
import { getToken } from 'src/services/keycloak';
import { dialog } from 'stores/utils';
import useLoader from 'stores/loader';
import useFlowStore from 'src/stores/flow';
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
@ -32,6 +33,7 @@ function parseError(
api.interceptors.request.use(async (config) => {
useLoader().show();
config.headers.Authorization = `Bearer ${await getToken()}`;
config.headers['X-Rtid'] = useFlowStore().rtid;
return config;
});

View file

@ -3,25 +3,22 @@ import { baseUrl } from 'stores/utils';
defineProps<{
inactive?: boolean;
color?: 'none' | 'hq' | 'br';
i18nKey?: string;
color?: 'none' | 'hq' | 'br' | 'br-virtual';
data: {
branchLabelCode: string;
branchLabelCode?: string;
branchLabelName: string;
branchLabelTel: string;
branchLabelAddress: string;
branchLabelType: string;
branchImgUrl: string;
taxNo?: string;
branchLabelTel?: string;
contactName?: string;
branchLabelAddress?: string;
branchImgUrl?: string;
};
virtualBranch: boolean;
metadata?: unknown;
badgeField?: string[];
fieldSelected?: (
| 'orderNumber'
| 'branchLabelName'
| 'branchLabelAddress'
| 'branchLabelTel'
| 'branchLabelType'
)[];
fieldSelected?: string[];
footer?: boolean;
}>();
</script>
@ -36,12 +33,15 @@ defineProps<{
color !== 'hq' && color !== 'br' && (!color || color === 'none'),
'branch-card__hq': color === 'hq',
'branch-card__br': color === 'br',
'branch-card__br-virtual': color === 'br-virtual',
}"
@click="$emit('open')"
>
<div class="branch-card__header">
<div class="branch-card__wrapper">
<slot name="image"></slot>
<q-img
v-if="!$slots.image"
:src="baseUrl + data.branchImgUrl"
style="
height: 3rem;
@ -58,13 +58,32 @@ defineProps<{
</template>
</q-img>
</div>
<div class="branch-card__name">
<div class="branch-card__name flex justify-center q-ml-sm">
<b>{{ data.branchLabelName }}</b>
<small class="branch-card__code">{{ data.branchLabelCode }}</small>
<small class="branch-card__code" v-if="data.branchLabelCode">
{{ data.branchLabelCode }}
</small>
</div>
<div class="branch-card__action">
<slot name="action" />
<div class="text-right">
<slot name="action" />
</div>
<div
v-if="color === 'br'"
class="q-pa-xs rounded"
:style="`background: hsl(var(${virtualBranch ? '--blue-6-hsl' : '--purple-6-hsl'}) / 0.1)`"
>
<b
:style="`color: hsl(var(${virtualBranch ? '--blue-8-hsl' : '--purple-8-hsl'}) )`"
>
{{
$t(
`${i18nKey || 'branch.card'}.${virtualBranch ? 'branchVirtual' : 'branchLabel'}`,
)
}}
</b>
</div>
</div>
</div>
<div
@ -76,17 +95,21 @@ defineProps<{
margin-bottom: var(--size-2);
"
/>
<div
v-for="key in fieldSelected?.sort() || [
'branchLabelAddress',
'branchLabelTel',
'branchLabelType',
]"
class="branch-card__data"
>
<div>{{ $t(`branch.card.${key}`) }}</div>
<div>{{ data[key as keyof typeof data] }}</div>
</div>
<slot name="data"></slot>
<template v-if="!$slots.data">
<div
v-for="key in fieldSelected || [
'branchLabelAddress',
'branchLabelTel',
'branchLabelType',
]"
:key="key"
class="branch-card__data"
>
<div>{{ $t(`${i18nKey || 'branch.card'}.${key}`) }}</div>
<div>{{ data[key as keyof typeof data] }}</div>
</div>
</template>
</div>
</template>
@ -189,6 +212,10 @@ defineProps<{
}
}
&.branch-card__br-virtual {
--_branch-card-bg: var(--blue-6-hsl);
}
&.branch-card__inactive {
--_branch-status-color: var(--red-4-hsl);
--_branch-badge-bg: var(--red-4-hsl);

View file

@ -8,10 +8,28 @@ import { QSelect } from 'quasar';
import { AddButton, DeleteButton } from 'components/button';
import ToggleButton from '../button/ToggleButton.vue';
import ImageHover from '../ImageHover.vue';
const optionStore = useOptionStore();
const bankBookList = defineModel<BankBook[]>('bankBookList', { default: [] });
const bankQrUrl = ref<string[]>([]);
const listIndex = ref(0);
const reader = new FileReader();
const inputFile = (() => {
const _element = document.createElement('input');
_element.type = 'file';
_element.accept = 'image/*';
_element.addEventListener('change', change);
return _element;
})();
reader.addEventListener('load', () => {
if (typeof reader.result === 'string')
bankQrUrl.value[listIndex.value] = reader.result;
});
defineProps<{
title?: string;
dense?: boolean;
@ -20,6 +38,11 @@ defineProps<{
view?: boolean;
}>();
defineEmits<{
(e: 'viewQr', index: number): void;
(e: 'editQr', index: number): void;
}>();
const bankBookOptions = ref<Record<string, unknown>[]>([]);
let bankBoookFilter: (
value: string,
@ -43,6 +66,15 @@ function addBankBook() {
});
}
function change(e: Event) {
const _element = e.target as HTMLInputElement | null;
const _file = _element?.files?.[0];
if (_file) {
bankBookList.value[listIndex.value].bankQr = _file;
reader.readAsDataURL(_file);
}
}
onMounted(() => {
if (optionStore.globalOption) {
bankBoookFilter = selectFilterOptionRefMod(
@ -99,7 +131,7 @@ watch(
<div
v-for="(book, i) in bankBookList"
class="col-12 row q-col-gutter-sm"
class="col-12 row"
:class="{ 'q-pt-lg': i !== 0 }"
:key="i"
>
@ -129,144 +161,132 @@ watch(
v-if="bankBookList.length !== 1 && !readonly"
id="btn-delete-bank"
icon-only
@click="deleteItem(bankBookList, i)"
@click="
() => {
deleteItem(bankBookList, i);
bankQrUrl[i] = '';
}
"
/>
</div>
</span>
<q-select
outlined
clearable
use-input
emit-value
fill-input
map-options
hide-bottom-space
option-value="value"
input-debounce="0"
option-label="label"
lazy-rules="ondemand"
class="col-12 col-md-4"
autocomplete="off"
:dense="dense"
:label="$t('branch.form.bank')"
:options="bankBookOptions"
:readonly="readonly"
:hide-dropdown-icon="readonly"
for="select-bankbook"
:model-value="readonly ? book.bankName || '-' : book.bankName"
@update:model-value="
(v) => (typeof v === 'string' ? (book.bankName = v) : '')
"
@filter="bankBoookFilter"
@clear="book.bankName = ''"
>
<template v-slot:option="scope">
<q-item
v-if="scope.opt"
v-bind="scope.itemProps"
class="row items-center"
>
<q-item-section avatar>
<q-img
:src="`/img/bank/${scope.opt.value}.png`"
class="bordered"
style="border-radius: 50%"
/>
</q-item-section>
<q-item-section>
{{ scope.opt.label }}
</q-item-section>
</q-item>
</template>
<template v-slot:selected-item="scope">
<q-item-section
v-if="scope.opt && book.bankName"
avatar
class="q-py-sm"
>
<q-img
:src="`/img/bank/${scope.opt.value}.png`"
class="bordered"
style="border-radius: 50%"
/>
</q-item-section>
</template>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
<q-input
outlined
for="input-bankbook"
class="col-md-4 col-12"
lazy-rules="ondemand"
hide-bottom-space
:dense="dense"
:readonly="readonly"
:label="$t('branch.form.bankBranch')"
:model-value="readonly ? book.bankBranch || '-' : book.bankBranch"
@update:model-value="
(v) => (typeof v === 'string' ? (book.bankBranch = v) : '')
"
/>
<q-select
outlined
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
option-value="value"
input-debounce="0"
option-label="label"
lazy-rules="ondemand"
class="col-12 col-md-4"
autocomplete="off"
:dense="dense"
:label="$t('branch.form.bankAccountType')"
:options="accountTypeOptions"
:readonly="readonly"
:hide-dropdown-icon="readonly"
for="select-bankbook"
:model-value="readonly ? book.accountType || '-' : book.accountType"
@update:model-value="
(v) => (typeof v === 'string' ? (book.accountType = v) : '')
"
@filter="accountTypeFilter"
@clear="book.accountType = ''"
<div
class="bordered q-mr-sm rounded"
:class="{ 'cursor-pointer': !readonly }"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
<q-input
outlined
for="input-bankbook"
class="col-12 col-md-4"
lazy-rules="ondemand"
hide-bottom-space
:dense="dense"
:readonly="readonly"
:label="$t('branch.form.bankAccountNumber')"
:maxlength="13"
:model-value="readonly ? book.accountNumber || '-' : book.accountNumber"
@update:model-value="
(v) => (typeof v === 'string' ? (book.accountNumber = v) : '')
"
/>
<!-- :rules="[
<ImageHover
:img="book.bankUrl"
@view="() => $emit('viewQr', i)"
@edit="
() => {
$emit('editQr', i);
}
"
icon="mdi-qrcode"
color="gray"
bg-color="white"
/>
</div>
<div class="col">
<div class="row q-col-gutter-sm">
<q-select
outlined
clearable
use-input
emit-value
fill-input
map-options
hide-bottom-space
option-value="value"
input-debounce="0"
option-label="label"
class="col-12 col-md-4"
autocomplete="off"
:dense="dense"
:label="$t('branch.form.bank')"
:options="bankBookOptions"
:readonly="readonly"
:hide-dropdown-icon="readonly"
for="select-bankbook"
:model-value="readonly ? book.bankName || '-' : book.bankName"
@update:model-value="
(v) => (typeof v === 'string' ? (book.bankName = v) : '')
"
@filter="bankBoookFilter"
@clear="book.bankName = ''"
>
<template v-slot:option="scope">
<q-item
v-if="scope.opt"
v-bind="scope.itemProps"
class="row items-center"
>
<q-item-section avatar>
<q-img
:src="`/img/bank/${scope.opt.value}.png`"
class="bordered"
style="border-radius: 50%"
/>
</q-item-section>
<q-item-section>
{{ scope.opt.label }}
</q-item-section>
</q-item>
</template>
<template v-slot:selected-item="scope">
<q-item-section
v-if="scope.opt && book.bankName"
avatar
class="q-py-sm"
>
<q-img
:src="`/img/bank/${scope.opt.value}.png`"
class="bordered"
style="border-radius: 50%"
/>
</q-item-section>
</template>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
<q-input
outlined
for="input-bankbook"
class="col-12 col-md-4"
hide-bottom-space
:dense="dense"
:readonly="readonly"
:label="$t('branch.form.bankAccountNumber')"
:maxlength="15"
:model-value="
readonly ? book.accountNumber || '-' : book.accountNumber
"
@update:model-value="
(v) => (typeof v === 'string' ? (book.accountNumber = v) : '')
"
/>
<q-input
outlined
for="input-bankbook"
class="col-md-4 col-12"
hide-bottom-space
:dense="dense"
:readonly="readonly"
:label="$t('branch.form.bankBranch')"
:model-value="readonly ? book.bankBranch || '-' : book.bankBranch"
@update:model-value="
(v) => (typeof v === 'string' ? (book.bankBranch = v) : '')
"
/>
<!-- :rules="[
(val: string) =>
(val.length >= 7 && val.length <= 13) ||
$t('form.error.please', {
@ -274,20 +294,57 @@ watch(
}),
]" -->
<q-input
outlined
for="input-bankbook"
class="col-12 col-md-4"
lazy-rules="ondemand"
hide-bottom-space
:dense="dense"
:readonly="readonly"
:label="$t('branch.form.bankAccountName')"
:model-value="readonly ? book.accountName || '-' : book.accountName"
@update:model-value="
(v) => (typeof v === 'string' ? (book.accountName = v) : '')
"
/>
<q-input
outlined
for="input-bankbook"
class="col-12 col-md-4"
hide-bottom-space
:dense="dense"
:readonly="readonly"
:label="$t('branch.form.bankAccountName')"
:model-value="readonly ? book.accountName || '-' : book.accountName"
@update:model-value="
(v) => (typeof v === 'string' ? (book.accountName = v) : '')
"
/>
<q-select
outlined
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
option-value="value"
input-debounce="0"
option-label="label"
class="col-12 col-md-4"
autocomplete="off"
:dense="dense"
:label="$t('branch.form.bankAccountType')"
:options="accountTypeOptions"
:readonly="readonly"
:hide-dropdown-icon="readonly"
for="select-bankbook"
:model-value="readonly ? book.accountType || '-' : book.accountType"
@update:model-value="
(v) => (typeof v === 'string' ? (book.accountType = v) : '')
"
@filter="accountTypeFilter"
@clear="book.accountType = ''"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss"></style>

View file

@ -32,13 +32,22 @@ defineProps<{
<div class="col-12 row q-col-gutter-sm">
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-4"
:label="$t('form.email')"
:rules="
readonly
? undefined
: [
(v: string) =>
!v ||
/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g.test(v) ||
$t('form.error.invalid'),
]
"
for="input-email"
:model-value="readonly ? email || '-' : email"
@update:model-value="(v) => (typeof v === 'string' ? (email = v) : '')"
@ -54,7 +63,6 @@ defineProps<{
</q-input>
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
@ -78,7 +86,6 @@ defineProps<{
</q-input>
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
@ -91,7 +98,6 @@ defineProps<{
/>
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
@ -106,7 +112,6 @@ defineProps<{
/>
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
@ -130,7 +135,6 @@ defineProps<{
</q-input>
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"

View file

@ -1,4 +1,7 @@
<script setup lang="ts">
import { isRoleInclude } from 'src/stores/utils';
import DatePicker from '../shared/DatePicker.vue';
const code = defineModel<string>('code');
const branchCount = defineModel<number>('branchCount', { default: 0 });
const codeSubBranch = defineModel<string>('codeSubBranch');
@ -7,6 +10,11 @@ const name = defineModel<string>('name');
const abbreviation = defineModel<string>('abbreviation');
const nameEN = defineModel<string>('nameEN');
const typeBranch = defineModel<string>('typeBranch');
const virtual = defineModel<boolean>('virtual');
const permitExpireDate = defineModel<Date>('permitExpireDate');
const permitIssueDate = defineModel<Date>('permitIssueDate');
const permitNo = defineModel<string>('permitNo');
defineProps<{
title?: string;
@ -42,7 +50,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
<div class="col-12 row q-col-gutter-sm">
<div class="col-12 row q-col-gutter-sm">
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:disable="view && !readonly"
@ -53,14 +60,16 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
for="input-abbreviation"
:model-value="view ? formatCode(abbreviation, 'code') : abbreviation"
:rules="[
(val) => (val && val.length > 0) || $t('form.error.required'),
(val) =>
(val && val.length > 0 && /^[a-zA-Z]+$/.test(val)) ||
$t('form.error.invalid'),
(val && val.length > 0 && /^[a-zA-Z0-9]+$/.test(val)) ||
$t('form.error.invalidCustomeMessage', {
msg: $t('form.error.letterAndNumOnly'),
}),
]"
@update:model-value="(v) => (abbreviation = v as string)"
/>
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
readonly
@ -83,26 +92,7 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
@update:model-value="(v) => (code = v as string)"
/>
<!-- view ? `${formatCode(code, 'number')}${branchCount}` : code -->
<!-- <q-input
lazy-rules="ondemand"
v-if="typeBranch !== 'headOffice'"
:dense="dense"
outlined
:disable="view && !readonly"
:readonly="readonly"
hide-bottom-space
class="col"
:label="$t('branch.form.codeBranch')"
for="input-code-sub-branch"
:model-value="
view ? formatCode(codeSubBranch, 'number') : codeSubBranch
"
@update:model-value="(v) => (codeSubBranch = v as string)"
/> -->
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
@ -110,11 +100,14 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
class="col-md-5 col-12"
:label="$t('branch.form.taxNo')"
v-model="taxNo"
mask="#############"
:rules="[
(val) => (val && val.length > 0) || $t('form.error.required'),
(val) =>
(val && val.length === 13 && /[0-9]+/.test(val)) ||
$t('form.error.invalid'),
$t('form.error.invalidCustomeMessage', {
msg: $t('form.error.requireLength', { msg: 13 }),
}),
]"
for="input-tax-no"
/>
@ -122,7 +115,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
<div class="col-12 row q-col-gutter-sm">
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
@ -139,7 +131,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
for="input-name"
/>
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
@ -151,9 +142,81 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
? $t('branch.form.headofficeNameEN')
: $t('branch.form.branchNameEN')
"
:rules="[
(val) => !!val || $t('form.error.required'),
(val) =>
/^[A-Za-z0-9\s.,]+$/.test(val) || $t('form.error.letterOnly'),
]"
for="input-name-en"
/>
<q-select
v-if="
typeBranch !== 'headOffice' &&
isRoleInclude(['head_of_admin', 'head_of_account'])
"
outlined
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="value"
class="col-2"
dense
for="input-branch-status"
:readonly="readonly || isRoleInclude(['head_of_account'])"
:options="['Virtual', 'Branch']"
:hide-dropdown-icon="readonly"
:label="$t('general.branchStatus')"
:model-value="virtual ? 'Virtual' : 'Branch'"
@update:model-value="(v) => (virtual = v === 'Virtual')"
:rules="[(val) => val && val.length > 0]"
:error-message="$t('form.error.required')"
for="input-name-en"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 row q-col-gutter-sm">
<q-input
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-4"
:label="$t('general.licenseNumber')"
v-model="permitNo"
:rules="[(val) => val && val.length > 0]"
:error-message="$t('form.error.required')"
for="input-name"
/>
<DatePicker
class="col-3"
id="input-start-date"
:readonly="readonly"
:label="$t('general.dateOfIssue')"
v-model="permitIssueDate"
clearable
/>
<DatePicker
class="col-3"
id="input-start-date"
:readonly="readonly"
:label="$t('general.expirationDate')"
v-model="permitExpireDate"
clearable
/>
</div>
</div>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import ImageHover from '../ImageHover.vue';
const qr = defineModel<string | null>('qr');
@ -13,6 +13,8 @@ defineProps<{
defineEmits<{
(e: 'upload'): void;
(e: 'viewQr'): void;
(e: 'editQr'): void;
}>();
</script>
<template>
@ -35,47 +37,14 @@ defineEmits<{
}"
class="col-12 row branch-form-show-qr-code"
>
<div class="col-12 column flex-center q-py-md">
<q-img
v-if="qr"
:src="qr as string"
style="width: 150px; height: 150px"
>
<template #error>
<div
style="background: none"
class="full-width full-height items-center justify-center flex"
>
<q-img src="/no-data.png" width="5rem" />
</div>
</template>
</q-img>
<q-btn
v-else
@click="$emit('upload')"
class="branch-form-btn-qr-code q-mb-md"
:class="{ 'dark-form-btn-qr-code': $q.dark.isActive }"
unelevated
:color="$q.dark.isActive ? 'black' : 'grey-2'"
:text-color="$q.dark.isActive ? 'white' : 'grey-5'"
>
<Icon icon="teenyicons:add-outline" width="30px" height="50px" />
</q-btn>
<q-btn
v-if="!readonly"
:text-color="$q.dark.isActive ? 'black' : 'white'"
style="
background: var(--blue-5);
color: var(--blue-0);
font-size: 12px;
"
unelevated
rounded
:label="$t('general.upload')"
@click="$emit('upload')"
id="btn-upload-qr-code"
<div class="col-12 column flex-center text-center q-py-md">
<ImageHover
:img="qr"
@view="() => $emit('viewQr')"
@edit="() => $emit('editQr')"
icon="mdi-qrcode"
color="gray"
bg-color="white"
/>
</div>
</div>

View file

@ -148,7 +148,6 @@ watch(
style="margin-left: 0px; padding-left: 0px"
>
<q-input
lazy-rules="ondemand"
for="input-regis-no"
:dense="dense"
outlined
@ -193,7 +192,6 @@ watch(
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
id="input-responsible-area"
:dense="dense"
:readonly="readonly"
@ -222,7 +220,6 @@ watch(
style="row-gap: 16px"
>
<q-input
lazy-rules="ondemand"
for="input-discount-condition"
:dense="dense"
outlined
@ -254,9 +251,9 @@ watch(
input-debounce="0"
option-value="value"
option-label="label"
lazy-rules="ondemand"
class="col-md-3 col-6"
id="input-source-nationality"
for="input-source-nationality"
:dense="dense"
:readonly="readonly"
:hide-dropdown-icon="readonly"
@ -290,8 +287,8 @@ watch(
option-value="value"
option-label="label"
class="col-md-3 col-6"
lazy-rules="ondemand"
id="input-import-nationality"
for="input-import-nationality"
:dense="dense"
:readonly="readonly"
:hide-dropdown-icon="readonly"
@ -324,9 +321,9 @@ watch(
input-debounce="0"
option-label="label"
option-value="label"
lazy-rules="ondemand"
class="col-md-6 col-12"
id="select-trainig-place"
for="select-trainig-place"
:dense="dense"
:readonly="readonly"
:hide-dropdown-icon="readonly"
@ -348,7 +345,6 @@ watch(
</template>
</q-select>
<q-input
lazy-rules="ondemand"
for="input-checkpoint"
:dense="dense"
outlined
@ -362,7 +358,6 @@ watch(
@clear="checkpoint = ''"
/>
<q-input
lazy-rules="ondemand"
for="input-checkpoint-en"
:dense="dense"
outlined
@ -416,6 +411,7 @@ watch(
<q-list bordered separator class="rounded" style="padding: 0">
<q-item
id="attachment-file"
for="attachment-file"
v-for="item in agencyFileList"
clickable
:key="item.url"

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import useUserStore from 'stores/user';
import { selectFilterOptionRefMod } from 'stores/utils';
import { onMounted, ref } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { isRoleInclude } from 'src/stores/utils';
@ -15,7 +15,7 @@ const userRole = defineModel<string>('userRole');
const username = defineModel<string | null | undefined>('username');
const userCode = defineModel<string>('userCode');
defineProps<{
const props = defineProps<{
title?: string;
dense?: boolean;
outlined?: boolean;
@ -73,9 +73,33 @@ const roleFilter = selectFilterOptionRefMod(
);
onMounted(async () => {
if (userStore.userOption.hqOpts[0].value)
if (userStore.userOption.hqOpts[0].value && !props.readonly) {
await userStore.fetchBrOption(userStore.userOption.hqOpts[0].value);
if (userStore.userOption.brOpts.length === 1) {
brId.value = userStore.userOption.brOpts[0].value;
}
brFilter = selectFilterOptionRefMod(
ref(userStore.userOption.brOpts),
brOptions,
'label',
);
}
});
watch(
() => hqId.value,
async (v) => {
if (v) {
userStore.userOption.brOpts = [];
await userStore.fetchBrOption(v);
brFilter = selectFilterOptionRefMod(
ref(userStore.userOption.brOpts),
brOptions,
'label',
);
}
},
);
</script>
<template>
<div class="row col-12">
@ -107,7 +131,6 @@ onMounted(async () => {
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
class="col-md-2 col-12"
:dense="dense"
:readonly="readonly"
@ -148,7 +171,6 @@ onMounted(async () => {
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
class="col-md-2 col-12"
:disable="isRoleInclude(['branch_manager']) && !readonly"
:dense="dense"
@ -170,7 +192,6 @@ onMounted(async () => {
</template>
</q-select>
<q-input
lazy-rules="ondemand"
for="input-username"
:dense="dense"
outlined
@ -204,7 +225,6 @@ onMounted(async () => {
input-debounce="0"
option-value="value"
option-label="label"
lazy-rules="ondemand"
for="select-user-type"
:dense="dense"
:readonly="readonly"
@ -241,7 +261,6 @@ onMounted(async () => {
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
for="select-user-role"
:dense="dense"
v-model="userRole"
@ -266,7 +285,7 @@ onMounted(async () => {
</q-item>
</template>
</q-select>
<!-- <q-input lazy-rules="ondemand"
<!-- <q-input
id="input-user-code"
:dense="dense"
:outlined="readonly ? false : outlined"

View file

@ -20,8 +20,11 @@ const birthDate = defineModel<Date | string | null>('birthDate');
const nationality = defineModel<string>('nationality');
const midName = defineModel<string | null>('midName');
const midNameEN = defineModel<string | null>('midNameEN');
const citizenId = defineModel<string>('citizenId');
const citizenIssue = defineModel<Date | null>('citizenIssue');
const citizenExpire = defineModel<Date | null>('citizenExpire');
defineProps<{
const props = defineProps<{
dense?: boolean;
outlined?: boolean;
readonly?: boolean;
@ -91,8 +94,9 @@ watch(
watch(
() => prefixName.value,
(v) => {
if (props.readonly) return;
if (v === 'mr') gender.value = 'male';
else gender.value = 'female';
else if (v !== '') gender.value = 'female';
},
);
</script>
@ -111,6 +115,26 @@ watch(
</div>
<div class="col-12 row q-col-gutter-sm">
<q-input
v-if="!employee"
outlined
class="col-md-5 col-12"
hide-bottom-space
v-model="citizenId"
mask="#############"
:readonly="readonly"
:dense="dense"
:label="$t('personnel.form.citizenId')"
:rules="[
(val) => (val && val.length > 0) || $t('form.error.required'),
(val) =>
(val && val.length === 13 && /[0-9]+/.test(val)) ||
$t('form.error.invalidCustomeMessage', {
msg: $t('form.error.requireLength', { msg: 13 }),
}),
]"
for="input-citizen-id"
/>
<div class="col-12 row" style="display: flex; gap: var(--size-2)">
<q-select
outlined
@ -123,7 +147,6 @@ watch(
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
hide-dropdown-icon
class="col-md-1 col-6"
:dense="dense"
@ -148,7 +171,6 @@ watch(
</q-select>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-first-name`"
:dense="dense"
outlined
@ -161,7 +183,6 @@ watch(
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-mid-name`"
:dense="dense"
outlined
@ -177,7 +198,6 @@ watch(
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-last-name`"
:dense="dense"
outlined
@ -192,7 +212,6 @@ watch(
<div class="col-12 row" style="display: flex; gap: var(--size-2)">
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-first-name`"
:dense="dense"
outlined
@ -200,7 +219,7 @@ watch(
:readonly="readonly"
:disable="!readonly"
class="col-md-1 col-6"
:label="$t('personnel.form.prefixName')"
label="Title"
:model-value="
readonly
? capitalize(prefixName || '') || '-'
@ -213,26 +232,28 @@ watch(
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-first-name-en`"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col"
:label="$t('personnel.form.firstNameEN')"
label="Name"
v-model="firstNameEN"
:rules="[(val: string) => !!val || $t('form.error.required')]"
:rules="[
(val: string) => !!val || $t('form.error.required'),
(val: string) =>
/^[A-Za-z]+$/.test(val) || $t('form.error.letterOnly'),
]"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-mid-name-en`"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-3 col-6"
:label="$t('personnel.form.middleNameEN')"
label="Mid Name"
:model-value="readonly ? midNameEN || '-' : midNameEN"
@update:model-value="
(v) => (typeof v === 'string' ? (midNameEN = v) : '')
@ -240,21 +261,23 @@ watch(
@clear="midNameEN = ''"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-last-name-en`"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col"
:label="$t('personnel.form.lastNameEN')"
label="Surname"
v-model="lastNameEN"
:rules="[(val: string) => !!val || $t('form.error.required')]"
:rules="[
(val: string) => !!val || $t('form.error.required'),
(val: string) =>
/^[A-Za-z]+$/.test(val) || $t('form.error.letterOnly'),
]"
/>
</div>
<q-input
lazy-rules="ondemand"
v-if="!employee"
:for="`${prefixId}-input-telephone`"
:dense="dense"
@ -268,20 +291,48 @@ watch(
(v) => (typeof v === 'string' ? (telephoneNo = v) : '')
"
@clear="telephoneNo = ''"
/>
>
<template #prepend>
<q-icon
size="xs"
name="mdi-phone-outline"
class="cursor-pointer"
color="primary"
/>
</template>
</q-input>
<q-input
lazy-rules="ondemand"
v-if="!employee"
:for="`${prefixId}-input-email`"
:dense="dense"
outlined
hide-bottom-space
:readonly="readonly"
:label="$t('form.email')"
:rules="
readonly
? undefined
: [
(v: string) =>
!v ||
/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g.test(v) ||
$t('form.error.invalid'),
]
"
class="col-md-3 col-6"
:model-value="readonly ? email || '-' : email"
@update:model-value="(v) => (typeof v === 'string' ? (email = v) : '')"
@clear="email = ''"
/>
>
<template #prepend>
<q-icon
size="xs"
name="mdi-email-outline"
class="cursor-pointer"
color="primary"
/>
</template>
</q-input>
<q-select
v-if="!employee"
@ -295,7 +346,6 @@ watch(
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
class="col-md-2 col-6"
:dense="dense"
:readonly="readonly"
@ -332,7 +382,6 @@ watch(
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-age`"
:id="`${prefixId}-input-age`"
:dense="dense"
@ -348,6 +397,33 @@ watch(
"
/>
<DatePicker
v-if="!employee"
v-model="citizenIssue"
class="col-md-2 col-6"
:id="`${prefixId}-input-citizen-issue`"
:readonly="readonly"
:label="$t('personnel.form.citizenIssue')"
:disabled-dates="disabledAfterToday"
:rules="[
(val: string) =>
!!val ||
$t('form.error.selectField', {
field: $t('personnel.form.citizenIssue'),
}),
]"
/>
<DatePicker
v-if="!employee"
v-model="citizenExpire"
class="col-md-2 col-6"
:id="`${prefixId}-input-citizen-expire`"
:readonly="readonly"
:label="$t('personnel.form.citizenExpire')"
:disabled-dates="disabledAfterToday"
/>
<q-select
v-if="employee"
outlined
@ -361,7 +437,6 @@ watch(
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
class="col-2"
:dense="dense"
v-model="gender"
@ -395,7 +470,6 @@ watch(
option-label="label"
option-value="value"
v-model="nationality"
lazy-rules="ondemand"
class="col-2"
:dense="dense"
:readonly="readonly"

View file

@ -1,14 +1,12 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue';
import { dateFormat, parseAndFormatDate } from 'src/utils/datetime';
import useAddressStore, {
District,
Province,
SubDistrict,
} from 'stores/address';
import { EmployeeCheckupCreate } from 'stores/employee/types';
import { useI18n } from 'vue-i18n';
import { checkTabBeforeAdd, selectFilterOptionRefMod } from 'stores/utils';
import { selectFilterOptionRefMod } from 'stores/utils';
import { QSelect } from 'quasar';
import DatePicker from '../shared/DatePicker.vue';
import {
@ -19,7 +17,6 @@ import {
UndoButton,
} from 'components/button';
const { locale } = useI18n();
const adrressStore = useAddressStore();
const addrOptions = reactive<{
@ -58,8 +55,10 @@ withDefaults(
typeCustomer?: string;
prefixId: string;
showBtnSave?: boolean;
hideAction?: boolean;
}>(),
{
hideAction: false,
showBtnSave: false,
},
);
@ -98,8 +97,10 @@ function addCheckup() {
checkupResult: '',
checkupType: '',
});
if (employeeCheckup.value)
if (employeeCheckup.value) {
tab.value = `tab${employeeCheckup.value.length - 1}`;
currentIndex.value = employeeCheckup.value.length - 1;
}
}
}
@ -160,7 +161,7 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
/>
{{ $t(`customerEmployee.formHealthCheck.title`) }}
<AddButton
v-if="!readonly"
v-if="currentIndex === -1 && !hideAction"
id="btn-add-bank"
icon-only
class="q-ml-sm"
@ -185,7 +186,7 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
/>
<span class="col-12 flex justify-between items-center">
{{ $t('general.times', { number: index + 1 }) }}
<div class="row items-center">
<div class="row items-center" v-if="!hideAction">
<UndoButton
v-if="!readonly && !!checkup.id && !checkup.statusSave"
id="btn-info-health-undo"
@ -222,7 +223,6 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
</span>
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly || checkup.statusSave"
@ -244,7 +244,6 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
input-debounce="0"
option-value="value"
option-label="label"
lazy-rules="ondemand"
v-model="checkup.checkupType"
:dense="dense"
:readonly="readonly || checkup.statusSave"
@ -274,7 +273,6 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
option-value="id"
input-debounce="0"
option-label="name"
lazy-rules="ondemand"
class="col-2"
v-model="checkup.provinceId"
:dense="dense"
@ -295,7 +293,6 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
</q-select>
<!-- @filter="provinceFilter" -->
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly || checkup.statusSave"
@ -318,7 +315,6 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
input-debounce="0"
option-value="value"
option-label="label"
lazy-rules="ondemand"
v-model="checkup.medicalBenefitScheme"
:dense="dense"
:readonly="readonly || checkup.statusSave"
@ -337,7 +333,6 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
</template>
</q-select>
<q-input
lazy-rules="ondemand"
:label="$t('general.remark')"
:dense="dense"
outlined
@ -378,7 +373,6 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
v-model="checkup.insuranceCompany"
:dense="dense"
:readonly="readonly || checkup.statusSave"

View file

@ -15,6 +15,7 @@ defineProps<{
separator?: boolean;
employee?: boolean;
prefixId: string;
hideAction?: boolean;
}>();
const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
@ -31,7 +32,7 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
style="background-color: var(--surface-3)"
/>
{{ $t('customerEmployee.form.group.family') }}
<div class="row q-ml-auto">
<div class="row q-ml-auto" v-if="!hideAction">
<UndoButton
v-if="!readonly && !!employeeOther.id && !employeeOther.statusSave"
id="btn-info-health-undo"
@ -66,7 +67,6 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
<div class="col-12 row q-col-gutter-y-sm">
<div class="col-12 row q-col-gutter-sm">
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-citizen-id`"
:dense="dense"
outlined
@ -83,7 +83,6 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
</div>
<div class="col-12 row q-col-gutter-sm">
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-father-first-name`"
:dense="dense"
outlined
@ -94,7 +93,6 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
v-model="employeeOther.fatherFirstName"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-father-last-name`"
:dense="dense"
outlined
@ -105,7 +103,6 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
v-model="employeeOther.fatherLastName"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-father-first-name-en`"
:dense="dense"
outlined
@ -116,7 +113,6 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
v-model="employeeOther.fatherFirstNameEN"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-father-last-name-en`"
:dense="dense"
outlined
@ -127,7 +123,6 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
v-model="employeeOther.fatherLastNameEN"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-father-birthplace`"
:dense="dense"
outlined
@ -145,7 +140,6 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
</div>
<div class="col-12 row q-col-gutter-sm">
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-mother-first-name`"
:dense="dense"
outlined
@ -156,7 +150,6 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
v-model="employeeOther.motherFirstName"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-mother-last-name`"
:dense="dense"
outlined
@ -167,7 +160,6 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
v-model="employeeOther.motherLastName"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-mother-first-name-en`"
:dense="dense"
outlined
@ -178,7 +170,6 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
v-model="employeeOther.motherFirstNameEN"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-mother-last-name-en`"
:dense="dense"
outlined
@ -189,7 +180,6 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
v-model="employeeOther.motherLastNameEN"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-mother-birthplace`"
:dense="dense"
outlined

View file

@ -95,7 +95,7 @@ watch(
name="mdi-passport"
style="background-color: var(--surface-3)"
/>
{{ $t(`${title}`) }}
{{ title }}
</div>
<div class="col-12 row q-col-gutter-sm">
@ -111,7 +111,6 @@ watch(
input-debounce="0"
option-value="value"
option-label="label"
lazy-rules="ondemand"
v-model="passportType"
class="col-12"
:class="{ 'col-md-3': !ocr }"
@ -122,8 +121,11 @@ watch(
:for="`${prefixId}-select-passport-type`"
:label="$t('customerEmployee.form.passportType')"
:rules="[
(val: string) =>
!!val || $t('selectValidate') + $t('formDialogInputPassportType'),
(val) =>
(val && val.length > 0) ||
$t('form.error.selectField', {
field: $t('customerEmployee.form.passportType'),
}),
]"
@filter="passportTypeFilter"
>
@ -136,7 +138,6 @@ watch(
</template>
</q-select>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-passport-no`"
:dense="dense"
outlined
@ -145,13 +146,9 @@ watch(
:class="{ 'col-12': ocr, 'col-6': !ocr, 'col-md-3': !ocr }"
:label="$t('customerEmployee.form.passportNo')"
v-model="passportNumber"
:rules="[
(val: string) =>
!!val || $t('inputValidate') + $t('formDialogInputPassportNo'),
]"
:rules="[(val) => (val && val.length > 0) || $t('form.error.required')]"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-passport-ref`"
:dense="dense"
outlined
@ -169,7 +166,6 @@ watch(
"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-passport-place`"
:dense="dense"
outlined
@ -178,10 +174,7 @@ watch(
:class="{ 'col-12': ocr, 'col-6': !ocr, 'col-md-3': !ocr }"
:label="$t('customerEmployee.form.passportPlace')"
v-model="passportIssuingPlace"
:rules="[
(val: string) =>
!!val || $t('inputValidate') + $t('formDialogInputWPassportPlace'),
]"
:rules="[(val) => (val && val.length > 0) || $t('form.error.required')]"
/>
<q-select
outlined
@ -195,7 +188,6 @@ watch(
input-debounce="0"
option-value="value"
option-label="label"
lazy-rules="ondemand"
:class="{ 'col-12': ocr, 'col-6': !ocr, 'col-md-3': !ocr }"
v-model="passportIssuingCountry"
:dense="dense"
@ -205,9 +197,11 @@ watch(
:for="`${prefixId}-select-passport-country`"
:label="$t('customerEmployee.form.passportIssuer')"
:rules="[
(val: string) =>
!!val ||
$t('selectValidate') + $t('formDialogInputPassportCountry'),
(val) =>
(val && val.length > 0) ||
$t('form.error.selectField', {
field: $t('customerEmployee.form.passportIssuer'),
}),
]"
@filter="passportIssuingCountryFilter"
>
@ -227,9 +221,11 @@ watch(
:class="{ 'col-md-3': !ocr }"
:readonly="readonly"
:rules="[
(val: string) =>
!!val ||
$t('selectValidate') + $t('formDialogInputPassportIssuance'),
(val) =>
(val && val.length > 0) ||
$t('form.error.selectField', {
field: $t('customerEmployee.form.passportIssueDate'),
}),
]"
/>
<DatePicker
@ -240,8 +236,11 @@ watch(
:class="{ 'col-md-3': !ocr }"
:readonly="readonly"
:rules="[
(val: string) =>
!!val || $t('selectValidate') + $t('formDialogInputPassportExpire'),
(val) =>
(val && val.length > 0) ||
$t('form.error.selectField', {
field: $t('customerEmployee.form.passportExpireDate'),
}),
]"
/>
</div>

View file

@ -112,7 +112,6 @@ watch(
input-debounce="0"
option-value="value"
option-label="label"
lazy-rules="ondemand"
:class="{ 'col-4': !ocr, 'col-6': ocr }"
:dense="dense"
:readonly="readonly"
@ -140,7 +139,6 @@ watch(
!!val || $t('selectValidate') + $t('formDialogInputVisaType'),
]" -->
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-visa-no`"
:dense="dense"
outlined
@ -175,7 +173,6 @@ watch(
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-visa-place`"
:dense="dense"
outlined
@ -202,7 +199,6 @@ watch(
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-tm6`"
:dense="dense"
outlined

View file

@ -1,9 +1,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { dateFormat, parseAndFormatDate } from 'src/utils/datetime';
import { EmployeeWorkCreate } from 'stores/employee/types';
import { useI18n } from 'vue-i18n';
import { checkTabBeforeAdd, selectFilterOptionRefMod } from 'stores/utils';
import { selectFilterOptionRefMod } from 'stores/utils';
import DatePicker from '../shared/DatePicker.vue';
import {
@ -13,7 +11,6 @@ import {
SaveButton,
UndoButton,
} from 'components/button';
const { locale } = useI18n();
const currentIndex = defineModel<number>('currentIndex');
const employeeWork = defineModel<EmployeeWorkCreate[]>('employeeWork');
@ -38,6 +35,7 @@ defineProps<{
outlined?: boolean;
readonly?: boolean;
prefixId: string;
hideAction?: boolean;
}>();
defineEmits<{
@ -62,7 +60,10 @@ function addData() {
ownerName: '',
remark: '',
});
if (employeeWork.value) tab.value = `tab${employeeWork.value.length - 1}`;
if (employeeWork.value) {
tab.value = `tab${employeeWork.value.length - 1}`;
currentIndex.value = employeeWork.value.length - 1;
}
}
}
@ -116,7 +117,7 @@ const workplaceFilter = selectFilterOptionRefMod(
/>
{{ $t(`customerEmployee.formWorkHistory.title`) }}
<AddButton
v-if="!readonly"
v-if="currentIndex === -1 && !hideAction"
id="btn-add-bank"
icon-only
class="q-ml-sm"
@ -142,7 +143,7 @@ const workplaceFilter = selectFilterOptionRefMod(
<span class="col-12 flex justify-between items-center">
{{ $t('general.times', { number: index + 1 }) }}
<div class="row items-center">
<div class="row items-center" v-if="!hideAction">
<UndoButton
v-if="!readonly && !!work.id && !work.statusSave"
id="btn-info-health-undo"
@ -189,7 +190,6 @@ const workplaceFilter = selectFilterOptionRefMod(
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
v-model="work.jobType"
:dense="dense"
:readonly="readonly || work.statusSave"
@ -218,7 +218,6 @@ const workplaceFilter = selectFilterOptionRefMod(
hide-bottom-space
class="col-6"
input-debounce="0"
lazy-rules="ondemand"
option-label="label"
option-value="value"
v-model="work.workplace"
@ -239,7 +238,6 @@ const workplaceFilter = selectFilterOptionRefMod(
</template>
</q-select>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-work-end-date`"
:label="$t('general.remark')"
:dense="dense"
@ -260,7 +258,6 @@ const workplaceFilter = selectFilterOptionRefMod(
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-work-permit-no`"
:dense="dense"
outlined
@ -288,7 +285,6 @@ const workplaceFilter = selectFilterOptionRefMod(
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-owner-name`"
:dense="dense"
outlined
@ -311,7 +307,6 @@ const workplaceFilter = selectFilterOptionRefMod(
input-debounce="0"
option-value="value"
option-label="label"
lazy-rules="ondemand"
v-model="work.positionName"
:dense="dense"
:readonly="readonly || work.statusSave"

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import { calculateAge, dateFormat } from 'src/utils/datetime';
import { baseUrl } from 'src/stores/utils';
import PersonCard from 'components/shared/PersonCard.vue';
import KebabAction from '../shared/KebabAction.vue';
@ -121,7 +122,7 @@ defineEmits<{
<q-avatar size="md">
<q-img
:src="
props.row.profileImageUrl ||
`${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}` ||
'/images/employee-avatar.png'
"
class="text-center"
@ -248,7 +249,10 @@ defineEmits<{
$i18n.locale === 'eng'
? `${props.row.firstNameEN} ${props.row.lastNameEN} `.trim()
: `${props.row.firstName} ${props.row.lastName} `.trim(),
img: props.row.profileImageUrl || '/images/employee-avatar.png',
img:
`${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}` ||
'/images/employee-avatar.png',
fallbackImg: '/images/employee-avatar.png',
male: props.row.gender === 'male',
female: props.row.gender === 'female',
detail: [

View file

@ -124,7 +124,6 @@ onMounted(() => {
<div class="col-12 row" style="gap: var(--size-2)">
<q-select
lazy-rules="ondemand"
:id="`${prefixId}-select-employer-branch`"
:for="`${prefixId}-select-employer-branch`"
:use-input="!customerBranch"
@ -156,7 +155,7 @@ onMounted(() => {
(val: string) =>
!!val ||
$t('form.error.selectField', {
field: $t('customer.form.employerBranch'),
field: $t('customerEmployee.branch'),
}),
]"
>
@ -274,7 +273,7 @@ onMounted(() => {
'-' + ' ' + scope.opt.customer.lastName
}}
{{ $t('address') }}
{{ $t('general.address') }}
{{
$i18n.locale === 'eng'
? `${scope.opt.addressEN || ''} ${scope.opt.subDistrict.nameEN || ''} ${scope.opt.district.nameEN || ''} ${scope.opt.province.nameEN || ''}`
@ -297,7 +296,6 @@ onMounted(() => {
</q-select>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-code`"
:dense="dense"
outlined
@ -310,7 +308,6 @@ onMounted(() => {
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-nrc-no`"
:dense="dense"
outlined

View file

@ -1,23 +1,21 @@
<script setup lang="ts">
import { QSelect } from 'quasar';
import { getRole } from 'src/services/keycloak';
import useOptionStore from 'src/stores/options';
import { selectFilterOptionRefMod } from 'stores/utils';
import { ref, onMounted } from 'vue';
import { ref, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { locale } = useI18n({ useScope: 'global' });
const optionStore = useOptionStore();
const remark = defineModel<string>('remark', { default: '' });
const detail = defineModel<string>('detail', { default: '' });
const process = defineModel<number>('process');
const name = defineModel<string>('name');
const code = defineModel<string>('code');
const expenseType = defineModel<string>('expenseType');
const registeredBranchId = defineModel<string>('registeredBranchId');
const codeOption = ref<{ id: string; name: string }[]>([]);
const optionsBranch = defineModel<{ id: string; name: string }[]>(
'optionsBranch',
{ default: [] },
);
defineProps<{
dense?: boolean;
@ -42,11 +40,31 @@ onMounted(async () => {
const codeOptions = ref<Record<string, unknown>[]>([]);
const codeFilter = selectFilterOptionRefMod(codeOption, codeOptions, 'label');
const branchOptions = ref<Record<string, unknown>[]>([]);
const branchFilter = selectFilterOptionRefMod(
optionsBranch,
branchOptions,
'name',
const expenseTypeOptions = ref<Record<string, unknown>[]>([]);
let expenseTypeFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
onMounted(() => {
if (optionStore.globalOption) {
expenseTypeFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.expenseType),
expenseTypeOptions,
'label',
);
}
});
watch(
() => optionStore.globalOption,
() => {
expenseTypeFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.expenseType),
expenseTypeOptions,
'label',
);
},
);
</script>
@ -67,7 +85,6 @@ const branchFilter = selectFilterOptionRefMod(
<div class="col-12 row q-col-gutter-sm">
<q-select
outlined
clearable
use-input
fill-input
emit-value
@ -76,12 +93,12 @@ const branchFilter = selectFilterOptionRefMod(
hide-bottom-space
input-debounce="0"
:disable="!readonly && disableCode"
class="col-md-3 col-6"
class="col-md-4 col-12"
v-model="code"
id="select-br-id"
for="select-br-id"
option-label="label"
option-value="value"
lazy-rules="ondemand"
:dense="dense"
:readonly="readonly"
:options="codeOptions"
@ -98,6 +115,42 @@ const branchFilter = selectFilterOptionRefMod(
</q-item>
</template>
</q-select>
<q-input
for="input-name"
id="input-name"
:dense="dense"
outlined
:readonly="readonly"
:borderless="readonly"
hide-bottom-space
class="col-md-8 col-12"
:label="$t('productService.product.name')"
v-model="name"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input
for="input-process"
id="input-process"
:dense="dense"
outlined
:readonly="readonly"
:borderless="readonly"
hide-bottom-space
class="col-md-3 col-6"
:label="$t('productService.product.processingTime')"
v-model="process"
type="number"
>
<template #prepend>
<q-icon
color="primary"
name="mdi-clock-outline"
size="xs"
class="q-mr-xs"
/>
</template>
</q-input>
<q-select
outlined
clearable
@ -109,32 +162,18 @@ const branchFilter = selectFilterOptionRefMod(
hide-bottom-space
input-debounce="0"
class="col-md-3 col-6"
option-value="id"
option-label="name"
v-model="expenseType"
id="select-br-id"
for="select-br-id"
option-label="label"
option-value="value"
lazy-rules="ondemand"
id="input-source-nationality"
v-model="registeredBranchId"
:dense="dense"
:readonly="readonly"
:options="branchOptions"
:options="expenseTypeOptions"
:label="$t('productService.product.expenseType')"
:hide-dropdown-icon="readonly"
:label="$t('productService.product.registeredBranch')"
:rules="[
(val) => {
const roles = getRole() || [];
const isSpecialRole = ['admin', 'system', 'head_of_admin'].some(
(role) => roles.includes(role),
);
return (
isSpecialRole ||
!!val ||
$t('form.error.selectField', {
field: $t('productService.product.registeredBranch'),
})
);
},
]"
@filter="branchFilter"
@filter="expenseTypeFilter"
>
<template v-slot:no-option>
<q-item>
@ -145,50 +184,12 @@ const branchFilter = selectFilterOptionRefMod(
</template>
</q-select>
<q-input
lazy-rules="ondemand"
for="input-name"
:dense="dense"
outlined
:readonly="readonly"
:borderless="readonly"
hide-bottom-space
class="col-6"
:label="$t('productService.product.name')"
v-model="name"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input
lazy-rules="ondemand"
for="input-process"
:dense="dense"
outlined
:readonly="readonly"
:borderless="readonly"
hide-bottom-space
class="col-md-3 col-6"
:label="$t('productService.product.processingTime')"
v-model="process"
type="number"
/>
<!-- <q-input
lazy-rules="ondemand"
for="input-detail"
:dense="dense"
outlined
:readonly="readonly"
:borderless="readonly"
hide-bottom-space
type="textarea"
class="col-12"
:label="$t('serviceDetail')"
v-model="detail"
/> -->
<div class="col-12">
<q-field
class="full-width"
outlined
for="input-detail"
id="input-detail"
:readonly="readonly"
:borderless="readonly"
:label="$t('general.detail')"
@ -219,8 +220,8 @@ const branchFilter = selectFilterOptionRefMod(
</q-field>
</div>
<q-input
lazy-rules="ondemand"
for="input-remark"
id="input-remark"
:dense="dense"
outlined
:readonly="readonly"

View file

@ -2,6 +2,7 @@
import { QSelect } from 'quasar';
import { getRole } from 'src/services/keycloak';
import { selectFilterOptionRefMod } from 'stores/utils';
import { watch } from 'vue';
import { ref } from 'vue';
const remark = defineModel<string>('remark');
@ -12,8 +13,7 @@ const code = defineModel<string>('code');
const serviceCode = defineModel<string>('serviceCode');
const serviceName = defineModel<string>('serviceNameTh');
const serviceDescription = defineModel<string>('serviceDescription');
const registeredBranchId = defineModel<string | null>('registeredBranchId');
const registeredBranchId = defineModel<string>('registeredBranchId');
const optionsBranch = defineModel<{ id: string; name: string }[]>(
'optionsBranch',
@ -31,18 +31,21 @@ defineProps<{
}>();
const branchOptions = ref<Record<string, unknown>[]>([]);
const branchFilter = selectFilterOptionRefMod(
let branchFilter = selectFilterOptionRefMod(
optionsBranch,
branchOptions,
'name',
);
const serviceCodeOpts = ref([{ label: 'mou', value: 'mou' }]);
const serviceCodeOptions = ref<Record<string, unknown>[]>([]);
const serviceCodeFilter = selectFilterOptionRefMod(
serviceCodeOpts,
serviceCodeOptions,
'label',
watch(
() => optionsBranch.value,
() => {
branchFilter = selectFilterOptionRefMod(
optionsBranch,
branchOptions,
'name',
);
},
);
</script>
@ -60,122 +63,17 @@ const serviceCodeFilter = selectFilterOptionRefMod(
{{ $t(`form.field.basicInformation`) }}
</div>
<div v-if="!service" class="col-12 row q-col-gutter-sm">
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
readonly
:disable="!readonly"
hide-bottom-space
class="col-6"
:label="
$t(isType ? 'productService.type.code' : 'productService.group.code')
"
v-model="code"
/>
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-6"
:label="
$t(isType ? 'productService.type.name' : 'productService.group.name')
"
v-model="name"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
type="textarea"
class="col-12"
:label="$t('general.detail')"
:model-value="readonly ? detail || '-' : detail"
@update:model-value="(v) => (typeof v === 'string' ? (detail = v) : '')"
:for="`input-detail`"
/>
<q-input
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
type="textarea"
class="col-12"
:label="$t('general.remark')"
:model-value="readonly ? remark || '-' : remark"
@update:model-value="(v) => (typeof v === 'string' ? (remark = v) : '')"
:for="`input-remark`"
/>
</div>
<div v-if="service" class="col-12 row q-col-gutter-sm">
<!-- <q-select
outlined
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
:disable="!readonly && disableCode"
class="col-3"
v-model="serviceCode"
id="select-br-id"
option-label="label"
option-value="value"
lazy-rules="ondemand"
:dense="dense"
:readonly="readonly"
:options="serviceCodeOptions"
:label="$t('serviceCode')"
:hide-dropdown-icon="readonly || disableCode"
:rules="[(val: string) => !!val]"
@filter="serviceCodeFilter"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select> -->
<q-input
lazy-rules="ondemand"
id="input-service-code"
for="input-service-code"
:disable="!readonly && disableCode"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-3 col-4"
:label="$t('productService.service.code')"
v-model="serviceCode"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-select
outlined
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
class="col-md-3 col-8"
class="col-md-12 col-12"
option-value="id"
option-label="name"
lazy-rules="ondemand"
v-model="registeredBranchId"
id="input-source-nationality"
for="input-source-nationality"
@ -209,22 +107,87 @@ const serviceCodeFilter = selectFilterOptionRefMod(
</q-item>
</template>
</q-select>
<q-input
for="input-code"
:dense="dense"
outlined
readonly
:disable="!readonly"
hide-bottom-space
class="col-6"
:label="
$t(isType ? 'productService.type.code' : 'productService.group.code')
"
v-model="code"
/>
<q-input
for="input-name"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-6"
:label="
$t(isType ? 'productService.type.name' : 'productService.group.name')
"
v-model="name"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input
for="input-detail"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
type="textarea"
class="col-12"
:label="$t('general.detail')"
:model-value="readonly ? detail || '-' : detail"
@update:model-value="(v) => (typeof v === 'string' ? (detail = v) : '')"
/>
<q-input
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
type="textarea"
class="col-12"
:label="$t('general.remark')"
:model-value="readonly ? remark || '-' : remark"
@update:model-value="(v) => (typeof v === 'string' ? (remark = v) : '')"
:for="`input-remark`"
/>
</div>
<div v-if="service" class="col-12 row q-col-gutter-sm">
<q-input
id="input-service-code"
for="input-service-code"
:disable="!readonly && disableCode"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-4 col-12"
:label="$t('productService.service.code')"
v-model="serviceCode"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input
lazy-rules="ondemand"
id="input-service-name"
for="input-service-name"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-6 col"
class="col-md-8 col-12"
:label="$t('productService.service.name')"
v-model="serviceName"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input
lazy-rules="ondemand"
id="input-service-description"
for="input-service-description"
:dense="dense"

View file

@ -24,8 +24,8 @@ defineEmits<{
style="background: hsla(var(--info-bg) / 0.1); min-height: 50px"
>
{{ $t(`productService.service.serviceProperties`) }}
<q-btn
id="btn-capitalize"
v-if="!readonly"
:disable="readonly"
dense

View file

@ -2,6 +2,7 @@
const serviceCharge = defineModel<number>('serviceCharge');
const agentPrice = defineModel<number>('agentPrice');
const price = defineModel<number>('price');
const vatIncluded = defineModel<boolean>('vatIncluded');
withDefaults(
defineProps<{
@ -38,11 +39,44 @@ withDefaults(
style="background-color: var(--surface-3)"
/>
{{ $t('productService.product.priceInformation') }}
<div
class="surface-3 q-px-sm q-py-xs row text-caption q-ml-md app-text-muted"
style="border-radius: var(--radius-3)"
>
<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 row q-col-gutter-sm">
<q-input
id="input-price"
for="input-price"
v-if="priceDisplay?.price"
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
@ -55,8 +89,9 @@ withDefaults(
/>
<q-input
id="input-agent-price"
for="input-agent-price"
v-if="priceDisplay?.agentPrice"
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
@ -69,8 +104,9 @@ withDefaults(
/>
<q-input
id="input-service-charge"
for="input-service-charge"
v-if="priceDisplay?.serviceCharge"
lazy-rules="ondemand"
:dense="dense"
outlined
:readonly="readonly"
@ -85,4 +121,14 @@ withDefaults(
</div>
</template>
<style scoped></style>
<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);
}
}
</style>

View file

@ -17,25 +17,25 @@ const formServiceProperties = defineModel<Attributes>('formServiceProperties');
const typeOption = ref([
{
label: t('text'),
label: 'Text',
value: 'string',
color: 'var(--pink-6-hsl)',
icon: 'mdi-alpha-t',
},
{
label: t('number'),
label: 'Number',
value: 'number',
color: 'var(--purple-11-hsl)',
icon: 'mdi-numeric',
},
{
label: t('date'),
label: 'Date',
value: 'date',
color: 'var(--green-9-hsl)',
icon: 'mdi-calendar-blank-outline',
},
{
label: t('selection'),
label: 'Selection',
value: 'array',
color: 'var(--indigo-7-hsl)',
icon: 'mdi-code-array',
@ -223,7 +223,8 @@ function confirmDelete(items: unknown[], index: number) {
<div class="full-width full-height column no-wrap">
<div class="row">
<q-btn-dropdown
icon="mdi-plus"
id="btn-dropdow-properties"
for="btn-dropdow-properties"
dense
unelevated
color="primary"
@ -232,7 +233,12 @@ function confirmDelete(items: unknown[], index: number) {
menu-anchor="bottom end"
>
<q-list dense v-if="formServiceProperties && propertiesOption">
<q-item clickable @click="manageProperties('all')">
<q-item
for="list-all"
id="list-all"
clickable
@click="manageProperties('all')"
>
<div class="full-width flex items-center">
<q-icon
v-if="
@ -260,6 +266,8 @@ function confirmDelete(items: unknown[], index: number) {
clickable
:key="index"
@click="manageProperties(ops.value, ops.type)"
:for="`list-${ops.value}`"
:id="`list-${ops.value}`"
>
<div class="full-width flex items-center no-wrap">
<q-icon
@ -344,12 +352,12 @@ function confirmDelete(items: unknown[], index: number) {
<!-- field name -->
<q-select
lazy-rules="ondemand"
dense
outlined
emit-value
map-options
hide-bottom-space
for="input-properties-name"
class="col-md col-12 q-mr-md"
:class="{ 'q-my-sm': $q.screen.lt.md }"
:label="$t('productService.service.propertiesName')"
@ -374,12 +382,13 @@ function confirmDelete(items: unknown[], index: number) {
<!-- type -->
<div class="col-md col-12">
<q-select
lazy-rules="ondemand"
dense
outlined
emit-value
map-options
hide-bottom-space
for="input-properties-type"
id="input-properties-type"
:label="$t('general.type')"
option-value="value"
@update:model-value="
@ -428,6 +437,7 @@ function confirmDelete(items: unknown[], index: number) {
v-if="scope.opt"
v-bind="scope.itemProps"
class="row items-center col-12"
:id="`type-${scope.itemProps}`"
>
<q-avatar
size="sm"
@ -476,6 +486,7 @@ function confirmDelete(items: unknown[], index: number) {
<div class="row items-center">
<div class="col-7 surface-3 rounded q-mr-sm q-py-xs">
<q-checkbox
:for="`checkbox-is-phone-number-${p.fieldName}`"
v-if="p.type === 'string'"
v-model="p.isPhoneNumber"
size="xs"
@ -483,8 +494,8 @@ function confirmDelete(items: unknown[], index: number) {
{{ $t('general.telephone') }}
</div>
<q-input
lazy-rules="ondemand"
v-if="p.type === 'string'"
:for="`input-max-length-${p.fieldName}`"
v-model="p.phoneNumberLength"
input-class="text-caption"
class="col additional-label"
@ -498,7 +509,12 @@ function confirmDelete(items: unknown[], index: number) {
<div v-if="p.type === 'number'" class="q-gutter-y-sm">
<div class="row items-center">
<div class="col-md-4 col-12 surface-3 rounded">
<q-checkbox v-model="p.comma" size="xs" class="q-py-xs" />
<q-checkbox
v-model="p.comma"
size="xs"
class="q-py-xs"
:for="`checkbox-is-comma-${p.fieldName}`"
/>
{{ $t('form.useComma') }}
</div>
<div
@ -512,11 +528,12 @@ function confirmDelete(items: unknown[], index: number) {
v-model="p.decimal"
size="xs"
class="q-py-xs"
:for="`checkbox-is-decimal-${p.fieldName}`"
/>
{{ $t('form.decimal') }}
</div>
<q-input
lazy-rules="ondemand"
:for="`input-decimal-place-${p.fieldName}`"
v-model="p.decimalPlace"
class="col additional-label"
:class="{ 'q-mt-xs': $q.screen.lt.md }"
@ -536,8 +553,8 @@ function confirmDelete(items: unknown[], index: number) {
>
<div class="col rounded">
<q-input
lazy-rules="ondemand"
v-model="p.options[i]"
:for="`input-selection-${p.fieldName}-${i}`"
class="col additional-label"
dense
outlined
@ -549,6 +566,8 @@ function confirmDelete(items: unknown[], index: number) {
</div>
<div class="col-1 q-pl-sm">
<q-btn
:id="`btn-delete-selection-${p.fieldName}-${i}`"
:for="`btn-delete-selection-${p.fieldName}-${i}`"
@click="
() => {
p.options.splice(i, 1);
@ -565,6 +584,8 @@ function confirmDelete(items: unknown[], index: number) {
</div>
<div class="row">
<q-btn
:for="`btn-add-selection-${p.fieldName}`"
:id="`btn-add-selection-${p.fieldName}`"
@click="
() => {
p.options.push('');

View file

@ -61,6 +61,8 @@ withDefaults(
style="left: 0px; bottom: 0px"
>
<KebabAction
:id-name="title"
:status="data?.status"
class="absolute-top-right"
@view="$emit('menuViewDetail')"
@edit="$emit('menuEdit')"
@ -144,7 +146,7 @@ withDefaults(
style="background-color: transparent"
loading="lazy"
:src="
`${baseUrl}/${data?.type === 'service' ? 'service' : 'product'}/${data?.id}/image`.concat(
`${baseUrl}/${data?.type}/${data?.id}/image/${data?.selectedImage}`.concat(
noTimeImg ? '' : `?ts=${Date.now()}`,
)
"

View file

@ -6,7 +6,7 @@ import useProductServiceStore from 'stores/product-service';
import useOptionStore from 'stores/options';
import { Attributes, ProductList } from 'stores/product-service/types';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { ref, watch } from 'vue';
const baseUrl = ref<string>(import.meta.env.VITE_API_BASE_URL);
const productServiceStore = useProductServiceStore();
@ -58,10 +58,26 @@ defineEmits<{
(e: 'manageWorkName'): void;
(e: 'workProperties'): void;
}>();
watch(
() => workNameItems.value,
(c, o) => {
const list = c.map((v: { name: string }) => v.name);
const oldList = o.map((v: { name: string }) => v.name);
const index = oldList.indexOf(workName.value);
if (list[index] !== oldList[index] && !list.includes(workName.value)) {
if (list.length - 1 === index - 1) workName.value = list[index - 1];
else workName.value = list[index];
}
},
);
</script>
<template>
<div class="bordered rounded">
<q-expansion-item
for="item-up"
id="item-up"
dense
switch-toggle-side
default-opened
@ -75,6 +91,7 @@ defineEmits<{
<q-btn
v-if="!readonly"
id="btn-work-up-product"
for="btn-work-up-product"
icon="mdi-arrow-up"
dense
flat
@ -86,6 +103,7 @@ defineEmits<{
<q-btn
v-if="!readonly"
id="btn-work-down-product"
for="btn-work-down-product"
icon="mdi-arrow-down"
dense
flat
@ -96,7 +114,7 @@ defineEmits<{
@click.stop="$emit('moveWorkDown')"
/>
<div
for="select-work-name"
:for="`select-work-name-${index + 1}`"
class="col q-py-sm q-px-md"
style="background-color: var(--surface-1); z-index: 2"
@click="() => (readonly ? '' : fetchListOfWork())"
@ -109,7 +127,6 @@ defineEmits<{
}}
</span>
</span>
<q-menu v-if="!readonly" fit anchor="bottom left" self="top left">
<q-item>
<div class="full-width flex items-center justify-between">
@ -249,6 +266,7 @@ defineEmits<{
<q-btn
v-if="!readonly"
id="btn-add-work-product"
for="btn-add-work-product"
flat
dense
icon="mdi-plus"
@ -292,6 +310,7 @@ defineEmits<{
<q-btn
v-if="!readonly && $q.screen.gt.xs"
id="btn-product-down"
for="btn-product-down"
icon="mdi-arrow-down"
dense
flat
@ -420,6 +439,7 @@ defineEmits<{
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

View file

@ -0,0 +1,142 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import useBranchStore from 'src/stores/branch';
import useCustomerStore from 'src/stores/customer';
import SelectInput from '../shared/SelectInput.vue';
import { QSelect } from 'quasar';
const { locale } = useI18n({ useScope: 'global' });
const branchStore = useBranchStore();
const customerStore = useCustomerStore();
const branch = defineModel<string>('branch');
const customer = defineModel<string>('customer');
const agentPrice = defineModel<boolean>('agentPrice');
const branchOption = ref();
const customerOption = ref();
defineProps<{
outlined?: boolean;
readonly?: boolean;
separator?: boolean;
employee?: boolean;
title?: string;
prefixId: string;
}>();
async function filter(
val: string,
update: (...args: unknown[]) => void,
type: 'branch' | 'customer',
) {
update(
async () => {
const res =
type === 'branch'
? await branchStore.fetchList({
query: val,
pageSize: 30,
})
: await customerStore.fetchList({
query: val,
pageSize: 30,
});
if (res) {
if (type === 'branch') {
branchOption.value = res.result.map((v) => ({
value: v.id,
label: v.name,
labelEN: v.nameEN,
}));
} else if (type === 'customer') {
customerOption.value = res.result.map((v) => ({
value: v.id,
label:
v.customerType === 'CORP'
? v.branch[0].registerName
: `${v.branch[0].firstName} ${v.branch[0].lastName}`,
labelEN:
v.customerType === 'CORP'
? v.branch[0].registerNameEN
: `${v.branch[0].firstNameEN} ${v.branch[0].lastNameEN}`,
}));
}
}
},
(ref: QSelect) => {
if (val !== '' && ref.options && ref.options?.length > 0) {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
},
);
}
</script>
<template>
<div class="row col-12">
<div class="col-12 row items-center q-pb-sm text-weight-bold text-body1">
<q-icon
flat
size="xs"
class="q-pa-sm rounded q-mr-sm"
color="info"
name="mdi-file-outline"
style="background-color: var(--surface-3)"
/>
{{ $t(`general.about`) }}
<div class="q-ml-md text-weight-regular">
<q-checkbox
:label="$t('productService.product.agentPrice')"
size="xs"
v-model="agentPrice"
style="font-size: 14px"
/>
</div>
</div>
<div class="col-12 row q-col-gutter-sm">
<SelectInput
:readonly
incremental
v-model="branch"
id="quotation-branch"
class="col-md col-12"
:option="branchOption"
:label="$t('quotation.branch')"
:option-label="locale === 'eng' ? 'labelEN' : 'label'"
:rules="[(val: string) => !!val || $t('form.error.required')]"
@filter="(val, update) => filter(val, update, 'branch')"
/>
<SelectInput
:readonly
incremental
v-model="customer"
class="col-md col-12"
id="quotation-customer"
:option="customerOption"
:label="$t('quotation.customer')"
:option-label="locale === 'eng' ? 'labelEN' : 'label'"
:rules="[(val: string) => !!val || $t('form.error.required')]"
@filter="(val, update) => filter(val, update, 'customer')"
>
<template #option="{ scope }">
<q-item clickable v-if="scope.index === 0">
<q-item-section>
{{ $t('general.add', { text: $t('quotation.newCustomer') }) }}
</q-item-section>
</q-item>
<q-separator v-if="scope.index === 0" />
<q-item clickable v-bind="scope.itemProps">
<q-item-section>
{{ locale === 'eng' ? scope.opt.labelEN : scope.opt.label }}
</q-item-section>
</q-item>
</template>
</SelectInput>
</div>
</div>
</template>

View file

@ -11,7 +11,7 @@ const dialogState = defineModel('state', { default: true });
</script>
<template>
<q-dialog v-model="dialogState">
<q-dialog v-model="dialogState" full-width full-height>
<AppBox
style="
padding: 0;
@ -19,10 +19,6 @@ const dialogState = defineModel('state', { default: true });
max-height: 100%;
flex-wrap: nowrap;
"
:style="{
width: width ?? '100%',
maxWidth: maxWidth ?? '98%',
}"
class="column"
>
<div class="row items-center q-py-sm q-px-md bordered-b">
@ -43,9 +39,11 @@ const dialogState = defineModel('state', { default: true });
class="dialog-body"
style="
flex: 1;
max-height: calc(100vh - 200px);
overflow-y: auto;
position: relative;
display: flex;
flex-direction: column;
height: 100%;
"
>
<slot />

View file

@ -0,0 +1,155 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
import { formatNumberDecimal } from 'src/stores/utils';
import KebabAction from '../shared/KebabAction.vue';
defineProps<{
type?:
| 'fullAmountCash'
| 'installmentsCash'
| 'fullAmountBill'
| 'installmentsBill'
| string;
title?: string;
code?: string;
amount?: number;
date?: string;
customerName?: string;
reporter?: string;
totalPrice?: number;
}>();
defineEmits<{
(e: 'view'): void;
(e: 'edit'): void;
(e: 'link'): void;
(e: 'upload'): void;
(e: 'delete'): void;
(e: 'changeStatus'): void;
(e: 'example'): void;
}>();
</script>
<template>
<div class="surface-1 rounded bordered q-pa-sm quo-card">
<!-- SEC: header -->
<header class="row items-center no-wrap">
<div
class="badge-card rounded q-pa-xs"
:class="{ [`badge-card__${type}`]: true }"
>
{{ $t(`quotation.type.${type}`) }}
</div>
<div class="column q-ml-md relative-position" style="font-size: 12px">
<span>
{{ $t('general.itemNo', { msg: $t('quotation.title') }) }}
</span>
<span class="text-caption app-text-muted" style="top: 10px">
{{ code }}
</span>
</div>
<nav class="col text-right">
<q-btn
flat
dense
rounded
icon="mdi-eye-outline"
size="12px"
@click.stop="$emit('view')"
/>
<KebabAction
:idName="code"
status="ACTIVE"
use-link
use-upload
@view="$emit('view')"
@edit="$emit('edit')"
@link="$emit('link')"
@upload="$emit('upload')"
@delete="$emit('delete')"
@change-status="$emit('changeStatus')"
/>
</nav>
</header>
<!-- SEC: body -->
<section class="row no-wrap q-py-md">
<q-img src="/images/quotation-avatar.png" width="4rem" class="q-mr-lg" />
<div class="column">
<span class="col q-pt-sm">{{ title || '-' }}</span>
<span class="app-text-muted">x {{ amount || '0' }}</span>
</div>
<div
class="col text-right app-text-muted q-mr-md self-end"
style="font-size: 12px"
>
{{ date || '-' }}
</div>
</section>
<q-separator />
<section class="row q-py-sm">
<div class="col-3 app-text-muted">{{ $t('quotation.customerName') }}</div>
<div class="col-9">{{ customerName || '-' }}</div>
<div class="col-3 app-text-muted">{{ $t('quotation.actor') }}</div>
<div class="col-9">{{ reporter || '-' }}</div>
</section>
<q-separator />
<footer class="row no-wrap items-center q-mt-sm">
<Icon
class="q-mr-md"
icon="ph:money-fill"
style="font-size: 24px; color: var(--green-9)"
/>
{{ $t('quotation.totalPrice') }} :
<div class="q-pl-xs" style="color: var(--orange-5)">
{{ formatNumberDecimal(totalPrice || 0, 2) }}
</div>
<q-btn
dense
outline
color="primary"
class="rounded q-ml-auto"
padding="2px 8px"
>
<q-icon name="mdi-play-box-outline" size="xs" class="q-mr-xs" />
{{ $t('general.view', { msg: $t('general.example') }) }}
</q-btn>
</footer>
</div>
</template>
<style scoped>
.badge-card {
font-size: 12px;
color: hsla(var(--gray-0-hsl) / 1);
background: hsla(var(--_color) / 1);
}
.badge-card__fullAmountCash {
--_color: var(--red-6-hsl);
}
.badge-card__installmentsCash {
--_color: var(--blue-6-hsl);
}
.badge-card__fullAmountBill {
--_color: var(--jungle-8-hsl);
}
.badge-card__installmentsBill {
--_color: var(--purple-7-hsl);
}
.dark .badge-card__installmentsCash {
--_color: var(--blue-10-hsl);
}
span {
display: inline-block;
/*new:*/
line-height: 12px;
height: 12px;
}
</style>

View file

@ -19,13 +19,14 @@ defineProps<{
height?: string;
employee?: boolean;
edit?: boolean;
hideDelete?: boolean;
saveAmount?: number;
submitLabel?: string;
submitIcon?: string;
isEdit?: boolean;
tabsList?: { name: string; label: string }[];
hideCloseEvent?: boolean;
editData?: (...args: unknown[]) => void;
deleteData?: (...args: unknown[]) => void;
show?: (...args: unknown[]) => void;
@ -43,7 +44,7 @@ const currentTab = defineModel<string>('currentTab');
:model-value="modal"
@update:model-value="(v) => (modal = beforeClose ? beforeClose() : v)"
@before-show="show"
@hide="close"
@hide="hideCloseEvent !== undefined && hideCloseEvent ? '' : close"
>
<div
class="surface-1"
@ -86,7 +87,7 @@ const currentTab = defineModel<string>('currentTab');
@click="editData"
/>
<q-btn
v-if="edit"
v-if="edit && !hideDelete"
round
flat
id="deleteDialog"
@ -217,6 +218,7 @@ const currentTab = defineModel<string>('currentTab');
id="btn-form-submit"
type="submit"
solid
:icon="submitIcon"
:label="submitLabel"
:amount="saveAmount"
/>

View file

@ -17,6 +17,7 @@ withDefaults(
statusBranch?: string;
badgeLabel?: string;
badgeClass?: string;
badgeStyle?: string;
bgColor?: string;
hideAction?: boolean;
editData?: (...args: unknown[]) => void;
@ -25,6 +26,7 @@ withDefaults(
close?: (...args: unknown[]) => void;
undo?: (...args: unknown[]) => void;
beforeClose?: (...args: unknown[]) => boolean;
show?: (...args: unknown[]) => void;
}>(),
{
hideAction: false,
@ -48,6 +50,7 @@ function reset() {
<template>
<q-drawer
no-swipe-open
@show="show"
@before-hide="reset"
@hide="close"
@update:model-value="(v) => (drawerOpen = beforeClose ? beforeClose() : v)"
@ -90,29 +93,32 @@ function reset() {
v-if="badgeLabel"
class="badge-label badge text-caption q-px-sm q-mr-sm"
:class="badgeClass"
:style="badgeStyle"
>
{{ badgeLabel }}
</text>
<text v-if="category" class="app-text-muted q-mr-sm">
{{ category }}
</text>
<span>
{{ title }}
</span>
<slot name="badgeList">
<span>
{{ title }}
</span>
<text
v-if="!!statusBranch"
class="branch-badge branch-card__badge q-ml-sm"
:class="{
'branch-card__inactive ': statusBranch === 'INACTIVE',
}"
>
{{
statusBranch === 'INACTIVE'
? $t('status.INACTIVE')
: $t('status.ACTIVE')
}}
</text>
<text
v-if="!!statusBranch"
class="branch-badge branch-card__badge q-ml-sm"
:class="{
'branch-card__inactive ': statusBranch === 'INACTIVE',
}"
>
{{
statusBranch === 'INACTIVE'
? $t('status.INACTIVE')
: $t('status.ACTIVE')
}}
</text>
</slot>
</div>
<div v-if="!hideAction" class="q-mr-md" style="width: 38.8px"></div>
@ -141,7 +147,16 @@ function reset() {
<!-- footer -->
<div class="bordered-t q-pr-lg row items-center justify-end q-py-md">
<CancelButton id="btn-info-cancel" outlined @click="close" />
<CancelButton
id="btn-info-cancel"
outlined
@click="
() => {
drawerOpen = beforeClose ? beforeClose() : !drawerOpen;
close?.();
}
"
/>
<SaveButton
class="q-ml-md"
id="btn-info-save"

View file

@ -0,0 +1,136 @@
<script setup lang="ts">
import { ref } from 'vue';
withDefaults(
defineProps<{
img?: string | null;
icon?: string;
title?: string;
caption?: string;
color?: string;
bgColor?: string;
toggleTitle?: string;
fallbackImg?: string;
fallbackCover?: string;
hideFade?: boolean;
hideActive?: boolean;
active?: boolean;
readonly?: boolean;
useToggle?: boolean;
labelAction?: string;
menu?: { icon: string; color: string; bgColor: string }[];
tabsList?: { name: string | number; label: string }[];
}>(),
{},
);
const showOverlay = ref(false);
defineEmits<{
(e: 'view'): void;
(e: 'edit'): void;
}>();
</script>
<template>
<div>
<div class="surface-1" style="border: 4px solid var(--surface-1)">
<q-avatar
square
size="10rem"
font-size="8rem"
class="relative-position"
style="z-index: 1; cursor: pointer"
@mouseover="showOverlay = true"
@mouseleave="showOverlay = false"
@click.stop="$emit('view')"
>
<div
v-if="img"
class="full-width full-height"
:style="{
background: `${bgColor || 'var(--brand-1)'}`,
color: `${color || 'white'}`,
}"
>
<q-img id="profile-view" :src="img" :ratio="1">
<template #error>
<q-img
v-if="fallbackImg"
:src="fallbackImg"
:ratio="1"
style="background-color: transparent"
>
<template #error>
<div
class="full-width full-height flex items-center justify-center"
:style="{
background: `${bgColor || 'var(--brand-1)'}`,
color: `${color || 'white'}`,
}"
>
<q-icon :name="icon || 'mdi-account'" />
</div>
</template>
</q-img>
<div
v-else
class="full-width full-height flex items-center justify-center"
:style="{
background: `${bgColor || 'var(--brand-1)'}`,
color: `${color || 'white'}`,
}"
>
<q-icon :name="icon || 'mdi-account'" />
</div>
</template>
</q-img>
</div>
<div
v-else
class="full-width full-height flex items-center justify-center"
:style="{
background: `${bgColor || 'var(--brand-1)'}`,
color: `${color || 'white'}`,
}"
>
<q-icon :name="icon || 'mdi-account'" />
</div>
<Transition name="slide-fade">
<div
v-if="showOverlay && !readonly"
class="absolute text-caption full-width full-height"
style="overflow: hidden"
:class="{ dark: $q.dark.isActive }"
>
<div
class="upload-overlay absolute-bottom flex items-center justify-center"
@click.stop="$emit('edit')"
>
{{
labelAction === undefined
? $t('general.editImage')
: labelAction
}}
</div>
</div>
</Transition>
</q-avatar>
</div>
</div>
</template>
<style scoped>
.upload-overlay {
top: 60%;
background-color: hsla(var(--gray-10-hsl) / 0.5);
color: white;
&.dark {
background-color: rgba(255, 255, 255, 0.2);
}
}
</style>

View file

@ -1,17 +1,23 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import AppBox from './app/AppBox.vue';
import { CancelButton, ClearButton, SaveButton } from './button';
import { dialog } from 'src/stores/utils';
defineExpose({ browse });
defineProps<{
const props = defineProps<{
changeDisabled?: boolean;
clearButtonDisabled?: boolean;
clearButton?: boolean;
hiddenFooter?: boolean;
defaultUrl?: string;
onCreate?: boolean;
}>();
const emit = defineEmits<{
(e: 'save', file: File | null, url: string | null): void;
(e: 'addImage', file: File | null): void;
(e: 'removeImage', name: string): void;
(e: 'submit', name: string): void;
}>();
const imageUrl = defineModel<string>('imageUrl', {
@ -29,7 +35,22 @@ const dialogState = defineModel<boolean>('dialogState', {
const file = defineModel<File | null>('file', {
required: true,
});
const dataList = defineModel<{ selectedImage: string; list: string[] }>(
'dataList',
{
required: false,
default: { selectedImage: '', list: [] },
},
);
const onCreateData = defineModel<{
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}>('onCreateDataList', {
required: false,
default: { selectedImage: '', list: [] },
});
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const reader = new FileReader();
const inputFile = (() => {
const _element = document.createElement('input');
@ -39,8 +60,26 @@ const inputFile = (() => {
return _element;
})();
const selectedImg = ref('');
const currentImag = ref('');
const tempImage = ref<string | null>('');
reader.addEventListener('load', () => {
if (typeof reader.result === 'string') imageUrl.value = reader.result;
if (typeof reader.result === 'string') {
tempImage.value = reader.result;
if (props.onCreate) {
onCreateData.value.list.push({
url: reader.result,
imgFile: file.value,
name: Date.now().toString(),
});
onCreateData.value.selectedImage =
onCreateData.value.list[onCreateData.value.list.length - 1].name;
selectedImg.value = reader.result;
} else {
emit('addImage', file.value);
}
}
});
function browse() {
@ -55,12 +94,13 @@ function change(e: Event) {
file.value = _file;
reader.readAsDataURL(_file);
if (!dialogState.value) {
emit('save', _file, imageUrl.value);
emit('save', _file, tempImage.value);
}
}
}
async function downloadImage(url: string) {
async function downloadImage(url: string | null) {
if (!url) return;
const res = await fetch(url);
const blob = await res.blob();
@ -76,57 +116,234 @@ async function downloadImage(url: string) {
a.click();
a.remove();
}
function selectImg(name: string) {
if (props.onCreate) {
selectedImg.value = name;
tempImage.value = name;
onCreateData.value.selectedImage =
onCreateData.value.list.find((v) => v.url === name)?.name || '';
} else {
selectedImg.value = name.split('/').pop() || '';
tempImage.value = `${apiBaseUrl}/${name}`;
}
}
function closeCheckToDefault() {
let imgNameList: string[];
let inList: boolean;
if (props.onCreate) {
imgNameList = onCreateData.value.list.map((v) => v.url || '');
} else {
imgNameList = dataList.value.list.map((v) => v.split('/').pop() || '');
}
inList = imgNameList.includes(currentImag.value);
if (!inList && currentImag.value !== '') {
selectImg('');
emit('submit', selectedImg.value);
}
}
watch(
() => dialogState.value,
() => {
if (dialogState.value) {
if (props.onCreate) {
tempImage.value = imageUrl.value;
selectedImg.value = imageUrl.value;
currentImag.value = imageUrl.value;
} else {
tempImage.value = `${imageUrl.value}?ts=${Date.now()}`;
selectedImg.value = dataList.value.selectedImage;
currentImag.value = dataList.value.selectedImage;
}
} else {
tempImage.value = '';
selectedImg.value = '';
}
},
);
watch(
() => dataList.value.list,
(n, o) => {
if (n.length > o.length) {
selectImg(n[n.length - 1]);
}
},
{ deep: true },
);
</script>
<template>
<q-dialog v-model="dialogState">
<AppBox class="image-dialog-content">
<q-dialog v-model="dialogState" @before-hide="closeCheckToDefault">
<AppBox class="image-dialog-content column">
<!-- header -->
<div class="row items-center q-py-sm q-px-md bordered-b">
<div style="width: 38.61px" />
<div style="flex: 1"><slot name="title" /></div>
<div>
<q-btn
round
flat
icon="mdi-close"
padding="xs"
class="close-btn"
:class="{ dark: $q.dark.isActive }"
v-close-popup
<CancelButton icon-only v-close-popup @click="closeCheckToDefault" />
</div>
</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"
>
<img
:src="tempImage || fallbackUrl"
v-if="tempImage || fallbackUrl"
style="object-fit: contain; width: 100%"
@error="
() => {
tempImage = '';
}
"
/>
<div
style="object-fit: contain; height: 100%; width: 100%"
v-if="!tempImage && !fallbackUrl"
>
<slot name="error"></slot>
</div>
<div class="absolute-top-left q-pa-md">
<q-btn
class="upload-image-btn q-mr-md"
icon="mdi-camera-plus-outline"
id="btn-add-img"
size="md"
unelevated
round
v-if="!changeDisabled"
@click="
() => {
inputFile?.click();
}
"
style="color: hsla(var(--stone-0-hsl) / 0.7)"
></q-btn>
<q-btn
v-if="!onCreate"
class="upload-image-btn"
icon="mdi-download-outline"
id="btn-download-img"
size="md"
unelevated
round
@click="downloadImage(tempImage)"
style="color: hsla(var(--stone-0-hsl) / 0.7)"
></q-btn>
</div>
</div>
<div
v-if="!hiddenFooter"
class="col q-ml-md q-pt-sm q-my-md"
style="width: 40em; height: 30em; overflow: auto"
>
<div class="row items-center q-gutter-sm">
<div
class="rounded surface-1 relative-position"
:class="{
'selected-img': selectedImg === '',
}"
style="
object-fit: cover;
height: 5vw;
width: 5vw;
overflow: hidden;
"
@click="selectImg('')"
>
<slot name="error"></slot>
</div>
<template v-if="dataList">
<div
v-for="(img, n) in onCreate
? onCreateData.list.map((item) => item.url)
: dataList.list"
:key="n"
class="rounded surface-1 relative-position"
:class="{
'selected-img':
selectedImg && onCreate
? selectedImg === img
: selectedImg === img.split('/').pop(),
}"
style="height: 5vw; width: 5vw"
@click="selectImg(img)"
>
<q-btn
icon="mdi-close"
class="absolute-top-right"
rounded
padding="0"
size="sm"
style="
z-index: 2;
top: -5px;
right: -5px;
background: var(--surface-1);
color: hsl(var(--negative-bg));
"
@click.stop="
() => {
if (onCreate) {
const v = onCreateData.list.splice(n, 1);
if (v[0].url === selectedImg) {
if (onCreateData.list.length === 0 || n === 0) {
selectImg('');
} else {
selectImg(onCreateData.list[n - 1].url);
}
}
} else {
dialog({
color: 'negative',
icon: 'mdi-alert',
title: $t('dialog.title.confirmDelete'),
actionText: $t('general.delete'),
message: $t('dialog.message.confirmDelete'),
action: async () => {
$emit('removeImage', img);
const index = dataList.list.indexOf(img);
if (img.split('/').pop() === selectedImg) {
if (dataList.list.length === 0 || index === 0) {
selectImg('');
} else {
selectImg(dataList.list[index - 1]);
}
}
},
cancel: () => {},
});
}
}
"
></q-btn>
<div
class="rounded full-width full-height surface-1"
style="overflow: hidden"
>
<img
v-if="img"
:src="onCreate ? img : `${apiBaseUrl}/${img}`"
class=""
style="object-fit: cover; height: 100%; width: 100%"
/>
</div>
</div>
</template>
</div>
</div>
</div>
<div class="image-dialog-body">
<img
:src="imageUrl || fallbackUrl"
v-if="imageUrl || fallbackUrl"
class="image-container"
style="object-fit: contain"
@error="imageUrl = ''"
/>
<div class="image-container" v-if="!imageUrl && !fallbackUrl">
<slot name="error"></slot>
</div>
<div style="position: fixed; padding: var(--size-2)">
<q-btn
class="upload-image-btn q-mr-md"
icon="mdi-camera-outline"
size="md"
unelevated
round
v-if="!changeDisabled"
@click="inputFile?.click()"
style="color: hsla(var(--stone-0-hsl) / 0.7)"
></q-btn>
<q-btn
class="upload-image-btn"
icon="mdi-download-outline"
size="md"
unelevated
round
@click="downloadImage(imageUrl)"
style="color: hsla(var(--stone-0-hsl) / 0.7)"
></q-btn>
</div>
</div>
<!-- footer -->
<div
class="row items-center justify-end q-py-sm q-px-md bordered-t"
v-if="!hiddenFooter"
@ -147,20 +364,25 @@ async function downloadImage(url: string) {
"
v-if="clearButton"
/>
<CancelButton
<!-- <CancelButton
id="btn-cancel-img"
outlined
class="q-px-md q-mr-sm"
@click="
inputFile && (inputFile.value = ''),
(imageUrl = defaultUrl || fallbackUrl || ''),
(tempImage = defaultUrl || fallbackUrl || ''),
(file = null)
"
v-close-popup
/>
/> -->
<SaveButton
id="btn-save-img"
outlined
@click="$emit('save', inputFile?.files?.[0] || null, imageUrl)"
:disabled="currentImag === selectedImg"
:label="$t('general.apply')"
@click="$emit('submit', selectedImg)"
/>
<!-- @click="$emit('save', inputFile?.files?.[0] || null, tempImage)" -->
</div>
</AppBox>
</q-dialog>
@ -171,24 +393,24 @@ async function downloadImage(url: string) {
padding: 0;
border-radius: var(--radius-2);
flex-wrap: nowrap;
width: 100%;
max-width: 80%;
}
@media (min-width: 768px) {
.image-dialog-content {
max-width: 60%;
max-width: 70%;
}
}
.image-dialog-body {
overflow-y: auto;
background-color: var(--surface-2) !important;
/* overflow-y: auto;
background-color: var(--surface-1) !important;
position: relative;
display: flex;
flex-direction: column;
flex: 1;
height: calc(100vh - 200px);
flex: 1; */
overflow: hidden;
width: 30em;
height: 30em;
}
.upload-image-btn {
@ -227,4 +449,21 @@ async function downloadImage(url: string) {
border: 1px solid hsl(var(--negative-bg));
}
}
.selected-img {
position: relative;
border: 2px solid hsl(var(--info-bg));
}
.selected-img:before {
content: ' ';
position: absolute;
z-index: 1;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
border: 2px solid var(--surface-1);
border-radius: 5px;
}
</style>

View file

@ -1,44 +1,51 @@
<script setup lang="ts">
import AppBox from './app/AppBox.vue';
import { ref } from 'vue';
import { Icon } from '@iconify/vue';
defineProps<{
import { onMounted } from 'vue';
const props = defineProps<{
icon: string;
text: string;
color: string;
iconColor: string;
bgColor: string;
changeColor?: boolean;
index?: number;
}>();
const _self = ref<InstanceType<typeof HTMLDivElement>>();
onMounted(() => {
if (props.index === 0) {
_self.value?.focus();
}
});
</script>
<template>
<AppBox
style="
padding: 0;
border-radius: var(--radius-2);
width: 320px;
height: 195px;
"
bordered
class="cursor-pointer"
<button
ref="_self"
class="wrapper surface-1 shadow-2"
type="submit"
@click="$emit('trigger')"
style="padding: 0; border-radius: var(--radius-2); width: 320px"
>
<div class="column" style="height: 100%">
<div class="col-9 flex justify-center items-center">
<Icon
:icon="icon"
width="64px"
:class="`${$q.dark.isActive ? '' : 'app-text-muted'}`"
:color="changeColor ? color : ''"
/>
<div class="column justify-center" style="height: 100%">
<div class="col-6 flex justify-center items-center">
<div
class="q-pa-md row items-center"
:style="`border-radius: 50%; background-color: hsla(${bgColor} / 0.1)`"
>
<Icon :icon="icon" width="53px" :color="`var(${iconColor})`" />
</div>
</div>
<div
class="col-3 flex text-bold text-h6 text-center justify-center items-center variable-item-card"
class="col-2 flex text-bold text-h6 text-center justify-center items-center variable-item-card"
:class="{ dark: $q.dark.isActive }"
:style="`background-color: ${color}`"
:style="`background-color: ${bgColor}`"
>
<div style="color: white">{{ $t(text) }}</div>
<div style="font-size: 14px">{{ $t(text) }}</div>
</div>
</div>
</AppBox>
</button>
</template>
<style scoped>
@ -59,4 +66,15 @@ defineProps<{
--_var-filter: color;
}
}
.wrapper {
appearance: none;
background: transparent;
outline: none;
border: none;
}
.wrapper:focus {
border: 2px solid var(--blue-6);
border-radius: 10px;
}
</style>

View file

@ -72,7 +72,7 @@ const showOverlay = ref(false);
size="6rem"
font-size="3rem"
class="relative-position"
style="z-index: 1"
style="z-index: 1; box-shadow: var(--shadow-2)"
:style="{
color: `${color || 'white'}`,
cursor: `${noImageAction ? 'default' : 'pointer'}`,

View file

@ -0,0 +1,231 @@
<script lang="ts" setup>
import AppBox from './app/AppBox.vue';
import { CancelButton, ClearButton, SaveButton } from './button';
defineExpose({ browse });
defineProps<{
changeDisabled?: boolean;
clearButtonDisabled?: boolean;
clearButton?: boolean;
hiddenFooter?: boolean;
defaultUrl?: string;
}>();
const emit = defineEmits<{
(e: 'save', file: File | null, url: string | null): void;
(e: 'clear'): void;
}>();
const imageUrl = defineModel<string>('imageUrl', {
required: false,
default: '',
});
const fallbackUrl = defineModel<string>('fallbackUrl', {
required: false,
default: '',
});
const dialogState = defineModel<boolean>('dialogState', {
required: false,
default: true,
});
const file = defineModel<File | null>('file', {
required: true,
});
const reader = new FileReader();
const inputFile = (() => {
const _element = document.createElement('input');
_element.type = 'file';
_element.accept = 'image/*';
_element.addEventListener('change', change);
return _element;
})();
reader.addEventListener('load', () => {
if (typeof reader.result === 'string') {
imageUrl.value = reader.result;
if (!dialogState.value) emit('save', file.value, imageUrl.value);
}
});
function browse() {
inputFile?.click();
}
function change(e: Event) {
const _element = e.target as HTMLInputElement | null;
const _file = _element?.files?.[0];
if (_file) {
file.value = _file;
reader.readAsDataURL(_file);
}
}
async function downloadImage(url: string) {
const res = await fetch(url);
const blob = await res.blob();
let extension = '';
if (blob.type === 'image/jpeg') extension = '.jpg';
else if (blob.type === 'image/png') extension = '.png';
else return;
let a = document.createElement('a');
a.download = `download${extension}`;
a.href = window.URL.createObjectURL(blob);
a.click();
a.remove();
}
</script>
<template>
<q-dialog v-model="dialogState">
<AppBox class="image-dialog-content">
<div class="row items-center q-py-sm q-px-md bordered-b">
<div style="flex: 1"><slot name="title" /></div>
<div>
<q-btn
round
flat
icon="mdi-close"
padding="xs"
class="close-btn"
:class="{ dark: $q.dark.isActive }"
v-close-popup
/>
</div>
</div>
<div class="image-dialog-body">
<img
:src="imageUrl || fallbackUrl"
v-if="imageUrl || fallbackUrl"
class="image-container"
style="object-fit: contain"
@error="imageUrl = ''"
/>
<div class="image-container" v-if="!imageUrl && !fallbackUrl">
<slot name="error"></slot>
</div>
<div style="position: fixed; padding: var(--size-2)">
<q-btn
class="upload-image-btn q-mr-md"
icon="mdi-camera-outline"
size="md"
unelevated
round
v-if="!changeDisabled"
@click="inputFile?.click()"
style="color: hsla(var(--stone-0-hsl) / 0.7)"
></q-btn>
<q-btn
class="upload-image-btn"
icon="mdi-download-outline"
size="md"
unelevated
round
@click="downloadImage(imageUrl)"
style="color: hsla(var(--stone-0-hsl) / 0.7)"
></q-btn>
</div>
</div>
<div
class="row items-center justify-end q-py-sm q-px-md bordered-t"
v-if="!hiddenFooter"
>
<ClearButton
outlined
@click="
inputFile && (inputFile.value = ''),
(imageUrl = defaultUrl || fallbackUrl || ''),
(file = null),
$emit('clear')
"
class="q-px-md q-mr-auto"
:disabled="
clearButtonDisabled ||
imageUrl === fallbackUrl ||
imageUrl === defaultUrl ||
!imageUrl
"
v-if="clearButton"
/>
<CancelButton
outlined
class="q-px-md q-mr-sm"
@click="
inputFile && (inputFile.value = ''),
(imageUrl = defaultUrl || fallbackUrl || ''),
(file = null)
"
v-close-popup
/>
<SaveButton
outlined
@click="$emit('save', inputFile?.files?.[0] || null, imageUrl)"
/>
</div>
</AppBox>
</q-dialog>
</template>
<style scoped>
.image-dialog-content {
padding: 0;
border-radius: var(--radius-2);
flex-wrap: nowrap;
width: 100%;
max-width: 80%;
}
@media (min-width: 768px) {
.image-dialog-content {
max-width: 60%;
}
}
.image-dialog-body {
overflow-y: auto;
background-color: var(--surface-2) !important;
position: relative;
display: flex;
flex-direction: column;
flex: 1;
height: calc(100vh - 200px);
}
.upload-image-btn {
transition: 0.3s background-color ease-in-out;
background-color: hsla(0 0% 0% / 0.2);
backdrop-filter: blur(1px);
&:not(:hover) {
color: hsla(0 0% 40% / 1);
background-color: hsla(0 0% 0% / 0.2);
}
}
.image-container {
width: 100%;
height: calc(100vh - 200px);
min-height: 480px;
& > :deep(*) {
height: 100%;
width: 100%;
}
}
.image-container > :deep(*:not(:first-child)) {
display: none;
}
.close-btn {
color: hsl(var(--negative-bg));
background-color: hsla(var(--negative-bg) / 0.1);
&.dark {
background-color: transparent;
border: 1px solid hsl(var(--negative-bg));
}
}
</style>

View file

@ -0,0 +1,59 @@
<script setup lang="ts">
const remark = defineModel<string>('remark', { default: '' });
defineProps<{
readonly: boolean;
}>();
</script>
<template>
<div class="col-12 row">
<div class="col-12 q-mb-md text-weight-bold text-body1">
<q-icon
flat
size="xs"
class="q-pa-sm rounded q-mr-xs"
color="info"
name="mdi-asterisk-circle-outline"
style="background-color: var(--surface-3)"
/>
{{ $t('general.remark') }}
</div>
<div class="col-12">
<q-field
class="full-width"
outlined
for="input-detail"
id="input-detail"
:readonly="readonly"
:borderless="readonly"
:label="$t('general.remark')"
stack-label
dense
>
<q-editor
dense
:model-value="readonly ? remark || '-' : remark"
@update:model-value="
(v) => (typeof v === 'string' ? (remark = v) : '')
"
min-height="5rem"
class="q-mt-sm q-mb-xs"
:flat="!readonly"
:readonly="readonly"
: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'}`"
/>
</q-field>
</div>
</div>
</template>

View file

@ -0,0 +1,84 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { VuePDF, usePDF } from '@tato30/vue-pdf';
const props = withDefaults(
defineProps<{ url: string; file: File | undefined }>(),
{},
);
const scale = ref(1);
const page = ref(1);
const { pdf, pages } = usePDF(computed(() => props.url));
</script>
<template>
<div class="col full-height column no-wrap">
<div
class="surface-0 bordered row items-center justify-evenly q-pa-sm no-wrap"
style="height: 50px"
>
<q-btn
@click="page = page > 1 ? page - 1 : page"
class="btn-next"
icon="mdi-chevron-left"
unelevated
dense
id="btn-prev-page-top"
/>
<div class="ellipsis">Page {{ page }} of {{ pages }}</div>
<q-btn
@click="scale = scale > 0.25 ? scale - 0.25 : scale"
flat
dense
round
size="12px"
icon="mdi-magnify-minus-outline"
class="app-text-dark"
>
<q-tooltip>{{ $t('zoomOut') }}</q-tooltip>
</q-btn>
<div>{{ scale * 100 }}%</div>
<q-btn
flat
dense
round
size="12px"
class="app-text-dark"
icon="mdi-magnify-plus-outline"
@click="scale = scale < 2 ? scale + 0.25 : scale"
>
<q-tooltip>{{ $t('general.zoomIn') }}</q-tooltip>
</q-btn>
<q-btn
@click="page = page < pages ? page + 1 : page"
class="btn-next"
icon="mdi-chevron-right"
unelevated
dense
id="btn-prev-page-top"
/>
</div>
<div
class="flex flex-center surface-2 bordered-l bordered-r bordered-b full-height scroll"
>
<VuePDF
v-if="
url?.split('?').at(0)?.endsWith('.pdf') ||
file?.type === 'application/pdf'
"
class="q-py-md"
:pdf="pdf"
:page="page"
:scale="scale"
/>
<q-img v-else class="q-py-md full-width" :src="url" />
</div>
</div>
</template>

View file

@ -9,6 +9,7 @@ type Menu = {
name: string;
sub?: boolean;
tab?: string;
useBtn?: boolean;
};
const props = defineProps<{
@ -86,14 +87,27 @@ onUnmounted(() => {
'--side-menu__fg-active': active?.foreground,
'--side-menu__bg-active': active?.background,
}"
class="side-menu__item"
class="side-menu__item row items-center justify-between no-wrap"
:class="{
'side-menu__active': activeMenu === v.anchor,
'side-menu__sub': v.sub || false,
}"
@click="handleClick(v)"
>
{{ v.name }}
<span
class="row no-wrap items-center"
:class="{ 'app-text-muted': v.sub && activeMenu !== v.anchor }"
>
<div v-if="v.sub" class="circle-2"></div>
<div
v-if="v.sub"
class="surface-tab circle flex justify-center q-mx-md"
>
{{ menu.filter((v) => v.sub === true).indexOf(v) + 1 }}
</div>
{{ v.name }}
</span>
<slot v-if="v.useBtn" :name="`btn-${v.anchor}`"></slot>
</span>
</template>
</div>
@ -115,7 +129,7 @@ onUnmounted(() => {
cursor: pointer;
&.side-menu__sub {
margin-left: 1rem;
/* margin-left: 1rem; */
}
&.side-menu__active {
@ -125,4 +139,18 @@ onUnmounted(() => {
}
}
}
.circle {
width: 15px;
height: 15px;
font-size: 12px;
border-radius: 50%;
}
.circle-2 {
background: var(--surface-tab);
width: 7px;
height: 7px;
border-radius: 50%;
}
</style>

View file

@ -13,13 +13,18 @@ const props = withDefaults(
| 'cyan'
| 'yellow'
| 'red'
| 'magenta';
| 'magenta'
| 'blue'
| 'lime'
| 'light-purple';
}[];
dark?: boolean;
textSize?: string;
labelI18n?: boolean;
}>(),
{
labelI18n: false,
textSize: '12px',
},
);
</script>
@ -42,8 +47,13 @@ const props = withDefaults(
/>
</div>
<div class="col-6 justify-center column">
<div class="col-6 ellipsis text-bold" style="width: 100%">
{{ labelI18n ? $t(v.label) : v.label }}
<div
class="col-6 ellipsis text-bold text-caption"
style="width: 100%"
>
<span :style="`font-size: ${textSize}`">
{{ labelI18n ? $t(v.label) : v.label }}
</span>
<q-tooltip
anchor="top middle"
self="bottom middle"
@ -107,6 +117,18 @@ const props = withDefaults(
--_color: var(--pink-8-hsl);
}
.stat-card__lime {
--_color: var(--jungle-8-hsl);
}
.stat-card__light-purple {
--_color: var(--purple-7-hsl);
}
.stat-card__blue {
--_color: var(--blue-6-hsl);
}
.dark .stat-card__purple {
--_color: var(--violet-10-hsl);
}
@ -122,4 +144,8 @@ const props = withDefaults(
.dark .stat-card__magenta {
--_color: var(--pink-7-hsl);
}
.dark .stat-card__blue {
--_color: var(--blue-10-hsl);
}
</style>

View file

@ -0,0 +1,125 @@
<script setup lang="ts">
import { QTableProps } from 'quasar';
import KebabAction from 'components/shared/KebabAction.vue';
import DeleteButton from './button/DeleteButton.vue';
const props = withDefaults(
defineProps<{
rows: QTableProps['rows'];
columns: QTableProps['columns'];
flat?: boolean;
bordered?: boolean;
grid?: boolean;
hideHeader?: boolean;
buttomDownload?: boolean;
buttonDelete?: boolean;
hidePagination?: boolean;
imgColumn?: string;
}>(),
{
row: () => [],
column: () => [],
flat: false,
bordered: false,
grid: false,
hideHeader: false,
buttomDownload: false,
imgColumn: '',
},
);
defineEmits<{
(e: 'view', index: number): void;
(e: 'edit', index: number): void;
(e: 'delete', index: number): void;
(e: 'toggleStatus', row: typeof props.rows): void;
(e: 'download', index: number): void;
}>();
</script>
<template>
<q-table
v-bind="props"
class="full-height"
:no-data-label="$t('general.noDataTable')"
:hide-pagination
>
<slot name="zxc"></slot>
<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">
<span v-if="col.label === 'nameEmployee'">
{{ $t('fullname') }}
</span>
<span v-if="col.label !== ''">
{{ $t(col.label) }}
</span>
</q-th>
</q-tr>
</template>
<template v-slot:body-cell-order="props">
<q-td class="text-center">
{{ props.rowIndex + 1 }}
</q-td>
</template>
<template v-slot:[`body-cell-${imgColumn}`]="props">
<q-td>
<slot name="img-column" :props="props"></slot>
{{ props.row[imgColumn] }}
</q-td>
</template>
<template v-slot:body-cell-action="props">
<q-td class="text-center">
<DeleteButton iconOnly v-if="buttonDelete" />
<q-btn
v-if="!buttonDelete"
icon="mdi-eye-outline"
size="sm"
dense
round
flat
@click.stop="$emit('view', props.rowIndex)"
/>
<q-btn
v-if="buttomDownload && props.row.id !== undefined"
icon="mdi-download-outline"
size="sm"
dense
round
flat
@click.stop="$emit('download', props.rowIndex)"
/>
<KebabAction
v-if="!buttonDelete"
hide-toggle
:id-name="props.row.code"
:status="props.row.status"
@view="$emit('view', props.rowIndex)"
@edit="$emit('edit', props.rowIndex)"
@delete="$emit('delete', props.rowIndex)"
@change-status="$emit('toggleStatus', props.row)"
/>
</q-td>
<slot name="dialog" :index="props.rowIndex" :row="props.row"></slot>
</template>
</q-table>
</template>
<style lang="scss" scoped>
:deep(i.q-icon.mdi.mdi-alert.q-table__bottom-nodata-icon) {
color: #ffc224 !important;
}
</style>

View file

@ -43,6 +43,7 @@ defineEmits<{
:node-key="nodeKey"
:label-key="labelKey"
:children-key="childrenKey"
:no-nodes-label="$t('general.noData')"
v-model:expanded="expandedTree"
style="color: var(--foreground)"
>
@ -51,7 +52,10 @@ defineEmits<{
class="full-width q-py-xs"
:class="{
'clickable-node': typeTree === 'product' || node.isHeadOffice,
'cursor-pointer': node.type === 'group' || node.type === 'type',
'cursor-pointer':
node.type === 'group' ||
node.type === 'type' ||
node.type === 'productService',
'active-node': expandedTree[expandedTree.length - 1] === node.id,
}"
v-touch-hold.mouse="() => $emit('handleHold', node)"
@ -60,12 +64,12 @@ defineEmits<{
$emit('select', node);
}
"
:id="`tree-enter-${node.name}`"
:id="`tree-enter-${node.name}${node.for ? `-${node.for}` : ''}`"
>
<div class="row col items-center justify-between full-width no-wrap">
<q-icon
v-if="
(node.type === 'group' && node._count.type > 0) ||
node.type === 'group' ||
(node.isHeadOffice && node._count.branch !== 0)
"
name="mdi-triangle-down"
@ -75,7 +79,10 @@ defineEmits<{
/>
<div
class="col row"
:class="{ 'q-pl-sm': node.type === 'type' }"
:class="{
'q-pl-sm q-py-xs':
node.type === 'type' || node.type === 'productService',
}"
:style="`padding-left:${(node.type === 'group' && node._count.type === 0) || (node.isHeadOffice && node._count.branch === 0) ? '36px' : ''}`"
>
<span
@ -101,7 +108,7 @@ defineEmits<{
>
<slot></slot>
<q-btn
v-if="typeTree === 'product'"
v-if="typeTree === 'product' && !node.actionDisabled"
icon="mdi-eye-outline"
:id="`btn-tree-eye-${node.name}`"
size="sm"
@ -125,8 +132,9 @@ defineEmits<{
<KebabAction
v-if="action && !node.actionDisabled"
:disable-delete="node.status !== 'CREATED'"
:id-name="node.name"
status="ACTIVE"
:status="node.status"
@view="$emit('view', node)"
@edit="$emit('edit', node)"
@delete="$emit('delete', node)"

View file

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

View file

@ -67,7 +67,7 @@ input:checked + .slider {
background-color: hsl(var(--positive-bg));
&.disable {
background-color: var(--stone-4);
// background-color: var(--stone-4);
}
}

View file

@ -8,3 +8,4 @@ export { default as UndoButton } from './UndoButton.vue';
export { default as ToggleButton } from './ToggleButton.vue';
export { default as ClearButton } from './ClearButton.vue';
export { default as CloseButton } from './CloseButton.vue';
export { default as ViewButton } from './viewButton.vue';

View file

@ -0,0 +1,27 @@
<script lang="ts" setup>
import MainButton from './MainButton.vue';
defineEmits<{
(e: 'click', v: MouseEvent): void;
}>();
defineProps<{
iconOnly?: boolean;
solid?: boolean;
outlined?: boolean;
disabled?: boolean;
dark?: boolean;
size?: string;
}>();
</script>
<template>
<MainButton
@click="(e) => $emit('click', e)"
v-bind="{ ...$props, ...$attrs }"
icon="mdi-eye-outline"
color="var(--gray-8-hsl)"
:title="iconOnly ? $t('general.viewDetail') : undefined"
>
{{ $t('general.viewDetail') }}
</MainButton>
</template>

View file

@ -15,7 +15,7 @@ defineProps<{
<slot name="title-after" />
</div>
<slot name="after">
<CancelButton icon-only v-close-popup />
<CancelButton id="btn-form-close" icon-only v-close-popup />
</slot>
</div>
</template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, watch, reactive, ref } from 'vue';
import { onMounted, watch, reactive, ref, computed } from 'vue';
import useAddressStore, {
District,
Province,
@ -7,6 +7,7 @@ import useAddressStore, {
} from 'stores/address';
import { selectFilterOptionRefMod } from 'stores/utils';
import { QSelect } from 'quasar';
import { useI18n } from 'vue-i18n';
defineProps<{
title?: string;
@ -21,7 +22,10 @@ defineProps<{
indexId?: number;
prefixId: string;
hideTitle?: boolean;
hideInputEn?: boolean;
hideIcon?: boolean;
useEmployment?: boolean;
useWorkPlace?: boolean;
}>();
@ -30,12 +34,26 @@ const workplace = defineModel<string>('workplace', { default: '' });
const workplaceEN = defineModel<string>('workplaceEn', { default: '' });
const address = defineModel('address', { default: '' });
const addressEN = defineModel('addressEN', { default: '' });
const street = defineModel('street', { default: '' });
const streetEN = defineModel('streetEN', { default: '' });
const moo = defineModel('moo', { default: '' });
const mooEN = defineModel('mooEN', { default: '' });
const soi = defineModel('soi', { default: '' });
const soiEN = defineModel('soiEN', { default: '' });
const provinceId = defineModel<string | null | undefined>('provinceId');
const districtId = defineModel<string | null | undefined>('districtId');
const subDistrictId = defineModel<string | null | undefined>('subDistrictId');
const zipCode = defineModel<string | null | undefined>('zipCode');
const sameWithEmployer = defineModel<boolean>('sameWithEmployer');
const homeCode = defineModel<string | null | undefined>('homeCode');
const employmentOffice = defineModel<string | null | undefined>(
'employmentOffice',
);
const employmentOfficeEN = defineModel<string | null | undefined>(
'employmentOfficeEN',
);
const addrOptions = reactive<{
provinceOps: Province[];
districtOps: District[];
@ -46,6 +64,80 @@ const addrOptions = reactive<{
subDistrictOps: [],
});
const { t } = useI18n();
const fullAddress = computed(() => {
const addressParts = [`${address.value},`];
const province = provinceOptions.value.find((v) => v.id === provinceId.value);
const district = districtOptions.value.find((v) => v.id === districtId.value);
const sDistrict = subDistrictOptions.value.find(
(v) => v.id === subDistrictId.value,
);
if (moo.value) addressParts.push(`${t('form.moo')} ${moo.value},`);
if (soi.value) addressParts.push(`${t('form.soi')} ${soi.value},`);
if (street.value) addressParts.push(`${t('form.road')} ${street.value},`);
if (subDistrictId.value && sDistrict) {
addressParts.push(
typeof sDistrict.name === 'string' ? `${sDistrict.name},` : '',
);
}
if (districtId.value && district)
addressParts.push(
typeof district.name === 'string' ? `${district.name},` : '',
);
if (provinceId.value && province) {
addressParts.push(
typeof province.name === 'string' ? `${province.name}` : '',
);
sDistrict &&
addressParts.push(
typeof sDistrict.zipCode === 'string' ? `${sDistrict.zipCode}` : '',
);
}
return addressParts.join(' ');
});
const fullAddressEN = computed(() => {
const addressParts = [`${addressEN.value},`];
const province = provinceOptions.value.find((v) => v.id === provinceId.value);
const district = districtOptions.value.find((v) => v.id === districtId.value);
const sDistrict = subDistrictOptions.value.find(
(v) => v.id === subDistrictId.value,
);
if (mooEN.value) addressParts.push(`Moo ${mooEN.value},`);
if (soiEN.value) addressParts.push(`Soi ${soiEN.value},`);
if (streetEN.value) addressParts.push(`${streetEN.value} Rd.`);
if (subDistrictId.value && sDistrict) {
addressParts.push(
typeof sDistrict.nameEN === 'string' ? `${sDistrict.nameEN},` : '',
);
}
if (districtId.value && district)
addressParts.push(
typeof district.nameEN === 'string' ? `${district.nameEN},` : '',
);
if (provinceId.value && province) {
addressParts.push(
typeof province.nameEN === 'string' ? `${province.nameEN}` : '',
);
sDistrict &&
addressParts.push(
typeof sDistrict.zipCode === 'string' ? `${sDistrict.zipCode}` : '',
);
}
return addressParts.join(' ');
});
async function fetchProvince() {
const result = await adrressStore.fetchProvince();
@ -199,10 +291,51 @@ watch(districtId, fetchSubDistrict);
</div>
<div class="col-12 row q-col-gutter-y-md">
<div class="col-12 app-text-muted-2">
<q-icon size="xs" class="q-mr-xs" name="mdi-map-marker-outline" />
{{ addressTitle || $t('form.address') }}
<div v-if="useEmployment" class="col-12 row q-col-gutter-sm">
<q-input
outlined
hide-bottom-space
class="col-3"
v-model="homeCode"
mask="###########"
:dense="dense"
:label="$t('customer.form.homeCode')"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-address-${indexId}` : 'input-address'}`"
:rules="
disabledRule
? []
: [(val) => (val && val.length > 0) || $t('form.error.required')]
"
/>
<q-input
outlined
hide-bottom-space
class="col"
v-model="employmentOffice"
:dense="dense"
:label="$t('customer.form.employmentOffice')"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-address-${indexId}` : 'input-address'}`"
/>
<q-input
outlined
hide-bottom-space
class="col"
v-model="employmentOfficeEN"
:dense="dense"
:label="`${$t('customer.form.employmentOffice')} (EN)`"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-address-${indexId}` : 'input-address'}`"
/>
</div>
<template v-if="!hideIcon">
<div class="col-12 app-text-muted-2">
<q-icon size="xs" class="q-mr-xs" name="mdi-map-marker-outline" />
{{ addressTitle || $t('form.address') }}
</div>
</template>
<div class="col-12 row q-col-gutter-sm">
<div class="row col-12" v-if="useWorkPlace">
<q-input
@ -210,7 +343,6 @@ watch(districtId, fetchSubDistrict);
hide-bottom-space
class="col-6"
v-model="workplace"
lazy-rules="ondemand"
:dense="dense"
:label="$t('customer.form.workplace')"
:readonly="readonly || sameWithEmployer"
@ -229,19 +361,53 @@ watch(districtId, fetchSubDistrict);
<q-input
outlined
hide-bottom-space
class="col-12"
class="col-md-5 col-8"
v-model="address"
lazy-rules="ondemand"
:dense="dense"
:label="$t('form.address')"
:label="$t('form.addressNo')"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-address-${indexId}` : 'input-address'}`"
:for="`${prefixId}-${indexId !== undefined ? `input-address-no-${indexId}` : 'input-address-no'}`"
:rules="
disabledRule
? []
: [(val) => (val && val.length > 0) || $t('form.error.required')]
"
/>
<q-input
outlined
hide-bottom-space
class="col-md-1 col-4"
:model-value="readonly ? moo || '-' : moo"
:dense="dense"
:label="$t('form.moo')"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-moo-${indexId}` : 'input-moo'}`"
@update:model-value="(v) => (typeof v === 'string' ? (moo = v) : '')"
/>
<q-input
outlined
hide-bottom-space
class="col-md-3 col-6"
:model-value="readonly ? soi || '-' : soi"
:dense="dense"
:label="$t('form.soi')"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-soi-${indexId}` : 'input-soi'}`"
@update:model-value="(v) => (typeof v === 'string' ? (soi = v) : '')"
/>
<q-input
outlined
hide-bottom-space
class="col-md-3 col-6"
:model-value="readonly ? street || '-' : street"
:dense="dense"
:label="$t('form.road')"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-street-${indexId}` : 'input-street'}`"
@update:model-value="
(v) => (typeof v === 'string' ? (street = v) : '')
"
/>
<q-select
autocomplete="off"
outlined
@ -256,7 +422,6 @@ watch(districtId, fetchSubDistrict);
input-debounce="0"
option-label="name"
v-model="provinceId"
lazy-rules="ondemand"
class="col-md-3 col-6"
:dense="dense"
:label="$t('form.province')"
@ -301,7 +466,6 @@ watch(districtId, fetchSubDistrict);
input-debounce="0"
option-label="name"
v-model="districtId"
lazy-rules="ondemand"
class="col-md-3 col-6"
:dense="dense"
:label="$t('form.district')"
@ -344,7 +508,6 @@ watch(districtId, fetchSubDistrict);
option-value="id"
input-debounce="0"
option-label="name"
lazy-rules="ondemand"
class="col-md-3 col-6"
v-model="subDistrictId"
:dense="dense"
@ -376,34 +539,46 @@ watch(districtId, fetchSubDistrict);
</template>
</q-select>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-${indexId !== undefined ? `input-zip-code-${indexId}` : 'input-zip-code'}`"
:dense="dense"
outlined
:disable="!readonly && !sameWithEmployer"
readonly
:label="$t('form.zipCode')"
class="col-md-2 col-6"
class="col-md-3 col-6"
:model-value="
addrOptions.subDistrictOps
?.filter((x) => x.id === subDistrictId)
.map((x) => x.zipCode)[0] ?? ''
"
/>
<q-input
outlined
hide-bottom-space
class="col-12"
:model-value="
address ? ($i18n.locale === 'eng' ? fullAddress : fullAddress) : ''
"
:dense="dense"
:label="$t('form.fullAddress')"
readonly
:disable="!readonly && !sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-full-address-${indexId}` : 'input-full-address'}`"
/>
</div>
<div class="col-12 app-text-muted-2">
<q-icon size="xs" class="q-mr-xs" name="mdi-map-marker-outline" />
{{ addressTitleEN || $t('form.address', { suffix: '(EN)' }) }}
</div>
<div class="col-12 row q-col-gutter-sm">
<template v-if="!hideIcon">
<div class="col-12 app-text-muted-2" v-if="!hideInputEn">
<q-icon size="xs" class="q-mr-xs" name="mdi-map-marker-outline" />
{{ addressTitleEN || $t('form.address', { suffix: '(EN)' }) }}
</div>
</template>
<div class="col-12 row q-col-gutter-sm" v-if="!hideInputEn">
<div class="row col-12" v-if="useWorkPlace">
<q-input
outlined
hide-bottom-space
class="col-6"
v-model="workplaceEN"
lazy-rules="ondemand"
:dense="dense"
:label="$t('customer.form.workplaceEN')"
:readonly="readonly || sameWithEmployer"
@ -420,14 +595,13 @@ watch(districtId, fetchSubDistrict);
</div>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-${indexId !== undefined ? `input-address-en-${indexId}` : 'input-address-en'}`"
:dense="dense"
:readonly="readonly || sameWithEmployer"
outlined
hide-bottom-space
:label="$t('form.address', { suffix: '(EN)' })"
class="col-12"
label="Address No."
class="col-md-5 col-8"
v-model="addressEN"
:rules="
disabledRule
@ -435,6 +609,45 @@ watch(districtId, fetchSubDistrict);
: [(val) => (val && val.length > 0) || $t('form.error.required')]
"
/>
<q-input
outlined
hide-bottom-space
class="col-md-1 col-4"
:model-value="readonly ? mooEN || '-' : mooEN"
:dense="dense"
label="Moo"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-moo-${indexId}` : 'input-moo'}`"
@update:model-value="
(v) => (typeof v === 'string' ? (mooEN = v) : '')
"
/>
<q-input
outlined
hide-bottom-space
class="col-md-3 col-6"
:model-value="readonly ? soiEN || '-' : soiEN"
:dense="dense"
label="Soi"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-soi-${indexId}` : 'input-soi'}`"
@update:model-value="
(v) => (typeof v === 'string' ? (soiEN = v) : '')
"
/>
<q-input
outlined
hide-bottom-space
class="col-md-3 col-6"
:model-value="readonly ? streetEN || '-' : streetEN"
:dense="dense"
label="Road"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-street-${indexId}` : 'input-street'}`"
@update:model-value="
(v) => (typeof v === 'string' ? (streetEN = v) : '')
"
/>
<q-select
autocomplete="off"
outlined
@ -448,11 +661,10 @@ watch(districtId, fetchSubDistrict);
option-value="id"
input-debounce="0"
v-model="provinceId"
lazy-rules="ondemand"
option-label="nameEN"
class="col-md-3 col-6"
:dense="dense"
:label="$t('form.province')"
label="Province"
:options="provinceOptions"
:readonly="readonly || sameWithEmployer"
:hide-dropdown-icon="readonly || sameWithEmployer"
@ -492,11 +704,10 @@ watch(districtId, fetchSubDistrict);
option-value="id"
input-debounce="0"
v-model="districtId"
lazy-rules="ondemand"
option-label="nameEN"
class="col-md-3 col-6"
:dense="dense"
:label="$t('form.district')"
label="District"
:options="districtOptions"
:readonly="readonly || sameWithEmployer"
:hide-dropdown-icon="readonly || sameWithEmployer"
@ -535,12 +746,11 @@ watch(districtId, fetchSubDistrict);
hide-bottom-space
option-value="id"
input-debounce="0"
lazy-rules="ondemand"
option-label="nameEN"
class="col-md-3 col-6"
v-model="subDistrictId"
:dense="dense"
:label="$t('form.subDistrict')"
label="Sub-District"
:options="subDistrictOptions"
:readonly="readonly || sameWithEmployer"
:hide-dropdown-icon="readonly || sameWithEmployer"
@ -568,7 +778,6 @@ watch(districtId, fetchSubDistrict);
</template>
</q-select>
<q-input
lazy-rules="ondemand"
hide-bottom-space
:for="`${prefixId}-${indexId !== undefined ? `input-zip-code-${indexId}` : 'input-zip-code'}`"
:dense="dense"
@ -576,14 +785,25 @@ watch(districtId, fetchSubDistrict);
readonly
:disable="!readonly && !sameWithEmployer"
zip="zip-en"
:label="$t('form.zipCode')"
class="col-md-2 col-6"
label="Zip Code"
class="col-md-3 col-6"
:model-value="
addrOptions.subDistrictOps
?.filter((x) => x.id === subDistrictId)
.map((x) => x.zipCode)[0] ?? ''
"
/>
<q-input
outlined
hide-bottom-space
class="col-12"
:model-value="addressEN ? fullAddressEN : ''"
:dense="dense"
label="Full Address"
readonly
:disable="!readonly && !sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-full-address-en-${indexId}` : 'input-full-address-en'}`"
/>
</div>
</div>
</div>

View file

@ -11,6 +11,7 @@ const props = defineProps<{
readonly?: boolean;
clearable?: boolean;
label?: string;
bgColor?: string;
rules?: ((value: string) => string | true)[];
disabledDates?: string[] | Date[] | ((date: Date) => boolean);
}>();
@ -75,11 +76,11 @@ function valueUpdate(value: string) {
</template>
<template #trigger>
<q-input
lazy-rules="ondemand"
placeholder="DD/MM/YYYY"
hide-bottom-space
dense
outlined
:bg-color="bgColor"
:rules
:label
:for="id"

View file

@ -1,10 +1,16 @@
<script lang="ts" setup>
import { ref } from 'vue';
import ToggleButton from '../button/ToggleButton.vue';
import { QMenu } from 'quasar';
import { watch } from 'vue';
withDefaults(
const props = withDefaults(
defineProps<{
idName: string;
status: string;
hideToggle?: boolean;
useLink?: boolean;
useUpload?: boolean;
disableDelete?: boolean;
}>(),
@ -17,9 +23,22 @@ withDefaults(
defineEmits<{
(e: 'view'): void;
(e: 'edit'): void;
(e: 'link'): void;
(e: 'upload'): void;
(e: 'delete'): void;
(e: 'changeStatus'): void;
}>();
const refMenu = ref<InstanceType<typeof QMenu>>();
watch(
() => props.status,
() => {
setTimeout(() => {
refMenu.value?.hide();
}, 100);
},
);
</script>
<template>
<q-btn
@ -31,7 +50,7 @@ defineEmits<{
:id="`btn-kebab-action-${idName}`"
@click.stop
>
<q-menu class="bordered">
<q-menu class="bordered" ref="refMenu" :key="idName">
<q-list>
<q-item
v-close-popup
@ -73,6 +92,49 @@ defineEmits<{
{{ $t('general.edit') }}
</span>
</q-item>
<q-item
v-if="status !== 'INACTIVE' && useLink"
v-close-popup
dense
clickable
class="row q-py-sm"
style="white-space: nowrap"
:id="`btn-kebab-edit-${idName}`"
@click.stop="() => $emit('link')"
>
<q-icon
size="xs"
class="col-3"
name="mdi-link-variant"
style="color: hsl(var(--yellow-6-hsl))"
/>
<span class="col-9 q-px-md flex items-center">
{{ $t('general.add', { text: $t('quotation.receipt') }) }}
</span>
</q-item>
<q-item
v-if="status !== 'INACTIVE' && useUpload"
v-close-popup
dense
clickable
class="row q-py-sm"
style="white-space: nowrap"
:id="`btn-kebab-edit-${idName}`"
@click.stop="() => $emit('upload')"
>
<q-icon
size="xs"
class="col-3"
name="mdi-upload"
style="color: hsl(var(--blue-10-hsl))"
/>
<span class="col-9 q-px-md flex items-center">
{{ $t('general.upload', { msg: $t('general.attachment') }) }}
</span>
</q-item>
<q-item
v-if="status !== 'INACTIVE'"
v-close-popup
@ -100,14 +162,18 @@ defineEmits<{
</span>
</q-item>
<q-item dense>
<q-item v-if="!hideToggle" dense>
<q-item-section class="q-py-sm">
<div class="q-pa-sm surface-2 rounded flex items-center">
<ToggleButton
two-way
:id="`btn-kebab-status-${idName}`"
:model-value="status !== 'INACTIVE'"
@click="() => $emit('changeStatus')"
@click="
() => {
$emit('changeStatus');
}
"
/>
<span class="q-pl-md">
{{

View file

@ -12,6 +12,7 @@ defineProps<{
male?: boolean;
female?: boolean;
img?: string;
fallbackImg?: string;
detail?: { icon: string; value: string }[];
};
tag?: [{ color: string; value: string }];
@ -48,7 +49,7 @@ defineEmits<{
}"
@click="$emit('enterCard', 'INFO')"
>
<div class="column items-center">
<div class="column items-center" :class="{ 'q-pt-sm': noAction }">
<!-- kebab menu -->
<div class="full-width flex no-wrap" v-if="!noAction">
<div style="margin-right: auto">
@ -98,19 +99,15 @@ defineEmits<{
<q-img
v-if="!$slots.img"
:src="data.img ?? '/no-profile.png'"
style="
object-fit: cover;
width: 100%;
height: 100%;
border-radius: 50%;
"
fit="cover"
style="width: 100%; height: 100%; border-radius: 50%"
>
<template #error>
<div
style="background: none"
class="full-width full-height items-center justify-center flex"
class="no-padding full-width full-height flex items-center justify-center"
>
<q-img src="/no-profile.png" width="5rem" />
<q-img :src="data.fallbackImg || '/no-profile.png'" fit="cover" />
</div>
</template>
</q-img>
@ -139,22 +136,26 @@ defineEmits<{
</AppCircle>
<!-- name symbol -->
<span class="items-center text-center row full-width">
<span class="col ellipsis" style="width: 0">
{{ data.name }}
<span class="full-width">
<span
class="items-center justify-center row text-center ellipsis col-6"
>
<div class="items-center ellipsis" style="max-width: 140px">
{{ data.name }}
</div>
<Icon
v-if="data.male || data.female"
class="q-pl-xs"
:class="{
'symbol-gender': data.male || data.female,
'symbol-gender__male': !disabled && data.male,
'symbol-gender__female': !disabled && data.female,
'symbol-gender__disable': disabled,
}"
:icon="`material-symbols:${data.male ? 'male' : 'female'}`"
width="24px"
/>
</span>
<Icon
v-if="data.male || data.female"
class="q-pl-xs"
:class="{
'symbol-gender': data.male || data.female,
'symbol-gender__male': !disabled && data.male,
'symbol-gender__female': !disabled && data.female,
'symbol-gender__disable': disabled,
}"
:icon="`material-symbols:${data.male ? 'male' : 'female'}`"
width="24px"
/>
</span>
<span style="color: hsl(var(--text-mute)); scale: 0.9">
{{ data.code || '-' }}

View file

@ -0,0 +1,97 @@
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { selectFilterOptionRefMod } from 'src/stores/utils';
import { QSelect } from 'quasar';
const model = defineModel<string | Date | null>();
const options = ref<Record<string, unknown>[]>([]);
let defaultFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
const props = withDefaults(
defineProps<{
id?: string;
label?: string;
option: Record<string, unknown>[];
optionLabel?: string;
optionValue?: string;
readonly?: boolean;
clearable?: boolean;
incremental?: boolean;
rules?: ((value: string) => string | true)[];
}>(),
{ option: () => [], optionLabel: 'label', optionValue: 'value' },
);
defineEmits<{
(e: 'filter', val: string, update: void): void;
}>();
onMounted(() => {
defaultFilter = selectFilterOptionRefMod(
ref(props.option),
options,
props.optionLabel,
);
});
watch(
() => props.option,
() => {
defaultFilter = selectFilterOptionRefMod(
ref(props.option),
options,
props.optionLabel,
);
},
);
</script>
<template>
<q-select
outlined
:clearable
use-input
emit-value
map-options
hide-selected
hide-bottom-space
:fill-input="!!model"
:hide-dropdown-icon="readonly"
input-debounce="0"
:option-value="optionValue"
:option-label="optionLabel"
v-model="model"
dense
:readonly
:label="label"
:options="incremental ? option : options"
:for="`select-${id}`"
@filter="
(val, update) => {
incremental ? $emit('filter', val, update) : defaultFilter(val, update);
}
"
:rules
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
<template v-if="$slots.name" v-slot:selected-item="scope">
<slot name="selected-item" :scope="scope"></slot>
</template>
<template v-if="$slots.option" v-slot:option="scope">
<slot name="option" :scope="scope"></slot>
</template>
</q-select>
</template>

View file

@ -0,0 +1,77 @@
<script setup lang="ts">
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;
color?: string;
}>(),
{
items: () => [],
color: 'var(--brand-1)',
},
);
function select(item: unknown) {
if (selectedItem.value.includes(item)) {
let index = selectedItem.value.indexOf(item);
selectedItem.value.splice(index, 1);
} else selectedItem.value.push(item);
}
</script>
<template>
<section class="full-width column q-pa-md">
<header class="row items-center no-wrap q-mb-md">
<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="200"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
</header>
<section class="col">
<div class="row q-col-gutter-md">
<div
v-for="(item, i) in items"
:key="i"
class="col-md-2 col-sm-6 col-12"
>
<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="data" :item="item"></slot>
</div>
</div>
</div>
</section>
</section>
</template>
<style lang="scss" scoped>
.badge {
border-radius: 50%;
width: 20px;
height: 20px;
color: var(--surface-1);
}
</style>

View file

@ -0,0 +1,169 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
type Node = {
[key: string]: any;
opened?: boolean;
selected?: boolean;
children?: Node[];
};
type Props = {
level?: number;
keyTitle?: string;
keySubtitle?: string;
expandable?: boolean;
decoration?: {
level?: number;
bg?: string;
fg?: string;
icon?: string;
}[];
};
const props = defineProps<Props>();
const nodes = defineModel<Node[]>('nodes', { required: true });
const emits = defineEmits<{ (e: 'checked'): void }>();
const dec = props.decoration?.find((v) => v.level === (props.level || 0));
function recursiveDeselect(node: Node) {
if (node.children) {
node.children.forEach((v) => {
v.selected = false;
recursiveDeselect(v);
});
}
}
function toggleCheck(node: Node) {
node.selected = !node.selected;
if (node.selected === false) recursiveDeselect(node);
if (node.selected === true) emits('checked');
}
function toggleExpand(node: Node) {
node.opened = !node.opened;
}
</script>
<template>
<div class="tree-container">
<div v-for="(node, i) in nodes" class="tree-item" :key="i">
<slot
v-if="$slots['item']"
name="item"
:data="{ node, toggleExpand, toggleCheck }"
/>
<template v-else>
<div
class="item__content row items-center no-wrap"
@click="toggleExpand(node)"
>
<label class="flex items-center item__checkbox" @click.stop>
<input
type="checkbox"
v-model="node.selected"
@click="toggleCheck(node)"
/>
</label>
<div
class="item__icon flex items-center justify-center"
:style="`background: ${dec?.bg}; color: ${dec?.fg}`"
>
<Icon v-if="dec && dec.icon" :icon="dec.icon" />
</div>
<div class="column">
<span class="item__title">
{{ node[keyTitle || 'title'] || 'No Title' }}
</span>
<span class="item__subtitle">
{{ node[keySubtitle || 'subtitle'] || 'No Subtitle' }}
</span>
</div>
</div>
</template>
<q-separator v-if="!level"></q-separator>
<transition name="slide">
<div
class="q-pl-lg q-pt-sm"
v-if="node.opened && node.children && node.children.length > 0"
>
<TreeView
class="item__children"
v-if="node.children"
v-model:nodes="node.children"
@checked="
() => {
node.selected = true;
$emit('checked');
}
"
:level="(level || 0) + 1"
:expandable
:decoration
/>
</div>
</transition>
</div>
</div>
</template>
<style lang="css">
.tree-container {
display: flex;
flex-direction: column;
user-select: none;
gap: 8px;
& .tree-item {
& .item__content {
padding: 0.1rem 0.5rem;
&:hover {
background: hsla(0 0% 0% / 0.1);
border-radius: var(--radius-2);
}
}
& .item__checkbox {
padding: 0.1rem 0.5rem;
margin-right: 1rem;
}
& .item__icon {
margin-right: 1rem;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
}
& .item__title {
font-weight: 600;
}
& .item__subtitle {
font-size: 80%;
color: hsla(var(--text-mute-2));
}
}
}
.slide-enter-active {
transition: all 0.1s ease-out;
}
.slide-leave-active {
transition: all 0.1s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-enter-from,
.slide-leave-to {
transform: translateY(-20px);
opacity: 0;
}
</style>

View file

@ -0,0 +1,76 @@
<script setup lang="ts">
import DatePicker from '../shared/DatePicker.vue';
const registrationNumber = defineModel<string>('registrationNumber', {
default: '',
});
const name = defineModel<string>('name', {
default: '',
});
const businessRegistrationDate = defineModel<string>(
'businessRegistrationDate',
{ default: '' },
);
const visaPlace = defineModel<string>('visaPlace', { default: '' });
defineProps<{
prefixId?: string;
outlined?: boolean;
readonly?: boolean;
customerType?: 'CORP' | 'PERS';
}>();
</script>
<template>
<div class="row q-mb-sm" style="gap: 10px">
<div class="col-12 text-subtitle1 text-weight-bold">
<p>Document Properties</p>
</div>
<div class="col-12 row q-col-gutter-sm">
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6"
:label="$t('form.businessRegistration.registrationNumber')"
for="input-citizen-id"
v-model="registrationNumber"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6"
:label="$t('form.businessRegistration.registrationNumber')"
for="input-citizen-id"
v-model="name"
/>
</div>
<div class="col-12 row q-col-gutter-sm">
<DatePicker
:label="$t('form.businessRegistration.businessRegistration')"
v-model="businessRegistrationDate"
class="col-4"
:id="`${prefixId}-input-birth-date`"
:readonly="readonly"
clearable
/>
<DatePicker
:label="$t('customerEmployee.form.visaPlace')"
v-model="visaPlace"
class="col-4"
:id="`${prefixId}-input-birth-date`"
:readonly="readonly"
clearable
/>
</div>
</div>
</template>
<style lang="scss" scoped></style>

View file

@ -2,6 +2,7 @@
import { ref, watch } from 'vue';
import { QSelect } from 'quasar';
import { selectFilterOptionRefMod } from 'stores/utils';
import { AddressForm } from 'components/form';
import { getRole } from 'src/services/keycloak';
import useOptionStore from 'stores/options';
import { onMounted } from 'vue';
@ -38,23 +39,48 @@ defineEmits<{
(e: 'cancel'): void;
}>();
const middleName = defineModel<string>('middleName', { default: '' });
const middleNameEn = defineModel<string>('middleNameEn', { default: '' });
const workplaceEN = defineModel<string>('workplaceEn', { default: '' });
const addressEN = defineModel('addressEN', { default: '' });
const street = defineModel('street', { default: '' });
const streetEN = defineModel('streetEN', { default: '' });
const moo = defineModel('moo', { default: '' });
const mooEN = defineModel('mooEN', { default: '' });
const soi = defineModel('soi', { default: '' });
const soiEN = defineModel('soiEN', { default: '' });
const provinceId = defineModel<string | null | undefined>('provinceId');
const districtId = defineModel<string | null | undefined>('districtId');
const subDistrictId = defineModel<string | null | undefined>('subDistrictId');
const zipCode = defineModel<string | null | undefined>('zipCode');
const sameWithEmployer = defineModel<boolean>('sameWithEmployer');
const homeCode = defineModel<string | null | undefined>('homeCode');
const employmentOffice = defineModel<string | null | undefined>(
'employmentOffice',
);
const employmentOfficeEN = defineModel<string | null | undefined>(
'employmentOfficeEN',
);
const optionStore = useOptionStore();
const namePrefix = defineModel<string | null>('namePrefix');
const birthDate = defineModel<Date | string | null>('birthDate');
const gender = defineModel<string>('gender');
const gender = defineModel<string>('gender', { required: true });
const address = defineModel<string>('address');
const firstName = defineModel<string>('firstName', { required: true });
const lastName = defineModel<string>('lastName', { required: true });
const firstNameEN = defineModel<string>('firstNameEn', { required: true });
const lastNameEN = defineModel<string>('lastNameEn', { required: true });
const issueDate = defineModel<Date>('issueDate', { required: true });
const expireDate = defineModel<Date>('expireDate', { required: true });
const citizenId = defineModel<string | undefined>('citizenId', {
required: true,
});
const nationality = defineModel<string>('nationality');
const nationality = defineModel<string>('nationality', { required: true });
const religion = defineModel<string>('religion');
const religion = defineModel<string>('religion', { required: true });
const branchOptions = defineModel<{ id: string; name: string }[]>(
'branchOptions',
@ -152,7 +178,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
</div>
<div class="col row" style="gap: 10px">
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
@ -173,7 +198,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
@ -185,7 +209,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
@ -207,7 +230,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
class="col"
dense
:readonly="readonly"
@ -244,7 +266,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
hide-dropdown-icon
class="col-2"
dense
@ -271,7 +292,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
</q-select>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
@ -283,7 +303,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
@ -295,7 +314,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:disable="!readonly"
@ -310,7 +328,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
@ -319,9 +336,10 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
:label="$t('customer.form.firstNameEN')"
for="input-first-name-en"
v-model="firstNameEN"
:rules="[(val) => /^[A-Za-z]+$/.test(val)]"
:error-message="$t('form.error.letterOnly')"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
@ -330,22 +348,33 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
:label="$t('customer.form.lastNameEN')"
for="input-last-name-en"
v-model="lastNameEN"
:rules="[(val) => /^[A-Za-z]+$/.test(val)]"
:error-message="$t('form.error.letterOnly')"
/>
<q-input
lazy-rules="ondemand"
<AddressForm
prefixId="citizen"
v-model:homeCode="homeCode"
v-model:employmentOffice="employmentOffice"
v-model:employmentOfficeEN="employmentOfficeEN"
v-model:address="address"
v-model:addressEN="addressEN"
v-model:street="street"
v-model:streetEN="streetEN"
v-model:moo="moo"
v-model:mooEN="mooEN"
v-model:soi="soi"
v-model:soiEN="soiEN"
v-model:province-id="provinceId"
v-model:district-id="districtId"
v-model:sub-district-id="subDistrictId"
hide-title
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12"
:label="$t('general.address')"
for="input-address"
v-model="address"
/>
<DatePicker
:label="$t('customer.form.issueDate')"
v-model="firstName"
v-model="issueDate"
class="col-6"
:id="`${prefixId}-input-issue-date`"
:readonly="readonly"
@ -354,7 +383,7 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
<DatePicker
:label="$t('customer.form.passportExpiryDate')"
v-model="firstName"
v-model="expireDate"
class="col-6"
:id="`${prefixId}-input-passport-expiry-date`"
:readonly="readonly"

View file

@ -0,0 +1,99 @@
<script setup lang="ts">
import DatePicker from '../shared/DatePicker.vue';
import { AddressForm } from 'components/form';
const transportation = defineModel<string>('transportation', { default: '' });
const travelDate = defineModel<string>('travelDate', { default: '' });
const entryCheckpoint = defineModel<string>('entryCheckpoint', { default: '' });
const entryCardNumber = defineModel<string>('entryCardNumber', { default: '' });
const address = defineModel<string>('address');
const street = defineModel('street', { default: '' });
const moo = defineModel('moo', { default: '' });
const soi = defineModel('soi', { default: '' });
const provinceId = defineModel<string | null | undefined>('provinceId');
const districtId = defineModel<string | null | undefined>('districtId');
const subDistrictId = defineModel<string | null | undefined>('subDistrictId');
const homeCode = defineModel<string | null | undefined>('homeCode');
const employmentOffice = defineModel<string | null | undefined>(
'employmentOffice',
);
defineProps<{
prefixId?: string;
outlined?: boolean;
readonly?: boolean;
}>();
</script>
<template>
<div class="row q-mb-sm" style="gap: 10px">
<div class="col-12 text-subtitle1 text-weight-bold">
<p>Document Properties</p>
</div>
<div class="col-12 row q-col-gutter-sm">
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6"
:label="$t('form.tm6.transportation')"
for="input-citizen-id"
v-model="transportation"
/>
<DatePicker
:label="$t('form.tm6.travelDate')"
v-model="travelDate"
class="col-4"
:id="`${prefixId}-input-birth-date`"
:readonly="readonly"
clearable
/>
</div>
<AddressForm
prefixId="form-tm6"
hide-icon
hide-title
hide-input-en
dense
v-model:homeCode="homeCode"
v-model:employmentOffice="employmentOffice"
v-model:address="address"
v-model:street="street"
v-model:moo="moo"
v-model:soi="soi"
v-model:province-id="provinceId"
v-model:district-id="districtId"
v-model:sub-district-id="subDistrictId"
/>
<div class="col-12 row q-col-gutter-sm">
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col"
:label="$t('form.tm6.entryCheckpoint')"
for="input-citizen-id"
v-model="entryCheckpoint"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col"
:label="$t('form.tm6.entryCardNumber')"
for="input-citizen-id"
v-model="entryCardNumber"
/>
</div>
</div>
</template>
<style lang="scss" scoped></style>

View file

@ -0,0 +1,233 @@
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import useOptionStore from 'stores/options';
import { selectFilterOptionRefMod } from 'stores/utils';
import DatePicker from '../shared/DatePicker.vue';
import { QSelect } from 'quasar';
import { AddressForm } from 'components/form';
const optionStore = useOptionStore();
const registrationNumber = defineModel<string>('registrationNumber', {
default: '',
});
const requestAt = defineModel<string>('requestAt', { default: '' });
const namePrefix = defineModel<string | null>('namePrefix');
const firstName = defineModel<string>('firstName', { default: '' });
const lastName = defineModel<string>('lastName', { default: '' });
const businessRegistrationDate = defineModel<string>(
'businessRegistrationDate',
{ default: '' },
);
const visaPlace = defineModel<string>('visaPlace', { default: '' });
const businessType = defineModel<string>('businessType', { default: '' });
const businessName = defineModel<string>('businessName', { default: '' });
const romanCharacters = defineModel<string>('romanCharacters', { default: '' });
const address = defineModel<string>('address');
const street = defineModel('street', { default: '' });
const moo = defineModel('moo', { default: '' });
const soi = defineModel('soi', { default: '' });
const provinceId = defineModel<string | null | undefined>('provinceId');
const districtId = defineModel<string | null | undefined>('districtId');
const subDistrictId = defineModel<string | null | undefined>('subDistrictId');
const homeCode = defineModel<string | null | undefined>('homeCode');
const employmentOffice = defineModel<string | null | undefined>(
'employmentOffice',
);
const prefixNameOptions = ref<Record<string, unknown>[]>([]);
let prefixNameFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
defineProps<{
prefixId?: string;
outlined?: boolean;
readonly?: boolean;
customerType?: 'CORP' | 'PERS';
}>();
onMounted(() => {
prefixNameFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.prefix),
prefixNameOptions,
'label',
);
});
watch(
() => optionStore.globalOption,
() => {
prefixNameFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.prefix),
prefixNameOptions,
'label',
);
},
);
</script>
<template>
<div class="row q-mb-sm" style="gap: 10px">
<div class="col-12 text-subtitle1 text-weight-bold">
<p>Document Properties</p>
</div>
<div class="col-12 row q-col-gutter-sm">
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6"
:label="$t('form.businessRegistration.registrationNumber')"
for="input-citizen-id"
v-model="registrationNumber"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-3"
:label="$t('form.businessRegistration.requestAt')"
for="input-citizen-id"
v-model="requestAt"
/>
</div>
<div class="col-12 row q-col-gutter-sm">
<q-select
outlined
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="value"
hide-dropdown-icon
class="col-2"
dense
:readonly="readonly"
:options="prefixNameOptions"
:for="`${prefixId}-select-prefix-name`"
:label="$t('form.prefixName')"
@filter="prefixNameFilter"
:model-value="readonly ? namePrefix || '-' : namePrefix"
@update:model-value="
(v) => {
typeof v === 'string' ? (namePrefix = v) : '';
}
"
@clear="namePrefix = ''"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-5"
:label="$t('customer.form.firstName')"
for="input-first-name"
v-model="firstName"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-5"
:label="$t('customer.form.lastName')"
for="input-last-name"
v-model="lastName"
/>
<DatePicker
:label="$t('form.businessRegistration.businessRegistration')"
v-model="businessRegistrationDate"
class="col-3"
:id="`${prefixId}-input-birth-date`"
:readonly="readonly"
clearable
/>
<DatePicker
:label="$t('customerEmployee.form.visaPlace')"
v-model="visaPlace"
class="col-3"
:id="`${prefixId}-input-birth-date`"
:readonly="readonly"
clearable
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6"
:label="$t('form.businessRegistration.businessType')"
for="input-last-name"
v-model="businessType"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12"
:label="$t('form.businessRegistration.businessName')"
for="input-last-name"
v-model="businessName"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12"
:label="$t('form.businessRegistration.romanCharacters')"
for="input-last-name"
v-model="romanCharacters"
/>
</div>
<AddressForm
prefixId="form-tm6"
hide-icon
hide-title
hide-input-en
dense
v-model:homeCode="homeCode"
v-model:employmentOffice="employmentOffice"
v-model:address="address"
v-model:street="street"
v-model:moo="moo"
v-model:soi="soi"
v-model:province-id="provinceId"
v-model:district-id="districtId"
v-model:sub-district-id="subDistrictId"
/>
</div>
</template>
<style lang="scss" scoped></style>

View file

@ -1,23 +1,25 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { SaveButton, UndoButton, CloseButton } from 'components/button';
import { DeleteButton } from 'components/button';
import { dialog } from 'stores/utils';
import { useI18n } from 'vue-i18n';
import { VuePDF, usePDF } from '@tato30/vue-pdf';
const { t } = useI18n();
const currentFileSelected = ref<string>('');
const file = defineModel<
const obj = defineModel<
{
name?: string;
group?: string;
url?: string;
file?: File;
}[]
>('file', {
>({
default: [],
});
const currentFile = computed(() => file.value.at(currentIndex.value));
const currentFile = computed(() => obj.value.at(currentIndex.value));
const statusOcr = defineModel<boolean>('statusOcr', { default: false });
const currentMode = ref<string>('');
const currentIndex = ref(0);
@ -33,8 +35,10 @@ const props = withDefaults(
readonly?: boolean;
dropdownList?: { label: string; value: string }[];
hideAction?: boolean;
autoSave?: boolean;
}>(),
{
autoSave: false,
treeFile: () => [],
},
);
@ -51,52 +55,82 @@ const inputFile = (() => {
return _element;
})();
function change(e: Event) {
async function change(e: Event) {
const _element = e.target as HTMLInputElement | null;
const _file = _element?.files?.[0];
currentIndex.value = file.value.length;
if (_file) {
currentIndex.value = file.length + 1;
if (!obj.value[currentIndex.value]) {
obj.value = [
...obj.value,
{
name: _file.name,
file: _file,
},
];
currentIndex.value = obj.value.length;
}
const reader = new FileReader();
reader.readAsDataURL(_file);
reader.onload = () => {
if (file.value[currentIndex.value]) {
file.value[currentIndex.value].url = reader.result as string;
if (obj.value[currentIndex.value]) {
obj.value[currentIndex.value].url = reader.result as string;
currentFileSelected.value = _file.name;
}
};
if (_file && file.value[currentIndex.value]) {
file.value[currentIndex.value].file = _file;
file.value[currentIndex.value].group =
props.dropdownList?.[currentIndexDropdownList.value].value;
} else {
const newName =
props.dropdownList?.[currentIndexDropdownList.value].value +
'-' +
_file.name;
file.value.push({
name: newName,
group: props.dropdownList?.[currentIndexDropdownList.value].value,
file: _file,
});
currentFileSelected.value = newName;
if (!!props.autoSave) {
emit(
'save',
props.dropdownList?.[currentIndexDropdownList.value].value || '',
inputFile?.files?.[0],
);
}
statusOcr.value = true;
emit(
'sendOcr',
props.dropdownList?.[currentIndexDropdownList.value].value || '',
inputFile?.files?.[0],
);
}
}
// function change(e: Event) {
// const _element = e.target as HTMLInputElement | null;
// const _file = _element?.files?.[0];
// currentIndex.value = obj.value.length;
// if (_file) {
// currentIndex.value = obj.value.length + 1;
// const reader = new FileReader();
// reader.readAsDataURL(_file);
// reader.onload = () => {
// if (obj.value[currentIndex.value]) {
// obj.value[currentIndex.value].url = reader.result as string;
// console.log('asd');
// }
// };
// if (_file && obj.value[currentIndex.value]) {
// obj.value[currentIndex.value].file = _file;
// obj.value[currentIndex.value].group =
// props.dropdownList?.[currentIndexDropdownList.value].value;
// } else {
// obj.value.push({
// name: _file.name,
// group: props.dropdownList?.[currentIndexDropdownList.value].value,
// file: _file,
// });
// }
// if (!!props.autoSave) {
// emit(
// 'save',
// props.dropdownList?.[currentIndexDropdownList.value].value || '',
// inputFile?.files?.[0],
// );
// } else {
// }
// }
// }
watch(currentFileSelected, () => {
file.value.findIndex((v, i) => {
obj.value.findIndex((v, i) => {
if (v.name?.includes(currentFileSelected.value)) {
currentIndex.value = i;
@ -112,45 +146,99 @@ watch(currentFileSelected, () => {
const emit = defineEmits<{
(e: 'sendOcr', dropdown: string, file?: File): void;
(e: 'save', group: string, file?: File): void;
(e: 'save', group: string, file?: File): void;
(e: 'deleteFile', filename: string): void;
}>();
const { pdf, pages } = usePDF(computed(() => currentFile.value?.url));
function deleteFileOfBranch(filename: string, index: number) {
dialog({
color: 'negative',
icon: 'mdi-alert',
title: t('dialog.title.confirmDelete'),
actionText: t('general.delete'),
persistent: true,
message: t('dialog.message.confirmDelete'),
action: async () => {
if (!!props.autoSave) {
emit('deleteFile', filename);
} else {
obj.value.splice(index, 1);
}
},
cancel: () => {},
});
}
</script>
<template>
<div class="full-width row no-wrap wrapper">
<div class="col full-height column no-wrap">
<div class="full-width row no-wrap wrapper" style="height: 250px">
<div class="col-3 full-height column no-wrap">
<div class="q-pa-sm text-center bordered" style="height: 50px">
<q-btn-dropdown
:disable="readonly"
icon="mdi-upload"
color="info"
label="อัปโหลดเอกสาร"
>
<q-list v-for="(v, i) in dropdownList" :key="v.value">
<q-item
clickable
v-close-popup
@click="
() => {
currentIndexDropdownList = i;
browse();
}
"
>
<q-item-section>
<q-item-label>{{ $t(v.label) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
:label="$t('general.uploadFile')"
@click="
() => {
currentIndex = obj.length;
browse();
}
"
></q-btn-dropdown>
</div>
<div class="bordered-l bordered-b q-pa-sm col full-height scroll">
<q-tree
:nodes="treeFile || []"
<div class="bordered-l bordered-b q-pa-sm full-height scroll">
<q-item
clickable
v-for="(v, i) in obj"
:key="v.name"
dense
type="button"
class="no-padding items-center rounded"
active-class="menu-active"
:active="currentFileSelected === v.name"
@click="
async () => {
currentFileSelected = v.name || '';
}
"
>
<div class="full-width row items-center justify-between">
<div class="ellipsis col-8">
<q-tooltip>{{ v.name }}</q-tooltip>
{{ v.name }}
</div>
<DeleteButton
iconOnly
@click.stop="
() => {
deleteFileOfBranch(v.name || '', i);
}
"
/>
</div>
</q-item>
<!-- <q-tree
:nodes="
Object.values(
obj.reduce<
Record<string, { label: string; file: { label: string }[] }>
>((a, b) => {
if (b.name && !a[b.name]) {
a[b.name] = {
label: b.name,
file: [],
};
}
return a;
}, {}) || {},
) || []
"
node-key="label"
label-key="label"
children-key="file"
@ -159,15 +247,24 @@ const { pdf, pages } = usePDF(computed(() => currentFile.value?.url));
default-expand-all
>
<template v-slot:default-header="prop">
<div class="ellipsis">
<q-tooltip>{{ prop.node.label }}</q-tooltip>
{{ prop.node.label }}
<div class="full-width row items-center justify-between">
<div class="col-8 ellipsis">
<q-tooltip>{{ prop.node.label }}</q-tooltip>
{{ prop.node.label }}
</div>
<DeleteButton
iconOnly
@click.stop="
() => {
deleteFileOfBranch(prop.node.label);
}
"
/>
</div>
</template>
</q-tree>
</q-tree> -->
</div>
</div>
<div class="col full-height column no-wrap">
<div
class="bordered row items-center justify-evenly q-pa-sm no-wrap"
@ -222,90 +319,23 @@ const { pdf, pages } = usePDF(computed(() => currentFile.value?.url));
<div
class="flex flex-center surface-2 bordered-l bordered-r bordered-b full-height scroll"
>
<template v-if="statusOcr">
<q-spinner color="primary" size="3em" :thickness="2" />
</template>
<template v-else>
<VuePDF
v-if="
currentFile?.url?.split('?').at(0)?.endsWith('.pdf') ||
currentFile?.file?.type === 'application/pdf'
"
class="q-py-md"
:pdf="pdf"
:page="page"
:scale="scale"
/>
<q-img v-else class="q-py-md full-width" :src="currentFile?.url" />
</template>
</div>
</div>
<div class="col-5 full-height column no-wrap">
<div
class="bordered row items-center justify-between q-pa-sm"
style="height: 50px"
>
{{ $t(currentMode) }}
<div class="row" v-if="!hideAction">
<UndoButton icon-only type="button" />
<SaveButton
icon-only
type="button"
@click="
$emit(
'save',
dropdownList?.[currentIndexDropdownList].value || '',
inputFile?.files?.[0],
)
"
/>
</div>
</div>
<div class="q-pa-sm bordered-r bordered-b full-height col scroll">
<slot name="form" :mode="currentMode.split('.').at(-1)" />
<div class="row items-center">
{{ currentFileSelected }}
<CloseButton
icon-only
v-if="!readonly && !!currentFileSelected"
type="button"
class="q-ml-sm"
@click="
() => {
const tempValue = treeFile.find(
(v: any) => v.label === $t(currentMode),
);
if (!tempValue) return;
const idx = tempValue.file?.findIndex(
(v: any) => v.label === currentFileSelected,
);
dialog({
color: 'negative',
icon: 'mdi-alert',
title: $t('dialog.title.confirmDelete'),
actionText: $t('general.delete'),
persistent: true,
message: $t('dialog.message.confirmDelete'),
action: async () => {
$emit('deleteFile', currentFileSelected);
currentFileSelected = tempValue.file?.[idx - 1].label || '';
},
cancel: () => {},
});
}
"
/>
</div>
<VuePDF
style="height: 100%"
v-if="
currentFile?.url?.split('?').at(0)?.endsWith('.pdf') ||
currentFile?.file?.type === 'application/pdf'
"
class="q-py-md"
:pdf="pdf"
:page="page"
:scale="scale"
/>
<q-img
v-else
class="q-py-md full-width"
:src="currentFile?.url"
style="height: 220px; max-width: 300px"
/>
</div>
</div>
</div>

View file

@ -0,0 +1,387 @@
<script setup lang="ts">
import { QTableProps } from 'quasar';
import { ref, toRaw, onMounted } from 'vue';
import { dialog } from 'stores/utils';
import { useI18n } from 'vue-i18n';
import TableComponents from 'src/components/TableComponents.vue';
import ShowAttachent from 'src/components/ShowAttachent.vue';
import DialogForm from 'components/DialogForm.vue';
const isEdit = ref(false);
const { t } = useI18n();
const obj = defineModel<
{
_meta?: Record<string, any>;
name?: string;
group?: string;
url?: string;
file?: File;
}[]
>({
default: [],
});
const modalDialog = ref<boolean>(false);
const splitAttachment = ref<number>(50);
const currentIndex = ref<number>(-1);
const statusOcr = ref<boolean>(false);
const props = defineProps<{
ocr?: (
group: any,
file: File,
) => void | Promise<{
status: boolean;
group: string;
meta: { name: string; value: string }[];
}>;
getFileList?: (group: any) => Promise<typeof obj.value>;
deleteItem?: (obj: any) => void | Promise<boolean>;
download?: (obj: any) => void;
save?: (
group: any,
meta: any,
file: File | undefined,
) => void | Promise<boolean>;
autoSave?: boolean;
readonly?: boolean;
hideAction?: boolean;
columns: QTableProps['columns'];
menu?: { label: string; value: string; _meta?: Record<string, any> }[];
}>();
async function triggerDelete(item: any) {
dialog({
color: 'negative',
icon: 'mdi-alert',
title: t('dialog.title.confirmDelete'),
actionText: t('general.delete'),
message: t('dialog.message.confirmDelete'),
action: async () => {
if (
!props.autoSave ||
!obj.value[currentIndex.value]?._meta?.hasOwnProperty('id')
) {
obj.value.splice(currentIndex.value, 1);
} else {
await props.deleteItem?.(item);
await fileList();
}
},
cancel: () => {},
});
}
function browse() {
inputFile?.click();
}
const inputFile = (() => {
const _element = document.createElement('input');
_element.type = 'file';
_element.accept = 'image/jpeg,image/png';
_element.addEventListener('change', change);
return _element;
})();
const selectedMenu = ref<NonNullable<typeof props.menu>[number]>();
async function change(e: Event) {
const _element = e.target as HTMLInputElement | null;
const _file = _element?.files?.[0];
if (_file) {
if (!obj.value[currentIndex.value] && selectedMenu.value) {
currentIndex.value = obj.value.length;
obj.value = [
...obj.value,
{
_meta: structuredClone(toRaw(selectedMenu.value)._meta || {}),
group: selectedMenu.value?.value,
file: _file,
},
];
}
const reader = new FileReader();
reader.readAsDataURL(_file);
reader.onload = () => {
if (obj.value[currentIndex.value]) {
obj.value[currentIndex.value].url = reader.result as string;
}
};
statusOcr.value = true;
const resOcr = await props.ocr?.(selectedMenu.value?.value, _file);
if (resOcr?.status) {
modalDialog.value = true;
const map = resOcr.meta.reduce<Record<string, string>>((a, c) => {
a[c.name] = c.value;
return a;
}, {});
if (resOcr.group === 'citizen') {
obj.value[currentIndex.value]._meta = {
citizenId: map['citizen_id'],
firstName: map['firstname'],
lastName: map['lastname'],
firstNameEN: map['firstname_en'],
lastNameEN: map['lastname_en'],
birthDate: map['birth_date'],
religion: map['religion'],
issueDate: map['issue_date'],
expireDate: map['expire_date'],
};
}
if (resOcr.group === 'passport') {
obj.value[currentIndex.value]._meta = {
type: map['type'],
number: map['passport_no'],
issueDate: map['issue_date'],
expireDate: map['expire_date'],
};
}
if (resOcr.group === 'visa') {
obj.value[currentIndex.value]._meta = {
type: map['visa_type'],
number: map['visa_no'],
issueDate: map['valid_until'],
expireDate: map['expire_date'],
issuePlace: map['issue_place'],
};
}
statusOcr.value = false;
}
}
}
async function fileList() {
if (props.getFileList) {
const res = await props.getFileList(selectedMenu.value?.value);
if (res && Array.isArray(res)) {
obj.value = [...res];
}
}
}
defineEmits<{
(e: 'submit', obj: any): void;
}>();
</script>
<template>
<div
class="relative-position full-width row no-wrap wrapper"
style="height: 250px"
>
<div class="col-3 full-height column no-wrap">
<div class="q-pa-sm text-center bordered" style="height: 50px">
<q-btn-dropdown
:disable="readonly"
icon="mdi-upload"
color="info"
:label="$t('general.uploadFile')"
>
<q-list v-for="(v, i) in menu" :key="v.value">
<q-item
clickable
v-close-popup
@click="
() => {
isEdit = true;
selectedMenu = menu?.[i];
currentIndex = obj.length;
browse();
}
"
>
<q-item-section>
<q-item-label>{{ $t(v.label) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
<div class="bordered-l bordered-b q-pa-sm col full-height scroll">
<template v-if="menu != undefined">
<q-item
clickable
v-for="v in menu"
:key="v.value"
dense
type="button"
class="no-padding items-center rounded full-width"
active-class="menu-active"
:active="selectedMenu?.value === v.value"
@click="
async () => {
selectedMenu = v;
if (autoSave) {
fileList();
}
}
"
>
<span class="q-px-md ellipsis q-pa-sm" style="font-size: 16px">
{{ $t(v.label) }}
</span>
</q-item>
</template>
</div>
</div>
<div
v-if="obj"
class="bordered col surface-2 column justify-center items-center no-wrap scroll"
>
<slot name="content">
<template v-if="columns !== undefined">
<div class="full-height full-width q-pa-md">
<TableComponents
buttomDownload
@download="
(index) => {
download?.(obj[index]);
}
"
@delete="
async (index) => {
currentIndex = index;
await triggerDelete(obj[index]);
}
"
@view="
(index) => {
isEdit = false;
currentIndex = index;
modalDialog = true;
}
"
@edit="
(index) => {
isEdit = true;
currentIndex = index;
modalDialog = true;
}
"
:rows="
obj
.filter((v) => {
if (!autoSave && v.group !== selectedMenu?.value) {
return false;
}
return true;
})
.map((v, index) => {
return {
id: v._meta?.id,
branchNo: index + 1,
attachmentName: v.file?.name || v.name,
uploadDate: '',
};
})
"
:columns="columns"
></TableComponents>
</div>
</template>
</slot>
</div>
<q-inner-loading
:showing="statusOcr"
label="Please wait..."
label-class="text-teal"
label-style="font-size: 1.1em"
/>
</div>
<DialogForm
edit
hideDelete
:is-edit="isEdit"
style="position: absolute"
height="100vh"
weight="90%"
v-model:modal="modalDialog"
title="ดูตัวอย่าง"
hideCloseEvent
v-if="obj.length > 0"
:undo="
() => {
isEdit = false;
}
"
:edit-data="
() => {
isEdit = !isEdit;
}
"
:close="
() => {
if (!autoSave || !obj[currentIndex]?._meta?.hasOwnProperty('id')) {
obj.splice(currentIndex, 1);
}
modalDialog = false;
}
"
:submit="
async () => {
modalDialog = false;
if (autoSave === true) {
const statusSave = await save?.(
obj[currentIndex].group,
obj[currentIndex]._meta,
obj[currentIndex].file,
);
if (statusSave) {
fileList();
}
}
}
"
>
<q-splitter class="full-height" v-model="splitAttachment">
<template v-slot:before>
<div class="full-height">
<ShowAttachent
v-if="obj[currentIndex]"
:url="obj[currentIndex].url || ''"
:file="obj[currentIndex].file"
/>
</div>
</template>
<template v-slot:after>
<div class="q-pa-md">
<slot
v-if="obj[currentIndex]"
name="form"
:mode="obj[currentIndex].group"
:meta="obj[currentIndex]._meta"
:isEdit="isEdit"
/>
</div>
</template>
</q-splitter>
</DialogForm>
</template>
<style lang="scss">
.wrapper > * {
height: 300px;
}
.menu-active {
background-color: hsla(var(--info-bg) / 0.1);
color: hsl(var(--info-bg));
}
</style>

View file

@ -1,2 +1,7 @@
export { default as UploadFile } from './UploadFile.vue';
export { default as UploadFileGroup } from './UploadFileGroup.vue';
export { default as FormCitizen } from './FormCitizen.vue';
export { default as FormTm6 } from './FormTm6.vue';
export { default as CorpFormBusinessRegistration } from './CorpFormBusinessRegistration.vue';
export { default as PersFormBusinessRegistration } from './PersFormBusinessRegistration.vue';
export { default as noticeJobEmployment } from './noticeJobEmployment.vue';

View file

@ -0,0 +1,72 @@
<script setup lang="ts">
import DatePicker from '../shared/DatePicker.vue';
const permitNumber = defineModel<string>('permitNumber', { default: '' });
const jobDescription = defineModel<string>('jobDescription', { default: '' });
const workplace = defineModel<string>('workplace', { default: '' });
const dateOfHire = defineModel<Date>('dateOfHire');
defineProps<{
prefixId?: string;
outlined?: boolean;
readonly?: boolean;
}>();
</script>
<template>
<div class="row q-mb-sm" style="gap: 10px">
<div class="col-12 text-subtitle1 text-weight-bold">
<p>Document Properties</p>
</div>
<div class="col-12 row q-col-gutter-sm">
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6"
:label="$t('form.noticeJobEmployment.permitNumber')"
for="input-citizen-id"
v-model="permitNumber"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6"
:label="$t('form.noticeJobEmployment.jobDescription')"
for="input-citizen-id"
v-model="jobDescription"
/>
</div>
<div class="col-12 row q-col-gutter-sm">
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6"
:label="$t('form.noticeJobEmployment.workplace')"
for="input-citizen-id"
v-model="workplace"
/>
</div>
<div class="col-12 row q-col-gutter-sm">
<DatePicker
:label="$t('form.noticeJobEmployment.dateOfHire')"
v-model="dateOfHire"
class="col-4"
:id="`${prefixId}-input-birth-date`"
:readonly="readonly"
clearable
/>
</div>
</div>
</template>
<style lang="scss" scoped></style>

View file

@ -19,8 +19,8 @@ html {
--surface-tab: var(--gray-2);
--text-mute: var(--stone-5-hsl);
--text-mute-2: var(--stone-7-hsl);
--text-mute: var(--stone-7-hsl);
--text-mute-2: var(--stone-8-hsl);
--info-fg: 0 0% 100%;
--info-bg: var(--blue-6-hsl);

View file

@ -15,6 +15,7 @@ $info: var(--blue-6);
$warning: #ffc224;
$disabled: var(--stone-4);
$input-border: var(--gray-2);
$separator-color: var(--border-color);
$separator-dark-color: var(--border-color);
@ -94,6 +95,21 @@ div.fullscreen.q-drawer__backdrop {
color: $disabled;
}
.input-border,
.input-border *,
[input-border],
[input-border] * :not(:deep(.q-checkbox)) {
color: hsl(var(--text-mute)) !important;
}
.bg-input-border {
background: $input-border;
}
.text-input-border {
color: $input-border;
}
.q-field--outlined.q-field--readonly .q-field__control:before {
border-color: transparent;
}
@ -133,3 +149,11 @@ div.fullscreen.q-drawer__backdrop {
.q-tree__node-header:before {
top: -32px !important;
}
.q-tree.q-tree--standard.text-transparent {
color: hsl(var(--text-mute)) !important;
}
.q-field__control {
align-items: center;
}

View file

@ -44,17 +44,42 @@ export default {
recordPerPage: 'Records per page',
recordsPage: 'Showing {resultcurrentPage} out of {total} records',
day: 'Days',
select: 'Select',
select: 'Select {msg}',
selectAll: 'Select All',
additional: 'Additional',
editImage: 'Edit Image',
upload: 'Upload',
upload: 'Upload{msg}',
baseOnDevice: 'Base on Device',
clickToCreate: 'Click to create',
age: 'Age',
nationality: 'Nationalality',
times: 'No. {number}',
uploadFile: 'Upload File',
typeBranch: 'Branch Type',
branchStatus: 'Branch Status',
success: 'Success',
taxNo: 'Legal Person',
contactName: 'Contact Name',
image: 'Image of ',
apply: 'Apply',
licenseNumber: 'License number',
dateOfIssue: 'Date of issue',
expirationDate: 'Expiration date',
document: 'Document',
uploadDate: 'Upload Date',
information: '{msg} Information',
itemNo: '{msg} No.',
example: 'Example',
view: 'View {msg}',
attachment: 'Attachment',
about: 'About',
total: 'Total',
discount: 'Discount',
totalAfterDiscount: 'Total after discount',
totalVatExcluded: 'Tax exemption amount',
totalVatIncluded: 'Taxable amount',
vat: 'VAT {msg}',
totalAmount: 'Total amount',
},
menu: {
@ -102,6 +127,27 @@ export default {
},
form: {
tm6: {
transportation: 'Flight/Vehicle',
travelDate: 'Date of Entry',
entryCheckpoint: 'Point of Entry',
entryCardNumber: 'Entry Card Number',
},
businessRegistration: {
registrationNumber: 'Registration Number',
requestAt: 'Request At',
businessRegistration: 'Business Registration',
businessType: 'Business Type',
businessName: 'Name Used for Business',
romanCharacters: 'Roman Characters',
},
noticeJobEmployment: {
permitNumber: 'Work Permit Number',
jobDescription: 'Job Description',
workplace: 'Workplace',
dateOfHire: 'Date of Hire',
},
title: {
info: '{name}',
create: 'Create {name}',
@ -116,8 +162,13 @@ export default {
telephone: 'Telephone',
gender: 'Gender',
address: 'Address {suffix}',
addressNo: 'Address No.',
moo: 'Moo',
soi: 'Soi',
road: 'Road',
province: 'Province',
district: 'District',
fullAddress: 'Full Address',
subDistrict: 'Sub-district',
zipCode: 'Zip Code',
prefixName: 'Prefix',
@ -135,7 +186,10 @@ export default {
please: 'Please enter {msg} correct information.',
invalid: 'Invalid value.',
invalidCustomeMessage: 'Invalid value. {msg}',
letterOnly: 'Only letters are allowed',
letterAndNumOnly: 'Only letters and number are allowed',
numOnly: 'Only number are allowed',
requireLength: 'Please enter {msg} character',
},
warning: {
title: 'Warning {msg}',
@ -162,14 +216,19 @@ export default {
branch: {
office: 'Office',
allBranch: 'All Branch',
card: {
office: 'Office',
orderNumber: 'No.',
branchLabelName: 'Name',
branchLabelAddress: 'Address',
branchLabelTel: 'Telephone',
branchLabelType: 'Type',
branchVirtual: 'Service Point',
branchLabel: 'Branch',
branchHQLabel: 'Headoffice',
taxNo: 'Legal Person',
contactName: 'Contact Name',
},
page: {
captionManage: 'Manage',
@ -247,6 +306,9 @@ export default {
checkpoint: 'Checkpoint',
checkpointEN: 'Checkpoint (EN)',
attachment: 'Attachment Document',
citizenId: 'Citizen ID',
citizenIssue: 'Citizen Issue',
citizenExpire: 'Citizen Expire',
},
},
customer: {
@ -283,8 +345,8 @@ export default {
issueDate: 'Issue Date',
passportExpiryDate: 'Passport Expiry Date',
firstName: 'First Name in Thai',
lastName: 'Last Name in Thai',
firstName: 'First Name ',
lastName: 'Last Name ',
firstNameEN: 'First Name in English',
lastNameEN: 'Last Name in English',
@ -318,6 +380,7 @@ export default {
},
headQuarters: {
title: 'Headoffice',
telephoneNo: 'Headoffice Telephone',
},
businessType: 'Business Type',
businessTypeEN: 'Business Type (EN)',
@ -327,17 +390,24 @@ export default {
payDay: 'Pay Day',
payRate: 'Pay Rate',
salesPerson: 'Sales Person',
employerName: 'Employer Name',
employmentOffice: 'Employment Office',
homeCode: 'Address Identification (11 characters)',
agent: 'Agent',
},
table: {
orderNumber: 'No.',
fullname: 'Full Name',
titleName: 'Name',
businessTypePure: 'Business Type',
jobPosition: 'Job Position',
address: 'Address',
workPlace: 'Workplace',
contactName: 'Contact Name',
contactPhone: 'Contact Phone',
totalEmployee: 'Total Employee',
officeTel: 'Headoffice Telephone',
},
},
@ -437,6 +507,8 @@ export default {
business: 'Business',
contact: 'Contact',
attachment: 'Upload Document',
remark: 'remark',
authorized: 'Authorized',
},
form: {
title: 'Branch',
@ -471,42 +543,82 @@ export default {
name: 'Products and Services Types Name',
},
service: {
title: 'Services',
title: 'Type',
totalWork: 'Total Work',
code: 'Services Code',
name: 'Services Name',
code: 'Type Code',
name: 'Type Name',
work: 'Work',
workName: 'Work Name',
showTotalPrice: 'Show Total Price',
addTitle: 'Add Services',
addTitle: 'Add Type',
registeredBranch: 'Registered Branch',
information: 'Services Information',
information: 'Type Information',
workInformation: 'Work Information',
serviceProperties: 'Services Properties',
serviceProperties: 'Type Properties',
propertiesName: 'Properties Name',
properties: 'Properties',
noProperties: 'No Properties',
propertiesInWork: 'Properties in work',
productInWork: 'Products in work',
totalProductWork: 'Total products of work',
productInWork: 'Products and Services in work',
totalProductWork: 'Total products and services of work',
list: 'Item',
addWork: 'Add Work',
workAlreadyExist: 'Work already exist',
},
product: {
title: 'Products',
code: 'Products Code',
name: 'Products Name',
title: 'Products and Services',
code: 'Products and Services Code',
name: 'Products and Services Name',
registeredBranch: 'Registered Branch',
noProduct: 'No Products',
allProduct: 'All Products',
addTitle: 'Add Products',
noProduct: 'No Products and Services',
allProduct: 'All Products and Services',
addTitle: 'Add Products and Services',
processingTime: 'Processing Time',
processingTimeDay: 'Processing Time (Days)',
priceInformation: 'Price Information',
salePrice: 'Sale Price',
agentPrice: 'Agent Price',
processingPrice: 'Processing Price',
expenseType: 'Expense Type',
vatIncluded: 'Include VAT',
vatExcluded: 'Exclude VAT',
vat: 'VAT',
},
},
quotation: {
title: 'Quotation',
customerName: 'Customer Name',
actor: 'Actor',
totalPrice: 'Total (Baht)',
receipt: 'Receipt/Tax Invoice',
branch: 'Branch that issues the quotation',
customer: 'Customer',
newCustomer: 'New Customer',
employeeList: 'Employee List',
employee: 'Employee',
workName: 'Work Name',
contactName: 'Contact Name',
documentReceivePoint: 'Document Drop-Off Point"',
dueDate: 'Quotation Due Date',
paymentCondition: 'Payment Terms',
payType: 'Payment Methods',
bank: 'Select Payment Account',
paySplitCount: 'Number of Installments',
payTotal: 'Total {msg}',
summary: 'Total Summary',
periodNo: 'Installment No."',
amount: 'Amount',
payDueDate: 'Pay Due Date',
callDueDate: 'Call Due Date',
type: {
all: 'All',
fullAmountCash: 'Full Amount Cash',
installmentsCash: 'Installments Cash',
fullAmountBill: 'Full Amount Bill',
installmentsBill: 'Installments Bill',
},
},
@ -519,6 +631,7 @@ export default {
confirmLogout: 'Confirm Logout',
},
message: {
beingUse: '"{msg}" is being used.',
incompleteDataEntry: 'Incomplete data entry on {tap} page',
confirmChangeStatusOn: 'Do you want to open?',
confirmChangeStatusOff: 'Do you want to close?',
@ -594,6 +707,7 @@ export default {
validateError: 'Validate Error',
codeMisMatch: 'Code Mismatch',
userExists: 'User already exits.',
crossCompanyNotPermit: 'Cannot move between different headoffice',
},
},
};

View file

@ -44,17 +44,42 @@ export default {
recordPerPage: 'แสดงทีละ',
recordsPage: 'แสดง {resultcurrentPage} รายการจาก {total} รายการ',
day: 'วัน',
select: 'เลือก',
select: 'เลือก{msg}',
selectAll: 'เลือกทั้งหมด',
additional: 'เพิ่มเติม',
editImage: 'แก้ไขรูป',
upload: 'อัปโหลด',
upload: 'อัปโหลด{msg}',
baseOnDevice: 'สีตามอุปกรณ์',
clickToCreate: 'กดเพื่อสร้าง',
age: 'อายุ',
nationality: 'สัญชาติ',
times: 'ครั้งที่ {number}',
uploadFile: 'อัปโหลดไฟล์',
uploadFile: 'อัปโหลดเอกสาร',
typeBranch: 'ประเภทสาขา',
branchStatus: 'สถานะสาขา',
success: 'สำเร็จ',
taxNo: 'ทะเบียนนิติบุคคล',
contactName: 'ติดต่อ',
image: 'รูปภาพ',
apply: 'นำไปใช้',
licenseNumber: 'เลขที่ใบอนุญาต',
dateOfIssue: 'วันที่อนุญาต',
expirationDate: 'วันที่หมดอายุ',
document: 'ชื่อเอกสาร',
uploadDate: 'วันที่อัปโหลด',
information: 'ข้อมูล{msg}',
itemNo: 'เลขที่{msg}',
example: 'ตัวอย่าง',
view: 'ดู{msg}',
attachment: 'เอกสาร',
about: 'เกี่ยวกับ',
total: 'ยอดรวม',
discount: 'ส่วนลด',
totalAfterDiscount: 'จำนวนเงินหลังหักส่วนลด',
totalVatExcluded: 'จำนวนเงินยกเว้นภาษี',
totalVatIncluded: 'จำนวนเงินที่คำนวณภาษี',
vat: 'ภาษีมูลค่าเพิ่ม {msg}',
totalAmount: 'จำนวนเงินรวมทั้งสิ้น',
},
menu: {
@ -102,6 +127,26 @@ export default {
},
form: {
tm6: {
transportation: 'เที่ยวบิน/พาหนะ',
travelDate: 'วันที่เดินทางเข้ามา',
entryCheckpoint: 'จุดผ่านแดนที่เข้าประเทศ',
entryCardNumber: 'หมายเลขบัตรขาเข้า',
},
businessRegistration: {
registrationNumber: 'ทะเบียนเลขที่',
requestAt: 'คำขอที่',
businessRegistration: 'จดทะเบียนพาณิชย์',
businessType: 'ชนิดพาณิชย์',
businessName: 'ชื่อที่ใช้ในการประกอบพาณิชย์',
romanCharacters: 'อักษรโรมัน',
},
noticeJobEmployment: {
permitNumber: 'หมายเลขใบอนุญาตทำงาน ',
jobDescription: 'ลักษณะงาน',
workplace: 'สถานที่ทำงาน',
dateOfHire: 'วันที่จ้าง',
},
title: {
info: '{name}',
create: 'สร้าง {name}',
@ -116,14 +161,19 @@ export default {
gender: 'เพศ',
telephone: 'เบอร์โทรศัพท์',
address: 'ที่อยู่ {suffix}',
addressNo: 'บ้านเลขที่',
moo: 'หมู่',
soi: 'ซอย',
road: 'ถนน',
province: 'จังหวัด',
district: 'อำเภอ',
fullAddress: 'ที่อยู่เต็ม',
subDistrict: 'ตำบล',
zipCode: 'รหัสไปรษณีย์',
prefixName: 'คํานําหน้า',
firstName: 'ชื่อ ภาษาไทย',
firstName: 'ชื่อ ',
firstNameEN: 'ชื่อ ภาษาอังกฤษ',
lastName: 'นามสกุล ภาษาไทย',
lastName: 'นามสกุล ',
lastNameEN: 'นามสกุล ภาษาอังกฤษ',
middleName: 'ชื่อกลาง',
middleNameEN: 'ชื่อกลาง ภาษาอังกฤษ',
@ -135,7 +185,10 @@ export default {
please: 'โปรดใส่ข้อมูล{msg}ให้ถูกต้อง',
invalid: 'ข้อมูลไม่ถูกต้อง',
invalidCustomeMessage: 'ข้อมูลไม่ถูกต้อง {msg}',
letterAndNumOnly: 'โปรดใช้เฉพาะภาษาอังกฤษและตัวเลขเท่านั้น',
letterOnly: 'โปรดใช้เฉพาะตัวอักษรภาษาอังกฤษเท่านั้น',
letterAndNumOnly: 'โปรดใช้เฉพาะตัวอักษรภาษาอังกฤษและตัวเลขเท่านั้น',
numOnly: 'โปรดใช้เฉพาะตัวเลขเท่านั้น',
requireLength: 'กรุณากรอกให้ครบ {msg} หลัก',
},
warning: {
title: 'แจ้งเตือน {msg}',
@ -162,14 +215,19 @@ export default {
branch: {
office: 'สำนักงาน',
allBranch: 'สาขาทั้งหมด',
card: {
orderNumber: 'เลขที่',
orderNumber: 'ลำดับที่',
branchLabelName: 'ชื่อ',
office: 'สำนักงาน',
branchLabelAddress: 'ที่อยู่',
branchLabelTel: 'เบอร์โทรศัพท์',
branchLabelTel: 'เบอร์โทรสำนักงาน',
branchLabelType: 'ประเภท',
branchLabel: 'สาขา',
branchVirtual: 'จุดรับบริการ',
branchHQLabel: 'สำนักงานใหญ่',
taxNo: 'ทะเบียนนิติบุคคล',
contactName: 'ติดต่อ',
},
page: {
captionManage: 'จัดการ',
@ -230,8 +288,8 @@ export default {
userType: 'ประเภทผู้ใช้งาน',
userRole: 'สิทธิ์ผู้ใช้งาน',
prefixName: 'คํานําหน้า',
firstName: 'ชื่อ ภาษาไทย',
lastName: 'นามสกุล ภาษาไทย',
firstName: 'ชื่อ ',
lastName: 'นามสกุล ',
firstNameEN: 'ชื่อ ภาษาอังกฤษ',
lastNameEN: 'นามสกุล ภาษาอังกฤษ',
middleName: 'ชื่อกลาง',
@ -247,6 +305,9 @@ export default {
checkpoint: 'ด่าน',
checkpointEN: 'ด่าน ภาษาอังกฤษ',
attachment: 'เอกสารประจำตัว',
citizenId: 'เลขที่บัตรประชาชน',
citizenIssue: 'วันที่ออกบัตร',
citizenExpire: 'วันที่หมดอายุ',
},
},
customer: {
@ -283,8 +344,8 @@ export default {
issueDate: 'วันที่ออกหนังสือ',
passportExpiryDate: 'วันหiมดอายุหนังสือเดินทาง',
firstName: 'ชื่อ ภาษาไทย',
lastName: 'นามสกุล ภาษาไทย',
firstName: 'ชื่อ ',
lastName: 'นามสกุล ',
firstNameEN: 'ชื่อ ภาษาอังกฤษ',
lastNameEN: 'นามสกุล ภาษาอังกฤษ',
@ -318,26 +379,34 @@ export default {
},
headQuarters: {
title: 'สำนักงานใหญ่',
telephoneNo: 'เบอร์โทรศัพท์สำนักงาน',
},
businessType: 'ประเภทธุรกิจ',
businessTypeEN: 'ประเภทธุรกิจ (ภาษาอังกฤษ)',
businessType: 'ประเภทกิจการ',
businessTypeEN: 'ประเภทกิจการ (ภาษาอังกฤษ)',
jobPosition: 'ตำแหน่งงาน',
jobPositionEN: 'ตำแหน่งงาน (ภาษาอังกฤษ)',
jobDescription: 'รายละเอียดงาน',
payDay: 'วันจ่ายเงินเดือน',
payRate: 'อัตราค่าจ้าง',
payRate: 'อัตราค่าจ้าง/วัน',
salesPerson: 'เจ้าหน้าที่ขาย',
employerName: 'ชื่อนายจ้าง',
employmentOffice: 'สำนักงานจัดหางาน',
homeCode: 'รหัสประจำบ้าน (11 หลัก)',
agent: 'ตัวแทน',
},
table: {
orderNumber: 'ลําดับ',
fullname: 'ชื่อ-นามสกุล',
titleName: 'ชื่อ บริษัท/นิติบุคคล',
businessTypePure: 'ประเภทกิจการ',
jobPosition: 'ตำแหน่งงาน',
address: 'ที่อยู่',
workPlace: 'สถานที่ทํางาน',
contactName: 'ชื่อผู้ติดต่อ',
contactPhone: 'โทรศัพท์ผู้ติดต่อ',
totalEmployee: 'ลูกจ้างทั้งหมด',
officeTel: 'เบอร์โทรสำนักงาน',
},
},
@ -349,7 +418,7 @@ export default {
passport: 'หนังสือเดินทาง',
visa: 'วีซ่า',
healthCheck: 'ตรวจสุขภาพ',
workHistory: 'ประวัติการทำงาน',
workHistory: 'ประวัติการทำงาน',
other: 'อื่นๆ',
family: 'ครอบครัว',
},
@ -431,10 +500,12 @@ export default {
customerBranch: {
tab: {
main: 'เกี่ยวกับ',
address: 'ที่อยู่',
business: 'ธุรกิจ',
contact: 'ติดต่อ',
attachment: 'อัปโหลดเอกสาร',
address: 'ที่อยู่นายจ้าง',
business: 'ข้อมูลธุรกิจ',
contact: 'ข้อมูลติดต่อ',
attachment: 'เอกสาร',
remark: 'หมายเหตุ',
authorized: 'ผู้มีอำนาจลงนาม',
},
form: {
title: 'สาขา',
@ -469,42 +540,82 @@ export default {
name: 'ชื่อสินค้าและบริการ',
},
service: {
title: 'บริการ',
title: 'ประเภท',
totalWork: 'งานทั้งหมด',
code: 'รหัสบริการ',
name: 'ชื่อบริการ',
code: 'รหัสประเภท',
name: 'ชื่อประเภท',
work: 'งาน',
workName: 'ชื่องาน',
showTotalPrice: 'แสดงราคารวม',
addTitle: 'เพิ่มบริการ',
addTitle: 'เพิ่มประเภท',
registeredBranch: 'สาขาที่ลงทะเบียน',
information: 'ข้อมูลบริการ',
information: 'ข้อมูลประเภท',
workInformation: 'ข้อมูลงาน',
serviceProperties: 'คุณสมบัติของบริการ',
serviceProperties: 'คุณสมบัติของประเภท',
propertiesName: 'ชื่อคุณสมบัติ',
properties: 'คุณสมบัติ',
noProperties: 'ยังไม่มีคุณสมบัติ',
propertiesInWork: 'คุณสมบัติภายในงาน',
productInWork: 'สินค้าภายในงาน',
totalProductWork: 'รวมสินค้างาน',
productInWork: 'สินค้าและบริการภายในงาน',
totalProductWork: 'รวมสินค้าและบริการงาน',
list: 'รายการ',
addWork: 'เพิ่มงาน',
workAlreadyExist: 'งานนี้มีอยู่แล้ว',
},
product: {
title: 'สินค้า',
code: 'รหัสสินค้า',
name: 'ชื่อสินค้า',
title: 'สินค้าและบริการ',
code: 'รหัสสินค้าและบริการ',
name: 'ชื่อสินค้าและบริการ',
registeredBranch: 'สาขาที่ลงทะเบียน',
noProduct: 'ยังไม่มีสินค้า',
allProduct: 'สินค้าทั้งหมด',
addTitle: 'เพิ่มสินค้า',
noProduct: 'ยังไม่มีสินค้าและบริการ',
allProduct: 'สินค้าและบริการทั้งหมด',
addTitle: 'เพิ่มสินค้าและบริการ',
processingTime: 'ระยะเวลาดำเนินการ',
processingTimeDay: 'ระยะเวลาดำเนินการ (วัน)',
priceInformation: 'ข้อมูลราคา',
salePrice: 'ราคาขาย',
agentPrice: 'ราคาตัวแทน',
processingPrice: 'ราคาดำเนินการ',
expenseType: 'ประเภทค่าใช้จ่าย',
vatIncluded: 'รวม VAT',
vatExcluded: 'ไม่รวม VAT',
vat: 'คำนวณ VAT',
},
},
quotation: {
title: 'ใบเสนอราคา',
customerName: 'ชื่อลูกค้า',
actor: 'ผู้ที่ทำรายงาน',
totalPrice: 'ยอดรวมสุทธิ(บาท)',
receipt: 'ใบเสร็จ/กำกับภาษี',
branch: 'สาขาที่ออกใบเสนอราคา',
customer: 'ลูกค้า',
newCustomer: 'ลูกค้าใหม่',
employeeList: 'รายชื่อแรงงาน',
employee: 'แรงงาน',
workName: 'ชื่องาน',
contactName: 'ชื่อผู้ติดต่อ',
documentReceivePoint: 'จุดรับเอกสาร',
dueDate: 'วันครบกำหนดใบเสนอราคา',
paymentCondition: 'เงื่อนไขการชำระเงิน',
payType: 'วิธีการชำระเงิน',
bank: 'เลือกบัญชีชำระเงิน',
paySplitCount: 'จำนวนงวด',
payTotal: 'ยอดชำระ {msg}',
summary: 'สรุปยอดทั้งหมด',
periodNo: 'งวดที่',
amount: 'จำนวนเงิน',
payDueDate: 'วันที่กำหนดจ่าย',
callDueDate: 'วันที่ครบกำหนดเรียก',
type: {
all: 'ทั้งหมด',
fullAmountCash: 'เงินสดเต็มจำนวน',
installmentsCash: 'เงินสดแบ่งจ่าย',
fullAmountBill: 'ใบเรียกเก็บเงินเต็มจำนวน',
installmentsBill: 'ใบเรียกเก็บเงินแบ่งจ่าย',
},
},
@ -517,6 +628,7 @@ export default {
confirmLogout: 'ยืนยันการออกจากระบบ',
},
message: {
beingUse: '"{msg}" มีการใช้งานอยู่',
incompleteDataEntry: 'กรอกข้อมูลไม่ครบในหน้า {tap}',
confirmChangeStatusOn: 'คุณต้องการเปิดใช่หรือไม่',
confirmChangeStatusOff: 'คุณต้องการปิดใช่หรือไม่',
@ -535,16 +647,16 @@ export default {
cancel: 'ยกเลิก',
},
backend: {
productGroupNotFound: 'ไม่พบกลุ่มสินค้า',
productGroupInUsed: 'กลุ่มสินค้าที่ใช้งานอยู่',
productNotFound: 'ไม่พบสินค้า',
productInUsed: 'สินค้าใช้งานอยู่',
productTypeNotFound: 'ไม่พบประเภทสินค้า',
productGroupAssociatedBadReq: 'ไม่พบกลุ่มสินค้าที่เกี่ยวข้อง',
productTypeInUsed: 'ประเภทสินค้าใช้งานอยู่',
productGroupBadReq: 'ไม่พบกลุ่มสินค้า',
productGroupNotFound: 'ไม่พบกลุ่มสินค้าและบริการ',
productGroupInUsed: 'กลุ่มสินค้าและบริการที่ใช้งานอยู่',
productNotFound: 'ไม่พบสินค้าและบริการ',
productInUsed: 'สินค้าและบริการใช้งานอยู่',
productTypeNotFound: 'ไม่พบประเภทสินค้าและบริการ',
productGroupAssociatedBadReq: 'ไม่พบกลุ่มสินค้าและบริการที่เกี่ยวข้อง',
productTypeInUsed: 'ประเภทสินค้าและบริการใช้งานอยู่',
productGroupBadReq: 'ไม่พบกลุ่มสินค้าและบริการ',
serviceNotFound: 'ไม่พบบริการ',
someProductBadReq: 'ไม่พบสินค้าบางส่วน',
someProductBadReq: 'ไม่พบสินค้าและบริการบางส่วน',
serviceInUsed: 'บริการใช้งานอยู่',
workNotFound: 'ไม่พบงาน',
workInUsed: 'งานที่ใช้งานอยู่',
@ -589,6 +701,7 @@ export default {
validateError: 'เกิดข้อผิดพลาดจากการตรวจสอบ',
codeMisMatch: 'รหัสไม่ตรงกัน',
userExists: 'ชื่อผู้ใช้นี้มีอยู่ในระบบอยู่แล้ว',
crossCompanyNotPermit: 'ไม่สามารถดำเนินการระหว่างสำนักงานใหญ่อื่นได้',
},
},
};

View file

@ -42,7 +42,8 @@ onMounted(async () => {
role.value.includes('branch_admin') ||
role.value.includes('head_of_admin') ||
role.value.includes('system') ||
role.value.includes('owner')
role.value.includes('owner') ||
role.value.includes('head_of_account')
? false
: true,
},
@ -79,8 +80,8 @@ onMounted(async () => {
{
label: 'menu.quotation',
icon: 'mdi-file-document',
route: '',
disabled: true,
route: '/quotation',
disabled: false,
},
{
label: 'menu.requestList',
@ -181,10 +182,11 @@ function branchSetting() {}
<div id="drawer-menu" class="q-pl-md q-mr-xs">
<q-item
v-for="v in labelMenu.filter((v) => !v.hidden)"
:id="`drawer-${v.label}`"
dense
clickable
class="row items-center q-my-xs q-px-xs q-py-sm"
:key="v.label"
:key="`drawer-${v.label}`"
:disable="!!v.disabled"
:class="{
active: currentPath === v.route,

View file

@ -453,6 +453,7 @@ onMounted(async () => {
<!-- User -->
<ProfileMenu
id="btn-profile-menu"
@logout="doLogout"
@edit-personal-info="console.log('edit')"
@signature="

View file

@ -141,12 +141,17 @@ onMounted(async () => {
filterRole.value = userRoles
.filter(
(role) =>
role !== 'default-roles-' + getRealm() &&
!role.includes('default-roles') &&
role !== 'offline_access' &&
role !== 'uma_authorization',
)
.map((role) =>
role.replace(/_/g, ' ').replace(/^./, (match) => match.toUpperCase()),
role
.replace(/_/g, ' ')
.toLowerCase()
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' '),
);
}
});
@ -332,7 +337,12 @@ onMounted(async () => {
<div class="column col-12">
<q-separator />
<div class="column justify-center">
<q-list :dense="true" v-for="op in options" :key="op.label">
<q-list
:dense="true"
v-for="op in options"
:key="op.label"
:id="op.label"
>
<q-item
v-if="op.label !== 'menu.profile.mode'"
clickable

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@ import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { QTableProps } from 'quasar';
import { baseUrl } from 'src/stores/utils';
import useCustomerStore from 'stores/customer';
import useFlowStore from 'stores/flow';
import useOptionStore from 'stores/options';
@ -16,6 +17,7 @@ import { CustomerBranch, CustomerType } from 'stores/customer/types';
import { columnsEmployee } from './constant';
import { useCustomerBranchForm, useEmployeeForm } from './form';
import EmployerFormAuthorized from './components/employer/EmployerFormAuthorized.vue';
import ButtonAddComponent from 'components/ButtonAddCompoent.vue';
import SideMenu from 'components/SideMenu.vue';
import { DialogFormContainer, DialogHeader } from 'components/dialog';
@ -75,6 +77,8 @@ const prop = withDefaults(
currentCustomerName?: string;
customerType: CustomerType;
countEmployee?: number;
gender: string;
selectedImage: string;
}>(),
{
color: 'green',
@ -248,6 +252,17 @@ watch([customerId, inputSearch, currentStatus], async () => {
style="border-radius: 0 0 40px 40px; position: relative"
>
<q-avatar no-padding size="50px">
<q-img
:src="`${baseUrl}/customer/${customerId}/image/${selectedImage}`"
class="full-height full-width"
>
<template #error>
<q-img
:src="`${customerType === 'CORP' ? `/images/customer-CORP-avartar-male.png` : `/images/customer-PERS-avartar-${gender}.png`}`"
/>
</template>
</q-img>
<img :src="currentCustomerUrlImage ?? '/no-profile.png'" />
</q-avatar>
</q-card-section>
@ -271,7 +286,6 @@ watch([customerId, inputSearch, currentStatus], async () => {
<div class="row items-center justify-end col-12 col-md q-py-sm no-wrap">
<q-input
lazy-rules="ondemand"
outlined
dense
class="col-6"
@ -286,7 +300,6 @@ watch([customerId, inputSearch, currentStatus], async () => {
</q-input>
<q-select
lazy-rules="ondemand"
id="select-status"
for="select-status"
v-model="currentStatus"
@ -422,9 +435,16 @@ watch([customerId, inputSearch, currentStatus], async () => {
<div class="col" style="min-width: fit-content">
<div class="col">
{{
$i18n.locale === 'eng'
? props.row.registerNameEN
: props.row.registerName
customerType === 'CORP'
? $i18n.locale === 'eng'
? props.row.registerNameEN || '-'
: props.row.registerName || '-'
: $i18n.locale === 'eng'
? props.row.firstNameEN +
' ' +
props.row.lastNameEN || '-'
: props.row.firstName + ' ' + props.row.lastName ||
'-'
}}
</div>
<div class="col app-text-muted">
@ -608,6 +628,7 @@ watch([customerId, inputSearch, currentStatus], async () => {
"
@submit="
async () => {
console.log('asasd');
const res = await customerBranchFormStore.submitForm();
if (res) {
@ -707,54 +728,30 @@ watch([customerId, inputSearch, currentStatus], async () => {
</div>
</div>
<EmployerFormAbout
:index="customerBranchFormData.code?.slice(-2) || '00'"
class="q-mb-xl"
:customer-type="customerType"
:customer-name="currentCustomerName"
readonly
v-model:citizen-id="customerBranchFormData.citizenId"
v-model:id="customerBranchFormData.id"
v-model:legal-person-no="customerBranchFormData.legalPersonNo"
v-model:branch-code="customerBranchFormData.code"
v-model:customer-code="customerBranchFormData.code"
v-model:register-company-name="
customerBranchFormData.registerCompanyName
"
v-model:register-name="customerBranchFormData.registerName"
v-model:register-name-en="customerBranchFormData.registerNameEN"
v-model:register-date="customerBranchFormData.registerDate"
v-model:authorized-capital="
customerBranchFormData.authorizedCapital
"
/>
<div 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
size="xs"
class="q-pa-sm rounded q-mr-sm"
color="info"
name="mdi-office-building-outline"
style="background-color: var(--surface-3)"
/>
<span>{{ $t('customerBranch.tab.address') }}</span>
</div>
</div>
<AddressForm
prefix-id="employer-branch"
class="q-mb-xl"
hide-title
dense
:readonly="customerBranchFormState.dialogType === 'info'"
outlined
:title="$t('form.address')"
v-model:address="customerBranchFormData.address"
v-model:addressEN="customerBranchFormData.addressEN"
v-model:province-id="customerBranchFormData.provinceId"
v-model:district-id="customerBranchFormData.districtId"
v-model:sub-district-id="customerBranchFormData.subDistrictId"
:addressTitle="$t('form.address')"
:addressTitleEN="$t('form.address', { suffix: '(EN)' })"
class="q-mb-xl"
:index="
customerBranchFormData.code &&
Number(customerBranchFormData.code.split('-').pop()).toString()
"
:customer-type="customerType"
v-model:citizen-id="customerBranchFormData.citizenId"
v-model:prefixName="customerBranchFormData.namePrefix"
v-model:firstName="customerBranchFormData.firstName"
v-model:lastName="customerBranchFormData.lastName"
v-model:firstNameEN="customerBranchFormData.firstNameEN"
v-model:lastNameEN="customerBranchFormData.lastNameEN"
v-model:gender="customerBranchFormData.gender"
v-model:birthDate="customerBranchFormData.birthDate"
v-model:customerName="customerBranchFormData.customerName"
v-model:legalPersonNo="customerBranchFormData.legalPersonNo"
v-model:branchCode="customerBranchFormData.code"
v-model:registerName="customerBranchFormData.registerName"
v-model:registerNameEN="customerBranchFormData.registerNameEN"
v-model:registerDate="customerBranchFormData.registerDate"
v-model:authorizedCapital="customerBranchFormData.authorizedCapital"
v-model:telephoneNo="customerBranchFormData.telephoneNo"
v-model:codeCustomer="customerBranchFormData.codeCustomer"
/>
<div
class="row q-col-gutter-sm q-mb-sm"
@ -778,15 +775,76 @@ watch([customerId, inputSearch, currentStatus], async () => {
outlined
prefix-id="employer-branch"
:readonly="customerBranchFormState.dialogType === 'info'"
v-model:employment-office="customerBranchFormData.employmentOffice"
v-model:bussiness-type="customerBranchFormData.businessType"
v-model:bussiness-type-en="customerBranchFormData.businessTypeEN"
v-model:job-position="customerBranchFormData.jobPosition"
v-model:job-position-en="customerBranchFormData.jobPositionEN"
v-model:job-description="customerBranchFormData.jobDescription"
v-model:sale-employee="customerBranchFormData.saleEmployee"
v-model:pay-date="customerBranchFormData.payDate"
v-model:pay-date-e-n="customerBranchFormData.payDateEN"
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 class="col-12 text-weight-bold text-body1 row items-center">
<q-icon
flat
size="xs"
class="q-pa-sm rounded q-mr-sm"
color="info"
name="mdi-office-building-outline"
style="background-color: var(--surface-3)"
/>
<span>{{ $t('customerBranch.tab.authorized') }}</span>
</div>
</div>
<EmployerFormAuthorized
class="q-mb-xl"
prefix-id="employer-branch"
:readonly="customerBranchFormState.dialogType === 'info'"
v-model:authorized-name="customerBranchFormData.authorizedName"
v-model:authorized-name-e-n="
customerBranchFormData.authorizedNameEN
"
/>
<div 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
size="xs"
class="q-pa-sm rounded q-mr-sm"
color="info"
name="mdi-office-building-outline"
style="background-color: var(--surface-3)"
/>
<span>{{ $t('customerBranch.tab.address') }}</span>
</div>
</div>
<AddressForm
prefix-id="employer-branch"
class="q-mb-xl"
hide-title
use-employment
dense
:readonly="customerBranchFormState.dialogType === 'info'"
outlined
:title="$t('form.address')"
v-model:homeCode="customerBranchFormData.homeCode"
v-model:employmentOffice="customerBranchFormData.employmentOffice"
v-model:employmentOfficeEN="
customerBranchFormData.employmentOfficeEN
"
v-model:address="customerBranchFormData.address"
v-model:addressEN="customerBranchFormData.addressEN"
v-model:province-id="customerBranchFormData.provinceId"
v-model:district-id="customerBranchFormData.districtId"
v-model:sub-district-id="customerBranchFormData.subDistrictId"
v-model:street="customerBranchFormData.street"
v-model:streetEN="customerBranchFormData.streetEN"
v-model:moo="customerBranchFormData.moo"
v-model:mooEN="customerBranchFormData.mooEN"
v-model:soi="customerBranchFormData.soi"
v-model:soiEN="customerBranchFormData.soiEN"
:addressTitle="$t('form.address')"
:addressTitleEN="$t('form.address', { suffix: '(EN)' })"
/>
<div class="row q-col-gutter-sm q-mb-sm" id="employer-branch-contact">
<div class="col-12 text-weight-bold text-body1 row items-center">
@ -804,8 +862,11 @@ watch([customerId, inputSearch, currentStatus], async () => {
<EmployerFormContact
class="q-mb-lg"
:readonly="customerBranchFormState.dialogType === 'info'"
v-model:contactName="customerBranchFormData.contactName"
v-model:email="customerBranchFormData.email"
v-model:telephone="customerBranchFormData.telephoneNo"
v-model:contactTel="customerBranchFormData.contactTel"
v-model:officeTel="customerBranchFormData.officeTel"
v-model:agent="customerBranchFormData.agent"
/>
</div>
</div>

File diff suppressed because it is too large Load diff

View file

@ -1,188 +1,483 @@
<script setup lang="ts">
import DatePicker from 'src/components/shared/DatePicker.vue';
import { formatNumberDecimal } from 'stores/utils';
import { QSelect } from 'quasar';
import { onMounted, ref, watch, capitalize } from 'vue';
import { formatNumberDecimal, selectFilterOptionRefMod } from 'stores/utils';
import { calculateAge, disabledAfterToday } from 'src/utils/datetime';
import useOptionStore from 'src/stores/options';
import DatePicker from 'src/components/shared/DatePicker.vue';
const props = defineProps<{
index: string;
code?: string;
readonly?: boolean;
prefixId?: string;
customerType?: 'PERS' | 'CORP';
}>();
const optionStore = useOptionStore();
// PERS
const citizenId = defineModel<string | undefined>('citizenId', {
required: true,
});
const prefixName = defineModel<string | null>('prefixName');
const firstName = defineModel<string>('firstName');
const lastName = defineModel<string>('lastName');
const firstNameEN = defineModel<string>('firstNameEN');
const lastNameEN = defineModel<string>('lastNameEN');
const gender = defineModel<string>('gender');
const birthDate = defineModel<Date | string | null>('birthDate');
// CORP
const customerName = defineModel<string>('customerName');
const legalPersonNo = defineModel<string | undefined>('legalPersonNo', {
required: true,
});
const branchCode = defineModel<string | undefined>('branchCode', {
required: true,
});
const customerCode = defineModel<string | undefined>('customerCode', {
required: true,
});
const registerName = defineModel<string | undefined>('registerName', {
required: true,
});
const registerNameEN = defineModel<string>('registerNameEn');
const registerNameEN = defineModel<string | undefined>('registerNameEN', {
required: true,
});
const registerDate = defineModel<Date | null>('registerDate');
const authorizedCapital = defineModel<string>('authorizedCapital', {
default: '0',
});
defineProps<{
index: string;
customerName?: string;
code?: string;
readonly?: boolean;
prefixId?: string;
customerType?: 'PERS' | 'CORP';
}>();
// both
const telephoneNo = defineModel<string>('telephoneNo');
const codeCustomer = defineModel<string | undefined>('codeCustomer', {
required: true,
});
const prefixNameOptions = ref<Record<string, unknown>[]>([]);
let prefixNameFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
const genderOptions = ref<Record<string, unknown>[]>([]);
let genderFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
onMounted(() => {
prefixNameFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.prefix),
prefixNameOptions,
'label',
);
genderFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.gender),
genderOptions,
'label',
);
});
watch(
() => optionStore.globalOption,
() => {
prefixNameFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.prefix),
prefixNameOptions,
'label',
);
genderFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.gender),
genderOptions,
'label',
);
},
);
watch(
() => prefixName.value,
(v) => {
if (props.readonly) return;
if (v === 'mr') gender.value = 'male';
else if (v !== '') gender.value = 'female';
},
);
</script>
<template>
<div class="row q-col-gutter-md">
<div class="row q-col-gutter-sm">
<template v-if="customerType === 'CORP'">
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-3"
:label="$t('customer.form.legalPersonNo')"
for="input-legal-person-no"
v-model="legalPersonNo"
/>
<div class="col-12 row q-col-gutter-sm">
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6 col-md-5"
:label="$t('customer.form.employerName')"
for="input-legal-person-no"
v-model="customerName"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
hide-bottom-space
class="col-12 col-md-3"
for="input-branch-code"
:label="$t('customer.form.branchCode')"
:disable="!readonly"
:readonly="readonly"
:model-value="`${branchCode?.slice(0, -2) || '-'}${index.toString().padStart(2, '0')}`"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md"
:label="$t('customer.form.legalPersonCode')"
for="input-legal-person-code"
v-model="legalPersonNo"
mask="#############"
:rules="[
(val) => (val && val.length > 0) || $t('form.error.required'),
(val) =>
(val && val.length === 13 && /[0-9]+/.test(val)) ||
$t('form.error.invalidCustomeMessage', {
msg: $t('form.error.requireLength', { msg: 13 }),
}),
]"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
hide-bottom-space
class="col-12 col-md-3"
for="input-customer-code"
:disable="!readonly"
:readonly="readonly"
:label="$t('customer.form.customerCode')"
:model-value="legalPersonNo"
/>
<q-input
dense
outlined
hide-bottom-space
class="col-12 col-md"
for="input-customer-code"
:disable="!readonly"
:readonly="readonly"
:label="$t('customer.form.customerCode')"
:model-value="legalPersonNo"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-3"
:label="$t('customer.form.legalPersonCode')"
for="input-legal-person-code"
:model-value="legalPersonNo"
/>
<q-input
dense
outlined
hide-bottom-space
class="col-12 col-md"
for="input-branch-code"
:label="$t('customer.form.branchCode')"
:disable="!readonly"
:readonly="readonly"
:model-value="branchCode"
/>
</div>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-6"
:label="$t('customer.form.registerName')"
for="input-register-name"
v-model="registerName"
/>
<div class="col-12 row q-col-gutter-sm">
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-6"
:label="$t('customer.form.registerName')"
for="input-register-name"
v-model="registerName"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-6"
:label="$t('customer.form.registerNameEN')"
for="input-register-name-en"
v-model="registerNameEN"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-6"
label="Company name"
for="input-register-name-en"
v-model="registerNameEN"
:rules="[
(val: string) => !!val || $t('form.error.required'),
(val: string) =>
/^[0-9A-Za-z\s.,]+$/.test(val) || $t('form.error.letterOnly'),
]"
/>
</div>
<q-input
lazy-rules="ondemand"
dense
outlined
type="text"
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-3"
:label="$t('customer.form.authorizedCapital')"
for="input-authorized-capital"
:model-value="
!readonly
? authorizedCapital
: formatNumberDecimal(+authorizedCapital, 2)
"
@update:model-value="
(v) => {
authorizedCapital = `${v}`;
}
"
/>
<DatePicker
v-model="registerDate"
:id="`${prefixId}-input-register-date`"
:label="$t('customer.form.registerDate')"
:readonly="readonly"
clearable
/>
<div class="col-12 row q-col-gutter-sm">
<DatePicker
v-model="registerDate"
class="col-6 col-md-2"
:id="`${prefixId}-input-register-date`"
:label="$t('customer.form.registerDate')"
:readonly="readonly"
clearable
/>
<q-input
dense
outlined
type="text"
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-2"
:label="$t('customer.form.authorizedCapital')"
for="input-authorized-capital"
:model-value="
!readonly
? authorizedCapital
: formatNumberDecimal(+authorizedCapital, 2)
"
@update:model-value="
(v) => {
authorizedCapital = `${v}`;
}
"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6 col-md-3"
:label="$t('customer.form.headQuarters.telephoneNo')"
for="input-first-name-en"
:model-value="readonly ? telephoneNo || '-' : telephoneNo"
@update:model-value="
(v) => (typeof v === 'string' ? (telephoneNo = v) : '')
"
>
<template #prepend>
<q-icon
size="xs"
name="mdi-phone-outline"
class="cursor-pointer"
color="primary"
/>
</template>
</q-input>
</div>
</template>
<template v-if="customerType === 'PERS'">
<q-input
lazy-rules="ondemand"
dense
outlined
:disable="index !== '0'"
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-3"
:label="$t('customer.form.cardNumber')"
for="input-legal-person-no"
v-model="citizenId"
/>
<div class="col-7 row q-col-gutter-sm">
<q-input
dense
outlined
hide-bottom-space
class="col-12 col-md-5"
for="input-customer-code"
:disable="!readonly"
:readonly="readonly"
:label="$t('customer.form.customerCode')"
:model-value="codeCustomer"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:disable="!readonly"
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-3"
:label="$t('customer.form.branchCode')"
for="input-branch-code"
:model-value="`${branchCode?.slice(0, -2) || '-'}${index.toString().padStart(2, '0')}`"
/>
<q-input
dense
outlined
:disable="index !== '0' && !readonly"
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-7"
:label="$t('customer.form.cardNumber')"
for="input-legal-person-no"
v-model="citizenId"
mask="#############"
:rules="[
(val) => (val && val.length > 0) || $t('form.error.required'),
(val) =>
(val && val.length === 13 && /[0-9]+/.test(val)) ||
$t('form.error.invalidCustomeMessage', {
msg: $t('form.error.requireLength', { msg: 13 }),
}),
]"
/>
</div>
<q-input
lazy-rules="ondemand"
dense
outlined
:disable="!readonly"
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-3"
:label="$t('customer.form.customerCode')"
for="input-customer-code"
:model-value="citizenId"
/>
<div class="col-9 row q-col-gutter-sm">
<q-select
outlined
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="value"
hide-dropdown-icon
class="col-md-2 col-6"
dense
:readonly="readonly"
:options="prefixNameOptions"
:for="`${prefixId}-select-prefix-name`"
:label="$t('personnel.form.prefixName')"
@filter="prefixNameFilter"
:model-value="readonly ? prefixName || '-' : prefixName"
@update:model-value="
(v) => (typeof v === 'string' ? (prefixName = v) : '')
"
@clear="prefixName = ''"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
<q-input
:for="`${prefixId}-input-first-name`"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col"
:label="$t('personnel.form.firstName')"
v-model="firstName"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input
:for="`${prefixId}-input-last-name`"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col"
:label="$t('personnel.form.lastName')"
v-model="lastName"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
</div>
<div class="col-9 row q-col-gutter-sm">
<q-input
:for="`${prefixId}-input-first-name`"
dense
outlined
hide-bottom-space
:readonly="readonly"
:disable="!readonly"
class="col-md-2 col-6"
label="Title"
:model-value="
readonly
? capitalize(prefixName || '') || '-'
: capitalize(prefixName || '')
"
@update:model-value="
(v) => (typeof v === 'string' ? (prefixName = v) : '')
"
@clear="prefixName = ''"
/>
<q-input
:for="`${prefixId}-input-first-name`"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col"
label="Name"
v-model="firstNameEN"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input
:for="`${prefixId}-input-last-name`"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col"
label="Surname"
v-model="lastNameEN"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
</div>
<div class="row col-9 q-col-gutter-sm">
<q-input
:for="`${prefixId}-input-telephone`"
dense
outlined
:readonly="readonly"
class="col-md col-6"
:label="$t('form.telephone')"
:mask="readonly ? '' : '##########'"
:model-value="readonly ? telephoneNo || '-' : telephoneNo"
@update:model-value="
(v) => (typeof v === 'string' ? (telephoneNo = v) : '')
"
@clear="telephoneNo = ''"
>
<template #prepend>
<q-icon
size="xs"
name="mdi-phone-outline"
class="cursor-pointer"
color="primary"
/>
</template>
</q-input>
<q-select
outlined
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="value"
class="col-md-2 col-6"
dense
:readonly="readonly"
:options="genderOptions"
:hide-dropdown-icon="readonly"
:for="`${prefixId}-select-gender`"
:label="$t('form.gender')"
@filter="genderFilter"
:model-value="readonly ? gender || '-' : gender"
@update:model-value="
(v) => (typeof v === 'string' ? (gender = v) : '')
"
@clear="gender = ''"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
<DatePicker
v-model="birthDate"
class="col-md col-6"
:id="`${prefixId}-input-birth-date`"
:readonly="readonly"
:label="$t('form.birthDate')"
:disabled-dates="disabledAfterToday"
:rules="[
(val: string) =>
!!val ||
$t('form.error.selectField', { field: $t('form.birthDate') }),
]"
/>
<q-input
:for="`${prefixId}-input-age`"
:id="`${prefixId}-input-age`"
dense
outlined
readonly
:label="$t('personnel.age')"
class="col-md-2 col-12"
:model-value="
birthDate?.toString() === 'Invalid Date' ||
birthDate?.toString() === undefined
? ''
: calculateAge(birthDate)
"
/>
</div>
</template>
</div>
</template>

View file

@ -0,0 +1,41 @@
<script lang="ts" setup>
defineProps<{
readonly?: boolean;
prefixId?: string;
}>();
const authorizedName = defineModel<string>('authorizedName');
const authorizedNameEN = defineModel<string>('authorizedNameEN');
</script>
<template>
<div class="col-md-9 col-12 row q-col-gutter-sm">
<q-input
:for="`${prefixId}-input-contact-name`"
:id="`${prefixId}-input-contact-name`"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-6 col-12"
:label="$t('customerBranch.tab.authorized')"
:model-value="readonly ? authorizedName || '-' : authorizedName"
@update:model-value="
(v) => (typeof v === 'string' ? (authorizedName = v) : '')
"
/>
<q-input
:for="`${prefixId}-input-contact-name`"
:id="`${prefixId}-input-contact-name`"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-6 col-12"
:label="`${$t('customerBranch.tab.authorized')} (EN)`"
:model-value="readonly ? authorizedNameEN || '-' : authorizedNameEN"
@update:model-value="
(v) => (typeof v === 'string' ? (authorizedNameEN = v) : '')
"
/>
</div>
</template>

View file

@ -1,19 +1,10 @@
<script lang="ts" setup>
import { ref, watch, capitalize } from 'vue';
import { ref, watch } from 'vue';
import { QSelect } from 'quasar';
import { selectFilterOptionRefMod } from 'stores/utils';
import { getRole } from 'src/services/keycloak';
import useOptionStore from 'stores/options';
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
const { locale } = useI18n();
import {
dateFormat,
calculateAge,
parseAndFormatDate,
disabledAfterToday,
} from 'src/utils/datetime';
import {
SaveButton,
@ -21,14 +12,21 @@ import {
DeleteButton,
UndoButton,
} from 'components/button';
defineProps<{
prefixId?: string;
outlined?: boolean;
readonly?: boolean;
create?: boolean;
actionDisabled?: boolean;
customerType?: 'CORP' | 'PERS';
}>();
withDefaults(
defineProps<{
prefixId?: string;
outlined?: boolean;
readonly?: boolean;
onCreate?: boolean;
actionDisabled?: boolean;
customerType?: 'CORP' | 'PERS';
hideAction?: boolean;
}>(),
{
hideAction: false,
},
);
defineEmits<{
(e: 'save'): void;
(e: 'edit'): void;
@ -37,20 +35,20 @@ defineEmits<{
}>();
const optionStore = useOptionStore();
const code = defineModel<string>('code', { required: true });
const namePrefix = defineModel<string | null>('namePrefix');
const birthDate = defineModel<Date | string | null>('birthDate');
const gender = defineModel<string>('gender');
const firstName = defineModel<string>('firstName', { required: true });
const lastName = defineModel<string>('lastName', { required: true });
const firstNameEN = defineModel<string>('firstNameEn', { required: true });
const lastNameEN = defineModel<string>('lastNameEn', { required: true });
const registeredBranchId = defineModel<string>('registeredBranchId', {
required: true,
});
const customerName = defineModel<string>('customerName', { default: '' });
const registerName = defineModel<string>('registerName', { default: '' });
const citizenId = defineModel<string>('citizenId', { default: '' });
const legalPersonNo = defineModel<string>('legalPersonNo', { default: '' });
const businessType = defineModel<'string'>('businessType');
const jobPosition = defineModel<'string'>('jobPosition');
const telephoneNo = defineModel<string>('telephoneNo', { default: '' });
const branchOptions = defineModel<{ id: string; name: string }[]>(
'branchOptions',
{ default: [] },
@ -80,70 +78,10 @@ watch(
);
},
);
const prefixNameOptions = ref<Record<string, unknown>[]>([]);
let prefixNameFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
const prefixNameEnOptions = ref<Record<string, unknown>[]>([]);
let prefixNameEnFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
const genderOptions = ref<Record<string, unknown>[]>([]);
let genderFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
onMounted(() => {
prefixNameFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.prefix),
prefixNameOptions,
'label',
);
genderFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.gender),
genderOptions,
'label',
);
});
watch(
() => optionStore.globalOption,
() => {
prefixNameFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.prefix),
prefixNameOptions,
'label',
);
genderFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.gender),
genderOptions,
'label',
);
},
);
watch(
() => namePrefix.value,
(v) => {
if (v === 'mr') gender.value = 'male';
else gender.value = 'female';
},
);
function formatCode(input: string | undefined, type: 'code' | 'number') {
if (!input) return;
return input.slice(...(type === 'code' ? [0, -6] : [-6]));
}
</script>
<template>
<div class="row q-col-gutter-sm q-mb-sm">
<div class="row q-col-gutter-sm">
<div class="col-12 text-weight-bold text-body1 row items-center">
<q-icon
flat
@ -153,8 +91,18 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
name="mdi-office-building-outline"
style="background-color: var(--surface-3)"
/>
<span>{{ $t('form.field.basicInformation') }}</span>
<EditButton
<span>
{{
$t('general.information', {
msg: `${$t('customer.employer')}${
customerType === 'CORP'
? $t('customer.employerLegalEntity')
: $t('customer.employerNaturalPerson')
}`,
})
}}
</span>
<!-- <EditButton
icon-only
v-if="readonly && !create"
type="button"
@ -184,295 +132,158 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
@click="$emit('save')"
type="submit"
:disabled="actionDisabled"
/>
/> -->
</div>
<div class="col-12 row q-col-gutter-sm">
<q-select
outlined
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
dense
class="col-12 col-md-7"
option-value="id"
input-debounce="0"
option-label="name"
lazy-rules="ondemand"
v-model="registeredBranchId"
:readonly="readonly"
:options="filteredBranchOptions"
:hide-dropdown-icon="readonly"
:label="$t('customer.form.registeredBranch')"
:for="`${prefixId}-input-source-nationality`"
:rules="[
(val) => {
const roles = getRole() || [];
return (
['admin', 'system', 'head_of_admin'].some((v) =>
roles.includes(v),
) ||
!!val ||
$t('form.error.required')
);
},
]"
@filter="branchFilter"
<div class="col-12 row q-col-gutter-sm">
<q-select
outlined
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
dense
class="col-6"
option-value="id"
input-debounce="0"
option-label="name"
v-model="registeredBranchId"
:readonly="readonly"
:options="filteredBranchOptions"
:hide-dropdown-icon="readonly"
:label="$t('customer.form.registeredBranch')"
:for="`${prefixId}-input-source-nationality`"
:rules="[
(val) => {
const roles = getRole() || [];
return (
['admin', 'system', 'head_of_admin'].some((v) =>
roles.includes(v),
) ||
!!val ||
$t('form.error.required')
);
},
]"
@filter="branchFilter"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div
v-if="customerType === 'CORP' && !onCreate"
class="row col-12 q-col-gutter-sm"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-5"
:label="$t('customer.form.customerName')"
for="input-first-name"
v-model="registerName"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6 col-md-3"
:label="$t('general.taxNo')"
for="input-first-name-en"
v-model="legalPersonNo"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6 col-md-3"
:label="$t('customer.table.businessTypePure')"
for="input-first-name-en"
:model-value="optionStore.mapOption(businessType)"
/>
</div>
<div class="col-12 row q-col-gutter-sm">
<q-select
outlined
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
hide-dropdown-icon
class="col-12 col-md-2"
dense
:readonly="readonly"
:options="prefixNameOptions"
:for="`${prefixId}-select-prefix-name`"
:label="$t('form.prefixName')"
@filter="prefixNameFilter"
:model-value="readonly ? namePrefix || '-' : namePrefix"
@update:model-value="
(v) => {
typeof v === 'string' ? (namePrefix = v) : '';
}
"
@clear="namePrefix = ''"
:rules="[
(val) => {
const roles = getRole() || [];
return !!val || $t('form.error.required');
},
]"
<div
v-if="customerType === 'PERS' && !onCreate"
class="row col-12 q-col-gutter-sm"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-6"
:label="$t('customer.form.employerName')"
for="input-first-name"
v-model="customerName"
/>
<q-input
outlined
class="col-md-3 col-6"
hide-bottom-space
v-model="citizenId"
mask="#############"
:readonly="readonly"
dense
:label="$t('personnel.form.citizenId')"
for="input-citizen-id"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6 col-md-3"
:label="$t('customer.table.businessTypePure')"
for="input-first-name-en"
:model-value="optionStore.mapOption(businessType)"
/>
</div>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-5"
:label="$t('customer.form.firstName')"
for="input-first-name"
v-model="firstName"
:rules="[
(val) => {
const roles = getRole() || [];
return !!val || $t('form.error.required');
},
]"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-5"
:label="$t('customer.form.lastName')"
for="input-last-name"
v-model="lastName"
:rules="[
(val) => {
const roles = getRole() || [];
return !!val || $t('form.error.required');
},
]"
/>
</div>
<div class="col-12 row q-col-gutter-sm">
<q-input
lazy-rules="ondemand"
dense
outlined
:disable="!readonly"
readonly
hide-bottom-space
class="col-12 col-md-2"
:label="$t('customer.form.prefixName')"
for="input-prefix-name"
:model-value="
readonly
? capitalize(namePrefix || '') || '-'
: capitalize(namePrefix || '')
"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-5"
:label="$t('customer.form.firstNameEN')"
for="input-first-name-en"
v-model="firstNameEN"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-5"
:label="$t('customer.form.lastNameEN')"
for="input-last-name-en"
v-model="lastNameEN"
/>
</div>
<div class="col-12 row q-col-gutter-sm">
<q-select
outlined
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
class="col-12 col-md-2"
dense
:readonly="readonly"
:options="genderOptions"
:hide-dropdown-icon="readonly"
:for="`${prefixId}-select-gender`"
:label="$t('form.gender')"
@filter="genderFilter"
:model-value="readonly ? gender || '-' : gender"
@update:model-value="(v) => (typeof v === 'string' ? (gender = v) : '')"
@clear="gender = ''"
:rules="[
(val) => {
const roles = getRole() || [];
return !!val || $t('form.error.required');
},
]"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
<VueDatePicker
:id="`${prefixId}-input-birth-date`"
:for="`${prefixId}-input-birth-date`"
utc
autoApply
v-model="birthDate"
:disabled-dates="disabledAfterToday"
:teleport="true"
:dark="$q.dark.isActive"
:locale="$i18n.locale === 'tha' ? 'th' : 'en'"
:enableTimePicker="false"
:disabled="readonly"
class="col-12 col-md-3"
>
<template #year="{ value }">
{{ $i18n.locale === 'tha' ? value + 543 : value }}
</template>
<template #year-overlay-value="{ value }">
{{ $i18n.locale === 'tha' ? value + 543 : value }}
</template>
<template #trigger>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-birth-date`"
hide-bottom-space
placeholder="DD/MM/YYYY"
:label="$t('form.birthDate')"
dense
outlined
:readonly="readonly"
:rules="[
(val: string) =>
!!val || $t('selectValidate') + $t('form.birthDate'),
]"
:mask="readonly ? '' : '##/##/####'"
:model-value="
birthDate && readonly
? dateFormat(birthDate)
: dateFormat(birthDate, false, false, true)
"
@update:model-value="
(v) => {
if (v && v.toString().length === 10) {
const today = new Date();
const date = parseAndFormatDate(v, locale);
birthDate = date && date > today ? today : date;
}
}
"
>
<template v-slot:prepend>
<q-icon
size="xs"
name="mdi-calendar-blank-outline"
class="cursor-pointer"
color="primary"
/>
</template>
</q-input>
</template>
</VueDatePicker>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-age`"
:id="`${prefixId}-input-age`"
dense
outlined
:readonly="readonly"
:label="$t('general.age')"
class="col-12 col-md-2"
:model-value="
birthDate?.toString() === 'Invalid Date' ||
birthDate?.toString() === undefined
? ''
: calculateAge(birthDate)
"
/>
<div v-if="!onCreate" class="row col-12 q-col-gutter-sm">
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6 col-md-5"
:label="$t('customer.form.jobPosition')"
for="input-first-name-en"
:model-value="optionStore.mapOption(jobPosition)"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-6 col-md-3"
:label="$t('customer.form.headQuarters.telephoneNo')"
for="input-first-name-en"
v-model="telephoneNo"
>
<template #prepend>
<q-icon
size="xs"
name="mdi-phone-outline"
class="cursor-pointer"
color="primary"
/>
</template>
</q-input>
</div>
</div>
</div>
</template>

View file

@ -5,27 +5,30 @@ import EmployerFormBusiness from './EmployerFormBusiness.vue';
import EmployerFormContact from './EmployerFormContact.vue';
import { CustomerCreate } from 'stores/customer/types';
import EmployerFormAbout from './EmployerFormAbout.vue';
import EmployerFormAuthorized from './EmployerFormAuthorized.vue';
import { useCustomerForm } from 'src/pages/03_customer-management/form';
const customerFormStore = useCustomerForm();
import { FormCitizen } from 'components/upload-file/';
import { waitAll } from 'src/stores/utils';
import {
FormCitizen,
CorpFormBusinessRegistration,
PersFormBusinessRegistration,
} from 'components/upload-file/';
import useOcrStore from 'stores/ocr';
import useCustomerStore from 'stores/customer';
const ocrStore = useOcrStore();
import {
SaveButton,
EditButton,
DeleteButton,
UndoButton,
} from 'components/button';
import UploadFile from 'src/components/upload-file/UploadFile.vue';
import { uploadFileListCustomer } from '../../constant';
const statusOcr = ref(false);
const customer = defineModel<CustomerCreate>('customer', { required: true });
import { UploadFileGroup } from 'src/components/upload-file/';
import { uploadFileListCustomer, columnsAttachment } from '../../constant';
import { group } from 'node:console';
const ocrStore = useOcrStore();
const customerStore = useCustomerStore();
const item = defineModel<NonNullable<CustomerCreate['customerBranch']>[number]>(
'customerBranch',
{ required: true },
@ -40,14 +43,21 @@ defineEmits<{
(e: 'delete'): void;
}>();
defineProps<{
index: number;
customerName: string;
readonly?: boolean;
prefixId?: string;
actionDisabled?: boolean;
customerType?: 'CORP' | 'PERS';
}>();
withDefaults(
defineProps<{
index: number;
customerName: string;
readonly?: boolean;
prefixId?: string;
onCreate?: boolean;
actionDisabled?: boolean;
customerType?: 'CORP' | 'PERS';
hideAction?: boolean;
}>(),
{
hideAction: false,
},
);
</script>
<template>
@ -55,42 +65,44 @@ defineProps<{
class="col-12 text-weight-bold row items-center q-mb-sm"
:style="{ opacity: actionDisabled ? '.5' : undefined }"
:id="`form-branch-customer-no-${index}`"
v-if="!hideAction"
>
{{
index === 0
? $t('customer.form.headQuarters.title')
: $t('customer.form.branch.title', { name: index || 0 })
}}
<EditButton
icon-only
v-if="readonly"
@click="$emit('edit')"
class="q-ml-auto"
type="button"
:disabled="actionDisabled"
/>
<DeleteButton
icon-only
v-if="readonly"
@click="$emit('delete')"
type="button"
:disabled="actionDisabled"
/>
<UndoButton
icon-only
v-if="!readonly"
@click="$emit('cancel')"
type="button"
class="q-ml-auto"
:disabled="actionDisabled"
/>
<SaveButton
icon-only
v-if="!readonly"
@click="$emit('save')"
type="submit"
:disabled="actionDisabled"
/>
<div class="q-ml-auto row no-warp">
<EditButton
icon-only
v-if="readonly"
@click="$emit('edit')"
class="q-ml-auto"
type="button"
:disabled="actionDisabled"
/>
<DeleteButton
icon-only
v-if="readonly"
@click="$emit('delete')"
type="button"
:disabled="actionDisabled"
/>
<UndoButton
icon-only
v-if="!readonly && !onCreate"
@click="$emit('cancel')"
type="button"
:disabled="actionDisabled"
/>
<SaveButton
v-if="!readonly"
icon-only
@click="$emit('save')"
type="submit"
:disabled="actionDisabled"
/>
</div>
</span>
<div class="col-12" :style="{ opacity: actionDisabled ? '.5' : undefined }">
@ -102,50 +114,43 @@ defineProps<{
class="bordered-b"
active-color="primary"
no-caps
style="color: hsl(var(--text-mute))"
>
<q-tab name="main" :label="$t('customerBranch.tab.main')" />
<q-tab name="address" :label="$t('customerBranch.tab.address')" />
<q-tab name="business" :label="$t('customerBranch.tab.business')" />
<q-tab
v-if="customerType === 'CORP'"
name="authorized"
:label="$t('customerBranch.tab.authorized')"
/>
<q-tab name="address" :label="$t('customerBranch.tab.address')" />
<q-tab name="contact" :label="$t('customerBranch.tab.contact')" />
<q-tab name="attachment" :label="$t('customerBranch.tab.attachment')" />
</q-tabs>
<q-tab-panels v-model="tab">
<q-tab-panel name="main">
<EmployerFormAbout
:prefixId="prefixId"
:index="index.toString()"
:readonly="readonly"
:customer-type="customerType"
:customer-name="customerName"
v-model:citizen-id="item.citizenId"
v-model:id="item.id"
v-model:legal-person-no="item.legalPersonNo"
v-model:branch-code="item.code"
v-model:customer-code="customer.code"
v-model:register-company-name="item.registerCompanyName"
v-model:register-name="item.registerName"
v-model:register-name-en="item.registerNameEN"
v-model:register-date="item.registerDate"
v-model:authorized-capital="item.authorizedCapital"
/>
</q-tab-panel>
<q-tab-panel name="address">
<AddressForm
use-work-place
:prefix-id="prefixId || 'employer'"
hide-title
dense
:readonly="readonly"
outlined
:title="$t('form.address')"
v-model:workplace="item.workplace"
v-model:workplace-en="item.workplaceEN"
v-model:address="item.address"
v-model:addressEN="item.addressEN"
v-model:province-id="item.provinceId"
v-model:district-id="item.districtId"
v-model:sub-district-id="item.subDistrictId"
:addressTitle="$t('form.address')"
:addressTitleEN="$t('form.address', { suffix: '(EN)' })"
v-model:prefixName="item.namePrefix"
v-model:firstName="item.firstName"
v-model:lastName="item.lastName"
v-model:firstNameEN="item.firstNameEN"
v-model:lastNameEN="item.lastNameEN"
v-model:gender="item.gender"
v-model:birthDate="item.birthDate"
v-model:customerName="item.customerName"
v-model:legalPersonNo="item.legalPersonNo"
v-model:branchCode="item.code"
v-model:registerName="item.registerName"
v-model:registerNameEN="item.registerNameEN"
v-model:registerDate="item.registerDate"
v-model:authorizedCapital="item.authorizedCapital"
v-model:telephoneNo="item.telephoneNo"
v-model:codeCustomer="item.codeCustomer"
/>
</q-tab-panel>
<q-tab-panel name="business">
@ -154,120 +159,283 @@ defineProps<{
outlined
:prefix-id="prefixId || 'employer'"
:readonly="readonly"
v-model:employment-office="item.employmentOffice"
v-model:bussiness-type="item.businessType"
v-model:bussiness-type-en="item.businessTypeEN"
v-model:job-position="item.jobPosition"
v-model:job-position-en="item.jobPositionEN"
v-model:job-description="item.jobDescription"
v-model:sale-employee="item.saleEmployee"
v-model:pay-date="item.payDate"
v-model:pay-date-e-n="item.payDateEN"
v-model:wage-rate="item.wageRate"
v-model:wage-rate-text="item.wageRateText"
/>
</q-tab-panel>
<q-tab-panel v-if="customerType === 'CORP'" name="authorized">
<EmployerFormAuthorized
:prefix-id="prefixId || 'employer'"
:readonly="readonly"
v-model:authorized-name="item.authorizedName"
v-model:authorized-name-e-n="item.authorizedNameEN"
/>
</q-tab-panel>
<q-tab-panel name="address">
<AddressForm
:prefix-id="prefixId || 'employer'"
hide-title
dense
outlined
use-employment
:readonly="readonly"
:title="$t('form.address')"
v-model:homeCode="item.homeCode"
v-model:employmentOffice="item.employmentOffice"
v-model:employmentOfficeEN="item.employmentOfficeEN"
v-model:address="item.address"
v-model:addressEN="item.addressEN"
v-model:street="item.street"
v-model:streetEN="item.streetEN"
v-model:moo="item.moo"
v-model:mooEN="item.mooEN"
v-model:soi="item.soi"
v-model:soiEN="item.soiEN"
v-model:province-id="item.provinceId"
v-model:district-id="item.districtId"
v-model:sub-district-id="item.subDistrictId"
:addressTitle="$t('form.address')"
:addressTitleEN="$t('form.address', { suffix: '(EN)' })"
/>
</q-tab-panel>
<q-tab-panel name="contact">
<EmployerFormContact
:readonly="readonly"
:prefixId="prefixId"
v-model:contactName="item.contactName"
v-model:email="item.email"
v-model:telephone="item.telephoneNo"
v-model:contactTel="item.contactTel"
v-model:officeTel="item.officeTel"
v-model:agent="item.agent"
/>
</q-tab-panel>
<q-tab-panel name="attachment">
<UploadFile
<UploadFileGroup
v-model:current-id="item.id"
v-model="item.file"
hide-action
:readonly="readonly"
:dropdown-list="uploadFileListCustomer"
v-model:status-ocr="statusOcr"
v-model:file="item.file"
:tree-file="
Object.values(
item.file?.reduce<
Record<string, { label: string; file: { label: string }[] }>
>((a, c) => {
const _group = c.group || 'other';
if (!a[_group]) {
a[_group] = {
label: $t(
uploadFileListCustomer.find((v) => v.value === _group)
?.label || _group,
),
file: [
{
label:
c.name ||
`${c.group}-${c.file?.name || Date.now()}`,
},
],
};
} else {
a[_group].file.push({
label:
c.name || `${c.group}-${c.file?.name || Date.now()}`,
});
}
return a;
}, {}) || {},
)
"
@send-ocr="
async (group: any, file: any) => {
:ocr="
async (group, file) => {
const res = await ocrStore.sendOcr({
file: file,
category: group,
});
if (res) {
const map = res.fields.reduce<Record<string, string>>(
(a, c) => {
a[c.name] = c.value;
return a;
},
{},
);
if (!item.citizenId) item.citizenId = map['citizen_id'] || '';
if (!item.address) item.address = map['address'] || '';
if (!customer.firstName)
customer.firstName = map['firstname'] || '';
if (!customer.lastName)
customer.lastName = map['lastname'] || '';
if (!customer.firstNameEN)
customer.firstNameEN = map['firstname_en'] || '';
if (!customer.lastNameEN)
customer.lastNameEN = map['lastname_en'] || '';
if (!customer.birthDate)
customer.birthDate = new Date(map['birth_date'] || '');
}
const tempValue = {
status: true,
group,
meta: res.fields,
};
statusOcr = false;
return tempValue;
}
return { status: false, group, meta: [] };
}
"
@delete-file="
(filename) => {
if (!item.id) return;
:menu="uploadFileListCustomer"
:columns="columnsAttachment"
:auto-save="item.id !== ''"
:download="
(obj) => {
customerStore.getFile({
parentId: item.id || '',
group: obj.group,
fileId: obj._meta.id,
download: true,
});
}
"
:delete-item="
async (obj) => {
const res = await customerStore.delMeta({
parentId: item.id || '',
group: obj.group,
metaId: obj._meta.id,
});
customerFormStore.deleteAttachment(
{ branchId: item.id, customerId: item.customerId },
filename,
);
if (res) {
return true;
}
return false;
}
"
:save="
async (
group:
| 'citizen'
| 'house-registration'
| 'commercial-registration'
| 'vat-registration'
| 'power-of-attorney',
_meta: any,
file: File | undefined,
) => {
if (file !== undefined && item.id) {
const res = await customerStore.postMeta({
parentId: item.id || '',
group,
meta: _meta,
file,
});
if (res) {
return true;
}
} else {
const {
customerBranchId,
id,
employeeId,
createdAt,
updatedAt,
middleName,
middleNameEN,
...payload
} = _meta;
const res = await customerStore.putMeta({
parentId: item.id || '',
group,
metaId: _meta.id,
meta: payload,
file,
});
if (res) {
return true;
}
}
return false;
}
"
:get-file-list="
async (
group:
| 'citizen'
| 'house-registration'
| 'commercial-registration'
| 'vat-registration'
| 'power-of-attorney',
) => {
if (!!item.id) {
if (group === 'citizen') {
const resMeta = await customerStore.getMetaList({
parentId: item.id,
group,
});
const tempValue = resMeta.map(async (v: any) => {
return {
_meta: { ...v },
name: v.id || '',
group: group,
url: await customerStore.getFile({
parentId: item.id || '',
group,
fileId: v.id,
}),
file: undefined,
};
});
return await waitAll(tempValue);
}
}
return [];
}
"
>
<template #form="{ mode }">
<template #form="{ mode, meta, isEdit }">
<FormCitizen
v-if="mode === 'citizenId'"
v-if="mode === 'citizen' && meta"
orc
v-model:citizen-id="item.citizenId"
v-model:birth-date="customer.birthDate"
v-model:first-name="customer.firstName"
v-model:first-name-en="customer.firstNameEN"
v-model:last-name="customer.lastName"
v-model:last-name-en="customer.lastNameEN"
v-model:address="item.address"
ra
:readonly="!isEdit"
v-model:name-prefix="meta.namePrefix"
v-model:citizen-id="meta.citizenId"
v-model:birth-date="meta.birthDate"
v-model:first-name="meta.firstName"
v-model:first-name-en="meta.firstNameEN"
v-model:last-name="meta.lastName"
v-model:last-name-en="meta.lastNameEN"
v-model:gender="meta.gender"
v-model:religion="meta.religion"
v-model:nationality="meta.nationality"
v-model:expire-date="meta.expireDate"
v-model:issue-date="meta.issueDate"
v-model:homeCode="meta.homeCode"
v-model:employmentOffice="meta.employmentOffice"
v-model:employmentOfficeEN="meta.employmentOfficeEN"
v-model:address="meta.address"
v-model:addressEN="meta.addressEN"
v-model:street="meta.street"
v-model:streetEN="meta.streetEN"
v-model:moo="meta.moo"
v-model:mooEN="meta.mooEN"
v-model:soi="meta.soi"
v-model:soiEN="meta.soiEN"
v-model:province-id="meta.provinceId"
v-model:district-id="meta.districtId"
v-model:sub-district-id="meta.subDistrictId"
v-model:middle-name="meta.middleName"
v-model:middle-name-en="meta.middleNameEN"
/>
<FormEmployeePassport
v-if="mode === 'passport' && meta"
prefix-id="drawer-info-employee"
id="form-passport"
dense
outlined
separator
ocr
:title="$t('customerEmployee.form.group.passport')"
:readonly="!isEdit"
v-model:passport-type="meta.type"
v-model:passport-number="meta.number"
v-model:passport-issue-date="meta.issueDate"
v-model:passport-expiry-date="meta.expireDate"
v-model:passport-issuing-place="meta.issuePlace"
v-model:passport-issuing-country="meta.issueCountry"
/>
<FormEmployeeVisa
v-if="mode === 'visa' && meta"
prefix-id="drawer-info-employee"
id="form-visa"
ocr
dense
outlined
title="customerEmployee.form.group.visa"
:readonly="!isEdit"
v-model:visa-type="meta.type"
v-model:visa-number="meta.number"
v-model:visa-issue-date="meta.issueDate"
v-model:visa-expiry-date="meta.expireDate"
v-model:visa-issuing-place="meta.issuePlace"
/>
<CorpFormBusinessRegistration
v-if="
mode === 'commercial-registration' &&
meta &&
customerType === 'CORP'
"
/>
<PersFormBusinessRegistration
v-if="
mode === 'commercial-registration' &&
meta &&
customerType === 'PERS'
"
/>
</template>
</UploadFile>
</UploadFileGroup>
<!-- <EmployerFormAttachment
:readonly="readonly"

View file

@ -1,31 +1,25 @@
<script setup lang="ts">
import { selectFilterOptionRefMod } from 'stores/utils';
import { dateFormat, parseAndFormatDate } from 'src/utils/datetime';
import { onMounted, watch } from 'vue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import DatePicker from 'src/components/shared/DatePicker.vue';
const { locale } = useI18n({ useScope: 'global' });
const employmentOffice = defineModel<string>('employmentOffice');
const bussinessType = defineModel<string>('bussinessType');
const jobPosition = defineModel<string>('jobPosition');
const bussinessTypeEN = defineModel<string>('bussinessTypeEn');
const jobPositionEN = defineModel<string>('jobPositionEn');
const jobDescription = defineModel<string>('jobDescription');
const payDate = defineModel<Date | null | string>('payDate');
const wageRate = defineModel<number>('wageRate');
const saleEmployee = defineModel<string>('saleEmployee');
const rawOption = ref();
const bussinessType = defineModel<string>('bussinessType');
const jobPosition = defineModel<string>('jobPosition');
const jobDescription = defineModel<string>('jobDescription');
const payDate = defineModel<string>('payDate');
const payDateEN = defineModel<string>('payDateEN');
const wageRate = defineModel<number>('wageRate');
const wageRateText = defineModel<string>('wageRateText');
const typeBusinessOption = ref([]);
const typeBusinessENOption = ref([]);
const jobPositionOption = ref([]);
const jobPositionENOption = ref([]);
defineProps<{
title?: string;
@ -38,6 +32,8 @@ defineProps<{
onMounted(async () => {
const resultOption = await fetch('/option/option.json');
rawOption.value = await resultOption.json();
typeBusinessENOption.value = rawOption.value.eng.businessType;
jobPositionENOption.value = rawOption.value.eng.position;
if (locale.value === 'eng') {
typeBusinessOption.value = rawOption.value.eng.businessType;
@ -48,20 +44,31 @@ onMounted(async () => {
jobPositionOption.value = rawOption.value.tha.position;
}
});
watch(typeBusinessOption, () => {
watch([typeBusinessOption, typeBusinessENOption], () => {
typeBusinessFilter = selectFilterOptionRefMod(
typeBusinessOption,
typeBusinessOptions,
'label',
);
typeBusinessENFilter = selectFilterOptionRefMod(
typeBusinessENOption,
typeBusinessENOptions,
'label',
);
});
watch(jobPositionOption, () => {
watch([jobPositionOption, jobPositionENOption], () => {
jobPositionFilter = selectFilterOptionRefMod(
jobPositionOption,
jobPositionOptions,
'label',
);
jobPositionENFilter = selectFilterOptionRefMod(
jobPositionENOption,
jobPositionENOptions,
'label',
);
});
const typeBusinessOptions = ref<Record<string, unknown>[]>([]);
@ -77,22 +84,23 @@ let jobPositionFilter = selectFilterOptionRefMod(
jobPositionOptions,
'label',
);
const typeBusinessENOptions = ref<Record<string, unknown>[]>([]);
let typeBusinessENFilter = selectFilterOptionRefMod(
typeBusinessENOption,
typeBusinessENOptions,
'label',
);
const jobPositionENOptions = ref<Record<string, unknown>[]>([]);
let jobPositionENFilter = selectFilterOptionRefMod(
jobPositionENOption,
jobPositionENOptions,
'label',
);
</script>
<template>
<div class="col-12 row q-col-gutter-sm">
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-employment-office`"
:id="`${prefixId}-input-employment-office`"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-12"
:label="$t('form.address')"
v-model="employmentOffice"
/>
<q-select
outlined
clearable
@ -102,10 +110,10 @@ let jobPositionFilter = selectFilterOptionRefMod(
map-options
hide-selected
hide-bottom-space
:hide-dropdown-icon="readonly"
input-debounce="0"
option-value="value"
option-label="label"
lazy-rules="ondemand"
v-model="bussinessType"
class="col-md-6 col-12"
:dense="dense"
@ -114,6 +122,7 @@ let jobPositionFilter = selectFilterOptionRefMod(
:options="typeBusinessOptions"
:for="`${prefixId}-select-business-type`"
@filter="typeBusinessFilter"
:rules="[(val: string) => !!val || $t('form.error.required')]"
>
<template v-slot:no-option>
<q-item>
@ -123,19 +132,38 @@ let jobPositionFilter = selectFilterOptionRefMod(
</q-item>
</template>
</q-select>
<q-input
lazy-rules="ondemand"
<q-select
:for="`${prefixId}-input-bussiness-type-en`"
:id="`${prefixId}-input-bussiness-type-en`"
:dense="dense"
:label="`${$t('customer.form.businessType')} (EN)`"
outlined
:readonly="readonly"
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
:hide-dropdown-icon="readonly"
input-debounce="0"
option-value="value"
option-label="label"
v-model="bussinessType"
class="col-md-6 col-12"
:label="$t('customer.form.businessTypeEN')"
v-model="bussinessTypeEN"
/>
:dense="dense"
:readonly="readonly"
:options="typeBusinessENOptions"
@filter="typeBusinessENFilter"
:rules="[(val: string) => !!val || $t('form.error.required')]"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
<q-select
outlined
@ -146,10 +174,10 @@ let jobPositionFilter = selectFilterOptionRefMod(
map-options
hide-selected
hide-bottom-space
:hide-dropdown-icon="readonly"
input-debounce="0"
option-value="value"
option-label="label"
lazy-rules="ondemand"
v-model="jobPosition"
class="col-md-6 col-12"
:dense="dense"
@ -158,6 +186,40 @@ let jobPositionFilter = selectFilterOptionRefMod(
:options="jobPositionOptions"
:for="`${prefixId}-select-job-position`"
@filter="jobPositionFilter"
:rules="[(val: string) => !!val || $t('form.error.required')]"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
<q-select
outlined
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
:hide-dropdown-icon="readonly"
input-debounce="0"
option-value="value"
option-label="label"
v-model="jobPosition"
class="col-md-6 col-12"
:dense="dense"
:readonly="readonly"
:label="`${$t('customer.form.jobPosition')} (EN)`"
:options="jobPositionENOptions"
:for="`${prefixId}-input-job-position-en`"
:id="`${prefixId}-input-job-position-en`"
@filter="jobPositionENFilter"
:rules="[(val: string) => !!val || $t('form.error.required')]"
>
<template v-slot:no-option>
<q-item>
@ -169,20 +231,6 @@ let jobPositionFilter = selectFilterOptionRefMod(
</q-select>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-job-position-en`"
:id="`${prefixId}-input-job-position-en`"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-6 col-12"
:label="$t('customer.form.jobPositionEN')"
v-model="jobPositionEN"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-job-description`"
:id="`${prefixId}-input-job-description`"
:dense="dense"
@ -191,19 +239,41 @@ let jobPositionFilter = selectFilterOptionRefMod(
hide-bottom-space
class="col-md-6 col-12"
:label="$t('customer.form.jobDescription')"
v-model="jobDescription"
/>
<DatePicker
:id="`${prefixId}-date-picker-pay-date`"
v-model="payDate"
:label="$t('customer.form.payDay')"
:readonly="readonly"
clearable
:model-value="readonly ? jobDescription || '-' : jobDescription"
@update:model-value="
(v) => (typeof v === 'string' ? (jobDescription = v) : '')
"
/>
<q-input
:for="`${prefixId}-input-pay-rate`"
:id="`${prefixId}-input-pay-rate`"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-3 col-6"
:label="$t('customer.form.payDay')"
:model-value="readonly ? payDate || '-' : payDate"
@update:model-value="(v) => (typeof v === 'string' ? (payDate = v) : '')"
/>
<q-input
:for="`${prefixId}-input-pay-rate`"
:id="`${prefixId}-input-pay-rate`"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-3 col-6"
label="Pay day"
:model-value="readonly ? payDateEN || '-' : payDateEN"
@update:model-value="
(v) => (typeof v === 'string' ? (payDateEN = v) : '')
"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-pay-rate`"
:id="`${prefixId}-input-pay-rate`"
:dense="dense"
@ -216,16 +286,18 @@ let jobPositionFilter = selectFilterOptionRefMod(
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-sales-person`"
:id="`${prefixId}-input-sales-person`"
:for="`${prefixId}-input-pay-rate`"
:id="`${prefixId}-input-pay-rate`"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-6"
:label="$t('customer.form.salesPerson')"
v-model="saleEmployee"
class="col-md-3 col-6"
:label="`${$t('customer.form.payRate')} (Text)`"
:model-value="readonly ? wageRateText || '-' : wageRateText"
@update:model-value="
(v) => (typeof v === 'string' ? (wageRateText = v) : '')
"
/>
</div>
</template>

View file

@ -4,14 +4,15 @@ defineProps<{
prefixId?: string;
}>();
const contactName = defineModel<string>('contactName');
const mail = defineModel<string>('email');
const telephone = defineModel<string>('telephone');
const email = defineModel<string>('email');
const contactTel = defineModel<string>('contactTel');
const officeTel = defineModel<string>('officeTel');
const agent = defineModel<string>('agent');
</script>
<template>
<div class="col-md-9 col-12 row q-col-gutter-md">
<div class="col-md-9 col-12 row q-col-gutter-sm">
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-contact-name`"
:id="`${prefixId}-input-contact-name`"
dense
@ -20,11 +21,13 @@ const telephone = defineModel<string>('telephone');
hide-bottom-space
class="col-md-6 col-12"
:label="$t('customer.table.contactName')"
v-model="contactName"
:model-value="readonly ? contactName || '-' : contactName"
@update:model-value="
(v) => (typeof v === 'string' ? (contactName = v) : '')
"
/>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-mail`"
:id="`${prefixId}-input-mail`"
dense
@ -33,10 +36,29 @@ const telephone = defineModel<string>('telephone');
hide-bottom-space
class="col-md-6 col-12"
:label="$t('form.email')"
v-model="mail"
/>
:rules="
readonly
? undefined
: [
(v: string) =>
!v ||
/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g.test(v) ||
$t('form.error.invalid'),
]
"
:model-value="readonly ? email || '-' : email"
@update:model-value="(v) => (typeof v === 'string' ? (email = v) : '')"
>
<template #prepend>
<q-icon
size="xs"
name="mdi-email-outline"
class="cursor-pointer"
color="primary"
/>
</template>
</q-input>
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-telephone`"
:id="`${prefixId}-input-telephone`"
dense
@ -45,7 +67,56 @@ const telephone = defineModel<string>('telephone');
hide-bottom-space
class="col-md-6 col-12"
:label="$t('form.telephone')"
v-model="telephone"
:model-value="readonly ? contactTel || '-' : contactTel"
@update:model-value="
(v) => (typeof v === 'string' ? (contactTel = v) : '')
"
>
<template #prepend>
<q-icon
size="xs"
name="mdi-phone-outline"
class="cursor-pointer"
color="primary"
/>
</template>
</q-input>
<q-input
:for="`${prefixId}-input-telephone`"
:id="`${prefixId}-input-telephone`"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-6 col-12"
:label="$t('customer.form.headQuarters.telephoneNo')"
:model-value="readonly ? officeTel || '-' : officeTel"
@update:model-value="
(v) => (typeof v === 'string' ? (officeTel = v) : '')
"
>
<template #prepend>
<q-icon
size="xs"
name="mdi-phone-outline"
class="cursor-pointer"
color="primary"
/>
</template>
</q-input>
<q-input
:for="`${prefixId}-input-telephone`"
:id="`${prefixId}-input-telephone`"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-6 col-12"
:label="$t('customer.form.agent')"
:model-value="readonly ? agent || '-' : agent"
@update:model-value="(v) => (typeof v === 'string' ? (agent = v) : '')"
/>
</div>
</template>

View file

@ -34,6 +34,7 @@ export const countryCode = [
export const uploadFileListCustomer: {
label: string;
value: string;
_meta?: Record<string, any>;
}[] = [
{
label: 'customer.typeFile.citizenId',
@ -41,22 +42,22 @@ export const uploadFileListCustomer: {
},
{
label: 'customer.typeFile.registrationBook',
value: 'registrationBook',
value: 'house-registration',
},
{
label: 'customer.typeFile.houseMap',
value: 'houseMap',
value: 'vat-registration',
},
{
label: 'customer.typeFile.businessRegistration',
value: 'businessRegistration',
value: 'commercial-registration',
},
{
label: 'customer.typeFile.dbdCertificate',
value: 'dbdCertificate',
value: 'power-of-attorney',
},
{
@ -78,6 +79,7 @@ export const uploadFileListCustomer: {
export const uploadFileListEmployee: {
label: string;
value: string;
_meta?: Record<string, any>;
}[] = [
{
label: 'customerEmployee.fileType.passport',
@ -86,6 +88,17 @@ export const uploadFileListEmployee: {
{
label: 'customerEmployee.fileType.visa',
value: 'visa',
_meta: {
number: '',
type: '',
entryCount: 0,
issueCountry: '',
issuePlace: '',
issueDate: new Date(),
expireDate: new Date(),
mrz: '',
remark: '',
},
},
{
label: 'customerEmployee.fileType.tm6',
@ -93,7 +106,7 @@ export const uploadFileListEmployee: {
},
{
label: 'customerEmployee.fileType.workPermit',
value: 'workPermit',
value: 'other',
},
{
label: 'customerEmployee.fileType.noticeJobEmployment',
@ -101,19 +114,19 @@ export const uploadFileListEmployee: {
},
{
label: 'customerEmployee.fileType.noticeJobEntry',
value: 'noticeJobEntry',
value: 'other',
},
{
label: 'customerEmployee.fileType.historyJob',
value: 'historyJob',
value: 'other',
},
{
label: 'customerEmployee.fileType.acceptJob',
value: 'acceptJob',
value: 'other',
},
{
label: 'customerEmployee.fileType.receipt',
value: 'receipt',
value: 'other',
},
{
label: 'customerEmployee.fileType.other',
@ -134,6 +147,33 @@ export const formMenuIconEmployee = [
},
];
export const columnsAttachment = [
{
name: 'orderNumber',
align: 'center',
label: 'general.orderNumber',
field: 'branchNo',
},
{
name: 'document',
align: 'center',
label: 'general.document',
field: 'attachmentName',
},
{
name: 'uploadDate',
align: 'center',
label: 'general.uploadDate',
field: 'uploadDate',
},
{
name: 'action',
label: '',
field: 'action',
},
] satisfies QTableProps['columns'];
export const columnsEmployee = [
{
name: 'orderNumber',
@ -207,10 +247,10 @@ export const columnsCustomer = [
},
{
name: 'customerName',
name: 'titleName',
align: 'left',
label: 'customer.table.fullname',
field: 'customerName',
label: 'customer.table.titleName',
field: 'titleName',
sortable: true,
},
@ -223,30 +263,33 @@ export const columnsCustomer = [
},
{
name: 'address',
align: 'left',
label: 'customer.table.address',
field: 'address',
name: 'jobPosition',
align: 'center',
label: 'customer.table.jobPosition',
field: 'jobPosition',
sortable: true,
},
{
name: 'workPlace',
align: 'left',
label: 'customer.table.workPlace',
field: 'workPlace',
name: 'address',
align: 'center',
label: 'customer.table.address',
field: 'address',
sortable: true,
},
{
name: 'contactName',
align: 'left',
align: 'center',
label: 'customer.table.contactName',
field: 'contactName',
sortable: true,
},
{
name: 'contactPhone',
name: 'officeTel',
align: 'left',
label: 'customer.table.contactPhone',
field: 'contactPhone',
label: 'customer.table.officeTel',
field: 'officeTel',
},
] satisfies QTableProps['columns'];

View file

@ -9,26 +9,28 @@ import useCustomerStore from 'stores/customer';
import useEmployeeStore from 'stores/employee';
import useFlowStore from 'stores/flow';
import { baseUrl } from 'src/stores/utils';
export const useCustomerForm = defineStore('form-customer', () => {
const { t } = useI18n();
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const customerStore = useCustomerStore();
const branchStore = useMyBranch();
const defaultFormData: CustomerCreate = {
code: '',
// code: '',
// namePrefix: '',
// firstName: '',
// lastName: '',
// firstNameEN: '',
// lastNameEN: '',
// gender: '',
// birthDate: new Date(),
customerBranch: [],
selectedImage: '',
status: 'CREATED',
customerType: 'CORP',
namePrefix: '',
firstName: '',
lastName: '',
firstNameEN: '',
lastNameEN: '',
gender: '',
birthDate: new Date(),
registeredBranchId: branchStore.currentMyBranch?.id || '',
customerBranch: [],
image: null,
};
let resetFormData = structuredClone(defaultFormData);
@ -50,6 +52,7 @@ export const useCustomerForm = defineStore('form-customer', () => {
editCustomerBranchId?: string;
treeFile: { label: string; file: { label: string }[] }[];
formDataOcr: Record<string, any>;
isImageEdit: boolean;
}>({
dialogType: 'info',
dialogOpen: false,
@ -65,6 +68,7 @@ export const useCustomerForm = defineStore('form-customer', () => {
defaultCustomerImageUrl: '',
treeFile: [],
formDataOcr: {},
isImageEdit: false,
});
watch(
@ -83,9 +87,10 @@ export const useCustomerForm = defineStore('form-customer', () => {
}
function isFormDataDifferent() {
return (
JSON.stringify(resetFormData) !== JSON.stringify(currentFormData.value)
);
const { status: resetStatus, ...resetData } = resetFormData;
const { status: currStatus, ...currData } = currentFormData.value;
return JSON.stringify(resetData) !== JSON.stringify(currData);
}
function resetForm(clean = false) {
@ -110,8 +115,9 @@ export const useCustomerForm = defineStore('form-customer', () => {
if (state.value.dialogType === 'create') {
state.value.editCustomerId = '';
}
const currentImg = currentFormData.value.selectedImage;
currentFormData.value = structuredClone(resetFormData);
currentFormData.value.selectedImage = currentImg;
}
async function assignFormData(id?: string) {
@ -129,56 +135,74 @@ export const useCustomerForm = defineStore('form-customer', () => {
state.value.dialogType = 'edit';
state.value.editCustomerId = id;
state.value.editCustomerCode = data.code;
state.value.customerImageUrl = `${apiBaseUrl}/customer/${id}/image`;
state.value.defaultCustomerImageUrl = `${apiBaseUrl}/customer/${id}/image`;
state.value.customerImageUrl = `${baseUrl}/customer/${id}/image/${data.selectedImage}`;
state.value.defaultCustomerImageUrl = `${baseUrl}/customer/${id}/image/${data.selectedImage}`;
resetFormData.registeredBranchId = data.registeredBranchId;
resetFormData.code = data.code || '';
resetFormData.status = data.status;
resetFormData.customerType = data.customerType;
resetFormData.namePrefix = data.namePrefix;
resetFormData.firstName = data.firstName;
resetFormData.lastName = data.lastName;
resetFormData.firstNameEN = data.firstNameEN;
resetFormData.lastNameEN = data.lastNameEN;
resetFormData.gender = data.gender;
resetFormData.birthDate = new Date(data.birthDate);
resetFormData.image = null;
resetFormData.selectedImage = data.selectedImage;
resetFormData.customerBranch = await Promise.all(
data.branch.map(async (v) => ({
id: v.id,
code: v.code || '',
customerCode: '',
provinceId: v.provinceId,
districtId: v.districtId,
subDistrictId: v.subDistrictId,
firstName: v.firstName,
firstNameEN: v.firstNameEN,
lastName: v.lastName,
lastNameEN: v.lastNameEN,
gender: v.gender,
birthDate: v.birthDate,
namePrefix: v.namePrefix,
wageRate: v.wageRate,
payDate: new Date(v.payDate), // Convert the string to a Date object
saleEmployee: v.saleEmployee,
wageRateText: v.wageRateText,
payDate: v.payDate,
jobDescription: v.jobDescription,
jobPositionEN: v.jobPositionEN,
jobPosition: v.jobPosition,
businessTypeEN: v.businessTypeEN,
businessType: v.businessType,
employmentOffice: v.employmentOffice,
employmentOfficeEN: v.employmentOfficeEN,
telephoneNo: v.telephoneNo,
contactName: v.contactName,
email: v.email,
subDistrictId: v.subDistrictId,
districtId: v.districtId,
provinceId: v.provinceId,
streetEN: v.streetEN,
street: v.street,
mooEN: v.mooEN,
moo: v.moo,
soiEN: v.soiEN,
soi: v.soi,
addressEN: v.addressEN,
address: v.address,
workplaceEN: v.workplaceEN,
workplace: v.workplace,
authorizedCapital: v.authorizedCapital,
registerDate: v.registerDate,
registerNameEN: v.registerNameEN,
registerName: v.registerName,
legalPersonNo: v.legalPersonNo,
citizenId: v.citizenId,
codeCustomer: v.codeCustomer,
updatedByUserId: v.updatedByUserId,
updatedAt: v.updatedAt,
createdByUserId: v.createdByUserId,
createdAt: v.createdAt,
code: v.code,
statusOrder: v.statusOrder,
status: v.status,
customerId: v.customerId,
citizenId: v.citizenId || '',
authorizedCapital: v.authorizedCapital || '',
registerDate: new Date(v.registerDate), // Convert the string to a Date object
registerNameEN: v.registerNameEN || '',
registerName: v.registerName || '',
legalPersonNo: v.legalPersonNo || '',
registerCompanyName: '',
id: v.id,
homeCode: v.homeCode,
contactTel: v.contactTel,
officeTel: v.officeTel,
agent: v.agent,
customerName: v.customerName,
authorizedName: v.authorizedName,
authorizedNameEN: v.authorizedNameEN,
payDateEN: v.payDateEN,
statusSave: true,
contactName: v.contactName || '',
file: await customerStore.listAttachment(v.id).then(async (r) => {
if (r) {
return await Promise.all(
@ -202,68 +226,91 @@ export const useCustomerForm = defineStore('form-customer', () => {
currentFormData.value = structuredClone(resetFormData);
}
function addCurrentCustomerBranch() {
async function addCurrentCustomerBranch() {
if (currentFormData.value.customerBranch?.some((v) => !v.id)) return;
currentFormData.value.customerBranch?.push({
id: '',
code:
currentFormData.value.customerBranch.length !== 0
? currentFormData.value.customerBranch?.[0].code === null
? ''
: currentFormData.value.customerBranch?.[0].code
: '',
customerCode: '',
provinceId: '',
districtId: '',
subDistrictId: '',
wageRate: 0,
payDate: new Date(), // Convert the string to a Date object
saleEmployee: '',
jobDescription: '',
jobPositionEN: '',
jobPosition: '',
businessTypeEN: '',
businessType: '',
employmentOffice: '',
telephoneNo: '',
email: '',
addressEN: '',
address: '',
workplaceEN: '',
workplace: '',
status: 'CREATED',
customerId: '',
citizenId:
branchCode:
currentFormData.value.customerBranch.length !== 0
? currentFormData.value.customerBranch?.[0].citizenId === null
? currentFormData.value.customerBranch?.[0].branchCode === null
? ''
: currentFormData.value.customerBranch?.[0].citizenId
: currentFormData.value.customerBranch?.[0].branchCode
: '',
authorizedCapital: '',
registerDate: new Date(), // Convert the string to a Date object
registerNameEN: '',
registerName: '',
codeCustomer: '',
legalPersonNo:
currentFormData.value.customerBranch.length !== 0
? currentFormData.value.customerBranch?.[0].legalPersonNo === null
? ''
: currentFormData.value.customerBranch?.[0].legalPersonNo
: '',
registerCompanyName: '',
citizenId:
currentFormData.value.customerBranch.length !== 0
? currentFormData.value.customerBranch?.[0].citizenId === null
? ''
: currentFormData.value.customerBranch?.[0].citizenId
: '',
namePrefix: '',
firstName: '',
lastName: '',
firstNameEN: '',
lastNameEN: '',
telephoneNo: '',
gender: '',
birthDate: '',
businessType: '',
jobPosition: '',
jobDescription: '',
payDate: '',
payDateEN: '',
wageRate: 0,
wageRateText: '',
homeCode: '',
employmentOffice: '',
employmentOfficeEN: '',
address: '',
addressEN: '',
street: '',
streetEN: '',
moo: '',
mooEN: '',
soi: '',
soiEN: '',
provinceId: '',
districtId: '',
subDistrictId: '',
contactName: '',
email: '',
contactTel: '',
officeTel: '',
agent: '',
status: 'CREATED',
customerName: '',
registerName: '',
registerNameEN: '',
registerDate: null,
authorizedCapital: '',
authorizedName: '',
authorizedNameEN: '',
file: [],
});
state.value.branchIndex =
(currentFormData.value.customerBranch?.length || 0) - 1;
}
async function submitFormCustomer() {
async function submitFormCustomer(imgList?: {
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}) {
if (state.value.dialogType === 'info') return;
if (state.value.dialogType === 'create') {
const _data = await customerStore.create(currentFormData.value);
const _data = await customerStore.create(currentFormData.value, imgList);
if (_data) await assignFormData(_data.id);
@ -313,38 +360,63 @@ export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
const customerStore = useCustomerStore();
const customerFormStore = useCustomerForm();
const defaultFormData: CustomerBranchCreate & { id?: string } = {
code: '',
customerCode: '',
const defaultFormData: CustomerBranchCreate & {
id?: string;
codeCustomer?: string;
} = {
id: '',
customerId: '',
// branchCode: '',
// codeCustomer: '',
legalPersonNo: '',
citizenId: '',
namePrefix: '',
firstName: '',
lastName: '',
firstNameEN: '',
lastNameEN: '',
telephoneNo: '',
gender: '',
birthDate: '',
businessType: '',
jobPosition: '',
jobDescription: '',
payDate: '',
payDateEN: '',
wageRate: 0,
wageRateText: '',
homeCode: '',
employmentOffice: '',
employmentOfficeEN: '',
address: '',
addressEN: '',
street: '',
streetEN: '',
moo: '',
mooEN: '',
soi: '',
soiEN: '',
provinceId: '',
districtId: '',
subDistrictId: '',
wageRate: 0,
payDate: new Date(), // Convert the string to a Date object
saleEmployee: '',
jobDescription: '',
jobPositionEN: '',
jobPosition: '',
businessTypeEN: '',
businessType: '',
employmentOffice: '',
telephoneNo: '',
email: '',
addressEN: '',
address: '',
workplaceEN: '',
workplace: '',
status: 'CREATED',
customerId: '',
citizenId: '',
authorizedCapital: '',
registerDate: new Date(), // Convert the string to a Date object
registerNameEN: '',
registerName: '',
legalPersonNo: '',
registerCompanyName: '',
statusSave: false,
contactName: '',
email: '',
contactTel: '',
officeTel: '',
agent: '',
status: 'CREATED',
customerName: '',
registerName: '',
registerNameEN: '',
registerDate: null,
authorizedCapital: '',
authorizedName: '',
authorizedNameEN: '',
file: [],
};
@ -378,28 +450,25 @@ export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
const _data = await customerStore.getBranchById(id);
if (!_data) return;
resetFormData = {
id: _data.id,
code: _data.code,
customerCode: '',
provinceId: _data.provinceId,
districtId: _data.districtId,
subDistrictId: _data.subDistrictId,
wageRate: _data.wageRate,
payDate: new Date(_data.payDate), // Convert the string to a Date object
saleEmployee: _data.saleEmployee,
payDate: _data.payDate, // Convert the string to a Date object
payDateEN: _data.payDateEN,
jobDescription: _data.jobDescription,
jobPositionEN: _data.jobPositionEN,
jobPosition: _data.jobPosition,
businessTypeEN: _data.businessTypeEN,
businessType: _data.businessType,
employmentOffice: _data.employmentOffice,
employmentOfficeEN: _data.employmentOfficeEN,
telephoneNo: _data.telephoneNo,
email: _data.email,
addressEN: _data.addressEN,
address: _data.address,
workplaceEN: _data.workplaceEN,
workplace: _data.workplace,
status: 'CREATED',
customerId: _data.customerId,
citizenId: _data.citizenId,
@ -409,7 +478,28 @@ export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
registerName: _data.registerName,
legalPersonNo: _data.legalPersonNo,
contactName: _data.contactName,
registerCompanyName: '',
namePrefix: _data.namePrefix,
firstName: _data.firstName,
firstNameEN: _data.firstNameEN,
lastName: _data.lastName,
lastNameEN: _data.lastNameEN,
gender: _data.gender,
birthDate: _data.birthDate,
moo: _data.moo,
mooEN: _data.mooEN,
soi: _data.soi,
soiEN: _data.soiEN,
street: _data.street,
streetEN: _data.streetEN,
wageRateText: _data.wageRateText,
contactTel: _data.contactTel,
officeTel: _data.officeTel,
agent: _data.agent,
codeCustomer: _data.codeCustomer,
customerName: _data.customerName,
homeCode: _data.homeCode,
authorizedName: _data.authorizedName,
authorizedNameEN: _data.authorizedNameEN,
statusSave: false,
file: [],
};
@ -433,6 +523,7 @@ export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
);
async function submitForm() {
console.log(currentFormData.value);
if (!state.value.currentCustomerId) {
throw new Error(
'Employer id cannot be found. Did you properly set employer id?',
@ -479,6 +570,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
currentTab: string;
dialogModal: boolean;
drawerModal: boolean;
isImageEdit: boolean;
currentEmployeeCode: string;
currentEmployee: Employee | null;
@ -511,6 +603,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
| undefined;
ocr: boolean;
}>({
isImageEdit: false,
currentIndex: -1,
statusSavePersonal: false,
drawerModal: false,
@ -533,7 +626,6 @@ export const useEmployeeForm = defineStore('form-employee', () => {
const defaultFormData: EmployeeCreate = {
id: '',
code: '',
image: null,
customerBranchId: '',
nrcNo: '',
dateOfBirth: null,
@ -553,24 +645,6 @@ export const useEmployeeForm = defineStore('form-employee', () => {
address: '',
zipCode: '',
passportType: '',
passportNumber: '',
passportIssueDate: null,
passportExpiryDate: null,
passportIssuingCountry: '',
passportIssuingPlace: '',
previousPassportReference: '',
visaType: '',
visaNumber: '',
visaIssueDate: null,
visaExpiryDate: null,
visaIssuingPlace: '',
visaStayUntilDate: null,
tm6Number: '',
entryDate: null,
workerStatus: '',
subDistrictId: '',
districtId: '',
provinceId: '',
@ -615,6 +689,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
motherLastNameEN: '',
motherBirthPlace: '',
},
image: null,
};
let resetEmployeeData = structuredClone(defaultFormData);
@ -642,6 +717,9 @@ export const useEmployeeForm = defineStore('form-employee', () => {
resetEmployeeData = structuredClone(defaultFormData);
state.value.statusSavePersonal = false;
state.value.profileUrl = '';
} else {
resetEmployeeData.selectedImage =
currentFromDataEmployee.value.selectedImage;
}
currentFromDataEmployee.value = structuredClone(resetEmployeeData);
}
@ -782,20 +860,27 @@ export const useEmployeeForm = defineStore('form-employee', () => {
await assignFormDataEmployee(currentFromDataEmployee.value.id);
}
async function submitPersonal() {
async function submitPersonal(imgList?: {
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}) {
if (state.value.dialogType === 'create') {
const res = await employeeStore.create({
...currentFromDataEmployee.value,
customerBranchId: state.value.formDataEmployeeOwner?.id || '',
const res = await employeeStore.create(
{
...currentFromDataEmployee.value,
customerBranchId: state.value.formDataEmployeeOwner?.id || '',
employeeWork: [],
employeeCheckup: [],
employeeOtherInfo: undefined,
});
employeeWork: [],
employeeCheckup: [],
employeeOtherInfo: undefined,
},
imgList,
);
if (res) {
await assignFormDataEmployee(res.id);
currentFromDataEmployee.value.id = res.id;
currentFromDataEmployee.value.file = res.file;
state.value.statusSavePersonal = true;
}
}
@ -890,7 +975,6 @@ export const useEmployeeForm = defineStore('form-employee', () => {
}),
)
: [],
image: null,
};
currentFromDataEmployee.value = structuredClone(resetEmployeeData);
@ -900,7 +984,8 @@ export const useEmployeeForm = defineStore('form-employee', () => {
state.value.currentEmployeeCode = payload.code;
state.value.profileUrl = profileImageUrl || '';
state.value.profileUrl =
`${baseUrl}/employee/${id}/image/${_data.selectedImage}` || '';
profileImageUrl
? (state.value.profileSubmit = true)

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,206 @@
<script lang="ts" setup>
import { ref } from 'vue';
import QuatationForm from './QuatationForm.vue';
import SideMenu from 'components/SideMenu.vue';
import ImageUploadDialog from 'components/ImageUploadDialog.vue';
import { watch } from 'vue';
import { onMounted, ref } from 'vue';
const isOpen = ref(true);
const imageUploadDialog = ref<InstanceType<typeof ImageUploadDialog>>();
const file = ref<File | null>(null);
import { productTreeDecoration } from './constants';
import useProductServiceStore from 'src/stores/product-service';
import {
ProductGroup,
ProductList,
Service,
} from 'src/stores/product-service/types';
import QuotationForm from './QuotationForm.vue';
import TreeView from 'src/components/shared/TreeView.vue';
import { AddButton } from 'src/components/button';
import MainButton from 'src/components/button/MainButton.vue';
const dialog = ref(true);
const nodes = ref([
{
title: 'กลุ่มสินค้าและบริการที่ 1',
subtitle: 'TG01000000001',
selected: false,
children: [
{
title: 'งานที่ 1',
subtitle: 'TG01000000001',
selected: false,
children: [
{
title: 'สินค้า 1',
subtitle: 'TG01000000001',
selected: false,
},
{
title: 'สินค้า 2',
subtitle: 'TG01000000001',
selected: false,
},
{
title: 'สินค้า 3',
subtitle: 'TG01000000001',
selected: false,
},
{
title: 'สินค้า 4',
subtitle: 'TG01000000001',
selected: false,
},
{
title: 'สินค้า 5',
subtitle: 'TG01000000001',
selected: false,
},
{
title: 'สินค้า 6',
subtitle: 'TG01000000001',
selected: false,
},
],
},
],
},
]);
const productServiceStore = useProductServiceStore();
type ProductGroupId = string;
const productGroup = ref<ProductGroup[]>([]);
const productList = ref<Partial<Record<ProductGroupId, ProductList[]>>>({});
const serviceList = ref<Partial<Record<ProductGroupId, Service[]>>>({});
type Id = string;
const product = ref<Record<Id, ProductList>>({});
const service = ref<Record<Id, Service>>({});
const selectedGroup = ref<ProductGroup | null>(null);
const selectedGroupSub = ref<'product' | 'service' | null>(null);
const selectedProductServiceId = ref('');
onMounted(async () => {
const ret = await productServiceStore.fetchListProductService({
page: 1,
pageSize: 9999,
});
if (ret) productGroup.value = ret.result;
});
async function getAllProduct(
groupId: string,
opts?: { force?: false; page?: number; pageSize?: number },
) {
selectedGroupSub.value = 'product';
if (!opts?.force && productList.value[groupId] !== undefined) return;
const ret = await productServiceStore.fetchListProduct({
page: opts?.page ?? 1,
pageSize: opts?.pageSize ?? 9999,
productGroupId: groupId,
});
if (ret) productList.value[groupId] = ret.result;
}
async function getAllService(
groupId: string,
opts?: { force?: false; page?: number; pageSize?: number },
) {
selectedGroupSub.value = 'service';
if (!opts?.force && serviceList.value[groupId] !== undefined) return;
const ret = await productServiceStore.fetchListService({
page: opts?.page ?? 1,
pageSize: opts?.pageSize ?? 9999,
productGroupId: groupId,
fullDetail: true,
});
if (ret) serviceList.value[groupId] = ret.result;
}
async function getProduct(id: string, force = false) {
selectedGroupSub.value = 'product';
selectedProductServiceId.value = id;
if (!force && product.value[id] !== undefined) return;
const ret = await productServiceStore.fetchListProductById(id);
if (ret) product.value[id] = ret;
}
async function getService(id: string, force = false) {
selectedGroupSub.value = 'service';
selectedProductServiceId.value = id;
if (!force && service.value[id] !== undefined) return;
const ret = await productServiceStore.fetchListServiceById(id);
if (ret) service.value[id] = ret;
}
function convertToTree() {
// TODO: convert product or service into selectable tree
// NOTE: this is meant to be used inside getService() and getProduct() before return and after return
}
</script>
<template>
<ImageUploadDialog
<div v-for="item in productGroup" class="row items-center">
{{ item.name }}
{{ item.code }}
<AddButton icon-only @click="selectedGroup = item" />
</div>
<template v-if="selectedGroup">
<MainButton
icon="mdi-check"
color="var(--blue-6-hsl)"
@click="getAllProduct(selectedGroup.id)"
>
Product
</MainButton>
<MainButton
icon="mdi-check"
color="var(--blue-6-hsl)"
@click="getAllService(selectedGroup.id)"
>
Service
</MainButton>
<div
v-if="selectedGroupSub === 'product' && productList[selectedGroup.id]"
v-for="item in productList[selectedGroup.id]"
class="row items-center"
>
{{ item.name }}
{{ item.code }}
<AddButton icon-only @click="getProduct(item.id)" />
</div>
<div
v-if="selectedGroupSub === 'service' && serviceList[selectedGroup.id]"
v-for="item in serviceList[selectedGroup.id]"
class="row items-center"
>
{{ item.name }}
{{ item.code }}
<AddButton icon-only @click="getService(item.id)" />
</div>
<template
v-if="selectedGroupSub === 'service' && service[selectedProductServiceId]"
>
{{ service[selectedProductServiceId] }}
</template>
<template
v-if="selectedGroupSub === 'product' && product[selectedProductServiceId]"
>
{{ product[selectedProductServiceId] }}
</template>
</template>
<div class="surface-1 rounded full-height q-pa-md">
<TreeView
:decoration="productTreeDecoration"
v-model:nodes="nodes"
expandable
/>
</div>
<!-- <ImageUploadDialog
v-model:dialog-state="isOpen"
v-model:file="file"
ref="imageUploadDialog"
@ -55,8 +244,8 @@ const file = ref<File | null>(null);
id="my-anchor"
>
My Menu
</div>
<QuatationForm v-model:dialog-state="isOpen" />
</div> -->
<QuotationForm v-model:dialog-state="dialog" readonly />
</template>
<style scoped></style>

View file

@ -1,200 +0,0 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Icon } from '@iconify/vue';
import MainDialog from 'components/05_quotation/MainDialog.vue';
import WorkerItem from 'components/05_quotation/WorkerItem.vue';
import AppBox from 'components/app/AppBox.vue';
const dialogState = defineModel<boolean>({ default: true });
const selectedBranchIssuer = ref('');
const selectedCustomer = ref('');
const toggleWorker = ref(true);
</script>
<template>
<MainDialog v-model:state="dialogState">
<template #title>
<span class="text-weight-medium" style="color: var(--brand-1)">
{{ $t('quotation.form.createTitle') }}
</span>
</template>
<div class="row q-pa-md items-center">
<div style="flex: 1"><q-img src="/logo.png" width="8rem" /></div>
<q-select
lazy-rules="ondemand"
v-model="selectedBranchIssuer"
:options="[{ label: 'Issuer 1', value: 'Issuer 1' }]"
:label="$t('quotation.form.customerBranchSelect')"
id="select-branch-issuer"
for="select-branch-issuer"
style="width: 300px"
class="q-mr-md"
outlined
dense
/>
<q-select
lazy-rules="ondemand"
v-model="selectedCustomer"
:options="[{ label: 'Customer 1', value: 'Customer 1' }]"
:label="$t('quotation.form.customerSelect')"
id="select-customer"
for="select-customer"
style="width: 300px"
outlined
dense
/>
</div>
<div
class="surface-2 bordered-t row"
style="align-items: stretch; overflow-y: auto"
>
<div
class="col-12 col-sm-9 row items-stretch"
style="padding: var(--size-3)"
>
<AppBox
class="full-width"
bordered
style="
padding: var(--size-3);
max-height: calc(100vh - 300px - var(--size-3) * 2);
overflow-y: auto;
"
>
<div class="row items-center q-mb-sm">
<span style="flex: 1">
{{ $t('quotation.form.listWorker') }}
<q-toggle
v-model="toggleWorker"
id="toggle-status"
size="md"
padding="none"
class="q-ml-md"
dense
/>
</span>
<button
id="add-btn-plus"
class="row items-center"
style="border: none; background: transparent"
>
<Icon
height="24"
icon="pixelarticons:plus"
class="app-text-info cursor-pointer"
/>
</button>
</div>
<AppBox class="surface-2 worker-list" bordered>
<WorkerItem
:data="{
no: 1,
refNo: 'CC6613334',
nationality: 'Thai',
birthDate: '1 May 2001',
age: '23 Years',
fullName: 'Methapon Metanipat',
docExpireDate: '16 May 2025',
}"
color="male"
/>
<WorkerItem
:data="{
no: 1,
refNo: 'CC6613334',
nationality: 'Thai',
birthDate: '1 May 2001',
age: '23 Years',
fullName: 'Methapon Metanipat',
docExpireDate: '16 May 2025',
}"
color="male"
/>
<q-tabs
inline-label
mobile-arrows
dense
align="left"
class="full-width"
active-color="info"
>
<q-tab name="ALL">
<div class="row">{{ $t('all') }}</div>
</q-tab>
<q-tab name="USER">
<div class="row">
{{ $t('USER') }}
</div>
</q-tab>
<q-tab name="MESSENGER">
<div class="row">
{{ $t('MESSENGER') }}
</div>
</q-tab>
<q-tab name="DELEGATE">
<div class="row">
{{ $t('DELEGATE') }}
</div>
</q-tab>
<q-tab name="AGENCY">
<div class="row">
{{ $t('AGENCY') }}
</div>
</q-tab>
</q-tabs>
</AppBox>
</AppBox>
</div>
<div
class="col-12 col-sm-3"
style="
padding: var(--size-3);
padding-left: 0;
overflow-y: auto;
max-height: calc(100vh - 300px);
"
>
<AppBox bordered class="column" style="gap: var(--size-3)">
<div class="rounded bordered q-px-md q-py-sm row">
<div class="col-4">{{ $t('quotation.form.labelNo') }}</div>
</div>
<div class="rounded bordered q-px-md q-py-sm row">
<div class="col-4">{{ $t('quotation.form.labelDate') }}</div>
</div>
<div class="rounded bordered q-px-md q-py-sm row">
<div class="col-4">{{ $t('quotation.form.labelTime') }}</div>
</div>
<div class="rounded bordered q-px-md q-py-sm row">
<div class="col-4">{{ $t('quotation.form.labelProcesser') }}</div>
</div>
</AppBox>
</div>
</div>
<template #footer>
<div class="row justify-end full-width">
<q-btn
color="primary"
dense
no-caps
:label="$t('quotation.form.buttonSave')"
class="q-px-md"
/>
</div>
</template>
</MainDialog>
</template>
<style scoped>
.worker-list > :deep(*:not(:last-child)) {
margin-bottom: var(--size-2);
}
</style>

View file

@ -0,0 +1,180 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { QSelect } from 'quasar';
import AppBox from 'components/app/AppBox.vue';
import MainDialog from 'components/05_quotation/MainDialog.vue';
import WorkerItem from 'components/05_quotation/WorkerItem.vue';
import QuotationFormInfo from './QuotationFormInfo.vue';
import { AddButton, SaveButton } from 'components/button';
defineProps<{
readonly?: boolean;
}>();
const dialogState = defineModel<boolean>({ default: true });
const selectedBranchIssuer = ref('');
const selectedCustomer = ref('');
const toggleWorker = ref(true);
const quotationNo = ref('');
const actor = ref('');
const workName = ref('');
const contactor = ref('');
const telephone = ref('');
const documentReceivePoint = ref('');
const dueDate = ref('');
const payType = ref('');
const paySplitCount = ref('');
const payBank = ref('');
</script>
<template>
<MainDialog v-model:state="dialogState">
<template #title>
<span class="text-weight-medium" style="color: var(--brand-1)">
{{ $t('quotation.form.createTitle') }}
</span>
</template>
<div class="row q-pa-md items-center">
<div style="flex: 1"><q-img src="/logo.png" width="8rem" /></div>
<q-select
v-model="selectedBranchIssuer"
:options="[{ label: 'Issuer 1', value: 'Issuer 1' }]"
:label="$t('quotation.form.customerBranchSelect')"
id="select-branch-issuer"
for="select-branch-issuer"
style="width: 300px"
class="q-mr-md"
outlined
dense
/>
<q-select
v-model="selectedCustomer"
:options="[{ label: 'Customer 1', value: 'Customer 1' }]"
:label="$t('quotation.form.customerSelect')"
id="select-customer"
for="select-customer"
style="width: 300px"
outlined
dense
/>
</div>
<div
class="surface-2 bordered-t row"
style="flex-grow: 1; overflow-y: hidden"
:style="{
overflowY: $q.screen.gt.sm ? 'hidden' : 'auto',
}"
>
<div
class="col-12 col-sm-9 row"
style="padding: var(--size-3); overflow-y: auto"
:style="{
paddingRight: $q.screen.gt.sm ? 'var(--size-2)' : 'var(--size-3)',
maxHeight: $q.screen.gt.sm ? '100%' : undefined,
}"
>
<div class="col">
<AppBox bordered style="padding: var(--size-3)">
<div class="row items-center q-mb-sm">
<span style="flex: 1">
{{ $t('quotation.label.listWorker') }}
<q-toggle
v-model="toggleWorker"
id="toggle-status"
size="md"
padding="none"
class="q-ml-md"
dense
/>
</span>
<AddButton icon-only />
</div>
<AppBox class="surface-2 worker-list" v-if="toggleWorker" bordered>
<WorkerItem
v-for="_ in Array(20)"
:data="{
no: 1,
refNo: 'CC6613334',
nationality: 'Thai',
birthDate: '1 May 2001',
age: '23 Years',
fullName: 'Methapon Metanipat',
docExpireDate: '16 May 2025',
}"
color="male"
/>
</AppBox>
</AppBox>
</div>
</div>
<div
class="col-12 col-sm-3"
style="padding: var(--size-3); overflow-y: auto"
:style="{
paddingLeft: $q.screen.gt.sm ? 'var(--size-1)' : 'var(--size-3)',
maxHeight: $q.screen.gt.sm ? '100%' : undefined,
}"
>
<QuotationFormInfo
v-model:quotation-no="quotationNo"
v-model:actor="actor"
v-model:work-name="workName"
v-model:contactor="contactor"
v-model:telephone="telephone"
v-model:document-receive-point="documentReceivePoint"
v-model:due-date="dueDate"
v-model:pay-type="payType"
v-model:pay-bank="payBank"
v-model:pay-split-count="paySplitCount"
:readonly
/>
</div>
</div>
<template #footer>
<div class="row justify-end full-width">
<SaveButton />
</div>
</template>
</MainDialog>
</template>
<style scoped>
.worker-list > :deep(*:not(:last-child)) {
margin-bottom: var(--size-2);
}
.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);
}
.bg-color-orange {
--_color: var(--yellow-7-hsl);
color: white;
background: hsla(var(--_color));
}
.dark .bg-color-orange {
--_color: var(--orange-6-hsl);
}
.bg-color-orange-light {
--_color: var(--yellow-7-hsl);
background: hsla(var(--_color) / 0.2);
}
.dark .bg-color-orange {
--_color: var(--orange-6-hsl / 0.2);
}
</style>

View file

@ -0,0 +1,328 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue';
import { QSelect } from 'quasar';
import { selectFilterOptionRefMod } from 'src/stores/utils';
import { onMounted, ref, watch } from 'vue';
import AppBox from 'components/app/AppBox.vue';
import useOptionStore from 'src/stores/options';
defineProps<{
readonly?: boolean;
data?: {
total: number;
discount: number;
totalVatExcluded: number;
totalVatIncluded: number;
totalAfterDiscount: number;
};
}>();
const quotationNo = defineModel<string>('quotationNo', { required: true });
const actor = defineModel<string>('actor', { required: true });
const workName = defineModel<string>('workName', { required: true });
const contactor = defineModel<string>('contactor', { required: true });
const telephone = defineModel<string>('telephone', { required: true });
const documentReceivePoint = defineModel<string>('documentReceivePoint', {
required: true,
});
const dueDate = defineModel<string>('dueDate', { required: true });
const payType = defineModel<string>('payType', { required: true });
const paySplitCount = defineModel<string>('paySplitCount', { required: true });
const payBank = defineModel<string>('payBank', { required: true });
const optionStore = useOptionStore();
const bankBookOptions = ref<Record<string, unknown>[]>([]);
let bankBoookFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
onMounted(() => {
if (optionStore.globalOption) {
bankBoookFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.bankBook),
bankBookOptions,
'label',
);
}
});
watch(
() => optionStore.globalOption,
() => {
bankBoookFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.bankBook),
bankBookOptions,
'label',
);
},
);
</script>
<template>
<AppBox no-padding bordered class="column">
<div
class="bordered-b q-px-md q-py-sm row bg-color-orange-light items-center"
>
<div class="icon-wrapper bg-color-orange q-mr-sm">
<q-icon name="mdi-file-outline" />
</div>
<span class="text-weight-bold">
{{ $t('quotation.label.infoDocument') }}
</span>
</div>
<div class="q-pa-sm">
<div class="row q-col-gutter-sm">
<q-input
:label="$t('quotation.label.quotationNo')"
:readonly
v-model="quotationNo"
class="col-12"
dense
outlined
/>
<q-input
:label="$t('quotation.label.actor')"
:readonly
v-model="actor"
class="col-12"
dense
outlined
/>
<q-input
:label="$t('quotation.label.workName')"
:readonly
v-model="workName"
class="col-12"
dense
outlined
/>
<q-input
:label="$t('quotation.label.contactor')"
:readonly
v-model="contactor"
class="col-12"
dense
outlined
/>
<q-input
:label="$t('quotation.label.telephone')"
:readonly="readonly"
v-model="telephone"
class="col-12"
dense
outlined
/>
<q-input
:label="$t('quotation.label.documentReceivePoint')"
:readonly
v-model="documentReceivePoint"
class="col-12"
dense
outlined
/>
<q-input
:label="$t('quotation.label.dueDate')"
:readonly
v-model="dueDate"
class="col-12"
dense
outlined
/>
</div>
</div>
<div
class="bordered-b q-px-md q-py-sm row bg-color-orange-light items-center"
>
<div class="icon-wrapper bg-color-orange q-mr-sm">
<q-icon name="mdi-bank-outline" />
</div>
<span class="text-weight-bold">
{{ $t('quotation.label.infoPayment') }}
</span>
</div>
<div class="q-pa-sm">
<div class="row q-col-gutter-sm">
<q-select
outlined
clearable
use-input
emit-value
map-options
hide-bottom-space
input-debounce="0"
option-value="value"
option-label="label"
class="col-12"
autocomplete="off"
for="select-payType"
dense
:label="$t('quotation.label.payType')"
:options="[]"
:readonly
:hide-dropdown-icon="readonly"
v-model="payType"
></q-select>
<div class="col-8 column">
<q-field readonly dense outlined>
<template #control>
<span>{{ $t('quotation.label.paySplitCount') }}</span>
<span class="app-text-muted">
({{ $t('quotation.label.payTotal') }})
</span>
</template>
</q-field>
</div>
<div class="col-4">
<q-input
v-model="paySplitCount"
:readonly
class="col-6"
type="number"
dense
outlined
/>
</div>
<q-select
outlined
clearable
use-input
emit-value
fill-input
map-options
hide-bottom-space
option-value="value"
input-debounce="0"
option-label="label"
class="col-12"
autocomplete="off"
for="select-bankbook"
dense
:label="$t('quotation.label.bank')"
:options="bankBookOptions"
:readonly
:hide-dropdown-icon="readonly"
v-model="payBank"
@filter="bankBoookFilter"
>
<template v-slot:option="scope">
<q-item
v-if="scope.opt"
v-bind="scope.itemProps"
class="row items-center"
>
<q-item-section avatar>
<q-img
:src="`/img/bank/${scope.opt.value}.png`"
class="bordered"
style="
border-radius: 50%;
aspect-ratio: 1;
height: 2rem;
width: 2rem;
"
/>
</q-item-section>
<q-item-section>
{{ scope.opt.label }}
</q-item-section>
</q-item>
</template>
<template v-slot:selected-item="scope">
<q-item-section
v-if="scope.opt && !!payBank"
avatar
class="q-py-sm row"
>
<q-img
:src="`/img/bank/${scope.opt.value}.png`"
class="bordered"
style="
border-radius: 50%;
aspect-ratio: 1;
height: 2rem;
width: 2rem;
"
/>
</q-item-section>
</template>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</div>
<div
class="bordered-b q-px-md q-py-sm row bg-color-orange-light items-center"
>
<div class="icon-wrapper bg-color-orange q-mr-sm">
<Icon icon="iconoir:coins" />
</div>
<span class="text-weight-bold">
{{ $t('quotation.label.infoSummary') }}
</span>
</div>
<div class="q-pa-sm">
<div class="row">
{{ $t('general.total') }}
<span class="q-ml-auto">{{ data?.total || 0 }} ฿</span>
</div>
<div class="row">
{{ $t('general.discount') }}
<span class="q-ml-auto">{{ data?.discount || 0 }} ฿</span>
</div>
<div class="row">
{{ $t('general.totalAfterDiscount') }}
<span class="q-ml-auto">{{ data?.totalAfterDiscount || 0 }} ฿</span>
</div>
<div class="row">
{{ $t('general.totalVatExcluded') }}
<span class="q-ml-auto">{{ data?.totalVatExcluded || 0 }} ฿</span>
</div>
<div class="row">
{{ $t('general.totalVatIncluded') }}
<span class="q-ml-auto">{{ data?.totalVatIncluded || 0 }} ฿</span>
</div>
</div>
</AppBox>
</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);
}
.bg-color-orange {
--_color: var(--yellow-7-hsl);
color: white;
background: hsla(var(--_color));
}
.dark .bg-color-orange {
--_color: var(--orange-6-hsl);
}
.bg-color-orange-light {
--_color: var(--yellow-7-hsl);
background: hsla(var(--_color) / 0.2);
}
.dark .bg-color-orange {
--_color: var(--orange-6-hsl / 0.2);
}
</style>

View file

@ -0,0 +1,30 @@
export const productTreeDecoration = [
{
level: 0,
icon: 'mdi-folder-outline',
bg: 'hsla(var(--pink-6-hsl)/0.1)',
fg: 'var(--pink-6)',
},
{
level: 1,
icon: 'mdi-server-outline',
bg: 'hsla(var(--orange-5-hsl)/0.1)',
fg: 'var(--orange-5)',
},
{
level: 2,
icon: 'mdi-shopping-outline',
bg: 'hsla(var(--teal-10-hsl)/0.1)',
fg: 'var(--teal-10)',
},
];
export const pageTabs = [
'all',
'fullAmountCash',
'installmentsCash',
'fullAmountBill',
'installmentsBill',
];
export const fieldSelectedOption = [{ label: 'general.type', value: 'value' }];

View file

@ -1,13 +1,28 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { getInstance } from 'src/services/keycloak';
import useUtilsStore, { dialog, notify } from 'stores/utils';
import useUtilsStore from 'stores/utils';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
const $q = useQuasar();
const { locale } = useI18n();
const utilsStore = useUtilsStore();
const EDM_SERVICE = import.meta.env.VITE_EDM_MICRO_FRONTEND_URL;
const kc = getInstance();
const at = ref(kc.token);
const rt = ref(kc.refreshToken);
const iframe = ref<InstanceType<typeof HTMLIFrameElement>>();
function sendMessage() {
iframe?.value?.contentWindow?.postMessage(
{
i18n: locale.value,
darkMode: $q.dark.isActive,
},
'*',
);
}
onMounted(() => {
utilsStore.currentTitle.title = 'menu.dms';
@ -18,6 +33,7 @@ onMounted(() => {
handler: () => {},
},
];
sendMessage();
});
watch(
@ -27,10 +43,15 @@ watch(
rt.value = kc.refreshToken;
},
);
watch([locale, $q.dark], () => {
sendMessage();
});
</script>
<template>
<iframe
ref="iframe"
:src="`${EDM_SERVICE}/?at=${at}&rt=${rt}`"
frameborder="0"
class="full-width full-height rounded"

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