343 lines
9.8 KiB
Vue
343 lines
9.8 KiB
Vue
<template>
|
||
<v-dialog
|
||
v-model="dialog"
|
||
max-width="600"
|
||
persistent
|
||
scrollable
|
||
>
|
||
<v-card>
|
||
<v-card-title class="d-flex align-center">
|
||
<v-icon class="mr-2">mdi-file-pdf</v-icon>
|
||
<span>Configure PDF Export</span>
|
||
<v-spacer />
|
||
<v-btn
|
||
@click="closeModal"
|
||
icon="mdi-close"
|
||
variant="text"
|
||
size="small"
|
||
/>
|
||
</v-card-title>
|
||
|
||
<v-card-text>
|
||
<v-form ref="form" @submit.prevent="handleGenerate">
|
||
<v-row>
|
||
<!-- Document Name -->
|
||
<v-col cols="12">
|
||
<v-text-field
|
||
v-model="options.documentName"
|
||
label="Document Name"
|
||
variant="outlined"
|
||
:rules="[rules.required]"
|
||
required
|
||
placeholder="e.g., May 2025 Expenses"
|
||
/>
|
||
</v-col>
|
||
|
||
<!-- Subheader -->
|
||
<v-col cols="12">
|
||
<v-text-field
|
||
v-model="options.subheader"
|
||
label="Subheader (optional)"
|
||
variant="outlined"
|
||
placeholder="e.g., Port Nimara Business Trip"
|
||
/>
|
||
</v-col>
|
||
|
||
<!-- Grouping Options -->
|
||
<v-col cols="12">
|
||
<v-select
|
||
v-model="options.groupBy"
|
||
:items="groupByOptions"
|
||
label="Group Expenses By"
|
||
variant="outlined"
|
||
item-title="text"
|
||
item-value="value"
|
||
/>
|
||
</v-col>
|
||
|
||
<!-- Include Options -->
|
||
<v-col cols="12">
|
||
<v-card variant="tonal" class="pa-4">
|
||
<v-card-subtitle class="px-0 pb-2">
|
||
<v-icon class="mr-2">mdi-checkbox-marked-circle</v-icon>
|
||
Include in PDF
|
||
</v-card-subtitle>
|
||
|
||
<div class="include-options">
|
||
<v-checkbox
|
||
v-model="options.includeReceipts"
|
||
color="primary"
|
||
hide-details
|
||
>
|
||
<template #label>
|
||
<div>
|
||
<div class="font-weight-medium">Include Receipt Images</div>
|
||
<div class="text-caption text-grey-darken-1">Attach receipt photos to the PDF document</div>
|
||
</div>
|
||
</template>
|
||
</v-checkbox>
|
||
|
||
<v-checkbox
|
||
v-model="options.includeSummary"
|
||
color="primary"
|
||
hide-details
|
||
>
|
||
<template #label>
|
||
<div>
|
||
<div class="font-weight-medium">Include Summary</div>
|
||
<div class="text-caption text-grey-darken-1">Add totals and breakdown at the end</div>
|
||
</div>
|
||
</template>
|
||
</v-checkbox>
|
||
|
||
<v-checkbox
|
||
v-model="options.includeDetails"
|
||
color="primary"
|
||
hide-details
|
||
>
|
||
<template #label>
|
||
<div>
|
||
<div class="font-weight-medium">Include Expense Details</div>
|
||
<div class="text-caption text-grey-darken-1">Show establishment name, date, description</div>
|
||
</div>
|
||
</template>
|
||
</v-checkbox>
|
||
|
||
<v-checkbox
|
||
v-model="options.includeReceiptContents"
|
||
color="primary"
|
||
hide-details
|
||
>
|
||
<template #label>
|
||
<div>
|
||
<div class="font-weight-medium">Include Receipt Contents</div>
|
||
<div class="text-caption text-grey-darken-1">Show receipt description/contents in detail table</div>
|
||
</div>
|
||
</template>
|
||
</v-checkbox>
|
||
|
||
<v-checkbox
|
||
v-model="options.includeProcessingFee"
|
||
color="primary"
|
||
hide-details
|
||
>
|
||
<template #label>
|
||
<div>
|
||
<div class="font-weight-medium">Include Processing Fee</div>
|
||
<div class="text-caption text-grey-darken-1">Add 5% processing fee to totals</div>
|
||
</div>
|
||
</template>
|
||
</v-checkbox>
|
||
</div>
|
||
</v-card>
|
||
</v-col>
|
||
|
||
<!-- Page Format -->
|
||
<v-col cols="12">
|
||
<v-select
|
||
v-model="options.pageFormat"
|
||
:items="pageFormatOptions"
|
||
label="Page Format"
|
||
variant="outlined"
|
||
item-title="text"
|
||
item-value="value"
|
||
/>
|
||
</v-col>
|
||
|
||
<!-- Preview Info -->
|
||
<v-col cols="12">
|
||
<v-alert
|
||
type="info"
|
||
variant="tonal"
|
||
class="mb-0"
|
||
>
|
||
<template #prepend>
|
||
<v-icon>mdi-information</v-icon>
|
||
</template>
|
||
|
||
<div class="font-weight-medium mb-2">PDF Preview</div>
|
||
<div class="text-body-2">
|
||
<div><strong>Selected expenses:</strong> {{ selectedExpenses.length }}</div>
|
||
<div><strong>Total amount:</strong> €{{ totalAmount.toFixed(2) }}</div>
|
||
<div v-if="options.groupBy !== 'none'">
|
||
<strong>Grouped by:</strong> {{ groupByLabel }}
|
||
</div>
|
||
</div>
|
||
</v-alert>
|
||
</v-col>
|
||
</v-row>
|
||
</v-form>
|
||
</v-card-text>
|
||
|
||
<v-card-actions class="px-6 pb-4">
|
||
<v-spacer />
|
||
<v-btn
|
||
@click="closeModal"
|
||
variant="text"
|
||
:disabled="generating"
|
||
>
|
||
Cancel
|
||
</v-btn>
|
||
|
||
<v-btn
|
||
@click="handleGenerate"
|
||
:disabled="!options.documentName || generating"
|
||
color="primary"
|
||
:loading="generating"
|
||
>
|
||
<v-icon v-if="!generating" class="mr-1">mdi-file-pdf</v-icon>
|
||
Generate PDF
|
||
</v-btn>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch } from 'vue';
|
||
|
||
// Props
|
||
interface Props {
|
||
modelValue: boolean;
|
||
selectedExpenses: number[];
|
||
expenses: any[]; // Add expenses array to calculate real totals
|
||
}
|
||
|
||
const props = defineProps<Props>();
|
||
|
||
// Emits
|
||
const emit = defineEmits<{
|
||
'update:modelValue': [value: boolean];
|
||
'generate': [options: PDFOptions];
|
||
}>();
|
||
|
||
// PDF Options interface
|
||
interface PDFOptions {
|
||
documentName: string;
|
||
subheader: string;
|
||
groupBy: 'none' | 'payer' | 'category' | 'date';
|
||
includeReceipts: boolean;
|
||
includeReceiptContents: boolean;
|
||
includeSummary: boolean;
|
||
includeDetails: boolean;
|
||
includeProcessingFee: boolean;
|
||
pageFormat: 'A4' | 'Letter' | 'Legal';
|
||
}
|
||
|
||
// Computed dialog model
|
||
const dialog = computed({
|
||
get: () => props.modelValue,
|
||
set: (value) => emit('update:modelValue', value)
|
||
});
|
||
|
||
// Reactive state
|
||
const form = ref();
|
||
const generating = ref(false);
|
||
|
||
const options = ref<PDFOptions>({
|
||
documentName: '',
|
||
subheader: '',
|
||
groupBy: 'payer',
|
||
includeReceipts: true,
|
||
includeReceiptContents: true,
|
||
includeSummary: true,
|
||
includeDetails: true,
|
||
includeProcessingFee: true,
|
||
pageFormat: 'A4'
|
||
});
|
||
|
||
// Form options
|
||
const groupByOptions = [
|
||
{ text: 'No Grouping', value: 'none' },
|
||
{ text: 'Group by Person', value: 'payer' },
|
||
{ text: 'Group by Category', value: 'category' },
|
||
{ text: 'Group by Date', value: 'date' }
|
||
];
|
||
|
||
const pageFormatOptions = [
|
||
{ text: 'A4 (210 × 297 mm)', value: 'A4' },
|
||
{ text: 'Letter (8.5 × 11 in)', value: 'Letter' },
|
||
{ text: 'Legal (8.5 × 14 in)', value: 'Legal' }
|
||
];
|
||
|
||
// Validation rules
|
||
const rules = {
|
||
required: (value: string) => !!value || 'This field is required'
|
||
};
|
||
|
||
// Computed
|
||
const totalAmount = computed(() => {
|
||
// Calculate actual total from selected expenses
|
||
if (!props.expenses || !props.selectedExpenses.length) return 0;
|
||
|
||
return props.expenses
|
||
.filter(expense => props.selectedExpenses.includes(expense.Id))
|
||
.reduce((total, expense) => {
|
||
const amount = expense.PriceNumber || parseFloat(expense.Price?.toString().replace(/[^\d.-]/g, '')) || 0;
|
||
return total + amount;
|
||
}, 0);
|
||
});
|
||
|
||
const groupByLabel = computed(() => {
|
||
switch (options.value.groupBy) {
|
||
case 'payer': return 'Person';
|
||
case 'category': return 'Category';
|
||
case 'date': return 'Date';
|
||
default: return 'None';
|
||
}
|
||
});
|
||
|
||
// Methods
|
||
const closeModal = () => {
|
||
dialog.value = false;
|
||
};
|
||
|
||
const handleGenerate = async () => {
|
||
if (!form.value) return;
|
||
|
||
const { valid } = await form.value.validate();
|
||
if (!valid) return;
|
||
|
||
generating.value = true;
|
||
|
||
try {
|
||
emit('generate', { ...options.value });
|
||
// Close modal on successful generation
|
||
dialog.value = false;
|
||
} catch (error) {
|
||
console.error('[PDFOptionsModal] Error generating PDF:', error);
|
||
} finally {
|
||
generating.value = false;
|
||
}
|
||
};
|
||
|
||
// Watch for modal open to set default document name
|
||
watch(dialog, (isOpen) => {
|
||
if (isOpen && !options.value.documentName) {
|
||
const now = new Date();
|
||
const monthName = now.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||
options.value.documentName = `${monthName} Expenses`;
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.include-options {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.include-options :deep(.v-checkbox) {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.include-options :deep(.v-checkbox .v-selection-control__wrapper) {
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.v-card-actions {
|
||
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||
}
|
||
</style>
|