This commit is contained in:
@@ -19,7 +19,8 @@ const currentTeamIdResolved = userMembership.organizationId;
|
||||
// Get filter parameters
|
||||
const currentYear = new Date().getFullYear();
|
||||
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 selectedStatus = Astro.url.searchParams.get('status') || 'all';
|
||||
const sortBy = Astro.url.searchParams.get('sort') || 'date-desc';
|
||||
@@ -43,8 +44,8 @@ if (!availableYears.includes(currentYear)) {
|
||||
}
|
||||
|
||||
// Filter by year
|
||||
const yearStart = selectedYear === 'current' ? new Date(currentYear, 0, 1) : new Date(selectedYear, 0, 1);
|
||||
const yearEnd = selectedYear === 'current' ? new Date() : new Date(selectedYear, 11, 31, 23, 59, 59);
|
||||
const yearStart = new Date(yearNum, 0, 1);
|
||||
const yearEnd = selectedYear === 'current' ? new Date() : new Date(yearNum, 11, 31, 23, 59, 59);
|
||||
|
||||
let filteredInvoices = allInvoicesRaw.filter(i => {
|
||||
const issueDate = i.invoice.issueDate;
|
||||
@@ -103,81 +104,67 @@ const getStatusColor = (status: string) => {
|
||||
<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>
|
||||
<h1 class="text-3xl font-bold">Invoices & Quotes</h1>
|
||||
<p class="text-base-content/60 mt-1">Manage your billing and estimates</p>
|
||||
<h1 class="text-2xl font-extrabold tracking-tight">Invoices & Quotes</h1>
|
||||
<p class="text-base-content/60 text-sm mt-1">Manage your billing and estimates</p>
|
||||
</div>
|
||||
<a href="/dashboard/invoices/new" class="btn btn-primary">
|
||||
<Icon name="heroicons:plus" class="w-5 h-5" />
|
||||
<a href="/dashboard/invoices/new" class="btn btn-primary btn-sm">
|
||||
<Icon name="heroicons:plus" class="w-4 h-4" />
|
||||
Create New
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div class="stats shadow bg-base-100 border border-base-200">
|
||||
<StatCard
|
||||
title="Total Invoices"
|
||||
value={String(yearInvoices.filter(i => i.invoice.type === 'invoice').length)}
|
||||
description={selectedYear === 'current' ? `${currentYear} (YTD)` : selectedYear}
|
||||
icon="heroicons:document-text"
|
||||
color="text-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow bg-base-100 border border-base-200">
|
||||
<StatCard
|
||||
title="Open Quotes"
|
||||
value={String(yearInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'sent').length)}
|
||||
description="Waiting for approval"
|
||||
icon="heroicons:clipboard-document-list"
|
||||
color="text-secondary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow bg-base-100 border border-base-200">
|
||||
<StatCard
|
||||
title="Total Revenue"
|
||||
value={formatCurrency(yearInvoices
|
||||
.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'paid')
|
||||
.reduce((acc, curr) => acc + curr.invoice.total, 0), 'USD')}
|
||||
description={`Paid invoices (${selectedYear === 'current' ? `${currentYear} YTD` : selectedYear})`}
|
||||
icon="heroicons:currency-dollar"
|
||||
color="text-success"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-6">
|
||||
<StatCard
|
||||
title="Total Invoices"
|
||||
value={String(yearInvoices.filter(i => i.invoice.type === 'invoice').length)}
|
||||
description={selectedYear === 'current' ? `${currentYear} (YTD)` : String(selectedYear)}
|
||||
icon="heroicons:document-text"
|
||||
color="text-primary"
|
||||
/>
|
||||
<StatCard
|
||||
title="Open Quotes"
|
||||
value={String(yearInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'sent').length)}
|
||||
description="Waiting for approval"
|
||||
icon="heroicons:clipboard-document-list"
|
||||
color="text-secondary"
|
||||
/>
|
||||
<StatCard
|
||||
title="Total Revenue"
|
||||
value={formatCurrency(yearInvoices
|
||||
.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'paid')
|
||||
.reduce((acc, curr) => acc + curr.invoice.total, 0), 'USD')}
|
||||
description={`Paid invoices (${selectedYear === 'current' ? `${currentYear} YTD` : selectedYear})`}
|
||||
icon="heroicons:currency-dollar"
|
||||
color="text-success"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
|
||||
<div class="card-body">
|
||||
<form method="GET" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Year</span>
|
||||
</label>
|
||||
<select name="year" class="select select-bordered w-full" onchange="this.form.submit()">
|
||||
<div class="card card-border bg-base-100 mb-6">
|
||||
<div class="card-body p-4">
|
||||
<form method="GET" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-xs">Year</legend>
|
||||
<select name="year" class="select w-full" onchange="this.form.submit()">
|
||||
<option value="current" selected={selectedYear === 'current'}>Current Year to Date ({currentYear})</option>
|
||||
{availableYears.map(year => (
|
||||
<option value={year} selected={year === selectedYear}>{year}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Type</span>
|
||||
</label>
|
||||
<select name="type" class="select select-bordered w-full" onchange="this.form.submit()">
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-xs">Type</legend>
|
||||
<select name="type" class="select w-full" onchange="this.form.submit()">
|
||||
<option value="all" selected={selectedType === 'all'}>All Types</option>
|
||||
<option value="invoice" selected={selectedType === 'invoice'}>Invoices</option>
|
||||
<option value="quote" selected={selectedType === 'quote'}>Quotes</option>
|
||||
</select>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Status</span>
|
||||
</label>
|
||||
<select name="status" class="select select-bordered w-full" onchange="this.form.submit()">
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-xs">Status</legend>
|
||||
<select name="status" class="select w-full" onchange="this.form.submit()">
|
||||
<option value="all" selected={selectedStatus === 'all'}>All Statuses</option>
|
||||
<option value="draft" selected={selectedStatus === 'draft'}>Draft</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="void" selected={selectedStatus === 'void'}>Void</option>
|
||||
</select>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Sort By</span>
|
||||
</label>
|
||||
<select name="sort" class="select select-bordered w-full" onchange="this.form.submit()">
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-xs">Sort By</legend>
|
||||
<select name="sort" class="select w-full" onchange="this.form.submit()">
|
||||
<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="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-asc" selected={sortBy === 'number-asc'}>Number (A-Z)</option>
|
||||
</select>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
{(selectedYear !== 'current' || selectedType !== 'all' || selectedStatus !== 'all' || sortBy !== 'date-desc') && (
|
||||
<div class="mt-4">
|
||||
<a href="/dashboard/invoices" class="btn btn-ghost btn-sm">
|
||||
<Icon name="heroicons:x-mark" class="w-4 h-4" />
|
||||
<div class="mt-3">
|
||||
<a href="/dashboard/invoices" class="btn btn-ghost btn-xs">
|
||||
<Icon name="heroicons:x-mark" class="w-3 h-3" />
|
||||
Clear Filters
|
||||
</a>
|
||||
</div>
|
||||
@@ -214,19 +199,19 @@ const getStatusColor = (status: string) => {
|
||||
</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="px-6 py-4 border-b border-base-200 bg-base-200/30">
|
||||
<p class="text-sm text-base-content/70">
|
||||
<div class="px-4 py-3 border-b border-base-200">
|
||||
<p class="text-xs text-base-content/50">
|
||||
Showing <span class="font-semibold text-base-content">{allInvoices.length}</span>
|
||||
{allInvoices.length === 1 ? 'result' : 'results'}
|
||||
{selectedYear === 'current' ? ` for ${currentYear} (year to date)` : ` for ${selectedYear}`}
|
||||
</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto md:overflow-visible pb-32 md:pb-0">
|
||||
<table class="table table-zebra">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr class="bg-base-200/50">
|
||||
<tr>
|
||||
<th>Number</th>
|
||||
<th>Client</th>
|
||||
<th>Date</th>
|
||||
@@ -240,14 +225,14 @@ const getStatusColor = (status: string) => {
|
||||
<tbody>
|
||||
{allInvoices.length === 0 ? (
|
||||
<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.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
allInvoices.map(({ invoice, client }) => (
|
||||
<tr class="hover:bg-base-200/50 transition-colors">
|
||||
<td class="font-mono font-medium">
|
||||
<tr class="hover">
|
||||
<td class="font-mono font-medium text-sm">
|
||||
<a href={`/dashboard/invoices/${invoice.id}`} class="link link-hover text-primary">
|
||||
{invoice.number}
|
||||
</a>
|
||||
@@ -265,7 +250,7 @@ const getStatusColor = (status: string) => {
|
||||
{formatCurrency(invoice.total, invoice.currency)}
|
||||
</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}
|
||||
</div>
|
||||
</td>
|
||||
@@ -274,10 +259,10 @@ const getStatusColor = (status: string) => {
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="dropdown dropdown-end">
|
||||
<div role="button" tabindex="0" class="btn btn-ghost btn-sm btn-square">
|
||||
<Icon name="heroicons:ellipsis-vertical" class="w-5 h-5" />
|
||||
<div role="button" tabindex="0" class="btn btn-ghost btn-xs btn-square">
|
||||
<Icon name="heroicons:ellipsis-vertical" class="w-4 h-4" />
|
||||
</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>
|
||||
<a href={`/dashboard/invoices/${invoice.id}`}>
|
||||
<Icon name="heroicons:eye" class="w-4 h-4" />
|
||||
|
||||
Reference in New Issue
Block a user