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