Clientes
This commit is contained in:
parent
d90eaccde0
commit
5acf018a22
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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">;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user