Skip to content
Merged
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
42 changes: 42 additions & 0 deletions src/app/payroll/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client';
import styles from './payroll.module.css';

interface Props {
total: number;
token: string;
recipientCount: number;
onConfirm: () => void;
onCancel: () => void;
submitting: boolean;
}

export function ConfirmDialog({ total, token, recipientCount, onConfirm, onCancel, submitting }: Props) {
return (
<div style={{
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 1000,
}}>
<div style={{
background: 'var(--surface)', border: '1px solid var(--border)',
borderRadius: 'var(--radius)', padding: '2rem', maxWidth: '400px',
width: '90%', textAlign: 'center',
}}>
<h3 style={{ marginBottom: '1rem' }}>Confirm Batch Payment</h3>
<p style={{ color: 'var(--text-muted)', marginBottom: '1.5rem', fontSize: '0.9rem' }}>
You are about to send <strong style={{ color: 'var(--accent)' }}>{total.toFixed(2)} {token === 'native' ? 'XLM' : 'USDC'}</strong> to{' '}
<strong>{recipientCount}</strong> recipient{recipientCount !== 1 ? 's' : ''}.
</p>
<p style={{ color: 'var(--text-muted)', fontSize: '0.8rem', marginBottom: '1.5rem' }}>
This action cannot be undone. Please confirm in your Freighter wallet.
</p>
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center' }}>
<button className={styles.backBtn} onClick={onCancel} disabled={submitting}>Cancel</button>
<button className={styles.submit} onClick={onConfirm} disabled={submitting}>
{submitting ? 'Submitting…' : 'Confirm'}
</button>
</div>
</div>
</div>
);
}
78 changes: 78 additions & 0 deletions src/app/payroll/CsvPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client';
import type { Recipient } from './types';
import { ADDRESS_RE } from './types';
import styles from './payroll.module.css';

interface Props {
text: string;
mode: 'uniform' | 'custom';
onAdd: (recipients: Recipient[]) => void;
onCancel: () => void;
}

function parsePreview(text: string, mode: 'uniform' | 'custom'): { valid: Recipient[]; invalid: string[] } {
const lines = text.split('\n').filter(l => l.trim());
const valid: Recipient[] = [];
const invalid: string[] = [];

for (const line of lines) {
const parts = line.split(',').map(p => p.trim());
const addr = parts[0];
if (!ADDRESS_RE.test(addr)) {
if (addr) invalid.push(addr);
continue;
}
const amount = mode === 'custom' ? (parts[1] ?? '') : '';
valid.push({ address: addr, amount });
}

return { valid, invalid };
}

export function CsvPreview({ text, mode, onAdd, onCancel }: Props) {
const { valid, invalid } = parsePreview(text, mode);

return (
<div style={{
background: 'var(--surface)', border: '1px solid var(--border)',
borderRadius: 'var(--radius)', padding: '1rem', marginTop: '0.75rem',
}}>
<p style={{ fontSize: '0.85rem', marginBottom: '0.5rem' }}>
Found <strong style={{ color: 'var(--success)' }}>{valid.length}</strong> valid address{valid.length !== 1 ? 'es' : ''}
{invalid.length > 0 && (
<> and <strong style={{ color: 'var(--danger)' }}>{invalid.length}</strong> invalid</>
)}
</p>

{valid.length > 0 && (
<div style={{ maxHeight: '120px', overflowY: 'auto', marginBottom: '0.75rem', fontSize: '0.8rem' }}>
{valid.map(r => (
<div key={r.address} style={{ fontFamily: 'monospace', color: 'var(--text-muted)', padding: '0.15rem 0' }}>
{r.address}{mode === 'custom' && r.amount ? ` — ${r.amount}` : ''}
</div>
))}
</div>
)}

{invalid.length > 0 && (
<div style={{ marginBottom: '0.75rem' }}>
<p style={{ fontSize: '0.75rem', color: 'var(--danger)', marginBottom: '0.25rem' }}>Skipped:</p>
{invalid.map(addr => (
<div key={addr} style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontFamily: 'monospace' }}>
{addr}
</div>
))}
</div>
)}

<div style={{ display: 'flex', gap: '0.5rem' }}>
<button type="button" className={styles.addBtn} onClick={() => onAdd(valid)} disabled={valid.length === 0}>
Add {valid.length} Recipient{valid.length !== 1 ? 's' : ''}
</button>
<button type="button" className={styles.backBtn} onClick={onCancel} style={{ fontSize: '0.8rem', padding: '0.5rem 1rem' }}>
Cancel
</button>
</div>
</div>
);
}
66 changes: 66 additions & 0 deletions src/app/payroll/DetailsStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use client';
import type { PayrollMode } from './types';
import styles from './payroll.module.css';

interface Props {
mode: PayrollMode;
token: string;
ratePerDay: string;
startDate: string;
stopDate: string;
errors: Partial<Record<string, string>>;
onChange: (field: string, value: string) => void;
}

export function DetailsStep({ mode, token, ratePerDay, startDate, stopDate, errors, onChange }: Props) {
return (
<div className={styles.form}>
<label className={styles.field}>
<span>Token</span>
<select className={styles.input} value={token} onChange={e => onChange('token', e.target.value)}>
<option value="native">XLM (Native)</option>
<option value="usdc">USDC</option>
</select>
</label>

{mode === 'uniform' && (
<label className={styles.field}>
<span>Rate per day (for all recipients)</span>
<input
className={errors.ratePerDay ? styles.inputError : styles.input}
type="number"
min="0"
step="0.01"
placeholder="e.g. 10"
value={ratePerDay}
onChange={e => onChange('ratePerDay', e.target.value)}
/>
{errors.ratePerDay && <span className={styles.error}>{errors.ratePerDay}</span>}
</label>
)}

<div className={styles.dateRow}>
<label className={styles.field}>
<span>Start Date</span>
<input
className={errors.startDate ? styles.inputError : styles.input}
type="datetime-local"
value={startDate}
onChange={e => onChange('startDate', e.target.value)}
/>
{errors.startDate && <span className={styles.error}>{errors.startDate}</span>}
</label>
<label className={styles.field}>
<span>End Date</span>
<input
className={errors.stopDate ? styles.inputError : styles.input}
type="datetime-local"
value={stopDate}
onChange={e => onChange('stopDate', e.target.value)}
/>
{errors.stopDate && <span className={styles.error}>{errors.stopDate}</span>}
</label>
</div>
</div>
);
}
26 changes: 26 additions & 0 deletions src/app/payroll/ErrorSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client';
import styles from './payroll.module.css';

interface Props {
errors: string[];
}

export function ErrorSummary({ errors }: Props) {
if (errors.length === 0) return null;

return (
<div style={{
background: 'var(--danger)', opacity: 0.1, border: '1px solid var(--danger)',
borderRadius: 'var(--radius)', padding: '0.75rem 1rem', marginBottom: '1rem',
}}>
<p style={{ fontSize: '0.85rem', color: 'var(--danger)', fontWeight: 600, marginBottom: '0.25rem' }}>
Please fix {errors.length} issue{errors.length !== 1 ? 's' : ''}:
</p>
{errors.map((err, i) => (
<p key={i} style={{ fontSize: '0.8rem', color: 'var(--danger)', margin: '0.15rem 0' }}>
{err}
</p>
))}
</div>
);
}
33 changes: 33 additions & 0 deletions src/app/payroll/ModeStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';
import type { PayrollMode } from './types';
import styles from './payroll.module.css';

interface Props {
selected: PayrollMode | null;
onSelect: (mode: PayrollMode) => void;
}

export function ModeStep({ selected, onSelect }: Props) {
return (
<div>
<div className={styles.modeGrid}>
<button
type="button"
className={`${styles.modeCard} ${selected === 'uniform' ? styles.modeCardSelected : ''}`}
onClick={() => onSelect('uniform')}
>
<h3>Uniform Rate</h3>
<p>Same rate for every recipient</p>
</button>
<button
type="button"
className={`${styles.modeCard} ${selected === 'custom' ? styles.modeCardSelected : ''}`}
onClick={() => onSelect('custom')}
>
<h3>Custom Amounts</h3>
<p>Different amount per person</p>
</button>
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions src/app/payroll/RecipientBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
interface Props {
count: number;
}

export function RecipientBadge({ count }: Props) {
if (count === 0) return null;

return (
<span style={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
background: 'var(--accent)', color: '#fff', borderRadius: '999px',
fontSize: '0.7rem', fontWeight: 700, padding: '0.1rem 0.5rem',
marginLeft: '0.4rem', minWidth: '1.2rem',
}}>
{count}
</span>
);
}
Loading
Loading