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
+2
View File
@@ -1 +1,3 @@
@import 'tailwindcss'; @import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));
+8
View File
@@ -4,6 +4,14 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function() {
var t = localStorage.getItem('theme');
if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
</script>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
+11 -11
View File
@@ -12,20 +12,20 @@
</script> </script>
<aside <aside
class="flex w-64 flex-col border-r border-gray-200 bg-white transition-transform duration-200 {open class="flex w-64 flex-col border-r border-gray-200 bg-white transition-transform duration-200 dark:border-gray-700 dark:bg-gray-800 {open
? 'translate-x-0' ? 'translate-x-0'
: '-translate-x-full'} fixed inset-y-0 left-0 z-30 lg:relative lg:translate-x-0" : '-translate-x-full'} fixed inset-y-0 left-0 z-30 lg:relative lg:translate-x-0"
> >
<!-- Logo --> <!-- Logo -->
<div class="flex h-14 items-center border-b border-gray-200 px-4"> <div class="flex h-14 items-center border-b border-gray-200 px-4 dark:border-gray-700">
<a href="/dashboard" class="text-lg font-bold text-gray-900">B4L Budget</a> <a href="/dashboard" class="text-lg font-bold text-gray-900 dark:text-white">B4L Budget</a>
</div> </div>
<!-- Navigation --> <!-- Navigation -->
<nav class="flex-1 overflow-y-auto px-3 py-4"> <nav class="flex-1 overflow-y-auto px-3 py-4">
<a <a
href="/dashboard" href="/dashboard"
class="mb-1 flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100" class="mb-1 flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
> >
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
@@ -34,32 +34,32 @@
</a> </a>
{#if companies.length > 0} {#if companies.length > 0}
<div class="mt-4 mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400"> <div class="mt-4 mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
Companies Companies
</div> </div>
{#each companies as company} {#each companies as company}
<a <a
href="/companies/{company.companyId}" href="/companies/{company.companyId}"
class="mb-0.5 flex items-center gap-2 rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100" class="mb-0.5 flex items-center gap-2 rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
> >
<span <span
class="flex h-5 w-5 items-center justify-center rounded bg-blue-100 text-xs font-medium text-blue-700" class="flex h-5 w-5 items-center justify-center rounded bg-blue-100 text-xs font-medium text-blue-700 dark:bg-blue-900 dark:text-blue-300"
> >
{company.companyName[0]?.toUpperCase()} {company.companyName[0]?.toUpperCase()}
</span> </span>
<span class="truncate">{company.companyName}</span> <span class="truncate">{company.companyName}</span>
<span class="ml-auto text-xs text-gray-400">{company.role}</span> <span class="ml-auto text-xs text-gray-400 dark:text-gray-500">{company.role}</span>
</a> </a>
{/each} {/each}
{/if} {/if}
{#if user.isSystemAdmin} {#if user.isSystemAdmin}
<div class="mt-4 mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400"> <div class="mt-4 mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
Administration Administration
</div> </div>
<a <a
href="/admin/users" href="/admin/users"
class="mb-0.5 flex items-center gap-2 rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100" class="mb-0.5 flex items-center gap-2 rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
> >
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
@@ -68,7 +68,7 @@
</a> </a>
<a <a
href="/admin/settings" href="/admin/settings"
class="mb-0.5 flex items-center gap-2 rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100" class="mb-0.5 flex items-center gap-2 rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
> >
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
@@ -0,0 +1,19 @@
<script lang="ts">
import { theme } from '$lib/stores/theme.svelte.js';
</script>
<button
onclick={() => theme.toggle()}
class="rounded-md p-1.5 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
title="Toggle {theme.isDark ? 'light' : 'dark'} mode"
>
{#if theme.isDark}
<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="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
{:else}
<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="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
{/if}
</button>
+40
View File
@@ -0,0 +1,40 @@
import { browser } from '$app/environment';
type Theme = 'light' | 'dark';
function getInitialTheme(): Theme {
if (!browser) return 'light';
const stored = localStorage.getItem('theme');
if (stored === 'dark' || stored === 'light') return stored;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
let current = $state<Theme>(getInitialTheme());
export const theme = {
get value() {
return current;
},
get isDark() {
return current === 'dark';
},
toggle() {
current = current === 'dark' ? 'light' : 'dark';
apply();
},
set(t: Theme) {
current = t;
apply();
}
};
function apply() {
if (!browser) return;
document.documentElement.classList.toggle('dark', current === 'dark');
localStorage.setItem('theme', current);
}
// Apply on load
if (browser) {
apply();
}
+7 -5
View File
@@ -1,12 +1,13 @@
<script lang="ts"> <script lang="ts">
import type { LayoutData } from './$types'; import type { LayoutData } from './$types';
import Sidebar from '$lib/components/layout/Sidebar.svelte'; import Sidebar from '$lib/components/layout/Sidebar.svelte';
import ThemeToggle from '$lib/components/layout/ThemeToggle.svelte';
let { data, children } = $props(); let { data, children } = $props();
let sidebarOpen = $state(true); let sidebarOpen = $state(true);
</script> </script>
<div class="flex h-screen bg-gray-50"> <div class="flex h-screen bg-gray-50 dark:bg-gray-900">
<Sidebar <Sidebar
user={data.user} user={data.user}
companies={data.companies} companies={data.companies}
@@ -16,10 +17,10 @@
<div class="flex flex-1 flex-col overflow-hidden"> <div class="flex flex-1 flex-col overflow-hidden">
<!-- Top bar --> <!-- 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 <button
onclick={() => (sidebarOpen = !sidebarOpen)} 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"> <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" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
@@ -27,11 +28,12 @@
</button> </button>
<div class="flex items-center gap-3 ml-auto"> <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"> <form method="POST" action="/logout">
<button <button
type="submit" 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 Sign Out
</button> </button>
+9 -9
View File
@@ -9,28 +9,28 @@
</svelte:head> </svelte:head>
<div class="mx-auto max-w-2xl"> <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="space-y-4">
<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">
<h2 class="mb-3 font-medium text-gray-900">System Status</h2> <h2 class="mb-3 font-medium text-gray-900 dark:text-white">System Status</h2>
<div class="space-y-2 text-sm"> <div class="space-y-2 text-sm">
<div class="flex justify-between"> <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> <span class="font-medium text-green-600">{data.databaseUrl}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500">OIDC</span> <span class="text-gray-500 dark:text-gray-400">OIDC</span>
<span class="font-medium {data.oidcConfigured ? 'text-green-600' : 'text-gray-400'}"> <span class="font-medium {data.oidcConfigured ? 'text-green-600' : 'text-gray-400 dark:text-gray-500'}">
{data.oidcConfigured ? 'Configured' : 'Not configured'} {data.oidcConfigured ? 'Configured' : 'Not configured'}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<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">
<h2 class="mb-3 font-medium text-gray-900">Configuration</h2> <h2 class="mb-3 font-medium text-gray-900 dark:text-white">Configuration</h2>
<p class="text-sm text-gray-500"> <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 System configuration is managed via environment variables. See the <code>.env.example</code> file
for available options. for available options.
</p> </p>
+21 -21
View File
@@ -13,19 +13,19 @@
</svelte:head> </svelte:head>
<div class="mx-auto max-w-5xl"> <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} {#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}
{#if form?.deleted} {#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} {/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"> <table class="w-full text-sm">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-800/50">
<tr class="text-left text-gray-500"> <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">Name</th>
<th class="px-4 py-3 font-medium">Email</th> <th class="px-4 py-3 font-medium">Email</th>
<th class="px-4 py-3 font-medium">Status</th> <th class="px-4 py-3 font-medium">Status</th>
@@ -37,34 +37,34 @@
</thead> </thead>
<tbody> <tbody>
{#each data.users as user} {#each data.users as user}
<tr class="border-t border-gray-100 {user.disabledAt ? 'bg-red-50/50 opacity-70' : ''}"> <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">{user.displayName ?? '—'}</td> <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">{user.email}</td> <td class="px-4 py-3 text-gray-500 dark:text-gray-400">{user.email}</td>
<td class="px-4 py-3"> <td class="px-4 py-3">
{#if user.disabledAt} {#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} {: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} {/if}
</td> </td>
<td class="px-4 py-3"> <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'} {user.oidcProvider ? 'SSO' : 'Local'}
</span> </span>
</td> </td>
<td class="px-4 py-3"> <td class="px-4 py-3">
{#if user.isSystemAdmin} {#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} {/if}
</td> </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"> <td class="px-4 py-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<form method="POST" action="?/toggleAdmin" use:enhance> <form method="POST" action="?/toggleAdmin" use:enhance>
<input type="hidden" name="userId" value={user.id} /> <input type="hidden" name="userId" value={user.id} />
<button <button
type="submit" 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'} {user.isSystemAdmin ? 'Remove Admin' : 'Make Admin'}
</button> </button>
@@ -74,7 +74,7 @@
<input type="hidden" name="userId" value={user.id} /> <input type="hidden" name="userId" value={user.id} />
<button <button
type="submit" 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'} {user.disabledAt ? 'Enable' : 'Disable'}
</button> </button>
@@ -82,7 +82,7 @@
<button <button
onclick={() => { confirmDeleteId = user.id; confirmDeleteName = user.displayName ?? user.email; }} 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 Delete
</button> </button>
@@ -98,19 +98,19 @@
<!-- Permanent delete confirmation modal --> <!-- Permanent delete confirmation modal -->
{#if confirmDeleteId} {#if confirmDeleteId}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> <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> <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. This will permanently delete <strong>{confirmDeleteName}</strong> and remove them from all companies. This action cannot be undone.
</p> </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. Their submitted expenses will be preserved but no longer linked to their account.
</p> </p>
<div class="mt-4 flex justify-end gap-2"> <div class="mt-4 flex justify-end gap-2">
<button <button
type="button" type="button"
onclick={() => (confirmDeleteId = null)} 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 Cancel
</button> </button>
+28 -28
View File
@@ -15,7 +15,7 @@
<div class="mx-auto max-w-6xl"> <div class="mx-auto max-w-6xl">
<div class="mb-6 flex items-center justify-between"> <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} {#if data.isSystemAdmin}
<button <button
onclick={() => (showCreateModal = true)} onclick={() => (showCreateModal = true)}
@@ -27,34 +27,34 @@
</div> </div>
{#if form?.error} {#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}
{#if form?.deleted} {#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}
{#if data.companies.length === 0} {#if data.companies.length === 0}
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center"> <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" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> <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> </svg>
<h2 class="mt-4 text-lg font-medium text-gray-900">No companies yet</h2> <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"> <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. You haven't been assigned to any company yet. Ask an administrator to invite you.
</p> </p>
</div> </div>
{:else} {:else}
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each data.companies as company} {#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"> <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} {#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} {/if}
<div class="mt-3 flex items-center justify-between text-sm"> <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="text-gray-500 dark:text-gray-400">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="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} {company.role}
</span> </span>
</div> </div>
@@ -62,7 +62,7 @@
{#if data.isSystemAdmin} {#if data.isSystemAdmin}
<button <button
onclick={(e) => { e.stopPropagation(); confirmDeleteId = company.id; confirmDeleteName = company.name; }} 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" title="Archive company"
> >
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -79,33 +79,33 @@
<!-- Create company modal (admin only) --> <!-- Create company modal (admin only) -->
{#if showCreateModal && data.isSystemAdmin} {#if showCreateModal && data.isSystemAdmin}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> <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"> <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">Create Company</h2> <h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Create Company</h2>
<form method="POST" action="?/create" use:enhance> <form method="POST" action="?/create" use:enhance>
<div class="mb-4"> <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 <input
type="text" type="text"
id="name" id="name"
name="name" name="name"
required 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>
<div class="mb-4"> <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 Description
</label> </label>
<textarea <textarea
id="description" id="description"
name="description" name="description"
rows="2" 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> ></textarea>
</div> </div>
<div class="mb-4 grid grid-cols-2 gap-3"> <div class="mb-4 grid grid-cols-2 gap-3">
<div> <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 Initial Budget
</label> </label>
<input <input
@@ -114,17 +114,17 @@
name="totalBudget" name="totalBudget"
step="0.01" step="0.01"
value="0" 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>
<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 Currency
</label> </label>
<select <select
id="currency" id="currency"
name="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="THB" selected>THB</option>
<option value="USD">USD</option> <option value="USD">USD</option>
@@ -136,7 +136,7 @@
<button <button
type="button" type="button"
onclick={() => (showCreateModal = false)} 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 Cancel
</button> </button>
@@ -155,16 +155,16 @@
<!-- Delete confirmation modal --> <!-- Delete confirmation modal -->
{#if confirmDeleteId} {#if confirmDeleteId}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> <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-gray-900">Archive Company</h2> <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Archive Company</h2>
<p class="mt-2 text-sm text-gray-500"> <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. Are you sure you want to archive <strong>{confirmDeleteName}</strong>? The company and all its data will be hidden but not permanently deleted.
</p> </p>
<div class="mt-4 flex justify-end gap-2"> <div class="mt-4 flex justify-end gap-2">
<button <button
type="button" type="button"
onclick={() => (confirmDeleteId = null)} 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 Cancel
</button> </button>
@@ -21,18 +21,18 @@
<div> <div>
<div class="mb-6"> <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} {#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} {/if}
</div> </div>
<!-- Tabs --> <!-- 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} {#each tabs as tab}
<a <a
href={tab.href} 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} {tab.label}
</a> </a>
@@ -18,12 +18,12 @@
<div class="grid gap-6 lg:grid-cols-2"> <div class="grid gap-6 lg:grid-cols-2">
<!-- Budget Summary --> <!-- 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> <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)} {formatCurrency(remaining, currency)}
</div> </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 <div
class="h-full rounded-full transition-all {remaining < 0 ? 'bg-red-500' : remainingPct < 20 ? 'bg-amber-500' : 'bg-green-500'}" 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))}%" 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="mt-4 space-y-1.5 text-sm">
<div class="flex justify-between"> <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="{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>
<div class="flex justify-between"> <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="{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>
<div class="flex justify-between"> <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="{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> </div>
</div> </div>
<!-- Projects --> <!-- 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"> <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'} {#if data.companyRole !== 'viewer'}
<a <a
href="/companies/{data.company.id}/projects/new" href="/companies/{data.company.id}/projects/new"
@@ -62,7 +62,7 @@
</div> </div>
{#if data.projects.length === 0} {#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} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each data.projects as project} {#each data.projects as project}
@@ -71,12 +71,12 @@
{@const pct = budgetNum > 0 ? Math.min((spentNum / budgetNum) * 100, 100) : 0} {@const pct = budgetNum > 0 ? Math.min((spentNum / budgetNum) * 100, 100) : 0}
<a href="/companies/{data.company.id}/projects/{project.id}" class="block"> <a href="/companies/{data.company.id}/projects/{project.id}" class="block">
<div class="flex items-center justify-between text-sm"> <div class="flex items-center justify-between text-sm">
<span class="font-medium text-gray-900">{project.name}</span> <span class="font-medium text-gray-900 dark:text-white">{project.name}</span>
<span class="text-gray-500"> <span class="text-gray-500 dark:text-gray-400">
{formatCurrency(project.spent, currency)} / {formatCurrency(project.allocatedBudget, currency)} {formatCurrency(project.spent, currency)} / {formatCurrency(project.allocatedBudget, currency)}
</span> </span>
</div> </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 <div
class="h-full rounded-full {pct > 90 ? 'bg-red-500' : pct > 70 ? 'bg-amber-500' : 'bg-blue-500'}" class="h-full rounded-full {pct > 90 ? 'bg-red-500' : pct > 70 ? 'bg-amber-500' : 'bg-blue-500'}"
style="width: {pct}%" style="width: {pct}%"
@@ -92,14 +92,14 @@
</div> </div>
<!-- Recent Expenses --> <!-- Recent Expenses -->
<div class="rounded-lg border border-gray-200 bg-white p-5 lg:col-span-2"> <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">Recent Expenses</h2> <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} {#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} {:else}
<table class="w-full text-sm"> <table class="w-full text-sm">
<thead> <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">Title</th>
<th class="pb-2 font-medium">Project</th> <th class="pb-2 font-medium">Project</th>
<th class="pb-2 font-medium">Amount</th> <th class="pb-2 font-medium">Amount</th>
@@ -109,19 +109,19 @@
</thead> </thead>
<tbody> <tbody>
{#each data.recentExpenses as expense} {#each data.recentExpenses as expense}
<tr class="border-b border-gray-50"> <tr class="border-b border-gray-50 dark:border-gray-700/50">
<td class="py-2 font-medium text-gray-900">{expense.title}</td> <td class="py-2 font-medium text-gray-900 dark:text-white">{expense.title}</td>
<td class="py-2 text-gray-500">{expense.projectName}</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">{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"> <td class="py-2">
<span <span
class="rounded-full px-2 py-0.5 text-xs font-medium class="rounded-full px-2 py-0.5 text-xs font-medium
{expense.status === 'approved' {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' : expense.status === 'rejected'
? 'bg-red-100 text-red-700' ? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
: 'bg-amber-100 text-amber-700'}" : 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300'}"
> >
{expense.status} {expense.status}
</span> </span>
@@ -18,24 +18,24 @@
function getEventStyle(event: string) { function getEventStyle(event: string) {
const styles: Record<string, { icon: string; bg: string; text: string; badge: string; label: 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_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', text: 'text-gray-600', badge: 'bg-gray-100 text-gray-600', label: 'Updated' }, 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', text: 'text-green-700', badge: 'bg-green-100 text-green-700', label: 'Initial Budget' }, 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', text: 'text-green-700', badge: 'bg-green-100 text-green-700', label: 'Budget Added' }, 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', text: 'text-blue-700', badge: 'bg-blue-100 text-blue-700', label: 'Allocated' }, 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', text: 'text-amber-700', badge: 'bg-amber-100 text-amber-700', label: 'Deallocated' }, 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', text: 'text-indigo-700', badge: 'bg-indigo-100 text-indigo-700', label: 'Project' }, 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', text: 'text-gray-600', badge: 'bg-gray-100 text-gray-600', 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', text: 'text-purple-700', badge: 'bg-purple-100 text-purple-700', label: 'Member' }, 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', text: 'text-red-700', badge: 'bg-red-100 text-red-700', 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', text: 'text-purple-700', badge: 'bg-purple-100 text-purple-700', label: 'Role Change' }, 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', text: 'text-amber-700', badge: 'bg-amber-100 text-amber-700', label: 'Expense' }, 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', text: 'text-green-700', badge: 'bg-green-100 text-green-700', label: 'Approved' }, 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', text: 'text-red-700', badge: 'bg-red-100 text-red-700', label: 'Rejected' }, 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', text: 'text-gray-600', badge: 'bg-gray-100 text-gray-600', label: 'Category' }, 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', text: 'text-blue-700', badge: 'bg-blue-100 text-blue-700', label: 'Import' }, 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> </script>
@@ -45,7 +45,7 @@
<div> <div>
<div class="mb-4 flex items-center justify-between"> <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} {#if isAdmin}
<button <button
onclick={() => (showAddBudget = !showAddBudget)} onclick={() => (showAddBudget = !showAddBudget)}
@@ -60,13 +60,13 @@
</div> </div>
{#if form?.error} {#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}
<!-- Add Budget form (admin only) --> <!-- Add Budget form (admin only) -->
{#if showAddBudget && isAdmin} {#if showAddBudget && isAdmin}
<div class="mb-6 rounded-lg border-2 border-green-200 bg-green-50 p-5"> <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">Replenish Company Budget</h3> <h3 class="mb-3 font-medium text-gray-900 dark:text-white">Replenish Company Budget</h3>
<form method="POST" action="?/addBudget" use:enhance={() => { <form method="POST" action="?/addBudget" use:enhance={() => {
return async ({ update }) => { return async ({ update }) => {
await update(); await update();
@@ -74,7 +74,7 @@
}; };
}} class="flex items-end gap-3"> }} class="flex items-end gap-3">
<div class="flex-1"> <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 <input
type="number" type="number"
id="addAmount" id="addAmount"
@@ -83,7 +83,7 @@
min="0.01" min="0.01"
required required
placeholder="e.g. 100000" 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> </div>
<button <button
@@ -95,7 +95,7 @@
<button <button
type="button" type="button"
onclick={() => (showAddBudget = false)} 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 Cancel
</button> </button>
@@ -106,12 +106,12 @@
<!-- Summary --> <!-- Summary -->
<div class="mb-6 grid gap-4 sm:grid-cols-4"> <div class="mb-6 grid gap-4 sm:grid-cols-4">
<!-- Remaining — hero card --> <!-- 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="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)} {formatCurrency(remaining, currency)}
</p> </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 <div
class="h-full rounded-full transition-all {remaining < 0 ? 'bg-red-500' : remainingPct < 20 ? 'bg-amber-500' : 'bg-green-500'}" 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))}%" style="width: {Math.max(0, Math.min(remainingPct, 100))}%"
@@ -123,27 +123,27 @@
</div> </div>
<!-- Total Budget --> <!-- Total Budget -->
<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">
<p class="text-xs font-medium uppercase tracking-wider text-gray-400">Total Budget</p> <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">{formatCurrency(total, currency)}</p> <p class="mt-1 text-lg font-bold text-gray-900 dark:text-white">{formatCurrency(total, currency)}</p>
</div> </div>
<!-- Total Spent --> <!-- Total Spent -->
<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">
<p class="text-xs font-medium uppercase tracking-wider text-gray-400">Total Spent</p> <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">{formatCurrency(totalSpent, currency)}</p> <p class="mt-1 text-lg font-bold text-gray-900 dark:text-white">{formatCurrency(totalSpent, currency)}</p>
</div> </div>
<!-- Allocated to Projects --> <!-- Allocated to Projects -->
<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">
<p class="text-xs font-medium uppercase tracking-wider text-gray-400">Allocated</p> <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">{formatCurrency(data.totalAllocated, currency)}</p> <p class="mt-1 text-lg font-bold text-gray-900 dark:text-white">{formatCurrency(data.totalAllocated, currency)}</p>
</div> </div>
<!-- Unallocated --> <!-- Unallocated -->
<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">
<p class="text-xs font-medium uppercase tracking-wider text-gray-400">Unallocated</p> <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'}"> <p class="mt-1 text-lg font-bold {unallocated < 0 ? 'text-red-600' : 'text-gray-900 dark:text-white'}">
{formatCurrency(unallocated, currency)} {formatCurrency(unallocated, currency)}
</p> </p>
</div> </div>
@@ -151,16 +151,16 @@
<!-- Allocate to project form --> <!-- Allocate to project form -->
{#if canAllocate && data.projects.length > 0} {#if canAllocate && data.projects.length > 0}
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5"> <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">Allocate Funds to Project</h3> <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"> <form method="POST" action="?/allocate" use:enhance class="flex flex-wrap items-end gap-3">
<div class="flex-1"> <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 <select
id="projectId" id="projectId"
name="projectId" name="projectId"
required 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} {#each data.projects as project}
<option value={project.id}>{project.name}</option> <option value={project.id}>{project.name}</option>
@@ -168,24 +168,24 @@
</select> </select>
</div> </div>
<div class="w-36"> <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 <input
type="number" type="number"
id="amount" id="amount"
name="amount" name="amount"
step="0.01" step="0.01"
required 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" placeholder="Negative to deallocate"
/> />
</div> </div>
<div class="flex-1"> <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 <input
type="text" type="text"
id="note" id="note"
name="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> </div>
<button <button
@@ -199,10 +199,10 @@
{/if} {/if}
<!-- Project budgets --> <!-- 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"> <table class="w-full text-sm">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-800/50">
<tr class="text-left text-gray-500"> <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">Project</th>
<th class="px-4 py-3 font-medium">Allocated</th> <th class="px-4 py-3 font-medium">Allocated</th>
<th class="px-4 py-3 font-medium">Spent</th> <th class="px-4 py-3 font-medium">Spent</th>
@@ -216,13 +216,13 @@
{@const spent = parseFloat(project.spent)} {@const spent = parseFloat(project.spent)}
{@const remaining = allocated - spent} {@const remaining = allocated - spent}
{@const pct = allocated > 0 ? Math.min((spent / allocated) * 100, 100) : 0} {@const pct = allocated > 0 ? Math.min((spent / allocated) * 100, 100) : 0}
<tr class="border-t border-gray-100"> <tr class="border-t border-gray-100 dark:border-gray-700">
<td class="px-4 py-3 font-medium text-gray-900">{project.name}</td> <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(allocated, currency)}</td>
<td class="px-4 py-3">{formatCurrency(spent, 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 {remaining < 0 ? 'text-red-600' : ''}">{formatCurrency(remaining, currency)}</td>
<td class="px-4 py-3 w-32"> <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 <div
class="h-full rounded-full {pct > 90 ? 'bg-red-500' : pct > 70 ? 'bg-amber-500' : 'bg-blue-500'}" class="h-full rounded-full {pct > 90 ? 'bg-red-500' : pct > 70 ? 'bg-amber-500' : 'bg-blue-500'}"
style="width: {pct}%" style="width: {pct}%"
@@ -237,9 +237,9 @@
<!-- Company Changelog --> <!-- Company Changelog -->
{#if data.changelog.length > 0} {#if data.changelog.length > 0}
<h3 class="mb-3 font-medium text-gray-900">Activity Log</h3> <h3 class="mb-3 font-medium text-gray-900 dark:text-white">Activity Log</h3>
<div class="rounded-lg border border-gray-200 bg-white"> <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"> <div class="divide-y divide-gray-100 dark:divide-gray-700">
{#each data.changelog as entry} {#each data.changelog as entry}
{@const eventStyle = getEventStyle(entry.event)} {@const eventStyle = getEventStyle(entry.event)}
<div class="flex items-start gap-3 px-4 py-3"> <div class="flex items-start gap-3 px-4 py-3">
@@ -247,8 +247,8 @@
<span class="text-xs {eventStyle.text}">{eventStyle.icon}</span> <span class="text-xs {eventStyle.text}">{eventStyle.icon}</span>
</div> </div>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="text-sm text-gray-900">{entry.description}</p> <p class="text-sm text-gray-900 dark:text-white">{entry.description}</p>
<p class="mt-0.5 text-xs text-gray-400"> <p class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">
{entry.userName ?? entry.userEmail ?? 'System'} · {timeAgo(entry.createdAt)} {entry.userName ?? entry.userEmail ?? 'System'} · {timeAgo(entry.createdAt)}
</p> </p>
</div> </div>
@@ -11,33 +11,33 @@
</svelte:head> </svelte:head>
<div> <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} {#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}
{#if canManage} {#if canManage}
<form method="POST" action="?/create" use:enhance class="mb-6 flex items-end gap-3"> <form method="POST" action="?/create" use:enhance class="mb-6 flex items-end gap-3">
<div class="flex-1"> <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 <input
type="text" type="text"
id="name" id="name"
name="name" name="name"
required required
placeholder="Category name" 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>
<div class="w-20"> <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 <input
type="color" type="color"
id="color" id="color"
name="color" name="color"
value="#3B82F6" 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> </div>
<button <button
@@ -50,16 +50,16 @@
{/if} {/if}
{#if data.categories.length === 0} {#if data.categories.length === 0}
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center"> <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">No categories yet.</p> <p class="text-gray-500 dark:text-gray-400">No categories yet.</p>
</div> </div>
{:else} {:else}
<div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{#each data.categories as cat} {#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="flex items-center gap-2">
<div class="h-4 w-4 rounded-full" style="background-color: {cat.color}"></div> <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> </div>
{#if canManage} {#if canManage}
<form method="POST" action="?/delete" use:enhance> <form method="POST" action="?/delete" use:enhance>
@@ -16,7 +16,7 @@
<div> <div>
<div class="mb-4 flex items-center justify-between"> <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> </div>
<!-- Status filter --> <!-- Status filter -->
@@ -27,7 +27,7 @@
class="rounded-full px-3 py-1 text-sm font-medium transition-colors class="rounded-full px-3 py-1 text-sm font-medium transition-colors
{data.statusFilter === status {data.statusFilter === status
? 'bg-blue-600 text-white' ? '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)} {status.charAt(0).toUpperCase() + status.slice(1)}
</a> </a>
@@ -35,24 +35,24 @@
</div> </div>
{#if data.expenses.length === 0} {#if data.expenses.length === 0}
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center"> <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">No expenses found.</p> <p class="text-gray-500 dark:text-gray-400">No expenses found.</p>
</div> </div>
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each data.expenses as expense} {#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 class="flex items-start justify-between">
<div> <div>
<h3 class="font-medium text-gray-900">{expense.title}</h3> <h3 class="font-medium text-gray-900 dark:text-white">{expense.title}</h3>
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500 dark:text-gray-400">
{expense.projectName} {expense.projectName}
{#if expense.categoryName}· {expense.categoryName}{/if} {#if expense.categoryName}· {expense.categoryName}{/if}
</p> </p>
{#if expense.description} {#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} {/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} By {expense.submitterName ?? expense.submitterEmail} · {expense.expenseDate}
</p> </p>
</div> </div>
@@ -61,10 +61,10 @@
<span <span
class="rounded-full px-2 py-0.5 text-xs font-medium class="rounded-full px-2 py-0.5 text-xs font-medium
{expense.status === 'approved' {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' : expense.status === 'rejected'
? 'bg-red-100 text-red-700' ? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
: 'bg-amber-100 text-amber-700'}" : 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300'}"
> >
{expense.status} {expense.status}
</span> </span>
@@ -72,13 +72,13 @@
</div> </div>
{#if expense.status === 'rejected' && expense.rejectionReason} {#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} Reason: {expense.rejectionReason}
</div> </div>
{/if} {/if}
{#if canApprove && expense.status === 'pending'} {#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> <form method="POST" action="?/approve" use:enhance>
<input type="hidden" name="expenseId" value={expense.id} /> <input type="hidden" name="expenseId" value={expense.id} />
<button <button
@@ -94,7 +94,7 @@
type="text" type="text"
name="reason" name="reason"
placeholder="Rejection reason (optional)" 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 <button
type="submit" type="submit"
@@ -45,33 +45,33 @@
</svelte:head> </svelte:head>
<div class="mx-auto max-w-3xl"> <div class="mx-auto max-w-3xl">
<h2 class="mb-4 text-lg font-semibold text-gray-900">Import Expenses</h2> <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"> <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. Paste CSV data from Actual Budget or any spreadsheet. Expected columns: title/name, amount, date.
Optional: description, category. Optional: description, category.
</p> </p>
{#if form?.error} {#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}
{#if form?.success} {#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. Successfully imported {form.imported} expenses.
</div> </div>
{/if} {/if}
<!-- Step 1: Paste CSV --> <!-- Step 1: Paste CSV -->
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5"> <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">1. Paste CSV Data</h3> <h3 class="mb-2 font-medium text-gray-900 dark:text-white">1. Paste CSV Data</h3>
<textarea <textarea
bind:value={csvText} bind:value={csvText}
rows="8" rows="8"
placeholder="title,amount,date,description&#10;Office Supplies,150.00,2024-01-15,Printer paper&#10;..." 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> ></textarea>
<button <button
onclick={parseCSV} 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 Parse CSV
</button> </button>
@@ -79,20 +79,20 @@
<!-- Step 2: Preview --> <!-- Step 2: Preview -->
{#if parsedRows.length > 0} {#if parsedRows.length > 0}
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5"> <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">2. Preview ({parsedRows.length} rows)</h3> <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"> <div class="max-h-64 overflow-auto">
<table class="w-full text-xs"> <table class="w-full text-xs">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-800/50">
<tr> <tr>
{#each Object.keys(parsedRows[0]) as header} {#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} {/each}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each parsedRows.slice(0, 20) as row} {#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} {#each Object.values(row) as val}
<td class="px-2 py-1">{val}</td> <td class="px-2 py-1">{val}</td>
{/each} {/each}
@@ -101,28 +101,28 @@
</tbody> </tbody>
</table> </table>
{#if parsedRows.length > 20} {#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} {/if}
</div> </div>
</div> </div>
<!-- Step 3: Import --> <!-- Step 3: Import -->
<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">
<h3 class="mb-2 font-medium text-gray-900">3. Import</h3> <h3 class="mb-2 font-medium text-gray-900 dark:text-white">3. Import</h3>
<form method="POST" action="?/import" use:enhance> <form method="POST" action="?/import" use:enhance>
<input type="hidden" name="data" value={jsonData} /> <input type="hidden" name="data" value={jsonData} />
<div class="mb-4 grid grid-cols-2 gap-3"> <div class="mb-4 grid grid-cols-2 gap-3">
<div> <div>
<label for="projectId" class="mb-1 block text-sm text-gray-700">Target Project</label> <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 px-3 py-2 text-sm"> <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} {#each data.projects as project}
<option value={project.id}>{project.name}</option> <option value={project.id}>{project.name}</option>
{/each} {/each}
</select> </select>
</div> </div>
<div> <div>
<label for="status" class="mb-1 block text-sm text-gray-700">Default Status</label> <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 px-3 py-2 text-sm"> <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="approved">Approved (import as finalized)</option>
<option value="pending">Pending (require approval)</option> <option value="pending">Pending (require approval)</option>
</select> </select>
@@ -12,7 +12,7 @@
</svelte:head> </svelte:head>
<div class="mb-4 flex items-center justify-between"> <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'} {#if data.companyRole !== 'viewer'}
<a <a
href="/companies/{data.company.id}/projects/new" href="/companies/{data.company.id}/projects/new"
@@ -24,32 +24,32 @@
</div> </div>
{#if data.projects.length === 0} {#if data.projects.length === 0}
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center"> <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">No projects yet. Create your first one.</p> <p class="text-gray-500 dark:text-gray-400">No projects yet. Create your first one.</p>
</div> </div>
{:else} {:else}
<div class="grid gap-4 sm:grid-cols-2"> <div class="grid gap-4 sm:grid-cols-2">
{#each data.projects as project} {#each data.projects as project}
<a <a
href="/companies/{data.company.id}/projects/{project.id}" 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"> <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} {#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} {/if}
</div> </div>
{#if project.description} {#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} {/if}
<div class="mt-3"> <div class="mt-3">
<div class="flex justify-between text-sm"> <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> <span>{formatCurrency(project.spent, currency)} / {formatCurrency(project.allocatedBudget, currency)}</span>
</div> </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 <div
class="h-full rounded-full {budgetColor(budgetPercent(project.spent, project.allocatedBudget))}" class="h-full rounded-full {budgetColor(budgetPercent(project.spent, project.allocatedBudget))}"
style="width: {budgetPercent(project.spent, project.allocatedBudget)}%" style="width: {budgetPercent(project.spent, project.allocatedBudget)}%"
@@ -57,7 +57,7 @@
</div> </div>
</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> <span>{project.expenseCount} expenses</span>
{#if project.pendingCount > 0} {#if project.pendingCount > 0}
<span class="text-amber-600">{project.pendingCount} pending</span> <span class="text-amber-600">{project.pendingCount} pending</span>
@@ -14,9 +14,9 @@
<div> <div>
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<div> <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} {#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} {/if}
</div> </div>
{#if canAddExpense} {#if canAddExpense}
@@ -31,30 +31,30 @@
<!-- Stats --> <!-- Stats -->
<div class="mb-6 grid gap-4 sm:grid-cols-3"> <div class="mb-6 grid gap-4 sm:grid-cols-3">
<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">
<p class="text-sm text-gray-500">Budget</p> <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> <p class="text-xl font-bold">{formatCurrency(data.project.allocatedBudget, currency)}</p>
</div> </div>
<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">
<p class="text-sm text-gray-500">Spent (Approved)</p> <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> <p class="text-xl font-bold">{formatCurrency(data.stats.totalApproved, currency)}</p>
</div> </div>
<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">
<p class="text-sm text-gray-500">Pending</p> <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> <p class="text-xl font-bold text-amber-600">{formatCurrency(data.stats.totalPending, currency)}</p>
</div> </div>
</div> </div>
<!-- Expense list --> <!-- Expense list -->
{#if data.expenses.length === 0} {#if data.expenses.length === 0}
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center"> <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">No expenses yet.</p> <p class="text-gray-500 dark:text-gray-400">No expenses yet.</p>
</div> </div>
{:else} {: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"> <table class="w-full text-sm">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-800/50">
<tr class="text-left text-gray-500"> <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">Title</th>
<th class="px-4 py-3 font-medium">Category</th> <th class="px-4 py-3 font-medium">Category</th>
<th class="px-4 py-3 font-medium">Amount</th> <th class="px-4 py-3 font-medium">Amount</th>
@@ -65,20 +65,20 @@
</thead> </thead>
<tbody> <tbody>
{#each data.expenses as expense} {#each data.expenses as expense}
<tr class="border-t border-gray-100"> <tr class="border-t border-gray-100 dark:border-gray-700">
<td class="px-4 py-3 font-medium text-gray-900">{expense.title}</td> <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">{expense.categoryName ?? '—'}</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">{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 dark:text-gray-400">{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.submitterName ?? expense.submitterEmail}</td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<span <span
class="rounded-full px-2 py-0.5 text-xs font-medium class="rounded-full px-2 py-0.5 text-xs font-medium
{expense.status === 'approved' {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' : expense.status === 'rejected'
? 'bg-red-100 text-red-700' ? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
: 'bg-amber-100 text-amber-700'}" : 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300'}"
> >
{expense.status} {expense.status}
</span> </span>
@@ -10,40 +10,40 @@
</svelte:head> </svelte:head>
<div class="mx-auto max-w-lg"> <div class="mx-auto max-w-lg">
<h2 class="mb-4 text-lg font-semibold text-gray-900">Add Expense</h2> <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">Project: {data.projectName}</p> <p class="mb-4 text-sm text-gray-500 dark:text-gray-400">Project: {data.projectName}</p>
{#if form?.error} {#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 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"> <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 <input
type="text" type="text"
id="title" id="title"
name="title" name="title"
required 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>
<div class="mb-4"> <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 Description
</label> </label>
<textarea <textarea
id="description" id="description"
name="description" name="description"
rows="2" 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> ></textarea>
</div> </div>
<div class="mb-4 grid grid-cols-2 gap-3"> <div class="mb-4 grid grid-cols-2 gap-3">
<div> <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 <input
type="number" type="number"
id="amount" id="amount"
@@ -51,30 +51,30 @@
step="0.01" step="0.01"
min="0.01" min="0.01"
required 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>
<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 <input
type="date" type="date"
id="expenseDate" id="expenseDate"
name="expenseDate" name="expenseDate"
required required
value={new Date().toISOString().split('T')[0]} 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> </div>
<div class="mb-4"> <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 Category
</label> </label>
<select <select
id="categoryId" id="categoryId"
name="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> <option value="">None</option>
{#each data.categories as cat} {#each data.categories as cat}
@@ -85,10 +85,10 @@
{#if data.tags.length > 0} {#if data.tags.length > 0}
<div class="mb-4"> <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"> <div class="flex flex-wrap gap-2">
{#each data.tags as tag} {#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" /> <input type="checkbox" name="tagIds" value={tag.id} class="rounded" />
{tag.name} {tag.name}
</label> </label>
@@ -100,7 +100,7 @@
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<a <a
href="javascript:history.back()" 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 Cancel
</a> </a>
@@ -10,38 +10,38 @@
</svelte:head> </svelte:head>
<div class="mx-auto max-w-lg"> <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} {#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 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"> <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 <input
type="text" type="text"
id="name" id="name"
name="name" name="name"
required 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>
<div class="mb-6"> <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 Description
</label> </label>
<textarea <textarea
id="description" id="description"
name="description" name="description"
rows="3" 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> ></textarea>
</div> </div>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<a <a
href="javascript:history.back()" 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 Cancel
</a> </a>
@@ -19,26 +19,26 @@
</svelte:head> </svelte:head>
<div> <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 --> <!-- 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> <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 <input
type="date" type="date"
id="from" id="from"
bind:value={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>
<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 <input
type="date" type="date"
id="to" id="to"
bind:value={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> </div>
<button <button
@@ -51,10 +51,10 @@
<div class="grid gap-6 lg:grid-cols-2"> <div class="grid gap-6 lg:grid-cols-2">
<!-- By Category --> <!-- By Category -->
<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">
<h3 class="mb-3 font-medium text-gray-900">Spending by Category</h3> <h3 class="mb-3 font-medium text-gray-900 dark:text-white">Spending by Category</h3>
{#if data.byCategory.length === 0} {#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} {:else}
<div class="space-y-2"> <div class="space-y-2">
{#each data.byCategory as cat} {#each data.byCategory as cat}
@@ -68,7 +68,7 @@
</div> </div>
<span class="font-medium">{formatCurrency(cat.total, currency)}</span> <span class="font-medium">{formatCurrency(cat.total, currency)}</span>
</div> </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 <div
class="h-full rounded-full" class="h-full rounded-full"
style="width: {pct}%; background-color: {cat.categoryColor}" style="width: {pct}%; background-color: {cat.categoryColor}"
@@ -81,10 +81,10 @@
</div> </div>
<!-- By Project (Budget vs Actual) --> <!-- By Project (Budget vs Actual) -->
<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">
<h3 class="mb-3 font-medium text-gray-900">Budget vs Actual by Project</h3> <h3 class="mb-3 font-medium text-gray-900 dark:text-white">Budget vs Actual by Project</h3>
{#if data.byProject.length === 0} {#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} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each data.byProject as project} {#each data.byProject as project}
@@ -99,7 +99,7 @@
</span> </span>
</div> </div>
<div class="mt-1 flex gap-1"> <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 <div
class="h-full rounded-full {pct > 90 ? 'bg-red-500' : pct > 70 ? 'bg-amber-500' : 'bg-blue-500'}" class="h-full rounded-full {pct > 90 ? 'bg-red-500' : pct > 70 ? 'bg-amber-500' : 'bg-blue-500'}"
style="width: {pct}%" style="width: {pct}%"
@@ -113,10 +113,10 @@
</div> </div>
<!-- Monthly Trend --> <!-- Monthly Trend -->
<div class="rounded-lg border border-gray-200 bg-white p-5 lg:col-span-2"> <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">Monthly Spending</h3> <h3 class="mb-3 font-medium text-gray-900 dark:text-white">Monthly Spending</h3>
{#if data.byMonth.length === 0} {#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} {:else}
{@const maxVal = Math.max(...data.byMonth.map((m) => parseFloat(m.total)))} {@const maxVal = Math.max(...data.byMonth.map((m) => parseFloat(m.total)))}
<div class="flex items-end gap-2" style="height: 200px;"> <div class="flex items-end gap-2" style="height: 200px;">
@@ -124,12 +124,12 @@
{@const val = parseFloat(month.total)} {@const val = parseFloat(month.total)}
{@const height = maxVal > 0 ? (val / maxVal) * 100 : 0} {@const height = maxVal > 0 ? (val / maxVal) * 100 : 0}
<div class="flex flex-1 flex-col items-center gap-1"> <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 <div
class="w-full rounded-t bg-blue-500" class="w-full rounded-t bg-blue-500"
style="height: {height}%" style="height: {height}%"
></div> ></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> </div>
{/each} {/each}
</div> </div>
@@ -12,38 +12,38 @@
<div class="space-y-8"> <div class="space-y-8">
{#if form?.error} {#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}
{#if form?.message} {#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} {/if}
<!-- Company details --> <!-- Company details -->
{#if isAdmin} {#if isAdmin}
<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">
<h2 class="mb-4 font-semibold text-gray-900">Company Details</h2> <h2 class="mb-4 font-semibold text-gray-900 dark:text-white">Company Details</h2>
<form method="POST" action="?/updateCompany" use:enhance> <form method="POST" action="?/updateCompany" use:enhance>
<div class="mb-4"> <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 <input
type="text" type="text"
id="name" id="name"
name="name" name="name"
required required
value={data.company.name} 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>
<div class="mb-4"> <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 <textarea
id="description" id="description"
name="description" name="description"
rows="2" 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> >{data.company.description ?? ''}</textarea>
</div> </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. 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> </p>
<button type="submit" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"> <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} {/if}
<!-- Members --> <!-- Members -->
<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">
<h2 class="mb-4 font-semibold text-gray-900">Members</h2> <h2 class="mb-4 font-semibold text-gray-900 dark:text-white">Members</h2>
{#if isAdmin} {#if isAdmin}
<form method="POST" action="?/addMember" use:enhance class="mb-4 flex items-end gap-3"> <form method="POST" action="?/addMember" use:enhance class="mb-4 flex items-end gap-3">
<div class="flex-1"> <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 <input
type="email" type="email"
id="email" id="email"
name="email" name="email"
required required
placeholder="user@example.com" 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>
<div class="w-32"> <div class="w-32">
<label for="role" class="mb-1 block text-sm text-gray-700">Role</label> <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 px-3 py-2 text-sm"> <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="viewer">Viewer</option>
<option value="user">User</option> <option value="user">User</option>
<option value="manager">Manager</option> <option value="manager">Manager</option>
@@ -86,8 +86,8 @@
{/if} {/if}
<table class="w-full text-sm"> <table class="w-full text-sm">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-800/50">
<tr class="text-left text-gray-500"> <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">User</th>
<th class="px-4 py-3 font-medium">Email</th> <th class="px-4 py-3 font-medium">Email</th>
<th class="px-4 py-3 font-medium">Role</th> <th class="px-4 py-3 font-medium">Role</th>
@@ -98,9 +98,9 @@
</thead> </thead>
<tbody> <tbody>
{#each data.members as member} {#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">{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"> <td class="px-4 py-3">
{#if isAdmin} {#if isAdmin}
<form method="POST" action="?/updateRole" use:enhance class="inline"> <form method="POST" action="?/updateRole" use:enhance class="inline">
@@ -108,7 +108,7 @@
<select <select
name="role" name="role"
onchange={(e) => e.currentTarget.form?.requestSubmit()} 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} {#each ['viewer', 'user', 'manager', 'admin'] as role}
<option value={role} selected={member.role === role}>{role}</option> <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="mx-auto max-w-6xl">
<div class="mb-6 flex items-center justify-between"> <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} {#if data.user?.isSystemAdmin}
<a <a
href="/companies" href="/companies"
@@ -24,12 +24,12 @@
</div> </div>
{#if data.companySummaries.length === 0} {#if data.companySummaries.length === 0}
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center"> <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" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> <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> </svg>
<h2 class="mt-4 text-lg font-medium text-gray-900">Waiting for access</h2> <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"> <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. You haven't been assigned to any company yet. Ask an administrator to invite you.
</p> </p>
</div> </div>
@@ -38,12 +38,12 @@
{#each data.companySummaries as company} {#each data.companySummaries as company}
<a <a
href="/companies/{company.id}" 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"> <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 <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} {company.role}
</span> </span>
@@ -51,33 +51,33 @@
<div class="space-y-2 text-sm"> <div class="space-y-2 text-sm">
<div class="flex justify-between"> <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> <span class="font-medium">{formatCurrency(company.totalBudget, company.currency)}</span>
</div> </div>
<div class="flex justify-between"> <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> <span class="font-medium">{formatCurrency(company.totalSpent, company.currency)}</span>
</div> </div>
<div class="flex justify-between"> <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> <span class="font-medium">{company.projectCount}</span>
</div> </div>
{#if company.pendingExpenses > 0} {#if company.pendingExpenses > 0}
<div class="flex justify-between"> <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> <span class="font-medium text-amber-600">{company.pendingExpenses}</span>
</div> </div>
{/if} {/if}
</div> </div>
<div class="mt-4"> <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 <div
class="h-full rounded-full transition-all {budgetColor(budgetPercent(company.totalSpent, company.totalBudget))}" class="h-full rounded-full transition-all {budgetColor(budgetPercent(company.totalSpent, company.totalBudget))}"
style="width: {budgetPercent(company.totalSpent, company.totalBudget)}%" style="width: {budgetPercent(company.totalSpent, company.totalBudget)}%"
></div> ></div>
</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> </div>
</a> </a>
{/each} {/each}
+6 -1
View File
@@ -1,8 +1,13 @@
<script lang="ts"> <script lang="ts">
import ThemeToggle from '$lib/components/layout/ThemeToggle.svelte';
let { children } = $props(); let { children } = $props();
</script> </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"> <div class="w-full max-w-md">
{@render children()} {@render children()}
</div> </div>
+12 -12
View File
@@ -9,36 +9,36 @@
<title>Login - Buildfor Life Budget</title> <title>Login - Buildfor Life Budget</title>
</svelte:head> </svelte:head>
<div class="rounded-lg border border-gray-200 bg-white p-8 shadow-sm"> <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">Sign In</h1> <h1 class="mb-6 text-center text-2xl font-bold text-gray-900 dark:text-white">Sign In</h1>
{#if form?.error} {#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} {form.error}
</div> </div>
{/if} {/if}
<form method="POST" use:enhance> <form method="POST" use:enhance>
<div class="mb-4"> <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 <input
type="email" type="email"
id="email" id="email"
name="email" name="email"
required 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 ?? ''} value={form?.email ?? ''}
/> />
</div> </div>
<div class="mb-6"> <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 <input
type="password" type="password"
id="password" id="password"
name="password" name="password"
required 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> </div>
@@ -54,23 +54,23 @@
<div class="mt-4"> <div class="mt-4">
<div class="relative"> <div class="relative">
<div class="absolute inset-0 flex items-center"> <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>
<div class="relative flex justify-center text-sm"> <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>
</div> </div>
<a <a
href="/oidc" 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) Single Sign-On (SSO)
</a> </a>
</div> </div>
{/if} {/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? 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> </p>
</div> </div>
+13 -13
View File
@@ -9,18 +9,18 @@
<title>Sign Up - Buildfor Life Budget</title> <title>Sign Up - Buildfor Life Budget</title>
</svelte:head> </svelte:head>
<div class="rounded-lg border border-gray-200 bg-white p-8 shadow-sm"> <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">Create Account</h1> <h1 class="mb-6 text-center text-2xl font-bold text-gray-900 dark:text-white">Create Account</h1>
{#if form?.error} {#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} {form.error}
</div> </div>
{/if} {/if}
<form method="POST" use:enhance> <form method="POST" use:enhance>
<div class="mb-4"> <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 Display Name
</label> </label>
<input <input
@@ -28,37 +28,37 @@
id="displayName" id="displayName"
name="displayName" name="displayName"
required 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 ?? ''} value={form?.displayName ?? ''}
/> />
</div> </div>
<div class="mb-4"> <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 <input
type="email" type="email"
id="email" id="email"
name="email" name="email"
required 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 ?? ''} value={form?.email ?? ''}
/> />
</div> </div>
<div class="mb-4"> <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 <input
type="password" type="password"
id="password" id="password"
name="password" name="password"
required required
minlength="8" 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>
<div class="mb-6"> <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 Confirm Password
</label> </label>
<input <input
@@ -67,7 +67,7 @@
name="confirmPassword" name="confirmPassword"
required required
minlength="8" 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>
@@ -79,8 +79,8 @@
</button> </button>
</form> </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? 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> </p>
</div> </div>