This commit is contained in:
David Arranz 2026-04-05 22:36:41 +02:00
parent cd0940cb20
commit dbd8f2b3f4
10 changed files with 0 additions and 634 deletions

View File

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

View File

@ -1,98 +0,0 @@
import {
MoneyDTOHelper,
PercentageDTOHelper,
QuantityDTOHelper,
type TaxCatalogProvider,
} from "@erp/core";
import type { Proforma, ProformaFormData, UpdateProformaInput } from "../types";
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 ProformaDtoAdapter = {
fromDto(dto: Proforma, context: ProformaDtoAdapterContext): ProformaFormData {
const { taxCatalog } = context;
return {
invoice_number: dto.invoice_number,
series: dto.series,
invoice_date: dto.invoice_date,
operation_date: dto.operation_date,
customer_id: dto.customer_id,
recipient: dto.recipient,
reference: dto.reference ?? "",
description: dto.description ?? "",
notes: dto.notes ?? "",
language_code: dto.language_code,
currency_code: dto.currency_code,
subtotal_amount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
items_discount_amount: 0,
discount_percentage: PercentageDTOHelper.toNumber(dto.discount_percentage),
discount_amount: MoneyDTOHelper.toNumber(dto.discount_amount),
taxable_amount: MoneyDTOHelper.toNumber(dto.taxable_amount),
taxes_amount: MoneyDTOHelper.toNumber(dto.taxes_amount),
total_amount: MoneyDTOHelper.toNumber(dto.total_amount),
taxes: dto.taxes.map((taxItem) => ({
tax_code: taxItem.tax_code,
tax_label: taxCatalog.findByCode(taxItem.tax_code).match(
(tax) => tax.name,
() => ""
),
taxable_amount: MoneyDTOHelper.toNumber(taxItem.taxable_amount),
taxes_amount: MoneyDTOHelper.toNumber(taxItem.taxes_amount),
})),
items: dto.items.map((item) => ({
description: item.description ?? "",
quantity: QuantityDTOHelper.toNumericString(item.quantity),
unit_amount: MoneyDTOHelper.toNumericString(item.unit_amount),
subtotal_amount: MoneyDTOHelper.toNumber(item.subtotal_amount),
discount_percentage: PercentageDTOHelper.toNumericString(item.discount_percentage),
discount_amount: MoneyDTOHelper.toNumber(item.discount_amount),
taxable_amount: MoneyDTOHelper.toNumber(item.taxable_amount),
tax_codes: item.tax_codes ?? [],
taxes_amount: MoneyDTOHelper.toNumber(item.taxes_amount),
total_amount: MoneyDTOHelper.toNumber(item.total_amount),
})),
};
},
toDto(form: ProformaFormData, context: ProformaDtoAdapterContext): UpdateProformaInput {
const { currency_code, language_code } = context;
return {
series: form.series,
invoice_date: form.invoice_date,
operation_date: form.operation_date,
customer_id: form.customer_id,
reference: form.reference,
description: form.description,
notes: form.notes,
language_code,
currency_code,
items: form.items?.map((item) => ({
description: item.description,
quantity: QuantityDTOHelper.fromNumericString(item.quantity, 4),
unit_amount: MoneyDTOHelper.fromNumericString(item.unit_amount, currency_code, 4),
discount_percentage: PercentageDTOHelper.fromNumericString(item.discount_percentage, 2),
tax_codes: item.tax_codes,
})),
};
},
};

View File

@ -1,71 +0,0 @@
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
import type { ProformaSummaryPage } from "../types/proforma.api.schema";
import type {
ProformaSummaryData,
ProformaSummaryPageData,
} from "../types/proforma-summary.web.schema";
/**
* Convierte el DTO completo de API a datos numéricos para el formulario.
*/
export const ProformaSummaryDtoAdapter = {
fromDto(pageDto: ProformaSummaryPage, context?: unknown): ProformaSummaryPageData {
return {
...pageDto,
items: pageDto.items.map(
(summaryDto) =>
({
...summaryDto,
subtotal_amount: MoneyDTOHelper.toNumber(summaryDto.subtotal_amount),
subtotal_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(summaryDto.subtotal_amount),
Number(summaryDto.total_amount.scale || 2),
summaryDto.currency_code,
summaryDto.language_code
),
discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage),
discount_percentage_fmt: PercentageDTOHelper.toNumericString(
summaryDto.discount_percentage
),
discount_amount: MoneyDTOHelper.toNumber(summaryDto.discount_amount),
discount_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(summaryDto.discount_amount),
Number(summaryDto.total_amount.scale || 2),
summaryDto.currency_code,
summaryDto.language_code
),
taxable_amount: MoneyDTOHelper.toNumber(summaryDto.taxable_amount),
taxable_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(summaryDto.taxable_amount),
Number(summaryDto.total_amount.scale || 2),
summaryDto.currency_code,
summaryDto.language_code
),
taxes_amount: MoneyDTOHelper.toNumber(summaryDto.taxes_amount),
taxes_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(summaryDto.taxes_amount),
Number(summaryDto.total_amount.scale || 2),
summaryDto.currency_code,
summaryDto.language_code
),
total_amount: MoneyDTOHelper.toNumber(summaryDto.total_amount),
total_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(summaryDto.total_amount),
Number(summaryDto.total_amount.scale || 2),
summaryDto.currency_code,
summaryDto.language_code
),
//taxes: dto.taxes,
}) as unknown as ProformaSummaryData
),
};
},
};

View File

@ -1,2 +0,0 @@
export * from "./use-issue-proforma-invoice";
export * from "./use-proformas-query";

View File

@ -1,70 +0,0 @@
import {
useDataSource
} from ("@erp/core/hooks");
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { InvoiceFormData } from "../schemas";
export const CUSTOMER_INVOICES_LIST_KEY = ["customer-invoices"] as const;
type UpdateCustomerInvoiceContext = {};
type UpdateCustomerInvoicePayload = {
id: string;
data: Partial<InvoiceFormData>;
};
export function useDeleteProforma() {
const queryClient = useQueryClient();
const dataSource = useDataSource();
return useMutation<{ id: string }, Error>({
mutationKey: ["customer-invoice:delete", id],
mutationFn: async (payload) => {
const { id: proformaId } = payload;
if (!proformaId) {
throw new Error("proformaId is required");
}
await dataSource.deleteOne("proformas", proformaId);
},
onMutate: async ({ id }) => {
await queryClient.cancelQueries({ queryKey: [CUSTOMERS_LIST_SCOPE] });
const snapshots = getAllCustomerListQueryKeys(queryClient).map((key) => ({
key,
page: queryClient.getQueryData<CustomersPage>(key),
}));
for (const { key, page } of snapshots) {
if (!page) continue;
queryClient.setQueryData<CustomersPage>(key, {
...page,
items: page.items.filter((c) => c.id !== id),
totalItems: Math.max(0, page.totalItems - 1),
});
}
return { snapshots };
},
onError: (_e, _v, ctx) => {
if (!ctx) return;
for (const snap of ctx.snapshots as Array<{ key: QueryKey; page?: CustomersPage }>) {
if (snap.page) queryClient.setQueryData(snap.key, snap.page);
}
},
onSuccess: ({ id }) => {
// Limpia cache de detalle
queryClient.removeQueries({ queryKey: buildCustomerQueryKey(id) });
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: [CUSTOMERS_LIST_SCOPE] });
},
});
}

View File

@ -1,26 +0,0 @@
// hooks/use-issue-proforma-invoice.ts
import { useDataSource } from "@erp/core/hooks";
import { useMutation, useQueryClient } from "@tanstack/react-query";
export const ISSUE_PROFORMA_INVOICE_KEY = ["proformas", "issue"] as const;
interface IssueProformaInvoicePayload {
proformaId: string;
}
export function useIssueProformaInvoice() {
const dataSource = useDataSource();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ISSUE_PROFORMA_INVOICE_KEY,
mutationFn: ({ proformaId }: IssueProformaInvoicePayload) =>
issueProformaInvoiceApi(dataSource, proformaId),
onSuccess() {
queryClient.invalidateQueries({ queryKey: ["proformas"] });
queryClient.invalidateQueries({ queryKey: ["invoices"] });
},
});
}

View File

@ -1,323 +0,0 @@
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
import { InputGroup, InputGroupTextarea } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import type { ColumnDef } from "@tanstack/react-table";
import * as React from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useProformaContext } from "../pages/update/context";
import { AmountInputField } from "../ui/components/amount-input-field";
import { PercentageInputField } from "../ui/components/percentage-input-field";
import { QuantityInputField } from "../ui/components/quantity-input-field";
export interface ProformaItemFormData {
id: string; // ← mapea RHF field.id aquí
description: string;
quantity: number | "";
unit_amount: number | "";
discount_percentage: number | "";
discount_amount: number | "";
taxable_amount: number | "";
tax_codes: string[];
taxes_amount: number | "";
total_amount: number | ""; // readonly calculado
}
export interface ProformaFormData {
items: ProformaItemFormData[];
}
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<ProformaItemFormData>[]>(
() => [
{
id: "position",
header: ({ column }) => (
<DataTableColumnHeader className="text-center" column={column} title={"#"} />
),
cell: ({ row }) => row.index + 1,
enableSorting: false,
size: 32,
},
{
accessorKey: "description",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left"
column={column}
title={t("form_fields.item.description.label")}
/>
),
cell: ({ row }) => (
<Controller
control={control}
name={`items.${row.index}.description`}
render={({ field }) => (
<InputGroup>
<InputGroupTextarea
{...field}
aria-label={t("form_fields.item.description.label")} // ← estable
className={cn(
"min-w-48 max-w-184 w-full resize-none bg-transparent border-dashed transition",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-background focus-visible:border-solid",
"focus:resize-y"
)}
data-cell-focus
id={`desc-${row.original.id}`}
onInput={(e) => {
const el = e.currentTarget;
el.style.height = "auto";
el.style.height = `${el.scrollHeight}px`;
}}
// auto-grow simple
readOnly={readOnly}
rows={1}
spellCheck
/>
{/*<InputGroupAddon align="block-end">
<InputGroupText>Line 1, Column 1</InputGroupText>
<InputGroupButton
variant="default"
className="rounded-full"
size="icon-xs"
disabled
>
<ArrowUpIcon />
<span className="sr-only">Send</span>
</InputGroupButton>
</InputGroupAddon>*/}
</InputGroup>
)}
/>
),
enableSorting: false,
size: 480,
minSize: 240,
maxSize: 768,
},
{
accessorKey: "quantity",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right"
column={column}
title={t("form_fields.item.quantity.label")}
/>
),
cell: ({ row }) => (
<QuantityInputField
className="font-base"
control={control}
data-cell-focus
data-col-index={4}
data-row-index={row.index}
emptyMode="blank"
inputId={`qty-${row.original.id}`}
name={`items.${row.index}.quantity`}
readOnly={readOnly}
/>
),
enableSorting: false,
size: 52,
minSize: 48,
maxSize: 64,
},
{
accessorKey: "unit_amount",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right"
column={column}
title={t("form_fields.item.unit_amount.label")}
/>
),
cell: ({ row }) => (
<AmountInputField
className="font-base"
control={control}
currencyCode={currency_code}
data-cell-focus
data-col-index={5}
data-row-index={row.index}
inputId={`unit-${row.original.id}`}
languageCode={language_code}
name={`items.${row.index}.unit_amount`}
readOnly={readOnly}
scale={4}
/>
),
enableSorting: false,
size: 120,
minSize: 100,
maxSize: 160,
},
{
accessorKey: "discount_percentage",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right"
column={column}
title={t("form_fields.item.discount_percentage.label")}
/>
),
cell: ({ row }) => (
<PercentageInputField
className="font-base"
control={control}
data-cell-focus
data-col-index={6}
data-row-index={row.index}
inputId={`disc-${row.original.id}`}
name={`items.${row.index}.discount_percentage`}
readOnly={readOnly}
scale={4}
/>
),
enableSorting: false,
size: 40,
minSize: 40,
},
{
accessorKey: "discount_amount",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right"
column={column}
title={t("form_fields.item.discount_amount.label")}
/>
),
cell: ({ row }) => (
<AmountInputField
control={control}
currencyCode={currency_code}
inputId={`discount_amount-${row.original.id}`}
languageCode={language_code}
name={`items.${row.index}.discount_amount`}
readOnly
/>
),
enableHiding: true,
enableSorting: false,
size: 120,
minSize: 100,
maxSize: 160,
},
{
accessorKey: "taxable_amount",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right"
column={column}
title={t("form_fields.item.taxable_amount.label")}
/>
),
cell: ({ row }) => (
<AmountInputField
control={control}
currencyCode={currency_code}
inputId={`taxable_amount-${row.original.id}`}
languageCode={language_code}
name={`items.${row.index}.taxable_amount`}
readOnly
/>
),
enableHiding: true,
enableSorting: false,
size: 120,
minSize: 100,
maxSize: 160,
},
{
accessorKey: "tax_codes",
header: ({ column }) => (
<DataTableColumnHeader column={column} title={t("form_fields.item.tax_codes.label")} />
),
cell: ({ row }) => (
<Controller
control={control}
name={`items.${row.index}.tax_codes`}
render={({ field }) => (
<ProformaTaxesMultiSelect
{...field}
data-cell-focus
data-col-index={7}
data-row-index={row.index}
inputId={`tax-${row.original.id}`}
/>
)}
/>
),
enableSorting: false,
size: 120,
minSize: 130,
maxSize: 180,
},
{
accessorKey: "taxes_amount",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right"
column={column}
title={t("form_fields.item.taxes_amount.label")}
/>
),
cell: ({ row }) => (
<AmountInputField
control={control}
currencyCode={currency_code}
inputId={`taxes_amount-${row.original.id}`}
languageCode={language_code}
name={`items.${row.index}.taxes_amount`}
readOnly
/>
),
enableSorting: false,
size: 120,
minSize: 100,
maxSize: 160,
},
{
accessorKey: "total_amount",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right"
column={column}
title={t("form_fields.item.total_amount.label")}
/>
),
cell: ({ row }) => (
<HoverCardTotalsSummary rowIndex={row.index}>
<AmountInputField
className="font-semibold"
control={control}
currencyCode={currency_code}
inputId={`total-${row.original.id}`}
languageCode={language_code}
name={`items.${row.index}.total_amount`}
readOnly
/>
</HoverCardTotalsSummary>
),
enableSorting: false,
size: 120,
minSize: 100,
maxSize: 160,
},
{
id: "actions",
header: ({ column }) => (
<DataTableColumnHeader column={column} title={t("components.datatable.actions")} />
),
cell: ({ row, table }) => <ItemDataTableRowActions row={row} table={table} />,
enableSorting: false,
size: 100,
minSize: 100,
maxSize: 100,
},
],
[t, readOnly, control, currency_code, language_code]
);
}

View File

@ -1,42 +0,0 @@
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 "../types/proforma.api.schema";
export const PROFORMAS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
"proforma",
{
pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX,
pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE,
q: criteria?.q ?? "",
filters: criteria?.filters ?? [],
orderBy: criteria?.orderBy ?? "",
order: criteria?.order ?? "",
},
];
type ProformasQueryOptions = {
enabled?: boolean;
criteria?: CriteriaDTO;
};
// Obtener todas las facturas
export const useProformasQuery = (options?: ProformasQueryOptions) => {
const dataSource = useDataSource();
const enabled = options?.enabled ?? true;
const criteria = options?.criteria ?? {};
return useQuery<ProformaSummaryPage, DefaultError>({
queryKey: PROFORMAS_QUERY_KEY(criteria),
queryFn: async ({ signal }) => {
return await dataSource.getList<ProformaSummaryPage>("proformas", {
signal,
...criteria,
});
},
enabled,
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
});
};