.
This commit is contained in:
parent
ad79b0dbc4
commit
ecdc0379bd
@ -122,7 +122,7 @@
|
||||
"noClassAssign": "error",
|
||||
"noCommentText": "error",
|
||||
"noCompareNegZero": "error",
|
||||
"noConsole": "warn",
|
||||
"noConsole": "off",
|
||||
"noConstEnum": "error",
|
||||
"noControlCharactersInRegex": "error",
|
||||
"noDoubleEquals": "error",
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./invoice-dto.adapter";
|
||||
export * from "../proformas/adapters/proforma-dto.adapter";
|
||||
|
||||
export * from "./invoice-resume-dto.adapter";
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./invoice-context";
|
||||
@ -1 +1 @@
|
||||
export * from "./use-invoice-auto-recalc";
|
||||
export * from "./use-proforma-auto-recalc";
|
||||
|
||||
@ -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);
|
||||
@ -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";
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./invoice-update-page";
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1 +1,2 @@
|
||||
export * from "./proforma-dto.adapter";
|
||||
export * from "./proforma-summary-dto.adapter";
|
||||
|
||||
@ -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,
|
||||
@ -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";
|
||||
|
||||
@ -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}
|
||||
@ -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() });
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -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`)
|
||||
});
|
||||
};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./proforma-context";
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./proforma-update-page";
|
||||
@ -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}
|
||||
@ -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>
|
||||
);
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
export * from "./items";
|
||||
export * from "./proforma-basic-info-fields";
|
||||
export * from "./proforma-totals";
|
||||
export * from "./recipient";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./proforma-items-editor";
|
||||
@ -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 (
|
||||
@ -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}>
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./proforma-recipient";
|
||||
export * from "./proforma-recipient-modal-selector-field";
|
||||
@ -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>
|
||||
);
|
||||
@ -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();
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./items-editor";
|
||||
export * from "./proforma-editor-skeleton";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./items-editor";
|
||||
@ -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>
|
||||
@ -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 (
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./blocks";
|
||||
export * from "./components";
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./proforma.api.schema";
|
||||
export * from "./proforma.form.schema";
|
||||
export * from "./proforma-summary.web.schema";
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./proforma-tax-summary";
|
||||
@ -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,
|
||||
2
modules/customer-invoices/src/web/proformas/ui/index.ts
Normal file
2
modules/customer-invoices/src/web/proformas/ui/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./blocks";
|
||||
export * from "./components";
|
||||
@ -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";
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,4 +1 @@
|
||||
export * from "./invoice-basic-info-fields";
|
||||
export * from "./invoice-items-editor";
|
||||
export * from "./invoice-totals";
|
||||
export * from "./recipient";
|
||||
export * from "./items";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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")}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1 +1 @@
|
||||
export * from "./items-editor";
|
||||
export * from "./percentage-input-field";
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./invoice-recipient";
|
||||
export * from "./recipient-modal-selector-field";
|
||||
@ -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";
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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());
|
||||
},
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user