Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions apps/web/app/(dashboard)/billing/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { trpc } from "@/lib/trpc";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
import { formatCurrency } from "@/lib/locale/format";

interface LineItem {
id: string;
Expand Down Expand Up @@ -62,6 +63,7 @@ export default function NewInvoicePage() {
);

const servicesQuery = trpc.billing.listServices.useQuery();
const taxConfigQuery = trpc.billing.getTaxConfig.useQuery();

// Mutation
const utils = trpc.useUtils();
Expand All @@ -76,19 +78,26 @@ export default function NewInvoicePage() {
},
});

// Calculations
// Calculations — preview only; the server recomputes tax authoritatively
// from the practice's configured (region-aware) rate.
const taxPercent = taxConfigQuery.data?.taxRatePercent ?? "8.00";
const taxRate = parseFloat(taxPercent) / 100;
const currency = taxConfigQuery.data?.currency ?? "usd";
const country = taxConfigQuery.data?.country ?? "US";
const fmt = (v: number | string | null | undefined) =>
formatCurrency(v, currency, country);
const { subtotal, tax, total } = useMemo(() => {
const sub = items.reduce(
(sum, item) => sum + item.quantity * parseFloat(item.unitPrice || "0"),
0
);
const t = Math.round(sub * 0.08 * 100) / 100;
const t = Math.round(sub * taxRate * 100) / 100;
return {
subtotal: sub,
tax: t,
total: Math.round((sub + t) * 100) / 100,
};
}, [items]);
}, [items, taxRate]);

function handleServiceSelect(serviceId: string) {
setSelectedServiceId(serviceId);
Expand Down Expand Up @@ -355,13 +364,10 @@ export default function NewInvoicePage() {
{item.quantity}
</td>
<td className="py-2 text-right tabular-nums">
${parseFloat(item.unitPrice).toFixed(2)}
{fmt(item.unitPrice)}
</td>
<td className="py-2 text-right tabular-nums">
$
{(
item.quantity * parseFloat(item.unitPrice)
).toFixed(2)}
{fmt(item.quantity * parseFloat(item.unitPrice))}
</td>
<td className="py-2 text-right">
<button
Expand All @@ -385,15 +391,15 @@ export default function NewInvoicePage() {
<div className="rounded-lg border border-border p-4 space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Subtotal</span>
<span className="tabular-nums">${subtotal.toFixed(2)}</span>
<span className="tabular-nums">{fmt(subtotal)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Tax (8%)</span>
<span className="tabular-nums">${tax.toFixed(2)}</span>
<span className="text-muted-foreground">Tax ({taxPercent}%)</span>
<span className="tabular-nums">{fmt(tax)}</span>
</div>
<div className="flex justify-between font-semibold border-t border-border pt-1">
<span>Total</span>
<span className="tabular-nums">${total.toFixed(2)}</span>
<span className="tabular-nums">{fmt(total)}</span>
</div>
</div>
)}
Expand Down
16 changes: 11 additions & 5 deletions apps/web/app/(dashboard)/billing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "lucide-react";
import { toast } from "sonner";
import { trpc } from "@/lib/trpc";
import { useCurrencyFormatter } from "@/lib/locale/useCurrency";
import { generateInvoicePdf } from "@/lib/pdf";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
Expand Down Expand Up @@ -50,11 +51,6 @@ const PAYMENT_METHODS = [
{ label: "Other", value: "other" },
] as const;

function formatCurrency(value: string | number | null | undefined): string {
const num = Number(value ?? 0);
return `$${num.toFixed(2)}`;
}

function getDisplayStatus(invoice: {
status: string;
paidAmount: string | null;
Expand Down Expand Up @@ -303,6 +299,7 @@ function InvoiceRow({
onConvertEstimate: (e: React.MouseEvent, id: string) => void;
isMutating: boolean;
}) {
const formatCurrency = useCurrencyFormatter();
const detail = trpc.billing.getInvoice.useQuery(
{ id: invoice.id },
{ enabled: isExpanded }
Expand Down Expand Up @@ -455,6 +452,10 @@ function InvoiceRow({
tax: formatCurrency(d.tax),
total: formatCurrency(d.total),
paidAmount: formatCurrency(d.paidAmount),
balanceDue: formatCurrency(
parseFloat(String(d.total)) -
parseFloat(String(d.paidAmount))
),
}).save(`estimate-${clientName || "unknown"}.pdf`);
}}
>
Expand Down Expand Up @@ -615,6 +616,10 @@ function InvoiceRow({
tax: formatCurrency(d.tax),
total: formatCurrency(d.total),
paidAmount: formatCurrency(d.paidAmount),
balanceDue: formatCurrency(
parseFloat(String(d.total)) -
parseFloat(String(d.paidAmount))
),
}).save(`invoice-${clientName || "unknown"}.pdf`);
}}
>
Expand Down Expand Up @@ -689,6 +694,7 @@ function PaymentSection({
invoicePaidAmount: string | null;
invoiceStatus: string;
}) {
const formatCurrency = useCurrencyFormatter();
const [showPaymentForm, setShowPaymentForm] = useState(false);
const [paymentAmount, setPaymentAmount] = useState("");
const [paymentMethod, setPaymentMethod] = useState<string>("cash");
Expand Down
7 changes: 2 additions & 5 deletions apps/web/app/(dashboard)/inventory/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Check,
} from "lucide-react";
import { trpc } from "@/lib/trpc";
import { useCurrencyFormatter } from "@/lib/locale/useCurrency";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
Expand All @@ -26,11 +27,6 @@ const CATEGORIES = [
{ label: "Supply", value: "supply" },
] as const;

function formatCurrency(value: string | number | null | undefined): string {
const num = Number(value ?? 0);
return `$${num.toFixed(2)}`;
}

function isExpiringSoon(expirationDate: string | null | undefined): boolean {
if (!expirationDate) return false;
const expDate = new Date(expirationDate);
Expand Down Expand Up @@ -490,6 +486,7 @@ function AddSupplierForm({ onClose }: { onClose: () => void }) {
// --- Main Page ---

export default function InventoryPage() {
const formatCurrency = useCurrencyFormatter();
const [tab, setTab] = useState<"products" | "suppliers">("products");
const [search, setSearch] = useState("");
const [category, setCategory] = useState("");
Expand Down
30 changes: 15 additions & 15 deletions apps/web/app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Calendar, PawPrint, DollarSign, FileText, Clock } from "lucide-react";
import { cn } from "@/lib/utils";
import { trpc } from "@/lib/trpc";
import { formatCurrency, localeForCountry } from "@/lib/locale/format";
import {
BarChart,
Bar,
Expand All @@ -19,13 +20,6 @@ import {
Line,
} from "recharts";

function formatCurrency(amount: number) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
}

function formatTime(date: Date | string) {
return new Date(date).toLocaleTimeString("en-US", {
hour: "numeric",
Expand All @@ -39,28 +33,28 @@ const kpiConfig = [
label: "Today's Appointments",
description: "Scheduled for today",
icon: Calendar,
format: (v: number) => String(v),
isCurrency: false,
},
{
key: "patientsSeen" as const,
label: "Patients Seen Today",
description: "Checked out today",
icon: PawPrint,
format: (v: number) => String(v),
isCurrency: false,
},
{
key: "revenueMtd" as const,
label: "Revenue (MTD)",
description: "Paid invoices this month",
icon: DollarSign,
format: (v: number) => formatCurrency(v),
isCurrency: true,
},
{
key: "pendingInvoices" as const,
label: "Pending Invoices",
description: "Sent or overdue",
icon: FileText,
format: (v: number) => String(v),
isCurrency: false,
},
];

Expand Down Expand Up @@ -147,6 +141,12 @@ function PieLabel({
export default function DashboardPage() {
const stats = trpc.dashboard.getStats.useQuery();
const charts = trpc.dashboard.getCharts.useQuery();
const taxConfig = trpc.billing.getTaxConfig.useQuery(undefined, {
staleTime: 5 * 60 * 1000,
});
const currency = taxConfig.data?.currency ?? "usd";
const country = taxConfig.data?.country ?? "US";
const fmtMoney = (v: number) => formatCurrency(v, currency, country);

const today = new Date();
const todayStr = today.toISOString().slice(0, 10);
Expand Down Expand Up @@ -195,7 +195,7 @@ export default function DashboardPage() {
{kpi.label}
</p>
<p className="font-heading text-2xl font-bold">
{kpi.format(value)}
{kpi.isCurrency ? fmtMoney(value) : String(value)}
</p>
</div>
</div>
Expand Down Expand Up @@ -385,9 +385,9 @@ export default function DashboardPage() {
className="text-xs fill-muted-foreground"
tick={{ fontSize: 12 }}
tickFormatter={(value: number) =>
new Intl.NumberFormat("en-US", {
new Intl.NumberFormat(localeForCountry(country), {
style: "currency",
currency: "USD",
currency: currency.toUpperCase(),
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value)
Expand All @@ -400,7 +400,7 @@ export default function DashboardPage() {
borderRadius: "0.5rem",
fontSize: "0.875rem",
}}
formatter={(value: number) => [formatCurrency(value), "Revenue"]}
formatter={(value: number) => [fmtMoney(value), "Revenue"]}
/>
<Line
type="monotone"
Expand Down
12 changes: 3 additions & 9 deletions apps/web/app/(dashboard)/reports/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import { trpc } from "@/lib/trpc";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { useCurrencyFormatter } from "@/lib/locale/useCurrency";

type Tab = "revenue" | "appointments" | "services" | "inventory";

Expand All @@ -37,15 +38,6 @@ const tabs: { key: Tab; label: string; icon: React.ElementType }[] = [
{ key: "inventory", label: "Inventory", icon: Package },
];

function formatCurrency(value: number) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
}

function KpiCard({
title,
value,
Expand Down Expand Up @@ -99,6 +91,7 @@ function LoadingSkeleton() {
}

function RevenueTab() {
const formatCurrency = useCurrencyFormatter();
const { data, isLoading } = trpc.reports.revenue.useQuery();

if (isLoading || !data) return <LoadingSkeleton />;
Expand Down Expand Up @@ -252,6 +245,7 @@ function AppointmentsTab() {
}

function ServicesTab() {
const formatCurrency = useCurrencyFormatter();
const { data, isLoading } = trpc.reports.topServices.useQuery();

if (isLoading || !data) return <LoadingSkeleton />;
Expand Down
Loading
Loading