Formas de pago y vencimientos

This commit is contained in:
David Arranz 2026-05-26 13:55:26 +02:00
parent 475ab9fec3
commit 66783e1383
96 changed files with 3539 additions and 2749 deletions

View File

@ -7,7 +7,7 @@
"start": "NODE_ENV=production node --env-file=.env.production dist/index.js", "start": "NODE_ENV=production node --env-file=.env.production dist/index.js",
"dev": "node --import=tsx --watch src/index.ts", "dev": "node --import=tsx --watch src/index.ts",
"clean": "rimraf .turbo node_modules dist", "clean": "rimraf .turbo node_modules dist",
"typecheck": "tsc --noEmit", "typecheck": "tsc -p tsconfig.json --noEmit",
"check": "biome check .", "check": "biome check .",
"lint": "biome lint .", "lint": "biome lint .",
"format": "biome format --write" "format": "biome format --write"

View File

@ -4,6 +4,7 @@
"version": "0.6.7", "version": "0.6.7",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit",
"dev": "vite --host --clearScreen false", "dev": "vite --host --clearScreen false",
"build": "tsc && vite build", "build": "tsc && vite build",
"build:rodax": "tsc && vite build --mode rodax", "build:rodax": "tsc && vite build --mode rodax",
@ -24,13 +25,13 @@
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"date-fns": "^4.1.0",
"typescript": "~6.0.2", "typescript": "~6.0.2",
"vite": "^8.0.8" "vite": "^8.0.8"
}, },
"dependencies": { "dependencies": {
"@erp/auth": "workspace:*", "@erp/auth": "workspace:*",
"@erp/core": "workspace:*", "@erp/core": "workspace:*",
"@erp/catalogs": "workspace:*",
"@erp/customer-invoices": "workspace:*", "@erp/customer-invoices": "workspace:*",
"@erp/customers": "workspace:*", "@erp/customers": "workspace:*",
"@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/geist": "^5.2.8",
@ -41,6 +42,7 @@
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"@tanstack/react-query": "^5.98.0", "@tanstack/react-query": "^5.98.0",
"axios": "^1.15.0", "axios": "^1.15.0",
"date-fns": "^4.1.0",
"dinero.js": "1.9.1", "dinero.js": "1.9.1",
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"react": "^19.2.5", "react": "^19.2.5",

View File

@ -1,11 +1,13 @@
import { AuthModuleManifiest } from "@erp/auth/client"; import { AuthModuleManifest } from "@erp/auth/client";
import CoreModuleManifiest, { type IModuleClient } from "@erp/core/client"; import { CatalogsModuleManifest } from "@erp/catalogs/client";
import { CustomerInvoicesModuleManifiest } from "@erp/customer-invoices/client"; import CoreModuleManifest, { type IModuleClient } from "@erp/core/client";
import { CustomersModuleManifiest } from "@erp/customers/client"; import { CustomerInvoicesModuleManifest } from "@erp/customer-invoices/client";
import { CustomersModuleManifest } from "@erp/customers/client";
export const modules: IModuleClient[] = [ export const modules: IModuleClient[] = [
AuthModuleManifiest, AuthModuleManifest,
CoreModuleManifiest, CoreModuleManifest,
CustomersModuleManifiest, CatalogsModuleManifest,
CustomerInvoicesModuleManifiest, CustomersModuleManifest,
CustomerInvoicesModuleManifest,
]; ];

View File

@ -10,11 +10,6 @@
"ignoreUnknown": true, "ignoreUnknown": true,
"includes": [ "includes": [
"**", "**",
"!!**/supplier-invoices",
"!!**/suppliers",
"!!**/auth",
"!!**/rdx-criteria",
"!!**/shadcn-ui",
"!!**/node_modules", "!!**/node_modules",
"!!**/.next", "!!**/.next",
"!!**/dist", "!!**/dist",
@ -50,10 +45,7 @@
"noInferrableTypes": "error", "noInferrableTypes": "error",
"noNamespace": "error", "noNamespace": "error",
"noNegationElse": "warn", "noNegationElse": "warn",
"noNonNullAssertion": { "noNonNullAssertion": "info",
"level": "info",
"fix": "none"
},
"noParameterAssign": "error", "noParameterAssign": "error",
"noUnusedTemplateLiteral": "error", "noUnusedTemplateLiteral": "error",
"noUselessElse": "warn", "noUselessElse": "warn",

View File

@ -24,6 +24,7 @@
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"react": "^19.2.5", "react": "^19.2.5",
"react-dom": "^19.2.5", "react-dom": "^19.2.5",
"rimraf": "^6.1.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"dependencies": { "dependencies": {

View File

@ -1,13 +1,14 @@
import { NextFunction, Response } from "express"; import { logger } from "@erp/core/api";
import type { NextFunction, Response } from "express";
import passport from "passport"; import passport from "passport";
import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt"; import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt";
import { TabContext } from "../../../../../../apps/server/archive/contexts/auth/domain";
import { import type { TabContext } from "../../../../../../apps/server/archive/contexts/auth/domain";
import type {
IAuthService, IAuthService,
ITabContextService, ITabContextService,
} from "../../../../../../apps/server/archive/contexts/auth/domain/services"; } from "../../../../../../apps/server/archive/contexts/auth/domain/services";
import { TabContextRequest } from "../../../../../../apps/server/archive/contexts/auth/infraestructure/express/types"; import type { TabContextRequest } from "../../../../../../apps/server/archive/contexts/auth/infraestructure/express/types";
const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey"; const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey";
@ -48,7 +49,7 @@ export class PassportAuthProvider {
const checkUserId = user.id.equals(userIdVO.data); const checkUserId = user.id.equals(userIdVO.data);
const checkRoles = true; //user.hasRoles(roles); const checkRoles = true; //user.hasRoles(roles);
if (!checkUserId || !checkRoles) { if (!(checkUserId && checkRoles)) {
return Result.fail(new Error("Invalid token data")); return Result.fail(new Error("Invalid token data"));
} }

View File

@ -8,7 +8,7 @@ import { AuthRoutes } from "./auth-routes";
const MODULE_NAME = "auth"; const MODULE_NAME = "auth";
const MODULE_VERSION = "1.0.0"; const MODULE_VERSION = "1.0.0";
export const AuthModuleManifiest: IModuleClient = { export const AuthModuleManifest: IModuleClient = {
name: MODULE_NAME, name: MODULE_NAME,
version: MODULE_VERSION, version: MODULE_VERSION,
dependencies: ["core"], dependencies: ["core"],
@ -22,4 +22,4 @@ export const AuthModuleManifiest: IModuleClient = {
}, },
}; };
export default AuthModuleManifiest; export default AuthModuleManifest;

View File

@ -1,7 +1,7 @@
{ {
"name": "@erp/catalogs", "name": "@erp/catalogs",
"description": "Catalogs module", "description": "Catalogs module",
"version": "0.1.0", "version": "0.6.7",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
@ -12,21 +12,45 @@
"clean": "rimraf .turbo node_modules dist" "clean": "rimraf .turbo node_modules dist"
}, },
"exports": { "exports": {
".": "./src/api/index.ts", ".": "./src/common/index.ts",
"./api": "./src/api/index.ts" "./common": "./src/common/index.ts",
"./api": "./src/api/index.ts",
"./client": "./src/web/manifest.ts",
"./client/payment-methods": "./src/web/payment-methods/index.ts",
"./client/payment-terms": "./src/web/payment-terms/index.ts"
}, },
"dependencies": { "peerDependencies": {
"@erp/core": "workspace:*", "react": "^19.2.5",
"@erp/auth": "workspace:*", "react-dom": "^19.2.5"
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-utils": "workspace:*",
"@repo/rdx-criteria": "workspace:*",
"express": "^4.22.1",
"sequelize": "^6.37.8",
"zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-router-dom": "^5.3.3",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"rimraf": "^6.1.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
},
"dependencies": {
"@erp/auth": "workspace:*",
"@erp/core": "workspace:*",
"@repo/i18next": "workspace:*",
"@repo/rdx-criteria": "workspace:*",
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-logger": "workspace:*",
"@repo/rdx-ui": "workspace:*",
"@repo/rdx-utils": "workspace:*",
"@repo/shadcn-ui": "workspace:*",
"@tanstack/react-query": "^5.98.0",
"express": "^4.22.1",
"lucide-react": "^1.8.0",
"react-hook-form": "^7.72.1",
"react-i18next": "^17.0.2",
"react-router-dom": "^7.14.0",
"sequelize": "^6.37.8",
"use-debounce": "^10.1.1",
"zod": "^4.3.6"
} }
} }

View File

@ -1,4 +1,3 @@
export * from "./errors"; export * from "./errors";
export * from "./payment-method.aggregate"; export * from "./payment-method.aggregate";
export * from "./payment-method-name"; export * from "./payment-method-name";
export * from "./payment-method-type";

View File

@ -1,43 +0,0 @@
import { Result } from "@repo/rdx-utils";
import { InvalidPaymentMethodTypeError } from "./errors";
export const PAYMENT_METHOD_TYPES = [
"cash",
"bank_transfer",
"card",
"direct_debit",
"other",
] as const;
export type PaymentMethodTypeValue = (typeof PAYMENT_METHOD_TYPES)[number];
export class PaymentMethodType {
private constructor(private readonly value: PaymentMethodTypeValue) {}
public static create(type: string): Result<PaymentMethodType, Error> {
const normalized = String(type).trim() as PaymentMethodTypeValue;
if (!PAYMENT_METHOD_TYPES.includes(normalized)) {
return Result.fail(
new InvalidPaymentMethodTypeError(
`Payment method type must be one of: ${PAYMENT_METHOD_TYPES.join(", ")}`
)
);
}
return Result.ok(new PaymentMethodType(normalized));
}
public static fromPersistence(type: PaymentMethodTypeValue): PaymentMethodType {
return new PaymentMethodType(type);
}
public toString(): PaymentMethodTypeValue {
return this.value;
}
public toPrimitive(): PaymentMethodTypeValue {
return this.value;
}
}

View File

@ -214,15 +214,4 @@ export class PaymentTerm extends AggregateRoot<PaymentTermInternalProps> {
this.props.isActive = true; this.props.isActive = true;
return Result.ok(true); return Result.ok(true);
} }
private static sameDescription(current: Maybe<TextValue>, next: Maybe<TextValue>): boolean {
return current.match(
(currentValue: TextValue) =>
next.match(
(nextValue: TextValue) => currentValue.toPrimitive() === nextValue.toPrimitive(),
() => false
),
() => next.isNone()
);
}
} }

View File

@ -8,7 +8,7 @@ import {
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { PaymentMethodSummary } from "../../../../../application/payment-methods/models"; import type { PaymentMethodSummary } from "../../../../../application";
import type { PaymentMethodModel } from "../models"; import type { PaymentMethodModel } from "../models";
export class SequelizePaymentMethodSummaryMapper extends SequelizeQueryMapper< export class SequelizePaymentMethodSummaryMapper extends SequelizeQueryMapper<

View File

@ -1,6 +1,9 @@
import { z } from "zod/v4"; import { createPaginatedListSchema } from "@erp/core";
import type { z } from "zod/v4";
import { PaymentMethodSummarySchema } from "../shared"; import { PaymentMethodSummarySchema } from "../shared";
export const ListPaymentMethodsResponseSchema = z.array(PaymentMethodSummarySchema); export const ListPaymentMethodsResponseSchema = createPaginatedListSchema(
PaymentMethodSummarySchema
);
export type ListPaymentMethodsResponseDTO = z.infer<typeof ListPaymentMethodsResponseSchema>; export type ListPaymentMethodsResponseDTO = z.infer<typeof ListPaymentMethodsResponseSchema>;

View File

@ -1,6 +1,7 @@
import { z } from "zod/v4"; import { createPaginatedListSchema } from "@erp/core";
import type { z } from "zod/v4";
import { PaymentTermSummarySchema } from "../shared/payment-term-summary.dto"; import { PaymentTermSummarySchema } from "../shared/payment-term-summary.dto";
export const ListPaymentTermsResponseSchema = z.array(PaymentTermSummarySchema); export const ListPaymentTermsResponseSchema = createPaginatedListSchema(PaymentTermSummarySchema);
export type ListPaymentTermsResponseDTO = z.infer<typeof ListPaymentTermsResponseSchema>; export type ListPaymentTermsResponseDTO = z.infer<typeof ListPaymentTermsResponseSchema>;

View File

@ -0,0 +1,6 @@
export {
CatalogsModuleManifest,
default,
} from "./manifest";
export * from "./payment-methods";
export * from "./payment-terms";

View File

@ -0,0 +1,18 @@
import type { IModuleClient, ModuleClientParams } from "@erp/core/client";
export const MODULE_NAME = "Catalogs";
const MODULE_VERSION = "1.0.0";
export const CatalogsModuleManifest: IModuleClient = {
name: MODULE_NAME,
version: MODULE_VERSION,
dependencies: ["auth", "Core"],
protected: true,
layout: "app",
routes: (params: ModuleClientParams) => {
return [];
},
};
export default CatalogsModuleManifest;

View File

@ -0,0 +1,2 @@
export * from "./shared";
export * from "./utils";

View File

@ -0,0 +1 @@
export * from './list-payment-methods.adapter';

View File

@ -0,0 +1,53 @@
import type { ListPaymentMethodsResponseDTO } from "../../../../common";
import type { ListPaymentMethodsResult } from "../api";
import type { PaymentMethodList, PaymentMethodListRow } from "../entities";
/**
* Adaptador para transformar los datos de la API de ListPaymentMethodsResult
* a la entidad PaymentMethodList utilizada en la aplicación.
* Reglas de adaptación:
* - page, per_page, total_pages, total_items se asignan directamente.
* - items se transforma utilizando PaymentMethodListRowAdapter para cada elemento.
*
* @param pageDto - lista de proformas desde la API.
* @param context - Contexto adicional opcional para la adaptación.
* @returns {PaymentMethodList} Objeto adaptado a PaymentMehodList.
*/
export const ListPaymentMethodsAdapter = {
fromDto(dto: ListPaymentMethodsResult, context?: unknown): PaymentMethodList {
return {
page: dto.page,
perPage: dto.per_page,
totalPages: dto.total_pages,
totalItems: dto.total_items,
items: dto.items.map((rowDto) => PaymentMehodListRowAdapter.fromDto(rowDto, context)),
};
},
};
/**
* Adaptador para transformar los items de la API de ListPaymentMehodsResult a la entidad PaymentMehodListRow.
* Reglas de adaptación:
* - id, company_id se asignan directamente.
*
* @param rowDto - item de proforma desde la API.
* @param context - Contexto adicional opcional para la adaptación.
* @returns {PaymentMethodListRow} Objeto adaptado a PaymentMehodListRow.
*/
type ListPaymentMethodsItemOutput = ListPaymentMethodsResponseDTO["items"][number];
const PaymentMehodListRowAdapter = {
fromDto(dto: ListPaymentMethodsItemOutput, context?: unknown): PaymentMethodListRow {
return {
id: dto.id,
companyId: dto.company_id,
name: dto.name,
isSystem: dto.is_system,
isActive: dto.is_active,
};
},
};

View File

@ -0,0 +1 @@
export * from "./list-payment-methods-by-criteria.api";

View File

@ -0,0 +1,38 @@
import type { CriteriaDTO } from "@erp/core";
import type { IDataSource } from "@erp/core/client";
import type { ListPaymentMethodsResponseDTO } from "../../../../common";
/**
* Recupera una lista de métodos de pago del sistema utilizando la
* fuente de datos proporcionada y los criterios de búsqueda especificados.
*
* @param dataSource - La fuente de datos para interactuar con la API.
* @param params - Los parámetros necesarios para listar los métodos de pago, incluyendo los criterios de búsqueda.
* @returns Una promesa que resuelve con una lista de métodos de pago que cumplen con los criterios especificados.
* @throws Error si la recuperación de la lista de métodos de pago falla.
*/
export type ListPaymentMethodsByCriteriaParams = {
criteria?: CriteriaDTO;
signal?: AbortSignal;
};
export type ListPaymentMethodsResult = ListPaymentMethodsResponseDTO;
export function getListPaymentMethodsByCriteria(
dataSource: IDataSource,
params: ListPaymentMethodsByCriteriaParams
): Promise<ListPaymentMethodsResult> {
const { criteria, signal } = params || {
criteria: {
page: 1,
per_page: 9999,
},
signal: undefined,
};
return dataSource.getList<ListPaymentMethodsResponseDTO>("catalogs/payment-methods", {
signal,
...criteria,
});
}

View File

@ -0,0 +1,3 @@
export * from "./payment-method.entity";
export * from "./payment-method-list.entity";
export * from "./payment-method-list-row.entity";

View File

@ -0,0 +1,16 @@
/**
* Interface que representa una fila de la lista de
* formas de pago en el sistema, adaptada desde la respuesta de la API.
* Contiene los campos justos para mostrar
* la información básica de cada forma de pago en la lista.
*/
export interface PaymentMethodListRow {
id: string;
companyId: string;
name: string;
isSystem: boolean;
isActive: boolean;
}

View File

@ -0,0 +1,14 @@
import type { PaymentMethodListRow } from "./payment-method-list-row.entity";
/**
* Interface que representa la respuesta paginada de una lista de formas de pago,
* adaptada desde la respuesta de la API.
*/
export interface PaymentMethodList {
items: PaymentMethodListRow[];
totalPages: number;
totalItems: number;
page: number;
perPage: number;
}

View File

@ -0,0 +1,10 @@
export interface PaymentMethod {
id: string;
companyId: string;
name: string;
description: string | null;
isSystem: boolean;
isActive: boolean;
}

View File

@ -0,0 +1 @@
export * from "./use-payment-method-list-query";

View File

@ -0,0 +1,44 @@
import type { QueryKey } from "@tanstack/react-query";
import type { ListPaymentMethodsRequestDTO } from "../../../../common";
/**
* Prefijo base para listados
*/
export const LIST_PAYMENT_METHODS_QUERY_KEY_PREFIX = ["payment-methods"] as const;
/**
* Query key para listado de payment methods
*/
export const LIST_PAYMENT_METHODS_QUERY_KEY = (criteria?: ListPaymentMethodsRequestDTO): QueryKey =>
[
...LIST_PAYMENT_METHODS_QUERY_KEY_PREFIX,
{
pageNumber: criteria?.pageNumber ?? 1,
pageSize: criteria?.pageSize ?? 5,
q: criteria?.q ?? "",
filters: criteria?.filters ?? [],
orderBy: criteria?.orderBy ?? "",
order: criteria?.order ?? "",
},
] as const;
/**
* Query key para detalle de payment method
*/
export const PAYMENT_METHODS_DETAIL_QUERY_KEY_PREFIX = ["payment-methods:detail"] as const;
export const PAYMENT_METHOD_QUERY_KEY = (paymentMethodId?: string): QueryKey => [
...PAYMENT_METHODS_DETAIL_QUERY_KEY_PREFIX,
{ paymentMethodId },
];
/**
* Keys para mutaciones
*/
export const CREATE_PAYMENT_METHOD_MUTATION_KEY = ["payment-methods:create"] as const;
export const UPDATE_PAYMENT_METHOD_MUTATION_KEY = ["payment-methods:update"] as const;
export const DELETE_PAYMENT_METHOD_MUTATION_KEY = ["payment-methods:delete"] as const;
/**
* Operaciones de dominio
*/

View File

@ -0,0 +1,32 @@
import type { CriteriaDTO } from "@erp/core";
import { useDataSource } from "@erp/core/hooks";
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
import { ListPaymentMethodsAdapter } from "../adapters";
import { getListPaymentMethodsByCriteria } from "../api";
import type { PaymentMethodList } from "../entities";
import { LIST_PAYMENT_METHODS_QUERY_KEY } from "./keys";
export interface PaymentMethodsListQueryOptions {
enabled?: boolean;
criteria?: Partial<CriteriaDTO>;
}
export const usePaymentMethodsListQuery = (
options?: PaymentMethodsListQueryOptions
): UseQueryResult<PaymentMethodList, DefaultError> => {
const dataSource = useDataSource();
const enabled = options?.enabled ?? true;
const criteria = options?.criteria ?? {};
return useQuery<PaymentMethodList, DefaultError>({
queryKey: LIST_PAYMENT_METHODS_QUERY_KEY(criteria),
queryFn: async ({ signal }) => {
const dto = await getListPaymentMethodsByCriteria(dataSource, { signal, criteria });
return ListPaymentMethodsAdapter.fromDto(dto);
},
enabled,
placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria
});
};

View File

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

View File

@ -0,0 +1 @@
export * from './payment-method-options.utils';

View File

@ -0,0 +1,12 @@
import type { SelectFieldItem } from "@repo/rdx-ui/components";
import type { PaymentMethodListRow } from "../shared";
export const getPaymentMethodOptions = (
paymentMethods: PaymentMethodListRow[]
): SelectFieldItem[] => {
return paymentMethods.map((paymentMethod) => ({
value: paymentMethod.id,
label: paymentMethod.name,
}));
};

View File

@ -0,0 +1,2 @@
export * from "./shared";
export * from "./utils";

View File

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

View File

@ -0,0 +1,68 @@
import type { ListPaymentTermsResponseDTO } from "../../../../common";
import type { ListPaymentTermsResult } from "../api";
import type { PaymentTermList, PaymentTermListRow } from "../entities";
/**
* Adaptador para transformar los datos de la API de ListPaymentTermsResult
* a la entidad PaymentTermList utilizada en la aplicación.
* Reglas de adaptación:
* - page, per_page, total_pages, total_items se asignan directamente.
* - items se transforma utilizando PaymentTermListRowAdapter para cada elemento.
*
* @param pageDto - lista de proformas desde la API.
* @param context - Contexto adicional opcional para la adaptación.
* @returns {PaymentTermList} Objeto adaptado a PaymentMehodList.
*/
export const ListPaymentTermsAdapter = {
fromDto(dto: ListPaymentTermsResult, _context?: unknown): PaymentTermList {
return {
page: dto.page,
perPage: dto.per_page,
totalPages: dto.total_pages,
totalItems: dto.total_items,
items: dto.items.map((rowDto) => PaymentTermListRowAdapter.fromDto(rowDto, _context)),
};
},
};
/**
* Adaptador para transformar los items de la API de ListPaymentTermsResult a la entidad PaymentTermListRow.
* Reglas de adaptación:
* - id, company_id, se asignan directamente.
*
* @param rowDto - item de proforma desde la API.
* @param context - Contexto adicional opcional para la adaptación.
* @returns {PaymentTermListRow} Objeto adaptado a PaymentMehodListRow.
*/
type ListPaymentTermsItemOutput = ListPaymentTermsResponseDTO["items"][number];
const PaymentTermListRowAdapter = {
fromDto(dto: ListPaymentTermsItemOutput, _context?: unknown): PaymentTermListRow {
return {
id: dto.id,
companyId: dto.company_id,
name: dto.name,
isSystem: dto.is_system,
isActive: dto.is_active,
//dueRules: dto.due_rules.map(PaymentTermDueRuleAdapter.fromDto),
};
},
};
/** */
/*
const PaymentTermDueRuleAdapter = {
fromDto(dto: PaymentTermDueRuleDTO): PaymentTermDueRule {
return {
dueDays: Number(dto.due_days),
percentage: percentageDtoToNumber(dto.percentage),
};
},
};
const percentageDtoToNumber = (dto: { value: string; scale: string }): number => {
return Number(dto.value) / 10 ** Number(dto.scale);
};
*/

View File

@ -0,0 +1 @@
export * from "./list-payment-terms-by-criteria.api";

View File

@ -0,0 +1,38 @@
import type { CriteriaDTO } from "@erp/core";
import type { IDataSource } from "@erp/core/client";
import type { ListPaymentTermsResponseDTO } from "../../../../common";
/**
* Recupera una lista de vencimientos del sistema utilizando la
* fuente de datos proporcionada y los criterios de búsqueda especificados.
*
* @param dataSource - La fuente de datos para interactuar con la API.
* @param params - Los parámetros necesarios para listar los vencimientos, incluyendo los criterios de búsqueda.
* @returns Una promesa que resuelve con una lista de vencimientos que cumplen con los criterios especificados.
* @throws Error si la recuperación de la lista de vencimientos falla.
*/
export type ListPaymentTermsByCriteriaParams = {
criteria?: CriteriaDTO;
signal?: AbortSignal;
};
export type ListPaymentTermsResult = ListPaymentTermsResponseDTO;
export function getListPaymentTermsByCriteria(
dataSource: IDataSource,
params: ListPaymentTermsByCriteriaParams
): Promise<ListPaymentTermsResult> {
const { criteria, signal } = params || {
criteria: {
page: 1,
per_page: 9999,
},
signal: undefined,
};
return dataSource.getList<ListPaymentTermsResponseDTO>("catalogs/payment-terms", {
signal,
...criteria,
});
}

View File

@ -0,0 +1,3 @@
export * from "./payment-term.entity";
export * from "./payment-term-list.entity";
export * from "./payment-term-list-row.entity";

View File

@ -0,0 +1,16 @@
/**
* Interface que representa una fila de la lista de
* vencimientos en el sistema, adaptada desde la respuesta de la API.
* Contiene los campos justos para mostrar
* la información básica de cada vencimiento en la lista.
*/
export interface PaymentTermListRow {
id: string;
companyId: string;
name: string;
isSystem: boolean;
isActive: boolean;
}

View File

@ -0,0 +1,14 @@
import type { PaymentTermListRow } from "./payment-term-list-row.entity";
/**
* Interface que representa la respuesta paginada de una lista de vencimientos,
* adaptada desde la respuesta de la API.
*/
export interface PaymentTermList {
items: PaymentTermListRow[];
totalPages: number;
totalItems: number;
page: number;
perPage: number;
}

View File

@ -0,0 +1,16 @@
export interface PaymentTerm {
id: string;
companyId: string;
name: string;
description: string | null;
isSystem: boolean;
isActive: boolean;
dueRules: PaymentTermDueRule[];
}
export interface PaymentTermDueRule {
dueDays: number;
percentage: number;
}

View File

@ -0,0 +1 @@
export * from "./use-payment-term-list-query";

View File

@ -0,0 +1,44 @@
import type { QueryKey } from "@tanstack/react-query";
import type { ListPaymentTermsRequestDTO } from "../../../../common";
/**
* Prefijo base para listados
*/
export const LIST_PAYMENT_TERMS_QUERY_KEY_PREFIX = ["payment-terms"] as const;
/**
* Query key para listado de payment terms
*/
export const LIST_PAYMENT_TERMS_QUERY_KEY = (criteria?: ListPaymentTermsRequestDTO): QueryKey =>
[
...LIST_PAYMENT_TERMS_QUERY_KEY_PREFIX,
{
pageNumber: criteria?.pageNumber ?? 1,
pageSize: criteria?.pageSize ?? 5,
q: criteria?.q ?? "",
filters: criteria?.filters ?? [],
orderBy: criteria?.orderBy ?? "",
order: criteria?.order ?? "",
},
] as const;
/**
* Query key para detalle de payment term
*/
export const PAYMENT_TERMS_DETAIL_QUERY_KEY_PREFIX = ["payment-terms:detail"] as const;
export const PAYMENT_TERM_QUERY_KEY = (paymentTermId?: string): QueryKey => [
...PAYMENT_TERMS_DETAIL_QUERY_KEY_PREFIX,
{ paymentTermId },
];
/**
* Keys para mutaciones
*/
export const CREATE_PAYMENT_TERM_MUTATION_KEY = ["payment-terms:create"] as const;
export const UPDATE_PAYMENT_TERM_MUTATION_KEY = ["payment-terms:update"] as const;
export const DELETE_PAYMENT_TERM_MUTATION_KEY = ["payment-terms:delete"] as const;
/**
* Operaciones de dominio
*/

View File

@ -0,0 +1,32 @@
import type { CriteriaDTO } from "@erp/core";
import { useDataSource } from "@erp/core/hooks";
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
import { ListPaymentTermsAdapter } from "../adapters";
import { getListPaymentTermsByCriteria } from "../api";
import type { PaymentTermList } from "../entities";
import { LIST_PAYMENT_TERMS_QUERY_KEY } from "./keys";
export interface PaymentTermsListQueryOptions {
enabled?: boolean;
criteria?: Partial<CriteriaDTO>;
}
export const usePaymentTermsListQuery = (
options?: PaymentTermsListQueryOptions
): UseQueryResult<PaymentTermList, DefaultError> => {
const dataSource = useDataSource();
const enabled = options?.enabled ?? true;
const criteria = options?.criteria ?? {};
return useQuery<PaymentTermList, DefaultError>({
queryKey: LIST_PAYMENT_TERMS_QUERY_KEY(criteria),
queryFn: async ({ signal }) => {
const dto = await getListPaymentTermsByCriteria(dataSource, { signal, criteria });
return ListPaymentTermsAdapter.fromDto(dto);
},
enabled,
placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria
});
};

View File

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

View File

@ -0,0 +1 @@
export * from './payment-term-options.utils';

View File

@ -0,0 +1,10 @@
import type { SelectFieldItem } from "@repo/rdx-ui/components";
import type { PaymentTermListRow } from "../shared/entities";
export const getPaymentTermOptions = (paymentTerms: PaymentTermListRow[]): SelectFieldItem[] => {
return paymentTerms.map((paymentTerm) => ({
value: paymentTerm.id,
label: paymentTerm.name,
}));
};

View File

@ -10,11 +10,16 @@
"lib": ["ES2022"], "lib": ["ES2022"],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"isolatedModules": true, "isolatedModules": true,
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,

View File

@ -31,6 +31,7 @@
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"react": "^19.2.5", "react": "^19.2.5",
"react-dom": "^19.2.5", "react-dom": "^19.2.5",
"rimraf": "^6.1.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"dependencies": { "dependencies": {

View File

@ -3,7 +3,7 @@ import type { IModuleClient, ModuleClientParams } from "./lib";
export const MODULE_NAME = "Core"; export const MODULE_NAME = "Core";
const MODULE_VERSION = "1.0.0"; const MODULE_VERSION = "1.0.0";
export const CoreModuleManifiest: IModuleClient = { export const CoreModuleManifest: IModuleClient = {
name: MODULE_NAME, name: MODULE_NAME,
version: MODULE_VERSION, version: MODULE_VERSION,
dependencies: [], dependencies: [],
@ -15,6 +15,6 @@ export const CoreModuleManifiest: IModuleClient = {
}, },
}; };
export default CoreModuleManifiest; export default CoreModuleManifest;
export * from "./lib"; export * from "./lib";

View File

@ -29,8 +29,10 @@
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"date-fns": "^4.1.0",
"react": "^19.2.5", "react": "^19.2.5",
"react-dom": "^19.2.5", "react-dom": "^19.2.5",
"rimraf": "^6.1.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"dependencies": { "dependencies": {
@ -39,6 +41,7 @@
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@erp/auth": "workspace:*", "@erp/auth": "workspace:*",
"@erp/core": "workspace:*", "@erp/core": "workspace:*",
"@erp/catalogs": "workspace:*",
"@erp/customers": "workspace:*", "@erp/customers": "workspace:*",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@lglab/react-qr-code": "^1.4.10", "@lglab/react-qr-code": "^1.4.10",
@ -52,7 +55,6 @@
"@tanstack/react-query": "^5.98.0", "@tanstack/react-query": "^5.98.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"date-fns": "^4.1.0",
"dinero.js": "1.9.1", "dinero.js": "1.9.1",
"express": "^4.22.1", "express": "^4.22.1",
"libphonenumber-js": "^1.12.41", "libphonenumber-js": "^1.12.41",

View File

@ -46,6 +46,7 @@ export const CreateProformaRequestSchema = z.object({
global_discount_percentage: PercentageSchema, global_discount_percentage: PercentageSchema,
payment_method_id: z.uuid().nullable().optional(), payment_method_id: z.uuid().nullable().optional(),
payment_term_id: z.uuid().nullable().optional(),
items: z.array(CreateProformaItemRequestSchema), items: z.array(CreateProformaItemRequestSchema),
}); });

View File

@ -52,6 +52,7 @@ export const UpdateProformaByIdRequestSchema = z.object({
global_discount_percentage: PercentageSchema.optional(), global_discount_percentage: PercentageSchema.optional(),
payment_method_id: z.uuid().nullable().optional(), payment_method_id: z.uuid().nullable().optional(),
payment_term_id: z.uuid().nullable().optional(),
// retención como código??? retencion_15 // retención como código??? retencion_15

View File

@ -8,7 +8,7 @@ import {
} from "@erp/core"; } from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { PaymentMethodRefSchema, TaxesBreakdownSchema } from "../../shared"; import { PaymentMethodRefSchema, PaymentTermRefSchema, TaxesBreakdownSchema } from "../../shared";
import { import {
ProformaItemDetailSchema, ProformaItemDetailSchema,
ProformaRecipientSummarySchema, ProformaRecipientSummarySchema,
@ -41,6 +41,7 @@ export const GetProformaByIdResponseSchema = z.object({
taxes: z.array(TaxesBreakdownSchema), taxes: z.array(TaxesBreakdownSchema),
payment_method: PaymentMethodRefSchema.nullable(), payment_method: PaymentMethodRefSchema.nullable(),
payment_term: PaymentTermRefSchema.nullable(),
subtotal_amount: MoneySchema, subtotal_amount: MoneySchema,
items_discount_amount: MoneySchema, items_discount_amount: MoneySchema,

View File

@ -4,5 +4,4 @@ import type { z } from "zod/v4";
import { ProformaSummarySchema } from "../../shared/proforma"; import { ProformaSummarySchema } from "../../shared/proforma";
export const ListProformasResponseSchema = createPaginatedListSchema(ProformaSummarySchema); export const ListProformasResponseSchema = createPaginatedListSchema(ProformaSummarySchema);
export type ListProformasResponseDTO = z.infer<typeof ListProformasResponseSchema>; export type ListProformasResponseDTO = z.infer<typeof ListProformasResponseSchema>;

View File

@ -1,6 +1,7 @@
export * from "./issued-invoices"; export * from "./issued-invoices";
export * from "./item-position.dto"; export * from "./item-position.dto";
export * from "./payment-method-ref.dto"; export * from "./payment-method-ref.dto";
export * from "./payment-term-ref.dto";
export * from "./proforma"; export * from "./proforma";
export * from "./tax-combination-code.dto"; export * from "./tax-combination-code.dto";
export * from "./taxes-breakdown.dto"; export * from "./taxes-breakdown.dto";

View File

@ -0,0 +1,8 @@
import { z } from "zod/v4";
export const PaymentTermRefSchema = z.object({
id: z.uuid(),
description: z.string(),
});
export type PaymentTermRefDTO = z.infer<typeof PaymentTermRefSchema>;

View File

@ -5,10 +5,10 @@ import { CustomerInvoiceRoutes } from "./customer-invoice-routes";
export const MODULE_NAME = "CustomerInvoices"; export const MODULE_NAME = "CustomerInvoices";
const MODULE_VERSION = "1.0.0"; const MODULE_VERSION = "1.0.0";
export const CustomerInvoicesModuleManifiest: IModuleClient = { export const CustomerInvoicesModuleManifest: IModuleClient = {
name: MODULE_NAME, name: MODULE_NAME,
version: MODULE_VERSION, version: MODULE_VERSION,
dependencies: ["auth", "Core", "Customers"], dependencies: ["auth", "Core", "Catalogs", "Customers"],
protected: true, protected: true,
layout: "app", layout: "app",
@ -17,4 +17,4 @@ export const CustomerInvoicesModuleManifiest: IModuleClient = {
}, },
}; };
export default CustomerInvoicesModuleManifiest; export default CustomerInvoicesModuleManifest;

View File

@ -0,0 +1,27 @@
export interface CalculateProformaDueDatesParams {
issueDate: string | null;
total: number;
dueRules: PaymentTermDueRule[];
}
export interface ProformaDueDatePreview {
dueDate: string | null;
dueDays: number;
percentage: number;
amount: number;
}
export const calculateProformaDueDates = ({
issueDate,
total,
dueRules,
}: CalculateProformaDueDatesParams): ProformaDueDatePreview[] => {
return dueRules.map((rule) => {
return {
dueDays: rule.dueDays,
percentage: rule.percentage,
dueDate: issueDate ? addDaysToDateOnly(issueDate, rule.dueDays) : null,
amount: roundMoney(total * (rule.percentage / 100)),
};
});
};

View File

@ -1 +1,2 @@
export * from "./calculate-proforma-due-dates";
export * from "./proforma-fiscal-options.utils"; export * from "./proforma-fiscal-options.utils";

View File

@ -1,5 +1,6 @@
export * from "./use-update-proforma-controller"; export * from "./use-update-proforma-controller";
export * from "./use-update-proforma-items-controller"; export * from "./use-update-proforma-items-controller";
export * from "./use-update-proforma-page-controller"; export * from "./use-update-proforma-page-controller";
export * from "./use-update-proforma-payment-controller";
export * from "./use-update-proforma-tax-controller"; export * from "./use-update-proforma-tax-controller";
export * from "./use-update-proforma-totals-controller"; export * from "./use-update-proforma-totals-controller";

View File

@ -25,6 +25,7 @@ import {
} from "../utils"; } from "../utils";
import { useUpdateProformaItemsController } from "./use-update-proforma-items-controller"; import { useUpdateProformaItemsController } from "./use-update-proforma-items-controller";
import { useUpdateProformaPaymentController } from "./use-update-proforma-payment-controller";
import { useUpdateProformaTaxController } from "./use-update-proforma-tax-controller"; import { useUpdateProformaTaxController } from "./use-update-proforma-tax-controller";
import { useUpdateProformaTotalsController } from "./use-update-proforma-totals-controller"; import { useUpdateProformaTotalsController } from "./use-update-proforma-totals-controller";
@ -251,6 +252,8 @@ export const useUpdateProformaController = (
form, form,
}); });
const paymentCtrl = useUpdateProformaPaymentController();
return { return {
// form // form
formId, formId,
@ -259,6 +262,7 @@ export const useUpdateProformaController = (
itemsCtrl, itemsCtrl,
taxCtrl, taxCtrl,
totalsCtrl, totalsCtrl,
paymentCtrl,
// //
currencyCode, currencyCode,

View File

@ -0,0 +1,40 @@
import {
getPaymentMethodOptions,
usePaymentMethodsListQuery,
} from "@erp/catalogs/client/payment-methods";
import {
getPaymentTermOptions,
usePaymentTermsListQuery,
} from "@erp/catalogs/client/payment-terms";
import { useMemo } from "react";
export const useUpdateProformaPaymentController = () => {
const paymentMethodsQuery = usePaymentMethodsListQuery();
const paymentTermsQuery = usePaymentTermsListQuery();
const paymentMethodOptions = useMemo(() => {
return getPaymentMethodOptions(paymentMethodsQuery.data?.items ?? []);
}, [paymentMethodsQuery.data?.items]);
const paymentTermOptions = useMemo(() => {
return getPaymentTermOptions(paymentTermsQuery.data?.items ?? []);
}, [paymentTermsQuery.data?.items]);
return {
paymentMethodOptions,
paymentTermOptions,
isLoading: paymentMethodsQuery.isLoading || paymentTermsQuery.isLoading,
isFetching: paymentMethodsQuery.isFetching || paymentTermsQuery.isFetching,
isError: paymentMethodsQuery.isError || paymentTermsQuery.isError,
error: paymentMethodsQuery.error ?? paymentTermsQuery.error,
};
};
export type UseUpdateProformaPaymentControllerResult = ReturnType<
typeof useUpdateProformaPaymentController
>;

View File

@ -47,6 +47,7 @@ export interface ProformaUpdateForm {
retentionPercentage: number | null; retentionPercentage: number | null;
paymentMethodId: string | null; paymentMethodId: string | null;
paymentTermId: string | null;
items: ProformaItemUpdateForm[]; items: ProformaItemUpdateForm[];
} }

View File

@ -48,6 +48,7 @@ export const ProformaUpdateFormSchema = z
retentionPercentage: z.number().nullable(), retentionPercentage: z.number().nullable(),
paymentMethodId: z.string().nullable(), paymentMethodId: z.string().nullable(),
paymentTermId: z.string().nullable(),
items: z.array(ProformaItemUpdateFormSchema).min(1), items: z.array(ProformaItemUpdateFormSchema).min(1),
}) })

View File

@ -38,6 +38,7 @@ export type ProformaUpdatePatch = {
retentionPercentage?: number | null; retentionPercentage?: number | null;
paymentMethodId?: string | null; paymentMethodId?: string | null;
paymentTermId?: string | null;
languageCode?: string; languageCode?: string;
currencyCode?: string; currencyCode?: string;

View File

@ -1 +1,2 @@
export * from "./proforma-info-alert";
export * from "./proforma-update-skeleton"; export * from "./proforma-update-skeleton";

View File

@ -0,0 +1,57 @@
// packages/rdx-ui/src/components/feedback/info-alert.tsx
import { Alert, AlertDescription, AlertTitle } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { InfoIcon } from "lucide-react";
import type * as React from "react";
type ProformaInfoAlertProps = {
title: string;
description?: string;
children?: React.ReactNode;
className?: string;
iconClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
};
export const ProformaInfoAlert = ({
title,
description,
children,
className,
iconClassName,
titleClassName,
descriptionClassName,
}: ProformaInfoAlertProps) => {
return (
<Alert
className={cn(
"relative flex items-start gap-3 rounded-md border border-primary-200 bg-primary-100 px-4 py-3 text-primary-950 shadow-none",
className
)}
>
<div className={cn("mt-0.5 flex shrink-0 items-center justify-center", iconClassName)}>
<InfoIcon className="size-10 text-primary-600" />
</div>
<div className="min-w-0 flex-1">
<AlertTitle
className={cn(
"mb-0.5 text-base font-semibold leading-none text-primary-800",
titleClassName
)}
>
{title}
</AlertTitle>
{(description || children) && (
<AlertDescription
className={cn("text-sm leading-5 text-foreground", descriptionClassName)}
>
{description ?? children}
</AlertDescription>
)}
</div>
</Alert>
);
};

View File

@ -1,2 +1,10 @@
export * from "./proforma-payment-editor";
export * from "./proforma-settings-editor";
export * from "./proforma-taxes-card";
export * from "./proforma-update-editor-form"; export * from "./proforma-update-editor-form";
export * from "./proforma-update-header-editor";
export * from "./proforma-update-item-row-editor";
export * from "./proforma-update-items-editor";
export * from "./proforma-update-items-totals";
export * from "./proforma-update-recipient-editor"; export * from "./proforma-update-recipient-editor";
export * from "./proforma-update-tax-editor";

View File

@ -0,0 +1,69 @@
import {
FormSectionCard,
FormSectionGrid,
SelectField,
type SelectFieldItem,
} from "@repo/rdx-ui/components";
import { CreditCardIcon } from "lucide-react";
import { useTranslation } from "../../../../i18n";
interface ProformaUpdatePaymentEditorProps {
paymentMethodOptions: SelectFieldItem[];
paymentTermOptions: SelectFieldItem[];
disabled?: boolean;
readOnly?: boolean;
className?: string;
}
export const ProformaUpdatePaymentEditor = ({
paymentMethodOptions,
paymentTermOptions,
disabled = false,
readOnly = false,
className,
}: ProformaUpdatePaymentEditorProps) => {
const { t } = useTranslation();
return (
<FormSectionCard
className={className}
description={t("form_groups.proformas.payment.description", "Forma de pago")}
disabled={disabled}
icon={<CreditCardIcon className="size-5" />}
title={t("form_groups.proformas.payment.title", "Condiciones de pago")}
>
<FormSectionGrid className="w-full">
<SelectField
className="col-span-full"
disabled={disabled}
inputClassName="bg-background"
items={paymentMethodOptions}
label={t("form_fields.proformas.payment_method.label", "Forma de pago")}
name="paymentMethodId"
placeholder={t(
"form_fields.proformas.payment_method.placeholder",
"Selecciona una forma de pago"
)}
readOnly={readOnly}
/>
<SelectField
className="col-span-full"
disabled={disabled}
inputClassName="bg-background"
items={paymentTermOptions}
label={t("form_fields.proformas.payment_term.label", "Vencimiento")}
name="paymentTermId"
placeholder={t(
"form_fields.proformas.payment_term.placeholder",
"Selecciona un vencimiento"
)}
readOnly={readOnly}
/>
</FormSectionGrid>
</FormSectionCard>
);
};

View File

@ -0,0 +1,51 @@
import { FormSectionCard, FormSectionGrid, SelectField } from "@repo/rdx-ui/components";
import { Settings2Icon } from "lucide-react";
import { useTranslation } from "../../../../i18n";
interface ProformaUpdateSettingsEditorProps {
disabled?: boolean;
readOnly?: boolean;
className?: string;
}
export const ProformaUpdateSettingsEditor = ({
disabled = false,
readOnly = false,
className,
}: ProformaUpdateSettingsEditorProps) => {
const { t } = useTranslation();
return (
<FormSectionCard
className={className}
description={t("form_groups.proformas.settings.description", "Configuración de la proforma")}
disabled={disabled}
icon={<Settings2Icon className="size-5" />}
title={t("form_groups.proformas.settings.title", "Configuración")}
>
<FormSectionGrid className="w-full">
<SelectField
className="col-span-full"
disabled={disabled}
inputClassName="bg-background"
label={t("form_fields.proformas.status.label")}
name="status"
placeholder={t("form_fields.proformas.status.placeholder")}
readOnly={readOnly}
/>
<SelectField
className="col-span-full"
disabled={disabled}
inputClassName="bg-background"
label={t("form_fields.proformas.currency_code.label")}
name="currencyCode"
placeholder={t("form_fields.proformas.currency_code.placeholder")}
readOnly={readOnly}
/>
</FormSectionGrid>
</FormSectionCard>
);
};

View File

@ -5,15 +5,21 @@ import { PercentageField } from "@repo/rdx-ui/components";
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers"; import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { ProformaUpdateRecipientEditor } from "."; import {
ProformaUpdatePaymentEditor,
ProformaUpdateRecipientEditor,
ProformaUpdateSettingsEditor,
} from ".";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../i18n";
import type { import type {
UseUpdateProformaItemsControllerResult, UseUpdateProformaItemsControllerResult,
UseUpdateProformaPaymentControllerResult,
UseUpdateProformaTaxControllerResult, UseUpdateProformaTaxControllerResult,
UseUpdateProformaTotalsControllerResult, UseUpdateProformaTotalsControllerResult,
} from "../../controllers"; } from "../../controllers";
import { ProformaTotalsSummary } from "../blocks"; import { ProformaTotalsSummary } from "../blocks";
import { ProformaInfoAlert } from "../components";
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor"; import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
import { ProformaUpdateItemsEditor } from "./proforma-update-items-editor"; import { ProformaUpdateItemsEditor } from "./proforma-update-items-editor";
@ -32,6 +38,7 @@ type ProformaUpdateEditorProps = {
itemsCtrl: UseUpdateProformaItemsControllerResult; itemsCtrl: UseUpdateProformaItemsControllerResult;
taxCtrl: UseUpdateProformaTaxControllerResult; taxCtrl: UseUpdateProformaTaxControllerResult;
totalsCtrl: UseUpdateProformaTotalsControllerResult; totalsCtrl: UseUpdateProformaTotalsControllerResult;
paymentCtrl: UseUpdateProformaPaymentControllerResult;
currencyCode?: string; currencyCode?: string;
languageCode?: string; languageCode?: string;
@ -48,6 +55,7 @@ export const ProformaUpdateEditorForm = ({
itemsCtrl, itemsCtrl,
taxCtrl, taxCtrl,
totalsCtrl, totalsCtrl,
paymentCtrl,
currencyCode, currencyCode,
languageCode, languageCode,
}: ProformaUpdateEditorProps) => { }: ProformaUpdateEditorProps) => {
@ -55,43 +63,64 @@ export const ProformaUpdateEditorForm = ({
return ( return (
<form <form
className="space-y-6" className="space-y-6 space-x-6 2xl:space-y-12"
id={formId} id={formId}
noValidate noValidate
onKeyDown={preventEnterKeySubmitForm} onKeyDown={preventEnterKeySubmitForm}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
<div className="grid grid-cols-1 gap-4 xl:grid-cols-12"> <ProformaInfoAlert
<ProformaUpdateHeaderEditor className="xl:col-span-9" disabled={isSubmitting} /> description="Esta proforma no tiene validez fiscal. Puedes convertirla en factura cuando sea aceptada por el cliente."
title="Información importante"
/>
<div className="flex flex-row">
<div className="basis-3/4 space-x-4 space-y-4 2xl:space-y-6 2xl:space-x-6">
<div className="grid grid-cols-1 2xl:grid-cols-12 gap-4">
<ProformaUpdateHeaderEditor className="2xl:col-span-8" disabled={isSubmitting} />
<ProformaUpdateRecipientEditor <ProformaUpdateRecipientEditor
className="xl:col-span-3" className="xl:col-span-4"
disabled={isSubmitting}
onChangeCustomerClick={onChangeCustomerClick}
onCreateCustomerClick={onCreateCustomerClick}
selectedCustomer={selectedCustomer}
/>
</div>
<ProformaUpdateItemsEditor disabled={isSubmitting} itemsCtrl={itemsCtrl} taxCtrl={taxCtrl} />
<div className="grid grid-cols-1 gap-4 md:grid-cols-12">
<ProformaUpdateTaxEditor className="md:col-span-4" taxCtrl={taxCtrl} />
<ProformaTotalsSummary
className="md:col-span-8"
currency={currencyCode}
globalDiscountField={
<PercentageField
disabled={isSubmitting} disabled={isSubmitting}
inputClassName="bg-background" onChangeCustomerClick={onChangeCustomerClick}
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")} onCreateCustomerClick={onCreateCustomerClick}
name="globalDiscountPercentage" selectedCustomer={selectedCustomer}
/> />
} </div>
showRec={taxCtrl.hasRecPercentage}
showRetention={taxCtrl.hasRetentionPercentage} <ProformaUpdateItemsEditor
totals={totalsCtrl.totals} disabled={isSubmitting}
/> itemsCtrl={itemsCtrl}
taxCtrl={taxCtrl}
/>
<div className="grid grid-cols-1 gap-4 md:grid-cols-12">
<ProformaTotalsSummary
className="md:col-span-8"
currency={currencyCode}
globalDiscountField={
<PercentageField
disabled={isSubmitting}
inputClassName="bg-background"
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")}
name="globalDiscountPercentage"
/>
}
showRec={taxCtrl.hasRecPercentage}
showRetention={taxCtrl.hasRetentionPercentage}
totals={totalsCtrl.totals}
/>
</div>
</div>
<div className="basis-1/4 space-x-4 space-y-4 2xl:space-y-6 2xl:space-x-6">
<ProformaUpdateSettingsEditor className="w-full bg-secondary ring-0" />
<ProformaUpdateTaxEditor className="w-full bg-secondary ring-0" taxCtrl={taxCtrl} />
<ProformaUpdatePaymentEditor
className="w-full bg-secondary ring-0"
disabled={isSubmitting || paymentCtrl.isLoading}
paymentMethodOptions={paymentCtrl.paymentMethodOptions}
paymentTermOptions={paymentCtrl.paymentTermOptions}
/>
</div>
</div> </div>
<div className="flex flex-col-reverse gap-3 border-t pt-4 sm:flex-row sm:justify-end"> <div className="flex flex-col-reverse gap-3 border-t pt-4 sm:flex-row sm:justify-end">

View File

@ -58,15 +58,6 @@ export const ProformaUpdateHeaderEditor = ({
} }
/> />
<SelectField
className="md:col-span-3"
disabled={disabled}
label={t("form_fields.proformas.currency_code.label")}
name="currencyCode"
placeholder={t("form_fields.proformas.currency_code.placeholder")}
readOnly={readOnly}
/>
<DatePickerField <DatePickerField
className="md:col-span-3 md:col-start-1" className="md:col-span-3 md:col-start-1"
disabled={disabled} disabled={disabled}

View File

@ -5,21 +5,6 @@ import {
SwitchField, SwitchField,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
import { PercentageHelper } from "@repo/rdx-utils"; import { PercentageHelper } from "@repo/rdx-utils";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Field,
FieldDescription,
FieldLegend,
FieldSeparator,
FieldSet,
Switch,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { ReceiptTextIcon } from "lucide-react"; import { ReceiptTextIcon } from "lucide-react";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../i18n";
@ -59,199 +44,159 @@ export const ProformaUpdateTaxEditor = ({
icon={<ReceiptTextIcon className="size-5" />} icon={<ReceiptTextIcon className="size-5" />}
title={t("form_groups.proformas.taxes.title", "Impuestos")} title={t("form_groups.proformas.taxes.title", "Impuestos")}
> >
<FieldSet> <FormSectionGrid>
<FieldLegend className="text-primary">1. Régimen fiscal</FieldLegend> <SelectField
<FieldDescription> className="col-span-full"
Selecciona el régimen fiscal que aplica a esta proforma. disabled={disabled}
</FieldDescription> inputClassName="bg-background"
<FormSectionGrid> items={[
<Field className="md:col-span-12 md:col-start-1" orientation="horizontal"> { value: "01", label: "01: Operación de régimen general." },
<SelectField { value: "02", label: "02: Exportación." },
className="md:col-span-4 md:col-start-1" {
disabled={disabled} value: "03",
items={[ label:
{ value: "01", label: "01: Operación de régimen general." }, "03: Operaciones a las que se aplique el régimen especial de bienes usados, objetos de arte, antigüedades y objetos de colección.",
{ value: "02", label: "02: Exportación." }, },
{ { value: "04", label: "04: Régimen especial del oro de inversión." },
value: "03", { value: "05", label: "05: Régimen especial de las agencias de viajes." },
label: {
"03: Operaciones a las que se aplique el régimen especial de bienes usados, objetos de arte, antigüedades y objetos de colección.", value: "06",
}, label: "06: Régimen especial grupo de entidades en IVA o IGIC (Nivel Avanzado)",
{ value: "04", label: "04: Régimen especial del oro de inversión." }, },
{ value: "05", label: "05: Régimen especial de las agencias de viajes." }, { value: "07", label: "07: Régimen especial del criterio de caja." },
{ { value: "08", label: "08: Operaciones sujetas al IPSI/IVA o IGIC." },
value: "06", {
label: "06: Régimen especial grupo de entidades en IVA o IGIC (Nivel Avanzado)", value: "09",
}, label:
{ value: "07", label: "07: Régimen especial del criterio de caja." }, "09: Facturación de las prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena (D.A.4ª RD1619/2012)",
{ value: "08", label: "08: Operaciones sujetas al IPSI/IVA o IGIC." }, },
{ {
value: "09", value: "10",
label: label:
"09: Facturación de las prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena (D.A.4ª RD1619/2012)", "10: Cobros por cuenta de terceros de honorarios profesionales o de derechos derivados de la propiedad industrial, de autor u otros por cuenta de sus socios, asociados o colegiados efectuados por sociedades, asociaciones, colegios profesionales u otras entidades que realicen estas funciones de cobro.",
}, },
{ { value: "11", label: "11: Operaciones de arrendamiento de local de negocio." },
value: "10", {
label: value: "14",
"10: Cobros por cuenta de terceros de honorarios profesionales o de derechos derivados de la propiedad industrial, de autor u otros por cuenta de sus socios, asociados o colegiados efectuados por sociedades, asociaciones, colegios profesionales u otras entidades que realicen estas funciones de cobro.", label:
}, "14: Factura con IVA o IGIC pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública.",
{ value: "11", label: "11: Operaciones de arrendamiento de local de negocio." }, },
{ {
value: "14", value: "15",
label: label:
"14: Factura con IVA o IGIC pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública.", "15: Factura con IVA o IGIC pendiente de devengo en operaciones de tracto sucesivo.",
}, },
{ {
value: "15", value: "17",
label: label:
"15: Factura con IVA o IGIC pendiente de devengo en operaciones de tracto sucesivo.", "17: Operación acogida a alguno de los regímenes previstos en el Capítulo XI del Título IX (OSS e IOSS) o régimen especial de comerciante minorista",
}, },
{ {
value: "17", value: "18",
label: label:
"17: Operación acogida a alguno de los regímenes previstos en el Capítulo XI del Título IX (OSS e IOSS) o régimen especial de comerciante minorista", "18: Recargo de equivalencia o régimen especial del pequeño empresario o profesional.",
}, },
{ {
value: "18", value: "19",
label: label:
"18: Recargo de equivalencia o régimen especial del pequeño empresario o profesional.", "19: Operaciones de actividades incluidas en el Régimen Especial de Agricultura, Ganadería y Pesca (REAGYP) u operaciones interiores exentas por aplicación artículo 25 Ley 19/1994",
}, },
{ { value: "20", label: "20: Régimen simplificado" },
value: "19", ]}
label: label={t("form_fields.proformas.tax_regime_code.label", "Régimen fiscal")}
"19: Operaciones de actividades incluidas en el Régimen Especial de Agricultura, Ganadería y Pesca (REAGYP) u operaciones interiores exentas por aplicación artículo 25 Ley 19/1994", name="taxRegimeCode"
}, placeholder={t(
{ value: "20", label: "20: Régimen simplificado" }, "form_fields.proformas.tax_regime_code.placeholder",
]} "Selecciona el régimen fiscal para esta proforma"
label={t("form_fields.proformas.tax_regime_code.label", "Régimen fiscal")} )}
name="taxRegimeCode" readOnly={readOnly || taxCtrl.usesPerLineTax}
placeholder={t( />
"form_fields.proformas.tax_regime_code.placeholder",
"Selecciona el régimen fiscal para esta proforma" <SelectField
className="md:col-span-4 md:col-start-1"
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
disabled={disabled}
inputClassName="bg-background"
items={getProformaTaxOptions()}
label={t("form_fields.proformas.default_tax_percentage.label", "IVA por defecto")}
name="taxPercentage"
onChange={(value) => {
const parsed = parseProformaTaxPercentage(value as number | null);
if (parsed !== null) {
taxCtrl.updateTaxPercentage(parsed);
}
}}
placeholder={t(
"form_fields.proformas.default_tax_percentage.placeholder",
"Selecciona IVA"
)}
readOnly={readOnly || taxCtrl.usesPerLineTax}
serialize={(value) => (typeof value === "number" ? String(value) : "")}
/>
<SwitchField
checked={taxCtrl.usesSingleTax}
className="md:col-span-12 md:col-start-1 not-disabled:cursor-pointer"
disabled={disabled || readOnly}
label="Mismo IVA en todas las líneas de la proforma"
name=""
onCheckedChange={(checked) =>
checked ? taxCtrl.disablePerLineTaxes() : taxCtrl.enablePerLineTaxes()
}
/>
<SwitchField
className="md:col-span-12 md:col-start-1"
disabled={disabled}
label={
<>
{t(
"form_fields.proformas.has_equivalence_surcharge.label",
"Recargo de equivalencia"
)} )}
readOnly={readOnly || taxCtrl.usesPerLineTax}
/>
</Field>
</FormSectionGrid>
</FieldSet>
<FieldSeparator className="my-4" />
<Card {taxCtrl.hasRecPercentage ? (
className={cn(disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary")} <span className="text-sm text-muted-foreground">
> {PercentageHelper.formatPercent(taxCtrl.recPercentage ?? 0)}
<CardHeader> </span>
<CardTitle> ) : null}
{" "} </>
{t( }
"proformas.update.taxes.disable_per_line", name="hasRecPercentage"
"Mismo IVA en todas las líneas de la proforma" onCheckedChange={taxCtrl.updateRecPercentage}
)} readOnly={readOnly}
</CardTitle> />
<CardDescription>
Puedes usar un tipo único para todos las líneas de detalle o permitir que cada línea
tenga su propio IVA.
</CardDescription>
<CardAction>
<Switch
checked={taxCtrl.usesSingleTax}
className="not-disabled:cursor-pointer"
disabled={disabled || readOnly}
onCheckedChange={(checked) =>
checked ? taxCtrl.disablePerLineTaxes() : taxCtrl.enablePerLineTaxes()
}
/>
</CardAction>
</CardHeader>
<CardContent>
<FieldSet>
<FormSectionGrid>
<SelectField
className="md:col-span-4 md:col-start-1"
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
disabled={disabled}
inputClassName="bg-background"
items={getProformaTaxOptions()}
label={t("form_fields.proformas.default_tax_percentage.label", "IVA por defecto")}
name="taxPercentage"
onChange={(value) => {
const parsed = parseProformaTaxPercentage(value as number | null);
if (parsed !== null) {
taxCtrl.updateTaxPercentage(parsed);
}
}}
placeholder={t(
"form_fields.proformas.default_tax_percentage.placeholder",
"Selecciona IVA"
)}
readOnly={readOnly || taxCtrl.usesPerLineTax}
serialize={(value) => (typeof value === "number" ? String(value) : "")}
/>
</FormSectionGrid>
</FieldSet>
<FieldSeparator className="my-4" />
<FieldSet>
<FieldLegend>Impuestos adicionales</FieldLegend>
<FieldDescription>
Activa opciones fiscales adicionales como recargo de equivalencia o retenciones
(IRPF), según el tipo de cliente y normativa aplicable.
</FieldDescription>
<FormSectionGrid> <SwitchField
<SwitchField className="md:col-span-12 md:col-start-1"
className="md:col-span-12 md:col-start-1" disabled={disabled}
disabled={disabled} label={t("form_fields.proformas.has_retention.label", "Incluir retención/IRPF")}
label={ name="hasRetentionPercentage"
<> readOnly={readOnly}
{t( />
"form_fields.proformas.has_equivalence_surcharge.label",
"Recargo de equivalencia"
)}
{taxCtrl.hasRecPercentage ? ( <SelectField
<span className="text-sm text-muted-foreground"> className="md:col-span-4 md:col-start-1"
{PercentageHelper.formatPercent(taxCtrl.recPercentage ?? 0)} deserialize={(value) => (value === null || value === "" ? null : Number(value))}
</span> disabled={disabled || !taxCtrl.hasRetentionPercentage}
) : null} inputClassName="bg-background"
</> items={getProformaRetentionOptions()}
} label={t("form_fields.proformas.retention_percentage.label", "Retención")}
name="hasRecPercentage" name="retentionPercentage"
onCheckedChange={taxCtrl.updateRecPercentage} onChange={(value) => {
readOnly={readOnly} const parsed = parseProformaRetentionPercentage(value as number | null);
/> if (parsed !== null) {
taxCtrl.updateRetentionPercentage(parsed);
<SwitchField }
className="md:col-span-12 md:col-start-1" }}
disabled={disabled} placeholder={t(
label={t("form_fields.proformas.has_retention.label", "Incluir retención/IRPF")} "form_fields.proformas.default_tax_percentage.placeholder",
name="hasRetentionPercentage" "Selecciona IVA"
readOnly={readOnly} )}
/> readOnly={readOnly || taxCtrl.usesPerLineTax}
serialize={(value) => (typeof value === "number" ? String(value) : "")}
<SelectField />
className="md:col-span-4 md:col-start-1" </FormSectionGrid>
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
disabled={disabled || !taxCtrl.hasRetentionPercentage}
inputClassName="bg-background"
items={getProformaRetentionOptions()}
label={t("form_fields.proformas.retention_percentage.label", "Retención")}
name="retentionPercentage"
onChange={(value) => {
const parsed = parseProformaRetentionPercentage(value as number | null);
if (parsed !== null) {
taxCtrl.updateRetentionPercentage(parsed);
}
}}
placeholder={t(
"form_fields.proformas.default_tax_percentage.placeholder",
"Selecciona IVA"
)}
readOnly={readOnly || taxCtrl.usesPerLineTax}
serialize={(value) => (typeof value === "number" ? String(value) : "")}
/>
</FormSectionGrid>
</FieldSet>
</CardContent>
</Card>
</FormSectionCard> </FormSectionCard>
); );
}; };

View File

@ -61,7 +61,7 @@ export const ProformaUpdatePage = () => {
return ( return (
<FormProvider {...updateCtrl.form}> <FormProvider {...updateCtrl.form}>
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}> <UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
<AppHeader className="mx-auto max-w-7xl space-y-4"> <AppHeader className="mx-auto max-w-[100rem]">
<PageHeader <PageHeader
description={t("pages.proformas.update.description")} description={t("pages.proformas.update.description")}
onBackClick={() => navigateBack()} onBackClick={() => navigateBack()}
@ -84,7 +84,7 @@ export const ProformaUpdatePage = () => {
/> />
</AppHeader> </AppHeader>
<AppContent className="mx-auto max-w-7xl space-y-4"> <AppContent className="mx-auto max-w-[100rem]">
{updateCtrl.isUpdateError && ( {updateCtrl.isUpdateError && (
<ErrorAlert <ErrorAlert
message={ message={
@ -105,6 +105,7 @@ export const ProformaUpdatePage = () => {
onCreateCustomerClick={() => null} onCreateCustomerClick={() => null}
onReset={updateCtrl.resetForm} onReset={updateCtrl.resetForm}
onSubmit={updateCtrl.onSubmit} onSubmit={updateCtrl.onSubmit}
paymentCtrl={updateCtrl.paymentCtrl}
selectedCustomer={updateCtrl.selectedCustomer} selectedCustomer={updateCtrl.selectedCustomer}
taxCtrl={updateCtrl.taxCtrl} taxCtrl={updateCtrl.taxCtrl}
totalsCtrl={updateCtrl.totalsCtrl} totalsCtrl={updateCtrl.totalsCtrl}

View File

@ -80,6 +80,10 @@ export const buildUpdateProformaByIdParams = (
data.payment_method_id = patch.paymentMethodId; data.payment_method_id = patch.paymentMethodId;
} }
if (ObjectHelper.hasOwn(patch, "paymentTermId")) {
data.payment_term_id = patch.paymentTermId;
}
if (ObjectHelper.hasOwn(patch, "globalDiscountPercentage")) { if (ObjectHelper.hasOwn(patch, "globalDiscountPercentage")) {
data.global_discount_percentage = PercentageDTOHelper.fromNumber( data.global_discount_percentage = PercentageDTOHelper.fromNumber(
patch.globalDiscountPercentage!, patch.globalDiscountPercentage!,

View File

@ -30,6 +30,7 @@
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"react": "^19.2.5", "react": "^19.2.5",
"react-dom": "^19.2.5", "react-dom": "^19.2.5",
"rimraf": "^6.1.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"dependencies": { "dependencies": {
@ -47,7 +48,6 @@
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"express": "^4.22.1", "express": "^4.22.1",
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"react-data-table-component": "^7.7.0",
"react-hook-form": "^7.72.1", "react-hook-form": "^7.72.1",
"react-i18next": "^17.0.2", "react-i18next": "^17.0.2",
"react-router-dom": "^7.14.0", "react-router-dom": "^7.14.0",

View File

@ -5,7 +5,7 @@ import { CustomerRoutes } from "./customer-routes";
export const MODULE_NAME = "Customers"; export const MODULE_NAME = "Customers";
const MODULE_VERSION = "1.0.0"; const MODULE_VERSION = "1.0.0";
export const CustomersModuleManifiest: IModuleClient = { export const CustomersModuleManifest: IModuleClient = {
name: MODULE_NAME, name: MODULE_NAME,
version: MODULE_VERSION, version: MODULE_VERSION,
dependencies: ["auth", "Core"], dependencies: ["auth", "Core"],
@ -17,4 +17,4 @@ export const CustomersModuleManifiest: IModuleClient = {
}, },
}; };
export default CustomersModuleManifiest; export default CustomersModuleManifest;

View File

@ -17,6 +17,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"rimraf": "^6.1.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"dependencies": { "dependencies": {

View File

@ -18,6 +18,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"rimraf": "^6.1.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"dependencies": { "dependencies": {

View File

@ -498,3 +498,4 @@ export class SequelizeSupplierInvoiceDomainMapper extends SequelizeDomainMapper<
); );
} }
} }
11111111

View File

@ -30,6 +30,7 @@
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"react": "^19.2.5", "react": "^19.2.5",
"react-dom": "^19.2.5", "react-dom": "^19.2.5",
"rimraf": "^6.1.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"dependencies": { "dependencies": {
@ -47,7 +48,6 @@
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"express": "^4.22.1", "express": "^4.22.1",
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"react-data-table-component": "^7.7.0",
"react-hook-form": "^7.72.1", "react-hook-form": "^7.72.1",
"react-i18next": "^17.0.2", "react-i18next": "^17.0.2",
"react-router-dom": "^7.14.0", "react-router-dom": "^7.14.0",

View File

@ -8,7 +8,7 @@
"packages/*" "packages/*"
], ],
"scripts": { "scripts": {
"build": "turbo build", "build": "turbo run build",
"build:templates": "bash scripts/build-templates.sh", "build:templates": "bash scripts/build-templates.sh",
"build:api": "bash scripts/build-api.sh rodax --api", "build:api": "bash scripts/build-api.sh rodax --api",
"dev": "turbo dev", "dev": "turbo dev",
@ -24,7 +24,7 @@
"format:check": "biome format .", "format:check": "biome format .",
"check": "biome check .", "check": "biome check .",
"check:write": "biome check --write .", "check:write": "biome check --write .",
"typecheck": "turbo run typecheck" "typecheck": "tsc -p tsconfig.json --noEmit"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.11", "@biomejs/biome": "2.4.11",
@ -39,7 +39,7 @@
"rimraf": "^6.1.3", "rimraf": "^6.1.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",
"turbo": "^2.9.6", "turbo": "^2.9.14",
"typescript": "6.0.2" "typescript": "6.0.2"
}, },
"engines": { "engines": {

View File

@ -12,15 +12,16 @@
"exports": { "exports": {
".": "./src/i18n.ts" ".": "./src/i18n.ts"
}, },
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"@types/node": "^25.6.0",
"rimraf": "^6.1.3",
"typescript": "^6.0.2"
},
"dependencies": { "dependencies": {
"i18next": "26.0.4", "i18next": "26.0.4",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^3.0.4", "i18next-http-backend": "^3.0.4",
"react-i18next": "^17.0.2" "react-i18next": "^17.0.2"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"@types/node": "^25.6.0",
"typescript": "^6.0.2"
} }
} }

View File

@ -17,6 +17,7 @@
"@repo/typescript-config": "workspace:*", "@repo/typescript-config": "workspace:*",
"@types/dinero.js": "1.9.1", "@types/dinero.js": "1.9.1",
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"rimraf": "^6.1.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"dependencies": { "dependencies": {

View File

@ -14,6 +14,7 @@
".": "./src/index.ts" ".": "./src/index.ts"
}, },
"devDependencies": { "devDependencies": {
"rimraf": "^6.1.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"dependencies": { "dependencies": {

View File

@ -5,9 +5,11 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit",
"ui:lint": "biome lint --fix", "ui:lint": "biome lint --fix",
"check": "biome check .", "check": "biome check .",
"lint": "biome lint ." "lint": "biome lint .",
"clean": "rimraf .turbo node_modules dist"
}, },
"exports": { "exports": {
"./helpers": "./src/helpers/index.ts", "./helpers": "./src/helpers/index.ts",
@ -20,10 +22,6 @@
"./src/hooks/index.ts" "./src/hooks/index.ts"
] ]
}, },
"peerDependencies": {
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.11", "@biomejs/biome": "^2.4.11",
"@repo/i18next": "workspace:*", "@repo/i18next": "workspace:*",
@ -34,10 +32,10 @@
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"date-fns": "^4.1.0",
"esbuild-plugin-react18": "^0.2.6", "esbuild-plugin-react18": "^0.2.6",
"esbuild-plugin-react18-css": "^0.0.4", "esbuild-plugin-react18-css": "^0.0.4",
"react": "^19.2.5", "rimraf": "^6.1.3",
"react-dom": "^19.2.5",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",
"tsup": "^8.5.1", "tsup": "^8.5.1",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
@ -58,6 +56,8 @@
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"esbuild-raw-plugin": "^0.3.1", "esbuild-raw-plugin": "^0.3.1",
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-hook-form": "^7.72.1", "react-hook-form": "^7.72.1",
"react-i18next": "^17.0.2", "react-i18next": "^17.0.2",
"react-router": "^7.14.0", "react-router": "^7.14.0",

View File

@ -1,8 +1,12 @@
import { FieldLabel } from "@repo/shadcn-ui/components"; import { FieldLabel } from "@repo/shadcn-ui/components";
import type { ReactNode } from "react";
interface FormFieldLabelProps extends React.ComponentProps<typeof FieldLabel> { interface FormFieldLabelProps extends React.ComponentProps<typeof FieldLabel> {
required?: boolean; required?: boolean;
optional?: boolean; optional?: boolean;
className?: string;
htmlFor?: string;
children: ReactNode;
} }
export const FormFieldLabel = ({ export const FormFieldLabel = ({

View File

@ -33,28 +33,30 @@ export const FormSectionCard = ({
return ( return (
<Card className={className}> <Card className={className}>
<CardHeader className={headerClassName}> {hasHeader && (
<div className="flex items-start gap-3"> <CardHeader className={headerClassName}>
{icon ? ( <div className="flex items-start gap-3">
<div {icon ? (
className={cn( <div
"flex size-12 shrink-0 items-center justify-center rounded-md", className={cn(
disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary" "flex size-12 shrink-0 items-center justify-center rounded-md",
)} disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary"
> )}
{" "} >
{icon}{" "} {" "}
</div> {icon}{" "}
) : null} </div>
<div className="space-y-1">
{title ? <CardTitle className="font-semibold">{title}</CardTitle> : null}
{description ? (
<CardDescription className="font-medium">{description}</CardDescription>
) : null} ) : null}
<div className="space-y-1">
{title ? <CardTitle className="font-semibold">{title}</CardTitle> : null}
{description ? (
<CardDescription className="font-medium">{description}</CardDescription>
) : null}
</div>
</div> </div>
</div> </CardHeader>
</CardHeader> )}
<CardContent className={contentClassName}>{children}</CardContent> <CardContent className={contentClassName}>{children}</CardContent>
</Card> </Card>

View File

@ -13,14 +13,14 @@ import { Controller, type FieldPath, type FieldValues, useFormContext } from "re
import { FormFieldLabel } from "./form-field-label.tsx"; import { FormFieldLabel } from "./form-field-label.tsx";
interface SelectFieldItem { export type SelectFieldItem = {
value: string; value: string;
label: string; label: string;
} };
type SelectFieldValue = string | null; export type SelectFieldValue = string | null;
type SelectFieldProps<TFormValues extends FieldValues> = { export type SelectFieldProps<TFormValues extends FieldValues> = {
name: FieldPath<TFormValues>; name: FieldPath<TFormValues>;
label?: string; label?: string;
@ -104,7 +104,7 @@ export function SelectField<TFormValues extends FieldValues>({
<Select <Select
disabled={isDisabled} disabled={isDisabled}
onValueChange={(value) => onValueChange={(value: any) =>
onChange && onChange &&
onChange(deserializeValue(value)) && onChange(deserializeValue(value)) &&
field.onChange(deserializeValue(value)) field.onChange(deserializeValue(value))

View File

@ -16,6 +16,7 @@
"devDependencies": { "devDependencies": {
"@repo/typescript-config": "workspace:*", "@repo/typescript-config": "workspace:*",
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"rimraf": "^6.1.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"dependencies": { "dependencies": {

View File

@ -6,12 +6,12 @@ const config = {
"apps/**/*.{ts,tsx}", "apps/**/*.{ts,tsx}",
"../../packages/shadcn-ui/src/**/*.{ts,tsx}", "../../packages/shadcn-ui/src/**/*.{ts,tsx}",
"../../modules/**/*.{ts,tsx}", "../../modules/**/*.{ts,tsx}",
], ],
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [], plugins: [],
extends: {},
}; };
export default config; export default config;

View File

@ -9,6 +9,9 @@
"@erp/auth/*": [ "@erp/auth/*": [
"modules/auth/src/*" "modules/auth/src/*"
], ],
"@erp/catalogs/*": [
"modules/catalogs/src/*"
],
"@erp/customers/*": [ "@erp/customers/*": [
"modules/customers/src/*" "modules/customers/src/*"
], ],

View File

@ -1,84 +0,0 @@
/**
* Generador de módulos ERP
* - module: bounded context (carpeta en modules/)
* - name: agregado principal (usa placeholders en plantillas)
* - plural: rutas/tabla (override del plural generado)
*/
import {
camelCase,
capitalCase,
constantCase,
dotCase,
kebabCase,
pascalCase,
snakeCase,
} from "change-case";
/**
* Generador de módulos ERP
* @remarks
* - `module` -> bounded context (carpeta en `modules/`)
* - `name` -> agregado principal (singular)
* - `plural` -> plural del agregado para rutas/tablas (override manual si hace falta)
* - Helpers -> registrados con los nombres usados en las plantillas (.hbs)
*/
export default function (plop) {
/** Helpers de casing para usar en hbs */
plop.setHelper("kebabCase", (s) => kebabCase(String(s || "")));
plop.setHelper("camelCase", (s) => camelCase(String(s || "")));
plop.setHelper("pascalCase", (s) => pascalCase(String(s || "")));
plop.setHelper("snakeCase", (s) => snakeCase(String(s || "")));
plop.setHelper("constantCase", (s) => constantCase(String(s || "")));
plop.setHelper("capitalCase", (s) => capitalCase(String(s || "")));
plop.setHelper("dotCase", (s) => dotCase(String(s || "")));
/** Validadores simples */
const isKebab = (v) =>
(!!v && /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(v)) ||
"Usa kebab-case (empieza por letra; letras/números/guiones).";
plop.setGenerator("module", {
description: "Crea un nuevo módulo (bounded context) con un agregado principal",
prompts: [
{
type: "input",
name: "module",
message: "Bounded context (kebab-case, p.ej. 'customer-payments'):",
validate: isKebab,
filter: (v) => kebabCase(v),
},
{
type: "input",
name: "name",
message: "Agregado principal (kebab-case, p.ej. 'customer-payment'):",
validate: isKebab,
filter: (v) => kebabCase(v),
},
{
type: "input",
name: "plural",
message: "Plural del agregado (kebab-case) — ENTER para sufijo 's':",
filter: (v, answers) => (v?.trim() ? kebabCase(v) : `${kebabCase(answers.name)}s`),
validate: isKebab,
},
],
actions: (answers) => {
const dest = `modules/${kebabCase(answers.module)}`;
/**
* addMany copiará la plantilla completa en la carpeta del bounded context
* usando los placeholders de las plantillas existentes (name/plural).
*/
const actions = [
{
type: "addMany",
destination: dest,
base: "templates/new-module",
templateFiles: "templates/new-module/**",
abortOnFail: true,
},
];
return actions;
},
});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
rm -rf node_modules
rm -rf .turbo
rm -rf pnpm-lock.yaml
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
find . -name ".turbo" -type d -prune -exec rm -rf '{}' +
pnpm store prune
pnpm install

View File

@ -1,17 +0,0 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"chatgpt.openOnStartup": true,
"chat.tools.terminal.autoApprove": {
"pnpm": true,
"/^cd /home/rodax/Documentos/uecko-erp && ls -l node_modules/@repo/typescript-config/root\\.json && node -p \"require\\.resolve\\('@repo/typescript-config/root\\.json'\\)\"$/": {
"approve": true,
"matchCommandLine": true
}
}
}
}