Files
personal-admin-2026/apps/frontend/src/features/expenses/components/PeriodicExpensesTabContent.tsx

224 lines
7.9 KiB
TypeScript

import { useState, useRef } from "react";
import { useExpensesContext } from "../ExpensesProvider";
import { PeriodicExpensesTable } from "./PeriodicExpensesTable";
import { PeriodicExpenseForm } from "./PeriodicExpenseForm";
import { GenerateCurrentMonthDialog } from "./GenerateCurrentMonthDialog";
import {
ResponsiveDialog,
ResponsiveDialogContent,
ResponsiveDialogHeader,
ResponsiveDialogTitle,
ResponsiveDialogDescription,
} from "@/components/ui/responsive-dialog";
import { Button } from "@/components/ui/button";
import { Plus, Upload } from "lucide-react";
import type { PeriodicExpense, ImportResult } from "@/lib/api";
export function PeriodicExpensesTabContent() {
const {
periodicExpenses,
isLoadingPeriodic,
createPeriodicExpense,
updatePeriodicExpense,
deletePeriodicExpense,
generateMonthlyExpense,
importPeriodicExpenses,
} = useExpensesContext();
const [dialogOpen, setDialogOpen] = useState(false);
const [editingItem, setEditingItem] = useState<PeriodicExpense | null>(null);
const [generateDialog, setGenerateDialog] = useState<{
open: boolean;
description: string;
periodicExpenseId: number;
}>({ open: false, description: "", periodicExpenseId: 0 });
const [importDialogOpen, setImportDialogOpen] = useState(false);
const [importing, setImporting] = useState(false);
const [importResult, setImportResult] = useState<ImportResult | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
function openCreate() {
setEditingItem(null);
setDialogOpen(true);
}
function openEdit(item: PeriodicExpense) {
setEditingItem(item);
setDialogOpen(true);
}
function closeDialog() {
setDialogOpen(false);
setEditingItem(null);
}
async function handleSubmit(data: { description: string; defaultDueDay: number; defaultAmount: number; periods: number[] }) {
if (editingItem) {
await updatePeriodicExpense(editingItem.id, data);
} else {
const numericData = {
description: data.description,
defaultDueDay: data.defaultDueDay,
defaultAmount: data.defaultAmount,
periods: data.periods,
};
const result = await createPeriodicExpense(numericData);
if (result.currentMonthApplicable) {
setGenerateDialog({
open: true,
description: result.description,
periodicExpenseId: result.id,
});
}
}
closeDialog();
}
async function handleConfirmGenerate() {
await generateMonthlyExpense(generateDialog.periodicExpenseId);
setGenerateDialog({ open: false, description: "", periodicExpenseId: 0 });
}
function handleSkipGenerate() {
setGenerateDialog({ open: false, description: "", periodicExpenseId: 0 });
}
async function handleDelete(id: number) {
if (confirm("¿Eliminar este gasto periódico?")) {
await deletePeriodicExpense(id);
}
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Administrá tus gastos recurrentes.
</p>
<div className="flex gap-2">
<Button onClick={() => setImportDialogOpen(true)} variant="outline" size="sm">
<Upload className="size-4" />
Importar
</Button>
<Button onClick={openCreate} size="sm">
<Plus className="size-4" />
Nuevo
</Button>
</div>
</div>
{isLoadingPeriodic ? (
<p className="text-sm text-muted-foreground">Cargando...</p>
) : (
<PeriodicExpensesTable
data={periodicExpenses}
onEdit={openEdit}
onDelete={handleDelete}
/>
)}
<ResponsiveDialog open={dialogOpen} onOpenChange={setDialogOpen}>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>
{editingItem ? "Editar gasto periódico" : "Nuevo gasto periódico"}
</ResponsiveDialogTitle>
</ResponsiveDialogHeader>
<PeriodicExpenseForm
initialData={editingItem ?? undefined}
onSubmit={handleSubmit}
onCancel={closeDialog}
/>
</ResponsiveDialogContent>
</ResponsiveDialog>
<GenerateCurrentMonthDialog
open={generateDialog.open}
onOpenChange={(open) => setGenerateDialog((prev) => ({ ...prev, open }))}
description={generateDialog.description}
onConfirm={handleConfirmGenerate}
onSkip={handleSkipGenerate}
/>
<ResponsiveDialog open={importDialogOpen} onOpenChange={(open) => {
setImportDialogOpen(open);
if (!open) {
setImportResult(null);
}
}}>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Importar gastos periódicos</ResponsiveDialogTitle>
<ResponsiveDialogDescription>
Seleccioná un archivo CSV (delimitado por pipes) para importar gastos periódicos.
</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
{importResult ? (
<div className="space-y-3 py-4">
<p className="text-sm font-medium">Importación completada</p>
<ul className="space-y-1 text-sm">
<li className="flex items-center gap-2">
<span className="size-2 rounded-full bg-green-500" />
Importados: {importResult.imported}
</li>
<li className="flex items-center gap-2">
<span className="size-2 rounded-full bg-yellow-500" />
Omitidos (sin períodos): {importResult.skippedNoPeriods}
</li>
<li className="flex items-center gap-2">
<span className="size-2 rounded-full bg-yellow-500" />
Omitidos (duplicados): {importResult.skippedDuplicate}
</li>
</ul>
{importResult.errors && importResult.errors.length > 0 && (
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3">
<p className="mb-1 text-xs font-medium text-destructive">Errores:</p>
<ul className="list-inside list-disc text-xs text-destructive/80">
{importResult.errors.map((err, i) => (
<li key={i}>{err}</li>
))}
</ul>
</div>
)}
<Button className="w-full" onClick={() => setImportDialogOpen(false)}>
Cerrar
</Button>
</div>
) : (
<div className="space-y-4 py-4">
<input
ref={fileInputRef}
type="file"
accept=".csv,.txt"
className="block w-full text-sm text-muted-foreground file:mr-3 file:rounded-md file:border-0 file:bg-primary file:px-3 file:py-1.5 file:text-xs file:text-primary-foreground hover:file:bg-primary/90"
/>
<Button
className="w-full"
disabled={importing}
onClick={async () => {
const file = fileInputRef.current?.files?.[0];
if (!file) return;
setImporting(true);
try {
const result = await importPeriodicExpenses(file);
setImportResult(result);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
setImportResult({ imported: 0, skipped: 0, skippedNoPeriods: 0, skippedDuplicate: 0, errors: [message] });
} finally {
setImporting(false);
}
}}
>
{importing ? "Importando..." : "Importar"}
</Button>
</div>
)}
</ResponsiveDialogContent>
</ResponsiveDialog>
</div>
);
}