Merge branch 'develop'
This commit is contained in:
commit
5d672ccda6
135 changed files with 4505 additions and 290 deletions
|
|
@ -40,6 +40,7 @@
|
||||||
"vue-router": "^4.4.3"
|
"vue-router": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^9.3.0",
|
||||||
"@iconify/vue": "^4.1.2",
|
"@iconify/vue": "^4.1.2",
|
||||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||||
"@playwright/test": "^1.46.1",
|
"@playwright/test": "^1.46.1",
|
||||||
|
|
@ -50,6 +51,7 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||||
"@typescript-eslint/parser": "^7.18.0",
|
"@typescript-eslint/parser": "^7.18.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
"eslint-plugin-vue": "^9.27.0",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dotenv/config';
|
||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -24,7 +25,7 @@ export default defineConfig({
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
// baseURL: 'http://127.0.0.1:3000',
|
baseURL: 'http://192.168.1.62:20101',
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
|
|
|
||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
|
|
@ -78,6 +78,9 @@ importers:
|
||||||
specifier: ^4.4.3
|
specifier: ^4.4.3
|
||||||
version: 4.4.3(vue@3.4.38(typescript@5.5.4))
|
version: 4.4.3(vue@3.4.38(typescript@5.5.4))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@faker-js/faker':
|
||||||
|
specifier: ^9.3.0
|
||||||
|
version: 9.3.0
|
||||||
'@iconify/vue':
|
'@iconify/vue':
|
||||||
specifier: ^4.1.2
|
specifier: ^4.1.2
|
||||||
version: 4.1.2(vue@3.4.38(typescript@5.5.4))
|
version: 4.1.2(vue@3.4.38(typescript@5.5.4))
|
||||||
|
|
@ -108,6 +111,9 @@ importers:
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.20
|
specifier: ^10.4.20
|
||||||
version: 10.4.20(postcss@8.4.41)
|
version: 10.4.20(postcss@8.4.41)
|
||||||
|
dotenv:
|
||||||
|
specifier: ^16.4.7
|
||||||
|
version: 16.4.7
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.57.0
|
specifier: ^8.57.0
|
||||||
version: 8.57.0
|
version: 8.57.0
|
||||||
|
|
@ -455,6 +461,10 @@ packages:
|
||||||
resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
|
resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
|
||||||
|
'@faker-js/faker@9.3.0':
|
||||||
|
resolution: {integrity: sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==}
|
||||||
|
engines: {node: '>=18.0.0', npm: '>=9.0.0'}
|
||||||
|
|
||||||
'@humanwhocodes/config-array@0.11.14':
|
'@humanwhocodes/config-array@0.11.14':
|
||||||
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
||||||
engines: {node: '>=10.10.0'}
|
engines: {node: '>=10.10.0'}
|
||||||
|
|
@ -1587,8 +1597,8 @@ packages:
|
||||||
resolution: {integrity: sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==}
|
resolution: {integrity: sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
dotenv@16.4.5:
|
dotenv@16.4.7:
|
||||||
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
|
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
duplexify@3.7.1:
|
duplexify@3.7.1:
|
||||||
|
|
@ -3902,6 +3912,8 @@ snapshots:
|
||||||
|
|
||||||
'@eslint/js@8.57.0': {}
|
'@eslint/js@8.57.0': {}
|
||||||
|
|
||||||
|
'@faker-js/faker@9.3.0': {}
|
||||||
|
|
||||||
'@humanwhocodes/config-array@0.11.14':
|
'@humanwhocodes/config-array@0.11.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 2.0.3
|
'@humanwhocodes/object-schema': 2.0.3
|
||||||
|
|
@ -4058,7 +4070,7 @@ snapshots:
|
||||||
compression: 1.7.4
|
compression: 1.7.4
|
||||||
cross-spawn: 7.0.3
|
cross-spawn: 7.0.3
|
||||||
dot-prop: 9.0.0
|
dot-prop: 9.0.0
|
||||||
dotenv: 16.4.5
|
dotenv: 16.4.7
|
||||||
dotenv-expand: 11.0.6
|
dotenv-expand: 11.0.6
|
||||||
elementtree: 0.1.7
|
elementtree: 0.1.7
|
||||||
esbuild: 0.23.1
|
esbuild: 0.23.1
|
||||||
|
|
@ -5270,9 +5282,9 @@ snapshots:
|
||||||
|
|
||||||
dotenv-expand@11.0.6:
|
dotenv-expand@11.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
dotenv: 16.4.5
|
dotenv: 16.4.7
|
||||||
|
|
||||||
dotenv@16.4.5: {}
|
dotenv@16.4.7: {}
|
||||||
|
|
||||||
duplexify@3.7.1:
|
duplexify@3.7.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
|
|
@ -57,17 +57,13 @@ const currentBtnOpen = ref<{ title: string; opened: boolean[] }[]>([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function calcPrice(c: (typeof rows.value)[number]) {
|
function calcPrice(c: (typeof rows.value)[number]) {
|
||||||
return precisionRound(
|
const price = c.pricePerUnit * c.amount;
|
||||||
c.pricePerUnit * c.amount -
|
const vat = c.product.calcVat
|
||||||
c.discount +
|
? (c.pricePerUnit * (c.discount ? c.amount : 1) - c.discount) *
|
||||||
precisionRound(
|
(config.value?.vat || 0.07) *
|
||||||
c.product.calcVat
|
(!c.discount ? c.amount : 1)
|
||||||
? (c.pricePerUnit * (c.discount ? c.amount : 1) - c.discount) *
|
: 0;
|
||||||
(config.value?.vat || 0.07)
|
return precisionRound(price + vat);
|
||||||
: 0,
|
|
||||||
) *
|
|
||||||
(!c.discount ? c.amount : 1),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const discount4Show = ref<string[]>([]);
|
const discount4Show = ref<string[]>([]);
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ defineProps<{
|
||||||
badgeColor?: string;
|
badgeColor?: string;
|
||||||
hideKebabView?: boolean;
|
hideKebabView?: boolean;
|
||||||
hideKebabEdit?: boolean;
|
hideKebabEdit?: boolean;
|
||||||
|
hideKebabDelete?: boolean;
|
||||||
hideAction?: boolean;
|
hideAction?: boolean;
|
||||||
|
useCancel?: boolean;
|
||||||
|
|
||||||
customData?: {
|
customData?: {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -39,6 +41,7 @@ defineEmits<{
|
||||||
(e: 'delete'): void;
|
(e: 'delete'): void;
|
||||||
(e: 'example'): void;
|
(e: 'example'): void;
|
||||||
(e: 'preview'): void;
|
(e: 'preview'): void;
|
||||||
|
(e: 'cancel'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const rand = Math.random();
|
const rand = Math.random();
|
||||||
|
|
@ -93,7 +96,8 @@ const rand = Math.random();
|
||||||
:idName="code"
|
:idName="code"
|
||||||
status="ACTIVE"
|
status="ACTIVE"
|
||||||
hide-toggle
|
hide-toggle
|
||||||
hide-delete
|
:use-cancel
|
||||||
|
:hide-delete="hideKebabDelete"
|
||||||
:hide-view="hideKebabView"
|
:hide-view="hideKebabView"
|
||||||
:hide-edit="hideKebabEdit"
|
:hide-edit="hideKebabEdit"
|
||||||
@view="$emit('view')"
|
@view="$emit('view')"
|
||||||
|
|
@ -101,6 +105,7 @@ const rand = Math.random();
|
||||||
@link="$emit('link')"
|
@link="$emit('link')"
|
||||||
@upload="$emit('upload')"
|
@upload="$emit('upload')"
|
||||||
@delete="$emit('delete')"
|
@delete="$emit('delete')"
|
||||||
|
@cancel="$emit('cancel')"
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
39
src/components/11_credit-note/FormCredit.vue
Normal file
39
src/components/11_credit-note/FormCredit.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import SelectQuotation from '../shared/select/SelectQuotation.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const quotationId = defineModel<string>('quotationId', {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="row col-12">
|
||||||
|
<section
|
||||||
|
:id="`form-credit`"
|
||||||
|
class="col-12 q-pb-sm text-weight-bold text-body1 row items-center"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
flat
|
||||||
|
size="xs"
|
||||||
|
class="q-pa-sm rounded q-mr-xs"
|
||||||
|
color="info"
|
||||||
|
name="mdi-file-outline"
|
||||||
|
style="background-color: var(--surface-3)"
|
||||||
|
/>
|
||||||
|
{{ $t(`general.document`) }}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="col-12 row q-col-gutter-sm">
|
||||||
|
<SelectQuotation
|
||||||
|
for="select-quotation"
|
||||||
|
class="col"
|
||||||
|
v-model:value="quotationId"
|
||||||
|
:label="$t('general.select', { msg: $t('quotation.title') })"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -6,29 +6,57 @@ import DialogHeader from './DialogHeader.vue';
|
||||||
import MainButton from '../button/MainButton.vue';
|
import MainButton from '../button/MainButton.vue';
|
||||||
import NoData from '../NoData.vue';
|
import NoData from '../NoData.vue';
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
title: string;
|
title?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
hideTab?: boolean;
|
||||||
|
download?: boolean;
|
||||||
|
transformUrl?: (url: string) => string | Promise<string>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const open = defineModel<boolean>({ default: false });
|
const open = defineModel<boolean>({ default: false });
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
imageZoom: 100,
|
imageZoom: 100,
|
||||||
|
transformedUrl: props.url,
|
||||||
});
|
});
|
||||||
|
|
||||||
function openDialog() {
|
async function openDialog() {
|
||||||
state.imageZoom = 100;
|
state.imageZoom = 100;
|
||||||
|
if (props.url && props.transformUrl) {
|
||||||
|
state.transformedUrl = await props.transformUrl(props.url);
|
||||||
|
} else {
|
||||||
|
state.transformedUrl = props.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadImage(url: string | null) {
|
||||||
|
if (!url) return;
|
||||||
|
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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<DialogFormContainer v-model="open" v-on:open="openDialog">
|
<DialogFormContainer v-model="open" v-on:open="openDialog">
|
||||||
<template #header>
|
<template #header>
|
||||||
<DialogHeader :title="title" />
|
<DialogHeader :title="title || ''" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<main class="column full-height">
|
<main class="column full-height">
|
||||||
<section
|
<section
|
||||||
|
v-if="!hideTab"
|
||||||
style="background: var(--gray-3)"
|
style="background: var(--gray-3)"
|
||||||
class="q-py-sm row justify-center"
|
class="q-py-sm row justify-center"
|
||||||
>
|
>
|
||||||
|
|
@ -74,18 +102,32 @@ function openDialog() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:class="{ 'cursor-pointer': state.imageZoom > 100 }"
|
|
||||||
class="full-height full-width flex justify-center items-center col scroll q-pa-md"
|
class="full-height full-width flex justify-center items-center col scroll q-pa-md"
|
||||||
|
:class="{ 'cursor-pointer': state.imageZoom > 100 }"
|
||||||
v-dragscroll
|
v-dragscroll
|
||||||
>
|
>
|
||||||
<q-img
|
<q-img
|
||||||
v-if="url"
|
v-if="state.transformedUrl"
|
||||||
class="full-height"
|
class="full-height"
|
||||||
:src="url"
|
:src="state.transformedUrl"
|
||||||
fit="contain"
|
fit="contain"
|
||||||
:style="{ transform: `scale(${state.imageZoom / 100})` }"
|
:style="{ transform: `scale(${state.imageZoom / 100})` }"
|
||||||
style="transform-origin: 0 0"
|
style="transform-origin: 0 0"
|
||||||
/>
|
>
|
||||||
|
<div v-if="download" style="border-radius: 50%" class="no-padding">
|
||||||
|
<q-btn
|
||||||
|
v-if="state.transformedUrl"
|
||||||
|
class="upload-image-btn"
|
||||||
|
icon="mdi-download-outline"
|
||||||
|
id="btn-download-img"
|
||||||
|
size="md"
|
||||||
|
unelevated
|
||||||
|
round
|
||||||
|
@click="downloadImage(state.transformedUrl)"
|
||||||
|
style="color: hsla(var(--stone-0-hsl) / 0.7)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
<NoData v-else />
|
<NoData v-else />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
212
src/components/shared/select/SelectQuotation.vue
Normal file
212
src/components/shared/select/SelectQuotation.vue
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { createSelect, SelectProps } from './select';
|
||||||
|
import SelectInput from '../SelectInput.vue';
|
||||||
|
|
||||||
|
import useOptionStore from 'src/stores/options';
|
||||||
|
|
||||||
|
import { Quotation, QuotationFull } from 'src/stores/quotations/types';
|
||||||
|
|
||||||
|
import { useQuotationStore as useStore } from 'src/stores/quotations';
|
||||||
|
|
||||||
|
type SelectOption = Quotation | QuotationFull;
|
||||||
|
|
||||||
|
const value = defineModel<string | null | undefined>('value', {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const valueOption = defineModel<SelectOption>('valueOption', {
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectOptions = ref<SelectOption[]>([]);
|
||||||
|
|
||||||
|
const { getQuotationList: getList, getQuotation: getById } = useStore();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'create'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
type ExclusiveProps = {
|
||||||
|
codeOnly?: boolean;
|
||||||
|
selectFirstValue?: boolean;
|
||||||
|
branchVirtual?: boolean;
|
||||||
|
checkRole?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
|
||||||
|
|
||||||
|
const { getOptions, setFirstValue, getSelectedOption, filter } =
|
||||||
|
createSelect<SelectOption>(
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
valueOption,
|
||||||
|
selectOptions,
|
||||||
|
getList: async (query) => {
|
||||||
|
const ret = await getList({
|
||||||
|
query,
|
||||||
|
...props.params,
|
||||||
|
pageSize: 30,
|
||||||
|
hasCancel: true,
|
||||||
|
includeRegisteredBranch: true,
|
||||||
|
});
|
||||||
|
if (ret) return ret.result;
|
||||||
|
},
|
||||||
|
getByValue: async (id) => {
|
||||||
|
const ret = await getById(id);
|
||||||
|
if (ret) return ret;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ valueField: 'id' },
|
||||||
|
);
|
||||||
|
|
||||||
|
function getCustomerName(
|
||||||
|
record: Quotation,
|
||||||
|
opts?: {
|
||||||
|
locale?: string;
|
||||||
|
noCode?: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const customer = record.customerBranch;
|
||||||
|
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
['CORP']: {
|
||||||
|
['eng']: customer.registerNameEN,
|
||||||
|
['tha']: customer.registerName,
|
||||||
|
}[opts?.locale || 'eng'],
|
||||||
|
['PERS']:
|
||||||
|
{
|
||||||
|
['eng']: `${useOptionStore().mapOption(customer?.namePrefix || '')} ${customer?.firstNameEN} ${customer?.lastNameEN}`,
|
||||||
|
['tha']: `${useOptionStore().mapOption(customer?.namePrefix || '')} ${customer?.firstName} ${customer?.lastName}`,
|
||||||
|
}[opts?.locale || 'eng'] || '-',
|
||||||
|
}[customer.customer.customerType] +
|
||||||
|
(opts?.noCode ? '' : ' ' + `(${customer.code})`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getOptions();
|
||||||
|
|
||||||
|
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
|
||||||
|
setFirstValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.selectFirstValue) {
|
||||||
|
setDefaultValue();
|
||||||
|
} else await getSelectedOption();
|
||||||
|
});
|
||||||
|
|
||||||
|
function setDefaultValue() {
|
||||||
|
setFirstValue();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<SelectInput
|
||||||
|
v-model="value"
|
||||||
|
option-value="id"
|
||||||
|
incremental
|
||||||
|
:label
|
||||||
|
:placeholder
|
||||||
|
:readonly
|
||||||
|
:disable="disabled"
|
||||||
|
:option="selectOptions"
|
||||||
|
:hide-selected="false"
|
||||||
|
:fill-input="false"
|
||||||
|
:rules="[
|
||||||
|
(v: string) => !props.required || !!v || $t('form.error.required'),
|
||||||
|
]"
|
||||||
|
@filter="filter"
|
||||||
|
>
|
||||||
|
<template #append v-if="clearable">
|
||||||
|
<q-icon
|
||||||
|
v-if="!readonly && value"
|
||||||
|
name="mdi-close-circle"
|
||||||
|
@click.stop="value = ''"
|
||||||
|
class="cursor-pointer clear-btn"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #option="{ scope }">
|
||||||
|
<q-item
|
||||||
|
v-if="scope.opt"
|
||||||
|
v-bind="scope.itemProps"
|
||||||
|
class="row items-start col-12 no-padding"
|
||||||
|
>
|
||||||
|
<div class="q-mx-sm q-my-xs">
|
||||||
|
<q-icon name="mdi-file" style="color: var(--brand-1)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-pt-xs">
|
||||||
|
<span class="row">
|
||||||
|
<span style="font-weight: 600">
|
||||||
|
{{ $t('productService.service.work') }}:
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{{ scope.opt.workName }}
|
||||||
|
({{ scope.opt.code }})
|
||||||
|
|
||||||
|
<q-badge
|
||||||
|
dense
|
||||||
|
class="surface-3 q-ml-sm"
|
||||||
|
rounded
|
||||||
|
style="color: var(--foreground)"
|
||||||
|
>
|
||||||
|
{{ scope.opt._count.canceledWork || 0 }}
|
||||||
|
</q-badge>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="text-caption app-text-muted-2 q-mb-xs">
|
||||||
|
<span class="col column">
|
||||||
|
<!-- TODO: register branch id -->
|
||||||
|
{{ $t(`creditNote.label.servicePoint`) }}
|
||||||
|
{{
|
||||||
|
$i18n.locale === 'eng'
|
||||||
|
? scope.opt.registeredBranch.nameEN
|
||||||
|
: scope.opt.registeredBranch.name
|
||||||
|
}}
|
||||||
|
({{ scope.opt.registeredBranch.code }})
|
||||||
|
</span>
|
||||||
|
<span class="col">
|
||||||
|
{{ $t('quotation.customer') }}
|
||||||
|
{{ getCustomerName(scope.opt, { locale: $i18n.locale }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-item>
|
||||||
|
<q-separator class="q-mx-sm" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #selected-item="{ scope }">
|
||||||
|
<div v-if="scope.opt" class="row items-center no-wrap">
|
||||||
|
<div class="q-mr-sm">
|
||||||
|
<span style="font-weight: 600">
|
||||||
|
{{ $t('productService.service.work') }}:
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{{ scope.opt.workName }}
|
||||||
|
({{ scope.opt.code }})
|
||||||
|
<q-badge
|
||||||
|
dense
|
||||||
|
class="surface-3 q-ml-xs"
|
||||||
|
rounded
|
||||||
|
style="color: var(--foreground)"
|
||||||
|
>
|
||||||
|
{{ scope.opt._count.canceledWork || 0 }}
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption app-text-muted-2">
|
||||||
|
{{ $t(`creditNote.label.servicePoint`) }}
|
||||||
|
{{
|
||||||
|
$i18n.locale === 'eng'
|
||||||
|
? scope.opt.registeredBranch.nameEN
|
||||||
|
: scope.opt.registeredBranch.name
|
||||||
|
}}
|
||||||
|
({{ scope.opt.registeredBranch.code }}),
|
||||||
|
{{ $t('quotation.customer') }}
|
||||||
|
{{ getCustomerName(scope.opt, { locale: $i18n.locale }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SelectInput>
|
||||||
|
</template>
|
||||||
|
|
@ -14,6 +14,7 @@ const props = withDefaults(
|
||||||
url?: string;
|
url?: string;
|
||||||
progress?: number;
|
progress?: number;
|
||||||
uploading?: { loaded: number; total: number };
|
uploading?: { loaded: number; total: number };
|
||||||
|
idle?: boolean;
|
||||||
|
|
||||||
clickable?: boolean;
|
clickable?: boolean;
|
||||||
closeable?: boolean;
|
closeable?: boolean;
|
||||||
|
|
@ -53,7 +54,7 @@ onMounted(() => {
|
||||||
@click="$emit('click')"
|
@click="$emit('click')"
|
||||||
>
|
>
|
||||||
<q-icon :name="icon" size="lg" :style="`color: ${color}`" />
|
<q-icon :name="icon" size="lg" :style="`color: ${color}`" />
|
||||||
<article class="col column q-pl-md">
|
<article class="col column q-pl-md ellipsis">
|
||||||
<span>{{ name }}</span>
|
<span>{{ name }}</span>
|
||||||
<span class="text-caption app-text-muted-2">
|
<span class="text-caption app-text-muted-2">
|
||||||
{{
|
{{
|
||||||
|
|
@ -68,8 +69,13 @@ onMounted(() => {
|
||||||
color="primary"
|
color="primary"
|
||||||
size="1.5em"
|
size="1.5em"
|
||||||
/>
|
/>
|
||||||
<q-icon v-else name="mdi-check-circle" color="positive" size="1rem" />
|
<q-icon
|
||||||
{{ progress !== 1 ? `Uploading...` : 'Completed' }}
|
v-if="progress === 1 && !idle"
|
||||||
|
name="mdi-check-circle"
|
||||||
|
color="positive"
|
||||||
|
size="1rem"
|
||||||
|
/>
|
||||||
|
{{ idle ? `Pending` : progress !== 1 ? `Uploading...` : 'Completed' }}
|
||||||
</span>
|
</span>
|
||||||
</article>
|
</article>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ const fileData = defineModel<
|
||||||
loaded: number;
|
loaded: number;
|
||||||
total: number;
|
total: number;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
placeholder?: boolean;
|
||||||
}[]
|
}[]
|
||||||
>('fileData', { required: true });
|
>('fileData', { required: true });
|
||||||
|
|
||||||
|
|
@ -54,7 +55,8 @@ function pickFile() {
|
||||||
<template>
|
<template>
|
||||||
<div :class="{ row: layout === 'column' }">
|
<div :class="{ row: layout === 'column' }">
|
||||||
<div
|
<div
|
||||||
class="upload-section column rounded q-py-md items-center justify-center no-wrap surface-2 col"
|
class="upload-section column rounded q-py-md items-center justify-center no-wrap surface-2"
|
||||||
|
:class="{ 'col-12': fileData.length === 0, col: fileData.length > 0 }"
|
||||||
>
|
>
|
||||||
<q-img src="/images/upload.png" width="150px" />
|
<q-img src="/images/upload.png" width="150px" />
|
||||||
{{ label }}
|
{{ label }}
|
||||||
|
|
@ -76,11 +78,10 @@ function pickFile() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- upload card -->
|
<!-- upload card -->
|
||||||
<section class="row col" :class="{ 'q-pl-md': layout === 'column' }">
|
<section class="column col" :class="{ 'q-pl-md': layout === 'column' }">
|
||||||
<div
|
<div
|
||||||
v-for="(d, j) in fileData"
|
v-for="(d, j) in fileData"
|
||||||
:key="j"
|
:key="j"
|
||||||
class="col-12"
|
|
||||||
:class="{
|
:class="{
|
||||||
'q-pt-md': layout === 'row' && j === 0,
|
'q-pt-md': layout === 'row' && j === 0,
|
||||||
'q-pt-sm': j > 0,
|
'q-pt-sm': j > 0,
|
||||||
|
|
@ -91,6 +92,7 @@ function pickFile() {
|
||||||
:progress="d.progress"
|
:progress="d.progress"
|
||||||
:uploading="{ loaded: d.loaded, total: d.total }"
|
:uploading="{ loaded: d.loaded, total: d.total }"
|
||||||
:url="d.url"
|
:url="d.url"
|
||||||
|
:idle="d.placeholder"
|
||||||
icon="mdi-file-image-outline"
|
icon="mdi-file-image-outline"
|
||||||
color="hsl(var(--text-mute))"
|
color="hsl(var(--text-mute))"
|
||||||
clickable
|
clickable
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,7 @@ export default {
|
||||||
sales: {
|
sales: {
|
||||||
title: 'Sales',
|
title: 'Sales',
|
||||||
quotation: 'Quotation',
|
quotation: 'Quotation',
|
||||||
|
invoice: 'Invoice',
|
||||||
},
|
},
|
||||||
|
|
||||||
order: {
|
order: {
|
||||||
|
|
@ -1100,6 +1101,7 @@ export default {
|
||||||
'Validation failed. Each installment must include at least one product. Please review and update the installments accordingly.',
|
'Validation failed. Each installment must include at least one product. Please review and update the installments accordingly.',
|
||||||
flowTemplateNotFound: 'Workflow template cannot be found',
|
flowTemplateNotFound: 'Workflow template cannot be found',
|
||||||
taskOrderNotFound: 'Task order cannot be found.',
|
taskOrderNotFound: 'Task order cannot be found.',
|
||||||
|
quotationNotFound: 'Quotation cannot be found.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1113,6 +1115,7 @@ export default {
|
||||||
discount: 'Discount',
|
discount: 'Discount',
|
||||||
vat: 'VAT',
|
vat: 'VAT',
|
||||||
value: 'Value',
|
value: 'Value',
|
||||||
|
netValue: 'Net Value',
|
||||||
title: {
|
title: {
|
||||||
quotation: 'Quotation',
|
quotation: 'Quotation',
|
||||||
invoice: 'Invoice',
|
invoice: 'Invoice',
|
||||||
|
|
@ -1135,4 +1138,71 @@ export default {
|
||||||
include: 'Include Duty',
|
include: 'Include Duty',
|
||||||
cost: 'Duty Cost (Baht)',
|
cost: 'Duty Cost (Baht)',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
creditNote: {
|
||||||
|
title: 'Credit Note',
|
||||||
|
caption: 'All Credit Notes',
|
||||||
|
label: {
|
||||||
|
code: 'Credit Note Code',
|
||||||
|
quotationCode: 'Quotation Code',
|
||||||
|
quotationWorkName: 'Work Name',
|
||||||
|
quotationPayment: 'Payment Method',
|
||||||
|
value: 'Value',
|
||||||
|
servicePoint: 'Service Point',
|
||||||
|
quotationRegisteredBranch: 'Service Point Issuing The Quotation',
|
||||||
|
quotationCreatedBy: 'Made By',
|
||||||
|
customer: 'Customer',
|
||||||
|
BankTransfer: 'Bank Transfer',
|
||||||
|
Cash: 'Cash',
|
||||||
|
accountNumber: 'Bank Account Number',
|
||||||
|
accountName: 'Bank Account Name',
|
||||||
|
creditNoteInformation: 'Credit Note Information',
|
||||||
|
additionalDetail: 'Detail',
|
||||||
|
specifyReason: 'Specify Reason',
|
||||||
|
reasonReturn:
|
||||||
|
'The customer returned all or part of the goods because they did not meet their requirements.',
|
||||||
|
reasonCanceled:
|
||||||
|
'The customer canceled certain items or services listed on the invoice.',
|
||||||
|
submit: 'Approve the credit note',
|
||||||
|
refund: 'Refund',
|
||||||
|
refundMethod: 'Refund Method',
|
||||||
|
totalRefund: 'Total refund amount',
|
||||||
|
totalAmount: 'Total',
|
||||||
|
paid: 'Paid',
|
||||||
|
remain: 'Remaining',
|
||||||
|
refundDocs: 'Refund Documents',
|
||||||
|
refundSuccess: 'Refund Success',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
Pending: 'Pending Refund',
|
||||||
|
Success: 'Refund Completed',
|
||||||
|
Canceled: 'Canceled',
|
||||||
|
payback: {
|
||||||
|
Pending: 'Pending',
|
||||||
|
Verify: 'Verify',
|
||||||
|
Done: 'Done',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
Pending: 'Pending Refund',
|
||||||
|
Success: 'Refund Completed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
invoice: {
|
||||||
|
title: 'Invoice',
|
||||||
|
caption: 'All Invoices',
|
||||||
|
workSheetName: 'Worksheet Name',
|
||||||
|
paymentDueDate: 'Payment Duedate',
|
||||||
|
status: {
|
||||||
|
PaymentSuccess: 'Payment Success',
|
||||||
|
PaymentWait: 'Waiting For Payment',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
receipt: {
|
||||||
|
title: 'Receipt / Tax Invoice',
|
||||||
|
caption: 'All Receipt / Tax Invoice',
|
||||||
|
dataSum: 'Summary of all receipts/tax invoices',
|
||||||
|
workSheetName: 'Worksheet name',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { title } from 'process';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
general: {
|
general: {
|
||||||
ok: 'ตกลง',
|
ok: 'ตกลง',
|
||||||
|
|
@ -192,6 +194,7 @@ export default {
|
||||||
sales: {
|
sales: {
|
||||||
title: 'งานซื้อขาย',
|
title: 'งานซื้อขาย',
|
||||||
quotation: 'ใบเสนอราคา',
|
quotation: 'ใบเสนอราคา',
|
||||||
|
invoice: 'ใบแจ้งหนี้',
|
||||||
},
|
},
|
||||||
|
|
||||||
order: {
|
order: {
|
||||||
|
|
@ -1001,7 +1004,8 @@ export default {
|
||||||
confirmSavingStatus:
|
confirmSavingStatus:
|
||||||
'คุณต้องการยืนยันการบันทึกข้อมูลการเปลี่ยนสถานะใช่หรือไม่',
|
'คุณต้องการยืนยันการบันทึกข้อมูลการเปลี่ยนสถานะใช่หรือไม่',
|
||||||
confirmSending: 'ยืนยันการส่งงานใช่หรือไม่',
|
confirmSending: 'ยืนยันการส่งงานใช่หรือไม่',
|
||||||
confirmEndWorkWarning: `ท่านต้องการให้ดำเนินการจบงานในขณะนี้หรือไม่? สถานะปัจจุบันที่แสดงว่า 'รอดำเนินการ' 'กำลังดำเนินการ' 'ไปดำเนินการใหม่' จะถูกเปลี่ยนเป็น 'ทำใหม่ทั้งหมด'`,
|
confirmEndWorkWarning:
|
||||||
|
"ท่านต้องการให้ดำเนินการจบงานในขณะนี้หรือไม่? สถานะปัจจุบันที่แสดงว่า 'รอดำเนินการ' 'กำลังดำเนินการ' 'ไปดำเนินการใหม่' จะถูกเปลี่ยนเป็น 'ทำใหม่ทั้งหมด'",
|
||||||
confirmEndWork: 'ท่านต้องการจบงานใช่หรือไม่',
|
confirmEndWork: 'ท่านต้องการจบงานใช่หรือไม่',
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
|
|
@ -1078,6 +1082,7 @@ export default {
|
||||||
'ข้อมูลงวดไม่ถูกต้อง กรุณาตรวจสอบและยืนยันว่าแต่ละงวดมีสินค้าอย่างน้อยหนึ่งรายการ',
|
'ข้อมูลงวดไม่ถูกต้อง กรุณาตรวจสอบและยืนยันว่าแต่ละงวดมีสินค้าอย่างน้อยหนึ่งรายการ',
|
||||||
flowTemplateNotFound: 'ไม่พบขั้นตอนการทำงาน',
|
flowTemplateNotFound: 'ไม่พบขั้นตอนการทำงาน',
|
||||||
taskOrderNotFound: 'ไม่พบใบสั่งงาน',
|
taskOrderNotFound: 'ไม่พบใบสั่งงาน',
|
||||||
|
quotationNotFound: 'ไม่พบใบเสนอราคา',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1091,6 +1096,7 @@ export default {
|
||||||
discount: 'ส่วนลด',
|
discount: 'ส่วนลด',
|
||||||
vat: 'ภาษี',
|
vat: 'ภาษี',
|
||||||
value: 'มูลค่า',
|
value: 'มูลค่า',
|
||||||
|
netValue: 'มูลค่าสุทธิ',
|
||||||
title: {
|
title: {
|
||||||
quotation: 'ใบเสนอราคา',
|
quotation: 'ใบเสนอราคา',
|
||||||
invoice: 'ใบแจ้งหนี้',
|
invoice: 'ใบแจ้งหนี้',
|
||||||
|
|
@ -1113,4 +1119,71 @@ export default {
|
||||||
include: 'ติดอากร',
|
include: 'ติดอากร',
|
||||||
cost: 'จำนวนเงินอากร (บาท)',
|
cost: 'จำนวนเงินอากร (บาท)',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
creditNote: {
|
||||||
|
title: 'ใบลดหนี้',
|
||||||
|
caption: 'ใบลดหนี้ทั้งหมด',
|
||||||
|
label: {
|
||||||
|
code: 'รหัสใบลดหนี้',
|
||||||
|
quotationCode: 'เลขที่ใบเสนอราคา',
|
||||||
|
quotationWorkName: 'ชื่อใบงาน',
|
||||||
|
quotationPayment: 'วิธีการชำระเงิน',
|
||||||
|
value: 'มูลค่า',
|
||||||
|
servicePoint: 'จุดรับบริการ',
|
||||||
|
quotationRegisteredBranch: 'จุดรับบริการที่ออกใบเสนอราคา',
|
||||||
|
quotationCreatedBy: 'ผู้ที่ทำรายการ',
|
||||||
|
customer: 'ลูกค้า',
|
||||||
|
BankTransfer: 'โอนธนาคาร',
|
||||||
|
Cash: 'เงินสด',
|
||||||
|
accountNumber: 'เลขที่บัญชี',
|
||||||
|
accountName: 'ชื่อบัญชี',
|
||||||
|
creditNoteInformation: 'ข้อมูลการลดหนี้',
|
||||||
|
additionalDetail: 'อธิบายเพิ่มเติม',
|
||||||
|
specifyReason: 'ระบุสาเหตุการลดหนี้',
|
||||||
|
reasonReturn:
|
||||||
|
'ลูกค้าคืนสินค้าทั้งหมดหรือบางส่วน เนื่องจากสินค้าไม่ตรงตามความต้องการ',
|
||||||
|
reasonCanceled:
|
||||||
|
'ลูกค้ายกเลิกคำสั่งซื้อบางรายการหรือบริการที่ระบุในใบแจ้งหนี้',
|
||||||
|
submit: 'อนุมัติใบลดหนี้',
|
||||||
|
refund: 'การคืนเงิน',
|
||||||
|
refundMethod: 'วิธีการคืนเงิน',
|
||||||
|
totalRefund: 'ยอดคืนเงินทั้งหมด',
|
||||||
|
totalAmount: 'ยอดทั้งหมด',
|
||||||
|
paid: 'ชำระไปแล้ว',
|
||||||
|
remain: 'คงเหลือ',
|
||||||
|
refundDocs: 'เอกสารการคืนเงิน',
|
||||||
|
refundSuccess: 'คืนเงินเสร็จเรียบร้อย',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
Pending: 'รอคืนเงิน',
|
||||||
|
Success: 'คืนเงินเสร็จสิ้น',
|
||||||
|
Canceled: 'ยกเลิกรายการ',
|
||||||
|
payback: {
|
||||||
|
Pending: 'รอคืนเงิน',
|
||||||
|
Verify: 'คืนเงินแล้ว รอตรวจสอบ',
|
||||||
|
Done: 'คืนเงินเรียบร้อย',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
Pending: 'รอคืนเงิน',
|
||||||
|
Success: 'คืนเงินเสร็จสิ้น',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
invoice: {
|
||||||
|
title: 'ใบแจ้งหนี้',
|
||||||
|
caption: 'ใบแจ้งหนี้ทั้งหมด',
|
||||||
|
workSheetName: 'ชื่อใบงาน',
|
||||||
|
paymentDueDate: 'กำหนดชำระ',
|
||||||
|
status: {
|
||||||
|
PaymentSuccess: 'ชำระเงินแล้ว',
|
||||||
|
PaymentWait: 'ยังไม่ชำระ',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
receipt: {
|
||||||
|
title: 'ใบเสร็จรับเงิน/กำกับภาษี',
|
||||||
|
caption: 'ใบเสร็จรับเงิน/กำกับภาษีทั้งหมด',
|
||||||
|
dataSum: 'สรุปใบเสร็จรับเงิน/กำกับภาษีทั้งหมด',
|
||||||
|
workSheetName: 'ชื่อใบงาน',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,10 @@ onMounted(async () => {
|
||||||
{
|
{
|
||||||
label: 'menu.sales',
|
label: 'menu.sales',
|
||||||
icon: 'mdi-store-settings-outline',
|
icon: 'mdi-store-settings-outline',
|
||||||
children: [{ label: 'quotation', route: '/quotation' }],
|
children: [
|
||||||
|
{ label: 'quotation', route: '/quotation' },
|
||||||
|
{ label: 'invoice', route: '/invoice' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'menu.order',
|
label: 'menu.order',
|
||||||
|
|
@ -152,12 +155,11 @@ onMounted(async () => {
|
||||||
{
|
{
|
||||||
label: 'menu.account',
|
label: 'menu.account',
|
||||||
icon: 'mdi-bank-outline',
|
icon: 'mdi-bank-outline',
|
||||||
disabled: true,
|
disabled: false,
|
||||||
children: [
|
children: [
|
||||||
{ label: 'uploadSlip', route: '' },
|
{ label: 'receipt', route: '/receipt' },
|
||||||
{ label: 'receipt', route: '' },
|
{ label: 'creditNote', route: '/credit-note', disabled: true },
|
||||||
{ label: 'creditNote', route: '' },
|
{ label: 'debitNote', route: '', disabled: true },
|
||||||
{ label: 'debitNote', route: '' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3886,7 +3886,7 @@ watch(
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="col surface-1 rounded bordered scroll row relative-position full-width"
|
class="col surface-1 rounded bordered scroll row relative-position"
|
||||||
id="product-form"
|
id="product-form"
|
||||||
:class="{
|
:class="{
|
||||||
'q-mb-lg q-mx-lg ': $q.screen.gt.sm,
|
'q-mb-lg q-mx-lg ': $q.screen.gt.sm,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { useQuotationForm } from './form';
|
||||||
import { hslaColors } from './constants';
|
import { hslaColors } from './constants';
|
||||||
|
|
||||||
// NOTE Import Types
|
// NOTE Import Types
|
||||||
import { CustomerBranchCreate } from 'stores/customer/types';
|
import { CustomerBranchCreate, CustomerType } from 'stores/customer/types';
|
||||||
|
|
||||||
// NOTE: Import Components
|
// NOTE: Import Components
|
||||||
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
|
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
|
||||||
|
|
@ -181,7 +181,7 @@ async function triggerDialogDeleteQuottaion(id: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerCreateCustomerd(opts: { type: 'CORP' | 'PERS' }) {
|
function triggerCreateCustomerd(opts: { type: CustomerType }) {
|
||||||
setDefaultCustomer();
|
setDefaultCustomer();
|
||||||
customerFormState.value.dialogType = 'create';
|
customerFormState.value.dialogType = 'create';
|
||||||
customerFormData.value.customerType = opts?.type;
|
customerFormData.value.customerType = opts?.type;
|
||||||
|
|
@ -656,6 +656,8 @@ async function storeDataLocal(id: string) {
|
||||||
<template #grid="{ item }">
|
<template #grid="{ item }">
|
||||||
<div class="col-md-4 col-sm-6 col-12">
|
<div class="col-md-4 col-sm-6 col-12">
|
||||||
<QuotationCard
|
<QuotationCard
|
||||||
|
hide-kebab-delete
|
||||||
|
:hide-kebab-edit="!(pageState.currentTab === 'Issued')"
|
||||||
:urgent="item.row.urgent"
|
:urgent="item.row.urgent"
|
||||||
:code="item.row.code"
|
:code="item.row.code"
|
||||||
:title="item.row.workName"
|
:title="item.row.workName"
|
||||||
|
|
|
||||||
|
|
@ -242,12 +242,11 @@ function getPrice(
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
const price = precisionRound(c.pricePerUnit * c.amount);
|
const price = c.pricePerUnit * c.amount;
|
||||||
const vat =
|
const vat =
|
||||||
precisionRound(
|
(c.pricePerUnit * (c.discount ? c.amount : 1) - c.discount) *
|
||||||
(c.pricePerUnit * (c.discount ? c.amount : 1) - c.discount) *
|
(config.value?.vat || 0.07) *
|
||||||
(config.value?.vat || 0.07),
|
(!c.discount ? c.amount : 1);
|
||||||
) * (!c.discount ? c.amount : 1);
|
|
||||||
|
|
||||||
a.totalPrice = precisionRound(a.totalPrice + price);
|
a.totalPrice = precisionRound(a.totalPrice + price);
|
||||||
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
|
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
|
||||||
|
|
@ -356,18 +355,8 @@ async function fetchStatus() {
|
||||||
code.value = '';
|
code.value = '';
|
||||||
selectedInstallmentNo.value = [];
|
selectedInstallmentNo.value = [];
|
||||||
selectedInstallment.value = [];
|
selectedInstallment.value = [];
|
||||||
view.value =
|
|
||||||
quotationFormData.value.payCondition === 'Full' ||
|
|
||||||
quotationFormData.value.payCondition === 'BillFull'
|
|
||||||
? View.Invoice
|
|
||||||
: View.InvoicePre;
|
|
||||||
|
|
||||||
if (
|
getInvoice();
|
||||||
quotationFormData.value.payCondition === 'Full' ||
|
|
||||||
quotationFormData.value.payCondition === 'BillFull'
|
|
||||||
) {
|
|
||||||
getInvoiceCodeFullPay();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1011,35 +1000,7 @@ onMounted(async () => {
|
||||||
pageState.isLoaded = true;
|
pageState.isLoaded = true;
|
||||||
|
|
||||||
if (route.query['tab'] === 'invoice') {
|
if (route.query['tab'] === 'invoice') {
|
||||||
if (route.query['id']) {
|
getInvoice();
|
||||||
const queryInvoiceId = route.query['id'] as string;
|
|
||||||
const queryInvoiceAmount = Number(route.query['amount']) || 0;
|
|
||||||
|
|
||||||
await getInvoiceCode(queryInvoiceId);
|
|
||||||
|
|
||||||
selectedInstallmentNo.value =
|
|
||||||
quotationFormState.value.source?.paySplit
|
|
||||||
.filter((v) => v.invoiceId === queryInvoiceId)
|
|
||||||
.map((v) => v.no) || [];
|
|
||||||
|
|
||||||
installmentAmount.value = queryInvoiceAmount;
|
|
||||||
view.value = View.Invoice;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedInstallmentNo.value = [];
|
|
||||||
selectedInstallment.value = [];
|
|
||||||
view.value =
|
|
||||||
quotationFormData.value.payCondition === 'Full' ||
|
|
||||||
quotationFormData.value.payCondition === 'BillFull'
|
|
||||||
? View.Invoice
|
|
||||||
: View.InvoicePre;
|
|
||||||
|
|
||||||
if (
|
|
||||||
quotationFormData.value.payCondition === 'Full' ||
|
|
||||||
quotationFormData.value.payCondition === 'BillFull'
|
|
||||||
) {
|
|
||||||
getInvoiceCodeFullPay();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (route.query['tab'] === 'receipt') {
|
if (route.query['tab'] === 'receipt') {
|
||||||
await fetchReceipt();
|
await fetchReceipt();
|
||||||
|
|
@ -1241,6 +1202,23 @@ async function exampleReceipt(id: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getInvoice() {
|
||||||
|
view.value =
|
||||||
|
quotationFormData.value.payCondition === 'Full' ||
|
||||||
|
quotationFormData.value.payCondition === 'BillFull'
|
||||||
|
? View.Invoice
|
||||||
|
: View.InvoicePre;
|
||||||
|
|
||||||
|
if (
|
||||||
|
quotationFormData.value.payCondition === 'Full' ||
|
||||||
|
quotationFormData.value.payCondition === 'BillFull'
|
||||||
|
) {
|
||||||
|
getInvoiceCodeFullPay();
|
||||||
|
} else {
|
||||||
|
code.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[
|
[
|
||||||
() => quotationFormState.value.statusFilterRequest,
|
() => quotationFormState.value.statusFilterRequest,
|
||||||
|
|
@ -1865,7 +1843,7 @@ watch(
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="view === View.Complete"
|
v-if="view === View.Complete"
|
||||||
class="surface-1 q-pa-md full-width q-gutter-y-md"
|
class="surface-1 q-pa-md full-width q-gutter-y-md rounded"
|
||||||
>
|
>
|
||||||
<div class="row justify-between items-center">
|
<div class="row justify-between items-center">
|
||||||
<q-input
|
<q-input
|
||||||
|
|
|
||||||
|
|
@ -11,31 +11,40 @@ defineEmits<{
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
payType: string;
|
title?: string;
|
||||||
amount: number;
|
hideExampleBtn?: boolean;
|
||||||
date: string | Date;
|
successLabel?: string;
|
||||||
index: number;
|
|
||||||
paySplitCount: number;
|
payType?: string;
|
||||||
|
amount?: number;
|
||||||
|
date?: string | Date;
|
||||||
|
index?: number;
|
||||||
|
paySplitCount?: number;
|
||||||
}>(),
|
}>(),
|
||||||
{ payType: 'Full', amount: 0, date: () => new Date() },
|
{ payType: 'Full', amount: 0, date: () => new Date(), index: 0 },
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<section class="surface-1 rounded row">
|
<section class="surface-1 rounded row">
|
||||||
<aside class="column col bordered-r q-py-md q-pl-md">
|
<aside class="column col bordered-r q-py-md q-pl-md">
|
||||||
<span class="text-weight-medium text-body1">
|
<span class="text-weight-medium text-body1">
|
||||||
{{ $t('quotation.receiptDialog.PaymentReceive') }}
|
{{ title || $t('quotation.receiptDialog.PaymentReceive') }}
|
||||||
</span>
|
</span>
|
||||||
<span class="app-text-muted-2 q-pt-md">
|
<span class="app-text-muted-2 q-pt-md">
|
||||||
{{ $t('quotation.receiptDialog.paymentMethod') }}
|
{{ $t('quotation.receiptDialog.paymentMethod') }}
|
||||||
</span>
|
</span>
|
||||||
<span class="row items-center">
|
<span class="row items-center">
|
||||||
{{ $t(`quotation.type.${payType}`) }}
|
<div v-if="$slots.payType" class="row items-center">
|
||||||
{{ payType !== 'Full' ? `${index + 1} / ${paySplitCount}` : `` }}
|
<slot name="payType"></slot>
|
||||||
<q-icon name="mdi-minus" class="q-px-xs" />
|
</div>
|
||||||
<article class="app-text-positive text-weight-medium">
|
<div v-else class="row items-center">
|
||||||
{{ $t('quotation.receiptDialog.PaymentSuccess') }}
|
{{ $t(`quotation.type.${payType}`) }}
|
||||||
</article>
|
{{ payType !== 'Full' ? `${index + 1} / ${paySplitCount}` : `` }}
|
||||||
|
<q-icon name="mdi-minus" class="q-px-xs" />
|
||||||
|
<article class="app-text-positive text-weight-medium">
|
||||||
|
{{ $t('quotation.receiptDialog.PaymentSuccess') }}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
<article class="q-ml-auto text-weight-bold text-body1 q-pr-lg">
|
<article class="q-ml-auto text-weight-bold text-body1 q-pr-lg">
|
||||||
฿ {{ formatNumberDecimal(amount, 2) }}
|
฿ {{ formatNumberDecimal(amount, 2) }}
|
||||||
</article>
|
</article>
|
||||||
|
|
@ -45,6 +54,7 @@ withDefaults(
|
||||||
<aside class="column col q-py-md text-right self-center q-px-md">
|
<aside class="column col q-py-md text-right self-center q-px-md">
|
||||||
<div class="q-gutter-x-xs">
|
<div class="q-gutter-x-xs">
|
||||||
<MainButton
|
<MainButton
|
||||||
|
v-if="!hideExampleBtn"
|
||||||
icon="mdi-play-box-outline"
|
icon="mdi-play-box-outline"
|
||||||
color="207 96% 32%"
|
color="207 96% 32%"
|
||||||
@click="() => $emit('example', index)"
|
@click="() => $emit('example', index)"
|
||||||
|
|
@ -53,7 +63,7 @@ withDefaults(
|
||||||
<ViewButton icon-only @click="() => $emit('view', index)" />
|
<ViewButton icon-only @click="() => $emit('view', index)" />
|
||||||
</div>
|
</div>
|
||||||
<span class="app-text-positive text-weight-bold text-body1">
|
<span class="app-text-positive text-weight-bold text-body1">
|
||||||
{{ $t('quotation.receiptDialog.receiptIssued') }}
|
{{ successLabel || $t('quotation.receiptDialog.receiptIssued') }}
|
||||||
</span>
|
</span>
|
||||||
<div class="app-text-muted-2">
|
<div class="app-text-muted-2">
|
||||||
<q-icon name="mdi-calendar-blank-outline" />
|
<q-icon name="mdi-calendar-blank-outline" />
|
||||||
|
|
@ -62,6 +72,7 @@ withDefaults(
|
||||||
date: date,
|
date: date,
|
||||||
dayStyle: '2-digit',
|
dayStyle: '2-digit',
|
||||||
monthStyle: '2-digit',
|
monthStyle: '2-digit',
|
||||||
|
withTime: true,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ import { precisionRound } from 'src/utils/arithmetic';
|
||||||
import ThaiBahtText from 'thai-baht-text';
|
import ThaiBahtText from 'thai-baht-text';
|
||||||
|
|
||||||
// NOTE: Import stores
|
// NOTE: Import stores
|
||||||
import useOptionStore from 'stores/options';
|
|
||||||
import { formatNumberDecimal } from 'stores/utils';
|
import { formatNumberDecimal } from 'stores/utils';
|
||||||
import { useQuotationForm } from 'pages/05_quotation/form';
|
|
||||||
import { useConfigStore } from 'stores/config';
|
import { useConfigStore } from 'stores/config';
|
||||||
import useBranchStore from 'stores/branch';
|
import useBranchStore from 'stores/branch';
|
||||||
import { baseUrl } from 'stores/utils';
|
import { baseUrl } from 'stores/utils';
|
||||||
|
|
@ -19,8 +17,8 @@ import { CustomerBranch } from 'stores/customer/types';
|
||||||
import { BankBook, Branch } from 'stores/branch/types';
|
import { BankBook, Branch } from 'stores/branch/types';
|
||||||
import {
|
import {
|
||||||
QuotationPayload,
|
QuotationPayload,
|
||||||
CustomerBranchRelation,
|
|
||||||
Details,
|
Details,
|
||||||
|
QuotationFull,
|
||||||
} from 'src/stores/quotations/types';
|
} from 'src/stores/quotations/types';
|
||||||
|
|
||||||
// NOTE: Import Components
|
// NOTE: Import Components
|
||||||
|
|
@ -70,9 +68,7 @@ const attachmentList = ref<
|
||||||
isPDF?: boolean;
|
isPDF?: boolean;
|
||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
const data = ref<
|
const data = ref<QuotationFull & { remark?: string }>();
|
||||||
QuotationPayload & { customerBranch: CustomerBranchRelation; id: string }
|
|
||||||
>();
|
|
||||||
|
|
||||||
const summaryPrice = ref<SummaryPrice>({
|
const summaryPrice = ref<SummaryPrice>({
|
||||||
totalPrice: 0,
|
totalPrice: 0,
|
||||||
|
|
@ -82,6 +78,12 @@ const summaryPrice = ref<SummaryPrice>({
|
||||||
finalPrice: 0,
|
finalPrice: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function fetchQuotationById(id: string) {
|
||||||
|
const res = await quotationStore.getQuotation(id);
|
||||||
|
|
||||||
|
if (res) data.value = res;
|
||||||
|
}
|
||||||
|
|
||||||
async function getAttachment(quotationId: string) {
|
async function getAttachment(quotationId: string) {
|
||||||
const attachment = await quotationStore.listAttachment({
|
const attachment = await quotationStore.listAttachment({
|
||||||
parentId: quotationId,
|
parentId: quotationId,
|
||||||
|
|
@ -178,10 +180,13 @@ onMounted(async () => {
|
||||||
data.value = 'data' in parsed ? parsed.data : undefined;
|
data.value = 'data' in parsed ? parsed.data : undefined;
|
||||||
|
|
||||||
if (data.value) {
|
if (data.value) {
|
||||||
await getAttachment(data.value.id);
|
if (!!data.value.id) {
|
||||||
|
await getAttachment(data.value.id);
|
||||||
|
await fetchQuotationById(data.value.id);
|
||||||
|
}
|
||||||
|
|
||||||
const resCustomerBranch = await customerStore.getBranchById(
|
const resCustomerBranch = await customerStore.getBranchById(
|
||||||
data.value.customerBranchId,
|
data.value?.customerBranchId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (resCustomerBranch) {
|
if (resCustomerBranch) {
|
||||||
|
|
@ -189,19 +194,27 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
details.value = {
|
details.value = {
|
||||||
code: parsed.meta.source.code,
|
code: parsed?.meta?.source?.code ?? data.value?.code,
|
||||||
createdAt: parsed.meta.source.createdAt,
|
createdAt:
|
||||||
createdBy: `${parsed.meta.createdBy} ${!parsed.meta.source.createdBy ? '' : parsed.meta.source.createdBy.telephoneNo}`,
|
parsed?.meta?.source?.createdAt ??
|
||||||
payCondition: parsed.meta.source.payCondition,
|
new Date(data.value?.createdAt || ''),
|
||||||
contactName: parsed.meta.source.contactName,
|
createdBy:
|
||||||
contactTel: parsed.meta.source.contactTel,
|
`${parsed?.meta?.source?.createdBy?.firstName ?? ''} ${parsed?.meta?.source?.createdBy?.telephoneNo ?? ''}`.trim() ||
|
||||||
workName: parsed.meta.source.workName,
|
`${data.value?.createdBy?.firstName ?? ''} ${data.value?.createdBy?.telephoneNo ?? ''}`.trim(),
|
||||||
dueDate: parsed.meta.source.dueDate,
|
payCondition:
|
||||||
worker: parsed.meta.selectedWorker,
|
parsed?.meta?.source?.payCondition ?? data.value?.payCondition,
|
||||||
|
contactName: parsed?.meta?.source?.contactName ?? data.value?.contactName,
|
||||||
|
contactTel: parsed?.meta?.source?.contactTel ?? data.value?.contactTel,
|
||||||
|
workName: parsed?.meta?.source?.workName ?? data.value?.workName,
|
||||||
|
dueDate:
|
||||||
|
parsed?.meta?.source?.dueDate ?? new Date(data.value?.dueDate || ''),
|
||||||
|
worker:
|
||||||
|
parsed?.meta?.selectedWorker ??
|
||||||
|
data.value?.worker.map((v) => v.employee),
|
||||||
};
|
};
|
||||||
|
|
||||||
const resBranch = await branchStore.fetchById(
|
const resBranch = await branchStore.fetchById(
|
||||||
data.value?.registeredBranchId,
|
data.value?.registeredBranchId ?? data.value?.registeredBranchId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (resBranch) {
|
if (resBranch) {
|
||||||
|
|
@ -214,26 +227,31 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
productList.value =
|
productList.value =
|
||||||
data.value?.productServiceList.map((v) => ({
|
(data?.value?.productServiceList ?? data.value?.productServiceList).map(
|
||||||
id: v.product.id,
|
(v) => ({
|
||||||
code: v.product.code,
|
id: v.product.id,
|
||||||
detail: v.product.name,
|
code: v.product.code,
|
||||||
amount: v.amount || 0,
|
detail: v.product.name,
|
||||||
priceUnit: v.pricePerUnit || 0,
|
amount: v.amount || 0,
|
||||||
discount: v.discount || 0,
|
priceUnit: v.pricePerUnit || 0,
|
||||||
vat: v.vat || 0,
|
discount: v.discount || 0,
|
||||||
value: precisionRound(
|
vat: v.vat || 0,
|
||||||
(v.pricePerUnit || 0) * v.amount -
|
value: precisionRound(
|
||||||
(v.discount || 0) +
|
(v.pricePerUnit || 0) * v.amount -
|
||||||
(v.product.calcVat
|
(v.discount || 0) +
|
||||||
? ((v.pricePerUnit || 0) * v.amount - (v.discount || 0)) *
|
(v.product.calcVat
|
||||||
(config.value?.vat || 0.07)
|
? ((v.pricePerUnit || 0) * v.amount - (v.discount || 0)) *
|
||||||
: 0),
|
(config.value?.vat || 0.07)
|
||||||
),
|
: 0),
|
||||||
})) || [];
|
),
|
||||||
|
}),
|
||||||
|
) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
summaryPrice.value = (data.value?.productServiceList || []).reduce(
|
summaryPrice.value = (
|
||||||
|
(data?.value?.productServiceList ?? data.value?.productServiceList) ||
|
||||||
|
[]
|
||||||
|
).reduce(
|
||||||
(a, c) => {
|
(a, c) => {
|
||||||
const price = precisionRound((c.pricePerUnit || 0) * c.amount);
|
const price = precisionRound((c.pricePerUnit || 0) * c.amount);
|
||||||
const vat = precisionRound(
|
const vat = precisionRound(
|
||||||
|
|
|
||||||
|
|
@ -121,14 +121,7 @@ function titleMode(mode: View): string {
|
||||||
<div>
|
<div>
|
||||||
<div>เงื่อนไขการชำระ</div>
|
<div>เงื่อนไขการชำระ</div>
|
||||||
<div>
|
<div>
|
||||||
{{
|
{{ $t(`quotation.type.${details.payCondition}`) }}
|
||||||
{
|
|
||||||
Full: $t('quotation.type.fullAmountCash'),
|
|
||||||
Split: $t('quotation.type.installmentsCash'),
|
|
||||||
BillFull: $t('quotation.type.fullAmountBill'),
|
|
||||||
BillSplit: $t('quotation.type.installmentsBill'),
|
|
||||||
}[details.payCondition]
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ function assignToForm() {
|
||||||
id="btn-info-basic-save"
|
id="btn-info-basic-save"
|
||||||
icon-only
|
icon-only
|
||||||
type="submit"
|
type="submit"
|
||||||
@click.stop="refForm?.submit"
|
@click.stop="(e) => refForm?.submit(e)"
|
||||||
/>
|
/>
|
||||||
<EditButton
|
<EditButton
|
||||||
v-if="!state.isEdit"
|
v-if="!state.isEdit"
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,13 @@ const props = withDefaults(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const readonlyField = ['quotationNo', 'contactPerson', 'telephone', 'employer'];
|
const readonlyField = [
|
||||||
|
'quotationNo',
|
||||||
|
'contactPerson',
|
||||||
|
'telephone',
|
||||||
|
'employer',
|
||||||
|
'employee',
|
||||||
|
];
|
||||||
|
|
||||||
const formRemark = ref<string>('');
|
const formRemark = ref<string>('');
|
||||||
const formData = ref<{
|
const formData = ref<{
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,8 @@ function getEmployeeName(
|
||||||
hide-preview
|
hide-preview
|
||||||
hide-kebab-view
|
hide-kebab-view
|
||||||
hide-kebab-edit
|
hide-kebab-edit
|
||||||
|
hide-kebab-delete
|
||||||
|
use-cancel
|
||||||
:badge-color="
|
:badge-color="
|
||||||
props.row.requestDataStatus === RequestDataStatus.Pending
|
props.row.requestDataStatus === RequestDataStatus.Pending
|
||||||
? '--orange-5-hsl'
|
? '--orange-5-hsl'
|
||||||
|
|
@ -259,6 +261,7 @@ function getEmployeeName(
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
@view="$emit('view', props.row)"
|
@view="$emit('view', props.row)"
|
||||||
|
@cancel="$emit('delete', props.row)"
|
||||||
>
|
>
|
||||||
<template v-slot:responsiblePerson="{ props: subProps }">
|
<template v-slot:responsiblePerson="{ props: subProps }">
|
||||||
<div class="col-4 app-text-muted q-pr-sm self-center">
|
<div class="col-4 app-text-muted q-pr-sm self-center">
|
||||||
|
|
|
||||||
|
|
@ -14,20 +14,25 @@ import FormGroupHead from '../08_request-list/FormGroupHead.vue';
|
||||||
import NoData from 'src/components/NoData.vue';
|
import NoData from 'src/components/NoData.vue';
|
||||||
|
|
||||||
import { baseUrl } from 'src/stores/utils';
|
import { baseUrl } from 'src/stores/utils';
|
||||||
import { Task } from 'src/stores/task-order/types';
|
import { Task, TaskStatus } from 'src/stores/task-order/types';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'select', value: RequestWork[]): void;
|
(e: 'select', value: RequestWork[]): void;
|
||||||
(e: 'afterSubmit'): void;
|
(e: 'afterSubmit'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
creditNote?: boolean;
|
||||||
|
fetchParams?: Parameters<typeof requestListStore.getRequestWorkList>[0];
|
||||||
|
}>();
|
||||||
|
|
||||||
const requestListStore = useRequestList();
|
const requestListStore = useRequestList();
|
||||||
|
|
||||||
const taskList = defineModel<
|
const taskList = defineModel<
|
||||||
{
|
{
|
||||||
step: number;
|
step?: number;
|
||||||
requestWorkId: string;
|
requestWorkId: string;
|
||||||
requestWorkStep?: Task;
|
requestWorkStep?: Task | { requestWork: RequestWork };
|
||||||
}[]
|
}[]
|
||||||
>('taskList', {
|
>('taskList', {
|
||||||
default: [],
|
default: [],
|
||||||
|
|
@ -36,6 +41,7 @@ const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
const selectedEmployee = ref<
|
const selectedEmployee = ref<
|
||||||
(RequestWork & {
|
(RequestWork & {
|
||||||
|
taskStatus: TaskStatus;
|
||||||
_template?: {
|
_template?: {
|
||||||
id: string;
|
id: string;
|
||||||
templateName: string;
|
templateName: string;
|
||||||
|
|
@ -76,7 +82,7 @@ async function getList() {
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 99999,
|
pageSize: 99999,
|
||||||
query: state.search,
|
query: state.search,
|
||||||
readyToTask: true,
|
...props.fetchParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
|
|
@ -141,13 +147,19 @@ function submit() {
|
||||||
|
|
||||||
selectedEmployee.value.forEach((v, i) => {
|
selectedEmployee.value.forEach((v, i) => {
|
||||||
const curr = v.stepStatus.find(
|
const curr = v.stepStatus.find(
|
||||||
(s) => s.workStatus === RequestWorkStatus.Ready,
|
(s) =>
|
||||||
|
s.workStatus ===
|
||||||
|
(props.creditNote
|
||||||
|
? RequestWorkStatus.Canceled
|
||||||
|
: RequestWorkStatus.Ready),
|
||||||
);
|
);
|
||||||
if (curr) {
|
if (curr) {
|
||||||
const task: Task = {
|
const task: Task = {
|
||||||
...curr,
|
...curr,
|
||||||
attributes: curr.attributes,
|
attributes: curr.attributes,
|
||||||
workStatus: RequestWorkStatus.Ready,
|
workStatus: props.creditNote
|
||||||
|
? RequestWorkStatus.Canceled
|
||||||
|
: RequestWorkStatus.Ready,
|
||||||
taskOrderId: '',
|
taskOrderId: '',
|
||||||
requestWork: selectedEmployee.value[i],
|
requestWork: selectedEmployee.value[i],
|
||||||
};
|
};
|
||||||
|
|
@ -254,7 +266,8 @@ function onDialogOpen() {
|
||||||
<div class="q-pa-md full-width">
|
<div class="q-pa-md full-width">
|
||||||
<TableEmployee
|
<TableEmployee
|
||||||
checkbox-on
|
checkbox-on
|
||||||
step-on
|
:step-on="!creditNote"
|
||||||
|
:statusOn="creditNote"
|
||||||
:rows="
|
:rows="
|
||||||
list.map((v) =>
|
list.map((v) =>
|
||||||
Object.assign(v, { _template: getTemplateData(v) }),
|
Object.assign(v, { _template: getTemplateData(v) }),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const props = withDefaults(
|
||||||
checkboxOn?: boolean;
|
checkboxOn?: boolean;
|
||||||
checkAll?: boolean;
|
checkAll?: boolean;
|
||||||
stepOn?: boolean;
|
stepOn?: boolean;
|
||||||
|
statusOn?: boolean;
|
||||||
rows: QTableProps['rows'];
|
rows: QTableProps['rows'];
|
||||||
grid?: boolean;
|
grid?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
|
|
@ -180,7 +181,23 @@ function handleCheck(
|
||||||
},
|
},
|
||||||
...employeeColumn.slice(2),
|
...employeeColumn.slice(2),
|
||||||
]
|
]
|
||||||
: employeeColumn
|
: statusOn
|
||||||
|
? [
|
||||||
|
...employeeColumn,
|
||||||
|
{
|
||||||
|
name: 'urgent',
|
||||||
|
align: 'center',
|
||||||
|
label: '',
|
||||||
|
field: (v) => v.product.code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.status',
|
||||||
|
field: (v) => v.product.code,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: employeeColumn
|
||||||
"
|
"
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
:no-data-label="$t('general.noDataTable')"
|
:no-data-label="$t('general.noDataTable')"
|
||||||
|
|
@ -316,7 +333,7 @@ function handleCheck(
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
{{ col.label && $t(col.label) }}
|
{{ col.label && $t(col.label) }}
|
||||||
</q-th>
|
</q-th>
|
||||||
<q-th></q-th>
|
<q-th v-if="!statusOn"></q-th>
|
||||||
<q-th v-if="$slots.append"></q-th>
|
<q-th v-if="$slots.append"></q-th>
|
||||||
|
|
||||||
<q-th v-if="$slots.action"></q-th>
|
<q-th v-if="$slots.action"></q-th>
|
||||||
|
|
@ -338,7 +355,7 @@ function handleCheck(
|
||||||
>
|
>
|
||||||
<q-tr
|
<q-tr
|
||||||
:class="{
|
:class="{
|
||||||
urgent: props.row.request.quotation.urgent,
|
urgent: props.row.request.quotation?.urgent,
|
||||||
dark: $q.dark.isActive,
|
dark: $q.dark.isActive,
|
||||||
['disabled-row']:
|
['disabled-row']:
|
||||||
selectedEmployee.length > 0 &&
|
selectedEmployee.length > 0 &&
|
||||||
|
|
@ -392,7 +409,7 @@ function handleCheck(
|
||||||
<q-img
|
<q-img
|
||||||
class="text-center"
|
class="text-center"
|
||||||
:ratio="1"
|
:ratio="1"
|
||||||
:src="`${baseUrl}/employee/${props.row.request.employee.id}/image/${props.row.request.employee.selectedImage}`"
|
:src="`${baseUrl}/employee/${props.row.request.employee?.id}/image/${props.row.request.employee?.selectedImage}`"
|
||||||
>
|
>
|
||||||
<template #error>
|
<template #error>
|
||||||
<span class="full-width full-height">
|
<span class="full-width full-height">
|
||||||
|
|
@ -407,27 +424,27 @@ function handleCheck(
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="app-text-muted">
|
<div class="app-text-muted">
|
||||||
{{ props.row.request.employee.code }}
|
{{ props.row.request.employee?.code }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Icon
|
<Icon
|
||||||
class="q-ml-md"
|
class="q-ml-md"
|
||||||
:class="`app-text-${props.row.request.employee.gender}`"
|
:class="`app-text-${props.row.request.employee?.gender}`"
|
||||||
:icon="`material-symbols:${props.row.request.employee.gender}`"
|
:icon="`material-symbols:${props.row.request.employee?.gender}`"
|
||||||
width="24px"
|
width="24px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td>{{ calculateAge(props.row.request.employee.dateOfBirth) }}</q-td>
|
<q-td>{{ calculateAge(props.row.request.employee?.dateOfBirth) }}</q-td>
|
||||||
<q-td>
|
<q-td>
|
||||||
{{
|
{{
|
||||||
useOptionStore().mapOption(props.row.request.employee.nationality)
|
useOptionStore().mapOption(props.row.request.employee?.nationality)
|
||||||
}}
|
}}
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td>
|
<q-td>
|
||||||
{{
|
{{
|
||||||
dateFormatJS({
|
dateFormatJS({
|
||||||
date: props.row.request.quotation.dueDate,
|
date: props.row.request.quotation?.dueDate,
|
||||||
locale: $i18n.locale,
|
locale: $i18n.locale,
|
||||||
dayStyle: '2-digit',
|
dayStyle: '2-digit',
|
||||||
monthStyle: 'short',
|
monthStyle: 'short',
|
||||||
|
|
@ -436,7 +453,7 @@ function handleCheck(
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td>
|
<q-td>
|
||||||
<ExpirationDate
|
<ExpirationDate
|
||||||
:expiration-date="new Date(props.row.request.quotation.dueDate)"
|
:expiration-date="new Date(props.row.request.quotation?.dueDate)"
|
||||||
/>
|
/>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td>
|
<q-td>
|
||||||
|
|
@ -444,12 +461,12 @@ function handleCheck(
|
||||||
class="cursor-pointer link"
|
class="cursor-pointer link"
|
||||||
@click="goToQuotation(props.row.request.quotation)"
|
@click="goToQuotation(props.row.request.quotation)"
|
||||||
>
|
>
|
||||||
{{ props.row.request.quotation.code }}
|
{{ props.row.request.quotation?.code }}
|
||||||
</span>
|
</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td>
|
<q-td>
|
||||||
<BadgeComponent
|
<BadgeComponent
|
||||||
v-if="props.row.request.quotation.urgent"
|
v-if="props.row.request.quotation?.urgent"
|
||||||
icon="mdi-fire"
|
icon="mdi-fire"
|
||||||
:title="$t('general.urgent2')"
|
:title="$t('general.urgent2')"
|
||||||
hsla-color="--gray-1-hsl"
|
hsla-color="--gray-1-hsl"
|
||||||
|
|
@ -457,6 +474,12 @@ function handleCheck(
|
||||||
solid
|
solid
|
||||||
/>
|
/>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
<q-td v-if="statusOn">
|
||||||
|
<BadgeComponent
|
||||||
|
:title="$t('creditNote.status.Canceled')"
|
||||||
|
hsla-color="--red-8-hsl"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
<q-td v-if="$slots.append">
|
<q-td v-if="$slots.append">
|
||||||
<slot name="append" :props="props"></slot>
|
<slot name="append" :props="props"></slot>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const fileData = defineModel<
|
||||||
loaded: number;
|
loaded: number;
|
||||||
total: number;
|
total: number;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
placeholder?: boolean;
|
||||||
}[]
|
}[]
|
||||||
>('fileData', { default: [] });
|
>('fileData', { default: [] });
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,14 @@ const taskProduct = defineModel<{ productId: string; discount?: number }[]>(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
agentPrice?: boolean;
|
||||||
taskList: {
|
taskList: {
|
||||||
product: RequestWork['productService']['product'];
|
product: RequestWork['productService']['product'];
|
||||||
list: RequestWork[];
|
list: RequestWork[];
|
||||||
}[];
|
}[];
|
||||||
|
creditNote?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
|
|
@ -55,15 +57,28 @@ function openList(index: number) {
|
||||||
|
|
||||||
function calcPricePerUnit(product: RequestWork['productService']['product']) {
|
function calcPricePerUnit(product: RequestWork['productService']['product']) {
|
||||||
return product.vatIncluded
|
return product.vatIncluded
|
||||||
? product.serviceCharge / (1 + (config.value?.vat || 0.07))
|
? (props.creditNote
|
||||||
: product.serviceCharge;
|
? props.agentPrice
|
||||||
|
? product.agentPrice
|
||||||
|
: product.price
|
||||||
|
: product.serviceCharge) /
|
||||||
|
(1 + (config.value?.vat || 0.07))
|
||||||
|
: props.creditNote
|
||||||
|
? props.agentPrice
|
||||||
|
? product.agentPrice
|
||||||
|
: product.price
|
||||||
|
: product.serviceCharge;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcPrice(
|
function calcPrice(
|
||||||
product: RequestWork['productService']['product'],
|
product: RequestWork['productService']['product'],
|
||||||
amount: number,
|
amount: number,
|
||||||
) {
|
) {
|
||||||
const pricePerUnit = product.serviceCharge;
|
const pricePerUnit = props.creditNote
|
||||||
|
? props.agentPrice
|
||||||
|
? product.agentPrice
|
||||||
|
: product.price
|
||||||
|
: product.serviceCharge;
|
||||||
const discount =
|
const discount =
|
||||||
taskProduct.value.find((v) => v.productId === product.id)?.discount || 0;
|
taskProduct.value.find((v) => v.productId === product.id)?.discount || 0;
|
||||||
const priceNoVat = product.vatIncluded
|
const priceNoVat = product.vatIncluded
|
||||||
|
|
@ -102,7 +117,16 @@ function calcPrice(
|
||||||
|
|
||||||
<main class="q-px-md q-py-sm surface-1">
|
<main class="q-px-md q-py-sm surface-1">
|
||||||
<q-table
|
<q-table
|
||||||
:columns="productColumn"
|
:columns="
|
||||||
|
creditNote
|
||||||
|
? productColumn.filter(
|
||||||
|
(v) =>
|
||||||
|
v.name !== 'discount' &&
|
||||||
|
v.name !== 'priceBeforeVat' &&
|
||||||
|
v.name !== 'vat',
|
||||||
|
)
|
||||||
|
: productColumn
|
||||||
|
"
|
||||||
:rows="taskList"
|
:rows="taskList"
|
||||||
bordered
|
bordered
|
||||||
flat
|
flat
|
||||||
|
|
@ -174,7 +198,7 @@ function calcPrice(
|
||||||
}}
|
}}
|
||||||
</q-td>
|
</q-td>
|
||||||
<!-- TODO: display price detail -->
|
<!-- TODO: display price detail -->
|
||||||
<q-td align="center">
|
<q-td align="center" v-if="!creditNote">
|
||||||
<q-input
|
<q-input
|
||||||
:readonly
|
:readonly
|
||||||
:bg-color="readonly ? 'transparent' : ''"
|
:bg-color="readonly ? 'transparent' : ''"
|
||||||
|
|
@ -213,11 +237,11 @@ function calcPrice(
|
||||||
/>
|
/>
|
||||||
</q-td>
|
</q-td>
|
||||||
<!-- before vat -->
|
<!-- before vat -->
|
||||||
<q-td class="text-right">
|
<q-td class="text-right" v-if="!creditNote">
|
||||||
{{ formatNumberDecimal(calcPricePerUnit(props.row.product), 2) }}
|
{{ formatNumberDecimal(calcPricePerUnit(props.row.product), 2) }}
|
||||||
</q-td>
|
</q-td>
|
||||||
<!-- vat -->
|
<!-- vat -->
|
||||||
<q-td class="text-right">
|
<q-td class="text-right" v-if="!creditNote">
|
||||||
{{
|
{{
|
||||||
formatNumberDecimal(
|
formatNumberDecimal(
|
||||||
props.row.product.calcVat
|
props.row.product.calcVat
|
||||||
|
|
@ -267,7 +291,11 @@ function calcPrice(
|
||||||
|
|
||||||
<q-tr v-show="currentBtnOpen[props.rowIndex]" :props="props">
|
<q-tr v-show="currentBtnOpen[props.rowIndex]" :props="props">
|
||||||
<q-td colspan="100%" style="padding: 16px">
|
<q-td colspan="100%" style="padding: 16px">
|
||||||
<TableEmployee step-on :rows="props.row.list" />
|
<TableEmployee
|
||||||
|
:step-on="!creditNote"
|
||||||
|
:status-on="creditNote"
|
||||||
|
:rows="props.row.list"
|
||||||
|
/>
|
||||||
</q-td>
|
</q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1130,6 +1130,7 @@ watch([currentFormData.value.taskStatus], () => {
|
||||||
|
|
||||||
<!-- SEC: Dialog -->
|
<!-- SEC: Dialog -->
|
||||||
<SelectReadyRequestWork
|
<SelectReadyRequestWork
|
||||||
|
:fetch-params="{ readyToTask: true }"
|
||||||
v-model:open="pageState.productDialog"
|
v-model:open="pageState.productDialog"
|
||||||
v-model:task-list="currentFormData.taskList"
|
v-model:task-list="currentFormData.taskList"
|
||||||
@after-submit="
|
@after-submit="
|
||||||
|
|
|
||||||
412
src/pages/10_invoice/MainPage.vue
Normal file
412
src/pages/10_invoice/MainPage.vue
Normal file
|
|
@ -0,0 +1,412 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// NOTE: Library
|
||||||
|
import { computed, onMounted, reactive, watch } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
// NOTE: Components
|
||||||
|
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||||
|
import NoData from 'src/components/NoData.vue';
|
||||||
|
import PaginationComponent from 'src/components/PaginationComponent.vue';
|
||||||
|
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||||
|
import TableInvoice from './TableInvoice.vue';
|
||||||
|
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
|
||||||
|
|
||||||
|
// NOTE: Stores & Type
|
||||||
|
import { useNavigator } from 'src/stores/navigator';
|
||||||
|
import { columns, hslaColors } from './constants';
|
||||||
|
import useFlowStore from 'src/stores/flow';
|
||||||
|
import { useRequestList } from 'src/stores/request-list';
|
||||||
|
import { useInvoice } from 'src/stores/payment';
|
||||||
|
import { Invoice, PaymentDataStatus } from 'src/stores/payment/types';
|
||||||
|
import { Quotation } from 'src/stores/quotations';
|
||||||
|
|
||||||
|
const navigatorStore = useNavigator();
|
||||||
|
const flowStore = useFlowStore();
|
||||||
|
const invoiceStore = useInvoice();
|
||||||
|
const { data, stats, page, pageMax, pageSize } = storeToRefs(invoiceStore);
|
||||||
|
|
||||||
|
// NOTE: Variable
|
||||||
|
const pageState = reactive({
|
||||||
|
hideStat: false,
|
||||||
|
statusFilter: 'None' as 'None' | PaymentDataStatus,
|
||||||
|
inputSearch: '',
|
||||||
|
fieldSelected: [...columns.map((v) => v.name)],
|
||||||
|
gridView: false,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldSelectedOption = computed(() => {
|
||||||
|
return columns.map((v) => ({
|
||||||
|
label: v.label,
|
||||||
|
value: v.name,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchList(opts?: { rotateFlowId?: boolean }) {
|
||||||
|
const ret = await invoiceStore.getInvoiceList({
|
||||||
|
page: page.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
query: pageState.inputSearch,
|
||||||
|
pay:
|
||||||
|
pageState.statusFilter === PaymentDataStatus.Success
|
||||||
|
? true
|
||||||
|
: pageState.statusFilter === PaymentDataStatus.Wait
|
||||||
|
? false
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
if (ret) {
|
||||||
|
data.value = ret.result;
|
||||||
|
pageState.total = ret.total;
|
||||||
|
pageMax.value = Math.ceil(ret.total / pageSize.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.rotateFlowId) flowStore.rotate();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchStats() {
|
||||||
|
const ret = await invoiceStore.getInvoiceStats();
|
||||||
|
if (ret) stats.value = ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerView(opts: { quotationId: string }) {
|
||||||
|
const url = new URL(`/quotation/view?tab=invoice`, window.location.origin);
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
'new-quotation',
|
||||||
|
JSON.stringify({
|
||||||
|
quotationId: opts.quotationId,
|
||||||
|
statusDialog: 'info',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
window.open(url.toString(), '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewDocExample(quotationId: string) {
|
||||||
|
localStorage.setItem(
|
||||||
|
'quotation-preview',
|
||||||
|
JSON.stringify({
|
||||||
|
data: {
|
||||||
|
id: quotationId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const url = new URL('/quotation/document-view', window.location.origin);
|
||||||
|
|
||||||
|
url.searchParams.append('type', 'invoice');
|
||||||
|
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
navigatorStore.current.title = 'invoice.title';
|
||||||
|
navigatorStore.current.path = [{ text: 'invoice.caption', i18n: true }];
|
||||||
|
|
||||||
|
await fetchStats();
|
||||||
|
await fetchList({ rotateFlowId: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
() => pageState.inputSearch,
|
||||||
|
() => pageState.statusFilter,
|
||||||
|
() => pageSize.value,
|
||||||
|
() => page.value,
|
||||||
|
],
|
||||||
|
() => fetchList({ rotateFlowId: true }),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="column full-height no-wrap">
|
||||||
|
<!-- SEC: stat -->
|
||||||
|
<section class="text-body-2 q-mb-xs flex items-center">
|
||||||
|
{{ $t('general.dataSum') }}
|
||||||
|
<q-badge
|
||||||
|
rounded
|
||||||
|
class="q-ml-sm"
|
||||||
|
style="
|
||||||
|
background-color: hsla(var(--info-bg) / 0.15);
|
||||||
|
color: hsl(var(--info-bg));
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ Object.values(stats).reduce((a, c) => a + c, 0) }}
|
||||||
|
</q-badge>
|
||||||
|
<q-btn
|
||||||
|
class="q-ml-sm"
|
||||||
|
icon="mdi-pin-outline"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
rounded
|
||||||
|
@click="pageState.hideStat = !pageState.hideStat"
|
||||||
|
:style="pageState.hideStat ? 'rotate: 90deg' : ''"
|
||||||
|
style="transition: 0.1s ease-in-out"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<transition name="slide">
|
||||||
|
<div v-if="!pageState.hideStat" class="scroll q-mb-md">
|
||||||
|
<div style="display: inline-block">
|
||||||
|
<StatCardComponent
|
||||||
|
labelI18n
|
||||||
|
:branch="[
|
||||||
|
{
|
||||||
|
icon: 'mdi-timer-sand',
|
||||||
|
count: stats[PaymentDataStatus.Wait],
|
||||||
|
label: 'invoice.status.PaymentWait',
|
||||||
|
color: 'orange',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-check-decagram-outline',
|
||||||
|
count: stats[PaymentDataStatus.Success],
|
||||||
|
label: 'invoice.status.PaymentSuccess',
|
||||||
|
color: 'light-green',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:dark="$q.dark.isActive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<section class="col surface-1 rounded bordered overflow-hidden">
|
||||||
|
<div class="column full-height">
|
||||||
|
<!-- SEC: header content -->
|
||||||
|
<header
|
||||||
|
class="row surface-3 justify-between full-width items-center bordered-b"
|
||||||
|
style="z-index: 1"
|
||||||
|
>
|
||||||
|
<div class="row q-py-sm q-px-md justify-between full-width">
|
||||||
|
<q-input
|
||||||
|
for="input-search"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
:label="$t('general.search')"
|
||||||
|
class="q-mr-md col-12 col-md-3"
|
||||||
|
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||||
|
v-model="pageState.inputSearch"
|
||||||
|
debounce="200"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-magnify" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="row col-12 col-md-5 justify-end"
|
||||||
|
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||||
|
style="white-space: nowrap"
|
||||||
|
>
|
||||||
|
<q-select
|
||||||
|
v-model="pageState.statusFilter"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
option-value="value"
|
||||||
|
option-label="label"
|
||||||
|
class="col"
|
||||||
|
:class="{ 'offset-md-5': pageState.gridView }"
|
||||||
|
map-options
|
||||||
|
emit-value
|
||||||
|
:for="'field-select-status'"
|
||||||
|
:hide-dropdown-icon="$q.screen.lt.sm"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
label: $t('general.all'),
|
||||||
|
value: 'None',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('invoice.status.PaymentWait'),
|
||||||
|
value: PaymentDataStatus.Wait,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('invoice.status.PaymentSuccess'),
|
||||||
|
value: PaymentDataStatus.Success,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-if="!pageState.gridView"
|
||||||
|
id="select-field"
|
||||||
|
for="select-field"
|
||||||
|
class="col q-ml-sm"
|
||||||
|
:options="
|
||||||
|
fieldSelectedOption.map((v) => ({
|
||||||
|
...v,
|
||||||
|
label: v.label && $t(v.label),
|
||||||
|
}))
|
||||||
|
"
|
||||||
|
:display-value="$t('general.displayField')"
|
||||||
|
:hide-dropdown-icon="$q.screen.lt.sm"
|
||||||
|
v-model="pageState.fieldSelected"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
map-options
|
||||||
|
emit-value
|
||||||
|
outlined
|
||||||
|
multiple
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn-toggle
|
||||||
|
id="btn-mode"
|
||||||
|
v-model="pageState.gridView"
|
||||||
|
dense
|
||||||
|
class="no-shadow bordered rounded surface-1 q-ml-sm"
|
||||||
|
:toggle-color="$q.dark.isActive ? 'grey-9' : 'grey-2'"
|
||||||
|
size="xs"
|
||||||
|
:options="[
|
||||||
|
{ value: true, slot: 'folder' },
|
||||||
|
{ value: false, slot: 'list' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<template v-slot:folder>
|
||||||
|
<q-icon
|
||||||
|
name="mdi-view-grid-outline"
|
||||||
|
size="16px"
|
||||||
|
class="q-px-sm q-py-xs rounded"
|
||||||
|
:style="{
|
||||||
|
color: $q.dark.isActive
|
||||||
|
? pageState.gridView
|
||||||
|
? '#C9D3DB '
|
||||||
|
: '#787B7C'
|
||||||
|
: pageState.gridView
|
||||||
|
? '#787B7C'
|
||||||
|
: '#C9D3DB',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:list>
|
||||||
|
<q-icon
|
||||||
|
name="mdi-format-list-bulleted"
|
||||||
|
class="q-px-sm q-py-xs rounded"
|
||||||
|
size="16px"
|
||||||
|
:style="{
|
||||||
|
color: $q.dark.isActive
|
||||||
|
? pageState.gridView === false
|
||||||
|
? '#C9D3DB'
|
||||||
|
: '#787B7C'
|
||||||
|
: pageState.gridView === false
|
||||||
|
? '#787B7C'
|
||||||
|
: '#C9D3DB',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-btn-toggle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- SEC: body content -->
|
||||||
|
<article
|
||||||
|
v-if="data.length === 0"
|
||||||
|
class="col surface-2 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<NoData :not-found="!!pageState.inputSearch" />
|
||||||
|
</article>
|
||||||
|
<article v-else class="col surface-2 full-width scroll q-pa-md">
|
||||||
|
<TableInvoice
|
||||||
|
:columns="columns"
|
||||||
|
:rows="data"
|
||||||
|
:grid="pageState.gridView"
|
||||||
|
@view="(quotationId) => triggerView({ quotationId })"
|
||||||
|
@preview="(quotationId) => viewDocExample(quotationId)"
|
||||||
|
>
|
||||||
|
<template #grid="{ item }">
|
||||||
|
<div class="col-md-4 col-sm-6 col-12">
|
||||||
|
<QuotationCard
|
||||||
|
hide-action
|
||||||
|
:code="item.row.code"
|
||||||
|
:title="item.row.quotation.workName"
|
||||||
|
:status="
|
||||||
|
$t(`invoice.status.${item.row.payment.paymentStatus}`)
|
||||||
|
"
|
||||||
|
:badge-color="
|
||||||
|
hslaColors[item.row.payment.paymentStatus] || ''
|
||||||
|
"
|
||||||
|
:custom-data="[
|
||||||
|
{
|
||||||
|
label: $t('general.customer'),
|
||||||
|
value:
|
||||||
|
item.row.quotation.customerBranch.registerName ||
|
||||||
|
`${item.row.quotation.customerBranch?.firstName || '-'} ${item.row.quotation.customerBranch?.lastName || ''}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('taskOrder.issueDate'),
|
||||||
|
value: new Date(item.row.createdAt).toLocaleString(
|
||||||
|
'th-TH',
|
||||||
|
{
|
||||||
|
hour12: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('invoice.paymentDueDate'),
|
||||||
|
value: new Date(
|
||||||
|
item.row.quotation.dueDate,
|
||||||
|
).toLocaleDateString('th-TH', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: 'numeric',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('quotation.payType'),
|
||||||
|
value: $t(
|
||||||
|
`quotation.type.${item.row.quotation.payCondition}`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('preview.netValue'),
|
||||||
|
value: item.row.amount,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
@view="
|
||||||
|
() => triggerView({ quotationId: item.row.quotation.id })
|
||||||
|
"
|
||||||
|
@preview="
|
||||||
|
() => {
|
||||||
|
viewDocExample(item.row.quotation.id);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TableInvoice>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- SEC: footer content -->
|
||||||
|
<footer
|
||||||
|
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
||||||
|
v-if="pageMax > 0"
|
||||||
|
>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="app-text-muted q-mr-sm" v-if="$q.screen.gt.sm">
|
||||||
|
{{ $t('general.recordPerPage') }}
|
||||||
|
</div>
|
||||||
|
<div><PaginationPageSize v-model="pageSize" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 row justify-center app-text-muted">
|
||||||
|
{{
|
||||||
|
$t('general.recordsPage', {
|
||||||
|
resultcurrentPage: data.length,
|
||||||
|
total: pageState.inputSearch ? data.length : pageState.total,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<nav class="col-4 row justify-end">
|
||||||
|
<PaginationComponent
|
||||||
|
v-model:current-page="page"
|
||||||
|
v-model:max-page="pageMax"
|
||||||
|
:fetch-data="() => fetchList({ rotateFlowId: true })"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
146
src/pages/10_invoice/TableInvoice.vue
Normal file
146
src/pages/10_invoice/TableInvoice.vue
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
<script setup lang="ts" generic="T">
|
||||||
|
import { QTableSlots } from 'quasar';
|
||||||
|
import { columns } from './constants';
|
||||||
|
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||||
|
|
||||||
|
import { Invoice, PaymentDataStatus } from 'src/stores/payment/types';
|
||||||
|
import { ViewButton } from 'components/button';
|
||||||
|
import { useInvoice } from 'src/stores/payment';
|
||||||
|
|
||||||
|
const invoiceStore = useInvoice();
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
rows: T[];
|
||||||
|
readonly?: boolean;
|
||||||
|
flat?: boolean;
|
||||||
|
bordered?: boolean;
|
||||||
|
grid?: boolean;
|
||||||
|
hideHeader?: boolean;
|
||||||
|
buttonDownload?: boolean;
|
||||||
|
buttonDelete?: boolean;
|
||||||
|
hidePagination?: boolean;
|
||||||
|
inTable?: boolean;
|
||||||
|
hideView?: boolean;
|
||||||
|
btnSelected?: boolean;
|
||||||
|
|
||||||
|
imgColumn?: string;
|
||||||
|
customColumn?: string[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
row: () => [],
|
||||||
|
flat: false,
|
||||||
|
bordered: false,
|
||||||
|
grid: false,
|
||||||
|
hideHeader: false,
|
||||||
|
buttonDownload: false,
|
||||||
|
imgColumn: '',
|
||||||
|
customColumn: () => [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'view', id: string): void;
|
||||||
|
(e: 'preview', id: string): void;
|
||||||
|
(e: 'edit', data: T, index: number): void;
|
||||||
|
(e: 'delete', data: T, index: number): void;
|
||||||
|
(e: 'download', data: T, index: number): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-table
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
:rows="rows.map((data, i) => ({ ...data, _index: i }))"
|
||||||
|
:columns
|
||||||
|
:grid
|
||||||
|
hide-bottom
|
||||||
|
bordered
|
||||||
|
flat
|
||||||
|
hide-pagination
|
||||||
|
selection="multiple"
|
||||||
|
card-container-class="q-col-gutter-sm"
|
||||||
|
class="full-width"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr
|
||||||
|
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
<q-th v-for="col in columns" :key="col.name" :props="props">
|
||||||
|
{{ $t(col.label) }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-slot:body="props: {
|
||||||
|
row: Invoice & { _index: number };
|
||||||
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||||
|
>
|
||||||
|
<q-tr :class="{ dark: $q.dark.isActive }" class="text-center">
|
||||||
|
<q-td v-for="col in columns" :align="col.align">
|
||||||
|
<!-- NOTE: custom column will starts with # -->
|
||||||
|
<template v-if="!col.name.startsWith('#')">
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
typeof col.field === 'string'
|
||||||
|
? props.row[col.field as keyof Invoice]
|
||||||
|
: col.field(props.row)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-if="col.name === '#order'">
|
||||||
|
{{
|
||||||
|
col.field(props.row) +
|
||||||
|
(invoiceStore.page - 1) * invoiceStore.pageSize
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<template v-if="col.name === '#status'">
|
||||||
|
<BadgeComponent
|
||||||
|
:hsla-color="
|
||||||
|
{
|
||||||
|
[PaymentDataStatus.Success]: '--green-8-hsl',
|
||||||
|
[PaymentDataStatus.Wait]: '--orange-5-hsl',
|
||||||
|
}[props.row.payment.paymentStatus]
|
||||||
|
"
|
||||||
|
:title="$t(`invoice.status.${props.row.payment.paymentStatus}`)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-if="col.name === '#action'">
|
||||||
|
<q-btn
|
||||||
|
:id="`btn-preview-${props.row.quotation.workName}`"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
rounded
|
||||||
|
icon="mdi-play-box-outline"
|
||||||
|
size="12px"
|
||||||
|
:title="$t('preview.doc')"
|
||||||
|
@click.stop="$emit('preview', props.row.quotation.id)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
:id="`btn-eye-${props.row.quotation.workName}`"
|
||||||
|
icon="mdi-eye-outline"
|
||||||
|
size="sm"
|
||||||
|
dense
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
@click.stop="$emit('view', props.row.quotation.id)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item="props: { row: Invoice }">
|
||||||
|
<slot name="grid" :item="props" />
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(i.q-icon.mdi.mdi-alert.q-table__bottom-nodata-icon) {
|
||||||
|
color: #ffc224 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
111
src/pages/10_invoice/constants.ts
Normal file
111
src/pages/10_invoice/constants.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { QTableProps } from 'quasar';
|
||||||
|
import { Invoice } from 'src/stores/payment/types';
|
||||||
|
import { formatNumberDecimal } from 'src/stores/utils';
|
||||||
|
import { dateFormatJS } from 'src/utils/datetime';
|
||||||
|
|
||||||
|
export const columns = [
|
||||||
|
{
|
||||||
|
name: '#order',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.order',
|
||||||
|
field: (v: Invoice & { _index: number }) => v._index + 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'invoiceCode',
|
||||||
|
align: 'center',
|
||||||
|
label: 'requestList.invoiceCode',
|
||||||
|
field: 'code',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'workSheetName',
|
||||||
|
align: 'center',
|
||||||
|
label: 'invoice.workSheetName',
|
||||||
|
field: (v: Invoice) => v.quotation.workName,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'customer',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.customer',
|
||||||
|
field: (v: Invoice) => v.quotation.customerBranch.customerName,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'issueDate',
|
||||||
|
align: 'center',
|
||||||
|
label: 'taskOrder.issueDate',
|
||||||
|
field: (v: Invoice) => dateFormatJS({ date: v.createdAt }),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'paymentDueDate',
|
||||||
|
align: 'center',
|
||||||
|
label: 'invoice.paymentDueDate',
|
||||||
|
field: (v: Invoice) => dateFormatJS({ date: v.quotation.dueDate }),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
align: 'center',
|
||||||
|
label: 'preview.value',
|
||||||
|
field: (v: Invoice) => formatNumberDecimal(v.amount, 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#status',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.status',
|
||||||
|
field: (_) => '#status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#action',
|
||||||
|
align: 'center',
|
||||||
|
label: '',
|
||||||
|
field: (_) => '#action',
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
||||||
|
export const docColumn = [
|
||||||
|
{
|
||||||
|
name: 'order',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.order',
|
||||||
|
field: 'no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document',
|
||||||
|
align: 'left',
|
||||||
|
label: 'general.document',
|
||||||
|
field: 'document',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'attachment',
|
||||||
|
align: 'left',
|
||||||
|
label: 'requestList.attachment',
|
||||||
|
field: 'attachment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.amount',
|
||||||
|
field: 'amount',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'documentInSystem',
|
||||||
|
align: 'center',
|
||||||
|
label: 'requestList.documentInSystem',
|
||||||
|
field: 'documentInSystem',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.status',
|
||||||
|
field: 'status',
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
||||||
|
export const hslaColors: Record<string, string> = {
|
||||||
|
PaymentWait: '--orange-5-hsl',
|
||||||
|
PaymentSuccess: '--green-8-hsl',
|
||||||
|
};
|
||||||
841
src/pages/11_credit-note/FormPage.vue
Normal file
841
src/pages/11_credit-note/FormPage.vue
Normal file
|
|
@ -0,0 +1,841 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, reactive, computed } from 'vue';
|
||||||
|
import { api } from 'src/boot/axios';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { dateFormatJS } from 'src/utils/datetime';
|
||||||
|
import { initLang, initTheme } from 'src/utils/ui';
|
||||||
|
import { useQuotationStore } from 'src/stores/quotations';
|
||||||
|
import {
|
||||||
|
CreditNote,
|
||||||
|
CreditNotePaybackStatus,
|
||||||
|
CreditNotePayload,
|
||||||
|
CreditNoteStatus,
|
||||||
|
useCreditNote,
|
||||||
|
} from 'src/stores/credit-note';
|
||||||
|
import { useConfigStore } from 'src/stores/config';
|
||||||
|
import DocumentExpansion from './expansion/DocumentExpansion.vue';
|
||||||
|
import RemarkExpansion from '../09_task-order/expansion/RemarkExpansion.vue';
|
||||||
|
import AdditionalFileExpansion from '../09_task-order/expansion/AdditionalFileExpansion.vue';
|
||||||
|
import PaymentExpansion from './expansion/PaymentExpansion.vue';
|
||||||
|
import CreditNoteExpansion from './expansion/CreditNoteExpansion.vue';
|
||||||
|
import StateButton from 'src/components/button/StateButton.vue';
|
||||||
|
import ProductExpansion from '../09_task-order/expansion/ProductExpansion.vue';
|
||||||
|
import SelectReadyRequestWork from '../09_task-order/SelectReadyRequestWork.vue';
|
||||||
|
import RefundInformation from './RefundInformation.vue';
|
||||||
|
import QuotationFormReceipt from '../05_quotation/QuotationFormReceipt.vue';
|
||||||
|
import DialogViewFile from 'src/components/dialog/DialogViewFile.vue';
|
||||||
|
import { MainButton, SaveButton } from 'src/components/button';
|
||||||
|
import { RequestWork } from 'src/stores/request-list/types';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import useOptionStore from 'src/stores/options';
|
||||||
|
import { dialogWarningClose } from 'src/stores/utils';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { QForm } from 'quasar';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const creditNote = useCreditNote();
|
||||||
|
const quotation = useQuotationStore();
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
const { data: config } = storeToRefs(configStore);
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const refForm = ref<InstanceType<typeof QForm>>();
|
||||||
|
const creditNoteData = ref<CreditNote>();
|
||||||
|
const quotationData = ref<CreditNote['quotation']>();
|
||||||
|
const view = ref<CreditNoteStatus | null>(null);
|
||||||
|
const fileList = ref<FileList>();
|
||||||
|
const attachmentList = ref<FileList>();
|
||||||
|
const fileData = ref<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
progress: number;
|
||||||
|
loaded: number;
|
||||||
|
total: number;
|
||||||
|
url?: string;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
const attachmentData = ref<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
progress: number;
|
||||||
|
loaded: number;
|
||||||
|
total: number;
|
||||||
|
url?: string;
|
||||||
|
placeholder?: boolean;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const statusTabForm = ref<
|
||||||
|
{
|
||||||
|
title: string;
|
||||||
|
status: 'done' | 'doing' | 'waiting';
|
||||||
|
handler: () => void;
|
||||||
|
active?: () => boolean;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const readonly = computed(
|
||||||
|
() =>
|
||||||
|
creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Pending ||
|
||||||
|
creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Success,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageState = reactive({
|
||||||
|
productDialog: false,
|
||||||
|
fileDialog: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formData = ref<CreditNotePayload>({
|
||||||
|
quotationId: '',
|
||||||
|
requestWorkId: [],
|
||||||
|
remark: '',
|
||||||
|
reason: '',
|
||||||
|
detail: '',
|
||||||
|
paybackType: 'Cash',
|
||||||
|
paybackBank: '',
|
||||||
|
paybackAccount: '',
|
||||||
|
paybackAccountName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const formTaskList = ref<
|
||||||
|
{
|
||||||
|
requestWorkId: string;
|
||||||
|
requestWorkStep?: { requestWork: RequestWork };
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
let taskListGroup = computed(() => {
|
||||||
|
const cacheData = formTaskList.value.reduce<
|
||||||
|
{
|
||||||
|
product: RequestWork['productService']['product'];
|
||||||
|
list: RequestWork[];
|
||||||
|
}[]
|
||||||
|
>((acc, curr) => {
|
||||||
|
const task = curr.requestWorkStep;
|
||||||
|
|
||||||
|
if (!task) return acc;
|
||||||
|
|
||||||
|
if (task.requestWork) {
|
||||||
|
let exist = acc.find(
|
||||||
|
(item) => task.requestWork.productService.productId == item.product.id,
|
||||||
|
);
|
||||||
|
const record = Object.assign(task.requestWork);
|
||||||
|
|
||||||
|
if (exist) {
|
||||||
|
exist.list.push(task.requestWork);
|
||||||
|
} else {
|
||||||
|
acc.push({
|
||||||
|
product: task.requestWork.productService.product,
|
||||||
|
list: [record],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return cacheData;
|
||||||
|
});
|
||||||
|
|
||||||
|
const summaryPrice = computed(() => getPrice(taskListGroup.value));
|
||||||
|
|
||||||
|
async function initStatus() {
|
||||||
|
if (creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Pending)
|
||||||
|
view.value = CreditNoteStatus.Pending;
|
||||||
|
if (creditNoteData.value?.creditNoteStatus === CreditNoteStatus.Success)
|
||||||
|
view.value = CreditNoteStatus.Success;
|
||||||
|
|
||||||
|
statusTabForm.value = [
|
||||||
|
{
|
||||||
|
title: 'title',
|
||||||
|
status: creditNoteData.value?.id !== undefined ? 'done' : 'doing',
|
||||||
|
active: () => view.value === null,
|
||||||
|
handler: () => {
|
||||||
|
view.value = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Pending',
|
||||||
|
status: creditNoteData.value?.id
|
||||||
|
? creditNoteData.value.creditNoteStatus === CreditNoteStatus.Success
|
||||||
|
? 'done'
|
||||||
|
: 'doing'
|
||||||
|
: 'waiting',
|
||||||
|
active: () => view.value === CreditNoteStatus.Pending,
|
||||||
|
handler: async () => {
|
||||||
|
view.value = CreditNoteStatus.Pending;
|
||||||
|
creditNoteData.value &&
|
||||||
|
(await getFileList(creditNoteData.value.id, true));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Success',
|
||||||
|
status:
|
||||||
|
creditNoteData.value?.id &&
|
||||||
|
creditNoteData.value.creditNoteStatus === CreditNoteStatus.Success
|
||||||
|
? 'done'
|
||||||
|
: 'waiting',
|
||||||
|
active: () => view.value === CreditNoteStatus.Success,
|
||||||
|
handler: () => {
|
||||||
|
view.value = CreditNoteStatus.Success;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrice(
|
||||||
|
list: {
|
||||||
|
product: RequestWork['productService']['product'];
|
||||||
|
list: RequestWork[];
|
||||||
|
}[],
|
||||||
|
) {
|
||||||
|
return list.reduce(
|
||||||
|
(a, c) => {
|
||||||
|
const pricePerUnit = quotationData.value?.agentPrice
|
||||||
|
? c.product.agentPrice
|
||||||
|
: c.product.price;
|
||||||
|
const amount = c.list.length;
|
||||||
|
const discount = 0;
|
||||||
|
const priceNoVat = c.product.vatIncluded
|
||||||
|
? pricePerUnit / (1 + (config.value?.vat || 0.07))
|
||||||
|
: pricePerUnit;
|
||||||
|
const priceDiscountNoVat = priceNoVat * amount - discount;
|
||||||
|
|
||||||
|
const rawVatTotal = priceDiscountNoVat * (config.value?.vat || 0.07);
|
||||||
|
// const rawVat = rawVatTotal / amount;
|
||||||
|
|
||||||
|
a.totalPrice = a.totalPrice + priceDiscountNoVat;
|
||||||
|
a.totalDiscount = a.totalDiscount + Number(discount);
|
||||||
|
a.vat = c.product.calcVat ? a.vat + rawVatTotal : a.vat;
|
||||||
|
a.vatExcluded = c.product.calcVat ? a.vatExcluded : a.vat + rawVatTotal;
|
||||||
|
a.finalPrice = a.totalPrice - a.totalDiscount + a.vat;
|
||||||
|
return a;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
totalPrice: 0,
|
||||||
|
totalDiscount: 0,
|
||||||
|
vat: 0,
|
||||||
|
vatExcluded: 0,
|
||||||
|
finalPrice: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openProductDialog() {
|
||||||
|
pageState.productDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCreditNote() {
|
||||||
|
if (typeof route.params['id'] !== 'string') return;
|
||||||
|
|
||||||
|
const ret = await creditNote.getCreditNote(route.params['id']);
|
||||||
|
|
||||||
|
if (!ret) return;
|
||||||
|
|
||||||
|
creditNoteData.value = ret;
|
||||||
|
assignFormData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignFormData() {
|
||||||
|
if (!creditNoteData.value) return;
|
||||||
|
|
||||||
|
const current = creditNoteData.value;
|
||||||
|
|
||||||
|
formData.value = {
|
||||||
|
quotationId: creditNoteData.value.quotationId,
|
||||||
|
requestWorkId: creditNoteData.value.requestWork.map((v) => v.id || ''),
|
||||||
|
reason: creditNoteData.value.reason,
|
||||||
|
remark: creditNoteData.value.remark,
|
||||||
|
detail: creditNoteData.value.detail,
|
||||||
|
paybackType: creditNoteData.value.paybackType,
|
||||||
|
paybackBank: creditNoteData.value.paybackBank,
|
||||||
|
paybackAccount: creditNoteData.value.paybackAccount,
|
||||||
|
paybackAccountName: creditNoteData.value.paybackAccountName,
|
||||||
|
};
|
||||||
|
|
||||||
|
formTaskList.value = creditNoteData.value.requestWork.map((v) => ({
|
||||||
|
requestWorkId: v.id || '',
|
||||||
|
requestWorkStep: {
|
||||||
|
requestWork: {
|
||||||
|
...v,
|
||||||
|
request: { ...v.request, quotation: current.quotation },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSlipDialog() {
|
||||||
|
pageState.fileDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getQuotation() {
|
||||||
|
if (creditNoteData.value) {
|
||||||
|
quotationData.value = creditNoteData.value.quotation;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
route.name !== 'CreditNoteNew' ||
|
||||||
|
typeof route.query['quotationId'] !== 'string'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = await quotation.getQuotation(route.query['quotationId']);
|
||||||
|
|
||||||
|
if (!ret) return;
|
||||||
|
|
||||||
|
quotationData.value = ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
const payload = formData.value;
|
||||||
|
payload.requestWorkId = formTaskList.value.map((v) => v.requestWorkId);
|
||||||
|
payload.quotationId =
|
||||||
|
typeof route.query['quotationId'] === 'string'
|
||||||
|
? route.query['quotationId']
|
||||||
|
: '';
|
||||||
|
const res = await creditNote.createCreditNote(payload);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
await router.push(`/credit-note/${res.id}`);
|
||||||
|
|
||||||
|
await getCreditNote();
|
||||||
|
|
||||||
|
if (attachmentList.value) {
|
||||||
|
await uploadFile(res.id, attachmentList.value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
initStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToQuotation() {
|
||||||
|
if (!quotationData.value) return;
|
||||||
|
|
||||||
|
const quotation = quotationData.value;
|
||||||
|
const url = new URL('/quotation/view', window.location.origin);
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
'new-quotation',
|
||||||
|
JSON.stringify({
|
||||||
|
customerBranchId: quotation.customerBranchId,
|
||||||
|
agentPrice: quotation.agentPrice,
|
||||||
|
statusDialog: 'info',
|
||||||
|
quotationId: quotation.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
window.open(url.toString(), '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changePaybackStatus(status: CreditNotePaybackStatus) {
|
||||||
|
if (!creditNoteData.value) return;
|
||||||
|
const res = await creditNote.action.updatePaybackStatus(
|
||||||
|
creditNoteData.value.id,
|
||||||
|
status,
|
||||||
|
);
|
||||||
|
if (res) {
|
||||||
|
creditNoteData.value.paybackStatus = status;
|
||||||
|
|
||||||
|
if (status === CreditNotePaybackStatus.Done) {
|
||||||
|
creditNoteData.value.creditNoteStatus = CreditNoteStatus.Success;
|
||||||
|
initStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFile(
|
||||||
|
creditNoteId: string,
|
||||||
|
list: FileList,
|
||||||
|
slip?: boolean,
|
||||||
|
) {
|
||||||
|
const promises: ReturnType<
|
||||||
|
typeof creditNote.putAttachment | typeof creditNote.putFile
|
||||||
|
>[] = [];
|
||||||
|
|
||||||
|
if (!slip) {
|
||||||
|
attachmentData.value = attachmentData.value.filter((v) => !v.placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
const data = {
|
||||||
|
name: list[i].name,
|
||||||
|
progress: 1,
|
||||||
|
loaded: 0,
|
||||||
|
total: 0,
|
||||||
|
url: `/credit-note/${creditNoteId}/${slip ? 'file-slip' : 'attachment'}/${list[i].name}`,
|
||||||
|
};
|
||||||
|
promises.push(
|
||||||
|
slip
|
||||||
|
? creditNote.putFile({
|
||||||
|
group: 'slip',
|
||||||
|
parentId: creditNoteId,
|
||||||
|
fileId: list[i].name,
|
||||||
|
file: list[i],
|
||||||
|
uploadUrl: true,
|
||||||
|
onUploadProgress: (e) => {
|
||||||
|
const exists = fileData?.value.find((v) => v.name === data.name);
|
||||||
|
if (!exists) return fileData?.value.push(data);
|
||||||
|
exists.total = e.total || 0;
|
||||||
|
exists.progress = e.progress || 0;
|
||||||
|
exists.loaded = e.loaded;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: creditNote.putAttachment({
|
||||||
|
parentId: creditNoteId,
|
||||||
|
name: list[i].name,
|
||||||
|
file: list[i],
|
||||||
|
onUploadProgress: (e) => {
|
||||||
|
const exists = attachmentData?.value.find(
|
||||||
|
(v) => v.name === data.name,
|
||||||
|
);
|
||||||
|
if (!exists) return attachmentData?.value.push(data);
|
||||||
|
exists.total = e.total || 0;
|
||||||
|
exists.progress = e.progress || 0;
|
||||||
|
exists.loaded = e.loaded;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
slip ? fileData?.value.push(data) : attachmentData?.value.push(data);
|
||||||
|
}
|
||||||
|
fileList.value = undefined;
|
||||||
|
attachmentList.value = undefined;
|
||||||
|
|
||||||
|
const beforeUnloadHandler = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', beforeUnloadHandler);
|
||||||
|
|
||||||
|
return await Promise.all(promises).then((v) => {
|
||||||
|
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(creditNoteId: string, n: string, slip?: boolean) {
|
||||||
|
dialogWarningClose(t, {
|
||||||
|
message: t('dialog.message.confirmDelete'),
|
||||||
|
actionText: t('dialog.action.ok'),
|
||||||
|
action: async () => {
|
||||||
|
const res = slip
|
||||||
|
? await creditNote.delFile({
|
||||||
|
group: 'slip',
|
||||||
|
parentId: creditNoteId,
|
||||||
|
fileId: n,
|
||||||
|
})
|
||||||
|
: await creditNote.delAttachment({
|
||||||
|
parentId: creditNoteId,
|
||||||
|
name: n,
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
getFileList(creditNoteId, slip);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancel: () => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileList(creditNoteId: string, slip?: boolean) {
|
||||||
|
const list = slip
|
||||||
|
? await creditNote.listFile({
|
||||||
|
group: 'slip',
|
||||||
|
parentId: creditNoteId,
|
||||||
|
})
|
||||||
|
: await creditNote.listAttachment({
|
||||||
|
parentId: creditNoteId,
|
||||||
|
});
|
||||||
|
if (list)
|
||||||
|
slip
|
||||||
|
? (fileData.value = await Promise.all(
|
||||||
|
list.map(async (v) => {
|
||||||
|
const rse = await creditNote.headFile({
|
||||||
|
group: 'slip',
|
||||||
|
parentId: creditNoteId,
|
||||||
|
fileId: v,
|
||||||
|
});
|
||||||
|
|
||||||
|
let contentLength = 0;
|
||||||
|
if (rse) contentLength = Number(rse['content-length']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: v,
|
||||||
|
progress: 1,
|
||||||
|
loaded: contentLength,
|
||||||
|
total: contentLength,
|
||||||
|
url: `/credit-note/${creditNoteId}/file-slip/${v}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
: (attachmentData.value = await Promise.all(
|
||||||
|
list.map(async (v) => {
|
||||||
|
const rse = await creditNote.headAttachment({
|
||||||
|
parentId: creditNoteId,
|
||||||
|
fileId: v,
|
||||||
|
});
|
||||||
|
|
||||||
|
let contentLength = 0;
|
||||||
|
if (rse) contentLength = Number(rse['content-length']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: v,
|
||||||
|
progress: 1,
|
||||||
|
loaded: contentLength,
|
||||||
|
total: contentLength,
|
||||||
|
url: `/credit-note/${creditNoteId}/attachment/${v}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
initTheme();
|
||||||
|
initLang();
|
||||||
|
await useConfigStore().getConfig();
|
||||||
|
await getCreditNote();
|
||||||
|
await getQuotation();
|
||||||
|
creditNoteData.value && (await getFileList(creditNoteData.value.id, true));
|
||||||
|
initStatus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="column surface-0 fullscreen">
|
||||||
|
<div class="color-bar" :class="{ ['dark']: $q.dark.isActive }">
|
||||||
|
<div :class="{ ['indigo-segment']: true }"></div>
|
||||||
|
<div :class="{ ['light-indigo-segment']: true }"></div>
|
||||||
|
<div class="white-segment"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SEC: Header -->
|
||||||
|
<header
|
||||||
|
class="row q-px-md q-py-sm items-center justify-between relative-position"
|
||||||
|
>
|
||||||
|
<section class="banner" :class="{ dark: $q.dark.isActive }"></section>
|
||||||
|
<div style="flex: 1" class="row items-center">
|
||||||
|
<RouterLink to="/credit-note">
|
||||||
|
<q-img src="/icons/favicon-512x512.png" width="3rem" />
|
||||||
|
</RouterLink>
|
||||||
|
<span class="column text-h6 text-bold q-ml-md">
|
||||||
|
{{ $t('creditNote.title') }}
|
||||||
|
<!-- {{ code || '' }} -->
|
||||||
|
<span class="text-caption text-regular app-text-muted">
|
||||||
|
{{
|
||||||
|
$t('quotation.processOn', {
|
||||||
|
msg: dateFormatJS({
|
||||||
|
date: creditNoteData?.createdAt || new Date(Date.now()),
|
||||||
|
monthStyle: 'long',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- SEC: Body -->
|
||||||
|
<article
|
||||||
|
class="col full-width q-pa-md"
|
||||||
|
style="flex-grow: 1; overflow-y: auto"
|
||||||
|
>
|
||||||
|
<section class="col-sm col-12">
|
||||||
|
<div class="col q-gutter-y-md">
|
||||||
|
<nav
|
||||||
|
class="surface-1 q-pa-sm row no-wrap full-width scroll rounded"
|
||||||
|
style="gap: 10px"
|
||||||
|
>
|
||||||
|
<StateButton
|
||||||
|
v-for="i in statusTabForm"
|
||||||
|
:key="i.title"
|
||||||
|
:label="
|
||||||
|
$t(
|
||||||
|
`creditNote${i.title === 'title' ? '' : '.stats'}.${i.title}`,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:status-active="i.active?.()"
|
||||||
|
:status-done="i.status === 'done'"
|
||||||
|
:status-waiting="i.status === 'waiting'"
|
||||||
|
@click="i.handler()"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<DocumentExpansion
|
||||||
|
readonly
|
||||||
|
:registered-branch-id="quotationData?.registeredBranchId"
|
||||||
|
:customer-id="quotationData?.customerBranchId"
|
||||||
|
:quotation-code="quotationData?.code || '-'"
|
||||||
|
:quotation-work-name="quotationData?.workName || '-'"
|
||||||
|
:quotation-contact-name="quotationData?.contactName || '-'"
|
||||||
|
:quotation-contact-tel="quotationData?.contactTel || '-'"
|
||||||
|
@goto-quotation="goToQuotation"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-form
|
||||||
|
ref="refForm"
|
||||||
|
greedy
|
||||||
|
@submit.prevent
|
||||||
|
@validation-success="submit"
|
||||||
|
>
|
||||||
|
<CreditNoteExpansion
|
||||||
|
v-if="view === null"
|
||||||
|
:readonly="readonly"
|
||||||
|
v-model:reason="formData.reason"
|
||||||
|
v-model:detail="formData.detail"
|
||||||
|
/>
|
||||||
|
</q-form>
|
||||||
|
|
||||||
|
<ProductExpansion
|
||||||
|
v-if="view === null"
|
||||||
|
creditNote
|
||||||
|
:readonly="readonly"
|
||||||
|
:agentPrice="quotationData?.agentPrice"
|
||||||
|
:task-list="taskListGroup"
|
||||||
|
@add-product="openProductDialog"
|
||||||
|
/>
|
||||||
|
<PaymentExpansion
|
||||||
|
v-if="view === null"
|
||||||
|
:readonly="readonly"
|
||||||
|
:total-price="summaryPrice.finalPrice"
|
||||||
|
v-model:payback-type="formData.paybackType"
|
||||||
|
v-model:payback-bank="formData.paybackBank"
|
||||||
|
v-model:payback-account="formData.paybackAccount"
|
||||||
|
v-model:payback-account-name="formData.paybackAccountName"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RefundInformation
|
||||||
|
v-if="view === CreditNoteStatus.Pending"
|
||||||
|
:total="creditNoteData?.value"
|
||||||
|
:paid="
|
||||||
|
creditNoteData?.paybackStatus === CreditNotePaybackStatus.Done
|
||||||
|
? creditNoteData?.value
|
||||||
|
: 0
|
||||||
|
"
|
||||||
|
:remain="
|
||||||
|
creditNoteData?.paybackStatus === CreditNotePaybackStatus.Pending
|
||||||
|
? creditNoteData?.value
|
||||||
|
: 0
|
||||||
|
"
|
||||||
|
:payback-status="creditNoteData?.paybackStatus"
|
||||||
|
v-model:payback-type="formData.paybackType"
|
||||||
|
v-model:payback-bank="formData.paybackBank"
|
||||||
|
v-model:payback-account="formData.paybackAccount"
|
||||||
|
v-model:payback-account-name="formData.paybackAccountName"
|
||||||
|
v-model:file-data="fileData"
|
||||||
|
:transform-url="
|
||||||
|
async (url: string) => {
|
||||||
|
const result = await api.get<string>(url);
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@change-status="changePaybackStatus"
|
||||||
|
@upload="
|
||||||
|
async (f) => {
|
||||||
|
if (!creditNoteData) return;
|
||||||
|
|
||||||
|
fileList = f;
|
||||||
|
|
||||||
|
await uploadFile(creditNoteData.id, f, true);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@remove="
|
||||||
|
async (n) => {
|
||||||
|
if (!creditNoteData) return;
|
||||||
|
await remove(creditNoteData.id, n, true);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AdditionalFileExpansion
|
||||||
|
v-if="view !== CreditNoteStatus.Success"
|
||||||
|
:readonly="false"
|
||||||
|
v-model:file-data="attachmentData"
|
||||||
|
:transform-url="
|
||||||
|
async (url: string) => {
|
||||||
|
const result = await api.get<string>(url);
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@fetch-file-list="
|
||||||
|
() => {
|
||||||
|
if (!creditNoteData) return;
|
||||||
|
getFileList(creditNoteData.id);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@upload="
|
||||||
|
async (f) => {
|
||||||
|
attachmentList = f;
|
||||||
|
attachmentData = [];
|
||||||
|
|
||||||
|
Array.from(f).forEach((el) => {
|
||||||
|
attachmentData.push({
|
||||||
|
name: el.name,
|
||||||
|
progress: 1,
|
||||||
|
loaded: 0,
|
||||||
|
total: el.size,
|
||||||
|
placeholder: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!creditNoteData) return;
|
||||||
|
|
||||||
|
await uploadFile(creditNoteData.id, f);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@remove="
|
||||||
|
async (n) => {
|
||||||
|
const attIndex = attachmentData.findIndex((v) => v.name === n);
|
||||||
|
|
||||||
|
attachmentData.splice(attIndex, 1);
|
||||||
|
|
||||||
|
if (!creditNoteData) return;
|
||||||
|
await remove(creditNoteData.id, n);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- TODO: bind remark -->
|
||||||
|
<RemarkExpansion
|
||||||
|
v-if="view !== CreditNoteStatus.Success"
|
||||||
|
:readonly="readonly"
|
||||||
|
v-model:remark="formData.remark"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QuotationFormReceipt
|
||||||
|
v-if="creditNoteData && view === CreditNoteStatus.Success"
|
||||||
|
hide-example-btn
|
||||||
|
:title="$t('creditNote.label.refund')"
|
||||||
|
:amount="creditNoteData.value"
|
||||||
|
:success-label="$t('creditNote.label.refundSuccess')"
|
||||||
|
:date="
|
||||||
|
creditNoteData.paybackDate
|
||||||
|
? new Date(creditNoteData.paybackDate)
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
|
@view="openSlipDialog"
|
||||||
|
>
|
||||||
|
<template #payType>
|
||||||
|
<span v-if="creditNoteData.paybackType === 'BankTransfer'">
|
||||||
|
<q-img
|
||||||
|
:src="`/img/bank/${creditNoteData?.paybackBank}.png`"
|
||||||
|
class="bordered q-mr-xs"
|
||||||
|
style="border-radius: 50%; width: 20px"
|
||||||
|
/>
|
||||||
|
{{ useOptionStore().mapOption(creditNoteData?.paybackBank) }}
|
||||||
|
{{ creditNoteData?.paybackAccount }}
|
||||||
|
{{
|
||||||
|
`${$t('creditNote.label.accountName')} ${creditNoteData?.paybackAccountName}`
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ $t('creditNote.label.Cash') }}</span>
|
||||||
|
</template>
|
||||||
|
</QuotationFormReceipt>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- SEC: footer -->
|
||||||
|
<footer class="surface-1 q-pa-md full-width">
|
||||||
|
<nav class="row justify-end">
|
||||||
|
<!-- TODO: view example -->
|
||||||
|
<MainButton
|
||||||
|
class="q-mr-auto"
|
||||||
|
outlined
|
||||||
|
icon="mdi-play-box-outline"
|
||||||
|
color="207 96% 32%"
|
||||||
|
@click="console.log('view example')"
|
||||||
|
>
|
||||||
|
{{ $t('general.view', { msg: $t('general.example') }) }}
|
||||||
|
</MainButton>
|
||||||
|
<!-- @click="submit" -->
|
||||||
|
<SaveButton
|
||||||
|
v-if="!readonly"
|
||||||
|
type="submit"
|
||||||
|
@click.stop="(e) => refForm?.submit(e)"
|
||||||
|
:label="$t('creditNote.label.submit')"
|
||||||
|
icon="mdi-account-multiple-check-outline"
|
||||||
|
solid
|
||||||
|
></SaveButton>
|
||||||
|
</nav>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SEC: Dialog -->
|
||||||
|
<SelectReadyRequestWork
|
||||||
|
v-if="quotationData"
|
||||||
|
creditNote
|
||||||
|
:fetch-params="{ cancelOnly: true, quotationId: quotationData.id }"
|
||||||
|
v-model:open="pageState.productDialog"
|
||||||
|
v-model:task-list="formTaskList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogViewFile
|
||||||
|
hide-tab
|
||||||
|
download
|
||||||
|
v-model="pageState.fileDialog"
|
||||||
|
:url="fileData[0]?.url"
|
||||||
|
:transform-url="
|
||||||
|
async (url: string) => {
|
||||||
|
const result = await api.get<string>(url);
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.color-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 1vh;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgb(47, 68, 173) 0%,
|
||||||
|
rgba(255, 255, 255, 1) 77%,
|
||||||
|
rgba(204, 204, 204, 1) 100%
|
||||||
|
);
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-bar.dark {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indigo-segment {
|
||||||
|
background-color: var(--indigo-10);
|
||||||
|
flex-grow: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-indigo-segment {
|
||||||
|
background-color: hsla(var(--indigo-10-hsl) / 0.2);
|
||||||
|
flex-grow: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white-segment {
|
||||||
|
background-color: #ffffff;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indigo-segment,
|
||||||
|
.light-indigo-segment,
|
||||||
|
.white-segment {
|
||||||
|
transform: skewX(-60deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: url('/images/building-banner.png');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
filter: invert(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
467
src/pages/11_credit-note/MainPage.vue
Normal file
467
src/pages/11_credit-note/MainPage.vue
Normal file
|
|
@ -0,0 +1,467 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// NOTE: Library
|
||||||
|
import { computed, onMounted, reactive, watch } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
// NOTE: Components
|
||||||
|
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||||
|
import NoData from 'src/components/NoData.vue';
|
||||||
|
import PaginationComponent from 'src/components/PaginationComponent.vue';
|
||||||
|
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||||
|
import FloatingActionButton from 'components/FloatingActionButton.vue';
|
||||||
|
import DialogFormContainer from 'src/components/dialog/DialogFormContainer.vue';
|
||||||
|
import DialogHeader from 'src/components/dialog/DialogHeader.vue';
|
||||||
|
import { CancelButton, SaveButton } from 'src/components/button';
|
||||||
|
import FormCredit from 'src/components/11_credit-note/FormCredit.vue';
|
||||||
|
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
|
||||||
|
|
||||||
|
// NOTE: Stores & Type
|
||||||
|
import { useNavigator } from 'src/stores/navigator';
|
||||||
|
import useFlowStore from 'src/stores/flow';
|
||||||
|
import { pageTabs, columns, hslaColors } from './constants';
|
||||||
|
import { CreditNoteStatus, useCreditNote } from 'src/stores/credit-note';
|
||||||
|
import TableCreditNote from './TableCreditNote.vue';
|
||||||
|
import { dialogWarningClose } from 'src/stores/utils';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const flow = useFlowStore();
|
||||||
|
const navigator = useNavigator();
|
||||||
|
const creditNote = useCreditNote();
|
||||||
|
const { stats, pageMax, page, data, pageSize } = storeToRefs(creditNote);
|
||||||
|
|
||||||
|
// NOTE: Variable
|
||||||
|
const pageState = reactive({
|
||||||
|
quotationId: '',
|
||||||
|
currentTab: CreditNoteStatus.Pending,
|
||||||
|
hideStat: false,
|
||||||
|
statusFilter: 'None',
|
||||||
|
inputSearch: '',
|
||||||
|
fieldSelected: columns
|
||||||
|
.filter((v) => !v.name.startsWith('#'))
|
||||||
|
.map((v) => v.name),
|
||||||
|
gridView: false,
|
||||||
|
total: 0,
|
||||||
|
|
||||||
|
creditDialog: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldSelectedOption = computed(() => {
|
||||||
|
return columns
|
||||||
|
.filter((v) => !v.name.startsWith('#'))
|
||||||
|
.map((v) => ({
|
||||||
|
label: v.label,
|
||||||
|
value: v.name,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: Function
|
||||||
|
async function getList(opts?: { page?: number; pageSize?: number }) {
|
||||||
|
const res = await creditNote.getCreditNoteList({
|
||||||
|
page: opts?.page || page.value,
|
||||||
|
pageSize: opts?.pageSize || pageSize.value,
|
||||||
|
query: pageState.inputSearch === '' ? undefined : pageState.inputSearch,
|
||||||
|
creditNoteStatus: pageState.currentTab as CreditNoteStatus | undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
data.value = res.result;
|
||||||
|
pageState.total = res.total;
|
||||||
|
pageMax.value = Math.ceil(res.total / pageSize.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerDelete(id: string) {
|
||||||
|
dialogWarningClose(t, {
|
||||||
|
message: t('dialog.message.confirmDelete'),
|
||||||
|
actionText: t('dialog.action.ok'),
|
||||||
|
action: async () => {
|
||||||
|
const res = await creditNote.deleteCreditNote(id);
|
||||||
|
if (!!res) {
|
||||||
|
getList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancel: () => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerCreateCreditNote() {
|
||||||
|
pageState.creditDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(opts: {
|
||||||
|
statusDialog: 'info' | 'edit' | 'create';
|
||||||
|
quotationId?: string;
|
||||||
|
creditId?: string;
|
||||||
|
}) {
|
||||||
|
const url = new URL(
|
||||||
|
`/credit-note/${opts.statusDialog === 'create' ? 'add' : opts.creditId}`,
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (opts.statusDialog === 'create') {
|
||||||
|
url.searchParams.append('quotationId', opts.quotationId || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(url.toString(), '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
navigateTo({ statusDialog: 'create', quotationId: pageState.quotationId });
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
pageState.creditDialog = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
navigator.current.title = 'creditNote.title';
|
||||||
|
navigator.current.path = [{ text: 'creditNote.caption', i18n: true }];
|
||||||
|
|
||||||
|
creditNote.getCreditNoteStats().then((res) => res && (stats.value = res));
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
() => pageState.currentTab,
|
||||||
|
() => pageState.inputSearch,
|
||||||
|
() => pageSize.value,
|
||||||
|
() => pageState.statusFilter,
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
getList();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<FloatingActionButton
|
||||||
|
style="z-index: 999"
|
||||||
|
hide-icon
|
||||||
|
@click.stop="triggerCreateCreditNote()"
|
||||||
|
></FloatingActionButton>
|
||||||
|
|
||||||
|
<div class="column full-height no-wrap">
|
||||||
|
<!-- SEC: stat -->
|
||||||
|
<section class="text-body-2 q-mb-xs flex items-center">
|
||||||
|
{{ $t('general.dataSum') }}
|
||||||
|
<q-badge
|
||||||
|
rounded
|
||||||
|
class="q-ml-sm"
|
||||||
|
style="
|
||||||
|
background-color: hsla(var(--info-bg) / 0.15);
|
||||||
|
color: hsl(var(--info-bg));
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ pageState.total }}
|
||||||
|
</q-badge>
|
||||||
|
<q-btn
|
||||||
|
class="q-ml-sm"
|
||||||
|
icon="mdi-pin-outline"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
rounded
|
||||||
|
@click="pageState.hideStat = !pageState.hideStat"
|
||||||
|
:style="pageState.hideStat ? 'rotate: 90deg' : ''"
|
||||||
|
style="transition: 0.1s ease-in-out"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<transition name="slide">
|
||||||
|
<div v-if="!pageState.hideStat" class="scroll q-mb-md">
|
||||||
|
<div style="display: inline-block">
|
||||||
|
<StatCardComponent
|
||||||
|
labelI18n
|
||||||
|
:branch="[
|
||||||
|
{
|
||||||
|
icon: 'material-symbols-light:receipt-long',
|
||||||
|
count: stats[CreditNoteStatus.Pending],
|
||||||
|
label: `creditNote.stats.${CreditNoteStatus.Pending}`,
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-check-decagram-outline',
|
||||||
|
count: stats[CreditNoteStatus.Success],
|
||||||
|
label: `creditNote.stats.${CreditNoteStatus.Success}`,
|
||||||
|
color: 'orange',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:dark="$q.dark.isActive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<section class="col surface-1 rounded bordered overflow-hidden">
|
||||||
|
<div class="column full-height">
|
||||||
|
<!-- SEC: header content -->
|
||||||
|
<header
|
||||||
|
class="row surface-3 justify-between full-width items-center"
|
||||||
|
style="z-index: 1"
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
class="row q-py-sm q-px-md bordered-b justify-between full-width"
|
||||||
|
>
|
||||||
|
<q-input
|
||||||
|
for="input-search"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
:label="$t('general.search')"
|
||||||
|
class="q-mr-md col-12 col-md-3"
|
||||||
|
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||||
|
v-model="pageState.inputSearch"
|
||||||
|
debounce="200"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-magnify" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="row col-12 col-md-3 justify-end"
|
||||||
|
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||||
|
style="white-space: nowrap"
|
||||||
|
>
|
||||||
|
<q-select
|
||||||
|
v-if="!pageState.gridView"
|
||||||
|
id="select-field"
|
||||||
|
for="select-field"
|
||||||
|
class="col"
|
||||||
|
:options="
|
||||||
|
fieldSelectedOption.map((v) => ({
|
||||||
|
...v,
|
||||||
|
label: v.label && $t(v.label),
|
||||||
|
}))
|
||||||
|
"
|
||||||
|
:display-value="$t('general.displayField')"
|
||||||
|
:hide-dropdown-icon="$q.screen.lt.sm"
|
||||||
|
v-model="pageState.fieldSelected"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
map-options
|
||||||
|
emit-value
|
||||||
|
outlined
|
||||||
|
multiple
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn-toggle
|
||||||
|
id="btn-mode"
|
||||||
|
v-model="pageState.gridView"
|
||||||
|
dense
|
||||||
|
class="no-shadow bordered rounded surface-1 q-ml-sm"
|
||||||
|
:toggle-color="$q.dark.isActive ? 'grey-9' : 'grey-2'"
|
||||||
|
size="xs"
|
||||||
|
:options="[
|
||||||
|
{ value: true, slot: 'folder' },
|
||||||
|
{ value: false, slot: 'list' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<template v-slot:folder>
|
||||||
|
<q-icon
|
||||||
|
name="mdi-view-grid-outline"
|
||||||
|
size="16px"
|
||||||
|
class="q-px-sm q-py-xs rounded"
|
||||||
|
:style="{
|
||||||
|
color: $q.dark.isActive
|
||||||
|
? pageState.gridView
|
||||||
|
? '#C9D3DB '
|
||||||
|
: '#787B7C'
|
||||||
|
: pageState.gridView
|
||||||
|
? '#787B7C'
|
||||||
|
: '#C9D3DB',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:list>
|
||||||
|
<q-icon
|
||||||
|
name="mdi-format-list-bulleted"
|
||||||
|
class="q-px-sm q-py-xs rounded"
|
||||||
|
size="16px"
|
||||||
|
:style="{
|
||||||
|
color: $q.dark.isActive
|
||||||
|
? pageState.gridView === false
|
||||||
|
? '#C9D3DB'
|
||||||
|
: '#787B7C'
|
||||||
|
: pageState.gridView === false
|
||||||
|
? '#787B7C'
|
||||||
|
: '#C9D3DB',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-btn-toggle>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav class="surface-2 bordered-b q-px-md full-width">
|
||||||
|
<q-tabs
|
||||||
|
inline-label
|
||||||
|
mobile-arrows
|
||||||
|
dense
|
||||||
|
v-model="pageState.currentTab"
|
||||||
|
align="left"
|
||||||
|
class="full-width"
|
||||||
|
active-color="info"
|
||||||
|
>
|
||||||
|
<q-tab
|
||||||
|
v-for="tab in pageTabs"
|
||||||
|
:name="tab.value"
|
||||||
|
:key="tab.value"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
pageState.currentTab = tab.value;
|
||||||
|
pageState.inputSearch = '';
|
||||||
|
|
||||||
|
flow.rotate();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row text-capitalize"
|
||||||
|
:class="
|
||||||
|
pageState.currentTab === tab.value
|
||||||
|
? 'text-bold'
|
||||||
|
: 'app-text-muted'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ $t(`creditNote.status.${tab.label}`) }}
|
||||||
|
</div>
|
||||||
|
</q-tab>
|
||||||
|
</q-tabs>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- SEC: body content -->
|
||||||
|
<article
|
||||||
|
v-if="data.length === 0"
|
||||||
|
class="col surface-2 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<NoData :not-found="!!pageState.inputSearch" />
|
||||||
|
</article>
|
||||||
|
<article v-else class="col surface-2 full-width scroll q-pa-md">
|
||||||
|
<TableCreditNote
|
||||||
|
:grid="pageState.gridView"
|
||||||
|
:visible-columns="pageState.fieldSelected"
|
||||||
|
:hide-delete="pageState.currentTab === CreditNoteStatus.Success"
|
||||||
|
@view="(v) => navigateTo({ statusDialog: 'info', creditId: v.id })"
|
||||||
|
@delete="(v) => triggerDelete(v.id)"
|
||||||
|
>
|
||||||
|
<template #grid="{ item }">
|
||||||
|
<div class="col-md-4 col-sm-6 col-12">
|
||||||
|
<QuotationCard
|
||||||
|
hide-kebab-edit
|
||||||
|
@view="
|
||||||
|
() =>
|
||||||
|
navigateTo({
|
||||||
|
statusDialog: 'info',
|
||||||
|
creditId: item.row.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@delete="() => triggerDelete(item.row.id)"
|
||||||
|
:title="item.row.quotation.workName"
|
||||||
|
:code="item.row.code"
|
||||||
|
:status="$t(`creditNote.status.${item.row.creditNoteStatus}`)"
|
||||||
|
:badge-color="hslaColors[item.row.creditNoteStatus] || ''"
|
||||||
|
:custom-data="[
|
||||||
|
{
|
||||||
|
label: $t('branch.card.branchVirtual'),
|
||||||
|
value:
|
||||||
|
$i18n.locale === 'tha'
|
||||||
|
? item.row.quotation.registeredBranch.name
|
||||||
|
: item.row.quotation.registeredBranch.nameEN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('quotation.customer'),
|
||||||
|
value:
|
||||||
|
item.row.quotation.customerBranch.customer
|
||||||
|
.customerType === 'CORP'
|
||||||
|
? item.row.quotation.customerBranch.customerName
|
||||||
|
: $i18n.locale === 'tha'
|
||||||
|
? `${item.row.quotation.customerBranch.firstName} ${item.row.quotation.customerBranch.lastName}`
|
||||||
|
: `${item.row.quotation.customerBranch.firstNameEN} ${item.row.quotation.customerBranch.lastNameEN}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('requestList.quotationCode'),
|
||||||
|
value: item.row.quotation.code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('creditNote.label.quotationPayment'),
|
||||||
|
value: $t(
|
||||||
|
`quotation.type.${item.row.quotation.payCondition}`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TableCreditNote>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<footer
|
||||||
|
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
||||||
|
v-if="pageMax > 0"
|
||||||
|
>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="app-text-muted q-mr-sm" v-if="$q.screen.gt.sm">
|
||||||
|
{{ $t('general.recordPerPage') }}
|
||||||
|
</div>
|
||||||
|
<div><PaginationPageSize v-model="pageSize" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 row justify-center app-text-muted">
|
||||||
|
{{
|
||||||
|
$t('general.recordsPage', {
|
||||||
|
resultcurrentPage: data.length,
|
||||||
|
total: pageState.inputSearch ? data.length : pageState.total,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<nav class="col-4 row justify-end">
|
||||||
|
<PaginationComponent
|
||||||
|
v-model:current-page="page"
|
||||||
|
v-model:max-page="pageMax"
|
||||||
|
:fetch-data="() => getList()"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SEC: Dialog -->
|
||||||
|
<!-- dialog create -->
|
||||||
|
<DialogFormContainer
|
||||||
|
width="60vw"
|
||||||
|
height="500px"
|
||||||
|
v-model="pageState.creditDialog"
|
||||||
|
@submit="submit"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<DialogHeader
|
||||||
|
:title="$t(`general.add`, { text: $t('creditNote.title') })"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<section class="q-pa-md col full-width">
|
||||||
|
<div class="surface-1 rounded bordered q-pa-md full-height full-width">
|
||||||
|
<!-- TODO: bind quotation id -->
|
||||||
|
<FormCredit v-model:quotation-id="pageState.quotationId" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<CancelButton class="q-ml-auto" outlined @click="close" />
|
||||||
|
<SaveButton
|
||||||
|
:label="$t(`general.add`, { text: $t('creditNote.title') })"
|
||||||
|
class="q-ml-sm"
|
||||||
|
icon="mdi-check"
|
||||||
|
solid
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DialogFormContainer>
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
280
src/pages/11_credit-note/RefundInformation.vue
Normal file
280
src/pages/11_credit-note/RefundInformation.vue
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import useOptionStore from 'src/stores/options';
|
||||||
|
import { formatNumberDecimal } from 'src/stores/utils';
|
||||||
|
import UploadFileSection from 'src/components/upload-file/UploadFileSection.vue';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { CreditNotePaybackStatus } from 'src/stores/credit-note/types';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
paybackType: 'Cash' | 'BankTransfer';
|
||||||
|
paybackBank: string;
|
||||||
|
paybackAccount: string;
|
||||||
|
paybackAccountName: string;
|
||||||
|
paybackStatus: CreditNotePaybackStatus;
|
||||||
|
readonly?: boolean;
|
||||||
|
|
||||||
|
total?: number;
|
||||||
|
paid?: number;
|
||||||
|
remain?: number;
|
||||||
|
transformUrl?: (url: string) => string | Promise<string>;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
paybackStatus: CreditNotePaybackStatus.Pending,
|
||||||
|
paybackType: 'Cash',
|
||||||
|
total: 0,
|
||||||
|
paid: 0,
|
||||||
|
remain: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'changeStatus', status: CreditNotePaybackStatus): void;
|
||||||
|
(e: 'upload', file: FileList): void;
|
||||||
|
(e: 'remove', name: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const fileData = defineModel<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
progress: number;
|
||||||
|
loaded: number;
|
||||||
|
total: number;
|
||||||
|
url?: string;
|
||||||
|
}[]
|
||||||
|
>('fileData', { default: [] });
|
||||||
|
|
||||||
|
const currStatus = computed(() =>
|
||||||
|
refundOpts.value.find((v) => v.value === props.paybackStatus),
|
||||||
|
);
|
||||||
|
|
||||||
|
const refundOpts = ref<
|
||||||
|
{
|
||||||
|
icon: string;
|
||||||
|
value: CreditNotePaybackStatus;
|
||||||
|
color: string;
|
||||||
|
}[]
|
||||||
|
>([
|
||||||
|
{
|
||||||
|
value: CreditNotePaybackStatus.Pending,
|
||||||
|
icon: 'mdi-hand-coin-outline',
|
||||||
|
color: 'warning',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: CreditNotePaybackStatus.Verify,
|
||||||
|
icon: 'mdi-credit-card-clock-outline',
|
||||||
|
color: 'danger',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: CreditNotePaybackStatus.Done,
|
||||||
|
icon: 'mdi-check-decagram-outline',
|
||||||
|
color: 'positive',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="surface-1 rounded q-px-md q-py-sm">
|
||||||
|
<span
|
||||||
|
class="row items-center justify-between full-width text-medium text-body1"
|
||||||
|
style="min-height: 31.01px"
|
||||||
|
>
|
||||||
|
{{ $t('general.information', { msg: $t('creditNote.label.refund') }) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<section :class="{ row: $q.screen.gt.sm }">
|
||||||
|
<article
|
||||||
|
class="col column q-pr-md"
|
||||||
|
:class="{
|
||||||
|
'bordered-r': $q.screen.gt.sm,
|
||||||
|
'bordered-b q-pb-sm': $q.screen.lt.md,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="row">
|
||||||
|
<span class="app-text-muted q-mr-auto">
|
||||||
|
{{ $t('creditNote.label.refundMethod') }}
|
||||||
|
</span>
|
||||||
|
<q-badge>
|
||||||
|
{{ $t(`creditNote.label.${paybackType}`) }}
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
<div class="row" v-if="paybackType === 'BankTransfer'">
|
||||||
|
<span class="app-text-muted q-mr-auto">
|
||||||
|
{{ $t('branch.form.bank') }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<q-img
|
||||||
|
:src="`/img/bank/${paybackBank}.png`"
|
||||||
|
class="bordered q-mr-xs"
|
||||||
|
style="border-radius: 50%; width: 20px"
|
||||||
|
/>
|
||||||
|
{{ useOptionStore().mapOption(paybackBank) }}
|
||||||
|
{{ paybackAccount }}
|
||||||
|
{{ `${$t('creditNote.label.accountName')} ${paybackAccountName}` }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article
|
||||||
|
class="col row"
|
||||||
|
:class="{ 'q-pl-md': $q.screen.gt.sm, 'q-pt-sm': $q.screen.lt.md }"
|
||||||
|
>
|
||||||
|
<span class="col-12 app-text-muted">
|
||||||
|
{{ $t('creditNote.label.totalRefund') }}
|
||||||
|
</span>
|
||||||
|
<article
|
||||||
|
class="row col-12 items-center surface-1 q-py-sm rounded gradient-stat"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="row col rounded q-px-sm q-py-md info"
|
||||||
|
style="border: 1px solid hsl(var(--info-bg))"
|
||||||
|
>
|
||||||
|
{{ $t('creditNote.label.totalAmount') }}
|
||||||
|
<span class="q-ml-auto">
|
||||||
|
{{ formatNumberDecimal(total) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="row col rounded q-px-sm q-mx-md q-py-md positive"
|
||||||
|
style="border: 1px solid hsl(var(--positive-bg))"
|
||||||
|
>
|
||||||
|
{{ $t('creditNote.label.paid') }}
|
||||||
|
<span class="q-ml-auto">
|
||||||
|
{{ formatNumberDecimal(paid) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="row col rounded q-px-sm q-py-md warning"
|
||||||
|
style="border: 1px solid hsl(var(--warning-bg))"
|
||||||
|
>
|
||||||
|
{{ $t('creditNote.label.remain') }}
|
||||||
|
<span class="q-ml-auto">
|
||||||
|
{{ formatNumberDecimal(remain) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</article>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="surface-1 rounded">
|
||||||
|
<span
|
||||||
|
class="bordered-b q-px-md q-py-sm row items-center justify-between full-width text-medium text-body1"
|
||||||
|
style="min-height: 31.01px"
|
||||||
|
>
|
||||||
|
{{ $t('creditNote.label.refund') }}
|
||||||
|
<q-btn-dropdown
|
||||||
|
dense
|
||||||
|
unelevated
|
||||||
|
:label="$t(`creditNote.status.payback.${paybackStatus}`)"
|
||||||
|
class="text-capitalize text-weight-regular product-status rounded"
|
||||||
|
:class="{
|
||||||
|
warning: paybackStatus === CreditNotePaybackStatus.Pending,
|
||||||
|
danger: paybackStatus === CreditNotePaybackStatus.Verify,
|
||||||
|
'positive hide-dropdown q-pr-md':
|
||||||
|
paybackStatus === CreditNotePaybackStatus.Done,
|
||||||
|
}"
|
||||||
|
:menu-offset="[0, 8]"
|
||||||
|
dropdown-icon="mdi-chevron-down"
|
||||||
|
content-class="bordered rounded"
|
||||||
|
@click.stop
|
||||||
|
:icon="currStatus?.icon"
|
||||||
|
>
|
||||||
|
<q-list v-if="paybackStatus !== CreditNotePaybackStatus.Done" dense>
|
||||||
|
<q-item
|
||||||
|
v-for="(v, index) in paybackStatus ===
|
||||||
|
CreditNotePaybackStatus.Verify
|
||||||
|
? refundOpts.filter(
|
||||||
|
(v) => v.value === CreditNotePaybackStatus.Done,
|
||||||
|
)
|
||||||
|
: refundOpts.filter(
|
||||||
|
(v) => v.value !== CreditNotePaybackStatus.Pending,
|
||||||
|
)"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
class="items-center"
|
||||||
|
:key="index"
|
||||||
|
@click="$emit('changeStatus', v.value)"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:style="`color: hsl(var(--${v.color}-bg))`"
|
||||||
|
:name="v.icon"
|
||||||
|
class="q-pr-sm"
|
||||||
|
size="xs"
|
||||||
|
></q-icon>
|
||||||
|
{{ $t(`creditNote.status.payback.${v.value}`) }}
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</span>
|
||||||
|
<section class="q-px-md q-py-sm">
|
||||||
|
<UploadFileSection
|
||||||
|
multiple
|
||||||
|
:layout="$q.screen.gt.sm ? 'column' : 'row'"
|
||||||
|
:readonly
|
||||||
|
:label="`${$t('general.upload', { msg: ' E-slip' })} ${$t(
|
||||||
|
'general.or',
|
||||||
|
{
|
||||||
|
msg: $t('general.upload', {
|
||||||
|
msg: $t('creditNote.label.refundDocs'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)}`"
|
||||||
|
:transform-url="transformUrl"
|
||||||
|
v-model:file-data="fileData"
|
||||||
|
@update:file="(f) => $emit('upload', f as unknown as FileList)"
|
||||||
|
@close="(v) => $emit('remove', v)"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.gradient-stat {
|
||||||
|
& .info {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
hsl(var(--info-bg) / 0.15),
|
||||||
|
var(--surface-1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .positive {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
hsl(var(--positive-bg) / 0.15),
|
||||||
|
var(--surface-1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .warning {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
hsl(var(--warning-bg) / 0.15),
|
||||||
|
var(--surface-1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-status {
|
||||||
|
padding-left: 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: hsl(var(--_color));
|
||||||
|
background: hsla(var(--_color) / 0.15);
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
--_color: var(--warning-bg);
|
||||||
|
}
|
||||||
|
&.danger {
|
||||||
|
--_color: var(--danger-bg);
|
||||||
|
}
|
||||||
|
&.positive {
|
||||||
|
--_color: var(--positive-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(
|
||||||
|
.hide-dropdown
|
||||||
|
i.q-icon.mdi.mdi-chevron-down.q-btn-dropdown__arrow.q-btn-dropdown__arrow-container
|
||||||
|
) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
92
src/pages/11_credit-note/TableCreditNote.vue
Normal file
92
src/pages/11_credit-note/TableCreditNote.vue
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { QTableSlots } from 'quasar';
|
||||||
|
import { CreditNote, useCreditNote } from 'src/stores/credit-note';
|
||||||
|
|
||||||
|
import { columns } from './constants.ts';
|
||||||
|
import KebabAction from 'src/components/shared/KebabAction.vue';
|
||||||
|
|
||||||
|
const creditNote = useCreditNote();
|
||||||
|
const { data, page } = storeToRefs(creditNote);
|
||||||
|
|
||||||
|
const prop = defineProps<{
|
||||||
|
grid: boolean;
|
||||||
|
visibleColumns: string[];
|
||||||
|
hideDelete: boolean;
|
||||||
|
}>();
|
||||||
|
defineEmits<{ (evt: 'view' | 'delete', val: CreditNote): void }>();
|
||||||
|
|
||||||
|
const visible = computed(() =>
|
||||||
|
columns.filter(
|
||||||
|
(v) => prop.visibleColumns.includes(v.name) || v.name === '#action',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-table
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
:rows="data.map((item, i) => ({ ...item, _index: i, _page: page }))"
|
||||||
|
:columns="visible"
|
||||||
|
:grid
|
||||||
|
hide-bottom
|
||||||
|
bordered
|
||||||
|
flat
|
||||||
|
hide-pagination
|
||||||
|
selection="multiple"
|
||||||
|
card-container-class="q-col-gutter-sm"
|
||||||
|
class="full-width"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr
|
||||||
|
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
<q-th v-for="col in visible" :key="col.name" :props="props">
|
||||||
|
<template v-if="!col.name.startsWith('#')">
|
||||||
|
{{ $t(col.label) }}
|
||||||
|
</template>
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-slot:body="props: {
|
||||||
|
row: CreditNote & { _index: number; _page: number };
|
||||||
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||||
|
>
|
||||||
|
<q-tr :class="{ dark: $q.dark.isActive }" class="text-center">
|
||||||
|
<q-td v-for="(col, i) in visible" :align="col.align" :key="i">
|
||||||
|
<!-- NOTE: custom column will starts with # -->
|
||||||
|
<template v-if="!col.name.startsWith('#')">
|
||||||
|
<span v-if="col.name !== 'quotationPayment'">
|
||||||
|
{{
|
||||||
|
typeof col.field === 'string'
|
||||||
|
? props.row[col.field as keyof CreditNote]
|
||||||
|
: col.field(props.row)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="col.name === 'quotationPayment'">
|
||||||
|
{{ $t(`quotation.type.${col.field(props.row)}`) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="col.name === '#action'">
|
||||||
|
<KebabAction
|
||||||
|
hide-edit
|
||||||
|
hide-toggle
|
||||||
|
:hide-delete
|
||||||
|
@delete="$emit('delete', props.row)"
|
||||||
|
@view="$emit('view', props.row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item="props: { row: CreditNote }">
|
||||||
|
<slot name="grid" :item="props" />
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</template>
|
||||||
78
src/pages/11_credit-note/constants.ts
Normal file
78
src/pages/11_credit-note/constants.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { QTableProps } from 'quasar';
|
||||||
|
import { CreditNote, CreditNoteStatus } from 'src/stores/credit-note';
|
||||||
|
import { formatNumberDecimal } from 'src/stores/utils';
|
||||||
|
|
||||||
|
export const taskStatusOpts = [
|
||||||
|
{
|
||||||
|
status: CreditNoteStatus.Pending,
|
||||||
|
name: `creditNote.status.${CreditNoteStatus.Pending}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: CreditNoteStatus.Success,
|
||||||
|
name: `creditNote.status.${CreditNoteStatus.Success}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const pageTabs = [
|
||||||
|
{ label: CreditNoteStatus.Pending, value: CreditNoteStatus.Pending },
|
||||||
|
{ label: CreditNoteStatus.Success, value: CreditNoteStatus.Success },
|
||||||
|
];
|
||||||
|
|
||||||
|
export enum Status {
|
||||||
|
taskOrder = 'taskOrder',
|
||||||
|
receiveTaskOrder = 'receiveTaskOrder',
|
||||||
|
sendTaskOrder = 'sendTaskOrder',
|
||||||
|
payment = 'payment',
|
||||||
|
goodReceipt = 'goodReceipt',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const columns = [
|
||||||
|
{
|
||||||
|
name: 'order',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.order',
|
||||||
|
field: (data: CreditNote & { _index: number; _page: number }) =>
|
||||||
|
data._page * (data._index + 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
align: 'center',
|
||||||
|
label: 'creditNote.label.code',
|
||||||
|
field: (data: CreditNote) => data.code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quotationCode',
|
||||||
|
align: 'center',
|
||||||
|
label: 'creditNote.label.quotationCode',
|
||||||
|
field: (data: CreditNote) => data.quotation.code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quotationWorkName',
|
||||||
|
align: 'center',
|
||||||
|
label: 'creditNote.label.quotationWorkName',
|
||||||
|
field: (data: CreditNote) => data.quotation.workName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quotationPayment',
|
||||||
|
align: 'center',
|
||||||
|
label: 'creditNote.label.quotationPayment',
|
||||||
|
field: (data: CreditNote) => data.quotation.payCondition,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'creditNoteValue',
|
||||||
|
align: 'center',
|
||||||
|
label: 'creditNote.label.value',
|
||||||
|
field: (data: CreditNote) => formatNumberDecimal(data.value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#action',
|
||||||
|
align: 'center',
|
||||||
|
label: '',
|
||||||
|
field: (_) => '#action',
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
||||||
|
export const hslaColors: Record<string, string> = {
|
||||||
|
Pending: '--blue-6-hsl',
|
||||||
|
Success: '--red-6-hsl',
|
||||||
|
};
|
||||||
53
src/pages/11_credit-note/expansion/CreditNoteExpansion.vue
Normal file
53
src/pages/11_credit-note/expansion/CreditNoteExpansion.vue
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SelectInput from 'src/components/shared/SelectInput.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const reason = defineModel<string>('reason');
|
||||||
|
const detail = defineModel<string>('detail');
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
:default-opened="true"
|
||||||
|
class="overflow-hidden bordered full-width"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('creditNote.label.creditNoteInformation') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main class="q-px-md q-py-sm surface-1 row q-col-gutter-sm">
|
||||||
|
<SelectInput
|
||||||
|
:readonly
|
||||||
|
for="select-credit-note-specify-reason"
|
||||||
|
:label="$t('creditNote.label.specifyReason')"
|
||||||
|
class="col-md col-12"
|
||||||
|
v-model="reason"
|
||||||
|
:option="[
|
||||||
|
{ label: $t('creditNote.label.reasonReturn'), value: 'Return' },
|
||||||
|
{ label: $t('creditNote.label.reasonCanceled'), value: 'Canceled' },
|
||||||
|
]"
|
||||||
|
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||||
|
></SelectInput>
|
||||||
|
<q-input
|
||||||
|
:readonly
|
||||||
|
for="input-credit-note-additional-detail"
|
||||||
|
:label="$t('creditNote.label.additionalDetail')"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
class="col"
|
||||||
|
v-model="detail"
|
||||||
|
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||||
|
></q-input>
|
||||||
|
</main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
108
src/pages/11_credit-note/expansion/DocumentExpansion.vue
Normal file
108
src/pages/11_credit-note/expansion/DocumentExpansion.vue
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import SelectBranch from 'src/components/shared/select/SelectBranch.vue';
|
||||||
|
import SelectCustomer from 'src/components/shared/select/SelectCustomer.vue';
|
||||||
|
import DatePicker from 'src/components/shared/DatePicker.vue';
|
||||||
|
import DataDisplay from 'src/components/08_request-list/DataDisplay.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'gotoQuotation'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const registeredBranchId = defineModel<string>('registeredBranchId');
|
||||||
|
const customerId = defineModel<string>('customerId');
|
||||||
|
const issueDate = defineModel<string>('issueDate');
|
||||||
|
|
||||||
|
const quotationCode = defineModel<string>('quotationCode');
|
||||||
|
const quotationWorkName = defineModel<string>('quotationWorkName');
|
||||||
|
const quotationContactName = defineModel<string>('quotationContactName');
|
||||||
|
const quotationContactTel = defineModel<string>('quotationContactTel');
|
||||||
|
const quotationCreatedBy = defineModel<string>('quotationCreatedBy');
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
default-opened
|
||||||
|
dense
|
||||||
|
class="overflow-hidden bordered full-width"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('general.information', { msg: $t('general.document') }) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main class="q-px-md q-py-sm surface-1 row q-col-gutter-sm">
|
||||||
|
<SelectBranch
|
||||||
|
readonly
|
||||||
|
class="col-md-4 col-12"
|
||||||
|
:label="`${$t('creditNote.label.quotationRegisteredBranch')}`"
|
||||||
|
v-model:value="registeredBranchId"
|
||||||
|
/>
|
||||||
|
<SelectCustomer
|
||||||
|
readonly
|
||||||
|
simple
|
||||||
|
class="col-md-4 col-12"
|
||||||
|
:label="`${$t('creditNote.label.customer')}`"
|
||||||
|
v-model:value="customerId"
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
:label="$t('general.createdAt')"
|
||||||
|
class="col-md-4 col-6"
|
||||||
|
:model-value="issueDate || new Date(Date.now())"
|
||||||
|
:readonly
|
||||||
|
:disabled="!readonly"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DataDisplay
|
||||||
|
clickable
|
||||||
|
class="col-md col-6"
|
||||||
|
style="padding-inline: 20px"
|
||||||
|
:label="$t('creditNote.label.quotationCode')"
|
||||||
|
:value="quotationCode"
|
||||||
|
@label-click="$emit('gotoQuotation')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
:readonly
|
||||||
|
:label="$t('creditNote.label.quotationWorkName')"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
class="col-md col-6"
|
||||||
|
v-model="quotationWorkName"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
:readonly
|
||||||
|
:label="$t('quotation.contactName')"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
class="col-md col-6"
|
||||||
|
v-model="quotationContactName"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
:readonly
|
||||||
|
:label="$t('general.telephone')"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
class="col-md col-6"
|
||||||
|
v-model="quotationContactTel"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
:readonly
|
||||||
|
:label="$t('creditNote.label.quotationCreatedBy')"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
class="col-md col-6"
|
||||||
|
:disable="!readonly"
|
||||||
|
:model-value="quotationCreatedBy || '-'"
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
235
src/pages/11_credit-note/expansion/PaymentExpansion.vue
Normal file
235
src/pages/11_credit-note/expansion/PaymentExpansion.vue
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import AppBox from 'src/components/app/AppBox.vue';
|
||||||
|
import useOptionStore from 'src/stores/options';
|
||||||
|
import SelectInput from 'src/components/shared/SelectInput.vue';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import { formatNumberDecimal } from 'src/stores/utils';
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
totalPrice?: number;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
totalPrice: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const optionStore = useOptionStore();
|
||||||
|
|
||||||
|
const paybackType = defineModel<'BankTransfer' | 'Cash'>('paybackType');
|
||||||
|
const paybackBank = defineModel<string>('paybackBank');
|
||||||
|
const paybackAccount = defineModel<string>('paybackAccount');
|
||||||
|
const paybackAccountName = defineModel<string>('paybackAccountName');
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => paybackType.value,
|
||||||
|
() => {
|
||||||
|
if (paybackType.value === 'Cash') {
|
||||||
|
paybackBank.value = paybackAccount.value = paybackAccountName.value = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
class="overflow-hidden bordered full-width"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('general.payment') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main class="q-px-md q-py-sm surface-1">
|
||||||
|
<AppBox
|
||||||
|
no-padding
|
||||||
|
bordered
|
||||||
|
class="credit-note-color"
|
||||||
|
:class="{
|
||||||
|
row: $q.screen.gt.sm,
|
||||||
|
column: $q.screen.lt.md,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<section class="col bordered-r">
|
||||||
|
<header
|
||||||
|
class="bordered-b q-px-md q-py-sm row bg-color-light items-center"
|
||||||
|
>
|
||||||
|
<div class="icon-wrapper bg-color q-mr-sm">
|
||||||
|
<q-icon name="mdi-bank-outline" />
|
||||||
|
</div>
|
||||||
|
<span class="text-weight-bold">
|
||||||
|
{{ $t('quotation.paymentCondition') }}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="q-pa-sm">
|
||||||
|
<div class="row q-col-gutter-sm">
|
||||||
|
<SelectInput
|
||||||
|
for="select-credit-note-pay-type"
|
||||||
|
:label="$t('quotation.payType')"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
:readonly
|
||||||
|
class="col-md-4 col-12"
|
||||||
|
v-model="paybackType"
|
||||||
|
:option="[
|
||||||
|
{
|
||||||
|
label: $t('creditNote.label.Cash'),
|
||||||
|
value: 'Cash',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('creditNote.label.BankTransfer'),
|
||||||
|
value: 'BankTransfer',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
:disable="paybackType === 'Cash'"
|
||||||
|
for="select-credit-note-bank"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
fill-input
|
||||||
|
:hide-selected="false"
|
||||||
|
:readonly
|
||||||
|
class="col-md-8 col-12"
|
||||||
|
:label="$t('branch.form.bank')"
|
||||||
|
:option="optionStore.globalOption?.bankBook"
|
||||||
|
v-model="paybackBank"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #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 #selected-item="{ scope }">
|
||||||
|
<q-item-section v-if="scope.opt" avatar class="q-py-sm">
|
||||||
|
<q-img
|
||||||
|
:src="`/img/bank/${scope.opt.value}.png`"
|
||||||
|
class="bordered"
|
||||||
|
style="border-radius: 50%; width: 30px"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</template>
|
||||||
|
</SelectInput>
|
||||||
|
<q-input
|
||||||
|
:disable="paybackType === 'Cash'"
|
||||||
|
for="input-credit-account-number"
|
||||||
|
v-model="paybackAccount"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
:readonly
|
||||||
|
class="col-md-4 col-12"
|
||||||
|
:label="$t('creditNote.label.accountNumber')"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
:disable="paybackType === 'Cash'"
|
||||||
|
for="input-credit-account-name"
|
||||||
|
v-model="paybackAccountName"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
:readonly
|
||||||
|
class="col-md-8 col-12"
|
||||||
|
:label="$t('creditNote.label.accountName')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="col column">
|
||||||
|
<header
|
||||||
|
class="bordered-b q-px-md q-py-sm row bg-color-light items-center"
|
||||||
|
>
|
||||||
|
<div class="icon-wrapper bg-color q-mr-sm">
|
||||||
|
<Icon icon="iconoir:coins" />
|
||||||
|
</div>
|
||||||
|
<span class="text-weight-bold">
|
||||||
|
{{ $t('quotation.summary') }}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="q-pa-sm price-container col">
|
||||||
|
<div class="row">
|
||||||
|
{{ $t('general.total') }}
|
||||||
|
<span class="q-ml-auto">
|
||||||
|
{{ formatNumberDecimal(totalPrice) }}
|
||||||
|
฿
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="q-pa-sm row surface-2 items-center text-weight-bold">
|
||||||
|
{{ $t('quotation.totalPriceBaht') }}
|
||||||
|
<span class="q-ml-auto" style="color: var(--brand-1)">
|
||||||
|
{{ formatNumberDecimal(totalPrice) }}
|
||||||
|
฿
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</AppBox>
|
||||||
|
</main>
|
||||||
|
</q-expansion-item>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.icon-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: var(--size-1);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.price-tag .q-field__control) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 90px;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit-note-color {
|
||||||
|
--_color: var(--indigo-10-hsl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-color {
|
||||||
|
color: white;
|
||||||
|
background: hsla(var(--_color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bg-color {
|
||||||
|
--_color: var(--orange-6-hsl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-color-light {
|
||||||
|
background: hsla(var(--_color) / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bg-color-light {
|
||||||
|
--_color: var(--orange-6-hsl / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-container > * {
|
||||||
|
padding: var(--size-1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
379
src/pages/13_receipt/MainPage.vue
Normal file
379
src/pages/13_receipt/MainPage.vue
Normal file
|
|
@ -0,0 +1,379 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// NOTE: Library
|
||||||
|
import { computed, onMounted, reactive, watch } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
// NOTE: Components
|
||||||
|
import StatCardComponent from 'src/components/StatCardComponent.vue';
|
||||||
|
import NoData from 'src/components/NoData.vue';
|
||||||
|
import PaginationComponent from 'src/components/PaginationComponent.vue';
|
||||||
|
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
|
||||||
|
import TableReceipt from './TableReceipt.vue';
|
||||||
|
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
|
||||||
|
|
||||||
|
// NOTE: Stores & Type
|
||||||
|
import { useNavigator } from 'src/stores/navigator';
|
||||||
|
import { columns, hslaColors } from './constants';
|
||||||
|
import useFlowStore from 'src/stores/flow';
|
||||||
|
import { useRequestList } from 'src/stores/request-list';
|
||||||
|
import { usePayment, useReceipt } from 'src/stores/payment';
|
||||||
|
import { Receipt, PaymentDataStatus } from 'src/stores/payment/types';
|
||||||
|
import { Quotation } from 'src/stores/quotations';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
const navigatorStore = useNavigator();
|
||||||
|
const flowStore = useFlowStore();
|
||||||
|
const receiptStore = useReceipt();
|
||||||
|
const { data, page, pageMax, pageSize } = storeToRefs(receiptStore);
|
||||||
|
|
||||||
|
// NOTE: Variable
|
||||||
|
const pageState = reactive({
|
||||||
|
hideStat: false,
|
||||||
|
statusFilter: 'None' as 'None' | PaymentDataStatus,
|
||||||
|
inputSearch: '',
|
||||||
|
fieldSelected: [...columns.map((v) => v.name)],
|
||||||
|
gridView: false,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldSelectedOption = computed(() => {
|
||||||
|
return columns.map((v) => ({
|
||||||
|
label: v.label,
|
||||||
|
value: v.name,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchList(opts?: { rotateFlowId?: boolean }) {
|
||||||
|
const ret = await receiptStore.getReceiptList({
|
||||||
|
page: page.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
query: pageState.inputSearch,
|
||||||
|
});
|
||||||
|
if (ret) {
|
||||||
|
data.value = ret.result;
|
||||||
|
pageState.total = ret.total;
|
||||||
|
pageMax.value = Math.ceil(ret.total / pageSize.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.rotateFlowId) flowStore.rotate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerView(opts: { quotationId: string }) {
|
||||||
|
const url = new URL(`/quotation/view?tab=receipt`, window.location.origin);
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
'new-quotation',
|
||||||
|
JSON.stringify({
|
||||||
|
quotationId: opts.quotationId,
|
||||||
|
statusDialog: 'info',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
window.open(url.toString(), '_blank');
|
||||||
|
}
|
||||||
|
const paymentStore = usePayment();
|
||||||
|
|
||||||
|
async function viewDocExample(id: string) {
|
||||||
|
const url = await paymentStore.getFlowAccount(id);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
window.open(url.data.link, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
navigatorStore.current.title = 'receipt.title';
|
||||||
|
navigatorStore.current.path = [{ text: 'receipt.caption', i18n: true }];
|
||||||
|
|
||||||
|
await fetchList({ rotateFlowId: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
() => pageState.inputSearch,
|
||||||
|
() => pageState.statusFilter,
|
||||||
|
() => pageSize.value,
|
||||||
|
() => page.value,
|
||||||
|
],
|
||||||
|
() => fetchList({ rotateFlowId: true }),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="column full-height no-wrap">
|
||||||
|
<!-- SEC: stat -->
|
||||||
|
<section class="text-body-2 q-mb-xs flex items-center">
|
||||||
|
{{ $t('receipt.dataSum') }}
|
||||||
|
<q-badge
|
||||||
|
rounded
|
||||||
|
class="q-ml-sm"
|
||||||
|
style="
|
||||||
|
background-color: hsla(var(--info-bg) / 0.15);
|
||||||
|
color: hsl(var(--info-bg));
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ pageState.total }}
|
||||||
|
</q-badge>
|
||||||
|
<q-btn
|
||||||
|
class="q-ml-sm"
|
||||||
|
icon="mdi-pin-outline"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
rounded
|
||||||
|
@click="pageState.hideStat = !pageState.hideStat"
|
||||||
|
:style="pageState.hideStat ? 'rotate: 90deg' : ''"
|
||||||
|
style="transition: 0.1s ease-in-out"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<transition name="slide">
|
||||||
|
<div v-if="!pageState.hideStat" class="scroll q-mb-md">
|
||||||
|
<div style="display: inline-block">
|
||||||
|
<StatCardComponent
|
||||||
|
labelI18n
|
||||||
|
:branch="[
|
||||||
|
{
|
||||||
|
icon: 'fluent:receipt-money-16-regular',
|
||||||
|
count: pageState.total,
|
||||||
|
label: 'quotation.status.PaymentSuccess',
|
||||||
|
color: 'light-green',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:dark="$q.dark.isActive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<section class="col surface-1 rounded bordered overflow-hidden">
|
||||||
|
<div class="column full-height">
|
||||||
|
<!-- SEC: header content -->
|
||||||
|
<header
|
||||||
|
class="row surface-3 justify-between full-width items-center bordered-b"
|
||||||
|
style="z-index: 1"
|
||||||
|
>
|
||||||
|
<div class="row q-py-sm q-px-md justify-between full-width">
|
||||||
|
<q-input
|
||||||
|
for="input-search"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
:label="$t('general.search')"
|
||||||
|
class="q-mr-md col-12 col-md-3"
|
||||||
|
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||||
|
v-model="pageState.inputSearch"
|
||||||
|
debounce="200"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-magnify" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="row col-12 col-md-5 justify-end"
|
||||||
|
:class="{ 'q-pt-xs': $q.screen.lt.md }"
|
||||||
|
style="white-space: nowrap"
|
||||||
|
>
|
||||||
|
<q-select
|
||||||
|
v-model="pageState.statusFilter"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
option-value="value"
|
||||||
|
option-label="label"
|
||||||
|
class="col"
|
||||||
|
:class="{ 'offset-md-5': pageState.gridView }"
|
||||||
|
map-options
|
||||||
|
emit-value
|
||||||
|
:for="'field-select-status'"
|
||||||
|
:hide-dropdown-icon="$q.screen.lt.sm"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
label: $t('general.all'),
|
||||||
|
value: 'None',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-if="!pageState.gridView"
|
||||||
|
id="select-field"
|
||||||
|
for="select-field"
|
||||||
|
class="col q-ml-sm"
|
||||||
|
:options="
|
||||||
|
fieldSelectedOption.map((v) => ({
|
||||||
|
...v,
|
||||||
|
label: v.label && $t(v.label),
|
||||||
|
}))
|
||||||
|
"
|
||||||
|
:display-value="$t('general.displayField')"
|
||||||
|
:hide-dropdown-icon="$q.screen.lt.sm"
|
||||||
|
v-model="pageState.fieldSelected"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
map-options
|
||||||
|
emit-value
|
||||||
|
outlined
|
||||||
|
multiple
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
<q-btn-toggle
|
||||||
|
id="btn-mode"
|
||||||
|
v-model="pageState.gridView"
|
||||||
|
dense
|
||||||
|
class="no-shadow bordered rounded surface-1 q-ml-sm"
|
||||||
|
:toggle-color="$q.dark.isActive ? 'grey-9' : 'grey-2'"
|
||||||
|
size="xs"
|
||||||
|
:options="[
|
||||||
|
{ value: true, slot: 'folder' },
|
||||||
|
{ value: false, slot: 'list' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<template v-slot:folder>
|
||||||
|
<q-icon
|
||||||
|
name="mdi-view-grid-outline"
|
||||||
|
size="16px"
|
||||||
|
class="q-px-sm q-py-xs rounded"
|
||||||
|
:style="{
|
||||||
|
color: $q.dark.isActive
|
||||||
|
? pageState.gridView
|
||||||
|
? '#C9D3DB '
|
||||||
|
: '#787B7C'
|
||||||
|
: pageState.gridView
|
||||||
|
? '#787B7C'
|
||||||
|
: '#C9D3DB',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:list>
|
||||||
|
<q-icon
|
||||||
|
name="mdi-format-list-bulleted"
|
||||||
|
class="q-px-sm q-py-xs rounded"
|
||||||
|
size="16px"
|
||||||
|
:style="{
|
||||||
|
color: $q.dark.isActive
|
||||||
|
? pageState.gridView === false
|
||||||
|
? '#C9D3DB'
|
||||||
|
: '#787B7C'
|
||||||
|
: pageState.gridView === false
|
||||||
|
? '#787B7C'
|
||||||
|
: '#C9D3DB',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-btn-toggle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- SEC: body content -->
|
||||||
|
<article
|
||||||
|
v-if="data.length === 0"
|
||||||
|
class="col surface-2 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<NoData :not-found="!!pageState.inputSearch" />
|
||||||
|
</article>
|
||||||
|
<article v-else class="col surface-2 full-width scroll q-pa-md">
|
||||||
|
<TableReceipt
|
||||||
|
:columns="columns"
|
||||||
|
:rows="data"
|
||||||
|
:grid="pageState.gridView"
|
||||||
|
@view="
|
||||||
|
(data) => triggerView({ quotationId: data.invoice.quotation.id })
|
||||||
|
"
|
||||||
|
@preview="(data) => viewDocExample(data.id)"
|
||||||
|
>
|
||||||
|
<template #grid="{ item }">
|
||||||
|
<div class="col-md-4 col-sm-6 col-12">
|
||||||
|
<QuotationCard
|
||||||
|
hide-action
|
||||||
|
:code="item.row.code"
|
||||||
|
:title="item.row.invoice.quotation.workName"
|
||||||
|
:status="$t(`invoice.status.${item.row.paymentStatus}`)"
|
||||||
|
:badge-color="hslaColors[item.row.paymentStatus] || ''"
|
||||||
|
:custom-data="[
|
||||||
|
{
|
||||||
|
label: $t('general.customer'),
|
||||||
|
value:
|
||||||
|
item.row.invoice.quotation.customerBranch
|
||||||
|
.registerName ||
|
||||||
|
`${item.row.invoice.quotation.customerBranch?.firstName || '-'} ${item.row.invoice.quotation.customerBranch?.lastName || ''}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('taskOrder.issueDate'),
|
||||||
|
value: new Date(
|
||||||
|
item.row.invoice.quotation.createdAt,
|
||||||
|
).toLocaleString('th-TH', {
|
||||||
|
hour12: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('invoice.paymentDueDate'),
|
||||||
|
value: new Date(
|
||||||
|
item.row.invoice.quotation.dueDate,
|
||||||
|
).toLocaleDateString('th-TH', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: 'numeric',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('quotation.payType'),
|
||||||
|
value: $t(
|
||||||
|
`quotation.type.${item.row.invoice.quotation.payCondition}`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('preview.netValue'),
|
||||||
|
value: item.row.amount,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
@view="
|
||||||
|
() =>
|
||||||
|
triggerView({
|
||||||
|
quotationId: item.row.invoice.quotation.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@preview="
|
||||||
|
() => {
|
||||||
|
viewDocExample(item.row.invoice.quotation.id);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TableReceipt>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- SEC: footer content -->
|
||||||
|
<footer
|
||||||
|
class="row justify-between items-center q-px-md q-py-sm surface-2"
|
||||||
|
v-if="pageMax > 0"
|
||||||
|
>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="app-text-muted q-mr-sm" v-if="$q.screen.gt.sm">
|
||||||
|
{{ $t('general.recordPerPage') }}
|
||||||
|
</div>
|
||||||
|
<div><PaginationPageSize v-model="pageSize" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 row justify-center app-text-muted">
|
||||||
|
{{
|
||||||
|
$t('general.recordsPage', {
|
||||||
|
resultcurrentPage: data.length,
|
||||||
|
total: pageState.inputSearch ? data.length : pageState.total,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<nav class="col-4 row justify-end">
|
||||||
|
<PaginationComponent
|
||||||
|
v-model:current-page="page"
|
||||||
|
v-model:max-page="pageMax"
|
||||||
|
:fetch-data="() => fetchList({ rotateFlowId: true })"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
131
src/pages/13_receipt/TableReceipt.vue
Normal file
131
src/pages/13_receipt/TableReceipt.vue
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { QTableSlots } from 'quasar';
|
||||||
|
import { columns } from './constants';
|
||||||
|
|
||||||
|
import { Receipt } from 'src/stores/payment/types';
|
||||||
|
import { useReceipt } from 'src/stores/payment';
|
||||||
|
|
||||||
|
const receiptStore = useReceipt();
|
||||||
|
|
||||||
|
type Data = Receipt;
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
rows: Data[];
|
||||||
|
readonly?: boolean;
|
||||||
|
flat?: boolean;
|
||||||
|
bordered?: boolean;
|
||||||
|
grid?: boolean;
|
||||||
|
hideHeader?: boolean;
|
||||||
|
buttonDownload?: boolean;
|
||||||
|
buttonDelete?: boolean;
|
||||||
|
hidePagination?: boolean;
|
||||||
|
inTable?: boolean;
|
||||||
|
hideView?: boolean;
|
||||||
|
btnSelected?: boolean;
|
||||||
|
|
||||||
|
imgColumn?: string;
|
||||||
|
customColumn?: string[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
row: () => [],
|
||||||
|
flat: false,
|
||||||
|
bordered: false,
|
||||||
|
grid: false,
|
||||||
|
hideHeader: false,
|
||||||
|
buttonDownload: false,
|
||||||
|
imgColumn: '',
|
||||||
|
customColumn: () => [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'preview' | 'view', data: Data): void;
|
||||||
|
(e: 'edit' | 'delete' | 'download', data: Data, index: number): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-table
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
:rows="rows.map((data, i) => ({ ...data, _index: i }))"
|
||||||
|
:columns
|
||||||
|
:grid
|
||||||
|
bordered
|
||||||
|
flat
|
||||||
|
hide-pagination
|
||||||
|
selection="multiple"
|
||||||
|
card-container-class="q-col-gutter-sm"
|
||||||
|
class="full-width"
|
||||||
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr
|
||||||
|
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
<q-th v-for="col in columns" :key="col.name" :props="props">
|
||||||
|
{{ $t(col.label) }}
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-slot:body="props: {
|
||||||
|
row: Receipt & { _index: number };
|
||||||
|
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||||
|
>
|
||||||
|
<q-tr :class="{ dark: $q.dark.isActive }" class="text-center">
|
||||||
|
<q-td v-for="col in columns" :align="col.align">
|
||||||
|
<!-- NOTE: custom column will starts with # -->
|
||||||
|
<template v-if="!col.name.startsWith('#')">
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
typeof col.field === 'string'
|
||||||
|
? props.row[col.field as keyof Receipt]
|
||||||
|
: col.field(props.row)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-if="col.name === '#order'">
|
||||||
|
{{
|
||||||
|
col.field(props.row) +
|
||||||
|
(receiptStore.page - 1) * receiptStore.pageSize
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<template v-if="col.name === '#action'">
|
||||||
|
<q-btn
|
||||||
|
:id="`btn-preview-${props.row.invoice.quotation.workName}`"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
rounded
|
||||||
|
icon="mdi-play-box-outline"
|
||||||
|
size="12px"
|
||||||
|
:title="$t('preview.doc')"
|
||||||
|
@click.stop="$emit('preview', props.row)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
:id="`btn-eye-${props.row.invoice.quotation.workName}`"
|
||||||
|
icon="mdi-eye-outline"
|
||||||
|
size="sm"
|
||||||
|
dense
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
@click.stop="$emit('view', props.row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item="props: { row: Receipt }">
|
||||||
|
<slot name="grid" :item="props" />
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(i.q-icon.mdi.mdi-alert.q-table__bottom-nodata-icon) {
|
||||||
|
color: #ffc224 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
src/pages/13_receipt/constants.ts
Normal file
95
src/pages/13_receipt/constants.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { QTableProps } from 'quasar';
|
||||||
|
import { Invoice, Receipt } from 'src/stores/payment/types';
|
||||||
|
import { formatNumberDecimal } from 'src/stores/utils';
|
||||||
|
import { dateFormatJS } from 'src/utils/datetime';
|
||||||
|
|
||||||
|
export const columns = [
|
||||||
|
{
|
||||||
|
name: '#order',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.order',
|
||||||
|
field: (v: Receipt & { _index: number }) => v._index + 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'quotation',
|
||||||
|
align: 'center',
|
||||||
|
label: 'requestList.quotationCode',
|
||||||
|
field: 'code',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'workSheetName',
|
||||||
|
align: 'left',
|
||||||
|
label: 'receipt.workSheetName',
|
||||||
|
field: (v: Receipt) => v.invoice.quotation.workName,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'issueDate',
|
||||||
|
align: 'center',
|
||||||
|
label: 'taskOrder.issueDate',
|
||||||
|
field: (v: Receipt) =>
|
||||||
|
dateFormatJS({
|
||||||
|
date: v.invoice.quotation.createdAt,
|
||||||
|
withTime: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
align: 'center',
|
||||||
|
label: 'preview.value',
|
||||||
|
field: (v: Receipt) => formatNumberDecimal(v.amount, 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#action',
|
||||||
|
align: 'center',
|
||||||
|
label: '',
|
||||||
|
field: (_) => '#action',
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
||||||
|
export const docColumn = [
|
||||||
|
{
|
||||||
|
name: 'order',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.order',
|
||||||
|
field: 'no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document',
|
||||||
|
align: 'left',
|
||||||
|
label: 'general.document',
|
||||||
|
field: 'document',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'attachment',
|
||||||
|
align: 'left',
|
||||||
|
label: 'requestList.attachment',
|
||||||
|
field: 'attachment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.amount',
|
||||||
|
field: 'amount',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'documentInSystem',
|
||||||
|
align: 'center',
|
||||||
|
label: 'requestList.documentInSystem',
|
||||||
|
field: 'documentInSystem',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
align: 'center',
|
||||||
|
label: 'general.status',
|
||||||
|
field: 'status',
|
||||||
|
},
|
||||||
|
] as const satisfies QTableProps['columns'];
|
||||||
|
|
||||||
|
export const hslaColors: Record<string, string> = {
|
||||||
|
PaymentWait: '--orange-5-hsl',
|
||||||
|
PaymentSuccess: '--green-8-hsl',
|
||||||
|
};
|
||||||
|
|
@ -105,6 +105,21 @@ const routes: RouteRecordRaw[] = [
|
||||||
name: 'TaskOrder',
|
name: 'TaskOrder',
|
||||||
component: () => import('pages/09_task-order/MainPage.vue'),
|
component: () => import('pages/09_task-order/MainPage.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/credit-note',
|
||||||
|
name: 'CreditNote',
|
||||||
|
component: () => import('pages/11_credit-note/MainPage.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/invoice',
|
||||||
|
name: '/Invoice',
|
||||||
|
component: () => import('pages/10_invoice/MainPage.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/receipt',
|
||||||
|
name: 'receipt',
|
||||||
|
component: () => import('pages/13_receipt/MainPage.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -148,6 +163,21 @@ const routes: RouteRecordRaw[] = [
|
||||||
name: 'docOrder',
|
name: 'docOrder',
|
||||||
component: () => import('pages/09_task-order/document_view/MainPage.vue'),
|
component: () => import('pages/09_task-order/document_view/MainPage.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/credit-note/add',
|
||||||
|
name: 'CreditNoteNew',
|
||||||
|
component: () => import('pages/11_credit-note/FormPage.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/credit-note/:id',
|
||||||
|
name: 'CreditNoteView',
|
||||||
|
component: () => import('pages/11_credit-note/FormPage.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/receipt/:id',
|
||||||
|
name: 'receiptform',
|
||||||
|
component: () => import('pages/13_receipt/MainPage.vue'),
|
||||||
|
},
|
||||||
|
|
||||||
// Always leave this as last one,
|
// Always leave this as last one,
|
||||||
// but you can also remove it
|
// but you can also remove it
|
||||||
|
|
|
||||||
103
src/stores/credit-note/index.ts
Normal file
103
src/stores/credit-note/index.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import {
|
||||||
|
CreditNote as Data,
|
||||||
|
CreditNoteStatus as Status,
|
||||||
|
CreditNotePayload as Payload,
|
||||||
|
CreditNotePaybackStatus,
|
||||||
|
} from './types.ts';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
import { api } from 'src/boot/axios.ts';
|
||||||
|
import { PaginationResult } from 'src/types.ts';
|
||||||
|
import { manageAttachment, manageFile } from '../utils/index.ts';
|
||||||
|
|
||||||
|
const ENDPOINT = 'credit-note';
|
||||||
|
|
||||||
|
export * from './types.ts';
|
||||||
|
|
||||||
|
export async function getCreditNoteStats() {
|
||||||
|
const res = await api.get<Record<Status, number>>(`/${ENDPOINT}/stats`);
|
||||||
|
if (res.status < 400) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCreditNoteList(params?: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
query?: string;
|
||||||
|
creditNoteStatus?: Status;
|
||||||
|
}) {
|
||||||
|
const res = await api.get<PaginationResult<Data>>(`/${ENDPOINT}`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
if (res.status < 400) return res.data;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCreditNote(id: string) {
|
||||||
|
const res = await api.get<Data>(`/${ENDPOINT}/${id}`);
|
||||||
|
if (res.status < 400) return res.data;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCreditNote(body: Payload) {
|
||||||
|
const res = await api.post<Data>(`/${ENDPOINT}`, body);
|
||||||
|
if (res.status < 400) return res.data;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCreditNote(id: string, body: Payload) {
|
||||||
|
const res = await api.put<Data>(`/${ENDPOINT}/${id}`, body);
|
||||||
|
if (res.status < 400) return res.data;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCreditNote(id: string) {
|
||||||
|
const res = await api.delete<Data>(`/${ENDPOINT}/${id}`);
|
||||||
|
if (res.status < 400) return res.data;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePaybackStatus(
|
||||||
|
creditNoteId: string,
|
||||||
|
status: CreditNotePaybackStatus,
|
||||||
|
) {
|
||||||
|
const res = await api.post(`/${ENDPOINT}/${creditNoteId}/payback-status`, {
|
||||||
|
paybackStatus: status,
|
||||||
|
});
|
||||||
|
if (res.status < 400) return true;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreditNote = defineStore('credit-note-store', () => {
|
||||||
|
const data = ref<Data[]>([]);
|
||||||
|
const page = ref<number>(1);
|
||||||
|
const pageMax = ref<number>(1);
|
||||||
|
const pageSize = ref<number>(30);
|
||||||
|
const stats = ref<Record<Status, number>>({
|
||||||
|
[Status.Pending]: 0,
|
||||||
|
[Status.Success]: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
pageMax,
|
||||||
|
pageSize,
|
||||||
|
stats,
|
||||||
|
|
||||||
|
getCreditNoteStats,
|
||||||
|
getCreditNote,
|
||||||
|
getCreditNoteList,
|
||||||
|
createCreditNote,
|
||||||
|
updateCreditNote,
|
||||||
|
deleteCreditNote,
|
||||||
|
|
||||||
|
...manageAttachment(api, ENDPOINT),
|
||||||
|
...manageFile<'slip'>(api, ENDPOINT),
|
||||||
|
|
||||||
|
action: { updatePaybackStatus },
|
||||||
|
};
|
||||||
|
});
|
||||||
53
src/stores/credit-note/types.ts
Normal file
53
src/stores/credit-note/types.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { QuotationFull } from '../quotations';
|
||||||
|
import { RequestWork } from '../request-list';
|
||||||
|
import { CreatedBy } from '../types';
|
||||||
|
|
||||||
|
export type CreditNotePayload = {
|
||||||
|
quotationId: string;
|
||||||
|
requestWorkId: string[];
|
||||||
|
remark?: string;
|
||||||
|
reason: string;
|
||||||
|
detail: string;
|
||||||
|
paybackType: 'BankTransfer' | 'Cash';
|
||||||
|
paybackBank: string;
|
||||||
|
paybackAccount: string;
|
||||||
|
paybackAccountName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreditNote = {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
quotationId: string;
|
||||||
|
quotation: QuotationFull;
|
||||||
|
requestWork: RequestWork[];
|
||||||
|
remark?: string;
|
||||||
|
reason: string;
|
||||||
|
detail: string;
|
||||||
|
paybackType: 'BankTransfer' | 'Cash';
|
||||||
|
paybackBank: string;
|
||||||
|
paybackAccount: string;
|
||||||
|
paybackAccountName: string;
|
||||||
|
paybackStatus: CreditNotePaybackStatus;
|
||||||
|
paybackDate?: string | null;
|
||||||
|
|
||||||
|
value: number;
|
||||||
|
|
||||||
|
createdAt: string;
|
||||||
|
createdBy?: CreatedBy;
|
||||||
|
createdByUserId?: string;
|
||||||
|
|
||||||
|
creditNoteStatus: CreditNoteStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum CreditNoteStatus {
|
||||||
|
Pending = 'Pending',
|
||||||
|
Success = 'Success',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CreditNotePaybackStatus {
|
||||||
|
Pending = 'Pending',
|
||||||
|
Verify = 'Verify',
|
||||||
|
Done = 'Done',
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
Payment,
|
Payment,
|
||||||
Receipt,
|
Receipt,
|
||||||
PaymentFlowAccount,
|
PaymentFlowAccount,
|
||||||
|
PaymentDataStatus,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { manageAttachment } from '../utils';
|
import { manageAttachment } from '../utils';
|
||||||
|
|
||||||
|
|
@ -122,6 +123,19 @@ export const useInvoice = defineStore('invoice-store', () => {
|
||||||
const page = ref<number>(1);
|
const page = ref<number>(1);
|
||||||
const pageMax = ref<number>(1);
|
const pageMax = ref<number>(1);
|
||||||
const pageSize = ref<number>(30);
|
const pageSize = ref<number>(30);
|
||||||
|
const stats = ref<Record<PaymentDataStatus, number>>({
|
||||||
|
[PaymentDataStatus.Success]: 0,
|
||||||
|
[PaymentDataStatus.Wait]: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getInvoiceStats(params?: { quotationId?: string }) {
|
||||||
|
const res = await api.get<Record<PaymentDataStatus, number>>(
|
||||||
|
'/invoice/stats',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
if (res.status >= 400) return null;
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
async function getInvoice(id: string) {
|
async function getInvoice(id: string) {
|
||||||
const res = await api.get<Invoice>(`/invoice/${id}`);
|
const res = await api.get<Invoice>(`/invoice/${id}`);
|
||||||
|
|
@ -136,6 +150,7 @@ export const useInvoice = defineStore('invoice-store', () => {
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
query?: string;
|
query?: string;
|
||||||
quotationId?: string;
|
quotationId?: string;
|
||||||
|
pay?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const res = await api.get<PaginationResult<Invoice>>('/invoice', {
|
const res = await api.get<PaginationResult<Invoice>>('/invoice', {
|
||||||
params,
|
params,
|
||||||
|
|
@ -171,7 +186,9 @@ export const useInvoice = defineStore('invoice-store', () => {
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
pageMax,
|
pageMax,
|
||||||
|
stats,
|
||||||
|
|
||||||
|
getInvoiceStats,
|
||||||
getInvoice,
|
getInvoice,
|
||||||
getInvoiceList,
|
getInvoiceList,
|
||||||
createInvoice,
|
createInvoice,
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,14 @@ export type Invoice = {
|
||||||
createdBy: CreatedBy;
|
createdBy: CreatedBy;
|
||||||
createdByUserId: string;
|
createdByUserId: string;
|
||||||
|
|
||||||
payment?: Payment;
|
payment: Payment;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum PaymentDataStatus {
|
||||||
|
Success = 'PaymentSuccess',
|
||||||
|
Wait = 'PaymentWait',
|
||||||
|
}
|
||||||
|
|
||||||
export type InvoicePayload = {
|
export type InvoicePayload = {
|
||||||
quotationId: string;
|
quotationId: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
|
@ -39,7 +44,16 @@ export type Payment = {
|
||||||
paymentStatus: string;
|
paymentStatus: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
amount: number;
|
amount: number;
|
||||||
invoice: { id: string; amount: number; installments: { no: number }[] };
|
invoice: {
|
||||||
|
id: string;
|
||||||
|
amount: number;
|
||||||
|
installments: { no: number }[];
|
||||||
|
quotation: Quotation;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum PaymentDataStatus {
|
||||||
|
Success = 'PaymentSuccess',
|
||||||
|
Wait = 'PaymentWait',
|
||||||
|
}
|
||||||
export type Receipt = Payment;
|
export type Receipt = Payment;
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ export const useQuotationStore = defineStore('quotation-store', () => {
|
||||||
| 'Canceled';
|
| 'Canceled';
|
||||||
urgentFirst?: boolean;
|
urgentFirst?: boolean;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
hasCancel?: boolean;
|
||||||
|
includeRegisteredBranch?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const res = await api.get<PaginationResult<Quotation>>('/quotation', {
|
const res = await api.get<PaginationResult<Quotation>>('/quotation', {
|
||||||
params,
|
params,
|
||||||
|
|
|
||||||
|
|
@ -5,24 +5,26 @@ import { Invoice } from '../payment/types';
|
||||||
import { Employee } from '../employee/types';
|
import { Employee } from '../employee/types';
|
||||||
import { WorkflowTemplate } from '../workflow-template/types';
|
import { WorkflowTemplate } from '../workflow-template/types';
|
||||||
|
|
||||||
export type PayCondition =
|
export enum PayCondition {
|
||||||
| 'Full'
|
Full = 'Full',
|
||||||
| 'Split'
|
Split = 'Split',
|
||||||
| 'SplitCustom'
|
SplitCustom = 'SplitCustom',
|
||||||
| 'BillFull'
|
BillFull = 'BillFull',
|
||||||
| 'BillSplit'
|
BillSplit = 'BillSplit',
|
||||||
| 'BillSplitCustom';
|
BillSplitCustom = 'BillSplitCustom',
|
||||||
|
}
|
||||||
|
|
||||||
export type QuotationStatus =
|
export enum QuotationStatus {
|
||||||
| 'Issued'
|
Issued = 'Issued',
|
||||||
| 'Accepted'
|
Accepted = 'Accepted',
|
||||||
| 'Expired'
|
Expired = 'Expired',
|
||||||
| 'Invoice'
|
Invoice = 'Invoice',
|
||||||
| 'PaymentPending'
|
PaymentPending = 'PaymentPending',
|
||||||
| 'PaymentInProcess'
|
PaymentInProcess = 'PaymentInProcess',
|
||||||
| 'PaymentSuccess'
|
PaymentSuccess = 'PaymentSuccess',
|
||||||
| 'ProcessComplete'
|
ProcessComplete = 'ProcessComplete',
|
||||||
| 'Canceled';
|
Canceled = 'Canceled',
|
||||||
|
}
|
||||||
|
|
||||||
export type CustomerBranchRelation = {
|
export type CustomerBranchRelation = {
|
||||||
district: District;
|
district: District;
|
||||||
|
|
@ -207,7 +209,7 @@ export type QuotationStats = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Quotation = {
|
export type Quotation = {
|
||||||
_count: { worker: number };
|
_count: { worker: number; canceledWork: number };
|
||||||
id: string;
|
id: string;
|
||||||
finalPrice: number;
|
finalPrice: number;
|
||||||
vat: number;
|
vat: number;
|
||||||
|
|
@ -251,6 +253,7 @@ export type Quotation = {
|
||||||
| 'canceled';
|
| 'canceled';
|
||||||
|
|
||||||
registeredBranchId: string;
|
registeredBranchId: string;
|
||||||
|
registeredBranch?: { id: string; name: string; nameEN: string; code: string };
|
||||||
|
|
||||||
customerBranchId: string;
|
customerBranchId: string;
|
||||||
customerBranch: CustomerBranchRelation;
|
customerBranch: CustomerBranchRelation;
|
||||||
|
|
@ -326,7 +329,7 @@ export type QuotationFull = {
|
||||||
customerBranchId: string;
|
customerBranchId: string;
|
||||||
customerBranch: CustomerBranchRelation;
|
customerBranch: CustomerBranchRelation;
|
||||||
registeredBranchId: string;
|
registeredBranchId: string;
|
||||||
registeredBranch: { id: string; name: string };
|
registeredBranch: { id: string; name: string; nameEN: string; code: string };
|
||||||
|
|
||||||
createdByUserId: string;
|
createdByUserId: string;
|
||||||
createdAt: string | Date;
|
createdAt: string | Date;
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,8 @@ export const useRequestList = defineStore('request-list', () => {
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
workStatus?: RequestWorkStatus;
|
workStatus?: RequestWorkStatus;
|
||||||
readyToTask?: boolean;
|
readyToTask?: boolean;
|
||||||
|
quotationId?: string;
|
||||||
|
cancelOnly?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const res = await api.get<PaginationResult<RequestWork>>('/request-work', {
|
const res = await api.get<PaginationResult<RequestWork>>('/request-work', {
|
||||||
params,
|
params,
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ export type RequestWork = {
|
||||||
productServiceId: string;
|
productServiceId: string;
|
||||||
request: RequestData;
|
request: RequestData;
|
||||||
attributes?: Attributes;
|
attributes?: Attributes;
|
||||||
|
creditNoteId?: string;
|
||||||
|
processByUserId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RowDocument = {
|
export type RowDocument = {
|
||||||
|
|
|
||||||
|
|
@ -355,13 +355,33 @@ export function manageFile<T extends string>(
|
||||||
parentId: string;
|
parentId: string;
|
||||||
fileId: string;
|
fileId: string;
|
||||||
file: File;
|
file: File;
|
||||||
|
uploadUrl?: boolean;
|
||||||
onUploadProgress?: (e: AxiosProgressEvent) => void;
|
onUploadProgress?: (e: AxiosProgressEvent) => void;
|
||||||
abortController?: AbortController;
|
abortController?: AbortController;
|
||||||
}) => {
|
}) => {
|
||||||
const res = await api.put(
|
const res = opts.uploadUrl
|
||||||
`/${base}/${opts.parentId}/file-${opts.group}/${opts.fileId}`,
|
? await api.put(
|
||||||
opts.file,
|
`/${base}/${opts.parentId}/file-${opts.group}/${opts.fileId}`,
|
||||||
{
|
)
|
||||||
|
: await api.put(
|
||||||
|
`/${base}/${opts.parentId}/file-${opts.group}/${opts.fileId}`,
|
||||||
|
opts.file,
|
||||||
|
{
|
||||||
|
headers: { 'Content-Type': opts.file.type },
|
||||||
|
onUploadProgress: opts.onUploadProgress
|
||||||
|
? opts.onUploadProgress
|
||||||
|
: option?.onUploadProgress
|
||||||
|
? option.onUploadProgress
|
||||||
|
: (e) => console.log(e),
|
||||||
|
signal: opts.abortController?.signal,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.status >= 400) return false;
|
||||||
|
|
||||||
|
if (opts.uploadUrl && typeof res.data === 'string') {
|
||||||
|
// NOTE: Must use axios instance or else CORS error.
|
||||||
|
const uploadRes = await axios.put(res.data, opts.file, {
|
||||||
headers: { 'Content-Type': opts.file.type },
|
headers: { 'Content-Type': opts.file.type },
|
||||||
onUploadProgress: opts.onUploadProgress
|
onUploadProgress: opts.onUploadProgress
|
||||||
? opts.onUploadProgress
|
? opts.onUploadProgress
|
||||||
|
|
@ -369,10 +389,12 @@ export function manageFile<T extends string>(
|
||||||
? option.onUploadProgress
|
? option.onUploadProgress
|
||||||
: (e) => console.log(e),
|
: (e) => console.log(e),
|
||||||
signal: opts.abortController?.signal,
|
signal: opts.abortController?.signal,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
if (res.status < 400) return true;
|
if (uploadRes.status >= 400) return true;
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
delFile: async (opts: { group: T; parentId: string; fileId: string }) => {
|
delFile: async (opts: { group: T; parentId: string; fileId: string }) => {
|
||||||
const res = await api.delete(
|
const res = await api.delete(
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test('Login', async () => {
|
test('Login', async () => {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ test.afterAll(async () => {
|
||||||
async function login(page) {
|
async function login(page) {
|
||||||
try {
|
try {
|
||||||
// Login
|
// Login
|
||||||
await page.goto('http://192.168.1.62:20101/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/^Sign in to /);
|
await expect(page).toHaveTitle(/^Sign in to /);
|
||||||
await page.fill("input[name='username']", 'admin');
|
await page.fill("input[name='username']", 'admin');
|
||||||
await page.fill("input[name='password']", '1234');
|
await page.fill("input[name='password']", '1234');
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue