Clientes y Facturas de cliente

This commit is contained in:
David Arranz 2025-10-20 12:26:29 +02:00
parent e337331650
commit c69a7cd142
12 changed files with 344 additions and 371 deletions

View File

@ -6,43 +6,39 @@ import type { ReactNode } from "react";
interface PageHeaderProps { interface PageHeaderProps {
/** Icono que aparece a la izquierda del título */ backIcon?: ReactNode;
icon?: ReactNode;
/** Contenido del título (texto plano o nodo complejo) */
title: ReactNode; title: ReactNode;
/** Descripción secundaria debajo del título */
description?: ReactNode; description?: ReactNode;
/** Estado opcional (ej. "draft", "paid") */
status?: string; status?: string;
/** Contenido del lado derecho (botones, menús, etc.) */
rightSlot?: ReactNode; rightSlot?: ReactNode;
className?: string; className?: string;
} }
export function PageHeader({ icon, title, description, status, rightSlot, className }: PageHeaderProps) { export function PageHeader({ backIcon, title, description, rightSlot, className }: PageHeaderProps) {
return ( return (
<div className={cn("py-4", className)}> <div className={cn("pt-4 pb-6 bg-background flex items-center justify-between", className)}>
<div className='flex items-center justify-between'> {/* Lado izquierdo */}
{/* Lado izquierdo */} <div className='flex items-center gap-4'>
<div className='flex items-center gap-4'> {backIcon && (
<Button variant="ghost" size="icon" className="cursor-pointer" onClick={() => window.history.back()}> <Button
<ChevronLeftIcon className="size-5" /> variant='ghost'
size='icon'
className='cursor-pointer'
onClick={() => window.history.back()}
>
<ChevronLeftIcon className='size-5' />
</Button> </Button>
{icon && <div className='shrink-0'>{icon}</div>} )}
<div> <div>
<div className='flex items-center gap-3'> <h2 className='text-2xl font-semibold text-foreground'>{title}</h2>
<h1 className='text-xl font-semibold text-foreground'>{title}</h1> {description && <p className='text-base text-muted-foreground'>{description}</p>}
</div>
{description && <p className='text-sm text-muted-foreground'>{description}</p>}
</div>
</div> </div>
{/* Lado derecho parametrizable */}
{rightSlot && <div>{rightSlot}</div>}
</div> </div>
{/* Lado derecho parametrizable */}
{rightSlot && <>{rightSlot}</>}
</div> </div>
); );
} }

View File

@ -34,7 +34,7 @@
"title": "Customer invoices", "title": "Customer invoices",
"description": "Manage your customer invoices", "description": "Manage your customer invoices",
"list": { "list": {
"title": "Customer invoice list", "title": "Customer invoices",
"description": "List all customer invoices", "description": "List all customer invoices",
"grid_columns": { "grid_columns": {
"invoice_number": "Inv. number", "invoice_number": "Inv. number",

View File

@ -33,7 +33,7 @@
"title": "Facturas de clientes", "title": "Facturas de clientes",
"description": "Gestiona tus facturas de clientes", "description": "Gestiona tus facturas de clientes",
"list": { "list": {
"title": "Listado de facturas de clientes", "title": "Facturas de clientes",
"description": "Lista todas las facturas de clientes", "description": "Lista todas las facturas de clientes",
"grid_columns": { "grid_columns": {
"invoice_number": "Nº factura", "invoice_number": "Nº factura",

View File

@ -81,30 +81,24 @@ export const InvoiceListPage = () => {
<AppHeader> <AppHeader>
<PageHeader <PageHeader
title={t("pages.list.title")} title={t("pages.list.title")}
description={t("pages.list.description")}
rightSlot={ rightSlot={
<></>} <div className='flex items-center space-x-2'>
<Button
onClick={() => navigate("/customer-invoices/create")}
variant={'default'}
aria-label={t("pages.create.title")}
className='cursor-pointer'
>
<PlusIcon className="mr-2 h-4 w-4" aria-hidden />
{t("pages.create.title")}
</Button>
</div>
}
/> />
</AppHeader> </AppHeader>
<AppContent> <AppContent>
<div className='flex items-center justify-between space-y-6'>
<div>
<h2 className='text-2xl font-bold tracking-tight'>{t("pages.list.title")}</h2>
<p className='text-muted-foreground'>{t("pages.list.description")}</p>
</div>
<div className='flex items-center space-x-2'>
<Button
onClick={() => navigate("/customer-invoices/create")}
variant={'default'}
aria-label={t("pages.create.title")}
className='cursor-pointer'
>
<PlusIcon className="mr-2 h-4 w-4" aria-hidden />
{t("pages.create.title")}
</Button>
</div>
</div>
<div className='flex flex-col w-full h-full py-3'> <div className='flex flex-col w-full h-full py-3'>
<div className={"flex-1"}> <div className={"flex-1"}>
<InvoicesListGrid <InvoicesListGrid

View File

@ -84,7 +84,9 @@ export const InvoiceUpdateComp = ({
<UnsavedChangesProvider isDirty={form.formState.isDirty}> <UnsavedChangesProvider isDirty={form.formState.isDirty}>
<AppHeader> <AppHeader>
<PageHeader <PageHeader
title={`${t("pages.edit.title")} ${invoiceData.invoice_number}`} backIcon
title={`${t("pages.edit.title")} #${invoiceData.invoice_number}`}
description={t("pages.edit.description")}
rightSlot={ rightSlot={
<UpdateCommitButtonGroup <UpdateCommitButtonGroup
isLoading={isPending} isLoading={isPending}
@ -93,9 +95,6 @@ export const InvoiceUpdateComp = ({
cancel={{ to: "/customer-invoices/list" }} cancel={{ to: "/customer-invoices/list" }}
onBack={() => navigate(-1)} onBack={() => navigate(-1)}
/> />
} }
/> />
</AppHeader> </AppHeader>
@ -106,7 +105,7 @@ export const InvoiceUpdateComp = ({
formId="invoice-update-form" formId="invoice-update-form"
onSubmit={handleSubmit} onSubmit={handleSubmit}
onError={handleError} onError={handleError}
className="max-w-full" className="bg-white rounded-xl border shadow-xl max-w-full"
/> />
</FormProvider> </FormProvider>
</AppContent> </AppContent>

View File

@ -9,7 +9,7 @@ interface InvoiceUpdateFormProps {
formId: string; formId: string;
onSubmit: (data: InvoiceFormData) => void; onSubmit: (data: InvoiceFormData) => void;
onError: (errors: FieldErrors<InvoiceFormData>) => void; onError: (errors: FieldErrors<InvoiceFormData>) => void;
className: string; className?: string;
} }
export const InvoiceUpdateForm = ({ export const InvoiceUpdateForm = ({
@ -22,7 +22,7 @@ export const InvoiceUpdateForm = ({
return ( return (
<form noValidate id={formId} onSubmit={form.handleSubmit(onSubmit, onError)} > <form noValidate id={formId} onSubmit={form.handleSubmit(onSubmit, onError)} >
<section className={cn("bg-white rounded-xl border shadow-xl space-y-6", className)}> <section className={cn("p-6 space-y-6", className)}>
<div className="w-full p-6 bg-transparent grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="w-full p-6 bg-transparent grid grid-cols-1 lg:grid-cols-3 gap-6">
<InvoiceRecipient className="flex flex-col" /> <InvoiceRecipient className="flex flex-col" />
<InvoiceBasicInfoFields className="flex flex-col lg:col-span-2" /> <InvoiceBasicInfoFields className="flex flex-col lg:col-span-2" />

View File

@ -1,6 +1,7 @@
import { FormDebug } from "@erp/core/components"; import { FormDebug } from "@erp/core/components";
import { FieldErrors, useFormContext } from "react-hook-form"; import { FieldErrors, useFormContext } from "react-hook-form";
import { cn } from '@repo/shadcn-ui/lib/utils';
import { CustomerFormData } from "../../schemas"; import { CustomerFormData } from "../../schemas";
import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields"; import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields";
import { CustomerAddressFields } from "./customer-address-fields"; import { CustomerAddressFields } from "./customer-address-fields";
@ -11,24 +12,27 @@ interface CustomerFormProps {
formId: string; formId: string;
onSubmit: (data: CustomerFormData) => void; onSubmit: (data: CustomerFormData) => void;
onError: (errors: FieldErrors<CustomerFormData>) => void; onError: (errors: FieldErrors<CustomerFormData>) => void;
className?: string;
} }
export const CustomerEditForm = ({ formId, onSubmit, onError }: CustomerFormProps) => { export const CustomerEditForm = ({ formId, onSubmit, onError, className }: CustomerFormProps) => {
const form = useFormContext<CustomerFormData>(); const form = useFormContext<CustomerFormData>();
return ( return (
<form id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}> <form id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}>
<div className='xl:flex xl:flex-row-reverse xl:items-start'> <section className={cn("p-6", className)}>
<div className='w-full xl:w-6/12'> <div className='xl:flex xl:flex-row-reverse xl:items-start'>
<FormDebug /> <div className='w-full xl:w-6/12'>
<FormDebug />
</div>
<div className='w-full xl:grow space-y-6'>
<CustomerBasicInfoFields />
<CustomerContactFields />
<CustomerAddressFields />
<CustomerAdditionalConfigFields />
</div>
</div> </div>
<div className='w-full xl:grow space-y-6'> </section>
<CustomerBasicInfoFields />
<CustomerContactFields />
<CustomerAddressFields />
<CustomerAdditionalConfigFields />
</div>
</div>
</form> </form>
); );
}; };

View File

@ -1,5 +1,5 @@
import { PageHeader } from '@erp/core/components'; import { PageHeader } from '@erp/core/components';
import { AppBreadcrumb, AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components"; import { AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
@ -72,33 +72,25 @@ export const CustomersListPage = () => {
return ( return (
<> <>
<AppHeader> <AppHeader>
<AppBreadcrumb />
<PageHeader <PageHeader
title={t("pages.list.title")} title={t("pages.list.title")}
description={t("pages.list.description")}
rightSlot={ rightSlot={
<></>} <div className='flex items-center space-x-2'>
<Button
onClick={() => navigate("/customers/create")}
variant={'default'}
aria-label={t("pages.create.title")}
className='cursor-pointer'
>
<PlusIcon className="mr-2 h-4 w-4" aria-hidden />
{t("pages.create.title")}
</Button>
</div>
}
/> />
</AppHeader> </AppHeader>
<AppContent> <AppContent>
<div className='flex items-center justify-between space-y-6'>
<div>
<h2 className='text-2xl font-bold tracking-tight'>{t("pages.list.title")}</h2>
<p className='text-muted-foreground'>{t("pages.list.description")}</p>
</div>
<div className='flex items-center space-x-2'>
<Button
onClick={() => navigate("/customer-invoices/create")}
variant={'default'}
aria-label={t("pages.create.title")}
className='cursor-pointer'
>
<PlusIcon className="mr-2 h-4 w-4" aria-hidden />
{t("pages.create.title")}
</Button>
</div>
</div>
<div className='flex flex-col w-full h-full py-3'> <div className='flex flex-col w-full h-full py-3'>
<div className={"flex-1"}> <div className={"flex-1"}>
<CustomersListGrid <CustomersListGrid

View File

@ -1,7 +1,8 @@
import { AppBreadcrumb, AppContent, BackHistoryButton } from "@repo/rdx-ui/components"; import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { formHasAnyDirty, pickFormDirtyValues } from "@erp/core/client"; import { formHasAnyDirty, pickFormDirtyValues } from "@erp/core/client";
import { PageHeader } from '@erp/core/components';
import { import {
UnsavedChangesProvider, UnsavedChangesProvider,
UpdateCommitButtonGroup, UpdateCommitButtonGroup,
@ -92,7 +93,7 @@ export const CustomerUpdatePage = () => {
if (isLoadError) { if (isLoadError) {
return ( return (
<> <>
<AppBreadcrumb />
<AppContent> <AppContent>
<ErrorAlert <ErrorAlert
title={t("pages.update.loadErrorTitle", "No se pudo cargar el cliente")} title={t("pages.update.loadErrorTitle", "No se pudo cargar el cliente")}
@ -113,7 +114,7 @@ export const CustomerUpdatePage = () => {
if (!customerData) if (!customerData)
return ( return (
<> <>
<AppBreadcrumb />
<AppContent> <AppContent>
<NotFoundCard <NotFoundCard
title={t("pages.update.notFoundTitle", "Cliente no encontrado")} title={t("pages.update.notFoundTitle", "Cliente no encontrado")}
@ -124,19 +125,13 @@ export const CustomerUpdatePage = () => {
); );
return ( return (
<> <UnsavedChangesProvider isDirty={form.formState.isDirty}>
<AppBreadcrumb /> <AppHeader>
<AppContent> <PageHeader
<UnsavedChangesProvider isDirty={form.formState.isDirty}> backIcon
<div className='flex items-center justify-between space-y-6'> title={t("pages.update.title")}
<div className='space-y-2'> description={t("pages.update.description")}
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'> rightSlot={
{t("pages.update.title")}
</h2>
<p className='text-muted-foreground scroll-m-20 tracking-tight text-balance'>
{t("pages.update.description")}
</p>
</div>
<UpdateCommitButtonGroup <UpdateCommitButtonGroup
isLoading={isUpdating} isLoading={isUpdating}
disabled={isUpdating} disabled={isUpdating}
@ -151,27 +146,31 @@ export const CustomerUpdatePage = () => {
onBack={() => handleBack()} onBack={() => handleBack()}
onReset={() => handleReset()} onReset={() => handleReset()}
/> />
</div> }
{/* Alerta de error de actualización (si ha fallado el último intento) */} />
{isUpdateError && ( </AppHeader>
<ErrorAlert <AppContent>
title={t("pages.update.errorTitle", "No se pudo guardar los cambios")} {/* Alerta de error de actualización (si ha fallado el último intento) */}
message={ {isUpdateError && (
(updateError as Error)?.message ?? <ErrorAlert
t("pages.update.errorMsg", "Revisa los datos e inténtalo de nuevo.") title={t("pages.update.errorTitle", "No se pudo guardar los cambios")}
} message={
/> (updateError as Error)?.message ??
)} t("pages.update.errorMsg", "Revisa los datos e inténtalo de nuevo.")
}
/>
)}
<FormProvider {...form}>
<CustomerEditForm
formId={"customer-update-form"} // para que el botón del header pueda hacer submit
onSubmit={handleSubmit}
onError={handleError}
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto"
/>
</FormProvider>
<FormProvider {...form}>
<CustomerEditForm
formId={"customer-update-form"} // para que el botón del header pueda hacer submit
onSubmit={handleSubmit}
onError={handleError}
/>
</FormProvider>
</UnsavedChangesProvider>
</AppContent> </AppContent>
</> </UnsavedChangesProvider>
); );
}; };

View File

@ -1,8 +1,7 @@
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components"; import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
import { Button, Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components"; import { Button, Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
import { import {
Banknote, Banknote,
Building2,
EditIcon, EditIcon,
FileText, FileText,
Globe, Globe,
@ -11,11 +10,11 @@ import {
MapPin, MapPin,
MoreVertical, MoreVertical,
Phone, Phone,
Smartphone, Smartphone
User,
} from "lucide-react"; } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { PageHeader } from '@erp/core/components';
import { useUrlParamId } from "@erp/core/hooks"; import { useUrlParamId } from "@erp/core/hooks";
import { Badge } from "@repo/shadcn-ui/components"; import { Badge } from "@repo/shadcn-ui/components";
import { CustomerEditorSkeleton, ErrorAlert } from "../../components"; import { CustomerEditorSkeleton, ErrorAlert } from "../../components";
@ -61,28 +60,17 @@ export const CustomerViewPage = () => {
return ( return (
<> <>
<AppContent> <AppHeader>
<div className='space-y-6 max-w-4xl'> <PageHeader
{/* Header */} backIcon
<div className='flex items-start justify-between'> title={(<div className="flex flex-wrap items-center gap-2">{customer?.name} {customer?.trade_name && <span className="text-muted-foreground">({customer.trade_name})</span>}</div>)}
<div className='flex items-start gap-4'> description={<div className='mt-2 flex items-center gap-3'>
<div className='flex h-16 w-16 items-center justify-center rounded-lg'> <Badge variant='secondary' className='font-mono'>
{customer?.is_company ? ( {customer?.tin}
<Building2 className='size-8 text-primary' /> </Badge>
) : ( <Badge variant='outline'>{customer?.is_company ? "Empresa" : "Persona"}</Badge>
<User className='size-8 text-primary' /> </div>}
)} rightSlot={
</div>
<div>
<h1 className='text-3xl font-bold text-foreground'>{customer?.name}</h1>
<div className='mt-2 flex items-center gap-3'>
<Badge variant='secondary' className='font-mono'>
{customer?.reference}
</Badge>
<Badge variant='outline'>{customer?.is_company ? "Empresa" : "Persona"}</Badge>
</div>
</div>
</div>
<div className='flex gap-2'> <div className='flex gap-2'>
<Button variant='outline' size='icon' onClick={() => navigate("/customers/list")}> <Button variant='outline' size='icon' onClick={() => navigate("/customers/list")}>
<MoreVertical className='h-4 w-4' /> <MoreVertical className='h-4 w-4' />
@ -92,248 +80,249 @@ export const CustomerViewPage = () => {
Editar Editar
</Button> </Button>
</div> </div>
</div> }
/>
</AppHeader>
<AppContent>
{/* Main Content Grid */}
<div className='grid gap-6 md:grid-cols-2'>
{/* Información Básica */}
<Card>
<CardHeader>
<CardTitle className='flex items-center gap-2 text-lg'>
<FileText className='size-5 text-primary' />
Información Básica
</CardTitle>
</CardHeader>
<CardContent className='space-y-4'>
<div>
<dt className='text-sm font-medium text-muted-foreground'>Nombre</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.name}</dd>
</div>
<div>
<dt className='text-sm font-medium text-muted-foreground'>Referencia</dt>
<dd className='mt-1 font-mono text-base text-foreground'>
{customer?.reference}
</dd>
</div>
<div>
<dt className='text-sm font-medium text-muted-foreground'>Registro Legal</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.legal_record}</dd>
</div>
<div>
<dt className='text-sm font-medium text-muted-foreground'>
Impuestos por Defecto
</dt>
<dd className='mt-1'>
{customer?.default_taxes.map((tax) => (<Badge key={tax} variant={"secondary"}>{tax}</Badge>))}
</dd>
</div>
</CardContent>
</Card>
{/* Main Content Grid */} {/* Dirección */}
<div className='grid gap-6 md:grid-cols-2'> <Card>
{/* Información Básica */} <CardHeader>
<Card> <CardTitle className='flex items-center gap-2 text-lg'>
<CardHeader> <MapPin className='size-5 text-primary' />
<CardTitle className='flex items-center gap-2 text-lg'> Dirección
<FileText className='size-5 text-primary' /> </CardTitle>
Información Básica </CardHeader>
</CardTitle> <CardContent className='space-y-4'>
</CardHeader> <div>
<CardContent className='space-y-4'> <dt className='text-sm font-medium text-muted-foreground'>Calle</dt>
<dd className='mt-1 text-base text-foreground'>
{customer?.street}
{customer?.street2 && (
<>
<br />
{customer?.street2}
</>
)}
</dd>
</div>
<div className='grid grid-cols-2 gap-4'>
<div> <div>
<dt className='text-sm font-medium text-muted-foreground'>Nombre</dt> <dt className='text-sm font-medium text-muted-foreground'>Ciudad</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.name}</dd> <dd className='mt-1 text-base text-foreground'>{customer?.city}</dd>
</div> </div>
<div> <div>
<dt className='text-sm font-medium text-muted-foreground'>Referencia</dt> <dt className='text-sm font-medium text-muted-foreground'>Código Postal</dt>
<dd className='mt-1 font-mono text-base text-foreground'> <dd className='mt-1 text-base text-foreground'>{customer?.postal_code}</dd>
{customer?.reference} </div>
</dd> </div>
<div className='grid grid-cols-2 gap-4'>
<div>
<dt className='text-sm font-medium text-muted-foreground'>Provincia</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.province}</dd>
</div> </div>
<div> <div>
<dt className='text-sm font-medium text-muted-foreground'>Registro Legal</dt> <dt className='text-sm font-medium text-muted-foreground'>País</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.legalRecord}</dd> <dd className='mt-1 text-base text-foreground'>{customer?.country}</dd>
</div> </div>
<div> </div>
<dt className='text-sm font-medium text-muted-foreground'> </CardContent>
Impuestos por Defecto </Card>
</dt>
<dd className='mt-1'>
<Badge className='bg-blue-600 hover:bg-blue-700'>{customer?.defaultTax}</Badge>
</dd>
</div>
</CardContent>
</Card>
{/* Dirección */} {/* Información de Contacto */}
<Card> <Card className='md:col-span-2'>
<CardHeader> <CardHeader>
<CardTitle className='flex items-center gap-2 text-lg'> <CardTitle className='flex items-center gap-2 text-lg'>
<MapPin className='size-5 text-primary' /> <Mail className='size-5 text-primary' />
Dirección Información de Contacto
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className='space-y-4'> <CardContent>
<div> <div className='grid gap-6 md:grid-cols-2'>
<dt className='text-sm font-medium text-muted-foreground'>Calle</dt> {/* Contacto Principal */}
<dd className='mt-1 text-base text-foreground'> <div className='space-y-4'>
{customer?.street1} <h3 className='font-semibold text-foreground'>Contacto Principal</h3>
{customer?.street2 && ( {customer?.email_primary && (
<> <div className='flex items-start gap-3'>
<br /> <Mail className='mt-0.5 h-4 w-4 text-muted-foreground' />
{customer?.street2} <div className='flex-1'>
</> <dt className='text-sm font-medium text-muted-foreground'>Email</dt>
)} <dd className='mt-1 text-base text-foreground'>
</dd> {customer?.email_primary}
</div> </dd>
<div className='grid grid-cols-2 gap-4'>
<div>
<dt className='text-sm font-medium text-muted-foreground'>Ciudad</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.city}</dd>
</div>
<div>
<dt className='text-sm font-medium text-muted-foreground'>Código Postal</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.postal_code}</dd>
</div>
</div>
<div className='grid grid-cols-2 gap-4'>
<div>
<dt className='text-sm font-medium text-muted-foreground'>Provincia</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.province}</dd>
</div>
<div>
<dt className='text-sm font-medium text-muted-foreground'>País</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.country}</dd>
</div>
</div>
</CardContent>
</Card>
{/* Información de Contacto */}
<Card className='md:col-span-2'>
<CardHeader>
<CardTitle className='flex items-center gap-2 text-lg'>
<Mail className='size-5 text-primary' />
Información de Contacto
</CardTitle>
</CardHeader>
<CardContent>
<div className='grid gap-6 md:grid-cols-2'>
{/* Contacto Principal */}
<div className='space-y-4'>
<h3 className='font-semibold text-foreground'>Contacto Principal</h3>
{customer?.email_primary && (
<div className='flex items-start gap-3'>
<Mail className='mt-0.5 h-4 w-4 text-muted-foreground' />
<div className='flex-1'>
<dt className='text-sm font-medium text-muted-foreground'>Email</dt>
<dd className='mt-1 text-base text-foreground'>
{customer?.email_primary}
</dd>
</div>
</div> </div>
)} </div>
{customer?.mobile_primary && ( )}
<div className='flex items-start gap-3'> {customer?.mobile_primary && (
<Smartphone className='mt-0.5 h-4 w-4 text-muted-foreground' /> <div className='flex items-start gap-3'>
<div className='flex-1'> <Smartphone className='mt-0.5 h-4 w-4 text-muted-foreground' />
<dt className='text-sm font-medium text-muted-foreground'>Móvil</dt> <div className='flex-1'>
<dd className='mt-1 text-base text-foreground'> <dt className='text-sm font-medium text-muted-foreground'>Móvil</dt>
{customer?.mobile_primary} <dd className='mt-1 text-base text-foreground'>
</dd> {customer?.mobile_primary}
</div> </dd>
</div> </div>
)} </div>
{customer?.phone_primary && ( )}
<div className='flex items-start gap-3'> {customer?.phone_primary && (
<Phone className='mt-0.5 h-4 w-4 text-muted-foreground' /> <div className='flex items-start gap-3'>
<div className='flex-1'> <Phone className='mt-0.5 h-4 w-4 text-muted-foreground' />
<dt className='text-sm font-medium text-muted-foreground'>Teléfono</dt> <div className='flex-1'>
<dd className='mt-1 text-base text-foreground'> <dt className='text-sm font-medium text-muted-foreground'>Teléfono</dt>
{customer?.phone_primary} <dd className='mt-1 text-base text-foreground'>
</dd> {customer?.phone_primary}
</div> </dd>
</div>
)}
</div>
{/* Contacto Secundario */}
<div className='space-y-4'>
<h3 className='font-semibold text-foreground'>Contacto Secundario</h3>
{customer?.email_secondary && (
<div className='flex items-start gap-3'>
<Mail className='mt-0.5 h-4 w-4 text-muted-foreground' />
<div className='flex-1'>
<dt className='text-sm font-medium text-muted-foreground'>Email</dt>
<dd className='mt-1 text-base text-foreground'>
{customer?.email_secondary}
</dd>
</div>
</div>
)}
{customer?.mobile_secondary && (
<div className='flex items-start gap-3'>
<Smartphone className='mt-0.5 h-4 w-4 text-muted-foreground' />
<div className='flex-1'>
<dt className='text-sm font-medium text-muted-foreground'>Móvil</dt>
<dd className='mt-1 text-base text-foreground'>
{customer?.mobile_secondary}
</dd>
</div>
</div>
)}
{customer?.phone_secondary && (
<div className='flex items-start gap-3'>
<Phone className='mt-0.5 h-4 w-4 text-muted-foreground' />
<div className='flex-1'>
<dt className='text-sm font-medium text-muted-foreground'>Teléfono</dt>
<dd className='mt-1 text-base text-foreground'>
{customer?.phone_secondary}
</dd>
</div>
</div>
)}
</div>
{/* Otros Contactos */}
{(customer?.website || customer?.fax) && (
<div className='space-y-4 md:col-span-2'>
<h3 className='font-semibold text-foreground'>Otros</h3>
<div className='grid gap-4 md:grid-cols-2'>
{customer?.website && (
<div className='flex items-start gap-3'>
<Globe className='mt-0.5 h-4 w-4 text-muted-foreground' />
<div className='flex-1'>
<dt className='text-sm font-medium text-muted-foreground'>
Sitio Web
</dt>
<dd className='mt-1 text-base text-primary hover:underline'>
<a
href={customer?.website}
target='_blank'
rel='noopener noreferrer'
>
{customer?.website}
</a>
</dd>
</div>
</div>
)}
{customer?.fax && (
<div className='flex items-start gap-3'>
<Phone className='mt-0.5 h-4 w-4 text-muted-foreground' />
<div className='flex-1'>
<dt className='text-sm font-medium text-muted-foreground'>Fax</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.fax}</dd>
</div>
</div>
)}
</div> </div>
</div> </div>
)} )}
</div> </div>
</CardContent>
</Card>
{/* Preferencias */} {/* Contacto Secundario */}
<Card className='md:col-span-2'> <div className='space-y-4'>
<CardHeader> <h3 className='font-semibold text-foreground'>Contacto Secundario</h3>
<CardTitle className='flex items-center gap-2 text-lg'> {customer?.email_secondary && (
<Languages className='size-5 text-primary' /> <div className='flex items-start gap-3'>
Preferencias <Mail className='mt-0.5 h-4 w-4 text-muted-foreground' />
</CardTitle> <div className='flex-1'>
</CardHeader> <dt className='text-sm font-medium text-muted-foreground'>Email</dt>
<CardContent> <dd className='mt-1 text-base text-foreground'>
<div className='grid gap-6 md:grid-cols-2'> {customer?.email_secondary}
<div className='flex items-start gap-3'> </dd>
<Languages className='mt-0.5 h-4 w-4 text-muted-foreground' /> </div>
<div className='flex-1'> </div>
<dt className='text-sm font-medium text-muted-foreground'> )}
Idioma Preferido {customer?.mobile_secondary && (
</dt> <div className='flex items-start gap-3'>
<dd className='mt-1 text-base text-foreground'>{customer?.language_code}</dd> <Smartphone className='mt-0.5 h-4 w-4 text-muted-foreground' />
<div className='flex-1'>
<dt className='text-sm font-medium text-muted-foreground'>Móvil</dt>
<dd className='mt-1 text-base text-foreground'>
{customer?.mobile_secondary}
</dd>
</div>
</div>
)}
{customer?.phone_secondary && (
<div className='flex items-start gap-3'>
<Phone className='mt-0.5 h-4 w-4 text-muted-foreground' />
<div className='flex-1'>
<dt className='text-sm font-medium text-muted-foreground'>Teléfono</dt>
<dd className='mt-1 text-base text-foreground'>
{customer?.phone_secondary}
</dd>
</div>
</div>
)}
</div>
{/* Otros Contactos */}
{(customer?.website || customer?.fax) && (
<div className='space-y-4 md:col-span-2'>
<h3 className='font-semibold text-foreground'>Otros</h3>
<div className='grid gap-4 md:grid-cols-2'>
{customer?.website && (
<div className='flex items-start gap-3'>
<Globe className='mt-0.5 h-4 w-4 text-muted-foreground' />
<div className='flex-1'>
<dt className='text-sm font-medium text-muted-foreground'>
Sitio Web
</dt>
<dd className='mt-1 text-base text-primary hover:underline'>
<a
href={customer?.website}
target='_blank'
rel='noopener noreferrer'
>
{customer?.website}
</a>
</dd>
</div>
</div>
)}
{customer?.fax && (
<div className='flex items-start gap-3'>
<Phone className='mt-0.5 h-4 w-4 text-muted-foreground' />
<div className='flex-1'>
<dt className='text-sm font-medium text-muted-foreground'>Fax</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.fax}</dd>
</div>
</div>
)}
</div> </div>
</div> </div>
<div className='flex items-start gap-3'> )}
<Banknote className='mt-0.5 h-4 w-4 text-muted-foreground' /> </div>
<div className='flex-1'> </CardContent>
<dt className='text-sm font-medium text-muted-foreground'> </Card>
Moneda Preferida
</dt> {/* Preferencias */}
<dd className='mt-1 text-base text-foreground'>{customer?.currency_code}</dd> <Card className='md:col-span-2'>
</div> <CardHeader>
<CardTitle className='flex items-center gap-2 text-lg'>
<Languages className='size-5 text-primary' />
Preferencias
</CardTitle>
</CardHeader>
<CardContent>
<div className='grid gap-6 md:grid-cols-2'>
<div className='flex items-start gap-3'>
<Languages className='mt-0.5 h-4 w-4 text-muted-foreground' />
<div className='flex-1'>
<dt className='text-sm font-medium text-muted-foreground'>
Idioma Preferido
</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.language_code}</dd>
</div> </div>
</div> </div>
</CardContent> <div className='flex items-start gap-3'>
</Card> <Banknote className='mt-0.5 h-4 w-4 text-muted-foreground' />
</div> <div className='flex-1'>
<dt className='text-sm font-medium text-muted-foreground'>
Moneda Preferida
</dt>
<dd className='mt-1 text-base text-foreground'>{customer?.currency_code}</dd>
</div>
</div>
</div>
</CardContent>
</Card>
</div> </div>
</AppContent> </AppContent >
</> </>
); );
}; };

View File

@ -7,7 +7,7 @@ export const AppHeader = ({
...props ...props
}: PropsWithChildren<{ className?: string }>) => { }: PropsWithChildren<{ className?: string }>) => {
return ( return (
<div className={cn("app-header bg-background gap-4 px-6 pt-0 border-b bg-card", className)} {...props}> <div className={cn("app-header gap-4 px-6 pt-0 border-b bg-background", className)} {...props}>
{children} {children}
</div> </div>
); );

View File

@ -14,7 +14,7 @@ export const AppLayout = () => {
> >
<AppSidebar variant='inset' /> <AppSidebar variant='inset' />
{/* Aquí está el MAIN */} {/* Aquí está el MAIN */}
<SidebarInset className='app-main bg-background'> <SidebarInset className='app-main'>
<Outlet /> <Outlet />
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>