feat: quotation attachment (#49)

* fix: i18n

* fix: 18n

* feat: file upload component

* feat: quotation attachment

---------

Co-authored-by: puriphatt <puriphat@frappet.com>
This commit is contained in:
Methapon Metanipat 2024-11-01 17:18:24 +07:00 committed by GitHub
parent 7817f8bd40
commit 0ef389c69b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 235 additions and 3 deletions

View file

@ -0,0 +1,108 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { QFile } from 'quasar';
import { baseUrl } from 'stores/utils';
import UploadFileCard from './UploadFileCard.vue';
const props = withDefaults(
defineProps<{
readonly?: boolean;
label?: string;
multiple?: boolean;
transformUrl?: (url: string) => string | Promise<string>;
}>(),
{
label: 'Upload',
readonly: false,
multiple: false,
},
);
defineEmits<{
(e: 'close', name: string): void;
(e: 'update:file', file: File): void;
}>();
const fileData = defineModel<
{
name: string;
progress: number;
loaded: number;
total: number;
url?: string;
}[]
>('fileData', { required: true });
const file = ref<File[]>([]);
const refQFile = ref<InstanceType<typeof QFile>>();
async function triggerViewSlip(url: string) {
window.open(
props.transformUrl ? await props.transformUrl(url) : `${baseUrl}/${url}`,
'_blank',
);
}
function pickFile() {
if (!refQFile.value) return;
refQFile.value.pickFiles();
}
</script>
<template>
<div>
<div
class="upload-section column rounded q-py-md full-height items-center justify-center no-wrap surface-2"
>
<q-img src="/images/upload.png" width="150px" />
{{ label }}
<q-btn
v-if="!readonly"
unelevated
:label="$t('general.upload')"
rounded
class="app-bg-info q-mt-sm"
@click.stop="() => pickFile()"
/>
<q-file
ref="refQFile"
:multiple
v-show="false"
v-model="file"
@update:model-value="(f) => $emit('update:file', f)"
/>
</div>
<!-- upload card -->
<section class="row">
<div
v-for="(d, j) in fileData"
:key="j"
class="col-12"
:class="{
'q-pt-md': j === 0,
'q-pt-sm': j > 0,
}"
>
<UploadFileCard
:name="d.name"
:progress="d.progress"
:uploading="{ loaded: d.loaded, total: d.total }"
:url="d.url"
icon="mdi-file-image-outline"
color="hsl(var(--text-mute))"
clickable
:closeable="!readonly"
@click="triggerViewSlip(d.url || '')"
@close="$emit('close', d.name)"
/>
</div>
</section>
</div>
</template>
<style scoped>
.upload-section {
border: 3px solid var(--border-color);
border-style: dashed;
}
</style>

View file

@ -701,6 +701,7 @@ export default {
paySplitCount: 'Number of Installments',
payTotal: 'Total {msg}',
customerAcceptance: 'Customer Acceptance',
additionalFile: 'Attachment File',
paySplitMessage: 'Amount to be paid (Baht)',
summary: 'Total Summary',

View file

@ -693,6 +693,7 @@ export default {
selectInvoice: 'เลือกใบแจ้งหนี้',
approveInvoice: 'อนุมัติใบแจ้งหนี้',
customerAcceptance: 'ลูกค้าตอบรับ',
additionalFile: 'ไฟล์เอกสารเพิ่มเติม',
paymentCondition: 'เงื่อนไขการชำระเงิน',
payType: 'วิธีการชำระเงิน',

View file

@ -13,6 +13,7 @@ import { ProductTree, quotationProductTree } from './utils';
// NOTE: Import stores
import { setLocale, dateFormat, calculateAge } from 'src/utils/datetime';
import { useEmployeeForm } from 'src/pages/03_customer-management/form';
import { useQuotationStore } from 'src/stores/quotations';
import useProductServiceStore from 'stores/product-service';
import {
baseUrl,
@ -71,6 +72,7 @@ import {
uploadFileListEmployee,
columnsAttachment,
} from 'src/pages/03_customer-management/constant';
import UploadFileSection from 'src/components/upload-file/UploadFileSection.vue';
import { columnPaySplit } from './constants';
import { precisionRound } from 'src/utils/arithmetic';
@ -78,6 +80,7 @@ import { useConfigStore } from 'src/stores/config';
import QuotationFormMetadata from './QuotationFormMetadata.vue';
import BadgeComponent from 'src/components/BadgeComponent.vue';
import PaymentForm from './PaymentForm.vue';
import { api } from 'src/boot/axios';
type Node = {
[key: string]: any;
@ -97,6 +100,7 @@ const productServiceStore = useProductServiceStore();
const employeeFormStore = useEmployeeForm();
const customerStore = useCustomerStore();
const quotationForm = useQuotationForm();
const quotationStore = useQuotationStore();
const optionStore = useOptionStore();
const { t, locale } = useI18n();
const ocrStore = useOcrStore();
@ -149,6 +153,16 @@ const selectedInstallmentNo = ref<number[]>([]);
const selectedInstallment = ref();
const agentPrice = ref(false);
const attachmentData = ref<
{
name: string;
progress: number;
loaded: number;
total: number;
url?: string;
}[]
>([]);
function getPrice(
list: typeof productServiceList.value,
filterHook?: (
@ -717,6 +731,66 @@ function changeMode(mode: string) {
}
}
async function triggerDelete(name: string) {
await quotationStore.delAttachment({
parentId: quotationFormData.value.id || '',
name,
});
await getAttachment();
}
async function getAttachment() {
const attachment = await quotationStore.listAttachment({
parentId: quotationFormData.value.id || '',
});
if (attachment && attachment.length > 0) {
attachmentData.value = attachmentData.value.filter((item) =>
attachment.includes(item.name),
);
attachment.forEach((v) => {
const exists = attachmentData.value.some((item) => item.name === v);
if (!exists) {
attachmentData.value.push({
name: v,
progress: 1,
loaded: 0,
total: 0,
url: `quotation/${quotationFormData.value.id || ''}/attachment/${v}`,
});
}
});
} else attachmentData.value = [];
}
async function uploadAttachment(file?: File) {
if (!file) return;
if (!quotationFormData.value) return;
attachmentData.value.push({
name: file.name,
progress: 0,
loaded: 0,
total: 0,
});
const ret = await quotationStore.putAttachment({
parentId: quotationFormData.value.id || '',
name: file.name,
file: file,
onUploadProgress: (e) => {
attachmentData.value[attachmentData.value.length - 1] = {
name: file.name,
progress: e.progress || 0,
loaded: e.loaded,
total: e.total || 0,
};
},
});
if (ret) await getAttachment();
}
const sessionData = ref<Record<string, any>>();
onMounted(async () => {
@ -774,6 +848,7 @@ onMounted(async () => {
if (locale.value === 'tha') optionStore.globalOption = rawOption.tha;
await fetchStatus();
await getAttachment();
pageState.isLoaded = true;
});
@ -1293,6 +1368,49 @@ const view = ref<View>(View.Quotation);
</div>
</q-expansion-item>
<q-expansion-item
for="item-up"
id="item-up"
dense
class="overflow-hidden"
switch-toggle-side
default-opened
style="border-radius: var(--radius-2)"
expand-icon="mdi-chevron-down-circle"
header-class="surface-1"
>
<template v-slot:header>
<section class="row items-center full-width">
<div class="row items-center q-pr-md q-py-sm">
<span
class="text-weight-medium q-mr-md"
style="font-size: 18px"
>
{{ $t('quotation.additionalFile') }}
</span>
</div>
</section>
</template>
<div class="surface-1 q-pa-md full-width">
<UploadFileSection
:readonly="view !== View.Quotation && view !== View.Invoice"
v-model:file-data="attachmentData"
:label="
$t('general.upload', { msg: $t('general.attachment') })
"
:transform-url="
async (url: string) => {
const result = await api.get<string>(url);
return result.data;
}
"
@update:file="(f) => uploadAttachment(f)"
@close="(v) => triggerDelete(v)"
/>
</div>
</q-expansion-item>
<q-expansion-item
for="item-up"
id="item-up"

View file

@ -58,7 +58,7 @@ export const useQuotationStore = defineStore('quotation-store', () => {
| 'BillSplitCustom';
query?: string;
}) {
const res = await api.get<PaginationResult<Quotation>>(`/quotation`, {
const res = await api.get<PaginationResult<Quotation>>('/quotation', {
params,
});
if (res.status < 400) {
@ -141,6 +141,8 @@ export const useQuotationStore = defineStore('quotation-store', () => {
return null;
}
const fileManager = manageAttachment(api, 'quotation');
return {
data,
page,
@ -154,6 +156,8 @@ export const useQuotationStore = defineStore('quotation-store', () => {
editQuotation,
deleteQuotation,
changeStatus,
...fileManager,
};
});
@ -163,7 +167,7 @@ export const useQuotationStore = defineStore('quotation-store', () => {
export const useQuotationPayment = defineStore('quotation-payment', () => {
async function getQuotationPayment(quotationId: string) {
const res = await api.get<PaginationResult<QuotationPaymentData>>(
`/payment`,
'/payment',
{ params: { quotationId } },
);
if (res.status < 400) {
@ -186,7 +190,7 @@ export const useQuotationPayment = defineStore('quotation-payment', () => {
return null;
}
const fileManager = manageAttachment(api, `payment`);
const fileManager = manageAttachment(api, 'payment');
return {
getQuotationPayment,