.
This commit is contained in:
parent
d216119a91
commit
482dd5ef56
@ -15,20 +15,20 @@ interface CustomerCardProps {
|
||||
|
||||
function buildAddress(customer: CustomerSummary) {
|
||||
// Línea 1: calle(s)
|
||||
const line1 = [customer.street, customer.street2].filter(Boolean).join(", ");
|
||||
const line1 = [customer.address.street, customer.address.street2].filter(Boolean).join(", ");
|
||||
|
||||
// Línea 2: CP + ciudad con espacio no rompible entre CP y ciudad
|
||||
const line2Parts: string[] = [];
|
||||
if (customer.postal_code && customer.city) {
|
||||
line2Parts.push(`${customer.postal_code}\u00A0${customer.city}`); // CP Ciudad
|
||||
if (customer.address.postal_code && customer.address.city) {
|
||||
line2Parts.push(`${customer.address.postal_code}\u00A0${customer.address.city}`); // CP Ciudad
|
||||
} else {
|
||||
if (customer.postal_code) line2Parts.push(customer.postal_code);
|
||||
if (customer.city) line2Parts.push(customer.city);
|
||||
if (customer.address.postal_code) line2Parts.push(customer.address.postal_code);
|
||||
if (customer.address.city) line2Parts.push(customer.address.city);
|
||||
}
|
||||
const line2 = line2Parts.join(" ");
|
||||
|
||||
// Línea 3: provincia + país
|
||||
const line3 = [customer.province, customer.country].filter(Boolean).join(", ");
|
||||
const line3 = [customer.address.province, customer.address.country].filter(Boolean).join(", ");
|
||||
|
||||
const stack = [line1, line2, line3].filter(Boolean);
|
||||
const inline = stack.join(" · "); // separador compacto
|
||||
|
||||
@ -1,5 +1,23 @@
|
||||
import type { CustomerSelectionOption } from "@erp/customers";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import {
|
||||
Building2Icon,
|
||||
ExternalLinkIcon,
|
||||
MailIcon,
|
||||
MapPinIcon,
|
||||
PhoneIcon,
|
||||
PlusIcon,
|
||||
RefreshCwIcon,
|
||||
UserIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../../i18n";
|
||||
|
||||
@ -8,8 +26,40 @@ type SelectedRecipientSummaryProps = {
|
||||
readOnly?: boolean;
|
||||
|
||||
recipient?: CustomerSelectionOption | null;
|
||||
|
||||
onChangeClick: () => void;
|
||||
onCreateClick?: () => void;
|
||||
onViewClick?: (recipient: CustomerSelectionOption) => void;
|
||||
};
|
||||
|
||||
const buildCustomerAddress = (recipient: CustomerSelectionOption): string => {
|
||||
return [
|
||||
recipient.street,
|
||||
recipient.street2,
|
||||
[recipient.postalCode, recipient.city].filter(Boolean).join(" "),
|
||||
recipient.province,
|
||||
recipient.country,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
};
|
||||
|
||||
const getCustomerStatusBadgeClassName = (status: string): string => {
|
||||
const normalizedStatus = status.toLowerCase();
|
||||
|
||||
if (["active", "activo", "enabled"].includes(normalizedStatus)) {
|
||||
return "border-emerald-200 bg-emerald-50 text-emerald-700";
|
||||
}
|
||||
|
||||
if (["inactive", "inactivo", "disabled"].includes(normalizedStatus)) {
|
||||
return "border-muted bg-muted text-muted-foreground";
|
||||
}
|
||||
|
||||
if (["blocked", "bloqueado"].includes(normalizedStatus)) {
|
||||
return "border-destructive/20 bg-destructive/10 text-destructive";
|
||||
}
|
||||
|
||||
return "border-border bg-muted/50 text-muted-foreground";
|
||||
};
|
||||
|
||||
export const SelectedRecipientSummary = ({
|
||||
@ -19,43 +69,177 @@ export const SelectedRecipientSummary = ({
|
||||
recipient,
|
||||
onChangeClick,
|
||||
onCreateClick,
|
||||
onViewClick,
|
||||
}: SelectedRecipientSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showActions = !(readOnly || disabled);
|
||||
const address = recipient ? buildCustomerAddress(recipient) : "";
|
||||
const phone = recipient?.primaryPhone ?? recipient?.primaryMobile;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div className="min-w-0">
|
||||
{recipient ? (
|
||||
<div className="mt-2 space-y-1">
|
||||
<p className="truncate font-medium">{recipient.name}</p>
|
||||
{recipient.tin ? (
|
||||
<p className="truncate text-sm text-muted-foreground">{recipient.tin}</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{t("customers.selected_customer.empty", "No hay ningún cliente seleccionado")}
|
||||
</p>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border p-4",
|
||||
recipient ? "border-primary/20 bg-primary/[0.03]" : "bg-muted/20",
|
||||
disabled && "opacity-70"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:justify-between">
|
||||
<div className="flex min-w-0 flex-1 gap-4">
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-14 shrink-0 items-center justify-center rounded-full",
|
||||
recipient ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{recipient?.isCompany ? (
|
||||
<Building2Icon className="size-6" />
|
||||
) : (
|
||||
<UserIcon className="size-6" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 space-y-2">
|
||||
{recipient ? (
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(260px,0.8fr)]">
|
||||
<div className="min-w-0 space-y-2">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<p className="truncate text-base font-semibold">{recipient.name}</p>
|
||||
|
||||
{recipient.status ? (
|
||||
<Badge
|
||||
className={cn(
|
||||
"border",
|
||||
getCustomerStatusBadgeClassName(recipient.status)
|
||||
)}
|
||||
variant="outline"
|
||||
>
|
||||
{recipient.status}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{recipient.tradeName && recipient.tradeName !== recipient.name ? (
|
||||
<p className="truncate text-sm text-muted-foreground">
|
||||
{recipient.tradeName}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{recipient.tin ? (
|
||||
<p className="text-xs text-muted-foreground">{recipient.tin}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
{recipient.primaryEmail ? (
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<MailIcon className="size-3.5 shrink-0" />
|
||||
<span className="truncate">{recipient.primaryEmail}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{phone ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<PhoneIcon className="size-3.5 shrink-0" />
|
||||
{phone}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 space-y-2 lg:border-l lg:pl-4">
|
||||
{address ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div className="flex items-start gap-1 text-sm text-muted-foreground">
|
||||
<MapPinIcon className="mt-0.5 size-3.5 shrink-0" />
|
||||
<span className="line-clamp-3">{address}</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<TooltipContent align="start" className="max-w-md">
|
||||
{address}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("customers.selected_customer.no_address", "Sin dirección registrada")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{t("customers.selected_customer.empty_title", "Cliente no seleccionado")}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("customers.selected_customer.empty", "Selecciona o crea un cliente")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{onCreateClick && !readOnly && !disabled && (
|
||||
<Button onClick={onCreateClick} type="button" variant="outline">
|
||||
{t("customers.selected_customer.new_customer", "Nuevo cliente")}
|
||||
</Button>
|
||||
)}
|
||||
{showActions ? (
|
||||
<div className="flex shrink-0 flex-wrap gap-2 md:flex-col md:items-stretch">
|
||||
{recipient && onViewClick ? (
|
||||
<Button onClick={() => onViewClick(recipient)} type="button" variant="ghost">
|
||||
<ExternalLinkIcon className="mr-2 size-4" />
|
||||
{t("customers.selected_customer.view", "Ver cliente")}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{onChangeClick && !readOnly && !disabled && (
|
||||
<Button onClick={onChangeClick} type="button" variant="secondary">
|
||||
{onCreateClick ? (
|
||||
<Button onClick={onCreateClick} type="button" variant="outline">
|
||||
<PlusIcon className="mr-2 size-4" />
|
||||
{t("customers.selected_customer.new_customer", "Nuevo cliente")}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
onClick={onChangeClick}
|
||||
type="button"
|
||||
variant={recipient ? "secondary" : "default"}
|
||||
>
|
||||
<RefreshCwIcon className="mr-2 size-4" />
|
||||
{recipient
|
||||
? t("customers.selected_customer.change", "Cambiar cliente")
|
||||
: t("customers.selected_customer.select", "Seleccionar cliente")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const buildAddress = (recipient: CustomerSelectionOption): string => {
|
||||
// Línea 1: calle(s)
|
||||
const line1 = [recipient.street, recipient.street2].filter(Boolean).join(", ");
|
||||
|
||||
// Línea 2: CP + ciudad con espacio no rompible entre CP y ciudad
|
||||
const line2Parts: string[] = [];
|
||||
if (recipient.postalCode && recipient.city) {
|
||||
line2Parts.push(`${recipient.postalCode}\u00A0${recipient.city}`); // CP Ciudad
|
||||
} else {
|
||||
if (recipient.postalCode) line2Parts.push(recipient.postalCode);
|
||||
if (recipient.city) line2Parts.push(recipient.city);
|
||||
}
|
||||
const line2 = line2Parts.join(" ");
|
||||
|
||||
// Línea 3: provincia + país
|
||||
const line3 = [recipient.province, recipient.country].filter(Boolean).join(", ");
|
||||
|
||||
const stack = [line1, line2, line3].filter(Boolean);
|
||||
const inline = stack.join(" · "); // separador compacto
|
||||
const full = stack.join(", ");
|
||||
|
||||
return { has: stack.length > 0, stack, inline, full };
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
TextAreaField,
|
||||
TextField,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { FileTextIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
|
||||
@ -23,6 +24,8 @@ export const ProformaUpdateHeaderEditor = ({
|
||||
return (
|
||||
<FormSectionCard
|
||||
description={t("form_groups.proformas.basic_info.description")}
|
||||
disabled={disabled}
|
||||
icon={<FileTextIcon className="size-5" />}
|
||||
title={t("form_groups.proformas.basic_info.title")}
|
||||
>
|
||||
<FormSectionGrid>
|
||||
@ -83,7 +86,6 @@ export const ProformaUpdateHeaderEditor = ({
|
||||
placeholder={t("form_fields.proformas.notes.placeholder")}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
</FormSectionGrid>
|
||||
</FormSectionCard>
|
||||
);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { FormSectionCard } from "@repo/rdx-ui/components";
|
||||
import { ListIcon } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
@ -19,6 +20,8 @@ export const ProformaUpdateItemsEditor = ({
|
||||
return (
|
||||
<FormSectionCard
|
||||
description={t("form_groups.items.description")}
|
||||
disabled={disabled}
|
||||
icon={<ListIcon className="size-5" />}
|
||||
title={t("form_groups.items.title")}
|
||||
>
|
||||
<ProformaLineEditor
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { CustomerSelectionOption } from "@erp/customers";
|
||||
import { FormSectionCard } from "@repo/rdx-ui/components";
|
||||
import { UserIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { SelectedRecipientSummary } from "../blocks";
|
||||
@ -24,7 +25,15 @@ export const ProformaUpdateRecipientEditor = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<FormSectionCard title={t("form_groups.proformas.customer.title")}>
|
||||
<FormSectionCard
|
||||
description={t(
|
||||
"form_groups.proformas.customer.description",
|
||||
"Cliente destinatario de la proforma"
|
||||
)}
|
||||
disabled={disabled}
|
||||
icon={<UserIcon className="size-5" />}
|
||||
title={t("form_groups.proformas.customer.title", "Cliente")}
|
||||
>
|
||||
<SelectedRecipientSummary
|
||||
disabled={disabled}
|
||||
onChangeClick={onChangeCustomerClick}
|
||||
|
||||
@ -76,7 +76,7 @@ export const ProformaUpdatePage = () => {
|
||||
}}
|
||||
/>
|
||||
}
|
||||
title={t("pages.proformas.update.title")}
|
||||
title={<>{t("pages.proformas.update.title")}</>}
|
||||
/>
|
||||
</AppHeader>
|
||||
<AppContent className="space-y-4 max-w-5xl mx-auto">
|
||||
|
||||
@ -5,25 +5,21 @@
|
||||
*/
|
||||
export interface CustomerSelectionOption {
|
||||
id: string;
|
||||
//status: string;
|
||||
status: string;
|
||||
|
||||
//isCompany: boolean;
|
||||
isCompany: boolean;
|
||||
name: string;
|
||||
//tradeName: string;
|
||||
tin: string;
|
||||
tradeName: string | null;
|
||||
tin: string | null;
|
||||
|
||||
/*street: string;
|
||||
street2: string;
|
||||
city: string;
|
||||
province: string;
|
||||
postalCode: string;
|
||||
country: string;
|
||||
*/
|
||||
street: string | null;
|
||||
street2: string | null;
|
||||
city: string | null;
|
||||
province: string | null;
|
||||
postalCode: string | null;
|
||||
country: string | null;
|
||||
|
||||
//emailPrimary: string;
|
||||
//primaryPhone: string;
|
||||
//primaryMobile: string;
|
||||
|
||||
languageCode: string;
|
||||
currencyCode: string;
|
||||
primaryEmail: string | null;
|
||||
primaryPhone: string | null;
|
||||
primaryMobile: string | null;
|
||||
}
|
||||
|
||||
@ -13,11 +13,18 @@ export const buildCustomerSelectionOption = (
|
||||
|
||||
isCompany: customer.isCompany,
|
||||
name: customer.name,
|
||||
tradeName: customer.tradeName,
|
||||
tin: customer.tin,
|
||||
|
||||
EmailPrimary: customer.EmailPrimary,
|
||||
street: customer.address.street,
|
||||
street2: null,
|
||||
city: customer.address.city,
|
||||
province: customer.address.province,
|
||||
postalCode: customer.address.postalCode,
|
||||
country: customer.address.country,
|
||||
|
||||
languageCode: customer.languageCode,
|
||||
currencyCode: customer.currencyCode,
|
||||
primaryEmail: customer.contact.primaryEmail,
|
||||
primaryPhone: customer.contact.primaryPhone,
|
||||
primaryMobile: customer.contact.primaryMobile,
|
||||
};
|
||||
};
|
||||
|
||||
@ -13,7 +13,11 @@ import type { ReactNode } from "react";
|
||||
interface FormSectionCardProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: ReactNode;
|
||||
disabled?: boolean;
|
||||
|
||||
children: ReactNode;
|
||||
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
contentClassName?: string;
|
||||
@ -22,7 +26,11 @@ interface FormSectionCardProps {
|
||||
export const FormSectionCard = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
disabled = false,
|
||||
|
||||
children,
|
||||
|
||||
className,
|
||||
headerClassName,
|
||||
contentClassName,
|
||||
@ -30,17 +38,32 @@ export const FormSectionCard = ({
|
||||
const hasHeader = Boolean(title || description);
|
||||
|
||||
return (
|
||||
<Card className={cn("", className)}>
|
||||
<FieldSet>
|
||||
<Card className={cn(className)}>
|
||||
<FieldSet disabled={disabled}>
|
||||
{hasHeader ? (
|
||||
<CardHeader className={headerClassName}>
|
||||
<FieldLegend>
|
||||
<div className="space-y-1">
|
||||
{title ? (
|
||||
<CardTitle className="text-base font-semibold tracking-tight">{title}</CardTitle>
|
||||
<CardHeader className={cn("flex flex-row items-start gap-4", headerClassName)}>
|
||||
<FieldLegend className="w-full">
|
||||
<div className="flex items-start gap-3">
|
||||
{icon ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-10 w-10 shrink-0 items-center justify-center rounded-md",
|
||||
disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary"
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{description ? <CardDescription>{description}</CardDescription> : null}
|
||||
<div className="space-y-1">
|
||||
{title ? (
|
||||
<CardTitle className="text-base font-semibold tracking-tight">
|
||||
{title}
|
||||
</CardTitle>
|
||||
) : null}
|
||||
|
||||
{description ? <CardDescription>{description}</CardDescription> : null}
|
||||
</div>
|
||||
</div>
|
||||
</FieldLegend>
|
||||
</CardHeader>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user