.
This commit is contained in:
parent
ad79b0dbc4
commit
ecdc0379bd
@ -122,7 +122,7 @@
|
|||||||
"noClassAssign": "error",
|
"noClassAssign": "error",
|
||||||
"noCommentText": "error",
|
"noCommentText": "error",
|
||||||
"noCompareNegZero": "error",
|
"noCompareNegZero": "error",
|
||||||
"noConsole": "warn",
|
"noConsole": "off",
|
||||||
"noConstEnum": "error",
|
"noConstEnum": "error",
|
||||||
"noControlCharactersInRegex": "error",
|
"noControlCharactersInRegex": "error",
|
||||||
"noDoubleEquals": "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";
|
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 React from "react";
|
||||||
import { UseFormReturn, useWatch } from "react-hook-form";
|
import { type UseFormReturn, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type InvoiceItemCalcResult,
|
||||||
calculateInvoiceHeaderAmounts,
|
calculateInvoiceHeaderAmounts,
|
||||||
calculateInvoiceItemAmounts,
|
calculateInvoiceItemAmounts,
|
||||||
InvoiceItemCalcResult,
|
|
||||||
} from "../../domain";
|
} 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;
|
currency_code: string;
|
||||||
taxCatalog: TaxCatalogProvider;
|
taxCatalog: TaxCatalogProvider;
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
@ -20,9 +22,9 @@ export type UseInvoiceAutoRecalcParams = {
|
|||||||
* Adaptado a formulario con números planos (no DTOs).
|
* Adaptado a formulario con números planos (no DTOs).
|
||||||
* Evita renders innecesarios (debounce + useDeferredValue).
|
* Evita renders innecesarios (debounce + useDeferredValue).
|
||||||
*/
|
*/
|
||||||
export function useInvoiceAutoRecalc(
|
export function useProformaAutoRecalc(
|
||||||
form: UseFormReturn<InvoiceFormData>,
|
form: UseFormReturn<ProformaFormData>,
|
||||||
{ currency_code, taxCatalog, debug = true }: UseInvoiceAutoRecalcParams
|
{ currency_code, taxCatalog, debug = true }: UseProformaAutoRecalcParams
|
||||||
) {
|
) {
|
||||||
const { trigger, control } = form;
|
const { trigger, control } = form;
|
||||||
|
|
||||||
@ -120,7 +122,7 @@ export function useInvoiceAutoRecalc(
|
|||||||
setInvoiceTotals(form, totals);
|
setInvoiceTotals(form, totals);
|
||||||
if (debug) console.log("📊 Recalc invoice totals", totals.subtotal_amount);
|
if (debug) console.log("📊 Recalc invoice totals", totals.subtotal_amount);
|
||||||
|
|
||||||
void trigger([
|
trigger([
|
||||||
"subtotal_amount",
|
"subtotal_amount",
|
||||||
"discount_amount",
|
"discount_amount",
|
||||||
"taxable_amount",
|
"taxable_amount",
|
||||||
@ -141,6 +143,7 @@ export function useInvoiceAutoRecalc(
|
|||||||
form,
|
form,
|
||||||
trigger,
|
trigger,
|
||||||
debug,
|
debug,
|
||||||
|
prevDiscount,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,8 +171,6 @@ function setInvoiceTotals(
|
|||||||
const { setValue } = form;
|
const { setValue } = form;
|
||||||
const opts = { shouldDirty: true, shouldValidate: false } as const;
|
const opts = { shouldDirty: true, shouldValidate: false } as const;
|
||||||
|
|
||||||
console.log(totals);
|
|
||||||
|
|
||||||
setValue("subtotal_amount", totals.subtotal_amount, opts);
|
setValue("subtotal_amount", totals.subtotal_amount, opts);
|
||||||
setValue("items_discount_amount", totals.items_discount_amount, opts);
|
setValue("items_discount_amount", totals.items_discount_amount, opts);
|
||||||
setValue("discount_amount", totals.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-invoice-query";
|
||||||
export * from "./use-items-table-navigation";
|
export * from "./use-items-table-navigation";
|
||||||
export * from "./use-pinned-preview-sheet";
|
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";
|
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 {
|
import type { Proforma, ProformaFormData, UpdateProformaInput } from "../schema";
|
||||||
GetIssuedInvoiceByIdResponseDTO,
|
|
||||||
UpdateCustomerInvoiceByIdRequestDTO,
|
export type ProformaDtoAdapterContext = {
|
||||||
} from "../../common";
|
taxCatalog: TaxCatalogProvider;
|
||||||
import type { InvoiceContextValue } from "../context";
|
currency_code: string;
|
||||||
import type { InvoiceFormData } from "../schemas/invoice.form.schema";
|
language_code: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
||||||
*/
|
*/
|
||||||
export const invoiceDtoToFormAdapter = {
|
export const ProformaDtoAdapter = {
|
||||||
fromDto(dto: GetIssuedInvoiceByIdResponseDTO, context: InvoiceContextValue): InvoiceFormData {
|
fromDto(dto: Proforma, context: ProformaDtoAdapterContext): ProformaFormData {
|
||||||
const { taxCatalog } = context;
|
const { taxCatalog } = context;
|
||||||
return {
|
return {
|
||||||
invoice_number: dto.invoice_number,
|
invoice_number: dto.invoice_number,
|
||||||
@ -63,8 +69,8 @@ export const invoiceDtoToFormAdapter = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
toDto(form: InvoiceFormData, context: InvoiceContextValue): UpdateCustomerInvoiceByIdRequestDTO {
|
toDto(form: ProformaFormData, context: ProformaDtoAdapterContext): UpdateProformaInput {
|
||||||
const { currency_code } = context;
|
const { currency_code, language_code } = context;
|
||||||
return {
|
return {
|
||||||
series: form.series,
|
series: form.series,
|
||||||
|
|
||||||
@ -77,8 +83,8 @@ export const invoiceDtoToFormAdapter = {
|
|||||||
description: form.description,
|
description: form.description,
|
||||||
notes: form.notes,
|
notes: form.notes,
|
||||||
|
|
||||||
language_code: context.language_code,
|
language_code,
|
||||||
currency_code: context.currency_code,
|
currency_code,
|
||||||
|
|
||||||
items: form.items?.map((item) => ({
|
items: form.items?.map((item) => ({
|
||||||
description: item.description,
|
description: item.description,
|
||||||
@ -1,2 +1,4 @@
|
|||||||
|
export * from "./use-proforma-items-columns";
|
||||||
export * from "./use-proforma-query";
|
export * from "./use-proforma-query";
|
||||||
|
export * from "./use-proforma-update-mutation";
|
||||||
export * from "./use-proformas-query";
|
export * from "./use-proformas-query";
|
||||||
|
|||||||
@ -5,16 +5,15 @@ import type { ColumnDef } from "@tanstack/react-table";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { useInvoiceContext } from "../../../../../context";
|
import { ProformaTaxesMultiSelect } from "../../shared";
|
||||||
import { CustomerInvoiceTaxesMultiSelect } from "../../customer-invoice-taxes-multi-select";
|
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";
|
export interface ProformaItemFormData {
|
||||||
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 {
|
|
||||||
id: string; // ← mapea RHF field.id aquí
|
id: string; // ← mapea RHF field.id aquí
|
||||||
description: string;
|
description: string;
|
||||||
quantity: number | "";
|
quantity: number | "";
|
||||||
@ -26,16 +25,16 @@ export interface InvoiceItemFormData {
|
|||||||
taxes_amount: number | "";
|
taxes_amount: number | "";
|
||||||
total_amount: number | ""; // readonly calculado
|
total_amount: number | ""; // readonly calculado
|
||||||
}
|
}
|
||||||
export interface InvoiceFormData {
|
export interface ProformaFormData {
|
||||||
items: InvoiceItemFormData[];
|
items: ProformaItemFormData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useItemsColumns(): ColumnDef<InvoiceItemFormData>[] {
|
export function useProformaItemsColumns(): ColumnDef<ProformaItemFormData>[] {
|
||||||
const { t, readOnly, currency_code, language_code } = useInvoiceContext();
|
const { t, readOnly, currency_code, language_code } = useProformaContext();
|
||||||
const { control } = useFormContext<InvoiceFormData>();
|
const { control } = useFormContext<ProformaFormData>();
|
||||||
|
|
||||||
// Atención: Memoizar siempre para evitar reconstrucciones y resets de estado de tabla
|
// 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",
|
id: "position",
|
||||||
@ -244,7 +243,7 @@ export function useItemsColumns(): ColumnDef<InvoiceItemFormData>[] {
|
|||||||
control={control}
|
control={control}
|
||||||
name={`items.${row.index}.tax_codes`}
|
name={`items.${row.index}.tax_codes`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<CustomerInvoiceTaxesMultiSelect
|
<ProformaTaxesMultiSelect
|
||||||
{...field}
|
{...field}
|
||||||
data-cell-focus
|
data-cell-focus
|
||||||
data-col-index={7}
|
data-col-index={7}
|
||||||
@ -2,16 +2,18 @@ import { useDataSource } from "@erp/core/hooks";
|
|||||||
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { type UpdateProformaByIdRequestDTO, UpdateProformaByIdRequestSchema } from "../../common";
|
import {
|
||||||
import type { InvoiceFormData } from "../schemas";
|
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 UpdateProformaPayload = {
|
||||||
|
|
||||||
type UpdateCustomerInvoicePayload = {
|
|
||||||
id: string;
|
id: string;
|
||||||
data: Partial<InvoiceFormData>;
|
data: Partial<InvoiceFormData>;
|
||||||
};
|
};
|
||||||
@ -21,27 +23,18 @@ export function useUpdateProforma() {
|
|||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const schema = UpdateProformaByIdRequestSchema;
|
const schema = UpdateProformaByIdRequestSchema;
|
||||||
|
|
||||||
return useMutation<
|
return useMutation<InvoiceFormData, Error, UpdateProformaPayload, UpdateProformaContext>({
|
||||||
InvoiceFormData,
|
mutationKey: ["proforma:update"], //, customerId],
|
||||||
Error,
|
|
||||||
UpdateCustomerInvoicePayload,
|
|
||||||
UpdateCustomerInvoiceContext
|
|
||||||
>({
|
|
||||||
mutationKey: ["customer-invoice:update"], //, customerId],
|
|
||||||
|
|
||||||
mutationFn: async (payload) => {
|
mutationFn: async (payload) => {
|
||||||
const { id: invoiceId, data } = payload;
|
const { id: invoiceId, data } = payload;
|
||||||
|
|
||||||
console.log(payload);
|
|
||||||
|
|
||||||
if (!invoiceId) {
|
if (!invoiceId) {
|
||||||
throw new Error("customerInvoiceId is required");
|
throw new Error("customerInvoiceId is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = schema.safeParse(data);
|
const result = schema.safeParse(data);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.log(result);
|
|
||||||
|
|
||||||
// Construye errores detallados
|
// Construye errores detallados
|
||||||
const validationErrors = result.error.issues.map((err) => ({
|
const validationErrors = result.error.issues.map((err) => ({
|
||||||
field: err.path.join("."),
|
field: err.path.join("."),
|
||||||
@ -59,7 +52,7 @@ export function useUpdateProforma() {
|
|||||||
|
|
||||||
// Refresca inmediatamente el detalle
|
// Refresca inmediatamente el detalle
|
||||||
queryClient.setQueryData<UpdateProformaByIdRequestDTO>(
|
queryClient.setQueryData<UpdateProformaByIdRequestDTO>(
|
||||||
CUSTOMER_INVOICE_QUERY_KEY(invoiceId),
|
PROFORMA_QUERY_KEY(invoiceId),
|
||||||
updated
|
updated
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -67,7 +60,7 @@ export function useUpdateProforma() {
|
|||||||
// queryClient.invalidateQueries({ queryKey: CUSTOMER_QUERY_KEY(customerId) });
|
// queryClient.invalidateQueries({ queryKey: CUSTOMER_QUERY_KEY(customerId) });
|
||||||
|
|
||||||
// Invalida el listado para refrescar desde servidor
|
// 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 type { CriteriaDTO } from "@erp/core";
|
||||||
import { useDataSource } from "@erp/core/hooks";
|
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 DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import type { ProformaSummaryPage } from "../schema/proforma.api.schema";
|
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",
|
"proforma",
|
||||||
{
|
{
|
||||||
pageNumber: criteria.pageNumber ?? 0,
|
pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX,
|
||||||
pageSize: criteria.pageSize ?? 10,
|
pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE,
|
||||||
q: criteria.q ?? "",
|
q: criteria?.q ?? "",
|
||||||
filters: criteria.filters ?? [],
|
filters: criteria?.filters ?? [],
|
||||||
orderBy: criteria.orderBy ?? "",
|
orderBy: criteria?.orderBy ?? "",
|
||||||
order: criteria.order ?? "",
|
order: criteria?.order ?? "",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -36,6 +37,6 @@ export const useProformasQuery = (options?: ProformasQueryOptions) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
enabled,
|
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 type { TaxCatalogProvider } from "@erp/core";
|
||||||
import { TFunction } from 'i18next';
|
import type { TFunction } from "i18next";
|
||||||
import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from "react";
|
import {
|
||||||
import { useTranslation } from "../i18n";
|
type PropsWithChildren,
|
||||||
import { MODULE_NAME } from '../manifest';
|
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;
|
company_id: string;
|
||||||
invoice_id: string;
|
proforma_id: string;
|
||||||
status: string;
|
status: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
language_code: string;
|
language_code: string;
|
||||||
@ -22,11 +30,11 @@ export type InvoiceContextValue = {
|
|||||||
changeIsProforma: (value: boolean) => void;
|
changeIsProforma: (value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InvoiceContext = createContext<InvoiceContextValue | null>(null);
|
const ProformaContext = createContext<ProformaContextValue | null>(null);
|
||||||
|
|
||||||
export interface InvoiceProviderParams {
|
export interface ProformaProviderParams {
|
||||||
taxCatalog: TaxCatalogProvider;
|
taxCatalog: TaxCatalogProvider;
|
||||||
invoice_id: string;
|
proforma_id: string;
|
||||||
company_id: string;
|
company_id: string;
|
||||||
status: string; // default "draft"
|
status: string; // default "draft"
|
||||||
language_code?: string; // default "es"
|
language_code?: string; // default "es"
|
||||||
@ -37,10 +45,17 @@ export interface InvoiceProviderParams {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, company_id, status: initialStatus = "draft", language_code: initialLang = "es",
|
export const ProformaProvider = ({
|
||||||
currency_code: initialCurrency = "EUR", readOnly: initialReadOnly = false,
|
taxCatalog: initialTaxCatalog,
|
||||||
is_proforma: initialProforma = true, children }: PropsWithChildren<InvoiceProviderParams>) => {
|
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();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Estado interno local para campos dinámicos
|
// 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 setIsProformaMemo = useCallback((is_proforma: boolean) => setIsProforma(is_proforma), []);
|
||||||
const setReadOnlyMemo = useCallback((readOnly: boolean) => setReadOnly(readOnly), []);
|
const setReadOnlyMemo = useCallback((readOnly: boolean) => setReadOnly(readOnly), []);
|
||||||
|
|
||||||
const value = useMemo<InvoiceContextValue>(() => {
|
const value = useMemo<ProformaContextValue>(() => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
t,
|
t,
|
||||||
|
|
||||||
invoice_id,
|
proforma_id,
|
||||||
company_id,
|
company_id,
|
||||||
status,
|
status,
|
||||||
language_code,
|
language_code,
|
||||||
@ -76,17 +90,30 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com
|
|||||||
changeCurrency: setCurrencyMemo,
|
changeCurrency: setCurrencyMemo,
|
||||||
changeIsProforma: setIsProformaMemo,
|
changeIsProforma: setIsProformaMemo,
|
||||||
setReadOnly: setReadOnlyMemo,
|
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 useProformaContext(): ProformaContextValue {
|
||||||
export function useInvoiceContext(): InvoiceContextValue {
|
const context = useContext(ProformaContext);
|
||||||
const context = useContext(InvoiceContext);
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useInvoiceContext must be used within <InvoiceProvider>");
|
throw new Error("useProformaContext must be used within <ProformaProvider>");
|
||||||
}
|
}
|
||||||
return context;
|
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 { type FieldErrors, FormProvider } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useInvoiceContext } from "../../context";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { useUpdateProforma } from "../../hooks";
|
import { ProformaDtoAdapter } from "../../adapters";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useUpdateProforma } from "../../hooks/use-proforma-update-mutation";
|
||||||
import {
|
import {
|
||||||
type InvoiceFormData,
|
|
||||||
InvoiceFormSchema,
|
|
||||||
type Proforma,
|
type Proforma,
|
||||||
defaultCustomerInvoiceFormData,
|
type ProformaFormData,
|
||||||
invoiceDtoToFormAdapter,
|
ProformaFormSchema,
|
||||||
} from "../../schemas";
|
defaultProformaFormData,
|
||||||
|
} from "../../schema";
|
||||||
|
|
||||||
import { InvoiceUpdateForm } from "./invoice-update-form";
|
import { useProformaContext } from "./context";
|
||||||
|
import { ProformaUpdateForm } from "./proforma-update-form";
|
||||||
|
|
||||||
export type InvoiceUpdateCompProps = {
|
export type ProformaUpdateCompProps = {
|
||||||
invoice: Proforma;
|
proforma: Proforma;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InvoiceUpdateComp = ({ invoice: invoiceData }: InvoiceUpdateCompProps) => {
|
export const ProformaUpdateComp = ({ proforma: proformaData }: ProformaUpdateCompProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
|
|
||||||
const context = useInvoiceContext();
|
const context = useProformaContext();
|
||||||
const { invoice_id } = context;
|
const { proforma_id } = context;
|
||||||
|
|
||||||
const isPending = !invoiceData;
|
const isPending = !proformaData;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutate,
|
mutate,
|
||||||
@ -41,23 +41,23 @@ export const InvoiceUpdateComp = ({ invoice: invoiceData }: InvoiceUpdateCompPro
|
|||||||
} = useUpdateProforma();
|
} = useUpdateProforma();
|
||||||
|
|
||||||
const initialValues = useMemo(() => {
|
const initialValues = useMemo(() => {
|
||||||
return invoiceData
|
return proformaData
|
||||||
? invoiceDtoToFormAdapter.fromDto(invoiceData, context)
|
? ProformaDtoAdapter.fromDto(proformaData, context)
|
||||||
: defaultCustomerInvoiceFormData;
|
: defaultProformaFormData;
|
||||||
}, [invoiceData, context]);
|
}, [proformaData, context]);
|
||||||
|
|
||||||
const form = useHookForm<InvoiceFormData>({
|
const form = useHookForm<ProformaFormData>({
|
||||||
resolverSchema: InvoiceFormSchema,
|
resolverSchema: ProformaFormSchema,
|
||||||
initialValues,
|
initialValues,
|
||||||
disabled: !invoiceData || isUpdating,
|
disabled: !proformaData || isUpdating,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (formData: InvoiceFormData) => {
|
const handleSubmit = (formData: ProformaFormData) => {
|
||||||
console.log("Guardo factura");
|
console.log("Guardo factura");
|
||||||
const dto = invoiceDtoToFormAdapter.toDto(formData, context);
|
const dto = ProformaDtoAdapter.toDto(formData, context);
|
||||||
console.log("dto => ", dto);
|
console.log("dto => ", dto);
|
||||||
mutate(
|
mutate(
|
||||||
{ id: invoice_id, data: dto as Partial<InvoiceFormData> },
|
{ id: proforma_id, data: dto as Partial<ProformaFormData> },
|
||||||
{
|
{
|
||||||
onSuccess: () =>
|
onSuccess: () =>
|
||||||
showSuccessToast(t("pages.update.success.title"), t("pages.update.success.message")),
|
showSuccessToast(t("pages.update.success.title"), t("pages.update.success.message")),
|
||||||
@ -67,13 +67,13 @@ export const InvoiceUpdateComp = ({ invoice: invoiceData }: InvoiceUpdateCompPro
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () =>
|
const handleReset = () =>
|
||||||
form.reset((invoiceData as unknown as InvoiceFormData) ?? defaultCustomerInvoiceFormData);
|
form.reset((proformaData as unknown as ProformaFormData) ?? defaultProformaFormData);
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = (errors: FieldErrors<InvoiceFormData>) => {
|
const handleError = (errors: FieldErrors<ProformaFormData>) => {
|
||||||
console.error("Errores en el formulario:", errors);
|
console.error("Errores en el formulario:", errors);
|
||||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||||
};
|
};
|
||||||
@ -85,26 +85,24 @@ export const InvoiceUpdateComp = ({ invoice: invoiceData }: InvoiceUpdateCompPro
|
|||||||
backIcon
|
backIcon
|
||||||
description={t("pages.edit.description")}
|
description={t("pages.edit.description")}
|
||||||
rightSlot={
|
rightSlot={
|
||||||
<>
|
<UpdateCommitButtonGroup
|
||||||
<UpdateCommitButtonGroup
|
cancel={{ formId, to: "/proformas/list" }}
|
||||||
cancel={{ formId, to: "/customer-invoices/list" }}
|
isLoading={isPending}
|
||||||
isLoading={isPending}
|
submit={{
|
||||||
submit={{
|
formId,
|
||||||
formId,
|
variant: "default",
|
||||||
variant: "default",
|
disabled: isPending,
|
||||||
disabled: isPending,
|
label: t("pages.proformas.edit.actions.save_draft"),
|
||||||
label: t("pages.edit.actions.save_draft"),
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
title={`${t("pages.edit.title")} #${invoiceData.invoice_number}`}
|
title={`${t("pages.proformas.edit.title")} #${proformaData.invoice_number}`}
|
||||||
/>
|
/>
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
|
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<InvoiceUpdateForm
|
<ProformaUpdateForm
|
||||||
className="bg-white rounded-xl border shadow-xl max-w-full"
|
className="bg-white rounded-xl border shadow-xl max-w-full"
|
||||||
formId={formId}
|
formId={formId}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
@ -2,28 +2,24 @@ import { FormDebug } from "@erp/core/components";
|
|||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { type FieldErrors, useFormContext } from "react-hook-form";
|
import { type FieldErrors, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import type { InvoiceFormData } from "../../schemas";
|
import type { ProformaFormData } from "../../schema";
|
||||||
import {
|
|
||||||
InvoiceBasicInfoFields,
|
|
||||||
InvoiceItems,
|
|
||||||
InvoiceRecipient,
|
|
||||||
InvoiceTotals,
|
|
||||||
} from "../../shared/ui/components";
|
|
||||||
|
|
||||||
interface InvoiceUpdateFormProps {
|
import { ProformaBasicInfoFields, ProformaItems, ProformaRecipient, ProformaTotals } from "./ui";
|
||||||
|
|
||||||
|
interface ProformaUpdateFormProps {
|
||||||
formId: string;
|
formId: string;
|
||||||
onSubmit: (data: InvoiceFormData) => void;
|
onSubmit: (data: ProformaFormData) => void;
|
||||||
onError: (errors: FieldErrors<InvoiceFormData>) => void;
|
onError: (errors: FieldErrors<ProformaFormData>) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InvoiceUpdateForm = ({
|
export const ProformaUpdateForm = ({
|
||||||
formId,
|
formId,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onError,
|
onError,
|
||||||
className,
|
className,
|
||||||
}: InvoiceUpdateFormProps) => {
|
}: ProformaUpdateFormProps) => {
|
||||||
const form = useFormContext<InvoiceFormData>();
|
const form = useFormContext<ProformaFormData>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
@ -38,17 +34,17 @@ export const InvoiceUpdateForm = ({
|
|||||||
|
|
||||||
<section className={cn("space-y-6 p-6", className)}>
|
<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">
|
<div className="w-full bg-transparent grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||||
<InvoiceRecipient className="flex flex-col" />
|
<ProformaRecipient className="flex flex-col" />
|
||||||
<InvoiceBasicInfoFields className="flex flex-col lg:col-span-2" />
|
<ProformaBasicInfoFields className="flex flex-col lg:col-span-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<InvoiceItems />
|
<ProformaItems />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full grid grid-cols-1 lg:grid-cols-2">
|
<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>
|
||||||
<div className="w-full"></div>
|
<div className="w-full" />
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</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 { FieldDescription, FieldGroup, FieldLegend, FieldSet } from "@repo/shadcn-ui/components";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../../../i18n";
|
||||||
|
import { ItemsEditor } from "../../components";
|
||||||
|
|
||||||
import { ItemsEditor } from "./items";
|
export const ProformaItems = (props: ComponentProps<"fieldset">) => {
|
||||||
|
|
||||||
export const InvoiceItems = (props: ComponentProps<"fieldset">) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -3,12 +3,12 @@ import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from "@repo/shadc
|
|||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../../i18n";
|
||||||
import type { InvoiceFormData } from "../../../../schemas";
|
import type { ProformaFormData } from "../../../../schema";
|
||||||
|
|
||||||
export const InvoiceBasicInfoFields = (props: ComponentProps<"fieldset">) => {
|
export const ProformaBasicInfoFields = (props: ComponentProps<"fieldset">) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control } = useFormContext<InvoiceFormData>();
|
const { control } = useFormContext<ProformaFormData>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldSet {...props}>
|
<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 { CustomerModalSelector } from "@erp/customers/components";
|
||||||
import { Field, FieldLabel } from "@repo/shadcn-ui/components";
|
import { Field, FieldLabel } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { CustomerSummary } from 'node_modules/@erp/customers/src/web/schemas';
|
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 RecipientModalSelectorFieldProps<TFormValues extends FieldValues> = {
|
||||||
|
|
||||||
type CustomerModalSelectorFieldProps<TFormValues extends FieldValues> = {
|
|
||||||
control: Control<TFormValues>;
|
control: Control<TFormValues>;
|
||||||
name: FieldPath<TFormValues>;
|
name: FieldPath<TFormValues>;
|
||||||
|
|
||||||
label?: string;
|
label?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
orientation?: "vertical" | "horizontal" | "responsive",
|
orientation?: "vertical" | "horizontal" | "responsive";
|
||||||
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@ -28,19 +27,17 @@ export function RecipientModalSelectorField<TFormValues extends FieldValues>({
|
|||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
|
|
||||||
orientation = 'vertical',
|
orientation = "vertical",
|
||||||
|
|
||||||
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
required = false,
|
required = false,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
className,
|
className,
|
||||||
initialRecipient = {},
|
initialRecipient = {},
|
||||||
}: CustomerModalSelectorFieldProps<TFormValues>) {
|
}: RecipientModalSelectorFieldProps<TFormValues>) {
|
||||||
const isDisabled = disabled;
|
const isDisabled = disabled;
|
||||||
const isReadOnly = readOnly && !disabled;
|
const isReadOnly = readOnly && !disabled;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@ -49,16 +46,24 @@ export function RecipientModalSelectorField<TFormValues extends FieldValues>({
|
|||||||
const { name, value, onChange, onBlur, ref } = field;
|
const { name, value, onChange, onBlur, ref } = field;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field data-invalid={fieldState.invalid} orientation={orientation} className={cn("gap-1", className)}>
|
<Field
|
||||||
{label && <FieldLabel className='text-xs text-muted-foreground text-nowrap' htmlFor={name}>{label}</FieldLabel>}
|
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
|
<CustomerModalSelector
|
||||||
value={value}
|
|
||||||
onValueChange={onChange}
|
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
readOnly={isReadOnly}
|
|
||||||
initialCustomer={{
|
initialCustomer={{
|
||||||
...initialRecipient as CustomerSummary
|
...(initialRecipient as CustomerSummary),
|
||||||
}}
|
}}
|
||||||
|
onValueChange={onChange}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
value={value}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
@ -2,11 +2,11 @@ import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from "@repo/shadc
|
|||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
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 { t } = useTranslation();
|
||||||
const { control, getValues } = useFormContext();
|
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 { Button, Input, Label, Textarea } from "@repo/shadcn-ui/components";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import type { InvoiceFormData, InvoiceItemFormData } from "../../../../../schemas";
|
import type { ProformaFormData, ProformaItemFormData } from "../../../../../schema";
|
||||||
|
|
||||||
export function ItemRowEditor({
|
export function ItemRowEditor({
|
||||||
row,
|
row,
|
||||||
index,
|
index,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
row: InvoiceItemFormData;
|
row: ProformaItemFormData;
|
||||||
index: number;
|
index: number;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
// Editor simple reutilizando el mismo RHF
|
// Editor simple reutilizando el mismo RHF
|
||||||
const { register } = useFormContext<InvoiceFormData>();
|
const { register } = useFormContext<ProformaFormData>();
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
<h3 className="text-base font-semibold">Edit line #{index + 1}</h3>
|
<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 { DataTable, useWithRowSelection } from "@repo/rdx-ui/components";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { useInvoiceContext } from "../../../../../context";
|
import { useProformaAutoRecalc } from "../../../../../../hooks";
|
||||||
import { useInvoiceAutoRecalc } from "../../../../../hooks";
|
import { useTranslation } from "../../../../../../i18n";
|
||||||
import { useTranslation } from "../../../../../i18n";
|
import { type ProformaFormData, defaultProformaItemFormData } from "../../../../../schema";
|
||||||
import { type InvoiceFormData, defaultCustomerInvoiceItemFormData } from "../../../../../schemas";
|
import { useProformaContext } from "../../../context";
|
||||||
|
|
||||||
import { ItemRowEditor } from "./item-row-editor";
|
import { ItemRowEditor } from "./item-row-editor";
|
||||||
import { useItemsColumns } from "./use-items-columns";
|
|
||||||
|
|
||||||
const createEmptyItem = () => defaultCustomerInvoiceItemFormData;
|
const createEmptyItem = () => defaultProformaItemFormData;
|
||||||
|
|
||||||
export const ItemsEditor = () => {
|
export const ItemsEditor = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const context = useInvoiceContext();
|
const context = useProformaContext();
|
||||||
const form = useFormContext<InvoiceFormData>();
|
const form = useFormContext<ProformaFormData>();
|
||||||
const { control } = form;
|
const { control } = form;
|
||||||
|
|
||||||
useInvoiceAutoRecalc(form, context);
|
useProformaAutoRecalc(form, context);
|
||||||
|
|
||||||
const { fields, append, remove, move, insert, update } = useFieldArray({
|
const { fields, append, remove, move, insert, update } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: "items",
|
name: "items",
|
||||||
});
|
});
|
||||||
|
|
||||||
const baseColumns = useWithRowSelection(useItemsColumns(), true);
|
const baseColumns = useWithRowSelection(useProformaItemsColumns(), true);
|
||||||
const columns = useMemo(() => baseColumns, [baseColumns]);
|
const columns = useMemo(() => baseColumns, [baseColumns]);
|
||||||
|
|
||||||
return (
|
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.api.schema";
|
||||||
|
export * from "./proforma.form.schema";
|
||||||
export * from "./proforma-summary.web.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 type { ComponentProps } from "react";
|
||||||
import { useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { useInvoiceContext } from "../../../../context";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useProformaContext } from "../../pages/update/context";
|
||||||
import type { InvoiceFormData } from "../../../../schemas";
|
import type { ProformaFormData } from "../../schema";
|
||||||
|
|
||||||
export const InvoiceTaxSummary = (props: ComponentProps<"fieldset">) => {
|
export const ProformaTaxSummary = (props: ComponentProps<"fieldset">) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control } = useFormContext<InvoiceFormData>();
|
const { control } = useFormContext<ProformaFormData>();
|
||||||
const { currency_code, language_code } = useInvoiceContext();
|
const { currency_code, language_code } = useProformaContext();
|
||||||
|
|
||||||
const taxes = useWatch({
|
const taxes = useWatch({
|
||||||
control,
|
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 "../adapters/invoice-resume-dto.adapter";
|
||||||
|
export * from "../proformas/adapters/proforma-dto.adapter";
|
||||||
|
|
||||||
export * from "./invoice.form.schema";
|
export * from "./invoice.form.schema";
|
||||||
export * from "./invoice-resume.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 "./items";
|
||||||
export * from "./invoice-items-editor";
|
|
||||||
export * from "./invoice-totals";
|
|
||||||
export * from "./recipient";
|
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import { ReceiptIcon } from "lucide-react";
|
|||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { useInvoiceContext } from "../../../../context";
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
import { useInvoiceContext } from "../../../../proformas/pages/update/context";
|
||||||
import type { InvoiceFormData } from "../../../../schemas";
|
import type { InvoiceFormData } from "../../../../schemas";
|
||||||
|
|
||||||
import { PercentageInputField } from "./items/percentage-input-field";
|
import { PercentageInputField } from "./items/percentage-input-field";
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useFormContext } from "react-hook-form";
|
|||||||
|
|
||||||
import { useTranslation } from "../../../../../i18n";
|
import { useTranslation } from "../../../../../i18n";
|
||||||
import type { InvoiceFormData } from "../../../../../schemas";
|
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";
|
import type { CustomItemViewProps } from "./types";
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ export const BlocksView = ({ items, removeItem, updateItem }: BlocksViewProps) =
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 col-start-1">
|
<div className="space-y-2 col-start-1">
|
||||||
<CustomerInvoiceTaxesMultiSelect
|
<ProformaTaxesMultiSelect
|
||||||
control={control}
|
control={control}
|
||||||
description={t("form_fields.item.tax_codes.description")}
|
description={t("form_fields.item.tax_codes.description")}
|
||||||
label={t("form_fields.item.tax_codes.label")}
|
label={t("form_fields.item.tax_codes.label")}
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import {
|
|||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
import { useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { useInvoiceContext } from "../../../../../context";
|
|
||||||
import { useTranslation } from "../../../../../i18n";
|
import { useTranslation } from "../../../../../i18n";
|
||||||
|
import { useInvoiceContext } from "../../../../../proformas/pages/update/context";
|
||||||
|
|
||||||
type HoverCardTotalsSummaryProps = PropsWithChildren & {
|
type HoverCardTotalsSummaryProps = PropsWithChildren & {
|
||||||
rowIndex: number;
|
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 { ArrowDownIcon, ArrowUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
|
||||||
import { type Control, Controller, type FieldValues } from "react-hook-form";
|
import { type Control, Controller, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
import { useInvoiceContext } from "../../../../../context";
|
|
||||||
import { useTranslation } from "../../../../../i18n";
|
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 { AmountInputField } from "./amount-input-field";
|
||||||
import { HoverCardTotalsSummary } from "./hover-card-total-summary";
|
import { HoverCardTotalsSummary } from "./hover-card-total-summary";
|
||||||
@ -154,7 +154,7 @@ export const ItemRow = <TFieldValues extends FieldValues = FieldValues>({
|
|||||||
data-cell-focus
|
data-cell-focus
|
||||||
name={`items.${rowIndex}.tax_codes`}
|
name={`items.${rowIndex}.tax_codes`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<CustomerInvoiceTaxesMultiSelect
|
<ProformaTaxesMultiSelect
|
||||||
data-col-index={7}
|
data-col-index={7}
|
||||||
data-row-index={rowIndex}
|
data-row-index={rowIndex}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
|
|||||||
@ -12,9 +12,9 @@ import {
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { useInvoiceContext } from "../../../../../context";
|
import { useItemsTableNavigation, useProformaAutoRecalc } from "../../../../../hooks";
|
||||||
import { useInvoiceAutoRecalc, useItemsTableNavigation } from "../../../../../hooks";
|
|
||||||
import { useTranslation } from "../../../../../i18n";
|
import { useTranslation } from "../../../../../i18n";
|
||||||
|
import { useInvoiceContext } from "../../../../../proformas/pages/update/context";
|
||||||
import {
|
import {
|
||||||
type InvoiceFormData,
|
type InvoiceFormData,
|
||||||
type InvoiceItemFormData,
|
type InvoiceItemFormData,
|
||||||
@ -51,7 +51,7 @@ export const ItemsEditor = ({ readOnly = false }: ItemsEditorProps) => {
|
|||||||
const { selectedRows, selectedIndexes, selectAllState, toggleRow, setSelectAll, clearSelection } =
|
const { selectedRows, selectedIndexes, selectAllState, toggleRow, setSelectAll, clearSelection } =
|
||||||
useRowSelection(fields.length);
|
useRowSelection(fields.length);
|
||||||
|
|
||||||
useInvoiceAutoRecalc(form, context);
|
useProformaAutoRecalc(form, context);
|
||||||
|
|
||||||
const handleAddSelection = useCallback(() => {
|
const handleAddSelection = useCallback(() => {
|
||||||
if (readOnly) return;
|
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-prices-card";
|
||||||
export * from "./customer-invoice-taxes-multi-select";
|
|
||||||
export * from "./editor";
|
export * from "./editor";
|
||||||
export * from "./editor/invoice-tax-summary";
|
|
||||||
export * from "./editor/invoice-totals";
|
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 { useTranslation } from "../../../../i18n";
|
||||||
import { formatCurrency } from "../../../../pages/create/utils";
|
import { formatCurrency } from "../../../../pages/create/utils";
|
||||||
import { CustomerInvoiceTaxesMultiSelect } from "../customer-invoice-taxes-multi-select";
|
import { ProformaTaxesMultiSelect } from "../proforma-taxes-multi-select";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CustomerInvoiceItemsSortableDataTable,
|
CustomerInvoiceItemsSortableDataTable,
|
||||||
@ -213,7 +213,7 @@ export const CustomerInvoiceItemsCardEditor = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CustomerInvoiceTaxesMultiSelect
|
<ProformaTaxesMultiSelect
|
||||||
{...field}
|
{...field}
|
||||||
//onChange={(e) => field.onChange(Number(e.target.value) * 100)}
|
//onChange={(e) => field.onChange(Number(e.target.value) * 100)}
|
||||||
//value={field.value / 100}
|
//value={field.value / 100}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useCallback, useMemo } from "react";
|
|||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
|
|
||||||
interface CustomerInvoiceTaxesMultiSelect {
|
interface ProformaTaxesMultiSelect {
|
||||||
value?: string[];
|
value?: string[];
|
||||||
onChange: (selectedValues: string[]) => void;
|
onChange: (selectedValues: string[]) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -13,7 +13,7 @@ interface CustomerInvoiceTaxesMultiSelect {
|
|||||||
[key: string]: any; // Allow other props to be passed
|
[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 { value, onChange, className, inputId, ...otherProps } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -28,11 +28,11 @@ export const CustomerInvoiceTaxesMultiSelect = (props: CustomerInvoiceTaxesMulti
|
|||||||
(selectedValues: string[]) => {
|
(selectedValues: string[]) => {
|
||||||
const groupMap = new Map<string | undefined, string>();
|
const groupMap = new Map<string | undefined, string>();
|
||||||
|
|
||||||
selectedValues.forEach((code) => {
|
for (const code of selectedValues) {
|
||||||
const item = taxCatalog.findByCode(code).getOrUndefined();
|
const item = taxCatalog.findByCode(code).getOrUndefined();
|
||||||
const group = item?.group ?? "ungrouped";
|
const group = item?.group ?? "ungrouped";
|
||||||
groupMap.set(group, code); // Sobrescribe el anterior del mismo grupo
|
groupMap.set(group, code); // Sobrescribe el anterior del mismo grupo
|
||||||
});
|
}
|
||||||
|
|
||||||
return Array.from(groupMap.values());
|
return Array.from(groupMap.values());
|
||||||
},
|
},
|
||||||
@ -17,7 +17,6 @@ import {
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { CustomerInvoiceTaxesMultiSelect } from "../../../../../customer-invoices/src/web/shared/ui/components";
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import type { CustomerFormData } from "../../schemas";
|
import type { CustomerFormData } from "../../schemas";
|
||||||
|
|
||||||
@ -123,7 +122,7 @@ export const CustomerBasicInfoFields = ({
|
|||||||
>
|
>
|
||||||
{t("form_fields.default_taxes.label")}
|
{t("form_fields.default_taxes.label")}
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<CustomerInvoiceTaxesMultiSelect
|
<ProformaTaxesMultiSelect
|
||||||
description={t("form_fields.default_taxes.description")}
|
description={t("form_fields.default_taxes.description")}
|
||||||
label={t("form_fields.default_taxes.label")}
|
label={t("form_fields.default_taxes.label")}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export interface DataTableProps<TData, TValue> {
|
|||||||
enablePagination?: boolean;
|
enablePagination?: boolean;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
enableRowSelection?: boolean;
|
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;
|
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
|
||||||
|
|
||||||
@ -299,7 +299,7 @@ export function DataTable<TData, TValue>({
|
|||||||
<EditorComponent
|
<EditorComponent
|
||||||
index={editIndex}
|
index={editIndex}
|
||||||
onClose={handleCloseEditor}
|
onClose={handleCloseEditor}
|
||||||
row={data[editIndex]}
|
row={data[editIndex]!}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user