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

309 lines
9.1 KiB
Vue
Raw Normal View History

<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 mx-4 sm:mx-0;
}
.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>