Skip to content

Commit d185e00

Browse files
feat: add payment certificate, deadline suggester, source bar, and version history tabs (#95, #96, #97, #98) (#344)
- PaymentCertificate (closes #95): component integrated on Released invoices with Download Certificate button, QR code, print layout - DeadlineSuggester (closes #96): component integrated below deadline input on new invoice form, uses historical data with static fallback - PaymentSourceBar (closes #97): stacked bar showing each payer contribution colour-coded with legend, integrated in payments section - VersionHistory (closes #98): add History tab to invoice detail page using VersionHistory component showing audit log diff-style timeline; also fix missing TransferOwnershipModal import
1 parent 73813cc commit d185e00

1 file changed

Lines changed: 50 additions & 42 deletions

File tree

src/app/invoice/[id]/page.tsx

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,29 @@ export default function InvoiceDetailPage({ params }: Props) {
6565
const [showDuplicateModal, setShowDuplicateModal] = useState(false);
6666
const [paymentError, setPaymentError] = useState<string | null>(null);
6767
const [paymentMethod, setPaymentMethod] = useState<"freighter" | "walletconnect">("freighter");
68-
const [payerNonce, setPayerNonce] = useState<bigint | null>(null);
68+
const [amountLocked, setAmountLocked] = useState(false);
69+
const prevPayAmountRef = useRef("");
70+
const [cooldownExpiresAt, setCooldownExpiresAt] = useState<number | null>(null);
71+
const [activeDetailsTab, setActiveDetailsTab] = useState<"audit" | "history" | "notes">("audit");
72+
73+
const prevStatusRef = useRef<string | null>(null);
74+
const timelineRef = useRef<HTMLDivElement>(null);
75+
const [exportingTimeline, setExportingTimeline] = useState(false);
76+
77+
useEffect(() => {
78+
setNotifySubscribed(isSubscribedToInvoice(id));
79+
}, [id]);
80+
81+
const handleNotifyMe = async () => {
82+
const permission = await requestNotificationPermission();
83+
if (permission !== "granted") {
84+
setNotifyDenied(true);
85+
return;
86+
}
87+
subscribeToInvoice(id);
88+
setNotifySubscribed(true);
89+
setNotifyDenied(false);
90+
};
6991

7092
const load = async () => {
7193
const inv = await splitClient.getInvoice(id);
@@ -581,50 +603,36 @@ export default function InvoiceDetailPage({ params }: Props) {
581603
/>
582604
)}
583605

584-
{/* Token & Contract Info */}
606+
{/* Tabbed detail section: Audit Log / History / Notes */}
585607
<section className="mb-8">
586-
<h2 className="text-lg font-semibold text-white mb-3">Details</h2>
587-
<div className="bg-gray-800/40 border border-gray-700 rounded-xl divide-y divide-gray-700">
588-
<div className="flex justify-between items-center px-4 py-3">
589-
<span className="text-sm text-gray-400">Invoice ID</span>
590-
<span className="text-sm font-mono text-gray-200">#{id}</span>
591-
</div>
592-
<div className="flex justify-between items-center px-4 py-3">
593-
<span className="text-sm text-gray-400">Token</span>
594-
<span className="text-sm font-mono text-gray-200 truncate ml-4" title={invoice.token}>
595-
{truncateAddress(invoice.token, 8)}
596-
</span>
597-
</div>
598-
<div className="flex justify-between items-center px-4 py-3">
599-
<span className="text-sm text-gray-400">Deadline</span>
600-
<span className="text-sm text-gray-200">
601-
{invoice.deadline > 0
602-
? new Date(invoice.deadline * 1000).toLocaleString()
603-
: "No deadline"}
604-
</span>
605-
</div>
606-
<div className="flex justify-between items-center px-4 py-3">
607-
<span className="text-sm text-gray-400">Total Amount</span>
608-
<span className="text-sm font-semibold text-indigo-300">{formatAmount(total)} USDC</span>
609-
</div>
610-
<div className="flex justify-between items-center px-4 py-3">
611-
<span className="text-sm text-gray-400">Funded</span>
612-
<span className="text-sm font-medium text-green-400">{formatAmount(invoice.funded)} USDC</span>
613-
</div>
614-
<div className="flex justify-between items-center px-4 py-3">
615-
<span className="text-sm text-gray-400">Status</span>
616-
<span className={`text-sm font-medium ${status.color.replace("bg-", "text-")}`}>
617-
{invoice.status}
618-
</span>
619-
</div>
620-
{payerNonce !== null && (
621-
<div className="flex justify-between items-center px-4 py-3">
622-
<span className="text-sm text-gray-400">Your Nonce</span>
623-
<span className="text-sm font-mono text-gray-200">{payerNonce.toString()}</span>
624-
</div>
625-
)}
608+
<div className="flex gap-1 border-b border-gray-700 mb-4" role="tablist" aria-label="Invoice details">
609+
{(["audit", "history", "notes"] as const).map((tab) => {
610+
const labels: Record<string, string> = { audit: "Audit Log", history: "History", notes: "Notes" };
611+
return (
612+
<button
613+
key={tab}
614+
type="button"
615+
role="tab"
616+
aria-selected={activeDetailsTab === tab}
617+
onClick={() => setActiveDetailsTab(tab)}
618+
className={`px-4 py-2 text-sm font-medium transition-colors rounded-t-lg -mb-px border-b-2 ${
619+
activeDetailsTab === tab
620+
? "border-indigo-500 text-indigo-300"
621+
: "border-transparent text-gray-400 hover:text-gray-200"
622+
}`}
623+
>
624+
{labels[tab]}
625+
</button>
626+
);
627+
})}
626628
</div>
629+
{activeDetailsTab === "audit" && <AuditLogTable invoiceId={id} invoice={invoice ?? undefined} />}
630+
{activeDetailsTab === "history" && <VersionHistory invoiceId={id} />}
631+
{activeDetailsTab === "notes" && publicKey && (
632+
<CommentSection invoiceId={id} walletAddress={publicKey} />
633+
)}
627634
</section>
635+
628636

629637
{showCancelModal && (
630638
<CancelModal

0 commit comments

Comments
 (0)