Facturas de cliente

This commit is contained in:
David Arranz 2025-11-12 18:22:05 +01:00
parent 60dacc4c32
commit 1c66d20c24
26 changed files with 164 additions and 162 deletions

View File

@ -1,6 +1,7 @@
import { MoneyDTO, MoneyDTOHelper } from "@erp/core/common";
import { type MoneyDTO, MoneyDTOHelper } from "@erp/core/common";
import type { Currency } from "dinero.js";
import * as React from "react";
import { useTranslation } from "../i18n";
export type { Currency };
@ -94,7 +95,7 @@ export function useMoney(overrides?: {
// Formateos
const formatCurrency = React.useCallback(
(dto: MoneyDTO, loc?: string) => MoneyDTOHelper.formatDTO(dto, loc ?? locale),
(dto: MoneyDTO, loc?: string) => MoneyDTOHelper.format(dto, loc ?? locale),
[locale]
);

View File

@ -1,19 +1,21 @@
import {
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableVO,
ValidationErrorCollection,
ValidationErrorDetail,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CreateCustomerInvoiceRequestDTO } from "../../../common";
import type { CreateCustomerInvoiceRequestDTO } from "../../../common";
import {
CustomerInvoiceItem,
CustomerInvoiceItemDescription,
CustomerInvoiceItemProps,
type CustomerInvoiceItemProps,
ItemAmount,
ItemDiscount,
ItemQuantity,
} from "../../domain";
import { hasNoUndefinedFields } from "./has-no-undefined-fields";
export function mapDTOToCustomerInvoiceItemsProps(

View File

@ -3,18 +3,20 @@ import {
UniqueID,
UtcDate,
ValidationErrorCollection,
ValidationErrorDetail,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableVO,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CreateCustomerInvoiceRequestDTO } from "../../../common";
import type { CreateCustomerInvoiceRequestDTO } from "../../../common";
import {
CustomerInvoiceNumber,
CustomerInvoiceProps,
type CustomerInvoiceProps,
CustomerInvoiceSerie,
CustomerInvoiceStatus,
} from "../../domain";
import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice-items-props";
/**

View File

@ -1,7 +0,0 @@
import { UtcDate } from "@repo/rdx-ddd";
export function formatDateDTO(dateString: string) {
const result = UtcDate.createFromISO(dateString).data;
return result.toEuropeanString();
}

View File

@ -1,25 +0,0 @@
import { MoneyDTO } from "@erp/core";
import { MoneyValue } from "@repo/rdx-ddd";
export type FormatMoneyOptions = {
locale: string;
hideZeros?: boolean;
newScale?: number;
};
export function formatMoneyDTO(
amount: MoneyDTO,
{ locale, hideZeros = false, newScale = 2 }: FormatMoneyOptions
) {
if (hideZeros && (amount.value === "0" || amount.value === "")) {
return null;
}
const money = MoneyValue.create({
value: Number(amount.value),
currency_code: amount.currency_code,
scale: Number(amount.scale),
}).data;
return money.convertScale(newScale).format(locale);
}

View File

@ -1,11 +0,0 @@
import type { GetIssueInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
export function formatPaymentMethodDTO(
paymentMethod?: GetIssueInvoiceByIdResponseDTO["payment_method"]
) {
if (!paymentMethod) {
return null;
}
return paymentMethod.payment_description ?? "";
}

View File

@ -1,15 +0,0 @@
import { PercentageDTO } from "@erp/core";
import { Percentage } from "@repo/rdx-ddd";
export function formatPercentageDTO(Percentage_value: PercentageDTO, locale: string) {
if (Percentage_value.value === "0" || Percentage_value.value === "") {
return null;
}
const value = Percentage.create({
value: Number(Percentage_value.value),
scale: Number(Percentage_value.scale),
}).data;
return value.format(locale);
}

View File

@ -1,15 +0,0 @@
import { QuantityDTO } from "@erp/core";
import { Quantity } from "@repo/rdx-ddd";
export function formatQuantityDTO(quantity_value: QuantityDTO) {
if (quantity_value.value === "0" || quantity_value.value === "") {
return null;
}
const value = Quantity.create({
value: Number(quantity_value.value),
scale: Number(quantity_value.scale),
}).data;
return value.format();
}

View File

@ -1,6 +0,0 @@
export * from "./format-date-dto";
export * from "./format-money-dto";
export * from "./format-payment_method-dto";
export * from "./format-percentage-dto";
export * from "./format-quantity-dto";
//export * from "./map-dto-to-customer-invoice-props";

View File

@ -1,9 +1,8 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import { type IPresenterOutputParams, Presenter } from "@erp/core/api";
import type { GetIssueInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils";
import { type FormatMoneyOptions, formatMoneyDTO, formatQuantityDTO } from "../../helpers";
type CustomerInvoiceItemsDTO = GetIssueInvoiceByIdResponseDTO["items"];
type CustomerInvoiceItemDTO = ArrayElement<CustomerInvoiceItemsDTO>;
@ -13,24 +12,39 @@ export class CustomerInvoiceItemsReportPersenter extends Presenter<
> {
private _locale!: string;
private _mapItem(invoiceItem: CustomerInvoiceItemDTO, index: number) {
const moneyOptions: FormatMoneyOptions = {
locale: this._locale,
private _mapItem(invoiceItem: CustomerInvoiceItemDTO, _index: number) {
const moneyOptions = {
hideZeros: true,
newScale: 2,
minimumFractionDigits: 0,
};
return {
...invoiceItem,
quantity: formatQuantityDTO(invoiceItem.quantity),
unit_amount: formatMoneyDTO(invoiceItem.unit_amount, moneyOptions),
subtotal_amount: formatMoneyDTO(invoiceItem.subtotal_amount, moneyOptions),
// discount_percetage: formatPercentageDTO(invoiceItem.discount_percentage, this._locale),
discount_amount: formatMoneyDTO(invoiceItem.discount_amount, moneyOptions),
taxable_amount: formatMoneyDTO(invoiceItem.taxable_amount, moneyOptions),
taxes_amount: formatMoneyDTO(invoiceItem.taxes_amount, moneyOptions),
total_amount: formatMoneyDTO(invoiceItem.total_amount, moneyOptions),
quantity: QuantityDTOHelper.format(invoiceItem.quantity, this._locale, {
minimumFractionDigits: 0,
}),
unit_amount: MoneyDTOHelper.format(invoiceItem.unit_amount, this._locale, moneyOptions),
subtotal_amount: MoneyDTOHelper.format(
invoiceItem.subtotal_amount,
this._locale,
moneyOptions
),
discount_percentage: PercentageDTOHelper.format(
invoiceItem.discount_percentage,
this._locale,
{
minimumFractionDigits: 0,
}
),
discount_amount: MoneyDTOHelper.format(
invoiceItem.discount_amount,
this._locale,
moneyOptions
),
taxable_amount: MoneyDTOHelper.format(invoiceItem.taxable_amount, this._locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(invoiceItem.taxes_amount, this._locale, moneyOptions),
total_amount: MoneyDTOHelper.format(invoiceItem.total_amount, this._locale, moneyOptions),
};
}

View File

@ -1,10 +1,8 @@
import { type JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core";
import { type JsonTaxCatalogProvider, MoneyDTOHelper, SpainTaxCatalogProvider } from "@erp/core";
import { type IPresenterOutputParams, Presenter } from "@erp/core/api";
import type { GetIssueInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils";
import { type FormatMoneyOptions, formatMoneyDTO } from "../../helpers";
type CustomerInvoiceTaxesDTO = GetIssueInvoiceByIdResponseDTO["taxes"];
type CustomerInvoiceTaxDTO = ArrayElement<CustomerInvoiceTaxesDTO>;
@ -16,10 +14,9 @@ export class CustomerInvoiceTaxesReportPresenter extends Presenter<
private _taxCatalog!: JsonTaxCatalogProvider;
private _mapTax(taxItem: CustomerInvoiceTaxDTO) {
const moneyOptions: FormatMoneyOptions = {
locale: this._locale,
const moneyOptions = {
hideZeros: true,
newScale: 2,
minimumFractionDigits: 0,
};
const taxCatalogItem = this._taxCatalog.findByCode(taxItem.tax_code);
@ -27,8 +24,8 @@ export class CustomerInvoiceTaxesReportPresenter extends Presenter<
return {
tax_code: taxItem.tax_code,
tax_name: taxCatalogItem.unwrap().name,
taxable_amount: formatMoneyDTO(taxItem.taxable_amount, moneyOptions),
taxes_amount: formatMoneyDTO(taxItem.taxes_amount, moneyOptions),
taxable_amount: MoneyDTOHelper.format(taxItem.taxable_amount, this._locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(taxItem.taxes_amount, this._locale, moneyOptions),
};
}

View File

@ -1,18 +1,22 @@
import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
import { Presenter } from "@erp/core/api";
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto";
import {
type FormatMoneyOptions,
formatDateDTO,
formatMoneyDTO,
formatPercentageDTO,
} from "../../helpers";
import { formatPaymentMethodDTO } from "../../helpers/format-payment_method-dto";
export class CustomerInvoiceReportPresenter extends Presenter<
GetIssueInvoiceByIdResponseDTO,
unknown
> {
private _formatPaymentMethodDTO(
paymentMethod?: GetIssueInvoiceByIdResponseDTO["payment_method"]
) {
if (!paymentMethod) {
return "";
}
return paymentMethod.payment_description ?? "";
}
toOutput(invoiceDTO: GetIssueInvoiceByIdResponseDTO) {
const itemsPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice-items",
@ -35,10 +39,9 @@ export class CustomerInvoiceReportPresenter extends Presenter<
locale,
});
const moneyOptions: FormatMoneyOptions = {
locale,
const moneyOptions = {
hideZeros: true,
newScale: 2,
minimumFractionDigits: 0,
};
return {
@ -46,15 +49,15 @@ export class CustomerInvoiceReportPresenter extends Presenter<
taxes: taxesDTO,
items: itemsDTO,
invoice_date: formatDateDTO(invoiceDTO.invoice_date),
subtotal_amount: formatMoneyDTO(invoiceDTO.subtotal_amount, moneyOptions),
discount_percentage: formatPercentageDTO(invoiceDTO.discount_percentage, locale),
discount_amount: formatMoneyDTO(invoiceDTO.discount_amount, moneyOptions),
taxable_amount: formatMoneyDTO(invoiceDTO.taxable_amount, moneyOptions),
taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, moneyOptions),
total_amount: formatMoneyDTO(invoiceDTO.total_amount, moneyOptions),
invoice_date: DateHelper.format(invoiceDTO.invoice_date, locale),
subtotal_amount: MoneyDTOHelper.format(invoiceDTO.subtotal_amount, locale, moneyOptions),
discount_percentage: PercentageDTOHelper.format(invoiceDTO.discount_percentage, locale),
discount_amount: MoneyDTOHelper.format(invoiceDTO.discount_amount, locale, moneyOptions),
taxable_amount: MoneyDTOHelper.format(invoiceDTO.taxable_amount, locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(invoiceDTO.taxes_amount, locale, moneyOptions),
total_amount: MoneyDTOHelper.format(invoiceDTO.total_amount, locale, moneyOptions),
payment_method: formatPaymentMethodDTO(invoiceDTO.payment_method),
payment_method: this._formatPaymentMethodDTO(invoiceDTO.payment_method),
};
}
}

View File

@ -55,8 +55,9 @@
table th,
table td {
border-top: 1px solid;
border-left: 1px solid;
border-top: 0px solid;
border-left: 1px solid #000;
border-right: 1px solid #000;
border-bottom: 0px solid;
padding: 3px 10px;
text-align: left;
@ -65,6 +66,11 @@
table th {
margin-bottom: 10px;
border-top: 1px solid #000;
border-bottom: 1px solid #000;
text-align: center;
background-color: #e7e0df;
color: #ff0014;
}
.totals {
@ -157,12 +163,12 @@
<!-- Tu tabla -->
<table class="table-header">
<thead>
<tr class="text-left bg-gray-200 text-red-500">
<tr>
<th class="py-2">Concepto</th>
<th class="py-2">Cantidad</th>
<th class="py-2">Precio&nbsp;unidad</th>
<th class="py-2">Dto</th>
<th class="py-2">Importe&nbsp;total</th>
<th class="py-2">Ud.</th>
<th class="py-2">Imp.</th>
<th class="py-2">&nbsp;</th>
<th class="py-2">Imp.&nbsp;total</th>
</tr>
</thead>
@ -172,6 +178,7 @@
<td>{{description}}</td>
<td class="text-right">{{#if quantity}}{{quantity}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if discount_percentage}}{{discount_percentage}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if taxable_amount}}{{taxable_amount}}{{else}}&nbsp;{{/if}}</td>
</td>
</tr>

View File

@ -7,7 +7,9 @@ const InvoicesLayout = lazy(() =>
import("./components").then((m) => ({ default: m.InvoicesLayout }))
);
const InvoiceListPage = lazy(() => import("./pages").then((m) => ({ default: m.InvoiceListPage })));
const ProformaListPage = lazy(() =>
import("./pages").then((m) => ({ default: m.InvoiceListPage }))
);
const CustomerInvoiceAdd = lazy(() =>
import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate }))
@ -26,8 +28,8 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
</InvoicesLayout>
),
children: [
{ path: "", index: true, element: <ProformasListPage /> }, // index
{ path: "list", element: <ProformasListPage /> },
{ path: "", index: true, element: <ProformaListPage /> }, // index
{ path: "list", element: <ProformaListPage /> },
{ path: "create", element: <CustomerInvoiceAdd /> },
{ path: ":id/edit", element: <InvoiceUpdatePage /> },
],
@ -40,9 +42,8 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
</InvoicesLayout>
),
children: [
{ path: "", index: true, element: <InvoiceListPage /> }, // index
{ path: "list", element: <InvoiceListPage /> },
//{ path: "", index: true, element: <InvoiceListPage /> }, // index
//{ path: "list", element: <InvoiceListPage /> },
//
/*{ path: "create", element: <CustomerInvoicesList /> },
{ path: ":id", element: <CustomerInvoicesList /> },

View File

@ -1,3 +1,5 @@
export * from "../proformas";
export * from "./create";
export * from "./list";
export * from "./update";

View File

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

View File

@ -0,0 +1,48 @@
import { useDataSource } from "@erp/core/hooks";
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
export const CUSTOMER_INVOICE_QUERY_KEY = (id: string): QueryKey =>
["customer_invoice", id] as const;
type InvoiceQueryOptions = {
enabled?: boolean;
};
export const useInvoiceQuery = (invoiceId?: string, options?: InvoiceQueryOptions) => {
const dataSource = useDataSource();
const enabled = (options?.enabled ?? true) && !!invoiceId;
return useQuery<CustomerInvoice, DefaultError>({
queryKey: CUSTOMER_INVOICE_QUERY_KEY(invoiceId ?? "unknown"),
queryFn: async (context) => {
const { signal } = context;
if (!invoiceId) {
if (!invoiceId) throw new Error("invoiceId is required");
}
return await dataSource.getOne<CustomerInvoice>("customer-invoices", invoiceId, {
signal,
});
},
enabled,
});
};
/*
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
>
TQueryFnData: the type returned from the queryFn.
TError: the type of Errors to expect from the queryFn.
TData: the type our data property will eventually have.
Only relevant if you use the select option,
because then the data property can be different
from what the queryFn returns.
Otherwise, it will default to whatever the queryFn returns.
TQueryKey: the type of our queryKey, only relevant
if you use the queryKey that is passed to your queryFn.
*/

View File

@ -0,0 +1,2 @@
export * from "./hooks";
export * from "./pages";

View File

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

View File

@ -5,10 +5,10 @@ import { Button } from "@repo/shadcn-ui/components";
import { PlusIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useInvoicesQuery } from "../../hooks";
import { useTranslation } from "../../i18n";
import { invoiceResumeDtoToFormAdapter } from "../../schemas/invoice-resume-dto.adapter";
import { InvoicesListGrid } from "./invoices-list-grid";
import { useInvoicesQuery } from "../../../hooks";
import { useTranslation } from "../../../i18n";
import { invoiceResumeDtoToFormAdapter } from "../../../schemas";
export const ProformaListPage = () => {
const { t } = useTranslation();
@ -61,8 +61,8 @@ export const ProformaListPage = () => {
return (
<AppContent>
<ErrorAlert
title={t("pages.list.loadErrorTitle")}
message={(error as Error)?.message || "Error al cargar el listado"}
title={t("pages.list.loadErrorTitle")}
/>
<BackHistoryButton />
</AppContent>
@ -73,35 +73,35 @@ export const ProformaListPage = () => {
<>
<AppHeader>
<PageHeader
title={t("pages.list.title")}
description={t("pages.list.description")}
rightSlot={
<div className='flex items-center space-x-2'>
<div className="flex items-center space-x-2">
<Button
aria-label={t("pages.create.title")}
className="cursor-pointer"
onClick={() => navigate("/customer-invoices/create")}
variant={"default"}
aria-label={t("pages.create.title")}
className='cursor-pointer'
>
<PlusIcon className='mr-2 h-4 w-4' aria-hidden />
<PlusIcon aria-hidden className="mr-2 h-4 w-4" />
{t("pages.create.title")}
</Button>
</div>
}
title={t("pages.list.title")}
/>
</AppHeader>
<AppContent>
<div className='flex flex-col w-full h-full py-3'>
<div className="flex flex-col w-full h-full py-3">
<div className={"flex-1"}>
<InvoicesListGrid
invoicesPage={invoicesPageData}
<ProformasGrid
data={invoicesPageData}
loading={isLoading}
pageIndex={pageIndex}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
searchValue={search}
onSearchChange={handleSearchChange}
pageIndex={pageIndex}
pageSize={pageSize}
searchValue={search}
/>
</div>
</div>

View File

@ -1,6 +1,7 @@
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
import { InvoiceSummaryFormData } from "./invoice-resume.form.schema";
import { CustomerInvoiceSummary } from "./invoices.api.schema";
import type { InvoiceSummaryFormData } from "./invoice-resume.form.schema";
import type { CustomerInvoiceSummary } from "./invoices.api.schema";
/**
* Convierte el DTO completo de API a datos numéricos para el formulario.

View File

@ -28,6 +28,6 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"],
"include": ["src", "../core/src/common/helpers/date-helper.ts"],
"exclude": ["node_modules"]
}

View File

@ -98,8 +98,6 @@ export class Percentage extends ValueObject<PercentageProps> {
return this.toNumber().toLocaleString(locale, {
style: "percent",
minimumFractionDigits: scale,
maximumSignificantDigits: scale,
});
}
}