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"; import { CustomerSummary } from "../../schemas";
interface CustomerCardProps { interface CustomerCardProps {
customer: CustomerSummary; 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 ( return (
<section>
<div className='flex items-start gap-4'> <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'> {/* Avatar mejorado con gradiente sutil */}
<User className='h-6 w-6 text-primary' /> <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>
<div className='flex-1 space-y-2'>
<p className='text-left font-semibold text-foreground text-base'>{customer.name}</p> <div className='flex-1 min-w-0 '>
<div className='space-y-1 text-sm text-muted-foreground'> {/* Nombre del cliente */}
{customer.email_primary && ( <h3 className='font-semibold text-foreground text-lg leading-tight mb-1 text-left text-balance'>
<div className='flex items-center gap-2'> {customer.name}
<Mail className='h-3 w-3' /> </h3>
{customer.email_primary}
{/* 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> </div>
)} )}
{customer.mobile_primary && ( {/* Separador si hay dirección */}
<div className='flex items-center gap-2'> {hasAddress && <Separator className='my-3' />}
<Phone className='h-3 w-3' />
{customer.mobile_primary} {/* 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>
)}
{customer.trade_name && (
<div className='flex items-center gap-2'>
<Building2Icon className='h-3 w-3' />
{customer.trade_name}
</div> </div>
)}
{customer.tin && (
<div className='flex items-center gap-2'>
<FileTextIcon className='h-3 w-3' />
{customer.tin}
</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> </div>
</section>
); );
}; };

View File

@ -1,7 +1,14 @@
import { UserSearchIcon } from "lucide-react"; import { UserSearchIcon } from "lucide-react";
export const CustomerEmptyCard = () => { export const CustomerEmptyCard = (props: React.ComponentProps<"button">) => {
return ( return (
<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 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'>
<UserSearchIcon className='size-6 text-muted-foreground group-hover:text-primary' /> <UserSearchIcon className='size-6 text-muted-foreground group-hover:text-primary' />
@ -15,5 +22,6 @@ export const CustomerEmptyCard = () => {
</p> </p>
</div> </div>
</div> </div>
</button>
); );
}; };

View File

@ -1,9 +1,9 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useCustomersSearchQuery } from "../../hooks"; import { useCustomersSearchQuery } from "../../hooks";
import { CustomerSummary, defaultCustomerFormData } from "../../schemas"; import { CustomerSummary, defaultCustomerFormData } from "../../schemas";
import { CreateCustomerFormDialog } from "./create-customer-form-dialog";
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 { CustomerSearchDialog } from "./customer-search-dialog"; import { CustomerSearchDialog } from "./customer-search-dialog";
// Debounce pequeño y tipado // Debounce pequeño y tipado
@ -75,16 +75,16 @@ export const CustomerModalSelector = ({ value, onValueChange }: CustomerModalSel
return ( return (
<> <>
<button <div>
tabIndex={0} {selected ? (
type='button' <CustomerCard customer={selected} onChangeCustomer={() => setShowSearch(true)} />
) : (
<CustomerEmptyCard
onClick={() => setShowSearch(true)} onClick={() => setShowSearch(true)}
onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && 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' )}
> </div>
{selected ? <CustomerCard customer={selected} /> : <CustomerEmptyCard />}
</button>
<CustomerSearchDialog <CustomerSearchDialog
open={showSearch} open={showSearch}
@ -99,8 +99,8 @@ export const CustomerModalSelector = ({ value, onValueChange }: CustomerModalSel
setShowSearch(false); setShowSearch(false);
}} }}
onCreateClient={(name) => { onCreateClient={(name) => {
/*setNewClient({ name: name ?? "", email: "" }); setNewClient({ name: name ?? "", email: "" });
setShowForm(true);*/ setShowForm(true);
}} }}
isLoading={isLoading} isLoading={isLoading}
isError={isError} isError={isError}

View File

@ -16,8 +16,8 @@ import {
} 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 { import {
AsteriskIcon,
Check, Check,
CreditCardIcon,
MailIcon, MailIcon,
SmartphoneIcon, SmartphoneIcon,
User, User,
@ -122,9 +122,10 @@ export const CustomerSearchDialog = ({
</div> </div>
<div className='flex items-center gap-6 text-sm font-medium text-muted-foreground'> <div className='flex items-center gap-6 text-sm font-medium text-muted-foreground'>
{customer.tin && ( {customer.tin && (
<span className='flex items-center gap-0'> <div className='flex items-center gap-2 text-sm text-muted-foreground'>
<AsteriskIcon className='size-4' /> {customer.tin} <CreditCardIcon className='h-4 w-4 shrink-0' />
</span> <span className='font-medium'>{customer.tin}</span>
</div>
)} )}
{customer.email_primary && ( {customer.email_primary && (
<span className='flex items-center gap-1'> <span className='flex items-center gap-1'>

View File

@ -56,8 +56,8 @@
--secondary-foreground: oklch(0.2069 0.0098 285.5081); --secondary-foreground: oklch(0.2069 0.0098 285.5081);
--muted: oklch(0.9674 0.0013 286.3752); --muted: oklch(0.9674 0.0013 286.3752);
--muted-foreground: oklch(0.5466 0.0216 285.664); --muted-foreground: oklch(0.5466 0.0216 285.664);
--accent: oklch(0.9674 0.0013 286.3752); --accent: oklch(0.9299 0.0334 272.7879);
--accent-foreground: oklch(0.2069 0.0098 285.5081); --accent-foreground: oklch(0.3729 0.0306 259.7328);
--destructive: oklch(0.583 0.2387 28.4765); --destructive: oklch(0.583 0.2387 28.4765);
--destructive-foreground: oklch(1.0 0 0); --destructive-foreground: oklch(1.0 0 0);
--border: oklch(0.9173 0.0067 286.2663); --border: oklch(0.9173 0.0067 286.2663);
@ -102,8 +102,8 @@
--secondary-foreground: oklch(0.9851 0 0); --secondary-foreground: oklch(0.9851 0 0);
--muted: oklch(0.2686 0 0); --muted: oklch(0.2686 0 0);
--muted-foreground: oklch(0.709 0 0); --muted-foreground: oklch(0.709 0 0);
--accent: oklch(0.2686 0 0); --accent: oklch(0.3729 0.0306 259.7328);
--accent-foreground: oklch(0.9851 0 0); --accent-foreground: oklch(0.8717 0.0093 258.3382);
--destructive: oklch(0.7022 0.1892 22.2279); --destructive: oklch(0.7022 0.1892 22.2279);
--destructive-foreground: oklch(0.2558 0.0412 235.1561); --destructive-foreground: oklch(0.2558 0.0412 235.1561);
--border: oklch(1.0 0 0); --border: oklch(1.0 0 0);