feat: improve the invoices in the portal

This commit is contained in:
Nathnael
2026-06-29 14:16:31 +00:00
parent 4cf8e89780
commit feae27d0c3
2 changed files with 64 additions and 70 deletions

View File

@@ -34,7 +34,6 @@ export default function MyPortalPage() {
totalOutstanding,
companyName,
greeting,
recentInvoices,
dashboard,
volumePoints,
maxVolume,
@@ -120,7 +119,7 @@ export default function MyPortalPage() {
</Grid.Col>
<Grid.Col span={{ base: 12, lg: 4 }}>
<InvoicesSection invoices={recentInvoices} />
<InvoicesSection invoices={outstandingInvoices} />
</Grid.Col>
</Grid>

View File

@@ -2,7 +2,7 @@ import { formatCurrency } from "@/lib/currency";
import type { PortalInvoice } from "@/services/invoices.service";
import { Box, Group, Stack, Text } from "@mantine/core";
import { Freight } from "@edr/types";
import { CheckCircle2, ChevronRight, Clock3, Zap } from "lucide-react";
import { ChevronRight, Clock3 } from "lucide-react";
import { memo } from "react";
import { Link } from "react-router-dom";
import { cv, INVOICE_BADGE } from "../constants";
@@ -13,18 +13,22 @@ interface InvoicesSectionProps {
invoices: PortalInvoice[];
}
/** Max rows to list in the compact dashboard card. */
const MAX_ROWS = 5;
const titleCase = (v: string) =>
v ? v.charAt(0).toUpperCase() + v.slice(1).toLowerCase() : "";
export const InvoicesSection = memo(function InvoicesSection({
invoices,
}: InvoicesSectionProps) {
const outstandingInvoices = invoices.filter(
// Dashboard shows unpaid invoices only (pending + overdue).
const pendingInvoices = invoices.filter(
(inv) =>
inv.status === Freight.InvoiceStatus.Pending ||
inv.status === Freight.InvoiceStatus.Overdue,
);
const totalOutstanding = outstandingInvoices.reduce(
const totalOutstanding = pendingInvoices.reduce(
(sum, inv) => sum + Number(inv.totalAmount),
0,
);
@@ -52,83 +56,74 @@ export const InvoicesSection = memo(function InvoicesSection({
<Text fz={24} fw={800} mt={4} c="edr-text">
{formatCurrency(totalOutstanding || 0, "ETB")}
</Text>
<Group justify="space-between" align="center" mt={8} wrap="nowrap">
<Text fz={12} c="edr-amber-text">
{outstandingInvoices.length} invoices unpaid
</Text>
<Group
gap={5}
align="center"
px={14}
py={8}
bg="edr-accent"
className="cursor-pointer rounded-[9px]"
>
<Zap size={15} color="#fff" />
<Text fz={13} fw={700} c="white">
Pay all
</Text>
</Group>
</Group>
<Text fz={12} mt={8} c="edr-amber-text">
{pendingInvoices.length} invoices unpaid
</Text>
</Box>
{invoices.length === 0 ? (
<EmptyState message="No invoices yet." />
{pendingInvoices.length === 0 ? (
<EmptyState message="No pending invoices." />
) : (
<Stack gap={0}>
{invoices.map((invoice, i) => {
{pendingInvoices.slice(0, MAX_ROWS).map((invoice, i) => {
const badge = INVOICE_BADGE[invoice.status];
const isPaid = invoice.status === Freight.InvoiceStatus.Paid;
const isOverdue = invoice.status === Freight.InvoiceStatus.Overdue;
const dueText = isPaid
? "Paid"
: isOverdue
? "Overdue"
: `Due ${new Date(invoice.dueAt).toLocaleDateString()}`;
const DueIcon = isPaid ? CheckCircle2 : Clock3;
const dueIconColor = isPaid ? cv("edr-green.5") : cv("edr-muted");
const dueText = isOverdue
? "Overdue"
: `Due ${new Date(invoice.dueAt).toLocaleDateString()}`;
const dueIconColor = isOverdue ? cv("edr-red") : cv("edr-muted");
return (
<Box key={invoice.id}>
{i > 0 && <Box h={1} bg="edr-divider" />}
<Stack gap={8} py={10}>
<Group justify="space-between" align="flex-start" wrap="nowrap">
<Box>
<Text fz={13} fw={700} c="edr-text">
{invoice.invoiceNumber}
</Text>
<Text fz={11} c="edr-muted">
{titleCase(invoice.source)} · {titleCase(invoice.type)}
</Text>
</Box>
<Text fz={14} fw={700} c="edr-text">
{formatCurrency(
Number(invoice.totalAmount),
invoice.currency,
)}
</Text>
</Group>
<Group justify="space-between" align="center" wrap="nowrap">
<Group gap={5} align="center">
<DueIcon size={13} color={dueIconColor} />
<Text fz={12} c="edr-muted">
{dueText}
</Text>
</Group>
{badge && (
<Box
bg={badge.bg}
px={10}
py={4}
className="rounded-full"
>
<Text fz={11} fw={700} c={badge.text}>
{badge.label}
<Link
to={`/billing/${invoice.id}`}
className="no-underline"
style={{ display: "block", color: "inherit" }}
>
<Stack
gap={8}
py={10}
className="cursor-pointer rounded-[9px] transition-colors hover:bg-[var(--mantine-color-edr-soft-0)]"
>
<Group justify="space-between" align="flex-start" wrap="nowrap">
<Box>
<Text fz={13} fw={700} c="edr-text">
{invoice.invoiceNumber}
</Text>
<Text fz={11} c="edr-muted">
{titleCase(invoice.source)} · {titleCase(invoice.type)}
</Text>
</Box>
)}
</Group>
</Stack>
<Text fz={14} fw={700} c="edr-text">
{formatCurrency(
Number(invoice.totalAmount),
invoice.currency,
)}
</Text>
</Group>
<Group justify="space-between" align="center" wrap="nowrap">
<Group gap={5} align="center">
<Clock3 size={13} color={dueIconColor} />
<Text fz={12} c="edr-muted">
{dueText}
</Text>
</Group>
{badge && (
<Box
bg={badge.bg}
px={10}
py={4}
className="rounded-full"
>
<Text fz={11} fw={700} c={badge.text}>
{badge.label}
</Text>
</Box>
)}
</Group>
</Stack>
</Link>
</Box>
);
})}