refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { toast } from 'sonner';
|
|
|
|
|
import { api } from '@/api.js';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
|
import {
|
|
|
|
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
|
|
|
|
} from '@/components/ui/dialog';
|
|
|
|
|
import {
|
|
|
|
|
AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle,
|
|
|
|
|
AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction,
|
|
|
|
|
} from '@/components/ui/alert-dialog';
|
|
|
|
|
import {
|
|
|
|
|
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
|
|
|
|
|
} from '@/components/ui/select';
|
|
|
|
|
|
|
|
|
|
const METHOD_NONE = 'none';
|
|
|
|
|
|
2026-06-07 14:49:39 -05:00
|
|
|
function PaymentModal({ payment, autopayEnabled, onClose, onSave }) {
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
const [amount, setAmount] = useState(String(payment.amount));
|
|
|
|
|
const [date, setDate] = useState(payment.paid_date);
|
|
|
|
|
// Use METHOD_NONE sentinel — empty string value crashes Radix Select
|
|
|
|
|
const [method, setMethod] = useState(payment.method || METHOD_NONE);
|
|
|
|
|
const [notes, setNotes] = useState(payment.notes || '');
|
2026-06-07 14:49:39 -05:00
|
|
|
const [autopayFailure, setAutopayFailure] = useState(!!payment.autopay_failure);
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
|
|
|
|
2026-06-07 14:49:39 -05:00
|
|
|
const showAutopayFailure = autopayEnabled || method === 'autopay';
|
|
|
|
|
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
async function handleSave(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setBusy(true);
|
|
|
|
|
try {
|
|
|
|
|
await api.updatePayment(payment.id, {
|
2026-06-07 14:49:39 -05:00
|
|
|
amount: parseFloat(amount),
|
|
|
|
|
paid_date: date,
|
|
|
|
|
method: method === METHOD_NONE ? null : method,
|
|
|
|
|
notes: notes || null,
|
|
|
|
|
autopay_failure: showAutopayFailure ? (autopayFailure ? 1 : 0) : undefined,
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
});
|
|
|
|
|
toast.success('Payment saved');
|
|
|
|
|
onSave(); onClose();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err.message);
|
|
|
|
|
} finally { setBusy(false); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleDelete() {
|
|
|
|
|
setBusy(true);
|
|
|
|
|
try {
|
|
|
|
|
await api.deletePayment(payment.id);
|
|
|
|
|
toast.success('Payment moved to recovery. Bill is now marked as unpaid.', {
|
|
|
|
|
action: {
|
|
|
|
|
label: 'Undo',
|
|
|
|
|
onClick: async () => {
|
|
|
|
|
try {
|
|
|
|
|
await api.restorePayment(payment.id);
|
|
|
|
|
toast.success('Payment restored');
|
|
|
|
|
onSave();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err.message || 'Failed to restore payment');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
onSave(); onClose();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err.message);
|
|
|
|
|
} finally { setBusy(false); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Dialog open onOpenChange={v => { if (!v) onClose(); }}>
|
|
|
|
|
<DialogContent className="sm:max-w-sm border-border/60 bg-card/95 backdrop-blur-xl">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle className="text-base font-semibold tracking-tight">Edit Payment</DialogTitle>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
<form id="payment-modal-form" onSubmit={handleSave} className="space-y-4">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="pm-amount" className="text-xs uppercase tracking-wider text-muted-foreground">Amount ($) *</Label>
|
|
|
|
|
<Input id="pm-amount" type="number" min="0" step="0.01" required
|
|
|
|
|
value={amount} onChange={e => setAmount(e.target.value)}
|
|
|
|
|
className="font-mono bg-background/50 border-border/60" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="pm-date" className="text-xs uppercase tracking-wider text-muted-foreground">Paid Date *</Label>
|
|
|
|
|
<Input id="pm-date" type="date" required
|
|
|
|
|
value={date} onChange={e => setDate(e.target.value)}
|
|
|
|
|
className="font-mono bg-background/50 border-border/60" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="pm-method" className="text-xs uppercase tracking-wider text-muted-foreground">Method</Label>
|
|
|
|
|
<Select value={method} onValueChange={setMethod}>
|
|
|
|
|
<SelectTrigger id="pm-method" className="bg-background/50 border-border/60">
|
|
|
|
|
<SelectValue placeholder="—" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value={METHOD_NONE}>—</SelectItem>
|
|
|
|
|
<SelectItem value="bank">Bank Transfer</SelectItem>
|
|
|
|
|
<SelectItem value="card">Card</SelectItem>
|
|
|
|
|
<SelectItem value="autopay">Autopay</SelectItem>
|
|
|
|
|
<SelectItem value="check">Check</SelectItem>
|
|
|
|
|
<SelectItem value="cash">Cash</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="pm-notes" className="text-xs uppercase tracking-wider text-muted-foreground">Notes</Label>
|
|
|
|
|
<Input id="pm-notes" value={notes} onChange={e => setNotes(e.target.value)}
|
|
|
|
|
className="bg-background/50 border-border/60" />
|
|
|
|
|
</div>
|
2026-06-07 14:49:39 -05:00
|
|
|
{showAutopayFailure && (
|
|
|
|
|
<label className="flex items-center gap-2.5 cursor-pointer group">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={autopayFailure}
|
|
|
|
|
onChange={e => setAutopayFailure(e.target.checked)}
|
|
|
|
|
className="h-4 w-4 rounded border-border accent-amber-500"
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-sm text-muted-foreground group-hover:text-foreground transition-colors">
|
|
|
|
|
Autopay failed — paid manually
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
refactor: component splits, PWA support, CommandPalette
Component Splits:
- AdminPage.jsx: 1,906 -> 82 lines (logic moved to client/components/admin/ — 9 files)
- DataPage.jsx: 3,132 -> 60 lines (logic moved to client/components/data/ — 8 files)
- TrackerPage.jsx: 2,566 -> 2,132 lines (MonthlyStateDialog, StartingAmountsEditDialog, PaymentModal)
PWA:
- vite-plugin-pwa installed with NetworkFirst caching for API routes
- Square PWA icons (192x192, 512x512, apple-touch-icon)
- theme-color, apple meta tags, touch icon in index.html
- Build generates dist/sw.js + Workbox runtime
CommandPalette:
- Navigation commands, Add bill action, month jumps
- Grouped results with empty/filtered states
2026-05-28 20:53:22 -05:00
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<DialogFooter className="flex-row gap-2 sm:justify-between mt-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button" variant="destructive" disabled={busy} onClick={() => setConfirmDelete(true)}
|
|
|
|
|
className="text-xs"
|
|
|
|
|
title="Removes this payment record. The bill itself is NOT deleted."
|
|
|
|
|
>
|
|
|
|
|
Remove Payment
|
|
|
|
|
</Button>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button type="button" variant="ghost" disabled={busy} onClick={onClose} className="text-xs">Cancel</Button>
|
|
|
|
|
<Button type="submit" form="payment-modal-form" disabled={busy} className="text-xs">Save</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
<AlertDialog open={confirmDelete} onOpenChange={setConfirmDelete}>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>Remove this payment?</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
This marks the payment as removed and reverses any debt balance update. You can undo it from the toast.
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel disabled={busy}>Cancel</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
|
|
|
disabled={busy}
|
|
|
|
|
onClick={handleDelete}
|
|
|
|
|
>
|
|
|
|
|
{busy ? 'Removing...' : 'Remove Payment'}
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default PaymentModal;
|