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",
"dev": "node --import=tsx --watch src/index.ts",
"clean": "rimraf .turbo node_modules dist",
"typecheck": "tsc --noEmit",
"typecheck": "tsc -p tsconfig.json --noEmit",
"check": "biome check .",
"lint": "biome lint .",
"format": "biome format --write"

View File

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

View File

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

View File

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

View File

@ -24,6 +24,7 @@
"@types/react-dom": "^19.2.3",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"rimraf": "^6.1.3",
"typescript": "^6.0.2"
},
"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 { 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,
ITabContextService,
} 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";
@ -48,7 +49,7 @@ export class PassportAuthProvider {
const checkUserId = user.id.equals(userIdVO.data);
const checkRoles = true; //user.hasRoles(roles);
if (!checkUserId || !checkRoles) {
if (!(checkUserId && checkRoles)) {
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_VERSION = "1.0.0";
export const AuthModuleManifiest: IModuleClient = {
export const AuthModuleManifest: IModuleClient = {
name: MODULE_NAME,
version: MODULE_VERSION,
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",
"description": "Catalogs module",
"version": "0.1.0",
"version": "0.6.7",
"private": true,
"type": "module",
"sideEffects": false,
@ -12,21 +12,45 @@
"clean": "rimraf .turbo node_modules dist"
},
"exports": {
".": "./src/api/index.ts",
"./api": "./src/api/index.ts"
".": "./src/common/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": {
"@erp/core": "workspace:*",
"@erp/auth": "workspace:*",
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-utils": "workspace:*",
"@repo/rdx-criteria": "workspace:*",
"express": "^4.22.1",
"sequelize": "^6.37.8",
"zod": "^4.3.6"
"peerDependencies": {
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
"devDependencies": {
"@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"
},
"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 "./payment-method.aggregate";
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;
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";
import { Result } from "@repo/rdx-utils";
import type { PaymentMethodSummary } from "../../../../../application/payment-methods/models";
import type { PaymentMethodSummary } from "../../../../../application";
import type { PaymentMethodModel } from "../models";
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";
export const ListPaymentMethodsResponseSchema = z.array(PaymentMethodSummarySchema);
export const ListPaymentMethodsResponseSchema = createPaginatedListSchema(
PaymentMethodSummarySchema
);
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";
export const ListPaymentTermsResponseSchema = z.array(PaymentTermSummarySchema);
export const ListPaymentTermsResponseSchema = createPaginatedListSchema(PaymentTermSummarySchema);
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"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
export * from "./issued-invoices";
export * from "./item-position.dto";
export * from "./payment-method-ref.dto";
export * from "./payment-term-ref.dto";
export * from "./proforma";
export * from "./tax-combination-code.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";
const MODULE_VERSION = "1.0.0";
export const CustomerInvoicesModuleManifiest: IModuleClient = {
export const CustomerInvoicesModuleManifest: IModuleClient = {
name: MODULE_NAME,
version: MODULE_VERSION,
dependencies: ["auth", "Core", "Customers"],
dependencies: ["auth", "Core", "Catalogs", "Customers"],
protected: true,
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";

View File

@ -1,5 +1,6 @@
export * from "./use-update-proforma-controller";
export * from "./use-update-proforma-items-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-totals-controller";

View File

@ -25,6 +25,7 @@ import {
} from "../utils";
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 { useUpdateProformaTotalsController } from "./use-update-proforma-totals-controller";
@ -251,6 +252,8 @@ export const useUpdateProformaController = (
form,
});
const paymentCtrl = useUpdateProformaPaymentController();
return {
// form
formId,
@ -259,6 +262,7 @@ export const useUpdateProformaController = (
itemsCtrl,
taxCtrl,
totalsCtrl,
paymentCtrl,
//
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;
paymentMethodId: string | null;
paymentTermId: string | null;
items: ProformaItemUpdateForm[];
}

View File

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

View File

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

View File

@ -1 +1,2 @@
export * from "./proforma-info-alert";
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-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-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 { Button } from "@repo/shadcn-ui/components";
import { ProformaUpdateRecipientEditor } from ".";
import {
ProformaUpdatePaymentEditor,
ProformaUpdateRecipientEditor,
ProformaUpdateSettingsEditor,
} from ".";
import { useTranslation } from "../../../../i18n";
import type {
UseUpdateProformaItemsControllerResult,
UseUpdateProformaPaymentControllerResult,
UseUpdateProformaTaxControllerResult,
UseUpdateProformaTotalsControllerResult,
} from "../../controllers";
import { ProformaTotalsSummary } from "../blocks";
import { ProformaInfoAlert } from "../components";
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
import { ProformaUpdateItemsEditor } from "./proforma-update-items-editor";
@ -32,6 +38,7 @@ type ProformaUpdateEditorProps = {
itemsCtrl: UseUpdateProformaItemsControllerResult;
taxCtrl: UseUpdateProformaTaxControllerResult;
totalsCtrl: UseUpdateProformaTotalsControllerResult;
paymentCtrl: UseUpdateProformaPaymentControllerResult;
currencyCode?: string;
languageCode?: string;
@ -48,6 +55,7 @@ export const ProformaUpdateEditorForm = ({
itemsCtrl,
taxCtrl,
totalsCtrl,
paymentCtrl,
currencyCode,
languageCode,
}: ProformaUpdateEditorProps) => {
@ -55,43 +63,64 @@ export const ProformaUpdateEditorForm = ({
return (
<form
className="space-y-6"
className="space-y-6 space-x-6 2xl:space-y-12"
id={formId}
noValidate
onKeyDown={preventEnterKeySubmitForm}
onSubmit={onSubmit}
>
<div className="grid grid-cols-1 gap-4 xl:grid-cols-12">
<ProformaUpdateHeaderEditor className="xl:col-span-9" disabled={isSubmitting} />
<ProformaInfoAlert
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
className="xl:col-span-3"
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
<ProformaUpdateRecipientEditor
className="xl:col-span-4"
disabled={isSubmitting}
inputClassName="bg-background"
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")}
name="globalDiscountPercentage"
onChangeCustomerClick={onChangeCustomerClick}
onCreateCustomerClick={onCreateCustomerClick}
selectedCustomer={selectedCustomer}
/>
}
showRec={taxCtrl.hasRecPercentage}
showRetention={taxCtrl.hasRetentionPercentage}
totals={totalsCtrl.totals}
/>
</div>
<ProformaUpdateItemsEditor
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 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
className="md:col-span-3 md:col-start-1"
disabled={disabled}

View File

@ -5,21 +5,6 @@ import {
SwitchField,
} from "@repo/rdx-ui/components";
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 { useTranslation } from "../../../../i18n";
@ -59,199 +44,159 @@ export const ProformaUpdateTaxEditor = ({
icon={<ReceiptTextIcon className="size-5" />}
title={t("form_groups.proformas.taxes.title", "Impuestos")}
>
<FieldSet>
<FieldLegend className="text-primary">1. Régimen fiscal</FieldLegend>
<FieldDescription>
Selecciona el régimen fiscal que aplica a esta proforma.
</FieldDescription>
<FormSectionGrid>
<Field className="md:col-span-12 md:col-start-1" orientation="horizontal">
<SelectField
className="md:col-span-4 md:col-start-1"
disabled={disabled}
items={[
{ value: "01", label: "01: Operación de régimen general." },
{ value: "02", label: "02: Exportación." },
{
value: "03",
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: "04", label: "04: Régimen especial del oro de inversión." },
{ value: "05", label: "05: Régimen especial de las agencias de viajes." },
{
value: "06",
label: "06: Régimen especial grupo de entidades en IVA o IGIC (Nivel Avanzado)",
},
{ value: "07", label: "07: Régimen especial del criterio de caja." },
{ value: "08", label: "08: Operaciones sujetas al IPSI/IVA o IGIC." },
{
value: "09",
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)",
},
{
value: "10",
label:
"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: "14",
label:
"14: Factura con IVA o IGIC pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública.",
},
{
value: "15",
label:
"15: Factura con IVA o IGIC pendiente de devengo en operaciones de tracto sucesivo.",
},
{
value: "17",
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",
},
{
value: "18",
label:
"18: Recargo de equivalencia o régimen especial del pequeño empresario o profesional.",
},
{
value: "19",
label:
"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" },
]}
label={t("form_fields.proformas.tax_regime_code.label", "Régimen fiscal")}
name="taxRegimeCode"
placeholder={t(
"form_fields.proformas.tax_regime_code.placeholder",
"Selecciona el régimen fiscal para esta proforma"
<FormSectionGrid>
<SelectField
className="col-span-full"
disabled={disabled}
inputClassName="bg-background"
items={[
{ value: "01", label: "01: Operación de régimen general." },
{ value: "02", label: "02: Exportación." },
{
value: "03",
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: "04", label: "04: Régimen especial del oro de inversión." },
{ value: "05", label: "05: Régimen especial de las agencias de viajes." },
{
value: "06",
label: "06: Régimen especial grupo de entidades en IVA o IGIC (Nivel Avanzado)",
},
{ value: "07", label: "07: Régimen especial del criterio de caja." },
{ value: "08", label: "08: Operaciones sujetas al IPSI/IVA o IGIC." },
{
value: "09",
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)",
},
{
value: "10",
label:
"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: "14",
label:
"14: Factura con IVA o IGIC pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública.",
},
{
value: "15",
label:
"15: Factura con IVA o IGIC pendiente de devengo en operaciones de tracto sucesivo.",
},
{
value: "17",
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",
},
{
value: "18",
label:
"18: Recargo de equivalencia o régimen especial del pequeño empresario o profesional.",
},
{
value: "19",
label:
"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" },
]}
label={t("form_fields.proformas.tax_regime_code.label", "Régimen fiscal")}
name="taxRegimeCode"
placeholder={t(
"form_fields.proformas.tax_regime_code.placeholder",
"Selecciona el régimen fiscal para esta proforma"
)}
readOnly={readOnly || taxCtrl.usesPerLineTax}
/>
<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
className={cn(disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary")}
>
<CardHeader>
<CardTitle>
{" "}
{t(
"proformas.update.taxes.disable_per_line",
"Mismo IVA en todas las líneas de la proforma"
)}
</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>
{taxCtrl.hasRecPercentage ? (
<span className="text-sm text-muted-foreground">
{PercentageHelper.formatPercent(taxCtrl.recPercentage ?? 0)}
</span>
) : null}
</>
}
name="hasRecPercentage"
onCheckedChange={taxCtrl.updateRecPercentage}
readOnly={readOnly}
/>
<FormSectionGrid>
<SwitchField
className="md:col-span-12 md:col-start-1"
disabled={disabled}
label={
<>
{t(
"form_fields.proformas.has_equivalence_surcharge.label",
"Recargo de equivalencia"
)}
<SwitchField
className="md:col-span-12 md:col-start-1"
disabled={disabled}
label={t("form_fields.proformas.has_retention.label", "Incluir retención/IRPF")}
name="hasRetentionPercentage"
readOnly={readOnly}
/>
{taxCtrl.hasRecPercentage ? (
<span className="text-sm text-muted-foreground">
{PercentageHelper.formatPercent(taxCtrl.recPercentage ?? 0)}
</span>
) : null}
</>
}
name="hasRecPercentage"
onCheckedChange={taxCtrl.updateRecPercentage}
readOnly={readOnly}
/>
<SwitchField
className="md:col-span-12 md:col-start-1"
disabled={disabled}
label={t("form_fields.proformas.has_retention.label", "Incluir retención/IRPF")}
name="hasRetentionPercentage"
readOnly={readOnly}
/>
<SelectField
className="md:col-span-4 md:col-start-1"
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>
<SelectField
className="md:col-span-4 md:col-start-1"
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>
</FormSectionCard>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@
},
"devDependencies": {
"@types/express": "^4.17.21",
"rimraf": "^6.1.3",
"typescript": "^6.0.2"
},
"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",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"rimraf": "^6.1.3",
"typescript": "^6.0.2"
},
"dependencies": {
@ -47,7 +48,6 @@
"@tanstack/react-table": "^8.21.3",
"express": "^4.22.1",
"lucide-react": "^1.8.0",
"react-data-table-component": "^7.7.0",
"react-hook-form": "^7.72.1",
"react-i18next": "^17.0.2",
"react-router-dom": "^7.14.0",

View File

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

View File

@ -12,15 +12,16 @@
"exports": {
".": "./src/i18n.ts"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"@types/node": "^25.6.0",
"rimraf": "^6.1.3",
"typescript": "^6.0.2"
},
"dependencies": {
"i18next": "26.0.4",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^3.0.4",
"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:*",
"@types/dinero.js": "1.9.1",
"@types/node": "^25.6.0",
"rimraf": "^6.1.3",
"typescript": "^6.0.2"
},
"dependencies": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,9 @@
"@erp/auth/*": [
"modules/auth/src/*"
],
"@erp/catalogs/*": [
"modules/catalogs/src/*"
],
"@erp/customers/*": [
"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
}
}
}
}