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 = () => {
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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">;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user