Last fix for the night...
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m9s
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m9s
This commit is contained in:
@@ -3,7 +3,7 @@ import DashboardLayout from '../../../layouts/DashboardLayout.astro';
|
|||||||
import { Icon } from 'astro-icon/components';
|
import { Icon } from 'astro-icon/components';
|
||||||
import { db } from '../../../db';
|
import { db } from '../../../db';
|
||||||
import { invoices, clients, members } from '../../../db/schema';
|
import { invoices, clients, members } from '../../../db/schema';
|
||||||
import { eq, desc, and } from 'drizzle-orm';
|
import { eq, desc, and, gte, lte, sql } from 'drizzle-orm';
|
||||||
|
|
||||||
const user = Astro.locals.user;
|
const user = Astro.locals.user;
|
||||||
if (!user) return Astro.redirect('/login');
|
if (!user) return Astro.redirect('/login');
|
||||||
@@ -25,17 +25,71 @@ const userMembership = currentTeamId
|
|||||||
|
|
||||||
const currentTeamIdResolved = userMembership.organizationId;
|
const currentTeamIdResolved = userMembership.organizationId;
|
||||||
|
|
||||||
// Fetch invoices and quotes
|
// Get filter parameters
|
||||||
const allInvoices = await db.select({
|
const currentYear = new Date().getFullYear();
|
||||||
|
const selectedYear = Astro.url.searchParams.get('year') ? parseInt(Astro.url.searchParams.get('year')!) : 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';
|
||||||
|
|
||||||
|
// Fetch all invoices for the organization (for year dropdown)
|
||||||
|
const allInvoicesRaw = await db.select({
|
||||||
invoice: invoices,
|
invoice: invoices,
|
||||||
client: clients,
|
client: clients,
|
||||||
})
|
})
|
||||||
.from(invoices)
|
.from(invoices)
|
||||||
.leftJoin(clients, eq(invoices.clientId, clients.id))
|
.leftJoin(clients, eq(invoices.clientId, clients.id))
|
||||||
.where(eq(invoices.organizationId, currentTeamIdResolved))
|
.where(eq(invoices.organizationId, currentTeamIdResolved))
|
||||||
.orderBy(desc(invoices.issueDate))
|
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
|
// Get unique years from invoices
|
||||||
|
const availableYears = [...new Set(allInvoicesRaw.map(i => i.invoice.issueDate.getFullYear()))].sort((a, b) => b - a);
|
||||||
|
|
||||||
|
// Filter by year
|
||||||
|
const yearStart = new Date(selectedYear, 0, 1);
|
||||||
|
const yearEnd = new Date(selectedYear, 11, 31, 23, 59, 59);
|
||||||
|
|
||||||
|
let filteredInvoices = allInvoicesRaw.filter(i => {
|
||||||
|
const issueDate = i.invoice.issueDate;
|
||||||
|
return issueDate >= yearStart && issueDate <= yearEnd;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter by type
|
||||||
|
if (selectedType !== 'all') {
|
||||||
|
filteredInvoices = filteredInvoices.filter(i => i.invoice.type === selectedType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by status
|
||||||
|
if (selectedStatus !== 'all') {
|
||||||
|
filteredInvoices = filteredInvoices.filter(i => i.invoice.status === selectedStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort invoices
|
||||||
|
const allInvoices = filteredInvoices.sort((a, b) => {
|
||||||
|
switch (sortBy) {
|
||||||
|
case 'date-desc':
|
||||||
|
return b.invoice.issueDate.getTime() - a.invoice.issueDate.getTime();
|
||||||
|
case 'date-asc':
|
||||||
|
return a.invoice.issueDate.getTime() - b.invoice.issueDate.getTime();
|
||||||
|
case 'amount-desc':
|
||||||
|
return b.invoice.total - a.invoice.total;
|
||||||
|
case 'amount-asc':
|
||||||
|
return a.invoice.total - b.invoice.total;
|
||||||
|
case 'number-desc':
|
||||||
|
return b.invoice.number.localeCompare(a.invoice.number);
|
||||||
|
case 'number-asc':
|
||||||
|
return a.invoice.number.localeCompare(b.invoice.number);
|
||||||
|
default:
|
||||||
|
return b.invoice.issueDate.getTime() - a.invoice.issueDate.getTime();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate stats for the selected year
|
||||||
|
const yearInvoices = allInvoicesRaw.filter(i => {
|
||||||
|
const issueDate = i.invoice.issueDate;
|
||||||
|
return issueDate >= yearStart && issueDate <= yearEnd;
|
||||||
|
});
|
||||||
|
|
||||||
const formatCurrency = (amount: number, currency: string) => {
|
const formatCurrency = (amount: number, currency: string) => {
|
||||||
return new Intl.NumberFormat('en-US', {
|
return new Intl.NumberFormat('en-US', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
@@ -75,8 +129,8 @@ const getStatusColor = (status: string) => {
|
|||||||
<Icon name="heroicons:document-text" class="w-8 h-8" />
|
<Icon name="heroicons:document-text" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-title">Total Invoices</div>
|
<div class="stat-title">Total Invoices</div>
|
||||||
<div class="stat-value text-primary">{allInvoices.filter(i => i.invoice.type === 'invoice').length}</div>
|
<div class="stat-value text-primary">{yearInvoices.filter(i => i.invoice.type === 'invoice').length}</div>
|
||||||
<div class="stat-desc">All time</div>
|
<div class="stat-desc">{selectedYear}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -86,7 +140,7 @@ const getStatusColor = (status: string) => {
|
|||||||
<Icon name="heroicons:clipboard-document-list" class="w-8 h-8" />
|
<Icon name="heroicons:clipboard-document-list" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-title">Open Quotes</div>
|
<div class="stat-title">Open Quotes</div>
|
||||||
<div class="stat-value text-secondary">{allInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'sent').length}</div>
|
<div class="stat-value text-secondary">{yearInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'sent').length}</div>
|
||||||
<div class="stat-desc">Waiting for approval</div>
|
<div class="stat-desc">Waiting for approval</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,17 +152,95 @@ const getStatusColor = (status: string) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat-title">Total Revenue</div>
|
<div class="stat-title">Total Revenue</div>
|
||||||
<div class="stat-value text-success">
|
<div class="stat-value text-success">
|
||||||
{formatCurrency(allInvoices
|
{formatCurrency(yearInvoices
|
||||||
.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'paid')
|
.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'paid')
|
||||||
.reduce((acc, curr) => acc + curr.invoice.total, 0), 'USD')}
|
.reduce((acc, curr) => acc + curr.invoice.total, 0), 'USD')}
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-desc">Paid invoices</div>
|
<div class="stat-desc">Paid invoices ({selectedYear})</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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()">
|
||||||
|
{availableYears.length > 0 ? (
|
||||||
|
availableYears.map(year => (
|
||||||
|
<option value={year} selected={year === selectedYear}>{year}</option>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<option value={currentYear}>{currentYear}</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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()">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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()">
|
||||||
|
<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>
|
||||||
|
<option value="paid" selected={selectedStatus === 'paid'}>Paid</option>
|
||||||
|
<option value="accepted" selected={selectedStatus === 'accepted'}>Accepted</option>
|
||||||
|
<option value="declined" selected={selectedStatus === 'declined'}>Declined</option>
|
||||||
|
<option value="void" selected={selectedStatus === 'void'}>Void</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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()">
|
||||||
|
<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>
|
||||||
|
<option value="amount-asc" selected={sortBy === 'amount-asc'}>Amount (Low to High)</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>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{(selectedYear !== currentYear || 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" />
|
||||||
|
Clear Filters
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-xl border border-base-200">
|
<div class="card bg-base-100 shadow-xl border border-base-200">
|
||||||
<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">
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Showing <span class="font-semibold text-base-content">{allInvoices.length}</span>
|
||||||
|
{allInvoices.length === 1 ? 'result' : 'results'}
|
||||||
|
{selectedYear && ` for ${selectedYear}`}
|
||||||
|
</p>
|
||||||
|
</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-zebra">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
Reference in New Issue
Block a user