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:
parent
7817f8bd40
commit
0ef389c69b
5 changed files with 235 additions and 3 deletions
108
src/components/upload-file/UploadFileSection.vue
Normal file
108
src/components/upload-file/UploadFileSection.vue
Normal 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>
|
||||||
|
|
@ -701,6 +701,7 @@ export default {
|
||||||
paySplitCount: 'Number of Installments',
|
paySplitCount: 'Number of Installments',
|
||||||
payTotal: 'Total {msg}',
|
payTotal: 'Total {msg}',
|
||||||
customerAcceptance: 'Customer Acceptance',
|
customerAcceptance: 'Customer Acceptance',
|
||||||
|
additionalFile: 'Attachment File',
|
||||||
|
|
||||||
paySplitMessage: 'Amount to be paid (Baht)',
|
paySplitMessage: 'Amount to be paid (Baht)',
|
||||||
summary: 'Total Summary',
|
summary: 'Total Summary',
|
||||||
|
|
|
||||||
|
|
@ -693,6 +693,7 @@ export default {
|
||||||
selectInvoice: 'เลือกใบแจ้งหนี้',
|
selectInvoice: 'เลือกใบแจ้งหนี้',
|
||||||
approveInvoice: 'อนุมัติใบแจ้งหนี้',
|
approveInvoice: 'อนุมัติใบแจ้งหนี้',
|
||||||
customerAcceptance: 'ลูกค้าตอบรับ',
|
customerAcceptance: 'ลูกค้าตอบรับ',
|
||||||
|
additionalFile: 'ไฟล์เอกสารเพิ่มเติม',
|
||||||
|
|
||||||
paymentCondition: 'เงื่อนไขการชำระเงิน',
|
paymentCondition: 'เงื่อนไขการชำระเงิน',
|
||||||
payType: 'วิธีการชำระเงิน',
|
payType: 'วิธีการชำระเงิน',
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { ProductTree, quotationProductTree } from './utils';
|
||||||
// NOTE: Import stores
|
// NOTE: Import stores
|
||||||
import { setLocale, dateFormat, calculateAge } from 'src/utils/datetime';
|
import { setLocale, dateFormat, calculateAge } from 'src/utils/datetime';
|
||||||
import { useEmployeeForm } from 'src/pages/03_customer-management/form';
|
import { useEmployeeForm } from 'src/pages/03_customer-management/form';
|
||||||
|
import { useQuotationStore } from 'src/stores/quotations';
|
||||||
import useProductServiceStore from 'stores/product-service';
|
import useProductServiceStore from 'stores/product-service';
|
||||||
import {
|
import {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
|
@ -71,6 +72,7 @@ import {
|
||||||
uploadFileListEmployee,
|
uploadFileListEmployee,
|
||||||
columnsAttachment,
|
columnsAttachment,
|
||||||
} from 'src/pages/03_customer-management/constant';
|
} from 'src/pages/03_customer-management/constant';
|
||||||
|
import UploadFileSection from 'src/components/upload-file/UploadFileSection.vue';
|
||||||
|
|
||||||
import { columnPaySplit } from './constants';
|
import { columnPaySplit } from './constants';
|
||||||
import { precisionRound } from 'src/utils/arithmetic';
|
import { precisionRound } from 'src/utils/arithmetic';
|
||||||
|
|
@ -78,6 +80,7 @@ import { useConfigStore } from 'src/stores/config';
|
||||||
import QuotationFormMetadata from './QuotationFormMetadata.vue';
|
import QuotationFormMetadata from './QuotationFormMetadata.vue';
|
||||||
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
import BadgeComponent from 'src/components/BadgeComponent.vue';
|
||||||
import PaymentForm from './PaymentForm.vue';
|
import PaymentForm from './PaymentForm.vue';
|
||||||
|
import { api } from 'src/boot/axios';
|
||||||
|
|
||||||
type Node = {
|
type Node = {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
|
@ -97,6 +100,7 @@ const productServiceStore = useProductServiceStore();
|
||||||
const employeeFormStore = useEmployeeForm();
|
const employeeFormStore = useEmployeeForm();
|
||||||
const customerStore = useCustomerStore();
|
const customerStore = useCustomerStore();
|
||||||
const quotationForm = useQuotationForm();
|
const quotationForm = useQuotationForm();
|
||||||
|
const quotationStore = useQuotationStore();
|
||||||
const optionStore = useOptionStore();
|
const optionStore = useOptionStore();
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const ocrStore = useOcrStore();
|
const ocrStore = useOcrStore();
|
||||||
|
|
@ -149,6 +153,16 @@ const selectedInstallmentNo = ref<number[]>([]);
|
||||||
const selectedInstallment = ref();
|
const selectedInstallment = ref();
|
||||||
const agentPrice = ref(false);
|
const agentPrice = ref(false);
|
||||||
|
|
||||||
|
const attachmentData = ref<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
progress: number;
|
||||||
|
loaded: number;
|
||||||
|
total: number;
|
||||||
|
url?: string;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
function getPrice(
|
function getPrice(
|
||||||
list: typeof productServiceList.value,
|
list: typeof productServiceList.value,
|
||||||
filterHook?: (
|
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>>();
|
const sessionData = ref<Record<string, any>>();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
@ -774,6 +848,7 @@ onMounted(async () => {
|
||||||
if (locale.value === 'tha') optionStore.globalOption = rawOption.tha;
|
if (locale.value === 'tha') optionStore.globalOption = rawOption.tha;
|
||||||
|
|
||||||
await fetchStatus();
|
await fetchStatus();
|
||||||
|
await getAttachment();
|
||||||
|
|
||||||
pageState.isLoaded = true;
|
pageState.isLoaded = true;
|
||||||
});
|
});
|
||||||
|
|
@ -1293,6 +1368,49 @@ const view = ref<View>(View.Quotation);
|
||||||
</div>
|
</div>
|
||||||
</q-expansion-item>
|
</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
|
<q-expansion-item
|
||||||
for="item-up"
|
for="item-up"
|
||||||
id="item-up"
|
id="item-up"
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export const useQuotationStore = defineStore('quotation-store', () => {
|
||||||
| 'BillSplitCustom';
|
| 'BillSplitCustom';
|
||||||
query?: string;
|
query?: string;
|
||||||
}) {
|
}) {
|
||||||
const res = await api.get<PaginationResult<Quotation>>(`/quotation`, {
|
const res = await api.get<PaginationResult<Quotation>>('/quotation', {
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
if (res.status < 400) {
|
if (res.status < 400) {
|
||||||
|
|
@ -141,6 +141,8 @@ export const useQuotationStore = defineStore('quotation-store', () => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileManager = manageAttachment(api, 'quotation');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
page,
|
page,
|
||||||
|
|
@ -154,6 +156,8 @@ export const useQuotationStore = defineStore('quotation-store', () => {
|
||||||
editQuotation,
|
editQuotation,
|
||||||
deleteQuotation,
|
deleteQuotation,
|
||||||
changeStatus,
|
changeStatus,
|
||||||
|
|
||||||
|
...fileManager,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -163,7 +167,7 @@ export const useQuotationStore = defineStore('quotation-store', () => {
|
||||||
export const useQuotationPayment = defineStore('quotation-payment', () => {
|
export const useQuotationPayment = defineStore('quotation-payment', () => {
|
||||||
async function getQuotationPayment(quotationId: string) {
|
async function getQuotationPayment(quotationId: string) {
|
||||||
const res = await api.get<PaginationResult<QuotationPaymentData>>(
|
const res = await api.get<PaginationResult<QuotationPaymentData>>(
|
||||||
`/payment`,
|
'/payment',
|
||||||
{ params: { quotationId } },
|
{ params: { quotationId } },
|
||||||
);
|
);
|
||||||
if (res.status < 400) {
|
if (res.status < 400) {
|
||||||
|
|
@ -186,7 +190,7 @@ export const useQuotationPayment = defineStore('quotation-payment', () => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileManager = manageAttachment(api, `payment`);
|
const fileManager = manageAttachment(api, 'payment');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getQuotationPayment,
|
getQuotationPayment,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue