BillTracker/client/components/tracker/StartingAmountsEditDialog.jsx

198 lines
7.7 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { api } from '@/api.js';
import { fmt } from '@/lib/utils';
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';
const MONTHS = [
'January','February','March','April','May','June',
'July','August','September','October','November','December',
];
function StartingAmountsEditDialog({ open, onClose, year, month, onSave }) {
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [firstAmount, setFirstAmount] = useState('0');
const [fifteenthAmount, setFifteenthAmount] = useState('0');
const [otherAmount, setOtherAmount] = useState('0');
const [preview, setPreview] = useState(null);
const monthName = `${MONTHS[month - 1]} ${year}`;
const localFirst = Number(firstAmount) || 0;
const localFifteenth = Number(fifteenthAmount) || 0;
const localOther = Number(otherAmount) || 0;
const totalStarting = localFirst + localFifteenth + localOther;
const paidSoFar = Number(preview?.paid_total || 0);
const firstRemaining = localFirst - Number(preview?.paid_from_first || 0);
const fifteenthRemaining = localFifteenth - Number(preview?.paid_from_fifteenth || 0);
const totalRemaining = totalStarting - paidSoFar;
useEffect(() => {
let alive = true;
async function loadStartingAmounts() {
if (!open) return;
setLoading(true);
setError('');
try {
const result = await api.getMonthlyStartingAmounts(year, month);
if (!alive) return;
setPreview(result);
setFirstAmount(String(result.first_amount ?? 0));
setFifteenthAmount(String(result.fifteenth_amount ?? 0));
setOtherAmount(String(result.other_amount ?? 0));
} catch (err) {
if (!alive) return;
setError(err.message || 'Monthly starting amounts could not be loaded.');
} finally {
if (alive) setLoading(false);
}
}
loadStartingAmounts();
return () => { alive = false; };
}, [open, year, month]);
async function handleSave(e) {
e.preventDefault();
const first = Number(firstAmount);
const fifteenth = Number(fifteenthAmount);
const other = Number(otherAmount);
if (![first, fifteenth, other].every(value => Number.isFinite(value) && value >= 0)) {
setError('Starting amounts must be non-negative numbers.');
return;
}
setSaving(true);
setError('');
try {
await api.updateMonthlyStartingAmounts({
year,
month,
first_amount: first,
fifteenth_amount: fifteenth,
other_amount: other,
});
toast.success('Monthly starting amounts saved.');
onSave();
} catch (err) {
setError(err.message || 'Monthly starting amounts could not be saved.');
} finally {
setSaving(false);
}
}
return (
<Dialog open={open} onOpenChange={value => { if (!value) onClose(); }}>
<DialogContent className="max-h-[92vh] overflow-y-auto border-border/60 bg-card/95 backdrop-blur-xl sm:max-w-lg">
<DialogHeader>
<DialogTitle className="text-lg font-semibold tracking-tight">Monthly Starting Amounts</DialogTitle>
<p className="text-sm text-muted-foreground">{monthName}</p>
</DialogHeader>
<form id="starting-amounts-form" onSubmit={handleSave} className="space-y-5">
{error && (
<div className="rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error}
</div>
)}
<div className="grid gap-3 sm:grid-cols-3">
<label className="space-y-1.5">
<Label htmlFor="starting-first" className="text-xs uppercase tracking-wider text-muted-foreground">
1st
</Label>
<Input
id="starting-first"
type="number"
min="0"
step="0.01"
value={firstAmount}
disabled={loading || saving}
onChange={e => setFirstAmount(e.target.value)}
className="font-mono bg-background/50 border-border/60"
/>
</label>
<label className="space-y-1.5">
<Label htmlFor="starting-fifteenth" className="text-xs uppercase tracking-wider text-muted-foreground">
15th
</Label>
<Input
id="starting-fifteenth"
type="number"
min="0"
step="0.01"
value={fifteenthAmount}
disabled={loading || saving}
onChange={e => setFifteenthAmount(e.target.value)}
className="font-mono bg-background/50 border-border/60"
/>
</label>
<label className="space-y-1.5">
<Label htmlFor="starting-other" className="text-xs uppercase tracking-wider text-muted-foreground">
Other
</Label>
<Input
id="starting-other"
type="number"
min="0"
step="0.01"
value={otherAmount}
disabled={loading || saving}
onChange={e => setOtherAmount(e.target.value)}
className="font-mono bg-background/50 border-border/60"
/>
</label>
</div>
<div className="rounded-xl border border-border/60 bg-muted/35 p-4">
<div className="grid gap-3 sm:grid-cols-3">
<div>
<p className="text-xs font-medium text-muted-foreground">Total starting</p>
<p className="mt-1 font-mono text-lg font-bold">{fmt(totalStarting)}</p>
</div>
<div>
<p className="text-xs font-medium text-muted-foreground">Paid so far</p>
<p className="mt-1 font-mono text-lg font-bold text-emerald-500">{fmt(paidSoFar)}</p>
</div>
<div>
<p className="text-xs font-medium text-muted-foreground">Total remaining</p>
<p className="mt-1 font-mono text-lg font-bold">{fmt(totalRemaining)}</p>
</div>
</div>
<div className="mt-4 grid gap-2 border-t border-border/60 pt-3 text-sm sm:grid-cols-3">
<div className="flex justify-between gap-3 sm:block">
<span className="text-muted-foreground">1st remaining</span>
<span className="font-mono font-semibold sm:block">{fmt(firstRemaining)}</span>
</div>
<div className="flex justify-between gap-3 sm:block">
<span className="text-muted-foreground">15th remaining</span>
<span className="font-mono font-semibold sm:block">{fmt(fifteenthRemaining)}</span>
</div>
<div className="flex justify-between gap-3 sm:block">
<span className="text-muted-foreground">Other</span>
<span className="font-mono font-semibold sm:block">{fmt(localOther)}</span>
</div>
</div>
</div>
</form>
<DialogFooter className="mt-2">
<Button type="button" variant="ghost" disabled={saving} onClick={onClose} className="text-xs">
Cancel
</Button>
<Button type="submit" form="starting-amounts-form" disabled={loading || saving} className="text-xs">
{saving ? 'Saving...' : 'Save'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export default StartingAmountsEditDialog;