198 lines
7.7 KiB
JavaScript
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;
|