Facturas de cliente

This commit is contained in:
David Arranz 2025-07-17 10:50:28 +02:00
parent 7059db0c5d
commit 75738dadde
10 changed files with 245 additions and 102 deletions

View File

@ -1,6 +1,20 @@
import { FormControl, FormField, FormItem, FormMessage, Input } from "@repo/shadcn-ui/components"; import {
Button,
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
FormControl,
FormField,
FormItem,
FormMessage,
Input,
} from "@repo/shadcn-ui/components";
import { TextAreaField } from "@repo/rdx-ui/components"; import { TextAreaField } from "@repo/rdx-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react"; import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@ -18,10 +32,12 @@ export const CustomerInvoiceItemsCardEditor = ({
//currency, //currency,
//language, //language,
defaultValues, defaultValues,
className = "",
}: { }: {
//currency: CurrencyData; //currency: CurrencyData;
//language: Language; //language: Language;
defaultValues: Readonly<{ [x: string]: any }> | undefined; defaultValues: Readonly<{ [x: string]: any }> | undefined;
className?: string;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -271,17 +287,29 @@ export const CustomerInvoiceItemsCardEditor = ({
const navCollapsedSize = 4; const navCollapsedSize = 4;
return ( return (
<div className='relative'> <Card className={cn("border-0 shadow-none", className)}>
<CustomerInvoiceItemsSortableDataTable <CardHeader>
actions={{ <CardTitle>Contenido</CardTitle>
...fieldActions, <CardDescription>Description</CardDescription>
//pickCatalogArticle: () => setArticlePickerDialogOpen(true), <CardAction>
//pickBlock: () => setBlockPickerDialogOpen(true), <Button variant='link'>Sign Up</Button>
}} <Button variant='link'>Sign Up</Button>
columns={columns} <Button variant='link'>Sign Up</Button>
data={fields} <Button variant='link'>Sign Up</Button>
defaultValues={defaultValues} </CardAction>
/> </CardHeader>
</div> <CardContent>
<CustomerInvoiceItemsSortableDataTable
actions={{
...fieldActions,
//pickCatalogArticle: () => setArticlePickerDialogOpen(true),
//pickBlock: () => setBlockPickerDialogOpen(true),
}}
columns={columns}
data={fields}
defaultValues={defaultValues}
/>
</CardContent>
</Card>
); );
}; };

View File

@ -63,7 +63,7 @@ export const CustomerInvoiceCreate = () => {
</Button> </Button>
</div> </div>
</div> </div>
<div className='flex flex-col w-full h-full py-4 @container'> <div className='flex flex-1 flex-col gap-4 p-4'>
<CustomerInvoiceEditForm onSubmit={handleSubmit} isPending={isPending} /> <CustomerInvoiceEditForm onSubmit={handleSubmit} isPending={isPending} />
</div> </div>
</AppContent> </AppContent>

View File

@ -10,8 +10,10 @@ import {
Button, Button,
Calendar, Calendar,
Card, Card,
CardAction,
CardContent, CardContent,
CardDescription, CardDescription,
CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle,
Form, Form,
@ -276,27 +278,138 @@ export const CustomerInvoiceEditForm = ({
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit, handleError)} className='grid gap-6'> <form onSubmit={form.handleSubmit(handleSubmit, handleError)}>
{/* Cliente */} <div className='grid xl:grid-cols-2 space-y-6'>
<Card> <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> <CardHeader>
<CardTitle>Cliente</CardTitle> <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> </CardHeader>
<CardContent className='grid grid-cols-1 gap-4 space-y-6'> <CardContent className='grid grid-cols-1 gap-4 space-y-6'>
<ClientSelector /> <div>
<TextField <div className='space-y-1'>
control={form.control} <h4 className='text-sm leading-none font-medium'>Radix Primitives</h4>
name='customer_id' <p className='text-muted-foreground text-sm'>
required An open-source UI component library.
label={t("form_fields.customer_id.label")} </p>
placeholder={t("form_fields.customer_id.placeholder")} </div>
description={t("form_fields.customer_id.description")} <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> </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> </Card>
{/* Información básica */} {/* Información básica */}
<Card> <Card className='border-0 shadow-none '>
<CardHeader> <CardHeader>
<CardTitle>Información Básica</CardTitle> <CardTitle>Información Básica</CardTitle>
<CardDescription>Detalles generales de la factura</CardDescription> <CardDescription>Detalles generales de la factura</CardDescription>
@ -355,8 +468,29 @@ export const CustomerInvoiceEditForm = ({
</CardContent> </CardContent>
</Card> </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 */} {/*Items */}
<CustomerInvoiceItemsCardEditor defaultValues={defaultInvoiceData} /> <CustomerInvoiceItemsCardEditor
defaultValues={defaultInvoiceData}
className='col-span-full'
/>
{/* Items */} {/* Items */}
<Card> <Card>

View File

@ -128,8 +128,8 @@ export const ClientSelector = () => {
return ( return (
<div className='w-full max-w-md space-y-4'> <div className='w-full max-w-md space-y-4'>
<div className='space-y-2'> <div className='space-y-0'>
<Label>Cliente</Label> <Label className='m-0'>Cliente</Label>
<Button <Button
variant='outline' variant='outline'
className='w-full justify-start bg-transparent' className='w-full justify-start bg-transparent'
@ -143,13 +143,17 @@ export const ClientSelector = () => {
</Button> </Button>
</div> </div>
<CommandDialog open={open} onOpenChange={setOpen}> <CommandDialog
open={open}
onOpenChange={setOpen}
className='[&>div]:max-w-3xl [&>div]:max-h-[80vh] [&>div]:w-full'
>
<CommandInput <CommandInput
placeholder='Buscar cliente por nombre, email o empresa...' placeholder='Buscar cliente por nombre, email o empresa...'
value={searchValue} value={searchValue}
onValueChange={setSearchValue} onValueChange={setSearchValue}
/> />
<CommandList> <CommandList className='max-w-screen'>
<CommandEmpty> <CommandEmpty>
<div className='p-6 text-center'> <div className='p-6 text-center'>
<User className='h-12 w-12 mx-auto text-muted-foreground mb-4' /> <User className='h-12 w-12 mx-auto text-muted-foreground mb-4' />

View File

@ -52,7 +52,7 @@ export function DatePickerField<TFormValues extends FieldValues>({
control={control} control={control}
name={name} name={name}
render={({ field }) => ( render={({ field }) => (
<FormItem className={cn("space-y-2", className)}> <FormItem className={cn("space-y-0", className)}>
{label && ( {label && (
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
<FormLabel className='m-0'>{label}</FormLabel> <FormLabel className='m-0'>{label}</FormLabel>

View File

@ -81,7 +81,7 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
}; };
return ( return (
<FormItem className={cn("space-y-2", className)}> <FormItem className={cn("space-y-0", className)}>
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
<FormLabel className='m-0'>{label}</FormLabel> <FormLabel className='m-0'>{label}</FormLabel>
{required && <span className='text-xs text-destructive'>{t("common.required")}</span>} {required && <span className='text-xs text-destructive'>{t("common.required")}</span>}

View File

@ -44,7 +44,7 @@ export function NumberField<TFormValues extends FieldValues>({
control={control} control={control}
name={name} name={name}
render={({ field }) => ( render={({ field }) => (
<FormItem className={cn("space-y-2", className)}> <FormItem className={cn("space-y-0", className)}>
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
<FormLabel className='m-0'>{label}</FormLabel> <FormLabel className='m-0'>{label}</FormLabel>
{required && <span className='text-xs text-destructive'>{t("common.required")}</span>} {required && <span className='text-xs text-destructive'>{t("common.required")}</span>}

View File

@ -44,7 +44,7 @@ export function TextAreaField<TFormValues extends FieldValues>({
control={control} control={control}
name={name} name={name}
render={({ field }) => ( render={({ field }) => (
<FormItem className={cn("space-y-2", className)}> <FormItem className={cn("space-y-0", className)}>
{label && ( {label && (
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
<FormLabel className='m-0'>{label}</FormLabel> <FormLabel className='m-0'>{label}</FormLabel>

View File

@ -44,7 +44,7 @@ export function TextField<TFormValues extends FieldValues>({
control={control} control={control}
name={name} name={name}
render={({ field }) => ( render={({ field }) => (
<FormItem className={cn("space-y-2", className)}> <FormItem className={cn("space-y-0", className)}>
{label && ( {label && (
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
<FormLabel className='m-0'>{label}</FormLabel> <FormLabel className='m-0'>{label}</FormLabel>

View File

@ -1,32 +1,29 @@
"use client" "use client";
import * as React from "react" import { Command as CommandPrimitive } from "cmdk";
import { Command as CommandPrimitive } from "cmdk" import { SearchIcon } from "lucide-react";
import { SearchIcon } from "lucide-react" import * as React from "react";
import { cn } from "@repo/shadcn-ui/lib/utils"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@repo/shadcn-ui/components/dialog" } from "@repo/shadcn-ui/components/dialog";
import { cn } from "@repo/shadcn-ui/lib/utils";
function Command({ function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return ( return (
<CommandPrimitive <CommandPrimitive
data-slot="command" data-slot='command'
className={cn( className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md", "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className className
)} )}
{...props} {...props}
/> />
) );
} }
function CommandDialog({ function CommandDialog({
@ -35,22 +32,22 @@ function CommandDialog({
children, children,
...props ...props
}: React.ComponentProps<typeof Dialog> & { }: React.ComponentProps<typeof Dialog> & {
title?: string title?: string;
description?: string description?: string;
}) { }) {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogHeader className="sr-only"> <DialogHeader className='sr-only'>
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription> <DialogDescription>{description}</DialogDescription>
</DialogHeader> </DialogHeader>
<DialogContent className="overflow-hidden p-0"> <DialogContent className='overflow-hidden p-0'>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> <Command className='[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'>
{children} {children}
</Command> </Command>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }
function CommandInput({ function CommandInput({
@ -58,13 +55,10 @@ function CommandInput({
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) { }: React.ComponentProps<typeof CommandPrimitive.Input>) {
return ( return (
<div <div data-slot='command-input-wrapper' className='flex h-9 items-center gap-2 border-b px-3'>
data-slot="command-input-wrapper" <SearchIcon className='size-4 shrink-0 opacity-50' />
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input <CommandPrimitive.Input
data-slot="command-input" data-slot='command-input'
className={cn( className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50", "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className className
@ -72,35 +66,27 @@ function CommandInput({
{...props} {...props}
/> />
</div> </div>
) );
} }
function CommandList({ function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return ( return (
<CommandPrimitive.List <CommandPrimitive.List
data-slot="command-list" data-slot='command-list'
className={cn( className={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props} {...props}
/> />
) );
} }
function CommandEmpty({ function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return ( return (
<CommandPrimitive.Empty <CommandPrimitive.Empty
data-slot="command-empty" data-slot='command-empty'
className="py-6 text-center text-sm" className='py-6 text-center text-sm'
{...props} {...props}
/> />
) );
} }
function CommandGroup({ function CommandGroup({
@ -109,14 +95,14 @@ function CommandGroup({
}: React.ComponentProps<typeof CommandPrimitive.Group>) { }: React.ComponentProps<typeof CommandPrimitive.Group>) {
return ( return (
<CommandPrimitive.Group <CommandPrimitive.Group
data-slot="command-group" data-slot='command-group'
className={cn( className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium", "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className className
)} )}
{...props} {...props}
/> />
) );
} }
function CommandSeparator({ function CommandSeparator({
@ -125,53 +111,44 @@ function CommandSeparator({
}: React.ComponentProps<typeof CommandPrimitive.Separator>) { }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return ( return (
<CommandPrimitive.Separator <CommandPrimitive.Separator
data-slot="command-separator" data-slot='command-separator'
className={cn("bg-border -mx-1 h-px", className)} className={cn("bg-border -mx-1 h-px", className)}
{...props} {...props}
/> />
) );
} }
function CommandItem({ function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return ( return (
<CommandPrimitive.Item <CommandPrimitive.Item
data-slot="command-item" data-slot='command-item'
className={cn( className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
/> />
) );
} }
function CommandShortcut({ function CommandShortcut({ className, ...props }: React.ComponentProps<"span">) {
className,
...props
}: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="command-shortcut" data-slot='command-shortcut'
className={cn( className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props} {...props}
/> />
) );
} }
export { export {
Command, Command,
CommandDialog, CommandDialog,
CommandInput,
CommandList,
CommandEmpty, CommandEmpty,
CommandGroup, CommandGroup,
CommandInput,
CommandItem, CommandItem,
CommandShortcut, CommandList,
CommandSeparator, CommandSeparator,
} CommandShortcut,
};