Files
chronus/src/pages/dashboard/clients/[id]/index.astro
Atridad Lahiji 0cd77677f2
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m6s
FINISHED
2026-01-17 15:56:25 -07:00

226 lines
8.1 KiB
Plaintext

---
import DashboardLayout from '../../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import { db } from '../../../../db';
import { clients, timeEntries, members, categories, users } from '../../../../db/schema';
import { eq, and, desc, sql } from 'drizzle-orm';
import { formatTimeRange } from '../../../../lib/formatTime';
const user = Astro.locals.user;
if (!user) return Astro.redirect('/login');
const { id } = Astro.params;
if (!id) return Astro.redirect('/dashboard/clients');
// Get current team from cookie
const currentTeamId = Astro.cookies.get('currentTeamId')?.value;
const userMemberships = await db.select()
.from(members)
.where(eq(members.userId, user.id))
.all();
if (userMemberships.length === 0) return Astro.redirect('/dashboard');
// Use current team or fallback to first membership
const userMembership = currentTeamId
? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0]
: userMemberships[0];
const client = await db.select()
.from(clients)
.where(and(
eq(clients.id, id),
eq(clients.organizationId, userMembership.organizationId)
))
.get();
if (!client) return Astro.redirect('/dashboard/clients');
// Get recent activity
const recentEntries = await db.select({
entry: timeEntries,
category: categories,
user: users,
})
.from(timeEntries)
.leftJoin(categories, eq(timeEntries.categoryId, categories.id))
.leftJoin(users, eq(timeEntries.userId, users.id))
.where(eq(timeEntries.clientId, client.id))
.orderBy(desc(timeEntries.startTime))
.limit(10)
.all();
// Calculate total time tracked
const totalTimeResult = await db.select({
totalDuration: sql<number>`sum(CASE WHEN ${timeEntries.endTime} IS NOT NULL THEN ${timeEntries.endTime} - ${timeEntries.startTime} ELSE 0 END)`
})
.from(timeEntries)
.where(eq(timeEntries.clientId, client.id))
.get();
const totalDurationMs = totalTimeResult?.totalDuration || 0;
const totalHours = Math.floor(totalDurationMs / (1000 * 60 * 60));
const totalMinutes = Math.floor((totalDurationMs % (1000 * 60 * 60)) / (1000 * 60));
// Get total entries count
const totalEntriesResult = await db.select({ count: sql<number>`count(*)` })
.from(timeEntries)
.where(eq(timeEntries.clientId, client.id))
.get();
const totalEntriesCount = totalEntriesResult?.count || 0;
---
<DashboardLayout title={`${client.name} - Clients - Chronus`}>
<div class="flex items-center gap-3 mb-6">
<a href="/dashboard/clients" class="btn btn-ghost btn-sm">
<Icon name="heroicons:arrow-left" class="w-5 h-5" />
</a>
<h1 class="text-3xl font-bold">{client.name}</h1>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- Client Details Card -->
<div class="card bg-base-100 shadow-xl border border-base-200 lg:col-span-2">
<div class="card-body">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-2xl mb-1">{client.name}</h2>
<div class="space-y-2 mb-4">
{client.email && (
<div class="flex items-center gap-2 text-base-content/70">
<Icon name="heroicons:envelope" class="w-4 h-4" />
<a href={`mailto:${client.email}`} class="link link-hover">{client.email}</a>
</div>
)}
{client.phone && (
<div class="flex items-center gap-2 text-base-content/70">
<Icon name="heroicons:phone" class="w-4 h-4" />
<a href={`tel:${client.phone}`} class="link link-hover">{client.phone}</a>
</div>
)}
{(client.street || client.city || client.state || client.zip || client.country) && (
<div class="flex items-start gap-2 text-base-content/70">
<Icon name="heroicons:map-pin" class="w-4 h-4 mt-0.5" />
<div class="text-sm space-y-0.5">
{client.street && <div>{client.street}</div>}
{(client.city || client.state || client.zip) && (
<div>
{[client.city, client.state, client.zip].filter(Boolean).join(', ')}
</div>
)}
{client.country && <div>{client.country}</div>}
</div>
</div>
)}
</div>
</div>
<div class="flex gap-2">
<a href={`/dashboard/clients/${client.id}/edit`} class="btn btn-primary btn-sm">
<Icon name="heroicons:pencil" class="w-4 h-4" />
Edit
</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.');">
<button type="submit" class="btn btn-error btn-outline btn-sm">
<Icon name="heroicons:trash" class="w-4 h-4" />
Delete
</button>
</form>
</div>
</div>
<div class="divider"></div>
<div class="stats shadow w-full">
<div class="stat">
<div class="stat-figure text-primary">
<Icon name="heroicons:clock" class="w-8 h-8" />
</div>
<div class="stat-title">Total Time Tracked</div>
<div class="stat-value text-primary">{totalHours}h {totalMinutes}m</div>
<div class="stat-desc">Across all projects</div>
</div>
<div class="stat">
<div class="stat-figure text-secondary">
<Icon name="heroicons:list-bullet" class="w-8 h-8" />
</div>
<div class="stat-title">Total Entries</div>
<div class="stat-value text-secondary">{totalEntriesCount}</div>
<div class="stat-desc">Recorded entries</div>
</div>
</div>
</div>
</div>
<!-- Meta Info Card -->
<div class="card bg-base-100 shadow-xl border border-base-200 h-fit">
<div class="card-body">
<h3 class="card-title text-lg mb-4">Information</h3>
<div class="space-y-4">
<div>
<div class="text-sm font-medium text-base-content/60">Created</div>
<div>{client.createdAt?.toLocaleDateString() ?? 'N/A'}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body">
<h2 class="card-title mb-4">Recent Activity</h2>
{recentEntries.length > 0 ? (
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Description</th>
<th>Category</th>
<th>User</th>
<th>Date</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{recentEntries.map(({ entry, category, user: entryUser }) => (
<tr>
<td>{entry.description || '-'}</td>
<td>
{category ? (
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full" style={`background-color: ${category.color}`}></span>
<span>{category.name}</span>
</div>
) : '-'}
</td>
<td>{entryUser?.name || 'Unknown'}</td>
<td>{entry.startTime.toLocaleDateString()}</td>
<td class="font-mono">{formatTimeRange(entry.startTime, entry.endTime)}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div class="text-center py-8 text-base-content/60">
No time entries recorded for this client yet.
</div>
)}
{recentEntries.length > 0 && (
<div class="card-actions justify-center mt-4">
<a href={`/dashboard/tracker?client=${client.id}`} class="btn btn-ghost btn-sm">
View All Entries
</a>
</div>
)}
</div>
</div>
</DashboardLayout>