feat(expenses): allow editing paid expenses (status, amountPayed, paymentDate)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DatePicker } from "@/components/ui/date-picker";
|
||||
@@ -17,6 +17,9 @@ import { useExpensesContext } from "../ExpensesProvider";
|
||||
const editSchema = z.object({
|
||||
amount: z.number().positive("El monto debe ser mayor a 0"),
|
||||
dueDate: z.date({ message: "La fecha es obligatoria" }),
|
||||
amountPayed: z.number().positive("El monto pagado debe ser mayor a 0").optional(),
|
||||
paymentDate: z.date().optional(),
|
||||
markAsPending: z.boolean(),
|
||||
});
|
||||
|
||||
type EditFormValues = z.infer<typeof editSchema>;
|
||||
@@ -35,6 +38,8 @@ export function EditExpenseDialog({
|
||||
const { updateExpense } = useExpensesContext();
|
||||
const amountInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const isPayed = expense?.status === "PAYED";
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -46,17 +51,29 @@ export function EditExpenseDialog({
|
||||
defaultValues: {
|
||||
amount: 0,
|
||||
dueDate: undefined,
|
||||
amountPayed: undefined,
|
||||
paymentDate: undefined,
|
||||
markAsPending: false,
|
||||
},
|
||||
});
|
||||
const { ref: amountRef, ...amountField } = register("amount", {
|
||||
setValueAs: (value) => Number(value),
|
||||
});
|
||||
|
||||
const markAsPending = useWatch({ control, name: "markAsPending" });
|
||||
|
||||
useEffect(() => {
|
||||
if (expense) {
|
||||
reset({
|
||||
amount: Number(expense.amount),
|
||||
dueDate: new Date(expense.dueDate),
|
||||
amountPayed: expense.amountPayed
|
||||
? Number(expense.amountPayed)
|
||||
: undefined,
|
||||
paymentDate: expense.paymentDate
|
||||
? new Date(expense.paymentDate)
|
||||
: undefined,
|
||||
markAsPending: false,
|
||||
});
|
||||
}
|
||||
}, [expense, reset]);
|
||||
@@ -79,10 +96,27 @@ export function EditExpenseDialog({
|
||||
|
||||
async function onSubmit(data: EditFormValues) {
|
||||
if (!expense) return;
|
||||
await updateExpense(expense.id, {
|
||||
amount: data.amount,
|
||||
dueDate: data.dueDate.toISOString(),
|
||||
});
|
||||
|
||||
if (isPayed && data.markAsPending) {
|
||||
await updateExpense(expense.id, {
|
||||
amount: data.amount,
|
||||
dueDate: data.dueDate.toISOString(),
|
||||
status: "PENDING",
|
||||
});
|
||||
} else if (isPayed) {
|
||||
await updateExpense(expense.id, {
|
||||
amount: data.amount,
|
||||
dueDate: data.dueDate.toISOString(),
|
||||
amountPayed: data.amountPayed!,
|
||||
paymentDate: data.paymentDate!.toISOString(),
|
||||
});
|
||||
} else {
|
||||
await updateExpense(expense.id, {
|
||||
amount: data.amount,
|
||||
dueDate: data.dueDate.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
handleClose(false);
|
||||
}
|
||||
|
||||
@@ -146,6 +180,111 @@ export function EditExpenseDialog({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isPayed && (
|
||||
<>
|
||||
<hr className="border-border" />
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<span className="text-sm font-medium">
|
||||
Información del pago
|
||||
</span>
|
||||
|
||||
{!markAsPending && (
|
||||
<>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="amountPayed" className="text-sm font-medium">
|
||||
Monto pagado
|
||||
</label>
|
||||
<input
|
||||
id="amountPayed"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
{...register("amountPayed", {
|
||||
setValueAs: (value) => Number(value),
|
||||
})}
|
||||
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.amountPayed && (
|
||||
<span className="text-xs text-destructive">
|
||||
{errors.amountPayed.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-sm font-medium">
|
||||
Fecha de pago
|
||||
</span>
|
||||
<Controller
|
||||
control={control}
|
||||
name="paymentDate"
|
||||
render={({ field }) => (
|
||||
<DatePicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder="Seleccionar fecha"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.paymentDate && (
|
||||
<span className="text-xs text-destructive">
|
||||
{errors.paymentDate.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Controller
|
||||
control={control}
|
||||
name="markAsPending"
|
||||
render={({ field }) => (
|
||||
<button
|
||||
type="button"
|
||||
role="checkbox"
|
||||
aria-checked={field.value}
|
||||
onClick={() => field.onChange(!field.value)}
|
||||
className={
|
||||
field.value
|
||||
? "flex size-4 shrink-0 items-center justify-center rounded-[4px] bg-primary text-primary-foreground ring-offset-background transition-colors"
|
||||
: "flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input bg-background ring-offset-background transition-colors hover:border-ring"
|
||||
}
|
||||
>
|
||||
{field.value && (
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2 5.5L4 7.5L8 3"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<span>Marcar como pendiente</span>
|
||||
</label>
|
||||
|
||||
{markAsPending && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Al marcar como pendiente, se borrarán el monto pagado y la
|
||||
fecha de pago.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<ResponsiveDialogFooter>
|
||||
|
||||
@@ -138,21 +138,22 @@ export function ExpensesTable({
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const expense = row.original;
|
||||
if (expense.status === "PAYED") return null;
|
||||
return (
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button variant="ghost" size="xs" onClick={() => onEdit(expense)}>
|
||||
<Pencil className="size-3.5" />
|
||||
Editar
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => onPay(expense)}
|
||||
>
|
||||
<CircleDollarSign className="size-3.5" />
|
||||
Pagar
|
||||
</Button>
|
||||
{expense.status !== "PAYED" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => onPay(expense)}
|
||||
>
|
||||
<CircleDollarSign className="size-3.5" />
|
||||
Pagar
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -233,16 +234,16 @@ export function ExpensesTable({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{expense.status !== "PAYED" && (
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={() => onEdit(expense)}
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
</Button>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={() => onEdit(expense)}
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
</Button>
|
||||
{expense.status !== "PAYED" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -252,8 +253,8 @@ export function ExpensesTable({
|
||||
<CircleDollarSign className="size-3.5" />
|
||||
Pagar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user