2025-07-03 21:29:42 +02:00
|
|
|
|
<template>
|
|
|
|
|
|
<div v-if="modelValue" class="modal-overlay" @click.self="closeModal">
|
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<h3 class="modal-title">Configure PDF Export</h3>
|
|
|
|
|
|
<button @click="closeModal" class="btn-close">
|
|
|
|
|
|
<Icon name="mdi:close" class="w-5 h-5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<form @submit.prevent="handleGenerate" class="modal-body">
|
|
|
|
|
|
<!-- Document Name -->
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="label">
|
|
|
|
|
|
<span class="label-text">Document Name <span class="text-red-500">*</span></span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="options.documentName"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="e.g., May 2025 Expenses"
|
|
|
|
|
|
class="input input-bordered w-full"
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Subheader -->
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="label">
|
|
|
|
|
|
<span class="label-text">Subheader (optional)</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="options.subheader"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="e.g., Port Nimara Business Trip"
|
|
|
|
|
|
class="input input-bordered w-full"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Grouping Options -->
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="label">
|
|
|
|
|
|
<span class="label-text">Group Expenses By</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select v-model="options.groupBy" class="select select-bordered w-full">
|
|
|
|
|
|
<option value="none">No Grouping</option>
|
|
|
|
|
|
<option value="payer">Group by Person</option>
|
|
|
|
|
|
<option value="category">Group by Category</option>
|
|
|
|
|
|
<option value="date">Group by Date</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Include Options -->
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="label">
|
|
|
|
|
|
<span class="label-text">Include in PDF</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="options.includeReceipts"
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
class="checkbox checkbox-primary"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="font-medium">Include Receipt Images</div>
|
|
|
|
|
|
<div class="text-sm text-gray-500">Attach receipt photos to the PDF document</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="options.includeSummary"
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
class="checkbox checkbox-primary"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="font-medium">Include Summary</div>
|
|
|
|
|
|
<div class="text-sm text-gray-500">Add totals and breakdown at the end</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="options.includeDetails"
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
class="checkbox checkbox-primary"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="font-medium">Include Expense Details</div>
|
|
|
|
|
|
<div class="text-sm text-gray-500">Show establishment name, date, description</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</label>
|
2025-07-04 15:27:43 +02:00
|
|
|
|
|
|
|
|
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="options.includeProcessingFee"
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
class="checkbox checkbox-primary"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="font-medium">Include Processing Fee</div>
|
|
|
|
|
|
<div class="text-sm text-gray-500">Add 5% processing fee to totals</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</label>
|
2025-07-03 21:29:42 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Page Format -->
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="label">
|
|
|
|
|
|
<span class="label-text">Page Format</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select v-model="options.pageFormat" class="select select-bordered w-full">
|
|
|
|
|
|
<option value="A4">A4 (210 × 297 mm)</option>
|
|
|
|
|
|
<option value="Letter">Letter (8.5 × 11 in)</option>
|
|
|
|
|
|
<option value="Legal">Legal (8.5 × 14 in)</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Preview Info -->
|
|
|
|
|
|
<div class="bg-blue-50 dark:bg-blue-950 rounded-lg p-4">
|
|
|
|
|
|
<div class="flex items-start gap-3">
|
|
|
|
|
|
<Icon name="mdi:information" class="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" />
|
|
|
|
|
|
<div class="text-sm">
|
|
|
|
|
|
<div class="font-medium text-blue-900 dark:text-blue-100 mb-1">
|
|
|
|
|
|
PDF Preview
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-blue-800 dark:text-blue-200">
|
|
|
|
|
|
<div>Selected expenses: <strong>{{ selectedExpenses.length }}</strong></div>
|
|
|
|
|
|
<div>Total amount: <strong>€{{ totalAmount.toFixed(2) }}</strong></div>
|
|
|
|
|
|
<div v-if="options.groupBy !== 'none'">
|
|
|
|
|
|
Grouped by: <strong>{{ groupByLabel }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="closeModal"
|
|
|
|
|
|
class="btn btn-ghost"
|
|
|
|
|
|
>
|
|
|
|
|
|
Cancel
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
@click="handleGenerate"
|
|
|
|
|
|
:disabled="!options.documentName || generating"
|
|
|
|
|
|
class="btn btn-primary"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span v-if="generating" class="loading loading-spinner loading-sm"></span>
|
|
|
|
|
|
<Icon v-else name="mdi:file-pdf" class="w-4 h-4" />
|
|
|
|
|
|
{{ generating ? 'Generating...' : 'Generate PDF' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, computed, watch } from 'vue';
|
|
|
|
|
|
|
|
|
|
|
|
// Props
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
modelValue: boolean;
|
|
|
|
|
|
selectedExpenses: number[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
includeSummary: boolean;
|
|
|
|
|
|
includeDetails: boolean;
|
2025-07-04 15:27:43 +02:00
|
|
|
|
includeProcessingFee: boolean;
|
2025-07-03 21:29:42 +02:00
|
|
|
|
pageFormat: 'A4' | 'Letter' | 'Legal';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Reactive state
|
|
|
|
|
|
const generating = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
const options = ref<PDFOptions>({
|
|
|
|
|
|
documentName: '',
|
|
|
|
|
|
subheader: '',
|
|
|
|
|
|
groupBy: 'payer',
|
|
|
|
|
|
includeReceipts: true,
|
|
|
|
|
|
includeSummary: true,
|
|
|
|
|
|
includeDetails: true,
|
2025-07-04 15:27:43 +02:00
|
|
|
|
includeProcessingFee: true,
|
2025-07-03 21:29:42 +02:00
|
|
|
|
pageFormat: 'A4'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Computed
|
|
|
|
|
|
const totalAmount = computed(() => {
|
|
|
|
|
|
// This would ideally come from the parent component
|
|
|
|
|
|
// For now, we'll use a placeholder
|
|
|
|
|
|
return props.selectedExpenses.length * 25; // Rough estimate
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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 = () => {
|
|
|
|
|
|
emit('update:modelValue', false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleGenerate = async () => {
|
|
|
|
|
|
if (!options.value.documentName) return;
|
|
|
|
|
|
|
|
|
|
|
|
generating.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
emit('generate', { ...options.value });
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[PDFOptionsModal] Error generating PDF:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
generating.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Watch for modal open to set default document name
|
|
|
|
|
|
watch(() => props.modelValue, (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>
|
|
|
|
|
|
.modal-overlay {
|
|
|
|
|
|
@apply fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-content {
|
|
|
|
|
|
@apply bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-header {
|
|
|
|
|
|
@apply flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-title {
|
|
|
|
|
|
@apply text-lg font-semibold text-gray-900 dark:text-white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-close {
|
|
|
|
|
|
@apply p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-body {
|
|
|
|
|
|
@apply p-6 space-y-4 overflow-y-auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group {
|
|
|
|
|
|
@apply space-y-2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.label-text {
|
|
|
|
|
|
@apply text-sm font-medium text-gray-700 dark:text-gray-300;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-footer {
|
|
|
|
|
|
@apply flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn {
|
|
|
|
|
|
@apply inline-flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-ghost {
|
|
|
|
|
|
@apply text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-primary {
|
|
|
|
|
|
@apply bg-primary text-white hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.input, .select {
|
|
|
|
|
|
@apply block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkbox {
|
|
|
|
|
|
@apply rounded border-gray-300 dark:border-gray-600 text-primary focus:ring-primary;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|