diff --git a/apps/web/src/layout/app-fullscreen-layout.tsx b/apps/web/src/layout/app-fullscreen-layout.tsx index 314ead21..81d07584 100644 --- a/apps/web/src/layout/app-fullscreen-layout.tsx +++ b/apps/web/src/layout/app-fullscreen-layout.tsx @@ -1,9 +1,11 @@ import { Outlet } from "react-router-dom"; +import { AppMain } from "./app-main"; + export const AppFullscreenLayout = () => { return ( -
+ -
+ ); }; diff --git a/modules/core/src/web/components/form/form-actions/page-form-header.tsx b/modules/core/src/web/components/form/form-actions/page-form-header.tsx index 8d3990e8..6fb4c658 100644 --- a/modules/core/src/web/components/form/form-actions/page-form-header.tsx +++ b/modules/core/src/web/components/form/form-actions/page-form-header.tsx @@ -32,7 +32,12 @@ export const PageFormHeader = ({ contentClassName, }: PageFormHeaderProps) => { return ( -
+
-

+

{title}

diff --git a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-items-form-to-proforma-line-inpus.ts b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-items-form-to-proforma-line-inpus.ts index 72d96598..4692c865 100644 --- a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-items-form-to-proforma-line-inpus.ts +++ b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-items-form-to-proforma-line-inpus.ts @@ -1,3 +1,5 @@ +import { isNullishOrEmpty } from "@repo/rdx-utils"; + import type { ProformaItemUpdateForm, ProformaLineInput } from "../entities"; /** @@ -16,16 +18,19 @@ export const mapProformaItemFormToProformaLineInputs = ( items: ProformaItemUpdateForm[], params: MapProformaItemFormToProformaLineInputsParams ): ProformaLineInput[] => { - return items - .filter((item) => item.isValued) - .map((item) => ({ - quantity: item.quantity, - unitAmount: item.unitAmount, + return ( + items + // quitar tuplas no valoradas + .filter((item) => !(isNullishOrEmpty(item.quantity) || isNullishOrEmpty(item.unitAmount))) + .map((item) => ({ + quantity: item.quantity, + unitAmount: item.unitAmount, - itemDiscountPercentage: item.itemDiscountPercentage, - globalDiscountPercentage: params.globalDiscountPercentage, + itemDiscountPercentage: item.itemDiscountPercentage, + globalDiscountPercentage: params.globalDiscountPercentage, - taxPercentage: item.taxPercentage, - recPercentage: item.recPercentage, - })); + taxPercentage: item.taxPercentage, + recPercentage: item.recPercentage, + })) + ); }; diff --git a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-items-to-proforma-items-update-form.adapter.ts b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-items-to-proforma-items-update-form.adapter.ts index be78eaac..b079c1d7 100644 --- a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-items-to-proforma-items-update-form.adapter.ts +++ b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-items-to-proforma-items-update-form.adapter.ts @@ -14,7 +14,7 @@ export const mapProformaItemsToProformaItemsUpdateForm = ( return { id: item.id, position: item.position, - isValued: item.isValued, + //isValued: item.isValued, <- campo calculado en frontend, no se mapea desde la API description: item.description ?? null, diff --git a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-to-proforma-update-form.adapter.ts b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-to-proforma-update-form.adapter.ts index 37ddbbe5..581e5ece 100644 --- a/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-to-proforma-update-form.adapter.ts +++ b/modules/customer-invoices/src/web/proformas/update/adapters/map-proforma-to-proforma-update-form.adapter.ts @@ -41,7 +41,8 @@ export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpd taxMode: fiscalDefaults.taxMode, taxRegimeCode: "01", //taxRegimeCode: proforma.taxRegimeCode ?? proformaDefaults.taxRegimeCode, // TODO: implementar en API - hasTaxPercentage: fiscalDefaults.defaultTaxPercentage !== null, + hasTaxPercentage: + fiscalDefaults.taxMode === "single" && fiscalDefaults.defaultTaxPercentage !== null, taxPercentage: fiscalDefaults.defaultTaxPercentage, hasRecPercentage: fiscalDefaults.defaultRecPercentage !== null, diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-items-controller.ts b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-items-controller.ts index 95007d50..a4f34463 100644 --- a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-items-controller.ts +++ b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-items-controller.ts @@ -1,3 +1,4 @@ +import { isNullishOrEmpty } from "@repo/rdx-utils"; import * as React from "react"; import { type FieldArrayWithId, @@ -16,6 +17,8 @@ import { calculateProformaLinesTotals } from "../utils/calculations/calculate-pr export interface ProformaItemAmounts { subtotal: number; itemDiscountAmount: number; + subtotalTaxable: number; + taxAmount: number; total: number; } @@ -35,6 +38,8 @@ export interface UseUpdateProformaItemsControllerResult { hasItems: boolean; itemCount: number; + getItemIsValued: (index: number) => boolean; + itemErrors: ProformaItemError[]; getItemError: (index: number) => ProformaItemError | undefined; getItemErrorMessage: (index: number) => string | undefined; @@ -96,9 +101,14 @@ export const useUpdateProformaItemsController = ({ const appendItem = React.useCallback(() => { const nextPosition = getValues("items")?.length ?? 0; - append(buildProformaItemUpdateDefault(nextPosition), { - shouldFocus: false, - }); + append( + buildProformaItemUpdateDefault(nextPosition, { + defaultTaxPercentage: getValues("taxPercentage") ?? undefined, + }), + { + shouldFocus: true, + } + ); }, [append, getValues]); const removeItem = React.useCallback( @@ -161,23 +171,6 @@ export const useUpdateProformaItemsController = ({ [getValues, replaceItems] ); - const getItemAmounts = React.useCallback( - (index: number): ProformaItemAmounts => { - const line = mapProformaItemFormToProformaLineInputs([items[index]], { - globalDiscountPercentage: null, - })[0]; - - const amounts = calculateProformaLineTotal(line); - - return { - subtotal: amounts.subtotalBeforeDiscounts, - itemDiscountAmount: amounts.itemDiscountAmount, - total: amounts.subtotalBeforeGlobalDiscount, - }; - }, - [items] - ); - const insertItemAt = React.useCallback( (index: number) => { const currentItems = getValues("items") ?? []; @@ -223,6 +216,19 @@ export const useUpdateProformaItemsController = ({ }; }, [items]); + const itemsIsValued = React.useMemo(() => { + return items.map((item, _) => { + return !(isNullishOrEmpty(item.quantity) || isNullishOrEmpty(item.unitAmount)); + }); + }, [items]); + + const getItemIsValued = React.useCallback( + (index: number): boolean => { + return itemsIsValued[index] ?? false; + }, + [itemsIsValued] + ); + const itemErrors = React.useMemo(() => { if (!Array.isArray(errors.items)) return []; @@ -241,7 +247,6 @@ export const useUpdateProformaItemsController = ({ const error = itemErrors[index]; return ( - error?.isValued?.message ?? error?.quantity?.message ?? error?.unitAmount?.message ?? error?.description?.message ?? @@ -251,6 +256,25 @@ export const useUpdateProformaItemsController = ({ [itemErrors] ); + const getItemAmounts = React.useCallback( + (index: number): ProformaItemAmounts => { + const line = mapProformaItemFormToProformaLineInputs([items[index]], { + globalDiscountPercentage: null, + })[0]; + + const amounts = calculateProformaLineTotal(line); + + return { + subtotal: amounts.subtotalBeforeDiscounts, + itemDiscountAmount: amounts.itemDiscountAmount, + subtotalTaxable: amounts.subtotalTaxable, + taxAmount: amounts.taxAmount, + total: amounts.totalAmount, + }; + }, + [items] + ); + return { fields, items, @@ -258,6 +282,8 @@ export const useUpdateProformaItemsController = ({ hasItems: items.length > 0, itemCount: items.length, + getItemIsValued, + itemErrors, getItemError, getItemErrorMessage, diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-tax-controller.ts b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-tax-controller.ts index d4484829..9adc9d1d 100644 --- a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-tax-controller.ts +++ b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-tax-controller.ts @@ -98,6 +98,8 @@ export const useUpdateProformaTaxController = ({ }, [taxPercentage, getValues, hasRecPercentage, setValue, taxMode]); const enablePerLineTaxes = useCallback(() => { + // Activar impuestos por línea + setValue("taxMode", "perLine", { shouldDirty: true, shouldTouch: true, @@ -106,6 +108,8 @@ export const useUpdateProformaTaxController = ({ }, [setValue]); const disablePerLineTaxes = useCallback(() => { + // Desactivar impuestos por línea + setValue("taxMode", "single", { shouldDirty: true, shouldTouch: true, diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-calculation.entity.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-calculation.entity.ts index eb4d7f67..499eb6d1 100644 --- a/modules/customer-invoices/src/web/proformas/update/entities/proforma-calculation.entity.ts +++ b/modules/customer-invoices/src/web/proformas/update/entities/proforma-calculation.entity.ts @@ -15,6 +15,9 @@ export interface ProformaLineTotal { subtotalBeforeGlobalDiscount: number; globalDiscountAmount: number; totalDiscountAmount: number; + subtotalTaxable: number; + taxAmount: number; + totalAmount: number; } export interface ProformaTaxBreakdown { diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-form.entity.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-form.entity.ts index 5e49915c..7766d3ee 100644 --- a/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-form.entity.ts +++ b/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-form.entity.ts @@ -1,7 +1,7 @@ export interface ProformaItemUpdateForm { id: string; position: number; - isValued: boolean; + //isValued: boolean; <- campo calculado en frontend, no se mapea desde la API description: string | null; diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-form.schema.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-form.schema.ts index e384da1d..fef5f62b 100644 --- a/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-form.schema.ts +++ b/modules/customer-invoices/src/web/proformas/update/entities/proforma-item-update-form.schema.ts @@ -17,7 +17,7 @@ import { z } from "zod/v4"; export const ProformaItemUpdateFormSchema = z.object({ id: z.string(), position: z.number(), - isValued: z.boolean(), + //isValued: z.boolean(), <- campo calculado en frontend, no se mapea desde la API description: z.string().nullable(), diff --git a/modules/customer-invoices/src/web/proformas/update/ui/blocks/line-editor.tsx b/modules/customer-invoices/src/web/proformas/update/ui/blocks/line-editor.tsx index b52ca175..0c2a7ddb 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/blocks/line-editor.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/blocks/line-editor.tsx @@ -63,6 +63,7 @@ export interface LineEditorProps { moveDownLabel: string; removeLabel: string; + disabled?: boolean; className?: string; } @@ -96,6 +97,8 @@ export const LineEditor = ({ moveDownLabel, removeLabel, + disabled = false, + className, }: LineEditorProps) => { return ( @@ -104,12 +107,24 @@ export const LineEditor = ({

{title}

- - @@ -132,7 +147,7 @@ export const LineEditor = ({ ))} - + Acciones @@ -162,13 +177,13 @@ export const LineEditor = ({ ))} - - + + boolean; getItemAmounts: (index: number) => ProformaItemAmounts; getItemErrorMessage: (index: number) => string | undefined; @@ -40,15 +41,16 @@ interface ProformaLineEditorProps { removeItem: (index: number) => void; currency?: string; + disabled?: boolean; className?: string; } -2; export const ProformaLineEditor = ({ fields, showLineTaxes, + getItemIsValued, getItemAmounts, getItemErrorMessage, @@ -65,6 +67,7 @@ export const ProformaLineEditor = ({ removeItem, currency = "EUR", + disabled = false, className, }: ProformaLineEditorProps) => { const { t } = useTranslation(); @@ -109,6 +112,22 @@ export const ProformaLineEditor = ({ /> ), }, + + { + id: "subtotal", + header: t("form_fields.items.subtotal.label", "Subtotal"), + headClassName: "text-right", + className: "text-right font-semibold tabular-nums pt-4", + //cell: ({ index }) => MoneyHelper.formatCurrency(getItemAmounts(index).total, 2, currency), + cell: ({ index }) => { + const isValued = getItemIsValued(index); + if (!isValued) return ""; + + const { subtotal } = getItemAmounts(index); + return MoneyHelper.formatCurrency(subtotal, 2, currency); + }, + }, + { id: "itemDiscountPercentage", header: t("form_fields.items.discount_percentage.label", "Dto (%)"), @@ -123,6 +142,21 @@ export const ProformaLineEditor = ({ ), }, + { + id: "itemDiscountAmount", + header: t("form_fields.items.itemDiscountAmount.label", "Imp. dto."), + headClassName: "text-right", + className: "text-right font-semibold tabular-nums pt-4", + //cell: ({ index }) => MoneyHelper.formatCurrency(getItemAmounts(index).total, 2, currency), + cell: ({ index }) => { + const isValued = getItemIsValued(index); + if (!isValued) return ""; + + const { itemDiscountAmount } = getItemAmounts(index); + return MoneyHelper.formatCurrency(itemDiscountAmount, 2, currency); + }, + }, + ...(showLineTaxes ? [ { @@ -142,13 +176,74 @@ export const ProformaLineEditor = ({ ] : []), - { - id: "total", - header: t("form_fields.items.total.label", "Total"), - headClassName: "w-[200px] text-right", - className: "text-right font-semibold tabular-nums pt-4", - cell: ({ index }) => MoneyHelper.formatCurrency(getItemAmounts(index).total, 2, currency), - }, + ...(showLineTaxes + ? [ + { + id: "taxAmount", + header: t("form_fields.items.total.label", "Imp. IVA"), + headClassName: "w-[200px] text-right", + className: "text-right font-semibold tabular-nums pt-4", + cell: ({ index }: { index: number }) => { + const isValued = getItemIsValued(index); + if (!isValued) return ""; + + const { taxAmount } = getItemAmounts(index); + return MoneyHelper.formatCurrency(taxAmount); + }, + }, + ] + : []), + + /*{ + id: "isValued", + header: t("form_fields.items.is_valued.label", "Val."), + headClassName: "text-center", + className: "w-[60px] text-center", + cell: ({ index }) => { + const isValued = getItemIsValued(index); + return isValued ? ( + + ) : ( + + ); + }, + },*/ + + ...(showLineTaxes + ? [ + { + id: "total", + header: t("form_fields.items.total.label", "Total"), + headClassName: "w-[200px] text-right", + className: "text-right font-semibold tabular-nums pt-4", + cell: ({ index }: { index: number }) => { + const isValued = getItemIsValued(index); + if (!isValued) return ""; + + const { total } = getItemAmounts(index); + return MoneyHelper.formatCurrency(total, 2, currency); + }, + }, + ] + : []), + + ...(showLineTaxes + ? [] + : [ + { + id: "subtotalTaxable", + header: t("form_fields.items.total.label", "Total"), + headClassName: "w-[200px] text-right", + className: "text-right font-semibold tabular-nums pt-4", + cell: ({ index }: { index: number }) => { + const isValued = getItemIsValued(index); + if (!isValued) return ""; + + const { subtotalTaxable } = getItemAmounts(index); + return MoneyHelper.formatCurrency(subtotalTaxable, 2, currency); + }, + }, + ]), ]; return ( @@ -158,6 +253,7 @@ export const ProformaLineEditor = ({ addAtStartLabel={t("common.add_at_start", "Al inicio")} className={className} columns={columns} + disabled={disabled} duplicateLabel={t("common.duplicate", "Duplicar")} getLineErrorMessage={(_, index) => { const message = getItemErrorMessage(index); @@ -179,7 +275,12 @@ export const ProformaLineEditor = ({ onRemove={removeItem} removeLabel={t("common.remove", "Eliminar")} renderFooter={() => ( - + )} title={t("form_fields.items.title", "Líneas de detalle")} /> @@ -188,13 +289,14 @@ export const ProformaLineEditor = ({ type ProformaLineFooterEditorProps = Pick< ProformaLineEditorProps, - "totals" | "itemsTotals" | "currency" + "totals" | "itemsTotals" | "currency" | "disabled" >; export const ProformaLineFooterEditor = ({ totals, itemsTotals, currency, + disabled, }: ProformaLineFooterEditorProps) => { const { t } = useTranslation(); @@ -205,6 +307,7 @@ export const ProformaLineFooterEditor = ({ Descuento global (%):
- { const { t } = useTranslation(); - const showActions = !(readOnly || disabled); + const showActions = !readOnly; return ( @@ -66,9 +66,10 @@ export const ProformaUpdateRecipientEditor = ({ {showActions && ( - + -