962 lines
32 KiB
TypeScript
962 lines
32 KiB
TypeScript
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { useFieldArray, useForm } from "react-hook-form";
|
|
import * as z from "zod";
|
|
|
|
import { ClientSelector } from "@erp/customers/components";
|
|
|
|
import { DevTool } from "@hookform/devtools";
|
|
import { DatePickerInputField, 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 { CustomerInvoicePricesCard } from "../../components";
|
|
import { CustomerInvoiceItemsCardEditor } from "../../components/items";
|
|
import { useTranslation } from "../../i18n";
|
|
import { 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"),
|
|
issue_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",
|
|
issue_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 CustomerInvoiceEditForm = ({
|
|
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 onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
|
<div className='grid xl:grid-cols-2 space-y-6'>
|
|
<Card className='border-0 shadow-none xl:border-r xl:border-dashed rounded-none'>
|
|
<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'>
|
|
<ClientSelector />
|
|
</CardContent>
|
|
<CardFooter className='flex-col gap-2'>
|
|
<Button type='submit' className='w-full'>
|
|
Login
|
|
</Button>
|
|
<Button variant='outline' className='w-full'>
|
|
Login with Google
|
|
</Button>
|
|
</CardFooter>{" "}
|
|
</Card>
|
|
{/* Información básica */}
|
|
<Card className='@container border-0 shadow-none'>
|
|
<CardHeader>
|
|
<CardTitle>Información Básica</CardTitle>
|
|
<CardDescription>Detalles generales de la factura</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className='@xl:grid @xl:grid-cols-2 @xl:gap-x-6 gap-y-8'>
|
|
<TextField
|
|
control={form.control}
|
|
name='invoice_number'
|
|
required
|
|
disabled
|
|
readOnly
|
|
label={t("form_fields.invoice_number.label")}
|
|
placeholder={t("form_fields.invoice_number.placeholder")}
|
|
description={t("form_fields.invoice_number.description")}
|
|
/>
|
|
|
|
<TextField
|
|
control={form.control}
|
|
name='invoice_series'
|
|
required
|
|
label={t("form_fields.invoice_series.label")}
|
|
placeholder={t("form_fields.invoice_series.placeholder")}
|
|
description={t("form_fields.invoice_series.description")}
|
|
/>
|
|
|
|
<DatePickerInputField
|
|
control={form.control}
|
|
name='issue_date'
|
|
required
|
|
label={t("form_fields.issue_date.label")}
|
|
placeholder={t("form_fields.issue_date.placeholder")}
|
|
description={t("form_fields.issue_date.description")}
|
|
/>
|
|
|
|
<TextField
|
|
className='@xl:col-start-1 @xl:col-span-full'
|
|
control={form.control}
|
|
name='description'
|
|
required
|
|
label={t("form_fields.description.label")}
|
|
placeholder={t("form_fields.description.placeholder")}
|
|
description={t("form_fields.description.description")}
|
|
/>
|
|
<TextAreaField
|
|
className='field-sizing-content @xl:col-start-1 @xl:col-span-full'
|
|
control={form.control}
|
|
name='notes'
|
|
label={t("form_fields.notes.label")}
|
|
placeholder={t("form_fields.notes.placeholder")}
|
|
description={t("form_fields.notes.description")}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
);
|
|
|
|
return (
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit(handleSubmit, handleError)}
|
|
className='grid grid-cols-1 md:gap-6 md:grid-cols-6'
|
|
>
|
|
<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 type='submit' className='w-full'>
|
|
Login
|
|
</Button>
|
|
<Button variant='outline' className='w-full'>
|
|
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}
|
|
name='invoice_number'
|
|
required
|
|
disabled
|
|
readOnly
|
|
label={t("form_fields.invoice_number.label")}
|
|
placeholder={t("form_fields.invoice_number.placeholder")}
|
|
description={t("form_fields.invoice_number.description")}
|
|
/>
|
|
|
|
<DatePickerInputField
|
|
control={form.control}
|
|
name='issue_date'
|
|
required
|
|
label={t("form_fields.issue_date.label")}
|
|
placeholder={t("form_fields.issue_date.placeholder")}
|
|
description={t("form_fields.issue_date.description")}
|
|
/>
|
|
|
|
<TextField
|
|
control={form.control}
|
|
name='invoice_series'
|
|
required
|
|
label={t("form_fields.invoice_series.label")}
|
|
placeholder={t("form_fields.invoice_series.placeholder")}
|
|
description={t("form_fields.invoice_series.description")}
|
|
/>
|
|
</div>
|
|
<div className='grid gap-y-6 gap-x-8 grid-cols-1'>
|
|
<TextField
|
|
control={form.control}
|
|
name='description'
|
|
required
|
|
label={t("form_fields.description.label")}
|
|
placeholder={t("form_fields.description.placeholder")}
|
|
description={t("form_fields.description.description")}
|
|
/>
|
|
</div>
|
|
<div className='grid gap-y-6 gap-x-8 grid-cols-1'>
|
|
<TextAreaField
|
|
control={form.control}
|
|
name='notes'
|
|
required
|
|
label={t("form_fields.notes.label")}
|
|
placeholder={t("form_fields.notes.placeholder")}
|
|
description={t("form_fields.notes.description")}
|
|
/>
|
|
</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'>
|
|
<ClientSelector />
|
|
<TextField
|
|
control={form.control}
|
|
name='customer_id'
|
|
required
|
|
label={t("form_fields.customer_id.label")}
|
|
placeholder={t("form_fields.customer_id.placeholder")}
|
|
description={t("form_fields.customer_id.description")}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/*Items */}
|
|
<CustomerInvoiceItemsCardEditor
|
|
defaultValues={defaultInvoiceData}
|
|
className='col-span-full'
|
|
/>
|
|
|
|
{/* 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 type='button' onClick={addItem} size='sm'>
|
|
<PlusIcon className='h-4 w-4 mr-2' />
|
|
Agregar Item
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent className='space-y-4'>
|
|
{fields.map((field, index) => (
|
|
<Card key={field.id} className='p-4'>
|
|
<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 type='button' variant='outline' size='sm' onClick={() => remove(index)}>
|
|
<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}
|
|
name={`items.${index}.description`}
|
|
label={t("form_fields.items.description.label")}
|
|
placeholder={t("form_fields.items.description.placeholder")}
|
|
description={t("form_fields.items.description.description")}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name={`items.${index}.quantity.amount`}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Cantidad</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type='number'
|
|
step='0.01'
|
|
min='0'
|
|
{...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
|
|
type='number'
|
|
step='0.01'
|
|
min='0'
|
|
{...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
|
|
type='number'
|
|
step='0.01'
|
|
min='0'
|
|
max='100'
|
|
{...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
|
|
type='number'
|
|
step='0.01'
|
|
min='0'
|
|
max='100'
|
|
{...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 type='button' variant='outline' disabled={isPending} onClick={handleCancel}>
|
|
Cancelar
|
|
</Button>
|
|
<Button type='submit' disabled={isPending}>
|
|
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 onSubmit={handleSubmit} className='space-y-6'>
|
|
{/* 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 id='id' value={formData.id} disabled className='bg-muted' />
|
|
</div>
|
|
|
|
<div className='space-y-2'>
|
|
<Label htmlFor='invoice_status'>Estado</Label>
|
|
<Select
|
|
value={formData.invoice_status}
|
|
onValueChange={(value) => handleInputChange("invoice_status", value)}
|
|
>
|
|
<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
|
|
value={formData.language_code}
|
|
onValueChange={(value) => handleInputChange("language_code", value)}
|
|
>
|
|
<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'
|
|
value={formData.invoice_series}
|
|
onChange={(e) => handleInputChange("invoice_series", e.target.value)}
|
|
placeholder='A'
|
|
/>
|
|
</div>
|
|
|
|
<div className='space-y-2'>
|
|
<Label htmlFor='invoice_number'>Número</Label>
|
|
<Input
|
|
id='invoice_number'
|
|
value={formData.invoice_number}
|
|
onChange={(e) => handleInputChange("invoice_number", e.target.value)}
|
|
placeholder='1'
|
|
/>
|
|
</div>
|
|
|
|
<div className='space-y-2 hidden'>
|
|
<Label htmlFor='currency'>Moneda</Label>
|
|
<Select
|
|
value={formData.currency}
|
|
onValueChange={(value) => handleInputChange("currency", value)}
|
|
>
|
|
<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 variant='outline' className='w-full justify-start text-left font-normal'>
|
|
<CalendarIcon className='mr-2 h-4 w-4' />
|
|
{format(issueDate, "PPP", { locale: es })}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className='w-auto p-0' align='start'>
|
|
<Calendar
|
|
mode='single'
|
|
selected={issueDate}
|
|
onSelect={(date) => handleDateChange("issue_date", date)}
|
|
initialFocus
|
|
/>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<div className='space-y-2'>
|
|
<Label>Fecha de Operación</Label>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button variant='outline' className='w-full justify-start text-left font-normal'>
|
|
<CalendarIcon className='mr-2 h-4 w-4' />
|
|
{format(operationDate, "PPP", { locale: es })}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className='w-auto p-0' align='start'>
|
|
<Calendar
|
|
mode='single'
|
|
selected={operationDate}
|
|
onSelect={(date) => handleDateChange("operation_date", date)}
|
|
initialFocus
|
|
/>
|
|
</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'
|
|
type='number'
|
|
step='0.01'
|
|
value={formData.subtotal.amount / Math.pow(10, formData.subtotal.scale)}
|
|
onChange={(e) =>
|
|
handleNestedChange(
|
|
"subtotal",
|
|
"amount",
|
|
Number.parseFloat(e.target.value) * Math.pow(10, formData.subtotal.scale)
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
<div className='space-y-2'>
|
|
<Label htmlFor='subtotal_currency'>Moneda</Label>
|
|
<Select
|
|
value={formData.subtotal.currency_code}
|
|
onValueChange={(value) =>
|
|
handleNestedChange("subtotal", "currency_code", value)
|
|
}
|
|
>
|
|
<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'
|
|
type='number'
|
|
step='0.01'
|
|
value={formData.total.amount / Math.pow(10, formData.total.scale)}
|
|
onChange={(e) =>
|
|
handleNestedChange(
|
|
"total",
|
|
"amount",
|
|
Number.parseFloat(e.target.value) * Math.pow(10, formData.total.scale)
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
<div className='space-y-2'>
|
|
<Label htmlFor='total_currency'>Moneda</Label>
|
|
<Select
|
|
value={formData.total.currency_code}
|
|
onValueChange={(value) => handleNestedChange("total", "currency_code", value)}
|
|
>
|
|
<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
|
|
type='button'
|
|
variant='outline'
|
|
onClick={handleCancel}
|
|
className='flex items-center gap-2'
|
|
>
|
|
<X className='h-4 w-4' />
|
|
Cancelar
|
|
</Button>
|
|
<Button type='submit' className='flex items-center gap-2'>
|
|
<Save className='h-4 w-4' />
|
|
Guardar Cambios
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|