Facturas de cliente

This commit is contained in:
David Arranz 2025-10-01 11:57:08 +02:00
parent d8f7c70e7e
commit 58eede59af
6 changed files with 141 additions and 64 deletions

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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}

View File

@ -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'>

View File

@ -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);