Add light/dark mode toggle across all pages

- Theme store with localStorage persistence and system preference detection
- Inline script in app.html to prevent flash of wrong theme
- Sun/moon toggle button in top bar and auth pages
- Tailwind v4 dark mode via @custom-variant with class strategy
- Dark mode classes applied to all 20+ pages: sidebar, auth forms,
  dashboard, companies, projects, expenses, budget, categories,
  reports, import, settings, admin pages, and all modals

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 13:23:15 +07:00
parent 1c7166adc5
commit 80e02030d6
25 changed files with 415 additions and 339 deletions
+7 -5
View File
@@ -1,12 +1,13 @@
<script lang="ts">
import type { LayoutData } from './$types';
import Sidebar from '$lib/components/layout/Sidebar.svelte';
import ThemeToggle from '$lib/components/layout/ThemeToggle.svelte';
let { data, children } = $props();
let sidebarOpen = $state(true);
</script>
<div class="flex h-screen bg-gray-50">
<div class="flex h-screen bg-gray-50 dark:bg-gray-900">
<Sidebar
user={data.user}
companies={data.companies}
@@ -16,10 +17,10 @@
<div class="flex flex-1 flex-col overflow-hidden">
<!-- Top bar -->
<header class="flex h-14 items-center justify-between border-b border-gray-200 bg-white px-6">
<header class="flex h-14 items-center justify-between border-b border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-800">
<button
onclick={() => (sidebarOpen = !sidebarOpen)}
class="rounded-md p-1.5 text-gray-500 hover:bg-gray-100 lg:hidden"
class="rounded-md p-1.5 text-gray-500 hover:bg-gray-100 lg:hidden dark:text-gray-400 dark:hover:bg-gray-700"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
@@ -27,11 +28,12 @@
</button>
<div class="flex items-center gap-3 ml-auto">
<span class="text-sm text-gray-700">{data.user.displayName ?? data.user.email}</span>
<ThemeToggle />
<span class="text-sm text-gray-700 dark:text-gray-300">{data.user.displayName ?? data.user.email}</span>
<form method="POST" action="/logout">
<button
type="submit"
class="rounded-md px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100"
class="rounded-md px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
>
Sign Out
</button>
+9 -9
View File
@@ -9,28 +9,28 @@
</svelte:head>
<div class="mx-auto max-w-2xl">
<h1 class="mb-6 text-2xl font-bold text-gray-900">System Settings</h1>
<h1 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">System Settings</h1>
<div class="space-y-4">
<div class="rounded-lg border border-gray-200 bg-white p-5">
<h2 class="mb-3 font-medium text-gray-900">System Status</h2>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5">
<h2 class="mb-3 font-medium text-gray-900 dark:text-white">System Status</h2>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-500">Database</span>
<span class="text-gray-500 dark:text-gray-400">Database</span>
<span class="font-medium text-green-600">{data.databaseUrl}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">OIDC</span>
<span class="font-medium {data.oidcConfigured ? 'text-green-600' : 'text-gray-400'}">
<span class="text-gray-500 dark:text-gray-400">OIDC</span>
<span class="font-medium {data.oidcConfigured ? 'text-green-600' : 'text-gray-400 dark:text-gray-500'}">
{data.oidcConfigured ? 'Configured' : 'Not configured'}
</span>
</div>
</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-5">
<h2 class="mb-3 font-medium text-gray-900">Configuration</h2>
<p class="text-sm text-gray-500">
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5">
<h2 class="mb-3 font-medium text-gray-900 dark:text-white">Configuration</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
System configuration is managed via environment variables. See the <code>.env.example</code> file
for available options.
</p>
+21 -21
View File
@@ -13,19 +13,19 @@
</svelte:head>
<div class="mx-auto max-w-5xl">
<h1 class="mb-6 text-2xl font-bold text-gray-900">Manage Users</h1>
<h1 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Manage Users</h1>
{#if form?.error}
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700">{form.error}</div>
<div class="mb-4 rounded-md bg-red-50 dark:bg-red-900/30 p-3 text-sm text-red-700 dark:text-red-300">{form.error}</div>
{/if}
{#if form?.deleted}
<div class="mb-4 rounded-md bg-green-50 p-3 text-sm text-green-700">User permanently deleted.</div>
<div class="mb-4 rounded-md bg-green-50 dark:bg-green-900/30 p-3 text-sm text-green-700 dark:text-green-300">User permanently deleted.</div>
{/if}
<div class="overflow-x-auto rounded-lg border border-gray-200 bg-white">
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr class="text-left text-gray-500">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr class="text-left text-gray-500 dark:text-gray-400">
<th class="px-4 py-3 font-medium">Name</th>
<th class="px-4 py-3 font-medium">Email</th>
<th class="px-4 py-3 font-medium">Status</th>
@@ -37,34 +37,34 @@
</thead>
<tbody>
{#each data.users as user}
<tr class="border-t border-gray-100 {user.disabledAt ? 'bg-red-50/50 opacity-70' : ''}">
<td class="px-4 py-3 font-medium text-gray-900">{user.displayName ?? '—'}</td>
<td class="px-4 py-3 text-gray-500">{user.email}</td>
<tr class="border-t border-gray-100 dark:border-gray-700 {user.disabledAt ? 'bg-red-50/50 dark:bg-red-900/20 opacity-70' : ''}">
<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{user.displayName ?? '—'}</td>
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{user.email}</td>
<td class="px-4 py-3">
{#if user.disabledAt}
<span class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">Disabled</span>
<span class="rounded-full bg-red-100 dark:bg-red-900/40 px-2 py-0.5 text-xs font-medium text-red-700 dark:text-red-300">Disabled</span>
{:else}
<span class="rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">Active</span>
<span class="rounded-full bg-green-100 dark:bg-green-900/40 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300">Active</span>
{/if}
</td>
<td class="px-4 py-3">
<span class="rounded-full px-2 py-0.5 text-xs font-medium {user.oidcProvider ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-700'}">
<span class="rounded-full px-2 py-0.5 text-xs font-medium {user.oidcProvider ? 'bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'}">
{user.oidcProvider ? 'SSO' : 'Local'}
</span>
</td>
<td class="px-4 py-3">
{#if user.isSystemAdmin}
<span class="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">Admin</span>
<span class="rounded-full bg-blue-100 dark:bg-blue-900/40 px-2 py-0.5 text-xs font-medium text-blue-700 dark:text-blue-300">Admin</span>
{/if}
</td>
<td class="px-4 py-3 text-gray-400">{formatDateTime(user.createdAt)}</td>
<td class="px-4 py-3 text-gray-400 dark:text-gray-500">{formatDateTime(user.createdAt)}</td>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<form method="POST" action="?/toggleAdmin" use:enhance>
<input type="hidden" name="userId" value={user.id} />
<button
type="submit"
class="rounded px-2 py-1 text-xs {user.isSystemAdmin ? 'text-red-600 hover:bg-red-50' : 'text-blue-600 hover:bg-blue-50'}"
class="rounded px-2 py-1 text-xs {user.isSystemAdmin ? 'text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30' : 'text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30'}"
>
{user.isSystemAdmin ? 'Remove Admin' : 'Make Admin'}
</button>
@@ -74,7 +74,7 @@
<input type="hidden" name="userId" value={user.id} />
<button
type="submit"
class="rounded px-2 py-1 text-xs {user.disabledAt ? 'text-green-600 hover:bg-green-50' : 'text-amber-600 hover:bg-amber-50'}"
class="rounded px-2 py-1 text-xs {user.disabledAt ? 'text-green-600 hover:bg-green-50 dark:hover:bg-green-900/30' : 'text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-900/30'}"
>
{user.disabledAt ? 'Enable' : 'Disable'}
</button>
@@ -82,7 +82,7 @@
<button
onclick={() => { confirmDeleteId = user.id; confirmDeleteName = user.displayName ?? user.email; }}
class="rounded px-2 py-1 text-xs text-red-600 hover:bg-red-50"
class="rounded px-2 py-1 text-xs text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30"
>
Delete
</button>
@@ -98,19 +98,19 @@
<!-- Permanent delete confirmation modal -->
{#if confirmDeleteId}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="w-full max-w-sm rounded-lg bg-white p-6 shadow-xl">
<div class="w-full max-w-sm rounded-lg bg-white dark:bg-gray-800 p-6 shadow-xl">
<h2 class="text-lg font-semibold text-red-600">Permanently Delete User</h2>
<p class="mt-2 text-sm text-gray-500">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
This will permanently delete <strong>{confirmDeleteName}</strong> and remove them from all companies. This action cannot be undone.
</p>
<p class="mt-2 text-sm text-gray-500">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Their submitted expenses will be preserved but no longer linked to their account.
</p>
<div class="mt-4 flex justify-end gap-2">
<button
type="button"
onclick={() => (confirmDeleteId = null)}
class="rounded-md px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
class="rounded-md px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
Cancel
</button>
+28 -28
View File
@@ -15,7 +15,7 @@
<div class="mx-auto max-w-6xl">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold text-gray-900">Companies</h1>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Companies</h1>
{#if data.isSystemAdmin}
<button
onclick={() => (showCreateModal = true)}
@@ -27,34 +27,34 @@
</div>
{#if form?.error}
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700">{form.error}</div>
<div class="mb-4 rounded-md bg-red-50 dark:bg-red-900/30 p-3 text-sm text-red-700 dark:text-red-300">{form.error}</div>
{/if}
{#if form?.deleted}
<div class="mb-4 rounded-md bg-green-50 p-3 text-sm text-green-700">Company "{form.deleted}" has been archived.</div>
<div class="mb-4 rounded-md bg-green-50 dark:bg-green-900/30 p-3 text-sm text-green-700 dark:text-green-300">Company "{form.deleted}" has been archived.</div>
{/if}
{#if data.companies.length === 0}
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center">
<svg class="mx-auto h-12 w-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-12 text-center">
<svg class="mx-auto h-12 w-12 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<h2 class="mt-4 text-lg font-medium text-gray-900">No companies yet</h2>
<p class="mt-2 text-sm text-gray-500">
<h2 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No companies yet</h2>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
You haven't been assigned to any company yet. Ask an administrator to invite you.
</p>
</div>
{:else}
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each data.companies as company}
<div class="relative rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow">
<div class="relative rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 hover:shadow-md transition-shadow">
<a href="/companies/{company.id}" class="block">
<h2 class="text-lg font-semibold text-gray-900">{company.name}</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{company.name}</h2>
{#if company.description}
<p class="mt-1 text-sm text-gray-500 line-clamp-2">{company.description}</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">{company.description}</p>
{/if}
<div class="mt-3 flex items-center justify-between text-sm">
<span class="text-gray-500">Budget: {formatCurrency(company.totalBudget, company.currency)}</span>
<span class="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">
<span class="text-gray-500 dark:text-gray-400">Budget: {formatCurrency(company.totalBudget, company.currency)}</span>
<span class="rounded-full bg-blue-100 dark:bg-blue-900/40 px-2 py-0.5 text-xs font-medium text-blue-700 dark:text-blue-300">
{company.role}
</span>
</div>
@@ -62,7 +62,7 @@
{#if data.isSystemAdmin}
<button
onclick={(e) => { e.stopPropagation(); confirmDeleteId = company.id; confirmDeleteName = company.name; }}
class="absolute top-3 right-3 rounded p-1 text-gray-300 hover:bg-red-50 hover:text-red-500"
class="absolute top-3 right-3 rounded p-1 text-gray-300 dark:text-gray-600 hover:bg-red-50 dark:hover:bg-red-900/30 hover:text-red-500"
title="Archive company"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -79,33 +79,33 @@
<!-- Create company modal (admin only) -->
{#if showCreateModal && data.isSystemAdmin}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
<h2 class="mb-4 text-lg font-semibold text-gray-900">Create Company</h2>
<div class="w-full max-w-md rounded-lg bg-white dark:bg-gray-800 p-6 shadow-xl">
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Create Company</h2>
<form method="POST" action="?/create" use:enhance>
<div class="mb-4">
<label for="name" class="mb-1 block text-sm font-medium text-gray-700">Name</label>
<label for="name" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
<input
type="text"
id="name"
name="name"
required
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
<div class="mb-4">
<label for="description" class="mb-1 block text-sm font-medium text-gray-700">
<label for="description" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Description
</label>
<textarea
id="description"
name="description"
rows="2"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
></textarea>
</div>
<div class="mb-4 grid grid-cols-2 gap-3">
<div>
<label for="totalBudget" class="mb-1 block text-sm font-medium text-gray-700">
<label for="totalBudget" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Initial Budget
</label>
<input
@@ -114,17 +114,17 @@
name="totalBudget"
step="0.01"
value="0"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
<div>
<label for="currency" class="mb-1 block text-sm font-medium text-gray-700">
<label for="currency" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Currency
</label>
<select
id="currency"
name="currency"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
>
<option value="THB" selected>THB</option>
<option value="USD">USD</option>
@@ -136,7 +136,7 @@
<button
type="button"
onclick={() => (showCreateModal = false)}
class="rounded-md px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
class="rounded-md px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
Cancel
</button>
@@ -155,16 +155,16 @@
<!-- Delete confirmation modal -->
{#if confirmDeleteId}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="w-full max-w-sm rounded-lg bg-white p-6 shadow-xl">
<h2 class="text-lg font-semibold text-gray-900">Archive Company</h2>
<p class="mt-2 text-sm text-gray-500">
<div class="w-full max-w-sm rounded-lg bg-white dark:bg-gray-800 p-6 shadow-xl">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Archive Company</h2>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Are you sure you want to archive <strong>{confirmDeleteName}</strong>? The company and all its data will be hidden but not permanently deleted.
</p>
<div class="mt-4 flex justify-end gap-2">
<button
type="button"
onclick={() => (confirmDeleteId = null)}
class="rounded-md px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
class="rounded-md px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
Cancel
</button>
@@ -21,18 +21,18 @@
<div>
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">{data.company.name}</h1>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{data.company.name}</h1>
{#if data.company.description}
<p class="mt-1 text-sm text-gray-500">{data.company.description}</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{data.company.description}</p>
{/if}
</div>
<!-- Tabs -->
<nav class="mb-6 flex gap-1 overflow-x-auto border-b border-gray-200">
<nav class="mb-6 flex gap-1 overflow-x-auto border-b border-gray-200 dark:border-gray-700">
{#each tabs as tab}
<a
href={tab.href}
class="whitespace-nowrap border-b-2 px-4 py-2 text-sm font-medium transition-colors border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
class="whitespace-nowrap border-b-2 px-4 py-2 text-sm font-medium transition-colors border-transparent text-gray-500 dark:text-gray-400 hover:border-gray-300 dark:hover:border-gray-600 hover:text-gray-700 dark:hover:text-gray-300"
>
{tab.label}
</a>
@@ -18,12 +18,12 @@
<div class="grid gap-6 lg:grid-cols-2">
<!-- Budget Summary -->
<div class="rounded-lg border-2 {remaining < 0 ? 'border-red-300 bg-red-50' : remainingPct < 20 ? 'border-amber-300 bg-amber-50' : 'border-green-300 bg-green-50'} p-5">
<div class="rounded-lg border-2 {remaining < 0 ? 'border-red-300 bg-red-50 dark:border-red-700 dark:bg-red-900/30' : remainingPct < 20 ? 'border-amber-300 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/30' : 'border-green-300 bg-green-50 dark:border-green-700 dark:bg-green-900/30'} p-5">
<h2 class="mb-1 text-sm font-semibold uppercase tracking-wider {remaining < 0 ? 'text-red-400' : remainingPct < 20 ? 'text-amber-400' : 'text-green-400'}">Remaining Budget</h2>
<div class="text-3xl font-bold {remaining < 0 ? 'text-red-700' : remainingPct < 20 ? 'text-amber-700' : 'text-green-700'}">
<div class="text-3xl font-bold {remaining < 0 ? 'text-red-700 dark:text-red-400' : remainingPct < 20 ? 'text-amber-700 dark:text-amber-400' : 'text-green-700 dark:text-green-400'}">
{formatCurrency(remaining, currency)}
</div>
<div class="mt-3 h-2.5 w-full overflow-hidden rounded-full bg-white/60">
<div class="mt-3 h-2.5 w-full overflow-hidden rounded-full bg-white/60 dark:bg-gray-700/60">
<div
class="h-full rounded-full transition-all {remaining < 0 ? 'bg-red-500' : remainingPct < 20 ? 'bg-amber-500' : 'bg-green-500'}"
style="width: {Math.max(0, Math.min(remainingPct, 100))}%"
@@ -34,23 +34,23 @@
<div class="mt-4 space-y-1.5 text-sm">
<div class="flex justify-between">
<span class="{remaining < 0 ? 'text-red-400' : remainingPct < 20 ? 'text-amber-400' : 'text-green-600/60'}">Total budget</span>
<span class="font-medium {remaining < 0 ? 'text-red-600' : remainingPct < 20 ? 'text-amber-600' : 'text-green-700'}">{formatCurrency(total, currency)}</span>
<span class="font-medium {remaining < 0 ? 'text-red-600 dark:text-red-400' : remainingPct < 20 ? 'text-amber-600 dark:text-amber-400' : 'text-green-700 dark:text-green-400'}">{formatCurrency(total, currency)}</span>
</div>
<div class="flex justify-between">
<span class="{remaining < 0 ? 'text-red-400' : remainingPct < 20 ? 'text-amber-400' : 'text-green-600/60'}">Total spent</span>
<span class="font-medium {remaining < 0 ? 'text-red-600' : remainingPct < 20 ? 'text-amber-600' : 'text-green-700'}">{formatCurrency(spent, currency)}</span>
<span class="font-medium {remaining < 0 ? 'text-red-600 dark:text-red-400' : remainingPct < 20 ? 'text-amber-600 dark:text-amber-400' : 'text-green-700 dark:text-green-400'}">{formatCurrency(spent, currency)}</span>
</div>
<div class="flex justify-between">
<span class="{remaining < 0 ? 'text-red-400' : remainingPct < 20 ? 'text-amber-400' : 'text-green-600/60'}">Allocated</span>
<span class="font-medium {remaining < 0 ? 'text-red-600' : remainingPct < 20 ? 'text-amber-600' : 'text-green-700'}">{formatCurrency(allocated, currency)}</span>
<span class="font-medium {remaining < 0 ? 'text-red-600 dark:text-red-400' : remainingPct < 20 ? 'text-amber-600 dark:text-amber-400' : 'text-green-700 dark:text-green-400'}">{formatCurrency(allocated, currency)}</span>
</div>
</div>
</div>
<!-- Projects -->
<div class="rounded-lg border border-gray-200 bg-white p-5">
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wider text-gray-400">Projects</h2>
<h2 class="text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Projects</h2>
{#if data.companyRole !== 'viewer'}
<a
href="/companies/{data.company.id}/projects/new"
@@ -62,7 +62,7 @@
</div>
{#if data.projects.length === 0}
<p class="py-4 text-center text-sm text-gray-500">No projects yet.</p>
<p class="py-4 text-center text-sm text-gray-500 dark:text-gray-400">No projects yet.</p>
{:else}
<div class="space-y-3">
{#each data.projects as project}
@@ -71,12 +71,12 @@
{@const pct = budgetNum > 0 ? Math.min((spentNum / budgetNum) * 100, 100) : 0}
<a href="/companies/{data.company.id}/projects/{project.id}" class="block">
<div class="flex items-center justify-between text-sm">
<span class="font-medium text-gray-900">{project.name}</span>
<span class="text-gray-500">
<span class="font-medium text-gray-900 dark:text-white">{project.name}</span>
<span class="text-gray-500 dark:text-gray-400">
{formatCurrency(project.spent, currency)} / {formatCurrency(project.allocatedBudget, currency)}
</span>
</div>
<div class="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-gray-100">
<div class="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
<div
class="h-full rounded-full {pct > 90 ? 'bg-red-500' : pct > 70 ? 'bg-amber-500' : 'bg-blue-500'}"
style="width: {pct}%"
@@ -92,14 +92,14 @@
</div>
<!-- Recent Expenses -->
<div class="rounded-lg border border-gray-200 bg-white p-5 lg:col-span-2">
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400">Recent Expenses</h2>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 lg:col-span-2">
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Recent Expenses</h2>
{#if data.recentExpenses.length === 0}
<p class="py-4 text-center text-sm text-gray-500">No expenses yet.</p>
<p class="py-4 text-center text-sm text-gray-500 dark:text-gray-400">No expenses yet.</p>
{:else}
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-100 text-left text-gray-500">
<tr class="border-b border-gray-100 dark:border-gray-700 text-left text-gray-500 dark:text-gray-400">
<th class="pb-2 font-medium">Title</th>
<th class="pb-2 font-medium">Project</th>
<th class="pb-2 font-medium">Amount</th>
@@ -109,19 +109,19 @@
</thead>
<tbody>
{#each data.recentExpenses as expense}
<tr class="border-b border-gray-50">
<td class="py-2 font-medium text-gray-900">{expense.title}</td>
<td class="py-2 text-gray-500">{expense.projectName}</td>
<tr class="border-b border-gray-50 dark:border-gray-700/50">
<td class="py-2 font-medium text-gray-900 dark:text-white">{expense.title}</td>
<td class="py-2 text-gray-500 dark:text-gray-400">{expense.projectName}</td>
<td class="py-2">{formatCurrency(expense.amount, currency)}</td>
<td class="py-2 text-gray-500">{expense.expenseDate}</td>
<td class="py-2 text-gray-500 dark:text-gray-400">{expense.expenseDate}</td>
<td class="py-2">
<span
class="rounded-full px-2 py-0.5 text-xs font-medium
{expense.status === 'approved'
? 'bg-green-100 text-green-700'
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
: expense.status === 'rejected'
? 'bg-red-100 text-red-700'
: 'bg-amber-100 text-amber-700'}"
? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300'}"
>
{expense.status}
</span>
@@ -18,24 +18,24 @@
function getEventStyle(event: string) {
const styles: Record<string, { icon: string; bg: string; text: string; badge: string; label: string }> = {
company_created: { icon: '🏢', bg: 'bg-blue-100', text: 'text-blue-700', badge: 'bg-blue-100 text-blue-700', label: 'Created' },
company_updated: { icon: '✏️', bg: 'bg-gray-100', text: 'text-gray-600', badge: 'bg-gray-100 text-gray-600', label: 'Updated' },
budget_initial: { icon: '💰', bg: 'bg-green-100', text: 'text-green-700', badge: 'bg-green-100 text-green-700', label: 'Initial Budget' },
budget_added: { icon: '', bg: 'bg-green-100', text: 'text-green-700', badge: 'bg-green-100 text-green-700', label: 'Budget Added' },
budget_allocated: { icon: '📊', bg: 'bg-blue-100', text: 'text-blue-700', badge: 'bg-blue-100 text-blue-700', label: 'Allocated' },
budget_deallocated: { icon: '↩️', bg: 'bg-amber-100', text: 'text-amber-700', badge: 'bg-amber-100 text-amber-700', label: 'Deallocated' },
project_created: { icon: '📁', bg: 'bg-indigo-100', text: 'text-indigo-700', badge: 'bg-indigo-100 text-indigo-700', label: 'Project' },
project_updated: { icon: '📁', bg: 'bg-gray-100', text: 'text-gray-600', badge: 'bg-gray-100 text-gray-600', label: 'Project' },
member_added: { icon: '👤', bg: 'bg-purple-100', text: 'text-purple-700', badge: 'bg-purple-100 text-purple-700', label: 'Member' },
member_removed: { icon: '👤', bg: 'bg-red-100', text: 'text-red-700', badge: 'bg-red-100 text-red-700', label: 'Member' },
member_role_changed: { icon: '🔑', bg: 'bg-purple-100', text: 'text-purple-700', badge: 'bg-purple-100 text-purple-700', label: 'Role Change' },
expense_submitted: { icon: '📝', bg: 'bg-amber-100', text: 'text-amber-700', badge: 'bg-amber-100 text-amber-700', label: 'Expense' },
expense_approved: { icon: '✅', bg: 'bg-green-100', text: 'text-green-700', badge: 'bg-green-100 text-green-700', label: 'Approved' },
expense_rejected: { icon: '❌', bg: 'bg-red-100', text: 'text-red-700', badge: 'bg-red-100 text-red-700', label: 'Rejected' },
category_created: { icon: '🏷️', bg: 'bg-gray-100', text: 'text-gray-600', badge: 'bg-gray-100 text-gray-600', label: 'Category' },
import_completed: { icon: '📥', bg: 'bg-blue-100', text: 'text-blue-700', badge: 'bg-blue-100 text-blue-700', label: 'Import' },
company_created: { icon: '🏢', bg: 'bg-blue-100 dark:bg-blue-900/40', text: 'text-blue-700 dark:text-blue-300', badge: 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300', label: 'Created' },
company_updated: { icon: '✏️', bg: 'bg-gray-100 dark:bg-gray-700', text: 'text-gray-600 dark:text-gray-400', badge: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400', label: 'Updated' },
budget_initial: { icon: '💰', bg: 'bg-green-100 dark:bg-green-900/40', text: 'text-green-700 dark:text-green-300', badge: 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300', label: 'Initial Budget' },
budget_added: { icon: '', bg: 'bg-green-100 dark:bg-green-900/40', text: 'text-green-700 dark:text-green-300', badge: 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300', label: 'Budget Added' },
budget_allocated: { icon: '📊', bg: 'bg-blue-100 dark:bg-blue-900/40', text: 'text-blue-700 dark:text-blue-300', badge: 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300', label: 'Allocated' },
budget_deallocated: { icon: '↩️', bg: 'bg-amber-100 dark:bg-amber-900/40', text: 'text-amber-700 dark:text-amber-300', badge: 'bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300', label: 'Deallocated' },
project_created: { icon: '📁', bg: 'bg-indigo-100 dark:bg-indigo-900/40', text: 'text-indigo-700 dark:text-indigo-300', badge: 'bg-indigo-100 dark:bg-indigo-900/40 text-indigo-700 dark:text-indigo-300', label: 'Project' },
project_updated: { icon: '📁', bg: 'bg-gray-100 dark:bg-gray-700', text: 'text-gray-600 dark:text-gray-400', badge: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400', label: 'Project' },
member_added: { icon: '👤', bg: 'bg-purple-100 dark:bg-purple-900/40', text: 'text-purple-700 dark:text-purple-300', badge: 'bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300', label: 'Member' },
member_removed: { icon: '👤', bg: 'bg-red-100 dark:bg-red-900/40', text: 'text-red-700 dark:text-red-300', badge: 'bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300', label: 'Member' },
member_role_changed: { icon: '🔑', bg: 'bg-purple-100 dark:bg-purple-900/40', text: 'text-purple-700 dark:text-purple-300', badge: 'bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300', label: 'Role Change' },
expense_submitted: { icon: '📝', bg: 'bg-amber-100 dark:bg-amber-900/40', text: 'text-amber-700 dark:text-amber-300', badge: 'bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300', label: 'Expense' },
expense_approved: { icon: '✅', bg: 'bg-green-100 dark:bg-green-900/40', text: 'text-green-700 dark:text-green-300', badge: 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300', label: 'Approved' },
expense_rejected: { icon: '❌', bg: 'bg-red-100 dark:bg-red-900/40', text: 'text-red-700 dark:text-red-300', badge: 'bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300', label: 'Rejected' },
category_created: { icon: '🏷️', bg: 'bg-gray-100 dark:bg-gray-700', text: 'text-gray-600 dark:text-gray-400', badge: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400', label: 'Category' },
import_completed: { icon: '📥', bg: 'bg-blue-100 dark:bg-blue-900/40', text: 'text-blue-700 dark:text-blue-300', badge: 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300', label: 'Import' },
};
return styles[event] ?? { icon: '•', bg: 'bg-gray-100', text: 'text-gray-600', badge: 'bg-gray-100 text-gray-600', label: event };
return styles[event] ?? { icon: '•', bg: 'bg-gray-100 dark:bg-gray-700', text: 'text-gray-600 dark:text-gray-400', badge: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400', label: event };
}
</script>
@@ -45,7 +45,7 @@
<div>
<div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900">Budget Allocation</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Budget Allocation</h2>
{#if isAdmin}
<button
onclick={() => (showAddBudget = !showAddBudget)}
@@ -60,13 +60,13 @@
</div>
{#if form?.error}
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700">{form.error}</div>
<div class="mb-4 rounded-md bg-red-50 dark:bg-red-900/30 p-3 text-sm text-red-700 dark:text-red-300">{form.error}</div>
{/if}
<!-- Add Budget form (admin only) -->
{#if showAddBudget && isAdmin}
<div class="mb-6 rounded-lg border-2 border-green-200 bg-green-50 p-5">
<h3 class="mb-3 font-medium text-gray-900">Replenish Company Budget</h3>
<div class="mb-6 rounded-lg border-2 border-green-200 dark:border-green-700 bg-green-50 dark:bg-green-900/30 p-5">
<h3 class="mb-3 font-medium text-gray-900 dark:text-white">Replenish Company Budget</h3>
<form method="POST" action="?/addBudget" use:enhance={() => {
return async ({ update }) => {
await update();
@@ -74,7 +74,7 @@
};
}} class="flex items-end gap-3">
<div class="flex-1">
<label for="addAmount" class="mb-1 block text-sm text-gray-700">Amount to Add ({currency})</label>
<label for="addAmount" class="mb-1 block text-sm text-gray-700 dark:text-gray-300">Amount to Add ({currency})</label>
<input
type="number"
id="addAmount"
@@ -83,7 +83,7 @@
min="0.01"
required
placeholder="e.g. 100000"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-green-500 focus:ring-1 focus:ring-green-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-green-500 focus:ring-1 focus:ring-green-500 focus:outline-none"
/>
</div>
<button
@@ -95,7 +95,7 @@
<button
type="button"
onclick={() => (showAddBudget = false)}
class="rounded-md px-4 py-2 text-sm text-gray-600 hover:bg-gray-100"
class="rounded-md px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
>
Cancel
</button>
@@ -106,12 +106,12 @@
<!-- Summary -->
<div class="mb-6 grid gap-4 sm:grid-cols-4">
<!-- Remaining — hero card -->
<div class="rounded-lg border-2 {remaining < 0 ? 'border-red-300 bg-red-50' : remainingPct < 20 ? 'border-amber-300 bg-amber-50' : 'border-green-300 bg-green-50'} p-5 sm:col-span-2 sm:row-span-2">
<div class="rounded-lg border-2 {remaining < 0 ? 'border-red-300 bg-red-50 dark:border-red-700 dark:bg-red-900/30' : remainingPct < 20 ? 'border-amber-300 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/30' : 'border-green-300 bg-green-50 dark:border-green-700 dark:bg-green-900/30'} p-5 sm:col-span-2 sm:row-span-2">
<p class="text-sm font-medium {remaining < 0 ? 'text-red-600' : remainingPct < 20 ? 'text-amber-600' : 'text-green-600'}">Remaining Budget</p>
<p class="mt-1 text-4xl font-bold {remaining < 0 ? 'text-red-700' : remainingPct < 20 ? 'text-amber-700' : 'text-green-700'}">
<p class="mt-1 text-4xl font-bold {remaining < 0 ? 'text-red-700 dark:text-red-400' : remainingPct < 20 ? 'text-amber-700 dark:text-amber-400' : 'text-green-700 dark:text-green-400'}">
{formatCurrency(remaining, currency)}
</p>
<div class="mt-3 h-3 w-full overflow-hidden rounded-full bg-white/60">
<div class="mt-3 h-3 w-full overflow-hidden rounded-full bg-white/60 dark:bg-gray-700/60">
<div
class="h-full rounded-full transition-all {remaining < 0 ? 'bg-red-500' : remainingPct < 20 ? 'bg-amber-500' : 'bg-green-500'}"
style="width: {Math.max(0, Math.min(remainingPct, 100))}%"
@@ -123,27 +123,27 @@
</div>
<!-- Total Budget -->
<div class="rounded-lg border border-gray-200 bg-white p-4">
<p class="text-xs font-medium uppercase tracking-wider text-gray-400">Total Budget</p>
<p class="mt-1 text-lg font-bold text-gray-900">{formatCurrency(total, currency)}</p>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
<p class="text-xs font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">Total Budget</p>
<p class="mt-1 text-lg font-bold text-gray-900 dark:text-white">{formatCurrency(total, currency)}</p>
</div>
<!-- Total Spent -->
<div class="rounded-lg border border-gray-200 bg-white p-4">
<p class="text-xs font-medium uppercase tracking-wider text-gray-400">Total Spent</p>
<p class="mt-1 text-lg font-bold text-gray-900">{formatCurrency(totalSpent, currency)}</p>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
<p class="text-xs font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">Total Spent</p>
<p class="mt-1 text-lg font-bold text-gray-900 dark:text-white">{formatCurrency(totalSpent, currency)}</p>
</div>
<!-- Allocated to Projects -->
<div class="rounded-lg border border-gray-200 bg-white p-4">
<p class="text-xs font-medium uppercase tracking-wider text-gray-400">Allocated</p>
<p class="mt-1 text-lg font-bold text-gray-900">{formatCurrency(data.totalAllocated, currency)}</p>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
<p class="text-xs font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">Allocated</p>
<p class="mt-1 text-lg font-bold text-gray-900 dark:text-white">{formatCurrency(data.totalAllocated, currency)}</p>
</div>
<!-- Unallocated -->
<div class="rounded-lg border border-gray-200 bg-white p-4">
<p class="text-xs font-medium uppercase tracking-wider text-gray-400">Unallocated</p>
<p class="mt-1 text-lg font-bold {unallocated < 0 ? 'text-red-600' : 'text-gray-900'}">
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
<p class="text-xs font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">Unallocated</p>
<p class="mt-1 text-lg font-bold {unallocated < 0 ? 'text-red-600' : 'text-gray-900 dark:text-white'}">
{formatCurrency(unallocated, currency)}
</p>
</div>
@@ -151,16 +151,16 @@
<!-- Allocate to project form -->
{#if canAllocate && data.projects.length > 0}
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5">
<h3 class="mb-3 font-medium text-gray-900">Allocate Funds to Project</h3>
<div class="mb-6 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5">
<h3 class="mb-3 font-medium text-gray-900 dark:text-white">Allocate Funds to Project</h3>
<form method="POST" action="?/allocate" use:enhance class="flex flex-wrap items-end gap-3">
<div class="flex-1">
<label for="projectId" class="mb-1 block text-sm text-gray-700">Project</label>
<label for="projectId" class="mb-1 block text-sm text-gray-700 dark:text-gray-300">Project</label>
<select
id="projectId"
name="projectId"
required
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm"
>
{#each data.projects as project}
<option value={project.id}>{project.name}</option>
@@ -168,24 +168,24 @@
</select>
</div>
<div class="w-36">
<label for="amount" class="mb-1 block text-sm text-gray-700">Amount</label>
<label for="amount" class="mb-1 block text-sm text-gray-700 dark:text-gray-300">Amount</label>
<input
type="number"
id="amount"
name="amount"
step="0.01"
required
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm"
placeholder="Negative to deallocate"
/>
</div>
<div class="flex-1">
<label for="note" class="mb-1 block text-sm text-gray-700">Note</label>
<label for="note" class="mb-1 block text-sm text-gray-700 dark:text-gray-300">Note</label>
<input
type="text"
id="note"
name="note"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm"
/>
</div>
<button
@@ -199,10 +199,10 @@
{/if}
<!-- Project budgets -->
<div class="mb-6 rounded-lg border border-gray-200 bg-white">
<div class="mb-6 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr class="text-left text-gray-500">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr class="text-left text-gray-500 dark:text-gray-400">
<th class="px-4 py-3 font-medium">Project</th>
<th class="px-4 py-3 font-medium">Allocated</th>
<th class="px-4 py-3 font-medium">Spent</th>
@@ -216,13 +216,13 @@
{@const spent = parseFloat(project.spent)}
{@const remaining = allocated - spent}
{@const pct = allocated > 0 ? Math.min((spent / allocated) * 100, 100) : 0}
<tr class="border-t border-gray-100">
<td class="px-4 py-3 font-medium text-gray-900">{project.name}</td>
<tr class="border-t border-gray-100 dark:border-gray-700">
<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{project.name}</td>
<td class="px-4 py-3">{formatCurrency(allocated, currency)}</td>
<td class="px-4 py-3">{formatCurrency(spent, currency)}</td>
<td class="px-4 py-3 {remaining < 0 ? 'text-red-600' : ''}">{formatCurrency(remaining, currency)}</td>
<td class="px-4 py-3 w-32">
<div class="h-2 w-full overflow-hidden rounded-full bg-gray-100">
<div class="h-2 w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
<div
class="h-full rounded-full {pct > 90 ? 'bg-red-500' : pct > 70 ? 'bg-amber-500' : 'bg-blue-500'}"
style="width: {pct}%"
@@ -237,9 +237,9 @@
<!-- Company Changelog -->
{#if data.changelog.length > 0}
<h3 class="mb-3 font-medium text-gray-900">Activity Log</h3>
<div class="rounded-lg border border-gray-200 bg-white">
<div class="divide-y divide-gray-100">
<h3 class="mb-3 font-medium text-gray-900 dark:text-white">Activity Log</h3>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div class="divide-y divide-gray-100 dark:divide-gray-700">
{#each data.changelog as entry}
{@const eventStyle = getEventStyle(entry.event)}
<div class="flex items-start gap-3 px-4 py-3">
@@ -247,8 +247,8 @@
<span class="text-xs {eventStyle.text}">{eventStyle.icon}</span>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm text-gray-900">{entry.description}</p>
<p class="mt-0.5 text-xs text-gray-400">
<p class="text-sm text-gray-900 dark:text-white">{entry.description}</p>
<p class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">
{entry.userName ?? entry.userEmail ?? 'System'} · {timeAgo(entry.createdAt)}
</p>
</div>
@@ -11,33 +11,33 @@
</svelte:head>
<div>
<h2 class="mb-4 text-lg font-semibold text-gray-900">Categories</h2>
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Categories</h2>
{#if form?.error}
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700">{form.error}</div>
<div class="mb-4 rounded-md bg-red-50 dark:bg-red-900/30 p-3 text-sm text-red-700 dark:text-red-300">{form.error}</div>
{/if}
{#if canManage}
<form method="POST" action="?/create" use:enhance class="mb-6 flex items-end gap-3">
<div class="flex-1">
<label for="name" class="mb-1 block text-sm font-medium text-gray-700">New Category</label>
<label for="name" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">New Category</label>
<input
type="text"
id="name"
name="name"
required
placeholder="Category name"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm"
/>
</div>
<div class="w-20">
<label for="color" class="mb-1 block text-sm font-medium text-gray-700">Color</label>
<label for="color" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Color</label>
<input
type="color"
id="color"
name="color"
value="#3B82F6"
class="h-10 w-full rounded-md border border-gray-300"
class="h-10 w-full rounded-md border border-gray-300 dark:border-gray-600"
/>
</div>
<button
@@ -50,16 +50,16 @@
{/if}
{#if data.categories.length === 0}
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center">
<p class="text-gray-500">No categories yet.</p>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-12 text-center">
<p class="text-gray-500 dark:text-gray-400">No categories yet.</p>
</div>
{:else}
<div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{#each data.categories as cat}
<div class="flex items-center justify-between rounded-lg border border-gray-200 bg-white p-3">
<div class="flex items-center justify-between rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3">
<div class="flex items-center gap-2">
<div class="h-4 w-4 rounded-full" style="background-color: {cat.color}"></div>
<span class="text-sm font-medium text-gray-900">{cat.name}</span>
<span class="text-sm font-medium text-gray-900 dark:text-white">{cat.name}</span>
</div>
{#if canManage}
<form method="POST" action="?/delete" use:enhance>
@@ -16,7 +16,7 @@
<div>
<div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900">Expenses</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Expenses</h2>
</div>
<!-- Status filter -->
@@ -27,7 +27,7 @@
class="rounded-full px-3 py-1 text-sm font-medium transition-colors
{data.statusFilter === status
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'}"
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'}"
>
{status.charAt(0).toUpperCase() + status.slice(1)}
</a>
@@ -35,24 +35,24 @@
</div>
{#if data.expenses.length === 0}
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center">
<p class="text-gray-500">No expenses found.</p>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-12 text-center">
<p class="text-gray-500 dark:text-gray-400">No expenses found.</p>
</div>
{:else}
<div class="space-y-3">
{#each data.expenses as expense}
<div class="rounded-lg border border-gray-200 bg-white p-4">
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
<div class="flex items-start justify-between">
<div>
<h3 class="font-medium text-gray-900">{expense.title}</h3>
<p class="text-sm text-gray-500">
<h3 class="font-medium text-gray-900 dark:text-white">{expense.title}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{expense.projectName}
{#if expense.categoryName}· {expense.categoryName}{/if}
</p>
{#if expense.description}
<p class="mt-1 text-sm text-gray-400">{expense.description}</p>
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">{expense.description}</p>
{/if}
<p class="mt-1 text-xs text-gray-400">
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
By {expense.submitterName ?? expense.submitterEmail} · {expense.expenseDate}
</p>
</div>
@@ -61,10 +61,10 @@
<span
class="rounded-full px-2 py-0.5 text-xs font-medium
{expense.status === 'approved'
? 'bg-green-100 text-green-700'
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
: expense.status === 'rejected'
? 'bg-red-100 text-red-700'
: 'bg-amber-100 text-amber-700'}"
? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300'}"
>
{expense.status}
</span>
@@ -72,13 +72,13 @@
</div>
{#if expense.status === 'rejected' && expense.rejectionReason}
<div class="mt-2 rounded bg-red-50 px-3 py-2 text-sm text-red-700">
<div class="mt-2 rounded bg-red-50 dark:bg-red-900/30 px-3 py-2 text-sm text-red-700 dark:text-red-300">
Reason: {expense.rejectionReason}
</div>
{/if}
{#if canApprove && expense.status === 'pending'}
<div class="mt-3 flex gap-2 border-t border-gray-100 pt-3">
<div class="mt-3 flex gap-2 border-t border-gray-100 dark:border-gray-700 pt-3">
<form method="POST" action="?/approve" use:enhance>
<input type="hidden" name="expenseId" value={expense.id} />
<button
@@ -94,7 +94,7 @@
type="text"
name="reason"
placeholder="Rejection reason (optional)"
class="rounded-md border border-gray-300 px-2 py-1 text-sm"
class="rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-2 py-1 text-sm"
/>
<button
type="submit"
@@ -45,33 +45,33 @@
</svelte:head>
<div class="mx-auto max-w-3xl">
<h2 class="mb-4 text-lg font-semibold text-gray-900">Import Expenses</h2>
<p class="mb-4 text-sm text-gray-500">
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Import Expenses</h2>
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">
Paste CSV data from Actual Budget or any spreadsheet. Expected columns: title/name, amount, date.
Optional: description, category.
</p>
{#if form?.error}
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700">{form.error}</div>
<div class="mb-4 rounded-md bg-red-50 dark:bg-red-900/30 p-3 text-sm text-red-700 dark:text-red-300">{form.error}</div>
{/if}
{#if form?.success}
<div class="mb-4 rounded-md bg-green-50 p-3 text-sm text-green-700">
<div class="mb-4 rounded-md bg-green-50 dark:bg-green-900/30 p-3 text-sm text-green-700 dark:text-green-300">
Successfully imported {form.imported} expenses.
</div>
{/if}
<!-- Step 1: Paste CSV -->
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5">
<h3 class="mb-2 font-medium text-gray-900">1. Paste CSV Data</h3>
<div class="mb-6 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5">
<h3 class="mb-2 font-medium text-gray-900 dark:text-white">1. Paste CSV Data</h3>
<textarea
bind:value={csvText}
rows="8"
placeholder="title,amount,date,description&#10;Office Supplies,150.00,2024-01-15,Printer paper&#10;..."
class="w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-sm"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 font-mono text-sm"
></textarea>
<button
onclick={parseCSV}
class="mt-2 rounded-md bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-200"
class="mt-2 rounded-md bg-gray-100 dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
>
Parse CSV
</button>
@@ -79,20 +79,20 @@
<!-- Step 2: Preview -->
{#if parsedRows.length > 0}
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5">
<h3 class="mb-2 font-medium text-gray-900">2. Preview ({parsedRows.length} rows)</h3>
<div class="mb-6 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5">
<h3 class="mb-2 font-medium text-gray-900 dark:text-white">2. Preview ({parsedRows.length} rows)</h3>
<div class="max-h-64 overflow-auto">
<table class="w-full text-xs">
<thead class="bg-gray-50">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
{#each Object.keys(parsedRows[0]) as header}
<th class="px-2 py-1 text-left font-medium text-gray-500">{header}</th>
<th class="px-2 py-1 text-left font-medium text-gray-500 dark:text-gray-400">{header}</th>
{/each}
</tr>
</thead>
<tbody>
{#each parsedRows.slice(0, 20) as row}
<tr class="border-t border-gray-100">
<tr class="border-t border-gray-100 dark:border-gray-700">
{#each Object.values(row) as val}
<td class="px-2 py-1">{val}</td>
{/each}
@@ -101,28 +101,28 @@
</tbody>
</table>
{#if parsedRows.length > 20}
<p class="mt-2 text-xs text-gray-400">Showing first 20 of {parsedRows.length} rows</p>
<p class="mt-2 text-xs text-gray-400 dark:text-gray-500">Showing first 20 of {parsedRows.length} rows</p>
{/if}
</div>
</div>
<!-- Step 3: Import -->
<div class="rounded-lg border border-gray-200 bg-white p-5">
<h3 class="mb-2 font-medium text-gray-900">3. Import</h3>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5">
<h3 class="mb-2 font-medium text-gray-900 dark:text-white">3. Import</h3>
<form method="POST" action="?/import" use:enhance>
<input type="hidden" name="data" value={jsonData} />
<div class="mb-4 grid grid-cols-2 gap-3">
<div>
<label for="projectId" class="mb-1 block text-sm text-gray-700">Target Project</label>
<select id="projectId" name="projectId" required class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm">
<label for="projectId" class="mb-1 block text-sm text-gray-700 dark:text-gray-300">Target Project</label>
<select id="projectId" name="projectId" required class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm">
{#each data.projects as project}
<option value={project.id}>{project.name}</option>
{/each}
</select>
</div>
<div>
<label for="status" class="mb-1 block text-sm text-gray-700">Default Status</label>
<select id="status" name="status" class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm">
<label for="status" class="mb-1 block text-sm text-gray-700 dark:text-gray-300">Default Status</label>
<select id="status" name="status" class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm">
<option value="approved">Approved (import as finalized)</option>
<option value="pending">Pending (require approval)</option>
</select>
@@ -12,7 +12,7 @@
</svelte:head>
<div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900">Projects</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Projects</h2>
{#if data.companyRole !== 'viewer'}
<a
href="/companies/{data.company.id}/projects/new"
@@ -24,32 +24,32 @@
</div>
{#if data.projects.length === 0}
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center">
<p class="text-gray-500">No projects yet. Create your first one.</p>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-12 text-center">
<p class="text-gray-500 dark:text-gray-400">No projects yet. Create your first one.</p>
</div>
{:else}
<div class="grid gap-4 sm:grid-cols-2">
{#each data.projects as project}
<a
href="/companies/{data.company.id}/projects/{project.id}"
class="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow"
class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 hover:shadow-md transition-shadow"
>
<div class="flex items-center justify-between">
<h3 class="font-semibold text-gray-900">{project.name}</h3>
<h3 class="font-semibold text-gray-900 dark:text-white">{project.name}</h3>
{#if !project.isActive}
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-500">Inactive</span>
<span class="rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-0.5 text-xs text-gray-500 dark:text-gray-400">Inactive</span>
{/if}
</div>
{#if project.description}
<p class="mt-1 text-sm text-gray-500 line-clamp-2">{project.description}</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">{project.description}</p>
{/if}
<div class="mt-3">
<div class="flex justify-between text-sm">
<span class="text-gray-500">Budget</span>
<span class="text-gray-500 dark:text-gray-400">Budget</span>
<span>{formatCurrency(project.spent, currency)} / {formatCurrency(project.allocatedBudget, currency)}</span>
</div>
<div class="mt-1 h-2 w-full overflow-hidden rounded-full bg-gray-100">
<div class="mt-1 h-2 w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
<div
class="h-full rounded-full {budgetColor(budgetPercent(project.spent, project.allocatedBudget))}"
style="width: {budgetPercent(project.spent, project.allocatedBudget)}%"
@@ -57,7 +57,7 @@
</div>
</div>
<div class="mt-3 flex gap-4 text-xs text-gray-400">
<div class="mt-3 flex gap-4 text-xs text-gray-400 dark:text-gray-500">
<span>{project.expenseCount} expenses</span>
{#if project.pendingCount > 0}
<span class="text-amber-600">{project.pendingCount} pending</span>
@@ -14,9 +14,9 @@
<div>
<div class="mb-4 flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold text-gray-900">{data.project.name}</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{data.project.name}</h2>
{#if data.project.description}
<p class="text-sm text-gray-500">{data.project.description}</p>
<p class="text-sm text-gray-500 dark:text-gray-400">{data.project.description}</p>
{/if}
</div>
{#if canAddExpense}
@@ -31,30 +31,30 @@
<!-- Stats -->
<div class="mb-6 grid gap-4 sm:grid-cols-3">
<div class="rounded-lg border border-gray-200 bg-white p-4">
<p class="text-sm text-gray-500">Budget</p>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Budget</p>
<p class="text-xl font-bold">{formatCurrency(data.project.allocatedBudget, currency)}</p>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4">
<p class="text-sm text-gray-500">Spent (Approved)</p>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Spent (Approved)</p>
<p class="text-xl font-bold">{formatCurrency(data.stats.totalApproved, currency)}</p>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4">
<p class="text-sm text-gray-500">Pending</p>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Pending</p>
<p class="text-xl font-bold text-amber-600">{formatCurrency(data.stats.totalPending, currency)}</p>
</div>
</div>
<!-- Expense list -->
{#if data.expenses.length === 0}
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center">
<p class="text-gray-500">No expenses yet.</p>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-12 text-center">
<p class="text-gray-500 dark:text-gray-400">No expenses yet.</p>
</div>
{:else}
<div class="overflow-x-auto rounded-lg border border-gray-200 bg-white">
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr class="text-left text-gray-500">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr class="text-left text-gray-500 dark:text-gray-400">
<th class="px-4 py-3 font-medium">Title</th>
<th class="px-4 py-3 font-medium">Category</th>
<th class="px-4 py-3 font-medium">Amount</th>
@@ -65,20 +65,20 @@
</thead>
<tbody>
{#each data.expenses as expense}
<tr class="border-t border-gray-100">
<td class="px-4 py-3 font-medium text-gray-900">{expense.title}</td>
<td class="px-4 py-3 text-gray-500">{expense.categoryName ?? '—'}</td>
<tr class="border-t border-gray-100 dark:border-gray-700">
<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{expense.title}</td>
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{expense.categoryName ?? '—'}</td>
<td class="px-4 py-3">{formatCurrency(expense.amount, expense.currency)}</td>
<td class="px-4 py-3 text-gray-500">{expense.expenseDate}</td>
<td class="px-4 py-3 text-gray-500">{expense.submitterName ?? expense.submitterEmail}</td>
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{expense.expenseDate}</td>
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{expense.submitterName ?? expense.submitterEmail}</td>
<td class="px-4 py-3">
<span
class="rounded-full px-2 py-0.5 text-xs font-medium
{expense.status === 'approved'
? 'bg-green-100 text-green-700'
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
: expense.status === 'rejected'
? 'bg-red-100 text-red-700'
: 'bg-amber-100 text-amber-700'}"
? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300'}"
>
{expense.status}
</span>
@@ -10,40 +10,40 @@
</svelte:head>
<div class="mx-auto max-w-lg">
<h2 class="mb-4 text-lg font-semibold text-gray-900">Add Expense</h2>
<p class="mb-4 text-sm text-gray-500">Project: {data.projectName}</p>
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Add Expense</h2>
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">Project: {data.projectName}</p>
{#if form?.error}
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700">{form.error}</div>
<div class="mb-4 rounded-md bg-red-50 dark:bg-red-900/30 p-3 text-sm text-red-700 dark:text-red-300">{form.error}</div>
{/if}
<form method="POST" use:enhance class="rounded-lg border border-gray-200 bg-white p-6">
<form method="POST" use:enhance class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-6">
<div class="mb-4">
<label for="title" class="mb-1 block text-sm font-medium text-gray-700">Title</label>
<label for="title" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Title</label>
<input
type="text"
id="title"
name="title"
required
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
<div class="mb-4">
<label for="description" class="mb-1 block text-sm font-medium text-gray-700">
<label for="description" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Description
</label>
<textarea
id="description"
name="description"
rows="2"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
></textarea>
</div>
<div class="mb-4 grid grid-cols-2 gap-3">
<div>
<label for="amount" class="mb-1 block text-sm font-medium text-gray-700">Amount</label>
<label for="amount" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Amount</label>
<input
type="number"
id="amount"
@@ -51,30 +51,30 @@
step="0.01"
min="0.01"
required
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
<div>
<label for="expenseDate" class="mb-1 block text-sm font-medium text-gray-700">Date</label>
<label for="expenseDate" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Date</label>
<input
type="date"
id="expenseDate"
name="expenseDate"
required
value={new Date().toISOString().split('T')[0]}
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
</div>
<div class="mb-4">
<label for="categoryId" class="mb-1 block text-sm font-medium text-gray-700">
<label for="categoryId" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Category
</label>
<select
id="categoryId"
name="categoryId"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
>
<option value="">None</option>
{#each data.categories as cat}
@@ -85,10 +85,10 @@
{#if data.tags.length > 0}
<div class="mb-4">
<label class="mb-1 block text-sm font-medium text-gray-700">Tags</label>
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Tags</label>
<div class="flex flex-wrap gap-2">
{#each data.tags as tag}
<label class="flex items-center gap-1 rounded-md border border-gray-200 px-2 py-1 text-sm hover:bg-gray-50">
<label class="flex items-center gap-1 rounded-md border border-gray-200 dark:border-gray-600 px-2 py-1 text-sm hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" name="tagIds" value={tag.id} class="rounded" />
{tag.name}
</label>
@@ -100,7 +100,7 @@
<div class="flex justify-end gap-2">
<a
href="javascript:history.back()"
class="rounded-md px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
class="rounded-md px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
Cancel
</a>
@@ -10,38 +10,38 @@
</svelte:head>
<div class="mx-auto max-w-lg">
<h2 class="mb-4 text-lg font-semibold text-gray-900">Create Project</h2>
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Create Project</h2>
{#if form?.error}
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700">{form.error}</div>
<div class="mb-4 rounded-md bg-red-50 dark:bg-red-900/30 p-3 text-sm text-red-700 dark:text-red-300">{form.error}</div>
{/if}
<form method="POST" use:enhance class="rounded-lg border border-gray-200 bg-white p-6">
<form method="POST" use:enhance class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-6">
<div class="mb-4">
<label for="name" class="mb-1 block text-sm font-medium text-gray-700">Project Name</label>
<label for="name" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Project Name</label>
<input
type="text"
id="name"
name="name"
required
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
</div>
<div class="mb-6">
<label for="description" class="mb-1 block text-sm font-medium text-gray-700">
<label for="description" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Description
</label>
<textarea
id="description"
name="description"
rows="3"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
></textarea>
</div>
<div class="flex justify-end gap-2">
<a
href="javascript:history.back()"
class="rounded-md px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
class="rounded-md px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
Cancel
</a>
@@ -19,26 +19,26 @@
</svelte:head>
<div>
<h2 class="mb-4 text-lg font-semibold text-gray-900">Reports</h2>
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Reports</h2>
<!-- Date range filter -->
<div class="mb-6 flex items-end gap-3 rounded-lg border border-gray-200 bg-white p-4">
<div class="mb-6 flex items-end gap-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
<div>
<label for="from" class="mb-1 block text-sm text-gray-700">From</label>
<label for="from" class="mb-1 block text-sm text-gray-700 dark:text-gray-300">From</label>
<input
type="date"
id="from"
bind:value={from}
class="rounded-md border border-gray-300 px-3 py-2 text-sm"
class="rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm"
/>
</div>
<div>
<label for="to" class="mb-1 block text-sm text-gray-700">To</label>
<label for="to" class="mb-1 block text-sm text-gray-700 dark:text-gray-300">To</label>
<input
type="date"
id="to"
bind:value={to}
class="rounded-md border border-gray-300 px-3 py-2 text-sm"
class="rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm"
/>
</div>
<button
@@ -51,10 +51,10 @@
<div class="grid gap-6 lg:grid-cols-2">
<!-- By Category -->
<div class="rounded-lg border border-gray-200 bg-white p-5">
<h3 class="mb-3 font-medium text-gray-900">Spending by Category</h3>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5">
<h3 class="mb-3 font-medium text-gray-900 dark:text-white">Spending by Category</h3>
{#if data.byCategory.length === 0}
<p class="text-sm text-gray-500">No data for this period.</p>
<p class="text-sm text-gray-500 dark:text-gray-400">No data for this period.</p>
{:else}
<div class="space-y-2">
{#each data.byCategory as cat}
@@ -68,7 +68,7 @@
</div>
<span class="font-medium">{formatCurrency(cat.total, currency)}</span>
</div>
<div class="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-gray-100">
<div class="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
<div
class="h-full rounded-full"
style="width: {pct}%; background-color: {cat.categoryColor}"
@@ -81,10 +81,10 @@
</div>
<!-- By Project (Budget vs Actual) -->
<div class="rounded-lg border border-gray-200 bg-white p-5">
<h3 class="mb-3 font-medium text-gray-900">Budget vs Actual by Project</h3>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5">
<h3 class="mb-3 font-medium text-gray-900 dark:text-white">Budget vs Actual by Project</h3>
{#if data.byProject.length === 0}
<p class="text-sm text-gray-500">No data for this period.</p>
<p class="text-sm text-gray-500 dark:text-gray-400">No data for this period.</p>
{:else}
<div class="space-y-3">
{#each data.byProject as project}
@@ -99,7 +99,7 @@
</span>
</div>
<div class="mt-1 flex gap-1">
<div class="h-3 flex-1 overflow-hidden rounded-full bg-gray-100">
<div class="h-3 flex-1 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
<div
class="h-full rounded-full {pct > 90 ? 'bg-red-500' : pct > 70 ? 'bg-amber-500' : 'bg-blue-500'}"
style="width: {pct}%"
@@ -113,10 +113,10 @@
</div>
<!-- Monthly Trend -->
<div class="rounded-lg border border-gray-200 bg-white p-5 lg:col-span-2">
<h3 class="mb-3 font-medium text-gray-900">Monthly Spending</h3>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 lg:col-span-2">
<h3 class="mb-3 font-medium text-gray-900 dark:text-white">Monthly Spending</h3>
{#if data.byMonth.length === 0}
<p class="text-sm text-gray-500">No data for this period.</p>
<p class="text-sm text-gray-500 dark:text-gray-400">No data for this period.</p>
{:else}
{@const maxVal = Math.max(...data.byMonth.map((m) => parseFloat(m.total)))}
<div class="flex items-end gap-2" style="height: 200px;">
@@ -124,12 +124,12 @@
{@const val = parseFloat(month.total)}
{@const height = maxVal > 0 ? (val / maxVal) * 100 : 0}
<div class="flex flex-1 flex-col items-center gap-1">
<span class="text-xs text-gray-500">{formatCurrency(val, currency)}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{formatCurrency(val, currency)}</span>
<div
class="w-full rounded-t bg-blue-500"
style="height: {height}%"
></div>
<span class="text-xs text-gray-400">{month.month}</span>
<span class="text-xs text-gray-400 dark:text-gray-500">{month.month}</span>
</div>
{/each}
</div>
@@ -12,38 +12,38 @@
<div class="space-y-8">
{#if form?.error}
<div class="rounded-md bg-red-50 p-3 text-sm text-red-700">{form.error}</div>
<div class="rounded-md bg-red-50 dark:bg-red-900/30 p-3 text-sm text-red-700 dark:text-red-300">{form.error}</div>
{/if}
{#if form?.message}
<div class="rounded-md bg-green-50 p-3 text-sm text-green-700">{form.message}</div>
<div class="rounded-md bg-green-50 dark:bg-green-900/30 p-3 text-sm text-green-700 dark:text-green-300">{form.message}</div>
{/if}
<!-- Company details -->
{#if isAdmin}
<div class="rounded-lg border border-gray-200 bg-white p-5">
<h2 class="mb-4 font-semibold text-gray-900">Company Details</h2>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5">
<h2 class="mb-4 font-semibold text-gray-900 dark:text-white">Company Details</h2>
<form method="POST" action="?/updateCompany" use:enhance>
<div class="mb-4">
<label for="name" class="mb-1 block text-sm font-medium text-gray-700">Name</label>
<label for="name" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
<input
type="text"
id="name"
name="name"
required
value={data.company.name}
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm"
/>
</div>
<div class="mb-4">
<label for="description" class="mb-1 block text-sm font-medium text-gray-700">Description</label>
<label for="description" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
<textarea
id="description"
name="description"
rows="2"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm"
>{data.company.description ?? ''}</textarea>
</div>
<p class="mb-4 text-sm text-gray-500">
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">
To add budget, go to the <a href="/companies/{data.company.id}/budget" class="font-medium text-blue-600 hover:text-blue-500">Budget</a> page and use the "+ Add Budget" button.
</p>
<button type="submit" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
@@ -54,25 +54,25 @@
{/if}
<!-- Members -->
<div class="rounded-lg border border-gray-200 bg-white p-5">
<h2 class="mb-4 font-semibold text-gray-900">Members</h2>
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5">
<h2 class="mb-4 font-semibold text-gray-900 dark:text-white">Members</h2>
{#if isAdmin}
<form method="POST" action="?/addMember" use:enhance class="mb-4 flex items-end gap-3">
<div class="flex-1">
<label for="email" class="mb-1 block text-sm text-gray-700">Add Member by Email</label>
<label for="email" class="mb-1 block text-sm text-gray-700 dark:text-gray-300">Add Member by Email</label>
<input
type="email"
id="email"
name="email"
required
placeholder="user@example.com"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm"
/>
</div>
<div class="w-32">
<label for="role" class="mb-1 block text-sm text-gray-700">Role</label>
<select id="role" name="role" class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm">
<label for="role" class="mb-1 block text-sm text-gray-700 dark:text-gray-300">Role</label>
<select id="role" name="role" class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm">
<option value="viewer">Viewer</option>
<option value="user">User</option>
<option value="manager">Manager</option>
@@ -86,8 +86,8 @@
{/if}
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr class="text-left text-gray-500">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr class="text-left text-gray-500 dark:text-gray-400">
<th class="px-4 py-3 font-medium">User</th>
<th class="px-4 py-3 font-medium">Email</th>
<th class="px-4 py-3 font-medium">Role</th>
@@ -98,9 +98,9 @@
</thead>
<tbody>
{#each data.members as member}
<tr class="border-t border-gray-100">
<tr class="border-t border-gray-100 dark:border-gray-700">
<td class="px-4 py-3">{member.displayName ?? '—'}</td>
<td class="px-4 py-3 text-gray-500">{member.email}</td>
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{member.email}</td>
<td class="px-4 py-3">
{#if isAdmin}
<form method="POST" action="?/updateRole" use:enhance class="inline">
@@ -108,7 +108,7 @@
<select
name="role"
onchange={(e) => e.currentTarget.form?.requestSubmit()}
class="rounded border border-gray-300 px-2 py-1 text-sm"
class="rounded border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-2 py-1 text-sm"
>
{#each ['viewer', 'user', 'manager', 'admin'] as role}
<option value={role} selected={member.role === role}>{role}</option>
+14 -14
View File
@@ -12,7 +12,7 @@
<div class="mx-auto max-w-6xl">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Dashboard</h1>
{#if data.user?.isSystemAdmin}
<a
href="/companies"
@@ -24,12 +24,12 @@
</div>
{#if data.companySummaries.length === 0}
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center">
<svg class="mx-auto h-12 w-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-12 text-center">
<svg class="mx-auto h-12 w-12 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h2 class="mt-4 text-lg font-medium text-gray-900">Waiting for access</h2>
<p class="mt-2 text-sm text-gray-500">
<h2 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">Waiting for access</h2>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
You haven't been assigned to any company yet. Ask an administrator to invite you.
</p>
</div>
@@ -38,12 +38,12 @@
{#each data.companySummaries as company}
<a
href="/companies/{company.id}"
class="rounded-lg border border-gray-200 bg-white p-6 transition-shadow hover:shadow-md"
class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-6 transition-shadow hover:shadow-md"
>
<div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900">{company.name}</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{company.name}</h2>
<span
class="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700"
class="rounded-full bg-blue-100 dark:bg-blue-900/40 px-2 py-0.5 text-xs font-medium text-blue-700 dark:text-blue-300"
>
{company.role}
</span>
@@ -51,33 +51,33 @@
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-500">Budget</span>
<span class="text-gray-500 dark:text-gray-400">Budget</span>
<span class="font-medium">{formatCurrency(company.totalBudget, company.currency)}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Spent</span>
<span class="text-gray-500 dark:text-gray-400">Spent</span>
<span class="font-medium">{formatCurrency(company.totalSpent, company.currency)}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Projects</span>
<span class="text-gray-500 dark:text-gray-400">Projects</span>
<span class="font-medium">{company.projectCount}</span>
</div>
{#if company.pendingExpenses > 0}
<div class="flex justify-between">
<span class="text-gray-500">Pending Approvals</span>
<span class="text-gray-500 dark:text-gray-400">Pending Approvals</span>
<span class="font-medium text-amber-600">{company.pendingExpenses}</span>
</div>
{/if}
</div>
<div class="mt-4">
<div class="h-2 w-full overflow-hidden rounded-full bg-gray-100">
<div class="h-2 w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
<div
class="h-full rounded-full transition-all {budgetColor(budgetPercent(company.totalSpent, company.totalBudget))}"
style="width: {budgetPercent(company.totalSpent, company.totalBudget)}%"
></div>
</div>
<p class="mt-1 text-xs text-gray-400">{budgetPercent(company.totalSpent, company.totalBudget).toFixed(1)}% spent</p>
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">{budgetPercent(company.totalSpent, company.totalBudget).toFixed(1)}% spent</p>
</div>
</a>
{/each}
+6 -1
View File
@@ -1,8 +1,13 @@
<script lang="ts">
import ThemeToggle from '$lib/components/layout/ThemeToggle.svelte';
let { children } = $props();
</script>
<div class="flex min-h-screen items-center justify-center bg-gray-50">
<div class="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
<div class="absolute top-4 right-4">
<ThemeToggle />
</div>
<div class="w-full max-w-md">
{@render children()}
</div>
+12 -12
View File
@@ -9,36 +9,36 @@
<title>Login - Buildfor Life Budget</title>
</svelte:head>
<div class="rounded-lg border border-gray-200 bg-white p-8 shadow-sm">
<h1 class="mb-6 text-center text-2xl font-bold text-gray-900">Sign In</h1>
<div class="rounded-lg border border-gray-200 bg-white p-8 shadow-sm dark:border-gray-700 dark:bg-gray-800">
<h1 class="mb-6 text-center text-2xl font-bold text-gray-900 dark:text-white">Sign In</h1>
{#if form?.error}
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700">
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-400">
{form.error}
</div>
{/if}
<form method="POST" use:enhance>
<div class="mb-4">
<label for="email" class="mb-1 block text-sm font-medium text-gray-700">Email</label>
<label for="email" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
<input
type="email"
id="email"
name="email"
required
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"
value={form?.email ?? ''}
/>
</div>
<div class="mb-6">
<label for="password" class="mb-1 block text-sm font-medium text-gray-700">Password</label>
<label for="password" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
<input
type="password"
id="password"
name="password"
required
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
@@ -54,23 +54,23 @@
<div class="mt-4">
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
<div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="bg-white px-2 text-gray-500">Or continue with</span>
<span class="bg-white px-2 text-gray-500 dark:bg-gray-800 dark:text-gray-400">Or continue with</span>
</div>
</div>
<a
href="/oidc"
class="mt-4 flex w-full items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
class="mt-4 flex w-full items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
>
Single Sign-On (SSO)
</a>
</div>
{/if}
<p class="mt-4 text-center text-sm text-gray-600">
<p class="mt-4 text-center text-sm text-gray-600 dark:text-gray-400">
Don't have an account?
<a href="/signup" class="font-medium text-blue-600 hover:text-blue-500">Sign up</a>
<a href="/signup" class="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400">Sign up</a>
</p>
</div>
+13 -13
View File
@@ -9,18 +9,18 @@
<title>Sign Up - Buildfor Life Budget</title>
</svelte:head>
<div class="rounded-lg border border-gray-200 bg-white p-8 shadow-sm">
<h1 class="mb-6 text-center text-2xl font-bold text-gray-900">Create Account</h1>
<div class="rounded-lg border border-gray-200 bg-white p-8 shadow-sm dark:border-gray-700 dark:bg-gray-800">
<h1 class="mb-6 text-center text-2xl font-bold text-gray-900 dark:text-white">Create Account</h1>
{#if form?.error}
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700">
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-400">
{form.error}
</div>
{/if}
<form method="POST" use:enhance>
<div class="mb-4">
<label for="displayName" class="mb-1 block text-sm font-medium text-gray-700">
<label for="displayName" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Display Name
</label>
<input
@@ -28,37 +28,37 @@
id="displayName"
name="displayName"
required
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value={form?.displayName ?? ''}
/>
</div>
<div class="mb-4">
<label for="email" class="mb-1 block text-sm font-medium text-gray-700">Email</label>
<label for="email" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
<input
type="email"
id="email"
name="email"
required
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value={form?.email ?? ''}
/>
</div>
<div class="mb-4">
<label for="password" class="mb-1 block text-sm font-medium text-gray-700">Password</label>
<label for="password" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
<input
type="password"
id="password"
name="password"
required
minlength="8"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
<div class="mb-6">
<label for="confirmPassword" class="mb-1 block text-sm font-medium text-gray-700">
<label for="confirmPassword" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Confirm Password
</label>
<input
@@ -67,7 +67,7 @@
name="confirmPassword"
required
minlength="8"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
</div>
@@ -79,8 +79,8 @@
</button>
</form>
<p class="mt-4 text-center text-sm text-gray-600">
<p class="mt-4 text-center text-sm text-gray-600 dark:text-gray-400">
Already have an account?
<a href="/login" class="font-medium text-blue-600 hover:text-blue-500">Sign in</a>
<a href="/login" class="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400">Sign in</a>
</p>
</div>