feat(expenses): implement responsive dialog for expense management and create expense provider
- Added ResponsiveDialog component for handling responsive dialogs in the UI. - Created ExpensesProvider to manage state and API interactions for expenses. - Developed ExpensesTabContent to display expenses with filtering options and dialogs for new and pay expense actions. - Implemented ExpensesTable for rendering expense data in a tabular format with actions. - Added dialogs for creating new expenses and paying existing ones with form validation. - Introduced PeriodicExpenseForm for managing periodic expenses with month selection. - Created PeriodicExpensesTabContent to manage and display periodic expenses with create/edit functionality. - Added GenerateCurrentMonthDialog for confirming monthly expense generation. - Implemented PeriodicExpensesTable for displaying periodic expenses with edit and delete actions.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { useState } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useExpensesContext } from "../ExpensesProvider";
|
||||
import { DatePicker } from "@/components/ui/date-picker";
|
||||
import {
|
||||
ResponsiveDialog,
|
||||
ResponsiveDialogContent,
|
||||
ResponsiveDialogHeader,
|
||||
ResponsiveDialogTitle,
|
||||
ResponsiveDialogFooter,
|
||||
} from "@/components/ui/responsive-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const newExpenseSchema = z.object({
|
||||
description: z.string().min(1, "La descripción es obligatoria").max(50, "Máximo 50 caracteres"),
|
||||
amount: z.number().positive("El monto debe ser mayor a 0"),
|
||||
dueDate: z.date({ message: "La fecha es obligatoria" }),
|
||||
});
|
||||
|
||||
type NewExpenseFormValues = z.infer<typeof newExpenseSchema>;
|
||||
|
||||
interface NewExpenseDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function NewExpenseDialog({ open, onOpenChange }: NewExpenseDialogProps) {
|
||||
const { createNonPeriodicExpense } = useExpensesContext();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
reset,
|
||||
} = useForm<NewExpenseFormValues>({
|
||||
resolver: zodResolver(newExpenseSchema),
|
||||
defaultValues: {
|
||||
description: "",
|
||||
amount: 0,
|
||||
dueDate: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
function handleClose(open: boolean) {
|
||||
onOpenChange(open);
|
||||
if (!open) reset();
|
||||
}
|
||||
|
||||
async function onSubmit(data: NewExpenseFormValues) {
|
||||
await createNonPeriodicExpense({
|
||||
description: data.description,
|
||||
amount: data.amount,
|
||||
dueDate: data.dueDate.toISOString(),
|
||||
});
|
||||
handleClose(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveDialog open={open} onOpenChange={handleClose}>
|
||||
<ResponsiveDialogContent>
|
||||
<ResponsiveDialogHeader>
|
||||
<ResponsiveDialogTitle>Nuevo gasto</ResponsiveDialogTitle>
|
||||
</ResponsiveDialogHeader>
|
||||
|
||||
<form id="new-expense-form" onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="description" className="text-sm font-medium">
|
||||
Descripción
|
||||
</label>
|
||||
<input
|
||||
id="description"
|
||||
{...register("description")}
|
||||
placeholder="Ej: Ropa"
|
||||
className="h-8 rounded-lg border border-input bg-background px-2.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
/>
|
||||
{errors.description && (
|
||||
<span className="text-xs text-destructive">{errors.description.message}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="amount" className="text-sm font-medium">
|
||||
Monto
|
||||
</label>
|
||||
<input
|
||||
id="amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
{...register("amount", { valueAsNumber: true })}
|
||||
placeholder="2500"
|
||||
className="h-8 rounded-lg border border-input bg-background px-2.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
/>
|
||||
{errors.amount && (
|
||||
<span className="text-xs text-destructive">{errors.amount.message}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium">
|
||||
Fecha del gasto
|
||||
</label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="dueDate"
|
||||
render={({ field }) => (
|
||||
<DatePicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder="Seleccionar fecha"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.dueDate && (
|
||||
<span className="text-xs text-destructive">{errors.dueDate.message}</span>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ResponsiveDialogFooter>
|
||||
<Button variant="outline" onClick={() => handleClose(false)} disabled={isSubmitting}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" form="new-expense-form" disabled={isSubmitting}>
|
||||
Crear gasto
|
||||
</Button>
|
||||
</ResponsiveDialogFooter>
|
||||
</ResponsiveDialogContent>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user