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

323 lines
7.6 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-receipt-text</v-icon>
<span>Add New Expense</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="saveExpense">
<v-row>
<!-- Merchant/Description -->
<v-col cols="12">
<v-text-field
v-model="expense.merchant"
label="Merchant/Description"
variant="outlined"
:rules="[rules.required]"
required
/>
</v-col>
<!-- Amount and Currency -->
<v-col cols="8">
<v-text-field
v-model="expense.amount"
label="Amount"
type="number"
step="0.01"
variant="outlined"
:rules="[rules.required, rules.positive]"
required
/>
</v-col>
<v-col cols="4">
<v-select
v-model="expense.currency"
:items="currencies"
label="Currency"
variant="outlined"
:rules="[rules.required]"
required
/>
</v-col>
<!-- Category -->
<v-col cols="12" md="6">
<v-select
v-model="expense.category"
:items="categories"
label="Category"
variant="outlined"
:rules="[rules.required]"
required
/>
</v-col>
<!-- Payer -->
<v-col cols="12" md="6">
<v-text-field
v-model="expense.payer"
label="Payer"
variant="outlined"
:rules="[rules.required]"
required
/>
</v-col>
<!-- Date -->
<v-col cols="12" md="6">
<v-text-field
v-model="expense.date"
label="Date"
type="date"
variant="outlined"
:rules="[rules.required]"
required
/>
</v-col>
<!-- Time -->
<v-col cols="12" md="6">
<v-text-field
v-model="expense.time"
label="Time"
type="time"
variant="outlined"
:rules="[rules.required]"
required
/>
</v-col>
<!-- Notes -->
<v-col cols="12">
<v-textarea
v-model="expense.notes"
label="Notes (Optional)"
variant="outlined"
rows="3"
auto-grow
/>
</v-col>
<!-- Receipt Upload -->
<v-col cols="12">
<v-file-input
v-model="expense.receipt"
label="Receipt Image (Optional)"
accept="image/*"
variant="outlined"
prepend-icon="mdi-camera"
show-size
/>
</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="saving"
>
Cancel
</v-btn>
<v-btn
@click="saveExpense"
color="primary"
:loading="saving"
:disabled="!isValid"
>
Add Expense
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue';
import type { Expense } from '@/utils/types';
interface Props {
modelValue: boolean;
}
interface Emits {
(e: 'update:modelValue', value: boolean): void;
(e: 'created', expense: Expense): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const dialog = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
});
const form = ref();
const saving = ref(false);
// Form data
const expense = ref({
merchant: '',
amount: '',
currency: 'EUR',
category: '',
payer: '',
date: '',
time: '',
notes: '',
receipt: null as File[] | null
});
// Form options
const currencies = ['EUR', 'USD', 'GBP', 'AUD', 'CAD', 'CHF', 'SEK', 'NOK', 'DKK'];
const categories = ['Food/Drinks', 'Shop', 'Online', 'Transportation', 'Accommodation', 'Entertainment', 'Other'];
// Validation rules
const rules = {
required: (value: any) => !!value || 'This field is required',
positive: (value: any) => {
const num = parseFloat(value);
return (!isNaN(num) && num > 0) || 'Amount must be positive';
}
};
// Computed properties
const isValid = computed(() => {
return !!(
expense.value.merchant &&
expense.value.amount &&
expense.value.currency &&
expense.value.category &&
expense.value.payer &&
expense.value.date &&
expense.value.time &&
parseFloat(expense.value.amount) > 0
);
});
// Methods
const resetForm = () => {
const now = new Date();
expense.value = {
merchant: '',
amount: '',
currency: 'EUR',
category: '',
payer: '',
date: now.toISOString().slice(0, 10),
time: now.toTimeString().slice(0, 5),
notes: '',
receipt: null
};
if (form.value) {
form.value.resetValidation();
}
};
const closeModal = () => {
if (!saving.value) {
dialog.value = false;
}
};
const saveExpense = async () => {
if (!form.value) return;
const { valid } = await form.value.validate();
if (!valid) return;
saving.value = true;
try {
// Combine date and time for the API
const dateTime = `${expense.value.date}T${expense.value.time}:00`;
// Prepare the expense data
const expenseData = {
"Establishment Name": expense.value.merchant,
Price: `${expense.value.currency}${expense.value.amount}`,
Category: expense.value.category,
Payer: expense.value.payer,
Time: dateTime,
Contents: expense.value.notes || null,
"Payment Method": "Card", // Default to Card for now
Paid: false,
currency: expense.value.currency
};
console.log('[ExpenseCreateModal] Creating expense:', expenseData);
// Call API to create expense
const response = await $fetch<Expense>('/api/create-expense', {
method: 'POST',
body: expenseData
});
console.log('[ExpenseCreateModal] Expense created successfully:', response);
// Emit the created event
emit('created', response);
// Close the modal
dialog.value = false;
} catch (error: any) {
console.error('[ExpenseCreateModal] Error creating expense:', error);
// Show error message (you might want to use a toast notification here)
alert('Failed to create expense. Please try again.');
} finally {
saving.value = false;
}
};
// Watch for modal open/close
watch(dialog, (newValue) => {
if (newValue) {
// Reset form when modal opens
nextTick(() => {
resetForm();
});
}
});
// Initialize form with current date/time
onMounted(() => {
resetForm();
});
</script>
<style scoped>
.v-dialog > .v-card {
overflow: visible;
}
.v-form {
width: 100%;
}
.v-card-actions {
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
</style>