.
This commit is contained in:
parent
02b10d4f85
commit
0e193ee9ce
@ -163,6 +163,9 @@ export class SequelizePaymentMethodRepository
|
|||||||
try {
|
try {
|
||||||
const criteriaConverter = new CriteriaToSequelizeConverter();
|
const criteriaConverter = new CriteriaToSequelizeConverter();
|
||||||
const query = criteriaConverter.convert(criteria, {
|
const query = criteriaConverter.convert(criteria, {
|
||||||
|
mappings: {
|
||||||
|
isActive: "is_active",
|
||||||
|
},
|
||||||
searchableFields: [],
|
searchableFields: [],
|
||||||
sortableFields: ["name"],
|
sortableFields: ["name"],
|
||||||
enableFullText: true,
|
enableFullText: true,
|
||||||
@ -170,6 +173,8 @@ export class SequelizePaymentMethodRepository
|
|||||||
strictMode: true, // fuerza error si ORDER BY no permitido
|
strictMode: true, // fuerza error si ORDER BY no permitido
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(query?.where);
|
||||||
|
|
||||||
query.where = {
|
query.where = {
|
||||||
...query.where,
|
...query.where,
|
||||||
company_id: companyId.toString(),
|
company_id: companyId.toString(),
|
||||||
|
|||||||
@ -117,6 +117,9 @@ export class SequelizePaymentTermRepository
|
|||||||
const criteriaConverter = new CriteriaToSequelizeConverter();
|
const criteriaConverter = new CriteriaToSequelizeConverter();
|
||||||
const query = criteriaConverter.convert(criteria, {
|
const query = criteriaConverter.convert(criteria, {
|
||||||
searchableFields: [],
|
searchableFields: [],
|
||||||
|
mappings: {
|
||||||
|
isActive: "is_active",
|
||||||
|
},
|
||||||
sortableFields: ["name"],
|
sortableFields: ["name"],
|
||||||
enableFullText: true,
|
enableFullText: true,
|
||||||
database: this.database,
|
database: this.database,
|
||||||
|
|||||||
@ -8,7 +8,16 @@ import { z } from "zod/v4";
|
|||||||
export const FilterPrimitiveSchema = z.object({
|
export const FilterPrimitiveSchema = z.object({
|
||||||
// Campos mínimos ya normalizados por el conversor
|
// Campos mínimos ya normalizados por el conversor
|
||||||
field: z.string(),
|
field: z.string(),
|
||||||
operator: z.string(),
|
operator: z.enum([
|
||||||
|
"CONTAINS",
|
||||||
|
"NOT_CONTAINS",
|
||||||
|
"NOT_EQUALS",
|
||||||
|
"GREATER_THAN",
|
||||||
|
"GREATER_THAN_OR_EQUAL",
|
||||||
|
"LOWER_THAN",
|
||||||
|
"LOWER_THAN_OR_EQUAL",
|
||||||
|
"EQUALS",
|
||||||
|
]),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export const GetProformaByIdAdapter = {
|
|||||||
taxes: dto.taxes.map(mapTaxSummary),
|
taxes: dto.taxes.map(mapTaxSummary),
|
||||||
|
|
||||||
paymentMethodId: dto.payment_method?.id ?? null,
|
paymentMethodId: dto.payment_method?.id ?? null,
|
||||||
|
paymentTermId: dto.payment_term?.id ?? null,
|
||||||
|
|
||||||
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
|
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export interface Proforma {
|
|||||||
taxes: ProformaTaxSummary[];
|
taxes: ProformaTaxSummary[];
|
||||||
|
|
||||||
paymentMethodId: string | null;
|
paymentMethodId: string | null;
|
||||||
|
paymentTermId: string | null;
|
||||||
|
|
||||||
subtotalAmount: number;
|
subtotalAmount: number;
|
||||||
|
|
||||||
|
|||||||
@ -50,7 +50,8 @@ export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpd
|
|||||||
hasRetentionPercentage: fiscalDefaults.defaultRetentionPercentage !== null,
|
hasRetentionPercentage: fiscalDefaults.defaultRetentionPercentage !== null,
|
||||||
retentionPercentage: fiscalDefaults.defaultRetentionPercentage,
|
retentionPercentage: fiscalDefaults.defaultRetentionPercentage,
|
||||||
|
|
||||||
paymentMethodId: proforma.paymentMethodId ?? proformaDefaults.paymentMethodId,
|
paymentMethodId: proforma.paymentMethodId,
|
||||||
|
paymentTermId: proforma.paymentTermId,
|
||||||
|
|
||||||
items: proforma.items.map(mapProformaItemsToProformaItemsUpdateForm),
|
items: proforma.items.map(mapProformaItemsToProformaItemsUpdateForm),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,9 +9,29 @@ import {
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export const useUpdateProformaPaymentController = () => {
|
export const useUpdateProformaPaymentController = () => {
|
||||||
const paymentMethodsQuery = usePaymentMethodsListQuery();
|
const paymentMethodsQuery = usePaymentMethodsListQuery({
|
||||||
|
criteria: {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: "isActive",
|
||||||
|
operator: "EQUALS",
|
||||||
|
value: "true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const paymentTermsQuery = usePaymentTermsListQuery();
|
const paymentTermsQuery = usePaymentTermsListQuery({
|
||||||
|
criteria: {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: "isActive",
|
||||||
|
operator: "EQUALS",
|
||||||
|
value: "true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const paymentMethodOptions = useMemo(() => {
|
const paymentMethodOptions = useMemo(() => {
|
||||||
return getPaymentMethodOptions(paymentMethodsQuery.data?.items ?? []);
|
return getPaymentMethodOptions(paymentMethodsQuery.data?.items ?? []);
|
||||||
|
|||||||
@ -39,8 +39,8 @@ export interface ProformaTotals {
|
|||||||
taxBreakdown: ProformaTaxBreakdown[];
|
taxBreakdown: ProformaTaxBreakdown[];
|
||||||
|
|
||||||
taxTotal: number;
|
taxTotal: number;
|
||||||
|
|
||||||
recTotal: number;
|
recTotal: number;
|
||||||
|
taxRecTotal: number;
|
||||||
|
|
||||||
retentionPercentage: number | null;
|
retentionPercentage: number | null;
|
||||||
retentionAmount: number;
|
retentionAmount: number;
|
||||||
|
|||||||
@ -0,0 +1,198 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { CustomerSelectionOption } from "@erp/customers";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import { CreditCard, Percent, Receipt, Settings, UserIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
|
||||||
|
import { SelectedRecipientSummary } from "./selected-recipient";
|
||||||
|
|
||||||
|
interface EditorSidebarProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
|
||||||
|
selectedCustomer?: CustomerSelectionOption | null;
|
||||||
|
onChangeCustomerClick: () => void;
|
||||||
|
onCreateCustomerClick: () => void;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditorSidebar = ({
|
||||||
|
disabled = false,
|
||||||
|
readOnly = false,
|
||||||
|
|
||||||
|
selectedCustomer,
|
||||||
|
onChangeCustomerClick,
|
||||||
|
onCreateCustomerClick,
|
||||||
|
|
||||||
|
className,
|
||||||
|
}: EditorSidebarProps) => {
|
||||||
|
const [isTotalsExpanded, setIsTotalsExpanded] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const formatMoney = (value: number) => {
|
||||||
|
return new Intl.NumberFormat("es-ES", {
|
||||||
|
style: "currency",
|
||||||
|
currency: proforma.config.currency,
|
||||||
|
}).format(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPercent = (value: number) => {
|
||||||
|
return new Intl.NumberFormat("es-ES", {
|
||||||
|
style: "percent",
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(value / 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
{/* Secciones en acordeon */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<Accordion className="divide-y" collapsible defaultValue="client" type="single">
|
||||||
|
{/* Cliente */}
|
||||||
|
<AccordionItem className="border-none px-4" value="client">
|
||||||
|
<AccordionTrigger className="hover:no-underline">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex size-8 items-center justify-center rounded-lg bg-primary/10">
|
||||||
|
<UserIcon className="size-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-sm font-medium">Cliente</div>
|
||||||
|
{t(
|
||||||
|
"form_groups.proformas.customer.description",
|
||||||
|
"Cliente destinatario de la proforma"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<SelectedRecipientSummary disabled={disabled} recipient={selectedCustomer} />
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
{/* Impuestos */}
|
||||||
|
<AccordionItem className="border-none px-4" value="taxes">
|
||||||
|
<AccordionTrigger className="hover:no-underline">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex size-8 items-center justify-center rounded-lg bg-sky-100 dark:bg-sky-900/30">
|
||||||
|
<Percent className="size-4 text-sky-600 dark:text-sky-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-sm font-medium">Impuestos</div>
|
||||||
|
<div className="text-xs text-muted-foreground">IVA 21%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent />
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
{/* Configuracion */}
|
||||||
|
<AccordionItem className="border-none px-4" value="config">
|
||||||
|
<AccordionTrigger className="hover:no-underline">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex size-8 items-center justify-center rounded-lg bg-muted">
|
||||||
|
<Settings className="size-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-sm font-medium">Configuracion</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Estado y moneda</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent />
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
{/* Condiciones de pago */}
|
||||||
|
<AccordionItem className="border-none px-4" value="payment">
|
||||||
|
<AccordionTrigger className="hover:no-underline">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex size-8 items-center justify-center rounded-lg bg-amber-100 dark:bg-amber-900/30">
|
||||||
|
<CreditCard className="size-4 text-amber-600 dark:text-amber-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-sm font-medium">Condiciones de pago</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Forma y vencimiento</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>{""}</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Resumen de totales compacto - colapsable */}
|
||||||
|
<div className="shrink-0 border-t bg-background"> {""}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TotalsRowProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
className?: string;
|
||||||
|
small?: boolean;
|
||||||
|
variant?: "default" | "discount" | "tax";
|
||||||
|
}
|
||||||
|
|
||||||
|
function TotalsRow({ label, value, className, small, variant = "default" }: TotalsRowProps) {
|
||||||
|
const valueColorClass = {
|
||||||
|
default: "text-foreground",
|
||||||
|
discount: "text-amber-700 dark:text-amber-400",
|
||||||
|
tax: "text-sky-700 dark:text-sky-400",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center justify-between gap-4", className)}>
|
||||||
|
<span className={cn("font-medium text-muted-foreground", small ? "text-xs" : "text-sm")}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"font-mono tabular-nums",
|
||||||
|
small ? "text-xs" : "text-sm font-semibold",
|
||||||
|
valueColorClass[variant]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version compacta del sidebar para movil (solo totales)
|
||||||
|
export function MobileTotalsBadge({
|
||||||
|
total,
|
||||||
|
currency = "EUR",
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
total: number;
|
||||||
|
currency?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) {
|
||||||
|
const formatMoney = (value: number) => {
|
||||||
|
return new Intl.NumberFormat("es-ES", {
|
||||||
|
style: "currency",
|
||||||
|
currency: currency,
|
||||||
|
}).format(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-primary-foreground shadow-lg transition-transform hover:scale-105 active:scale-95"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Receipt className="size-4" />
|
||||||
|
<span className="font-mono text-sm font-bold">{formatMoney(total)}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,3 +1,8 @@
|
|||||||
|
export * from "./editor-sidebar";
|
||||||
|
export * from "./line-editor";
|
||||||
|
export * from "./new-proforma-totals-summary";
|
||||||
|
export * from "./proforma-compact-totals";
|
||||||
export * from "./proforma-line-editor";
|
export * from "./proforma-line-editor";
|
||||||
export * from "./proforma-totals-summary";
|
export * from "./proforma-totals-summary";
|
||||||
|
export * from "./proforma-update-header";
|
||||||
export * from "./selected-recipient";
|
export * from "./selected-recipient";
|
||||||
|
|||||||
@ -0,0 +1,284 @@
|
|||||||
|
import { MoneyHelper, PercentageHelper } from "@repo/rdx-utils";
|
||||||
|
import { Card, CardContent, Separator } from "@repo/shadcn-ui/components";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import { Percent, Receipt, Tag } from "lucide-react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
import type { ProformaTotals } from "../../entities";
|
||||||
|
|
||||||
|
interface NewProformaTotalsSummaryProps {
|
||||||
|
totals: ProformaTotals;
|
||||||
|
currency?: string;
|
||||||
|
|
||||||
|
showRec?: boolean;
|
||||||
|
showRetention?: boolean;
|
||||||
|
|
||||||
|
globalDiscountField?: ReactNode;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
layout?: "vertical" | "horizontal";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NewProformaTotalsSummary = ({
|
||||||
|
totals,
|
||||||
|
currency = "EUR",
|
||||||
|
showRec = false,
|
||||||
|
showRetention = false,
|
||||||
|
globalDiscountField,
|
||||||
|
disabled = false,
|
||||||
|
className,
|
||||||
|
layout = "vertical",
|
||||||
|
}: NewProformaTotalsSummaryProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const formatMoney = (value: number): string => {
|
||||||
|
return MoneyHelper.formatCurrency(value, 2, currency);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPercent = (value: number): string => {
|
||||||
|
return PercentageHelper.formatPercent(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const retentionLabel =
|
||||||
|
totals.retentionPercentage === null
|
||||||
|
? "Total retenciones"
|
||||||
|
: `Total retenciones ${formatPercent(totals.retentionPercentage)}`;
|
||||||
|
|
||||||
|
const isHorizontal = layout === "horizontal";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"w-full overflow-hidden",
|
||||||
|
!isHorizontal && "max-w-md",
|
||||||
|
disabled && "opacity-60",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-3 px-5 pb-4">
|
||||||
|
<div className="flex size-9 items-center justify-center rounded-lg">
|
||||||
|
<Receipt className="size-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-semibold text-foreground">Resumen de totales</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Desglose de la factura</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardContent className={cn("p-5", isHorizontal ? "space-y-4" : "space-y-6")}>
|
||||||
|
{/* Layout horizontal: grid de 3 columnas en pantallas grandes */}
|
||||||
|
<div className={cn(isHorizontal ? "grid gap-4 lg:grid-cols-3 lg:gap-6" : "space-y-6")}>
|
||||||
|
{/* Columna 1: Subtotal + Descuentos */}
|
||||||
|
<div className={cn(isHorizontal ? "space-y-4" : "space-y-6")}>
|
||||||
|
{/* Subtotal */}
|
||||||
|
<div className="rounded-lg bg-muted/50 px-4 py-3">
|
||||||
|
<TotalsRow
|
||||||
|
label="Subtotal líneas"
|
||||||
|
strong
|
||||||
|
value={formatMoney(totals.subtotalBeforeDiscounts)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sección Descuentos */}
|
||||||
|
<section className="rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-950/20">
|
||||||
|
<div className="flex items-center gap-2 border-b border-amber-200 px-4 py-3 dark:border-amber-900/50">
|
||||||
|
<Tag className="size-4 text-amber-600 dark:text-amber-500" />
|
||||||
|
<span className="text-sm font-semibold text-amber-700 dark:text-amber-400">
|
||||||
|
Descuentos
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3 p-4">
|
||||||
|
<TotalsRow
|
||||||
|
label="Descuento en líneas"
|
||||||
|
value={`-${formatMoney(totals.lineDiscountTotal)}`}
|
||||||
|
variant="discount"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{globalDiscountField && (
|
||||||
|
<div className="rounded-md border border-amber-200 bg-white p-3 dark:border-amber-900/50 dark:bg-background">
|
||||||
|
<label className="mb-2 block text-xs font-medium text-muted-foreground">
|
||||||
|
Descuento global
|
||||||
|
</label>
|
||||||
|
{globalDiscountField}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TotalsRow
|
||||||
|
description={formatPercent(totals.globalDiscountPercentage)}
|
||||||
|
label="Descuento global"
|
||||||
|
value={`-${formatMoney(totals.globalDiscountAmount)}`}
|
||||||
|
variant="discount"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Separator className="bg-amber-200 dark:bg-amber-900/50" />
|
||||||
|
|
||||||
|
<TotalsRow
|
||||||
|
label="Total descuentos"
|
||||||
|
strong
|
||||||
|
value={`-${formatMoney(totals.totalDiscountAmount)}`}
|
||||||
|
variant="discount"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Columna 2: Base Imponible + Impuestos */}
|
||||||
|
<div className={cn(isHorizontal ? "space-y-4" : "space-y-6")}>
|
||||||
|
{/* Base Imponible */}
|
||||||
|
<div className="rounded-lg bg-muted/50 px-4 py-3">
|
||||||
|
<TotalsRow label="Base imponible" strong value={formatMoney(totals.taxableBase)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sección Impuestos */}
|
||||||
|
<section className="rounded-lg border border-primary-200 bg-primary-50 dark:border-primary-900/50 dark:bg-primary-950/20">
|
||||||
|
<div className="flex items-center gap-2 border-b border-primary-200 px-4 py-3 dark:border-primary-900/50">
|
||||||
|
<Percent className="size-4 text-primary-600 dark:text-primary-500" />
|
||||||
|
<span className="text-sm font-semibold text-primary-700 dark:text-primary-400">
|
||||||
|
Impuestos
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3 p-4">
|
||||||
|
{totals.taxBreakdown.length > 0 ? (
|
||||||
|
totals.taxBreakdown.map((tax) => {
|
||||||
|
const key = `${tax.taxPercentage}:${tax.recPercentage ?? "none"}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2" key={key}>
|
||||||
|
<TotalsRow
|
||||||
|
label={`IVA ${formatPercent(tax.taxPercentage)}`}
|
||||||
|
value={formatMoney(tax.taxAmount)}
|
||||||
|
variant="tax"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showRec && tax.recPercentage !== null && (
|
||||||
|
<TotalsRow
|
||||||
|
label={`Recargo equiv. ${formatPercent(tax.recPercentage)}`}
|
||||||
|
value={formatMoney(tax.recAmount)}
|
||||||
|
variant="tax"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">Sin impuestos aplicados</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Separator className="bg-primary-200 dark:bg-primary-900/50" />
|
||||||
|
|
||||||
|
<TotalsRow
|
||||||
|
label="Total impuestos"
|
||||||
|
strong
|
||||||
|
value={formatMoney(totals.taxTotal)}
|
||||||
|
variant="tax"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showRec && (
|
||||||
|
<TotalsRow
|
||||||
|
label="Total recargo equiv."
|
||||||
|
strong
|
||||||
|
value={formatMoney(totals.recTotal)}
|
||||||
|
variant="tax"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Columna 3: Total Final (y retenciones si aplica) */}
|
||||||
|
<div className={cn(isHorizontal ? "flex flex-col justify-start space-y-4" : "space-y-6")}>
|
||||||
|
{/* Retenciones */}
|
||||||
|
{
|
||||||
|
<div className="rounded-lg bg-muted/50 px-4 py-3">
|
||||||
|
<TotalsRow
|
||||||
|
label={retentionLabel}
|
||||||
|
strong
|
||||||
|
value={`-${formatMoney(totals.retentionAmount)}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{/* Spacer para empujar el total hacia abajo en horizontal */}
|
||||||
|
{/* isHorizontal && <div className="flex-1" /> */}
|
||||||
|
|
||||||
|
{/* Total Final */}
|
||||||
|
<div className="rounded-xl bg-gradient-to-r from-primary to-primary/90 p-4 shadow-lg">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-between",
|
||||||
|
isHorizontal && "lg:flex-col lg:items-start lg:gap-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-semibold text-primary-foreground/90">
|
||||||
|
Total factura
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"font-mono font-bold tracking-tight text-primary-foreground",
|
||||||
|
isHorizontal ? "text-2xl lg:text-3xl" : "text-2xl"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatMoney(totals.total)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TotalsRowProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
className?: string;
|
||||||
|
description?: string;
|
||||||
|
strong?: boolean;
|
||||||
|
variant?: "default" | "discount" | "tax";
|
||||||
|
}
|
||||||
|
|
||||||
|
const TotalsRow = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
description,
|
||||||
|
className,
|
||||||
|
strong = false,
|
||||||
|
variant = "default",
|
||||||
|
}: TotalsRowProps) => {
|
||||||
|
const valueColorClass = {
|
||||||
|
default: strong ? "text-foreground" : "text-muted-foreground",
|
||||||
|
discount: "text-amber-700 dark:text-amber-400",
|
||||||
|
tax: "text-primary-700 dark:text-primary-400",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-start justify-between gap-4", className)}>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"text-sm",
|
||||||
|
strong ? "font-semibold text-foreground" : "font-medium text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{description && <div className="text-xs text-muted-foreground">{description}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 text-right font-mono text-sm tabular-nums",
|
||||||
|
strong ? "font-bold" : "font-medium",
|
||||||
|
valueColorClass[variant]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,266 @@
|
|||||||
|
import { MoneyHelper, PercentageHelper } from "@repo/rdx-utils";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
Item,
|
||||||
|
ItemContent,
|
||||||
|
ItemMedia,
|
||||||
|
ItemTitle,
|
||||||
|
Separator,
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
CircleMinus,
|
||||||
|
LandmarkIcon,
|
||||||
|
PercentIcon,
|
||||||
|
ReceiptTextIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { type ReactNode, useState } from "react";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
import type { ProformaTotals } from "../../entities";
|
||||||
|
|
||||||
|
interface ProformaCompactTotalsProps {
|
||||||
|
totals: ProformaTotals;
|
||||||
|
currency?: string;
|
||||||
|
|
||||||
|
showRec?: boolean;
|
||||||
|
showRetention?: boolean;
|
||||||
|
|
||||||
|
globalDiscountField?: ReactNode;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProformaCompactTotals = ({
|
||||||
|
totals,
|
||||||
|
currency = "EUR",
|
||||||
|
showRec = false,
|
||||||
|
showRetention = false,
|
||||||
|
globalDiscountField,
|
||||||
|
disabled = false,
|
||||||
|
className,
|
||||||
|
}: ProformaCompactTotalsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const formatMoney = (value: number): string => {
|
||||||
|
return MoneyHelper.formatCurrency(value, 2, currency);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPercent = (value: number): string => {
|
||||||
|
return PercentageHelper.formatPercent(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={cn("rounded-none bg-muted-foreground p-0 gap-0", className)}>
|
||||||
|
{/* Panel expandible con desglose */}
|
||||||
|
|
||||||
|
<CardContent
|
||||||
|
className={cn(
|
||||||
|
"grid overflow-hidden transition-all duration-200 bg-background",
|
||||||
|
isExpanded ? "grid-rows-[1fr] py-6" : "grid-rows-[0fr] py-0"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<div className="grid max-w-3xl gap-4 xl:max-w-5xl xl:gap-8 lg:grid-cols-3 text-sm 2xl:gap-16">
|
||||||
|
{/* Descuentos */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 font-medium text-teal-700 dark:text-teal-400">
|
||||||
|
<PercentIcon className="size-3.5" />
|
||||||
|
Descuentos
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">En lineas</span>
|
||||||
|
<span className="tabular-nums">-{formatMoney(totals.lineDiscountTotal)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Global</span>
|
||||||
|
<span> ({formatPercent(totals.globalDiscountPercentage)})</span>
|
||||||
|
</div>
|
||||||
|
<span className="tabular-nums">-{formatMoney(totals.globalDiscountAmount)}</span>
|
||||||
|
</div>
|
||||||
|
<Separator className={"bg-foreground"} />
|
||||||
|
<div className="flex justify-between pt-1 font-medium">
|
||||||
|
<span>Total descuentos</span>
|
||||||
|
<span className="tabular-nums text-teal-700 dark:text-teal-400">
|
||||||
|
-{formatMoney(totals.totalDiscountAmount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Impuestos */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 font-medium text-orange-700 dark:text-orange-400">
|
||||||
|
<LandmarkIcon className="size-3.5" />
|
||||||
|
Impuestos
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{totals.taxBreakdown.map((tax) => {
|
||||||
|
const key = `${tax.taxPercentage}:${tax.recPercentage ?? "none"}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key}>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Impuesto</span>
|
||||||
|
<span> ({formatPercent(tax.taxPercentage)})</span>
|
||||||
|
</div>
|
||||||
|
<span className="tabular-nums">{formatMoney(tax.taxAmount)}</span>
|
||||||
|
</div>
|
||||||
|
{showRec && tax.recPercentage !== null ? (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("proformas.update.totals.recPercentage", "Rec. equiv.")}
|
||||||
|
</span>
|
||||||
|
<span> ({formatPercent(tax.recPercentage)})</span>
|
||||||
|
</div>
|
||||||
|
<span className="tabular-nums">{formatMoney(totals.recTotal)}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Separator className={"bg-foreground"} />
|
||||||
|
<div className="flex justify-between pt-1 font-medium ">
|
||||||
|
<span>Total impuestos</span>
|
||||||
|
<span className="tabular-nums text-orange-700 dark:text-orange-400">
|
||||||
|
{formatMoney(totals.taxRecTotal)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Retenciones */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 font-medium text-muted-foreground">
|
||||||
|
<CircleMinus className="size-3.5" />
|
||||||
|
Retenciones
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{totals.retentionPercentage ? (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">IRPF</span>
|
||||||
|
<span> ({formatPercent(totals.retentionPercentage)})</span>
|
||||||
|
</div>
|
||||||
|
<span className="tabular-nums">-{formatMoney(totals.retentionAmount)}</span>
|
||||||
|
</div>
|
||||||
|
<Separator className={"bg-foreground"} />
|
||||||
|
<div className="flex justify-between pt-1 font-medium">
|
||||||
|
<span>Total retenciones</span>
|
||||||
|
<span className="tabular-nums">-{formatMoney(totals.retentionAmount)}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground">Sin retenciones</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
{/* Barra principal de totales - siempre visible */}
|
||||||
|
<CardFooter className="flex items-center justify-between gap-4 bg-muted rounded-none py-3">
|
||||||
|
{/* Boton expandir */}
|
||||||
|
<Button onClick={() => setIsExpanded(!isExpanded)} size="default" variant="outline">
|
||||||
|
{isExpanded ? <ChevronDown className="size-5" /> : <ChevronUp className="size-5" />}
|
||||||
|
{isExpanded ? "Ocultar desglose" : "Ver desglose"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Totales en linea */}
|
||||||
|
<div className="flex items-center gap-8 text-sm">
|
||||||
|
<div className="hidden items-center gap-2 xl:flex">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("proformas.update.totals.subtotalBeforeDiscounts", "Subtotal")}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium tabular-nums">
|
||||||
|
{formatMoney(totals.subtotalBeforeDiscounts)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden items-center gap-2 xl:flex">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger render={<PercentIcon className="text-teal-700 size-4" />} />
|
||||||
|
<TooltipContent>
|
||||||
|
{t("proformas.update.totals.totalDiscountAmount", "Total descuentos")}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<span className="sr-only">
|
||||||
|
{t("proformas.update.totals.totalDiscountAmount", "Dtos")}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium tabular-nums text-teal-700 dark:text-teal-400">{`-${formatMoney(totals.totalDiscountAmount)}`}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden items-center gap-2 xl:flex">
|
||||||
|
<span className="text-muted-foreground text-nowrap">
|
||||||
|
{t("proformas.update.totals.taxableBase", "Base imp.")}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium tabular-nums">{formatMoney(totals.taxableBase)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden items-center gap-2 xl:flex">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger render={<LandmarkIcon className="text-orange-700 size-4" />} />
|
||||||
|
<TooltipContent>
|
||||||
|
{t("proformas.update.totals.taxTotal", "Total impuestos")}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<span className="text-muted-foreground sr-only">
|
||||||
|
{t("proformas.update.totals.taxTotal", "Impuestos")}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium tabular-nums text-orange-700 dark:text-orange-400">
|
||||||
|
{formatMoney(totals.taxRecTotal)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showRetention ? (
|
||||||
|
<div className="hidden items-center gap-2 xl:flex">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger render={<CircleMinus className="size-4" />} />
|
||||||
|
<TooltipContent>
|
||||||
|
{t("proformas.update.totals.retentionAmount", "Total retenciones")}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<span className="sr-only">
|
||||||
|
{t("proformas.update.totals.retentionAmount", "Retenc.")}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium tabular-nums">
|
||||||
|
{`-${formatMoney(totals.retentionAmount)}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Total destacado */}
|
||||||
|
<Item className="bg-muted-foreground">
|
||||||
|
<ItemMedia className="text-muted">
|
||||||
|
<ReceiptTextIcon className="size-5" />
|
||||||
|
</ItemMedia>
|
||||||
|
<ItemContent className="items-end">
|
||||||
|
<ItemTitle className="text-base gap-2 text-muted">
|
||||||
|
<span className="font-medium">Total</span>{" "}
|
||||||
|
<span className="font-bold tabular-nums">{formatMoney(totals.total)}</span>
|
||||||
|
</ItemTitle>
|
||||||
|
</ItemContent>
|
||||||
|
</Item>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
AmountField,
|
AmountField,
|
||||||
CheckboxField,
|
|
||||||
LineDescriptionField,
|
LineDescriptionField,
|
||||||
PercentageField,
|
PercentageField,
|
||||||
QuantityField,
|
QuantityField,
|
||||||
@ -66,12 +65,6 @@ export const ProformaLineEditor = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const columns: LineEditorColumn<ProformaItemField>[] = [
|
const columns: LineEditorColumn<ProformaItemField>[] = [
|
||||||
{
|
|
||||||
id: "isValued",
|
|
||||||
header: t("form_fields.items.description.is_valued", "¿Valorado?"),
|
|
||||||
headClassName: "w-[100px] text-right",
|
|
||||||
cell: ({ index }) => <CheckboxField name={`items.${index}.isValued`} />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "description",
|
id: "description",
|
||||||
header: t("form_fields.items.description.label", "Descripción"),
|
header: t("form_fields.items.description.label", "Descripción"),
|
||||||
@ -101,7 +94,7 @@ export const ProformaLineEditor = ({
|
|||||||
{
|
{
|
||||||
id: "unitAmount",
|
id: "unitAmount",
|
||||||
header: t("form_fields.items.unit_amount.label", "Importe unitario"),
|
header: t("form_fields.items.unit_amount.label", "Importe unitario"),
|
||||||
headClassName: "w-[120px] text-right",
|
headClassName: "text-right",
|
||||||
cell: ({ index }) => (
|
cell: ({ index }) => (
|
||||||
<AmountField
|
<AmountField
|
||||||
inputClassName="border-none"
|
inputClassName="border-none"
|
||||||
@ -114,7 +107,7 @@ export const ProformaLineEditor = ({
|
|||||||
{
|
{
|
||||||
id: "itemDiscountPercentage",
|
id: "itemDiscountPercentage",
|
||||||
header: t("form_fields.items.discount_percentage.label", "Dto (%)"),
|
header: t("form_fields.items.discount_percentage.label", "Dto (%)"),
|
||||||
headClassName: "w-[100px] text-left",
|
headClassName: "text-left w-20",
|
||||||
cell: ({ index }) => (
|
cell: ({ index }) => (
|
||||||
<PercentageField
|
<PercentageField
|
||||||
inputClassName="border-none"
|
inputClassName="border-none"
|
||||||
@ -148,7 +141,7 @@ export const ProformaLineEditor = ({
|
|||||||
id: "total",
|
id: "total",
|
||||||
header: t("form_fields.items.total.label", "Total"),
|
header: t("form_fields.items.total.label", "Total"),
|
||||||
headClassName: "w-[120px] text-right",
|
headClassName: "w-[120px] text-right",
|
||||||
className: "text-right font-medium tabular-nums pt-4",
|
className: "text-right font-semibold tabular-nums pt-4",
|
||||||
cell: ({ index }) => MoneyHelper.formatCurrency(getItemAmounts(index).total, 2, currency),
|
cell: ({ index }) => MoneyHelper.formatCurrency(getItemAmounts(index).total, 2, currency),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export const ProformaTotalsSummary = ({
|
|||||||
value={`-${formatMoney(totals.lineDiscountTotal)}`}
|
value={`-${formatMoney(totals.lineDiscountTotal)}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{globalDiscountField ? <div>{globalDiscountField}</div> : null}
|
{globalDiscountField ? <>{globalDiscountField}</> : null}
|
||||||
|
|
||||||
<TotalsRow
|
<TotalsRow
|
||||||
description={formatPercent(totals.globalDiscountPercentage)}
|
description={formatPercent(totals.globalDiscountPercentage)}
|
||||||
@ -192,7 +192,7 @@ const TotalsRow = ({ label, value, description, className, strong = false }: Tot
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm",
|
"text-base",
|
||||||
strong ? "font-bold text-foreground" : "font-medium text-muted-foreground"
|
strong ? "font-bold text-foreground" : "font-medium text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -204,8 +204,8 @@ const TotalsRow = ({ label, value, description, className, strong = false }: Tot
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 text-right font-mono tabular-nums",
|
"shrink-0 text-right tabular-nums text-base",
|
||||||
strong ? "text-base font-bold" : "text-sm font-medium"
|
strong ? "font-bold" : "font-medium"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { ArrowLeftIcon, KeyboardIcon, MoreHorizontalIcon, SaveIcon, XIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export interface ProformaUpdateHeaderProps {
|
||||||
|
onSave?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
hasChanges?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProformaUpdateHeader = ({
|
||||||
|
onSave,
|
||||||
|
onCancel,
|
||||||
|
disabled = false,
|
||||||
|
readOnly = false,
|
||||||
|
hasChanges = false,
|
||||||
|
}: ProformaUpdateHeaderProps) => {
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-20">
|
||||||
|
<div className="flex h-14 items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button onClick={onCancel} size="icon" variant="outline">
|
||||||
|
<ArrowLeftIcon className="size-5" />
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h1 className="min-w-0 truncate text-xl font-semibold tracking-tight text-foreground lg:text-2xl">
|
||||||
|
Editar proforma
|
||||||
|
</h1>
|
||||||
|
{hasChanges && <span className="size-2 rounded-full bg-amber-500" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Atajo de teclado info */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger
|
||||||
|
render={
|
||||||
|
<Button className="hidden sm:flex" size="icon" variant="ghost">
|
||||||
|
<KeyboardIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TooltipContent className="max-w-xs" side="bottom">
|
||||||
|
<div className="space-y-1 text-xs">
|
||||||
|
<div>
|
||||||
|
<kbd className="rounded bg-muted px-1">Ctrl+S</kbd> Guardar
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<kbd className="rounded bg-muted px-1">Esc</kbd> Cancelar
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Button className="hidden sm:flex" onClick={onCancel} variant="ghost">
|
||||||
|
<XIcon className="mr-2 size-4" />
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={onSave}>
|
||||||
|
<SaveIcon className="mr-2 size-4" />
|
||||||
|
Guardar
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
render={
|
||||||
|
<Button size="icon" variant="ghost">
|
||||||
|
<MoreHorizontalIcon className="size-5" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem>Duplicar proforma</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Exportar PDF</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem className="text-destructive">Eliminar</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,11 +1,11 @@
|
|||||||
// packages/rdx-ui/src/components/feedback/info-alert.tsx
|
// packages/rdx-ui/src/components/feedback/info-alert.tsx
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@repo/shadcn-ui/components";
|
import { Alert, AlertDescription, AlertTitle } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { InfoIcon } from "lucide-react";
|
import { AlertCircleIcon } from "lucide-react";
|
||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
|
|
||||||
type ProformaInfoAlertProps = {
|
type ProformaInfoAlertProps = {
|
||||||
title: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -26,27 +26,32 @@ export const ProformaInfoAlert = ({
|
|||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex items-start gap-3 rounded-md border border-primary-200 bg-primary-100 px-4 py-3 text-primary-950 shadow-none",
|
"flex items-center gap-2 border-t bg-amber-50 px-4 py-2 text-sm text-amber-800 dark:bg-amber-950/30 dark:text-amber-200",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={cn("mt-0.5 flex shrink-0 items-center justify-center", iconClassName)}>
|
<div className={cn("mt-0.5 flex shrink-0 items-center justify-center", iconClassName)}>
|
||||||
<InfoIcon className="size-10 text-primary-600" />
|
<AlertCircleIcon className="size-4 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<AlertTitle
|
{title && (
|
||||||
className={cn(
|
<AlertTitle
|
||||||
"mb-0.5 text-base font-semibold leading-none text-primary-800",
|
className={cn(
|
||||||
titleClassName
|
"mb-0.5 text-sm font-semibold leading-none text-amber-800 dark:text-amber-200",
|
||||||
)}
|
titleClassName
|
||||||
>
|
)}
|
||||||
{title}
|
>
|
||||||
</AlertTitle>
|
{title}
|
||||||
|
</AlertTitle>
|
||||||
|
)}
|
||||||
|
|
||||||
{(description || children) && (
|
{(description || children) && (
|
||||||
<AlertDescription
|
<AlertDescription
|
||||||
className={cn("text-sm leading-5 text-foreground", descriptionClassName)}
|
className={cn(
|
||||||
|
"text-sm leading-5 text-amber-800 dark:text-amber-200",
|
||||||
|
descriptionClassName
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{description ?? children}
|
{description ?? children}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
|||||||
@ -38,7 +38,6 @@ export const ProformaUpdatePaymentEditor = ({
|
|||||||
<FormSectionGrid className="w-full">
|
<FormSectionGrid className="w-full">
|
||||||
<SelectField
|
<SelectField
|
||||||
className="col-span-full"
|
className="col-span-full"
|
||||||
disabled={disabled}
|
|
||||||
inputClassName="bg-background"
|
inputClassName="bg-background"
|
||||||
items={paymentMethodOptions}
|
items={paymentMethodOptions}
|
||||||
label={t("form_fields.proformas.payment_method.label", "Forma de pago")}
|
label={t("form_fields.proformas.payment_method.label", "Forma de pago")}
|
||||||
@ -47,7 +46,6 @@ export const ProformaUpdatePaymentEditor = ({
|
|||||||
"form_fields.proformas.payment_method.placeholder",
|
"form_fields.proformas.payment_method.placeholder",
|
||||||
"Selecciona una forma de pago"
|
"Selecciona una forma de pago"
|
||||||
)}
|
)}
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|||||||
@ -25,22 +25,20 @@ export const ProformaUpdateSettingsEditor = ({
|
|||||||
icon={<Settings2Icon className="size-5" />}
|
icon={<Settings2Icon className="size-5" />}
|
||||||
title={t("form_groups.proformas.settings.title", "Configuración")}
|
title={t("form_groups.proformas.settings.title", "Configuración")}
|
||||||
>
|
>
|
||||||
<FormSectionGrid className="w-full">
|
<FormSectionGrid className="md:grid-cols-1">
|
||||||
<SelectField
|
<SelectField
|
||||||
className="col-span-full"
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
inputClassName="bg-background"
|
inputClassName="bg-background"
|
||||||
label={t("form_fields.proformas.status.label")}
|
label={t("form_fields.proformas.status.label", "Estado")}
|
||||||
name="status"
|
name="status"
|
||||||
placeholder={t("form_fields.proformas.status.placeholder")}
|
placeholder={t("form_fields.proformas.status.placeholder")}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
className="col-span-full"
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
inputClassName="bg-background"
|
inputClassName="bg-background"
|
||||||
label={t("form_fields.proformas.currency_code.label")}
|
label={t("form_fields.proformas.currency_code.label", "Moneda")}
|
||||||
name="currencyCode"
|
name="currencyCode"
|
||||||
placeholder={t("form_fields.proformas.currency_code.placeholder")}
|
placeholder={t("form_fields.proformas.currency_code.placeholder")}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
import type { CustomerSelectionOption } from "@erp/customers";
|
import type { CustomerSelectionOption } from "@erp/customers";
|
||||||
import { PercentageField } from "@repo/rdx-ui/components";
|
import { PercentageField } from "@repo/rdx-ui/components";
|
||||||
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ProformaUpdatePaymentEditor,
|
ProformaUpdatePaymentEditor,
|
||||||
@ -18,7 +17,8 @@ import type {
|
|||||||
UseUpdateProformaTaxControllerResult,
|
UseUpdateProformaTaxControllerResult,
|
||||||
UseUpdateProformaTotalsControllerResult,
|
UseUpdateProformaTotalsControllerResult,
|
||||||
} from "../../controllers";
|
} from "../../controllers";
|
||||||
import { ProformaTotalsSummary } from "../blocks";
|
import { EditorSidebar, ProformaCompactTotals, ProformaTotalsSummary } from "../blocks";
|
||||||
|
import { NewProformaTotalsSummary } from "../blocks/new-proforma-totals-summary";
|
||||||
import { ProformaInfoAlert } from "../components";
|
import { ProformaInfoAlert } from "../components";
|
||||||
|
|
||||||
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
|
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
|
||||||
@ -63,29 +63,52 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="space-y-6 space-x-6 2xl:space-y-12"
|
className="space-y-6 2xl:space-y-12"
|
||||||
id={formId}
|
id={formId}
|
||||||
noValidate
|
noValidate
|
||||||
onKeyDown={preventEnterKeySubmitForm}
|
onKeyDown={preventEnterKeySubmitForm}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
<ProformaInfoAlert
|
{/* Alerta informativa */}
|
||||||
description="Esta proforma no tiene validez fiscal. Puedes convertirla en factura cuando sea aceptada por el cliente."
|
<ProformaInfoAlert>
|
||||||
title="Información importante"
|
<span className="font-semibold">Importante: </span>Esta proforma no tiene validez fiscal.
|
||||||
/>
|
Puedes convertirla en factura cuando sea aceptada por el cliente.
|
||||||
<div className="flex flex-row">
|
</ProformaInfoAlert>
|
||||||
<div className="basis-3/4 space-x-4 space-y-4 2xl:space-y-6 2xl:space-x-6">
|
|
||||||
<div className="grid grid-cols-1 2xl:grid-cols-12 gap-4">
|
|
||||||
<ProformaUpdateHeaderEditor className="2xl:col-span-8" disabled={isSubmitting} />
|
|
||||||
|
|
||||||
<ProformaUpdateRecipientEditor
|
{/* Contenido principal */}
|
||||||
className="xl:col-span-4"
|
<div className="flex flex-1 overflow-hidden hidden">
|
||||||
|
{/* Área principal */}
|
||||||
|
<main className="flex-1 overflow-y-auto">
|
||||||
|
<div className="mx-auto space-y-6 p-4 pb-24 lg:p-6">
|
||||||
|
{/* Formulario de datos básicos */}
|
||||||
|
<ProformaUpdateHeaderEditor className="2xl:col-span-full" disabled={isSubmitting} />
|
||||||
|
|
||||||
|
{/* Tabla de líneas */}
|
||||||
|
<ProformaUpdateItemsEditor
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onChangeCustomerClick={onChangeCustomerClick}
|
itemsCtrl={itemsCtrl}
|
||||||
onCreateCustomerClick={onCreateCustomerClick}
|
taxCtrl={taxCtrl}
|
||||||
selectedCustomer={selectedCustomer}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Sidebar - desktop */}
|
||||||
|
<aside className="hidden w-96 shrink-0 border-none bg-transparent lg:block">
|
||||||
|
<EditorSidebar
|
||||||
|
className="2xl:col-span-1"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onChangeCustomerClick={onChangeCustomerClick}
|
||||||
|
onCreateCustomerClick={onCreateCustomerClick}
|
||||||
|
selectedCustomer={selectedCustomer}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 2xl:grid-cols-4 gap-4 2xl:gap-6">
|
||||||
|
<div className="2xl:col-span-3 space-y-4 gap-4 2xl:gap-6 2xl:space-y-6">
|
||||||
|
<div className="grid 2xl:grid-cols-3 gap-4 2xl:gap-6 space-y-4 2xl:space-y-6">
|
||||||
|
<ProformaUpdateHeaderEditor className="2xl:col-span-full" disabled={isSubmitting} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<ProformaUpdateItemsEditor
|
<ProformaUpdateItemsEditor
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
@ -93,12 +116,13 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
taxCtrl={taxCtrl}
|
taxCtrl={taxCtrl}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-12">
|
<div className="grid 2xl:grid-cols-1 gap-4 2xl:gap-6 space-y-4 2xl:space-y-6">
|
||||||
<ProformaTotalsSummary
|
<ProformaTotalsSummary
|
||||||
className="md:col-span-8"
|
className="hidden"
|
||||||
currency={currencyCode}
|
currency={currencyCode}
|
||||||
globalDiscountField={
|
globalDiscountField={
|
||||||
<PercentageField
|
<PercentageField
|
||||||
|
className="md:col-span-4 md:col-start-1"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
inputClassName="bg-background"
|
inputClassName="bg-background"
|
||||||
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")}
|
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")}
|
||||||
@ -111,9 +135,35 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="basis-1/4 space-x-4 space-y-4 2xl:space-y-6 2xl:space-x-6">
|
<div className="2xl:col-span-1 space-y-4 2xl:space-y-6">
|
||||||
|
<ProformaUpdateRecipientEditor
|
||||||
|
className="2xl:col-span-1"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onChangeCustomerClick={onChangeCustomerClick}
|
||||||
|
onCreateCustomerClick={onCreateCustomerClick}
|
||||||
|
selectedCustomer={selectedCustomer}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProformaUpdateTaxEditor className="2xl:col-span-1" taxCtrl={taxCtrl} />
|
||||||
|
|
||||||
|
<NewProformaTotalsSummary
|
||||||
|
currency={currencyCode}
|
||||||
|
globalDiscountField={
|
||||||
|
<PercentageField
|
||||||
|
className="md:col-span-4 md:col-start-1"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
inputClassName="bg-background"
|
||||||
|
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")}
|
||||||
|
name="globalDiscountPercentage"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
layout="vertical"
|
||||||
|
showRec={taxCtrl.hasRecPercentage}
|
||||||
|
showRetention={taxCtrl.hasRetentionPercentage}
|
||||||
|
totals={totalsCtrl.totals}
|
||||||
|
/>
|
||||||
|
|
||||||
<ProformaUpdateSettingsEditor className="w-full bg-secondary ring-0" />
|
<ProformaUpdateSettingsEditor className="w-full bg-secondary ring-0" />
|
||||||
<ProformaUpdateTaxEditor className="w-full bg-secondary ring-0" taxCtrl={taxCtrl} />
|
|
||||||
<ProformaUpdatePaymentEditor
|
<ProformaUpdatePaymentEditor
|
||||||
className="w-full bg-secondary ring-0"
|
className="w-full bg-secondary ring-0"
|
||||||
disabled={isSubmitting || paymentCtrl.isLoading}
|
disabled={isSubmitting || paymentCtrl.isLoading}
|
||||||
@ -122,16 +172,24 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Footer fijo con totales */}
|
||||||
<div className="flex flex-col-reverse gap-3 border-t pt-4 sm:flex-row sm:justify-end">
|
<footer className="sticky bottom-0 z-30 bg-transparent">
|
||||||
<Button disabled={isSubmitting} onClick={onReset} type="button" variant="outline">
|
<ProformaCompactTotals
|
||||||
{t("common.reset", "Restablecer")}
|
currency={currencyCode}
|
||||||
</Button>
|
globalDiscountField={
|
||||||
|
<PercentageField
|
||||||
<Button disabled={isSubmitting} type="submit">
|
className="md:col-span-4 md:col-start-1"
|
||||||
{isSubmitting ? t("common.saving", "Guardando...") : t("common.save", "Guardar")}
|
disabled={isSubmitting}
|
||||||
</Button>
|
inputClassName="bg-background"
|
||||||
</div>
|
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")}
|
||||||
|
name="globalDiscountPercentage"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
showRec={taxCtrl.hasRecPercentage}
|
||||||
|
showRetention={taxCtrl.hasRetentionPercentage}
|
||||||
|
totals={totalsCtrl.totals}
|
||||||
|
/>
|
||||||
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -28,14 +28,13 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
return (
|
return (
|
||||||
<FormSectionCard
|
<FormSectionCard
|
||||||
className={className}
|
className={className}
|
||||||
description={t("form_groups.proformas.basic_info.description")}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
icon={<FileTextIcon className="size-5" />}
|
icon={<FileTextIcon className="size-5" />}
|
||||||
title={t("form_groups.proformas.basic_info.title")}
|
title={t("form_groups.proformas.basic_info.title")}
|
||||||
>
|
>
|
||||||
<FormSectionGrid>
|
<FormSectionGrid>
|
||||||
<SelectField
|
<SelectField
|
||||||
className="md:col-span-3"
|
className="md:col-span-1"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("form_fields.proformas.series.label")}
|
label={t("form_fields.proformas.series.label")}
|
||||||
name="series"
|
name="series"
|
||||||
@ -44,22 +43,31 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="md:col-span-6"
|
className="md:col-span-2"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("form_fields.proformas.invoice_number.label")}
|
label={t("form_fields.proformas.invoice_number.label")}
|
||||||
maxLength={16}
|
maxLength={16}
|
||||||
name="reference"
|
name="reference"
|
||||||
placeholder={t("form_fields.proformas.invoice_number.placeholder")}
|
placeholder={t("form_fields.proformas.invoice_number.placeholder")}
|
||||||
readOnly={readOnly}
|
readOnly={true}
|
||||||
rightIcon={
|
rightIcon={
|
||||||
<Badge className="text-sm" variant="secondary">
|
<Badge className="text-sm" variant="secondary">
|
||||||
Autom.
|
Autom.
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<TextField
|
||||||
|
className="md:col-span-5"
|
||||||
|
disabled={disabled}
|
||||||
|
label={t("form_fields.proformas.reference.label")}
|
||||||
|
maxLength={256}
|
||||||
|
name="reference"
|
||||||
|
placeholder={t("form_fields.proformas.reference.placeholder")}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
<DatePickerField
|
<DatePickerField
|
||||||
className="md:col-span-3 md:col-start-1"
|
className="md:col-span-2"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("form_fields.proformas.invoice_date.label")}
|
label={t("form_fields.proformas.invoice_date.label")}
|
||||||
name="invoiceDate"
|
name="invoiceDate"
|
||||||
@ -69,7 +77,7 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DatePickerField
|
<DatePickerField
|
||||||
className="md:col-span-3"
|
className="md:col-span-2"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("form_fields.proformas.operation_date.label")}
|
label={t("form_fields.proformas.operation_date.label")}
|
||||||
name="operationDate"
|
name="operationDate"
|
||||||
@ -77,16 +85,6 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
|
||||||
className="md:col-span-6"
|
|
||||||
disabled={disabled}
|
|
||||||
label={t("form_fields.proformas.reference.label")}
|
|
||||||
maxLength={256}
|
|
||||||
name="reference"
|
|
||||||
placeholder={t("form_fields.proformas.reference.placeholder")}
|
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="md:col-span-12"
|
className="md:col-span-12"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -96,9 +94,8 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
placeholder={t("form_fields.proformas.description.placeholder")}
|
placeholder={t("form_fields.proformas.description.placeholder")}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextAreaField
|
<TextAreaField
|
||||||
className="md:col-span-12"
|
className="md:col-span-12 hidden"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("form_fields.proformas.notes.label")}
|
label={t("form_fields.proformas.notes.label")}
|
||||||
maxLength={256}
|
maxLength={256}
|
||||||
|
|||||||
@ -22,10 +22,9 @@ export const ProformaUpdateItemsEditor = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSectionCard
|
<FormSectionCard
|
||||||
description={t("form_groups.items.description")}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
icon={<ListIcon className="size-5" />}
|
icon={<ListIcon className="size-3" />}
|
||||||
title={t("form_groups.items.title")}
|
title={t("form_groups.items.title", "Líneas de detalle")}
|
||||||
>
|
>
|
||||||
<ProformaLineEditor
|
<ProformaLineEditor
|
||||||
addItemAtStart={itemsCtrl.addItemAtStart}
|
addItemAtStart={itemsCtrl.addItemAtStart}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { FormProvider } from "react-hook-form";
|
|||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import { useUpdateProformaPageController } from "../../controllers/use-update-proforma-page-controller";
|
import { useUpdateProformaPageController } from "../../controllers/use-update-proforma-page-controller";
|
||||||
|
import { ProformaUpdateHeader } from "../blocks";
|
||||||
import { ProformaUpdateSkeleton } from "../components";
|
import { ProformaUpdateSkeleton } from "../components";
|
||||||
import { ProformaUpdateEditorForm } from "../editors";
|
import { ProformaUpdateEditorForm } from "../editors";
|
||||||
|
|
||||||
@ -58,6 +59,8 @@ export const ProformaUpdatePage = () => {
|
|||||||
</AppContent>
|
</AppContent>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
updateCtrl.form.d;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...updateCtrl.form}>
|
<FormProvider {...updateCtrl.form}>
|
||||||
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
||||||
@ -85,6 +88,10 @@ export const ProformaUpdatePage = () => {
|
|||||||
</AppHeader>
|
</AppHeader>
|
||||||
|
|
||||||
<AppContent className="mx-auto max-w-[100rem]">
|
<AppContent className="mx-auto max-w-[100rem]">
|
||||||
|
<ProformaUpdateHeader
|
||||||
|
disabled={updateCtrl.isUpdating}
|
||||||
|
hasChanges={updateCtrl.form.formState.isDirty}
|
||||||
|
/>
|
||||||
{updateCtrl.isUpdateError && (
|
{updateCtrl.isUpdateError && (
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
message={
|
message={
|
||||||
|
|||||||
@ -40,6 +40,8 @@ export const calculateProformaTotalsFromLines = ({
|
|||||||
|
|
||||||
const recTotal = taxBreakdown.reduce((acc, item) => acc + item.recAmount, 0);
|
const recTotal = taxBreakdown.reduce((acc, item) => acc + item.recAmount, 0);
|
||||||
|
|
||||||
|
const taxRecTotal = roundMoney(taxTotal + recTotal);
|
||||||
|
|
||||||
const normalizedRetentionPercentage =
|
const normalizedRetentionPercentage =
|
||||||
retentionPercentage === null ? null : toCalculationNumber(retentionPercentage);
|
retentionPercentage === null ? null : toCalculationNumber(retentionPercentage);
|
||||||
|
|
||||||
@ -67,6 +69,8 @@ export const calculateProformaTotalsFromLines = ({
|
|||||||
|
|
||||||
recTotal: roundMoney(recTotal),
|
recTotal: roundMoney(recTotal),
|
||||||
|
|
||||||
|
taxRecTotal: roundMoney(taxRecTotal),
|
||||||
|
|
||||||
retentionPercentage: normalizedRetentionPercentage,
|
retentionPercentage: normalizedRetentionPercentage,
|
||||||
|
|
||||||
retentionAmount: roundMoney(retentionAmount),
|
retentionAmount: roundMoney(retentionAmount),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user