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',
|
||||
payTotal: 'Total {msg}',
|
||||
customerAcceptance: 'Customer Acceptance',
|
||||
additionalFile: 'Attachment File',
|
||||
|
||||
paySplitMessage: 'Amount to be paid (Baht)',
|
||||
summary: 'Total Summary',
|
||||
|
|
|
|||
|
|
@ -693,6 +693,7 @@ export default {
|
|||
selectInvoice: 'เลือกใบแจ้งหนี้',
|
||||
approveInvoice: 'อนุมัติใบแจ้งหนี้',
|
||||
customerAcceptance: 'ลูกค้าตอบรับ',
|
||||
additionalFile: 'ไฟล์เอกสารเพิ่มเติม',
|
||||
|
||||
paymentCondition: 'เงื่อนไขการชำระเงิน',
|
||||
payType: 'วิธีการชำระเงิน',
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue