All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m11s
218 lines
8.6 KiB
Plaintext
218 lines
8.6 KiB
Plaintext
---
|
|
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
|
|
import { Icon } from 'astro-icon/components';
|
|
import { db } from '../../../db';
|
|
import { invoices, clients, members } from '../../../db/schema';
|
|
import { eq, desc, and } from 'drizzle-orm';
|
|
|
|
const user = Astro.locals.user;
|
|
if (!user) return Astro.redirect('/login');
|
|
|
|
// 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 currentTeamIdResolved = userMembership.organizationId;
|
|
|
|
// Fetch invoices and quotes
|
|
const allInvoices = await db.select({
|
|
invoice: invoices,
|
|
client: clients,
|
|
})
|
|
.from(invoices)
|
|
.leftJoin(clients, eq(invoices.clientId, clients.id))
|
|
.where(eq(invoices.organizationId, currentTeamIdResolved))
|
|
.orderBy(desc(invoices.issueDate))
|
|
.all();
|
|
|
|
const formatCurrency = (amount: number, currency: string) => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: currency,
|
|
}).format(amount / 100);
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'paid': return 'badge-success';
|
|
case 'accepted': return 'badge-success';
|
|
case 'sent': return 'badge-info';
|
|
case 'draft': return 'badge-ghost';
|
|
case 'void': return 'badge-error';
|
|
case 'declined': return 'badge-error';
|
|
default: return 'badge-ghost';
|
|
}
|
|
};
|
|
---
|
|
|
|
<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>
|
|
</div>
|
|
<a href="/dashboard/invoices/new" class="btn btn-primary">
|
|
<Icon name="heroicons:plus" class="w-5 h-5" />
|
|
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">
|
|
<div class="stat">
|
|
<div class="stat-figure text-primary">
|
|
<Icon name="heroicons:document-text" class="w-8 h-8" />
|
|
</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-desc">All time</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats shadow bg-base-100 border border-base-200">
|
|
<div class="stat">
|
|
<div class="stat-figure text-secondary">
|
|
<Icon name="heroicons:clipboard-document-list" class="w-8 h-8" />
|
|
</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-desc">Waiting for approval</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats shadow bg-base-100 border border-base-200">
|
|
<div class="stat">
|
|
<div class="stat-figure text-success">
|
|
<Icon name="heroicons:currency-dollar" class="w-8 h-8" />
|
|
</div>
|
|
<div class="stat-title">Total Revenue</div>
|
|
<div class="stat-value text-success">
|
|
{formatCurrency(allInvoices
|
|
.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'paid')
|
|
.reduce((acc, curr) => acc + curr.invoice.total, 0), 'USD')}
|
|
</div>
|
|
<div class="stat-desc">Paid invoices</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card bg-base-100 shadow-xl border border-base-200">
|
|
<div class="card-body p-0">
|
|
<div class="overflow-x-auto md:overflow-visible pb-32 md:pb-0">
|
|
<table class="table table-zebra">
|
|
<thead>
|
|
<tr class="bg-base-200/50">
|
|
<th>Number</th>
|
|
<th>Client</th>
|
|
<th>Date</th>
|
|
<th>Due Date</th>
|
|
<th>Amount</th>
|
|
<th>Status</th>
|
|
<th>Type</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{allInvoices.length === 0 ? (
|
|
<tr>
|
|
<td colspan="8" class="text-center py-8 text-base-content/60">
|
|
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">
|
|
<a href={`/dashboard/invoices/${invoice.id}`} class="link link-hover text-primary">
|
|
{invoice.number}
|
|
</a>
|
|
</td>
|
|
<td>
|
|
{client ? (
|
|
<div class="font-medium">{client.name}</div>
|
|
) : (
|
|
<span class="text-base-content/40 italic">Deleted Client</span>
|
|
)}
|
|
</td>
|
|
<td>{invoice.issueDate.toLocaleDateString()}</td>
|
|
<td>{invoice.dueDate.toLocaleDateString()}</td>
|
|
<td class="font-mono font-medium">
|
|
{formatCurrency(invoice.total, invoice.currency)}
|
|
</td>
|
|
<td>
|
|
<div class={`badge ${getStatusColor(invoice.status)} badge-sm uppercase font-bold tracking-wider`}>
|
|
{invoice.status}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="capitalize text-sm">{invoice.type}</span>
|
|
</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>
|
|
<ul tabindex="0" class="dropdown-content menu p-2 shadow-lg 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" />
|
|
View Details
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href={`/dashboard/invoices/${invoice.id}/edit`}>
|
|
<Icon name="heroicons:pencil-square" class="w-4 h-4" />
|
|
Edit
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href={`/api/invoices/${invoice.id}/generate`} download>
|
|
<Icon name="heroicons:arrow-down-tray" class="w-4 h-4" />
|
|
Download PDF
|
|
</a>
|
|
</li>
|
|
{invoice.status === 'draft' && (
|
|
<li>
|
|
<form method="POST" action={`/api/invoices/${invoice.id}/status`} class="w-full">
|
|
<input type="hidden" name="status" value="sent" />
|
|
<button type="submit" class="w-full justify-start">
|
|
<Icon name="heroicons:paper-airplane" class="w-4 h-4" />
|
|
Mark as Sent
|
|
</button>
|
|
</form>
|
|
</li>
|
|
)}
|
|
<div class="divider my-1"></div>
|
|
<li>
|
|
<form method="POST" action={`/api/invoices/delete`} onsubmit="return confirm('Are you sure? This action cannot be undone.');" class="w-full">
|
|
<input type="hidden" name="id" value={invoice.id} />
|
|
<button type="submit" class="w-full justify-start text-error hover:bg-error/10">
|
|
<Icon name="heroicons:trash" class="w-4 h-4" />
|
|
Delete
|
|
</button>
|
|
</form>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DashboardLayout>
|