Moar
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m6s

This commit is contained in:
2026-02-09 02:28:54 -07:00
parent 12d59bb42f
commit caf763aa1e
24 changed files with 1003 additions and 1169 deletions

View File

@@ -9,7 +9,7 @@ const initial = name ? name.charAt(0).toUpperCase() : '?';
--- ---
<div class:list={["avatar placeholder", className]}> <div class:list={["avatar placeholder", className]}>
<div class="bg-primary text-primary-content w-10 rounded-full flex items-center justify-center"> <div class="bg-primary/15 text-primary w-9 h-9 rounded-full flex items-center justify-center">
<span class="text-lg font-semibold">{initial}</span> <span class="text-sm font-semibold">{initial}</span>
</div> </div>
</div> </div>

View File

@@ -13,13 +13,17 @@ interface Props {
const { title, value, description, icon, color = 'text-primary', valueClass } = Astro.props; const { title, value, description, icon, color = 'text-primary', valueClass } = Astro.props;
--- ---
<div class="stat"> <div class="card card-border bg-base-100">
<div class="card-body p-4 gap-1">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wider text-base-content/60">{title}</span>
{icon && ( {icon && (
<div class:list={["stat-figure", color]}> <div class:list={[color, "opacity-40"]}>
<Icon name={icon} class="w-8 h-8" /> <Icon name={icon} class="w-5 h-5" />
</div> </div>
)} )}
<div class="stat-title">{title}</div> </div>
<div class:list={["stat-value", color, valueClass]}>{value}</div> <div class:list={["text-2xl font-bold", color, valueClass]}>{value}</div>
{description && <div class="stat-desc">{description}</div>} {description && <div class="text-xs text-base-content/50">{description}</div>}
</div>
</div> </div>

View File

@@ -30,6 +30,20 @@ const userMemberships = await db.select({
const currentTeamId = Astro.cookies.get('currentTeamId')?.value || userMemberships[0]?.organization.id; const currentTeamId = Astro.cookies.get('currentTeamId')?.value || userMemberships[0]?.organization.id;
const currentTeam = userMemberships.find(m => m.organization.id === currentTeamId); const currentTeam = userMemberships.find(m => m.organization.id === currentTeamId);
const navItems = [
{ href: '/dashboard', label: 'Dashboard', icon: 'heroicons:home', exact: true },
{ href: '/dashboard/tracker', label: 'Time Tracker', icon: 'heroicons:clock' },
{ href: '/dashboard/invoices', label: 'Invoices & Quotes', icon: 'heroicons:document-currency-dollar' },
{ href: '/dashboard/reports', label: 'Reports', icon: 'heroicons:chart-bar' },
{ href: '/dashboard/clients', label: 'Clients', icon: 'heroicons:building-office' },
{ href: '/dashboard/team', label: 'Team', icon: 'heroicons:user-group' },
];
function isActive(item: { href: string; exact?: boolean }) {
if (item.exact) return Astro.url.pathname === item.href;
return Astro.url.pathname.startsWith(item.href);
}
--- ---
<!doctype html> <!doctype html>
@@ -51,45 +65,46 @@ const currentTeam = userMemberships.find(m => m.organization.id === currentTeamI
<div class="drawer lg:drawer-open flex-1 overflow-auto"> <div class="drawer lg:drawer-open flex-1 overflow-auto">
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" /> <input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col h-full overflow-auto"> <div class="drawer-content flex flex-col h-full overflow-auto">
<!-- Navbar --> <!-- Mobile Navbar -->
<div class="navbar bg-base-200/50 backdrop-blur-sm sticky top-0 z-50 lg:hidden border-b border-base-300/50"> <div class="navbar bg-base-100 sticky top-0 z-50 lg:hidden border-b border-base-200">
<div class="flex-none lg:hidden"> <div class="flex-none">
<label for="my-drawer-2" aria-label="open sidebar" class="btn btn-square btn-ghost"> <label for="my-drawer-2" aria-label="open sidebar" class="btn btn-square btn-ghost btn-sm">
<Icon name="heroicons:bars-3" class="w-6 h-6" /> <Icon name="heroicons:bars-3" class="w-5 h-5" />
</label> </label>
</div> </div>
<div class="flex-1 px-2 flex items-center gap-2"> <div class="flex-1 px-2 flex items-center gap-2">
<img src="/logo.webp" alt="Chronus" class="h-8 w-8" /> <img src="/logo.webp" alt="Chronus" class="h-7 w-7" />
<span class="text-xl font-bold text-primary">Chronus</span> <span class="text-lg font-bold text-primary">Chronus</span>
</div> </div>
<div class="flex-none"> <div class="flex-none">
<ThemeToggle client:load /> <ThemeToggle client:load />
</div> </div>
</div> </div>
<!-- Page content here --> <!-- Page content -->
<main class="p-6 md:p-8"> <main class="flex-1 p-4 sm:p-6 lg:p-8">
<slot /> <slot />
</main> </main>
</div> </div>
<div class="drawer-side z-50"> <div class="drawer-side z-50">
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label> <label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-200/95 backdrop-blur-sm min-h-full w-80 p-4 border-r border-base-300/30"> <aside class="bg-base-200 min-h-full w-72 flex flex-col border-r border-base-300/40">
<!-- Sidebar content here --> <!-- Logo -->
<li class="mb-6"> <div class="px-5 pt-5 pb-3">
<a href="/dashboard" class="flex items-center gap-3 text-2xl font-bold text-primary hover:bg-transparent"> <a href="/dashboard" class="flex items-center gap-2.5 group">
<img src="/logo.webp" alt="Chronus" class="h-10 w-10" /> <img src="/logo.webp" alt="Chronus" class="h-8 w-8" />
Chronus <span class="text-xl font-bold text-primary">Chronus</span>
</a> </a>
</li> </div>
{/* Team Switcher */} <!-- Team Switcher -->
{userMemberships.length > 0 && ( {userMemberships.length > 0 && (
<li class="mb-4"> <div class="px-4 pb-2">
<div class="form-control">
<select <select
class="select select-bordered w-full font-semibold bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary focus:outline-none focus:outline-offset-0 transition-all duration-200 hover:border-primary/40 focus:ring-3 focus:ring-primary/15 [&>option]:bg-base-300 [&>option]:text-base-content [&>option]:p-2" class="select select-sm w-full bg-base-300/40 border-base-300/60 focus:border-primary/50 focus:outline-none text-sm font-medium"
id="team-switcher" id="team-switcher"
aria-label="Switch team"
onchange="document.cookie = 'currentTeamId=' + this.value + '; path=/'; window.location.reload();" onchange="document.cookie = 'currentTeamId=' + this.value + '; path=/'; window.location.reload();"
> >
{userMemberships.map(({ membership, organization }) => ( {userMemberships.map(({ membership, organization }) => (
@@ -102,106 +117,85 @@ const currentTeam = userMemberships.find(m => m.organization.id === currentTeamI
))} ))}
</select> </select>
</div> </div>
</li>
)} )}
{userMemberships.length === 0 && ( {userMemberships.length === 0 && (
<li class="mb-4"> <div class="px-4 pb-2">
<a href="/dashboard/organizations/new" class="btn btn-primary btn-sm"> <a href="/dashboard/organizations/new" class="btn btn-primary btn-sm btn-block">
<Icon name="heroicons:plus" class="w-4 h-4" /> <Icon name="heroicons:plus" class="w-4 h-4" />
Create Team Create Team
</a> </a>
</li> </div>
)} )}
<div class="divider my-2"></div> <div class="divider my-1 mx-4"></div>
<li><a href="/dashboard" class:list={[ <!-- Navigation -->
"hover:bg-base-300/50 rounded-lg transition-colors active:bg-base-300/50!", <nav class="flex-1 px-3">
{ "bg-primary/10 text-primary relative before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:w-0.75 before:h-[70%] before:bg-primary before:rounded-r-full": Astro.url.pathname === "/dashboard" } <ul class="menu menu-sm gap-0.5 p-0">
{navItems.map(item => (
<li>
<a href={item.href} class:list={[
"rounded-lg gap-3 px-3 py-2.5 font-medium text-sm",
isActive(item)
? "bg-primary/10 text-primary"
: "text-base-content/70 hover:text-base-content hover:bg-base-300/50"
]}> ]}>
<Icon name="heroicons:home" class="w-5 h-5" /> <Icon name={item.icon} class="w-[18px] h-[18px]" />
Dashboard {item.label}
</a></li> </a>
<li><a href="/dashboard/tracker" class:list={[ </li>
"hover:bg-base-300/50 rounded-lg transition-colors active:bg-base-300/50!", ))}
{ "bg-primary/10 text-primary relative before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:w-0.75 before:h-[70%] before:bg-primary before:rounded-r-full": Astro.url.pathname.startsWith("/dashboard/tracker") } </ul>
]}>
<Icon name="heroicons:clock" class="w-5 h-5" />
Time Tracker
</a></li>
<li><a href="/dashboard/invoices" class:list={[
"hover:bg-base-300/50 rounded-lg transition-colors active:bg-base-300/50!",
{ "bg-primary/10 text-primary relative before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:w-0.75 before:h-[70%] before:bg-primary before:rounded-r-full": Astro.url.pathname.startsWith("/dashboard/invoices") }
]}>
<Icon name="heroicons:document-currency-dollar" class="w-5 h-5" />
Invoices & Quotes
</a></li>
<li><a href="/dashboard/reports" class:list={[
"hover:bg-base-300/50 rounded-lg transition-colors active:bg-base-300/50!",
{ "bg-primary/10 text-primary relative before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:w-0.75 before:h-[70%] before:bg-primary before:rounded-r-full": Astro.url.pathname.startsWith("/dashboard/reports") }
]}>
<Icon name="heroicons:chart-bar" class="w-5 h-5" />
Reports
</a></li>
<li><a href="/dashboard/clients" class:list={[
"hover:bg-base-300/50 rounded-lg transition-colors active:bg-base-300/50!",
{ "bg-primary/10 text-primary relative before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:w-0.75 before:h-[70%] before:bg-primary before:rounded-r-full": Astro.url.pathname.startsWith("/dashboard/clients") }
]}>
<Icon name="heroicons:building-office" class="w-5 h-5" />
Clients
</a></li>
<li><a href="/dashboard/team" class:list={[
"hover:bg-base-300/50 rounded-lg transition-colors active:bg-base-300/50!",
{ "bg-primary/10 text-primary relative before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:w-0.75 before:h-[70%] before:bg-primary before:rounded-r-full": Astro.url.pathname.startsWith("/dashboard/team") }
]}>
<Icon name="heroicons:user-group" class="w-5 h-5" />
Team
</a></li>
{user.isSiteAdmin && ( {user.isSiteAdmin && (
<> <>
<div class="divider my-2"></div> <div class="divider my-1"></div>
<li><a href="/admin" class:list={[ <ul class="menu menu-sm p-0">
"font-semibold hover:bg-base-300/50 rounded-lg transition-colors active:bg-base-300/50!",
{ "bg-primary/10 text-primary relative before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:w-0.75 before:h-[70%] before:bg-primary before:rounded-r-full": Astro.url.pathname.startsWith("/admin") }
]}>
<Icon name="heroicons:cog-6-tooth" class="w-5 h-5" />
Site Admin
</a></li>
</>
)}
<div class="divider my-2"></div>
<li> <li>
<a href="/dashboard/settings" class="flex items-center gap-3 bg-base-300/30 hover:bg-base-300/60 rounded-lg p-3 transition-colors"> <a href="/admin" class:list={[
<Avatar name={user.name} /> "rounded-lg gap-3 px-3 py-2.5 font-medium text-sm",
<div class="flex-1 min-w-0"> Astro.url.pathname.startsWith("/admin")
<div class="font-semibold text-sm truncate">{user.name}</div> ? "bg-primary/10 text-primary"
<div class="text-xs text-base-content/50 truncate">{user.email}</div> : "text-base-content/70 hover:text-base-content hover:bg-base-300/50"
</div> ]}>
<Icon name="heroicons:chevron-right" class="w-4 h-4 opacity-40" /> <Icon name="heroicons:cog-6-tooth" class="w-[18px] h-[18px]" />
Site Admin
</a> </a>
</li> </li>
</ul>
</>
)}
</nav>
<li> <!-- Bottom Section -->
<div class="flex justify-between items-center p-2 hover:bg-transparent"> <div class="mt-auto border-t border-base-300/40">
<span class="font-semibold text-sm text-base-content/70 pl-2">Theme</span> <div class="p-3">
<a href="/dashboard/settings" class="flex items-center gap-3 rounded-lg p-2.5 hover:bg-base-300/40 group">
<Avatar name={user.name} />
<div class="flex-1 min-w-0">
<div class="font-medium text-sm truncate">{user.name}</div>
<div class="text-xs text-base-content/50 truncate">{user.email}</div>
</div>
<Icon name="heroicons:chevron-right" class="w-4 h-4 text-base-content/30 group-hover:text-base-content/50" />
</a>
</div>
<div class="flex items-center justify-between px-5 pb-2">
<span class="text-xs text-base-content/40 font-medium">Theme</span>
<ThemeToggle client:load /> <ThemeToggle client:load />
</div> </div>
</li>
<li> <div class="px-3 pb-3">
<form action="/api/auth/logout" method="POST" class="contents"> <form action="/api/auth/logout" method="POST">
<button type="submit" class="flex w-full items-center gap-2 py-2 px-4 text-error hover:bg-error/10 rounded-lg transition-colors active:bg-base-300/50!"> <button type="submit" class="btn btn-ghost btn-sm btn-block justify-start gap-2 text-base-content/60 hover:text-error hover:bg-error/10 font-medium">
<Icon name="heroicons:arrow-right-on-rectangle" class="w-5 h-5" /> <Icon name="heroicons:arrow-right-on-rectangle" class="w-[18px] h-[18px]" />
Logout Logout
</button> </button>
</form> </form>
</li> </div>
</ul> </div>
</aside>
</div> </div>
</div> </div>
</body> </body>

View File

@@ -1,6 +1,7 @@
--- ---
import DashboardLayout from '../../layouts/DashboardLayout.astro'; import DashboardLayout from '../../layouts/DashboardLayout.astro';
import Avatar from '../../components/Avatar.astro'; import Avatar from '../../components/Avatar.astro';
import StatCard from '../../components/StatCard.astro';
import { db } from '../../db'; import { db } from '../../db';
import { siteSettings, users } from '../../db/schema'; import { siteSettings, users } from '../../db/schema';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
@@ -21,52 +22,52 @@ const allUsers = await db.select().from(users).all();
--- ---
<DashboardLayout title="Site Admin - Chronus"> <DashboardLayout title="Site Admin - Chronus">
<h1 class="text-3xl font-bold mb-6">Site Administration</h1> <div class="mb-6">
<h1 class="text-2xl font-extrabold tracking-tight">Site Administration</h1>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <p class="text-base-content/60 text-sm mt-1">Manage users and site settings</p>
<!-- Statistics -->
<div class="stats shadow border border-base-200">
<div class="stat">
<div class="stat-title">Total Users</div>
<div class="stat-value">{allUsers.length}</div>
</div>
</div>
</div> </div>
<!-- Settings --> <div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-6">
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6"> <StatCard
<div class="card-body"> title="Total Users"
<h2 class="card-title mb-4">Site Settings</h2> value={String(allUsers.length)}
description="Registered accounts"
<form method="POST" action="/api/admin/settings"> icon="heroicons:users"
<div class="form-control"> color="text-primary"
<label for="registration_enabled" class="label pb-2 font-medium text-sm sm:text-base">
Allow New Registrations
</label>
<br>
<input
type="checkbox"
name="registration_enabled"
class="toggle toggle-primary shrink-0 mt-1"
checked={registrationEnabled}
/> />
</div> </div>
<div class="card-actions justify-end mt-6"> <!-- Settings -->
<button type="submit" class="btn btn-primary">Save Settings</button> <div class="card card-border bg-base-100 mb-6">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-4">Site Settings</h2>
<form method="POST" action="/api/admin/settings">
<fieldset class="fieldset">
<legend class="fieldset-legend text-xs">Allow New Registrations</legend>
<input
type="checkbox"
name="registration_enabled"
class="toggle toggle-primary shrink-0"
checked={registrationEnabled}
/>
</fieldset>
<div class="flex justify-end mt-4">
<button type="submit" class="btn btn-primary btn-sm">Save Settings</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Users List --> <!-- Users List -->
<div class="card bg-base-100 shadow-xl border border-base-200"> <div class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-0">
<h2 class="card-title mb-4">All Users</h2> <div class="px-4 py-3 border-b border-base-200">
<h2 class="text-sm font-semibold">All Users</h2>
</div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@@ -77,22 +78,22 @@ const allUsers = await db.select().from(users).all();
</thead> </thead>
<tbody> <tbody>
{allUsers.map(u => ( {allUsers.map(u => (
<tr> <tr class="hover">
<td> <td>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<Avatar name={u.name} /> <Avatar name={u.name} />
<div class="font-bold">{u.name}</div> <div class="font-medium">{u.name}</div>
</div> </div>
</td> </td>
<td>{u.email}</td> <td class="text-base-content/60">{u.email}</td>
<td> <td>
{u.isSiteAdmin ? ( {u.isSiteAdmin ? (
<span class="badge badge-primary">Yes</span> <span class="badge badge-xs badge-primary">Yes</span>
) : ( ) : (
<span class="badge badge-ghost">No</span> <span class="badge badge-xs badge-ghost">No</span>
)} )}
</td> </td>
<td>{u.createdAt?.toLocaleDateString() ?? 'N/A'}</td> <td class="text-base-content/40">{u.createdAt?.toLocaleDateString() ?? 'N/A'}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@@ -21,20 +21,20 @@ const allClients = await db.select()
<DashboardLayout title="Clients - Chronus"> <DashboardLayout title="Clients - Chronus">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">Clients</h1> <h1 class="text-2xl font-extrabold tracking-tight">Clients</h1>
<a href="/dashboard/clients/new" class="btn btn-primary">Add Client</a> <a href="/dashboard/clients/new" class="btn btn-primary btn-sm">Add Client</a>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{allClients.map(client => ( {allClients.map(client => (
<div class="card bg-base-100 shadow-xl border border-base-200"> <div class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4 gap-1">
<h2 class="card-title">{client.name}</h2> <h2 class="font-semibold">{client.name}</h2>
{client.email && <p class="text-sm text-gray-500">{client.email}</p>} {client.email && <p class="text-sm text-base-content/60">{client.email}</p>}
<p class="text-xs text-gray-400">Created {client.createdAt?.toLocaleDateString() ?? 'N/A'}</p> <p class="text-xs text-base-content/40">Created {client.createdAt?.toLocaleDateString() ?? 'N/A'}</p>
<div class="card-actions justify-end mt-4"> <div class="card-actions justify-end mt-3">
<a href={`/dashboard/clients/${client.id}`} class="btn btn-sm btn-ghost">View</a> <a href={`/dashboard/clients/${client.id}`} class="btn btn-xs btn-ghost">View</a>
<a href={`/dashboard/clients/${client.id}/edit`} class="btn btn-sm btn-primary">Edit</a> <a href={`/dashboard/clients/${client.id}/edit`} class="btn btn-xs btn-primary">Edit</a>
</div> </div>
</div> </div>
</div> </div>
@@ -42,9 +42,9 @@ const allClients = await db.select()
</div> </div>
{allClients.length === 0 && ( {allClients.length === 0 && (
<div class="text-center py-12"> <div class="flex flex-col items-center justify-center py-12 text-center">
<p class="text-gray-500 mb-4">No clients yet</p> <p class="text-base-content/50 text-sm mb-4">No clients yet</p>
<a href="/dashboard/clients/new" class="btn btn-primary">Add Your First Client</a> <a href="/dashboard/clients/new" class="btn btn-primary btn-sm">Add Your First Client</a>
</div> </div>
)} )}
</DashboardLayout> </DashboardLayout>

View File

@@ -29,145 +29,129 @@ if (!client) return Astro.redirect('/dashboard/clients');
<DashboardLayout title={`Edit ${client.name} - Chronus`}> <DashboardLayout title={`Edit ${client.name} - Chronus`}>
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="flex items-center gap-3 mb-6"> <div class="flex items-center gap-3 mb-6">
<a href={`/dashboard/clients/${client.id}`} class="btn btn-ghost btn-sm"> <a href={`/dashboard/clients/${client.id}`} class="btn btn-ghost btn-xs">
<Icon name="heroicons:arrow-left" class="w-5 h-5" /> <Icon name="heroicons:arrow-left" class="w-4 h-4" />
</a> </a>
<h1 class="text-3xl font-bold">Edit Client</h1> <h1 class="text-2xl font-extrabold tracking-tight">Edit Client</h1>
</div> </div>
<form method="POST" action={`/api/clients/${client.id}/update`} class="card bg-base-100 shadow-xl border border-base-200"> <form method="POST" action={`/api/clients/${client.id}/update`} class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4">
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="name"> <legend class="fieldset-legend text-xs">Client Name</legend>
Client Name
</label>
<input <input
type="text" type="text"
id="name" id="name"
name="name" name="name"
value={client.name} value={client.name}
placeholder="Acme Corp" placeholder="Acme Corp"
class="input input-bordered w-full" class="input w-full"
required required
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="email"> <legend class="fieldset-legend text-xs">Email (optional)</legend>
Email (optional)
</label>
<input <input
type="email" type="email"
id="email" id="email"
name="email" name="email"
value={client.email || ''} value={client.email || ''}
placeholder="jason.borne@cia.com" placeholder="jason.borne@cia.com"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="phone"> <legend class="fieldset-legend text-xs">Phone (optional)</legend>
Phone (optional)
</label>
<input <input
type="tel" type="tel"
id="phone" id="phone"
name="phone" name="phone"
value={client.phone || ''} value={client.phone || ''}
placeholder="+1 (780) 420-1337" placeholder="+1 (780) 420-1337"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="divider">Address Details</div> <div class="divider text-xs text-base-content/40">Address Details</div>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="street"> <legend class="fieldset-legend text-xs">Street Address (optional)</legend>
Street Address (optional)
</label>
<input <input
type="text" type="text"
id="street" id="street"
name="street" name="street"
value={client.street || ''} value={client.street || ''}
placeholder="123 Business Rd" placeholder="123 Business Rd"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="city"> <legend class="fieldset-legend text-xs">City (optional)</legend>
City (optional)
</label>
<input <input
type="text" type="text"
id="city" id="city"
name="city" name="city"
value={client.city || ''} value={client.city || ''}
placeholder="Edmonton" placeholder="Edmonton"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="state"> <legend class="fieldset-legend text-xs">State / Province (optional)</legend>
State / Province (optional)
</label>
<input <input
type="text" type="text"
id="state" id="state"
name="state" name="state"
value={client.state || ''} value={client.state || ''}
placeholder="AB" placeholder="AB"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="zip"> <legend class="fieldset-legend text-xs">Zip / Postal Code (optional)</legend>
Zip / Postal Code (optional)
</label>
<input <input
type="text" type="text"
id="zip" id="zip"
name="zip" name="zip"
value={client.zip || ''} value={client.zip || ''}
placeholder="10001" placeholder="10001"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="country"> <legend class="fieldset-legend text-xs">Country (optional)</legend>
Country (optional)
</label>
<input <input
type="text" type="text"
id="country" id="country"
name="country" name="country"
value={client.country || ''} value={client.country || ''}
placeholder="Canada" placeholder="Canada"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
</div> </div>
<div class="card-actions justify-between mt-6"> <div class="flex justify-between items-center mt-4">
<button <button
type="button" type="button"
class="btn btn-error btn-outline" class="btn btn-error btn-outline btn-sm"
onclick={`document.getElementById('delete_modal').showModal()`} onclick={`document.getElementById('delete_modal').showModal()`}
> >
Delete Client Delete Client
</button> </button>
<div class="flex gap-2"> <div class="flex gap-2">
<a href={`/dashboard/clients/${client.id}`} class="btn btn-ghost">Cancel</a> <a href={`/dashboard/clients/${client.id}`} class="btn btn-ghost btn-sm">Cancel</a>
<button type="submit" class="btn btn-primary">Save Changes</button> <button type="submit" class="btn btn-primary btn-sm">Save Changes</button>
</div> </div>
</div> </div>
</div> </div>
@@ -177,17 +161,17 @@ if (!client) return Astro.redirect('/dashboard/clients');
<!-- Delete Confirmation Modal --> <!-- Delete Confirmation Modal -->
<dialog id="delete_modal" class="modal"> <dialog id="delete_modal" class="modal">
<div class="modal-box"> <div class="modal-box">
<h3 class="font-bold text-lg text-error">Delete Client?</h3> <h3 class="font-semibold text-base text-error">Delete Client?</h3>
<p class="py-4"> <p class="py-4 text-sm">
Are you sure you want to delete <strong>{client.name}</strong>? Are you sure you want to delete <strong>{client.name}</strong>?
This action cannot be undone and will delete all associated time entries. This action cannot be undone and will delete all associated time entries.
</p> </p>
<div class="modal-action"> <div class="modal-action">
<form method="dialog"> <form method="dialog">
<button class="btn">Cancel</button> <button class="btn btn-sm">Cancel</button>
</form> </form>
<form method="POST" action={`/api/clients/${client.id}/delete`}> <form method="POST" action={`/api/clients/${client.id}/delete`}>
<button type="submit" class="btn btn-error">Delete</button> <button type="submit" class="btn btn-error btn-sm">Delete</button>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -63,34 +63,34 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
<DashboardLayout title={`${client.name} - Clients - Chronus`}> <DashboardLayout title={`${client.name} - Clients - Chronus`}>
<div class="flex items-center gap-3 mb-6"> <div class="flex items-center gap-3 mb-6">
<a href="/dashboard/clients" class="btn btn-ghost btn-sm"> <a href="/dashboard/clients" class="btn btn-ghost btn-xs">
<Icon name="heroicons:arrow-left" class="w-5 h-5" /> <Icon name="heroicons:arrow-left" class="w-4 h-4" />
</a> </a>
<h1 class="text-3xl font-bold">{client.name}</h1> <h1 class="text-2xl font-extrabold tracking-tight">{client.name}</h1>
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-3 mb-6">
<!-- Client Details Card --> <!-- Client Details Card -->
<div class="card bg-base-100 shadow-xl border border-base-200 lg:col-span-2"> <div class="card card-border bg-base-100 lg:col-span-2">
<div class="card-body"> <div class="card-body p-4">
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div> <div>
<h2 class="card-title text-2xl mb-1">{client.name}</h2> <h2 class="text-sm font-semibold mb-3">{client.name}</h2>
<div class="space-y-2 mb-4"> <div class="space-y-2 mb-4">
{client.email && ( {client.email && (
<div class="flex items-center gap-2 text-base-content/70"> <div class="flex items-center gap-2 text-base-content/60 text-sm">
<Icon name="heroicons:envelope" class="w-4 h-4" /> <Icon name="heroicons:envelope" class="w-4 h-4" />
<a href={`mailto:${client.email}`} class="link link-hover">{client.email}</a> <a href={`mailto:${client.email}`} class="link link-hover">{client.email}</a>
</div> </div>
)} )}
{client.phone && ( {client.phone && (
<div class="flex items-center gap-2 text-base-content/70"> <div class="flex items-center gap-2 text-base-content/60 text-sm">
<Icon name="heroicons:phone" class="w-4 h-4" /> <Icon name="heroicons:phone" class="w-4 h-4" />
<a href={`tel:${client.phone}`} class="link link-hover">{client.phone}</a> <a href={`tel:${client.phone}`} class="link link-hover">{client.phone}</a>
</div> </div>
)} )}
{(client.street || client.city || client.state || client.zip || client.country) && ( {(client.street || client.city || client.state || client.zip || client.country) && (
<div class="flex items-start gap-2 text-base-content/70"> <div class="flex items-start gap-2 text-base-content/60">
<Icon name="heroicons:map-pin" class="w-4 h-4 mt-0.5" /> <Icon name="heroicons:map-pin" class="w-4 h-4 mt-0.5" />
<div class="text-sm space-y-0.5"> <div class="text-sm space-y-0.5">
{client.street && <div>{client.street}</div>} {client.street && <div>{client.street}</div>}
@@ -106,22 +106,22 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
</div> </div>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<a href={`/dashboard/clients/${client.id}/edit`} class="btn btn-primary btn-sm"> <a href={`/dashboard/clients/${client.id}/edit`} class="btn btn-primary btn-xs">
<Icon name="heroicons:pencil" class="w-4 h-4" /> <Icon name="heroicons:pencil" class="w-3 h-3" />
Edit Edit
</a> </a>
<form method="POST" action={`/api/clients/${client.id}/delete`} onsubmit="return confirm('Are you sure you want to delete this client? This will also delete all associated time entries.');"> <form method="POST" action={`/api/clients/${client.id}/delete`} onsubmit="return confirm('Are you sure you want to delete this client? This will also delete all associated time entries.');">
<button type="submit" class="btn btn-error btn-outline btn-sm"> <button type="submit" class="btn btn-error btn-outline btn-xs">
<Icon name="heroicons:trash" class="w-4 h-4" /> <Icon name="heroicons:trash" class="w-3 h-3" />
Delete Delete
</button> </button>
</form> </form>
</div> </div>
</div> </div>
<div class="divider"></div> <div class="divider my-2"></div>
<div class="stats shadow w-full"> <div class="grid grid-cols-2 gap-3">
<StatCard <StatCard
title="Total Time Tracked" title="Total Time Tracked"
value={`${totalHours}h ${totalMinutes}m`} value={`${totalHours}h ${totalMinutes}m`}
@@ -141,30 +141,30 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
</div> </div>
<!-- Meta Info Card --> <!-- Meta Info Card -->
<div class="card bg-base-100 shadow-xl border border-base-200 h-fit"> <div class="card card-border bg-base-100 h-fit">
<div class="card-body"> <div class="card-body p-4">
<h3 class="card-title text-lg mb-4">Information</h3> <h3 class="text-sm font-semibold mb-3">Information</h3>
<div class="space-y-4"> <div class="space-y-3">
<div> <div>
<div class="text-sm font-medium text-base-content/60">Created</div> <div class="text-xs text-base-content/40">Created</div>
<div>{client.createdAt?.toLocaleDateString() ?? 'N/A'}</div> <div class="text-sm">{client.createdAt?.toLocaleDateString() ?? 'N/A'}</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Recent Activity --> <!-- Recent Activity -->
<div class="card bg-base-100 shadow-xl border border-base-200"> <div class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-0">
<h2 class="card-title mb-4">Recent Activity</h2> <div class="px-4 py-3 border-b border-base-200">
<h2 class="text-sm font-semibold">Recent Activity</h2>
</div>
{recentEntries.length > 0 ? ( {recentEntries.length > 0 ? (
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Description</th> <th>Description</th>
@@ -176,11 +176,11 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
</thead> </thead>
<tbody> <tbody>
{recentEntries.map(({ entry, tag, user: entryUser }) => ( {recentEntries.map(({ entry, tag, user: entryUser }) => (
<tr> <tr class="hover">
<td>{entry.description || '-'}</td> <td>{entry.description || '-'}</td>
<td> <td>
{tag ? ( {tag ? (
<div class="badge badge-sm badge-outline flex items-center gap-1"> <div class="badge badge-xs badge-outline flex items-center gap-1">
{tag.color && ( {tag.color && (
<span class="w-2 h-2 rounded-full" style={`background-color: ${tag.color}`}></span> <span class="w-2 h-2 rounded-full" style={`background-color: ${tag.color}`}></span>
)} )}
@@ -188,8 +188,8 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
</div> </div>
) : '-'} ) : '-'}
</td> </td>
<td>{entryUser?.name || 'Unknown'}</td> <td class="text-base-content/60">{entryUser?.name || 'Unknown'}</td>
<td>{entry.startTime.toLocaleDateString()}</td> <td class="text-base-content/40">{entry.startTime.toLocaleDateString()}</td>
<td class="font-mono">{formatTimeRange(entry.startTime, entry.endTime)}</td> <td class="font-mono">{formatTimeRange(entry.startTime, entry.endTime)}</td>
</tr> </tr>
))} ))}
@@ -197,14 +197,14 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
</table> </table>
</div> </div>
) : ( ) : (
<div class="text-center py-8 text-base-content/60"> <div class="text-center py-8 text-base-content/40 text-sm">
No time entries recorded for this client yet. No time entries recorded for this client yet.
</div> </div>
)} )}
{recentEntries.length > 0 && ( {recentEntries.length > 0 && (
<div class="card-actions justify-center mt-4"> <div class="flex justify-center py-3 border-t border-base-200">
<a href={`/dashboard/tracker?client=${client.id}`} class="btn btn-ghost btn-sm"> <a href={`/dashboard/tracker?client=${client.id}`} class="btn btn-ghost btn-xs">
View All Entries View All Entries
</a> </a>
</div> </div>

View File

@@ -7,124 +7,108 @@ if (!user) return Astro.redirect('/login');
<DashboardLayout title="New Client - Chronus"> <DashboardLayout title="New Client - Chronus">
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Add New Client</h1> <h1 class="text-2xl font-extrabold tracking-tight mb-6">Add New Client</h1>
<form method="POST" action="/api/clients/create" class="card bg-base-100 shadow-xl border border-base-200"> <form method="POST" action="/api/clients/create" class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4">
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="name"> <legend class="fieldset-legend text-xs">Client Name</legend>
Client Name
</label>
<input <input
type="text" type="text"
id="name" id="name"
name="name" name="name"
placeholder="Acme Corp" placeholder="Acme Corp"
class="input input-bordered w-full" class="input w-full"
required required
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="email"> <legend class="fieldset-legend text-xs">Email (optional)</legend>
Email (optional)
</label>
<input <input
type="email" type="email"
id="email" id="email"
name="email" name="email"
placeholder="jason.borne@cia.com" placeholder="jason.borne@cia.com"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="phone"> <legend class="fieldset-legend text-xs">Phone (optional)</legend>
Phone (optional)
</label>
<input <input
type="tel" type="tel"
id="phone" id="phone"
name="phone" name="phone"
placeholder="+1 (780) 420-1337" placeholder="+1 (780) 420-1337"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="divider">Address Details</div> <div class="divider text-xs text-base-content/40">Address Details</div>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="street"> <legend class="fieldset-legend text-xs">Street Address (optional)</legend>
Street Address (optional)
</label>
<input <input
type="text" type="text"
id="street" id="street"
name="street" name="street"
placeholder="123 Business Rd" placeholder="123 Business Rd"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="city"> <legend class="fieldset-legend text-xs">City (optional)</legend>
City (optional)
</label>
<input <input
type="text" type="text"
id="city" id="city"
name="city" name="city"
placeholder="Edmonton" placeholder="Edmonton"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="state"> <legend class="fieldset-legend text-xs">State / Province (optional)</legend>
State / Province (optional)
</label>
<input <input
type="text" type="text"
id="state" id="state"
name="state" name="state"
placeholder="AB" placeholder="AB"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="zip"> <legend class="fieldset-legend text-xs">Zip / Postal Code (optional)</legend>
Zip / Postal Code (optional)
</label>
<input <input
type="text" type="text"
id="zip" id="zip"
name="zip" name="zip"
placeholder="10001" placeholder="10001"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="country"> <legend class="fieldset-legend text-xs">Country (optional)</legend>
Country (optional)
</label>
<input <input
type="text" type="text"
id="country" id="country"
name="country" name="country"
placeholder="Canada" placeholder="Canada"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
</div> </div>
<div class="card-actions justify-end mt-6"> <div class="flex justify-end gap-2 mt-4">
<a href="/dashboard/clients" class="btn btn-ghost">Cancel</a> <a href="/dashboard/clients" class="btn btn-ghost btn-sm">Cancel</a>
<button type="submit" class="btn btn-primary">Create Client</button> <button type="submit" class="btn btn-primary btn-sm">Create Client</button>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -104,25 +104,25 @@ const hasMembership = userOrgs.length > 0;
--- ---
<DashboardLayout title="Dashboard - Chronus"> <DashboardLayout title="Dashboard - Chronus">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 sm:gap-0 mb-8"> <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 sm:gap-0 mb-6">
<div> <div>
<h1 class="text-4xl font-bold text-primary mb-2"> <h1 class="text-2xl font-extrabold tracking-tight">
Dashboard Dashboard
</h1> </h1>
<p class="text-base-content/60">Welcome back, {user.name}!</p> <p class="text-base-content/60 text-sm mt-1">Welcome back, {user.name}!</p>
</div> </div>
<a href="/dashboard/organizations/new" class="btn btn-outline"> <a href="/dashboard/organizations/new" class="btn btn-ghost btn-sm">
<Icon name="heroicons:plus" class="w-5 h-5" /> <Icon name="heroicons:plus" class="w-4 h-4" />
New Team New Team
</a> </a>
</div> </div>
{!hasMembership && ( {!hasMembership && (
<div class="alert alert-info mb-8"> <div class="alert alert-info mb-6 text-sm">
<Icon name="heroicons:information-circle" class="w-6 h-6" /> <Icon name="heroicons:information-circle" class="w-5 h-5" />
<div> <div>
<h3 class="font-bold">Welcome to Chronus!</h3> <h3 class="font-bold">Welcome to Chronus!</h3>
<div class="text-sm">You're not part of any team yet. Create one or wait for an invitation.</div> <div class="text-xs">You're not part of any team yet. Create one or wait for an invitation.</div>
</div> </div>
<a href="/dashboard/organizations/new" class="btn btn-primary btn-sm"> <a href="/dashboard/organizations/new" class="btn btn-primary btn-sm">
<Icon name="heroicons:plus" class="w-4 h-4" /> <Icon name="heroicons:plus" class="w-4 h-4" />
@@ -134,14 +134,13 @@ const hasMembership = userOrgs.length > 0;
{hasMembership && ( {hasMembership && (
<> <>
<!-- Stats Overview --> <!-- Stats Overview -->
<div class="stats stats-vertical lg:stats-horizontal shadow-lg w-full mb-8"> <div class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
<StatCard <StatCard
title="This Week" title="This Week"
value={formatDuration(stats.totalTimeThisWeek)} value={formatDuration(stats.totalTimeThisWeek)}
description="Total tracked time" description="Total tracked time"
icon="heroicons:clock" icon="heroicons:clock"
color="text-primary" color="text-primary"
valueClass="text-3xl"
/> />
<StatCard <StatCard
title="This Month" title="This Month"
@@ -149,7 +148,6 @@ const hasMembership = userOrgs.length > 0;
description="Total tracked time" description="Total tracked time"
icon="heroicons:calendar" icon="heroicons:calendar"
color="text-secondary" color="text-secondary"
valueClass="text-3xl"
/> />
<StatCard <StatCard
title="Active Timers" title="Active Timers"
@@ -157,7 +155,6 @@ const hasMembership = userOrgs.length > 0;
description="Currently running" description="Currently running"
icon="heroicons:play-circle" icon="heroicons:play-circle"
color="text-accent" color="text-accent"
valueClass="text-3xl"
/> />
<StatCard <StatCard
title="Clients" title="Clients"
@@ -165,29 +162,28 @@ const hasMembership = userOrgs.length > 0;
description="Total active" description="Total active"
icon="heroicons:building-office" icon="heroicons:building-office"
color="text-info" color="text-info"
valueClass="text-3xl"
/> />
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="card bg-base-100 shadow-xl"> <div class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4">
<h2 class="card-title"> <h2 class="text-sm font-semibold flex items-center gap-2">
<Icon name="heroicons:bolt" class="w-6 h-6 text-warning" /> <Icon name="heroicons:bolt" class="w-4 h-4 text-warning" />
Quick Actions Quick Actions
</h2> </h2>
<div class="flex flex-col gap-3 mt-4"> <div class="flex flex-col gap-2 mt-3">
<a href="/dashboard/tracker" class="btn btn-primary"> <a href="/dashboard/tracker" class="btn btn-primary btn-sm">
<Icon name="heroicons:play" class="w-5 h-5" /> <Icon name="heroicons:play" class="w-4 h-4" />
Start Timer Start Timer
</a> </a>
<a href="/dashboard/clients/new" class="btn btn-outline"> <a href="/dashboard/clients/new" class="btn btn-ghost btn-sm">
<Icon name="heroicons:plus" class="w-5 h-5" /> <Icon name="heroicons:plus" class="w-4 h-4" />
Add Client Add Client
</a> </a>
<a href="/dashboard/reports" class="btn btn-outline"> <a href="/dashboard/reports" class="btn btn-ghost btn-sm">
<Icon name="heroicons:chart-bar" class="w-5 h-5" /> <Icon name="heroicons:chart-bar" class="w-4 h-4" />
View Reports View Reports
</a> </a>
</div> </div>
@@ -195,32 +191,32 @@ const hasMembership = userOrgs.length > 0;
</div> </div>
<!-- Recent Activity --> <!-- Recent Activity -->
<div class="card bg-base-100 shadow-xl"> <div class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4">
<h2 class="card-title"> <h2 class="text-sm font-semibold flex items-center gap-2">
<Icon name="heroicons:clock" class="w-6 h-6 text-success" /> <Icon name="heroicons:clock" class="w-4 h-4 text-success" />
Recent Activity Recent Activity
</h2> </h2>
{stats.recentEntries.length > 0 ? ( {stats.recentEntries.length > 0 ? (
<ul class="space-y-3 mt-4"> <ul class="space-y-2 mt-3">
{stats.recentEntries.map(({ entry, client, tag }) => ( {stats.recentEntries.map(({ entry, client, tag }) => (
<li class="p-3 rounded-lg bg-base-200 border-l-4 hover:bg-base-300 transition-colors" style={`border-color: ${tag?.color || '#3b82f6'}`}> <li class="p-2.5 rounded-lg bg-base-200/50 border-l-3 hover:bg-base-200 transition-colors" style={`border-color: ${tag?.color || 'oklch(var(--p))'}`}>
<div class="font-semibold text-sm">{client.name}</div> <div class="font-medium text-sm">{client.name}</div>
<div class="text-xs text-base-content/60 mt-1 flex flex-wrap gap-2 items-center"> <div class="text-xs text-base-content/50 mt-0.5 flex flex-wrap gap-2 items-center">
<span class="flex gap-1 flex-wrap"> <span class="flex gap-1 flex-wrap">
{tag ? ( {tag ? (
<span class="badge badge-xs badge-outline">{tag.name}</span> <span class="badge badge-xs badge-outline">{tag.name}</span>
) : <span class="italic opacity-50">No tag</span>} ) : <span class="italic opacity-50">No tag</span>}
</span> </span>
<span> {entry.endTime ? formatDuration(entry.endTime.getTime() - entry.startTime.getTime()) : 'Running...'}</span> <span>· {entry.endTime ? formatDuration(entry.endTime.getTime() - entry.startTime.getTime()) : 'Running...'}</span>
</div> </div>
</li> </li>
))} ))}
</ul> </ul>
) : ( ) : (
<div class="flex flex-col items-center justify-center py-8 text-center mt-4"> <div class="flex flex-col items-center justify-center py-6 text-center mt-3">
<Icon name="heroicons:clock" class="w-12 h-12 text-base-content/20 mb-3" /> <Icon name="heroicons:clock" class="w-10 h-10 text-base-content/15 mb-2" />
<p class="text-base-content/60 text-sm">No recent time entries</p> <p class="text-base-content/40 text-sm">No recent time entries</p>
</div> </div>
)} )}
</div> </div>

View File

@@ -62,7 +62,7 @@ const isDraft = invoice.status === 'draft';
<a href="/dashboard/invoices" class="btn btn-ghost btn-xs btn-square"> <a href="/dashboard/invoices" class="btn btn-ghost btn-xs btn-square">
<Icon name="heroicons:arrow-left" class="w-4 h-4" /> <Icon name="heroicons:arrow-left" class="w-4 h-4" />
</a> </a>
<div class={`badge ${ <div class={`badge badge-xs ${
invoice.status === 'paid' || invoice.status === 'accepted' ? 'badge-success' : invoice.status === 'paid' || invoice.status === 'accepted' ? 'badge-success' :
invoice.status === 'sent' ? 'badge-info' : invoice.status === 'sent' ? 'badge-info' :
invoice.status === 'void' || invoice.status === 'declined' ? 'badge-error' : invoice.status === 'void' || invoice.status === 'declined' ? 'badge-error' :
@@ -71,15 +71,15 @@ const isDraft = invoice.status === 'draft';
{invoice.status} {invoice.status}
</div> </div>
</div> </div>
<h1 class="text-3xl font-bold">{invoice.number}</h1> <h1 class="text-2xl font-extrabold tracking-tight">{invoice.number}</h1>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
{isDraft && ( {isDraft && (
<form method="POST" action={`/api/invoices/${invoice.id}/status`}> <form method="POST" action={`/api/invoices/${invoice.id}/status`}>
<input type="hidden" name="status" value="sent" /> <input type="hidden" name="status" value="sent" />
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary btn-sm">
<Icon name="heroicons:paper-airplane" class="w-5 h-5" /> <Icon name="heroicons:paper-airplane" class="w-4 h-4" />
Mark Sent Mark Sent
</button> </button>
</form> </form>
@@ -87,8 +87,8 @@ const isDraft = invoice.status === 'draft';
{(invoice.status !== 'paid' && invoice.status !== 'void' && invoice.type === 'invoice') && ( {(invoice.status !== 'paid' && invoice.status !== 'void' && invoice.type === 'invoice') && (
<form method="POST" action={`/api/invoices/${invoice.id}/status`}> <form method="POST" action={`/api/invoices/${invoice.id}/status`}>
<input type="hidden" name="status" value="paid" /> <input type="hidden" name="status" value="paid" />
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success btn-sm">
<Icon name="heroicons:check" class="w-5 h-5" /> <Icon name="heroicons:check" class="w-4 h-4" />
Mark Paid Mark Paid
</button> </button>
</form> </form>
@@ -96,25 +96,25 @@ const isDraft = invoice.status === 'draft';
{(invoice.status !== 'accepted' && invoice.status !== 'declined' && invoice.status !== 'void' && invoice.type === 'quote') && ( {(invoice.status !== 'accepted' && invoice.status !== 'declined' && invoice.status !== 'void' && invoice.type === 'quote') && (
<form method="POST" action={`/api/invoices/${invoice.id}/status`}> <form method="POST" action={`/api/invoices/${invoice.id}/status`}>
<input type="hidden" name="status" value="accepted" /> <input type="hidden" name="status" value="accepted" />
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success btn-sm">
<Icon name="heroicons:check" class="w-5 h-5" /> <Icon name="heroicons:check" class="w-4 h-4" />
Mark Accepted Mark Accepted
</button> </button>
</form> </form>
)} )}
{(invoice.type === 'quote' && invoice.status === 'accepted') && ( {(invoice.type === 'quote' && invoice.status === 'accepted') && (
<form method="POST" action={`/api/invoices/${invoice.id}/convert`}> <form method="POST" action={`/api/invoices/${invoice.id}/convert`}>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary btn-sm">
<Icon name="heroicons:document-duplicate" class="w-5 h-5" /> <Icon name="heroicons:document-duplicate" class="w-4 h-4" />
Convert to Invoice Convert to Invoice
</button> </button>
</form> </form>
)} )}
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<div role="button" tabindex="0" class="btn btn-square btn-ghost border border-base-300"> <div role="button" tabindex="0" class="btn btn-square btn-ghost btn-sm border border-base-200">
<Icon name="heroicons:ellipsis-horizontal" class="w-6 h-6" /> <Icon name="heroicons:ellipsis-horizontal" class="w-4 h-4" />
</div> </div>
<ul tabindex="0" class="dropdown-content z-1 menu p-2 shadow bg-base-100 rounded-box w-52 border border-base-200"> <ul tabindex="0" class="dropdown-content z-1 menu p-2 bg-base-100 rounded-box w-52 border border-base-200">
<li> <li>
<a href={`/dashboard/invoices/${invoice.id}/edit`}> <a href={`/dashboard/invoices/${invoice.id}/edit`}>
<Icon name="heroicons:pencil-square" class="w-4 h-4" /> <Icon name="heroicons:pencil-square" class="w-4 h-4" />
@@ -153,7 +153,7 @@ const isDraft = invoice.status === 'draft';
</div> </div>
<!-- Invoice Paper --> <!-- Invoice Paper -->
<div class="card bg-base-100 shadow-xl border border-base-200 print:shadow-none print:border-none"> <div class="card card-border bg-base-100 print:shadow-none print:border-none">
<div class="card-body p-8 sm:p-12"> <div class="card-body p-8 sm:p-12">
<!-- Header Section --> <!-- Header Section -->
<div class="flex flex-col sm:flex-row justify-between gap-8 mb-12"> <div class="flex flex-col sm:flex-row justify-between gap-8 mb-12">
@@ -264,20 +264,20 @@ const isDraft = invoice.status === 'draft';
</button> </button>
</div> </div>
<form method="POST" action={`/api/invoices/${invoice.id}/items/add`} class="bg-base-200/50 p-4 rounded-lg mb-8 border border-base-300/50"> <form method="POST" action={`/api/invoices/${invoice.id}/items/add`} class="bg-base-200/50 p-4 rounded-lg mb-8 border border-base-200">
<h4 class="text-sm font-bold mb-3">Add Item</h4> <h4 class="text-xs font-semibold mb-3">Add Item</h4>
<div class="grid grid-cols-1 sm:grid-cols-12 gap-4 items-end"> <div class="grid grid-cols-1 sm:grid-cols-12 gap-3 items-end">
<div class="sm:col-span-6"> <div class="sm:col-span-6">
<label class="label text-xs pt-0" for="item-description">Description</label> <label class="text-xs text-base-content/60" for="item-description">Description</label>
<input type="text" id="item-description" name="description" class="input input-sm input-bordered w-full" required placeholder="Service or product..." /> <input type="text" id="item-description" name="description" class="input input-sm w-full" required placeholder="Service or product..." />
</div> </div>
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<label class="label text-xs pt-0" for="item-quantity">Qty</label> <label class="text-xs text-base-content/60" for="item-quantity">Qty</label>
<input type="number" id="item-quantity" name="quantity" step="0.01" class="input input-sm input-bordered w-full" required value="1" /> <input type="number" id="item-quantity" name="quantity" step="0.01" class="input input-sm w-full" required value="1" />
</div> </div>
<div class="sm:col-span-3"> <div class="sm:col-span-3">
<label class="label text-xs pt-0" for="item-unit-price">Unit Price ({invoice.currency})</label> <label class="text-xs text-base-content/60" for="item-unit-price">Unit Price ({invoice.currency})</label>
<input type="number" id="item-unit-price" name="unitPrice" step="0.01" class="input input-sm input-bordered w-full" required placeholder="0.00" /> <input type="number" id="item-unit-price" name="unitPrice" step="0.01" class="input input-sm w-full" required placeholder="0.00" />
</div> </div>
<div class="sm:col-span-1"> <div class="sm:col-span-1">
<button type="submit" class="btn btn-sm btn-primary w-full"> <button type="submit" class="btn btn-sm btn-primary w-full">
@@ -346,13 +346,11 @@ const isDraft = invoice.status === 'draft';
<!-- Tax Modal --> <!-- Tax Modal -->
<dialog id="tax_modal" class="modal"> <dialog id="tax_modal" class="modal">
<div class="modal-box"> <div class="modal-box">
<h3 class="font-bold text-lg">Update Tax Rate</h3> <h3 class="font-semibold text-base">Update Tax Rate</h3>
<p class="py-4">Enter the tax percentage to apply to the subtotal.</p> <p class="py-3 text-sm text-base-content/60">Enter the tax percentage to apply to the subtotal.</p>
<form method="POST" action={`/api/invoices/${invoice.id}/update-tax`}> <form method="POST" action={`/api/invoices/${invoice.id}/update-tax`}>
<div class="form-control mb-6"> <fieldset class="fieldset mb-4">
<label class="label" for="tax-rate"> <legend class="fieldset-legend text-xs">Tax Rate (%)</legend>
Tax Rate (%)
</label>
<input <input
type="number" type="number"
id="tax-rate" id="tax-rate"
@@ -360,14 +358,14 @@ const isDraft = invoice.status === 'draft';
step="0.01" step="0.01"
min="0" min="0"
max="100" max="100"
class="input input-bordered w-full" class="input w-full"
value={invoice.taxRate ?? 0} value={invoice.taxRate ?? 0}
required required
/> />
</div> </fieldset>
<div class="modal-action"> <div class="modal-action">
<button type="button" class="btn" onclick="document.getElementById('tax_modal').close()">Cancel</button> <button type="button" class="btn btn-sm" onclick="document.getElementById('tax_modal').close()">Cancel</button>
<button type="submit" class="btn btn-primary">Update</button> <button type="submit" class="btn btn-primary btn-sm">Update</button>
</div> </div>
</form> </form>
</div> </div>
@@ -379,30 +377,28 @@ const isDraft = invoice.status === 'draft';
<!-- Import Time Modal --> <!-- Import Time Modal -->
<dialog id="import_time_modal" class="modal"> <dialog id="import_time_modal" class="modal">
<div class="modal-box"> <div class="modal-box">
<h3 class="font-bold text-lg">Import Time Entries</h3> <h3 class="font-semibold text-base">Import Time Entries</h3>
<p class="py-4">Import billable time entries for this client.</p> <p class="py-3 text-sm text-base-content/60">Import billable time entries for this client.</p>
<form method="POST" action={`/api/invoices/${invoice.id}/import-time`}> <form method="POST" action={`/api/invoices/${invoice.id}/import-time`}>
<div class="grid grid-cols-2 gap-4 mb-4"> <div class="grid grid-cols-2 gap-3 mb-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="start-date">Start Date</label> <legend class="fieldset-legend text-xs">Start Date</legend>
<input type="date" id="start-date" name="startDate" class="input input-bordered" required /> <input type="date" id="start-date" name="startDate" class="input" required />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="end-date">End Date</label> <legend class="fieldset-legend text-xs">End Date</legend>
<input type="date" id="end-date" name="endDate" class="input input-bordered" required /> <input type="date" id="end-date" name="endDate" class="input" required />
</div> </fieldset>
</div> </div>
<div class="form-control mb-6"> <label class="label cursor-pointer justify-start gap-3 mb-4">
<label class="label cursor-pointer justify-start gap-4"> <input type="checkbox" name="groupByDay" class="checkbox checkbox-sm" />
<input type="checkbox" name="groupByDay" class="checkbox" /> <span class="text-sm">Group entries by day</span>
<span class="label-text">Group entries by day</span>
</label> </label>
</div>
<div class="modal-action"> <div class="modal-action">
<button type="button" class="btn" onclick="document.getElementById('import_time_modal').close()">Cancel</button> <button type="button" class="btn btn-sm" onclick="document.getElementById('import_time_modal').close()">Cancel</button>
<button type="submit" class="btn btn-primary">Import</button> <button type="submit" class="btn btn-primary btn-sm">Import</button>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -47,83 +47,73 @@ const discountValueDisplay = invoice.discountType === 'fixed'
<DashboardLayout title={`Edit ${invoice.number} - Chronus`}> <DashboardLayout title={`Edit ${invoice.number} - Chronus`}>
<div class="max-w-3xl mx-auto"> <div class="max-w-3xl mx-auto">
<div class="mb-6"> <div class="mb-6">
<a href={`/dashboard/invoices/${invoice.id}`} class="btn btn-ghost btn-sm gap-2 pl-0 hover:bg-transparent text-base-content/60"> <a href={`/dashboard/invoices/${invoice.id}`} class="btn btn-ghost btn-xs gap-2 pl-0 hover:bg-transparent text-base-content/60">
<Icon name="heroicons:arrow-left" class="w-4 h-4" /> <Icon name="heroicons:arrow-left" class="w-4 h-4" />
Back to Invoice Back to Invoice
</a> </a>
<h1 class="text-3xl font-bold mt-2">Edit Details</h1> <h1 class="text-2xl font-extrabold tracking-tight mt-2">Edit Details</h1>
</div> </div>
<form method="POST" action={`/api/invoices/${invoice.id}/update`} class="card bg-base-100 shadow-xl border border-base-200"> <form method="POST" action={`/api/invoices/${invoice.id}/update`} class="card card-border bg-base-100">
<div class="card-body gap-6"> <div class="card-body p-4 gap-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- Number --> <!-- Number -->
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-semibold" for="invoice-number"> <legend class="fieldset-legend text-xs">Number</legend>
Number
</label>
<input <input
type="text" type="text"
id="invoice-number" id="invoice-number"
name="number" name="number"
class="input input-bordered font-mono" class="input font-mono"
value={invoice.number} value={invoice.number}
required required
/> />
</div> </fieldset>
<!-- Currency --> <!-- Currency -->
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-semibold" for="invoice-currency"> <legend class="fieldset-legend text-xs">Currency</legend>
Currency <select id="invoice-currency" name="currency" class="select w-full">
</label>
<select id="invoice-currency" name="currency" class="select select-bordered w-full">
<option value="USD" selected={invoice.currency === 'USD'}>USD ($)</option> <option value="USD" selected={invoice.currency === 'USD'}>USD ($)</option>
<option value="EUR" selected={invoice.currency === 'EUR'}>EUR (€)</option> <option value="EUR" selected={invoice.currency === 'EUR'}>EUR (€)</option>
<option value="GBP" selected={invoice.currency === 'GBP'}>GBP (£)</option> <option value="GBP" selected={invoice.currency === 'GBP'}>GBP (£)</option>
<option value="CAD" selected={invoice.currency === 'CAD'}>CAD ($)</option> <option value="CAD" selected={invoice.currency === 'CAD'}>CAD ($)</option>
<option value="AUD" selected={invoice.currency === 'AUD'}>AUD ($)</option> <option value="AUD" selected={invoice.currency === 'AUD'}>AUD ($)</option>
</select> </select>
</div> </fieldset>
<!-- Issue Date --> <!-- Issue Date -->
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-semibold" for="invoice-issue-date"> <legend class="fieldset-legend text-xs">Issue Date</legend>
Issue Date
</label>
<input <input
type="date" type="date"
id="invoice-issue-date" id="invoice-issue-date"
name="issueDate" name="issueDate"
class="input input-bordered" class="input"
value={issueDateStr} value={issueDateStr}
required required
/> />
</div> </fieldset>
<!-- Due Date --> <!-- Due Date -->
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-semibold" for="invoice-due-date"> <legend class="fieldset-legend text-xs">{invoice.type === 'quote' ? 'Valid Until' : 'Due Date'}</legend>
{invoice.type === 'quote' ? 'Valid Until' : 'Due Date'}
</label>
<input <input
type="date" type="date"
id="invoice-due-date" id="invoice-due-date"
name="dueDate" name="dueDate"
class="input input-bordered" class="input"
value={dueDateStr} value={dueDateStr}
required required
/> />
</div> </fieldset>
<!-- Discount --> <!-- Discount -->
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-semibold" for="invoice-discount-type"> <legend class="fieldset-legend text-xs">Discount</legend>
Discount
</label>
<div class="join w-full"> <div class="join w-full">
<select id="invoice-discount-type" name="discountType" class="select select-bordered join-item"> <select id="invoice-discount-type" name="discountType" class="select join-item">
<option value="percentage" selected={!invoice.discountType || invoice.discountType === 'percentage'}>%</option> <option value="percentage" selected={!invoice.discountType || invoice.discountType === 'percentage'}>%</option>
<option value="fixed" selected={invoice.discountType === 'fixed'}>Fixed</option> <option value="fixed" selected={invoice.discountType === 'fixed'}>Fixed</option>
</select> </select>
@@ -133,17 +123,15 @@ const discountValueDisplay = invoice.discountType === 'fixed'
name="discountValue" name="discountValue"
step="0.01" step="0.01"
min="0" min="0"
class="input input-bordered join-item w-full" class="input join-item w-full"
value={discountValueDisplay} value={discountValueDisplay}
/> />
</div> </div>
</div> </fieldset>
<!-- Tax Rate --> <!-- Tax Rate -->
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-semibold" for="invoice-tax-rate"> <legend class="fieldset-legend text-xs">Tax Rate (%)</legend>
Tax Rate (%)
</label>
<input <input
type="number" type="number"
id="invoice-tax-rate" id="invoice-tax-rate"
@@ -151,30 +139,28 @@ const discountValueDisplay = invoice.discountType === 'fixed'
step="0.01" step="0.01"
min="0" min="0"
max="100" max="100"
class="input input-bordered" class="input"
value={invoice.taxRate} value={invoice.taxRate}
/> />
</div> </fieldset>
</div> </div>
<!-- Notes --> <!-- Notes -->
<div class="form-control flex flex-col"> <fieldset class="fieldset">
<label class="label font-semibold" for="invoice-notes"> <legend class="fieldset-legend text-xs">Notes / Terms</legend>
Notes / Terms
</label>
<textarea <textarea
id="invoice-notes" id="invoice-notes"
name="notes" name="notes"
class="textarea textarea-bordered h-32 font-mono text-sm" class="textarea h-32 font-mono text-sm"
placeholder="Payment terms, bank details, or thank you notes..." placeholder="Payment terms, bank details, or thank you notes..."
>{invoice.notes}</textarea> >{invoice.notes}</textarea>
</div> </fieldset>
<div class="divider"></div> <div class="divider my-0"></div>
<div class="card-actions justify-end"> <div class="flex justify-end gap-2">
<a href={`/dashboard/invoices/${invoice.id}`} class="btn btn-ghost">Cancel</a> <a href={`/dashboard/invoices/${invoice.id}`} class="btn btn-ghost btn-sm">Cancel</a>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary btn-sm">
Save Changes Save Changes
</button> </button>
</div> </div>

View File

@@ -19,7 +19,8 @@ const currentTeamIdResolved = userMembership.organizationId;
// Get filter parameters // Get filter parameters
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const yearParam = Astro.url.searchParams.get('year'); const yearParam = Astro.url.searchParams.get('year');
const selectedYear = yearParam === 'current' || !yearParam ? 'current' : parseInt(yearParam); const selectedYear: string | number = yearParam === 'current' || !yearParam ? 'current' : parseInt(yearParam);
const yearNum = typeof selectedYear === 'number' ? selectedYear : currentYear;
const selectedType = Astro.url.searchParams.get('type') || 'all'; const selectedType = Astro.url.searchParams.get('type') || 'all';
const selectedStatus = Astro.url.searchParams.get('status') || 'all'; const selectedStatus = Astro.url.searchParams.get('status') || 'all';
const sortBy = Astro.url.searchParams.get('sort') || 'date-desc'; const sortBy = Astro.url.searchParams.get('sort') || 'date-desc';
@@ -43,8 +44,8 @@ if (!availableYears.includes(currentYear)) {
} }
// Filter by year // Filter by year
const yearStart = selectedYear === 'current' ? new Date(currentYear, 0, 1) : new Date(selectedYear, 0, 1); const yearStart = new Date(yearNum, 0, 1);
const yearEnd = selectedYear === 'current' ? new Date() : new Date(selectedYear, 11, 31, 23, 59, 59); const yearEnd = selectedYear === 'current' ? new Date() : new Date(yearNum, 11, 31, 23, 59, 59);
let filteredInvoices = allInvoicesRaw.filter(i => { let filteredInvoices = allInvoicesRaw.filter(i => {
const issueDate = i.invoice.issueDate; const issueDate = i.invoice.issueDate;
@@ -103,27 +104,23 @@ const getStatusColor = (status: string) => {
<DashboardLayout title="Invoices & Quotes - Chronus"> <DashboardLayout title="Invoices & Quotes - Chronus">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6"> <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
<div> <div>
<h1 class="text-3xl font-bold">Invoices & Quotes</h1> <h1 class="text-2xl font-extrabold tracking-tight">Invoices & Quotes</h1>
<p class="text-base-content/60 mt-1">Manage your billing and estimates</p> <p class="text-base-content/60 text-sm mt-1">Manage your billing and estimates</p>
</div> </div>
<a href="/dashboard/invoices/new" class="btn btn-primary"> <a href="/dashboard/invoices/new" class="btn btn-primary btn-sm">
<Icon name="heroicons:plus" class="w-5 h-5" /> <Icon name="heroicons:plus" class="w-4 h-4" />
Create New Create New
</a> </a>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> <div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-6">
<div class="stats shadow bg-base-100 border border-base-200">
<StatCard <StatCard
title="Total Invoices" title="Total Invoices"
value={String(yearInvoices.filter(i => i.invoice.type === 'invoice').length)} value={String(yearInvoices.filter(i => i.invoice.type === 'invoice').length)}
description={selectedYear === 'current' ? `${currentYear} (YTD)` : selectedYear} description={selectedYear === 'current' ? `${currentYear} (YTD)` : String(selectedYear)}
icon="heroicons:document-text" icon="heroicons:document-text"
color="text-primary" color="text-primary"
/> />
</div>
<div class="stats shadow bg-base-100 border border-base-200">
<StatCard <StatCard
title="Open Quotes" title="Open Quotes"
value={String(yearInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'sent').length)} value={String(yearInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'sent').length)}
@@ -131,9 +128,6 @@ const getStatusColor = (status: string) => {
icon="heroicons:clipboard-document-list" icon="heroicons:clipboard-document-list"
color="text-secondary" color="text-secondary"
/> />
</div>
<div class="stats shadow bg-base-100 border border-base-200">
<StatCard <StatCard
title="Total Revenue" title="Total Revenue"
value={formatCurrency(yearInvoices value={formatCurrency(yearInvoices
@@ -144,40 +138,33 @@ const getStatusColor = (status: string) => {
color="text-success" color="text-success"
/> />
</div> </div>
</div>
<!-- Filters --> <!-- Filters -->
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6"> <div class="card card-border bg-base-100 mb-6">
<div class="card-body"> <div class="card-body p-4">
<form method="GET" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <form method="GET" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label"> <legend class="fieldset-legend text-xs">Year</legend>
<span class="label-text font-medium">Year</span> <select name="year" class="select w-full" onchange="this.form.submit()">
</label>
<select name="year" class="select select-bordered w-full" onchange="this.form.submit()">
<option value="current" selected={selectedYear === 'current'}>Current Year to Date ({currentYear})</option> <option value="current" selected={selectedYear === 'current'}>Current Year to Date ({currentYear})</option>
{availableYears.map(year => ( {availableYears.map(year => (
<option value={year} selected={year === selectedYear}>{year}</option> <option value={year} selected={year === selectedYear}>{year}</option>
))} ))}
</select> </select>
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label"> <legend class="fieldset-legend text-xs">Type</legend>
<span class="label-text font-medium">Type</span> <select name="type" class="select w-full" onchange="this.form.submit()">
</label>
<select name="type" class="select select-bordered w-full" onchange="this.form.submit()">
<option value="all" selected={selectedType === 'all'}>All Types</option> <option value="all" selected={selectedType === 'all'}>All Types</option>
<option value="invoice" selected={selectedType === 'invoice'}>Invoices</option> <option value="invoice" selected={selectedType === 'invoice'}>Invoices</option>
<option value="quote" selected={selectedType === 'quote'}>Quotes</option> <option value="quote" selected={selectedType === 'quote'}>Quotes</option>
</select> </select>
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label"> <legend class="fieldset-legend text-xs">Status</legend>
<span class="label-text font-medium">Status</span> <select name="status" class="select w-full" onchange="this.form.submit()">
</label>
<select name="status" class="select select-bordered w-full" onchange="this.form.submit()">
<option value="all" selected={selectedStatus === 'all'}>All Statuses</option> <option value="all" selected={selectedStatus === 'all'}>All Statuses</option>
<option value="draft" selected={selectedStatus === 'draft'}>Draft</option> <option value="draft" selected={selectedStatus === 'draft'}>Draft</option>
<option value="sent" selected={selectedStatus === 'sent'}>Sent</option> <option value="sent" selected={selectedStatus === 'sent'}>Sent</option>
@@ -186,13 +173,11 @@ const getStatusColor = (status: string) => {
<option value="declined" selected={selectedStatus === 'declined'}>Declined</option> <option value="declined" selected={selectedStatus === 'declined'}>Declined</option>
<option value="void" selected={selectedStatus === 'void'}>Void</option> <option value="void" selected={selectedStatus === 'void'}>Void</option>
</select> </select>
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label"> <legend class="fieldset-legend text-xs">Sort By</legend>
<span class="label-text font-medium">Sort By</span> <select name="sort" class="select w-full" onchange="this.form.submit()">
</label>
<select name="sort" class="select select-bordered w-full" onchange="this.form.submit()">
<option value="date-desc" selected={sortBy === 'date-desc'}>Date (Newest First)</option> <option value="date-desc" selected={sortBy === 'date-desc'}>Date (Newest First)</option>
<option value="date-asc" selected={sortBy === 'date-asc'}>Date (Oldest First)</option> <option value="date-asc" selected={sortBy === 'date-asc'}>Date (Oldest First)</option>
<option value="amount-desc" selected={sortBy === 'amount-desc'}>Amount (High to Low)</option> <option value="amount-desc" selected={sortBy === 'amount-desc'}>Amount (High to Low)</option>
@@ -200,13 +185,13 @@ const getStatusColor = (status: string) => {
<option value="number-desc" selected={sortBy === 'number-desc'}>Number (Z-A)</option> <option value="number-desc" selected={sortBy === 'number-desc'}>Number (Z-A)</option>
<option value="number-asc" selected={sortBy === 'number-asc'}>Number (A-Z)</option> <option value="number-asc" selected={sortBy === 'number-asc'}>Number (A-Z)</option>
</select> </select>
</div> </fieldset>
</form> </form>
{(selectedYear !== 'current' || selectedType !== 'all' || selectedStatus !== 'all' || sortBy !== 'date-desc') && ( {(selectedYear !== 'current' || selectedType !== 'all' || selectedStatus !== 'all' || sortBy !== 'date-desc') && (
<div class="mt-4"> <div class="mt-3">
<a href="/dashboard/invoices" class="btn btn-ghost btn-sm"> <a href="/dashboard/invoices" class="btn btn-ghost btn-xs">
<Icon name="heroicons:x-mark" class="w-4 h-4" /> <Icon name="heroicons:x-mark" class="w-3 h-3" />
Clear Filters Clear Filters
</a> </a>
</div> </div>
@@ -214,19 +199,19 @@ const getStatusColor = (status: string) => {
</div> </div>
</div> </div>
<div class="card bg-base-100 shadow-xl border border-base-200"> <div class="card card-border bg-base-100">
<div class="card-body p-0"> <div class="card-body p-0">
<div class="px-6 py-4 border-b border-base-200 bg-base-200/30"> <div class="px-4 py-3 border-b border-base-200">
<p class="text-sm text-base-content/70"> <p class="text-xs text-base-content/50">
Showing <span class="font-semibold text-base-content">{allInvoices.length}</span> Showing <span class="font-semibold text-base-content">{allInvoices.length}</span>
{allInvoices.length === 1 ? 'result' : 'results'} {allInvoices.length === 1 ? 'result' : 'results'}
{selectedYear === 'current' ? ` for ${currentYear} (year to date)` : ` for ${selectedYear}`} {selectedYear === 'current' ? ` for ${currentYear} (year to date)` : ` for ${selectedYear}`}
</p> </p>
</div> </div>
<div class="overflow-x-auto md:overflow-visible pb-32 md:pb-0"> <div class="overflow-x-auto md:overflow-visible pb-32 md:pb-0">
<table class="table table-zebra"> <table class="table table-sm">
<thead> <thead>
<tr class="bg-base-200/50"> <tr>
<th>Number</th> <th>Number</th>
<th>Client</th> <th>Client</th>
<th>Date</th> <th>Date</th>
@@ -240,14 +225,14 @@ const getStatusColor = (status: string) => {
<tbody> <tbody>
{allInvoices.length === 0 ? ( {allInvoices.length === 0 ? (
<tr> <tr>
<td colspan="8" class="text-center py-8 text-base-content/60"> <td colspan="8" class="text-center py-8 text-base-content/50 text-sm">
No invoices or quotes found. Create one to get started. No invoices or quotes found. Create one to get started.
</td> </td>
</tr> </tr>
) : ( ) : (
allInvoices.map(({ invoice, client }) => ( allInvoices.map(({ invoice, client }) => (
<tr class="hover:bg-base-200/50 transition-colors"> <tr class="hover">
<td class="font-mono font-medium"> <td class="font-mono font-medium text-sm">
<a href={`/dashboard/invoices/${invoice.id}`} class="link link-hover text-primary"> <a href={`/dashboard/invoices/${invoice.id}`} class="link link-hover text-primary">
{invoice.number} {invoice.number}
</a> </a>
@@ -265,7 +250,7 @@ const getStatusColor = (status: string) => {
{formatCurrency(invoice.total, invoice.currency)} {formatCurrency(invoice.total, invoice.currency)}
</td> </td>
<td> <td>
<div class={`badge ${getStatusColor(invoice.status)} badge-sm uppercase font-bold tracking-wider`}> <div class={`badge ${getStatusColor(invoice.status)} badge-xs uppercase font-bold tracking-wider`}>
{invoice.status} {invoice.status}
</div> </div>
</td> </td>
@@ -274,10 +259,10 @@ const getStatusColor = (status: string) => {
</td> </td>
<td class="text-right"> <td class="text-right">
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<div role="button" tabindex="0" class="btn btn-ghost btn-sm btn-square"> <div role="button" tabindex="0" class="btn btn-ghost btn-xs btn-square">
<Icon name="heroicons:ellipsis-vertical" class="w-5 h-5" /> <Icon name="heroicons:ellipsis-vertical" class="w-4 h-4" />
</div> </div>
<ul tabindex="0" class="dropdown-content menu p-2 shadow-lg bg-base-100 rounded-box w-52 border border-base-200 z-100"> <ul tabindex="0" class="dropdown-content menu p-2 bg-base-100 rounded-box w-52 border border-base-200 z-100">
<li> <li>
<a href={`/dashboard/invoices/${invoice.id}`}> <a href={`/dashboard/invoices/${invoice.id}`}>
<Icon name="heroicons:eye" class="w-4 h-4" /> <Icon name="heroicons:eye" class="w-4 h-4" />

View File

@@ -80,124 +80,112 @@ const defaultDueDate = nextMonth.toISOString().split('T')[0];
<DashboardLayout title="New Document - Chronus"> <DashboardLayout title="New Document - Chronus">
<div class="max-w-3xl mx-auto"> <div class="max-w-3xl mx-auto">
<div class="mb-6"> <div class="mb-6">
<a href="/dashboard/invoices" class="btn btn-ghost btn-sm gap-2 pl-0 hover:bg-transparent text-base-content/60"> <a href="/dashboard/invoices" class="btn btn-ghost btn-xs gap-2 pl-0 hover:bg-transparent text-base-content/60">
<Icon name="heroicons:arrow-left" class="w-4 h-4" /> <Icon name="heroicons:arrow-left" class="w-4 h-4" />
Back to Invoices Back to Invoices
</a> </a>
<h1 class="text-3xl font-bold mt-2">Create New Document</h1> <h1 class="text-2xl font-extrabold tracking-tight mt-2">Create New Document</h1>
</div> </div>
{teamClients.length === 0 ? ( {teamClients.length === 0 ? (
<div role="alert" class="alert alert-warning shadow-lg"> <div role="alert" class="alert alert-warning">
<Icon name="heroicons:exclamation-triangle" class="w-6 h-6" /> <Icon name="heroicons:exclamation-triangle" class="w-5 h-5" />
<div> <div>
<h3 class="font-bold">No Clients Found</h3> <h3 class="font-semibold text-sm">No Clients Found</h3>
<div class="text-xs">You need to add a client before you can create an invoice or quote.</div> <div class="text-xs">You need to add a client before you can create an invoice or quote.</div>
</div> </div>
<a href="/dashboard/clients" class="btn btn-sm">Manage Clients</a> <a href="/dashboard/clients" class="btn btn-sm">Manage Clients</a>
</div> </div>
) : ( ) : (
<form method="POST" action="/api/invoices/create" class="card bg-base-100 shadow-xl border border-base-200"> <form method="POST" action="/api/invoices/create" class="card card-border bg-base-100">
<div class="card-body gap-6"> <div class="card-body p-4 gap-4">
<!-- Document Type --> <!-- Document Type -->
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-semibold" for="document-type-invoice"> <legend class="fieldset-legend text-xs">Document Type</legend>
Document Type <div class="flex gap-3">
</label> <label class="label cursor-pointer justify-start gap-2 border border-base-200 rounded-lg p-3 flex-1 hover:border-primary has-checked:border-primary has-checked:bg-primary/5 transition-all font-medium text-sm" for="document-type-invoice">
<div class="flex gap-4"> <input type="radio" id="document-type-invoice" name="type" value="invoice" class="radio radio-primary radio-sm" checked />
<label class="label cursor-pointer justify-start gap-2 border border-base-300 rounded-lg p-3 flex-1 hover:border-primary has-checked:border-primary has-checked:bg-primary/5 transition-all font-medium" for="document-type-invoice">
<input type="radio" id="document-type-invoice" name="type" value="invoice" class="radio radio-primary" checked />
Invoice Invoice
</label> </label>
<label class="label cursor-pointer justify-start gap-2 border border-base-300 rounded-lg p-3 flex-1 hover:border-primary has-checked:border-primary has-checked:bg-primary/5 transition-all font-medium" for="document-type-quote"> <label class="label cursor-pointer justify-start gap-2 border border-base-200 rounded-lg p-3 flex-1 hover:border-primary has-checked:border-primary has-checked:bg-primary/5 transition-all font-medium text-sm" for="document-type-quote">
<input type="radio" id="document-type-quote" name="type" value="quote" class="radio radio-primary" /> <input type="radio" id="document-type-quote" name="type" value="quote" class="radio radio-primary radio-sm" />
Quote / Estimate Quote / Estimate
</label> </label>
</div> </div>
</div> </fieldset>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- Client --> <!-- Client -->
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-semibold" for="invoice-client"> <legend class="fieldset-legend text-xs">Client</legend>
Client <select id="invoice-client" name="clientId" class="select w-full" required>
</label>
<select id="invoice-client" name="clientId" class="select select-bordered w-full" required>
<option value="" disabled selected>Select a client...</option> <option value="" disabled selected>Select a client...</option>
{teamClients.map(client => ( {teamClients.map(client => (
<option value={client.id}>{client.name}</option> <option value={client.id}>{client.name}</option>
))} ))}
</select> </select>
</div> </fieldset>
<!-- Number --> <!-- Number -->
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-semibold" for="documentNumber"> <legend class="fieldset-legend text-xs">Number</legend>
Number
</label>
<input <input
type="text" type="text"
name="number" name="number"
id="documentNumber" id="documentNumber"
class="input input-bordered font-mono" class="input font-mono"
value={nextInvoiceNumber} value={nextInvoiceNumber}
data-invoice-number={nextInvoiceNumber} data-invoice-number={nextInvoiceNumber}
data-quote-number={nextQuoteNumber} data-quote-number={nextQuoteNumber}
required required
/> />
</div> </fieldset>
<!-- Issue Date --> <!-- Issue Date -->
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-semibold" for="invoice-issue-date"> <legend class="fieldset-legend text-xs">Issue Date</legend>
Issue Date
</label>
<input <input
type="date" type="date"
id="invoice-issue-date" id="invoice-issue-date"
name="issueDate" name="issueDate"
class="input input-bordered" class="input"
value={today} value={today}
required required
/> />
</div> </fieldset>
<!-- Due Date --> <!-- Due Date -->
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-semibold" for="invoice-due-date" id="dueDateLabel"> <legend class="fieldset-legend text-xs" id="dueDateLabel">Due Date</legend>
Due Date
</label>
<input <input
type="date" type="date"
id="invoice-due-date" id="invoice-due-date"
name="dueDate" name="dueDate"
class="input input-bordered" class="input"
value={defaultDueDate} value={defaultDueDate}
required required
/> />
</div> </fieldset>
<!-- Currency --> <!-- Currency -->
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-semibold" for="invoice-currency"> <legend class="fieldset-legend text-xs">Currency</legend>
Currency <select id="invoice-currency" name="currency" class="select w-full">
</label>
<select id="invoice-currency" name="currency" class="select select-bordered w-full">
<option value="USD" selected={currentOrganization?.defaultCurrency === 'USD'}>USD ($)</option> <option value="USD" selected={currentOrganization?.defaultCurrency === 'USD'}>USD ($)</option>
<option value="EUR" selected={currentOrganization?.defaultCurrency === 'EUR'}>EUR (€)</option> <option value="EUR" selected={currentOrganization?.defaultCurrency === 'EUR'}>EUR (€)</option>
<option value="GBP" selected={currentOrganization?.defaultCurrency === 'GBP'}>GBP (£)</option> <option value="GBP" selected={currentOrganization?.defaultCurrency === 'GBP'}>GBP (£)</option>
<option value="CAD" selected={currentOrganization?.defaultCurrency === 'CAD'}>CAD ($)</option> <option value="CAD" selected={currentOrganization?.defaultCurrency === 'CAD'}>CAD ($)</option>
<option value="AUD" selected={currentOrganization?.defaultCurrency === 'AUD'}>AUD ($)</option> <option value="AUD" selected={currentOrganization?.defaultCurrency === 'AUD'}>AUD ($)</option>
</select> </select>
</div> </fieldset>
</div> </div>
<div class="divider"></div> <div class="divider my-0"></div>
<div class="card-actions justify-end"> <div class="flex justify-end gap-2">
<a href="/dashboard/invoices" class="btn btn-ghost">Cancel</a> <a href="/dashboard/invoices" class="btn btn-ghost btn-sm">Cancel</a>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary btn-sm">
Create Draft Create Draft
<Icon name="heroicons:arrow-right" class="w-4 h-4" /> <Icon name="heroicons:arrow-right" class="w-4 h-4" />
</button> </button>

View File

@@ -12,36 +12,34 @@ if (!user) return Astro.redirect('/login');
<DashboardLayout title="Create Team - Chronus"> <DashboardLayout title="Create Team - Chronus">
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="flex items-center gap-3 mb-6"> <div class="flex items-center gap-3 mb-6">
<a href="/dashboard" class="btn btn-ghost btn-sm"> <a href="/dashboard" class="btn btn-ghost btn-xs">
<Icon name="heroicons:arrow-left" class="w-5 h-5" /> <Icon name="heroicons:arrow-left" class="w-4 h-4" />
</a> </a>
<h1 class="text-3xl font-bold">Create New Team</h1> <h1 class="text-2xl font-extrabold tracking-tight">Create New Team</h1>
</div> </div>
<form method="POST" action="/api/organizations/create" class="card bg-base-200 shadow-xl border border-base-300"> <form method="POST" action="/api/organizations/create" class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4">
<div class="alert alert-info mb-4"> <div class="alert alert-info mb-4">
<Icon name="heroicons:information-circle" class="w-6 h-6" /> <Icon name="heroicons:information-circle" class="w-4 h-4" />
<span>Create a new team to manage separate projects and collaborators. You'll be the owner.</span> <span class="text-sm">Create a new team to manage separate projects and collaborators. You'll be the owner.</span>
</div> </div>
<div class="form-control"> <fieldset class="fieldset">
<label class="label pb-2 font-medium" for="name"> <legend class="fieldset-legend text-xs">Team Name</legend>
Team Name
</label>
<input <input
type="text" type="text"
id="name" id="name"
name="name" name="name"
placeholder="Acme Corp" placeholder="Acme Corp"
class="input input-bordered w-full" class="input w-full"
required required
/> />
</div> </fieldset>
<div class="card-actions justify-end mt-6"> <div class="flex justify-end gap-2 mt-4">
<a href="/dashboard" class="btn btn-ghost">Cancel</a> <a href="/dashboard" class="btn btn-ghost btn-sm">Cancel</a>
<button type="submit" class="btn btn-primary">Create Team</button> <button type="submit" class="btn btn-primary btn-sm">Create Team</button>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -252,17 +252,15 @@ function getTimeRangeLabel(range: string) {
--- ---
<DashboardLayout title="Reports - Chronus"> <DashboardLayout title="Reports - Chronus">
<h1 class="text-3xl font-bold mb-6">Team Reports</h1> <h1 class="text-2xl font-extrabold tracking-tight mb-6">Team Reports</h1>
<!-- Filters --> <!-- Filters -->
<div class="card bg-base-200 shadow-xl border border-base-300 mb-6"> <div class="card card-border bg-base-100 mb-6">
<div class="card-body"> <div class="card-body p-4">
<form method="GET" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <form method="GET" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="reports-range"> <legend class="fieldset-legend text-xs">Time Range</legend>
Time Range <select id="reports-range" name="range" class="select w-full" onchange="this.form.submit()">
</label>
<select id="reports-range" name="range" class="select select-bordered" onchange="this.form.submit()">
<option value="today" selected={timeRange === 'today'}>Today</option> <option value="today" selected={timeRange === 'today'}>Today</option>
<option value="week" selected={timeRange === 'week'}>Last 7 Days</option> <option value="week" selected={timeRange === 'week'}>Last 7 Days</option>
<option value="month" selected={timeRange === 'month'}>Last 30 Days</option> <option value="month" selected={timeRange === 'month'}>Last 30 Days</option>
@@ -271,44 +269,38 @@ function getTimeRangeLabel(range: string) {
<option value="last-month" selected={timeRange === 'last-month'}>Last Month</option> <option value="last-month" selected={timeRange === 'last-month'}>Last Month</option>
<option value="custom" selected={timeRange === 'custom'}>Custom Range</option> <option value="custom" selected={timeRange === 'custom'}>Custom Range</option>
</select> </select>
</div> </fieldset>
{timeRange === 'custom' && ( {timeRange === 'custom' && (
<> <>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="reports-from"> <legend class="fieldset-legend text-xs">From Date</legend>
From Date
</label>
<input <input
type="date" type="date"
id="reports-from" id="reports-from"
name="from" name="from"
class="input input-bordered w-full" class="input w-full"
value={customFrom || (startDate.getFullYear() + '-' + String(startDate.getMonth() + 1).padStart(2, '0') + '-' + String(startDate.getDate()).padStart(2, '0'))} value={customFrom || (startDate.getFullYear() + '-' + String(startDate.getMonth() + 1).padStart(2, '0') + '-' + String(startDate.getDate()).padStart(2, '0'))}
onchange="this.form.submit()" onchange="this.form.submit()"
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="reports-to"> <legend class="fieldset-legend text-xs">To Date</legend>
To Date
</label>
<input <input
type="date" type="date"
id="reports-to" id="reports-to"
name="to" name="to"
class="input input-bordered w-full" class="input w-full"
value={customTo || (endDate.getFullYear() + '-' + String(endDate.getMonth() + 1).padStart(2, '0') + '-' + String(endDate.getDate()).padStart(2, '0'))} value={customTo || (endDate.getFullYear() + '-' + String(endDate.getMonth() + 1).padStart(2, '0') + '-' + String(endDate.getDate()).padStart(2, '0'))}
onchange="this.form.submit()" onchange="this.form.submit()"
/> />
</div> </fieldset>
</> </>
)} )}
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="reports-member"> <legend class="fieldset-legend text-xs">Team Member</legend>
Team Member <select id="reports-member" name="member" class="select w-full" onchange="this.form.submit()">
</label>
<select id="reports-member" name="member" class="select select-bordered" onchange="this.form.submit()">
<option value="">All Members</option> <option value="">All Members</option>
{teamMembers.map(member => ( {teamMembers.map(member => (
<option value={member.id} selected={selectedMemberId === member.id}> <option value={member.id} selected={selectedMemberId === member.id}>
@@ -316,13 +308,11 @@ function getTimeRangeLabel(range: string) {
</option> </option>
))} ))}
</select> </select>
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="reports-tag"> <legend class="fieldset-legend text-xs">Tag</legend>
Tag <select id="reports-tag" name="tag" class="select w-full" onchange="this.form.submit()">
</label>
<select id="reports-tag" name="tag" class="select select-bordered" onchange="this.form.submit()">
<option value="">All Tags</option> <option value="">All Tags</option>
{allTags.map(tag => ( {allTags.map(tag => (
<option value={tag.id} selected={selectedTagId === tag.id}> <option value={tag.id} selected={selectedTagId === tag.id}>
@@ -330,13 +320,11 @@ function getTimeRangeLabel(range: string) {
</option> </option>
))} ))}
</select> </select>
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="reports-client"> <legend class="fieldset-legend text-xs">Client</legend>
Client <select id="reports-client" name="client" class="select w-full" onchange="this.form.submit()">
</label>
<select id="reports-client" name="client" class="select select-bordered" onchange="this.form.submit()">
<option value="">All Clients</option> <option value="">All Clients</option>
{allClients.map(client => ( {allClients.map(client => (
<option value={client.id} selected={selectedClientId === client.id}> <option value={client.id} selected={selectedClientId === client.id}>
@@ -344,28 +332,13 @@ function getTimeRangeLabel(range: string) {
</option> </option>
))} ))}
</select> </select>
</div> </fieldset>
</form> </form>
<style>
@media (max-width: 767px) {
form {
align-items: stretch !important;
}
.form-control {
width: 100%;
}
}
select, input {
width: 100%;
}
</style>
</div> </div>
</div> </div>
<!-- Summary Stats --> <!-- Summary Stats -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"> <div class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
<div class="stats shadow border border-base-300">
<StatCard <StatCard
title="Total Time" title="Total Time"
value={formatDuration(totalTime)} value={formatDuration(totalTime)}
@@ -373,9 +346,6 @@ function getTimeRangeLabel(range: string) {
icon="heroicons:clock" icon="heroicons:clock"
color="text-primary" color="text-primary"
/> />
</div>
<div class="stats shadow border border-base-300">
<StatCard <StatCard
title="Total Entries" title="Total Entries"
value={String(entries.length)} value={String(entries.length)}
@@ -383,9 +353,6 @@ function getTimeRangeLabel(range: string) {
icon="heroicons:list-bullet" icon="heroicons:list-bullet"
color="text-secondary" color="text-secondary"
/> />
</div>
<div class="stats shadow border border-base-300">
<StatCard <StatCard
title="Revenue" title="Revenue"
value={formatCurrency(revenueStats.total)} value={formatCurrency(revenueStats.total)}
@@ -393,9 +360,6 @@ function getTimeRangeLabel(range: string) {
icon="heroicons:currency-dollar" icon="heroicons:currency-dollar"
color="text-success" color="text-success"
/> />
</div>
<div class="stats shadow border border-base-300">
<StatCard <StatCard
title="Active Members" title="Active Members"
value={String(statsByMember.filter(s => s.entryCount > 0).length)} value={String(statsByMember.filter(s => s.entryCount > 0).length)}
@@ -404,16 +368,13 @@ function getTimeRangeLabel(range: string) {
color="text-accent" color="text-accent"
/> />
</div> </div>
</div>
</div>
</div>
<!-- Invoice & Quote Stats --> <!-- Invoice & Quote Stats -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div class="card bg-base-100 shadow-xl border border-base-200"> <div class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4">
<h2 class="card-title mb-4"> <h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:document-text" class="w-6 h-6" /> <Icon name="heroicons:document-text" class="w-4 h-4" />
Invoices Overview Invoices Overview
</h2> </h2>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
@@ -446,10 +407,10 @@ function getTimeRangeLabel(range: string) {
</div> </div>
</div> </div>
<div class="card bg-base-100 shadow-xl border border-base-200"> <div class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4">
<h2 class="card-title mb-4"> <h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:clipboard-document-list" class="w-6 h-6" /> <Icon name="heroicons:clipboard-document-list" class="w-4 h-4" />
Quotes Overview Quotes Overview
</h2> </h2>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
@@ -487,14 +448,14 @@ function getTimeRangeLabel(range: string) {
<!-- Revenue by Client - Only show if there's revenue data and no client filter --> <!-- Revenue by Client - Only show if there's revenue data and no client filter -->
{!selectedClientId && revenueByClient.length > 0 && ( {!selectedClientId && revenueByClient.length > 0 && (
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6"> <div class="card card-border bg-base-100 mb-6">
<div class="card-body"> <div class="card-body p-4">
<h2 class="card-title mb-4"> <h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:banknotes" class="w-6 h-6" /> <Icon name="heroicons:banknotes" class="w-4 h-4" />
Revenue by Client Revenue by Client
</h2> </h2>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Client</th> <th>Client</th>
@@ -507,11 +468,11 @@ function getTimeRangeLabel(range: string) {
{revenueByClient.slice(0, 10).map(stat => ( {revenueByClient.slice(0, 10).map(stat => (
<tr> <tr>
<td> <td>
<div class="font-bold">{stat.client.name}</div> <div class="font-medium">{stat.client.name}</div>
</td> </td>
<td class="font-mono font-bold text-success">{formatCurrency(stat.revenue)}</td> <td class="font-mono font-semibold text-success text-sm">{formatCurrency(stat.revenue)}</td>
<td>{stat.invoiceCount}</td> <td>{stat.invoiceCount}</td>
<td class="font-mono"> <td class="font-mono text-sm">
{stat.invoiceCount > 0 ? formatCurrency(stat.revenue / stat.invoiceCount) : formatCurrency(0)} {stat.invoiceCount > 0 ? formatCurrency(stat.revenue / stat.invoiceCount) : formatCurrency(0)}
</td> </td>
</tr> </tr>
@@ -526,13 +487,13 @@ function getTimeRangeLabel(range: string) {
{/* Charts Section - Only show if there's data */} {/* Charts Section - Only show if there's data */}
{totalTime > 0 && ( {totalTime > 0 && (
<> <>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
{/* Tag Distribution Chart - Only show when no tag filter */} {/* Tag Distribution Chart - Only show when no tag filter */}
{!selectedTagId && statsByTag.filter(s => s.totalTime > 0).length > 0 && ( {!selectedTagId && statsByTag.filter(s => s.totalTime > 0).length > 0 && (
<div class="card bg-base-100 shadow-xl border border-base-200"> <div class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4">
<h2 class="card-title mb-4"> <h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:chart-pie" class="w-6 h-6" /> <Icon name="heroicons:chart-pie" class="w-4 h-4" />
Tag Distribution Tag Distribution
</h2> </h2>
<div class="h-64 w-full"> <div class="h-64 w-full">
@@ -551,10 +512,10 @@ function getTimeRangeLabel(range: string) {
{/* Client Distribution Chart - Only show when no client filter */} {/* Client Distribution Chart - Only show when no client filter */}
{!selectedClientId && statsByClient.filter(s => s.totalTime > 0).length > 0 && ( {!selectedClientId && statsByClient.filter(s => s.totalTime > 0).length > 0 && (
<div class="card bg-base-100 shadow-xl border border-base-200"> <div class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4">
<h2 class="card-title mb-4"> <h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:chart-bar" class="w-6 h-6" /> <Icon name="heroicons:chart-bar" class="w-4 h-4" />
Time by Client Time by Client
</h2> </h2>
<div class="h-64 w-full"> <div class="h-64 w-full">
@@ -573,10 +534,10 @@ function getTimeRangeLabel(range: string) {
{/* Team Member Chart - Only show when no member filter */} {/* Team Member Chart - Only show when no member filter */}
{!selectedMemberId && statsByMember.filter(s => s.totalTime > 0).length > 0 && ( {!selectedMemberId && statsByMember.filter(s => s.totalTime > 0).length > 0 && (
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6"> <div class="card card-border bg-base-100 mb-6">
<div class="card-body"> <div class="card-body p-4">
<h2 class="card-title mb-4"> <h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:users" class="w-6 h-6" /> <Icon name="heroicons:users" class="w-4 h-4" />
Time by Team Member Time by Team Member
</h2> </h2>
<div class="h-64 w-full"> <div class="h-64 w-full">
@@ -596,14 +557,14 @@ function getTimeRangeLabel(range: string) {
{/* Stats by Member - Only show if there's data and no member filter */} {/* Stats by Member - Only show if there's data and no member filter */}
{!selectedMemberId && statsByMember.filter(s => s.totalTime > 0).length > 0 && ( {!selectedMemberId && statsByMember.filter(s => s.totalTime > 0).length > 0 && (
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6"> <div class="card card-border bg-base-100 mb-6">
<div class="card-body"> <div class="card-body p-4">
<h2 class="card-title mb-4"> <h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:users" class="w-6 h-6" /> <Icon name="heroicons:users" class="w-4 h-4" />
By Team Member By Team Member
</h2> </h2>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Member</th> <th>Member</th>
@@ -617,13 +578,13 @@ function getTimeRangeLabel(range: string) {
<tr> <tr>
<td> <td>
<div> <div>
<div class="font-bold">{stat.member.name}</div> <div class="font-medium">{stat.member.name}</div>
<div class="text-sm opacity-50">{stat.member.email}</div> <div class="text-xs text-base-content/40">{stat.member.email}</div>
</div> </div>
</td> </td>
<td class="font-mono">{formatDuration(stat.totalTime)}</td> <td class="font-mono text-sm">{formatDuration(stat.totalTime)}</td>
<td>{stat.entryCount}</td> <td>{stat.entryCount}</td>
<td class="font-mono"> <td class="font-mono text-sm">
{stat.entryCount > 0 ? formatDuration(stat.totalTime / stat.entryCount) : '00:00:00 (0m)'} {stat.entryCount > 0 ? formatDuration(stat.totalTime / stat.entryCount) : '00:00:00 (0m)'}
</td> </td>
</tr> </tr>
@@ -637,14 +598,14 @@ function getTimeRangeLabel(range: string) {
{/* Stats by Tag - Only show if there's data and no tag filter */} {/* Stats by Tag - Only show if there's data and no tag filter */}
{!selectedTagId && statsByTag.filter(s => s.totalTime > 0).length > 0 && ( {!selectedTagId && statsByTag.filter(s => s.totalTime > 0).length > 0 && (
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6"> <div class="card card-border bg-base-100 mb-6">
<div class="card-body"> <div class="card-body p-4">
<h2 class="card-title mb-4"> <h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:tag" class="w-6 h-6" /> <Icon name="heroicons:tag" class="w-4 h-4" />
By Tag By Tag
</h2> </h2>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Tag</th> <th>Tag</th>
@@ -659,21 +620,21 @@ function getTimeRangeLabel(range: string) {
<td> <td>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{stat.tag.color && ( {stat.tag.color && (
<span class="w-4 h-4 rounded-full" style={`background-color: ${stat.tag.color}`}></span> <span class="w-3 h-3 rounded-full" style={`background-color: ${stat.tag.color}`}></span>
)} )}
<span>{stat.tag.name}</span> <span>{stat.tag.name}</span>
</div> </div>
</td> </td>
<td class="font-mono">{formatDuration(stat.totalTime)}</td> <td class="font-mono text-sm">{formatDuration(stat.totalTime)}</td>
<td>{stat.entryCount}</td> <td>{stat.entryCount}</td>
<td> <td>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<progress <progress
class="progress progress-primary w-20" class="progress progress-primary w-16"
value={stat.totalTime} value={stat.totalTime}
max={totalTime} max={totalTime}
></progress> ></progress>
<span class="text-sm"> <span class="text-xs">
{totalTime > 0 ? Math.round((stat.totalTime / totalTime) * 100) : 0}% {totalTime > 0 ? Math.round((stat.totalTime / totalTime) * 100) : 0}%
</span> </span>
</div> </div>
@@ -689,14 +650,14 @@ function getTimeRangeLabel(range: string) {
{/* Stats by Client - Only show if there's data and no client filter */} {/* Stats by Client - Only show if there's data and no client filter */}
{!selectedClientId && statsByClient.filter(s => s.totalTime > 0).length > 0 && ( {!selectedClientId && statsByClient.filter(s => s.totalTime > 0).length > 0 && (
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6"> <div class="card card-border bg-base-100 mb-6">
<div class="card-body"> <div class="card-body p-4">
<h2 class="card-title mb-4"> <h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:building-office" class="w-6 h-6" /> <Icon name="heroicons:building-office" class="w-4 h-4" />
By Client By Client
</h2> </h2>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Client</th> <th>Client</th>
@@ -709,16 +670,16 @@ function getTimeRangeLabel(range: string) {
{statsByClient.filter(s => s.totalTime > 0).map(stat => ( {statsByClient.filter(s => s.totalTime > 0).map(stat => (
<tr> <tr>
<td>{stat.client.name}</td> <td>{stat.client.name}</td>
<td class="font-mono">{formatDuration(stat.totalTime)}</td> <td class="font-mono text-sm">{formatDuration(stat.totalTime)}</td>
<td>{stat.entryCount}</td> <td>{stat.entryCount}</td>
<td> <td>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<progress <progress
class="progress progress-secondary w-20" class="progress progress-secondary w-16"
value={stat.totalTime} value={stat.totalTime}
max={totalTime} max={totalTime}
></progress> ></progress>
<span class="text-sm"> <span class="text-xs">
{totalTime > 0 ? Math.round((stat.totalTime / totalTime) * 100) : 0}% {totalTime > 0 ? Math.round((stat.totalTime / totalTime) * 100) : 0}%
</span> </span>
</div> </div>
@@ -733,23 +694,23 @@ function getTimeRangeLabel(range: string) {
)} )}
{/* Detailed Entries */} {/* Detailed Entries */}
<div class="card bg-base-100 shadow-xl border border-base-200"> <div class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-3">
<h2 class="card-title"> <h2 class="text-sm font-semibold flex items-center gap-2">
<Icon name="heroicons:document-text" class="w-6 h-6" /> <Icon name="heroicons:document-text" class="w-4 h-4" />
Detailed Entries ({entries.length}) Detailed Entries ({entries.length})
</h2> </h2>
{entries.length > 0 && ( {entries.length > 0 && (
<a href={`/api/reports/export${url.search}`} class="btn btn-sm btn-outline" target="_blank"> <a href={`/api/reports/export${url.search}`} class="btn btn-xs btn-ghost" target="_blank">
<Icon name="heroicons:arrow-down-tray" class="w-4 h-4" /> <Icon name="heroicons:arrow-down-tray" class="w-3.5 h-3.5" />
Export CSV Export CSV
</a> </a>
)} )}
</div> </div>
{entries.length > 0 ? ( {entries.length > 0 ? (
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table table-zebra"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Date</th> <th>Date</th>
@@ -765,7 +726,7 @@ function getTimeRangeLabel(range: string) {
<tr> <tr>
<td class="whitespace-nowrap"> <td class="whitespace-nowrap">
{e.entry.startTime.toLocaleDateString()}<br/> {e.entry.startTime.toLocaleDateString()}<br/>
<span class="text-xs opacity-50"> <span class="text-xs text-base-content/40">
{e.entry.startTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} {e.entry.startTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</span> </span>
</td> </td>
@@ -773,18 +734,18 @@ function getTimeRangeLabel(range: string) {
<td>{e.client.name}</td> <td>{e.client.name}</td>
<td> <td>
{e.tag ? ( {e.tag ? (
<div class="badge badge-sm badge-outline flex items-center gap-1"> <div class="badge badge-xs badge-outline flex items-center gap-1">
{e.tag.color && ( {e.tag.color && (
<span class="w-2 h-2 rounded-full" style={`background-color: ${e.tag.color}`}></span> <span class="w-2 h-2 rounded-full" style={`background-color: ${e.tag.color}`}></span>
)} )}
<span>{e.tag.name}</span> <span>{e.tag.name}</span>
</div> </div>
) : ( ) : (
<span class="opacity-50">-</span> <span class="text-base-content/30">-</span>
)} )}
</td> </td>
<td>{e.entry.description || '-'}</td> <td class="text-base-content/60">{e.entry.description || '-'}</td>
<td class="font-mono"> <td class="font-mono text-sm">
{e.entry.endTime {e.entry.endTime
? formatDuration(e.entry.endTime.getTime() - e.entry.startTime.getTime()) ? formatDuration(e.entry.endTime.getTime() - e.entry.startTime.getTime())
: 'Running...' : 'Running...'
@@ -796,12 +757,12 @@ function getTimeRangeLabel(range: string) {
</table> </table>
</div> </div>
) : ( ) : (
<div class="flex flex-col items-center justify-center py-12 text-center"> <div class="flex flex-col items-center justify-center py-10 text-center">
<Icon name="heroicons:inbox" class="w-16 h-16 text-base-content/20 mb-4" /> <Icon name="heroicons:inbox" class="w-12 h-12 text-base-content/15 mb-3" />
<h3 class="text-lg font-semibold mb-2">No time entries found</h3> <h3 class="text-base font-semibold mb-1">No time entries found</h3>
<p class="text-base-content/60 mb-4">Try adjusting your filters or select a different time range.</p> <p class="text-base-content/50 text-sm mb-4">Try adjusting your filters or select a different time range.</p>
<a href="/dashboard/tracker" class="btn btn-primary"> <a href="/dashboard/tracker" class="btn btn-primary btn-sm">
<Icon name="heroicons:play" class="w-5 h-5" /> <Icon name="heroicons:play" class="w-4 h-4" />
Start Tracking Time Start Tracking Time
</a> </a>
</div> </div>

View File

@@ -30,7 +30,7 @@ const userPasskeys = await db.select()
<DashboardLayout title="Account Settings - Chronus"> <DashboardLayout title="Account Settings - Chronus">
<div class="max-w-4xl mx-auto px-4 sm:px-6"> <div class="max-w-4xl mx-auto px-4 sm:px-6">
<h1 class="text-2xl sm:text-3xl font-bold mb-6 sm:mb-8 text-primary"> <h1 class="text-2xl font-extrabold tracking-tight mb-6 sm:mb-8">
Account Settings Account Settings
</h1> </h1>
@@ -69,25 +69,25 @@ const userPasskeys = await db.select()
createdAt: t.createdAt ? t.createdAt.toISOString() : '' createdAt: t.createdAt ? t.createdAt.toISOString() : ''
}))} /> }))} />
<div class="card bg-base-100 shadow-xl border border-base-200"> <div class="card card-border bg-base-100">
<div class="card-body p-4 sm:p-6"> <div class="card-body p-4">
<h2 class="card-title mb-6 text-lg sm:text-xl"> <h2 class="text-sm font-semibold flex items-center gap-2 mb-4">
<Icon name="heroicons:information-circle" class="w-5 h-5 sm:w-6 sm:h-6" /> <Icon name="heroicons:information-circle" class="w-4 h-4" />
Account Information Account Information
</h2> </h2>
<div class="space-y-3"> <div class="space-y-3">
<div class="flex flex-col sm:flex-row sm:justify-between py-3 border-b border-base-300 gap-2 sm:gap-0"> <div class="flex flex-col sm:flex-row sm:justify-between py-3 border-b border-base-200 gap-2 sm:gap-0">
<span class="text-base-content/70 text-sm sm:text-base">Account ID</span> <span class="text-base-content/60 text-sm">Account ID</span>
<span class="font-mono text-xs sm:text-sm break-all">{user.id}</span> <span class="font-mono text-xs break-all">{user.id}</span>
</div> </div>
<div class="flex flex-col sm:flex-row sm:justify-between py-3 border-b border-base-300 gap-2 sm:gap-0"> <div class="flex flex-col sm:flex-row sm:justify-between py-3 border-b border-base-200 gap-2 sm:gap-0">
<span class="text-base-content/70 text-sm sm:text-base">Email</span> <span class="text-base-content/60 text-sm">Email</span>
<span class="text-sm sm:text-base break-all">{user.email}</span> <span class="text-sm break-all">{user.email}</span>
</div> </div>
<div class="flex flex-col sm:flex-row sm:justify-between py-3 gap-2 sm:gap-0"> <div class="flex flex-col sm:flex-row sm:justify-between py-3 gap-2 sm:gap-0">
<span class="text-base-content/70 text-sm sm:text-base">Site Administrator</span> <span class="text-base-content/60 text-sm">Site Administrator</span>
<span class={user.isSiteAdmin ? "badge badge-primary" : "badge badge-ghost"}> <span class={user.isSiteAdmin ? "badge badge-xs badge-primary" : "badge badge-xs badge-ghost"}>
{user.isSiteAdmin ? "Yes" : "No"} {user.isSiteAdmin ? "Yes" : "No"}
</span> </span>
</div> </div>

View File

@@ -28,24 +28,27 @@ const isAdmin = currentUserMember?.member.role === 'owner' || currentUserMember?
<DashboardLayout title="Team - Chronus"> <DashboardLayout title="Team - Chronus">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6"> <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
<h1 class="text-3xl font-bold">Team Members</h1> <div>
<h1 class="text-2xl font-extrabold tracking-tight">Team Members</h1>
<p class="text-base-content/60 text-sm mt-1">Manage your organization's team</p>
</div>
<div class="flex gap-2"> <div class="flex gap-2">
{isAdmin && ( {isAdmin && (
<> <>
<a href="/dashboard/team/settings" class="btn btn-ghost"> <a href="/dashboard/team/settings" class="btn btn-ghost btn-sm">
<Icon name="heroicons:cog-6-tooth" class="w-5 h-5" /> <Icon name="heroicons:cog-6-tooth" class="w-4 h-4" />
Settings Settings
</a> </a>
<a href="/dashboard/team/invite" class="btn btn-primary">Invite Member</a> <a href="/dashboard/team/invite" class="btn btn-primary btn-sm">Invite Member</a>
</> </>
)} )}
</div> </div>
</div> </div>
<div class="card bg-base-100 shadow-xl border border-base-200"> <div class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-0">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@@ -57,21 +60,21 @@ const isAdmin = currentUserMember?.member.role === 'owner' || currentUserMember?
</thead> </thead>
<tbody> <tbody>
{teamMembers.map(({ member, user: teamUser }) => ( {teamMembers.map(({ member, user: teamUser }) => (
<tr> <tr class="hover">
<td> <td>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<Avatar name={teamUser.name} /> <Avatar name={teamUser.name} />
<div> <div>
<div class="font-bold">{teamUser.name}</div> <div class="font-medium">{teamUser.name}</div>
{teamUser.id === user.id && ( {teamUser.id === user.id && (
<span class="badge badge-sm">You</span> <span class="badge badge-xs">You</span>
)} )}
</div> </div>
</div> </div>
</td> </td>
<td>{teamUser.email}</td> <td class="text-base-content/60">{teamUser.email}</td>
<td> <td>
<span class={`badge ${ <span class={`badge badge-xs ${
member.role === 'owner' ? 'badge-primary' : member.role === 'owner' ? 'badge-primary' :
member.role === 'admin' ? 'badge-secondary' : member.role === 'admin' ? 'badge-secondary' :
'badge-ghost' 'badge-ghost'
@@ -79,15 +82,15 @@ const isAdmin = currentUserMember?.member.role === 'owner' || currentUserMember?
{member.role} {member.role}
</span> </span>
</td> </td>
<td>{member.joinedAt?.toLocaleDateString() ?? 'N/A'}</td> <td class="text-base-content/40">{member.joinedAt?.toLocaleDateString() ?? 'N/A'}</td>
{isAdmin && ( {isAdmin && (
<td> <td>
{teamUser.id !== user.id && member.role !== 'owner' && ( {teamUser.id !== user.id && member.role !== 'owner' && (
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-sm"> <div role="button" tabindex="0" class="btn btn-ghost btn-xs btn-square">
<Icon name="heroicons:ellipsis-vertical" class="w-5 h-5" /> <Icon name="heroicons:ellipsis-vertical" class="w-4 h-4" />
</label> </div>
<ul tabindex="0" class="dropdown-content z-1 menu p-2 shadow bg-base-100 rounded-box w-52 border border-base-200"> <ul tabindex="0" class="dropdown-content z-1 menu p-2 bg-base-100 rounded-box w-52 border border-base-200">
<li> <li>
<form method="POST" action={`/api/team/change-role`}> <form method="POST" action={`/api/team/change-role`}>
<input type="hidden" name="userId" value={teamUser.id} /> <input type="hidden" name="userId" value={teamUser.id} />

View File

@@ -29,45 +29,39 @@ if (!isAdmin) return Astro.redirect('/dashboard/team');
<DashboardLayout title="Invite Team Member - Chronus"> <DashboardLayout title="Invite Team Member - Chronus">
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Invite Team Member</h1> <h1 class="text-2xl font-extrabold tracking-tight mb-6">Invite Team Member</h1>
<form method="POST" action="/api/team/invite" class="card bg-base-100 shadow-xl border border-base-200"> <form method="POST" action="/api/team/invite" class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4">
<div class="alert alert-info mb-4"> <div class="alert alert-info mb-4">
<Icon name="heroicons:information-circle" class="w-6 h-6 shrink-0" /> <Icon name="heroicons:information-circle" class="w-4 h-4 shrink-0" />
<span>The user must already have an account. They'll be added to your organization.</span> <span class="text-sm">The user must already have an account. They'll be added to your organization.</span>
</div> </div>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="email"> <legend class="fieldset-legend text-xs">Email Address</legend>
Email Address
</label>
<input <input
type="email" type="email"
id="email" id="email"
name="email" name="email"
placeholder="user@example.com" placeholder="user@example.com"
class="input input-bordered" class="input"
required required
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="role"> <legend class="fieldset-legend text-xs">Role</legend>
Role <select id="role" name="role" class="select" required>
</label>
<select id="role" name="role" class="select select-bordered" required>
<option value="member">Member</option> <option value="member">Member</option>
<option value="admin">Admin</option> <option value="admin">Admin</option>
</select> </select>
<label class="label h-auto block"> <p class="text-xs text-base-content/40 mt-1">Members can track time. Admins can manage team and clients.</p>
<span class="label-text-alt">Members can track time. Admins can manage team and clients.</span> </fieldset>
</label>
</div>
<div class="card-actions justify-end mt-6"> <div class="flex justify-end gap-2 mt-4">
<a href="/dashboard/team" class="btn btn-ghost">Cancel</a> <a href="/dashboard/team" class="btn btn-ghost btn-sm">Cancel</a>
<button type="submit" class="btn btn-primary">Invite Member</button> <button type="submit" class="btn btn-primary btn-sm">Invite Member</button>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -37,42 +37,40 @@ const successType = url.searchParams.get('success');
<DashboardLayout title="Team Settings - Chronus"> <DashboardLayout title="Team Settings - Chronus">
<div class="flex items-center gap-3 mb-6"> <div class="flex items-center gap-3 mb-6">
<a href="/dashboard/team" class="btn btn-ghost btn-sm"> <a href="/dashboard/team" class="btn btn-ghost btn-xs">
<Icon name="heroicons:arrow-left" class="w-5 h-5" /> <Icon name="heroicons:arrow-left" class="w-4 h-4" />
</a> </a>
<h1 class="text-3xl font-bold">Team Settings</h1> <h1 class="text-2xl font-extrabold tracking-tight">Team Settings</h1>
</div> </div>
<!-- Team Settings --> <!-- Team Settings -->
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6"> <div class="card card-border bg-base-100 mb-6">
<div class="card-body"> <div class="card-body p-4">
<h2 class="card-title mb-4"> <h2 class="text-sm font-semibold flex items-center gap-2 mb-4">
<Icon name="heroicons:building-office-2" class="w-6 h-6" /> <Icon name="heroicons:building-office-2" class="w-4 h-4" />
Team Settings Team Settings
</h2> </h2>
{successType === 'org-name' && ( {successType === 'org-name' && (
<div class="alert alert-success mb-4"> <div class="alert alert-success mb-4">
<Icon name="heroicons:check-circle" class="w-6 h-6" /> <Icon name="heroicons:check-circle" class="w-4 h-4" />
<span>Team information updated successfully!</span> <span class="text-sm">Team information updated successfully!</span>
</div> </div>
)} )}
<form <form
action="/api/organizations/update-name" action="/api/organizations/update-name"
method="POST" method="POST"
class="space-y-4" class="space-y-3"
enctype="multipart/form-data" enctype="multipart/form-data"
> >
<input type="hidden" name="organizationId" value={organization.id} /> <input type="hidden" name="organizationId" value={organization.id} />
<div class="form-control"> <fieldset class="fieldset">
<div class="label"> <legend class="fieldset-legend text-xs">Team Logo</legend>
<span class="label-text font-medium">Team Logo</span> <div class="flex items-center gap-4">
</div>
<div class="flex items-center gap-6">
<div class="avatar placeholder"> <div class="avatar placeholder">
<div class="bg-base-200 text-neutral-content rounded-xl w-24 border border-base-300 flex items-center justify-center overflow-hidden"> <div class="bg-base-200 text-neutral-content rounded-xl w-20 border border-base-200 flex items-center justify-center overflow-hidden">
{organization.logoUrl ? ( {organization.logoUrl ? (
<img <img
src={organization.logoUrl} src={organization.logoUrl}
@@ -82,7 +80,7 @@ const successType = url.searchParams.get('success');
) : ( ) : (
<Icon <Icon
name="heroicons:photo" name="heroicons:photo"
class="w-8 h-8 opacity-40 text-base-content" class="w-6 h-6 opacity-40 text-base-content"
/> />
)} )}
</div> </div>
@@ -92,118 +90,100 @@ const successType = url.searchParams.get('success');
type="file" type="file"
name="logo" name="logo"
accept="image/png, image/jpeg" accept="image/png, image/jpeg"
class="file-input file-input-bordered w-full max-w-xs" class="file-input file-input-bordered file-input-sm w-full max-w-xs"
/> />
<div class="text-xs text-base-content/60 mt-2"> <div class="text-xs text-base-content/40 mt-1">
Upload a company logo (PNG, JPG). Upload a company logo (PNG, JPG). Will be displayed on invoices and quotes.
<br />
Will be displayed on invoices and quotes.
</div>
</div> </div>
</div> </div>
</div> </div>
</fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="team-name"> <legend class="fieldset-legend text-xs">Team Name</legend>
Team Name
</label>
<input <input
type="text" type="text"
id="team-name" id="team-name"
name="name" name="name"
value={organization.name} value={organization.name}
placeholder="Organization name" placeholder="Organization name"
class="input input-bordered w-full" class="input w-full"
required required
/> />
<div class="label"> <p class="text-xs text-base-content/40 mt-1">This name is visible to all team members</p>
<span class="label-text-alt text-base-content/60">This name is visible to all team members</span> </fieldset>
</div>
</div>
<div class="divider">Address Information</div> <div class="divider text-xs text-base-content/40 my-2">Address Information</div>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="team-street"> <legend class="fieldset-legend text-xs">Street Address</legend>
Street Address
</label>
<input <input
type="text" type="text"
id="team-street" id="team-street"
name="street" name="street"
value={organization.street || ''} value={organization.street || ''}
placeholder="123 Main Street" placeholder="123 Main Street"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="team-city"> <legend class="fieldset-legend text-xs">City</legend>
City
</label>
<input <input
type="text" type="text"
id="team-city" id="team-city"
name="city" name="city"
value={organization.city || ''} value={organization.city || ''}
placeholder="City" placeholder="City"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="team-state"> <legend class="fieldset-legend text-xs">State/Province</legend>
State/Province
</label>
<input <input
type="text" type="text"
id="team-state" id="team-state"
name="state" name="state"
value={organization.state || ''} value={organization.state || ''}
placeholder="State/Province" placeholder="State/Province"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="team-zip"> <legend class="fieldset-legend text-xs">Postal Code</legend>
Postal Code
</label>
<input <input
type="text" type="text"
id="team-zip" id="team-zip"
name="zip" name="zip"
value={organization.zip || ''} value={organization.zip || ''}
placeholder="12345" placeholder="12345"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="team-country"> <legend class="fieldset-legend text-xs">Country</legend>
Country
</label>
<input <input
type="text" type="text"
id="team-country" id="team-country"
name="country" name="country"
value={organization.country || ''} value={organization.country || ''}
placeholder="Country" placeholder="Country"
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
</div> </div>
<div class="divider">Defaults</div> <div class="divider text-xs text-base-content/40 my-2">Defaults</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="default-tax-rate"> <legend class="fieldset-legend text-xs">Default Tax Rate (%)</legend>
Default Tax Rate (%)
</label>
<input <input
type="number" type="number"
id="default-tax-rate" id="default-tax-rate"
@@ -212,18 +192,16 @@ const successType = url.searchParams.get('success');
min="0" min="0"
max="100" max="100"
value={organization.defaultTaxRate || 0} value={organization.defaultTaxRate || 0}
class="input input-bordered w-full" class="input w-full"
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="default-currency"> <legend class="fieldset-legend text-xs">Default Currency</legend>
Default Currency
</label>
<select <select
id="default-currency" id="default-currency"
name="defaultCurrency" name="defaultCurrency"
class="select select-bordered w-full" class="select w-full"
> >
<option value="USD" selected={!organization.defaultCurrency || organization.defaultCurrency === 'USD'}>USD ($)</option> <option value="USD" selected={!organization.defaultCurrency || organization.defaultCurrency === 'USD'}>USD ($)</option>
<option value="EUR" selected={organization.defaultCurrency === 'EUR'}>EUR (€)</option> <option value="EUR" selected={organization.defaultCurrency === 'EUR'}>EUR (€)</option>
@@ -231,16 +209,16 @@ const successType = url.searchParams.get('success');
<option value="CAD" selected={organization.defaultCurrency === 'CAD'}>CAD ($)</option> <option value="CAD" selected={organization.defaultCurrency === 'CAD'}>CAD ($)</option>
<option value="AUD" selected={organization.defaultCurrency === 'AUD'}>AUD ($)</option> <option value="AUD" selected={organization.defaultCurrency === 'AUD'}>AUD ($)</option>
</select> </select>
</div> </fieldset>
</div> </div>
<div class="flex flex-col sm:flex-row justify-between items-center gap-4 mt-6"> <div class="flex flex-col sm:flex-row justify-between items-center gap-3 mt-4">
<span class="text-xs text-base-content/60 text-center sm:text-left"> <span class="text-xs text-base-content/40 text-center sm:text-left">
Address information appears on invoices and quotes Address information appears on invoices and quotes
</span> </span>
<button type="submit" class="btn btn-primary w-full sm:w-auto"> <button type="submit" class="btn btn-primary btn-sm w-full sm:w-auto">
<Icon name="heroicons:check" class="w-5 h-5" /> <Icon name="heroicons:check" class="w-4 h-4" />
Save Changes Save Changes
</button> </button>
</div> </div>
@@ -249,35 +227,34 @@ const successType = url.searchParams.get('success');
</div> </div>
<!-- Tags Section --> <!-- Tags Section -->
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6"> <div class="card card-border bg-base-100 mb-6">
<div class="card-body"> <div class="card-body p-4">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h2 class="card-title"> <h2 class="text-sm font-semibold flex items-center gap-2">
<Icon name="heroicons:tag" class="w-6 h-6" /> <Icon name="heroicons:tag" class="w-4 h-4" />
Tags & Rates Tags & Rates
</h2> </h2>
{/* We'll use a simple form submission for now or client-side JS for better UX later */} <button onclick="document.getElementById('new_tag_modal').showModal()" class="btn btn-primary btn-xs">
<button onclick="document.getElementById('new_tag_modal').showModal()" class="btn btn-primary btn-sm"> <Icon name="heroicons:plus" class="w-3 h-3" />
<Icon name="heroicons:plus" class="w-5 h-5" />
Add Tag Add Tag
</button> </button>
</div> </div>
<p class="text-base-content/70 mb-4"> <p class="text-base-content/60 text-xs mb-4">
Tags can be used to categorize time entries. You can also associate an hourly rate with a tag for billing purposes. Tags can be used to categorize time entries. You can also associate an hourly rate with a tag for billing purposes.
</p> </p>
{allTags.length === 0 ? ( {allTags.length === 0 ? (
<div class="alert alert-info"> <div class="alert alert-info">
<Icon name="heroicons:information-circle" class="w-6 h-6" /> <Icon name="heroicons:information-circle" class="w-4 h-4" />
<div> <div>
<div class="font-bold">No tags yet</div> <div class="font-semibold text-sm">No tags yet</div>
<div class="text-sm">Create tags to add context and rates to your time entries.</div> <div class="text-xs">Create tags to add context and rates to your time entries.</div>
</div> </div>
</div> </div>
) : ( ) : (
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@@ -287,7 +264,7 @@ const successType = url.searchParams.get('success');
</thead> </thead>
<tbody> <tbody>
{allTags.map(tag => ( {allTags.map(tag => (
<tr> <tr class="hover">
<td> <td>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{tag.color && ( {tag.color && (
@@ -298,22 +275,22 @@ const successType = url.searchParams.get('success');
</td> </td>
<td> <td>
{tag.rate ? ( {tag.rate ? (
<span class="font-mono">{new Intl.NumberFormat('en-US', { style: 'currency', currency: organization.defaultCurrency || 'USD' }).format(tag.rate / 100)}</span> <span class="font-mono text-sm">{new Intl.NumberFormat('en-US', { style: 'currency', currency: organization.defaultCurrency || 'USD' }).format(tag.rate / 100)}</span>
) : ( ) : (
<span class="text-base-content/40 text-xs italic">No rate</span> <span class="text-base-content/40 text-xs italic">No rate</span>
)} )}
</td> </td>
<td> <td>
<div class="flex gap-2"> <div class="flex gap-1">
<button <button
onclick={`document.getElementById('edit_tag_modal_${tag.id}').showModal()`} onclick={`document.getElementById('edit_tag_modal_${tag.id}').showModal()`}
class="btn btn-ghost btn-xs btn-square" class="btn btn-ghost btn-xs btn-square"
> >
<Icon name="heroicons:pencil" class="w-4 h-4" /> <Icon name="heroicons:pencil" class="w-3 h-3" />
</button> </button>
<form method="POST" action={`/api/tags/${tag.id}/delete`} onsubmit="return confirm('Are you sure you want to delete this tag?');"> <form method="POST" action={`/api/tags/${tag.id}/delete`} onsubmit="return confirm('Are you sure you want to delete this tag?');">
<button class="btn btn-ghost btn-xs btn-square text-error"> <button class="btn btn-ghost btn-xs btn-square text-error">
<Icon name="heroicons:trash" class="w-4 h-4" /> <Icon name="heroicons:trash" class="w-3 h-3" />
</button> </button>
</form> </form>
</div> </div>
@@ -321,32 +298,24 @@ const successType = url.searchParams.get('success');
{/* Edit Modal */} {/* Edit Modal */}
<dialog id={`edit_tag_modal_${tag.id}`} class="modal"> <dialog id={`edit_tag_modal_${tag.id}`} class="modal">
<div class="modal-box"> <div class="modal-box">
<h3 class="font-bold text-lg">Edit Tag</h3> <h3 class="font-semibold text-base">Edit Tag</h3>
<form method="POST" action={`/api/tags/${tag.id}/update`}> <form method="POST" action={`/api/tags/${tag.id}/update`}>
<div class="form-control w-full mb-4"> <fieldset class="fieldset mb-3">
<label class="label"> <legend class="fieldset-legend text-xs">Name</legend>
<span class="label-text">Name</span> <input type="text" name="name" value={tag.name} class="input w-full" required />
</label> </fieldset>
<input type="text" name="name" value={tag.name} class="input input-bordered w-full" required /> <fieldset class="fieldset mb-3">
</div> <legend class="fieldset-legend text-xs">Color</legend>
<div class="form-control w-full mb-4"> <input type="color" name="color" value={tag.color || '#3b82f6'} class="input w-full h-12 p-1" />
<label class="label"> </fieldset>
<span class="label-text">Color</span> <fieldset class="fieldset mb-4">
</label> <legend class="fieldset-legend text-xs">Hourly Rate (cents)</legend>
<input type="color" name="color" value={tag.color || '#3b82f6'} class="input input-bordered w-full h-12 p-1" /> <input type="number" name="rate" value={tag.rate || 0} min="0" class="input w-full" />
</div> <p class="text-xs text-base-content/40 mt-1">Enter rate in cents (e.g. 5000 = $50.00)</p>
<div class="form-control w-full mb-6"> </fieldset>
<label class="label">
<span class="label-text">Hourly Rate (cents)</span>
</label>
<input type="number" name="rate" value={tag.rate || 0} min="0" class="input input-bordered w-full" />
<label class="label">
<span class="label-text-alt text-base-content/60">Enter rate in cents (e.g. 5000 = $50.00)</span>
</label>
</div>
<div class="modal-action"> <div class="modal-action">
<button type="button" class="btn" onclick={`document.getElementById('edit_tag_modal_${tag.id}').close()`}>Cancel</button> <button type="button" class="btn btn-sm" onclick={`document.getElementById('edit_tag_modal_${tag.id}').close()`}>Cancel</button>
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary btn-sm">Save</button>
</div> </div>
</form> </form>
</div> </div>
@@ -366,33 +335,25 @@ const successType = url.searchParams.get('success');
<dialog id="new_tag_modal" class="modal"> <dialog id="new_tag_modal" class="modal">
<div class="modal-box"> <div class="modal-box">
<h3 class="font-bold text-lg">New Tag</h3> <h3 class="font-semibold text-base">New Tag</h3>
<form method="POST" action="/api/tags/create"> <form method="POST" action="/api/tags/create">
<input type="hidden" name="organizationId" value={organization.id} /> <input type="hidden" name="organizationId" value={organization.id} />
<div class="form-control w-full mb-4"> <fieldset class="fieldset mb-3">
<label class="label"> <legend class="fieldset-legend text-xs">Name</legend>
<span class="label-text">Name</span> <input type="text" name="name" class="input w-full" required placeholder="e.g. Billable, Rush" />
</label> </fieldset>
<input type="text" name="name" class="input input-bordered w-full" required placeholder="e.g. Billable, Rush" /> <fieldset class="fieldset mb-3">
</div> <legend class="fieldset-legend text-xs">Color</legend>
<div class="form-control w-full mb-4"> <input type="color" name="color" value="#3b82f6" class="input w-full h-12 p-1" />
<label class="label"> </fieldset>
<span class="label-text">Color</span> <fieldset class="fieldset mb-4">
</label> <legend class="fieldset-legend text-xs">Hourly Rate (cents)</legend>
<input type="color" name="color" value="#3b82f6" class="input input-bordered w-full h-12 p-1" /> <input type="number" name="rate" value="0" min="0" class="input w-full" />
</div> <p class="text-xs text-base-content/40 mt-1">Enter rate in cents (e.g. 5000 = $50.00)</p>
<div class="form-control w-full mb-6"> </fieldset>
<label class="label">
<span class="label-text">Hourly Rate (cents)</span>
</label>
<input type="number" name="rate" value="0" min="0" class="input input-bordered w-full" />
<label class="label">
<span class="label-text-alt text-base-content/60">Enter rate in cents (e.g. 5000 = $50.00)</span>
</label>
</div>
<div class="modal-action"> <div class="modal-action">
<button type="button" class="btn" onclick="document.getElementById('new_tag_modal').close()">Cancel</button> <button type="button" class="btn btn-sm" onclick="document.getElementById('new_tag_modal').close()">Cancel</button>
<button type="submit" class="btn btn-primary">Create Tag</button> <button type="submit" class="btn btn-primary btn-sm">Create Tag</button>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -139,7 +139,7 @@ const paginationPages = getPaginationPages(page, totalPages);
--- ---
<DashboardLayout title="Time Tracker - Chronus"> <DashboardLayout title="Time Tracker - Chronus">
<h1 class="text-3xl font-bold mb-6">Time Tracker</h1> <h1 class="text-2xl font-extrabold tracking-tight mb-6">Time Tracker</h1>
<!-- Tabs for Timer and Manual Entry --> <!-- Tabs for Timer and Manual Entry -->
<div class="tabs tabs-lift mb-6"> <div class="tabs tabs-lift mb-6">
@@ -189,28 +189,24 @@ const paginationPages = getPaginationPages(page, totalPages);
) : null} ) : null}
<!-- Filters and Search --> <!-- Filters and Search -->
<div class="card bg-base-200/50 backdrop-blur-sm shadow-lg border border-base-300/50 hover:border-base-300 transition-all duration-200 mb-6"> <div class="card card-border bg-base-100 mb-6">
<div class="card-body"> <div class="card-body p-4">
<form method="GET" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4"> <form method="GET" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="tracker-search"> <legend class="fieldset-legend text-xs">Search</legend>
Search
</label>
<input <input
type="text" type="text"
id="tracker-search" id="tracker-search"
name="search" name="search"
placeholder="Search descriptions..." placeholder="Search descriptions..."
class="input input-bordered bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors w-full" class="input w-full"
value={searchTerm} value={searchTerm}
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="tracker-client"> <legend class="fieldset-legend text-xs">Client</legend>
Client <select id="tracker-client" name="client" class="select w-full" onchange="this.form.submit()">
</label>
<select id="tracker-client" name="client" class="select select-bordered bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors w-full" onchange="this.form.submit()">
<option value="">All Clients</option> <option value="">All Clients</option>
{allClients.map(client => ( {allClients.map(client => (
<option value={client.id} selected={filterClient === client.id}> <option value={client.id} selected={filterClient === client.id}>
@@ -218,48 +214,40 @@ const paginationPages = getPaginationPages(page, totalPages);
</option> </option>
))} ))}
</select> </select>
</div> </fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend text-xs">Status</legend>
<div class="form-control"> <select id="tracker-status" name="status" class="select w-full" onchange="this.form.submit()">
<label class="label font-medium" for="tracker-status">
Status
</label>
<select id="tracker-status" name="status" class="select select-bordered bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors w-full" onchange="this.form.submit()">
<option value="" selected={filterStatus === ''}>All Entries</option> <option value="" selected={filterStatus === ''}>All Entries</option>
<option value="completed" selected={filterStatus === 'completed'}>Completed</option> <option value="completed" selected={filterStatus === 'completed'}>Completed</option>
<option value="running" selected={filterStatus === 'running'}>Running</option> <option value="running" selected={filterStatus === 'running'}>Running</option>
</select> </select>
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="tracker-type"> <legend class="fieldset-legend text-xs">Entry Type</legend>
Entry Type <select id="tracker-type" name="type" class="select w-full" onchange="this.form.submit()">
</label>
<select id="tracker-type" name="type" class="select select-bordered bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors w-full" onchange="this.form.submit()">
<option value="" selected={filterType === ''}>All Types</option> <option value="" selected={filterType === ''}>All Types</option>
<option value="timed" selected={filterType === 'timed'}>Timed</option> <option value="timed" selected={filterType === 'timed'}>Timed</option>
<option value="manual" selected={filterType === 'manual'}>Manual</option> <option value="manual" selected={filterType === 'manual'}>Manual</option>
</select> </select>
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="tracker-sort"> <legend class="fieldset-legend text-xs">Sort By</legend>
Sort By <select id="tracker-sort" name="sort" class="select w-full" onchange="this.form.submit()">
</label>
<select id="tracker-sort" name="sort" class="select select-bordered bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors w-full" onchange="this.form.submit()">
<option value="start-desc" selected={sortBy === 'start-desc'}>Newest First</option> <option value="start-desc" selected={sortBy === 'start-desc'}>Newest First</option>
<option value="start-asc" selected={sortBy === 'start-asc'}>Oldest First</option> <option value="start-asc" selected={sortBy === 'start-asc'}>Oldest First</option>
<option value="duration-desc" selected={sortBy === 'duration-desc'}>Longest Duration</option> <option value="duration-desc" selected={sortBy === 'duration-desc'}>Longest Duration</option>
<option value="duration-asc" selected={sortBy === 'duration-asc'}>Shortest Duration</option> <option value="duration-asc" selected={sortBy === 'duration-asc'}>Shortest Duration</option>
</select> </select>
</div> </fieldset>
<input type="hidden" name="page" value="1" /> <input type="hidden" name="page" value="1" />
<div class="form-control md:col-span-2 lg:col-span-6"> <div class="flex items-end md:col-span-2 lg:col-span-1">
<button type="submit" class="btn btn-primary shadow-lg shadow-primary/20 hover:shadow-xl hover:shadow-primary/30 transition-all"> <button type="submit" class="btn btn-primary btn-sm w-full">
<Icon name="heroicons:magnifying-glass" class="w-5 h-5" /> <Icon name="heroicons:magnifying-glass" class="w-4 h-4" />
Search Search
</button> </button>
</div> </div>
@@ -267,24 +255,24 @@ const paginationPages = getPaginationPages(page, totalPages);
</div> </div>
</div> </div>
<div class="card bg-base-200/30 backdrop-blur-sm shadow-lg border border-base-300/50 hover:border-base-300 transition-all duration-200"> <div class="card card-border bg-base-100">
<div class="card-body"> <div class="card-body p-4">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-3">
<h2 class="card-title"> <h2 class="text-sm font-semibold flex items-center gap-2">
<Icon name="heroicons:list-bullet" class="w-6 h-6" /> <Icon name="heroicons:list-bullet" class="w-4 h-4" />
Time Entries ({totalCount?.count || 0} total) Time Entries ({totalCount?.count || 0} total)
</h2> </h2>
{(filterClient || filterStatus || filterType || searchTerm) && ( {(filterClient || filterStatus || filterType || searchTerm) && (
<a href="/dashboard/tracker" class="btn btn-sm btn-ghost hover:bg-base-300/50 transition-colors"> <a href="/dashboard/tracker" class="btn btn-xs btn-ghost">
<Icon name="heroicons:x-mark" class="w-4 h-4" /> <Icon name="heroicons:x-mark" class="w-3 h-3" />
Clear Filters Clear Filters
</a> </a>
)} )}
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table table-zebra"> <table class="table table-sm">
<thead> <thead>
<tr class="bg-base-300/30"> <tr>
<th>Type</th> <th>Type</th>
<th>Client</th> <th>Client</th>
<th>Description</th> <th>Description</th>
@@ -297,26 +285,26 @@ const paginationPages = getPaginationPages(page, totalPages);
</thead> </thead>
<tbody> <tbody>
{entries.map(({ entry, client, user: entryUser }) => ( {entries.map(({ entry, client, user: entryUser }) => (
<tr class="hover:bg-base-300/20 transition-colors"> <tr class="hover">
<td> <td>
{entry.isManual ? ( {entry.isManual ? (
<span class="badge badge-info badge-sm gap-1 shadow-sm" title="Manual Entry"> <span class="badge badge-info badge-xs gap-1" title="Manual Entry">
<Icon name="heroicons:pencil" class="w-3 h-3" /> <Icon name="heroicons:pencil" class="w-3 h-3" />
Manual Manual
</span> </span>
) : ( ) : (
<span class="badge badge-success badge-sm gap-1 shadow-sm" title="Timed Entry"> <span class="badge badge-success badge-xs gap-1" title="Timed Entry">
<Icon name="heroicons:clock" class="w-3 h-3" /> <Icon name="heroicons:clock" class="w-3 h-3" />
Timed Timed
</span> </span>
)} )}
</td> </td>
<td class="font-medium">{client?.name || 'Unknown'}</td> <td class="font-medium">{client?.name || 'Unknown'}</td>
<td class="text-base-content/80">{entry.description || '-'}</td> <td class="text-base-content/60">{entry.description || '-'}</td>
<td>{entryUser?.name || 'Unknown'}</td> <td>{entryUser?.name || 'Unknown'}</td>
<td class="whitespace-nowrap"> <td class="whitespace-nowrap">
{entry.startTime.toLocaleDateString()}<br/> {entry.startTime.toLocaleDateString()}<br/>
<span class="text-xs opacity-50"> <span class="text-xs text-base-content/40">
{entry.startTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} {entry.startTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</span> </span>
</td> </td>
@@ -324,23 +312,23 @@ const paginationPages = getPaginationPages(page, totalPages);
{entry.endTime ? ( {entry.endTime ? (
<> <>
{entry.endTime.toLocaleDateString()}<br/> {entry.endTime.toLocaleDateString()}<br/>
<span class="text-xs opacity-50"> <span class="text-xs text-base-content/40">
{entry.endTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} {entry.endTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</span> </span>
</> </>
) : ( ) : (
<span class="badge badge-success shadow-sm">Running</span> <span class="badge badge-success badge-xs">Running</span>
)} )}
</td> </td>
<td class="font-mono font-semibold text-primary">{formatTimeRange(entry.startTime, entry.endTime)}</td> <td class="font-mono font-semibold text-primary text-sm">{formatTimeRange(entry.startTime, entry.endTime)}</td>
<td> <td>
<form method="POST" action={`/api/time-entries/${entry.id}/delete`} class="inline"> <form method="POST" action={`/api/time-entries/${entry.id}/delete`} class="inline">
<button <button
type="submit" type="submit"
class="btn btn-ghost btn-sm text-error hover:bg-error/10 transition-colors" class="btn btn-ghost btn-xs text-error"
onclick="return confirm('Are you sure you want to delete this entry?')" onclick="return confirm('Are you sure you want to delete this entry?')"
> >
<Icon name="heroicons:trash" class="w-4 h-4" /> <Icon name="heroicons:trash" class="w-3.5 h-3.5" />
</button> </button>
</form> </form>
</td> </td>
@@ -352,20 +340,20 @@ const paginationPages = getPaginationPages(page, totalPages);
<!-- Pagination --> <!-- Pagination -->
{totalPages > 1 && ( {totalPages > 1 && (
<div class="flex justify-center items-center gap-2 mt-6"> <div class="flex justify-center items-center gap-1 mt-4">
<a <a
href={`?page=${Math.max(1, page - 1)}${filterClient ? `&client=${filterClient}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`} href={`?page=${Math.max(1, page - 1)}${filterClient ? `&client=${filterClient}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`}
class={`btn btn-sm transition-all ${page === 1 ? 'btn-disabled' : 'hover:bg-base-300/50'}`} class={`btn btn-xs ${page === 1 ? 'btn-disabled' : ''}`}
> >
<Icon name="heroicons:chevron-left" class="w-4 h-4" /> <Icon name="heroicons:chevron-left" class="w-3 h-3" />
Previous Prev
</a> </a>
<div class="flex gap-1"> <div class="flex gap-0.5">
{paginationPages.map(pageNum => ( {paginationPages.map(pageNum => (
<a <a
href={`?page=${pageNum}${filterClient ? `&client=${filterClient}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`} href={`?page=${pageNum}${filterClient ? `&client=${filterClient}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`}
class={`btn btn-sm transition-all ${page === pageNum ? 'btn-active' : 'hover:bg-base-300/50'}`} class={`btn btn-xs ${page === pageNum ? 'btn-active' : ''}`}
> >
{pageNum} {pageNum}
</a> </a>
@@ -374,10 +362,10 @@ const paginationPages = getPaginationPages(page, totalPages);
<a <a
href={`?page=${Math.min(totalPages, page + 1)}${filterClient ? `&client=${filterClient}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`} href={`?page=${Math.min(totalPages, page + 1)}${filterClient ? `&client=${filterClient}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`}
class={`btn btn-sm transition-all ${page === totalPages ? 'btn-disabled' : 'hover:bg-base-300/50'}`} class={`btn btn-xs ${page === totalPages ? 'btn-disabled' : ''}`}
> >
Next Next
<Icon name="heroicons:chevron-right" class="w-4 h-4" /> <Icon name="heroicons:chevron-right" class="w-3 h-3" />
</a> </a>
</div> </div>
)} )}

View File

@@ -7,48 +7,64 @@ if (Astro.locals.user) {
--- ---
<Layout title="Chronus - Time Tracking"> <Layout title="Chronus - Time Tracking">
<div class="hero flex-1 bg-linear-to-br from-base-100 via-base-200 to-base-300 flex items-center justify-center py-12"> <div class="flex-1 flex flex-col">
<div class="hero-content text-center"> <!-- Hero -->
<div class="max-w-4xl"> <div class="flex-1 flex items-center justify-center px-4 py-16 sm:py-24 bg-base-100">
<img src="/logo.webp" alt="Chronus Logo" class="h-24 w-24 mx-auto mb-6" /> <div class="max-w-3xl text-center">
<h1 class="text-6xl md:text-7xl font-bold mb-6 text-primary"> <img src="/logo.webp" alt="Chronus Logo" class="h-20 w-20 mx-auto mb-8" />
Chronus <h1 class="text-5xl sm:text-6xl lg:text-7xl font-extrabold tracking-tight text-base-content mb-4">
Track time,<br />
<span class="text-primary">effortlessly.</span>
</h1> </h1>
<p class="text-xl md:text-2xl py-6 text-base-content/80 font-light max-w-2xl mx-auto"> <p class="text-lg sm:text-xl text-base-content/60 max-w-xl mx-auto mb-10 leading-relaxed">
Modern time tracking designed for teams that value simplicity and precision. Modern time tracking designed for teams that value simplicity and precision.
</p> </p>
<div class="flex gap-4 justify-center mt-8 flex-wrap"> <div class="flex gap-3 justify-center flex-wrap">
<a href="/signup" class="btn btn-primary btn-lg"> <a href="/signup" class="btn btn-primary btn-lg px-8">
Get Started Get Started
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg> </svg>
</a> </a>
<a href="/login" class="btn btn-outline btn-lg">Login</a> <a href="/login" class="btn btn-ghost btn-lg px-8">Login</a>
</div>
</div>
</div> </div>
<!-- Feature highlights --> <!-- Features -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16"> <div class="bg-base-200/50 border-t border-base-200 px-4 py-16 sm:py-20">
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow"> <div class="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="card-body items-start"> <div class="card bg-base-100 card-border">
<div class="text-4xl mb-3">⚡</div> <div class="card-body">
<h3 class="card-title text-lg">Lightning Fast</h3> <div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-2">
<p class="text-sm text-base-content/70">Track tasks with a single click.</p> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd" />
</svg>
</div>
<h3 class="card-title text-base">Lightning Fast</h3>
<p class="text-sm text-base-content/60">Track tasks with a single click. Start, stop, and organize in seconds.</p>
</div> </div>
</div> </div>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow"> <div class="card bg-base-100 card-border">
<div class="card-body items-start"> <div class="card-body">
<div class="text-4xl mb-3">📊</div> <div class="w-10 h-10 rounded-lg bg-secondary/10 flex items-center justify-center mb-2">
<h3 class="card-title text-lg">Detailed Reports</h3> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-secondary" viewBox="0 0 20 20" fill="currentColor">
<p class="text-sm text-base-content/70">Get actionable insights into your team's tasks.</p> <path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z" />
</svg>
</div>
<h3 class="card-title text-base">Detailed Reports</h3>
<p class="text-sm text-base-content/60">Get actionable insights with charts, filters, and CSV exports.</p>
</div> </div>
</div> </div>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow"> <div class="card bg-base-100 card-border">
<div class="card-body items-start"> <div class="card-body">
<div class="text-4xl mb-3">👥</div> <div class="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center mb-2">
<h3 class="card-title text-lg">Team Collaboration</h3> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-accent" viewBox="0 0 20 20" fill="currentColor">
<p class="text-sm text-base-content/70">Built for multiple team members.</p> <path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
</svg>
</div> </div>
<h3 class="card-title text-base">Team Collaboration</h3>
<p class="text-sm text-base-content/60">Built for teams with roles, permissions, and shared workspaces.</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -18,64 +18,58 @@ const errorMessage =
<Layout title="Login - Chronus"> <Layout title="Login - Chronus">
<div class="flex justify-center items-center flex-1 bg-base-100"> <div class="flex justify-center items-center flex-1 bg-base-100">
<div class="card bg-base-100 shadow-2xl w-full max-w-md mx-4"> <div class="card card-border bg-base-100 w-full max-w-sm mx-4">
<div class="card-body"> <div class="card-body gap-0">
<img src="/logo.webp" alt="Chronus" class="h-16 w-16 mx-auto mb-4" /> <img src="/logo.webp" alt="Chronus" class="h-14 w-14 mx-auto mb-3" />
<h2 class="text-3xl font-bold text-center mb-2">Welcome Back</h2> <h2 class="text-2xl font-extrabold tracking-tight text-center">Welcome Back</h2>
<p class="text-center text-base-content/60 mb-6">Sign in to continue to Chronus</p> <p class="text-center text-base-content/60 text-sm mt-1 mb-5">Sign in to continue to Chronus</p>
{errorMessage && ( {errorMessage && (
<div role="alert" class="alert alert-error mb-4"> <div role="alert" class="alert alert-error mb-4 text-sm">
<Icon name="heroicons:exclamation-circle" class="w-6 h-6" /> <Icon name="heroicons:exclamation-circle" class="w-5 h-5" />
<span>{errorMessage}</span> <span>{errorMessage}</span>
</div> </div>
)} )}
<form action="/api/auth/login" method="POST" class="space-y-4"> <form action="/api/auth/login" method="POST" class="space-y-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="email"> <legend class="fieldset-legend text-xs">Email</legend>
Email
</label>
<input <input
type="email" type="email"
id="email" id="email"
name="email" name="email"
placeholder="your@email.com" placeholder="your@email.com"
class="input input-bordered w-full" class="input w-full"
autocomplete="email" autocomplete="email"
required required
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="password"> <legend class="fieldset-legend text-xs">Password</legend>
Password
</label>
<input <input
type="password" type="password"
id="password" id="password"
name="password" name="password"
placeholder="Enter your password" placeholder="Enter your password"
class="input input-bordered w-full" class="input w-full"
autocomplete="current-password" autocomplete="current-password"
required required
/> />
</div> </fieldset>
<button class="btn btn-primary w-full mt-6">Sign In</button> <button class="btn btn-primary w-full my-4">Sign In</button>
</form> </form>
<PasskeyLogin client:idle /> <PasskeyLogin client:idle />
<div class="divider">OR</div> <div class="divider text-xs">OR</div>
<div class="text-center"> <p class="text-center text-sm text-base-content/60">
<p class="text-sm text-base-content/70">
Don't have an account? Don't have an account?
<a href="/signup" class="link link-primary font-semibold">Create one</a> <a href="/signup" class="link link-primary font-semibold">Create one</a>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div>
</Layout> </Layout>

View File

@@ -34,92 +34,82 @@ const errorMessage =
<Layout title="Sign Up - Chronus"> <Layout title="Sign Up - Chronus">
<div class="flex justify-center items-center flex-1 bg-base-100"> <div class="flex justify-center items-center flex-1 bg-base-100">
<div class="card bg-base-100 shadow-2xl w-full max-w-md mx-4"> <div class="card card-border bg-base-100 w-full max-w-sm mx-4">
<div class="card-body"> <div class="card-body gap-0">
<img src="/logo.webp" alt="Chronus" class="h-16 w-16 mx-auto mb-4" /> <img src="/logo.webp" alt="Chronus" class="h-14 w-14 mx-auto mb-3" />
<h2 class="text-3xl font-bold text-center mb-2">Create Account</h2> <h2 class="text-2xl font-extrabold tracking-tight text-center">Create Account</h2>
<p class="text-center text-base-content/60 mb-6">Join Chronus to start tracking time</p> <p class="text-center text-base-content/60 text-sm mt-1 mb-5">Join Chronus to start tracking time</p>
{errorMessage && ( {errorMessage && (
<div role="alert" class="alert alert-error mb-4"> <div role="alert" class="alert alert-error mb-4 text-sm">
<Icon name="heroicons:exclamation-circle" class="w-6 h-6" /> <Icon name="heroicons:exclamation-circle" class="w-5 h-5" />
<span>{errorMessage}</span> <span>{errorMessage}</span>
</div> </div>
)} )}
{registrationDisabled ? ( {registrationDisabled ? (
<> <>
<div class="alert alert-warning"> <div class="alert alert-warning text-sm">
<Icon name="heroicons:exclamation-triangle" class="w-6 h-6" /> <Icon name="heroicons:exclamation-triangle" class="w-5 h-5" />
<span>Registration is currently disabled by the site administrator.</span> <span>Registration is currently disabled by the site administrator.</span>
</div> </div>
<div class="divider"></div> <div class="divider text-xs"></div>
<div class="text-center"> <p class="text-center text-sm text-base-content/60">
<p class="text-sm text-base-content/70">
Already have an account? Already have an account?
<a href="/login" class="link link-primary font-semibold">Sign in</a> <a href="/login" class="link link-primary font-semibold">Sign in</a>
</p> </p>
</div>
</> </>
) : ( ) : (
<> <>
<form action="/api/auth/signup" method="POST" class="space-y-4"> <form action="/api/auth/signup" method="POST" class="space-y-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="name"> <legend class="fieldset-legend text-xs">Full Name</legend>
Full Name
</label>
<input <input
type="text" type="text"
id="name" id="name"
name="name" name="name"
placeholder="John Doe" placeholder="John Doe"
class="input input-bordered w-full" class="input w-full"
autocomplete="name" autocomplete="name"
required required
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="email"> <legend class="fieldset-legend text-xs">Email</legend>
Email
</label>
<input <input
type="email" type="email"
id="email" id="email"
name="email" name="email"
placeholder="your@email.com" placeholder="your@email.com"
class="input input-bordered w-full" class="input w-full"
autocomplete="email" autocomplete="email"
required required
/> />
</div> </fieldset>
<div class="form-control"> <fieldset class="fieldset">
<label class="label font-medium" for="password"> <legend class="fieldset-legend text-xs">Password</legend>
Password
</label>
<input <input
type="password" type="password"
id="password" id="password"
name="password" name="password"
placeholder="Create a strong password" placeholder="Create a strong password"
class="input input-bordered w-full" class="input w-full"
autocomplete="new-password" autocomplete="new-password"
required required
/> />
</div> </fieldset>
<button class="btn btn-primary w-full mt-6">Create Account</button> <button class="btn btn-primary w-full mt-4">Create Account</button>
</form> </form>
<div class="divider">OR</div> <div class="divider text-xs">OR</div>
<div class="text-center"> <p class="text-center text-sm text-base-content/60">
<p class="text-sm text-base-content/70">
Already have an account? Already have an account?
<a href="/login" class="link link-primary font-semibold">Sign in</a> <a href="/login" class="link link-primary font-semibold">Sign in</a>
</p> </p>
</div>
</> </>
)} )}
</div> </div>

View File

@@ -4,3 +4,14 @@
} }
@plugin "./theme-dark.ts"; @plugin "./theme-dark.ts";
@plugin "./theme-light.ts"; @plugin "./theme-light.ts";
/* Smoother transitions globally */
@layer base {
* {
@apply transition-colors duration-150;
}
/* Opt out for elements where color transitions are unwanted */
input, select, textarea, progress, .loading, .countdown, svg {
transition: none;
}
}