271 lines
6.2 KiB
Vue
271 lines
6.2 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-plus</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="handleSubmit">
|
|
<v-row>
|
|
<!-- Establishment Name -->
|
|
<v-col cols="12">
|
|
<v-text-field
|
|
v-model="expense.establishmentName"
|
|
label="Establishment Name"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
required
|
|
placeholder="e.g., Shell, American Airlines, etc."
|
|
/>
|
|
</v-col>
|
|
|
|
<!-- Price -->
|
|
<v-col cols="12" sm="6">
|
|
<v-text-field
|
|
v-model="expense.price"
|
|
label="Price"
|
|
variant="outlined"
|
|
:rules="[rules.required, rules.price]"
|
|
required
|
|
placeholder="e.g., 59.95"
|
|
prepend-inner-icon="mdi-currency-eur"
|
|
/>
|
|
</v-col>
|
|
|
|
<!-- Category -->
|
|
<v-col cols="12" sm="6">
|
|
<v-select
|
|
v-model="expense.category"
|
|
:items="categories"
|
|
label="Category"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
required
|
|
/>
|
|
</v-col>
|
|
|
|
<!-- Payer -->
|
|
<v-col cols="12" sm="6">
|
|
<v-text-field
|
|
v-model="expense.payer"
|
|
label="Payer"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
required
|
|
placeholder="e.g., John, Mary, etc."
|
|
/>
|
|
</v-col>
|
|
|
|
<!-- Payment Method -->
|
|
<v-col cols="12" sm="6">
|
|
<v-select
|
|
v-model="expense.paymentMethod"
|
|
:items="paymentMethods"
|
|
label="Payment Method"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
required
|
|
/>
|
|
</v-col>
|
|
|
|
<!-- Date -->
|
|
<v-col cols="12">
|
|
<v-text-field
|
|
v-model="expense.date"
|
|
type="date"
|
|
label="Date"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
required
|
|
/>
|
|
</v-col>
|
|
|
|
<!-- Contents/Description -->
|
|
<v-col cols="12">
|
|
<v-textarea
|
|
v-model="expense.contents"
|
|
label="Description (optional)"
|
|
variant="outlined"
|
|
rows="3"
|
|
placeholder="Additional details about the expense..."
|
|
/>
|
|
</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="creating"
|
|
>
|
|
Cancel
|
|
</v-btn>
|
|
|
|
<v-btn
|
|
@click="handleSubmit"
|
|
:disabled="creating"
|
|
color="primary"
|
|
:loading="creating"
|
|
>
|
|
<v-icon v-if="!creating" class="mr-1">mdi-plus</v-icon>
|
|
Create Expense
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue';
|
|
|
|
// Props
|
|
interface Props {
|
|
modelValue: boolean;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
|
|
// Emits
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean];
|
|
'created': [expense: any];
|
|
}>();
|
|
|
|
// Computed dialog model
|
|
const dialog = computed({
|
|
get: () => props.modelValue,
|
|
set: (value) => emit('update:modelValue', value)
|
|
});
|
|
|
|
// Reactive state
|
|
const form = ref();
|
|
const creating = ref(false);
|
|
|
|
const expense = ref({
|
|
establishmentName: '',
|
|
price: '',
|
|
category: '',
|
|
payer: '',
|
|
paymentMethod: '',
|
|
date: '',
|
|
contents: ''
|
|
});
|
|
|
|
// Form options
|
|
const categories = [
|
|
'Food/Drinks',
|
|
'Shop',
|
|
'Online',
|
|
'Other'
|
|
];
|
|
|
|
const paymentMethods = [
|
|
'Card',
|
|
'Cash',
|
|
'N/A'
|
|
];
|
|
|
|
// Validation rules
|
|
const rules = {
|
|
required: (value: string) => !!value || 'This field is required',
|
|
price: (value: string) => {
|
|
if (!value) return 'Price is required';
|
|
const num = parseFloat(value);
|
|
if (isNaN(num) || num <= 0) return 'Please enter a valid price';
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// Methods
|
|
const closeModal = () => {
|
|
dialog.value = false;
|
|
resetForm();
|
|
};
|
|
|
|
const resetForm = () => {
|
|
expense.value = {
|
|
establishmentName: '',
|
|
price: '',
|
|
category: '',
|
|
payer: '',
|
|
paymentMethod: '',
|
|
date: '',
|
|
contents: ''
|
|
};
|
|
|
|
if (form.value) {
|
|
form.value.resetValidation();
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!form.value) return;
|
|
|
|
const { valid } = await form.value.validate();
|
|
if (!valid) return;
|
|
|
|
creating.value = true;
|
|
|
|
try {
|
|
// Create expense via API
|
|
const response = await $fetch<{
|
|
success: boolean;
|
|
data?: any;
|
|
message?: string;
|
|
}>('/api/create-expense', {
|
|
method: 'POST',
|
|
body: {
|
|
'Establishment Name': expense.value.establishmentName,
|
|
'Price': expense.value.price,
|
|
'Category': expense.value.category,
|
|
'Payer': expense.value.payer,
|
|
'Payment Method': expense.value.paymentMethod,
|
|
'Time': expense.value.date,
|
|
'Contents': expense.value.contents
|
|
}
|
|
});
|
|
|
|
if (response.success) {
|
|
emit('created', response.data);
|
|
closeModal();
|
|
}
|
|
|
|
} catch (error: any) {
|
|
console.error('[ExpenseCreateModal] Error creating expense:', error);
|
|
// Handle error display here if needed
|
|
} finally {
|
|
creating.value = false;
|
|
}
|
|
};
|
|
|
|
// Watch for modal open to set default date
|
|
watch(dialog, (isOpen) => {
|
|
if (isOpen && !expense.value.date) {
|
|
expense.value.date = new Date().toISOString().slice(0, 10);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.v-card-actions {
|
|
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
}
|
|
</style>
|