Skip to content
Open
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
28 changes: 16 additions & 12 deletions src/components/CompareInvoicesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<p className="text-gray-600 dark:text-gray-400">
Invoice #{invoiceA.id} vs Invoice #{invoiceB.id}
</p>
<div className="flex items-center gap-2 mt-3 text-xs text-gray-600 dark:text-gray-400">
<span className="inline-block w-3 h-3 rounded bg-yellow-100 dark:bg-yellow-900" />
Highlighted fields differ between the two invoices
</div>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
Expand All @@ -89,7 +93,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<div
className={
isDifferent(statusA, statusB)
? "bg-yellow-200 dark:bg-yellow-900/30 px-3 py-2 rounded-lg inline-block"
? "bg-yellow-100 dark:bg-yellow-900 px-3 py-2 rounded-lg inline-block"
: ""
}
>
Expand All @@ -105,7 +109,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<p
className={`text-lg font-semibold ${
isDifferent(totalFormatA, totalFormatB)
? "bg-yellow-200 dark:bg-yellow-900/30 px-3 py-2 rounded-lg inline-block"
? "bg-yellow-100 dark:bg-yellow-900 px-3 py-2 rounded-lg inline-block"
: ""
}`}
>
Expand All @@ -121,7 +125,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<p
className={
isDifferent(fundedA, fundedB)
? "bg-yellow-200 dark:bg-yellow-900/30 px-3 py-2 rounded-lg inline-block"
? "bg-yellow-100 dark:bg-yellow-900 px-3 py-2 rounded-lg inline-block"
: ""
}
>
Expand Down Expand Up @@ -149,7 +153,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<p
className={
isDifferent(deadlineA, deadlineB)
? "bg-yellow-200 dark:bg-yellow-900/30 px-3 py-2 rounded-lg inline-block"
? "bg-yellow-100 dark:bg-yellow-900 px-3 py-2 rounded-lg inline-block"
: ""
}
>
Expand All @@ -165,7 +169,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<div
className={
isDifferent(recipientsA, recipientsB)
? "bg-yellow-200 dark:bg-yellow-900/30 p-3 rounded-lg"
? "bg-yellow-100 dark:bg-yellow-900 p-3 rounded-lg"
: ""
}
>
Expand All @@ -190,7 +194,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<p
className={
isDifferent(paymentCountA, paymentCountB)
? "bg-yellow-200 dark:bg-yellow-900/30 px-3 py-2 rounded-lg inline-block"
? "bg-yellow-100 dark:bg-yellow-900 px-3 py-2 rounded-lg inline-block"
: ""
}
>
Expand Down Expand Up @@ -223,7 +227,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<div
className={
isDifferent(statusA, statusB)
? "bg-yellow-200 dark:bg-yellow-900/30 px-3 py-2 rounded-lg inline-block"
? "bg-yellow-100 dark:bg-yellow-900 px-3 py-2 rounded-lg inline-block"
: ""
}
>
Expand All @@ -239,7 +243,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<p
className={`text-lg font-semibold ${
isDifferent(totalFormatA, totalFormatB)
? "bg-yellow-200 dark:bg-yellow-900/30 px-3 py-2 rounded-lg inline-block"
? "bg-yellow-100 dark:bg-yellow-900 px-3 py-2 rounded-lg inline-block"
: ""
}`}
>
Expand All @@ -255,7 +259,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<p
className={
isDifferent(fundedA, fundedB)
? "bg-yellow-200 dark:bg-yellow-900/30 px-3 py-2 rounded-lg inline-block"
? "bg-yellow-100 dark:bg-yellow-900 px-3 py-2 rounded-lg inline-block"
: ""
}
>
Expand Down Expand Up @@ -283,7 +287,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<p
className={
isDifferent(deadlineA, deadlineB)
? "bg-yellow-200 dark:bg-yellow-900/30 px-3 py-2 rounded-lg inline-block"
? "bg-yellow-100 dark:bg-yellow-900 px-3 py-2 rounded-lg inline-block"
: ""
}
>
Expand All @@ -299,7 +303,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<div
className={
isDifferent(recipientsA, recipientsB)
? "bg-yellow-200 dark:bg-yellow-900/30 p-3 rounded-lg"
? "bg-yellow-100 dark:bg-yellow-900 p-3 rounded-lg"
: ""
}
>
Expand All @@ -324,7 +328,7 @@ export default function CompareInvoicesView({ invoiceA, invoiceB, onPayA, onPayB
<p
className={
isDifferent(paymentCountA, paymentCountB)
? "bg-yellow-200 dark:bg-yellow-900/30 px-3 py-2 rounded-lg inline-block"
? "bg-yellow-100 dark:bg-yellow-900 px-3 py-2 rounded-lg inline-block"
: ""
}
>
Expand Down
43 changes: 41 additions & 2 deletions src/components/PaymentExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
downloadCSV,
generatePaymentExportFilename,
filterPaymentsByDateRange,
PAYMENT_EXPORT_COLUMNS,
PAYMENT_EXPORT_COLUMN_LABELS,
type PaymentExportColumn,
} from '@/lib/csvExport';

interface Props {
Expand All @@ -19,10 +22,23 @@ export default function PaymentExport({ invoiceId, payments }: Props) {
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [exporting, setExporting] = useState(false);
const [selectedColumns, setSelectedColumns] = useState<PaymentExportColumn[]>([
...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(
Expand All @@ -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);
Expand Down Expand Up @@ -134,6 +150,29 @@ export default function PaymentExport({ invoiceId, payments }: Props) {
</div>
)}

{/* Column Selection */}
{payments.length > 0 && (
<div className="border-t border-gray-800 pt-4 mt-4">
<p className="text-xs text-gray-400 mb-2">Columns to include</p>
<div className="flex flex-wrap gap-3">
{PAYMENT_EXPORT_COLUMNS.map((column) => (
<label
key={column}
className="flex items-center gap-1.5 text-xs text-gray-300"
>
<input
type="checkbox"
checked={selectedColumns.includes(column)}
onChange={() => toggleColumn(column)}
className="rounded border-gray-700 bg-gray-800 text-indigo-500 focus:ring-indigo-500"
/>
{PAYMENT_EXPORT_COLUMN_LABELS[column]}
</label>
))}
</div>
</div>
)}

{/* Empty State */}
{payments.length === 0 && (
<p className="text-sm text-gray-500 mt-2">
Expand Down
45 changes: 44 additions & 1 deletion src/components/PlanGeneratorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -83,6 +83,49 @@ export default function PlanGeneratorModal({
setSelectedPlanIndex(index);
}}
/>

{/* Live Preview Table */}
<div className="mt-8">
<h3 className="text-sm font-semibold text-gray-900 mb-3">
Installment Schedule Preview
</h3>
<div className="overflow-x-auto border border-gray-200 rounded-lg">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left font-medium text-gray-500">#</th>
<th className="px-4 py-2 text-left font-medium text-gray-500">Due Date</th>
<th className="px-4 py-2 text-right font-medium text-gray-500">Amount</th>
<th className="px-4 py-2 text-right font-medium text-gray-500">
Running Total
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{(() => {
const display = formatPlanForDisplay(selectedPlan);
let runningTotal = 0;
return selectedPlan.milestones.map((milestone, i) => {
const amount = Number(display.amounts[i]);
runningTotal += amount;
return (
<tr key={milestone.dueDate}>
<td className="px-4 py-2 text-gray-700">{i + 1}</td>
<td className="px-4 py-2 text-gray-700">{display.dueDates[i]}</td>
<td className="px-4 py-2 text-right text-gray-700">
{display.amounts[i]}
</td>
<td className="px-4 py-2 text-right text-gray-700">
{runningTotal.toFixed(2)}
</td>
</tr>
);
});
})()}
</tbody>
</table>
</div>
</div>
</div>

{/* Footer */}
Expand Down
35 changes: 33 additions & 2 deletions src/components/SubscriptionsClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<string | null>(null);
const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");

useEffect(() => {
getFreighterPublicKey()
Expand Down Expand Up @@ -154,6 +164,25 @@ export default function SubscriptionsClient() {
</div>
)}

{/* Status Filter */}
{!loading && subscriptions.length > 0 && (
<div className="flex gap-2 flex-wrap mb-6">
{STATUS_FILTERS.map((filter) => (
<button
key={filter.value}
onClick={() => setStatusFilter(filter.value)}
className={`text-xs px-3 py-1.5 rounded-full border transition-colors ${
statusFilter === filter.value
? "bg-indigo-600 border-indigo-600 text-white"
: "border-gray-700 text-gray-300 hover:bg-gray-800"
}`}
>
{filter.label}
</button>
))}
</div>
)}

{/* Subscription List */}
{loading ? (
<SubscriptionListSkeleton />
Expand All @@ -171,7 +200,9 @@ export default function SubscriptionsClient() {
</div>
) : (
<div className="flex flex-col gap-4">
{subscriptions.map((sub) => (
{subscriptions
.filter((sub) => statusFilter === "all" || sub.status === statusFilter)
.map((sub) => (
<div key={sub.id} className="flex flex-col gap-2">
<SubscriptionCard subscription={sub} />
<div className="flex gap-2 px-1">
Expand Down
45 changes: 35 additions & 10 deletions src/lib/csvExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PaymentExportColumn, string> = {
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(","));
}

Expand Down
Loading