Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 10 additions & 28 deletions app/components/accounting/ExpenseFilters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const emit = defineEmits<{
const { t } = useI18n();

const categoryOptions = computed(() => [
{ value: '', label: t('common.all') },
{ value: 'all', label: t('common.all') },
...props.categories.map(c => ({ value: c.value, label: t(`accounting.expenses.expenseCategories.${c.value}`) }))
]);
</script>
Expand All @@ -35,39 +35,21 @@ const categoryOptions = computed(() => [
<UCard>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<UFormField :label="t('accounting.expenses.filterCategory')">
<USelect
:model-value="category"
:items="categoryOptions"
value-key="value"
label-key="label"
:placeholder="t('common.all')"
@update:model-value="emit('update:category', $event)"
/>
<USelectMenu :model-value="category" :items="categoryOptions" value-key="value" label-key="label" class="w-full"
:placeholder="t('common.all')" @update:model-value="emit('update:category', $event)" />
</UFormField>

<UFormField :label="t('accounting.expenses.startDate')">
<UInput
:model-value="startDate"
type="date"
@update:model-value="emit('update:startDate', $event)"
/>
<UInput :model-value="startDate" type="date" @update:model-value="emit('update:startDate', $event)" />
</UFormField>

<UFormField :label="t('accounting.expenses.endDate')">
<UInput
:model-value="endDate"
type="date"
@update:model-value="emit('update:endDate', $event)"
/>
<UInput :model-value="endDate" type="date" @update:model-value="emit('update:endDate', $event)" />
</UFormField>

<UFormField :label="t('accounting.expenses.search')">
<UInput
:model-value="search"
:placeholder="t('accounting.expenses.searchPlaceholder')"
icon="i-heroicons-magnifying-glass"
@update:model-value="emit('update:search', $event)"
/>
<UInput :model-value="search" :placeholder="t('accounting.expenses.searchPlaceholder')"
icon="i-heroicons-magnifying-glass" class="w-full" @update:model-value="emit('update:search', $event)" />
</UFormField>
</div>
</UCard>
Expand Down
63 changes: 14 additions & 49 deletions app/components/accounting/ExpenseFormModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const form = reactive<{
receipt: string;
}>({
amount: 0,
category: '',
category: 'other',
description: '',
date: new Date().toISOString().split('T')[0]!,
vendor: '',
Expand Down Expand Up @@ -78,7 +78,7 @@ watch(() => props.expense, (expense) => {

function resetForm() {
form.amount = 0;
form.category = '';
form.category = 'other';
form.description = '';
form.date = new Date().toISOString().split('T')[0]!;
form.vendor = '';
Expand Down Expand Up @@ -122,76 +122,45 @@ function handleSave() {
<template #body>
<div class="space-y-4">
<UFormField :label="t('accounting.expenses.amount')" required>
<UInput
v-model.number="form.amount"
type="number"
min="0"
step="0.01"
:placeholder="t('accounting.expenses.amountPlaceholder')"
/>
<UInput v-model.number="form.amount" type="number" min="0" step="0.01"
:placeholder="t('accounting.expenses.amountPlaceholder')" />
</UFormField>

<UFormField :label="t('accounting.expenses.category')" required>
<USelect
v-model="form.category"
<USelect v-model="form.category"
:items="categories.map(c => ({ value: c.value, label: t(`accounting.expenses.expenseCategories.${c.value}`) }))"
value-key="value"
label-key="label"
:placeholder="t('accounting.expenses.selectCategory')"
/>
value-key="value" label-key="label" :placeholder="t('accounting.expenses.selectCategory')" />
</UFormField>

<UFormField :label="t('accounting.expenses.description')" required>
<UInput
v-model="form.description"
:placeholder="t('accounting.expenses.descriptionPlaceholder')"
/>
<UInput v-model="form.description" :placeholder="t('accounting.expenses.descriptionPlaceholder')" />
</UFormField>

<UFormField :label="t('accounting.expenses.date')" required>
<UInput v-model="form.date" type="date" />
</UFormField>

<UFormField :label="t('accounting.expenses.vendor')">
<UInput
v-model="form.vendor"
:placeholder="t('accounting.expenses.vendorPlaceholder')"
/>
<UInput v-model="form.vendor" :placeholder="t('accounting.expenses.vendorPlaceholder')" />
</UFormField>

<UFormField :label="t('accounting.expenses.paymentMethod')">
<USelect
v-model="form.paymentMethod"
<USelect v-model="form.paymentMethod"
:items="paymentMethods.map(p => ({ value: p.value, label: t(`accounting.expenses.paymentMethods.${p.value}`) }))"
value-key="value"
label-key="label"
/>
value-key="value" label-key="label" />
</UFormField>

<UFormField :label="t('accounting.expenses.reference')">
<UInput
v-model="form.reference"
:placeholder="t('accounting.expenses.referencePlaceholder')"
/>
<UInput v-model="form.reference" :placeholder="t('accounting.expenses.referencePlaceholder')" />
</UFormField>

<UFormField :label="t('accounting.expenses.notes')">
<UTextarea
v-model="form.notes"
:placeholder="t('accounting.expenses.notesPlaceholder')"
:rows="2"
/>
<UTextarea v-model="form.notes" :placeholder="t('accounting.expenses.notesPlaceholder')" :rows="2" />
</UFormField>

<UFormField :label="t('accounting.expenses.receipt')">
<div class="border-2 border-dashed rounded-lg p-4 text-center">
<input
ref="receiptInputRef"
type="file"
accept="image/*,.pdf"
class="hidden"
@change="handleReceiptUpload"
>
<input ref="receiptInputRef" type="file" accept="image/*,.pdf" class="hidden" @change="handleReceiptUpload">
<div v-if="!form.receipt">
<UIcon name="i-heroicons-camera" class="text-2xl text-muted mb-2" />
<p class="text-sm text-muted mb-2">{{ t('accounting.expenses.uploadReceipt') }}</p>
Expand All @@ -214,11 +183,7 @@ function handleSave() {
<UButton variant="ghost" @click="isOpen = false">
{{ t('common.cancel') }}
</UButton>
<UButton
:loading="saving"
:disabled="!form.amount || !form.category || !form.description"
@click="handleSave"
>
<UButton :loading="saving" :disabled="!form.amount || !form.category || !form.description" @click="handleSave">
{{ expense?.id ? t('common.update') : t('common.create') }}
</UButton>
</div>
Expand Down
169 changes: 169 additions & 0 deletions app/components/accounting/IncomeFormModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<script setup lang="ts">
/**
* 💰 Income Form Modal Component
* For manual income entries (tips, deposits, refunds, etc.)
*/

interface IncomeCategory {
value: string;
label: string;
icon: string;
}

const props = defineProps<{
open: boolean;
saving: boolean;
}>();

const emit = defineEmits<{
'update:open': [value: boolean];
'save': [data: {
amount: number;
category: string;
description: string;
date: string;
source?: string;
paymentMethod: string;
reference?: string;
notes?: string;
}];
}>();

const { t } = useI18n();

const isOpen = computed({
get: () => props.open,
set: (val) => emit('update:open', val),
});

// Income categories
const INCOME_CATEGORIES: IncomeCategory[] = [
{ value: 'sales', label: 'Sales', icon: 'i-heroicons-shopping-cart' },
{ value: 'tips', label: 'Tips', icon: 'i-heroicons-hand-thumb-up' },
{ value: 'investment', label: 'Investment', icon: 'i-heroicons-chart-bar' },
{ value: 'refund', label: 'Refund Received', icon: 'i-heroicons-arrow-uturn-left' },
{ value: 'deposit', label: 'Deposit', icon: 'i-heroicons-banknotes' },
{ value: 'other', label: 'Other', icon: 'i-heroicons-document' },
];

const PAYMENT_METHODS = [
{ value: 'cash', label: 'Cash' },
{ value: 'bank_transfer', label: 'Bank Transfer' },
{ value: 'lightning', label: 'Lightning' },
{ value: 'check', label: 'Check' },
];

// Form state
const form = reactive({
amount: 0,
category: 'other',
description: '',
date: new Date().toISOString().split('T')[0]!,
source: '',
paymentMethod: 'cash',
reference: '',
notes: '',
});

function resetForm() {
form.amount = 0;
form.category = 'other';
form.description = '';
form.date = new Date().toISOString().split('T')[0]!;
form.source = '';
form.paymentMethod = 'cash';
form.reference = '';
form.notes = '';
}

function handleSave() {
emit('save', {
amount: form.amount,
category: form.category,
description: form.description,
date: form.date,
source: form.source || undefined,
paymentMethod: form.paymentMethod,
reference: form.reference || undefined,
notes: form.notes || undefined,
});
}

// Reset form when modal closes
watch(() => props.open, (open) => {
if (!open) {
resetForm();
}
});
</script>

<template>
<UModal v-model:open="isOpen">
<template #header>
<div class="flex items-center gap-3">
<div class="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
<UIcon name="i-heroicons-arrow-trending-up" class="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<h3 class="font-semibold">{{ t('accounting.income.addIncome') }}</h3>
</div>
</template>

<template #body>
<div class="space-y-4">
<UFormField :label="t('accounting.income.amount')" required>
<UInput v-model.number="form.amount" type="number" min="0" step="0.01"
:placeholder="t('accounting.income.amountPlaceholder')" />
</UFormField>

<UFormField :label="t('accounting.income.category')" required>
<USelectMenu v-model="form.category"
:items="INCOME_CATEGORIES.map(c => ({ value: c.value, label: t(`accounting.income.categories.${c.value}`) }))"
value-key="value" label-key="label" class="w-full" />
</UFormField>

<UFormField :label="t('accounting.income.description')" required>
<UInput v-model="form.description" :placeholder="t('accounting.income.descriptionPlaceholder')"
class="w-full" />
</UFormField>

<UFormField :label="t('accounting.income.date')" required>
<UInput v-model="form.date" type="date" />
</UFormField>

<UFormField :label="t('accounting.income.source')">
<UInput v-model="form.source" :placeholder="t('accounting.income.sourcePlaceholder')"
class="w-full" />
</UFormField>

<UFormField :label="t('accounting.income.paymentMethod')">
<USelectMenu v-model="form.paymentMethod"
:items="PAYMENT_METHODS.map(p => ({ value: p.value, label: t(`accounting.expenses.paymentMethods.${p.value}`) }))"
value-key="value" label-key="label" class="w-full" />
</UFormField>

<UFormField :label="t('accounting.income.reference')">
<UInput v-model="form.reference" :placeholder="t('accounting.income.referencePlaceholder')"
class="w-full" />
</UFormField>

<UFormField :label="t('accounting.income.notes')">
<UTextarea v-model="form.notes" :placeholder="t('accounting.income.notesPlaceholder')" :rows="2"
class="w-full" />
</UFormField>
</div>
</template>

<template #footer>
<div class="flex justify-end w-full gap-2">
<UButton variant="ghost" block @click="isOpen = false">
{{ t('common.cancel') }}
</UButton>
<UButton color="success" block :loading="saving"
:disabled="!form.amount || !form.category || !form.description" @click="handleSave">
<UIcon name="i-heroicons-plus" class="w-4 h-4 mr-1" />
{{ t('accounting.income.addIncome') }}
</UButton>
</div>
</template>
</UModal>
</template>
Loading