port-nimara-client-portal/components/PDFOptionsModal.vue

363 lines
10 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
<!-- Currency Selection -->
<v-col cols="12" md="6">
<v-select
v-model="options.targetCurrency"
:items="currencyOptions"
label="Export Currency"
variant="outlined"
item-title="text"
item-value="value"
prepend-inner-icon="mdi-currency-usd"
/>
</v-col>
<!-- Page Format -->
<v-col cols="12" md="6">
<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';
targetCurrency: 'USD' | 'EUR';
}
// 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',
targetCurrency: 'EUR'
});
// 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' }
];
const currencyOptions = [
{ text: 'Euro (EUR)', value: 'EUR' },
{ text: 'US Dollar (USD)', value: 'USD' }
];
// 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>