Refactored a bunch of shit
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m57s
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m57s
This commit is contained in:
@@ -4,6 +4,7 @@ import { Icon } from 'astro-icon/components';
|
||||
import { db } from '../../../db';
|
||||
import { invoices, invoiceItems, clients, members, organizations } from '../../../db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { formatCurrency } from '../../../lib/formatTime';
|
||||
|
||||
const { id } = Astro.params;
|
||||
const user = Astro.locals.user;
|
||||
@@ -49,13 +50,6 @@ const items = await db.select()
|
||||
.where(eq(invoiceItems.invoiceId, invoice.id))
|
||||
.all();
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: invoice.currency,
|
||||
}).format(amount / 100);
|
||||
};
|
||||
|
||||
const isDraft = invoice.status === 'draft';
|
||||
---
|
||||
|
||||
@@ -235,8 +229,8 @@ const isDraft = invoice.status === 'draft';
|
||||
<tr>
|
||||
<td class="py-4">{item.description}</td>
|
||||
<td class="py-4 text-right">{item.quantity}</td>
|
||||
<td class="py-4 text-right">{formatCurrency(item.unitPrice)}</td>
|
||||
<td class="py-4 text-right font-medium">{formatCurrency(item.amount)}</td>
|
||||
<td class="py-4 text-right">{formatCurrency(item.unitPrice, invoice.currency)}</td>
|
||||
<td class="py-4 text-right font-medium">{formatCurrency(item.amount, invoice.currency)}</td>
|
||||
{isDraft && (
|
||||
<td class="py-4 text-right">
|
||||
<form method="POST" action={`/api/invoices/${invoice.id}/items/delete`}>
|
||||
@@ -299,7 +293,7 @@ const isDraft = invoice.status === 'draft';
|
||||
<div class="w-64 space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-base-content/60">Subtotal</span>
|
||||
<span class="font-medium">{formatCurrency(invoice.subtotal)}</span>
|
||||
<span class="font-medium">{formatCurrency(invoice.subtotal, invoice.currency)}</span>
|
||||
</div>
|
||||
{(invoice.discountAmount && invoice.discountAmount > 0) && (
|
||||
<div class="flex justify-between text-sm">
|
||||
@@ -307,7 +301,7 @@ const isDraft = invoice.status === 'draft';
|
||||
Discount
|
||||
{invoice.discountType === 'percentage' && ` (${invoice.discountValue}%)`}
|
||||
</span>
|
||||
<span class="font-medium text-success">-{formatCurrency(invoice.discountAmount)}</span>
|
||||
<span class="font-medium text-success">-{formatCurrency(invoice.discountAmount, invoice.currency)}</span>
|
||||
</div>
|
||||
)}
|
||||
{((invoice.taxRate ?? 0) > 0 || isDraft) && (
|
||||
@@ -320,13 +314,13 @@ const isDraft = invoice.status === 'draft';
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
<span class="font-medium">{formatCurrency(invoice.taxAmount)}</span>
|
||||
<span class="font-medium">{formatCurrency(invoice.taxAmount, invoice.currency)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-between text-lg font-bold">
|
||||
<span>Total</span>
|
||||
<span class="text-primary">{formatCurrency(invoice.total)}</span>
|
||||
<span class="text-primary">{formatCurrency(invoice.total, invoice.currency)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
---
|
||||
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import StatCard from '../../../components/StatCard.astro';
|
||||
import { db } from '../../../db';
|
||||
import { invoices, clients, members } from '../../../db/schema';
|
||||
import { invoices, clients } from '../../../db/schema';
|
||||
import { eq, desc, and, gte, lte, sql } from 'drizzle-orm';
|
||||
import { getCurrentTeam } from '../../../lib/getCurrentTeam';
|
||||
import { formatCurrency } from '../../../lib/formatTime';
|
||||
|
||||
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 userMembership = await getCurrentTeam(user, Astro.cookies.get('currentTeamId')?.value);
|
||||
if (!userMembership) return Astro.redirect('/dashboard');
|
||||
|
||||
const currentTeamIdResolved = userMembership.organizationId;
|
||||
|
||||
@@ -96,13 +87,6 @@ const yearInvoices = allInvoicesRaw.filter(i => {
|
||||
return issueDate >= yearStart && issueDate <= yearEnd;
|
||||
});
|
||||
|
||||
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';
|
||||
@@ -130,40 +114,35 @@ const getStatusColor = (status: string) => {
|
||||
|
||||
<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">{yearInvoices.filter(i => i.invoice.type === 'invoice').length}</div>
|
||||
<div class="stat-desc">{selectedYear === 'current' ? `${currentYear} (YTD)` : selectedYear}</div>
|
||||
</div>
|
||||
<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">
|
||||
<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">{yearInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'sent').length}</div>
|
||||
<div class="stat-desc">Waiting for approval</div>
|
||||
</div>
|
||||
<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">
|
||||
<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(yearInvoices
|
||||
.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 ({selectedYear === 'current' ? `${currentYear} YTD` : selectedYear})</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -2,26 +2,15 @@
|
||||
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { db } from '../../../db';
|
||||
import { clients, members, invoices, organizations } from '../../../db/schema';
|
||||
import { clients, invoices, organizations } from '../../../db/schema';
|
||||
import { eq, desc, and } from 'drizzle-orm';
|
||||
import { getCurrentTeam } from '../../../lib/getCurrentTeam';
|
||||
|
||||
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 userMembership = await getCurrentTeam(user, Astro.cookies.get('currentTeamId')?.value);
|
||||
if (!userMembership) return Astro.redirect('/dashboard');
|
||||
|
||||
const currentTeamIdResolved = userMembership.organizationId;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user