First pass

This commit is contained in:
2025-12-25 22:10:06 -07:00
parent a2af6195f9
commit 455c3dbd9a
58 changed files with 10299 additions and 3 deletions

View File

@@ -0,0 +1,238 @@
---
import DashboardLayout from '../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import { db } from '../../db';
import { organizations, members, timeEntries, clients, categories } from '../../db/schema';
import { eq, desc, and, isNull, gte, sql } from 'drizzle-orm';
const user = Astro.locals.user;
if (!user) return Astro.redirect('/login');
const userOrgs = await db.select({
id: organizations.id,
name: organizations.name,
role: members.role,
organizationId: members.organizationId,
})
.from(members)
.innerJoin(organizations, eq(members.organizationId, organizations.id))
.where(eq(members.userId, user.id))
.all();
// Get stats for first organization
const firstOrg = userOrgs[0];
let stats = {
totalTimeThisWeek: 0,
totalTimeThisMonth: 0,
activeTimers: 0,
totalClients: 0,
recentEntries: [] as any[],
};
if (firstOrg) {
// Calculate date ranges
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
// Get time entries for this week
const weekEntries = await db.select()
.from(timeEntries)
.where(and(
eq(timeEntries.organizationId, firstOrg.organizationId),
gte(timeEntries.startTime, weekAgo)
))
.all();
stats.totalTimeThisWeek = weekEntries.reduce((sum, e) => {
if (e.endTime) {
return sum + (e.endTime.getTime() - e.startTime.getTime());
}
return sum;
}, 0);
// Get time entries for this month
const monthEntries = await db.select()
.from(timeEntries)
.where(and(
eq(timeEntries.organizationId, firstOrg.organizationId),
gte(timeEntries.startTime, monthAgo)
))
.all();
stats.totalTimeThisMonth = monthEntries.reduce((sum, e) => {
if (e.endTime) {
return sum + (e.endTime.getTime() - e.startTime.getTime());
}
return sum;
}, 0);
// Count active timers
const activeCount = await db.select()
.from(timeEntries)
.where(and(
eq(timeEntries.organizationId, firstOrg.organizationId),
isNull(timeEntries.endTime)
))
.all();
stats.activeTimers = activeCount.length;
// Count clients
const clientCount = await db.select()
.from(clients)
.where(eq(clients.organizationId, firstOrg.organizationId))
.all();
stats.totalClients = clientCount.length;
// Get recent entries
stats.recentEntries = await db.select({
entry: timeEntries,
client: clients,
category: categories,
})
.from(timeEntries)
.innerJoin(clients, eq(timeEntries.clientId, clients.id))
.innerJoin(categories, eq(timeEntries.categoryId, categories.id))
.where(eq(timeEntries.userId, user.id))
.orderBy(desc(timeEntries.startTime))
.limit(5)
.all();
}
function formatDuration(ms: number) {
const totalMinutes = Math.round(ms / 1000 / 60);
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return `${hours}h ${minutes}m`;
}
---
<DashboardLayout title="Dashboard - Zamaan">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">Dashboard</h1>
<a href="/dashboard/organizations/new" class="btn btn-outline btn-sm">
<Icon name="heroicons:plus" class="w-5 h-5" />
New Organization
</a>
</div>
<!-- Stats Overview -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="stats shadow border border-base-300">
<div class="stat">
<div class="stat-figure text-primary">
<Icon name="heroicons:clock" class="w-8 h-8" />
</div>
<div class="stat-title">This Week</div>
<div class="stat-value text-primary text-2xl">{formatDuration(stats.totalTimeThisWeek)}</div>
<div class="stat-desc">Total tracked time</div>
</div>
</div>
<div class="stats shadow border border-base-300">
<div class="stat">
<div class="stat-figure text-secondary">
<Icon name="heroicons:calendar" class="w-8 h-8" />
</div>
<div class="stat-title">This Month</div>
<div class="stat-value text-secondary text-2xl">{formatDuration(stats.totalTimeThisMonth)}</div>
<div class="stat-desc">Total tracked time</div>
</div>
</div>
<div class="stats shadow border border-base-300">
<div class="stat">
<div class="stat-figure text-accent">
<Icon name="heroicons:play-circle" class="w-8 h-8" />
</div>
<div class="stat-title">Active Timers</div>
<div class="stat-value text-accent text-2xl">{stats.activeTimers}</div>
<div class="stat-desc">Currently running</div>
</div>
</div>
<div class="stats shadow border border-base-300">
<div class="stat">
<div class="stat-figure text-info">
<Icon name="heroicons:building-office" class="w-8 h-8" />
</div>
<div class="stat-title">Clients</div>
<div class="stat-value text-info text-2xl">{stats.totalClients}</div>
<div class="stat-desc">Total active</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Organizations -->
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body">
<h2 class="card-title">
<Icon name="heroicons:building-office-2" class="w-6 h-6" />
Your Organizations
</h2>
<ul class="menu bg-base-100 w-full p-0">
{userOrgs.map(org => (
<li>
<a class="flex justify-between">
<span>{org.name}</span>
<span class="badge badge-sm">{org.role}</span>
</a>
</li>
))}
</ul>
</div>
</div>
<!-- Quick Actions -->
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body">
<h2 class="card-title">
<Icon name="heroicons:bolt" class="w-6 h-6" />
Quick Actions
</h2>
<div class="flex flex-col gap-2">
<a href="/dashboard/tracker" class="btn btn-primary">
<Icon name="heroicons:play" class="w-5 h-5" />
Start Timer
</a>
<a href="/dashboard/clients/new" class="btn btn-outline">
<Icon name="heroicons:plus" class="w-5 h-5" />
Add Client
</a>
<a href="/dashboard/reports" class="btn btn-outline">
<Icon name="heroicons:chart-bar" class="w-5 h-5" />
View Reports
</a>
</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">
<Icon name="heroicons:clock" class="w-6 h-6" />
Recent Activity
</h2>
{stats.recentEntries.length > 0 ? (
<ul class="space-y-2">
{stats.recentEntries.map(({ entry, client, category }) => (
<li class="text-sm border-l-2 pl-2" style={`border-color: ${category.color || '#3b82f6'}`}>
<div class="font-semibold">{client.name}</div>
<div class="text-xs text-base-content/60">
{category.name} • {entry.endTime ? formatDuration(entry.endTime.getTime() - entry.startTime.getTime()) : 'Running...'}
</div>
</li>
))}
</ul>
) : (
<p class="text-base-content/60 text-sm">No recent time entries</p>
)}
</div>
</div>
</div>
</DashboardLayout>