7a4ba0537f
Multi-company budget/project tracking tool built with SvelteKit 5, PostgreSQL (Drizzle ORM), and Tailwind CSS v4. Features: - Auth: local (email/password with Argon2) + generic OIDC - 4 roles per company: admin, manager, user, viewer - Multi-company with per-company user membership - Projects with budget allocation from company pool - Expense submission with approval workflow - Categories and tags for expense organization - Reports with spending breakdowns (by category, project, time) - CSV import for Actual Budget migration - Company audit log tracking all budget and admin actions - Remaining budget hero display on overview and budget pages - Admin-only company creation; new users wait for invitation - Deployment configs for systemd + nginx (bare metal/Proxmox) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
139 lines
4.7 KiB
Svelte
139 lines
4.7 KiB
Svelte
<script lang="ts">
|
|
import { enhance } from '$app/forms';
|
|
import { formatCurrency } from '$lib/utils/currency.js';
|
|
import type { PageData, ActionData } from './$types';
|
|
|
|
let { data, form } = $props();
|
|
let showCreateModal = $state(false);
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Companies - B4L Budget</title>
|
|
</svelte:head>
|
|
|
|
<div class="mx-auto max-w-6xl">
|
|
<div class="mb-6 flex items-center justify-between">
|
|
<h1 class="text-2xl font-bold text-gray-900">Companies</h1>
|
|
{#if data.isSystemAdmin}
|
|
<button
|
|
onclick={() => (showCreateModal = true)}
|
|
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
>
|
|
New Company
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if form?.error}
|
|
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700">{form.error}</div>
|
|
{/if}
|
|
|
|
{#if data.companies.length === 0}
|
|
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center">
|
|
<svg class="mx-auto h-12 w-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
|
</svg>
|
|
<h2 class="mt-4 text-lg font-medium text-gray-900">No companies yet</h2>
|
|
<p class="mt-2 text-sm text-gray-500">
|
|
You haven't been assigned to any company yet. Ask an administrator to invite you.
|
|
</p>
|
|
</div>
|
|
{:else}
|
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{#each data.companies as company}
|
|
<a
|
|
href="/companies/{company.id}"
|
|
class="rounded-lg border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow"
|
|
>
|
|
<h2 class="text-lg font-semibold text-gray-900">{company.name}</h2>
|
|
{#if company.description}
|
|
<p class="mt-1 text-sm text-gray-500 line-clamp-2">{company.description}</p>
|
|
{/if}
|
|
<div class="mt-3 flex items-center justify-between text-sm">
|
|
<span class="text-gray-500">Budget: {formatCurrency(company.totalBudget, company.currency)}</span>
|
|
<span class="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">
|
|
{company.role}
|
|
</span>
|
|
</div>
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Create company modal (admin only) -->
|
|
{#if showCreateModal && data.isSystemAdmin}
|
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
<div class="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
|
<h2 class="mb-4 text-lg font-semibold text-gray-900">Create Company</h2>
|
|
<form method="POST" action="?/create" use:enhance>
|
|
<div class="mb-4">
|
|
<label for="name" class="mb-1 block text-sm font-medium text-gray-700">Name</label>
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
name="name"
|
|
required
|
|
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label for="description" class="mb-1 block text-sm font-medium text-gray-700">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
id="description"
|
|
name="description"
|
|
rows="2"
|
|
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
|
></textarea>
|
|
</div>
|
|
<div class="mb-4 grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label for="totalBudget" class="mb-1 block text-sm font-medium text-gray-700">
|
|
Initial Budget
|
|
</label>
|
|
<input
|
|
type="number"
|
|
id="totalBudget"
|
|
name="totalBudget"
|
|
step="0.01"
|
|
value="0"
|
|
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label for="currency" class="mb-1 block text-sm font-medium text-gray-700">
|
|
Currency
|
|
</label>
|
|
<select
|
|
id="currency"
|
|
name="currency"
|
|
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
|
>
|
|
<option value="THB" selected>THB</option>
|
|
<option value="USD">USD</option>
|
|
<option value="EUR">EUR</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
onclick={() => (showCreateModal = false)}
|
|
class="rounded-md px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
>
|
|
Create
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{/if}
|