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 = () => {
return (
<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'>
<UserPlusIcon className='size-6 text-muted-foreground group-hover:text-primary' />
<UserSearchIcon className='size-6 text-muted-foreground group-hover:text-primary' />
</div>
<div className='flex-1 text-left'>
<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
</p>
</div>
<SearchIcon className='size-5 text-muted-foreground group-hover:text-primary' />
</div>
);
};

View File

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

View File

@ -10,11 +10,20 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@repo/shadcn-ui/components";
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";
interface CustomerSearchDialogProps {
@ -40,7 +49,12 @@ export const CustomerSearchDialog = ({
selectedClient,
onSelectClient,
onCreateClient,
isLoading,
isError,
errorMessage,
}: CustomerSearchDialogProps) => {
const isEmpty = !isLoading && !isError && customers && customers.length === 0;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<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>
</DialogHeader>
<div className='px-6 pb-6'>
<Command className='border rounded-lg'>
<div className='px-6 pb-3'>
<Command className='border rounded-lg' shouldFilter={false}>
<CommandInput
placeholder='Buscar cliente...'
value={searchQuery}
onValueChange={onSearchQueryChange}
/>
<CommandList className='max-h-[400px]'>
<CommandList className='max-h-[600px]'>
<CommandEmpty>
<div className='flex flex-col items-center gap-2 py-6 text-sm'>
<User className='h-8 w-8 text-muted-foreground/50' />
<p>No se encontraron clientes</p>
{searchQuery && (
<Button variant='outline' size='sm' onClick={() => onCreateClient(searchQuery)}>
<Plus className='mr-2 size-4' />
Crear cliente "{searchQuery}"
</Button>
<User className='size-8 text-muted-foreground/50' />
{isLoading && <p>Cargando</p>}
{isError && <p className='text-destructive'>{errorMessage}</p>}
{!isLoading && !isError && (
<>
<p>No se encontraron clientes</p>
{searchQuery && (
<Button
onClick={() => onCreateClient(searchQuery)}
className='cursor-pointer'
>
<UserPlusIcon className='mr-2 size-4' />
Crear cliente "{searchQuery}"
</Button>
)}
</>
)}
</div>
</CommandEmpty>
<CommandGroup>
{customers.map((customer) => (
<CommandItem
key={customer.id}
value={customer.id}
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>
<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>
)}
{customers.map((customer) => {
return (
<CommandItem
key={customer.id}
value={customer.id}
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>
<div className='flex items-center gap-4 text-sm font-medium text-muted-foreground'>
{customer.tin && (
<span className='flex items-center gap-1'>
<AsteriskIcon className='size-4' /> {customer.tin}
</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 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 className='flex items-center gap-6 text-sm font-medium text-muted-foreground'>
{customer.tin && (
<span className='flex items-center gap-0'>
<AsteriskIcon className='size-4' /> {customer.tin}
</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>
<Check
className={cn(
"ml-auto size-4",
selectedClient?.id === customer.id ? "opacity-100" : "opacity-0"
)}
/>
</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>
<Check
className={cn(
"ml-auto size-4",
selectedClient?.id === customer.id ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</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>
</Dialog>
);

View File

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