323 lines
7.6 KiB
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>
|