Replace teal-brand→primary, teal-dark→primary-dark, teal-light→primary-light, gray-soft→gray-200, gray-brand→gray-900 across 53 files. Fixes theming not applying — colors now use CSS custom properties from org_config. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
118 lines
4.9 KiB
Svelte
118 lines
4.9 KiB
Svelte
<script lang="ts">
|
|
import { goto } from '$app/navigation';
|
|
import { page } from '$app/state';
|
|
import Pagination from '$lib/components/ui/Pagination.svelte';
|
|
import DataTable from '$lib/components/admin/DataTable.svelte';
|
|
|
|
let { data } = $props();
|
|
|
|
const columns = [
|
|
{ key: 'description', label: 'Description' },
|
|
{ key: 'amount', label: 'Amount' },
|
|
{ key: 'category', label: 'Category' },
|
|
{ key: 'petName', label: 'Pet' },
|
|
{ key: 'vetName', label: 'Vet' },
|
|
{ key: 'date', label: 'Date' },
|
|
{ key: 'actions', label: '' }
|
|
];
|
|
|
|
const fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
|
|
|
|
function handleFilterChange(param: string, value: string) {
|
|
const url = new URL(page.url);
|
|
if (value) { url.searchParams.set(param, value); } else { url.searchParams.delete(param); }
|
|
url.searchParams.delete('page');
|
|
goto(url.pathname + url.search);
|
|
}
|
|
|
|
function buildBaseUrl(): string {
|
|
const url = new URL(page.url);
|
|
return url.pathname + '?' + url.searchParams.toString();
|
|
}
|
|
|
|
function formatDate(d: string | Date): string {
|
|
return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
}
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Expenses — Admin</title>
|
|
</svelte:head>
|
|
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h1 class="text-2xl font-bold text-gray-900">Expenses</h1>
|
|
<a href="/admin/expenses/new" class="inline-flex items-center gap-2 px-4 py-2.5 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-dark transition-colors">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
Add Expense
|
|
</a>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
|
<p class="text-sm text-gray-500">This Month</p>
|
|
<p class="text-2xl font-bold text-gray-900 mt-1">{fmt.format((data.monthTotal ?? 0) / 100)}</p>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
|
<p class="text-sm text-gray-500">This Year</p>
|
|
<p class="text-2xl font-bold text-gray-900 mt-1">{fmt.format((data.yearTotal ?? 0) / 100)}</p>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
|
<p class="text-sm text-gray-500">Filtered Total</p>
|
|
<p class="text-2xl font-bold text-gray-900 mt-1">{fmt.format((data.filteredTotal ?? 0) / 100)}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-6">
|
|
<div class="flex flex-col lg:flex-row gap-4">
|
|
<div class="flex-1">
|
|
<input
|
|
type="text"
|
|
placeholder="Filter by category..."
|
|
value={data.category}
|
|
onchange={(e) => handleFilterChange('category', e.currentTarget.value)}
|
|
class="w-full px-3 py-2.5 rounded-lg border border-gray-200 bg-white text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary"
|
|
/>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<label for="startDate" class="text-sm text-gray-500 whitespace-nowrap">From</label>
|
|
<input type="date" id="startDate" value={data.startDate} onchange={(e) => handleFilterChange('startDate', e.currentTarget.value)} class="px-3 py-2.5 rounded-lg border border-gray-200 bg-white text-sm focus:outline-none focus:ring-2 focus:ring-primary" />
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<label for="endDate" class="text-sm text-gray-500 whitespace-nowrap">To</label>
|
|
<input type="date" id="endDate" value={data.endDate} onchange={(e) => handleFilterChange('endDate', e.currentTarget.value)} class="px-3 py-2.5 rounded-lg border border-gray-200 bg-white text-sm focus:outline-none focus:ring-2 focus:ring-primary" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="text-sm text-gray-500 mb-3">{data.total} expense{data.total !== 1 ? 's' : ''} found</p>
|
|
|
|
<DataTable {columns} rows={data.expenses}>
|
|
{#snippet row(expense)}
|
|
<td class="px-4 py-3">
|
|
<a href="/admin/expenses/{expense.id}" class="font-medium text-primary hover:text-primary-dark hover:underline">
|
|
{expense.description || 'No description'}
|
|
</a>
|
|
</td>
|
|
<td class="px-4 py-3 font-medium text-gray-900">{fmt.format(expense.amount / 100)}</td>
|
|
<td class="px-4 py-3">
|
|
{#if expense.category}
|
|
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold bg-gray-100 text-gray-800 ring-1 ring-inset ring-gray-600/20">
|
|
{expense.category}
|
|
</span>
|
|
{:else}
|
|
<span class="text-gray-400">—</span>
|
|
{/if}
|
|
</td>
|
|
<td class="px-4 py-3 text-gray-600">{expense.petName ?? '—'}</td>
|
|
<td class="px-4 py-3 text-gray-600">{expense.vetName ?? '—'}</td>
|
|
<td class="px-4 py-3 text-gray-600">{formatDate(expense.date)}</td>
|
|
<td class="px-4 py-3 text-right">
|
|
<a href="/admin/expenses/{expense.id}" class="text-primary hover:text-primary-dark text-sm font-medium transition-colors">Edit</a>
|
|
</td>
|
|
{/snippet}
|
|
</DataTable>
|
|
|
|
<Pagination page={data.page} totalPages={data.totalPages} baseUrl={buildBaseUrl()} />
|