Uecko_ERP/modules/customer-invoices/src/web/pages/create/create-customer-invoice-edit-form.tsx
2025-11-14 16:48:09 +01:00

875 lines
29 KiB
TypeScript

import { CustomerModalSelector } from "@erp/customers/components";
import { DevTool } from "@hookform/devtools";
import { zodResolver } from "@hookform/resolvers/zod";
import { TextAreaField, TextField } from "@repo/rdx-ui/components";
import {
Button,
Calendar,
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Separator,
Textarea,
} from "@repo/shadcn-ui/components";
import { format } from "date-fns";
import { es } from "date-fns/locale";
import { CalendarIcon, PlusIcon, Save, Trash2Icon, X } from "lucide-react";
import { useFieldArray, useForm } from "react-hook-form";
import * as z from "zod";
import { useTranslation } from "../../i18n";
import { CustomerInvoicePricesCard } from "../../shared/ui/components";
import { CustomerInvoiceItemsCardEditor } from "../../shared/ui/components/items";
import type { CustomerInvoiceData } from "./customer-invoice.schema";
import { formatCurrency } from "./utils";
const invoiceFormSchema = z.object({
id: z.string(),
invoice_status: z.string(),
invoice_number: z.string().min(1, "Número de factura requerido"),
invoice_series: z.string().min(1, "Serie requerida"),
invoice_date: z.string(),
operation_date: z.string(),
language_code: z.string(),
currency: z.string(),
customer_id: z.string().min(1, "ID de cliente requerido"),
items: z
.array(
z.object({
description: z.string().optional(),
quantity: z
.object({
amount: z.number().nullable(),
scale: z.number(),
})
.optional(),
unit_price: z
.object({
amount: z.number().nullable(),
scale: z.number(),
currency_code: z.string(),
})
.optional(),
subtotal_price: z
.object({
amount: z.number().nullable(),
scale: z.number(),
currency_code: z.string(),
})
.optional(),
discount: z
.object({
amount: z.number().min(0).max(100).nullable(),
scale: z.number(),
})
.optional(),
discount_price: z
.object({
amount: z.number().nullable(),
scale: z.number(),
currency_code: z.string(),
})
.optional(),
total_price: z
.object({
amount: z.number().nullable(),
scale: z.number(),
currency_code: z.string(),
})
.optional(),
})
)
.min(1, "Al menos un item es requerido"),
subtotal_price: z.object({
amount: z.number().nullable(),
scale: z.number(),
currency_code: z.string(),
}),
discount: z.object({
amount: z.number().nullable(),
scale: z.number(),
}),
discount_price: z.object({
amount: z.number().nullable(),
scale: z.number(),
currency_code: z.string(),
}),
before_tax_price: z.object({
amount: z.number().nullable(),
scale: z.number(),
currency_code: z.string(),
}),
tax: z.object({
amount: z.number().nullable(),
scale: z.number(),
}),
tax_price: z.object({
amount: z.number().nullable(),
scale: z.number(),
currency_code: z.string(),
}),
total_price: z.object({
amount: z.number().nullable(),
scale: z.number(),
currency_code: z.string(),
}),
metadata: z.object({
entity: z.string(),
}),
});
const defaultInvoiceData = {
id: "34ae34af-1ffc-4de5-b0a8-c2cf203ef011",
invoice_status: "draft",
invoice_number: "1",
invoice_series: "A",
invoice_date: "2025-04-30T00:00:00.000Z",
operation_date: "2025-04-30T00:00:00.000Z",
description: "",
language_code: "ES",
currency: "EUR",
customer_id: "5e4dc5b3-96b9-4968-9490-14bd032fec5f",
items: [
{
description: "",
quantity: {
amount: 100,
scale: 2,
},
unit_price: {
amount: 100,
scale: 2,
currency_code: "EUR",
},
subtotal_price: {
amount: 100,
scale: 2,
currency_code: "EUR",
},
discount: {
amount: 0,
scale: 2,
},
discount_price: {
amount: 0,
scale: 2,
currency_code: "EUR",
},
total_price: {
amount: 100,
scale: 2,
currency_code: "EUR",
},
},
],
subtotal_price: {
amount: 0,
scale: 2,
currency_code: "EUR",
},
discount: {
amount: 0,
scale: 0,
},
discount_price: {
amount: 0,
scale: 0,
currency_code: "EUR",
},
before_tax_price: {
amount: 0,
scale: 2,
currency_code: "EUR",
},
tax: {
amount: 2100,
scale: 2,
},
tax_price: {
amount: 0,
scale: 2,
currency_code: "EUR",
},
total_price: {
amount: 0,
scale: 2,
currency_code: "EUR",
},
};
interface InvoiceFormProps {
initialData?: CustomerInvoiceData;
isPending?: boolean;
/**
* Callback function to handle form submission.
* @param data - The invoice data submitted by the form.
*/
onSubmit?: (data: CustomerInvoiceData) => void;
}
export const CreateCustomerInvoiceEditForm = ({
initialData = defaultInvoiceData,
onSubmit,
isPending,
}: InvoiceFormProps) => {
const { t } = useTranslation();
const form = useForm<CustomerInvoiceData>({
resolver: zodResolver(invoiceFormSchema),
defaultValues: initialData,
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "items",
});
const watchedItems = form.watch("items");
const watchedTaxRate = form.watch("tax.amount");
const addItem = () => {
append({
id_article: "",
description: "",
quantity: { amount: 100, scale: 2 },
unit_price: { amount: 0, scale: 2, currency_code: form.getValues("currency") },
subtotal_price: { amount: 0, scale: 2, currency_code: form.getValues("currency") },
discount: { amount: 0, scale: 2 },
discount_price: { amount: 0, scale: 2, currency_code: form.getValues("currency") },
total_price: { amount: 0, scale: 2, currency_code: form.getValues("currency") },
});
};
const handleSubmit = (data: CustomerInvoiceData) => {
console.log("Datos del formulario:", data);
onSubmit?.(data);
};
const handleError = (errors: any) => {
console.error("Errores en el formulario:", errors);
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
};
const handleCancel = () => {
form.reset(initialData);
};
return (
<Form {...form}>
<form
className="grid grid-cols-1 md:gap-6 md:grid-cols-2"
onSubmit={form.handleSubmit(handleSubmit, handleError)}
>
<Card className="border-0 shadow-none md:grid-span-2">
<CardHeader>
<CardTitle>Cliente</CardTitle>
<CardDescription>Description</CardDescription>
<CardAction>
<Button variant="link">Sign Up</Button>
<Button variant="link">Sign Up</Button>
<Button variant="link">Sign Up</Button>
<Button variant="link">Sign Up</Button>
</CardAction>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-4 space-y-6">
<div>
<div className="space-y-1">
<h4 className="text-sm leading-none font-medium">Radix Primitives</h4>
<p className="text-muted-foreground text-sm">
An open-source UI component library.
</p>
</div>
<Separator className="my-4" />
<div className="flex h-5 items-center space-x-4 text-sm">
<div>Blog</div>
<Separator orientation="vertical" />
<div>Docs</div>
<Separator orientation="vertical" />
<div>Source</div>
</div>
</div>
</CardContent>
<CardFooter className="flex-col gap-2">
<Button className="w-full" type="submit">
Login
</Button>
<Button className="w-full" variant="outline">
Login with Google
</Button>
</CardFooter>{" "}
</Card>
{/* Información básica */}
<Card className="border-0 shadow-none ">
<CardHeader>
<CardTitle>Información Básica</CardTitle>
<CardDescription>Detalles generales de la factura</CardDescription>
</CardHeader>
<CardContent className="space-y-8">
<div className="grid gap-y-6 gap-x-8 md:grid-cols-4">
<TextField
control={form.control}
description={t("form_fields.invoice_number.description")}
disabled
label={t("form_fields.invoice_number.label")}
name="invoice_number"
placeholder={t("form_fields.invoice_number.placeholder")}
readOnly
required
/>
<DatePickerInputField
control={form.control}
description={t("form_fields.invoice_date.description")}
label={t("form_fields.invoice_date.label")}
name="invoice_date"
placeholder={t("form_fields.invoice_date.placeholder")}
required
/>
<TextField
control={form.control}
description={t("form_fields.invoice_series.description")}
label={t("form_fields.invoice_series.label")}
name="invoice_series"
placeholder={t("form_fields.invoice_series.placeholder")}
required
/>
</div>
<div className="grid gap-y-6 gap-x-8 grid-cols-1">
<TextField
control={form.control}
description={t("form_fields.description.description")}
label={t("form_fields.description.label")}
name="description"
placeholder={t("form_fields.description.placeholder")}
required
/>
</div>
<div className="grid gap-y-6 gap-x-8 grid-cols-1">
<TextAreaField
control={form.control}
description={t("form_fields.notes.description")}
label={t("form_fields.notes.label")}
name="notes"
placeholder={t("form_fields.notes.placeholder")}
required
/>
</div>
</CardContent>
</Card>
{/* Cliente */}
<Card className="col-span-full">
<CardHeader>
<CardTitle>Cliente</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-4 space-y-6">
<CustomerModalSelector />
<TextField
control={form.control}
description={t("form_fields.customer_id.description")}
label={t("form_fields.customer_id.label")}
name="customer_id"
placeholder={t("form_fields.customer_id.placeholder")}
required
/>
</CardContent>
</Card>
{/*Items */}
<CustomerInvoiceItemsCardEditor
className="col-span-full"
defaultValues={defaultInvoiceData}
/>
{/* Items */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>Artículos</CardTitle>
<CardDescription>Lista de productos o servicios facturados</CardDescription>
</div>
<Button onClick={addItem} size="sm" type="button">
<PlusIcon className="h-4 w-4 mr-2" />
Agregar Item
</Button>
</CardHeader>
<CardContent className="space-y-4">
{fields.map((field, index) => (
<Card className="p-4" key={field.id}>
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-2">
<h4 className="font-medium">Item {index + 1}</h4>
</div>
{fields.length > 1 && (
<Button onClick={() => remove(index)} size="sm" type="button" variant="outline">
<Trash2Icon className="h-4 w-4" />
</Button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<FormField
control={form.control}
name={`items.${index}.id_article`}
render={({ field }) => (
<FormItem>
<FormLabel>Código Artículo</FormLabel>
<FormControl>
<Input placeholder="Código" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`items.${index}.description`}
render={({ field }) => (
<FormItem className="md:col-span-2">
<FormLabel>Descripción</FormLabel>
<FormControl>
<Textarea placeholder="Descripción del producto/servicio" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<TextAreaField
control={form.control}
description={t("form_fields.items.description.description")}
label={t("form_fields.items.description.label")}
name={`items.${index}.description`}
placeholder={t("form_fields.items.description.placeholder")}
/>
<FormField
control={form.control}
name={`items.${index}.quantity.amount`}
render={({ field }) => (
<FormItem>
<FormLabel>Cantidad</FormLabel>
<FormControl>
<Input
min="0"
step="0.01"
type="number"
{...field}
onChange={(e) => field.onChange(Number(e.target.value) * 100)}
value={field.value / 100}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`items.${index}.unit_price.amount`}
render={({ field }) => (
<FormItem>
<FormLabel>Precio Unitario</FormLabel>
<FormControl>
<Input
min="0"
step="0.01"
type="number"
{...field}
onChange={(e) => field.onChange(Number(e.target.value) * 100)}
value={field.value / 100}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`items.${index}.discount.amount`}
render={({ field }) => (
<FormItem>
<FormLabel>Descuento (%)</FormLabel>
<FormControl>
<Input
max="100"
min="0"
step="0.01"
type="number"
{...field}
onChange={(e) => field.onChange(Number(e.target.value) * 100)}
value={field.value / 100}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="mt-4 p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground">
Total del item:{" "}
{formatCurrency(
watchedItems[index]?.total_price?.amount || 0,
2,
form.getValues("currency")
)}
</div>
</div>
</Card>
))}
</CardContent>
</Card>
<CustomerInvoicePricesCard />
{/* Configuración de Impuestos */}
<Card>
<CardHeader>
<CardTitle>Impuestos y Totales</CardTitle>
<CardDescription>Configuración de impuestos y resumen de totales</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="tax.amount"
render={({ field }) => (
<FormItem>
<FormLabel>Tasa de Impuesto (%)</FormLabel>
<FormControl>
<Input
max="100"
min="0"
step="0.01"
type="number"
{...field}
onChange={(e) => field.onChange(Number(e.target.value) * 100)}
value={field.value / 100}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Separator />
{/* Resumen de totales */}
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span>Subtotal:</span>
<span>
{formatCurrency(form.watch("subtotal_price.amount"), 2, form.watch("currency"))}
</span>
</div>
<div className="flex justify-between text-sm">
<span>Descuento:</span>
<span>
-{formatCurrency(form.watch("discount_price.amount"), 2, form.watch("currency"))}
</span>
</div>
<div className="flex justify-between text-sm">
<span>Base imponible:</span>
<span>
{formatCurrency(form.watch("before_tax_price.amount"), 2, form.watch("currency"))}
</span>
</div>
<div className="flex justify-between text-sm">
<span>Impuestos ({(form.watch("tax.amount") / 100).toFixed(2)}%):</span>
<span>
{formatCurrency(form.watch("tax_price.amount"), 2, form.watch("currency"))}
</span>
</div>
<Separator />
<div className="flex justify-between text-lg font-semibold">
<span>Total:</span>
<span>
{formatCurrency(form.watch("total_price.amount"), 2, form.watch("currency"))}
</span>
</div>
</div>
</CardContent>
</Card>
<div className="flex justify-end space-x-4">
<Button disabled={isPending} onClick={handleCancel} type="button" variant="outline">
Cancelar
</Button>
<Button disabled={isPending} type="submit">
Guardar Factura
</Button>
</div>
</form>
<DevTool control={form.control} />
</Form>
);
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 bg-muted/50">
<form className="space-y-6" onSubmit={handleSubmit}>
{/* Información básica */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="id">ID de Factura</Label>
<Input className="bg-muted" disabled id="id" value={formData.id} />
</div>
<div className="space-y-2">
<Label htmlFor="invoice_status">Estado</Label>
<Select
onValueChange={(value) => handleInputChange("invoice_status", value)}
value={formData.invoice_status}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="draft">Borrador</SelectItem>
<SelectItem value="sent">Enviada</SelectItem>
<SelectItem value="paid">Pagada</SelectItem>
<SelectItem value="cancelled">Cancelada</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="language_code">Idioma</Label>
<Select
onValueChange={(value) => handleInputChange("language_code", value)}
value={formData.language_code}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ES">Español</SelectItem>
<SelectItem value="EN">English</SelectItem>
<SelectItem value="FR">Français</SelectItem>
<SelectItem value="DE">Deutsch</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Numeración */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="invoice_series">Serie</Label>
<Input
id="invoice_series"
onChange={(e) => handleInputChange("invoice_series", e.target.value)}
placeholder="A"
value={formData.invoice_series}
/>
</div>
<div className="space-y-2">
<Label htmlFor="invoice_number">Número</Label>
<Input
id="invoice_number"
onChange={(e) => handleInputChange("invoice_number", e.target.value)}
placeholder="1"
value={formData.invoice_number}
/>
</div>
<div className="space-y-2 hidden">
<Label htmlFor="currency">Moneda</Label>
<Select
onValueChange={(value) => handleInputChange("currency", value)}
value={formData.currency}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="EUR">EUR ()</SelectItem>
<SelectItem value="USD">USD ($)</SelectItem>
<SelectItem value="GBP">GBP (£)</SelectItem>
<SelectItem value="JPY">JPY (¥)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Fechas */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Fecha de Emisión</Label>
<Popover>
<PopoverTrigger asChild>
<Button className="w-full justify-start text-left font-normal" variant="outline">
<CalendarIcon className="mr-2 h-4 w-4" />
{format(invoiceDate, "PPP", { locale: es })}
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="w-auto p-0">
<Calendar
initialFocus
mode="single"
onSelect={(date) => handleDateChange("invoice_date", date)}
selected={invoiceDate}
/>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<Label>Fecha de Operación</Label>
<Popover>
<PopoverTrigger asChild>
<Button className="w-full justify-start text-left font-normal" variant="outline">
<CalendarIcon className="mr-2 h-4 w-4" />
{format(operationDate, "PPP", { locale: es })}
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="w-auto p-0">
<Calendar
initialFocus
mode="single"
onSelect={(date) => handleDateChange("operation_date", date)}
selected={operationDate}
/>
</PopoverContent>
</Popover>
</div>
</div>
{/* Importes */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Importes</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">Subtotal</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="space-y-2">
<Label htmlFor="subtotal_amount">Importe</Label>
<Input
id="subtotal_amount"
onChange={(e) =>
handleNestedChange(
"subtotal",
"amount",
Number.parseFloat(e.target.value) * 10 ** formData.subtotal.scale
)
}
step="0.01"
type="number"
value={formData.subtotal.amount / 10 ** formData.subtotal.scale}
/>
</div>
<div className="space-y-2">
<Label htmlFor="subtotal_currency">Moneda</Label>
<Select
onValueChange={(value) =>
handleNestedChange("subtotal", "currency_code", value)
}
value={formData.subtotal.currency_code}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="EUR">EUR</SelectItem>
<SelectItem value="USD">USD</SelectItem>
<SelectItem value="GBP">GBP</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">Total</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="space-y-2">
<Label htmlFor="total_amount">Importe</Label>
<Input
id="total_amount"
onChange={(e) =>
handleNestedChange(
"total",
"amount",
Number.parseFloat(e.target.value) * 10 ** formData.total.scale
)
}
step="0.01"
type="number"
value={formData.total.amount / 10 ** formData.total.scale}
/>
</div>
<div className="space-y-2">
<Label htmlFor="total_currency">Moneda</Label>
<Select
onValueChange={(value) => handleNestedChange("total", "currency_code", value)}
value={formData.total.currency_code}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="EUR">EUR</SelectItem>
<SelectItem value="USD">USD</SelectItem>
<SelectItem value="GBP">GBP</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
</div>
</div>
{/* Botones de acción */}
<div className="flex justify-end gap-3 pt-6 border-t">
<Button
className="flex items-center gap-2"
onClick={handleCancel}
type="button"
variant="outline"
>
<X className="h-4 w-4" />
Cancelar
</Button>
<Button className="flex items-center gap-2" type="submit">
<Save className="h-4 w-4" />
Guardar Cambios
</Button>
</div>
</form>
</div>
);
};