This commit is contained in:
David Arranz 2025-11-17 10:32:25 +01:00
parent ad79b0dbc4
commit ecdc0379bd
55 changed files with 633 additions and 323 deletions

View File

@ -122,7 +122,7 @@
"noClassAssign": "error",
"noCommentText": "error",
"noCompareNegZero": "error",
"noConsole": "warn",
"noConsole": "off",
"noConstEnum": "error",
"noControlCharactersInRegex": "error",
"noDoubleEquals": "error",

View File

@ -1,2 +1,3 @@
export * from "./invoice-dto.adapter";
export * from "../proformas/adapters/proforma-dto.adapter";
export * from "./invoice-resume-dto.adapter";

View File

@ -1 +0,0 @@
export * from "./invoice-context";

View File

@ -1 +1 @@
export * from "./use-invoice-auto-recalc";
export * from "./use-proforma-auto-recalc";

View File

@ -1,14 +1,16 @@
import { TaxCatalogProvider } from "@erp/core";
import type { TaxCatalogProvider } from "@erp/core";
import React from "react";
import { UseFormReturn, useWatch } from "react-hook-form";
import { type UseFormReturn, useWatch } from "react-hook-form";
import {
type InvoiceItemCalcResult,
calculateInvoiceHeaderAmounts,
calculateInvoiceItemAmounts,
InvoiceItemCalcResult,
} from "../../domain";
import { InvoiceFormData, InvoiceItemFormData } from "../../schemas";
import type { ProformaFormData } from "../../proformas/schema";
import type { InvoiceFormData, InvoiceItemFormData } from "../../schemas";
export type UseInvoiceAutoRecalcParams = {
export type UseProformaAutoRecalcParams = {
currency_code: string;
taxCatalog: TaxCatalogProvider;
debug?: boolean;
@ -20,9 +22,9 @@ export type UseInvoiceAutoRecalcParams = {
* Adaptado a formulario con números planos (no DTOs).
* Evita renders innecesarios (debounce + useDeferredValue).
*/
export function useInvoiceAutoRecalc(
form: UseFormReturn<InvoiceFormData>,
{ currency_code, taxCatalog, debug = true }: UseInvoiceAutoRecalcParams
export function useProformaAutoRecalc(
form: UseFormReturn<ProformaFormData>,
{ currency_code, taxCatalog, debug = true }: UseProformaAutoRecalcParams
) {
const { trigger, control } = form;
@ -120,7 +122,7 @@ export function useInvoiceAutoRecalc(
setInvoiceTotals(form, totals);
if (debug) console.log("📊 Recalc invoice totals", totals.subtotal_amount);
void trigger([
trigger([
"subtotal_amount",
"discount_amount",
"taxable_amount",
@ -141,6 +143,7 @@ export function useInvoiceAutoRecalc(
form,
trigger,
debug,
prevDiscount,
]);
}
@ -168,8 +171,6 @@ function setInvoiceTotals(
const { setValue } = form;
const opts = { shouldDirty: true, shouldValidate: false } as const;
console.log(totals);
setValue("subtotal_amount", totals.subtotal_amount, opts);
setValue("items_discount_amount", totals.items_discount_amount, opts);
setValue("discount_amount", totals.discount_amount, opts);

View File

@ -4,4 +4,3 @@ export * from "./use-customer-invoices-query";
export * from "./use-invoice-query";
export * from "./use-items-table-navigation";
export * from "./use-pinned-preview-sheet";
export * from "./use-update-customer-invoice-mutation";

View File

@ -1 +0,0 @@
export * from "./invoice-update-page";

View File

@ -1,51 +0,0 @@
import { SpainTaxCatalogProvider } from "@erp/core";
import { useUrlParamId } from "@erp/core/hooks";
import { ErrorAlert } from "@erp/customers/components";
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
import { useMemo } from "react";
import { InvoiceProvider } from "../../context";
import { useInvoiceQuery } from "../../hooks";
import { useTranslation } from "../../i18n";
import { CustomerInvoiceEditorSkeleton } from "../../shared/ui/components";
import { InvoiceUpdateComp } from "./invoice-update-comp";
export const InvoiceUpdatePage = () => {
const invoice_id = useUrlParamId();
const { t } = useTranslation();
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
const invoiceQuery = useInvoiceQuery(invoice_id, { enabled: !!invoice_id });
const { data: invoiceData, isLoading, isError, error } = invoiceQuery;
if (isLoading) {
return <CustomerInvoiceEditorSkeleton />;
}
if (isError || !invoiceData) {
return (
<AppContent>
<ErrorAlert
message={(error as Error)?.message || "Error al cargar la factura"}
title={t("pages.update.loadErrorTitle")}
/>
<BackHistoryButton />
</AppContent>
);
}
// Monta el contexto aquí, así todo lo que esté dentro puede usar hooks
return (
<InvoiceProvider
company_id={invoiceData.company_id}
currency_code={invoiceData.currency_code}
invoice_id={invoice_id!}
language_code={invoiceData.language_code}
status={invoiceData.status}
taxCatalog={taxCatalog}
>
<InvoiceUpdateComp invoice={invoiceData} />
</InvoiceProvider>
);
};

View File

@ -1 +1,2 @@
export * from "./proforma-dto.adapter";
export * from "./proforma-summary-dto.adapter";

View File

@ -1,17 +1,23 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import {
MoneyDTOHelper,
PercentageDTOHelper,
QuantityDTOHelper,
type TaxCatalogProvider,
} from "@erp/core";
import type {
GetIssuedInvoiceByIdResponseDTO,
UpdateCustomerInvoiceByIdRequestDTO,
} from "../../common";
import type { InvoiceContextValue } from "../context";
import type { InvoiceFormData } from "../schemas/invoice.form.schema";
import type { Proforma, ProformaFormData, UpdateProformaInput } from "../schema";
export type ProformaDtoAdapterContext = {
taxCatalog: TaxCatalogProvider;
currency_code: string;
language_code: string;
};
/**
* Convierte el DTO completo de API a datos numéricos para el formulario.
*/
export const invoiceDtoToFormAdapter = {
fromDto(dto: GetIssuedInvoiceByIdResponseDTO, context: InvoiceContextValue): InvoiceFormData {
export const ProformaDtoAdapter = {
fromDto(dto: Proforma, context: ProformaDtoAdapterContext): ProformaFormData {
const { taxCatalog } = context;
return {
invoice_number: dto.invoice_number,
@ -63,8 +69,8 @@ export const invoiceDtoToFormAdapter = {
};
},
toDto(form: InvoiceFormData, context: InvoiceContextValue): UpdateCustomerInvoiceByIdRequestDTO {
const { currency_code } = context;
toDto(form: ProformaFormData, context: ProformaDtoAdapterContext): UpdateProformaInput {
const { currency_code, language_code } = context;
return {
series: form.series,
@ -77,8 +83,8 @@ export const invoiceDtoToFormAdapter = {
description: form.description,
notes: form.notes,
language_code: context.language_code,
currency_code: context.currency_code,
language_code,
currency_code,
items: form.items?.map((item) => ({
description: item.description,

View File

@ -1,2 +1,4 @@
export * from "./use-proforma-items-columns";
export * from "./use-proforma-query";
export * from "./use-proforma-update-mutation";
export * from "./use-proformas-query";

View File

@ -5,16 +5,15 @@ import type { ColumnDef } from "@tanstack/react-table";
import * as React from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useInvoiceContext } from "../../../../../context";
import { CustomerInvoiceTaxesMultiSelect } from "../../customer-invoice-taxes-multi-select";
import { ProformaTaxesMultiSelect } from "../../shared";
import { AmountInputField } from "../../shared/ui/components/editor/items/amount-input-field";
import { HoverCardTotalsSummary } from "../../shared/ui/components/editor/items/hover-card-total-summary";
import { ItemDataTableRowActions } from "../../shared/ui/components/editor/items/items-data-table-row-actions";
import { PercentageInputField } from "../../shared/ui/components/editor/items/percentage-input-field";
import { QuantityInputField } from "../../shared/ui/components/editor/items/quantity-input-field";
import { useProformaContext } from "../pages/update/context";
import { AmountInputField } from "./amount-input-field";
import { HoverCardTotalsSummary } from "./hover-card-total-summary";
import { ItemDataTableRowActions } from "./items-data-table-row-actions";
import { PercentageInputField } from "./percentage-input-field";
import { QuantityInputField } from "./quantity-input-field";
export interface InvoiceItemFormData {
export interface ProformaItemFormData {
id: string; // ← mapea RHF field.id aquí
description: string;
quantity: number | "";
@ -26,16 +25,16 @@ export interface InvoiceItemFormData {
taxes_amount: number | "";
total_amount: number | ""; // readonly calculado
}
export interface InvoiceFormData {
items: InvoiceItemFormData[];
export interface ProformaFormData {
items: ProformaItemFormData[];
}
export function useItemsColumns(): ColumnDef<InvoiceItemFormData>[] {
const { t, readOnly, currency_code, language_code } = useInvoiceContext();
const { control } = useFormContext<InvoiceFormData>();
export function useProformaItemsColumns(): ColumnDef<ProformaItemFormData>[] {
const { t, readOnly, currency_code, language_code } = useProformaContext();
const { control } = useFormContext<ProformaFormData>();
// Atención: Memoizar siempre para evitar reconstrucciones y resets de estado de tabla
return React.useMemo<ColumnDef<InvoiceItemFormData>[]>(
return React.useMemo<ColumnDef<ProformaItemFormData>[]>(
() => [
{
id: "position",
@ -244,7 +243,7 @@ export function useItemsColumns(): ColumnDef<InvoiceItemFormData>[] {
control={control}
name={`items.${row.index}.tax_codes`}
render={({ field }) => (
<CustomerInvoiceTaxesMultiSelect
<ProformaTaxesMultiSelect
{...field}
data-cell-focus
data-col-index={7}

View File

@ -2,16 +2,18 @@ import { useDataSource } from "@erp/core/hooks";
import { ValidationErrorCollection } from "@repo/rdx-ddd";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { type UpdateProformaByIdRequestDTO, UpdateProformaByIdRequestSchema } from "../../common";
import type { InvoiceFormData } from "../schemas";
import {
type UpdateProformaByIdRequestDTO,
UpdateProformaByIdRequestSchema,
} from "../../../common";
import type { InvoiceFormData } from "../../schemas";
import { CUSTOMER_INVOICE_QUERY_KEY } from "./use-invoice-query";
import { PROFORMA_QUERY_KEY } from "./use-proforma-query";
import { PROFORMAS_QUERY_KEY } from "./use-proformas-query";
export const CUSTOMER_INVOICES_LIST_KEY = ["customer-invoices"] as const;
type UpdateProformaContext = unknown;
type UpdateCustomerInvoiceContext = {};
type UpdateCustomerInvoicePayload = {
type UpdateProformaPayload = {
id: string;
data: Partial<InvoiceFormData>;
};
@ -21,27 +23,18 @@ export function useUpdateProforma() {
const dataSource = useDataSource();
const schema = UpdateProformaByIdRequestSchema;
return useMutation<
InvoiceFormData,
Error,
UpdateCustomerInvoicePayload,
UpdateCustomerInvoiceContext
>({
mutationKey: ["customer-invoice:update"], //, customerId],
return useMutation<InvoiceFormData, Error, UpdateProformaPayload, UpdateProformaContext>({
mutationKey: ["proforma:update"], //, customerId],
mutationFn: async (payload) => {
const { id: invoiceId, data } = payload;
console.log(payload);
if (!invoiceId) {
throw new Error("customerInvoiceId is required");
}
const result = schema.safeParse(data);
if (!result.success) {
console.log(result);
// Construye errores detallados
const validationErrors = result.error.issues.map((err) => ({
field: err.path.join("."),
@ -59,7 +52,7 @@ export function useUpdateProforma() {
// Refresca inmediatamente el detalle
queryClient.setQueryData<UpdateProformaByIdRequestDTO>(
CUSTOMER_INVOICE_QUERY_KEY(invoiceId),
PROFORMA_QUERY_KEY(invoiceId),
updated
);
@ -67,7 +60,7 @@ export function useUpdateProforma() {
// queryClient.invalidateQueries({ queryKey: CUSTOMER_QUERY_KEY(customerId) });
// Invalida el listado para refrescar desde servidor
queryClient.invalidateQueries({ queryKey: CUSTOMER_INVOICES_LIST_KEY });
queryClient.invalidateQueries({ queryKey: PROFORMAS_QUERY_KEY() });
},
});
}

View File

@ -1,18 +1,19 @@
import type { CriteriaDTO } from "@erp/core";
import { useDataSource } from "@erp/core/hooks";
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
import type { ProformaSummaryPage } from "../schema/proforma.api.schema";
export const PROFORMAS_QUERY_KEY = (criteria: CriteriaDTO): QueryKey => [
export const PROFORMAS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
"proforma",
{
pageNumber: criteria.pageNumber ?? 0,
pageSize: criteria.pageSize ?? 10,
q: criteria.q ?? "",
filters: criteria.filters ?? [],
orderBy: criteria.orderBy ?? "",
order: criteria.order ?? "",
pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX,
pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE,
q: criteria?.q ?? "",
filters: criteria?.filters ?? [],
orderBy: criteria?.orderBy ?? "",
order: criteria?.order ?? "",
},
];
@ -36,6 +37,6 @@ export const useProformasQuery = (options?: ProformasQueryOptions) => {
});
},
enabled,
placeholderData: (previousData, previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
});
};

View File

@ -0,0 +1 @@
export * from "./proforma-context";

View File

@ -1,12 +1,20 @@
import { TaxCatalogProvider } from '@erp/core';
import { TFunction } from 'i18next';
import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from "react";
import { useTranslation } from "../i18n";
import { MODULE_NAME } from '../manifest';
import type { TaxCatalogProvider } from "@erp/core";
import type { TFunction } from "i18next";
import {
type PropsWithChildren,
createContext,
useCallback,
useContext,
useMemo,
useState,
} from "react";
export type InvoiceContextValue = {
import { useTranslation } from "../../../../i18n";
import type { MODULE_NAME } from "../../../../manifest";
export type ProformaContextValue = {
company_id: string;
invoice_id: string;
proforma_id: string;
status: string;
currency_code: string;
language_code: string;
@ -22,11 +30,11 @@ export type InvoiceContextValue = {
changeIsProforma: (value: boolean) => void;
};
const InvoiceContext = createContext<InvoiceContextValue | null>(null);
const ProformaContext = createContext<ProformaContextValue | null>(null);
export interface InvoiceProviderParams {
export interface ProformaProviderParams {
taxCatalog: TaxCatalogProvider;
invoice_id: string;
proforma_id: string;
company_id: string;
status: string; // default "draft"
language_code?: string; // default "es"
@ -37,10 +45,17 @@ export interface InvoiceProviderParams {
children: React.ReactNode;
}
export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, company_id, status: initialStatus = "draft", language_code: initialLang = "es",
currency_code: initialCurrency = "EUR", readOnly: initialReadOnly = false,
is_proforma: initialProforma = true, children }: PropsWithChildren<InvoiceProviderParams>) => {
export const ProformaProvider = ({
taxCatalog: initialTaxCatalog,
proforma_id,
company_id,
status: initialStatus = "draft",
language_code: initialLang = "es",
currency_code: initialCurrency = "EUR",
readOnly: initialReadOnly = false,
is_proforma: initialProforma = true,
children,
}: PropsWithChildren<ProformaProviderParams>) => {
const { t } = useTranslation();
// Estado interno local para campos dinámicos
@ -57,12 +72,11 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com
const setIsProformaMemo = useCallback((is_proforma: boolean) => setIsProforma(is_proforma), []);
const setReadOnlyMemo = useCallback((readOnly: boolean) => setReadOnly(readOnly), []);
const value = useMemo<InvoiceContextValue>(() => {
const value = useMemo<ProformaContextValue>(() => {
return {
t,
invoice_id,
proforma_id,
company_id,
status,
language_code,
@ -76,17 +90,30 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com
changeCurrency: setCurrencyMemo,
changeIsProforma: setIsProformaMemo,
setReadOnly: setReadOnlyMemo,
}
}, [t, readOnly, company_id, invoice_id, status, language_code, currency_code, is_proforma, taxCatalog, setLanguageMemo, setCurrencyMemo, setIsProformaMemo, setReadOnlyMemo]);
};
}, [
t,
readOnly,
company_id,
proforma_id,
status,
language_code,
currency_code,
is_proforma,
taxCatalog,
setLanguageMemo,
setCurrencyMemo,
setIsProformaMemo,
setReadOnlyMemo,
]);
return <InvoiceContext.Provider value={value}>{children}</InvoiceContext.Provider>;
return <ProformaContext.Provider value={value}>{children}</ProformaContext.Provider>;
};
export function useInvoiceContext(): InvoiceContextValue {
const context = useContext(InvoiceContext);
export function useProformaContext(): ProformaContextValue {
const context = useContext(ProformaContext);
if (!context) {
throw new Error("useInvoiceContext must be used within <InvoiceProvider>");
throw new Error("useProformaContext must be used within <ProformaProvider>");
}
return context;
}

View File

@ -0,0 +1 @@
export * from "./proforma-update-page";

View File

@ -6,32 +6,32 @@ import { useId, useMemo } from "react";
import { type FieldErrors, FormProvider } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useInvoiceContext } from "../../context";
import { useUpdateProforma } from "../../hooks";
import { useTranslation } from "../../i18n";
import { useTranslation } from "../../../i18n";
import { ProformaDtoAdapter } from "../../adapters";
import { useUpdateProforma } from "../../hooks/use-proforma-update-mutation";
import {
type InvoiceFormData,
InvoiceFormSchema,
type Proforma,
defaultCustomerInvoiceFormData,
invoiceDtoToFormAdapter,
} from "../../schemas";
type ProformaFormData,
ProformaFormSchema,
defaultProformaFormData,
} from "../../schema";
import { InvoiceUpdateForm } from "./invoice-update-form";
import { useProformaContext } from "./context";
import { ProformaUpdateForm } from "./proforma-update-form";
export type InvoiceUpdateCompProps = {
invoice: Proforma;
export type ProformaUpdateCompProps = {
proforma: Proforma;
};
export const InvoiceUpdateComp = ({ invoice: invoiceData }: InvoiceUpdateCompProps) => {
export const ProformaUpdateComp = ({ proforma: proformaData }: ProformaUpdateCompProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const formId = useId();
const context = useInvoiceContext();
const { invoice_id } = context;
const context = useProformaContext();
const { proforma_id } = context;
const isPending = !invoiceData;
const isPending = !proformaData;
const {
mutate,
@ -41,23 +41,23 @@ export const InvoiceUpdateComp = ({ invoice: invoiceData }: InvoiceUpdateCompPro
} = useUpdateProforma();
const initialValues = useMemo(() => {
return invoiceData
? invoiceDtoToFormAdapter.fromDto(invoiceData, context)
: defaultCustomerInvoiceFormData;
}, [invoiceData, context]);
return proformaData
? ProformaDtoAdapter.fromDto(proformaData, context)
: defaultProformaFormData;
}, [proformaData, context]);
const form = useHookForm<InvoiceFormData>({
resolverSchema: InvoiceFormSchema,
const form = useHookForm<ProformaFormData>({
resolverSchema: ProformaFormSchema,
initialValues,
disabled: !invoiceData || isUpdating,
disabled: !proformaData || isUpdating,
});
const handleSubmit = (formData: InvoiceFormData) => {
const handleSubmit = (formData: ProformaFormData) => {
console.log("Guardo factura");
const dto = invoiceDtoToFormAdapter.toDto(formData, context);
const dto = ProformaDtoAdapter.toDto(formData, context);
console.log("dto => ", dto);
mutate(
{ id: invoice_id, data: dto as Partial<InvoiceFormData> },
{ id: proforma_id, data: dto as Partial<ProformaFormData> },
{
onSuccess: () =>
showSuccessToast(t("pages.update.success.title"), t("pages.update.success.message")),
@ -67,13 +67,13 @@ export const InvoiceUpdateComp = ({ invoice: invoiceData }: InvoiceUpdateCompPro
};
const handleReset = () =>
form.reset((invoiceData as unknown as InvoiceFormData) ?? defaultCustomerInvoiceFormData);
form.reset((proformaData as unknown as ProformaFormData) ?? defaultProformaFormData);
const handleBack = () => {
navigate(-1);
};
const handleError = (errors: FieldErrors<InvoiceFormData>) => {
const handleError = (errors: FieldErrors<ProformaFormData>) => {
console.error("Errores en el formulario:", errors);
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
};
@ -85,26 +85,24 @@ export const InvoiceUpdateComp = ({ invoice: invoiceData }: InvoiceUpdateCompPro
backIcon
description={t("pages.edit.description")}
rightSlot={
<>
<UpdateCommitButtonGroup
cancel={{ formId, to: "/customer-invoices/list" }}
isLoading={isPending}
submit={{
formId,
variant: "default",
disabled: isPending,
label: t("pages.edit.actions.save_draft"),
}}
/>
</>
<UpdateCommitButtonGroup
cancel={{ formId, to: "/proformas/list" }}
isLoading={isPending}
submit={{
formId,
variant: "default",
disabled: isPending,
label: t("pages.proformas.edit.actions.save_draft"),
}}
/>
}
title={`${t("pages.edit.title")} #${invoiceData.invoice_number}`}
title={`${t("pages.proformas.edit.title")} #${proformaData.invoice_number}`}
/>
</AppHeader>
<AppContent>
<FormProvider {...form}>
<InvoiceUpdateForm
<ProformaUpdateForm
className="bg-white rounded-xl border shadow-xl max-w-full"
formId={formId}
onError={handleError}

View File

@ -2,28 +2,24 @@ import { FormDebug } from "@erp/core/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { type FieldErrors, useFormContext } from "react-hook-form";
import type { InvoiceFormData } from "../../schemas";
import {
InvoiceBasicInfoFields,
InvoiceItems,
InvoiceRecipient,
InvoiceTotals,
} from "../../shared/ui/components";
import type { ProformaFormData } from "../../schema";
interface InvoiceUpdateFormProps {
import { ProformaBasicInfoFields, ProformaItems, ProformaRecipient, ProformaTotals } from "./ui";
interface ProformaUpdateFormProps {
formId: string;
onSubmit: (data: InvoiceFormData) => void;
onError: (errors: FieldErrors<InvoiceFormData>) => void;
onSubmit: (data: ProformaFormData) => void;
onError: (errors: FieldErrors<ProformaFormData>) => void;
className?: string;
}
export const InvoiceUpdateForm = ({
export const ProformaUpdateForm = ({
formId,
onSubmit,
onError,
className,
}: InvoiceUpdateFormProps) => {
const form = useFormContext<InvoiceFormData>();
}: ProformaUpdateFormProps) => {
const form = useFormContext<ProformaFormData>();
return (
<form
@ -38,17 +34,17 @@ export const InvoiceUpdateForm = ({
<section className={cn("space-y-6 p-6", className)}>
<div className="w-full bg-transparent grid grid-cols-1 lg:grid-cols-3 gap-4">
<InvoiceRecipient className="flex flex-col" />
<InvoiceBasicInfoFields className="flex flex-col lg:col-span-2" />
<ProformaRecipient className="flex flex-col" />
<ProformaBasicInfoFields className="flex flex-col lg:col-span-2" />
</div>
<div className="w-full">
<InvoiceItems />
<ProformaItems />
</div>
<div className="w-full grid grid-cols-1 lg:grid-cols-2">
<InvoiceTotals className="lg:col-start-2" />
<ProformaTotals className="lg:col-start-2" />
</div>
<div className="w-full"></div>
<div className="w-full" />
</section>
</form>
);

View File

@ -0,0 +1,51 @@
import { SpainTaxCatalogProvider } from "@erp/core";
import { useUrlParamId } from "@erp/core/hooks";
import { ErrorAlert } from "@erp/customers/components";
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
import { useMemo } from "react";
import { useTranslation } from "../../../i18n";
import { useProformaQuery } from "../../hooks";
import { ProformaProvider } from "./context";
import { ProformaUpdateComp } from "./proforma-update-comp";
import { ProformaEditorSkeleton } from "./ui/components";
export const ProformaUpdatePage = () => {
const { t } = useTranslation();
const proforma_id = useUrlParamId();
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
const proformaQuery = useProformaQuery(proforma_id, { enabled: !!proforma_id });
const { data: proformaData, isLoading, isError, error } = proformaQuery;
if (isLoading) {
return <ProformaEditorSkeleton />;
}
if (isError || !proformaData) {
return (
<AppContent>
<ErrorAlert
message={(error as Error)?.message || "Error al cargar la factura"}
title={t("pages.update.loadErrorTitle")}
/>
<BackHistoryButton />
</AppContent>
);
}
// Monta el contexto aquí, así todo lo que esté dentro puede usar hooks
return (
<ProformaProvider
company_id={proformaData.company_id}
currency_code={proformaData.currency_code}
language_code={proformaData.language_code}
proforma_id={proforma_id!}
status={proformaData.status}
taxCatalog={taxCatalog}
>
<ProformaUpdateComp proforma={proformaData} />
</ProformaProvider>
);
};

View File

@ -0,0 +1,4 @@
export * from "./items";
export * from "./proforma-basic-info-fields";
export * from "./proforma-totals";
export * from "./recipient";

View File

@ -0,0 +1 @@
export * from "./proforma-items-editor";

View File

@ -1,11 +1,10 @@
import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from "@repo/shadcn-ui/components";
import type { ComponentProps } from "react";
import { useTranslation } from "../../../../i18n";
import { useTranslation } from "../../../../../../i18n";
import { ItemsEditor } from "../../components";
import { ItemsEditor } from "./items";
export const InvoiceItems = (props: ComponentProps<"fieldset">) => {
export const ProformaItems = (props: ComponentProps<"fieldset">) => {
const { t } = useTranslation();
return (

View File

@ -3,12 +3,12 @@ import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from "@repo/shadc
import type { ComponentProps } from "react";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "../../../../i18n";
import type { InvoiceFormData } from "../../../../schemas";
import { useTranslation } from "../../../../../i18n";
import type { ProformaFormData } from "../../../../schema";
export const InvoiceBasicInfoFields = (props: ComponentProps<"fieldset">) => {
export const ProformaBasicInfoFields = (props: ComponentProps<"fieldset">) => {
const { t } = useTranslation();
const { control } = useFormContext<InvoiceFormData>();
const { control } = useFormContext<ProformaFormData>();
return (
<FieldSet {...props}>

View File

@ -0,0 +1,155 @@
import { formatCurrency } from "@erp/core";
import {
FieldDescription,
FieldGroup,
FieldLegend,
FieldSet,
Separator,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { ReceiptIcon } from "lucide-react";
import type { ComponentProps } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { useTranslation } from "../../../../../i18n";
import { PercentageInputField } from "../../../../../shared/ui/";
import type { ProformaFormData } from "../../../../schema";
import { useProformaContext } from "../../context";
export const ProformaTotals = (props: ComponentProps<"fieldset">) => {
const { t } = useTranslation();
const { control } = useFormContext<ProformaFormData>();
const { currency_code, language_code, readOnly, taxCatalog } = useProformaContext();
const displayTaxes = useWatch({ control, name: "taxes", defaultValue: [] });
const subtotal_amount = useWatch({ control, name: "subtotal_amount", defaultValue: 0 });
const items_discount_amount = useWatch({
control,
name: "items_discount_amount",
defaultValue: 0,
});
const discount_amount = useWatch({ control, name: "discount_amount", defaultValue: 0 });
const taxable_amount = useWatch({ control, name: "taxable_amount", defaultValue: 0 });
const taxes_amount = useWatch({ control, name: "taxes_amount", defaultValue: 0 });
const total_amount = useWatch({ control, name: "total_amount", defaultValue: 0 });
return (
<FieldSet {...props}>
<FieldLegend className="hidden">
<ReceiptIcon className="size-6 text-muted-foreground" />
{t("form_groups.totals.title")}
</FieldLegend>
<FieldDescription className="hidden">{t("form_groups.totals.description")}</FieldDescription>
<FieldGroup className="grid grid-cols-1 border rounded-lg bg-muted/10 p-4 gap-4">
<div className="space-y-1.5">
{/* Sección: Subtotal y Descuentos */}
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Subtotal sin descuentos</span>
<span className="font-medium tabular-nums text-muted-foreground">
{formatCurrency(subtotal_amount, 2, currency_code, language_code)}
</span>
</div>
<div className="flex justify-between text-sm">
<div className="flex items-center gap-3">
<span className="text-muted-foreground">Descuento en líneas</span>
</div>
<span className="font-medium text-destructive tabular-nums">
-{formatCurrency(items_discount_amount, 2, currency_code, language_code)}
</span>
</div>
<div className="flex justify-between text-sm">
<div className="flex items-center gap-3">
<span className="text-muted-foreground">Descuento global</span>
<PercentageInputField
className={cn(
"w-20 text-right tabular-nums bg-background",
"border-input border text-sm shadow-xs"
)}
control={control}
inputId={"discount-percentage"}
name={"discount_percentage"}
readOnly={readOnly}
showSuffix={true}
/>
</div>
<span className="font-medium text-destructive tabular-nums">
-{formatCurrency(discount_amount, 2, currency_code, language_code)}
</span>
</div>
{/* Sección: Base Imponible */}
<div className="flex justify-between text-sm">
<span className="text-foreground">Base imponible</span>
<span className="font-medium tabular-nums">
{formatCurrency(taxable_amount, 2, currency_code, language_code)}
</span>
</div>
</div>
<Separator />
{/* Sección: Impuestos */}
<div className="space-y-1.5">
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
Impuestos y retenciones
</h3>
{taxCatalog.groups().map((group) => {
// Filtra impuestos de ese grupo
const taxesInGroup = displayTaxes?.filter((item) => {
const tax = taxCatalog.findByCode(item.tax_code).match(
(t) => t,
() => undefined
);
return tax?.group === group;
});
// Si el grupo no tiene impuestos, no renderiza nada
if (taxesInGroup?.length === 0) return null;
return (
<div className="space-y-1.5 leading-3" key={`tax-group-${group}`}>
{taxesInGroup?.map((item) => {
const tax = taxCatalog.findByCode(item.tax_code).match(
(t) => t,
() => undefined
);
return (
<div
className="flex items-center justify-between text-sm"
key={`${group}:${item.tax_code}`}
>
<span className="text-muted-foreground text-sm">{tax?.name}</span>
<span className="font-medium tabular-nums text-sm text-muted-foreground">
{formatCurrency(item.taxes_amount, 2, currency_code, language_code)}
</span>
</div>
);
})}
</div>
);
})}
<div className="flex justify-between text-sm mt-3">
<span className="text-foreground">Total de impuestos</span>
<span className="font-medium tabular-nums">
{formatCurrency(taxes_amount, 2, currency_code, language_code)}
</span>
</div>
</div>
<Separator />
<div className="flex justify-between text-sm ">
<span className="font-bold text-foreground">Total de la factura</span>
<span className="font-bold tabular-nums">
{formatCurrency(total_amount, 2, currency_code, language_code)}
</span>
</div>
</FieldGroup>
</FieldSet>
);
};

View File

@ -0,0 +1,2 @@
export * from "./proforma-recipient";
export * from "./proforma-recipient-modal-selector-field";

View File

@ -1,18 +1,17 @@
import { CustomerModalSelector } from "@erp/customers/components";
import { Field, FieldLabel } from "@repo/shadcn-ui/components";
import { cn } from '@repo/shadcn-ui/lib/utils';
import { CustomerSummary } from 'node_modules/@erp/customers/src/web/schemas';
import { cn } from "@repo/shadcn-ui/lib/utils";
import type { CustomerSummary } from "node_modules/@erp/customers/src/web/schemas";
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
import { Control, Controller, FieldPath, FieldValues } from "react-hook-form";
type CustomerModalSelectorFieldProps<TFormValues extends FieldValues> = {
type RecipientModalSelectorFieldProps<TFormValues extends FieldValues> = {
control: Control<TFormValues>;
name: FieldPath<TFormValues>;
label?: string;
description?: string;
orientation?: "vertical" | "horizontal" | "responsive",
orientation?: "vertical" | "horizontal" | "responsive";
disabled?: boolean;
required?: boolean;
@ -28,19 +27,17 @@ export function RecipientModalSelectorField<TFormValues extends FieldValues>({
label,
description,
orientation = 'vertical',
orientation = "vertical",
disabled = false,
required = false,
readOnly = false,
className,
initialRecipient = {},
}: CustomerModalSelectorFieldProps<TFormValues>) {
}: RecipientModalSelectorFieldProps<TFormValues>) {
const isDisabled = disabled;
const isReadOnly = readOnly && !disabled;
return (
<Controller
control={control}
@ -49,16 +46,24 @@ export function RecipientModalSelectorField<TFormValues extends FieldValues>({
const { name, value, onChange, onBlur, ref } = field;
return (
<Field data-invalid={fieldState.invalid} orientation={orientation} className={cn("gap-1", className)}>
{label && <FieldLabel className='text-xs text-muted-foreground text-nowrap' htmlFor={name}>{label}</FieldLabel>}
<Field
className={cn("gap-1", className)}
data-invalid={fieldState.invalid}
orientation={orientation}
>
{label && (
<FieldLabel className="text-xs text-muted-foreground text-nowrap" htmlFor={name}>
{label}
</FieldLabel>
)}
<CustomerModalSelector
value={value}
onValueChange={onChange}
disabled={isDisabled}
readOnly={isReadOnly}
initialCustomer={{
...initialRecipient as CustomerSummary
...(initialRecipient as CustomerSummary),
}}
onValueChange={onChange}
readOnly={isReadOnly}
value={value}
/>
</Field>
);

View File

@ -2,11 +2,11 @@ import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from "@repo/shadc
import type { ComponentProps } from "react";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "../../../../../i18n";
import { useTranslation } from "../../../../../../i18n";
import { RecipientModalSelectorField } from "./recipient-modal-selector-field";
import { RecipientModalSelectorField } from "./proforma-recipient-modal-selector-field";
export const InvoiceRecipient = (props: ComponentProps<"fieldset">) => {
export const ProformaRecipient = (props: ComponentProps<"fieldset">) => {
const { t } = useTranslation();
const { control, getValues } = useFormContext();

View File

@ -0,0 +1,2 @@
export * from "./items-editor";
export * from "./proforma-editor-skeleton";

View File

@ -0,0 +1 @@
export * from "./items-editor";

View File

@ -1,19 +1,19 @@
import { Button, Input, Label, Textarea } from "@repo/shadcn-ui/components";
import { useFormContext } from "react-hook-form";
import type { InvoiceFormData, InvoiceItemFormData } from "../../../../../schemas";
import type { ProformaFormData, ProformaItemFormData } from "../../../../../schema";
export function ItemRowEditor({
row,
index,
onClose,
}: {
row: InvoiceItemFormData;
row: ProformaItemFormData;
index: number;
onClose: () => void;
}) {
// Editor simple reutilizando el mismo RHF
const { register } = useFormContext<InvoiceFormData>();
const { register } = useFormContext<ProformaFormData>();
return (
<div className="grid gap-3">
<h3 className="text-base font-semibold">Edit line #{index + 1}</h3>

View File

@ -1,31 +1,34 @@
/** biome-ignore-all lint/complexity/noForEach: <explanation> */
/** biome-ignore-all lint/suspicious/useIterableCallbackReturn: <explanation> */
import { useProformaItemsColumns } from "@erp/customer-invoices/web/proformas/hooks";
import { DataTable, useWithRowSelection } from "@repo/rdx-ui/components";
import { useMemo } from "react";
import { useFieldArray, useFormContext } from "react-hook-form";
import { useInvoiceContext } from "../../../../../context";
import { useInvoiceAutoRecalc } from "../../../../../hooks";
import { useTranslation } from "../../../../../i18n";
import { type InvoiceFormData, defaultCustomerInvoiceItemFormData } from "../../../../../schemas";
import { useProformaAutoRecalc } from "../../../../../../hooks";
import { useTranslation } from "../../../../../../i18n";
import { type ProformaFormData, defaultProformaItemFormData } from "../../../../../schema";
import { useProformaContext } from "../../../context";
import { ItemRowEditor } from "./item-row-editor";
import { useItemsColumns } from "./use-items-columns";
const createEmptyItem = () => defaultCustomerInvoiceItemFormData;
const createEmptyItem = () => defaultProformaItemFormData;
export const ItemsEditor = () => {
const { t } = useTranslation();
const context = useInvoiceContext();
const form = useFormContext<InvoiceFormData>();
const context = useProformaContext();
const form = useFormContext<ProformaFormData>();
const { control } = form;
useInvoiceAutoRecalc(form, context);
useProformaAutoRecalc(form, context);
const { fields, append, remove, move, insert, update } = useFieldArray({
control,
name: "items",
});
const baseColumns = useWithRowSelection(useItemsColumns(), true);
const baseColumns = useWithRowSelection(useProformaItemsColumns(), true);
const columns = useMemo(() => baseColumns, [baseColumns]);
return (

View File

@ -0,0 +1,31 @@
// components/CustomerSkeleton.tsx
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components";
import { useTranslation } from "../../../../../i18n";
export const ProformaEditorSkeleton = () => {
const { t } = useTranslation();
return (
<AppContent>
<div className="flex items-center justify-between">
<div aria-hidden="true" className="space-y-2">
<div className="h-7 w-64 rounded-md bg-muted animate-pulse" />
<div className="h-5 w-96 rounded-md bg-muted animate-pulse" />
</div>
<div className="flex items-center gap-2">
<BackHistoryButton />
<Button aria-busy disabled>
{t("pages.update.submit")}
</Button>
</div>
</div>
<div aria-hidden="true" className="mt-6 grid gap-4">
<div className="h-10 w-full rounded-md bg-muted animate-pulse" />
<div className="h-10 w-full rounded-md bg-muted animate-pulse" />
<div className="h-28 w-full rounded-md bg-muted animate-pulse" />
</div>
<span className="sr-only">{t("pages.update.loading", "Cargando factura de cliente...")}</span>
</AppContent>
);
};

View File

@ -0,0 +1,2 @@
export * from "./blocks";
export * from "./components";

View File

@ -1,2 +1,3 @@
export * from "./proforma.api.schema";
export * from "./proforma.form.schema";
export * from "./proforma-summary.web.schema";

View File

@ -0,0 +1,122 @@
import { z } from "zod/v4";
export const ProformaItemFormSchema = z.object({
description: z.string().max(2000).optional().default(""),
quantity: z.any(), //NumericStringSchema.optional(),
unit_amount: z.any(), //NumericStringSchema.optional(),
subtotal_amount: z.any(), //z.number(),
discount_percentage: z.any(), //NumericStringSchema.optional(),
discount_amount: z.number(),
taxable_amount: z.number(),
tax_codes: z.array(z.string()).default([]),
taxes_amount: z.number(),
total_amount: z.number(),
});
export const ProformaFormSchema = z.object({
invoice_number: z.string().optional(),
series: z.string().optional(),
invoice_date: z.string().optional(),
operation_date: z.string().optional(),
customer_id: z.string().optional(),
recipient: z
.object({
id: z.string().optional(),
name: z.string().optional(),
tin: z.string().optional(),
street: z.string().optional(),
street2: z.string().optional(),
city: z.string().optional(),
province: z.string().optional(),
postal_code: z.string().optional(),
country: z.string().optional(),
})
.optional(),
reference: z.string().optional(),
description: z.string().optional(),
notes: z.string().optional(),
language_code: z
.string({
error: "El idioma es obligatorio",
})
.min(1, "Debe indicar un idioma")
.toUpperCase() // asegura mayúsculas
.default("es"),
currency_code: z
.string({
error: "La moneda es obligatoria",
})
.min(1, "La moneda no puede estar vacía")
.toUpperCase() // asegura mayúsculas
.default("EUR"),
taxes: z
.array(
z.object({
tax_code: z.string(),
tax_label: z.string(),
taxable_amount: z.number(),
taxes_amount: z.number(),
})
)
.optional(),
items: z.array(ProformaItemFormSchema).optional(),
subtotal_amount: z.number(),
items_discount_amount: z.number(),
discount_percentage: z.number(),
discount_amount: z.number(),
taxable_amount: z.number(),
taxes_amount: z.number(),
total_amount: z.number(),
});
export type ProformaFormData = z.infer<typeof ProformaFormSchema>;
export type ProformaItemFormData = z.infer<typeof ProformaItemFormSchema>;
export const defaultProformaItemFormData: ProformaItemFormData = {
description: "",
quantity: "",
unit_amount: "",
subtotal_amount: 0,
discount_percentage: "",
discount_amount: 0,
taxable_amount: 0,
tax_codes: ["iva_21"],
taxes_amount: 0,
total_amount: 0,
};
export const defaultProformaFormData: ProformaFormData = {
invoice_number: "",
series: "",
invoice_date: "",
operation_date: "",
reference: "",
description: "",
notes: "",
language_code: "es",
currency_code: "EUR",
items: [],
subtotal_amount: 0,
items_discount_amount: 0,
discount_amount: 0,
discount_percentage: 0,
taxable_amount: 0,
taxes_amount: 0,
total_amount: 0,
};

View File

@ -0,0 +1 @@
export * from "./proforma-tax-summary";

View File

@ -10,14 +10,14 @@ import { ReceiptIcon } from "lucide-react";
import type { ComponentProps } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { useInvoiceContext } from "../../../../context";
import { useTranslation } from "../../../../i18n";
import type { InvoiceFormData } from "../../../../schemas";
import { useTranslation } from "../../../i18n";
import { useProformaContext } from "../../pages/update/context";
import type { ProformaFormData } from "../../schema";
export const InvoiceTaxSummary = (props: ComponentProps<"fieldset">) => {
export const ProformaTaxSummary = (props: ComponentProps<"fieldset">) => {
const { t } = useTranslation();
const { control } = useFormContext<InvoiceFormData>();
const { currency_code, language_code } = useInvoiceContext();
const { control } = useFormContext<ProformaFormData>();
const { currency_code, language_code } = useProformaContext();
const taxes = useWatch({
control,

View File

@ -0,0 +1,2 @@
export * from "./blocks";
export * from "./components";

View File

@ -1,5 +1,5 @@
export * from "../adapters/invoice-dto.adapter";
export * from "../adapters/invoice-resume-dto.adapter";
export * from "../proformas/adapters/proforma-dto.adapter";
export * from "./invoice.form.schema";
export * from "./invoice-resume.form.schema";

View File

@ -1,35 +0,0 @@
// components/CustomerSkeleton.tsx
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components";
import { useTranslation } from "../../../i18n";
export const CustomerInvoiceEditorSkeleton = () => {
const { t } = useTranslation();
return (
<>
<AppContent>
<div className="flex items-center justify-between">
<div aria-hidden="true" className="space-y-2">
<div className="h-7 w-64 rounded-md bg-muted animate-pulse" />
<div className="h-5 w-96 rounded-md bg-muted animate-pulse" />
</div>
<div className="flex items-center gap-2">
<BackHistoryButton />
<Button aria-busy disabled>
{t("pages.update.submit")}
</Button>
</div>
</div>
<div aria-hidden="true" className="mt-6 grid gap-4">
<div className="h-10 w-full rounded-md bg-muted animate-pulse" />
<div className="h-10 w-full rounded-md bg-muted animate-pulse" />
<div className="h-28 w-full rounded-md bg-muted animate-pulse" />
</div>
<span className="sr-only">
{t("pages.update.loading", "Cargando factura de cliente...")}
</span>
</AppContent>
</>
);
};

View File

@ -1,4 +1 @@
export * from "./invoice-basic-info-fields";
export * from "./invoice-items-editor";
export * from "./invoice-totals";
export * from "./recipient";
export * from "./items";

View File

@ -11,8 +11,8 @@ import { ReceiptIcon } from "lucide-react";
import type { ComponentProps } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { useInvoiceContext } from "../../../../context";
import { useTranslation } from "../../../../i18n";
import { useInvoiceContext } from "../../../../proformas/pages/update/context";
import type { InvoiceFormData } from "../../../../schemas";
import { PercentageInputField } from "./items/percentage-input-field";

View File

@ -4,7 +4,7 @@ import { useFormContext } from "react-hook-form";
import { useTranslation } from "../../../../../i18n";
import type { InvoiceFormData } from "../../../../../schemas";
import { CustomerInvoiceTaxesMultiSelect } from "../../customer-invoice-taxes-multi-select";
import { ProformaTaxesMultiSelect } from "../../proforma-taxes-multi-select";
import type { CustomItemViewProps } from "./types";
@ -81,7 +81,7 @@ export const BlocksView = ({ items, removeItem, updateItem }: BlocksViewProps) =
</div>
<div className="space-y-2 col-start-1">
<CustomerInvoiceTaxesMultiSelect
<ProformaTaxesMultiSelect
control={control}
description={t("form_fields.item.tax_codes.description")}
label={t("form_fields.item.tax_codes.label")}

View File

@ -12,8 +12,8 @@ import {
import type { PropsWithChildren } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { useInvoiceContext } from "../../../../../context";
import { useTranslation } from "../../../../../i18n";
import { useInvoiceContext } from "../../../../../proformas/pages/update/context";
type HoverCardTotalsSummaryProps = PropsWithChildren & {
rowIndex: number;

View File

@ -1 +1 @@
export * from "./items-editor";
export * from "./percentage-input-field";

View File

@ -11,9 +11,9 @@ import { cn } from "@repo/shadcn-ui/lib/utils";
import { ArrowDownIcon, ArrowUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
import { type Control, Controller, type FieldValues } from "react-hook-form";
import { useInvoiceContext } from "../../../../../context";
import { useTranslation } from "../../../../../i18n";
import { CustomerInvoiceTaxesMultiSelect } from "../../customer-invoice-taxes-multi-select";
import { useInvoiceContext } from "../../../../../proformas/pages/update/context";
import { ProformaTaxesMultiSelect } from "../../proforma-taxes-multi-select";
import { AmountInputField } from "./amount-input-field";
import { HoverCardTotalsSummary } from "./hover-card-total-summary";
@ -154,7 +154,7 @@ export const ItemRow = <TFieldValues extends FieldValues = FieldValues>({
data-cell-focus
name={`items.${rowIndex}.tax_codes`}
render={({ field }) => (
<CustomerInvoiceTaxesMultiSelect
<ProformaTaxesMultiSelect
data-col-index={7}
data-row-index={rowIndex}
onChange={field.onChange}

View File

@ -12,9 +12,9 @@ import {
import { useCallback } from "react";
import { useFormContext } from "react-hook-form";
import { useInvoiceContext } from "../../../../../context";
import { useInvoiceAutoRecalc, useItemsTableNavigation } from "../../../../../hooks";
import { useItemsTableNavigation, useProformaAutoRecalc } from "../../../../../hooks";
import { useTranslation } from "../../../../../i18n";
import { useInvoiceContext } from "../../../../../proformas/pages/update/context";
import {
type InvoiceFormData,
type InvoiceItemFormData,
@ -51,7 +51,7 @@ export const ItemsEditor = ({ readOnly = false }: ItemsEditorProps) => {
const { selectedRows, selectedIndexes, selectAllState, toggleRow, setSelectAll, clearSelection } =
useRowSelection(fields.length);
useInvoiceAutoRecalc(form, context);
useProformaAutoRecalc(form, context);
const handleAddSelection = useCallback(() => {
if (readOnly) return;

View File

@ -1,2 +0,0 @@
export * from "./invoice-recipient";
export * from "./recipient-modal-selector-field";

View File

@ -1,8 +1,4 @@
export * from "../../../proformas/pages/list/ui/proforma-status-badge";
export * from "./customer-invoice-editor-skeleton";
export * from "./customer-invoice-prices-card";
export * from "./customer-invoice-taxes-multi-select";
export * from "./editor";
export * from "./editor/invoice-tax-summary";
export * from "./editor/invoice-totals";
export * from "./proforma-taxes-multi-select";

View File

@ -21,7 +21,7 @@ import { useFieldArray, useFormContext } from "react-hook-form";
import { useTranslation } from "../../../../i18n";
import { formatCurrency } from "../../../../pages/create/utils";
import { CustomerInvoiceTaxesMultiSelect } from "../customer-invoice-taxes-multi-select";
import { ProformaTaxesMultiSelect } from "../proforma-taxes-multi-select";
import {
CustomerInvoiceItemsSortableDataTable,
@ -213,7 +213,7 @@ export const CustomerInvoiceItemsCardEditor = ({
render={({ field }) => (
<FormItem>
<FormControl>
<CustomerInvoiceTaxesMultiSelect
<ProformaTaxesMultiSelect
{...field}
//onChange={(e) => field.onChange(Number(e.target.value) * 100)}
//value={field.value / 100}

View File

@ -5,7 +5,7 @@ import { useCallback, useMemo } from "react";
import { useTranslation } from "../../../i18n";
interface CustomerInvoiceTaxesMultiSelect {
interface ProformaTaxesMultiSelect {
value?: string[];
onChange: (selectedValues: string[]) => void;
className?: string;
@ -13,7 +13,7 @@ interface CustomerInvoiceTaxesMultiSelect {
[key: string]: any; // Allow other props to be passed
}
export const CustomerInvoiceTaxesMultiSelect = (props: CustomerInvoiceTaxesMultiSelect) => {
export const ProformaTaxesMultiSelect = (props: ProformaTaxesMultiSelect) => {
const { value, onChange, className, inputId, ...otherProps } = props;
const { t } = useTranslation();
@ -28,11 +28,11 @@ export const CustomerInvoiceTaxesMultiSelect = (props: CustomerInvoiceTaxesMulti
(selectedValues: string[]) => {
const groupMap = new Map<string | undefined, string>();
selectedValues.forEach((code) => {
for (const code of selectedValues) {
const item = taxCatalog.findByCode(code).getOrUndefined();
const group = item?.group ?? "ungrouped";
groupMap.set(group, code); // Sobrescribe el anterior del mismo grupo
});
}
return Array.from(groupMap.values());
},

View File

@ -17,7 +17,6 @@ import {
import { useEffect } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { CustomerInvoiceTaxesMultiSelect } from "../../../../../customer-invoices/src/web/shared/ui/components";
import { useTranslation } from "../../i18n";
import type { CustomerFormData } from "../../schemas";
@ -123,7 +122,7 @@ export const CustomerBasicInfoFields = ({
>
{t("form_fields.default_taxes.label")}
</FieldLabel>
<CustomerInvoiceTaxesMultiSelect
<ProformaTaxesMultiSelect
description={t("form_fields.default_taxes.description")}
label={t("form_fields.default_taxes.label")}
onChange={field.onChange}

View File

@ -80,7 +80,7 @@ export interface DataTableProps<TData, TValue> {
enablePagination?: boolean;
pageSize?: number;
enableRowSelection?: boolean;
EditorComponent?: React.ComponentType<{ row?: TData; index: number; onClose: () => void }>;
EditorComponent?: React.ComponentType<{ row: TData; index: number; onClose: () => void }>;
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
@ -299,7 +299,7 @@ export function DataTable<TData, TValue>({
<EditorComponent
index={editIndex}
onClose={handleCloseEditor}
row={data[editIndex]}
row={data[editIndex]!}
/>
</div>