Proformas

This commit is contained in:
David Arranz 2025-11-16 20:30:11 +01:00
parent 9b874eebf8
commit f19ab6022b
74 changed files with 810 additions and 940 deletions

View File

@ -1,12 +1,12 @@
import { Presenter } from "@erp/core/api"; import { Presenter } from "@erp/core/api";
import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import { toEmptyString } from "@repo/rdx-ddd"; import { toEmptyString } from "@repo/rdx-ddd";
import type { ArrayElement } from "@repo/rdx-utils"; import type { ArrayElement } from "@repo/rdx-utils";
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto";
import type { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../domain"; import type { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../domain";
type GetCustomerInvoiceItemByInvoiceIdResponseDTO = ArrayElement< type GetCustomerInvoiceItemByInvoiceIdResponseDTO = ArrayElement<
GetIssueInvoiceByIdResponseDTO["items"] GetIssuedInvoiceByIdResponseDTO["items"]
>; >;
export class CustomerInvoiceItemsFullPresenter extends Presenter { export class CustomerInvoiceItemsFullPresenter extends Presenter {
@ -49,7 +49,7 @@ export class CustomerInvoiceItemsFullPresenter extends Presenter {
}; };
} }
toOutput(invoiceItems: CustomerInvoiceItems): GetIssueInvoiceByIdResponseDTO["items"] { toOutput(invoiceItems: CustomerInvoiceItems): GetIssuedInvoiceByIdResponseDTO["items"] {
return invoiceItems.map(this._mapItem); return invoiceItems.map(this._mapItem);
} }
} }

View File

@ -1,7 +1,7 @@
import { Presenter } from "@erp/core/api"; import { Presenter } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd"; import { toEmptyString } from "@repo/rdx-ddd";
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto"; import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../common/dto";
import type { CustomerInvoice } from "../../../domain"; import type { CustomerInvoice } from "../../../domain";
import type { CustomerInvoiceItemsFullPresenter } from "./customer-invoice-items.full.presenter"; import type { CustomerInvoiceItemsFullPresenter } from "./customer-invoice-items.full.presenter";
@ -9,9 +9,9 @@ import type { RecipientInvoiceFullPresenter } from "./recipient-invoice.full.rep
export class CustomerInvoiceFullPresenter extends Presenter< export class CustomerInvoiceFullPresenter extends Presenter<
CustomerInvoice, CustomerInvoice,
GetIssueInvoiceByIdResponseDTO GetIssuedInvoiceByIdResponseDTO
> { > {
toOutput(invoice: CustomerInvoice): GetIssueInvoiceByIdResponseDTO { toOutput(invoice: CustomerInvoice): GetIssuedInvoiceByIdResponseDTO {
const itemsPresenter = this.presenterRegistry.getPresenter({ const itemsPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice-items", resource: "customer-invoice-items",
projection: "FULL", projection: "FULL",

View File

@ -1,10 +1,10 @@
import { Presenter } from "@erp/core/api"; import { Presenter } from "@erp/core/api";
import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd"; import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd";
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto"; import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../common/dto";
import type { CustomerInvoice, InvoiceRecipient } from "../../../domain"; import type { CustomerInvoice, InvoiceRecipient } from "../../../domain";
type GetRecipientInvoiceByInvoiceIdResponseDTO = GetIssueInvoiceByIdResponseDTO["recipient"]; type GetRecipientInvoiceByInvoiceIdResponseDTO = GetIssuedInvoiceByIdResponseDTO["recipient"];
export class RecipientInvoiceFullPresenter extends Presenter { export class RecipientInvoiceFullPresenter extends Presenter {
toOutput(invoice: CustomerInvoice): GetRecipientInvoiceByInvoiceIdResponseDTO { toOutput(invoice: CustomerInvoice): GetRecipientInvoiceByInvoiceIdResponseDTO {

View File

@ -1,9 +1,9 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core"; import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import { type IPresenterOutputParams, Presenter } from "@erp/core/api"; import { type IPresenterOutputParams, Presenter } from "@erp/core/api";
import type { GetIssueInvoiceByIdResponseDTO } from "@erp/customer-invoices/common"; import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils"; import type { ArrayElement } from "@repo/rdx-utils";
type CustomerInvoiceItemsDTO = GetIssueInvoiceByIdResponseDTO["items"]; type CustomerInvoiceItemsDTO = GetIssuedInvoiceByIdResponseDTO["items"];
type CustomerInvoiceItemDTO = ArrayElement<CustomerInvoiceItemsDTO>; type CustomerInvoiceItemDTO = ArrayElement<CustomerInvoiceItemsDTO>;
export class CustomerInvoiceItemsReportPersenter extends Presenter< export class CustomerInvoiceItemsReportPersenter extends Presenter<

View File

@ -1,9 +1,9 @@
import { type JsonTaxCatalogProvider, MoneyDTOHelper, SpainTaxCatalogProvider } from "@erp/core"; import { type JsonTaxCatalogProvider, MoneyDTOHelper, SpainTaxCatalogProvider } from "@erp/core";
import { type IPresenterOutputParams, Presenter } from "@erp/core/api"; import { type IPresenterOutputParams, Presenter } from "@erp/core/api";
import type { GetIssueInvoiceByIdResponseDTO } from "@erp/customer-invoices/common"; import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils"; import type { ArrayElement } from "@repo/rdx-utils";
type CustomerInvoiceTaxesDTO = GetIssueInvoiceByIdResponseDTO["taxes"]; type CustomerInvoiceTaxesDTO = GetIssuedInvoiceByIdResponseDTO["taxes"];
type CustomerInvoiceTaxDTO = ArrayElement<CustomerInvoiceTaxesDTO>; type CustomerInvoiceTaxDTO = ArrayElement<CustomerInvoiceTaxesDTO>;
export class CustomerInvoiceTaxesReportPresenter extends Presenter< export class CustomerInvoiceTaxesReportPresenter extends Presenter<

View File

@ -1,14 +1,14 @@
import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core"; import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
import { Presenter } from "@erp/core/api"; import { Presenter } from "@erp/core/api";
import type { GetIssueInvoiceByIdResponseDTO } from "../../../../common/dto"; import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../common/dto";
export class CustomerInvoiceReportPresenter extends Presenter< export class CustomerInvoiceReportPresenter extends Presenter<
GetIssueInvoiceByIdResponseDTO, GetIssuedInvoiceByIdResponseDTO,
unknown unknown
> { > {
private _formatPaymentMethodDTO( private _formatPaymentMethodDTO(
paymentMethod?: GetIssueInvoiceByIdResponseDTO["payment_method"] paymentMethod?: GetIssuedInvoiceByIdResponseDTO["payment_method"]
) { ) {
if (!paymentMethod) { if (!paymentMethod) {
return ""; return "";
@ -17,7 +17,7 @@ export class CustomerInvoiceReportPresenter extends Presenter<
return paymentMethod.payment_description ?? ""; return paymentMethod.payment_description ?? "";
} }
toOutput(invoiceDTO: GetIssueInvoiceByIdResponseDTO) { toOutput(invoiceDTO: GetIssuedInvoiceByIdResponseDTO) {
const itemsPresenter = this.presenterRegistry.getPresenter({ const itemsPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice-items", resource: "customer-invoice-items",
projection: "REPORT", projection: "REPORT",

View File

@ -3,14 +3,14 @@ import type { Criteria } from "@repo/rdx-criteria/server";
import { toEmptyString } from "@repo/rdx-ddd"; import { toEmptyString } from "@repo/rdx-ddd";
import type { ArrayElement, Collection } from "@repo/rdx-utils"; import type { ArrayElement, Collection } from "@repo/rdx-utils";
import type { ListIssueInvoicesResponseDTO } from "../../../../common/dto"; import type { ListIssuedInvoicesResponseDTO } from "../../../../common/dto";
import type { CustomerInvoiceListDTO } from "../../../infrastructure"; import type { CustomerInvoiceListDTO } from "../../../infrastructure";
export class ListCustomerInvoicesPresenter extends Presenter { export class ListCustomerInvoicesPresenter extends Presenter {
protected _mapInvoice(invoice: CustomerInvoiceListDTO) { protected _mapInvoice(invoice: CustomerInvoiceListDTO) {
const recipientDTO = invoice.recipient.toObjectString(); const recipientDTO = invoice.recipient.toObjectString();
const invoiceDTO: ArrayElement<ListIssueInvoicesResponseDTO["items"]> = { const invoiceDTO: ArrayElement<ListIssuedInvoicesResponseDTO["items"]> = {
id: invoice.id.toString(), id: invoice.id.toString(),
company_id: invoice.companyId.toString(), company_id: invoice.companyId.toString(),
is_proforma: invoice.isProforma, is_proforma: invoice.isProforma,
@ -50,7 +50,7 @@ export class ListCustomerInvoicesPresenter extends Presenter {
toOutput(params: { toOutput(params: {
customerInvoices: Collection<CustomerInvoiceListDTO>; customerInvoices: Collection<CustomerInvoiceListDTO>;
criteria: Criteria; criteria: Criteria;
}): ListIssueInvoicesResponseDTO { }): ListIssuedInvoicesResponseDTO {
const { customerInvoices, criteria } = params; const { customerInvoices, criteria } = params;
const invoices = customerInvoices.map((invoice) => this._mapInvoice(invoice)); const invoices = customerInvoices.map((invoice) => this._mapInvoice(invoice));

View File

@ -48,7 +48,7 @@ export class CustomerInvoiceApplicationService {
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoiceNumber, Error> - El agregado construido o un error si falla la creación. * @returns Result<CustomerInvoiceNumber, Error> - El agregado construido o un error si falla la creación.
*/ */
async getNextIssueInvoiceNumber( async getNextIssuedInvoiceNumber(
companyId: UniqueID, companyId: UniqueID,
series: Maybe<CustomerInvoiceSerie>, series: Maybe<CustomerInvoiceSerie>,
transaction: Transaction transaction: Transaction
@ -80,7 +80,7 @@ export class CustomerInvoiceApplicationService {
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - La factura guardada o un error si falla la operación. * @returns Result<CustomerInvoice, Error> - La factura guardada o un error si falla la operación.
*/ */
async createIssueInvoiceInCompany( async createIssuedInvoiceInCompany(
companyId: UniqueID, companyId: UniqueID,
invoice: CustomerInvoice, invoice: CustomerInvoice,
transaction: Transaction transaction: Transaction
@ -90,7 +90,7 @@ export class CustomerInvoiceApplicationService {
return Result.fail(result.error); return Result.fail(result.error);
} }
return this.getIssueInvoiceByIdInCompany(companyId, invoice.id, transaction); return this.getIssuedInvoiceByIdInCompany(companyId, invoice.id, transaction);
} }
/** /**
@ -157,7 +157,7 @@ export class CustomerInvoiceApplicationService {
/** /**
* *
* Comprueba si existe o no en persistencia una proforma con el ID proporcionado * Comprueba si existe o no en persistencia una factura con el ID proporcionado
* *
* @param companyId - Identificador de la empresa a la que pertenece la factura. * @param companyId - Identificador de la empresa a la que pertenece la factura.
* @param invoiceId - Identificador UUID de la factura. * @param invoiceId - Identificador UUID de la factura.
@ -165,7 +165,7 @@ export class CustomerInvoiceApplicationService {
* @returns Result<Boolean, Error> - Existe la factura o no. * @returns Result<Boolean, Error> - Existe la factura o no.
*/ */
async existsIssueInvoiceByIdInCompany( async existsIssuedInvoiceByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: Transaction
@ -178,7 +178,7 @@ export class CustomerInvoiceApplicationService {
/** /**
* Obtiene una colección de proformas que cumplen con los filtros definidos en un objeto Criteria. * Obtiene una colección de proformas que cumplen con los filtros definidos en un objeto Criteria.
* *
* @param companyId - Identificador de la empresa a la que pertenece la factura. * @param companyId - Identificador de la empresa a la que pertenece la proforma.
* @param criteria - Objeto con condiciones de filtro, paginación y orden. * @param criteria - Objeto con condiciones de filtro, paginación y orden.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<Collection<ProformasListDTO>, Error> - Colección de proformas o error. * @returns Result<Collection<ProformasListDTO>, Error> - Colección de proformas o error.
@ -203,7 +203,7 @@ export class CustomerInvoiceApplicationService {
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<Collection<CustomerInvoiceListDTO>, Error> - Colección de facturas o error. * @returns Result<Collection<CustomerInvoiceListDTO>, Error> - Colección de facturas o error.
*/ */
async findIssueInvoiceByCriteriaInCompany( async findIssuedInvoiceByCriteriaInCompany(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: Transaction transaction?: Transaction
@ -222,7 +222,7 @@ export class CustomerInvoiceApplicationService {
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - Factura encontrada o error. * @returns Result<CustomerInvoice, Error> - Factura encontrada o error.
*/ */
async getIssueInvoiceByIdInCompany( async getIssuedInvoiceByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: Transaction

View File

@ -1,2 +1,2 @@
export * from "./issue-invoices"; export * from "./issued-invoices";
export * from "./proformas"; export * from "./proformas";

View File

@ -1,3 +0,0 @@
export * from "./get-issue-invoice.use-case";
export * from "./list-issue-invoices.use-case";
export * from "./report-issue-invoice.use-case";

View File

@ -5,19 +5,19 @@ import { Result } from "@repo/rdx-utils";
import type { CustomerInvoiceFullPresenter } from "../../presenters/domain"; import type { CustomerInvoiceFullPresenter } from "../../presenters/domain";
import type { CustomerInvoiceApplicationService } from "../../services"; import type { CustomerInvoiceApplicationService } from "../../services";
type GetIssueInvoiceUseCaseInput = { type GetIssuedInvoiceUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
invoice_id: string; invoice_id: string;
}; };
export class GetIssueInvoiceUseCase { export class GetIssuedInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceApplicationService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}
public execute(params: GetIssueInvoiceUseCaseInput) { public execute(params: GetIssuedInvoiceUseCaseInput) {
const { invoice_id, companyId } = params; const { invoice_id, companyId } = params;
const idOrError = UniqueID.create(invoice_id); const idOrError = UniqueID.create(invoice_id);
@ -33,7 +33,7 @@ export class GetIssueInvoiceUseCase {
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
const invoiceOrError = await this.service.getIssueInvoiceByIdInCompany( const invoiceOrError = await this.service.getIssuedInvoiceByIdInCompany(
companyId, companyId,
invoiceId, invoiceId,
transaction transaction

View File

@ -0,0 +1,3 @@
export * from "./get-issued-invoice.use-case";
export * from "./list-issued-invoices.use-case";
export * from "./report-issued-invoice.use-case";

View File

@ -4,16 +4,16 @@ import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import type { ListIssueInvoicesResponseDTO } from "../../../../common/dto"; import type { ListIssuedInvoicesResponseDTO } from "../../../../common/dto";
import type { ListCustomerInvoicesPresenter } from "../../presenters"; import type { ListCustomerInvoicesPresenter } from "../../presenters";
import type { CustomerInvoiceApplicationService } from "../../services"; import type { CustomerInvoiceApplicationService } from "../../services";
type ListIssueInvoicesUseCaseInput = { type ListIssuedInvoicesUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
criteria: Criteria; criteria: Criteria;
}; };
export class ListIssueInvoicesUseCase { export class ListIssuedInvoicesUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceApplicationService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
@ -21,8 +21,8 @@ export class ListIssueInvoicesUseCase {
) {} ) {}
public execute( public execute(
params: ListIssueInvoicesUseCaseInput params: ListIssuedInvoicesUseCaseInput
): Promise<Result<ListIssueInvoicesResponseDTO, Error>> { ): Promise<Result<ListIssuedInvoicesResponseDTO, Error>> {
const { criteria, companyId } = params; const { criteria, companyId } = params;
const presenter = this.presenterRegistry.getPresenter({ const presenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice", resource: "customer-invoice",
@ -31,7 +31,7 @@ export class ListIssueInvoicesUseCase {
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
try { try {
const result = await this.service.findIssueInvoiceByCriteriaInCompany( const result = await this.service.findIssuedInvoiceByCriteriaInCompany(
companyId, companyId,
criteria, criteria,
transaction transaction

View File

@ -5,19 +5,19 @@ import { Result } from "@repo/rdx-utils";
import type { CustomerInvoiceApplicationService } from "../../services"; import type { CustomerInvoiceApplicationService } from "../../services";
import type { CustomerInvoiceReportPDFPresenter } from "../proformas"; import type { CustomerInvoiceReportPDFPresenter } from "../proformas";
type ReportIssueInvoiceUseCaseInput = { type ReportIssuedInvoiceUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
invoice_id: string; invoice_id: string;
}; };
export class ReportIssueInvoiceUseCase { export class ReportIssuedInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceApplicationService, private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry private readonly presenterRegistry: IPresenterRegistry
) {} ) {}
public async execute(params: ReportIssueInvoiceUseCaseInput) { public async execute(params: ReportIssuedInvoiceUseCaseInput) {
const { invoice_id, companyId } = params; const { invoice_id, companyId } = params;
const idOrError = UniqueID.create(invoice_id); const idOrError = UniqueID.create(invoice_id);
@ -35,7 +35,7 @@ export class ReportIssueInvoiceUseCase {
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
const invoiceOrError = await this.service.getIssueInvoiceByIdInCompany( const invoiceOrError = await this.service.getIssuedInvoiceByIdInCompany(
companyId, companyId,
invoiceId, invoiceId,
transaction transaction

View File

@ -9,7 +9,7 @@ import {
import type { CustomerInvoiceFullPresenter } from "../../presenters"; import type { CustomerInvoiceFullPresenter } from "../../presenters";
import type { CustomerInvoiceApplicationService } from "../../services"; import type { CustomerInvoiceApplicationService } from "../../services";
type IssueCustomerInvoiceUseCaseInput = { type IssueProformaUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
proforma_id: string; proforma_id: string;
}; };
@ -23,7 +23,7 @@ type IssueCustomerInvoiceUseCaseInput = {
* - Marca la proforma como "issued" * - Marca la proforma como "issued"
* - Persiste ambas dentro de la misma transacción * - Persiste ambas dentro de la misma transacción
*/ */
export class IssueProformaInvoiceUseCase { export class IssueProformaUseCase {
private readonly issueDomainService: IssueCustomerInvoiceDomainService; private readonly issueDomainService: IssueCustomerInvoiceDomainService;
private readonly proformaDomainService: ProformaCustomerInvoiceDomainService; private readonly proformaDomainService: ProformaCustomerInvoiceDomainService;
@ -36,7 +36,7 @@ export class IssueProformaInvoiceUseCase {
this.proformaDomainService = new ProformaCustomerInvoiceDomainService(); this.proformaDomainService = new ProformaCustomerInvoiceDomainService();
} }
public execute(params: IssueCustomerInvoiceUseCaseInput) { public execute(params: IssueProformaUseCaseInput) {
const { proforma_id, companyId } = params; const { proforma_id, companyId } = params;
const idOrError = UniqueID.create(proforma_id); const idOrError = UniqueID.create(proforma_id);
if (idOrError.isFailure) return Result.fail(idOrError.error); if (idOrError.isFailure) return Result.fail(idOrError.error);
@ -59,7 +59,7 @@ export class IssueProformaInvoiceUseCase {
const proforma = proformaResult.data; const proforma = proformaResult.data;
/** 2. Generar nueva factura */ /** 2. Generar nueva factura */
const nextNumberResult = await this.service.getNextIssueInvoiceNumber( const nextNumberResult = await this.service.getNextIssuedInvoiceNumber(
companyId, companyId,
proforma.series, proforma.series,
transaction transaction
@ -74,7 +74,7 @@ export class IssueProformaInvoiceUseCase {
if (issuedInvoiceOrError.isFailure) return Result.fail(issuedInvoiceOrError.error); if (issuedInvoiceOrError.isFailure) return Result.fail(issuedInvoiceOrError.error);
/** 5. Guardar la nueva factura */ /** 5. Guardar la nueva factura */
const saveInvoiceResult = await this.service.createIssueInvoiceInCompany( const saveInvoiceResult = await this.service.createIssuedInvoiceInCompany(
companyId, companyId,
issuedInvoiceOrError.data, issuedInvoiceOrError.data,
transaction transaction

View File

@ -4,7 +4,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import type { ListIssueInvoicesResponseDTO } from "../../../../common/dto"; import type { ListIssuedInvoicesResponseDTO } from "../../../../common/dto";
import type { ListCustomerInvoicesPresenter } from "../../presenters"; import type { ListCustomerInvoicesPresenter } from "../../presenters";
import type { CustomerInvoiceApplicationService } from "../../services"; import type { CustomerInvoiceApplicationService } from "../../services";
@ -22,7 +22,7 @@ export class ListProformasUseCase {
public execute( public execute(
params: ListProformasUseCaseInput params: ListProformasUseCaseInput
): Promise<Result<ListIssueInvoicesResponseDTO, Error>> { ): Promise<Result<ListIssuedInvoicesResponseDTO, Error>> {
const { criteria, companyId } = params; const { criteria, companyId } = params;
const presenter = this.presenterRegistry.getPresenter({ const presenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice", resource: "customer-invoice",

View File

@ -1,9 +1,11 @@
import { Tax } from "@erp/core/api"; import type { Tax } from "@erp/core/api";
import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils"; import { Collection } from "@repo/rdx-utils";
import { ItemAmount } from "../../value-objects"; import { ItemAmount } from "../../value-objects";
import { ItemTaxes } from "../item-taxes"; import { ItemTaxes } from "../item-taxes";
import { CustomerInvoiceItem } from "./customer-invoice-item";
import type { CustomerInvoiceItem } from "./customer-invoice-item";
export interface CustomerInvoiceItemsProps { export interface CustomerInvoiceItemsProps {
items?: CustomerInvoiceItem[]; items?: CustomerInvoiceItem[];
@ -39,8 +41,9 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
// Antes de añadir un nuevo item, debo comprobar que el item a añadir // Antes de añadir un nuevo item, debo comprobar que el item a añadir
// tiene el mismo "currencyCode" y "languageCode" que la colección de items. // tiene el mismo "currencyCode" y "languageCode" que la colección de items.
if ( if (
!this._languageCode.equals(item.languageCode) || !(
!this._currencyCode.equals(item.currencyCode) this._languageCode.equals(item.languageCode) && this._currencyCode.equals(item.currencyCode)
)
) { ) {
return false; return false;
} }

View File

@ -1,15 +1,15 @@
import { import {
City, type City,
Country, type Country,
Name, type Name,
PostalCode, type PostalCode,
Province, type Province,
Street, type Street,
TINNumber, type TINNumber,
ValueObject, ValueObject,
toEmptyString, toEmptyString,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { type Maybe, Result } from "@repo/rdx-utils";
export interface InvoiceRecipientProps { export interface InvoiceRecipientProps {
name: Name; name: Name;

View File

@ -3,7 +3,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
import type { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import { buildCustomerInvoiceDependencies, models, proformasRouter } from "./infrastructure"; import { buildCustomerInvoiceDependencies, models, proformasRouter } from "./infrastructure";
import { issueInvoicesRouter } from "./infrastructure/express/issue-invoices.routes"; import { issuedInvoicesRouter } from "./infrastructure/express/issued-invoices.routes";
export const customerInvoicesAPIModule: IModuleServer = { export const customerInvoicesAPIModule: IModuleServer = {
name: "customer-invoices", name: "customer-invoices",
@ -14,7 +14,7 @@ export const customerInvoicesAPIModule: IModuleServer = {
// const contacts = getService<ContactsService>("contacts"); // const contacts = getService<ContactsService>("contacts");
const { logger } = params; const { logger } = params;
proformasRouter(params); proformasRouter(params);
issueInvoicesRouter(params); issuedInvoicesRouter(params);
logger.info("🚀 CustomerInvoices module initialized", { label: this.name }); logger.info("🚀 CustomerInvoices module initialized", { label: this.name });
}, },

View File

@ -20,14 +20,14 @@ import {
CustomerInvoiceReportPresenter, CustomerInvoiceReportPresenter,
CustomerInvoiceTaxesReportPresenter, CustomerInvoiceTaxesReportPresenter,
DeleteProformaUseCase, DeleteProformaUseCase,
GetIssueInvoiceUseCase, GetIssuedInvoiceUseCase,
GetProformaUseCase, GetProformaUseCase,
IssueProformaInvoiceUseCase, IssueProformaUseCase,
ListCustomerInvoicesPresenter, ListCustomerInvoicesPresenter,
ListIssueInvoicesUseCase, ListIssuedInvoicesUseCase,
ListProformasUseCase, ListProformasUseCase,
RecipientInvoiceFullPresenter, RecipientInvoiceFullPresenter,
ReportIssueInvoiceUseCase, ReportIssuedInvoiceUseCase,
ReportProformaUseCase, ReportProformaUseCase,
UpdateProformaUseCase, UpdateProformaUseCase,
} from "../application"; } from "../application";
@ -52,12 +52,12 @@ export type CustomerInvoiceDeps = {
update_proforma: () => UpdateProformaUseCase; update_proforma: () => UpdateProformaUseCase;
delete_proforma: () => DeleteProformaUseCase; delete_proforma: () => DeleteProformaUseCase;
report_proforma: () => ReportProformaUseCase; report_proforma: () => ReportProformaUseCase;
issue_proforma: () => IssueProformaInvoiceUseCase; issue_proforma: () => IssueProformaUseCase;
changeStatus_proforma: () => ChangeStatusProformaUseCase; changeStatus_proforma: () => ChangeStatusProformaUseCase;
list_issue_invoices: () => ListIssueInvoicesUseCase; list_issued_invoices: () => ListIssuedInvoicesUseCase;
get_issue_invoice: () => GetIssueInvoiceUseCase; get_issued_invoice: () => GetIssuedInvoiceUseCase;
report_issue_invoice: () => ReportIssueInvoiceUseCase; report_issued_invoice: () => ReportIssuedInvoiceUseCase;
}; };
}; };
@ -144,16 +144,16 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
report_proforma: () => report_proforma: () =>
new ReportProformaUseCase(appService, transactionManager, presenterRegistry), new ReportProformaUseCase(appService, transactionManager, presenterRegistry),
issue_proforma: () => issue_proforma: () =>
new IssueProformaInvoiceUseCase(appService, transactionManager, presenterRegistry), new IssueProformaUseCase(appService, transactionManager, presenterRegistry),
changeStatus_proforma: () => new ChangeStatusProformaUseCase(appService, transactionManager), changeStatus_proforma: () => new ChangeStatusProformaUseCase(appService, transactionManager),
// Issue Invoices // Issue Invoices
list_issue_invoices: () => list_issued_invoices: () =>
new ListIssueInvoicesUseCase(appService, transactionManager, presenterRegistry), new ListIssuedInvoicesUseCase(appService, transactionManager, presenterRegistry),
get_issue_invoice: () => get_issued_invoice: () =>
new GetIssueInvoiceUseCase(appService, transactionManager, presenterRegistry), new GetIssuedInvoiceUseCase(appService, transactionManager, presenterRegistry),
report_issue_invoice: () => report_issued_invoice: () =>
new ReportIssueInvoiceUseCase(appService, transactionManager, presenterRegistry), new ReportIssuedInvoiceUseCase(appService, transactionManager, presenterRegistry),
}; };
return { return {

View File

@ -1,2 +1,2 @@
export * from "./issue-invoices"; export * from "./issued-invoices";
export * from "./proformas"; export * from "./proformas";

View File

@ -1,3 +0,0 @@
export * from "./get-issue-invoice.controller";
export * from "./list-issue-invoices.controller";
export * from "./report-issue-invoice.controller";

View File

@ -1,10 +1,10 @@
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import type { GetIssueInvoiceUseCase } from "../../../../application"; import type { GetIssuedInvoiceUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class GetIssueInvoiceController extends ExpressController { export class GetIssueInvoiceController extends ExpressController {
public constructor(private readonly useCase: GetIssueInvoiceUseCase) { public constructor(private readonly useCase: GetIssuedInvoiceUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;

View File

@ -0,0 +1,3 @@
export * from "./get-issued-invoice.controller";
export * from "./list-issued-invoices.controller";
export * from "./report-issued-invoice.controller";

View File

@ -1,11 +1,11 @@
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
import type { ListIssueInvoicesUseCase } from "../../../../application"; import type { ListIssuedInvoicesUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class ListIssueInvoicesController extends ExpressController { export class ListIssuedInvoicesController extends ExpressController {
public constructor(private readonly useCase: ListIssueInvoicesUseCase) { public constructor(private readonly useCase: ListIssuedInvoicesUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;

View File

@ -1,10 +1,10 @@
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import type { ReportIssueInvoiceUseCase } from "../../../../application"; import type { ReportIssuedInvoiceUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class ReportIssueInvoiceController extends ExpressController { export class ReportIssuedInvoiceController extends ExpressController {
public constructor(private readonly useCase: ReportIssueInvoiceUseCase) { public constructor(private readonly useCase: ReportIssuedInvoiceUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;

View File

@ -1,10 +1,10 @@
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import type { IssueProformaInvoiceUseCase } from "../../../../application"; import type { IssueProformaUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper";
export class IssueProformaController extends ExpressController { export class IssueProformaController extends ExpressController {
public constructor(private readonly useCase: IssueProformaInvoiceUseCase) { public constructor(private readonly useCase: IssueProformaUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;

View File

@ -11,9 +11,11 @@ import {
import { import {
type CustomerInvoiceIdAlreadyExistsError, type CustomerInvoiceIdAlreadyExistsError,
type EntityIsNotProformaError, type EntityIsNotProformaError,
type InvalidProformaTransitionError,
type ProformaCannotBeConvertedToInvoiceError, type ProformaCannotBeConvertedToInvoiceError,
isCustomerInvoiceIdAlreadyExistsError, isCustomerInvoiceIdAlreadyExistsError,
isEntityIsNotProformaError, isEntityIsNotProformaError,
isInvalidProformaTransitionError,
isProformaCannotBeConvertedToInvoiceError, isProformaCannotBeConvertedToInvoiceError,
isProformaCannotBeDeletedError, isProformaCannotBeDeletedError,
} from "../../domain"; } from "../../domain";
@ -38,6 +40,15 @@ const entityIsNotProformaError: ErrorToApiRule = {
), ),
}; };
const proformaTransitionRule: ErrorToApiRule = {
priority: 120,
matches: (e) => isInvalidProformaTransitionError(e),
build: (e) =>
new ValidationApiError(
(e as InvalidProformaTransitionError).message || "Invalid transition for proforma."
),
};
const proformaConversionRule: ErrorToApiRule = { const proformaConversionRule: ErrorToApiRule = {
priority: 120, priority: 120,
matches: (e) => isProformaCannotBeConvertedToInvoiceError(e), matches: (e) => isProformaCannotBeConvertedToInvoiceError(e),
@ -62,4 +73,5 @@ export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.def
.register(invoiceDuplicateRule) .register(invoiceDuplicateRule)
.register(entityIsNotProformaError) .register(entityIsNotProformaError)
.register(proformaConversionRule) .register(proformaConversionRule)
.register(proformaCannotBeDeletedRule); .register(proformaCannotBeDeletedRule)
.register(proformaTransitionRule);

View File

@ -1,2 +1,2 @@
export * from "./issue-invoices.routes"; export * from "./issued-invoices.routes";
export * from "./proformas.routes"; export * from "./proformas.routes";

View File

@ -6,18 +6,18 @@ import type { Sequelize } from "sequelize";
import { import {
GetIssueInvoiceByIdRequestSchema, GetIssueInvoiceByIdRequestSchema,
ListIssueInvoicesRequestSchema, ListIssuedInvoicesRequestSchema,
ReportIssueInvoiceByIdRequestSchema, ReportIssueInvoiceByIdRequestSchema,
} from "../../../common/dto"; } from "../../../common/dto";
import { buildCustomerInvoiceDependencies } from "../dependencies"; import { buildCustomerInvoiceDependencies } from "../dependencies";
import { import {
GetIssueInvoiceController, GetIssueInvoiceController,
ListIssueInvoicesController, ListIssuedInvoicesController,
ReportIssueInvoiceController, ReportIssuedInvoiceController,
} from "./controllers"; } from "./controllers";
export const issueInvoicesRouter = (params: ModuleParams) => { export const issuedInvoicesRouter = (params: ModuleParams) => {
const { app, baseRoutePath, logger } = params as { const { app, baseRoutePath, logger } = params as {
app: Application; app: Application;
database: Sequelize; database: Sequelize;
@ -49,10 +49,10 @@ export const issueInvoicesRouter = (params: ModuleParams) => {
router.get( router.get(
"/", "/",
//checkTabContext, //checkTabContext,
validateRequest(ListIssueInvoicesRequestSchema, "params"), validateRequest(ListIssuedInvoicesRequestSchema, "params"),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.list_issue_invoices(); const useCase = deps.useCases.list_issued_invoices();
const controller = new ListIssueInvoicesController(useCase /*, deps.presenters.list */); const controller = new ListIssuedInvoicesController(useCase /*, deps.presenters.list */);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );
@ -62,7 +62,7 @@ export const issueInvoicesRouter = (params: ModuleParams) => {
//checkTabContext, //checkTabContext,
validateRequest(GetIssueInvoiceByIdRequestSchema, "params"), validateRequest(GetIssueInvoiceByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.get_issue_invoice(); const useCase = deps.useCases.get_issued_invoice();
const controller = new GetIssueInvoiceController(useCase); const controller = new GetIssueInvoiceController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
@ -73,11 +73,11 @@ export const issueInvoicesRouter = (params: ModuleParams) => {
//checkTabContext, //checkTabContext,
validateRequest(ReportIssueInvoiceByIdRequestSchema, "params"), validateRequest(ReportIssueInvoiceByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.report_issue_invoice(); const useCase = deps.useCases.report_issued_invoice();
const controller = new ReportIssueInvoiceController(useCase); const controller = new ReportIssuedInvoiceController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );
app.use(`${baseRoutePath}/issue-invoices`, router); app.use(`${baseRoutePath}/issued-invoices`, router);
}; };

View File

@ -1,5 +1,9 @@
import { type RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api"; import { type RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
import { type ModuleParams, validateRequest } from "@erp/core/api"; import { type ModuleParams, validateRequest } from "@erp/core/api";
import type { ILogger } from "@repo/rdx-logger";
import { type Application, type NextFunction, type Request, type Response, Router } from "express";
import type { Sequelize } from "sequelize";
import { import {
ChangeStatusProformaByIdParamsRequestSchema, ChangeStatusProformaByIdParamsRequestSchema,
ChangeStatusProformaByIdRequestSchema, ChangeStatusProformaByIdRequestSchema,
@ -11,11 +15,7 @@ import {
ReportProformaByIdRequestSchema, ReportProformaByIdRequestSchema,
UpdateProformaByIdParamsRequestSchema, UpdateProformaByIdParamsRequestSchema,
UpdateProformaByIdRequestSchema, UpdateProformaByIdRequestSchema,
} from "@erp/customer-invoices/common"; } from "../../../common";
import type { ILogger } from "@repo/rdx-logger";
import { type Application, type NextFunction, type Request, type Response, Router } from "express";
import type { Sequelize } from "sequelize";
import { buildCustomerInvoiceDependencies } from "../dependencies"; import { buildCustomerInvoiceDependencies } from "../dependencies";
import { import {
@ -23,7 +23,7 @@ import {
CreateProformaController, CreateProformaController,
DeleteProformaController, DeleteProformaController,
GetProformaController, GetProformaController,
IssueProformaController, IssueProformaController as IssuedProformaController,
ListProformasController, ListProformasController,
ReportProformaController, ReportProformaController,
UpdateProformaController, UpdateProformaController,
@ -150,7 +150,7 @@ export const proformasRouter = (params: ModuleParams) => {
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.issue_proforma(); const useCase = deps.useCases.issue_proforma();
const controller = new IssueProformaController(useCase); const controller = new IssuedProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );

View File

@ -1,2 +1,2 @@
export * from "./issue-invoices"; export * from "./issued-invoices";
export * from "./proformas"; export * from "./proformas";

View File

@ -1,3 +0,0 @@
export * from "./get-issue-invoice-by-id.request.dto";
export * from "./list-issue-invoices.request.dto";
export * from "./report-issue-invoice-by-id.request.dto";

View File

@ -1,5 +0,0 @@
import { CriteriaSchema } from "@erp/core";
import { z } from "zod/v4";
export const ListIssueInvoicesRequestSchema = CriteriaSchema;
export type ListIssueInvoicesRequestDTO = z.infer<typeof ListIssueInvoicesRequestSchema>;

View File

@ -0,0 +1,3 @@
export * from "./get-issued-invoice-by-id.request.dto";
export * from "./list-issued-invoices.request.dto";
export * from "./report-issued-invoice-by-id.request.dto";

View File

@ -0,0 +1,5 @@
import { CriteriaSchema } from "@erp/core";
import type { z } from "zod/v4";
export const ListIssuedInvoicesRequestSchema = CriteriaSchema;
export type ListIssuedInvoicesRequestDTO = z.infer<typeof ListIssuedInvoicesRequestSchema>;

View File

@ -1,2 +1,2 @@
export * from "./issue-invoices"; export * from "./issued-invoices";
export * from "./proformas"; export * from "./proformas";

View File

@ -1,2 +0,0 @@
export * from "./get-issue-invoice-by-id.response.dto";
export * from "./list-issue-invoices.response.dto";

View File

@ -1,7 +1,7 @@
import { MetadataSchema, MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core"; import { MetadataSchema, MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
export const GetIssueInvoiceByIdResponseSchema = z.object({ export const GetIssuedInvoiceByIdResponseSchema = z.object({
id: z.uuid(), id: z.uuid(),
company_id: z.uuid(), company_id: z.uuid(),
@ -79,4 +79,4 @@ export const GetIssueInvoiceByIdResponseSchema = z.object({
metadata: MetadataSchema.optional(), metadata: MetadataSchema.optional(),
}); });
export type GetIssueInvoiceByIdResponseDTO = z.infer<typeof GetIssueInvoiceByIdResponseSchema>; export type GetIssuedInvoiceByIdResponseDTO = z.infer<typeof GetIssuedInvoiceByIdResponseSchema>;

View File

@ -0,0 +1,2 @@
export * from "./get-issued-invoice-by-id.response.dto";
export * from "./list-issued-invoices.response.dto";

View File

@ -6,7 +6,7 @@ import {
} from "@erp/core"; } from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
export const ListIssueInvoicesResponseSchema = createPaginatedListSchema( export const ListIssuedInvoicesResponseSchema = createPaginatedListSchema(
z.object({ z.object({
id: z.uuid(), id: z.uuid(),
company_id: z.uuid(), company_id: z.uuid(),
@ -51,4 +51,4 @@ export const ListIssueInvoicesResponseSchema = createPaginatedListSchema(
}) })
); );
export type ListIssueInvoicesResponseDTO = z.infer<typeof ListIssueInvoicesResponseSchema>; export type ListIssuedInvoicesResponseDTO = z.infer<typeof ListIssuedInvoicesResponseSchema>;

View File

@ -1,16 +1,20 @@
{ {
"common": { "common": {
"more_actions": "More actions",
"append_empty_row": "Append row", "append_empty_row": "Append row",
"append_empty_row_tooltip": "Append a empty row", "append_empty_row_tooltip": "Append a empty row",
"edit_row": "Edit",
"duplicate_row": "Duplicate", "duplicate_row": "Duplicate",
"duplicate_selected_rows": "Duplicate", "duplicate_selected_rows": "Duplicate",
"duplicate_selected_rows_tooltip": "Duplicate selected row(s)", "duplicate_selected_rows_tooltip": "Duplicate selected row(s)",
"remove_selected_rows": "Remove", "remove_selected_rows": "Remove",
"remove_selected_rows_tooltip": "Remove selected row(s)", "remove_selected_rows_tooltip": "Remove selected row(s)",
"download_pdf": "Download PDF",
"send_email": "Send email",
"insert_row_above": "Insert row above", "insert_row_above": "Insert row above",
"insert_row_below": "Insert row below", "insert_row_below": "Insert row below",
"remove_row": "Remove", "delete_row": "Delete",
"actions": "Actions", "actions": "Actions",
"rows_selected": "{{count}} fila(s) seleccionadas.", "rows_selected": "{{count}} fila(s) seleccionadas.",
@ -22,12 +26,15 @@
}, },
"catalog": { "catalog": {
"status": { "proformas": {
"draft": "Draft", "status": {
"issued": "Issued", "all": "All",
"sent": "Sent", "draft": "Draft",
"received": "Received", "sent": "Sent",
"rejected": "Rejected" "approved": "Approved",
"rejected": "Rejected",
"issued": "Issued"
}
} }
}, },
"pages": { "pages": {
@ -40,9 +47,11 @@
"grid_columns": { "grid_columns": {
"invoice_number": "Inv. number", "invoice_number": "Inv. number",
"series": "Serie", "series": "Serie",
"reference": "Reference",
"status": "Status", "status": "Status",
"invoice_date": "Proforma date", "invoice_date": "Proforma date",
"operation_date": "Operation date", "operation_date": "Operation date",
"recipient": "Customer",
"recipient_tin": "TIN", "recipient_tin": "TIN",
"recipient_name": "Customer name", "recipient_name": "Customer name",
"recipient_street": "Street", "recipient_street": "Street",
@ -50,7 +59,10 @@
"recipient_province": "Province", "recipient_province": "Province",
"recipient_postal_code": "Postal code", "recipient_postal_code": "Postal code",
"recipient_country": "Country", "recipient_country": "Country",
"total_amount": "Total price" "subtotal_amount": "Subtotal",
"discount_amount": "Discount",
"taxes_amount": "Taxes",
"total_amount": "Total"
} }
}, },
"create": { "create": {

View File

@ -1,16 +1,20 @@
{ {
"common": { "common": {
"more_actions": "Más acciones",
"append_empty_row": "Añadir fila", "append_empty_row": "Añadir fila",
"append_empty_row_tooltip": "Añadir una fila vacía", "append_empty_row_tooltip": "Añadir una fila vacía",
"edit_row": "Modificar",
"duplicate_row": "Duplicar", "duplicate_row": "Duplicar",
"duplicate_selected_rows": "Duplicar", "duplicate_selected_rows": "Duplicar",
"duplicate_selected_rows_tooltip": "Duplicar fila(s) seleccionada(s)", "duplicate_selected_rows_tooltip": "Duplicar fila(s) seleccionada(s)",
"remove_selected_rows": "Eliminar", "remove_selected_rows": "Eliminar",
"remove_selected_rows_tooltip": "Eliminar fila(s) seleccionada(s)", "remove_selected_rows_tooltip": "Eliminar fila(s) seleccionada(s)",
"download_pdf": "Descargar en PDF",
"send_email": "Enviar por email",
"insert_row_above": "Insertar fila arriba", "insert_row_above": "Insertar fila arriba",
"insert_row_below": "Insertar fila abajo", "insert_row_below": "Insertar fila abajo",
"remove_row": "Eliminar", "delete_row": "Eliminar",
"actions": "Acciones", "actions": "Acciones",
"rows_selected": "{{count}} fila(s) seleccionadas.", "rows_selected": "{{count}} fila(s) seleccionadas.",
@ -21,12 +25,15 @@
"clear": "Limpiar" "clear": "Limpiar"
}, },
"catalog": { "catalog": {
"status": { "proformas": {
"draft": "Borrador", "status": {
"issued": "Emitida", "all": "Todas",
"sent": "Enviada", "draft": "Borradores",
"received": "Recibida", "sent": "Enviadas",
"rejected": "Rechazada" "approved": "Aprovadas",
"rejected": "Rechazadas",
"issued": "Emitidas"
}
} }
}, },
"pages": { "pages": {
@ -39,9 +46,11 @@
"grid_columns": { "grid_columns": {
"invoice_number": "Nº proforma", "invoice_number": "Nº proforma",
"series": "Serie", "series": "Serie",
"reference": "Reference",
"status": "Estado", "status": "Estado",
"invoice_date": "Fecha de proforma", "invoice_date": "Fecha de proforma",
"operation_date": "Fecha de operación", "operation_date": "Fecha de operación",
"recipient": "Cliente",
"recipient_tin": "NIF/CIF", "recipient_tin": "NIF/CIF",
"recipient_name": "Cliente", "recipient_name": "Cliente",
"recipient_street": "Dirección", "recipient_street": "Dirección",
@ -49,6 +58,9 @@
"recipient_province": "Provincia", "recipient_province": "Provincia",
"recipient_postal_code": "Código postal", "recipient_postal_code": "Código postal",
"recipient_country": "País", "recipient_country": "País",
"subtotal_amount": "Subtotal",
"discount_amount": "Descuentos",
"taxes_amount": "Impuestos",
"total_amount": "Importe total" "total_amount": "Importe total"
} }
}, },

View File

@ -1,7 +1,7 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core"; import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import type { import type {
GetIssueInvoiceByIdResponseDTO, GetIssuedInvoiceByIdResponseDTO,
UpdateCustomerInvoiceByIdRequestDTO, UpdateCustomerInvoiceByIdRequestDTO,
} from "../../common"; } from "../../common";
import type { InvoiceContextValue } from "../context"; import type { InvoiceContextValue } from "../context";
@ -11,7 +11,7 @@ import type { InvoiceFormData } from "../schemas/invoice.form.schema";
* 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 invoiceDtoToFormAdapter = {
fromDto(dto: GetIssueInvoiceByIdResponseDTO, context: InvoiceContextValue): InvoiceFormData { fromDto(dto: GetIssuedInvoiceByIdResponseDTO, context: InvoiceContextValue): InvoiceFormData {
const { taxCatalog } = context; const { taxCatalog } = context;
return { return {
invoice_number: dto.invoice_number, invoice_number: dto.invoice_number,

View File

@ -1,2 +1 @@
@source "./components"; @source "**/*.{ts,tsx}";
@source "./pages";

View File

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

View File

@ -1,23 +0,0 @@
import {
GetIssueInvoiceByIdResponseSchema,
ListIssueInvoicesResponseSchema,
} from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils";
import type { z } from "zod/v4";
// IssueInvoices
export const IssueInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({
metadata: true,
});
export type IssueInvoice = z.infer<typeof IssueInvoiceSchema>;
export type IssueInvoiceRecipient = IssueInvoice["recipient"];
export type IssueInvoiceItem = ArrayElement<IssueInvoice["items"]>;
// Resultado de consulta con criteria (paginado, etc.)
export const IssueInvoiceSummaryPageSchema = ListIssueInvoicesResponseSchema.omit({
metadata: true,
});
export type IssueInvoiceSummaryPage = z.infer<typeof IssueInvoiceSummaryPageSchema>;
export type IssueInvoiceSummary = Omit<ArrayElement<IssueInvoiceSummaryPage["items"]>, "metadata">;

View File

@ -0,0 +1,2 @@
export * from "./use-issued-invoice-query";
export * from "./use-issued-invoices-query";

View File

@ -1,26 +1,26 @@
import { useDataSource } from "@erp/core/hooks"; import { useDataSource } from "@erp/core/hooks";
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query"; import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
import type { IssueInvoice } from "../issue-invoice.api.schema"; import type { IssueInvoice } from "../issued-invoice.api.schema";
export const ISSUE_INVOICE_QUERY_KEY = (id: string): QueryKey => ["issue-invoice", id] as const; export const ISSUED_INVOICE_QUERY_KEY = (id: string): QueryKey => ["issued-invoice", id] as const;
type InvoiceQueryOptions = { type InvoiceQueryOptions = {
enabled?: boolean; enabled?: boolean;
}; };
export const useIssueInvoiceQuery = (issueInvoiceId?: string, options?: InvoiceQueryOptions) => { export const useIssuedInvoiceQuery = (issuedInvoiceId?: string, options?: InvoiceQueryOptions) => {
const dataSource = useDataSource(); const dataSource = useDataSource();
const enabled = (options?.enabled ?? true) && !!issueInvoiceId; const enabled = (options?.enabled ?? true) && !!issuedInvoiceId;
return useQuery<IssueInvoice, DefaultError>({ return useQuery<IssueInvoice, DefaultError>({
queryKey: ISSUE_INVOICE_QUERY_KEY(issueInvoiceId ?? "unknown"), queryKey: ISSUED_INVOICE_QUERY_KEY(issuedInvoiceId ?? "unknown"),
queryFn: async (context) => { queryFn: async (context) => {
const { signal } = context; const { signal } = context;
if (!issueInvoiceId) { if (!issuedInvoiceId) {
if (!issueInvoiceId) throw new Error("issueInvoiceId is required"); if (!issuedInvoiceId) throw new Error("issueInvoiceId is required");
} }
return await dataSource.getOne<IssueInvoice>("issue-invoices", issueInvoiceId, { return await dataSource.getOne<IssueInvoice>("issued-invoices", issuedInvoiceId, {
signal, signal,
}); });
}, },

View File

@ -2,10 +2,10 @@ import type { CriteriaDTO } from "@erp/core";
import { useDataSource } from "@erp/core/hooks"; import { useDataSource } from "@erp/core/hooks";
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query"; import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
import type { IssueInvoiceSummaryPage } from "../issue-invoice.api.schema"; import type { IssuedInvoicesummaryPage } from "../issued-invoice.api.schema";
export const ISSUE_INVOICES_QUERY_KEY = (criteria: CriteriaDTO): QueryKey => [ export const ISSUED_INVOICES_QUERY_KEY = (criteria: CriteriaDTO): QueryKey => [
"issue-invoice", "issued-invoice",
{ {
pageNumber: criteria.pageNumber ?? 0, pageNumber: criteria.pageNumber ?? 0,
pageSize: criteria.pageSize ?? 10, pageSize: criteria.pageSize ?? 10,
@ -16,21 +16,21 @@ export const ISSUE_INVOICES_QUERY_KEY = (criteria: CriteriaDTO): QueryKey => [
}, },
]; ];
type IssueInvoicesQueryOptions = { type IssuedInvoicesQueryOptions = {
enabled?: boolean; enabled?: boolean;
criteria?: CriteriaDTO; criteria?: CriteriaDTO;
}; };
// Obtener todas las facturas // Obtener todas las facturas
export const useIssueInvoicesQuery = (options?: IssueInvoicesQueryOptions) => { export const useIssuedInvoicesQuery = (options?: IssuedInvoicesQueryOptions) => {
const dataSource = useDataSource(); const dataSource = useDataSource();
const enabled = options?.enabled ?? true; const enabled = options?.enabled ?? true;
const criteria = options?.criteria ?? {}; const criteria = options?.criteria ?? {};
return useQuery<IssueInvoiceSummaryPage, DefaultError>({ return useQuery<IssuedInvoicesummaryPage, DefaultError>({
queryKey: ISSUE_INVOICES_QUERY_KEY(criteria), queryKey: ISSUED_INVOICES_QUERY_KEY(criteria),
queryFn: async ({ signal }) => { queryFn: async ({ signal }) => {
return await dataSource.getList<IssueInvoiceSummaryPage>("issue-invoices", { return await dataSource.getList<IssuedInvoicesummaryPage>("issued-invoices", {
signal, signal,
...criteria, ...criteria,
}); });

View File

@ -1,16 +1,16 @@
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core"; import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
import type { IssueInvoiceSummaryPage } from "./issue-invoice.api.schema"; import type { IssuedInvoicesummaryPage } from "./issued-invoice.api.schema";
import type { import type {
IssueInvoiceSummaryData, IssuedInvoicesummaryData,
IssueInvoiceSummaryPageData, IssuedInvoicesummaryPageData,
} from "./issue-invoice-resume.form.schema"; } from "./issued-invoice-resume.form.schema";
/** /**
* 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 IssueInvoiceResumeDtoAdapter = { export const IssueInvoiceResumeDtoAdapter = {
fromDto(pageDto: IssueInvoiceSummaryPage, context?: unknown): IssueInvoiceSummaryPageData { fromDto(pageDto: IssuedInvoicesummaryPage, context?: unknown): IssuedInvoicesummaryPageData {
return { return {
...pageDto, ...pageDto,
items: pageDto.items.map( items: pageDto.items.map(
@ -64,7 +64,7 @@ export const IssueInvoiceResumeDtoAdapter = {
), ),
//taxes: dto.taxes, //taxes: dto.taxes,
}) as unknown as IssueInvoiceSummaryData }) as unknown as IssuedInvoicesummaryData
), ),
}; };
}, },

View File

@ -1,6 +1,6 @@
import type { IssueInvoiceSummary, IssueInvoiceSummaryPage } from "./issue-invoice.api.schema"; import type { IssuedInvoicesummary, IssuedInvoicesummaryPage } from "./issued-invoice.api.schema";
export type IssueInvoiceSummaryData = IssueInvoiceSummary & { export type IssuedInvoicesummaryData = IssuedInvoicesummary & {
subtotal_amount_fmt: string; subtotal_amount_fmt: string;
subtotal_amount: number; subtotal_amount: number;
@ -20,6 +20,6 @@ export type IssueInvoiceSummaryData = IssueInvoiceSummary & {
total_amount: number; total_amount: number;
}; };
export type IssueInvoiceSummaryPageData = IssueInvoiceSummaryPage & { export type IssuedInvoicesummaryPageData = IssuedInvoicesummaryPage & {
items: IssueInvoiceSummary[]; items: IssuedInvoicesummary[];
}; };

View File

@ -0,0 +1,26 @@
import {
GetIssuedInvoiceByIdResponseSchema,
ListIssuedInvoicesResponseSchema,
} from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils";
import type { z } from "zod/v4";
// IssuedInvoices
export const IssuedInvoiceschema = GetIssuedInvoiceByIdResponseSchema.omit({
metadata: true,
});
export type IssueInvoice = z.infer<typeof IssuedInvoiceschema>;
export type IssueInvoiceRecipient = IssueInvoice["recipient"];
export type IssueInvoiceItem = ArrayElement<IssueInvoice["items"]>;
// Resultado de consulta con criteria (paginado, etc.)
export const IssuedInvoicesummaryPageSchema = ListIssuedInvoicesResponseSchema.omit({
metadata: true,
});
export type IssuedInvoicesummaryPage = z.infer<typeof IssuedInvoicesummaryPageSchema>;
export type IssuedInvoicesummary = Omit<
ArrayElement<IssuedInvoicesummaryPage["items"]>,
"metadata"
>;

View File

@ -5,7 +5,6 @@ import { useNavigate } from "react-router-dom";
import { usePinnedPreviewSheet } from "../../hooks"; import { usePinnedPreviewSheet } from "../../hooks";
import { useTranslation } from "../../i18n"; import { useTranslation } from "../../i18n";
import { useProformasGridColumns } from "../../proformas/pages/list/use-proformas-grid-columns";
import type { InvoiceSummaryFormData, InvoicesPageFormData } from "../../schemas"; import type { InvoiceSummaryFormData, InvoicesPageFormData } from "../../schemas";
export type InvoiceUpdateCompProps = { export type InvoiceUpdateCompProps = {

View File

@ -10,8 +10,6 @@ import { invoiceResumeDtoToFormAdapter } from "../../adapters/invoice-resume-dto
import { useInvoicesQuery } from "../../hooks"; import { useInvoicesQuery } from "../../hooks";
import { useTranslation } from "../../i18n"; import { useTranslation } from "../../i18n";
import { InvoicesListGrid } from "./invoices-list-grid";
export const InvoiceListPage = () => { export const InvoiceListPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@ -95,16 +93,7 @@ export const InvoiceListPage = () => {
<AppContent> <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"}> <div className={"flex-1"}>
<InvoicesListGrid <>hola</>
invoicesPage={invoicesPageData}
loading={isLoading}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
onSearchChange={handleSearchChange}
pageIndex={pageIndex}
pageSize={pageSize}
searchValue={search}
/>
</div> </div>
</div> </div>
</AppContent> </AppContent>

View File

@ -1 +1,2 @@
export * from "./use-proformas-grid-columns";
export * from "./use-proformas-list"; export * from "./use-proformas-list";

View File

@ -0,0 +1,443 @@
import { formatDate } from "@erp/core/client";
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
import {
Button,
ButtonGroup,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@repo/shadcn-ui/components";
import type { ColumnDef } from "@tanstack/react-table";
import {
CopyIcon,
DownloadIcon,
EditIcon,
MailIcon,
MoreVerticalIcon,
Trash2Icon,
} from "lucide-react";
import * as React from "react";
import { useTranslation } from "../../../../i18n";
import type { ProformaSummaryData } from "../../../schema";
import { ProformaStatusBadge } from "../ui";
type GridActionHandlers = {
onEdit?: (proforma: ProformaSummaryData) => void;
onDuplicate?: (proforma: ProformaSummaryData) => void;
onDownloadPdf?: (proforma: ProformaSummaryData) => void;
onSendEmail?: (proforma: ProformaSummaryData) => void;
onDelete?: (proforma: ProformaSummaryData) => void;
};
export function useProformasGridColumns(
actionHandlers: GridActionHandlers = {}
): ColumnDef<ProformaSummaryData, unknown>[] {
const { t } = useTranslation();
const { onEdit, onDuplicate, onDownloadPdf, onSendEmail, onDelete } = actionHandlers;
return React.useMemo<ColumnDef<ProformaSummaryData>[]>(
() => [
// Nº
{
accessorKey: "invoice_number",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.proformas.list.grid_columns.invoice_number")}
/>
),
cell: ({ row }) => (
<div className="font-semibold text-left text-primary">{row.original.invoice_number}</div>
),
enableHiding: false,
enableSorting: false,
size: 160,
minSize: 120,
meta: {
title: t("pages.proformas.list.grid_columns.invoice_number"),
},
},
// Estado
{
accessorKey: "status",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.proformas.list.grid_columns.status")}
/>
),
cell: ({ row }) => <ProformaStatusBadge status={row.original.status} />,
enableSorting: false,
size: 140,
minSize: 120,
meta: {
title: t("pages.proformas.list.grid_columns.status"),
},
},
{
id: "recipient",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.proformas.list.grid_columns.recipient")}
/>
),
accessorFn: (row) => row.recipient.name, // para ordenar/buscar por nombre
enableHiding: false,
size: 140,
minSize: 120,
cell: ({ row }) => {
const c = row.original.recipient;
return (
<div className="flex items-start gap-1 my-1.5">
<div className="min-w-0 grid gap-1">
<div className="flex flex-wrap items-center gap-2">
<span className="font-medium truncate text-primary">{c.name}</span>
</div>
<div className="flex flex-wrap items-center gap-2">
{c.tin && <span className="font-base truncate">{c.tin}</span>}
</div>
</div>
</div>
);
},
meta: {
title: t("pages.proformas.list.grid_columns.recipient"),
},
},
// Serie
{
accessorKey: "series",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.proformas.list.grid_columns.series")}
/>
),
cell: ({ row }) => <div className="font-normal text-left">{row.original.series}</div>,
enableSorting: false,
size: 120,
minSize: 100,
meta: {
title: t("pages.proformas.list.grid_columns.series"),
},
},
// Referencia
{
accessorKey: "reference",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.proformas.list.grid_columns.reference")}
/>
),
cell: ({ row }) => <div className="font-medium text-left">{row.original.reference}</div>,
enableSorting: false,
size: 120,
minSize: 100,
meta: {
title: t("pages.proformas.list.grid_columns.reference"),
},
},
// Fecha factura
{
accessorKey: "invoice_date",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left tabular-nums"
column={column}
title={t("pages.proformas.list.grid_columns.invoice_date")}
/>
),
cell: ({ row }) => (
<div className="font-medium text-left tabular-nums">
{formatDate(row.original.invoice_date)}
</div>
),
enableSorting: false,
size: 140,
minSize: 120,
meta: {
title: t("pages.proformas.list.grid_columns.invoice_date"),
},
},
// Fecha operación
{
accessorKey: "operation_date",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left tabular-nums"
column={column}
title={t("pages.proformas.list.grid_columns.operation_date")}
/>
),
cell: ({ row }) => (
<div className="font-medium text-left tabular-nums">
{formatDate(row.original.operation_date)}
</div>
),
enableSorting: false,
size: 140,
minSize: 120,
meta: {
title: t("pages.proformas.list.grid_columns.operation_date"),
},
},
// Subtotal amount
{
accessorKey: "subtotal_amount_fmt",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right tabular-nums"
column={column}
title={t("pages.proformas.list.grid_columns.subtotal_amount")}
/>
),
cell: ({ row }) => (
<div className="font-medium text-right tabular-nums">
{row.original.subtotal_amount_fmt}
</div>
),
enableSorting: false,
size: 120,
minSize: 100,
meta: {
title: t("pages.proformas.list.grid_columns.subtotal_amount"),
},
},
// Discount amount
{
accessorKey: "discount_amount_fmt",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right tabular-nums"
column={column}
title={t("pages.proformas.list.grid_columns.discount_amount")}
/>
),
cell: ({ row }) => (
<div className="font-medium text-right tabular-nums">
{row.original.discount_amount_fmt}
</div>
),
enableSorting: false,
size: 120,
minSize: 100,
meta: {
title: t("pages.proformas.list.grid_columns.discount_amount"),
},
},
// Taxes amount
{
accessorKey: "taxes_amount_fmt",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right tabular-nums"
column={column}
title={t("pages.proformas.list.grid_columns.taxes_amount")}
/>
),
cell: ({ row }) => (
<div className="font-medium text-right tabular-nums">{row.original.taxes_amount_fmt}</div>
),
enableSorting: false,
size: 120,
minSize: 100,
meta: {
title: t("pages.proformas.list.grid_columns.taxes_amount"),
},
},
// Total amount
{
accessorKey: "total_amount_fmt",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right tabular-nums"
column={column}
title={t("pages.proformas.list.grid_columns.total_amount")}
/>
),
cell: ({ row }) => (
<div className="font-semibold text-right tabular-nums">
{row.original.total_amount_fmt}
</div>
),
enableSorting: false,
size: 140,
minSize: 120,
meta: {
title: t("pages.proformas.list.grid_columns.total_amount"),
},
},
// ─────────────────────────────
// Acciones
// ─────────────────────────────
{
id: "actions",
header: () => <span className="sr-only">{t("common.actions")}</span>,
enableSorting: false,
enableHiding: false,
size: 110,
minSize: 96,
cell: ({ row }) => {
const proforma = row.original;
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
return (
<ButtonGroup>
{/* Editar (acción primaria) */}
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label={t("common.edit_row")}
className="cursor-pointer text-muted-foreground hover:text-primary"
onClick={(e) => {
e.stopPropagation();
onEdit?.(proforma);
}}
size="sm"
type="button"
variant="ghost"
>
<EditIcon aria-hidden="true" className="size-4 " />
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.edit_row")}</TooltipContent>
</Tooltip>
{/* Duplicar */}
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label={t("common.duplicate_row")}
className="cursor-pointer text-muted-foreground hover:text-primary"
onClick={(e) => {
e.stopPropagation();
onDuplicate?.(proforma);
}}
size="sm"
type="button"
variant="ghost"
>
<CopyIcon aria-hidden="true" className="size-4 " />
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.duplicate_row")}</TooltipContent>
</Tooltip>
{/* Descargar en PDF */}
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label={t("common.download_pdf")}
className="cursor-pointer text-muted-foreground hover:text-primary"
onClick={(e) => {
e.stopPropagation();
onDownloadPdf?.(proforma);
}}
size="icon-sm"
type="button"
variant="ghost"
>
<DownloadIcon aria-hidden="true" className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.download_pdf")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label={t("common.delete_row")}
className="cursor-pointer text-destructive hover:bg-destructive/90 hover:text-white"
onClick={() => onDelete?.(proforma)}
size="icon-sm"
type="button"
variant="ghost"
>
<Trash2Icon aria-hidden="true" className="size-4" />
<span className="sr-only">{t("common.delete_row")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.delete_row")}</TooltipContent>
</Tooltip>
{/* Menú demás acciones */}
{/** biome-ignore lint/suspicious/noSelfCompare: <Desactivado por ahora> */}
{false === false && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label={t("common.more_actions")}
className="cursor-pointer text-muted-foreground hover:text-primary"
onClick={stop}
size="sm"
type="button"
variant="ghost"
>
<MoreVerticalIcon aria-hidden="true" className="size-4" />
<span className="sr-only">{t("common.more_actions")}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem
className="cursor-pointer"
onClick={() => onDuplicate?.(proforma)}
>
<CopyIcon className="mr-2 size-4" />
{t("common.duplicate_row")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer"
onClick={() => onDownloadPdf?.(proforma)}
>
<DownloadIcon className="mr-2 size-4" />
{t("common.download_pdf")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => onSendEmail?.(proforma)}
>
<MailIcon className="mr-2 size-4" />
{t("common.send_email")}
</DropdownMenuItem>{" "}
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive-foreground focus:bg-destructive cursor-pointer"
onClick={() => onDelete?.(proforma)}
>
<Trash2Icon className="mr-2 size-4 text-destructive focus:text-destructive-foreground" />
{t("common.delete_row")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</ButtonGroup>
);
},
meta: {
title: t("common.actions"),
},
},
],
[t, onEdit, onDuplicate, onDownloadPdf, onSendEmail, onDelete]
);
}

View File

@ -11,19 +11,23 @@ export const useProformasList = () => {
const [pageIndex, setPageIndex] = useState(0); const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [status, setStatus] = useState("all");
const debouncedQ = useDebounce(search, 300); const debouncedQ = useDebounce(search, 300);
const criteria = useMemo<CriteriaDTO>( const criteria = useMemo<CriteriaDTO>(() => {
() => ({ const baseFilters =
status !== "all" ? [{ field: "status", operator: "CONTAINS", value: status }] : [];
return {
q: debouncedQ || "", q: debouncedQ || "",
pageSize, pageSize,
pageNumber: pageIndex, pageNumber: pageIndex,
order: "desc", order: "desc",
orderBy: "invoice_date", orderBy: "invoice_date",
}), filters: baseFilters,
[pageSize, pageIndex, debouncedQ] };
); }, [pageSize, pageIndex, debouncedQ, status]);
const query = useProformasQuery({ criteria }); const query = useProformasQuery({ criteria });
const data = useMemo( const data = useMemo(
@ -33,6 +37,8 @@ export const useProformasList = () => {
const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " ")); const setSearchValue = (value: string) => setSearch(value.trim().replace(/\s+/g, " "));
const setStatusFilter = (newStatus: string) => setStatus(newStatus);
return { return {
...query, ...query,
data, data,
@ -42,5 +48,6 @@ export const useProformasList = () => {
setPageIndex, setPageIndex,
setPageSize, setPageSize,
setSearchValue, setSearchValue,
setStatusFilter,
}; };
}; };

View File

@ -51,6 +51,7 @@ export const ProformaListPage = () => {
onPageChange={list.setPageIndex} onPageChange={list.setPageIndex}
onPageSizeChange={list.setPageSize} onPageSizeChange={list.setPageSize}
onSearchChange={list.setSearchValue} onSearchChange={list.setSearchValue}
onStatusFilterChange={list.setStatusFilter}
pageIndex={list.pageIndex} pageIndex={list.pageIndex}
pageSize={list.pageSize} pageSize={list.pageSize}
searchValue={list.search} searchValue={list.search}

View File

@ -1,114 +0,0 @@
import type { CriteriaDTO } from "@erp/core";
import { PageHeader } from "@erp/core/components";
import { ErrorAlert } from "@erp/customers/components";
import { AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components";
import { PlusIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../../i18n";
import { useProformasQuery } from "../../hooks";
import { ProformaSummaryDtoAdapter } from "../../../adapters/proforma-summary-dto.adapter";
import { ProformasGrid } from "./proformas-grid";
export const ProformaListPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [search, setSearch] = useState("");
const debouncedQ = useDebounce(search, 300);
const criteria = useMemo(
() =>
({
q: debouncedQ || "",
pageSize,
pageNumber: pageIndex,
order: "desc",
orderBy: "invoice_date",
}) as CriteriaDTO,
[pageSize, pageIndex, debouncedQ]
);
const { data, isLoading, isError, error } = useProformasQuery({
criteria,
});
const proformaPageData = useMemo(() => {
if (!data) return undefined;
return ProformaSummaryDtoAdapter.fromDto(data);
}, [data]);
const handlePageChange = (newPageIndex: number) => {
setPageIndex(newPageIndex);
};
const handlePageSizeChange = (newSize: number) => {
setPageSize(newSize);
setPageIndex(0);
};
const handleSearchChange = (value: string) => {
// Normalización ligera: recorta y colapsa espacios internos
const cleaned = value.trim().replace(/\s+/g, " ");
setSearch(cleaned);
setPageIndex(0);
};
if (isError || !proformaPageData) {
return (
<AppContent>
<ErrorAlert
message={(error as Error)?.message || "Error al cargar el listado"}
title={t("pages.proformas.list.loadErrorTitle")}
/>
<BackHistoryButton />
</AppContent>
);
}
return (
<>
<AppHeader>
<PageHeader
description={t("pages.proformas.list.description")}
rightSlot={
<div className="flex items-center space-x-2">
<Button
aria-label={t("pages.proformas.create.title")}
className="cursor-pointer"
onClick={() => navigate("/proformas/create")}
variant={"default"}
>
<PlusIcon aria-hidden className="mr-2 h-4 w-4" />
{t("pages.proformas.create.title")}
</Button>
</div>
}
title={t("pages.proformas.list.title")}
/>
</AppHeader>
<AppContent>
<div className="flex flex-col w-full h-full py-3">
<div className={"flex-1"}>
<ProformasGrid
data={proformaPageData}
loading={isLoading}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
onSearchChange={handleSearchChange}
pageIndex={pageIndex}
pageSize={pageSize}
searchValue={search}
/>
</div>
</div>
</AppContent>
</>
);
};

View File

@ -1,196 +0,0 @@
import { SimpleSearchInput } from "@erp/core/components";
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
import {
Button,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@repo/shadcn-ui/components";
import { FileDownIcon, FilterIcon } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { usePinnedPreviewSheet } from "../../../hooks";
import { useTranslation } from "../../../i18n";
import type { InvoiceSummaryFormData } from "../../../schemas";
import type { ProformaSummaryPageData } from "../../schema/proforma-summary.web.schema";
import { useProformasGridColumns } from "./use-proformas-grid-columns";
export type ProformaGridProps = {
data: ProformaSummaryPageData;
loading?: boolean;
pageIndex: number;
pageSize: number;
onPageChange?: (pageNumber: number) => void;
onPageSizeChange?: (pageSize: number) => void;
searchValue: string;
onSearchChange: (value: string) => void;
onRowClick?: (
row: ProformaSummaryPageData,
index: number,
event: React.MouseEvent<HTMLTableRowElement>
) => void;
};
// Create new GridExample component
export const ProformasGrid = ({
data,
loading,
pageIndex,
pageSize,
onPageChange,
onPageSizeChange,
searchValue,
onSearchChange,
onRowClick,
}: ProformaGridProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { items, total_items } = data;
// Hook con Sheet de shadcn
const preview = usePinnedPreviewSheet<InvoiceSummaryFormData>({
persistKey: "invoice-preview-pin",
widthClass: "w-[500px]",
});
const [statusFilter, setStatusFilter] = useState("todas");
const columns = useProformasGridColumns({
onEdit: (proforma) => navigate(`/proformas/${proforma.id}/edit`),
onDuplicate: (proforma) => null, //duplicateInvoice(inv.id),
onDownloadPdf: (proforma) => null, //downloadInvoicePdf(inv.id),
onSendEmail: (proforma) => null, //sendInvoiceEmail(inv.id),
onDelete: (proforma) => null, //confirmDelete(inv.id),
});
// Navegación accesible (click o teclado)
/*const goToRow = useCallback(
(id: string, newTab = false) => {
const url = `/customer-invoices/${id}/edit`;
newTab ? window.open(url, "_blank", "noopener,noreferrer") : navigate(url);
},
[navigate]
);*/
/*const onRowClicked = useCallback(
(e: RowClickedEvent<any>) => {
if (!e.data) return;
const newTab = e.event instanceof MouseEvent && (e.event.metaKey || e.event.ctrlKey);
goToRow(e.data.id, newTab);
},
[goToRow]
);
const onCellKeyDown = useCallback(
(e: CellKeyDownEvent<any>) => {
if (!e.data) return;
const ev = e.event;
if (!(ev && ev instanceof KeyboardEvent)) return;
const key = ev.key;
if (key === "Enter" || key === " ") {
ev.preventDefault();
goToRow(e.data.id);
}
if ((ev.ctrlKey || ev.metaKey) && key === "Enter") {
ev.preventDefault();
goToRow(e.data.id, true);
}
},
[goToRow]
);
const handleRowClick = useCallback(
(invoice: InvoiceSummaryFormData, _i: number, e: React.MouseEvent) => {
const url = `/customer-invoices/${invoice.id}/edit`;
if (e.metaKey || e.ctrlKey) {
window.open(url, "_blank", "noopener,noreferrer");
return;
}
preview.open(invoice);
},
[preview]
);*/
if (loading) {
return (
<div className="flex flex-col gap-4">
<SkeletonDataTable
columns={columns.length}
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
rows={Math.max(6, pageSize)}
showFooter
/>
</div>
);
}
// Render principal
return (
<div className="flex flex-col gap-4">
{/* Barra de filtros */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<SimpleSearchInput loading={loading} onSearchChange={onSearchChange} />
<Select onValueChange={setStatusFilter} value={statusFilter}>
<SelectTrigger className="w-full sm:w-48 bg-white border-gray-200 shadow-sm">
<FilterIcon className="mr-2 h-4 w-4" />
<SelectValue placeholder="Estado" />
</SelectTrigger>
<SelectContent>
<SelectItem value="todas">Todas</SelectItem>
<SelectItem value="pagada">Borradores</SelectItem>
<SelectItem value="pendiente">Enviadas</SelectItem>
<SelectItem value="vencida">Aprobadas</SelectItem>
<SelectItem value="vencida">Rechazadas</SelectItem>
<SelectItem value="vencida">Emitidas</SelectItem>
</SelectContent>
</Select>
<Button
className="border-blue-200 text-blue-600 hover:bg-blue-50 bg-transparent"
variant="outline"
>
<FileDownIcon className="mr-2 h-4 w-4" />
Exportar
</Button>
</div>
<div className="relative flex">
<div className={preview.isPinned ? "flex-1 mr-[500px]" : "flex-1"}>
<DataTable
columns={columns}
data={items}
enablePagination
enableRowSelection
manualPagination
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
//onRowClick={handleRowClick}
pageIndex={pageIndex}
pageSize={pageSize}
readOnly
totalItems={total_items}
/>
</div>
{/*<preview.Preview>
{({ item, isPinned, close, togglePin }) => (
<InvoicePreviewPanel
invoice={item}
isPinned={isPinned}
onClose={close}
onTogglePin={togglePin}
/>
)}
</preview.Preview>*/}
</div>
</div>
);
};

View File

@ -1 +1,2 @@
export * from "./proforma-status-badge";
export * from "./proformas-grid"; export * from "./proformas-grid";

View File

@ -0,0 +1,68 @@
import { Badge } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { forwardRef } from "react";
import { useTranslation } from "../../../../i18n";
export type ProformaStatus = "draft" | "sent" | "approved" | "rejected" | "issued";
export type ProformaStatusBadgeProps = {
status: string | ProformaStatus; // permitir cualquier valor
dotVisible?: boolean;
className?: string;
};
const statusColorConfig: Record<ProformaStatus, { badge: string; dot: string }> = {
draft: {
badge:
"bg-gray-500/10 dark:bg-gray-500/20 hover:bg-gray-500/10 text-gray-600 border-gray-400/60",
dot: "bg-gray-500",
},
sent: {
badge:
"bg-amber-500/10 dark:bg-amber-500/20 hover:bg-amber-500/10 text-amber-500 border-amber-600/60",
dot: "bg-amber-500",
},
approved: {
badge:
"bg-emerald-500/10 dark:bg-emerald-500/20 hover:bg-emerald-500/10 text-emerald-500 border-emerald-600/60",
dot: "bg-emerald-500",
},
rejected: {
badge: "bg-red-500/10 dark:bg-red-500/20 hover:bg-red-500/10 text-red-500 border-red-600/60",
dot: "bg-red-500",
},
issued: {
badge:
"bg-blue-600/10 dark:bg-blue-600/20 hover:bg-blue-600/10 text-blue-500 border-blue-600/60",
dot: "bg-blue-500",
},
};
export const ProformaStatusBadge = forwardRef<HTMLDivElement, ProformaStatusBadgeProps>(
({ status, dotVisible, className, ...props }, ref) => {
const { t } = useTranslation();
const normalizedStatus = status.toLowerCase() as ProformaStatus;
const config = statusColorConfig[normalizedStatus];
const commonClassName =
"transition-colors duration-200 cursor-pointer shadow-none rounded-full";
if (!config) {
return (
<Badge className={cn(commonClassName, className)} ref={ref} {...props}>
{status}
</Badge>
);
}
return (
<Badge className={cn(commonClassName, config.badge, className)} {...props}>
{dotVisible && <div className={cn("h-1.5 w-1.5 rounded-full mr-2", config.dot)} />}
{t(`catalog.proformas.status.${normalizedStatus}`, { defaultValue: status })}
</Badge>
);
}
);
ProformaStatusBadge.displayName = "ProformaStatusBadge";

View File

@ -1,18 +1,18 @@
import { SimpleSearchInput } from "@erp/core/components"; import { SimpleSearchInput } from "@erp/core/components";
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components"; import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
import { import {
Button,
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
import { FileDownIcon, FilterIcon } from "lucide-react"; import { FilterIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../i18n";
import type { ProformaSummaryPageData } from "../../../schema/proforma-summary.web.schema"; import type { ProformaSummaryPageData } from "../../../schema/proforma-summary.web.schema";
import { useProformasGridColumns } from "../use-proformas-grid-columns"; import { useProformasGridColumns } from "../hooks";
interface ProformasGridProps { interface ProformasGridProps {
data: ProformaSummaryPageData; data: ProformaSummaryPageData;
@ -25,6 +25,7 @@ interface ProformasGridProps {
onPageSizeChange: (s: number) => void; onPageSizeChange: (s: number) => void;
onRowClick?: (id: string) => void; onRowClick?: (id: string) => void;
onExportClick?: () => void; onExportClick?: () => void;
onStatusFilterChange?: (newStatus: string) => void;
} }
export const ProformasGrid = ({ export const ProformasGrid = ({
@ -38,10 +39,19 @@ export const ProformasGrid = ({
onPageSizeChange, onPageSizeChange,
onRowClick, onRowClick,
onExportClick, onExportClick,
onStatusFilterChange,
}: ProformasGridProps) => { }: ProformasGridProps) => {
const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const { items, total_items } = data; const { items, total_items } = data;
const columns = useProformasGridColumns();
const columns = useProformasGridColumns({
onEdit: (proforma) => navigate(`/proformas/${proforma.id}/edit`),
onDuplicate: (proforma) => null, //duplicateInvoice(inv.id),
onDownloadPdf: (proforma) => null, //downloadInvoicePdf(inv.id),
onSendEmail: (proforma) => null, //sendInvoiceEmail(inv.id),
onDelete: (proforma) => null, //confirmDelete(inv.id),
});
if (loading) if (loading)
return ( return (
@ -55,33 +65,37 @@ export const ProformasGrid = ({
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row gap-4 mb-6"> <div className="flex flex-col sm:flex-row gap-4">
<SimpleSearchInput onSearchChange={onSearchChange} value={searchValue} /> <SimpleSearchInput loading={loading} onSearchChange={onSearchChange} />
<Select defaultValue="all"> <Select defaultValue="all" onValueChange={onStatusFilterChange}>
<SelectTrigger className="w-full sm:w-48"> <SelectTrigger className="w-full sm:w-48">
<FilterIcon aria-hidden className="mr-2 size-4" /> <FilterIcon aria-hidden className="mr-2 size-4" />
<SelectValue placeholder={t("filters.status")} /> <SelectValue placeholder={t("filters.status")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">{t("filters.all")}</SelectItem> <SelectItem value="all">{t("catalog.proformas.status.all")}</SelectItem>
<SelectItem value="draft">{t("filters.draft")}</SelectItem> <SelectItem value="draft">{t("catalog.proformas.status.draft")}</SelectItem>
<SelectItem value="sent">{t("filters.sent")}</SelectItem> <SelectItem value="sent">{t("catalog.proformas.status.sent")}</SelectItem>
<SelectItem value="approved">{t("catalog.proformas.status.approved")}</SelectItem>
<SelectItem value="rejected">{t("catalog.proformas.status.rejected")}</SelectItem>
<SelectItem value="issued">{t("catalog.proformas.status.issued")}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button onClick={onExportClick} variant="outline">
<FileDownIcon aria-hidden className="mr-2 size-4" />
{t("actions.export")}
</Button>
</div> </div>
<DataTable <DataTable
columns={columns} columns={columns}
columnVisibility={{
subtotal_amount_fmt: false,
discount_amount_fmt: false,
taxes_amount_fmt: false,
}}
data={items} data={items}
enablePagination enablePagination
manualPagination manualPagination
onPageChange={onPageChange} onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange} onPageSizeChange={onPageSizeChange}
onRowClick={(_, row) => onRowClick?.(row.id)} onRowClick={(row, _index) => onRowClick?.(row.id)}
pageIndex={pageIndex} pageIndex={pageIndex}
pageSize={pageSize} pageSize={pageSize}
totalItems={total_items} totalItems={total_items}

View File

@ -1,314 +0,0 @@
import { formatDate } from "@erp/core/client";
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
import {
Avatar,
AvatarFallback,
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@repo/shadcn-ui/components";
import type { ColumnDef } from "@tanstack/react-table";
import {
Building2Icon,
CopyIcon,
DownloadIcon,
EditIcon,
MailIcon,
MoreVerticalIcon,
Trash2Icon,
User2Icon,
} from "lucide-react";
import * as React from "react";
import { useTranslation } from "../../../i18n";
import type { InvoiceSummaryFormData } from "../../../schemas";
import { CustomerInvoiceStatusBadge } from "../../../shared/ui/components";
type GridActionHandlers = {
onEdit?: (invoice: InvoiceSummaryFormData) => void;
onDuplicate?: (invoice: InvoiceSummaryFormData) => void;
onDownloadPdf?: (invoice: InvoiceSummaryFormData) => void;
onSendEmail?: (invoice: InvoiceSummaryFormData) => void;
onDelete?: (invoice: InvoiceSummaryFormData) => void;
};
function initials(name: string) {
const parts = name.trim().split(/\s+/).slice(0, 2);
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
}
const KindBadge = ({ isCompany }: { isCompany: boolean }) => (
<Badge className="gap-1 tracking-wide text-xs text-foreground/70" variant="outline">
{isCompany ? <Building2Icon className="size-3.5" /> : <User2Icon className="size-3.5" />}
{isCompany ? "Company" : "Person"}
</Badge>
);
const Soft = ({ children }: { children: React.ReactNode }) => (
<span className="text-muted-foreground">{children}</span>
);
export function useProformasGridColumns(
actionHandlers: GridActionHandlers = {}
): ColumnDef<InvoiceSummaryFormData>[] {
const { t } = useTranslation();
const { onEdit, onDuplicate, onDownloadPdf, onSendEmail, onDelete } = actionHandlers;
return React.useMemo<ColumnDef<InvoiceSummaryFormData>[]>(
() => [
// Nº
{
accessorKey: "invoice_number",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.proformas.list.grid_columns.invoice_number")}
/>
),
cell: ({ row }) => (
<div className="font-semibold text-left text-primary">{row.original.invoice_number}</div>
),
enableHiding: false,
enableSorting: false,
size: 160,
minSize: 120,
},
// Estado
{
accessorKey: "status",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.proformas.list.grid_columns.status")}
/>
),
cell: ({ row }) => <CustomerInvoiceStatusBadge status={row.original.status} />,
enableSorting: false,
size: 140,
minSize: 120,
},
{
id: "recipient",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.list.grid_columns.recipient")}
/>
),
accessorFn: (row) => row.recipient.name, // para ordenar/buscar por nombre
enableHiding: false,
size: 140,
minSize: 120,
cell: ({ row }) => {
const c = row.original.recipient;
const isCompany = String(c.is_company).toLowerCase() === "true";
return (
<div className="flex items-start gap-1 my-1.5">
<Avatar className="size-10 hidden">
<AvatarFallback aria-label={c.name}>{initials(c.name)}</AvatarFallback>
</Avatar>
<div className="min-w-0 grid gap-1">
<div className="flex flex-wrap items-center gap-2">
<span className="font-medium truncate text-primary">{c.name}</span>
{c.trade_name && <Soft>({c.trade_name})</Soft>}
</div>
<div className="flex flex-wrap items-center gap-2">
{c.tin && <span className="font-base truncate">{c.tin}</span>}
<KindBadge isCompany={isCompany} />
</div>
</div>
</div>
);
},
},
// Serie
{
accessorKey: "series",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.proformas.list.grid_columns.series")}
/>
),
cell: ({ row }) => <div className="font-normal text-left">{row.original.series}</div>,
enableSorting: false,
size: 120,
minSize: 100,
},
// Referencia
{
accessorKey: "reference",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.proformas.list.grid_columns.reference")}
/>
),
cell: ({ row }) => <div className="font-medium text-left">{row.original.reference}</div>,
enableSorting: false,
size: 120,
minSize: 100,
},
// Fecha factura
{
accessorKey: "invoice_date",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left tabular-nums"
column={column}
title={t("pages.proformas.list.grid_columns.invoice_date")}
/>
),
cell: ({ row }) => (
<div className="font-medium text-left tabular-nums">
{formatDate(row.original.invoice_date)}
</div>
),
enableSorting: false,
size: 140,
minSize: 120,
},
// Fecha operación
{
accessorKey: "operation_date",
header: ({ column }) => (
<DataTableColumnHeader
className="text-left tabular-nums"
column={column}
title={t("pages.proformas.list.grid_columns.operation_date")}
/>
),
cell: ({ row }) => (
<div className="font-medium text-left tabular-nums">
{formatDate(row.original.operation_date)}
</div>
),
enableSorting: false,
size: 140,
minSize: 120,
},
// Total
{
accessorKey: "total_amount_fmt",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right tabular-nums"
column={column}
title={t("pages.proformas.list.grid_columns.total_amount")}
/>
),
cell: ({ row }) => (
<div className="font-semibold text-right tabular-nums">
{row.original.total_amount_fmt}
</div>
),
enableSorting: false,
size: 140,
minSize: 120,
},
// ─────────────────────────────
// Acciones
// ─────────────────────────────
{
id: "actions",
header: () => <span className="sr-only">{t("common.actions")}</span>,
enableSorting: false,
enableHiding: false,
size: 110,
minSize: 96,
cell: ({ row }) => {
const invoice = row.original;
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
return (
<div className="flex items-center justify-end gap-1 pr-1">
{/* Editar (acción primaria) */}
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label={t("common.edit")}
className="cursor-pointer text-muted-foreground hover:text-primary"
onClick={(e) => {
e.stopPropagation();
onEdit?.(invoice);
}}
size="sm"
type="button"
variant="ghost"
>
<EditIcon aria-hidden="true" className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.edit")}</TooltipContent>
</Tooltip>
{/* Menú demás acciones */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label={t("common.more_actions")}
className="cursor-pointer text-muted-foreground hover:text-primary"
onClick={stop}
size="sm"
type="button"
variant="ghost"
>
<MoreVerticalIcon aria-hidden="true" className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem
className="cursor-pointer"
onClick={() => onDuplicate?.(invoice)}
>
<CopyIcon className="mr-2 size-4" />
{t("common.duplicate")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer"
onClick={() => onDownloadPdf?.(invoice)}
>
<DownloadIcon className="mr-2 size-4" />
{t("common.download_pdf")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => onSendEmail?.(invoice)}
>
<MailIcon className="mr-2 size-4" />
{t("common.send_email")}
</DropdownMenuItem>{" "}
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive-foreground focus:bg-destructive cursor-pointer"
onClick={() => onDelete?.(invoice)}
>
<Trash2Icon className="mr-2 size-4 text-destructive focus:text-destructive-foreground" />
{t("common.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
},
},
],
[t, onEdit, onDuplicate, onDownloadPdf, onSendEmail, onDelete]
);
}

View File

@ -13,7 +13,7 @@ export type ProformaSummaryData = ProformaSummary & {
taxable_amount_fmt: string; taxable_amount_fmt: string;
taxable_amount: number; taxable_amount: number;
taxes_amoun_fmt: string; taxes_amount_fmt: string;
taxes_amount: number; taxes_amount: number;
total_amount_fmt: string; total_amount_fmt: string;
@ -21,5 +21,5 @@ export type ProformaSummaryData = ProformaSummary & {
}; };
export type ProformaSummaryPageData = ProformaSummaryPage & { export type ProformaSummaryPageData = ProformaSummaryPage & {
items: ProformaSummary[]; items: ProformaSummaryData[];
}; };

View File

@ -1,7 +1,7 @@
import { import {
CreateProformaRequestSchema, CreateProformaRequestSchema,
GetProformaByIdResponseSchema, GetProformaByIdResponseSchema,
ListProformasResponseSchema, type ListProformasResponseDTO,
UpdateProformaByIdRequestSchema, UpdateProformaByIdRequestSchema,
} from "@erp/customer-invoices/common"; } from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils"; import type { ArrayElement } from "@repo/rdx-utils";
@ -23,9 +23,5 @@ export type CreateProformaInput = z.infer<typeof CreateProformaSchema>; // Cuerp
export type UpdateProformaInput = z.infer<typeof UpdateProformaSchema>; // Cuerpo para actualizar export type UpdateProformaInput = z.infer<typeof UpdateProformaSchema>; // Cuerpo para actualizar
// Resultado de consulta con criteria (paginado, etc.) // Resultado de consulta con criteria (paginado, etc.)
export const ProformaSummaryPageSchema = ListProformasResponseSchema.omit({ export type ProformaSummaryPage = Omit<ListProformasResponseDTO, "metadata">;
metadata: true,
});
export type ProformaSummaryPage = z.infer<typeof ProformaSummaryPageSchema>;
export type ProformaSummary = Omit<ArrayElement<ProformaSummaryPage["items"]>, "metadata">; export type ProformaSummary = Omit<ArrayElement<ProformaSummaryPage["items"]>, "metadata">;

View File

@ -1,18 +1,18 @@
import type { ArrayElement } from "@repo/rdx-utils"; import type { ArrayElement } from "@repo/rdx-utils";
import type { z } from "zod/v4"; import type { z } from "zod/v4";
import { GetIssueInvoiceByIdResponseSchema, ListIssueInvoicesResponseSchema } from "../../common"; import { GetIssuedInvoiceByIdResponseSchema, ListIssuedInvoicesResponseSchema } from "../../common";
export const IssueInvoiceSchema = GetIssueInvoiceByIdResponseSchema.omit({ export const IssuedInvoiceschema = GetIssuedInvoiceByIdResponseSchema.omit({
metadata: true, metadata: true,
}); });
export type IssueInvoice = z.infer<typeof IssueInvoiceSchema>; export type IssueInvoice = z.infer<typeof IssuedInvoiceschema>;
export type IssueInvoiceRecipient = IssueInvoice["recipient"]; export type IssueInvoiceRecipient = IssueInvoice["recipient"];
export type IssueInvoiceItem = ArrayElement<IssueInvoice["items"]>; export type IssueInvoiceItem = ArrayElement<IssueInvoice["items"]>;
// Resultado de consulta con criteria (paginado, etc.) // Resultado de consulta con criteria (paginado, etc.)
export const IssueInvoicesPageSchema = ListIssueInvoicesResponseSchema.omit({ export const IssuedInvoicesPageSchema = ListIssuedInvoicesResponseSchema.omit({
metadata: true, metadata: true,
}); });

View File

@ -1,67 +0,0 @@
import { Badge } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { forwardRef } from "react";
import { useTranslation } from "../../../i18n";
export type CustomerInvoiceStatus = "draft" | "sent" | "approved" | "rejected" | "issued";
export type CustomerInvoiceStatusBadgeProps = {
status: string | CustomerInvoiceStatus; // permitir cualquier valor
dotVisible?: boolean;
className?: string;
};
const statusColorConfig: Record<CustomerInvoiceStatus, { badge: string; dot: string }> = {
draft: {
badge:
"bg-gray-500/10 dark:bg-gray-500/20 hover:bg-gray-500/10 text-gray-600 border-gray-400/60",
dot: "bg-gray-500",
},
sent: {
badge:
"bg-amber-500/10 dark:bg-amber-500/20 hover:bg-amber-500/10 text-amber-500 border-amber-600/60",
dot: "bg-amber-500",
},
approved: {
badge:
"bg-emerald-500/10 dark:bg-emerald-500/20 hover:bg-emerald-500/10 text-emerald-500 border-emerald-600/60",
dot: "bg-emerald-500",
},
rejected: {
badge: "bg-red-500/10 dark:bg-red-500/20 hover:bg-red-500/10 text-red-500 border-red-600/60",
dot: "bg-red-500",
},
issued: {
badge:
"bg-blue-600/10 dark:bg-blue-600/20 hover:bg-blue-600/10 text-blue-500 border-blue-600/60",
dot: "bg-blue-500",
},
};
export const CustomerInvoiceStatusBadge = forwardRef<
HTMLDivElement,
CustomerInvoiceStatusBadgeProps
>(({ status, dotVisible, className, ...props }, ref) => {
const { t } = useTranslation();
const normalizedStatus = status.toLowerCase() as CustomerInvoiceStatus;
const config = statusColorConfig[normalizedStatus];
const commonClassName = "transition-colors duration-200 cursor-pointer shadow-none rounded-full";
if (!config) {
return (
<Badge className={cn(commonClassName, className)} ref={ref} {...props}>
{status}
</Badge>
);
}
return (
<Badge className={cn(commonClassName, config.badge, className)} {...props}>
{dotVisible && <div className={cn("h-1.5 w-1.5 rounded-full mr-2", config.dot)} />}
{t(`catalog.status.${normalizedStatus}`, { defaultValue: status })}
</Badge>
);
});
CustomerInvoiceStatusBadge.displayName = "CustomerInvoiceStatusBadge";

View File

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

View File

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