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