Files
chronus/src/pages/dashboard/reports.astro

785 lines
28 KiB
Plaintext

---
import DashboardLayout from '../../layouts/DashboardLayout.astro';
import Icon from '../../components/Icon.astro';
import StatCard from '../../components/StatCard.astro';
import TagChart from '../../components/TagChart.vue';
import ClientChart from '../../components/ClientChart.vue';
import MemberChart from '../../components/MemberChart.vue';
import AutoSubmit from '../../components/AutoSubmit.vue';
import ColorDot from '../../components/ColorDot.vue';
import { db } from '../../db';
import { timeEntries, members, users, clients, tags, invoices } from '../../db/schema';
import { eq, and, gte, lte, sql, desc } from 'drizzle-orm';
import { formatDuration, formatTimeRange, formatCurrency } from '../../lib/formatTime';
import { getCurrentTeam } from '../../lib/getCurrentTeam';
const user = Astro.locals.user;
if (!user) return Astro.redirect('/login');
const userMembership = await getCurrentTeam(user, Astro.cookies.get('currentTeamId')?.value);
if (!userMembership) return Astro.redirect('/dashboard');
const teamMembers = await db.select({
id: users.id,
name: users.name,
email: users.email,
})
.from(members)
.innerJoin(users, eq(members.userId, users.id))
.where(eq(members.organizationId, userMembership.organizationId))
.all();
const allTags = await db.select()
.from(tags)
.where(eq(tags.organizationId, userMembership.organizationId))
.all();
const allClients = await db.select()
.from(clients)
.where(eq(clients.organizationId, userMembership.organizationId))
.all();
const url = new URL(Astro.request.url);
const selectedMemberId = url.searchParams.get('member') || '';
const selectedTagId = url.searchParams.get('tag') || '';
const selectedClientId = url.searchParams.get('client') || '';
const timeRange = url.searchParams.get('range') || 'week';
const customFrom = url.searchParams.get('from');
const customTo = url.searchParams.get('to');
const now = new Date();
let startDate = new Date();
let endDate = new Date();
switch (timeRange) {
case 'today':
startDate.setHours(0, 0, 0, 0);
endDate.setHours(23, 59, 59, 999);
break;
case 'week':
startDate.setDate(now.getDate() - 7);
break;
case 'month':
startDate.setMonth(now.getMonth() - 1);
break;
case 'mtd':
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
break;
case 'ytd':
startDate = new Date(now.getFullYear(), 0, 1);
break;
case 'last-month':
startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
endDate = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
break;
case 'custom':
if (customFrom) {
const parts = customFrom.split('-');
startDate = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]), 0, 0, 0, 0);
}
if (customTo) {
const parts = customTo.split('-');
endDate = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]), 23, 59, 59, 999);
}
break;
}
const conditions = [
eq(timeEntries.organizationId, userMembership.organizationId),
gte(timeEntries.startTime, startDate),
lte(timeEntries.startTime, endDate),
];
if (selectedMemberId) {
conditions.push(eq(timeEntries.userId, selectedMemberId));
}
if (selectedTagId) {
conditions.push(eq(timeEntries.tagId, selectedTagId));
}
if (selectedClientId) {
conditions.push(eq(timeEntries.clientId, selectedClientId));
}
const entries = await db.select({
entry: timeEntries,
user: users,
client: clients,
tag: tags,
})
.from(timeEntries)
.innerJoin(users, eq(timeEntries.userId, users.id))
.innerJoin(clients, eq(timeEntries.clientId, clients.id))
.leftJoin(tags, eq(timeEntries.tagId, tags.id))
.where(and(...conditions))
.orderBy(desc(timeEntries.startTime))
.all();
const statsByMember = teamMembers.map(member => {
const memberEntries = entries.filter(e => e.user.id === member.id);
const totalTime = memberEntries.reduce((sum, e) => {
if (e.entry.endTime) {
return sum + (e.entry.endTime.getTime() - e.entry.startTime.getTime());
}
return sum;
}, 0);
return {
member,
totalTime,
entryCount: memberEntries.length,
};
}).sort((a, b) => b.totalTime - a.totalTime);
const statsByTag = allTags.map(tag => {
const tagEntries = entries.filter(e => e.tag?.id === tag.id);
const totalTime = tagEntries.reduce((sum, e) => {
if (e.entry.endTime) {
return sum + (e.entry.endTime.getTime() - e.entry.startTime.getTime());
}
return sum;
}, 0);
return {
tag,
totalTime,
entryCount: tagEntries.length,
};
}).sort((a, b) => b.totalTime - a.totalTime);
const statsByClient = allClients.map(client => {
const clientEntries = entries.filter(e => e.client.id === client.id);
const totalTime = clientEntries.reduce((sum, e) => {
if (e.entry.endTime) {
return sum + (e.entry.endTime.getTime() - e.entry.startTime.getTime());
}
return sum;
}, 0);
return {
client,
totalTime,
entryCount: clientEntries.length,
};
}).sort((a, b) => b.totalTime - a.totalTime);
const totalTime = entries.reduce((sum, e) => {
if (e.entry.endTime) {
return sum + (e.entry.endTime.getTime() - e.entry.startTime.getTime());
}
return sum;
}, 0);
// Fetch invoices and quotes for the same time period
const invoiceConditions = [
eq(invoices.organizationId, userMembership.organizationId),
gte(invoices.issueDate, startDate),
lte(invoices.issueDate, endDate),
];
if (selectedClientId) {
invoiceConditions.push(eq(invoices.clientId, selectedClientId));
}
const allInvoices = await db.select({
invoice: invoices,
client: clients,
})
.from(invoices)
.leftJoin(clients, eq(invoices.clientId, clients.id))
.where(and(...invoiceConditions))
.orderBy(desc(invoices.issueDate))
.all();
// Invoice statistics
const invoiceStats = {
total: allInvoices.filter(i => i.invoice.type === 'invoice').length,
paid: allInvoices.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'paid').length,
sent: allInvoices.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'sent').length,
draft: allInvoices.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'draft').length,
void: allInvoices.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'void').length,
};
// Quote statistics
const quoteStats = {
total: allInvoices.filter(i => i.invoice.type === 'quote').length,
accepted: allInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'accepted').length,
sent: allInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'sent').length,
declined: allInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'declined').length,
draft: allInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'draft').length,
};
// Revenue statistics
const revenueStats = {
total: allInvoices
.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'paid')
.reduce((sum, i) => sum + i.invoice.total, 0),
pending: allInvoices
.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'sent')
.reduce((sum, i) => sum + i.invoice.total, 0),
quotedValue: allInvoices
.filter(i => i.invoice.type === 'quote' && (i.invoice.status === 'sent' || i.invoice.status === 'accepted'))
.reduce((sum, i) => sum + i.invoice.total, 0),
};
// Revenue by client
const revenueByClient = allClients.map(client => {
const clientInvoices = allInvoices.filter(i =>
i.client?.id === client.id &&
i.invoice.type === 'invoice' &&
i.invoice.status === 'paid'
);
const revenue = clientInvoices.reduce((sum, i) => sum + i.invoice.total, 0);
return {
client,
revenue,
invoiceCount: clientInvoices.length,
};
}).filter(s => s.revenue > 0).sort((a, b) => b.revenue - a.revenue);
function getTimeRangeLabel(range: string) {
switch (range) {
case 'today': return 'Today';
case 'week': return 'Last 7 Days';
case 'month': return 'Last 30 Days';
case 'mtd': return 'Month to Date';
case 'ytd': return 'Year to Date';
case 'last-month': return 'Last Month';
case 'custom': return 'Custom Range';
default: return 'Last 7 Days';
}
}
---
<DashboardLayout title="Reports - Chronus">
<h1 class="text-2xl font-extrabold tracking-tight mb-6">Team Reports</h1>
<!-- Filters -->
<div class="card card-border bg-base-100 mb-6">
<div class="card-body p-4">
<form method="GET" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
<fieldset class="fieldset">
<legend class="fieldset-legend text-xs">Time Range</legend>
<AutoSubmit client:load>
<select id="reports-range" name="range" class="select w-full">
<option value="today" selected={timeRange === 'today'}>Today</option>
<option value="week" selected={timeRange === 'week'}>Last 7 Days</option>
<option value="month" selected={timeRange === 'month'}>Last 30 Days</option>
<option value="mtd" selected={timeRange === 'mtd'}>Month to Date</option>
<option value="ytd" selected={timeRange === 'ytd'}>Year to Date</option>
<option value="last-month" selected={timeRange === 'last-month'}>Last Month</option>
<option value="custom" selected={timeRange === 'custom'}>Custom Range</option>
</select>
</AutoSubmit>
</fieldset>
{timeRange === 'custom' && (
<>
<fieldset class="fieldset">
<legend class="fieldset-legend text-xs">From Date</legend>
<AutoSubmit client:load>
<input
type="date"
id="reports-from"
name="from"
class="input w-full"
value={customFrom || (startDate.getFullYear() + '-' + String(startDate.getMonth() + 1).padStart(2, '0') + '-' + String(startDate.getDate()).padStart(2, '0'))}
/>
</AutoSubmit>
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend text-xs">To Date</legend>
<AutoSubmit client:load>
<input
type="date"
id="reports-to"
name="to"
class="input w-full"
value={customTo || (endDate.getFullYear() + '-' + String(endDate.getMonth() + 1).padStart(2, '0') + '-' + String(endDate.getDate()).padStart(2, '0'))}
/>
</AutoSubmit>
</fieldset>
</>
)}
<fieldset class="fieldset">
<legend class="fieldset-legend text-xs">Team Member</legend>
<AutoSubmit client:load>
<select id="reports-member" name="member" class="select w-full">
<option value="">All Members</option>
{teamMembers.map(member => (
<option value={member.id} selected={selectedMemberId === member.id}>
{member.name}
</option>
))}
</select>
</AutoSubmit>
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend text-xs">Tag</legend>
<AutoSubmit client:load>
<select id="reports-tag" name="tag" class="select w-full">
<option value="">All Tags</option>
{allTags.map(tag => (
<option value={tag.id} selected={selectedTagId === tag.id}>
{tag.name}
</option>
))}
</select>
</AutoSubmit>
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend text-xs">Client</legend>
<AutoSubmit client:load>
<select id="reports-client" name="client" class="select w-full">
<option value="">All Clients</option>
{allClients.map(client => (
<option value={client.id} selected={selectedClientId === client.id}>
{client.name}
</option>
))}
</select>
</AutoSubmit>
</fieldset>
</form>
</div>
</div>
<!-- Summary Stats -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
<StatCard
title="Total Time"
value={formatDuration(totalTime)}
description={getTimeRangeLabel(timeRange)}
icon="clock"
color="text-primary"
/>
<StatCard
title="Total Entries"
value={String(entries.length)}
description={getTimeRangeLabel(timeRange)}
icon="list-bullet"
color="text-secondary"
/>
<StatCard
title="Revenue"
value={formatCurrency(revenueStats.total)}
description={`${invoiceStats.paid} paid invoices`}
icon="currency-dollar"
color="text-success"
/>
<StatCard
title="Active Members"
value={String(statsByMember.filter(s => s.entryCount > 0).length)}
description={`of ${teamMembers.length} total`}
icon="user-group"
color="text-accent"
/>
</div>
<!-- Invoice & Quote Stats -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div class="card card-border bg-base-100">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="document-text" class="w-4 h-4" />
Invoices Overview
</h2>
<div class="grid grid-cols-2 gap-4">
<div class="stat bg-base-200 rounded-lg">
<div class="stat-title text-xs">Total Invoices</div>
<div class="stat-value text-2xl">{invoiceStats.total}</div>
</div>
<div class="stat bg-base-200 rounded-lg">
<div class="stat-title text-xs">Paid</div>
<div class="stat-value text-2xl text-success">{invoiceStats.paid}</div>
</div>
<div class="stat bg-base-200 rounded-lg">
<div class="stat-title text-xs">Sent</div>
<div class="stat-value text-2xl text-info">{invoiceStats.sent}</div>
</div>
<div class="stat bg-base-200 rounded-lg">
<div class="stat-title text-xs">Draft</div>
<div class="stat-value text-2xl">{invoiceStats.draft}</div>
</div>
</div>
<div class="divider my-2"></div>
<div class="flex justify-between text-sm">
<span class="text-base-content/60">Revenue (Paid)</span>
<span class="font-bold text-success">{formatCurrency(revenueStats.total)}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-base-content/60">Pending (Sent)</span>
<span class="font-bold text-warning">{formatCurrency(revenueStats.pending)}</span>
</div>
</div>
</div>
<div class="card card-border bg-base-100">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="clipboard-document-list" class="w-4 h-4" />
Quotes Overview
</h2>
<div class="grid grid-cols-2 gap-4">
<div class="stat bg-base-200 rounded-lg">
<div class="stat-title text-xs">Total Quotes</div>
<div class="stat-value text-2xl">{quoteStats.total}</div>
</div>
<div class="stat bg-base-200 rounded-lg">
<div class="stat-title text-xs">Accepted</div>
<div class="stat-value text-2xl text-success">{quoteStats.accepted}</div>
</div>
<div class="stat bg-base-200 rounded-lg">
<div class="stat-title text-xs">Pending</div>
<div class="stat-value text-2xl text-info">{quoteStats.sent}</div>
</div>
<div class="stat bg-base-200 rounded-lg">
<div class="stat-title text-xs">Declined</div>
<div class="stat-value text-2xl text-error">{quoteStats.declined}</div>
</div>
</div>
<div class="divider my-2"></div>
<div class="flex justify-between text-sm">
<span class="text-base-content/60">Quoted Value</span>
<span class="font-bold">{formatCurrency(revenueStats.quotedValue)}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-base-content/60">Conversion Rate</span>
<span class="font-bold">
{quoteStats.total > 0 ? Math.round((quoteStats.accepted / quoteStats.total) * 100) : 0}%
</span>
</div>
</div>
</div>
</div>
<!-- Revenue by Client - Only show if there's revenue data and no client filter -->
{!selectedClientId && revenueByClient.length > 0 && (
<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-3">
<Icon name="banknotes" class="w-4 h-4" />
Revenue by Client
</h2>
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>Client</th>
<th>Revenue</th>
<th>Invoices</th>
<th>Avg Invoice</th>
</tr>
</thead>
<tbody>
{revenueByClient.slice(0, 10).map(stat => (
<tr>
<td>
<div class="font-medium">{stat.client.name}</div>
</td>
<td class="font-mono font-semibold text-success text-sm">{formatCurrency(stat.revenue)}</td>
<td>{stat.invoiceCount}</td>
<td class="font-mono text-sm">
{stat.invoiceCount > 0 ? formatCurrency(stat.revenue / stat.invoiceCount) : formatCurrency(0)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Charts Section - Only show if there's data */}
{totalTime > 0 && (
<>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
{/* Tag Distribution Chart - Only show when no tag filter */}
{!selectedTagId && statsByTag.filter(s => s.totalTime > 0).length > 0 && (
<div class="card card-border bg-base-100">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="chart-pie" class="w-4 h-4" />
Tag Distribution
</h2>
<div class="h-64 w-full">
<TagChart
client:visible
tags={statsByTag.filter(s => s.totalTime > 0).map(s => ({
name: s.tag.name,
totalTime: s.totalTime,
color: s.tag.color || '#3b82f6'
}))}
/>
</div>
</div>
</div>
)}
{/* Client Distribution Chart - Only show when no client filter */}
{!selectedClientId && statsByClient.filter(s => s.totalTime > 0).length > 0 && (
<div class="card card-border bg-base-100">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="chart-bar" class="w-4 h-4" />
Time by Client
</h2>
<div class="h-64 w-full">
<ClientChart
client:visible
clients={statsByClient.filter(s => s.totalTime > 0).map(s => ({
name: s.client.name,
totalTime: s.totalTime
}))}
/>
</div>
</div>
</div>
)}
</div>
{/* Team Member Chart - Only show when no member filter */}
{!selectedMemberId && statsByMember.filter(s => s.totalTime > 0).length > 0 && (
<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-3">
<Icon name="users" class="w-4 h-4" />
Time by Team Member
</h2>
<div class="h-64 w-full">
<MemberChart
client:visible
members={statsByMember.filter(s => s.totalTime > 0).map(s => ({
name: s.member.name,
totalTime: s.totalTime
}))}
/>
</div>
</div>
</div>
)}
</>
)}
{/* Stats by Member - Only show if there's data and no member filter */}
{!selectedMemberId && statsByMember.filter(s => s.totalTime > 0).length > 0 && (
<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-3">
<Icon name="users" class="w-4 h-4" />
By Team Member
</h2>
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>Member</th>
<th>Total Time</th>
<th>Entries</th>
<th>Avg per Entry</th>
</tr>
</thead>
<tbody>
{statsByMember.filter(s => s.totalTime > 0).map(stat => (
<tr>
<td>
<div>
<div class="font-medium">{stat.member.name}</div>
<div class="text-xs text-base-content/60">{stat.member.email}</div>
</div>
</td>
<td class="font-mono text-sm">{formatDuration(stat.totalTime)}</td>
<td>{stat.entryCount}</td>
<td class="font-mono text-sm">
{stat.entryCount > 0 ? formatDuration(stat.totalTime / stat.entryCount) : '00:00:00 (0m)'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Stats by Tag - Only show if there's data and no tag filter */}
{!selectedTagId && statsByTag.filter(s => s.totalTime > 0).length > 0 && (
<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-3">
<Icon name="tag" class="w-4 h-4" />
By Tag
</h2>
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>Tag</th>
<th>Total Time</th>
<th>Entries</th>
<th>% of Total</th>
</tr>
</thead>
<tbody>
{statsByTag.filter(s => s.totalTime > 0).map(stat => (
<tr>
<td>
<div class="flex items-center gap-2">
{stat.tag.color && (
<ColorDot client:load color={stat.tag.color} class="w-3 h-3 rounded-full" />
)}
<span>{stat.tag.name}</span>
</div>
</td>
<td class="font-mono text-sm">{formatDuration(stat.totalTime)}</td>
<td>{stat.entryCount}</td>
<td>
<div class="flex items-center gap-2">
<progress
class="progress progress-primary w-16"
value={stat.totalTime}
max={totalTime}
></progress>
<span class="text-xs">
{totalTime > 0 ? Math.round((stat.totalTime / totalTime) * 100) : 0}%
</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Stats by Client - Only show if there's data and no client filter */}
{!selectedClientId && statsByClient.filter(s => s.totalTime > 0).length > 0 && (
<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-3">
<Icon name="building-office" class="w-4 h-4" />
By Client
</h2>
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>Client</th>
<th>Total Time</th>
<th>Entries</th>
<th>% of Total</th>
</tr>
</thead>
<tbody>
{statsByClient.filter(s => s.totalTime > 0).map(stat => (
<tr>
<td>{stat.client.name}</td>
<td class="font-mono text-sm">{formatDuration(stat.totalTime)}</td>
<td>{stat.entryCount}</td>
<td>
<div class="flex items-center gap-2">
<progress
class="progress progress-secondary w-16"
value={stat.totalTime}
max={totalTime}
></progress>
<span class="text-xs">
{totalTime > 0 ? Math.round((stat.totalTime / totalTime) * 100) : 0}%
</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Detailed Entries */}
<div class="card card-border bg-base-100">
<div class="card-body p-4">
<div class="flex justify-between items-center mb-3">
<h2 class="text-sm font-semibold flex items-center gap-2">
<Icon name="document-text" class="w-4 h-4" />
Detailed Entries ({entries.length})
</h2>
{entries.length > 0 && (
<a href={`/api/reports/export${url.search}`} class="btn btn-xs btn-ghost" target="_blank">
<Icon name="arrow-down-tray" class="w-3.5 h-3.5" />
Export CSV
</a>
)}
</div>
{entries.length > 0 ? (
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>Date</th>
<th>Member</th>
<th>Client</th>
<th>Tag</th>
<th>Description</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{entries.map(e => (
<tr>
<td class="whitespace-nowrap">
{e.entry.startTime.toLocaleDateString()}<br/>
<span class="text-xs text-base-content/60">
{e.entry.startTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</span>
</td>
<td>{e.user.name}</td>
<td>{e.client.name}</td>
<td>
{e.tag ? (
<div class="badge badge-xs badge-outline flex items-center gap-1">
{e.tag.color && (
<ColorDot client:load color={e.tag.color} class="w-2 h-2 rounded-full" />
)}
<span>{e.tag.name}</span>
</div>
) : (
<span class="text-base-content/60">-</span>
)}
</td>
<td class="text-base-content/60">{e.entry.description || '-'}</td>
<td class="font-mono text-sm">
{e.entry.endTime
? formatDuration(e.entry.endTime.getTime() - e.entry.startTime.getTime())
: 'Running...'
}
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div class="flex flex-col items-center justify-center py-10 text-center">
<Icon name="inbox" class="w-12 h-12 text-base-content/30 mb-3" />
<h3 class="text-base font-semibold mb-1">No time entries found</h3>
<p class="text-base-content/60 text-sm mb-4">Try adjusting your filters or select a different time range.</p>
<a href="/dashboard/tracker" class="btn btn-primary btn-sm">
<Icon name="play" class="w-4 h-4" />
Start Tracking Time
</a>
</div>
)}
</div>
</div>
</DashboardLayout>