feat: credit note (#171)
* feat: add main page credit note * feat: enable credit note route and update menu item states * refactor: add i18n * refactor: edit i18n status * feat: add action column * feat: add empty form page * feat: add get data * feat: add type credit note status * refactor: add type name en * refactor: add type credit note status in type credit note * feat: add hsla colors * refactor: add slot grid * refactor: handle hide kebab edit show only tab tssued * feat: show grid card * feat: i18n * feat: add credit note form and dialog * refactor: add props hide kebab deelete * refactor: hide kebab * style: update color segments to indigo theme * feat: i18n * fix: update labels for credit note fields * refactor: add type * feat: new select quotation * refactor: use new select quotation * feat: navigate to * refactor: function trigger and navigate to * feat: i18n bank * feat: add payment expansion component and integrate into credit note form * refactor: bind i18n pay condition * refactor: navigate to get quotation id * feat: i18n * fix: update label for createdBy field in credit note form * feat: add credit note information expansion component * feat: add Credit Note expansion component and update form layout * refactor: bind quotation id and send * refactor: deelete duplicate type * refactor: show state button * refactor: handle show status * feat: add function update payback status * feat: add return and canceled reasons to credit note translations * feat: enhance SelectReadyRequestWork component with credit note handling and fetch parameters * feat: type * feat: add status handling and optional display for employee table * refactor: rename selectedQuotationId to quotationId in FormCredit component * feat: set default opened state for CreditNoteExpansion and add reason options * feat: update PaymentExpansion to handle payback type selection and clear fields for cash payments * feat: enhance ProductExpansion to support credit note handling and adjust price calculations * feat: implement product handling and price calculation in CreditNote form * feat: add manage attachment function to store * refactor: bind delete credit note * feat: add credit note status and reference fields to types * refactor: update task step handling and simplify request work structure in credit note form * feat: add navigation to quotation from credit note form * feat: enhance upload section layout based on file data * feat: add readonly functionality to credit note form and related components * refactor: remove console log * feat: update i18n * style: add rounded corners to complete view container in quotation form * feat: add RefundInformation component and update credit note form status handling * feat: i18n * feat: update payback status endpoint and add paybackStatus to CreditNote type * feat: enhance QuotationFormReceipt component with optional props and slot support * feat: integrate payback status handling in RefundInformation and FormPage components * feat: add external file group * feat: update API endpoint paths for credit note operations * feat: improve layout and styling in UploadFile components * feat: implement file upload and management in Credit Note * refactor: update upload to check if it is redirect or not * feat: upload file slips * feat: add payback date dispaly * refactor: change module no * fix: icon link to main page instead * feat: add file dialog with image download functionality * fix: view slip * feat: add download button to image viewer * feat: handle after submit * feat: conditionally render bank transfer information * feat: handle upload file on create * feat: handle change payback status * feat: payback type in credit note form * fix: correct reference to quotation data in goToQuotation function --------- Co-authored-by: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Co-authored-by: puriphatt <puriphat@frappet.com> Co-authored-by: Thanaphon Frappet <thanaphon@frappet.com>
This commit is contained in:
parent
0c694dee5d
commit
5e2100eb8d
34 changed files with 2897 additions and 77 deletions
|
|
@ -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 ? '' : 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
|
||||||
|
|
|
||||||
|
|
@ -1100,6 +1100,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.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1135,4 +1136,54 @@ 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { title } from 'process';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
general: {
|
general: {
|
||||||
ok: 'ตกลง',
|
ok: 'ตกลง',
|
||||||
|
|
@ -1001,7 +1003,8 @@ export default {
|
||||||
confirmSavingStatus:
|
confirmSavingStatus:
|
||||||
'คุณต้องการยืนยันการบันทึกข้อมูลการเปลี่ยนสถานะใช่หรือไม่',
|
'คุณต้องการยืนยันการบันทึกข้อมูลการเปลี่ยนสถานะใช่หรือไม่',
|
||||||
confirmSending: 'ยืนยันการส่งงานใช่หรือไม่',
|
confirmSending: 'ยืนยันการส่งงานใช่หรือไม่',
|
||||||
confirmEndWorkWarning: `ท่านต้องการให้ดำเนินการจบงานในขณะนี้หรือไม่? สถานะปัจจุบันที่แสดงว่า 'รอดำเนินการ' 'กำลังดำเนินการ' 'ไปดำเนินการใหม่' จะถูกเปลี่ยนเป็น 'ทำใหม่ทั้งหมด'`,
|
confirmEndWorkWarning:
|
||||||
|
"ท่านต้องการให้ดำเนินการจบงานในขณะนี้หรือไม่? สถานะปัจจุบันที่แสดงว่า 'รอดำเนินการ' 'กำลังดำเนินการ' 'ไปดำเนินการใหม่' จะถูกเปลี่ยนเป็น 'ทำใหม่ทั้งหมด'",
|
||||||
confirmEndWork: 'ท่านต้องการจบงานใช่หรือไม่',
|
confirmEndWork: 'ท่านต้องการจบงานใช่หรือไม่',
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
|
|
@ -1078,6 +1081,7 @@ export default {
|
||||||
'ข้อมูลงวดไม่ถูกต้อง กรุณาตรวจสอบและยืนยันว่าแต่ละงวดมีสินค้าอย่างน้อยหนึ่งรายการ',
|
'ข้อมูลงวดไม่ถูกต้อง กรุณาตรวจสอบและยืนยันว่าแต่ละงวดมีสินค้าอย่างน้อยหนึ่งรายการ',
|
||||||
flowTemplateNotFound: 'ไม่พบขั้นตอนการทำงาน',
|
flowTemplateNotFound: 'ไม่พบขั้นตอนการทำงาน',
|
||||||
taskOrderNotFound: 'ไม่พบใบสั่งงาน',
|
taskOrderNotFound: 'ไม่พบใบสั่งงาน',
|
||||||
|
quotationNotFound: 'ไม่พบใบเสนอราคา',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1113,4 +1117,54 @@ 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: 'คืนเงินเสร็จสิ้น',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -152,12 +152,12 @@ 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: 'uploadSlip', route: '', disabled: true },
|
||||||
{ label: 'receipt', route: '' },
|
{ label: 'receipt', route: '', disabled: true },
|
||||||
{ label: 'creditNote', route: '' },
|
{ label: 'creditNote', route: '/credit-note' },
|
||||||
{ label: 'debitNote', route: '' },
|
{ label: 'debitNote', route: '', disabled: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -1865,7 +1865,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>
|
||||||
|
|
|
||||||
|
|
@ -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="
|
||||||
|
|
|
||||||
822
src/pages/11_credit-note/FormPage.vue
Normal file
822
src/pages/11_credit-note/FormPage.vue
Normal file
|
|
@ -0,0 +1,822 @@
|
||||||
|
<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';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const creditNote = useCreditNote();
|
||||||
|
const quotation = useQuotationStore();
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
const { data: config } = storeToRefs(configStore);
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
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 currentFormData = ref<CreditNotePayload>({
|
||||||
|
quotationId: '',
|
||||||
|
requestWorkId: [],
|
||||||
|
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;
|
||||||
|
|
||||||
|
currentFormData.value = {
|
||||||
|
quotationId: creditNoteData.value.quotationId,
|
||||||
|
requestWorkId: creditNoteData.value.requestWork.map((v) => v.id || ''),
|
||||||
|
reason: creditNoteData.value.reason,
|
||||||
|
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 = currentFormData.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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CreditNoteExpansion
|
||||||
|
v-if="view === null"
|
||||||
|
:readonly="readonly"
|
||||||
|
v-model:reason="currentFormData.reason"
|
||||||
|
v-model:detail="currentFormData.detail"
|
||||||
|
/>
|
||||||
|
<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="currentFormData.paybackType"
|
||||||
|
v-model:payback-bank="currentFormData.paybackBank"
|
||||||
|
v-model:payback-account="currentFormData.paybackAccount"
|
||||||
|
v-model:payback-account-name="currentFormData.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="currentFormData.paybackType"
|
||||||
|
v-model:payback-bank="currentFormData.paybackBank"
|
||||||
|
v-model:payback-account="currentFormData.paybackAccount"
|
||||||
|
v-model:payback-account-name="currentFormData.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);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- TODO: bind additional file -->
|
||||||
|
<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(({ name }) => {
|
||||||
|
attachmentData.push({
|
||||||
|
name: name,
|
||||||
|
progress: 1,
|
||||||
|
loaded: 0,
|
||||||
|
total: 0,
|
||||||
|
placeholder: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!creditNoteData) return;
|
||||||
|
|
||||||
|
await uploadFile(creditNoteData.id, f);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@remove="
|
||||||
|
async (n) => {
|
||||||
|
if (!creditNoteData) return;
|
||||||
|
await remove(creditNoteData.id, n);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- TODO: bind remark -->
|
||||||
|
<RemarkExpansion
|
||||||
|
v-if="view !== CreditNoteStatus.Success"
|
||||||
|
:readonly="readonly"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<SaveButton
|
||||||
|
v-if="!readonly"
|
||||||
|
@click="submit"
|
||||||
|
: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>
|
||||||
468
src/pages/11_credit-note/MainPage.vue
Normal file
468
src/pages/11_credit-note/MainPage.vue
Normal file
|
|
@ -0,0 +1,468 @@
|
||||||
|
<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 { ref } from 'vue';
|
||||||
|
import { dialogWarningClose } from 'src/stores/utils';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const flow = useFlowStore();
|
||||||
|
const navigator = useNavigator();
|
||||||
|
const creditNote = useCreditNote();
|
||||||
|
const selectedQuotationId = ref<string>('');
|
||||||
|
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"
|
||||||
|
@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>
|
||||||
87
src/pages/11_credit-note/TableCreditNote.vue
Normal file
87
src/pages/11_credit-note/TableCreditNote.vue
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
<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[] }>();
|
||||||
|
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 in visible" :align="col.align">
|
||||||
|
<!-- NOTE: custom column will starts with # -->
|
||||||
|
<template v-if="!col.name.startsWith('#')">
|
||||||
|
<span v-if="col.name !== 'quotationPayment'">
|
||||||
|
{{
|
||||||
|
typeof col.field === 'string'
|
||||||
|
? props.row[col.field as keyof 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
|
||||||
|
@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',
|
||||||
|
};
|
||||||
51
src/pages/11_credit-note/expansion/CreditNoteExpansion.vue
Normal file
51
src/pages/11_credit-note/expansion/CreditNoteExpansion.vue
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SelectInput from 'src/components/shared/SelectInput.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
readonly?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const reason = defineModel<string>('reason');
|
||||||
|
const detail = defineModel<string>('detail');
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<q-expansion-item
|
||||||
|
dense
|
||||||
|
:default-opened="true"
|
||||||
|
class="overflow-hidden bordered full-width"
|
||||||
|
switch-toggle-side
|
||||||
|
style="border-radius: var(--radius-2)"
|
||||||
|
expand-icon="mdi-chevron-down-circle"
|
||||||
|
header-class="surface-1 q-py-sm text-medium text-body1"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>
|
||||||
|
{{ $t('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' },
|
||||||
|
]"
|
||||||
|
></SelectInput>
|
||||||
|
<q-input
|
||||||
|
:readonly
|
||||||
|
for="input-credit-note-additional-detail"
|
||||||
|
:label="$t('creditNote.label.additionalDetail')"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
class="col"
|
||||||
|
v-model="detail"
|
||||||
|
></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>
|
||||||
|
|
@ -105,6 +105,11 @@ 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'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -148,6 +153,16 @@ 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'),
|
||||||
|
},
|
||||||
|
|
||||||
// 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 },
|
||||||
|
};
|
||||||
|
});
|
||||||
51
src/stores/credit-note/types.ts
Normal file
51
src/stores/credit-note/types.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { QuotationFull } from '../quotations';
|
||||||
|
import { RequestWork } from '../request-list';
|
||||||
|
import { CreatedBy } from '../types';
|
||||||
|
|
||||||
|
export type CreditNotePayload = {
|
||||||
|
quotationId: string;
|
||||||
|
requestWorkId: 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[];
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -209,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;
|
||||||
|
|
@ -253,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;
|
||||||
|
|
@ -328,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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue