refactor: update app.vue and various components to enhance UI consistency, replace color classes for improved accessibility, and refine layout for better user experience

This commit is contained in:
Jennie Robinson Faber 2025-09-10 11:02:54 +01:00
parent 7b4fb6c2fd
commit 24e8b7a3a8
41 changed files with 2395 additions and 1603 deletions

View file

@ -3,10 +3,10 @@
<template #header>
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
<h3 class="text-lg font-semibold text-neutral-900 dark:text-white">
One-Off Transactions
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
<p class="text-sm text-neutral-600 dark:text-neutral-400 mt-1">
Add one-time income or expense transactions with expected dates.
</p>
</div>
@ -18,11 +18,13 @@
<!-- Empty state -->
<div v-if="sortedEvents.length === 0" class="text-center py-8">
<UIcon name="i-heroicons-banknotes" class="w-12 h-12 mx-auto text-gray-400 mb-4" />
<h4 class="text-lg font-medium text-gray-900 dark:text-white mb-2">
<UIcon
name="i-heroicons-banknotes"
class="w-12 h-12 mx-auto text-neutral-400 mb-4" />
<h4 class="text-lg font-medium text-neutral-900 dark:text-white mb-2">
No transactions yet
</h4>
<p class="text-gray-600 dark:text-gray-400 mb-4">
<p class="text-neutral-600 dark:text-neutral-400 mb-4">
Add one-off income or expense transactions.
</p>
<UButton @click="addEvent" color="primary">
@ -36,19 +38,26 @@
<div
v-for="monthGroup in eventsByMonth"
:key="monthGroup.month"
class="space-y-3"
>
class="space-y-3">
<!-- Month header -->
<div class="flex items-center justify-between py-2 border-b border-gray-200 dark:border-gray-700">
<h4 class="font-medium text-gray-900 dark:text-white">
<div
class="flex items-center justify-between py-2 border-b border-neutral-200 dark:border-neutral-700">
<h4 class="font-medium text-neutral-900 dark:text-white">
{{ monthGroup.monthName }}
</h4>
<div class="flex items-center gap-3">
<UBadge variant="subtle" color="gray">
{{ monthGroup.events.length }} transaction{{ monthGroup.events.length !== 1 ? 's' : '' }}
<UBadge variant="subtle" color="neutral">
{{ monthGroup.events.length }} transaction{{
monthGroup.events.length !== 1 ? "s" : ""
}}
</UBadge>
<div class="text-sm font-medium" :class="monthGroup.netAmount >= 0 ? 'text-green-600' : 'text-red-600'">
{{ monthGroup.netAmount >= 0 ? '+' : '' }}{{ formatCurrency(monthGroup.netAmount) }}
<div
class="text-sm font-medium"
:class="
monthGroup.netAmount >= 0 ? 'text-green-600' : 'text-red-600'
">
{{ monthGroup.netAmount >= 0 ? "+" : ""
}}{{ formatCurrency(monthGroup.netAmount) }}
</div>
</div>
</div>
@ -59,10 +68,15 @@
v-for="event in monthGroup.events"
:key="event.id"
:ui="{
background: event.type === 'income' ? 'bg-green-50 dark:bg-green-900/20' : 'bg-red-50 dark:bg-red-900/20',
ring: event.type === 'income' ? 'ring-green-200 dark:ring-green-800' : 'ring-red-200 dark:ring-red-800'
}"
>
background:
event.type === 'income'
? 'bg-green-50 dark:bg-green-900/20'
: 'bg-red-50 dark:bg-red-900/20',
ring:
event.type === 'income'
? 'ring-green-200 dark:ring-green-800'
: 'ring-red-200 dark:ring-red-800',
}">
<UForm :state="event" @submit="() => {}">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- Category -->
@ -70,8 +84,9 @@
<USelect
v-model="event.category"
:options="categoryOptions"
@update:model-value="updateEvent(event.id, { category: $event })"
/>
@update:model-value="
updateEvent(event.id, { category: $event })
" />
</UFormField>
<!-- Name -->
@ -79,8 +94,9 @@
<UInput
v-model="event.name"
placeholder="e.g., Equipment purchase"
@update:model-value="updateEvent(event.id, { name: $event })"
/>
@update:model-value="
updateEvent(event.id, { name: $event })
" />
</UFormField>
<!-- Type -->
@ -88,8 +104,9 @@
<USelect
v-model="event.type"
:options="typeOptions"
@update:model-value="updateEvent(event.id, { type: $event })"
/>
@update:model-value="
updateEvent(event.id, { type: $event })
" />
</UFormField>
<!-- Amount -->
@ -98,10 +115,11 @@
v-model="event.amount"
type="number"
placeholder="5000"
@update:model-value="updateEvent(event.id, { amount: Number($event) })"
>
@update:model-value="
updateEvent(event.id, { amount: Number($event) })
">
<template #leading>
<span class="text-gray-500">$</span>
<span class="text-neutral-500">$</span>
</template>
</UInput>
</UFormField>
@ -113,8 +131,9 @@
<UInput
v-model="event.dateExpected"
type="date"
@update:model-value="updateEventWithDate(event.id, $event)"
/>
@update:model-value="
updateEventWithDate(event.id, $event)
" />
</UFormField>
</div>
</UForm>
@ -126,12 +145,15 @@
color="red"
size="sm"
icon="i-heroicons-trash"
@click="removeEvent(event.id)"
>
@click="removeEvent(event.id)">
Delete
</UButton>
<UDropdown :items="getEventActions(event)">
<UButton variant="ghost" color="gray" size="sm" icon="i-heroicons-ellipsis-horizontal" />
<UButton
variant="ghost"
color="neutral"
size="sm"
icon="i-heroicons-ellipsis-horizontal" />
</UDropdown>
</div>
</template>
@ -142,11 +164,16 @@
<!-- Summary -->
<UCard>
<div class="flex items-center justify-between">
<span class="font-medium text-gray-900 dark:text-white">
Total {{ sortedEvents.length }} transaction{{ sortedEvents.length !== 1 ? 's' : '' }}
<span class="font-medium text-neutral-900 dark:text-white">
Total {{ sortedEvents.length }} transaction{{
sortedEvents.length !== 1 ? "s" : ""
}}
</span>
<span class="text-lg font-bold" :class="totalAnnualImpact >= 0 ? 'text-green-600' : 'text-red-600'">
{{ totalAnnualImpact >= 0 ? '+' : '' }}{{ formatCurrency(totalAnnualImpact) }}
<span
class="text-lg font-bold"
:class="totalAnnualImpact >= 0 ? 'text-green-600' : 'text-red-600'">
{{ totalAnnualImpact >= 0 ? "+" : ""
}}{{ formatCurrency(totalAnnualImpact) }}
</span>
</div>
</UCard>
@ -155,126 +182,146 @@
</template>
<script setup lang="ts">
import type { OneOffEvent } from '~/types/cash'
import type { OneOffEvent } from "~/types/cash";
const cashStore = useCashStore()
const cashStore = useCashStore();
// Constants
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December']
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const typeOptions = [
{ label: 'Income', value: 'income' },
{ label: 'Expense', value: 'expense' }
]
{ label: "Income", value: "income" },
{ label: "Expense", value: "expense" },
];
const categoryOptions = [
{ label: 'Equipment', value: 'Equipment' },
{ label: 'Marketing', value: 'Marketing' },
{ label: 'Legal', value: 'Legal' },
{ label: 'Contractors', value: 'Contractors' },
{ label: 'Office', value: 'Office' },
{ label: 'Development', value: 'Development' },
{ label: 'Other', value: 'Other' }
]
{ label: "Equipment", value: "Equipment" },
{ label: "Marketing", value: "Marketing" },
{ label: "Legal", value: "Legal" },
{ label: "Contractors", value: "Contractors" },
{ label: "Office", value: "Office" },
{ label: "Development", value: "Development" },
{ label: "Other", value: "Other" },
];
// Computed
const { oneOffEvents } = storeToRefs(cashStore)
const { oneOffEvents } = storeToRefs(cashStore);
const sortedEvents = computed(() => {
return oneOffEvents.value
.slice()
.sort((a, b) => a.month - b.month || a.name.localeCompare(b.name))
})
.sort((a, b) => a.month - b.month || a.name.localeCompare(b.name));
});
const eventsByMonth = computed(() => {
const groups: Record<number, OneOffEvent[]> = {}
sortedEvents.value.forEach(event => {
const groups: Record<number, OneOffEvent[]> = {};
sortedEvents.value.forEach((event) => {
if (!groups[event.month]) {
groups[event.month] = []
groups[event.month] = [];
}
groups[event.month].push(event)
})
return Object.entries(groups).map(([month, events]) => {
const monthNum = parseInt(month)
const netAmount = events.reduce((sum, event) => {
return sum + (event.type === 'income' ? event.amount : -event.amount)
}, 0)
return {
month: monthNum,
monthName: monthNames[monthNum],
events,
netAmount
}
}).sort((a, b) => a.month - b.month)
})
groups[event.month].push(event);
});
return Object.entries(groups)
.map(([month, events]) => {
const monthNum = parseInt(month);
const netAmount = events.reduce((sum, event) => {
return sum + (event.type === "income" ? event.amount : -event.amount);
}, 0);
return {
month: monthNum,
monthName: monthNames[monthNum],
events,
netAmount,
};
})
.sort((a, b) => a.month - b.month);
});
const totalIncome = computed(() => {
return oneOffEvents.value
.filter(e => e.type === 'income')
.reduce((sum, e) => sum + e.amount, 0)
})
.filter((e) => e.type === "income")
.reduce((sum, e) => sum + e.amount, 0);
});
const totalExpenses = computed(() => {
return oneOffEvents.value
.filter(e => e.type === 'expense')
.reduce((sum, e) => sum + e.amount, 0)
})
.filter((e) => e.type === "expense")
.reduce((sum, e) => sum + e.amount, 0);
});
const totalAnnualImpact = computed(() => totalIncome.value - totalExpenses.value)
const totalAnnualImpact = computed(
() => totalIncome.value - totalExpenses.value
);
// Methods
function addEvent() {
const currentMonth = new Date().getMonth()
const today = new Date().toISOString().split('T')[0]
const currentMonth = new Date().getMonth();
const today = new Date().toISOString().split("T")[0];
cashStore.addOneOffEvent({
name: '',
type: 'income',
name: "",
type: "income",
amount: 0,
category: 'Other',
dateExpected: today
})
category: "Other",
dateExpected: today,
});
}
function updateEvent(eventId: string, updates: Partial<OneOffEvent>) {
cashStore.updateOneOffEvent(eventId, updates)
cashStore.updateOneOffEvent(eventId, updates);
}
function updateEventWithDate(eventId: string, dateExpected: string) {
const eventDate = new Date(dateExpected)
const month = eventDate.getMonth()
cashStore.updateOneOffEvent(eventId, { dateExpected, month })
const eventDate = new Date(dateExpected);
const month = eventDate.getMonth();
cashStore.updateOneOffEvent(eventId, { dateExpected, month });
}
function removeEvent(eventId: string) {
cashStore.removeOneOffEvent(eventId)
cashStore.removeOneOffEvent(eventId);
}
function getEventActions(event: OneOffEvent) {
return [
[{
label: 'Move to Different Month',
icon: 'i-heroicons-arrow-right',
click: () => moveToMonth(event.id)
}],
[{
label: 'Duplicate Event',
icon: 'i-heroicons-document-duplicate',
click: () => duplicateEvent(event)
}]
]
[
{
label: "Move to Different Month",
icon: "i-heroicons-arrow-right",
click: () => moveToMonth(event.id),
},
],
[
{
label: "Duplicate Event",
icon: "i-heroicons-document-duplicate",
click: () => duplicateEvent(event),
},
],
];
}
function moveToMonth(eventId: string) {
// This could open a month selector modal
// For now, just move to next month
const event = oneOffEvents.value.find(e => e.id === eventId)
const event = oneOffEvents.value.find((e) => e.id === eventId);
if (event) {
const newMonth = (event.month + 1) % 12
updateEvent(eventId, { month: newMonth })
const newMonth = (event.month + 1) % 12;
updateEvent(eventId, { month: newMonth });
}
}
@ -284,22 +331,22 @@ function duplicateEvent(event: OneOffEvent) {
type: event.type,
amount: event.amount,
category: event.category,
dateExpected: event.dateExpected
})
dateExpected: event.dateExpected,
});
}
function getDateValue(dateExpected: string | undefined): string {
if (!dateExpected) {
return new Date().toISOString().split('T')[0]
return new Date().toISOString().split("T")[0];
}
return dateExpected
return dateExpected;
}
function formatCurrency(amount: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0
}).format(Math.abs(amount))
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(Math.abs(amount));
}
</script>
</script>