309 lines
9.1 KiB
Vue
309 lines
9.1 KiB
Vue
<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>
|
||
|
||
<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>
|
||
</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;
|
||
includeProcessingFee: boolean;
|
||
pageFormat: 'A4' | 'Letter' | 'Legal';
|
||
}
|
||
|
||
// Reactive state
|
||
const generating = ref(false);
|
||
|
||
const options = ref<PDFOptions>({
|
||
documentName: '',
|
||
subheader: '',
|
||
groupBy: 'payer',
|
||
includeReceipts: true,
|
||
includeSummary: true,
|
||
includeDetails: true,
|
||
includeProcessingFee: true,
|
||
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>
|