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";
|
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 (
|
||||||
<div className='flex items-start gap-4'>
|
<section>
|
||||||
<div className='flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 border border-primary/20'>
|
<div className='flex items-start gap-4'>
|
||||||
<User className='h-6 w-6 text-primary' />
|
{/* Avatar mejorado con gradiente sutil */}
|
||||||
</div>
|
<div className='flex size-12 items-center justify-center rounded-full bg-muted group-hover:bg-primary/15'>
|
||||||
<div className='flex-1 space-y-2'>
|
<UserIcon className='size-6 text-muted-foreground group-hover:text-primary' />
|
||||||
<p className='text-left font-semibold text-foreground text-base'>{customer.name}</p>
|
</div>
|
||||||
<div className='space-y-1 text-sm text-muted-foreground'>
|
|
||||||
{customer.email_primary && (
|
<div className='flex-1 min-w-0 '>
|
||||||
<div className='flex items-center gap-2'>
|
{/* Nombre del cliente */}
|
||||||
<Mail className='h-3 w-3' />
|
<h3 className='font-semibold text-foreground text-lg leading-tight mb-1 text-left text-balance'>
|
||||||
{customer.email_primary}
|
{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>
|
</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 */}
|
||||||
</div>
|
{hasAddress && (
|
||||||
)}
|
<div className='space-y-2'>
|
||||||
{customer.trade_name && (
|
<div className='flex items-start gap-2 text-sm text-muted-foreground'>
|
||||||
<div className='flex items-center gap-2'>
|
<MapPinHouseIcon className='h-4 w-4 shrink-0 mt-0.5 text-primary/60' />
|
||||||
<Building2Icon className='h-3 w-3' />
|
<div className='space-y-0.5 leading-relaxed flex-1 text-left'>
|
||||||
{customer.trade_name}
|
{customer.street && <div>{customer.street}</div>}
|
||||||
</div>
|
{customer.street2 && <div>{customer.street2}</div>}
|
||||||
)}
|
<div className='flex flex-wrap gap-x-2'>
|
||||||
{customer.tin && (
|
{customer.postal_code && <span>{customer.postal_code}</span>}
|
||||||
<div className='flex items-center gap-2'>
|
{customer.city && <span>{customer.city}</span>}
|
||||||
<FileTextIcon className='h-3 w-3' />
|
</div>
|
||||||
{customer.tin}
|
<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>
|
||||||
</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";
|
import { UserSearchIcon } from "lucide-react";
|
||||||
|
|
||||||
export const CustomerEmptyCard = () => {
|
export const CustomerEmptyCard = (props: React.ComponentProps<"button">) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-4 group-hover:text-primary'>
|
<button
|
||||||
<div className='flex size-12 items-center justify-center rounded-full bg-muted group-hover:bg-primary/15'>
|
tabIndex={0}
|
||||||
<UserSearchIcon className='size-6 text-muted-foreground group-hover:text-primary' />
|
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>
|
||||||
<div className='flex-1 text-left'>
|
</button>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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)} />
|
||||||
onClick={() => setShowSearch(true)}
|
) : (
|
||||||
onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && setShowSearch(true)}
|
<CustomerEmptyCard
|
||||||
className='group w-full cursor-pointer rounded-lg border border-border bg-card p-4 transition hover:bg-accent/50 hover:border-primary'
|
onClick={() => setShowSearch(true)}
|
||||||
aria-label='Seleccionar cliente'
|
onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && setShowSearch(true)}
|
||||||
>
|
/>
|
||||||
{selected ? <CustomerCard customer={selected} /> : <CustomerEmptyCard />}
|
)}
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
<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}
|
||||||
|
|||||||
@ -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'>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user