diff --git a/src/components/CompareInvoicesView.tsx b/src/components/CompareInvoicesView.tsx index 0a68a96..ac7e2e3 100644 --- a/src/components/CompareInvoicesView.tsx +++ b/src/components/CompareInvoicesView.tsx @@ -63,6 +63,10 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB

Invoice #{invoiceA.id} vs Invoice #{invoiceB.id}

+
+ + Highlighted fields differ between the two invoices +
@@ -89,7 +93,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
@@ -105,7 +109,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB

@@ -121,7 +125,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB

@@ -149,7 +153,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB

@@ -165,7 +169,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB

@@ -190,7 +194,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB

@@ -223,7 +227,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB

@@ -239,7 +243,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB

@@ -255,7 +259,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB

@@ -283,7 +287,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB

@@ -299,7 +303,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB

@@ -324,7 +328,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB

diff --git a/src/components/PaymentExport.tsx b/src/components/PaymentExport.tsx index 4b1d084..8767eab 100644 --- a/src/components/PaymentExport.tsx +++ b/src/components/PaymentExport.tsx @@ -7,6 +7,9 @@ import { downloadCSV, generatePaymentExportFilename, filterPaymentsByDateRange, + PAYMENT_EXPORT_COLUMNS, + PAYMENT_EXPORT_COLUMN_LABELS, + type PaymentExportColumn, } from '@/lib/csvExport'; interface Props { @@ -19,10 +22,23 @@ export default function PaymentExport({ invoiceId, payments }: Props) { const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [exporting, setExporting] = useState(false); + const [selectedColumns, setSelectedColumns] = useState([ + ...PAYMENT_EXPORT_COLUMNS, + ]); + + const toggleColumn = (column: PaymentExportColumn) => { + setSelectedColumns((current) => { + if (current.includes(column)) { + if (current.length <= 1) return current; + return current.filter((c) => c !== column); + } + return [...current, column]; + }); + }; const handleExport = () => { setExporting(true); - + try { // Filter payments by date range if specified const filteredPayments = filterPaymentsByDateRange( @@ -32,7 +48,7 @@ export default function PaymentExport({ invoiceId, payments }: Props) { ); // Convert to CSV - const csv = paymentsToCSV(filteredPayments); + const csv = paymentsToCSV(filteredPayments, selectedColumns); // Download const filename = generatePaymentExportFilename(invoiceId); @@ -134,6 +150,29 @@ export default function PaymentExport({ invoiceId, payments }: Props) {

)} + {/* Column Selection */} + {payments.length > 0 && ( +
+

Columns to include

+
+ {PAYMENT_EXPORT_COLUMNS.map((column) => ( + + ))} +
+
+ )} + {/* Empty State */} {payments.length === 0 && (

diff --git a/src/components/PlanGeneratorModal.tsx b/src/components/PlanGeneratorModal.tsx index 33f49a7..030ba56 100644 --- a/src/components/PlanGeneratorModal.tsx +++ b/src/components/PlanGeneratorModal.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import type { PaymentPlan } from '@/lib/planCalculator'; -import { generateSuggestedPlans } from '@/lib/planCalculator'; +import { generateSuggestedPlans, formatPlanForDisplay } from '@/lib/planCalculator'; import PaymentPlanComparison from './PaymentPlanComparison'; interface PlanGeneratorModalProps { @@ -83,6 +83,49 @@ export default function PlanGeneratorModal({ setSelectedPlanIndex(index); }} /> + + {/* Live Preview Table */} +

+

+ Installment Schedule Preview +

+
+ + + + + + + + + + + {(() => { + const display = formatPlanForDisplay(selectedPlan); + let runningTotal = 0; + return selectedPlan.milestones.map((milestone, i) => { + const amount = Number(display.amounts[i]); + runningTotal += amount; + return ( + + + + + + + ); + }); + })()} + +
#Due DateAmount + Running Total +
{i + 1}{display.dueDates[i]} + {display.amounts[i]} + + {runningTotal.toFixed(2)} +
+
+
{/* Footer */} diff --git a/src/components/SubscriptionsClient.tsx b/src/components/SubscriptionsClient.tsx index dbaafc1..983cceb 100644 --- a/src/components/SubscriptionsClient.tsx +++ b/src/components/SubscriptionsClient.tsx @@ -7,7 +7,7 @@ import { splitClient } from "@/lib/stellar"; import { getFreighterPublicKey } from "@/lib/freighter"; import { SubscriptionListSkeleton } from "@/components/Skeleton"; import SubscriptionCard from "@/components/SubscriptionCard"; -import type { Subscription } from "@/types/subscription"; +import type { Subscription, SubscriptionStatus } from "@/types/subscription"; import { loadSubscriptions, saveSubscriptions, @@ -18,12 +18,22 @@ import { const POLL_INTERVAL_MS = 30_000; +type StatusFilter = "all" | SubscriptionStatus; + +const STATUS_FILTERS: { value: StatusFilter; label: string }[] = [ + { value: "all", label: "All" }, + { value: "active", label: "Active" }, + { value: "paused", label: "Paused" }, + { value: "cancelled", label: "Cancelled" }, +]; + export default function SubscriptionsClient() { const router = useRouter(); const [publicKey, setPublicKey] = useState(null); const [subscriptions, setSubscriptions] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [statusFilter, setStatusFilter] = useState("all"); useEffect(() => { getFreighterPublicKey() @@ -154,6 +164,25 @@ export default function SubscriptionsClient() {
)} + {/* Status Filter */} + {!loading && subscriptions.length > 0 && ( +
+ {STATUS_FILTERS.map((filter) => ( + + ))} +
+ )} + {/* Subscription List */} {loading ? ( @@ -171,7 +200,9 @@ export default function SubscriptionsClient() {
) : (
- {subscriptions.map((sub) => ( + {subscriptions + .filter((sub) => statusFilter === "all" || sub.status === statusFilter) + .map((sub) => (
diff --git a/src/lib/csvExport.ts b/src/lib/csvExport.ts index bce7512..2222445 100644 --- a/src/lib/csvExport.ts +++ b/src/lib/csvExport.ts @@ -14,22 +14,47 @@ export interface PaymentExportRow { tx_hash: string; } +export const PAYMENT_EXPORT_COLUMNS = [ + "payer_address", + "amount_usdc", + "timestamp", + "tx_hash", +] as const; + +export type PaymentExportColumn = (typeof PAYMENT_EXPORT_COLUMNS)[number]; + +export const PAYMENT_EXPORT_COLUMN_LABELS: Record = { + payer_address: "Payer Address", + amount_usdc: "Amount (USDC)", + timestamp: "Timestamp", + tx_hash: "Transaction Hash", +}; + +const PAYMENT_FIELD_GETTERS: Record< + PaymentExportColumn, + (payment: PaymentWithMeta) => string +> = { + payer_address: (payment) => escapeCSVField(payment.payer), + amount_usdc: (payment) => formatAmount(payment.amount), + timestamp: (payment) => formatTimestamp(payment.timestamp), + tx_hash: (payment) => escapeCSVField(payment.txHash || ""), +}; + /** * Convert payments array to CSV string */ -export function paymentsToCSV(payments: PaymentWithMeta[]): string { - // CSV header - const headers = ["payer_address", "amount_usdc", "timestamp", "tx_hash"]; - const rows = [headers.join(",")]; +export function paymentsToCSV( + payments: PaymentWithMeta[], + columns: readonly PaymentExportColumn[] = PAYMENT_EXPORT_COLUMNS +): string { + const activeColumns = columns.length > 0 ? columns : PAYMENT_EXPORT_COLUMNS; + const rows = [activeColumns.join(",")]; // Add data rows for (const payment of payments) { - const row = [ - escapeCSVField(payment.payer), - formatAmount(payment.amount), - formatTimestamp(payment.timestamp), - escapeCSVField(payment.txHash || ""), - ]; + const row = activeColumns.map((column) => + PAYMENT_FIELD_GETTERS[column](payment) + ); rows.push(row.join(",")); }