Facturas de cliente
This commit is contained in:
parent
d8f7c70e7e
commit
58eede59af
@ -1,46 +1,114 @@
|
||||
import { Building2Icon, FileTextIcon, Mail, Phone, User } from "lucide-react";
|
||||
import { Button, Separator } from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
CreditCard,
|
||||
EyeIcon,
|
||||
MapPinHouseIcon,
|
||||
RefreshCwIcon,
|
||||
UserIcon,
|
||||
UserPlusIcon,
|
||||
} from "lucide-react";
|
||||
import { CustomerSummary } from "../../schemas";
|
||||
|
||||
interface CustomerCardProps {
|
||||
customer: CustomerSummary;
|
||||
|
||||
onViewCustomer?: () => void;
|
||||
onChangeCustomer?: () => void;
|
||||
onAddNewCustomer?: () => void;
|
||||
}
|
||||
|
||||
export const CustomerCard = ({ customer }: CustomerCardProps) => {
|
||||
export const CustomerCard = ({
|
||||
customer,
|
||||
onViewCustomer,
|
||||
onChangeCustomer,
|
||||
onAddNewCustomer,
|
||||
}: CustomerCardProps) => {
|
||||
const hasAddress =
|
||||
customer.street ||
|
||||
customer.street2 ||
|
||||
customer.city ||
|
||||
customer.postal_code ||
|
||||
customer.province ||
|
||||
customer.country;
|
||||
|
||||
return (
|
||||
<div className='flex items-start gap-4'>
|
||||
<div className='flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 border border-primary/20'>
|
||||
<User className='h-6 w-6 text-primary' />
|
||||
</div>
|
||||
<div className='flex-1 space-y-2'>
|
||||
<p className='text-left font-semibold text-foreground text-base'>{customer.name}</p>
|
||||
<div className='space-y-1 text-sm text-muted-foreground'>
|
||||
{customer.email_primary && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Mail className='h-3 w-3' />
|
||||
{customer.email_primary}
|
||||
<section>
|
||||
<div className='flex items-start gap-4'>
|
||||
{/* Avatar mejorado con gradiente sutil */}
|
||||
<div className='flex size-12 items-center justify-center rounded-full bg-muted group-hover:bg-primary/15'>
|
||||
<UserIcon className='size-6 text-muted-foreground group-hover:text-primary' />
|
||||
</div>
|
||||
|
||||
<div className='flex-1 min-w-0 '>
|
||||
{/* Nombre del cliente */}
|
||||
<h3 className='font-semibold text-foreground text-lg leading-tight mb-1 text-left text-balance'>
|
||||
{customer.name}
|
||||
</h3>
|
||||
|
||||
{/* NIF/CIF con icono */}
|
||||
{customer.tin && (
|
||||
<div className='flex items-center gap-2 text-sm text-muted-foreground mb-3'>
|
||||
<CreditCard className='h-4 w-4 shrink-0' />
|
||||
<span className='font-medium'>{customer.tin}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{customer.mobile_primary && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Phone className='h-3 w-3' />
|
||||
{customer.mobile_primary}
|
||||
</div>
|
||||
)}
|
||||
{customer.trade_name && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Building2Icon className='h-3 w-3' />
|
||||
{customer.trade_name}
|
||||
</div>
|
||||
)}
|
||||
{customer.tin && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<FileTextIcon className='h-3 w-3' />
|
||||
{customer.tin}
|
||||
{/* Separador si hay dirección */}
|
||||
{hasAddress && <Separator className='my-3' />}
|
||||
|
||||
{/* Dirección con mejor estructura */}
|
||||
{hasAddress && (
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-start gap-2 text-sm text-muted-foreground'>
|
||||
<MapPinHouseIcon className='h-4 w-4 shrink-0 mt-0.5 text-primary/60' />
|
||||
<div className='space-y-0.5 leading-relaxed flex-1 text-left'>
|
||||
{customer.street && <div>{customer.street}</div>}
|
||||
{customer.street2 && <div>{customer.street2}</div>}
|
||||
<div className='flex flex-wrap gap-x-2'>
|
||||
{customer.postal_code && <span>{customer.postal_code}</span>}
|
||||
{customer.city && <span>{customer.city}</span>}
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-x-2'>
|
||||
{customer.province && <span>{customer.province}</span>}
|
||||
{customer.country && <span>{customer.country}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className='my-4' />
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={onViewCustomer}
|
||||
className='flex-1 min-w-[140px] gap-2 bg-transparent'
|
||||
>
|
||||
<EyeIcon className='h-4 w-4' />
|
||||
Ver ficha completa
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={onChangeCustomer}
|
||||
className='flex-1 min-w-[140px] gap-2 bg-transparent'
|
||||
>
|
||||
<RefreshCwIcon className='h-4 w-4' />
|
||||
Cambiar cliente
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={onAddNewCustomer}
|
||||
className='flex-1 min-w-[140px] gap-2 bg-transparent'
|
||||
>
|
||||
<UserPlusIcon className='h-4 w-4' />
|
||||
Nuevo cliente
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,19 +1,27 @@
|
||||
import { UserSearchIcon } from "lucide-react";
|
||||
|
||||
export const CustomerEmptyCard = () => {
|
||||
export const CustomerEmptyCard = (props: React.ComponentProps<"button">) => {
|
||||
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'>
|
||||
<UserSearchIcon className='size-6 text-muted-foreground group-hover:text-primary' />
|
||||
<button
|
||||
tabIndex={0}
|
||||
type='button'
|
||||
className='group w-full cursor-pointer rounded-lg border border-border bg-card p-4 transition hover:bg-accent/50 hover:border-primary'
|
||||
aria-label='Seleccionar cliente'
|
||||
{...props}
|
||||
>
|
||||
<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'>
|
||||
<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'>
|
||||
Seleccionar Cliente
|
||||
</h3>
|
||||
<p className='text-sm text-muted-foreground/70 group-hover:text-primary'>
|
||||
Haz clic para buscar un cliente existente o crear uno nuevo
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-1 text-left'>
|
||||
<h3 className='font-medium text-muted-foreground mb-1 group-hover:text-primary'>
|
||||
Seleccionar Cliente
|
||||
</h3>
|
||||
<p className='text-sm text-muted-foreground/70 group-hover:text-primary'>
|
||||
Haz clic para buscar un cliente existente o crear uno nuevo
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCustomersSearchQuery } from "../../hooks";
|
||||
import { CustomerSummary, defaultCustomerFormData } from "../../schemas";
|
||||
import { CreateCustomerFormDialog } from "./create-customer-form-dialog";
|
||||
import { CustomerCard } from "./customer-card";
|
||||
import { CustomerEmptyCard } from "./customer-empty-card";
|
||||
import { CreateCustomerFormDialog } from "./customer-form-dialog";
|
||||
import { CustomerSearchDialog } from "./customer-search-dialog";
|
||||
|
||||
// Debounce pequeño y tipado
|
||||
@ -75,16 +75,16 @@ export const CustomerModalSelector = ({ value, onValueChange }: CustomerModalSel
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
tabIndex={0}
|
||||
type='button'
|
||||
onClick={() => setShowSearch(true)}
|
||||
onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && setShowSearch(true)}
|
||||
className='group w-full cursor-pointer rounded-lg border border-border bg-card p-4 transition hover:bg-accent/50 hover:border-primary'
|
||||
aria-label='Seleccionar cliente'
|
||||
>
|
||||
{selected ? <CustomerCard customer={selected} /> : <CustomerEmptyCard />}
|
||||
</button>
|
||||
<div>
|
||||
{selected ? (
|
||||
<CustomerCard customer={selected} onChangeCustomer={() => setShowSearch(true)} />
|
||||
) : (
|
||||
<CustomerEmptyCard
|
||||
onClick={() => setShowSearch(true)}
|
||||
onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && setShowSearch(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CustomerSearchDialog
|
||||
open={showSearch}
|
||||
@ -99,8 +99,8 @@ export const CustomerModalSelector = ({ value, onValueChange }: CustomerModalSel
|
||||
setShowSearch(false);
|
||||
}}
|
||||
onCreateClient={(name) => {
|
||||
/*setNewClient({ name: name ?? "", email: "" });
|
||||
setShowForm(true);*/
|
||||
setNewClient({ name: name ?? "", email: "" });
|
||||
setShowForm(true);
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
|
||||
@ -16,8 +16,8 @@ import {
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import {
|
||||
AsteriskIcon,
|
||||
Check,
|
||||
CreditCardIcon,
|
||||
MailIcon,
|
||||
SmartphoneIcon,
|
||||
User,
|
||||
@ -122,9 +122,10 @@ export const CustomerSearchDialog = ({
|
||||
</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>
|
||||
<div className='flex items-center gap-2 text-sm text-muted-foreground'>
|
||||
<CreditCardIcon className='h-4 w-4 shrink-0' />
|
||||
<span className='font-medium'>{customer.tin}</span>
|
||||
</div>
|
||||
)}
|
||||
{customer.email_primary && (
|
||||
<span className='flex items-center gap-1'>
|
||||
|
||||
@ -56,8 +56,8 @@
|
||||
--secondary-foreground: oklch(0.2069 0.0098 285.5081);
|
||||
--muted: oklch(0.9674 0.0013 286.3752);
|
||||
--muted-foreground: oklch(0.5466 0.0216 285.664);
|
||||
--accent: oklch(0.9674 0.0013 286.3752);
|
||||
--accent-foreground: oklch(0.2069 0.0098 285.5081);
|
||||
--accent: oklch(0.9299 0.0334 272.7879);
|
||||
--accent-foreground: oklch(0.3729 0.0306 259.7328);
|
||||
--destructive: oklch(0.583 0.2387 28.4765);
|
||||
--destructive-foreground: oklch(1.0 0 0);
|
||||
--border: oklch(0.9173 0.0067 286.2663);
|
||||
@ -102,8 +102,8 @@
|
||||
--secondary-foreground: oklch(0.9851 0 0);
|
||||
--muted: oklch(0.2686 0 0);
|
||||
--muted-foreground: oklch(0.709 0 0);
|
||||
--accent: oklch(0.2686 0 0);
|
||||
--accent-foreground: oklch(0.9851 0 0);
|
||||
--accent: oklch(0.3729 0.0306 259.7328);
|
||||
--accent-foreground: oklch(0.8717 0.0093 258.3382);
|
||||
--destructive: oklch(0.7022 0.1892 22.2279);
|
||||
--destructive-foreground: oklch(0.2558 0.0412 235.1561);
|
||||
--border: oklch(1.0 0 0);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user