+
+
+
+
+
+
+ {tab === 'manual' && (
+
+
+ setManualAddr(e.target.value)}
+ onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); addManual(); } }}
+ />
+
+
+
+ )}
+
+ {tab === 'paste' && (
+
+
+ )}
+
+ {tab === 'upload' && (
+
+
fileRef.current?.click()}>
+
Click to upload a CSV file
+
Format: address,amount (amount optional for uniform mode)
+
+
+
+ )}
+
+ {recipients.length > 0 && (
+
+
{recipients.length} recipient{recipients.length !== 1 ? 's' : ''}
+ {recipients.map(r => (
+
+ {r.address}
+ {mode === 'custom' && (
+ updateAmount(r.address, e.target.value)}
+ />
+ )}
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/app/payroll/ResultsStep.tsx b/src/app/payroll/ResultsStep.tsx
new file mode 100644
index 0000000..3827d05
--- /dev/null
+++ b/src/app/payroll/ResultsStep.tsx
@@ -0,0 +1,40 @@
+'use client';
+import Link from 'next/link';
+import type { RecipientResult } from './types';
+import styles from './payroll.module.css';
+
+interface Props {
+ results: RecipientResult[];
+}
+
+export function ResultsStep({ results }: Props) {
+ const successes = results.filter(r => r.success).length;
+ const failures = results.filter(r => !r.success).length;
+
+ return (
+
+
+ {successes} succeeded, {failures} failed
+
+
+
+ {results.map(r => (
+
+ {r.success ? '✓' : '✗'}
+ {r.address}
+ {r.txHash && (
+
+ {r.txHash.slice(0, 8)}…
+
+ )}
+ {r.error && {r.error}}
+
+ ))}
+
+
+
+ View Dashboard
+
+
+ );
+}
diff --git a/src/app/payroll/ReviewStep.tsx b/src/app/payroll/ReviewStep.tsx
new file mode 100644
index 0000000..f72c0c4
--- /dev/null
+++ b/src/app/payroll/ReviewStep.tsx
@@ -0,0 +1,56 @@
+'use client';
+import type { PayrollState } from './types';
+import { calcEscrow, calcDurationDays, calcUniformPerRecipient } from '@/lib/payroll';
+import styles from './payroll.module.css';
+
+interface Props {
+ state: PayrollState;
+}
+
+export function ReviewStep({ state }: Props) {
+ const days = calcDurationDays(state.startDate, state.stopDate);
+ const total = state.mode
+ ? calcEscrow(state.mode, state.recipients, state.ratePerDay, state.startDate, state.stopDate)
+ : 0;
+
+ return (
+
+
+ Total Escrow Cost
+ {total.toFixed(2)} {state.token === 'native' ? 'XLM' : 'USDC'}
+
+
+
+ {state.recipients.length} recipient{state.recipients.length !== 1 ? 's' : ''} · {days.toFixed(1)} days · {state.mode === 'uniform' ? 'Uniform' : 'Custom'} mode
+
+
+
+
+
+
+ | Address |
+ {state.mode === 'uniform' ? 'Rate/Day' : 'Amount'} |
+ Escrow |
+
+
+
+ {state.recipients.map(r => {
+ const escrow = state.mode === 'uniform'
+ ? calcUniformPerRecipient(state.ratePerDay, state.startDate, state.stopDate)
+ : Number(r.amount || 0);
+ return (
+
+ | {r.address.slice(0, 8)}…{r.address.slice(-4)} |
+
+ {state.mode === 'uniform' ? `${Number(state.ratePerDay || 0)}/day` : r.amount}
+ |
+ {escrow.toFixed(2)} |
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/src/app/payroll/page.tsx b/src/app/payroll/page.tsx
new file mode 100644
index 0000000..e9dca15
--- /dev/null
+++ b/src/app/payroll/page.tsx
@@ -0,0 +1,175 @@
+'use client';
+import Link from 'next/link';
+import { useState } from 'react';
+import { useWallet } from '@/context/WalletContext';
+import { useToast } from '@/context/ToastContext';
+import { usePayrollForm } from '@/hooks/usePayrollForm';
+import { useKeyboardNav } from '@/hooks/useKeyboardNav';
+import { distribute, distributeCustom } from '@/lib/contracts/distributor';
+import type { RecipientResult } from './types';
+import { calcEscrow } from '@/lib/payroll';
+import { ModeStep } from './ModeStep';
+import { RecipientsStep } from './RecipientsStep';
+import { DetailsStep } from './DetailsStep';
+import { ReviewStep } from './ReviewStep';
+import { ResultsStep } from './ResultsStep';
+import { ConfirmDialog } from './ConfirmDialog';
+import styles from './payroll.module.css';
+
+const STEPS = ['Mode', 'Recipients', 'Details', 'Review', 'Results'];
+
+export default function PayrollPage() {
+ const { address } = useWallet();
+ const toast = useToast();
+ const {
+ state, errors, setMode, setRecipients, setField,
+ validateDetails, validateRecipients, totalEscrow, reset,
+ } = usePayrollForm();
+
+ const [step, setStep] = useState(0);
+ const [results, setResults] = useState