feat(expenses): add import functionality for expenses and total pending calculation
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import { useExpensesContext } from "../ExpensesProvider";
|
||||
import { PeriodicExpensesTable } from "./PeriodicExpensesTable";
|
||||
import { PeriodicExpenseForm } from "./PeriodicExpenseForm";
|
||||
@@ -8,10 +8,11 @@ import {
|
||||
ResponsiveDialogContent,
|
||||
ResponsiveDialogHeader,
|
||||
ResponsiveDialogTitle,
|
||||
ResponsiveDialogDescription,
|
||||
} from "@/components/ui/responsive-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus } from "lucide-react";
|
||||
import type { PeriodicExpense } from "@/lib/api";
|
||||
import { Plus, Upload } from "lucide-react";
|
||||
import type { PeriodicExpense, ImportResult } from "@/lib/api";
|
||||
|
||||
export function PeriodicExpensesTabContent() {
|
||||
const {
|
||||
@@ -21,6 +22,7 @@ export function PeriodicExpensesTabContent() {
|
||||
updatePeriodicExpense,
|
||||
deletePeriodicExpense,
|
||||
generateMonthlyExpense,
|
||||
importPeriodicExpenses,
|
||||
} = useExpensesContext();
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
@@ -31,6 +33,11 @@ export function PeriodicExpensesTabContent() {
|
||||
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);
|
||||
@@ -89,10 +96,16 @@ export function PeriodicExpensesTabContent() {
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Administrá tus gastos recurrentes.
|
||||
</p>
|
||||
<Button onClick={openCreate} size="sm">
|
||||
<Plus className="size-4" />
|
||||
Nuevo
|
||||
</Button>
|
||||
<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 ? (
|
||||
@@ -127,6 +140,84 @@ export function PeriodicExpensesTabContent() {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user