mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 08:28:48 +02:00
Repoints imports at the relocated lib/api and src/contexts modules, and refreshes Layout + Login alongside the rest of the migration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
310 lines
10 KiB
TypeScript
310 lines
10 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import {
|
|
type RecurringItem,
|
|
type RecurringItemCreate,
|
|
type RecurringItemUpdate,
|
|
type RecurringItemType,
|
|
type RecurringFrequency,
|
|
} from '@/lib/api';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Plus, Trash2 } from 'lucide-react';
|
|
|
|
const TYPE_OPTIONS: { value: RecurringItemType; label: string }[] = [
|
|
{ value: 'INCOME', label: 'Ingreso' },
|
|
{ value: 'EXPENSE', label: 'Egreso' },
|
|
];
|
|
|
|
const FREQ_OPTIONS: { value: RecurringFrequency; label: string }[] = [
|
|
{ value: 'WEEKLY', label: 'Semanal' },
|
|
{ value: 'MONTHLY', label: 'Mensual' },
|
|
{ value: 'QUARTERLY', label: 'Trimestral' },
|
|
{ value: 'BIANNUAL', label: 'Semestral' },
|
|
{ value: 'YEARLY', label: 'Anual' },
|
|
];
|
|
|
|
const MONTH_LABELS = [
|
|
'', 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
|
|
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre',
|
|
];
|
|
|
|
interface RecurringItemDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
item?: RecurringItem | null;
|
|
onSave: (data: RecurringItemCreate | RecurringItemUpdate) => Promise<void>;
|
|
}
|
|
|
|
export default function RecurringItemDialog({
|
|
open,
|
|
onOpenChange,
|
|
item,
|
|
onSave,
|
|
}: RecurringItemDialogProps) {
|
|
const isEdit = !!item;
|
|
|
|
const [name, setName] = useState('');
|
|
const [amount, setAmount] = useState('');
|
|
const [itemType, setItemType] = useState<RecurringItemType>('EXPENSE');
|
|
const [frequency, setFrequency] = useState<RecurringFrequency>('MONTHLY');
|
|
const [dayOfMonth, setDayOfMonth] = useState('');
|
|
const [monthOfYear, setMonthOfYear] = useState('');
|
|
const [overrides, setOverrides] = useState<{ month: string; amount: string }[]>([]);
|
|
const [notes, setNotes] = useState('');
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
if (item) {
|
|
setName(item.name);
|
|
setAmount(String(item.amount));
|
|
setItemType(item.item_type);
|
|
setFrequency(item.frequency);
|
|
setDayOfMonth(item.day_of_month != null ? String(item.day_of_month) : '');
|
|
setMonthOfYear(item.month_of_year != null ? String(item.month_of_year) : '');
|
|
setOverrides(
|
|
item.override_amounts
|
|
? Object.entries(item.override_amounts).map(([m, a]) => ({
|
|
month: m,
|
|
amount: String(a),
|
|
}))
|
|
: [],
|
|
);
|
|
setNotes(item.notes || '');
|
|
} else {
|
|
setName('');
|
|
setAmount('');
|
|
setItemType('EXPENSE');
|
|
setFrequency('MONTHLY');
|
|
setDayOfMonth('');
|
|
setMonthOfYear('');
|
|
setOverrides([]);
|
|
setNotes('');
|
|
}
|
|
}
|
|
}, [open, item]);
|
|
|
|
const showDayOfMonth = frequency === 'MONTHLY' || frequency === 'WEEKLY';
|
|
const showMonthOfYear = frequency === 'YEARLY' || frequency === 'BIANNUAL';
|
|
const showOverrides = frequency === 'MONTHLY';
|
|
|
|
const handleSubmit = async () => {
|
|
setSaving(true);
|
|
try {
|
|
const overrideAmounts =
|
|
overrides.length > 0
|
|
? Object.fromEntries(
|
|
overrides
|
|
.filter((o) => o.month && o.amount)
|
|
.map((o) => [o.month, parseFloat(o.amount)]),
|
|
)
|
|
: null;
|
|
|
|
const data = {
|
|
name,
|
|
amount: parseFloat(amount),
|
|
item_type: itemType,
|
|
frequency,
|
|
day_of_month: dayOfMonth ? parseInt(dayOfMonth) : null,
|
|
month_of_year: monthOfYear ? parseInt(monthOfYear) : null,
|
|
override_amounts: overrideAmounts,
|
|
notes: notes || null,
|
|
};
|
|
|
|
await onSave(data);
|
|
onOpenChange(false);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{isEdit ? 'Editar' : 'Nuevo'} Item Recurrente</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-2">
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="ri-name">Nombre</Label>
|
|
<Input id="ri-name" value={name} onChange={(e) => setName(e.target.value)} />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="ri-amount">Monto (CRC)</Label>
|
|
<Input
|
|
id="ri-amount"
|
|
type="number"
|
|
value={amount}
|
|
onChange={(e) => setAmount(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Tipo</Label>
|
|
<Select value={itemType} onValueChange={(v) => v && setItemType(v as RecurringItemType)}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{TYPE_OPTIONS.map((o) => (
|
|
<SelectItem key={o.value} value={o.value}>
|
|
{o.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1.5">
|
|
<Label>Frecuencia</Label>
|
|
<Select value={frequency} onValueChange={(v) => v && setFrequency(v as RecurringFrequency)}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{FREQ_OPTIONS.map((o) => (
|
|
<SelectItem key={o.value} value={o.value}>
|
|
{o.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{showDayOfMonth && (
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="ri-day">
|
|
{frequency === 'WEEKLY' ? 'Día de semana (0=Lun)' : 'Día del mes'}
|
|
</Label>
|
|
<Input
|
|
id="ri-day"
|
|
type="number"
|
|
value={dayOfMonth}
|
|
onChange={(e) => setDayOfMonth(e.target.value)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{showMonthOfYear && (
|
|
<div className="space-y-1.5">
|
|
<Label>Mes</Label>
|
|
<Select value={monthOfYear} onValueChange={(v) => v && setMonthOfYear(v)}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Seleccionar" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Array.from({ length: 12 }, (_, i) => i + 1).map((m) => (
|
|
<SelectItem key={m} value={String(m)}>
|
|
{MONTH_LABELS[m]}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{showOverrides && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs text-muted-foreground">
|
|
Montos por mes (sobreescrituras)
|
|
</Label>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setOverrides([...overrides, { month: '', amount: '' }])}
|
|
>
|
|
<Plus className="w-3 h-3 mr-1" />
|
|
Agregar
|
|
</Button>
|
|
</div>
|
|
{overrides.map((o, idx) => (
|
|
<div key={idx} className="flex items-center gap-2">
|
|
<Select
|
|
value={o.month}
|
|
onValueChange={(v) => {
|
|
if (!v) return;
|
|
const next = [...overrides];
|
|
next[idx].month = v;
|
|
setOverrides(next);
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-28">
|
|
<SelectValue placeholder="Mes" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Array.from({ length: 12 }, (_, i) => i + 1).map((m) => (
|
|
<SelectItem key={m} value={String(m)}>
|
|
{MONTH_LABELS[m]}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Input
|
|
type="number"
|
|
placeholder="Monto"
|
|
value={o.amount}
|
|
onChange={(e) => {
|
|
const next = [...overrides];
|
|
next[idx].amount = e.target.value;
|
|
setOverrides(next);
|
|
}}
|
|
className="flex-1"
|
|
/>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setOverrides(overrides.filter((_, i) => i !== idx))}
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="ri-notes">Notas</Label>
|
|
<Textarea
|
|
id="ri-notes"
|
|
value={notes}
|
|
onChange={(e) => setNotes(e.target.value)}
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
Cancelar
|
|
</Button>
|
|
<Button onClick={handleSubmit} disabled={saving || !name || !amount}>
|
|
{saving ? 'Guardando...' : isEdit ? 'Guardar' : 'Crear'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|