.
This commit is contained in:
parent
f6f57359d5
commit
1551d65f63
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/factuges-server",
|
"name": "@erp/factuges-server",
|
||||||
"version": "0.6.9",
|
"version": "0.7.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup src/index.ts --config tsup.config.ts",
|
"build": "tsup src/index.ts --config tsup.config.ts",
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export function getServiceScoped<T = unknown>(
|
|||||||
if (!allowedDeps.includes(serviceModule)) {
|
if (!allowedDeps.includes(serviceModule)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`❌ Module "${requesterModule}" tried to access service "${name}" ` +
|
`❌ Module "${requesterModule}" tried to access service "${name}" ` +
|
||||||
`without declaring dependency on "${serviceModule}"`
|
`without declaring dependency on "${serviceModule}" (web/manifest.ts api/index.ts file)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ export function getServiceScoped<T = unknown>(
|
|||||||
function getService<T = unknown>(name: string): T {
|
function getService<T = unknown>(name: string): T {
|
||||||
const service = services[name];
|
const service = services[name];
|
||||||
if (!service) {
|
if (!service) {
|
||||||
throw new Error(`❌ Servicio "${name}" no encontrado.`);
|
throw new Error(`❌ Service "${name}" not found.`);
|
||||||
}
|
}
|
||||||
return service as T;
|
return service as T;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/factuges-web",
|
"name": "@erp/factuges-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.6.9",
|
"version": "0.7.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/auth",
|
"name": "@erp/auth",
|
||||||
"version": "0.6.9",
|
"version": "0.7.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/catalogs",
|
"name": "@erp/catalogs",
|
||||||
"description": "Catalogs module",
|
"description": "Catalogs module",
|
||||||
"version": "0.6.9",
|
"version": "0.7.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import { Maybe, Result } from "@repo/rdx-utils";
|
|||||||
|
|
||||||
import type { PaymentTermPublicModelMapper } from "../mappers";
|
import type { PaymentTermPublicModelMapper } from "../mappers";
|
||||||
import type { PaymentTermPublicModel } from "../models";
|
import type { PaymentTermPublicModel } from "../models";
|
||||||
import type { IPaymentTermRepository } from "../repositories";
|
import { IPaymentTermRepository } from '../../repositories';
|
||||||
|
|
||||||
|
|
||||||
export interface FindPaymentTermByIdInCompanyParams {
|
export interface FindPaymentTermByIdInCompanyParams {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import type {
|
|||||||
|
|
||||||
export class TaxDefinitionPublicModelMapper {
|
export class TaxDefinitionPublicModelMapper {
|
||||||
public toPublicModel(taxDefinition: TaxDefinition): TaxDefinitionPublicModel {
|
public toPublicModel(taxDefinition: TaxDefinition): TaxDefinitionPublicModel {
|
||||||
|
console.log(taxDefinition.description)
|
||||||
return {
|
return {
|
||||||
id: taxDefinition.id,
|
id: taxDefinition.id,
|
||||||
companyId: Maybe.some(taxDefinition.companyId),
|
companyId: Maybe.some(taxDefinition.companyId),
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import {
|
|||||||
EnablePaymentTermByIdUseCase,
|
EnablePaymentTermByIdUseCase,
|
||||||
GetPaymentTermByIdUseCase,
|
GetPaymentTermByIdUseCase,
|
||||||
ListPaymentTermsUseCase,
|
ListPaymentTermsUseCase,
|
||||||
|
PaymentTermPublicFinder,
|
||||||
|
PaymentTermPublicModelMapper,
|
||||||
UpdatePaymentTermByIdUseCase,
|
UpdatePaymentTermByIdUseCase,
|
||||||
buildPaymentTermCreator,
|
buildPaymentTermCreator,
|
||||||
buildPaymentTermDeleter,
|
buildPaymentTermDeleter,
|
||||||
@ -19,7 +21,6 @@ import {
|
|||||||
buildPaymentTermUpdater,
|
buildPaymentTermUpdater,
|
||||||
} from "../../../application/payment-terms";
|
} from "../../../application/payment-terms";
|
||||||
import type { IPaymentTermRepository } from "../../../application/payment-terms/repositories";
|
import type { IPaymentTermRepository } from "../../../application/payment-terms/repositories";
|
||||||
import { PaymentTermFinder } from "../../../application/payment-terms/services";
|
|
||||||
|
|
||||||
import { buildPaymentTermPersistenceMappers } from "./payment-term-persistence-mappers.di";
|
import { buildPaymentTermPersistenceMappers } from "./payment-term-persistence-mappers.di";
|
||||||
import { buildPaymentTermRepository } from "./payment-term-repositories.di";
|
import { buildPaymentTermRepository } from "./payment-term-repositories.di";
|
||||||
@ -110,8 +111,10 @@ export const buildPaymentTermsDependencies = (params: ModuleParams): PaymentTerm
|
|||||||
export const buildPaymentTermsPublicServices = (
|
export const buildPaymentTermsPublicServices = (
|
||||||
_params: SetupParams,
|
_params: SetupParams,
|
||||||
deps: PaymentTermsInternalDeps
|
deps: PaymentTermsInternalDeps
|
||||||
): { finder: PaymentTermFinder } => {
|
): { finder: PaymentTermPublicFinder } => {
|
||||||
|
const mapper = new PaymentTermPublicModelMapper();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
finder: new PaymentTermFinder(deps.repository),
|
finder: new PaymentTermPublicFinder({ repository: deps.repository, mapper }),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -38,7 +38,11 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeDomainMapper<
|
|||||||
const code = extractOrPushError(TaxDefinitionCodeVO.create(raw.code), "code", errors);
|
const code = extractOrPushError(TaxDefinitionCodeVO.create(raw.code), "code", errors);
|
||||||
const name = extractOrPushError(TaxDefinitionNameVO.create(raw.name), "name", errors);
|
const name = extractOrPushError(TaxDefinitionNameVO.create(raw.name), "name", errors);
|
||||||
|
|
||||||
const description = maybeFromNullableResult(raw.description, (v) => TextValue.create(v));
|
const description = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.description, (v) => TextValue.create(v)),
|
||||||
|
"description",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
const rate = extractOrPushError(
|
const rate = extractOrPushError(
|
||||||
TaxRateVO.create({ value: raw.rate_value, scale: raw.rate_scale }),
|
TaxRateVO.create({ value: raw.rate_value, scale: raw.rate_scale }),
|
||||||
@ -60,19 +64,30 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeDomainMapper<
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const jurisdictionRegionCode = maybeFromNullableResult(raw.jurisdiction_region_code, (v) =>
|
const jurisdictionRegionCode = extractOrPushError(
|
||||||
CountryRegionCode.create(v)
|
maybeFromNullableResult(raw.jurisdiction_region_code, (v) => CountryRegionCode.create(v)),
|
||||||
|
"jurisdiction_region_code",
|
||||||
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const taxScope = extractOrPushError(TaxScope.create(raw.tax_scope), "tax_scope", errors);
|
const taxScope = extractOrPushError(TaxScope.create(raw.tax_scope), "tax_scope", errors);
|
||||||
|
|
||||||
const invoiceNote = maybeFromNullableResult(raw.invoice_note, (v) => TextValue.create(v));
|
const invoiceNote = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.invoice_note, (v) => TextValue.create(v)),
|
||||||
|
"invoice_note",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
const allowedSurchargeCodes = maybeFromNullableResult(raw.allowed_surcharge_codes, (v) => {
|
const allowedSurchargeCodes = extractOrPushError(
|
||||||
if (!Array.isArray(v)) return Result.fail(new Error("Invalid allowed_surcharge_codes"));
|
maybeFromNullableResult(raw.allowed_surcharge_codes, (v) => {
|
||||||
|
console.log(v);
|
||||||
|
|
||||||
|
const values = v.split(";");
|
||||||
|
if (!Array.isArray(values))
|
||||||
|
return Result.fail(new Error("Invalid allowed_surcharge_codes"));
|
||||||
|
|
||||||
const arr: any[] = [];
|
const arr: any[] = [];
|
||||||
for (const el of v) {
|
for (const el of values) {
|
||||||
const _result = TaxDefinitionCodeVO.create(String(el));
|
const _result = TaxDefinitionCodeVO.create(String(el));
|
||||||
if (_result.isFailure)
|
if (_result.isFailure)
|
||||||
return Result.fail(new Error("Invalid allowed_surcharge_codes element"));
|
return Result.fail(new Error("Invalid allowed_surcharge_codes element"));
|
||||||
@ -80,13 +95,26 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeDomainMapper<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok(arr);
|
return Result.ok(arr);
|
||||||
});
|
}),
|
||||||
|
"allowed_surcharge_codes",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
// valid from / to
|
// valid from / to
|
||||||
const validFrom = maybeFromNullableResult(raw.valid_from, (v) => Result.ok(String(v)));
|
const validFrom = extractOrPushError(
|
||||||
const validTo = maybeFromNullableResult(raw.valid_to, (v) => Result.ok(String(v)));
|
maybeFromNullableResult(raw.valid_from, (v) => Result.ok(String(v))),
|
||||||
|
"valid_from",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const validTo = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.valid_to, (v) => Result.ok(String(v))),
|
||||||
|
"valid_to",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
|
console.error(errors);
|
||||||
return Result.fail(
|
return Result.fail(
|
||||||
new ValidationErrorCollection("TaxDefinition mapping failed [mapToDomain]", errors)
|
new ValidationErrorCollection("TaxDefinition mapping failed [mapToDomain]", errors)
|
||||||
);
|
);
|
||||||
@ -130,8 +158,8 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeDomainMapper<
|
|||||||
(v) => v.toPrimitive(),
|
(v) => v.toPrimitive(),
|
||||||
() => null
|
() => null
|
||||||
),
|
),
|
||||||
rate_value: source.rate.toPrimitive(),
|
rate_value: source.rate.toPrimitive().value,
|
||||||
rate_scale: (source.rate as any).scale ?? 2,
|
rate_scale: source.rate.toPrimitive().scale,
|
||||||
tax_family: source.taxFamily.toPrimitive(),
|
tax_family: source.taxFamily.toPrimitive(),
|
||||||
calculation_behavior: source.calculationBehavior.toPrimitive(),
|
calculation_behavior: source.calculationBehavior.toPrimitive(),
|
||||||
jurisdiction_country_code: source.jurisdictionCountryCode.toPrimitive(),
|
jurisdiction_country_code: source.jurisdictionCountryCode.toPrimitive(),
|
||||||
@ -145,7 +173,7 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeDomainMapper<
|
|||||||
() => null
|
() => null
|
||||||
),
|
),
|
||||||
allowed_surcharge_codes: source.allowedSurchargeCodes.match(
|
allowed_surcharge_codes: source.allowedSurchargeCodes.match(
|
||||||
(arr) => arr.map((c) => c.toPrimitive()),
|
(arr) => arr.map((c) => c.toPrimitive()).join(";"),
|
||||||
() => null
|
() => null
|
||||||
),
|
),
|
||||||
is_system: source.isSystem,
|
is_system: source.isSystem,
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export class TaxDefinitionModel extends Model<
|
|||||||
declare tax_scope: string;
|
declare tax_scope: string;
|
||||||
|
|
||||||
declare invoice_note: CreationOptional<string | null>;
|
declare invoice_note: CreationOptional<string | null>;
|
||||||
declare allowed_surcharge_codes: CreationOptional<object | null>;
|
declare allowed_surcharge_codes: CreationOptional<string | null>;
|
||||||
|
|
||||||
declare is_system: boolean;
|
declare is_system: boolean;
|
||||||
declare is_active: boolean;
|
declare is_active: boolean;
|
||||||
@ -111,7 +111,7 @@ export default (database: Sequelize) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
allowed_surcharge_codes: {
|
allowed_surcharge_codes: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/core",
|
"name": "@erp/core",
|
||||||
"version": "0.6.9",
|
"version": "0.7.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export const globalErrorHandler = async (
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) => {
|
||||||
//console.error(`❌ Global unhandled error: ${error.message}`);
|
console.error(`❌ Global unhandled error: ${error.message}`);
|
||||||
|
|
||||||
// Si ya se envió una respuesta, delegamos al siguiente error handler
|
// Si ya se envió una respuesta, delegamos al siguiente error handler
|
||||||
if (res.headersSent) {
|
if (res.headersSent) {
|
||||||
|
|||||||
@ -16,3 +16,4 @@ export * from "./postal-code.dto";
|
|||||||
export * from "./quantity.dto";
|
export * from "./quantity.dto";
|
||||||
export * from "./tin.dto";
|
export * from "./tin.dto";
|
||||||
export * from "./url.dto";
|
export * from "./url.dto";
|
||||||
|
export * from "./website.dto";
|
||||||
|
|||||||
@ -8,14 +8,13 @@ import { z } from "zod/v4";
|
|||||||
*
|
*
|
||||||
* Ejemplos válidos:
|
* Ejemplos válidos:
|
||||||
* - "28013" (ES)
|
* - "28013" (ES)
|
||||||
* - "SW1A 1AA" (UK)
|
|
||||||
* - "75008" (FR)
|
* - "75008" (FR)
|
||||||
* - "10115" (DE)
|
* - "10115" (DE)
|
||||||
*/
|
*/
|
||||||
export const PostalCodeSchema = z
|
export const PostalCodeSchema = z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Postal code cannot be empty")
|
.min(1, "Postal code cannot be empty")
|
||||||
.max(16, "Postal code too long")
|
.max(5, "Postal code too long")
|
||||||
.regex(/^[A-Za-z0-9\- ]+$/, "Invalid postal code format");
|
.regex(/^[0-9\- ]+$/, "Invalid postal code format");
|
||||||
|
|
||||||
export type PostalCodeDTO = z.infer<typeof PostalCodeSchema>;
|
export type PostalCodeDTO = z.infer<typeof PostalCodeSchema>;
|
||||||
|
|||||||
38
modules/core/src/common/dto/website.dto.ts
Normal file
38
modules/core/src/common/dto/website.dto.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const WebsiteSchema = z.preprocess(
|
||||||
|
(value) => {
|
||||||
|
if (typeof value !== "string") return value;
|
||||||
|
|
||||||
|
const trimmed = value.trim();
|
||||||
|
|
||||||
|
return trimmed === "" ? "" : trimmed;
|
||||||
|
},
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.refine(
|
||||||
|
(value) => {
|
||||||
|
if (!value) return true;
|
||||||
|
|
||||||
|
const candidate = /^https?:\/\//i.test(value) ? value : `https://${value}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(candidate);
|
||||||
|
|
||||||
|
return Boolean(url.hostname.includes("."));
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Introduce una URL válida",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.transform((value) => {
|
||||||
|
if (!value) return "";
|
||||||
|
|
||||||
|
return /^https?:\/\//i.test(value) ? value : `https://${value}`;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export type WebsiteDTO = z.infer<typeof WebsiteSchema>;
|
||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./catalogs";
|
export * from "./catalogs";
|
||||||
export * from "./dto";
|
export * from "./dto";
|
||||||
export * from "./helpers";
|
export * from "./helpers";
|
||||||
|
export * from "./json-data";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
|||||||
2
modules/core/src/common/json-data/index.ts
Normal file
2
modules/core/src/common/json-data/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./json-collection.provider";
|
||||||
|
export * from "./json-data.provider";
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { JsonDataProvider } from "./json-data.provider";
|
||||||
|
|
||||||
|
export abstract class JsonCollectionProvider<TItem> extends JsonDataProvider<readonly TItem[]> {
|
||||||
|
protected constructor(items: readonly TItem[]) {
|
||||||
|
super(Object.freeze([...items]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve la colección original en modo de solo lectura.
|
||||||
|
*
|
||||||
|
* El provider no debe modificar datos de referencia cargados desde JSON.
|
||||||
|
*/
|
||||||
|
public getAll(): readonly TItem[] {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construye un índice único y falla al cargar datos inconsistentes.
|
||||||
|
*
|
||||||
|
* No se permite que el último elemento sobrescriba silenciosamente al anterior,
|
||||||
|
* porque ocultaría errores en ficheros JSON de referencia.
|
||||||
|
*/
|
||||||
|
protected buildUniqueIndex(
|
||||||
|
indexName: string,
|
||||||
|
getKey: (item: TItem) => string
|
||||||
|
): ReadonlyMap<string, TItem> {
|
||||||
|
const index = new Map<string, TItem>();
|
||||||
|
|
||||||
|
for (const item of this.data) {
|
||||||
|
const key = getKey(item);
|
||||||
|
|
||||||
|
if (key.length === 0) {
|
||||||
|
throw new Error(`Invalid empty key for JSON index "${indexName}".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index.has(key)) {
|
||||||
|
throw new Error(`Duplicated key "${key}" for JSON index "${indexName}".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
index.set(key, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
modules/core/src/common/json-data/json-data.provider.ts
Normal file
7
modules/core/src/common/json-data/json-data.provider.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export abstract class JsonDataProvider<TData> {
|
||||||
|
protected readonly data: TData;
|
||||||
|
|
||||||
|
protected constructor(data: TData) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/customer-invoices",
|
"name": "@erp/customer-invoices",
|
||||||
"version": "0.6.9",
|
"version": "0.7.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
IPaymentMethodPublicFinder,
|
IPaymentMethodPublicFinder,
|
||||||
|
IPaymentTermPublicFinder,
|
||||||
ITaxDefinitionPublicFinder,
|
ITaxDefinitionPublicFinder,
|
||||||
ITaxRegimePublicFinder,
|
ITaxRegimePublicFinder,
|
||||||
} from "@erp/catalogs/api";
|
} from "@erp/catalogs/api";
|
||||||
@ -19,6 +20,7 @@ export function buildProformaCatalogResolvers(params: {
|
|||||||
taxDefinitionFinder: ITaxDefinitionPublicFinder;
|
taxDefinitionFinder: ITaxDefinitionPublicFinder;
|
||||||
taxRegimeFinder: ITaxRegimePublicFinder;
|
taxRegimeFinder: ITaxRegimePublicFinder;
|
||||||
paymentMethodFinder: IPaymentMethodPublicFinder;
|
paymentMethodFinder: IPaymentMethodPublicFinder;
|
||||||
|
paymentTermFinder: IPaymentTermPublicFinder;
|
||||||
}): IProformaCatalogResolvers {
|
}): IProformaCatalogResolvers {
|
||||||
return {
|
return {
|
||||||
taxResolver: buildProformaTaxResolver({
|
taxResolver: buildProformaTaxResolver({
|
||||||
@ -28,6 +30,7 @@ export function buildProformaCatalogResolvers(params: {
|
|||||||
|
|
||||||
paymentResolver: new ProformaPaymentResolver({
|
paymentResolver: new ProformaPaymentResolver({
|
||||||
paymentMethodFinder: params.paymentMethodFinder,
|
paymentMethodFinder: params.paymentMethodFinder,
|
||||||
|
paymentTermFinder: params.paymentTermFinder,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,7 +146,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
|||||||
companyId: params.companyId,
|
companyId: params.companyId,
|
||||||
status: InvoiceStatus.draft(),
|
status: InvoiceStatus.draft(),
|
||||||
|
|
||||||
invoiceNumber: invoiceNumber!,
|
//invoiceNumber: invoiceNumber!,
|
||||||
series: series!,
|
series: series!,
|
||||||
|
|
||||||
invoiceDate: invoiceDate!,
|
invoiceDate: invoiceDate!,
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.
|
|||||||
* - Se encargan de validar y convertir los datos recibidos en formatos adecuados para el dominio.
|
* - Se encargan de validar y convertir los datos recibidos en formatos adecuados para el dominio.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ProformaCreateInputProps = Omit<IProformaCreateProps, "items"> & {
|
export type ProformaCreateInputProps = Omit<IProformaCreateProps, "items" | "invoiceNumber"> & {
|
||||||
items: ProformaItemCreateInputProps[];
|
items: ProformaItemCreateInputProps[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { IPaymentMethodPublicFinder } from "@erp/catalogs/api";
|
import type { IPaymentMethodPublicFinder, IPaymentTermPublicFinder } from "@erp/catalogs/api";
|
||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
@ -26,13 +26,10 @@ export class ProformaPaymentResolver {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly deps: {
|
private readonly deps: {
|
||||||
paymentMethodFinder: IPaymentMethodPublicFinder;
|
paymentMethodFinder: IPaymentMethodPublicFinder;
|
||||||
|
paymentTermFinder: IPaymentTermPublicFinder;
|
||||||
}
|
}
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// Comprobar si existe un método de pago (boolean)
|
|
||||||
|
|
||||||
// Recuperar un método de pago si existe (Maybe)
|
|
||||||
|
|
||||||
// Solo asegurarnos que existe ese método de pago
|
// Solo asegurarnos que existe ese método de pago
|
||||||
public async ensurePaymentMethodById(params: {
|
public async ensurePaymentMethodById(params: {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
@ -59,4 +56,31 @@ export class ProformaPaymentResolver {
|
|||||||
|
|
||||||
return Result.ok(undefined);
|
return Result.ok(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Solo asegurarnos que existe ese método de pago
|
||||||
|
public async ensurePaymentTermById(params: {
|
||||||
|
companyId: UniqueID;
|
||||||
|
id: Maybe<UniqueID>;
|
||||||
|
}): Promise<Result<void, Error>> {
|
||||||
|
if (params.id.isNone()) {
|
||||||
|
return Result.ok(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = params.id.unwrap();
|
||||||
|
|
||||||
|
const result = await this.deps.paymentTermFinder.findByIdInCompany({
|
||||||
|
companyId: params.companyId,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Result.fail(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data.isNone()) {
|
||||||
|
return Result.fail(new Error(`Payment term not found: ${id.toString()}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,7 +78,7 @@ export class ProformaCreator implements IProformaCreator {
|
|||||||
|
|
||||||
private async resolveCreateProps(
|
private async resolveCreateProps(
|
||||||
props: ProformaCreateInputProps
|
props: ProformaCreateInputProps
|
||||||
): Promise<Result<IProformaCreateProps, Error>> {
|
): Promise<Result<Omit<IProformaCreateProps, "invoiceNumber">, Error>> {
|
||||||
// Tax Regime => comprobar que existe
|
// Tax Regime => comprobar que existe
|
||||||
const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({
|
const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({
|
||||||
companyId: props.companyId,
|
companyId: props.companyId,
|
||||||
|
|||||||
@ -74,6 +74,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
|||||||
taxDefinitionFinder: catalogs.taxDefinition.finder,
|
taxDefinitionFinder: catalogs.taxDefinition.finder,
|
||||||
taxRegimeFinder: catalogs.taxRegime.finder,
|
taxRegimeFinder: catalogs.taxRegime.finder,
|
||||||
paymentMethodFinder: catalogs.paymentMethod.finder,
|
paymentMethodFinder: catalogs.paymentMethod.finder,
|
||||||
|
paymentTermFinder: catalogs.paymentTerm.finder,
|
||||||
});
|
});
|
||||||
|
|
||||||
const readModelAssemblers = buildProformaReadModelAssemblers({
|
const readModelAssemblers = buildProformaReadModelAssemblers({
|
||||||
@ -82,6 +83,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
|||||||
});
|
});
|
||||||
|
|
||||||
const proformaNumberService = buildProformaNumberGenerator();
|
const proformaNumberService = buildProformaNumberGenerator();
|
||||||
|
|
||||||
const finder = buildProformaFinder(repository);
|
const finder = buildProformaFinder(repository);
|
||||||
|
|
||||||
const creator = buildProformaCreator({
|
const creator = buildProformaCreator({
|
||||||
|
|||||||
@ -11,6 +11,9 @@ type ProformaCatalogsDeps = {
|
|||||||
paymentMethod: {
|
paymentMethod: {
|
||||||
finder: CatalogsPublicServicesType["paymentMethods"]["finder"];
|
finder: CatalogsPublicServicesType["paymentMethods"]["finder"];
|
||||||
};
|
};
|
||||||
|
paymentTerm: {
|
||||||
|
finder: CatalogsPublicServicesType["paymentTerms"]["finder"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resolveProformaCatalogsDeps(params: ModuleParams): ProformaCatalogsDeps {
|
export function resolveProformaCatalogsDeps(params: ModuleParams): ProformaCatalogsDeps {
|
||||||
@ -35,6 +38,13 @@ export function resolveProformaCatalogsDeps(params: ModuleParams): ProformaCatal
|
|||||||
throw new Error("Missing public service: catalogs:paymentMethods.finder");
|
throw new Error("Missing public service: catalogs:paymentMethods.finder");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const paymentTerm =
|
||||||
|
params.getService<CatalogsPublicServicesType["paymentTerms"]>("catalogs:paymentTerms");
|
||||||
|
|
||||||
|
if (!paymentTerm?.finder) {
|
||||||
|
throw new Error("Missing public service: catalogs:paymentTerms.finder");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
taxDefinition: {
|
taxDefinition: {
|
||||||
finder: taxDefinition.finder,
|
finder: taxDefinition.finder,
|
||||||
@ -45,5 +55,8 @@ export function resolveProformaCatalogsDeps(params: ModuleParams): ProformaCatal
|
|||||||
paymentMethod: {
|
paymentMethod: {
|
||||||
finder: paymentMethod.finder,
|
finder: paymentMethod.finder,
|
||||||
},
|
},
|
||||||
|
paymentTerm: {
|
||||||
|
finder: paymentTerm.finder,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
|||||||
private _recipientMapper: SequelizeProformaRecipientDomainMapper;
|
private _recipientMapper: SequelizeProformaRecipientDomainMapper;
|
||||||
private _taxesMapper: SequelizeProformaTaxesDomainMapper;
|
private _taxesMapper: SequelizeProformaTaxesDomainMapper;
|
||||||
|
|
||||||
constructor(params: MapperParamsType) {
|
constructor(params?: MapperParamsType) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._itemsMapper = new SequelizeProformaItemDomainMapper();
|
this._itemsMapper = new SequelizeProformaItemDomainMapper();
|
||||||
|
|||||||
@ -57,7 +57,11 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: <ProformaUpdatePage />,
|
element: (
|
||||||
|
<ProformaLayout>
|
||||||
|
<ProformaUpdatePage />
|
||||||
|
</ProformaLayout>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { IModuleClient } from "@erp/core/client";
|
import type { IModuleClient, ModuleClientParams } from "@erp/core/client";
|
||||||
|
|
||||||
import { CustomerInvoiceRoutes } from "./customer-invoice-routes";
|
import { CustomerInvoiceRoutes } from "./customer-invoice-routes";
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ export const CustomerInvoicesModuleManifest: IModuleClient = {
|
|||||||
protected: true, // protegido por defecto
|
protected: true, // protegido por defecto
|
||||||
layout: "app-sidebar", // layout por defecto
|
layout: "app-sidebar", // layout por defecto
|
||||||
|
|
||||||
routes: (params) => CustomerInvoiceRoutes(params),
|
routes: (params: ModuleClientParams) => CustomerInvoiceRoutes(params),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CustomerInvoicesModuleManifest;
|
export default CustomerInvoicesModuleManifest;
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
|
|
||||||
export function ProformaLayout({ children }: PropsWithChildren) {
|
export function ProformaLayout({ children }: PropsWithChildren) {
|
||||||
return <div className="flex flex-col h-full w-full">{children}</div>;
|
return (
|
||||||
|
<div className="flex flex-col h-full w-full" id="proformas ">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -180,6 +180,7 @@ export const useUpdateProformaController = (
|
|||||||
|
|
||||||
options?.onUpdated?.(updated);
|
options?.onUpdated?.(updated);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
console.log(error);
|
||||||
const normalizedError = normalizeSubmitError(error);
|
const normalizedError = normalizeSubmitError(error);
|
||||||
|
|
||||||
// No revierto el form para que no se pierdan los cambios que
|
// No revierto el form para que no se pierdan los cambios que
|
||||||
@ -221,7 +222,6 @@ export const useUpdateProformaController = (
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
(errors: FieldErrors<ProformaUpdateForm>) => {
|
(errors: FieldErrors<ProformaUpdateForm>) => {
|
||||||
console.error(errors);
|
|
||||||
focusFirstInputFormError(form);
|
focusFirstInputFormError(form);
|
||||||
|
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
|
|||||||
@ -33,9 +33,6 @@ type ProformaUpdateEditorProps = {
|
|||||||
totalsCtrl: UseUpdateProformaTotalsControllerResult;
|
totalsCtrl: UseUpdateProformaTotalsControllerResult;
|
||||||
paymentCtrl: UseUpdateProformaPaymentControllerResult;
|
paymentCtrl: UseUpdateProformaPaymentControllerResult;
|
||||||
|
|
||||||
currencyCode?: string;
|
|
||||||
languageCode?: string;
|
|
||||||
|
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -51,23 +48,16 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
taxCtrl,
|
taxCtrl,
|
||||||
totalsCtrl,
|
totalsCtrl,
|
||||||
paymentCtrl,
|
paymentCtrl,
|
||||||
currencyCode,
|
|
||||||
languageCode,
|
|
||||||
className,
|
className,
|
||||||
}: ProformaUpdateEditorProps) => {
|
}: ProformaUpdateEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<div className={cn("p-6", className)}>
|
||||||
className={cn("space-y-6 2xl:space-y-12", className)}
|
<form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}>
|
||||||
id={formId}
|
<div className="grid grid-cols-1 2xl:grid-cols-4 gap-6 2xl:gap-12">
|
||||||
noValidate
|
<div className="2xl:col-span-3 space-y-6 gap-6 2xl:gap-12 2xl:space-y-12">
|
||||||
onKeyDown={preventEnterKeySubmitForm}
|
<div className="grid 2xl:grid-cols-3 gap-6 2xl:gap-6 space-y-12 2xl:space-y-12">
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-1 2xl:grid-cols-4 gap-4 2xl:gap-6">
|
|
||||||
<div className="2xl:col-span-3 space-y-4 gap-4 2xl:gap-6 2xl:space-y-6">
|
|
||||||
<div className="grid 2xl:grid-cols-3 gap-4 2xl:gap-6 space-y-4 2xl:space-y-6">
|
|
||||||
<ProformaUpdateHeaderEditor className="2xl:col-span-full" disabled={isSubmitting} />
|
<ProformaUpdateHeaderEditor className="2xl:col-span-full" disabled={isSubmitting} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -78,7 +68,7 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
totalsCtrl={totalsCtrl}
|
totalsCtrl={totalsCtrl}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="2xl:col-span-1 space-y-4 2xl:space-y-6">
|
<div className="2xl:col-span-1 space-y-6 2xl:space-y-12">
|
||||||
<ProformaUpdateRecipientEditor
|
<ProformaUpdateRecipientEditor
|
||||||
className="2xl:col-span-1"
|
className="2xl:col-span-1"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
@ -103,5 +93,6 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
</div>
|
</div>
|
||||||
{/* Footer fijo con totales */}
|
{/* Footer fijo con totales */}
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("form_fields.proformas.invoice_number.label")}
|
label={t("form_fields.proformas.invoice_number.label")}
|
||||||
maxLength={16}
|
maxLength={16}
|
||||||
name="reference"
|
name="invoiceNumber"
|
||||||
placeholder={t("form_fields.proformas.invoice_number.placeholder")}
|
placeholder={t("form_fields.proformas.invoice_number.placeholder")}
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
rightIcon={
|
rightIcon={
|
||||||
|
|||||||
@ -101,12 +101,9 @@ export const ProformaUpdatePage = () => {
|
|||||||
)}
|
)}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<ProformaUpdateEditorForm
|
<ProformaUpdateEditorForm
|
||||||
className="p-6 space-y-6 "
|
|
||||||
currencyCode={updateCtrl.currencyCode}
|
|
||||||
formId={updateCtrl.formId}
|
formId={updateCtrl.formId}
|
||||||
isSubmitting={updateCtrl.isUpdating}
|
isSubmitting={updateCtrl.isUpdating}
|
||||||
itemsCtrl={updateCtrl.itemsCtrl}
|
itemsCtrl={updateCtrl.itemsCtrl}
|
||||||
languageCode={updateCtrl.languageCode}
|
|
||||||
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
||||||
onCreateCustomerClick={() => null}
|
onCreateCustomerClick={() => null}
|
||||||
onReset={updateCtrl.resetForm}
|
onReset={updateCtrl.resetForm}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/customers",
|
"name": "@erp/customers",
|
||||||
"description": "Customers",
|
"description": "Customers",
|
||||||
"version": "0.6.9",
|
"version": "0.7.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
import type {
|
||||||
|
IPaymentMethodPublicFinder,
|
||||||
|
IPaymentTermPublicFinder,
|
||||||
|
ITaxDefinitionPublicFinder,
|
||||||
|
ITaxRegimePublicFinder,
|
||||||
|
} from "@erp/catalogs/api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CustomerPaymentResolver,
|
||||||
|
type CustomerTaxResolver,
|
||||||
|
buildCustomerTaxResolver,
|
||||||
|
} from "../services/catalog-resolver";
|
||||||
|
|
||||||
|
export interface ICustomerCatalogResolvers {
|
||||||
|
taxResolver: CustomerTaxResolver;
|
||||||
|
paymentResolver: CustomerPaymentResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCustomerCatalogResolvers(params: {
|
||||||
|
taxDefinitionFinder: ITaxDefinitionPublicFinder;
|
||||||
|
taxRegimeFinder: ITaxRegimePublicFinder;
|
||||||
|
paymentMethodFinder: IPaymentMethodPublicFinder;
|
||||||
|
paymentTermFinder: IPaymentTermPublicFinder;
|
||||||
|
}): ICustomerCatalogResolvers {
|
||||||
|
return {
|
||||||
|
taxResolver: buildCustomerTaxResolver({
|
||||||
|
taxDefinitionFinder: params.taxDefinitionFinder,
|
||||||
|
taxRegimeFinder: params.taxRegimeFinder,
|
||||||
|
}),
|
||||||
|
|
||||||
|
paymentResolver: new CustomerPaymentResolver({
|
||||||
|
paymentMethodFinder: params.paymentMethodFinder,
|
||||||
|
paymentTermFinder: params.paymentTermFinder,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import type { ICatalogs } from "@erp/core/api";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CreateCustomerInputMapper,
|
CreateCustomerInputMapper,
|
||||||
type ICreateCustomerInputMapper,
|
type ICreateCustomerInputMapper,
|
||||||
@ -12,11 +10,9 @@ export interface ICustomerInputMappers {
|
|||||||
updateInputMapper: IUpdateCustomerInputMapper;
|
updateInputMapper: IUpdateCustomerInputMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildCustomerInputMappers = (catalogs: ICatalogs): ICustomerInputMappers => {
|
export const buildCustomerInputMappers = (): ICustomerInputMappers => {
|
||||||
const { taxCatalog } = catalogs;
|
|
||||||
|
|
||||||
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
|
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
|
||||||
const createInputMapper = new CreateCustomerInputMapper({ taxCatalog });
|
const createInputMapper = new CreateCustomerInputMapper();
|
||||||
const updateInputMapper = new UpdateCustomerInputMapper();
|
const updateInputMapper = new UpdateCustomerInputMapper();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
import type {
|
||||||
|
IPaymentMethodPublicFinder,
|
||||||
|
IPaymentTermPublicFinder,
|
||||||
|
ITaxRegimePublicFinder,
|
||||||
|
} from "@erp/catalogs/api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CustomerFullReadModelAssembler,
|
||||||
|
type ICustomerFullReadModelAssembler,
|
||||||
|
} from "../services/assemblers";
|
||||||
|
|
||||||
|
export interface ICustomerReadModelAssemblers {
|
||||||
|
full: ICustomerFullReadModelAssembler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCustomerReadModelAssemblers(params: {
|
||||||
|
paymentMethodFinder: IPaymentMethodPublicFinder;
|
||||||
|
paymentTermFinder: IPaymentTermPublicFinder;
|
||||||
|
taxRegimeFinder: ITaxRegimePublicFinder;
|
||||||
|
}): ICustomerReadModelAssemblers {
|
||||||
|
return {
|
||||||
|
full: new CustomerFullReadModelAssembler({
|
||||||
|
paymentMethodFinder: params.paymentMethodFinder,
|
||||||
|
paymentTermFinder: params.paymentTermFinder,
|
||||||
|
taxRegimeFinder: params.taxRegimeFinder,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import type { ITransactionManager } from "@erp/core/api";
|
|||||||
|
|
||||||
import type { ICreateCustomerInputMapper, IUpdateCustomerInputMapper } from "../mappers";
|
import type { ICreateCustomerInputMapper, IUpdateCustomerInputMapper } from "../mappers";
|
||||||
import type { ICustomerCreator, ICustomerFinder, ICustomerUpdater } from "../services";
|
import type { ICustomerCreator, ICustomerFinder, ICustomerUpdater } from "../services";
|
||||||
|
import type { ICustomerFullReadModelAssembler } from "../services/assemblers";
|
||||||
import type {
|
import type {
|
||||||
ICustomerFullSnapshotBuilder,
|
ICustomerFullSnapshotBuilder,
|
||||||
ICustomerSummarySnapshotBuilder,
|
ICustomerSummarySnapshotBuilder,
|
||||||
@ -15,10 +16,11 @@ import {
|
|||||||
|
|
||||||
export function buildGetCustomerByIdUseCase(deps: {
|
export function buildGetCustomerByIdUseCase(deps: {
|
||||||
finder: ICustomerFinder;
|
finder: ICustomerFinder;
|
||||||
|
fullReadModelAssembler: ICustomerFullReadModelAssembler;
|
||||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||||
transactionManager: ITransactionManager;
|
transactionManager: ITransactionManager;
|
||||||
}) {
|
}) {
|
||||||
return new GetCustomerByIdUseCase(deps.finder, deps.fullSnapshotBuilder, deps.transactionManager);
|
return new GetCustomerByIdUseCase(deps);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildListCustomersUseCase(deps: {
|
export function buildListCustomersUseCase(deps: {
|
||||||
@ -36,29 +38,21 @@ export function buildListCustomersUseCase(deps: {
|
|||||||
export function buildCreateCustomerUseCase(deps: {
|
export function buildCreateCustomerUseCase(deps: {
|
||||||
creator: ICustomerCreator;
|
creator: ICustomerCreator;
|
||||||
dtoMapper: ICreateCustomerInputMapper;
|
dtoMapper: ICreateCustomerInputMapper;
|
||||||
|
fullReadModelAssembler: ICustomerFullReadModelAssembler;
|
||||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||||
transactionManager: ITransactionManager;
|
transactionManager: ITransactionManager;
|
||||||
}) {
|
}) {
|
||||||
return new CreateCustomerUseCase({
|
return new CreateCustomerUseCase(deps);
|
||||||
dtoMapper: deps.dtoMapper,
|
|
||||||
creator: deps.creator,
|
|
||||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
|
||||||
transactionManager: deps.transactionManager,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildUpdateCustomerUseCase(deps: {
|
export function buildUpdateCustomerUseCase(deps: {
|
||||||
updater: ICustomerUpdater;
|
updater: ICustomerUpdater;
|
||||||
dtoMapper: IUpdateCustomerInputMapper;
|
dtoMapper: IUpdateCustomerInputMapper;
|
||||||
|
fullReadModelAssembler: ICustomerFullReadModelAssembler;
|
||||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||||
transactionManager: ITransactionManager;
|
transactionManager: ITransactionManager;
|
||||||
}) {
|
}) {
|
||||||
return new UpdateCustomerUseCase({
|
return new UpdateCustomerUseCase(deps);
|
||||||
dtoMapper: deps.dtoMapper,
|
|
||||||
updater: deps.updater,
|
|
||||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
|
||||||
transactionManager: deps.transactionManager,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*export function buildReportCustomerUseCase(deps: {
|
/*export function buildReportCustomerUseCase(deps: {
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
export * from "./customer-catalog-resolvers.di";
|
||||||
export * from "./customer-creator.di";
|
export * from "./customer-creator.di";
|
||||||
export * from "./customer-finder.di";
|
export * from "./customer-finder.di";
|
||||||
export * from "./customer-input-mappers.di";
|
export * from "./customer-input-mappers.di";
|
||||||
|
export * from "./customer-read-model-assemblers.di";
|
||||||
export * from "./customer-snapshot-builders.di";
|
export * from "./customer-snapshot-builders.di";
|
||||||
export * from "./customer-updater.di";
|
export * from "./customer-updater.di";
|
||||||
export * from "./customer-use-cases.di";
|
export * from "./customer-use-cases.di";
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { JsonTaxCatalogProvider } from "@erp/core";
|
|
||||||
import {
|
import {
|
||||||
City,
|
City,
|
||||||
Country,
|
Country,
|
||||||
@ -35,12 +34,6 @@ export interface ICreateCustomerInputMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
||||||
private readonly taxCatalog: JsonTaxCatalogProvider;
|
|
||||||
|
|
||||||
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
|
|
||||||
this.taxCatalog = params.taxCatalog;
|
|
||||||
}
|
|
||||||
|
|
||||||
public map(
|
public map(
|
||||||
dto: CreateCustomerRequestDTO,
|
dto: CreateCustomerRequestDTO,
|
||||||
params: { companyId: UniqueID }
|
params: { companyId: UniqueID }
|
||||||
@ -52,7 +45,7 @@ export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
|||||||
const customerId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
const customerId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
||||||
|
|
||||||
const status = CustomerStatus.createActive();
|
const status = CustomerStatus.createActive();
|
||||||
const isCompany = dto.is_company === "true";
|
const isCompany = Boolean(dto.is_company);
|
||||||
|
|
||||||
const reference = extractOrPushError(
|
const reference = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.reference, (value) => Name.create(value)),
|
maybeFromNullableResult(dto.reference, (value) => Name.create(value)),
|
||||||
@ -164,6 +157,27 @@ export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const paymentMethodId = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.payment_method_id, (value) => UniqueID.create(value)),
|
||||||
|
"payment_method_id",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const paymentTermId = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.payment_term_id, (value) => UniqueID.create(value)),
|
||||||
|
"payment_term_id",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const taxRegimeCode = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.tax_regime_code, (value) => Result.ok(String(value))),
|
||||||
|
"tax_regime_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const usesEquivalenceSurcharge = Boolean(dto.uses_equivalence_surcharge);
|
||||||
|
const usesRetention = Boolean(dto.uses_retention);
|
||||||
|
|
||||||
const languageCode = extractOrPushError(
|
const languageCode = extractOrPushError(
|
||||||
LanguageCode.create(dto.language_code),
|
LanguageCode.create(dto.language_code),
|
||||||
"language_code",
|
"language_code",
|
||||||
@ -178,15 +192,6 @@ export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
|||||||
|
|
||||||
const defaultTaxes: TaxCode[] = [];
|
const defaultTaxes: TaxCode[] = [];
|
||||||
|
|
||||||
/*if (!isNullishOrEmpty(dto.default_taxes)) {
|
|
||||||
dto.default_taxes!.map((taxCode, index) => {
|
|
||||||
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
|
|
||||||
if (tax) {
|
|
||||||
defaultTaxes.add(tax!);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}*/
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors));
|
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors));
|
||||||
}
|
}
|
||||||
@ -225,7 +230,14 @@ export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
|||||||
website: website!,
|
website: website!,
|
||||||
|
|
||||||
legalRecord: legalRecord!,
|
legalRecord: legalRecord!,
|
||||||
defaultTaxes: defaultTaxes!,
|
|
||||||
|
paymentMethodId: paymentMethodId!,
|
||||||
|
paymentTermId: paymentTermId!,
|
||||||
|
taxRegimeCode: taxRegimeCode!,
|
||||||
|
|
||||||
|
usesEquivalenceSurcharge: usesEquivalenceSurcharge,
|
||||||
|
usesRetention: usesRetention,
|
||||||
|
|
||||||
languageCode: languageCode!,
|
languageCode: languageCode!,
|
||||||
currencyCode: currencyCode!,
|
currencyCode: currencyCode!,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./create-customer-input.mapper";
|
export * from "./create-customer-input.mapper";
|
||||||
|
export * from "./tax-definition-to-tax.mapper";
|
||||||
export * from "./update-customer-input.mapper";
|
export * from "./update-customer-input.mapper";
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
import type { TaxDefinitionPublicFamily, TaxDefinitionPublicModel } from "@erp/catalogs/api";
|
||||||
|
import { Tax, type TaxGroup, TaxPercentage } from "@erp/core/api";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sirve para mapear TaxDefinitionPublicModel a Tax,
|
||||||
|
* que es el modelo de dominio que se para proforma e issued-invoices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class TaxDefinitionToTaxMapper {
|
||||||
|
public toTax(taxDefinition: TaxDefinitionPublicModel): Result<Tax> {
|
||||||
|
const group = this.toTaxGroup(taxDefinition.taxFamily);
|
||||||
|
|
||||||
|
if (group.isFailure) {
|
||||||
|
return Result.fail(group.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Tax.create({
|
||||||
|
code: taxDefinition.code,
|
||||||
|
name: taxDefinition.name,
|
||||||
|
rate: TaxPercentage.create({
|
||||||
|
value: taxDefinition.rate.value,
|
||||||
|
}).data,
|
||||||
|
group: group.data,
|
||||||
|
calculationBehavior: taxDefinition.calculationBehavior,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private toTaxGroup(taxFamily: TaxDefinitionPublicFamily): Result<TaxGroup> {
|
||||||
|
switch (taxFamily) {
|
||||||
|
case "iva":
|
||||||
|
case "igic":
|
||||||
|
case "ipsi":
|
||||||
|
case "retention":
|
||||||
|
return Result.ok(taxFamily);
|
||||||
|
|
||||||
|
case "surcharge":
|
||||||
|
return Result.ok("surcharge");
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Result.fail(new Error(`Unsupported tax family: ${String(taxFamily)}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
TINNumber,
|
TINNumber,
|
||||||
TextValue,
|
TextValue,
|
||||||
URLAddress,
|
URLAddress,
|
||||||
type UniqueID,
|
UniqueID,
|
||||||
ValidationErrorCollection,
|
ValidationErrorCollection,
|
||||||
type ValidationErrorDetail,
|
type ValidationErrorDetail,
|
||||||
extractOrPushError,
|
extractOrPushError,
|
||||||
@ -115,30 +115,55 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
toPatchField(dto.payment_method_id).ifSetOrNull((paymentMethodId) => {
|
||||||
* Se mantiene la compatibilidad con el contrato actual.
|
customerPatchProps.paymentMethodId = extractOrPushError(
|
||||||
* Cuando defaultTaxes tenga un contrato final estable, aquí conviene
|
maybeFromNullableResult(paymentMethodId, (value) => UniqueID.create(value)),
|
||||||
* mapearlo a CustomerTaxes/Collection<TaxCode> con su schema semántico real.
|
"payment_method_id",
|
||||||
*/
|
errors
|
||||||
toPatchField(dto.default_taxes).ifSet((_defaultTaxes) => {
|
);
|
||||||
errors.push({
|
|
||||||
path: "default_taxes",
|
|
||||||
message: "default_taxes mapping is not implemented yet",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.payment_method_id).ifSetOrNull((paymentMethodId) => {
|
||||||
|
customerPatchProps.paymentMethodId = extractOrPushError(
|
||||||
|
maybeFromNullableResult(paymentMethodId, (value) => UniqueID.create(value)),
|
||||||
|
"payment_method_id",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.tax_regime_code).ifSetOrNull((taxRegimeCode) => {
|
||||||
|
console.log("Hay taxRegimeCode", taxRegimeCode);
|
||||||
|
customerPatchProps.taxRegimeCode = extractOrPushError(
|
||||||
|
maybeFromNullableResult(taxRegimeCode, (value) => Result.ok(String(value))),
|
||||||
|
"tax_regime_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.uses_equivalence_surcharge).ifSet((usesEquivalenceSurcharge) => {
|
||||||
|
customerPatchProps.usesEquivalenceSurcharge = usesEquivalenceSurcharge;
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.uses_retention).ifSet((usesRetention) => {
|
||||||
|
customerPatchProps.usesRetention = usesRetention;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.throwIfValidationErrors(errors);
|
||||||
|
|
||||||
const addressPatchProps = this.mapPostalAddress(dto.address, errors);
|
const addressPatchProps = this.mapPostalAddress(dto.address, errors);
|
||||||
|
|
||||||
|
this.throwIfValidationErrors(errors);
|
||||||
|
|
||||||
if (addressPatchProps) {
|
if (addressPatchProps) {
|
||||||
customerPatchProps.address = addressPatchProps;
|
customerPatchProps.address = addressPatchProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mapContact(dto.contact, customerPatchProps, errors);
|
this.mapContact(dto.contact, customerPatchProps, errors);
|
||||||
|
|
||||||
if (errors.length > 0) {
|
this.throwIfValidationErrors(errors);
|
||||||
return Result.fail(
|
|
||||||
new ValidationErrorCollection("Customer props mapping failed (update)", errors)
|
console.log("dto => ", dto);
|
||||||
);
|
console.log("customerPatchProps => ", customerPatchProps);
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(customerPatchProps);
|
return Result.ok(customerPatchProps);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@ -280,4 +305,10 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
import type { ITaxDefinitionPublicFinder, ITaxRegimePublicFinder } from "@erp/catalogs/api";
|
||||||
|
|
||||||
|
import { TaxDefinitionToTaxMapper } from "../../mappers";
|
||||||
|
|
||||||
|
import { CustomerTaxResolver } from "./customer-tax-resolver";
|
||||||
|
|
||||||
|
export function buildCustomerTaxResolver(deps: {
|
||||||
|
taxDefinitionFinder: ITaxDefinitionPublicFinder;
|
||||||
|
taxRegimeFinder: ITaxRegimePublicFinder;
|
||||||
|
}): CustomerTaxResolver {
|
||||||
|
return new CustomerTaxResolver({
|
||||||
|
taxDefinitionFinder: deps.taxDefinitionFinder,
|
||||||
|
taxRegimeFinder: deps.taxRegimeFinder,
|
||||||
|
taxMapper: new TaxDefinitionToTaxMapper(),
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import type { IPaymentMethodPublicFinder, IPaymentTermPublicFinder } from "@erp/catalogs/api";
|
||||||
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
export interface EnsureCustomerPaymentMethodByIdParams {
|
||||||
|
companyId: UniqueID;
|
||||||
|
id: Maybe<UniqueID>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resuelve formas de pago externas al agregado de customer usando servicios públicos de `catalogs`.
|
||||||
|
*
|
||||||
|
* Este servicio pertenece a Application porque coordina dependencias entre módulos
|
||||||
|
* antes de crear o actualizar una customer.
|
||||||
|
*
|
||||||
|
* No accede a repositorios de `catalogs`, no usa Sequelize y no debe utilizarse
|
||||||
|
* desde mappers de persistencia. Su responsabilidad es convertir contratos públicos
|
||||||
|
* de catálogos (`payment-methods`) en Value Objects propios del
|
||||||
|
* dominio de `customer`.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class CustomerPaymentResolver {
|
||||||
|
public constructor(
|
||||||
|
private readonly deps: {
|
||||||
|
paymentMethodFinder: IPaymentMethodPublicFinder;
|
||||||
|
paymentTermFinder: IPaymentTermPublicFinder;
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// Solo asegurarnos que existe ese plazo de apgo
|
||||||
|
public async ensurePaymentTermById(params: {
|
||||||
|
companyId: UniqueID;
|
||||||
|
id: Maybe<UniqueID>;
|
||||||
|
}): Promise<Result<void, Error>> {
|
||||||
|
if (params.id.isNone()) {
|
||||||
|
return Result.ok(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = params.id.unwrap();
|
||||||
|
|
||||||
|
const result = await this.deps.paymentTermFinder.findByIdInCompany({
|
||||||
|
companyId: params.companyId,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Result.fail(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data.isNone()) {
|
||||||
|
return Result.fail(new Error(`Payment term not found: ${id.toString()}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import type { ITaxDefinitionPublicFinder, ITaxRegimePublicFinder } from "@erp/catalogs/api";
|
||||||
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { TaxDefinitionToTaxMapper } from "../../mappers";
|
||||||
|
|
||||||
|
export interface EnsureCustomerTaxRegimeByCodeParams {
|
||||||
|
companyId: UniqueID;
|
||||||
|
code: Maybe<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resuelve datos fiscales externos al agregado de customer usando servicios públicos de `catalogs`.
|
||||||
|
*
|
||||||
|
* Este servicio pertenece a Application porque coordina dependencias entre módulos
|
||||||
|
* antes de crear o actualizar una customer.
|
||||||
|
*
|
||||||
|
* No accede a repositorios de `catalogs`, no usa Sequelize y no debe utilizarse
|
||||||
|
* desde mappers de persistencia. Su responsabilidad es convertir contratos públicos
|
||||||
|
* de catálogos (`tax-definitions` y `tax-regimes`) en Value Objects propios del
|
||||||
|
* dominio de `customer`.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class CustomerTaxResolver {
|
||||||
|
public constructor(
|
||||||
|
private readonly deps: {
|
||||||
|
taxDefinitionFinder: ITaxDefinitionPublicFinder;
|
||||||
|
taxRegimeFinder: ITaxRegimePublicFinder;
|
||||||
|
taxMapper: TaxDefinitionToTaxMapper;
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// Solo asegurarnos que existe un TaxRegime con ese "code"
|
||||||
|
public async ensureTaxRegimeByCode(params: {
|
||||||
|
companyId: UniqueID;
|
||||||
|
code: Maybe<string>;
|
||||||
|
}): Promise<Result<void, Error>> {
|
||||||
|
if (params.code.isNone()) {
|
||||||
|
return Result.ok(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = params.code.unwrap();
|
||||||
|
|
||||||
|
const result = await this.deps.taxRegimeFinder.findByCodeInCompany({
|
||||||
|
companyId: params.companyId,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Result.fail(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data.isNone()) {
|
||||||
|
return Result.fail(new Error(`Tax regime not found: ${code}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./build-customer-tax-resolver";
|
||||||
|
export * from "./customer-payment-resolver";
|
||||||
|
export * from "./customer-tax-resolver";
|
||||||
@ -5,6 +5,7 @@ import { Result } from "@repo/rdx-utils";
|
|||||||
import type { CreateCustomerRequestDTO } from "../../../common";
|
import type { CreateCustomerRequestDTO } from "../../../common";
|
||||||
import type { ICreateCustomerInputMapper } from "../mappers";
|
import type { ICreateCustomerInputMapper } from "../mappers";
|
||||||
import type { ICustomerCreator } from "../services";
|
import type { ICustomerCreator } from "../services";
|
||||||
|
import type { ICustomerFullReadModelAssembler } from "../services/assemblers";
|
||||||
import type { ICustomerFullSnapshotBuilder } from "../snapshot-builders";
|
import type { ICustomerFullSnapshotBuilder } from "../snapshot-builders";
|
||||||
|
|
||||||
type CreateCustomerUseCaseInput = {
|
type CreateCustomerUseCaseInput = {
|
||||||
@ -12,47 +13,46 @@ type CreateCustomerUseCaseInput = {
|
|||||||
dto: CreateCustomerRequestDTO;
|
dto: CreateCustomerRequestDTO;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CreateCustomerUseCaseDeps = {
|
export class CreateCustomerUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly deps: {
|
||||||
dtoMapper: ICreateCustomerInputMapper;
|
dtoMapper: ICreateCustomerInputMapper;
|
||||||
creator: ICustomerCreator;
|
creator: ICustomerCreator;
|
||||||
|
fullReadModelAssembler: ICustomerFullReadModelAssembler;
|
||||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||||
transactionManager: ITransactionManager;
|
transactionManager: ITransactionManager;
|
||||||
};
|
|
||||||
|
|
||||||
export class CreateCustomerUseCase {
|
|
||||||
private readonly dtoMapper: ICreateCustomerInputMapper;
|
|
||||||
private readonly creator: ICustomerCreator;
|
|
||||||
private readonly fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
|
||||||
private readonly transactionManager: ITransactionManager;
|
|
||||||
|
|
||||||
constructor(deps: CreateCustomerUseCaseDeps) {
|
|
||||||
this.dtoMapper = deps.dtoMapper;
|
|
||||||
this.creator = deps.creator;
|
|
||||||
this.fullSnapshotBuilder = deps.fullSnapshotBuilder;
|
|
||||||
this.transactionManager = deps.transactionManager;
|
|
||||||
}
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
public execute(params: CreateCustomerUseCaseInput) {
|
public execute(params: CreateCustomerUseCaseInput) {
|
||||||
const { dto, companyId } = params;
|
const { dto, companyId } = params;
|
||||||
|
|
||||||
const mappedPropsResult = this.dtoMapper.map(dto, { companyId });
|
const mappedPropsResult = this.deps.dtoMapper.map(dto, { companyId });
|
||||||
if (mappedPropsResult.isFailure) {
|
if (mappedPropsResult.isFailure) {
|
||||||
return mappedPropsResult;
|
return mappedPropsResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { props, id } = mappedPropsResult.data;
|
const { props, id } = mappedPropsResult.data;
|
||||||
|
|
||||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
return this.deps.transactionManager.complete(async (transaction: unknown) => {
|
||||||
try {
|
try {
|
||||||
const createResult = await this.creator.create({ companyId, id, props, transaction });
|
const createResult = await this.deps.creator.create({ companyId, id, props, transaction });
|
||||||
|
|
||||||
if (createResult.isFailure) {
|
if (createResult.isFailure) {
|
||||||
return createResult;
|
return createResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCustomer = createResult.data;
|
const readModelResult = await this.deps.fullReadModelAssembler.assemble({
|
||||||
|
companyId,
|
||||||
|
customer: createResult.data,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
const snapshot = this.fullSnapshotBuilder.toOutput(newCustomer);
|
if (readModelResult.isFailure) {
|
||||||
|
return Result.fail(readModelResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = this.deps.fullSnapshotBuilder.toOutput(readModelResult.data);
|
||||||
|
|
||||||
return Result.ok(snapshot);
|
return Result.ok(snapshot);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export * from "./create-customer.use-case";
|
export * from "./create-customer.use-case";
|
||||||
export * from "./get-customer-by-id.use-case";
|
export * from "./get-customer-by-id.use-case";
|
||||||
export * from "./list-customers.use-case";
|
export * from "./list-customers.use-case";
|
||||||
export * from "./update/update-customer.use-case";
|
export * from "./update-customer.use-case";
|
||||||
|
|||||||
@ -0,0 +1,75 @@
|
|||||||
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { UpdateCustomerByIdRequestDTO } from "../../../common/dto";
|
||||||
|
import type { CustomerPatchProps } from "../../domain";
|
||||||
|
import type { IUpdateCustomerInputMapper } from "../mappers";
|
||||||
|
import type { ICustomerUpdater } from "../services";
|
||||||
|
import type { ICustomerFullReadModelAssembler } from "../services/assemblers";
|
||||||
|
import type { ICustomerFullSnapshotBuilder } from "../snapshot-builders";
|
||||||
|
|
||||||
|
type UpdateCustomerUseCaseInput = {
|
||||||
|
companyId: UniqueID;
|
||||||
|
customer_id: string;
|
||||||
|
dto: UpdateCustomerByIdRequestDTO;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UpdateCustomerUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly deps: {
|
||||||
|
dtoMapper: IUpdateCustomerInputMapper;
|
||||||
|
updater: ICustomerUpdater;
|
||||||
|
fullReadModelAssembler: ICustomerFullReadModelAssembler;
|
||||||
|
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public execute(params: UpdateCustomerUseCaseInput) {
|
||||||
|
const { companyId, customer_id, dto } = params;
|
||||||
|
|
||||||
|
const idOrError = UniqueID.create(customer_id);
|
||||||
|
if (idOrError.isFailure) {
|
||||||
|
return Result.fail(idOrError.error);
|
||||||
|
}
|
||||||
|
const id = idOrError.data;
|
||||||
|
|
||||||
|
// Mapear DTO → props de dominio
|
||||||
|
const patchPropsResult = this.deps.dtoMapper.map(dto, { companyId });
|
||||||
|
if (patchPropsResult.isFailure) {
|
||||||
|
return patchPropsResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchProps: CustomerPatchProps = patchPropsResult.data;
|
||||||
|
|
||||||
|
return this.deps.transactionManager.complete(async (transaction: unknown) => {
|
||||||
|
try {
|
||||||
|
const updateResult = await this.deps.updater.update({
|
||||||
|
companyId,
|
||||||
|
id,
|
||||||
|
props: patchProps,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updateResult.isFailure) {
|
||||||
|
return Result.fail(updateResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const readModelResult = await this.deps.fullReadModelAssembler.assemble({
|
||||||
|
companyId,
|
||||||
|
customer: updateResult.data,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (readModelResult.isFailure) {
|
||||||
|
return Result.fail(readModelResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(this.deps.fullSnapshotBuilder.toOutput(readModelResult.data));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(error as Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./update-customer.use-case";
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
import type { ITransactionManager } from "@erp/core/api";
|
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
|
|
||||||
import type { CustomerPatchProps } from "../../../domain";
|
|
||||||
import type { IUpdateCustomerInputMapper } from "../../mappers";
|
|
||||||
import type { ICustomerUpdater } from "../../services";
|
|
||||||
import type { ICustomerFullSnapshotBuilder } from "../../snapshot-builders";
|
|
||||||
|
|
||||||
type UpdateCustomerUseCaseInput = {
|
|
||||||
companyId: UniqueID;
|
|
||||||
customer_id: string;
|
|
||||||
dto: UpdateCustomerByIdRequestDTO;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UpdateCustomerUseCaseDeps = {
|
|
||||||
dtoMapper: IUpdateCustomerInputMapper;
|
|
||||||
updater: ICustomerUpdater;
|
|
||||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
|
||||||
transactionManager: ITransactionManager;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class UpdateCustomerUseCase {
|
|
||||||
private readonly dtoMapper: IUpdateCustomerInputMapper;
|
|
||||||
private readonly updater: ICustomerUpdater;
|
|
||||||
private readonly fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
|
||||||
private readonly transactionManager: ITransactionManager;
|
|
||||||
|
|
||||||
constructor(deps: UpdateCustomerUseCaseDeps) {
|
|
||||||
this.dtoMapper = deps.dtoMapper;
|
|
||||||
this.updater = deps.updater;
|
|
||||||
this.fullSnapshotBuilder = deps.fullSnapshotBuilder;
|
|
||||||
this.transactionManager = deps.transactionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public execute(params: UpdateCustomerUseCaseInput) {
|
|
||||||
const { companyId, customer_id, dto } = params;
|
|
||||||
|
|
||||||
const idOrError = UniqueID.create(customer_id);
|
|
||||||
if (idOrError.isFailure) {
|
|
||||||
return Result.fail(idOrError.error);
|
|
||||||
}
|
|
||||||
const id = idOrError.data;
|
|
||||||
|
|
||||||
// Mapear DTO → props de dominio
|
|
||||||
const patchPropsResult = this.dtoMapper.map(dto, { companyId });
|
|
||||||
if (patchPropsResult.isFailure) {
|
|
||||||
return patchPropsResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const patchProps: CustomerPatchProps = patchPropsResult.data;
|
|
||||||
|
|
||||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
|
||||||
try {
|
|
||||||
const updateResult = await this.updater.update({
|
|
||||||
companyId,
|
|
||||||
id,
|
|
||||||
props: patchProps,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (updateResult.isFailure) {
|
|
||||||
return Result.fail(updateResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(this.fullSnapshotBuilder.toOutput(updateResult.data));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -10,7 +10,7 @@ export * from "./infrastructure/persistence/sequelize";
|
|||||||
export const customersAPIModule: IModuleServer = {
|
export const customersAPIModule: IModuleServer = {
|
||||||
name: "customers",
|
name: "customers",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
dependencies: [],
|
dependencies: ["catalogs"],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase de SETUP
|
* Fase de SETUP
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
import type { CatalogsPublicServicesType } from "@erp/catalogs/api";
|
||||||
|
import type { ModuleParams } from "@erp/core/api";
|
||||||
|
|
||||||
|
type CustomerCatalogsDeps = {
|
||||||
|
taxDefinition: {
|
||||||
|
finder: CatalogsPublicServicesType["taxDefinitions"]["finder"];
|
||||||
|
};
|
||||||
|
taxRegime: {
|
||||||
|
finder: CatalogsPublicServicesType["taxRegimes"]["finder"];
|
||||||
|
};
|
||||||
|
paymentMethod: {
|
||||||
|
finder: CatalogsPublicServicesType["paymentMethods"]["finder"];
|
||||||
|
};
|
||||||
|
paymentTerm: {
|
||||||
|
finder: CatalogsPublicServicesType["paymentTerms"]["finder"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveCustomerCatalogsDeps(params: ModuleParams): CustomerCatalogsDeps {
|
||||||
|
const taxDefinition =
|
||||||
|
params.getService<CatalogsPublicServicesType["taxDefinitions"]>("catalogs:taxDefinitions");
|
||||||
|
|
||||||
|
if (!taxDefinition?.finder) {
|
||||||
|
throw new Error("Missing public service: catalogs:taxDefinitions.finder");
|
||||||
|
}
|
||||||
|
|
||||||
|
const taxRegime =
|
||||||
|
params.getService<CatalogsPublicServicesType["taxRegimes"]>("catalogs:taxRegimes");
|
||||||
|
|
||||||
|
if (!taxRegime?.finder) {
|
||||||
|
throw new Error("Missing public service: catalogs:taxRegimes.finder");
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentMethod =
|
||||||
|
params.getService<CatalogsPublicServicesType["paymentMethods"]>("catalogs:paymentMethods");
|
||||||
|
|
||||||
|
if (!paymentMethod?.finder) {
|
||||||
|
throw new Error("Missing public service: catalogs:paymentMethods.finder");
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentTerm =
|
||||||
|
params.getService<CatalogsPublicServicesType["paymentTerms"]>("catalogs:paymentTerms");
|
||||||
|
|
||||||
|
if (!paymentTerm?.finder) {
|
||||||
|
throw new Error("Missing public service: catalogs:paymentTerms.finder");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
taxDefinition: {
|
||||||
|
finder: taxDefinition.finder,
|
||||||
|
},
|
||||||
|
taxRegime: {
|
||||||
|
finder: taxRegime.finder,
|
||||||
|
},
|
||||||
|
paymentMethod: {
|
||||||
|
finder: paymentMethod.finder,
|
||||||
|
},
|
||||||
|
paymentTerm: {
|
||||||
|
finder: paymentTerm.finder,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import type { ICatalogs } from "@erp/core/api";
|
|
||||||
|
|
||||||
import { SequelizeCustomerDomainMapper, SequelizeCustomerSummaryMapper } from "../persistence";
|
import { SequelizeCustomerDomainMapper, SequelizeCustomerSummaryMapper } from "../persistence";
|
||||||
|
|
||||||
export interface ICustomerPersistenceMappers {
|
export interface ICustomerPersistenceMappers {
|
||||||
@ -9,13 +7,9 @@ export interface ICustomerPersistenceMappers {
|
|||||||
//createMapper: CreateCustomerInputMapper;
|
//createMapper: CreateCustomerInputMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildCustomerPersistenceMappers = (
|
export const buildCustomerPersistenceMappers = (): ICustomerPersistenceMappers => {
|
||||||
catalogs: ICatalogs
|
|
||||||
): ICustomerPersistenceMappers => {
|
|
||||||
const { taxCatalog } = catalogs;
|
|
||||||
|
|
||||||
// Mappers para el repositorio
|
// Mappers para el repositorio
|
||||||
const domainMapper = new SequelizeCustomerDomainMapper({ taxCatalog });
|
const domainMapper = new SequelizeCustomerDomainMapper();
|
||||||
const summaryMapper = new SequelizeCustomerSummaryMapper();
|
const summaryMapper = new SequelizeCustomerSummaryMapper();
|
||||||
|
|
||||||
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
|
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { type SetupParams, buildCatalogs } from "@erp/core/api";
|
import type { SetupParams } from "@erp/core/api";
|
||||||
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
} from "../../application";
|
} from "../../application";
|
||||||
import type { ICustomerCreateProps } from "../../domain";
|
import type { ICustomerCreateProps } from "../../domain";
|
||||||
|
|
||||||
|
import { resolveCustomerCatalogsDeps } from "./customer-catalog-deps.di";
|
||||||
import { buildCustomerPersistenceMappers } from "./customer-persistence-mappers.di";
|
import { buildCustomerPersistenceMappers } from "./customer-persistence-mappers.di";
|
||||||
import { buildCustomerRepository } from "./customer-repositories.di";
|
import { buildCustomerRepository } from "./customer-repositories.di";
|
||||||
import type { CustomersInternalDeps } from "./customers.di";
|
import type { CustomersInternalDeps } from "./customers.di";
|
||||||
@ -19,10 +20,10 @@ export function buildCustomerPublicServices(
|
|||||||
deps: CustomersInternalDeps
|
deps: CustomersInternalDeps
|
||||||
): ICustomerPublicServices {
|
): ICustomerPublicServices {
|
||||||
const { database } = params;
|
const { database } = params;
|
||||||
const catalogs = buildCatalogs();
|
const catalogs = resolveCustomerCatalogsDeps(params);
|
||||||
|
|
||||||
// Infrastructure
|
// Infrastructure
|
||||||
const persistenceMappers = buildCustomerPersistenceMappers(catalogs);
|
const persistenceMappers = buildCustomerPersistenceMappers();
|
||||||
const repository = buildCustomerRepository({ database, mappers: persistenceMappers });
|
const repository = buildCustomerRepository({ database, mappers: persistenceMappers });
|
||||||
|
|
||||||
const finder = buildCustomerFinder({ repository });
|
const finder = buildCustomerFinder({ repository });
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
|
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type CreateCustomerUseCase,
|
type CreateCustomerUseCase,
|
||||||
@ -15,7 +15,10 @@ import {
|
|||||||
buildListCustomersUseCase,
|
buildListCustomersUseCase,
|
||||||
buildUpdateCustomerUseCase,
|
buildUpdateCustomerUseCase,
|
||||||
} from "../../application";
|
} from "../../application";
|
||||||
|
import { buildCustomerReadModelAssemblers } from "../../application/di";
|
||||||
|
import { buildCustomerCatalogResolvers } from "../../application/di/customer-catalog-resolvers.di";
|
||||||
|
|
||||||
|
import { resolveCustomerCatalogsDeps } from "./customer-catalog-deps.di";
|
||||||
import { buildCustomerPersistenceMappers } from "./customer-persistence-mappers.di";
|
import { buildCustomerPersistenceMappers } from "./customer-persistence-mappers.di";
|
||||||
import { buildCustomerRepository } from "./customer-repositories.di";
|
import { buildCustomerRepository } from "./customer-repositories.di";
|
||||||
|
|
||||||
@ -39,21 +42,33 @@ export type CustomersInternalDeps = {
|
|||||||
export function buildCustomersDependencies(params: ModuleParams): CustomersInternalDeps {
|
export function buildCustomersDependencies(params: ModuleParams): CustomersInternalDeps {
|
||||||
const { database } = params;
|
const { database } = params;
|
||||||
|
|
||||||
// Infrastructure
|
const catalogs = resolveCustomerCatalogsDeps(params);
|
||||||
|
|
||||||
const transactionManager = buildTransactionManager(database);
|
const transactionManager = buildTransactionManager(database);
|
||||||
const catalogs = buildCatalogs();
|
const persistenceMappers = buildCustomerPersistenceMappers();
|
||||||
const persistenceMappers = buildCustomerPersistenceMappers(catalogs);
|
|
||||||
|
|
||||||
const repository = buildCustomerRepository({ database, mappers: persistenceMappers });
|
const repository = buildCustomerRepository({ database, mappers: persistenceMappers });
|
||||||
//const numberService = buildCustomerNumberGenerator();
|
|
||||||
|
|
||||||
// Application helpers
|
const inputMappers = buildCustomerInputMappers();
|
||||||
const inputMappers = buildCustomerInputMappers(catalogs);
|
const snapshotBuilders = buildCustomerSnapshotBuilders();
|
||||||
|
|
||||||
|
const catalogResolvers = buildCustomerCatalogResolvers({
|
||||||
|
taxDefinitionFinder: catalogs.taxDefinition.finder,
|
||||||
|
taxRegimeFinder: catalogs.taxRegime.finder,
|
||||||
|
paymentMethodFinder: catalogs.paymentMethod.finder,
|
||||||
|
paymentTermFinder: catalogs.paymentTerm.finder,
|
||||||
|
});
|
||||||
|
|
||||||
|
const readModelAssemblers = buildCustomerReadModelAssemblers({
|
||||||
|
paymentMethodFinder: catalogs.paymentMethod.finder,
|
||||||
|
taxRegimeFinder: catalogs.taxRegime.finder,
|
||||||
|
paymentTermFinder: catalogs.paymentTerm.finder,
|
||||||
|
});
|
||||||
|
|
||||||
const finder = buildCustomerFinder({ repository });
|
const finder = buildCustomerFinder({ repository });
|
||||||
const creator = buildCustomerCreator({ repository });
|
const creator = buildCustomerCreator({ repository });
|
||||||
const updater = buildCustomerUpdater({ repository });
|
const updater = buildCustomerUpdater({ repository });
|
||||||
|
|
||||||
const snapshotBuilders = buildCustomerSnapshotBuilders();
|
|
||||||
//const documentGeneratorPipeline = buildCustomerDocumentService(params);
|
//const documentGeneratorPipeline = buildCustomerDocumentService(params);
|
||||||
|
|
||||||
// Internal use cases (factories)
|
// Internal use cases (factories)
|
||||||
@ -69,6 +84,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
|
|||||||
getCustomerById: () =>
|
getCustomerById: () =>
|
||||||
buildGetCustomerByIdUseCase({
|
buildGetCustomerByIdUseCase({
|
||||||
finder,
|
finder,
|
||||||
|
fullReadModelAssembler: readModelAssemblers.full,
|
||||||
fullSnapshotBuilder: snapshotBuilders.full,
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
}),
|
}),
|
||||||
@ -77,6 +93,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
|
|||||||
buildCreateCustomerUseCase({
|
buildCreateCustomerUseCase({
|
||||||
creator,
|
creator,
|
||||||
dtoMapper: inputMappers.createInputMapper,
|
dtoMapper: inputMappers.createInputMapper,
|
||||||
|
fullReadModelAssembler: readModelAssemblers.full,
|
||||||
fullSnapshotBuilder: snapshotBuilders.full,
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
}),
|
}),
|
||||||
@ -85,6 +102,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
|
|||||||
buildUpdateCustomerUseCase({
|
buildUpdateCustomerUseCase({
|
||||||
updater,
|
updater,
|
||||||
dtoMapper: inputMappers.updateInputMapper,
|
dtoMapper: inputMappers.updateInputMapper,
|
||||||
|
fullReadModelAssembler: readModelAssemblers.full,
|
||||||
fullSnapshotBuilder: snapshotBuilders.full,
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { TaxCatalogProvider } from "@erp/core";
|
|
||||||
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||||
import {
|
import {
|
||||||
City,
|
City,
|
||||||
@ -32,13 +31,6 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
|
|||||||
CustomerCreationAttributes,
|
CustomerCreationAttributes,
|
||||||
Customer
|
Customer
|
||||||
> {
|
> {
|
||||||
private readonly taxCatalog: TaxCatalogProvider;
|
|
||||||
|
|
||||||
constructor(params: { taxCatalog: TaxCatalogProvider }) {
|
|
||||||
super();
|
|
||||||
this.taxCatalog = params.taxCatalog;
|
|
||||||
}
|
|
||||||
|
|
||||||
public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result<Customer, Error> {
|
public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result<Customer, Error> {
|
||||||
try {
|
try {
|
||||||
const errors: ValidationErrorDetail[] = [];
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
|||||||
@ -67,11 +67,13 @@ export class CustomerRepository
|
|||||||
const dtoResult = this.domainMapper.mapToPersistence(customer);
|
const dtoResult = this.domainMapper.mapToPersistence(customer);
|
||||||
|
|
||||||
const { id, ...updatePayload } = dtoResult.data;
|
const { id, ...updatePayload } = dtoResult.data;
|
||||||
|
|
||||||
|
console.log(dtoResult.data);
|
||||||
|
|
||||||
const [affected] = await CustomerModel.update(updatePayload, {
|
const [affected] = await CustomerModel.update(updatePayload, {
|
||||||
where: { id /*, version */ },
|
where: { id /*, version */ },
|
||||||
//fields: Object.keys(updatePayload),
|
|
||||||
transaction,
|
transaction,
|
||||||
individualHooks: true,
|
//individualHooks: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (affected === 0) {
|
if (affected === 0) {
|
||||||
|
|||||||
@ -4,11 +4,10 @@ export const CreateCustomerRequestSchema = z.object({
|
|||||||
id: z.string().nonempty(),
|
id: z.string().nonempty(),
|
||||||
|
|
||||||
reference: z.string().optional(),
|
reference: z.string().optional(),
|
||||||
is_company: z.string().toLowerCase().default("true"),
|
is_company: z.boolean().default(true),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
trade_name: z.string().optional(),
|
trade_name: z.string().optional(),
|
||||||
tin: z.string().optional(),
|
tin: z.string().optional(),
|
||||||
default_taxes: z.string().default(""),
|
|
||||||
|
|
||||||
street: z.string().optional(),
|
street: z.string().optional(),
|
||||||
street2: z.string().optional(),
|
street2: z.string().optional(),
|
||||||
@ -32,8 +31,8 @@ export const CreateCustomerRequestSchema = z.object({
|
|||||||
payment_method_id: z.uuid().nullable().optional(),
|
payment_method_id: z.uuid().nullable().optional(),
|
||||||
payment_term_id: z.uuid().nullable().optional(),
|
payment_term_id: z.uuid().nullable().optional(),
|
||||||
tax_regime_code: z.string().nullable().optional(),
|
tax_regime_code: z.string().nullable().optional(),
|
||||||
uses_equivalence_surcharge: z.boolean().optional(),
|
uses_equivalence_surcharge: z.boolean().default(false),
|
||||||
uses_retention: z.boolean().optional(),
|
uses_retention: z.boolean().default(false),
|
||||||
|
|
||||||
language_code: z.string().toLowerCase().default("es"),
|
language_code: z.string().toLowerCase().default("es"),
|
||||||
currency_code: z.string().toUpperCase().default("EUR"),
|
currency_code: z.string().toUpperCase().default("EUR"),
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { useCustomerCreateController } from "./use-customer-create.controller";
|
import { useCustomerCreateController } from "./use-customer-create.controller";
|
||||||
|
|
||||||
export const useCustomerCreatePageController = () => {
|
export const useCustomerCreatePageController = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const returnTo = searchParams.get("returnTo") ?? "/customers";
|
||||||
|
|
||||||
const createCtrl = useCustomerCreateController({
|
const createCtrl = useCustomerCreateController({
|
||||||
onCreated: (customer) => navigate(`/customers/${customer.id}`),
|
onCreated: (customer) => navigate(`/customers/${customer.id}`),
|
||||||
@ -11,5 +14,6 @@ export const useCustomerCreatePageController = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
createCtrl,
|
createCtrl,
|
||||||
|
returnTo,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import type { FieldErrors } from "react-hook-form";
|
|||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import type { CreateCustomerParams, Customer } from "../../shared";
|
import type { CreateCustomerParams, Customer } from "../../shared";
|
||||||
import { useCustomerCreateMutation } from "../../shared/hooks/use-customer-create-mutation";
|
import { useCustomerCreateMutation } from "../../shared/hooks/use-customer-create-mutation";
|
||||||
|
import { mapCustomerToCustomerCreateForm } from "../adapters";
|
||||||
import {
|
import {
|
||||||
type CustomerCreateForm,
|
type CustomerCreateForm,
|
||||||
CustomerCreateFormSchema,
|
CustomerCreateFormSchema,
|
||||||
@ -47,13 +48,19 @@ export const useCustomerCreateController = (options?: UseCustomerCreateControlle
|
|||||||
};
|
};
|
||||||
|
|
||||||
const submitHandler = form.handleSubmit(
|
const submitHandler = form.handleSubmit(
|
||||||
async (formData) => {
|
async (formData: CustomerCreateForm) => {
|
||||||
const params: CreateCustomerParams = buildCreateCustomerParams(formData);
|
const params: CreateCustomerParams = buildCreateCustomerParams(formData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Enviamos cambios al servidor
|
// Enviamos cambios al servidor
|
||||||
const created = await mutateAsync(params); // payload es CustomerCreatePayload
|
const created = await mutateAsync(params); // payload es CustomerCreatePayload
|
||||||
|
|
||||||
|
// Ha ido bien -> actualizamos form con datos reales
|
||||||
|
// keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render.
|
||||||
|
form.reset(mapCustomerToCustomerCreateForm(created), {
|
||||||
|
keepDirty: false,
|
||||||
|
});
|
||||||
|
|
||||||
if (options?.successToasts !== false) {
|
if (options?.successToasts !== false) {
|
||||||
showSuccessToast(
|
showSuccessToast(
|
||||||
t("create.feedback.success.title"),
|
t("create.feedback.success.title"),
|
||||||
|
|||||||
@ -19,7 +19,6 @@ export interface CustomerCreateForm {
|
|||||||
name: string;
|
name: string;
|
||||||
tradeName: string;
|
tradeName: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
defaultTaxes: string[];
|
|
||||||
|
|
||||||
street: string;
|
street: string;
|
||||||
street2: string;
|
street2: string;
|
||||||
|
|||||||
@ -1,3 +1,13 @@
|
|||||||
|
import {
|
||||||
|
CountryCodeSchema,
|
||||||
|
CurrencyCodeSchema,
|
||||||
|
LandPhoneSchema,
|
||||||
|
LanguageCodeSchema,
|
||||||
|
MobilePhoneSchema,
|
||||||
|
PostalCodeSchema,
|
||||||
|
TinSchema,
|
||||||
|
WebsiteSchema,
|
||||||
|
} from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,32 +27,31 @@ import { z } from "zod/v4";
|
|||||||
export const CustomerCreateFormSchema = z.object({
|
export const CustomerCreateFormSchema = z.object({
|
||||||
reference: z.string(),
|
reference: z.string(),
|
||||||
isCompany: z.boolean(),
|
isCompany: z.boolean(),
|
||||||
|
|
||||||
name: z.string().min(1, "El nombre es obligatorio"),
|
name: z.string().min(1, "El nombre es obligatorio"),
|
||||||
tradeName: z.string(),
|
tradeName: z.string(),
|
||||||
tin: z.string(),
|
tin: TinSchema.or(z.literal("")),
|
||||||
|
|
||||||
defaultTaxes: z.array(z.string()),
|
|
||||||
|
|
||||||
street: z.string(),
|
street: z.string(),
|
||||||
street2: z.string(),
|
street2: z.string(),
|
||||||
city: z.string(),
|
city: z.string(),
|
||||||
province: z.string(),
|
province: z.string(),
|
||||||
postalCode: z.string(),
|
postalCode: PostalCodeSchema.or(z.literal("")),
|
||||||
country: z.string().min(1, "El país es obligatorio"),
|
country: CountryCodeSchema.or(z.literal("")),
|
||||||
|
|
||||||
primaryEmail: z.email("Email inválido"),
|
primaryEmail: z.email("Email inválido").or(z.literal("")),
|
||||||
secondaryEmail: z.email("Email inválido"),
|
secondaryEmail: z.email("Email inválido").or(z.literal("")),
|
||||||
|
|
||||||
primaryPhone: z.string(),
|
primaryPhone: LandPhoneSchema.or(z.literal("")),
|
||||||
secondaryPhone: z.string(),
|
secondaryPhone: LandPhoneSchema.or(z.literal("")),
|
||||||
primaryMobile: z.string(),
|
primaryMobile: MobilePhoneSchema.or(z.literal("")),
|
||||||
secondaryMobile: z.string(),
|
secondaryMobile: MobilePhoneSchema.or(z.literal("")),
|
||||||
|
|
||||||
fax: z.string(),
|
fax: LandPhoneSchema.or(z.literal("")),
|
||||||
website: z.url("URL inválida"),
|
website: WebsiteSchema.or(z.literal("")),
|
||||||
|
|
||||||
legalRecord: z.string(),
|
legalRecord: z.string().or(z.literal("")),
|
||||||
|
|
||||||
languageCode: z.string().min(1, "El idioma es obligatorio"),
|
languageCode: LanguageCodeSchema,
|
||||||
currencyCode: z.string().min(1, "La moneda es obligatoria"),
|
currencyCode: CurrencyCodeSchema,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,122 @@
|
|||||||
|
import { PageFormHeader, PageKeyboardShortcutsButton } from "@erp/core/components";
|
||||||
|
import {
|
||||||
|
CancelActionButton,
|
||||||
|
FormActionsBar,
|
||||||
|
type FormSecondaryAction,
|
||||||
|
FormSecondaryActionsMenu,
|
||||||
|
RhfSubmitActionButton,
|
||||||
|
} from "@repo/rdx-ui/components";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
export interface CustomerCreateHeaderLabels {
|
||||||
|
title: string;
|
||||||
|
back: string;
|
||||||
|
modified: string;
|
||||||
|
cancel: string;
|
||||||
|
save: string;
|
||||||
|
saving: string;
|
||||||
|
moreActions: string;
|
||||||
|
keyboardShortcuts: string;
|
||||||
|
delete: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerCreateHeaderProps {
|
||||||
|
formId?: string;
|
||||||
|
|
||||||
|
labels: CustomerCreateHeaderLabels;
|
||||||
|
|
||||||
|
onCancel?: () => void;
|
||||||
|
onDuplicate?: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
isSaving?: boolean;
|
||||||
|
hasChanges?: boolean;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomerCreateHeader = ({
|
||||||
|
formId,
|
||||||
|
labels,
|
||||||
|
onCancel,
|
||||||
|
onDuplicate,
|
||||||
|
onDelete,
|
||||||
|
disabled = false,
|
||||||
|
readOnly = false,
|
||||||
|
isSaving = false,
|
||||||
|
hasChanges = false,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: CustomerCreateHeaderProps) => {
|
||||||
|
const computedDisabled = disabled || isSaving;
|
||||||
|
|
||||||
|
const secondaryActions: FormSecondaryAction[] = [
|
||||||
|
/*{
|
||||||
|
id: "duplicate",
|
||||||
|
label: labels.duplicate,
|
||||||
|
icon: <CopyIcon aria-hidden="true" className="mr-2 size-4" />,
|
||||||
|
hidden: !onDuplicate,
|
||||||
|
disabled: computedDisabled,
|
||||||
|
onSelect: () => onDuplicate?.(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "delete",
|
||||||
|
label: labels.delete,
|
||||||
|
icon: <Trash2Icon aria-hidden="true" className="mr-2 size-4" />,
|
||||||
|
hidden: !onDelete || readOnly,
|
||||||
|
disabled: computedDisabled,
|
||||||
|
destructive: true,
|
||||||
|
onSelect: () => onDelete?.(),
|
||||||
|
},*/
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageFormHeader
|
||||||
|
actions={
|
||||||
|
<FormActionsBar align="end" reverseOnMobile={false}>
|
||||||
|
<PageKeyboardShortcutsButton
|
||||||
|
className="hidden sm:flex"
|
||||||
|
label={labels.keyboardShortcuts}
|
||||||
|
shortcuts={[
|
||||||
|
{ keys: "Ctrl+S", label: labels.save },
|
||||||
|
{ keys: "Esc", label: labels.cancel },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CancelActionButton
|
||||||
|
className="hidden sm:flex"
|
||||||
|
disabled={computedDisabled}
|
||||||
|
label={labels.cancel}
|
||||||
|
onCancel={() => onCancel?.()}
|
||||||
|
variant="outline"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RhfSubmitActionButton
|
||||||
|
busyLabel={labels.saving}
|
||||||
|
disabled={computedDisabled || readOnly}
|
||||||
|
formId={formId}
|
||||||
|
isBusy={isSaving}
|
||||||
|
label={labels.save}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormSecondaryActionsMenu
|
||||||
|
actions={secondaryActions}
|
||||||
|
disabled={computedDisabled}
|
||||||
|
label={labels.moreActions}
|
||||||
|
/>
|
||||||
|
</FormActionsBar>
|
||||||
|
}
|
||||||
|
backLabel={labels.back}
|
||||||
|
className={className}
|
||||||
|
onBack={onCancel}
|
||||||
|
showStatus={hasChanges}
|
||||||
|
statusLabel={labels.modified}
|
||||||
|
title={labels.title}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PageFormHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
modules/customers/src/web/create/ui/blocks/index.ts
Normal file
1
modules/customers/src/web/create/ui/blocks/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-create-header";
|
||||||
@ -15,15 +15,13 @@ import { useEffect } from "react";
|
|||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import type { CustomerUpdateForm } from "../../entities";
|
import type { CustomerCreateForm } from "../../entities";
|
||||||
|
|
||||||
import { CustomerTaxesMultiSelect } from "./customer-taxes-multi-select";
|
|
||||||
|
|
||||||
interface CustomerBasicInfoFieldsProps extends React.ComponentProps<typeof FieldSet> {}
|
interface CustomerBasicInfoFieldsProps extends React.ComponentProps<typeof FieldSet> {}
|
||||||
|
|
||||||
export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicInfoFieldsProps) => {
|
export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicInfoFieldsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control, setFocus } = useFormContext<CustomerUpdateForm>();
|
const { control, setFocus } = useFormContext<CustomerCreateForm>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFocus("name");
|
setFocus("name");
|
||||||
@ -60,17 +58,17 @@ export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicIn
|
|||||||
disabled={field.disabled}
|
disabled={field.disabled}
|
||||||
name={field.name}
|
name={field.name}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
field.onChange(value === "true");
|
field.onChange(value === "company");
|
||||||
}}
|
}}
|
||||||
required
|
required
|
||||||
value={field.value ? "true" : "false"}
|
value={field.value ? "company" : "individual"}
|
||||||
>
|
>
|
||||||
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
||||||
<FieldContent>
|
<FieldContent>
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
aria-invalid={fieldState.invalid}
|
aria-invalid={fieldState.invalid}
|
||||||
id="customer-type-company"
|
id="customer-type-company"
|
||||||
value="true"
|
value="company"
|
||||||
/>
|
/>
|
||||||
<FieldLabel
|
<FieldLabel
|
||||||
className="cursor-pointer text-sm font-medium leading-none"
|
className="cursor-pointer text-sm font-medium leading-none"
|
||||||
@ -86,7 +84,7 @@ export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicIn
|
|||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
aria-invalid={fieldState.invalid}
|
aria-invalid={fieldState.invalid}
|
||||||
id="customer-type-individual"
|
id="customer-type-individual"
|
||||||
value="false"
|
value="individual"
|
||||||
/>
|
/>
|
||||||
<FieldLabel
|
<FieldLabel
|
||||||
className="cursor-pointer text-sm font-medium leading-none"
|
className="cursor-pointer text-sm font-medium leading-none"
|
||||||
@ -111,7 +109,6 @@ export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicIn
|
|||||||
label={t("fields.tin.label")}
|
label={t("fields.tin.label")}
|
||||||
name="tin"
|
name="tin"
|
||||||
placeholder={t("fields.tin.placeholder")}
|
placeholder={t("fields.tin.placeholder")}
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@ -130,32 +127,6 @@ export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicIn
|
|||||||
placeholder={t("fields.reference.placeholder")}
|
placeholder={t("fields.reference.placeholder")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Field className="lg:col-span-2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="defaultTaxes"
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<Field className="gap-1" data-invalid={fieldState.invalid}>
|
|
||||||
<FieldLabel htmlFor="defaultTaxes">
|
|
||||||
{t("fields.default_taxes.label")}
|
|
||||||
</FieldLabel>
|
|
||||||
|
|
||||||
<CustomerTaxesMultiSelect
|
|
||||||
description={t("fields.default_taxes.description")}
|
|
||||||
label={t("fields.default_taxes.label")}
|
|
||||||
onChange={field.onChange}
|
|
||||||
placeholder={t("fields.default_taxes.placeholder")}
|
|
||||||
required
|
|
||||||
value={field.value}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FieldDescription>{t("fields.default_taxes.description")}</FieldDescription>
|
|
||||||
<FieldError errors={[fieldState.error]} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<TextAreaField
|
<TextAreaField
|
||||||
className="lg:col-span-full"
|
className="lg:col-span-full"
|
||||||
description={t("fields.legal_record.description")}
|
description={t("fields.legal_record.description")}
|
||||||
|
|||||||
@ -37,7 +37,6 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
|
|||||||
}
|
}
|
||||||
name="primaryEmail"
|
name="primaryEmail"
|
||||||
placeholder={t("fields.email_primary.placeholder")}
|
placeholder={t("fields.email_primary.placeholder")}
|
||||||
required
|
|
||||||
typePreset="email"
|
typePreset="email"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -1,61 +1,64 @@
|
|||||||
import { ErrorAlert, PageHeader } from "@erp/core/components";
|
import { ErrorAlert } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
|
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { useCustomerCreatePageController } from "../../controllers";
|
import { useCustomerCreatePageController } from "../../controllers";
|
||||||
|
import { CustomerCreateHeader } from "../blocks";
|
||||||
import { CustomerCreateEditorForm } from "../editor";
|
import { CustomerCreateEditorForm } from "../editor";
|
||||||
|
|
||||||
export const CustomerCreatePage = () => {
|
export const CustomerCreatePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { createCtrl } = useCustomerCreatePageController();
|
const { createCtrl, returnTo } = useCustomerCreatePageController();
|
||||||
|
|
||||||
const { form, formId, onSubmit, resetForm, isCreating, isCreateError, createError } = createCtrl;
|
const { form, formId, onSubmit, resetForm, isCreating, isCreateError, createError } = createCtrl;
|
||||||
|
|
||||||
|
const { navigateBack } = useReturnToNavigation({
|
||||||
|
fallbackPath: returnTo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCancel = () => navigateBack();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
||||||
<FormProvider {...form}>
|
<FormProvider {...createCtrl.form}>
|
||||||
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
|
<UnsavedChangesProvider isDirty={createCtrl.form.formState.isDirty}>
|
||||||
<AppHeader className="mx-auto max-w-7xl space-y-4">
|
<CustomerCreateHeader
|
||||||
<PageHeader
|
formId={createCtrl.formId}
|
||||||
description={t("create.description")}
|
hasChanges={createCtrl.form.formState.isDirty}
|
||||||
onBackClick={() => navigate("/customers/list")}
|
isSaving={createCtrl.form.formState.isSubmitting}
|
||||||
rightSlot={
|
labels={{
|
||||||
/*
|
title: "Nuevo cliente",
|
||||||
<FormCommitButtonGroup
|
back: "Volver",
|
||||||
cancel={{
|
modified: "",
|
||||||
to: "/customers/list",
|
cancel: "Cancelar",
|
||||||
}}
|
save: "Guardar",
|
||||||
disabled={createCtrl.isCreating}
|
saving: "Guardando...",
|
||||||
isLoading={createCtrl.isCreating}
|
moreActions: "Más acciones",
|
||||||
onReset={createCtrl.form.formState.isDirty ? createCtrl.resetForm : undefined}
|
keyboardShortcuts: "Ver atajos de teclado",
|
||||||
submit={{
|
delete: "Eliminar",
|
||||||
formId: createCtrl.formId,
|
|
||||||
}}
|
}}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
//onDelete={openDeleteDialog}
|
||||||
|
//onDuplicate={handleDuplicate}
|
||||||
|
//onExportPdf={handleExportPdf}
|
||||||
/>
|
/>
|
||||||
*/ <></>
|
|
||||||
}
|
|
||||||
title={t("create.title")}
|
|
||||||
/>
|
|
||||||
</AppHeader>
|
|
||||||
|
|
||||||
<AppContent className="mx-auto max-w-7xl space-y-4">
|
|
||||||
{isCreateError && (
|
{isCreateError && (
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
message={(createError as Error)?.message ?? t("create.errors.message")}
|
message={(createError as Error)?.message ?? t("create.errors.message")}
|
||||||
title={t("create.errors.title")}
|
title={t("create.errors.title")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
<CustomerCreateEditorForm
|
<CustomerCreateEditorForm
|
||||||
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto mt-6"
|
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto my-6"
|
||||||
formId={formId}
|
formId={formId}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
</AppContent>
|
</div>
|
||||||
</UnsavedChangesProvider>
|
</UnsavedChangesProvider>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -30,11 +30,10 @@ export const buildCreateCustomerParams = (formData: CustomerCreateForm): CreateC
|
|||||||
data: {
|
data: {
|
||||||
id,
|
id,
|
||||||
reference: formData.reference,
|
reference: formData.reference,
|
||||||
is_company: formData.isCompany ? "1" : "0",
|
is_company: formData.isCompany,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
trade_name: formData.tradeName,
|
trade_name: formData.tradeName,
|
||||||
tin: formData.tin,
|
tin: formData.tin,
|
||||||
default_taxes: formData.defaultTaxes.join(";"),
|
|
||||||
|
|
||||||
street: formData.street,
|
street: formData.street,
|
||||||
street2: formData.street2,
|
street2: formData.street2,
|
||||||
@ -55,6 +54,9 @@ export const buildCreateCustomerParams = (formData: CustomerCreateForm): CreateC
|
|||||||
|
|
||||||
legal_record: formData.legalRecord,
|
legal_record: formData.legalRecord,
|
||||||
|
|
||||||
|
uses_equivalence_surcharge: false,
|
||||||
|
uses_retention: false,
|
||||||
|
|
||||||
language_code: formData.languageCode,
|
language_code: formData.languageCode,
|
||||||
currency_code: formData.currencyCode,
|
currency_code: formData.currencyCode,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -51,7 +51,11 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: <CustomerCreatePage />,
|
element: (
|
||||||
|
<CustomerLayout>
|
||||||
|
<CustomerCreatePage />
|
||||||
|
</CustomerLayout>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -60,7 +64,11 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: <CustomerUpdatePage />,
|
element: (
|
||||||
|
<CustomerLayout>
|
||||||
|
<CustomerUpdatePage />
|
||||||
|
</CustomerLayout>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -69,7 +77,11 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: <CustomerViewPage />,
|
element: (
|
||||||
|
<CustomerLayout>
|
||||||
|
<CustomerViewPage />
|
||||||
|
</CustomerLayout>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -8,13 +8,12 @@ const MODULE_VERSION = "1.0.0";
|
|||||||
export const CustomersModuleManifest: IModuleClient = {
|
export const CustomersModuleManifest: IModuleClient = {
|
||||||
name: MODULE_NAME,
|
name: MODULE_NAME,
|
||||||
version: MODULE_VERSION,
|
version: MODULE_VERSION,
|
||||||
dependencies: ["auth", "Core"],
|
dependencies: ["auth", "Core", "Catalogs"],
|
||||||
protected: true,
|
|
||||||
layout: "app",
|
|
||||||
|
|
||||||
routes: (params: ModuleClientParams) => {
|
protected: true, // protegido por defecto
|
||||||
return CustomerRoutes(params);
|
layout: "app-sidebar", // layout por defecto
|
||||||
},
|
|
||||||
|
routes: (params: ModuleClientParams) => CustomerRoutes(params),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CustomersModuleManifest;
|
export default CustomersModuleManifest;
|
||||||
|
|||||||
@ -12,11 +12,6 @@ import type { Customer } from "../entities";
|
|||||||
|
|
||||||
export const GetCustomerByIdAdapter = {
|
export const GetCustomerByIdAdapter = {
|
||||||
fromDTO(dto: GetCustomerByIdResult, context?: unknown): Customer {
|
fromDTO(dto: GetCustomerByIdResult, context?: unknown): Customer {
|
||||||
const taxesAdapter = (taxes: string | null) =>
|
|
||||||
taxes?.split(";").filter((item) => item !== "#" && item.trim() !== "") || null;
|
|
||||||
|
|
||||||
const defaultTaxes = taxesAdapter(dto.default_taxes);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
companyId: dto.company_id,
|
companyId: dto.company_id,
|
||||||
@ -49,7 +44,14 @@ export const GetCustomerByIdAdapter = {
|
|||||||
|
|
||||||
legalRecord: dto.legal_record,
|
legalRecord: dto.legal_record,
|
||||||
|
|
||||||
defaultTaxes: defaultTaxes,
|
paymentMethod: dto.payment_method,
|
||||||
|
paymentTerm: dto.payment_term,
|
||||||
|
taxRegime: dto.tax_regime,
|
||||||
|
fiscalDefaults: {
|
||||||
|
usesEquivalenceSurcharge: dto.fiscal_defaults.uses_equivalence_surcharge,
|
||||||
|
usesRetention: dto.fiscal_defaults.uses_retention,
|
||||||
|
},
|
||||||
|
|
||||||
status: dto.status,
|
status: dto.status,
|
||||||
languageCode: dto.language_code,
|
languageCode: dto.language_code,
|
||||||
currencyCode: dto.currency_code,
|
currencyCode: dto.currency_code,
|
||||||
|
|||||||
@ -38,7 +38,26 @@ export interface Customer {
|
|||||||
|
|
||||||
legalRecord: string | null;
|
legalRecord: string | null;
|
||||||
|
|
||||||
defaultTaxes: string[] | null;
|
paymentMethod: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
paymentTerm: {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
taxRegime: {
|
||||||
|
code: string;
|
||||||
|
description: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
fiscalDefaults: {
|
||||||
|
usesEquivalenceSurcharge: boolean;
|
||||||
|
usesRetention: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
languageCode: string;
|
languageCode: string;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
|
|||||||
@ -13,3 +13,17 @@ export function toValidationErrors(error: ZodError<unknown>) {
|
|||||||
message: err.message,
|
message: err.message,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
ZodError: [
|
||||||
|
{
|
||||||
|
"expected": "boolean",
|
||||||
|
"code": "invalid_type",
|
||||||
|
"path": [
|
||||||
|
"is_company"
|
||||||
|
],
|
||||||
|
"message": "Invalid input: expected boolean, received string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
|
|
||||||
export const CustomerLayout = ({ children }: PropsWithChildren) => {
|
export const CustomerLayout = ({ children }: PropsWithChildren) => {
|
||||||
return <div className="flex flex-col h-full w-full">{children}</div>;
|
return (
|
||||||
|
<div className="flex flex-col h-full w-full" id="customers">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,15 +13,16 @@ import type { CustomerUpdateForm } from "../entities"; /**
|
|||||||
*/
|
*/
|
||||||
import { buildCustomerUpdateDefault } from "../utils";
|
import { buildCustomerUpdateDefault } from "../utils";
|
||||||
|
|
||||||
export const mapCustomerToCustomerUpdateForm = (customer: Customer): CustomerUpdateForm => ({
|
export const mapCustomerToCustomerUpdateForm = (customer: Customer): CustomerUpdateForm => {
|
||||||
|
console.log(customer);
|
||||||
|
|
||||||
|
return {
|
||||||
reference: customer.reference ?? "",
|
reference: customer.reference ?? "",
|
||||||
isCompany: customer.isCompany ?? buildCustomerUpdateDefault().isCompany,
|
isCompany: customer.isCompany ?? buildCustomerUpdateDefault().isCompany,
|
||||||
name: customer.name,
|
name: customer.name,
|
||||||
tradeName: customer.tradeName ?? "",
|
tradeName: customer.tradeName ?? "",
|
||||||
tin: customer.tin ?? "",
|
tin: customer.tin ?? "",
|
||||||
|
|
||||||
defaultTaxes: customer.defaultTaxes ?? [],
|
|
||||||
|
|
||||||
street: customer.address.street ?? "",
|
street: customer.address.street ?? "",
|
||||||
street2: customer.address.street2 ?? "",
|
street2: customer.address.street2 ?? "",
|
||||||
city: customer.address.city ?? "",
|
city: customer.address.city ?? "",
|
||||||
@ -41,6 +42,17 @@ export const mapCustomerToCustomerUpdateForm = (customer: Customer): CustomerUpd
|
|||||||
|
|
||||||
legalRecord: customer.legalRecord ?? "",
|
legalRecord: customer.legalRecord ?? "",
|
||||||
|
|
||||||
|
paymentMethodId: customer.paymentMethod?.id ?? "",
|
||||||
|
paymentTermId: customer.paymentTerm?.id ?? "",
|
||||||
|
taxRegimeCode: customer.taxRegime?.code ?? "",
|
||||||
|
|
||||||
|
usesEquivalenceSurcharge:
|
||||||
|
customer.fiscalDefaults.usesEquivalenceSurcharge ??
|
||||||
|
buildCustomerUpdateDefault().usesEquivalenceSurcharge,
|
||||||
|
usesRetention:
|
||||||
|
customer.fiscalDefaults.usesRetention ?? buildCustomerUpdateDefault().usesRetention,
|
||||||
|
|
||||||
languageCode: customer.languageCode ?? buildCustomerUpdateDefault().languageCode,
|
languageCode: customer.languageCode ?? buildCustomerUpdateDefault().languageCode,
|
||||||
currencyCode: customer.currencyCode ?? buildCustomerUpdateDefault().currencyCode,
|
currencyCode: customer.currencyCode ?? buildCustomerUpdateDefault().currencyCode,
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./use-customer-update.controller";
|
export * from "./use-customer-update.controller";
|
||||||
|
export * from "./use-customer-update-fiscal-controller";
|
||||||
export * from "./use-customer-update-page.controller";
|
export * from "./use-customer-update-page.controller";
|
||||||
|
|||||||
@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
getPaymentMethodOptions,
|
||||||
|
usePaymentMethodsListQuery,
|
||||||
|
} from "@erp/catalogs/client/payment-methods";
|
||||||
|
import {
|
||||||
|
getPaymentTermOptions,
|
||||||
|
usePaymentTermsListQuery,
|
||||||
|
} from "@erp/catalogs/client/payment-terms";
|
||||||
|
import { getTaxRegimeOptions, useTaxRegimesListQuery } from "@erp/catalogs/client/tax-regimes";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export const useCustomerUpdateFiscalController = () => {
|
||||||
|
const paymentMethodsQuery = usePaymentMethodsListQuery({
|
||||||
|
criteria: {
|
||||||
|
pageSize: 999,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: "isActive",
|
||||||
|
operator: "EQUALS",
|
||||||
|
value: "true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const paymentTermsQuery = usePaymentTermsListQuery({
|
||||||
|
criteria: {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: "isActive",
|
||||||
|
operator: "EQUALS",
|
||||||
|
value: "true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const taxRegimesQuery = useTaxRegimesListQuery({
|
||||||
|
criteria: {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: "isActive",
|
||||||
|
operator: "EQUALS",
|
||||||
|
value: "true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const paymentMethodOptions = useMemo(() => {
|
||||||
|
return getPaymentMethodOptions(paymentMethodsQuery.data?.items ?? []);
|
||||||
|
}, [paymentMethodsQuery.data?.items]);
|
||||||
|
|
||||||
|
const paymentTermOptions = useMemo(() => {
|
||||||
|
return getPaymentTermOptions(paymentTermsQuery.data?.items ?? []);
|
||||||
|
}, [paymentTermsQuery.data?.items]);
|
||||||
|
|
||||||
|
const taxRegimeOptions = useMemo(() => {
|
||||||
|
return getTaxRegimeOptions(taxRegimesQuery.data?.items ?? []);
|
||||||
|
}, [taxRegimesQuery.data?.items]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
paymentMethodOptions,
|
||||||
|
paymentTermOptions,
|
||||||
|
taxRegimeOptions,
|
||||||
|
|
||||||
|
isLoading: paymentMethodsQuery.isLoading || paymentTermsQuery.isLoading,
|
||||||
|
|
||||||
|
isFetching: paymentMethodsQuery.isFetching || paymentTermsQuery.isFetching,
|
||||||
|
|
||||||
|
isError: paymentMethodsQuery.isError || paymentTermsQuery.isError,
|
||||||
|
|
||||||
|
error: paymentMethodsQuery.error ?? paymentTermsQuery.error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseCustomerUpdateFiscalControllerResult = ReturnType<
|
||||||
|
typeof useCustomerUpdateFiscalController
|
||||||
|
>;
|
||||||
@ -26,6 +26,8 @@ import {
|
|||||||
buildUpdateCustomerByIdParams,
|
buildUpdateCustomerByIdParams,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
|
import { useCustomerUpdateFiscalController } from "./use-customer-update-fiscal-controller";
|
||||||
|
|
||||||
export interface UseCustomerUpdateControllerOptions {
|
export interface UseCustomerUpdateControllerOptions {
|
||||||
onUpdated?(updated: Customer): void;
|
onUpdated?(updated: Customer): void;
|
||||||
successToasts?: boolean; // mostrar o no toast automáticamente
|
successToasts?: boolean; // mostrar o no toast automáticamente
|
||||||
@ -155,6 +157,7 @@ export const useCustomerUpdateController = (
|
|||||||
options?.onUpdated?.(updated);
|
options?.onUpdated?.(updated);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const normalizedError = normalizeSubmitError(error);
|
const normalizedError = normalizeSubmitError(error);
|
||||||
|
console.error(normalizedError);
|
||||||
|
|
||||||
// No revierto el form para que no se pierdan los cambios que
|
// No revierto el form para que no se pierdan los cambios que
|
||||||
// ha hecho el usuario y no han sido guardados.
|
// ha hecho el usuario y no han sido guardados.
|
||||||
@ -192,6 +195,7 @@ export const useCustomerUpdateController = (
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
(errors: FieldErrors<CustomerUpdateForm>) => {
|
(errors: FieldErrors<CustomerUpdateForm>) => {
|
||||||
|
console.error(errors);
|
||||||
focusFirstInputFormError(form);
|
focusFirstInputFormError(form);
|
||||||
|
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
@ -207,6 +211,8 @@ export const useCustomerUpdateController = (
|
|||||||
submitHandler(event);
|
submitHandler(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fiscalCtrl = useCustomerUpdateFiscalController();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// form
|
// form
|
||||||
form,
|
form,
|
||||||
@ -227,6 +233,9 @@ export const useCustomerUpdateController = (
|
|||||||
isUpdateError,
|
isUpdateError,
|
||||||
updateError,
|
updateError,
|
||||||
|
|
||||||
|
// Otros controladores
|
||||||
|
fiscalCtrl,
|
||||||
|
|
||||||
// No devolver FormProvider, así el controller es más
|
// No devolver FormProvider, así el controller es más
|
||||||
// flexible y reusable (p.ej. para un modal)
|
// flexible y reusable (p.ej. para un modal)
|
||||||
// FormProvider,
|
// FormProvider,
|
||||||
|
|||||||
@ -20,8 +20,6 @@ export interface CustomerUpdateForm {
|
|||||||
tradeName: string;
|
tradeName: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
|
|
||||||
defaultTaxes: string[];
|
|
||||||
|
|
||||||
street: string;
|
street: string;
|
||||||
street2: string;
|
street2: string;
|
||||||
city: string;
|
city: string;
|
||||||
@ -41,6 +39,13 @@ export interface CustomerUpdateForm {
|
|||||||
|
|
||||||
legalRecord: string;
|
legalRecord: string;
|
||||||
|
|
||||||
|
paymentMethodId: string;
|
||||||
|
paymentTermId: string;
|
||||||
|
taxRegimeCode: string;
|
||||||
|
|
||||||
|
usesEquivalenceSurcharge: boolean;
|
||||||
|
usesRetention: boolean;
|
||||||
|
|
||||||
languageCode: string;
|
languageCode: string;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
MobilePhoneSchema,
|
MobilePhoneSchema,
|
||||||
PostalCodeSchema,
|
PostalCodeSchema,
|
||||||
TinSchema,
|
TinSchema,
|
||||||
URLSchema,
|
WebsiteSchema,
|
||||||
} from "@erp/core";
|
} from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
@ -31,9 +31,7 @@ export const CustomerUpdateFormSchema = z.object({
|
|||||||
|
|
||||||
name: z.string().min(1, "El nombre es obligatorio"),
|
name: z.string().min(1, "El nombre es obligatorio"),
|
||||||
tradeName: z.string(),
|
tradeName: z.string(),
|
||||||
tin: TinSchema,
|
tin: TinSchema.or(z.literal("")),
|
||||||
|
|
||||||
defaultTaxes: z.array(z.string()),
|
|
||||||
|
|
||||||
street: z.string(),
|
street: z.string(),
|
||||||
street2: z.string(),
|
street2: z.string(),
|
||||||
@ -50,11 +48,18 @@ export const CustomerUpdateFormSchema = z.object({
|
|||||||
primaryMobile: MobilePhoneSchema.or(z.literal("")),
|
primaryMobile: MobilePhoneSchema.or(z.literal("")),
|
||||||
secondaryMobile: MobilePhoneSchema.or(z.literal("")),
|
secondaryMobile: MobilePhoneSchema.or(z.literal("")),
|
||||||
|
|
||||||
fax: LandPhoneSchema.or(z.literal("")).or(z.literal("")),
|
fax: LandPhoneSchema.or(z.literal("")),
|
||||||
website: URLSchema.or(z.literal("")).or(z.literal("")),
|
website: WebsiteSchema.or(z.literal("")),
|
||||||
|
|
||||||
legalRecord: z.string().or(z.literal("")),
|
legalRecord: z.string().or(z.literal("")),
|
||||||
|
|
||||||
|
paymentMethodId: z.string().or(z.literal("")),
|
||||||
|
paymentTermId: z.string().or(z.literal("")),
|
||||||
|
taxRegimeCode: z.string().or(z.literal("")),
|
||||||
|
|
||||||
|
usesEquivalenceSurcharge: z.boolean(),
|
||||||
|
usesRetention: z.boolean(),
|
||||||
|
|
||||||
languageCode: LanguageCodeSchema,
|
languageCode: LanguageCodeSchema,
|
||||||
currencyCode: CurrencyCodeSchema,
|
currencyCode: CurrencyCodeSchema,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,8 +16,6 @@ export type CustomerUpdatePatch = {
|
|||||||
tradeName?: string | null;
|
tradeName?: string | null;
|
||||||
tin?: string | null;
|
tin?: string | null;
|
||||||
|
|
||||||
defaultTaxes?: string[];
|
|
||||||
|
|
||||||
street?: string | null;
|
street?: string | null;
|
||||||
street2?: string | null;
|
street2?: string | null;
|
||||||
city?: string | null;
|
city?: string | null;
|
||||||
@ -37,6 +35,13 @@ export type CustomerUpdatePatch = {
|
|||||||
|
|
||||||
legalRecord?: string | null;
|
legalRecord?: string | null;
|
||||||
|
|
||||||
|
paymentMethodId?: string | null;
|
||||||
|
paymentTermId?: string | null;
|
||||||
|
taxRegimeCode?: string | null;
|
||||||
|
|
||||||
|
usesEquivalenceSurcharge?: boolean;
|
||||||
|
usesRetention?: boolean;
|
||||||
|
|
||||||
languageCode?: string;
|
languageCode?: string;
|
||||||
currencyCode?: string;
|
currencyCode?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { FormSectionCard, FormSectionGrid, SelectField, TextField } from "@repo/rdx-ui/components";
|
import { FormSectionCard, FormSectionGrid, SelectField, TextField } from "@repo/rdx-ui/components";
|
||||||
|
import { MapPinIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { COUNTRY_OPTIONS } from "../../../shared";
|
import { COUNTRY_OPTIONS } from "../../../shared";
|
||||||
@ -17,11 +18,11 @@ export const CustomerAddressEditor = ({
|
|||||||
return (
|
return (
|
||||||
<FormSectionCard
|
<FormSectionCard
|
||||||
description={t("form_groups.address.description")}
|
description={t("form_groups.address.description")}
|
||||||
|
icon={<MapPinIcon className="size-5" />}
|
||||||
title={t("form_groups.address.title")}
|
title={t("form_groups.address.title")}
|
||||||
>
|
>
|
||||||
<FormSectionGrid>
|
<FormSectionGrid className="md:grid-cols-2">
|
||||||
<TextField
|
<TextField
|
||||||
className="md:col-span-6"
|
|
||||||
description={t("fields.street.description")}
|
description={t("fields.street.description")}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("fields.street.label")}
|
label={t("fields.street.label")}
|
||||||
@ -30,7 +31,6 @@ export const CustomerAddressEditor = ({
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="md:col-span-6"
|
|
||||||
description={t("fields.street2.description")}
|
description={t("fields.street2.description")}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("fields.street2.label")}
|
label={t("fields.street2.label")}
|
||||||
@ -40,7 +40,6 @@ export const CustomerAddressEditor = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="md:col-span-6"
|
|
||||||
description={t("fields.city.description")}
|
description={t("fields.city.description")}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("fields.city.label")}
|
label={t("fields.city.label")}
|
||||||
@ -50,17 +49,6 @@ export const CustomerAddressEditor = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="md:col-span-6"
|
|
||||||
description={t("fields.province.description")}
|
|
||||||
disabled={disabled}
|
|
||||||
label={t("fields.province.label")}
|
|
||||||
name="province"
|
|
||||||
placeholder={t("fields.province.placeholder")}
|
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
className="md:col-span-2 md:col-start-1"
|
|
||||||
description={t("fields.postal_code.description")}
|
description={t("fields.postal_code.description")}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("fields.postal_code.label")}
|
label={t("fields.postal_code.label")}
|
||||||
@ -69,8 +57,16 @@ export const CustomerAddressEditor = ({
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
description={t("fields.province.description")}
|
||||||
|
disabled={disabled}
|
||||||
|
label={t("fields.province.label")}
|
||||||
|
name="province"
|
||||||
|
placeholder={t("fields.province.placeholder")}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
className="md:col-span-2"
|
|
||||||
description={t("fields.country.description")}
|
description={t("fields.country.description")}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
items={[...COUNTRY_OPTIONS]}
|
items={[...COUNTRY_OPTIONS]}
|
||||||
|
|||||||
@ -14,12 +14,11 @@ import {
|
|||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioGroupItem,
|
RadioGroupItem,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { Building2Icon, FileTextIcon, UserIcon } from "lucide-react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
|
|
||||||
import { CustomerTaxesMultiSelect } from "./customer-taxes-multi-select";
|
|
||||||
|
|
||||||
interface CustomerBasicInfoEditorProps {
|
interface CustomerBasicInfoEditorProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@ -35,11 +34,11 @@ export const CustomerBasicInfoEditor = ({
|
|||||||
return (
|
return (
|
||||||
<FormSectionCard
|
<FormSectionCard
|
||||||
description={t("form_groups.basic_info.description")}
|
description={t("form_groups.basic_info.description")}
|
||||||
|
icon={<FileTextIcon className="size-5" />}
|
||||||
title={t("form_groups.basic_info.title")}
|
title={t("form_groups.basic_info.title")}
|
||||||
>
|
>
|
||||||
<FormSectionGrid>
|
<FormSectionGrid className="md:grid-cols-2">
|
||||||
<TextField
|
<TextField
|
||||||
className="md:col-span-6"
|
|
||||||
description={t("fields.name.description")}
|
description={t("fields.name.description")}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("fields.name.label")}
|
label={t("fields.name.label")}
|
||||||
@ -49,35 +48,46 @@ export const CustomerBasicInfoEditor = ({
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
description={t("fields.tin.description")}
|
||||||
|
disabled={disabled}
|
||||||
|
label={t("fields.tin.label")}
|
||||||
|
name="tin"
|
||||||
|
placeholder={t("fields.tin.placeholder")}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="isCompany"
|
name="isCompany"
|
||||||
render={({ field, fieldState }) => {
|
render={({ field, fieldState }) => {
|
||||||
return (
|
return (
|
||||||
<Field className="md:col-span-3" data-invalid={fieldState.invalid}>
|
<Field data-invalid={fieldState.invalid}>
|
||||||
|
<div className="flex flex-col gap-2 pt-1">
|
||||||
<FormFieldLabel required>{t("fields.customer_type.label")}</FormFieldLabel>
|
<FormFieldLabel required>{t("fields.customer_type.label")}</FormFieldLabel>
|
||||||
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
className="gap-3"
|
className="gap-3"
|
||||||
disabled={field.disabled}
|
disabled={field.disabled}
|
||||||
name={field.name}
|
name={field.name}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value: boolean) => {
|
||||||
field.onChange(value === "true");
|
field.onChange(value);
|
||||||
}}
|
}}
|
||||||
required
|
required
|
||||||
value={field.value ? "true" : "false"}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
||||||
<FieldContent>
|
<FieldContent>
|
||||||
<RadioGroupItem
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
id="customer-type-company"
|
|
||||||
value="true"
|
|
||||||
/>
|
|
||||||
<FieldLabel
|
<FieldLabel
|
||||||
className="cursor-pointer text-sm font-medium leading-none"
|
className="cursor-pointer text-sm font-medium leading-none"
|
||||||
htmlFor="customer-type-company"
|
htmlFor="customer-type-company"
|
||||||
>
|
>
|
||||||
|
<RadioGroupItem
|
||||||
|
aria-invalid={fieldState.invalid}
|
||||||
|
id="customer-type-company"
|
||||||
|
value={true}
|
||||||
|
/>
|
||||||
|
<Building2Icon className="size-4 text-muted-foreground" />
|
||||||
{t("fields.customer_type.options.company")}
|
{t("fields.customer_type.options.company")}
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
</FieldContent>
|
</FieldContent>
|
||||||
@ -85,21 +95,22 @@ export const CustomerBasicInfoEditor = ({
|
|||||||
|
|
||||||
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
||||||
<FieldContent>
|
<FieldContent>
|
||||||
<RadioGroupItem
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
id="customer-type-individual"
|
|
||||||
value="false"
|
|
||||||
/>
|
|
||||||
<FieldLabel
|
<FieldLabel
|
||||||
className="cursor-pointer text-sm font-medium leading-none"
|
className="cursor-pointer text-sm font-medium leading-none"
|
||||||
htmlFor="customer-type-individual"
|
htmlFor="customer-type-individual"
|
||||||
>
|
>
|
||||||
|
<RadioGroupItem
|
||||||
|
aria-invalid={fieldState.invalid}
|
||||||
|
id="customer-type-individual"
|
||||||
|
value={false}
|
||||||
|
/>
|
||||||
|
<UserIcon className="size-4 text-muted-foreground" />
|
||||||
{t("fields.customer_type.options.individual")}
|
{t("fields.customer_type.options.individual")}
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
</FieldContent>
|
</FieldContent>
|
||||||
</Field>
|
</Field>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
<FieldDescription>{t("fields.customer_type.description")}</FieldDescription>
|
<FieldDescription>{t("fields.customer_type.description")}</FieldDescription>
|
||||||
<FieldError errors={[fieldState.error]} />
|
<FieldError errors={[fieldState.error]} />
|
||||||
</Field>
|
</Field>
|
||||||
@ -108,18 +119,6 @@ export const CustomerBasicInfoEditor = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="md:col-start-1 md:col-span-4"
|
|
||||||
description={t("fields.tin.description")}
|
|
||||||
disabled={disabled}
|
|
||||||
label={t("fields.tin.label")}
|
|
||||||
name="tin"
|
|
||||||
placeholder={t("fields.tin.placeholder")}
|
|
||||||
readOnly={readOnly}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
className="md:col-span-full"
|
|
||||||
description={t("fields.trade_name.description")}
|
description={t("fields.trade_name.description")}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("fields.trade_name.label")}
|
label={t("fields.trade_name.label")}
|
||||||
@ -129,7 +128,6 @@ export const CustomerBasicInfoEditor = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="md:col-span-6 md:col-start-1"
|
|
||||||
description={t("fields.reference.description")}
|
description={t("fields.reference.description")}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("fields.reference.label")}
|
label={t("fields.reference.label")}
|
||||||
@ -138,34 +136,7 @@ export const CustomerBasicInfoEditor = ({
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Field className="md:col-span-6">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="defaultTaxes"
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<Field className="gap-1" data-invalid={fieldState.invalid}>
|
|
||||||
<FieldLabel htmlFor="defaultTaxes">
|
|
||||||
{t("fields.default_taxes.label")}
|
|
||||||
</FieldLabel>
|
|
||||||
|
|
||||||
<CustomerTaxesMultiSelect
|
|
||||||
description={t("fields.default_taxes.description")}
|
|
||||||
label={t("fields.default_taxes.label")}
|
|
||||||
onChange={field.onChange}
|
|
||||||
placeholder={t("fields.default_taxes.placeholder")}
|
|
||||||
required
|
|
||||||
value={field.value}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FieldDescription>{t("fields.default_taxes.description")}</FieldDescription>
|
|
||||||
<FieldError errors={[fieldState.error]} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<TextAreaField
|
<TextAreaField
|
||||||
className="md:col-span-full"
|
|
||||||
description={t("fields.legal_record.description")}
|
description={t("fields.legal_record.description")}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("fields.legal_record.label")}
|
label={t("fields.legal_record.label")}
|
||||||
|
|||||||
@ -1,15 +1,23 @@
|
|||||||
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
|
||||||
|
import type { UseCustomerUpdateFiscalControllerResult } from "../../controllers";
|
||||||
|
|
||||||
import { CustomerAdditionalConfigEditor } from "./customer-additional-config-fields";
|
import { CustomerAdditionalConfigEditor } from "./customer-additional-config-fields";
|
||||||
import { CustomerAddressEditor } from "./customer-address-editor";
|
import { CustomerAddressEditor } from "./customer-address-editor";
|
||||||
import { CustomerBasicInfoEditor } from "./customer-basic-info-editor";
|
import { CustomerBasicInfoEditor } from "./customer-basic-info-editor";
|
||||||
import { CustomerContactEditor } from "./customer-contact-fields";
|
import { CustomerContactEditor } from "./customer-contact-fields";
|
||||||
|
import { CustomerFiscalEditor } from "./customer-fiscal-editor";
|
||||||
|
|
||||||
type CustomerUpdateEditorFormProps = {
|
type CustomerUpdateEditorFormProps = {
|
||||||
formId: string;
|
formId: string;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
onSubmit: React.SubmitEventHandler<HTMLFormElement>;
|
onSubmit: React.SubmitEventHandler<HTMLFormElement>;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
|
|
||||||
|
fiscalCtrl: UseCustomerUpdateFiscalControllerResult;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomerUpdateEditorForm = ({
|
export const CustomerUpdateEditorForm = ({
|
||||||
@ -17,10 +25,15 @@ export const CustomerUpdateEditorForm = ({
|
|||||||
isSubmitting,
|
isSubmitting,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
|
|
||||||
|
fiscalCtrl,
|
||||||
|
|
||||||
|
className,
|
||||||
}: CustomerUpdateEditorFormProps) => {
|
}: CustomerUpdateEditorFormProps) => {
|
||||||
return (
|
return (
|
||||||
|
<div className={cn("p-6", className)}>
|
||||||
<form
|
<form
|
||||||
className="space-y-6"
|
className="space-y-6 2xl:space-y-12"
|
||||||
id={formId}
|
id={formId}
|
||||||
noValidate
|
noValidate
|
||||||
onKeyDown={preventEnterKeySubmitForm}
|
onKeyDown={preventEnterKeySubmitForm}
|
||||||
@ -28,8 +41,14 @@ export const CustomerUpdateEditorForm = ({
|
|||||||
>
|
>
|
||||||
<CustomerBasicInfoEditor disabled={isSubmitting} />
|
<CustomerBasicInfoEditor disabled={isSubmitting} />
|
||||||
<CustomerAddressEditor />
|
<CustomerAddressEditor />
|
||||||
|
<CustomerFiscalEditor
|
||||||
|
paymentMethodOptions={fiscalCtrl.paymentMethodOptions}
|
||||||
|
paymentTermOptions={fiscalCtrl.paymentTermOptions}
|
||||||
|
taxRegimeOptions={fiscalCtrl.taxRegimeOptions}
|
||||||
|
/>
|
||||||
<CustomerContactEditor />
|
<CustomerContactEditor />
|
||||||
<CustomerAdditionalConfigEditor />
|
<CustomerAdditionalConfigEditor />
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
FormSectionCard,
|
||||||
|
FormSectionGrid,
|
||||||
|
SelectField,
|
||||||
|
type SelectFieldItem,
|
||||||
|
SwitchField,
|
||||||
|
} from "@repo/rdx-ui/components";
|
||||||
|
import { LandmarkIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../i18n";
|
||||||
|
|
||||||
|
interface CustomerFiscalEditorProps {
|
||||||
|
paymentMethodOptions: SelectFieldItem[];
|
||||||
|
paymentTermOptions: SelectFieldItem[];
|
||||||
|
taxRegimeOptions: SelectFieldItem[];
|
||||||
|
disabled?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomerFiscalEditor = ({
|
||||||
|
paymentMethodOptions,
|
||||||
|
paymentTermOptions,
|
||||||
|
taxRegimeOptions,
|
||||||
|
disabled = false,
|
||||||
|
readOnly = false,
|
||||||
|
}: CustomerFiscalEditorProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormSectionCard
|
||||||
|
description={t("form_groups.fiscal.description")}
|
||||||
|
icon={<LandmarkIcon className="size-5" />}
|
||||||
|
title={t("form_groups.fiscal.title")}
|
||||||
|
>
|
||||||
|
<FormSectionGrid className="md:grid-cols-2">
|
||||||
|
<SelectField
|
||||||
|
clearable
|
||||||
|
description={t("fields.payment_method.description")}
|
||||||
|
disabled={disabled}
|
||||||
|
emptyValue={""}
|
||||||
|
items={paymentMethodOptions}
|
||||||
|
label={t("fields.payment_method.label")}
|
||||||
|
name="paymentMethodId"
|
||||||
|
placeholder={t("fields.payment_method.placeholder")}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectField
|
||||||
|
clearable
|
||||||
|
description={t("fields.payment_term.description")}
|
||||||
|
disabled={disabled}
|
||||||
|
emptyValue={""}
|
||||||
|
items={paymentTermOptions}
|
||||||
|
label={t("fields.payment_term.label")}
|
||||||
|
name="paymentTermId"
|
||||||
|
placeholder={t("fields.payment_term.placeholder")}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectField
|
||||||
|
className="col-span-full"
|
||||||
|
clearable
|
||||||
|
description={t("fields.tax_regime.description")}
|
||||||
|
disabled={disabled}
|
||||||
|
emptyValue={""}
|
||||||
|
items={taxRegimeOptions}
|
||||||
|
label={t("fields.tax_regime.label")}
|
||||||
|
name="taxRegimeCode"
|
||||||
|
placeholder={t("fields.tax_regime.placeholder")}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SwitchField
|
||||||
|
className="flex items-center justify-between rounded-lg border border-border px-4 py-3"
|
||||||
|
description={t("fields.uses_equivalence_surcharge.description")}
|
||||||
|
disabled={disabled}
|
||||||
|
label={t("fields.uses_equivalence_surcharge.label")}
|
||||||
|
name="usesEquivalenceSurcharge"
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SwitchField
|
||||||
|
className="flex items-center justify-between rounded-lg border border-border px-4 py-3"
|
||||||
|
description={t("fields.uses_retention.description")}
|
||||||
|
disabled={disabled}
|
||||||
|
label={t("fields.uses_retention.label")}
|
||||||
|
name="usesRetention"
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
</FormSectionGrid>
|
||||||
|
</FormSectionCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { ErrorAlert, NotFoundCard } from "@erp/core/components";
|
import { ErrorAlert, NotFoundCard } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import { Spinner } from "@repo/shadcn-ui/components";
|
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
@ -81,16 +80,25 @@ export const CustomerUpdatePage = () => {
|
|||||||
//onExportPdf={handleExportPdf}
|
//onExportPdf={handleExportPdf}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{updateCtrl.isLoading && <Spinner />}
|
{updateCtrl.isUpdateError && (
|
||||||
|
<ErrorAlert
|
||||||
{!updateCtrl.isLoading && (
|
message={
|
||||||
|
(updateCtrl.updateError as Error)?.message ??
|
||||||
|
t("pages.customers.update.error_msg", "Revisa los datos e inténtalo de nuevo.")
|
||||||
|
}
|
||||||
|
title={t("pages.customers.update.error_title", "No se pudo guardar los cambios")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
<CustomerUpdateEditorForm
|
<CustomerUpdateEditorForm
|
||||||
|
className="max-w-5xl mx-auto"
|
||||||
|
fiscalCtrl={updateCtrl.fiscalCtrl}
|
||||||
formId={updateCtrl.formId}
|
formId={updateCtrl.formId}
|
||||||
isSubmitting={updateCtrl.isUpdating}
|
isSubmitting={updateCtrl.isUpdating}
|
||||||
onReset={updateCtrl.resetForm}
|
onReset={updateCtrl.resetForm}
|
||||||
onSubmit={updateCtrl.onSubmit}
|
onSubmit={updateCtrl.onSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</UnsavedChangesProvider>
|
</UnsavedChangesProvider>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,8 +8,6 @@ export const buildCustomerUpdateDefault = (): CustomerUpdateForm => {
|
|||||||
tradeName: "",
|
tradeName: "",
|
||||||
tin: "",
|
tin: "",
|
||||||
|
|
||||||
defaultTaxes: [],
|
|
||||||
|
|
||||||
street: "",
|
street: "",
|
||||||
street2: "",
|
street2: "",
|
||||||
city: "",
|
city: "",
|
||||||
@ -29,6 +27,13 @@ export const buildCustomerUpdateDefault = (): CustomerUpdateForm => {
|
|||||||
|
|
||||||
legalRecord: "",
|
legalRecord: "",
|
||||||
|
|
||||||
|
paymentMethodId: "",
|
||||||
|
paymentTermId: "",
|
||||||
|
taxRegimeCode: "",
|
||||||
|
|
||||||
|
usesEquivalenceSurcharge: false,
|
||||||
|
usesRetention: false,
|
||||||
|
|
||||||
languageCode: "es",
|
languageCode: "es",
|
||||||
currencyCode: "EUR",
|
currencyCode: "EUR",
|
||||||
};
|
};
|
||||||
|
|||||||
@ -47,10 +47,6 @@ export const buildCustomerUpdatePatch = (
|
|||||||
patch.tin = toNullableText(formData.tin);
|
patch.tin = toNullableText(formData.tin);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dirtyFields.defaultTaxes) {
|
|
||||||
patch.defaultTaxes = formData.defaultTaxes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dirtyFields.street) {
|
if (dirtyFields.street) {
|
||||||
patch.street = toNullableText(formData.street);
|
patch.street = toNullableText(formData.street);
|
||||||
}
|
}
|
||||||
@ -111,6 +107,26 @@ export const buildCustomerUpdatePatch = (
|
|||||||
patch.legalRecord = toNullableText(formData.legalRecord);
|
patch.legalRecord = toNullableText(formData.legalRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dirtyFields.paymentMethodId) {
|
||||||
|
patch.paymentMethodId = toNullableText(formData.paymentMethodId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirtyFields.paymentTermId) {
|
||||||
|
patch.paymentTermId = toNullableText(formData.paymentTermId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirtyFields.taxRegimeCode) {
|
||||||
|
patch.taxRegimeCode = toNullableText(formData.taxRegimeCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirtyFields.usesEquivalenceSurcharge) {
|
||||||
|
patch.usesEquivalenceSurcharge = formData.usesEquivalenceSurcharge;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirtyFields.usesRetention) {
|
||||||
|
patch.usesRetention = formData.usesRetention;
|
||||||
|
}
|
||||||
|
|
||||||
if (dirtyFields.languageCode) {
|
if (dirtyFields.languageCode) {
|
||||||
patch.languageCode = toRequiredText(formData.languageCode);
|
patch.languageCode = toRequiredText(formData.languageCode);
|
||||||
}
|
}
|
||||||
@ -119,6 +135,8 @@ export const buildCustomerUpdatePatch = (
|
|||||||
patch.currencyCode = toRequiredText(formData.currencyCode);
|
patch.currencyCode = toRequiredText(formData.currencyCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(dirtyFields, patch);
|
||||||
|
|
||||||
return patch;
|
return patch;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -46,10 +46,6 @@ export const buildUpdateCustomerByIdParams = (
|
|||||||
data.tin = patchData.tin;
|
data.tin = patchData.tin;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patchData.defaultTaxes !== undefined) {
|
|
||||||
data.default_taxes = patchData.defaultTaxes?.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patchData.legalRecord !== undefined) {
|
if (patchData.legalRecord !== undefined) {
|
||||||
data.legal_record = patchData.legalRecord;
|
data.legal_record = patchData.legalRecord;
|
||||||
}
|
}
|
||||||
@ -62,6 +58,27 @@ export const buildUpdateCustomerByIdParams = (
|
|||||||
data.currency_code = patchData.currencyCode;
|
data.currency_code = patchData.currencyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- legal ---
|
||||||
|
if (patchData.paymentMethodId !== undefined) {
|
||||||
|
data.payment_method_id = patchData.paymentMethodId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patchData.paymentTermId !== undefined) {
|
||||||
|
data.payment_term_id = patchData.paymentTermId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patchData.taxRegimeCode !== undefined) {
|
||||||
|
data.tax_regime_code = patchData.taxRegimeCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patchData.usesEquivalenceSurcharge !== undefined) {
|
||||||
|
data.uses_equivalence_surcharge = patchData.usesEquivalenceSurcharge;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patchData.usesRetention !== undefined) {
|
||||||
|
data.uses_retention = patchData.usesRetention;
|
||||||
|
}
|
||||||
|
|
||||||
// --- address ---
|
// --- address ---
|
||||||
const address: Record<string, unknown> = {};
|
const address: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,18 @@
|
|||||||
|
import { useUrlParamId } from "@erp/core/hooks";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { useCustomerViewController } from "./use-customer-view.controller";
|
import { useCustomerViewController } from "./use-customer-view.controller";
|
||||||
|
|
||||||
export function useCustomerViewPageController() {
|
export function useCustomerViewPageController() {
|
||||||
const viewCtrl = useCustomerViewController();
|
const customerId = useUrlParamId();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const viewCtrl = useCustomerViewController(customerId);
|
||||||
|
|
||||||
|
const returnTo = searchParams.get("returnTo") ?? "/customers";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
viewCtrl,
|
viewCtrl,
|
||||||
|
returnTo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,24 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { useCustomerGetQuery } from "../../shared/hooks";
|
import { useCustomerGetQuery } from "../../shared/hooks";
|
||||||
|
|
||||||
export const useCustomerViewController = (initialCustomerId = "") => {
|
export const useCustomerViewController = (customerId?: string) => {
|
||||||
const [customerId, setCustomerId] = useState(initialCustomerId);
|
const {
|
||||||
|
data: customerData,
|
||||||
const query = useCustomerGetQuery(customerId);
|
isLoading,
|
||||||
|
isError: isLoadError,
|
||||||
|
error: loadError,
|
||||||
|
refetch,
|
||||||
|
} = useCustomerGetQuery({
|
||||||
|
id: customerId,
|
||||||
|
enabled: Boolean(customerId),
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: query.data,
|
// carga de datos
|
||||||
isLoading: query.isLoading,
|
customerData,
|
||||||
isFetching: query.isFetching,
|
isLoading,
|
||||||
|
isLoadError,
|
||||||
|
loadError,
|
||||||
|
|
||||||
isError: query.isError,
|
refetch,
|
||||||
error: query.error,
|
|
||||||
|
|
||||||
refetch: query.refetch,
|
|
||||||
|
|
||||||
customerId,
|
|
||||||
setCustomerId,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,110 @@
|
|||||||
|
import { PageFormHeader, PageKeyboardShortcutsButton } from "@erp/core/components";
|
||||||
|
import {
|
||||||
|
CancelActionButton,
|
||||||
|
FormActionsBar,
|
||||||
|
type FormSecondaryAction,
|
||||||
|
FormSecondaryActionsMenu,
|
||||||
|
} from "@repo/rdx-ui/components";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
export interface CustomerViewHeaderLabels {
|
||||||
|
title: string;
|
||||||
|
back: string;
|
||||||
|
modified: string;
|
||||||
|
cancel: string;
|
||||||
|
save: string;
|
||||||
|
saving: string;
|
||||||
|
moreActions: string;
|
||||||
|
keyboardShortcuts: string;
|
||||||
|
delete: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerViewHeaderProps {
|
||||||
|
labels: CustomerViewHeaderLabels;
|
||||||
|
|
||||||
|
onCancel?: () => void;
|
||||||
|
onDuplicate?: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
isSaving?: boolean;
|
||||||
|
hasChanges?: boolean;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomerViewHeader = ({
|
||||||
|
labels,
|
||||||
|
onCancel,
|
||||||
|
onDuplicate,
|
||||||
|
onDelete,
|
||||||
|
disabled = false,
|
||||||
|
readOnly = false,
|
||||||
|
isSaving = false,
|
||||||
|
hasChanges = false,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: CustomerViewHeaderProps) => {
|
||||||
|
const computedDisabled = disabled || isSaving;
|
||||||
|
|
||||||
|
const secondaryActions: FormSecondaryAction[] = [
|
||||||
|
/*{
|
||||||
|
id: "duplicate",
|
||||||
|
label: labels.duplicate,
|
||||||
|
icon: <CopyIcon aria-hidden="true" className="mr-2 size-4" />,
|
||||||
|
hidden: !onDuplicate,
|
||||||
|
disabled: computedDisabled,
|
||||||
|
onSelect: () => onDuplicate?.(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "delete",
|
||||||
|
label: labels.delete,
|
||||||
|
icon: <Trash2Icon aria-hidden="true" className="mr-2 size-4" />,
|
||||||
|
hidden: !onDelete || readOnly,
|
||||||
|
disabled: computedDisabled,
|
||||||
|
destructive: true,
|
||||||
|
onSelect: () => onDelete?.(),
|
||||||
|
},*/
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageFormHeader
|
||||||
|
actions={
|
||||||
|
<FormActionsBar align="end" reverseOnMobile={false}>
|
||||||
|
<PageKeyboardShortcutsButton
|
||||||
|
className="hidden sm:flex"
|
||||||
|
label={labels.keyboardShortcuts}
|
||||||
|
shortcuts={[
|
||||||
|
{ keys: "Ctrl+S", label: labels.save },
|
||||||
|
{ keys: "Esc", label: labels.cancel },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CancelActionButton
|
||||||
|
className="hidden sm:flex"
|
||||||
|
disabled={computedDisabled}
|
||||||
|
label={labels.cancel}
|
||||||
|
onCancel={() => onCancel?.()}
|
||||||
|
variant="outline"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormSecondaryActionsMenu
|
||||||
|
actions={secondaryActions}
|
||||||
|
disabled={computedDisabled}
|
||||||
|
label={labels.moreActions}
|
||||||
|
/>
|
||||||
|
</FormActionsBar>
|
||||||
|
}
|
||||||
|
backLabel={labels.back}
|
||||||
|
className={className}
|
||||||
|
onBack={onCancel}
|
||||||
|
showStatus={hasChanges}
|
||||||
|
statusLabel={labels.modified}
|
||||||
|
title={labels.title}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PageFormHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
modules/customers/src/web/view/ui/blocks/index.ts
Normal file
1
modules/customers/src/web/view/ui/blocks/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-view-header";
|
||||||
@ -1,61 +1,44 @@
|
|||||||
import { ErrorAlert, PageHeader } from "@erp/core/components";
|
import { ErrorAlert, NotFoundCard } from "@erp/core/components";
|
||||||
import { useUrlParamId } from "@erp/core/hooks";
|
import { useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import {
|
import { Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import {
|
import {
|
||||||
Banknote,
|
Banknote,
|
||||||
EditIcon,
|
|
||||||
FileText,
|
FileText,
|
||||||
Globe,
|
Globe,
|
||||||
Languages,
|
Languages,
|
||||||
Mail,
|
Mail,
|
||||||
MapPin,
|
MapPin,
|
||||||
MoreVertical,
|
|
||||||
Phone,
|
Phone,
|
||||||
Smartphone,
|
Smartphone,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { CustomerEditorSkeleton } from "../../../update/ui/components";
|
import { CustomerEditorSkeleton } from "../../../update/ui/components";
|
||||||
import { useCustomerViewPageController } from "../../controllers";
|
import { useCustomerViewPageController } from "../../controllers";
|
||||||
|
import { CustomerViewHeader } from "../blocks";
|
||||||
|
|
||||||
export const CustomerViewPage = () => {
|
export const CustomerViewPage = () => {
|
||||||
const initialCustomerId = useUrlParamId();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const {
|
const { viewCtrl, returnTo } = useCustomerViewPageController();
|
||||||
viewCtrl: { setCustomerId, customerId, data: customerData, isError, error, isLoading },
|
|
||||||
} = useCustomerViewPageController();
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { navigateBack } = useReturnToNavigation({
|
||||||
if (initialCustomerId && customerId !== initialCustomerId) {
|
fallbackPath: returnTo,
|
||||||
setCustomerId(initialCustomerId);
|
});
|
||||||
}
|
|
||||||
}, [initialCustomerId, customerId, setCustomerId]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
const handleCancel = () => navigateBack();
|
||||||
return <CustomerEditorSkeleton />;
|
|
||||||
|
if (viewCtrl.isLoading) {
|
||||||
|
return <CustomerEditorSkeleton />; /*<CustomerViewSkeleton />;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (viewCtrl.isLoadError) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
message={
|
message={(viewCtrl.loadError as Error)?.message ?? t("view.errors.load_message")}
|
||||||
(error as Error)?.message ??
|
|
||||||
t("view.errors.load_message")
|
|
||||||
}
|
|
||||||
title={t("view.errors.load_title")}
|
title={t("view.errors.load_title")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -67,55 +50,40 @@ export const CustomerViewPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!viewCtrl.customerData)
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<AppHeader>
|
|
||||||
<PageHeader
|
|
||||||
backIcon
|
|
||||||
description={
|
|
||||||
<div className="mt-2 flex items-center gap-3">
|
|
||||||
<Badge className="font-mono" variant="secondary">
|
|
||||||
{customerData?.tin}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="outline">
|
|
||||||
{customerData?.isCompany
|
|
||||||
? t("fields.customer_type.options.company")
|
|
||||||
: t("fields.customer_type.options.individual")}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
rightSlot={
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => navigate("/customers/list")}
|
|
||||||
size="icon"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
<MoreVertical className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => navigate(`/customers/${customerId}/edit`)}
|
|
||||||
>
|
|
||||||
<EditIcon className="mr-2 h-4 w-4" />
|
|
||||||
{t("view.actions.edit")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
title={
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
{customerData?.name}{" "}
|
|
||||||
{customerData?.tradeName && (
|
|
||||||
<span className="text-muted-foreground">({customerData.tradeName})</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</AppHeader>
|
|
||||||
<AppContent>
|
<AppContent>
|
||||||
{/* Main Content Grid */}
|
<NotFoundCard
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
message={t(
|
||||||
|
"pages.customers.view.not_found_msg",
|
||||||
|
"Revisa el identificador o vuelve al listado."
|
||||||
|
)}
|
||||||
|
title={t("pages.customers.view.not_found_title", "Customer not found")}
|
||||||
|
/>
|
||||||
|
</AppContent>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
||||||
|
<CustomerViewHeader
|
||||||
|
labels={{
|
||||||
|
title: "Ver cliente",
|
||||||
|
back: "Volver",
|
||||||
|
modified: "Modificada",
|
||||||
|
cancel: "Cancelar",
|
||||||
|
save: "Guardar",
|
||||||
|
saving: "Guardando...",
|
||||||
|
moreActions: "Más acciones",
|
||||||
|
keyboardShortcuts: "Ver atajos de teclado",
|
||||||
|
delete: "Eliminar",
|
||||||
|
}}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
//onDelete={openDeleteDialog}
|
||||||
|
//onDuplicate={handleDuplicate}
|
||||||
|
//onExportPdf={handleExportPdf}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
{/* Información Básica */}
|
{/* Información Básica */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -126,35 +94,23 @@ export const CustomerViewPage = () => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-sm font-medium text-muted-foreground">
|
<dt className="text-sm font-medium text-muted-foreground">{t("view.fields.name")}</dt>
|
||||||
{t("view.fields.name")}
|
<dd className="mt-1 text-base text-foreground">{viewCtrl.customerData?.name}</dd>
|
||||||
</dt>
|
|
||||||
<dd className="mt-1 text-base text-foreground">{customerData?.name}</dd>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-sm font-medium text-muted-foreground">
|
<dt className="text-sm font-medium text-muted-foreground">
|
||||||
{t("view.fields.reference")}
|
{t("view.fields.reference")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 font-mono text-base text-foreground">
|
<dd className="mt-1 font-mono text-base text-foreground">
|
||||||
{customerData?.reference}
|
{viewCtrl.customerData?.reference}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-sm font-medium text-muted-foreground">
|
<dt className="text-sm font-medium text-muted-foreground">
|
||||||
{t("view.fields.legal_record")}
|
{t("view.fields.legal_record")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">{customerData?.legalRecord}</dd>
|
<dd className="mt-1 text-base text-foreground">
|
||||||
</div>
|
{viewCtrl.customerData?.legalRecord}
|
||||||
<div>
|
|
||||||
<dt className="text-sm font-medium text-muted-foreground">
|
|
||||||
{t("view.fields.default_taxes")}
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-1">
|
|
||||||
{customerData?.defaultTaxes.map((tax: string) => (
|
|
||||||
<Badge key={tax} variant={"secondary"}>
|
|
||||||
{tax}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -174,11 +130,11 @@ export const CustomerViewPage = () => {
|
|||||||
{t("view.fields.street")}
|
{t("view.fields.street")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">
|
<dd className="mt-1 text-base text-foreground">
|
||||||
{customerData?.street}
|
{viewCtrl.customerData?.street}
|
||||||
{customerData?.street2 && (
|
{viewCtrl.customerData?.street2 && (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
{customerData?.street2}
|
{viewCtrl.customerData?.street2}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</dd>
|
</dd>
|
||||||
@ -188,13 +144,15 @@ export const CustomerViewPage = () => {
|
|||||||
<dt className="text-sm font-medium text-muted-foreground">
|
<dt className="text-sm font-medium text-muted-foreground">
|
||||||
{t("view.fields.city")}
|
{t("view.fields.city")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">{customerData?.city}</dd>
|
<dd className="mt-1 text-base text-foreground">{viewCtrl.customerData?.city}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-sm font-medium text-muted-foreground">
|
<dt className="text-sm font-medium text-muted-foreground">
|
||||||
{t("view.fields.postal_code")}
|
{t("view.fields.postal_code")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">{customerData?.postalCode}</dd>
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{viewCtrl.customerData?.postalCode}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
@ -202,13 +160,15 @@ export const CustomerViewPage = () => {
|
|||||||
<dt className="text-sm font-medium text-muted-foreground">
|
<dt className="text-sm font-medium text-muted-foreground">
|
||||||
{t("view.fields.province")}
|
{t("view.fields.province")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">{customerData?.province}</dd>
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{viewCtrl.customerData?.province}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-sm font-medium text-muted-foreground">
|
<dt className="text-sm font-medium text-muted-foreground">
|
||||||
{t("view.fields.country")}
|
{t("view.fields.country")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">{customerData?.country}</dd>
|
<dd className="mt-1 text-base text-foreground">{viewCtrl.customerData?.country}</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -229,7 +189,7 @@ export const CustomerViewPage = () => {
|
|||||||
<h3 className="font-semibold text-foreground">
|
<h3 className="font-semibold text-foreground">
|
||||||
{t("view.sections.primary_contact")}
|
{t("view.sections.primary_contact")}
|
||||||
</h3>
|
</h3>
|
||||||
{customerData?.primaryEmail && (
|
{viewCtrl.customerData?.primaryEmail && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -237,12 +197,12 @@ export const CustomerViewPage = () => {
|
|||||||
{t("view.fields.email")}
|
{t("view.fields.email")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">
|
<dd className="mt-1 text-base text-foreground">
|
||||||
{customerData?.primaryEmail}
|
{viewCtrl.customerData?.primaryEmail}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{customerData?.primaryMobile && (
|
{viewCtrl.customerData?.primaryMobile && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -250,12 +210,12 @@ export const CustomerViewPage = () => {
|
|||||||
{t("view.fields.mobile")}
|
{t("view.fields.mobile")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">
|
<dd className="mt-1 text-base text-foreground">
|
||||||
{customerData?.primaryMobile}
|
{viewCtrl.customerData?.primaryMobile}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{customerData?.primaryPhone && (
|
{viewCtrl.customerData?.primaryPhone && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -263,7 +223,7 @@ export const CustomerViewPage = () => {
|
|||||||
{t("view.fields.phone")}
|
{t("view.fields.phone")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">
|
<dd className="mt-1 text-base text-foreground">
|
||||||
{customerData?.primaryPhone}
|
{viewCtrl.customerData?.primaryPhone}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -275,7 +235,7 @@ export const CustomerViewPage = () => {
|
|||||||
<h3 className="font-semibold text-foreground">
|
<h3 className="font-semibold text-foreground">
|
||||||
{t("view.sections.secondary_contact")}
|
{t("view.sections.secondary_contact")}
|
||||||
</h3>
|
</h3>
|
||||||
{customerData?.secondaryEmail && (
|
{viewCtrl.customerData?.secondaryEmail && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -283,12 +243,12 @@ export const CustomerViewPage = () => {
|
|||||||
{t("view.fields.email")}
|
{t("view.fields.email")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">
|
<dd className="mt-1 text-base text-foreground">
|
||||||
{customerData?.secondaryEmail}
|
{viewCtrl.customerData?.secondaryEmail}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{customerData?.secondaryMobile && (
|
{viewCtrl.customerData?.secondaryMobile && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -296,12 +256,12 @@ export const CustomerViewPage = () => {
|
|||||||
{t("view.fields.mobile")}
|
{t("view.fields.mobile")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">
|
<dd className="mt-1 text-base text-foreground">
|
||||||
{customerData?.secondaryMobile}
|
{viewCtrl.customerData?.secondaryMobile}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{customerData?.secondaryPhone && (
|
{viewCtrl.customerData?.secondaryPhone && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -309,7 +269,7 @@ export const CustomerViewPage = () => {
|
|||||||
{t("view.fields.phone")}
|
{t("view.fields.phone")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">
|
<dd className="mt-1 text-base text-foreground">
|
||||||
{customerData?.secondaryPhone}
|
{viewCtrl.customerData?.secondaryPhone}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -317,13 +277,13 @@ export const CustomerViewPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Otros Contactos */}
|
{/* Otros Contactos */}
|
||||||
{(customerData?.website || customerData?.fax) && (
|
{(viewCtrl.customerData?.website || viewCtrl.customerData?.fax) && (
|
||||||
<div className="space-y-4 md:col-span-2">
|
<div className="space-y-4 md:col-span-2">
|
||||||
<h3 className="font-semibold text-foreground">
|
<h3 className="font-semibold text-foreground">
|
||||||
{t("view.sections.other_contacts")}
|
{t("view.sections.other_contacts")}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{customerData?.website && (
|
{viewCtrl.customerData?.website && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Globe className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
<Globe className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -332,24 +292,26 @@ export const CustomerViewPage = () => {
|
|||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-primary hover:underline">
|
<dd className="mt-1 text-base text-primary hover:underline">
|
||||||
<a
|
<a
|
||||||
href={customerData?.website}
|
href={viewCtrl.customerData?.website}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{customerData?.website}
|
{viewCtrl.customerData?.website}
|
||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{customerData?.fax && (
|
{viewCtrl.customerData?.fax && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<dt className="text-sm font-medium text-muted-foreground">
|
<dt className="text-sm font-medium text-muted-foreground">
|
||||||
{t("view.fields.fax")}
|
{t("view.fields.fax")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">{customerData?.fax}</dd>
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{viewCtrl.customerData?.fax}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -376,7 +338,9 @@ export const CustomerViewPage = () => {
|
|||||||
<dt className="text-sm font-medium text-muted-foreground">
|
<dt className="text-sm font-medium text-muted-foreground">
|
||||||
{t("view.fields.language_code")}
|
{t("view.fields.language_code")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">{customerData?.languageCode}</dd>
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{viewCtrl.customerData?.languageCode}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
@ -385,14 +349,15 @@ export const CustomerViewPage = () => {
|
|||||||
<dt className="text-sm font-medium text-muted-foreground">
|
<dt className="text-sm font-medium text-muted-foreground">
|
||||||
{t("view.fields.currency_code")}
|
{t("view.fields.currency_code")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-base text-foreground">{customerData?.currencyCode}</dd>
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{viewCtrl.customerData?.currencyCode}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</AppContent>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/factuges",
|
"name": "@erp/factuges",
|
||||||
"version": "0.6.9",
|
"version": "0.7.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
@ -23,6 +23,7 @@
|
|||||||
"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:*",
|
||||||
"@repo/i18next": "workspace:*",
|
"@repo/i18next": "workspace:*",
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
import type {
|
||||||
|
IPaymentMethodPublicFinder,
|
||||||
|
IPaymentTermPublicFinder,
|
||||||
|
ITaxDefinitionPublicFinder,
|
||||||
|
ITaxRegimePublicFinder,
|
||||||
|
} from "@erp/catalogs/api";
|
||||||
|
|
||||||
|
import { FactuGESPaymentResolver } from "../services";
|
||||||
|
import type { FactuGESPaymentMethodReferenceProvider } from "../services/json-data";
|
||||||
|
|
||||||
|
export interface IFactuGESCatalogResolvers {
|
||||||
|
//taxResolver: FactuGESTaxResolver;
|
||||||
|
paymentResolver: FactuGESPaymentResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFactuGESCatalogResolvers(params: {
|
||||||
|
taxDefinitionFinder: ITaxDefinitionPublicFinder;
|
||||||
|
taxRegimeFinder: ITaxRegimePublicFinder;
|
||||||
|
paymentMethodFinder: IPaymentMethodPublicFinder;
|
||||||
|
paymentTermFinder: IPaymentTermPublicFinder;
|
||||||
|
paymentReferenceProvider: FactuGESPaymentMethodReferenceProvider;
|
||||||
|
}): IFactuGESCatalogResolvers {
|
||||||
|
return {
|
||||||
|
/*taxResolver: buildFactuGESTaxResolver({
|
||||||
|
taxDefinitionFinder: params.taxDefinitionFinder,
|
||||||
|
taxRegimeFinder: params.taxRegimeFinder,
|
||||||
|
}),*/
|
||||||
|
|
||||||
|
paymentResolver: new FactuGESPaymentResolver({
|
||||||
|
paymentMethodFinder: params.paymentMethodFinder,
|
||||||
|
paymentTermFinder: params.paymentTermFinder,
|
||||||
|
paymentReferenceProvider: params.paymentReferenceProvider,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user