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:
Methapon Metanipat 2025-01-14 09:08:31 +07:00 committed by GitHub
parent 0c694dee5d
commit 5e2100eb8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 2897 additions and 77 deletions

View file

@ -22,7 +22,9 @@ defineProps<{
badgeColor?: string;
hideKebabView?: boolean;
hideKebabEdit?: boolean;
hideKebabDelete?: boolean;
hideAction?: boolean;
useCancel?: boolean;
customData?: {
label: string;
@ -39,6 +41,7 @@ defineEmits<{
(e: 'delete'): void;
(e: 'example'): void;
(e: 'preview'): void;
(e: 'cancel'): void;
}>();
const rand = Math.random();
@ -93,7 +96,8 @@ const rand = Math.random();
:idName="code"
status="ACTIVE"
hide-toggle
hide-delete
:use-cancel
:hide-delete="hideKebabDelete"
:hide-view="hideKebabView"
:hide-edit="hideKebabEdit"
@view="$emit('view')"
@ -101,6 +105,7 @@ const rand = Math.random();
@link="$emit('link')"
@upload="$emit('upload')"
@delete="$emit('delete')"
@cancel="$emit('cancel')"
/>
</nav>
</header>

View 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>

View file

@ -6,29 +6,57 @@ import DialogHeader from './DialogHeader.vue';
import MainButton from '../button/MainButton.vue';
import NoData from '../NoData.vue';
defineProps<{
title: string;
const props = defineProps<{
title?: string;
url?: string;
hideTab?: boolean;
download?: boolean;
transformUrl?: (url: string) => string | Promise<string>;
}>();
const open = defineModel<boolean>({ default: false });
const state = reactive({
imageZoom: 100,
transformedUrl: props.url,
});
function openDialog() {
async function openDialog() {
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>
<template>
<DialogFormContainer v-model="open" v-on:open="openDialog">
<template #header>
<DialogHeader :title="title" />
<DialogHeader :title="title || ''" />
</template>
<main class="column full-height">
<section
v-if="!hideTab"
style="background: var(--gray-3)"
class="q-py-sm row justify-center"
>
@ -74,18 +102,32 @@ function openDialog() {
</section>
<div
:class="{ 'cursor-pointer': state.imageZoom > 100 }"
class="full-height full-width flex justify-center items-center col scroll q-pa-md"
:class="{ 'cursor-pointer': state.imageZoom > 100 }"
v-dragscroll
>
<q-img
v-if="url"
v-if="state.transformedUrl"
class="full-height"
:src="url"
:src="state.transformedUrl"
fit="contain"
:style="{ transform: `scale(${state.imageZoom / 100})` }"
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 />
</div>
</main>

View 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>
&nbsp;
{{ 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>
&nbsp;
{{ 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>

View file

@ -14,6 +14,7 @@ const props = withDefaults(
url?: string;
progress?: number;
uploading?: { loaded: number; total: number };
idle?: boolean;
clickable?: boolean;
closeable?: boolean;
@ -53,7 +54,7 @@ onMounted(() => {
@click="$emit('click')"
>
<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 class="text-caption app-text-muted-2">
{{
@ -68,8 +69,13 @@ onMounted(() => {
color="primary"
size="1.5em"
/>
<q-icon v-else name="mdi-check-circle" color="positive" size="1rem" />
{{ progress !== 1 ? `Uploading...` : 'Completed' }}
<q-icon
v-if="progress === 1 && !idle"
name="mdi-check-circle"
color="positive"
size="1rem"
/>
{{ idle ? '' : progress !== 1 ? `Uploading...` : 'Completed' }}
</span>
</article>
<q-btn

View file

@ -33,6 +33,7 @@ const fileData = defineModel<
loaded: number;
total: number;
url?: string;
placeholder?: boolean;
}[]
>('fileData', { required: true });
@ -54,7 +55,8 @@ function pickFile() {
<template>
<div :class="{ row: layout === 'column' }">
<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" />
{{ label }}
@ -76,11 +78,10 @@ function pickFile() {
</div>
<!-- upload card -->
<section class="row col" :class="{ 'q-pl-md': layout === 'column' }">
<section class="column col" :class="{ 'q-pl-md': layout === 'column' }">
<div
v-for="(d, j) in fileData"
:key="j"
class="col-12"
:class="{
'q-pt-md': layout === 'row' && j === 0,
'q-pt-sm': j > 0,
@ -91,6 +92,7 @@ function pickFile() {
:progress="d.progress"
:uploading="{ loaded: d.loaded, total: d.total }"
:url="d.url"
:idle="d.placeholder"
icon="mdi-file-image-outline"
color="hsl(var(--text-mute))"
clickable