Redesign company overview: 4 compact KPIs, side-by-side projects + recent expenses
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { formatCurrency } from '$lib/utils/currency.js';
|
import { formatCurrency } from '$lib/utils/currency.js';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
const currency = $derived(data.company.currency);
|
const currency = $derived(data.company.currency);
|
||||||
const allocated = $derived(data.projects.reduce((s, p) => s + parseFloat(p.allocatedBudget), 0));
|
const allocated = $derived(data.projects.reduce((s, p) => s + parseFloat(p.allocatedBudget), 0));
|
||||||
@@ -10,51 +10,101 @@
|
|||||||
const total = $derived(parseFloat(data.company.totalBudget));
|
const total = $derived(parseFloat(data.company.totalBudget));
|
||||||
const remaining = $derived(total - spent);
|
const remaining = $derived(total - spent);
|
||||||
const remainingPct = $derived(total > 0 ? (remaining / total) * 100 : 0);
|
const remainingPct = $derived(total > 0 ? (remaining / total) * 100 : 0);
|
||||||
|
|
||||||
|
const tone = $derived(remaining < 0 ? 'red' : remainingPct < 20 ? 'amber' : 'green');
|
||||||
|
|
||||||
|
const toneRing: Record<string, string> = {
|
||||||
|
green: 'border-green-300 dark:border-green-700',
|
||||||
|
amber: 'border-amber-300 dark:border-amber-700',
|
||||||
|
red: 'border-red-300 dark:border-red-700'
|
||||||
|
};
|
||||||
|
const toneText: Record<string, string> = {
|
||||||
|
green: 'text-green-700 dark:text-green-400',
|
||||||
|
amber: 'text-amber-700 dark:text-amber-400',
|
||||||
|
red: 'text-red-700 dark:text-red-400'
|
||||||
|
};
|
||||||
|
const toneBar: Record<string, string> = {
|
||||||
|
green: 'bg-green-500',
|
||||||
|
amber: 'bg-amber-500',
|
||||||
|
red: 'bg-red-500'
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{data.company.name} - {data.appName}</title>
|
<title>{data.company.name} - {data.appName}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="grid gap-6 lg:grid-cols-2">
|
<div class="space-y-6">
|
||||||
<!-- Budget Summary -->
|
<!-- KPI row -->
|
||||||
<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">
|
<div class="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||||
<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="rounded-lg border-2 {toneRing[tone]} bg-white p-4 dark:bg-gray-800">
|
||||||
<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'}">
|
<p class="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
|
Remaining
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-2xl font-bold {toneText[tone]}">
|
||||||
{formatCurrency(remaining, currency)}
|
{formatCurrency(remaining, currency)}
|
||||||
</div>
|
</p>
|
||||||
<div class="mt-3 h-2.5 w-full overflow-hidden rounded-full bg-white/60 dark:bg-gray-700/60">
|
<div class="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
|
||||||
<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 transition-all {toneBar[tone]}"
|
||||||
style="width: {Math.max(0, Math.min(remainingPct, 100))}%"
|
style="width: {Math.max(0, Math.min(remainingPct, 100))}%"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs {remaining < 0 ? 'text-red-500' : remainingPct < 20 ? 'text-amber-500' : 'text-green-500'}">{remainingPct.toFixed(1)}% remaining</p>
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{remainingPct.toFixed(1)}% of total
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 space-y-1.5 text-sm">
|
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
|
||||||
<div class="flex justify-between">
|
<p class="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
<span class="{remaining < 0 ? 'text-red-400' : remainingPct < 20 ? 'text-amber-400' : 'text-green-600/60'}">Total budget</span>
|
Total Budget
|
||||||
<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>
|
</p>
|
||||||
|
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{formatCurrency(total, currency)}
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Company-wide</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="{remaining < 0 ? 'text-red-400' : remainingPct < 20 ? 'text-amber-400' : 'text-green-600/60'}">Total spent</span>
|
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
|
||||||
<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>
|
<p class="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
</div>
|
Spent
|
||||||
<div class="flex justify-between">
|
</p>
|
||||||
<span class="{remaining < 0 ? 'text-red-400' : remainingPct < 20 ? 'text-amber-400' : 'text-green-600/60'}">Allocated</span>
|
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
<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>
|
{formatCurrency(spent, currency)}
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Across {data.projects.length} {data.projects.length === 1 ? 'project' : 'projects'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
|
Allocated
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{formatCurrency(allocated, currency)}
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{total > 0 ? ((allocated / total) * 100).toFixed(1) : '0'}% of total
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-6 lg:grid-cols-2">
|
||||||
<!-- Projects -->
|
<!-- Projects -->
|
||||||
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5">
|
<div
|
||||||
|
class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
||||||
|
>
|
||||||
<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 dark:text-gray-500">Projects</h2>
|
<h2
|
||||||
{#if data.companyRoles.some(r => r === 'admin' || r === 'manager' || r === 'user' || r === 'hr')}
|
class="text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500"
|
||||||
|
>
|
||||||
|
Projects
|
||||||
|
</h2>
|
||||||
|
{#if data.companyRoles.some((r) => r === 'admin' || r === 'manager' || r === 'user' || r === 'hr')}
|
||||||
<a
|
<a
|
||||||
href="/companies/{data.company.id}/projects/new"
|
href="/companies/{data.company.id}/projects/new"
|
||||||
class="text-sm font-medium text-blue-600 hover:text-blue-700"
|
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
|
||||||
>
|
>
|
||||||
+ New Project
|
+ New Project
|
||||||
</a>
|
</a>
|
||||||
@@ -65,7 +115,7 @@
|
|||||||
<p class="py-4 text-center text-sm text-gray-500 dark:text-gray-400">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 (project.id)}
|
||||||
{@const budgetNum = parseFloat(project.allocatedBudget)}
|
{@const budgetNum = parseFloat(project.allocatedBudget)}
|
||||||
{@const spentNum = parseFloat(project.spent)}
|
{@const spentNum = parseFloat(project.spent)}
|
||||||
{@const pct = budgetNum > 0 ? Math.min((spentNum / budgetNum) * 100, 100) : 0}
|
{@const pct = budgetNum > 0 ? Math.min((spentNum / budgetNum) * 100, 100) : 0}
|
||||||
@@ -73,12 +123,21 @@
|
|||||||
<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 dark:text-white">{project.name}</span>
|
<span class="font-medium text-gray-900 dark:text-white">{project.name}</span>
|
||||||
<span class="text-gray-500 dark:text-gray-400">
|
<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 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="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full {pct > 90
|
||||||
|
? 'bg-red-500'
|
||||||
|
: pct > 70
|
||||||
|
? 'bg-amber-500'
|
||||||
|
: 'bg-blue-500'}"
|
||||||
style="width: {pct}%"
|
style="width: {pct}%"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,32 +151,43 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recent Expenses -->
|
<!-- Recent Expenses -->
|
||||||
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 lg:col-span-2">
|
<div
|
||||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Recent Expenses</h2>
|
class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
<h2
|
||||||
|
class="text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500"
|
||||||
|
>
|
||||||
|
Recent Expenses
|
||||||
|
</h2>
|
||||||
|
<a
|
||||||
|
href="/companies/{data.company.id}/expenses"
|
||||||
|
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
View all →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{#if data.recentExpenses.length === 0}
|
{#if data.recentExpenses.length === 0}
|
||||||
<p class="py-4 text-center text-sm text-gray-500 dark:text-gray-400">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">
|
<ul class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
<thead>
|
{#each data.recentExpenses as expense (expense.id)}
|
||||||
<tr class="border-b border-gray-100 dark:border-gray-700 text-left text-gray-500 dark:text-gray-400">
|
<li class="flex items-center justify-between gap-3 py-2">
|
||||||
<th class="pb-2 font-medium">Title</th>
|
<div class="min-w-0 flex-1">
|
||||||
<th class="pb-2 font-medium">Project</th>
|
<p class="truncate text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<th class="pb-2 font-medium">Amount</th>
|
{expense.title}
|
||||||
<th class="pb-2 font-medium">Date</th>
|
</p>
|
||||||
<th class="pb-2 font-medium">Status</th>
|
<p class="truncate text-xs text-gray-500 dark:text-gray-400">
|
||||||
</tr>
|
{expense.projectName} · {expense.expenseDate}
|
||||||
</thead>
|
</p>
|
||||||
<tbody>
|
</div>
|
||||||
{#each data.recentExpenses as expense}
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<tr class="border-b border-gray-50 dark:border-gray-700/50">
|
<span class="font-medium text-gray-900 dark:text-white">
|
||||||
<td class="py-2 font-medium text-gray-900 dark:text-white">{expense.title}</td>
|
{formatCurrency(expense.amount, currency)}
|
||||||
<td class="py-2 text-gray-500 dark:text-gray-400">{expense.projectName}</td>
|
</span>
|
||||||
<td class="py-2 dark:text-white">{formatCurrency(expense.amount, currency)}</td>
|
|
||||||
<td class="py-2 text-gray-500 dark:text-gray-400">{expense.expenseDate}</td>
|
|
||||||
<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 ===
|
||||||
{expense.status === 'approved'
|
'approved'
|
||||||
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
? '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 dark:bg-red-900/40 dark:text-red-300'
|
? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
|
||||||
@@ -125,11 +195,11 @@
|
|||||||
>
|
>
|
||||||
{expense.status}
|
{expense.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</ul>
|
||||||
</table>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user