This commit is contained in:
David Arranz 2025-09-30 19:03:20 +02:00
parent d90eaccde0
commit 5acf018a22
4 changed files with 100 additions and 77 deletions

View File

@ -1,10 +1,10 @@
import { SearchIcon, UserPlusIcon } from "lucide-react"; import { UserSearchIcon } from "lucide-react";
export const CustomerEmptyCard = () => { export const CustomerEmptyCard = () => {
return ( return (
<div className='flex items-center gap-4 group-hover:text-primary'> <div className='flex items-center gap-4 group-hover:text-primary'>
<div className='flex size-12 items-center justify-center rounded-full bg-muted group-hover:bg-primary/15'> <div className='flex size-12 items-center justify-center rounded-full bg-muted group-hover:bg-primary/15'>
<UserPlusIcon className='size-6 text-muted-foreground group-hover:text-primary' /> <UserSearchIcon className='size-6 text-muted-foreground group-hover:text-primary' />
</div> </div>
<div className='flex-1 text-left'> <div className='flex-1 text-left'>
<h3 className='font-medium text-muted-foreground mb-1 group-hover:text-primary'> <h3 className='font-medium text-muted-foreground mb-1 group-hover:text-primary'>
@ -14,7 +14,6 @@ export const CustomerEmptyCard = () => {
Haz clic para buscar un cliente existente o crear uno nuevo Haz clic para buscar un cliente existente o crear uno nuevo
</p> </p>
</div> </div>
<SearchIcon className='size-5 text-muted-foreground group-hover:text-primary' />
</div> </div>
); );
}; };

View File

@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useCustomersSearchQuery } from "../../hooks"; import { useCustomersSearchQuery } from "../../hooks";
import { CustomerSummary } from "../../schemas"; import { CustomerSummary, defaultCustomerFormData } from "../../schemas";
import { CustomerCard } from "./customer-card"; import { CustomerCard } from "./customer-card";
import { CustomerEmptyCard } from "./customer-empty-card"; import { CustomerEmptyCard } from "./customer-empty-card";
import { CreateCustomerFormDialog } from "./customer-form-dialog"; import { CreateCustomerFormDialog } from "./customer-form-dialog";
@ -30,7 +30,8 @@ export const CustomerModalSelector = ({ value, onValueChange }: CustomerModalSel
// Cliente seleccionado y creación local optimista // Cliente seleccionado y creación local optimista
const [selected, setSelected] = useState<CustomerSummary | null>(null); const [selected, setSelected] = useState<CustomerSummary | null>(null);
const [newClient, setNewClient] = useState<Omit<CustomerSummary, "id">>({ name: "", email: "" }); const [newClient, setNewClient] =
useState<Omit<CustomerSummary, "id" | "status" | "company_id">>(defaultCustomerFormData);
const [localCreated, setLocalCreated] = useState<CustomerSummary[]>([]); const [localCreated, setLocalCreated] = useState<CustomerSummary[]>([]);
const { const {
@ -61,19 +62,17 @@ export const CustomerModalSelector = ({ value, onValueChange }: CustomerModalSel
const handleCreate = () => { const handleCreate = () => {
if (!newClient.name || !newClient.email) return; if (!newClient.name || !newClient.email) return;
const client: CustomerSummary = { const newCustomer: CustomerSummary = {
id: crypto.randomUUID?.() ?? Date.now().toString(), id: crypto.randomUUID?.() ?? Date.now().toString(),
...newClient, ...newClient,
}; };
setLocalCreated((prev) => [client, ...prev]); setLocalCreated((prev) => [newCustomer, ...prev]);
setSelected(client); setSelected(newCustomer);
onValueChange?.(client.id); onValueChange?.(newCustomer.id);
setShowForm(false); setShowForm(false);
setShowSearch(false); setShowSearch(false);
}; };
console.log(customers);
return ( return (
<> <>
<button <button

View File

@ -10,11 +10,20 @@ import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import { AsteriskIcon, Check, MailIcon, Plus, SmartphoneIcon, User, UserIcon } from "lucide-react"; import {
AsteriskIcon,
Check,
MailIcon,
SmartphoneIcon,
User,
UserIcon,
UserPlusIcon,
} from "lucide-react";
import { CustomerSummary } from "../../schemas"; import { CustomerSummary } from "../../schemas";
interface CustomerSearchDialogProps { interface CustomerSearchDialogProps {
@ -40,7 +49,12 @@ export const CustomerSearchDialog = ({
selectedClient, selectedClient,
onSelectClient, onSelectClient,
onCreateClient, onCreateClient,
isLoading,
isError,
errorMessage,
}: CustomerSearchDialogProps) => { }: CustomerSearchDialogProps) => {
const isEmpty = !isLoading && !isError && customers && customers.length === 0;
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-2xl bg-card border-border p-0'> <DialogContent className='sm:max-w-2xl bg-card border-border p-0'>
@ -54,87 +68,98 @@ export const CustomerSearchDialog = ({
<DialogDescription>Busca un cliente existente o crea uno nuevo.</DialogDescription> <DialogDescription>Busca un cliente existente o crea uno nuevo.</DialogDescription>
</DialogHeader> </DialogHeader>
<div className='px-6 pb-6'> <div className='px-6 pb-3'>
<Command className='border rounded-lg'> <Command className='border rounded-lg' shouldFilter={false}>
<CommandInput <CommandInput
placeholder='Buscar cliente...' placeholder='Buscar cliente...'
value={searchQuery} value={searchQuery}
onValueChange={onSearchQueryChange} onValueChange={onSearchQueryChange}
/> />
<CommandList className='max-h-[400px]'>
<CommandList className='max-h-[600px]'>
<CommandEmpty> <CommandEmpty>
<div className='flex flex-col items-center gap-2 py-6 text-sm'> <div className='flex flex-col items-center gap-2 py-6 text-sm'>
<User className='h-8 w-8 text-muted-foreground/50' /> <User className='size-8 text-muted-foreground/50' />
<p>No se encontraron clientes</p> {isLoading && <p>Cargando</p>}
{searchQuery && ( {isError && <p className='text-destructive'>{errorMessage}</p>}
<Button variant='outline' size='sm' onClick={() => onCreateClient(searchQuery)}> {!isLoading && !isError && (
<Plus className='mr-2 size-4' /> <>
Crear cliente "{searchQuery}" <p>No se encontraron clientes</p>
</Button> {searchQuery && (
<Button
onClick={() => onCreateClient(searchQuery)}
className='cursor-pointer'
>
<UserPlusIcon className='mr-2 size-4' />
Crear cliente "{searchQuery}"
</Button>
)}
</>
)} )}
</div> </div>
</CommandEmpty> </CommandEmpty>
<CommandGroup> <CommandGroup>
{customers.map((customer) => ( {customers.map((customer) => {
<CommandItem return (
key={customer.id} <CommandItem
value={customer.id} key={customer.id}
onSelect={() => onSelectClient(customer)} value={customer.id}
className='flex items-center gap-x-4 py-5 cursor-pointer' onSelect={() => onSelectClient(customer)}
> className='flex items-center gap-x-4 py-5 cursor-pointer'
<div className='flex size-12 items-center justify-center rounded-full bg-primary/10'> >
<UserIcon className='size-8 stroke-1 text-primary' /> <div className='flex size-12 items-center justify-center rounded-full bg-primary/10'>
</div> <UserIcon className='size-8 stroke-1 text-primary' />
<div className='flex-1 space-y-1 min-w-0'>
<div className='flex items-center gap-2'>
<span className='text-sm font-semibold'>{customer.name}</span>
{customer.trade_name && (
<Badge variant='secondary' className='text-sm'>
{customer.trade_name}
</Badge>
)}
</div> </div>
<div className='flex items-center gap-4 text-sm font-medium text-muted-foreground'> <div className='flex-1 space-y-1 min-w-0'>
{customer.tin && ( <div className='flex items-center gap-2'>
<span className='flex items-center gap-1'> <span className='text-sm font-semibold'>{customer.name}</span>
<AsteriskIcon className='size-4' /> {customer.tin} {customer.trade_name && (
</span> <Badge variant='secondary' className='text-sm'>
)} {customer.trade_name}
{customer.email_primary && ( </Badge>
<span className='flex items-center gap-1'> )}
<MailIcon className='size-4' /> {customer.email_primary} </div>
</span> <div className='flex items-center gap-6 text-sm font-medium text-muted-foreground'>
)} {customer.tin && (
{customer.mobile_primary && ( <span className='flex items-center gap-0'>
<span className='flex items-center gap-1'> <AsteriskIcon className='size-4' /> {customer.tin}
<SmartphoneIcon className='size-4' /> {customer.mobile_primary} </span>
</span> )}
)} {customer.email_primary && (
<span className='flex items-center gap-1'>
<MailIcon className='size-4' /> {customer.email_primary}
</span>
)}
{customer.mobile_primary && (
<span className='flex items-center gap-1'>
<SmartphoneIcon className='size-4' /> {customer.mobile_primary}
</span>
)}
</div>
</div> </div>
</div> <Check
<Check className={cn(
className={cn( "ml-auto size-4",
"ml-auto size-4", selectedClient?.id === customer.id ? "opacity-100" : "opacity-0"
selectedClient?.id === customer.id ? "opacity-100" : "opacity-0" )}
)} />
/> </CommandItem>
</CommandItem> );
))} })}
</CommandGroup>
<CommandGroup>
<CommandItem
onSelect={() => onCreateClient()}
className='flex items-center gap-3 p-3 border-t'
>
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-primary'>
<Plus className='size-4 text-primary-foreground' />
</div>
<span className='font-medium'>Agregar nuevo cliente</span>
</CommandItem>
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</Command> </Command>
</div> </div>
<DialogFooter className='sm:justify-center px-6 pb-6'>
<Button
onClick={() => onCreateClient(searchQuery)}
className='cursor-pointer text-center'
>
<UserPlusIcon className='mr-2 size-4' />
Añadir nuevo cliente
</Button>
</DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -22,4 +22,4 @@ export type CustomerUpdateInput = z.infer<typeof CustomerUpdateSchema>; // Cuerp
export type CustomersPage = ListCustomersResponseDTO; export type CustomersPage = ListCustomersResponseDTO;
// Ítem simplificado dentro del listado (no toda la entidad) // Ítem simplificado dentro del listado (no toda la entidad)
export type CustomerSummary = ArrayElement<CustomersPage["items"]>; export type CustomerSummary = Omit<ArrayElement<CustomersPage["items"]>, "metadata">;