.
This commit is contained in:
parent
f6f57359d5
commit
1551d65f63
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/factuges-server",
|
||||
"version": "0.6.9",
|
||||
"version": "0.7.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --config tsup.config.ts",
|
||||
|
||||
@ -36,7 +36,7 @@ export function getServiceScoped<T = unknown>(
|
||||
if (!allowedDeps.includes(serviceModule)) {
|
||||
throw new Error(
|
||||
`❌ 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 {
|
||||
const service = services[name];
|
||||
if (!service) {
|
||||
throw new Error(`❌ Servicio "${name}" no encontrado.`);
|
||||
throw new Error(`❌ Service "${name}" not found.`);
|
||||
}
|
||||
return service as T;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/factuges-web",
|
||||
"private": true,
|
||||
"version": "0.6.9",
|
||||
"version": "0.7.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/auth",
|
||||
"version": "0.6.9",
|
||||
"version": "0.7.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/catalogs",
|
||||
"description": "Catalogs module",
|
||||
"version": "0.6.9",
|
||||
"version": "0.7.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -3,7 +3,8 @@ import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { PaymentTermPublicModelMapper } from "../mappers";
|
||||
import type { PaymentTermPublicModel } from "../models";
|
||||
import type { IPaymentTermRepository } from "../repositories";
|
||||
import { IPaymentTermRepository } from '../../repositories';
|
||||
|
||||
|
||||
export interface FindPaymentTermByIdInCompanyParams {
|
||||
companyId: UniqueID;
|
||||
|
||||
@ -10,6 +10,7 @@ import type {
|
||||
|
||||
export class TaxDefinitionPublicModelMapper {
|
||||
public toPublicModel(taxDefinition: TaxDefinition): TaxDefinitionPublicModel {
|
||||
console.log(taxDefinition.description)
|
||||
return {
|
||||
id: taxDefinition.id,
|
||||
companyId: Maybe.some(taxDefinition.companyId),
|
||||
|
||||
@ -9,6 +9,8 @@ import {
|
||||
EnablePaymentTermByIdUseCase,
|
||||
GetPaymentTermByIdUseCase,
|
||||
ListPaymentTermsUseCase,
|
||||
PaymentTermPublicFinder,
|
||||
PaymentTermPublicModelMapper,
|
||||
UpdatePaymentTermByIdUseCase,
|
||||
buildPaymentTermCreator,
|
||||
buildPaymentTermDeleter,
|
||||
@ -19,7 +21,6 @@ import {
|
||||
buildPaymentTermUpdater,
|
||||
} from "../../../application/payment-terms";
|
||||
import type { IPaymentTermRepository } from "../../../application/payment-terms/repositories";
|
||||
import { PaymentTermFinder } from "../../../application/payment-terms/services";
|
||||
|
||||
import { buildPaymentTermPersistenceMappers } from "./payment-term-persistence-mappers.di";
|
||||
import { buildPaymentTermRepository } from "./payment-term-repositories.di";
|
||||
@ -110,8 +111,10 @@ export const buildPaymentTermsDependencies = (params: ModuleParams): PaymentTerm
|
||||
export const buildPaymentTermsPublicServices = (
|
||||
_params: SetupParams,
|
||||
deps: PaymentTermsInternalDeps
|
||||
): { finder: PaymentTermFinder } => {
|
||||
): { finder: PaymentTermPublicFinder } => {
|
||||
const mapper = new PaymentTermPublicModelMapper();
|
||||
|
||||
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 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(
|
||||
TaxRateVO.create({ value: raw.rate_value, scale: raw.rate_scale }),
|
||||
@ -60,33 +64,57 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeDomainMapper<
|
||||
errors
|
||||
);
|
||||
|
||||
const jurisdictionRegionCode = maybeFromNullableResult(raw.jurisdiction_region_code, (v) =>
|
||||
CountryRegionCode.create(v)
|
||||
const jurisdictionRegionCode = extractOrPushError(
|
||||
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 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) => {
|
||||
if (!Array.isArray(v)) return Result.fail(new Error("Invalid allowed_surcharge_codes"));
|
||||
const allowedSurchargeCodes = extractOrPushError(
|
||||
maybeFromNullableResult(raw.allowed_surcharge_codes, (v) => {
|
||||
console.log(v);
|
||||
|
||||
const arr: any[] = [];
|
||||
for (const el of v) {
|
||||
const _result = TaxDefinitionCodeVO.create(String(el));
|
||||
if (_result.isFailure)
|
||||
return Result.fail(new Error("Invalid allowed_surcharge_codes element"));
|
||||
arr.push(_result.data);
|
||||
}
|
||||
const values = v.split(";");
|
||||
if (!Array.isArray(values))
|
||||
return Result.fail(new Error("Invalid allowed_surcharge_codes"));
|
||||
|
||||
return Result.ok(arr);
|
||||
});
|
||||
const arr: any[] = [];
|
||||
for (const el of values) {
|
||||
const _result = TaxDefinitionCodeVO.create(String(el));
|
||||
if (_result.isFailure)
|
||||
return Result.fail(new Error("Invalid allowed_surcharge_codes element"));
|
||||
arr.push(_result.data);
|
||||
}
|
||||
|
||||
return Result.ok(arr);
|
||||
}),
|
||||
"allowed_surcharge_codes",
|
||||
errors
|
||||
);
|
||||
|
||||
// valid from / to
|
||||
const validFrom = maybeFromNullableResult(raw.valid_from, (v) => Result.ok(String(v)));
|
||||
const validTo = maybeFromNullableResult(raw.valid_to, (v) => Result.ok(String(v)));
|
||||
const validFrom = extractOrPushError(
|
||||
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) {
|
||||
console.error(errors);
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("TaxDefinition mapping failed [mapToDomain]", errors)
|
||||
);
|
||||
@ -130,8 +158,8 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeDomainMapper<
|
||||
(v) => v.toPrimitive(),
|
||||
() => null
|
||||
),
|
||||
rate_value: source.rate.toPrimitive(),
|
||||
rate_scale: (source.rate as any).scale ?? 2,
|
||||
rate_value: source.rate.toPrimitive().value,
|
||||
rate_scale: source.rate.toPrimitive().scale,
|
||||
tax_family: source.taxFamily.toPrimitive(),
|
||||
calculation_behavior: source.calculationBehavior.toPrimitive(),
|
||||
jurisdiction_country_code: source.jurisdictionCountryCode.toPrimitive(),
|
||||
@ -145,7 +173,7 @@ export class SequelizeTaxDefinitionDomainMapper extends SequelizeDomainMapper<
|
||||
() => null
|
||||
),
|
||||
allowed_surcharge_codes: source.allowedSurchargeCodes.match(
|
||||
(arr) => arr.map((c) => c.toPrimitive()),
|
||||
(arr) => arr.map((c) => c.toPrimitive()).join(";"),
|
||||
() => null
|
||||
),
|
||||
is_system: source.isSystem,
|
||||
|
||||
@ -29,7 +29,7 @@ export class TaxDefinitionModel extends Model<
|
||||
declare tax_scope: string;
|
||||
|
||||
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_active: boolean;
|
||||
@ -111,7 +111,7 @@ export default (database: Sequelize) => {
|
||||
},
|
||||
|
||||
allowed_surcharge_codes: {
|
||||
type: DataTypes.JSON,
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/core",
|
||||
"version": "0.6.9",
|
||||
"version": "0.7.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -12,7 +12,7 @@ export const globalErrorHandler = async (
|
||||
res: Response,
|
||||
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
|
||||
if (res.headersSent) {
|
||||
|
||||
@ -16,3 +16,4 @@ export * from "./postal-code.dto";
|
||||
export * from "./quantity.dto";
|
||||
export * from "./tin.dto";
|
||||
export * from "./url.dto";
|
||||
export * from "./website.dto";
|
||||
|
||||
@ -8,14 +8,13 @@ import { z } from "zod/v4";
|
||||
*
|
||||
* Ejemplos válidos:
|
||||
* - "28013" (ES)
|
||||
* - "SW1A 1AA" (UK)
|
||||
* - "75008" (FR)
|
||||
* - "10115" (DE)
|
||||
*/
|
||||
export const PostalCodeSchema = z
|
||||
.string()
|
||||
.min(1, "Postal code cannot be empty")
|
||||
.max(16, "Postal code too long")
|
||||
.regex(/^[A-Za-z0-9\- ]+$/, "Invalid postal code format");
|
||||
.max(5, "Postal code too long")
|
||||
.regex(/^[0-9\- ]+$/, "Invalid postal code format");
|
||||
|
||||
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 "./dto";
|
||||
export * from "./helpers";
|
||||
export * from "./json-data";
|
||||
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",
|
||||
"version": "0.6.9",
|
||||
"version": "0.7.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type {
|
||||
IPaymentMethodPublicFinder,
|
||||
IPaymentTermPublicFinder,
|
||||
ITaxDefinitionPublicFinder,
|
||||
ITaxRegimePublicFinder,
|
||||
} from "@erp/catalogs/api";
|
||||
@ -19,6 +20,7 @@ export function buildProformaCatalogResolvers(params: {
|
||||
taxDefinitionFinder: ITaxDefinitionPublicFinder;
|
||||
taxRegimeFinder: ITaxRegimePublicFinder;
|
||||
paymentMethodFinder: IPaymentMethodPublicFinder;
|
||||
paymentTermFinder: IPaymentTermPublicFinder;
|
||||
}): IProformaCatalogResolvers {
|
||||
return {
|
||||
taxResolver: buildProformaTaxResolver({
|
||||
@ -28,6 +30,7 @@ export function buildProformaCatalogResolvers(params: {
|
||||
|
||||
paymentResolver: new ProformaPaymentResolver({
|
||||
paymentMethodFinder: params.paymentMethodFinder,
|
||||
paymentTermFinder: params.paymentTermFinder,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -146,7 +146,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
||||
companyId: params.companyId,
|
||||
status: InvoiceStatus.draft(),
|
||||
|
||||
invoiceNumber: invoiceNumber!,
|
||||
//invoiceNumber: invoiceNumber!,
|
||||
series: series!,
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
export type ProformaCreateInputProps = Omit<IProformaCreateProps, "items"> & {
|
||||
export type ProformaCreateInputProps = Omit<IProformaCreateProps, "items" | "invoiceNumber"> & {
|
||||
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 Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
@ -26,13 +26,10 @@ export class ProformaPaymentResolver {
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
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
|
||||
public async ensurePaymentMethodById(params: {
|
||||
companyId: UniqueID;
|
||||
@ -59,4 +56,31 @@ export class ProformaPaymentResolver {
|
||||
|
||||
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(
|
||||
props: ProformaCreateInputProps
|
||||
): Promise<Result<IProformaCreateProps, Error>> {
|
||||
): Promise<Result<Omit<IProformaCreateProps, "invoiceNumber">, Error>> {
|
||||
// Tax Regime => comprobar que existe
|
||||
const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({
|
||||
companyId: props.companyId,
|
||||
|
||||
@ -74,6 +74,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
taxDefinitionFinder: catalogs.taxDefinition.finder,
|
||||
taxRegimeFinder: catalogs.taxRegime.finder,
|
||||
paymentMethodFinder: catalogs.paymentMethod.finder,
|
||||
paymentTermFinder: catalogs.paymentTerm.finder,
|
||||
});
|
||||
|
||||
const readModelAssemblers = buildProformaReadModelAssemblers({
|
||||
@ -82,6 +83,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
});
|
||||
|
||||
const proformaNumberService = buildProformaNumberGenerator();
|
||||
|
||||
const finder = buildProformaFinder(repository);
|
||||
|
||||
const creator = buildProformaCreator({
|
||||
|
||||
@ -11,6 +11,9 @@ type ProformaCatalogsDeps = {
|
||||
paymentMethod: {
|
||||
finder: CatalogsPublicServicesType["paymentMethods"]["finder"];
|
||||
};
|
||||
paymentTerm: {
|
||||
finder: CatalogsPublicServicesType["paymentTerms"]["finder"];
|
||||
};
|
||||
};
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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,
|
||||
@ -45,5 +55,8 @@ export function resolveProformaCatalogsDeps(params: ModuleParams): ProformaCatal
|
||||
paymentMethod: {
|
||||
finder: paymentMethod.finder,
|
||||
},
|
||||
paymentTerm: {
|
||||
finder: paymentTerm.finder,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
||||
private _recipientMapper: SequelizeProformaRecipientDomainMapper;
|
||||
private _taxesMapper: SequelizeProformaTaxesDomainMapper;
|
||||
|
||||
constructor(params: MapperParamsType) {
|
||||
constructor(params?: MapperParamsType) {
|
||||
super();
|
||||
|
||||
this._itemsMapper = new SequelizeProformaItemDomainMapper();
|
||||
|
||||
@ -57,7 +57,11 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
||||
layout: "app-fullscreen",
|
||||
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";
|
||||
|
||||
@ -13,7 +13,7 @@ export const CustomerInvoicesModuleManifest: IModuleClient = {
|
||||
protected: true, // protegido por defecto
|
||||
layout: "app-sidebar", // layout por defecto
|
||||
|
||||
routes: (params) => CustomerInvoiceRoutes(params),
|
||||
routes: (params: ModuleClientParams) => CustomerInvoiceRoutes(params),
|
||||
};
|
||||
|
||||
export default CustomerInvoicesModuleManifest;
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
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);
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
const normalizedError = normalizeSubmitError(error);
|
||||
|
||||
// No revierto el form para que no se pierdan los cambios que
|
||||
@ -221,7 +222,6 @@ export const useUpdateProformaController = (
|
||||
}
|
||||
},
|
||||
(errors: FieldErrors<ProformaUpdateForm>) => {
|
||||
console.error(errors);
|
||||
focusFirstInputFormError(form);
|
||||
|
||||
showWarningToast(
|
||||
|
||||
@ -33,9 +33,6 @@ type ProformaUpdateEditorProps = {
|
||||
totalsCtrl: UseUpdateProformaTotalsControllerResult;
|
||||
paymentCtrl: UseUpdateProformaPaymentControllerResult;
|
||||
|
||||
currencyCode?: string;
|
||||
languageCode?: string;
|
||||
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@ -51,57 +48,51 @@ export const ProformaUpdateEditorForm = ({
|
||||
taxCtrl,
|
||||
totalsCtrl,
|
||||
paymentCtrl,
|
||||
currencyCode,
|
||||
languageCode,
|
||||
className,
|
||||
}: ProformaUpdateEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<form
|
||||
className={cn("space-y-6 2xl:space-y-12", className)}
|
||||
id={formId}
|
||||
noValidate
|
||||
onKeyDown={preventEnterKeySubmitForm}
|
||||
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} />
|
||||
<div className={cn("p-6", className)}>
|
||||
<form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}>
|
||||
<div className="grid grid-cols-1 2xl:grid-cols-4 gap-6 2xl:gap-12">
|
||||
<div className="2xl:col-span-3 space-y-6 gap-6 2xl:gap-12 2xl:space-y-12">
|
||||
<div className="grid 2xl:grid-cols-3 gap-6 2xl:gap-6 space-y-12 2xl:space-y-12">
|
||||
<ProformaUpdateHeaderEditor className="2xl:col-span-full" disabled={isSubmitting} />
|
||||
</div>
|
||||
|
||||
<ProformaUpdateItemsEditor
|
||||
disabled={isSubmitting}
|
||||
itemsCtrl={itemsCtrl}
|
||||
taxCtrl={taxCtrl}
|
||||
totalsCtrl={totalsCtrl}
|
||||
/>
|
||||
</div>
|
||||
<div className="2xl:col-span-1 space-y-6 2xl:space-y-12">
|
||||
<ProformaUpdateRecipientEditor
|
||||
className="2xl:col-span-1"
|
||||
disabled={isSubmitting}
|
||||
onChangeCustomerClick={onChangeCustomerClick}
|
||||
onCreateCustomerClick={onCreateCustomerClick}
|
||||
selectedCustomer={selectedCustomer}
|
||||
/>
|
||||
|
||||
<ProformaUpdateItemsEditor
|
||||
disabled={isSubmitting}
|
||||
itemsCtrl={itemsCtrl}
|
||||
taxCtrl={taxCtrl}
|
||||
totalsCtrl={totalsCtrl}
|
||||
/>
|
||||
<ProformaUpdateTaxEditor
|
||||
className="2xl:col-span-1"
|
||||
taxCtrl={taxCtrl}
|
||||
taxRegimeOptions={taxCtrl.taxRegimeOptions}
|
||||
/>
|
||||
|
||||
<ProformaUpdatePaymentEditor
|
||||
className="w-full"
|
||||
disabled={isSubmitting || paymentCtrl.isLoading}
|
||||
paymentMethodOptions={paymentCtrl.paymentMethodOptions}
|
||||
paymentTermOptions={paymentCtrl.paymentTermOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="2xl:col-span-1 space-y-4 2xl:space-y-6">
|
||||
<ProformaUpdateRecipientEditor
|
||||
className="2xl:col-span-1"
|
||||
disabled={isSubmitting}
|
||||
onChangeCustomerClick={onChangeCustomerClick}
|
||||
onCreateCustomerClick={onCreateCustomerClick}
|
||||
selectedCustomer={selectedCustomer}
|
||||
/>
|
||||
|
||||
<ProformaUpdateTaxEditor
|
||||
className="2xl:col-span-1"
|
||||
taxCtrl={taxCtrl}
|
||||
taxRegimeOptions={taxCtrl.taxRegimeOptions}
|
||||
/>
|
||||
|
||||
<ProformaUpdatePaymentEditor
|
||||
className="w-full"
|
||||
disabled={isSubmitting || paymentCtrl.isLoading}
|
||||
paymentMethodOptions={paymentCtrl.paymentMethodOptions}
|
||||
paymentTermOptions={paymentCtrl.paymentTermOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Footer fijo con totales */}
|
||||
</form>
|
||||
{/* Footer fijo con totales */}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -47,7 +47,7 @@ export const ProformaUpdateHeaderEditor = ({
|
||||
disabled={disabled}
|
||||
label={t("form_fields.proformas.invoice_number.label")}
|
||||
maxLength={16}
|
||||
name="reference"
|
||||
name="invoiceNumber"
|
||||
placeholder={t("form_fields.proformas.invoice_number.placeholder")}
|
||||
readOnly={true}
|
||||
rightIcon={
|
||||
|
||||
@ -101,12 +101,9 @@ export const ProformaUpdatePage = () => {
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<ProformaUpdateEditorForm
|
||||
className="p-6 space-y-6 "
|
||||
currencyCode={updateCtrl.currencyCode}
|
||||
formId={updateCtrl.formId}
|
||||
isSubmitting={updateCtrl.isUpdating}
|
||||
itemsCtrl={updateCtrl.itemsCtrl}
|
||||
languageCode={updateCtrl.languageCode}
|
||||
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
||||
onCreateCustomerClick={() => null}
|
||||
onReset={updateCtrl.resetForm}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/customers",
|
||||
"description": "Customers",
|
||||
"version": "0.6.9",
|
||||
"version": "0.7.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"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 {
|
||||
CreateCustomerInputMapper,
|
||||
type ICreateCustomerInputMapper,
|
||||
@ -12,11 +10,9 @@ export interface ICustomerInputMappers {
|
||||
updateInputMapper: IUpdateCustomerInputMapper;
|
||||
}
|
||||
|
||||
export const buildCustomerInputMappers = (catalogs: ICatalogs): ICustomerInputMappers => {
|
||||
const { taxCatalog } = catalogs;
|
||||
|
||||
export const buildCustomerInputMappers = (): ICustomerInputMappers => {
|
||||
// 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();
|
||||
|
||||
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 { ICustomerCreator, ICustomerFinder, ICustomerUpdater } from "../services";
|
||||
import type { ICustomerFullReadModelAssembler } from "../services/assemblers";
|
||||
import type {
|
||||
ICustomerFullSnapshotBuilder,
|
||||
ICustomerSummarySnapshotBuilder,
|
||||
@ -15,10 +16,11 @@ import {
|
||||
|
||||
export function buildGetCustomerByIdUseCase(deps: {
|
||||
finder: ICustomerFinder;
|
||||
fullReadModelAssembler: ICustomerFullReadModelAssembler;
|
||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new GetCustomerByIdUseCase(deps.finder, deps.fullSnapshotBuilder, deps.transactionManager);
|
||||
return new GetCustomerByIdUseCase(deps);
|
||||
}
|
||||
|
||||
export function buildListCustomersUseCase(deps: {
|
||||
@ -36,29 +38,21 @@ export function buildListCustomersUseCase(deps: {
|
||||
export function buildCreateCustomerUseCase(deps: {
|
||||
creator: ICustomerCreator;
|
||||
dtoMapper: ICreateCustomerInputMapper;
|
||||
fullReadModelAssembler: ICustomerFullReadModelAssembler;
|
||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new CreateCustomerUseCase({
|
||||
dtoMapper: deps.dtoMapper,
|
||||
creator: deps.creator,
|
||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||
transactionManager: deps.transactionManager,
|
||||
});
|
||||
return new CreateCustomerUseCase(deps);
|
||||
}
|
||||
|
||||
export function buildUpdateCustomerUseCase(deps: {
|
||||
updater: ICustomerUpdater;
|
||||
dtoMapper: IUpdateCustomerInputMapper;
|
||||
fullReadModelAssembler: ICustomerFullReadModelAssembler;
|
||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new UpdateCustomerUseCase({
|
||||
dtoMapper: deps.dtoMapper,
|
||||
updater: deps.updater,
|
||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||
transactionManager: deps.transactionManager,
|
||||
});
|
||||
return new UpdateCustomerUseCase(deps);
|
||||
}
|
||||
|
||||
/*export function buildReportCustomerUseCase(deps: {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
export * from "./customer-catalog-resolvers.di";
|
||||
export * from "./customer-creator.di";
|
||||
export * from "./customer-finder.di";
|
||||
export * from "./customer-input-mappers.di";
|
||||
export * from "./customer-read-model-assemblers.di";
|
||||
export * from "./customer-snapshot-builders.di";
|
||||
export * from "./customer-updater.di";
|
||||
export * from "./customer-use-cases.di";
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { JsonTaxCatalogProvider } from "@erp/core";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
@ -35,12 +34,6 @@ export interface ICreateCustomerInputMapper {
|
||||
}
|
||||
|
||||
export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
||||
private readonly taxCatalog: JsonTaxCatalogProvider;
|
||||
|
||||
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
|
||||
this.taxCatalog = params.taxCatalog;
|
||||
}
|
||||
|
||||
public map(
|
||||
dto: CreateCustomerRequestDTO,
|
||||
params: { companyId: UniqueID }
|
||||
@ -52,7 +45,7 @@ export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
||||
const customerId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
||||
|
||||
const status = CustomerStatus.createActive();
|
||||
const isCompany = dto.is_company === "true";
|
||||
const isCompany = Boolean(dto.is_company);
|
||||
|
||||
const reference = extractOrPushError(
|
||||
maybeFromNullableResult(dto.reference, (value) => Name.create(value)),
|
||||
@ -164,6 +157,27 @@ export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
||||
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(
|
||||
LanguageCode.create(dto.language_code),
|
||||
"language_code",
|
||||
@ -178,15 +192,6 @@ export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
||||
|
||||
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) {
|
||||
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors));
|
||||
}
|
||||
@ -225,7 +230,14 @@ export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
||||
website: website!,
|
||||
|
||||
legalRecord: legalRecord!,
|
||||
defaultTaxes: defaultTaxes!,
|
||||
|
||||
paymentMethodId: paymentMethodId!,
|
||||
paymentTermId: paymentTermId!,
|
||||
taxRegimeCode: taxRegimeCode!,
|
||||
|
||||
usesEquivalenceSurcharge: usesEquivalenceSurcharge,
|
||||
usesRetention: usesRetention,
|
||||
|
||||
languageCode: languageCode!,
|
||||
currencyCode: currencyCode!,
|
||||
};
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./create-customer-input.mapper";
|
||||
export * from "./tax-definition-to-tax.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,
|
||||
TextValue,
|
||||
URLAddress,
|
||||
type UniqueID,
|
||||
UniqueID,
|
||||
ValidationErrorCollection,
|
||||
type ValidationErrorDetail,
|
||||
extractOrPushError,
|
||||
@ -115,30 +115,55 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Se mantiene la compatibilidad con el contrato actual.
|
||||
* Cuando defaultTaxes tenga un contrato final estable, aquí conviene
|
||||
* mapearlo a CustomerTaxes/Collection<TaxCode> con su schema semántico real.
|
||||
*/
|
||||
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.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);
|
||||
|
||||
this.throwIfValidationErrors(errors);
|
||||
|
||||
if (addressPatchProps) {
|
||||
customerPatchProps.address = addressPatchProps;
|
||||
}
|
||||
|
||||
this.mapContact(dto.contact, customerPatchProps, errors);
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Customer props mapping failed (update)", errors)
|
||||
);
|
||||
}
|
||||
this.throwIfValidationErrors(errors);
|
||||
|
||||
console.log("dto => ", dto);
|
||||
console.log("customerPatchProps => ", customerPatchProps);
|
||||
|
||||
return Result.ok(customerPatchProps);
|
||||
} 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 { ICreateCustomerInputMapper } from "../mappers";
|
||||
import type { ICustomerCreator } from "../services";
|
||||
import type { ICustomerFullReadModelAssembler } from "../services/assemblers";
|
||||
import type { ICustomerFullSnapshotBuilder } from "../snapshot-builders";
|
||||
|
||||
type CreateCustomerUseCaseInput = {
|
||||
@ -12,47 +13,46 @@ type CreateCustomerUseCaseInput = {
|
||||
dto: CreateCustomerRequestDTO;
|
||||
};
|
||||
|
||||
type CreateCustomerUseCaseDeps = {
|
||||
dtoMapper: ICreateCustomerInputMapper;
|
||||
creator: ICustomerCreator;
|
||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||
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;
|
||||
}
|
||||
constructor(
|
||||
private readonly deps: {
|
||||
dtoMapper: ICreateCustomerInputMapper;
|
||||
creator: ICustomerCreator;
|
||||
fullReadModelAssembler: ICustomerFullReadModelAssembler;
|
||||
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
}
|
||||
) {}
|
||||
|
||||
public execute(params: CreateCustomerUseCaseInput) {
|
||||
const { dto, companyId } = params;
|
||||
|
||||
const mappedPropsResult = this.dtoMapper.map(dto, { companyId });
|
||||
const mappedPropsResult = this.deps.dtoMapper.map(dto, { companyId });
|
||||
if (mappedPropsResult.isFailure) {
|
||||
return mappedPropsResult;
|
||||
}
|
||||
|
||||
const { props, id } = mappedPropsResult.data;
|
||||
|
||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
||||
return this.deps.transactionManager.complete(async (transaction: unknown) => {
|
||||
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) {
|
||||
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);
|
||||
} catch (error: unknown) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export * from "./create-customer.use-case";
|
||||
export * from "./get-customer-by-id.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 = {
|
||||
name: "customers",
|
||||
version: "1.0.0",
|
||||
dependencies: [],
|
||||
dependencies: ["catalogs"],
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
export interface ICustomerPersistenceMappers {
|
||||
@ -9,13 +7,9 @@ export interface ICustomerPersistenceMappers {
|
||||
//createMapper: CreateCustomerInputMapper;
|
||||
}
|
||||
|
||||
export const buildCustomerPersistenceMappers = (
|
||||
catalogs: ICatalogs
|
||||
): ICustomerPersistenceMappers => {
|
||||
const { taxCatalog } = catalogs;
|
||||
|
||||
export const buildCustomerPersistenceMappers = (): ICustomerPersistenceMappers => {
|
||||
// Mappers para el repositorio
|
||||
const domainMapper = new SequelizeCustomerDomainMapper({ taxCatalog });
|
||||
const domainMapper = new SequelizeCustomerDomainMapper();
|
||||
const summaryMapper = new SequelizeCustomerSummaryMapper();
|
||||
|
||||
// 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 { Result } from "@repo/rdx-utils";
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from "../../application";
|
||||
import type { ICustomerCreateProps } from "../../domain";
|
||||
|
||||
import { resolveCustomerCatalogsDeps } from "./customer-catalog-deps.di";
|
||||
import { buildCustomerPersistenceMappers } from "./customer-persistence-mappers.di";
|
||||
import { buildCustomerRepository } from "./customer-repositories.di";
|
||||
import type { CustomersInternalDeps } from "./customers.di";
|
||||
@ -19,10 +20,10 @@ export function buildCustomerPublicServices(
|
||||
deps: CustomersInternalDeps
|
||||
): ICustomerPublicServices {
|
||||
const { database } = params;
|
||||
const catalogs = buildCatalogs();
|
||||
const catalogs = resolveCustomerCatalogsDeps(params);
|
||||
|
||||
// Infrastructure
|
||||
const persistenceMappers = buildCustomerPersistenceMappers(catalogs);
|
||||
const persistenceMappers = buildCustomerPersistenceMappers();
|
||||
const repository = buildCustomerRepository({ database, mappers: persistenceMappers });
|
||||
|
||||
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 {
|
||||
type CreateCustomerUseCase,
|
||||
@ -15,7 +15,10 @@ import {
|
||||
buildListCustomersUseCase,
|
||||
buildUpdateCustomerUseCase,
|
||||
} 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 { buildCustomerRepository } from "./customer-repositories.di";
|
||||
|
||||
@ -39,21 +42,33 @@ export type CustomersInternalDeps = {
|
||||
export function buildCustomersDependencies(params: ModuleParams): CustomersInternalDeps {
|
||||
const { database } = params;
|
||||
|
||||
// Infrastructure
|
||||
const catalogs = resolveCustomerCatalogsDeps(params);
|
||||
|
||||
const transactionManager = buildTransactionManager(database);
|
||||
const catalogs = buildCatalogs();
|
||||
const persistenceMappers = buildCustomerPersistenceMappers(catalogs);
|
||||
const persistenceMappers = buildCustomerPersistenceMappers();
|
||||
|
||||
const repository = buildCustomerRepository({ database, mappers: persistenceMappers });
|
||||
//const numberService = buildCustomerNumberGenerator();
|
||||
|
||||
// Application helpers
|
||||
const inputMappers = buildCustomerInputMappers(catalogs);
|
||||
const inputMappers = buildCustomerInputMappers();
|
||||
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 creator = buildCustomerCreator({ repository });
|
||||
const updater = buildCustomerUpdater({ repository });
|
||||
|
||||
const snapshotBuilders = buildCustomerSnapshotBuilders();
|
||||
//const documentGeneratorPipeline = buildCustomerDocumentService(params);
|
||||
|
||||
// Internal use cases (factories)
|
||||
@ -69,6 +84,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
|
||||
getCustomerById: () =>
|
||||
buildGetCustomerByIdUseCase({
|
||||
finder,
|
||||
fullReadModelAssembler: readModelAssemblers.full,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
transactionManager,
|
||||
}),
|
||||
@ -77,6 +93,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
|
||||
buildCreateCustomerUseCase({
|
||||
creator,
|
||||
dtoMapper: inputMappers.createInputMapper,
|
||||
fullReadModelAssembler: readModelAssemblers.full,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
transactionManager,
|
||||
}),
|
||||
@ -85,6 +102,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
|
||||
buildUpdateCustomerUseCase({
|
||||
updater,
|
||||
dtoMapper: inputMappers.updateInputMapper,
|
||||
fullReadModelAssembler: readModelAssemblers.full,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { TaxCatalogProvider } from "@erp/core";
|
||||
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
City,
|
||||
@ -32,13 +31,6 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
|
||||
CustomerCreationAttributes,
|
||||
Customer
|
||||
> {
|
||||
private readonly taxCatalog: TaxCatalogProvider;
|
||||
|
||||
constructor(params: { taxCatalog: TaxCatalogProvider }) {
|
||||
super();
|
||||
this.taxCatalog = params.taxCatalog;
|
||||
}
|
||||
|
||||
public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result<Customer, Error> {
|
||||
try {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
|
||||
@ -67,11 +67,13 @@ export class CustomerRepository
|
||||
const dtoResult = this.domainMapper.mapToPersistence(customer);
|
||||
|
||||
const { id, ...updatePayload } = dtoResult.data;
|
||||
|
||||
console.log(dtoResult.data);
|
||||
|
||||
const [affected] = await CustomerModel.update(updatePayload, {
|
||||
where: { id /*, version */ },
|
||||
//fields: Object.keys(updatePayload),
|
||||
transaction,
|
||||
individualHooks: true,
|
||||
//individualHooks: true,
|
||||
});
|
||||
|
||||
if (affected === 0) {
|
||||
|
||||
@ -4,11 +4,10 @@ export const CreateCustomerRequestSchema = z.object({
|
||||
id: z.string().nonempty(),
|
||||
|
||||
reference: z.string().optional(),
|
||||
is_company: z.string().toLowerCase().default("true"),
|
||||
is_company: z.boolean().default(true),
|
||||
name: z.string(),
|
||||
trade_name: z.string().optional(),
|
||||
tin: z.string().optional(),
|
||||
default_taxes: z.string().default(""),
|
||||
|
||||
street: z.string().optional(),
|
||||
street2: z.string().optional(),
|
||||
@ -32,8 +31,8 @@ export const CreateCustomerRequestSchema = z.object({
|
||||
payment_method_id: z.uuid().nullable().optional(),
|
||||
payment_term_id: z.uuid().nullable().optional(),
|
||||
tax_regime_code: z.string().nullable().optional(),
|
||||
uses_equivalence_surcharge: z.boolean().optional(),
|
||||
uses_retention: z.boolean().optional(),
|
||||
uses_equivalence_surcharge: z.boolean().default(false),
|
||||
uses_retention: z.boolean().default(false),
|
||||
|
||||
language_code: z.string().toLowerCase().default("es"),
|
||||
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";
|
||||
|
||||
export const useCustomerCreatePageController = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const returnTo = searchParams.get("returnTo") ?? "/customers";
|
||||
|
||||
const createCtrl = useCustomerCreateController({
|
||||
onCreated: (customer) => navigate(`/customers/${customer.id}`),
|
||||
@ -11,5 +14,6 @@ export const useCustomerCreatePageController = () => {
|
||||
|
||||
return {
|
||||
createCtrl,
|
||||
returnTo,
|
||||
};
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ import type { FieldErrors } from "react-hook-form";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import type { CreateCustomerParams, Customer } from "../../shared";
|
||||
import { useCustomerCreateMutation } from "../../shared/hooks/use-customer-create-mutation";
|
||||
import { mapCustomerToCustomerCreateForm } from "../adapters";
|
||||
import {
|
||||
type CustomerCreateForm,
|
||||
CustomerCreateFormSchema,
|
||||
@ -47,13 +48,19 @@ export const useCustomerCreateController = (options?: UseCustomerCreateControlle
|
||||
};
|
||||
|
||||
const submitHandler = form.handleSubmit(
|
||||
async (formData) => {
|
||||
async (formData: CustomerCreateForm) => {
|
||||
const params: CreateCustomerParams = buildCreateCustomerParams(formData);
|
||||
|
||||
try {
|
||||
// Enviamos cambios al servidor
|
||||
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) {
|
||||
showSuccessToast(
|
||||
t("create.feedback.success.title"),
|
||||
|
||||
@ -19,7 +19,6 @@ export interface CustomerCreateForm {
|
||||
name: string;
|
||||
tradeName: string;
|
||||
tin: string;
|
||||
defaultTaxes: string[];
|
||||
|
||||
street: string;
|
||||
street2: string;
|
||||
|
||||
@ -1,3 +1,13 @@
|
||||
import {
|
||||
CountryCodeSchema,
|
||||
CurrencyCodeSchema,
|
||||
LandPhoneSchema,
|
||||
LanguageCodeSchema,
|
||||
MobilePhoneSchema,
|
||||
PostalCodeSchema,
|
||||
TinSchema,
|
||||
WebsiteSchema,
|
||||
} from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
/**
|
||||
@ -17,32 +27,31 @@ import { z } from "zod/v4";
|
||||
export const CustomerCreateFormSchema = z.object({
|
||||
reference: z.string(),
|
||||
isCompany: z.boolean(),
|
||||
|
||||
name: z.string().min(1, "El nombre es obligatorio"),
|
||||
tradeName: z.string(),
|
||||
tin: z.string(),
|
||||
|
||||
defaultTaxes: z.array(z.string()),
|
||||
tin: TinSchema.or(z.literal("")),
|
||||
|
||||
street: z.string(),
|
||||
street2: z.string(),
|
||||
city: z.string(),
|
||||
province: z.string(),
|
||||
postalCode: z.string(),
|
||||
country: z.string().min(1, "El país es obligatorio"),
|
||||
postalCode: PostalCodeSchema.or(z.literal("")),
|
||||
country: CountryCodeSchema.or(z.literal("")),
|
||||
|
||||
primaryEmail: z.email("Email inválido"),
|
||||
secondaryEmail: z.email("Email inválido"),
|
||||
primaryEmail: z.email("Email inválido").or(z.literal("")),
|
||||
secondaryEmail: z.email("Email inválido").or(z.literal("")),
|
||||
|
||||
primaryPhone: z.string(),
|
||||
secondaryPhone: z.string(),
|
||||
primaryMobile: z.string(),
|
||||
secondaryMobile: z.string(),
|
||||
primaryPhone: LandPhoneSchema.or(z.literal("")),
|
||||
secondaryPhone: LandPhoneSchema.or(z.literal("")),
|
||||
primaryMobile: MobilePhoneSchema.or(z.literal("")),
|
||||
secondaryMobile: MobilePhoneSchema.or(z.literal("")),
|
||||
|
||||
fax: z.string(),
|
||||
website: z.url("URL inválida"),
|
||||
fax: LandPhoneSchema.or(z.literal("")),
|
||||
website: WebsiteSchema.or(z.literal("")),
|
||||
|
||||
legalRecord: z.string(),
|
||||
legalRecord: z.string().or(z.literal("")),
|
||||
|
||||
languageCode: z.string().min(1, "El idioma es obligatorio"),
|
||||
currencyCode: z.string().min(1, "La moneda es obligatoria"),
|
||||
languageCode: LanguageCodeSchema,
|
||||
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 { useTranslation } from "../../../i18n";
|
||||
import type { CustomerUpdateForm } from "../../entities";
|
||||
|
||||
import { CustomerTaxesMultiSelect } from "./customer-taxes-multi-select";
|
||||
import type { CustomerCreateForm } from "../../entities";
|
||||
|
||||
interface CustomerBasicInfoFieldsProps extends React.ComponentProps<typeof FieldSet> {}
|
||||
|
||||
export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicInfoFieldsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { control, setFocus } = useFormContext<CustomerUpdateForm>();
|
||||
const { control, setFocus } = useFormContext<CustomerCreateForm>();
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
@ -60,17 +58,17 @@ export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicIn
|
||||
disabled={field.disabled}
|
||||
name={field.name}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value === "true");
|
||||
field.onChange(value === "company");
|
||||
}}
|
||||
required
|
||||
value={field.value ? "true" : "false"}
|
||||
value={field.value ? "company" : "individual"}
|
||||
>
|
||||
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
||||
<FieldContent>
|
||||
<RadioGroupItem
|
||||
aria-invalid={fieldState.invalid}
|
||||
id="customer-type-company"
|
||||
value="true"
|
||||
value="company"
|
||||
/>
|
||||
<FieldLabel
|
||||
className="cursor-pointer text-sm font-medium leading-none"
|
||||
@ -86,7 +84,7 @@ export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicIn
|
||||
<RadioGroupItem
|
||||
aria-invalid={fieldState.invalid}
|
||||
id="customer-type-individual"
|
||||
value="false"
|
||||
value="individual"
|
||||
/>
|
||||
<FieldLabel
|
||||
className="cursor-pointer text-sm font-medium leading-none"
|
||||
@ -111,7 +109,6 @@ export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicIn
|
||||
label={t("fields.tin.label")}
|
||||
name="tin"
|
||||
placeholder={t("fields.tin.placeholder")}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextField
|
||||
@ -130,32 +127,6 @@ export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicIn
|
||||
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
|
||||
className="lg:col-span-full"
|
||||
description={t("fields.legal_record.description")}
|
||||
|
||||
@ -37,7 +37,6 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
|
||||
}
|
||||
name="primaryEmail"
|
||||
placeholder={t("fields.email_primary.placeholder")}
|
||||
required
|
||||
typePreset="email"
|
||||
/>
|
||||
|
||||
|
||||
@ -1,61 +1,64 @@
|
||||
import { ErrorAlert, PageHeader } from "@erp/core/components";
|
||||
import { UnsavedChangesProvider } from "@erp/core/hooks";
|
||||
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
|
||||
import { ErrorAlert } from "@erp/core/components";
|
||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||
import { FormProvider } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import { useCustomerCreatePageController } from "../../controllers";
|
||||
import { CustomerCreateHeader } from "../blocks";
|
||||
import { CustomerCreateEditorForm } from "../editor";
|
||||
|
||||
export const CustomerCreatePage = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { createCtrl } = useCustomerCreatePageController();
|
||||
const { createCtrl, returnTo } = useCustomerCreatePageController();
|
||||
|
||||
const { form, formId, onSubmit, resetForm, isCreating, isCreateError, createError } = createCtrl;
|
||||
|
||||
const { navigateBack } = useReturnToNavigation({
|
||||
fallbackPath: returnTo,
|
||||
});
|
||||
|
||||
const handleCancel = () => navigateBack();
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
||||
<FormProvider {...form}>
|
||||
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
|
||||
<AppHeader className="mx-auto max-w-7xl space-y-4">
|
||||
<PageHeader
|
||||
description={t("create.description")}
|
||||
onBackClick={() => navigate("/customers/list")}
|
||||
rightSlot={
|
||||
/*
|
||||
<FormCommitButtonGroup
|
||||
cancel={{
|
||||
to: "/customers/list",
|
||||
}}
|
||||
disabled={createCtrl.isCreating}
|
||||
isLoading={createCtrl.isCreating}
|
||||
onReset={createCtrl.form.formState.isDirty ? createCtrl.resetForm : undefined}
|
||||
submit={{
|
||||
formId: createCtrl.formId,
|
||||
}}
|
||||
/>
|
||||
*/ <></>
|
||||
}
|
||||
title={t("create.title")}
|
||||
<FormProvider {...createCtrl.form}>
|
||||
<UnsavedChangesProvider isDirty={createCtrl.form.formState.isDirty}>
|
||||
<CustomerCreateHeader
|
||||
formId={createCtrl.formId}
|
||||
hasChanges={createCtrl.form.formState.isDirty}
|
||||
isSaving={createCtrl.form.formState.isSubmitting}
|
||||
labels={{
|
||||
title: "Nuevo cliente",
|
||||
back: "Volver",
|
||||
modified: "",
|
||||
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}
|
||||
/>
|
||||
|
||||
{isCreateError && (
|
||||
<ErrorAlert
|
||||
message={(createError as Error)?.message ?? t("create.errors.message")}
|
||||
title={t("create.errors.title")}
|
||||
/>
|
||||
</AppHeader>
|
||||
|
||||
<AppContent className="mx-auto max-w-7xl space-y-4">
|
||||
{isCreateError && (
|
||||
<ErrorAlert
|
||||
message={(createError as Error)?.message ?? t("create.errors.message")}
|
||||
title={t("create.errors.title")}
|
||||
/>
|
||||
)}
|
||||
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<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}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</AppContent>
|
||||
</div>
|
||||
</UnsavedChangesProvider>
|
||||
</FormProvider>
|
||||
</div>
|
||||
|
||||
@ -30,11 +30,10 @@ export const buildCreateCustomerParams = (formData: CustomerCreateForm): CreateC
|
||||
data: {
|
||||
id,
|
||||
reference: formData.reference,
|
||||
is_company: formData.isCompany ? "1" : "0",
|
||||
is_company: formData.isCompany,
|
||||
name: formData.name,
|
||||
trade_name: formData.tradeName,
|
||||
tin: formData.tin,
|
||||
default_taxes: formData.defaultTaxes.join(";"),
|
||||
|
||||
street: formData.street,
|
||||
street2: formData.street2,
|
||||
@ -55,6 +54,9 @@ export const buildCreateCustomerParams = (formData: CustomerCreateForm): CreateC
|
||||
|
||||
legal_record: formData.legalRecord,
|
||||
|
||||
uses_equivalence_surcharge: false,
|
||||
uses_retention: false,
|
||||
|
||||
language_code: formData.languageCode,
|
||||
currency_code: formData.currencyCode,
|
||||
},
|
||||
|
||||
@ -51,7 +51,11 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||
layout: "app-fullscreen",
|
||||
protected: true,
|
||||
},
|
||||
element: <CustomerCreatePage />,
|
||||
element: (
|
||||
<CustomerLayout>
|
||||
<CustomerCreatePage />
|
||||
</CustomerLayout>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
@ -60,7 +64,11 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||
layout: "app-fullscreen",
|
||||
protected: true,
|
||||
},
|
||||
element: <CustomerUpdatePage />,
|
||||
element: (
|
||||
<CustomerLayout>
|
||||
<CustomerUpdatePage />
|
||||
</CustomerLayout>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
@ -69,7 +77,11 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||
layout: "app-fullscreen",
|
||||
protected: true,
|
||||
},
|
||||
element: <CustomerViewPage />,
|
||||
element: (
|
||||
<CustomerLayout>
|
||||
<CustomerViewPage />
|
||||
</CustomerLayout>
|
||||
),
|
||||
},
|
||||
|
||||
/*
|
||||
|
||||
@ -8,13 +8,12 @@ const MODULE_VERSION = "1.0.0";
|
||||
export const CustomersModuleManifest: IModuleClient = {
|
||||
name: MODULE_NAME,
|
||||
version: MODULE_VERSION,
|
||||
dependencies: ["auth", "Core"],
|
||||
protected: true,
|
||||
layout: "app",
|
||||
dependencies: ["auth", "Core", "Catalogs"],
|
||||
|
||||
routes: (params: ModuleClientParams) => {
|
||||
return CustomerRoutes(params);
|
||||
},
|
||||
protected: true, // protegido por defecto
|
||||
layout: "app-sidebar", // layout por defecto
|
||||
|
||||
routes: (params: ModuleClientParams) => CustomerRoutes(params),
|
||||
};
|
||||
|
||||
export default CustomersModuleManifest;
|
||||
|
||||
@ -12,11 +12,6 @@ import type { Customer } from "../entities";
|
||||
|
||||
export const GetCustomerByIdAdapter = {
|
||||
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 {
|
||||
id: dto.id,
|
||||
companyId: dto.company_id,
|
||||
@ -49,7 +44,14 @@ export const GetCustomerByIdAdapter = {
|
||||
|
||||
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,
|
||||
languageCode: dto.language_code,
|
||||
currencyCode: dto.currency_code,
|
||||
|
||||
@ -38,7 +38,26 @@ export interface Customer {
|
||||
|
||||
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;
|
||||
currencyCode: string;
|
||||
|
||||
@ -13,3 +13,17 @@ export function toValidationErrors(error: ZodError<unknown>) {
|
||||
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";
|
||||
|
||||
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,34 +13,46 @@ import type { CustomerUpdateForm } from "../entities"; /**
|
||||
*/
|
||||
import { buildCustomerUpdateDefault } from "../utils";
|
||||
|
||||
export const mapCustomerToCustomerUpdateForm = (customer: Customer): CustomerUpdateForm => ({
|
||||
reference: customer.reference ?? "",
|
||||
isCompany: customer.isCompany ?? buildCustomerUpdateDefault().isCompany,
|
||||
name: customer.name,
|
||||
tradeName: customer.tradeName ?? "",
|
||||
tin: customer.tin ?? "",
|
||||
export const mapCustomerToCustomerUpdateForm = (customer: Customer): CustomerUpdateForm => {
|
||||
console.log(customer);
|
||||
|
||||
defaultTaxes: customer.defaultTaxes ?? [],
|
||||
return {
|
||||
reference: customer.reference ?? "",
|
||||
isCompany: customer.isCompany ?? buildCustomerUpdateDefault().isCompany,
|
||||
name: customer.name,
|
||||
tradeName: customer.tradeName ?? "",
|
||||
tin: customer.tin ?? "",
|
||||
|
||||
street: customer.address.street ?? "",
|
||||
street2: customer.address.street2 ?? "",
|
||||
city: customer.address.city ?? "",
|
||||
province: customer.address.province ?? "",
|
||||
postalCode: customer.address.postalCode ?? "",
|
||||
country: customer.address.country ?? buildCustomerUpdateDefault().country,
|
||||
street: customer.address.street ?? "",
|
||||
street2: customer.address.street2 ?? "",
|
||||
city: customer.address.city ?? "",
|
||||
province: customer.address.province ?? "",
|
||||
postalCode: customer.address.postalCode ?? "",
|
||||
country: customer.address.country ?? buildCustomerUpdateDefault().country,
|
||||
|
||||
primaryEmail: customer.contact.primaryEmail ?? "",
|
||||
secondaryEmail: customer.contact.secondaryEmail ?? "",
|
||||
primaryPhone: customer.contact.primaryPhone ?? "",
|
||||
secondaryPhone: customer.contact.secondaryPhone ?? "",
|
||||
primaryMobile: customer.contact.primaryMobile ?? "",
|
||||
secondaryMobile: customer.contact.secondaryMobile ?? "",
|
||||
primaryEmail: customer.contact.primaryEmail ?? "",
|
||||
secondaryEmail: customer.contact.secondaryEmail ?? "",
|
||||
primaryPhone: customer.contact.primaryPhone ?? "",
|
||||
secondaryPhone: customer.contact.secondaryPhone ?? "",
|
||||
primaryMobile: customer.contact.primaryMobile ?? "",
|
||||
secondaryMobile: customer.contact.secondaryMobile ?? "",
|
||||
|
||||
fax: customer.contact.fax ?? "",
|
||||
website: customer.contact.website ?? "",
|
||||
fax: customer.contact.fax ?? "",
|
||||
website: customer.contact.website ?? "",
|
||||
|
||||
legalRecord: customer.legalRecord ?? "",
|
||||
legalRecord: customer.legalRecord ?? "",
|
||||
|
||||
languageCode: customer.languageCode ?? buildCustomerUpdateDefault().languageCode,
|
||||
currencyCode: customer.currencyCode ?? buildCustomerUpdateDefault().currencyCode,
|
||||
});
|
||||
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,
|
||||
currencyCode: customer.currencyCode ?? buildCustomerUpdateDefault().currencyCode,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./use-customer-update.controller";
|
||||
export * from "./use-customer-update-fiscal-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,
|
||||
} from "../utils";
|
||||
|
||||
import { useCustomerUpdateFiscalController } from "./use-customer-update-fiscal-controller";
|
||||
|
||||
export interface UseCustomerUpdateControllerOptions {
|
||||
onUpdated?(updated: Customer): void;
|
||||
successToasts?: boolean; // mostrar o no toast automáticamente
|
||||
@ -155,6 +157,7 @@ export const useCustomerUpdateController = (
|
||||
options?.onUpdated?.(updated);
|
||||
} catch (error: unknown) {
|
||||
const normalizedError = normalizeSubmitError(error);
|
||||
console.error(normalizedError);
|
||||
|
||||
// No revierto el form para que no se pierdan los cambios que
|
||||
// ha hecho el usuario y no han sido guardados.
|
||||
@ -192,6 +195,7 @@ export const useCustomerUpdateController = (
|
||||
}
|
||||
},
|
||||
(errors: FieldErrors<CustomerUpdateForm>) => {
|
||||
console.error(errors);
|
||||
focusFirstInputFormError(form);
|
||||
|
||||
showWarningToast(
|
||||
@ -207,6 +211,8 @@ export const useCustomerUpdateController = (
|
||||
submitHandler(event);
|
||||
};
|
||||
|
||||
const fiscalCtrl = useCustomerUpdateFiscalController();
|
||||
|
||||
return {
|
||||
// form
|
||||
form,
|
||||
@ -227,6 +233,9 @@ export const useCustomerUpdateController = (
|
||||
isUpdateError,
|
||||
updateError,
|
||||
|
||||
// Otros controladores
|
||||
fiscalCtrl,
|
||||
|
||||
// No devolver FormProvider, así el controller es más
|
||||
// flexible y reusable (p.ej. para un modal)
|
||||
// FormProvider,
|
||||
|
||||
@ -20,8 +20,6 @@ export interface CustomerUpdateForm {
|
||||
tradeName: string;
|
||||
tin: string;
|
||||
|
||||
defaultTaxes: string[];
|
||||
|
||||
street: string;
|
||||
street2: string;
|
||||
city: string;
|
||||
@ -41,6 +39,13 @@ export interface CustomerUpdateForm {
|
||||
|
||||
legalRecord: string;
|
||||
|
||||
paymentMethodId: string;
|
||||
paymentTermId: string;
|
||||
taxRegimeCode: string;
|
||||
|
||||
usesEquivalenceSurcharge: boolean;
|
||||
usesRetention: boolean;
|
||||
|
||||
languageCode: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
MobilePhoneSchema,
|
||||
PostalCodeSchema,
|
||||
TinSchema,
|
||||
URLSchema,
|
||||
WebsiteSchema,
|
||||
} from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
@ -31,9 +31,7 @@ export const CustomerUpdateFormSchema = z.object({
|
||||
|
||||
name: z.string().min(1, "El nombre es obligatorio"),
|
||||
tradeName: z.string(),
|
||||
tin: TinSchema,
|
||||
|
||||
defaultTaxes: z.array(z.string()),
|
||||
tin: TinSchema.or(z.literal("")),
|
||||
|
||||
street: z.string(),
|
||||
street2: z.string(),
|
||||
@ -50,11 +48,18 @@ export const CustomerUpdateFormSchema = z.object({
|
||||
primaryMobile: MobilePhoneSchema.or(z.literal("")),
|
||||
secondaryMobile: MobilePhoneSchema.or(z.literal("")),
|
||||
|
||||
fax: LandPhoneSchema.or(z.literal("")).or(z.literal("")),
|
||||
website: URLSchema.or(z.literal("")).or(z.literal("")),
|
||||
fax: LandPhoneSchema.or(z.literal("")),
|
||||
website: WebsiteSchema.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,
|
||||
currencyCode: CurrencyCodeSchema,
|
||||
});
|
||||
|
||||
@ -16,8 +16,6 @@ export type CustomerUpdatePatch = {
|
||||
tradeName?: string | null;
|
||||
tin?: string | null;
|
||||
|
||||
defaultTaxes?: string[];
|
||||
|
||||
street?: string | null;
|
||||
street2?: string | null;
|
||||
city?: string | null;
|
||||
@ -37,6 +35,13 @@ export type CustomerUpdatePatch = {
|
||||
|
||||
legalRecord?: string | null;
|
||||
|
||||
paymentMethodId?: string | null;
|
||||
paymentTermId?: string | null;
|
||||
taxRegimeCode?: string | null;
|
||||
|
||||
usesEquivalenceSurcharge?: boolean;
|
||||
usesRetention?: boolean;
|
||||
|
||||
languageCode?: string;
|
||||
currencyCode?: string;
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { FormSectionCard, FormSectionGrid, SelectField, TextField } from "@repo/rdx-ui/components";
|
||||
import { MapPinIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import { COUNTRY_OPTIONS } from "../../../shared";
|
||||
@ -17,11 +18,11 @@ export const CustomerAddressEditor = ({
|
||||
return (
|
||||
<FormSectionCard
|
||||
description={t("form_groups.address.description")}
|
||||
icon={<MapPinIcon className="size-5" />}
|
||||
title={t("form_groups.address.title")}
|
||||
>
|
||||
<FormSectionGrid>
|
||||
<FormSectionGrid className="md:grid-cols-2">
|
||||
<TextField
|
||||
className="md:col-span-6"
|
||||
description={t("fields.street.description")}
|
||||
disabled={disabled}
|
||||
label={t("fields.street.label")}
|
||||
@ -30,7 +31,6 @@ export const CustomerAddressEditor = ({
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
<TextField
|
||||
className="md:col-span-6"
|
||||
description={t("fields.street2.description")}
|
||||
disabled={disabled}
|
||||
label={t("fields.street2.label")}
|
||||
@ -40,7 +40,6 @@ export const CustomerAddressEditor = ({
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="md:col-span-6"
|
||||
description={t("fields.city.description")}
|
||||
disabled={disabled}
|
||||
label={t("fields.city.label")}
|
||||
@ -50,17 +49,6 @@ export const CustomerAddressEditor = ({
|
||||
/>
|
||||
|
||||
<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")}
|
||||
disabled={disabled}
|
||||
label={t("fields.postal_code.label")}
|
||||
@ -69,8 +57,16 @@ export const CustomerAddressEditor = ({
|
||||
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
|
||||
className="md:col-span-2"
|
||||
description={t("fields.country.description")}
|
||||
disabled={disabled}
|
||||
items={[...COUNTRY_OPTIONS]}
|
||||
|
||||
@ -14,12 +14,11 @@ import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { Building2Icon, FileTextIcon, UserIcon } from "lucide-react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
|
||||
import { CustomerTaxesMultiSelect } from "./customer-taxes-multi-select";
|
||||
|
||||
interface CustomerBasicInfoEditorProps {
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
@ -35,11 +34,11 @@ export const CustomerBasicInfoEditor = ({
|
||||
return (
|
||||
<FormSectionCard
|
||||
description={t("form_groups.basic_info.description")}
|
||||
icon={<FileTextIcon className="size-5" />}
|
||||
title={t("form_groups.basic_info.title")}
|
||||
>
|
||||
<FormSectionGrid>
|
||||
<FormSectionGrid className="md:grid-cols-2">
|
||||
<TextField
|
||||
className="md:col-span-6"
|
||||
description={t("fields.name.description")}
|
||||
disabled={disabled}
|
||||
label={t("fields.name.label")}
|
||||
@ -49,57 +48,69 @@ export const CustomerBasicInfoEditor = ({
|
||||
required
|
||||
/>
|
||||
|
||||
<TextField
|
||||
description={t("fields.tin.description")}
|
||||
disabled={disabled}
|
||||
label={t("fields.tin.label")}
|
||||
name="tin"
|
||||
placeholder={t("fields.tin.placeholder")}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="isCompany"
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<Field className="md:col-span-3" data-invalid={fieldState.invalid}>
|
||||
<FormFieldLabel required>{t("fields.customer_type.label")}</FormFieldLabel>
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<div className="flex flex-col gap-2 pt-1">
|
||||
<FormFieldLabel required>{t("fields.customer_type.label")}</FormFieldLabel>
|
||||
|
||||
<RadioGroup
|
||||
className="gap-3"
|
||||
disabled={field.disabled}
|
||||
name={field.name}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value === "true");
|
||||
}}
|
||||
required
|
||||
value={field.value ? "true" : "false"}
|
||||
>
|
||||
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
||||
<FieldContent>
|
||||
<RadioGroupItem
|
||||
aria-invalid={fieldState.invalid}
|
||||
id="customer-type-company"
|
||||
value="true"
|
||||
/>
|
||||
<FieldLabel
|
||||
className="cursor-pointer text-sm font-medium leading-none"
|
||||
htmlFor="customer-type-company"
|
||||
>
|
||||
{t("fields.customer_type.options.company")}
|
||||
</FieldLabel>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
||||
<FieldContent>
|
||||
<RadioGroupItem
|
||||
aria-invalid={fieldState.invalid}
|
||||
id="customer-type-individual"
|
||||
value="false"
|
||||
/>
|
||||
<FieldLabel
|
||||
className="cursor-pointer text-sm font-medium leading-none"
|
||||
htmlFor="customer-type-individual"
|
||||
>
|
||||
{t("fields.customer_type.options.individual")}
|
||||
</FieldLabel>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</RadioGroup>
|
||||
<RadioGroup
|
||||
className="gap-3"
|
||||
disabled={field.disabled}
|
||||
name={field.name}
|
||||
onValueChange={(value: boolean) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
required
|
||||
value={field.value}
|
||||
>
|
||||
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel
|
||||
className="cursor-pointer text-sm font-medium leading-none"
|
||||
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")}
|
||||
</FieldLabel>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel
|
||||
className="cursor-pointer text-sm font-medium leading-none"
|
||||
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")}
|
||||
</FieldLabel>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<FieldDescription>{t("fields.customer_type.description")}</FieldDescription>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
@ -108,18 +119,6 @@ export const CustomerBasicInfoEditor = ({
|
||||
/>
|
||||
|
||||
<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")}
|
||||
disabled={disabled}
|
||||
label={t("fields.trade_name.label")}
|
||||
@ -129,7 +128,6 @@ export const CustomerBasicInfoEditor = ({
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="md:col-span-6 md:col-start-1"
|
||||
description={t("fields.reference.description")}
|
||||
disabled={disabled}
|
||||
label={t("fields.reference.label")}
|
||||
@ -138,34 +136,7 @@ export const CustomerBasicInfoEditor = ({
|
||||
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
|
||||
className="md:col-span-full"
|
||||
description={t("fields.legal_record.description")}
|
||||
disabled={disabled}
|
||||
label={t("fields.legal_record.label")}
|
||||
|
||||
@ -1,15 +1,23 @@
|
||||
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 { CustomerAddressEditor } from "./customer-address-editor";
|
||||
import { CustomerBasicInfoEditor } from "./customer-basic-info-editor";
|
||||
import { CustomerContactEditor } from "./customer-contact-fields";
|
||||
import { CustomerFiscalEditor } from "./customer-fiscal-editor";
|
||||
|
||||
type CustomerUpdateEditorFormProps = {
|
||||
formId: string;
|
||||
isSubmitting: boolean;
|
||||
onSubmit: React.SubmitEventHandler<HTMLFormElement>;
|
||||
onReset: () => void;
|
||||
|
||||
fiscalCtrl: UseCustomerUpdateFiscalControllerResult;
|
||||
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const CustomerUpdateEditorForm = ({
|
||||
@ -17,19 +25,30 @@ export const CustomerUpdateEditorForm = ({
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
onReset,
|
||||
|
||||
fiscalCtrl,
|
||||
|
||||
className,
|
||||
}: CustomerUpdateEditorFormProps) => {
|
||||
return (
|
||||
<form
|
||||
className="space-y-6"
|
||||
id={formId}
|
||||
noValidate
|
||||
onKeyDown={preventEnterKeySubmitForm}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<CustomerBasicInfoEditor disabled={isSubmitting} />
|
||||
<CustomerAddressEditor />
|
||||
<CustomerContactEditor />
|
||||
<CustomerAdditionalConfigEditor />
|
||||
</form>
|
||||
<div className={cn("p-6", className)}>
|
||||
<form
|
||||
className="space-y-6 2xl:space-y-12"
|
||||
id={formId}
|
||||
noValidate
|
||||
onKeyDown={preventEnterKeySubmitForm}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<CustomerBasicInfoEditor disabled={isSubmitting} />
|
||||
<CustomerAddressEditor />
|
||||
<CustomerFiscalEditor
|
||||
paymentMethodOptions={fiscalCtrl.paymentMethodOptions}
|
||||
paymentTermOptions={fiscalCtrl.paymentTermOptions}
|
||||
taxRegimeOptions={fiscalCtrl.taxRegimeOptions}
|
||||
/>
|
||||
<CustomerContactEditor />
|
||||
<CustomerAdditionalConfigEditor />
|
||||
</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 { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import { Spinner } from "@repo/shadcn-ui/components";
|
||||
import { FormProvider } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
@ -81,16 +80,25 @@ export const CustomerUpdatePage = () => {
|
||||
//onExportPdf={handleExportPdf}
|
||||
/>
|
||||
|
||||
{updateCtrl.isLoading && <Spinner />}
|
||||
|
||||
{!updateCtrl.isLoading && (
|
||||
{updateCtrl.isUpdateError && (
|
||||
<ErrorAlert
|
||||
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
|
||||
className="max-w-5xl mx-auto"
|
||||
fiscalCtrl={updateCtrl.fiscalCtrl}
|
||||
formId={updateCtrl.formId}
|
||||
isSubmitting={updateCtrl.isUpdating}
|
||||
onReset={updateCtrl.resetForm}
|
||||
onSubmit={updateCtrl.onSubmit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</UnsavedChangesProvider>
|
||||
</FormProvider>
|
||||
</div>
|
||||
|
||||
@ -8,8 +8,6 @@ export const buildCustomerUpdateDefault = (): CustomerUpdateForm => {
|
||||
tradeName: "",
|
||||
tin: "",
|
||||
|
||||
defaultTaxes: [],
|
||||
|
||||
street: "",
|
||||
street2: "",
|
||||
city: "",
|
||||
@ -29,6 +27,13 @@ export const buildCustomerUpdateDefault = (): CustomerUpdateForm => {
|
||||
|
||||
legalRecord: "",
|
||||
|
||||
paymentMethodId: "",
|
||||
paymentTermId: "",
|
||||
taxRegimeCode: "",
|
||||
|
||||
usesEquivalenceSurcharge: false,
|
||||
usesRetention: false,
|
||||
|
||||
languageCode: "es",
|
||||
currencyCode: "EUR",
|
||||
};
|
||||
|
||||
@ -47,10 +47,6 @@ export const buildCustomerUpdatePatch = (
|
||||
patch.tin = toNullableText(formData.tin);
|
||||
}
|
||||
|
||||
if (dirtyFields.defaultTaxes) {
|
||||
patch.defaultTaxes = formData.defaultTaxes;
|
||||
}
|
||||
|
||||
if (dirtyFields.street) {
|
||||
patch.street = toNullableText(formData.street);
|
||||
}
|
||||
@ -111,6 +107,26 @@ export const buildCustomerUpdatePatch = (
|
||||
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) {
|
||||
patch.languageCode = toRequiredText(formData.languageCode);
|
||||
}
|
||||
@ -119,6 +135,8 @@ export const buildCustomerUpdatePatch = (
|
||||
patch.currencyCode = toRequiredText(formData.currencyCode);
|
||||
}
|
||||
|
||||
console.log(dirtyFields, patch);
|
||||
|
||||
return patch;
|
||||
};
|
||||
|
||||
|
||||
@ -46,10 +46,6 @@ export const buildUpdateCustomerByIdParams = (
|
||||
data.tin = patchData.tin;
|
||||
}
|
||||
|
||||
if (patchData.defaultTaxes !== undefined) {
|
||||
data.default_taxes = patchData.defaultTaxes?.toString();
|
||||
}
|
||||
|
||||
if (patchData.legalRecord !== undefined) {
|
||||
data.legal_record = patchData.legalRecord;
|
||||
}
|
||||
@ -62,6 +58,27 @@ export const buildUpdateCustomerByIdParams = (
|
||||
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 ---
|
||||
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";
|
||||
|
||||
export function useCustomerViewPageController() {
|
||||
const viewCtrl = useCustomerViewController();
|
||||
const customerId = useUrlParamId();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const viewCtrl = useCustomerViewController(customerId);
|
||||
|
||||
const returnTo = searchParams.get("returnTo") ?? "/customers";
|
||||
|
||||
return {
|
||||
viewCtrl,
|
||||
returnTo,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,23 +1,24 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useCustomerGetQuery } from "../../shared/hooks";
|
||||
|
||||
export const useCustomerViewController = (initialCustomerId = "") => {
|
||||
const [customerId, setCustomerId] = useState(initialCustomerId);
|
||||
|
||||
const query = useCustomerGetQuery(customerId);
|
||||
export const useCustomerViewController = (customerId?: string) => {
|
||||
const {
|
||||
data: customerData,
|
||||
isLoading,
|
||||
isError: isLoadError,
|
||||
error: loadError,
|
||||
refetch,
|
||||
} = useCustomerGetQuery({
|
||||
id: customerId,
|
||||
enabled: Boolean(customerId),
|
||||
});
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
// carga de datos
|
||||
customerData,
|
||||
isLoading,
|
||||
isLoadError,
|
||||
loadError,
|
||||
|
||||
isError: query.isError,
|
||||
error: query.error,
|
||||
|
||||
refetch: query.refetch,
|
||||
|
||||
customerId,
|
||||
setCustomerId,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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 { useUrlParamId } from "@erp/core/hooks";
|
||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { ErrorAlert, NotFoundCard } from "@erp/core/components";
|
||||
import { useReturnToNavigation } from "@erp/core/hooks";
|
||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
Banknote,
|
||||
EditIcon,
|
||||
FileText,
|
||||
Globe,
|
||||
Languages,
|
||||
Mail,
|
||||
MapPin,
|
||||
MoreVertical,
|
||||
Phone,
|
||||
Smartphone,
|
||||
} from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import { CustomerEditorSkeleton } from "../../../update/ui/components";
|
||||
import { useCustomerViewPageController } from "../../controllers";
|
||||
import { CustomerViewHeader } from "../blocks";
|
||||
|
||||
export const CustomerViewPage = () => {
|
||||
const initialCustomerId = useUrlParamId();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
viewCtrl: { setCustomerId, customerId, data: customerData, isError, error, isLoading },
|
||||
} = useCustomerViewPageController();
|
||||
const { viewCtrl, returnTo } = useCustomerViewPageController();
|
||||
|
||||
useEffect(() => {
|
||||
if (initialCustomerId && customerId !== initialCustomerId) {
|
||||
setCustomerId(initialCustomerId);
|
||||
}
|
||||
}, [initialCustomerId, customerId, setCustomerId]);
|
||||
const { navigateBack } = useReturnToNavigation({
|
||||
fallbackPath: returnTo,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <CustomerEditorSkeleton />;
|
||||
const handleCancel = () => navigateBack();
|
||||
|
||||
if (viewCtrl.isLoading) {
|
||||
return <CustomerEditorSkeleton />; /*<CustomerViewSkeleton />;*/
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (viewCtrl.isLoadError) {
|
||||
return (
|
||||
<>
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
message={
|
||||
(error as Error)?.message ??
|
||||
t("view.errors.load_message")
|
||||
}
|
||||
message={(viewCtrl.loadError as Error)?.message ?? t("view.errors.load_message")}
|
||||
title={t("view.errors.load_title")}
|
||||
/>
|
||||
|
||||
@ -67,332 +50,314 @@ export const CustomerViewPage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
if (!viewCtrl.customerData)
|
||||
return (
|
||||
<AppContent>
|
||||
{/* Main Content Grid */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Información Básica */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<FileText className="size-5 text-primary" />
|
||||
{t("view.sections.basic_info")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.name")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.name}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.reference")}
|
||||
</dt>
|
||||
<dd className="mt-1 font-mono text-base text-foreground">
|
||||
{customerData?.reference}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.legal_record")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.legalRecord}</dd>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<NotFoundCard
|
||||
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>
|
||||
);
|
||||
|
||||
{/* Dirección */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<MapPin className="size-5 text-primary" />
|
||||
{t("view.sections.address")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
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 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<FileText className="size-5 text-primary" />
|
||||
{t("view.sections.basic_info")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">{t("view.fields.name")}</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{viewCtrl.customerData?.name}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.reference")}
|
||||
</dt>
|
||||
<dd className="mt-1 font-mono text-base text-foreground">
|
||||
{viewCtrl.customerData?.reference}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.legal_record")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{viewCtrl.customerData?.legalRecord}
|
||||
</dd>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Dirección */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<MapPin className="size-5 text-primary" />
|
||||
{t("view.sections.address")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.street")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{viewCtrl.customerData?.street}
|
||||
{viewCtrl.customerData?.street2 && (
|
||||
<>
|
||||
<br />
|
||||
{viewCtrl.customerData?.street2}
|
||||
</>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.street")}
|
||||
{t("view.fields.city")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{viewCtrl.customerData?.city}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.postal_code")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.street}
|
||||
{customerData?.street2 && (
|
||||
<>
|
||||
<br />
|
||||
{customerData?.street2}
|
||||
</>
|
||||
)}
|
||||
{viewCtrl.customerData?.postalCode}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.city")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.city}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.postal_code")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.postalCode}</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.province")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{viewCtrl.customerData?.province}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.province")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.province}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.country")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.country}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.country")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{viewCtrl.customerData?.country}</dd>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Información de Contacto */}
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Mail className="size-5 text-primary" />
|
||||
{t("view.sections.contact_info")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Contacto Principal */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{t("view.sections.primary_contact")}
|
||||
</h3>
|
||||
{customerData?.primaryEmail && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.email")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.primaryEmail}
|
||||
</dd>
|
||||
</div>
|
||||
{/* Información de Contacto */}
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Mail className="size-5 text-primary" />
|
||||
{t("view.sections.contact_info")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Contacto Principal */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{t("view.sections.primary_contact")}
|
||||
</h3>
|
||||
{viewCtrl.customerData?.primaryEmail && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.email")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{viewCtrl.customerData?.primaryEmail}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{customerData?.primaryMobile && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.mobile")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.primaryMobile}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{viewCtrl.customerData?.primaryMobile && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.mobile")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{viewCtrl.customerData?.primaryMobile}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{customerData?.primaryPhone && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.phone")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.primaryPhone}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contacto Secundario */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{t("view.sections.secondary_contact")}
|
||||
</h3>
|
||||
{customerData?.secondaryEmail && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.email")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.secondaryEmail}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{customerData?.secondaryMobile && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.mobile")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.secondaryMobile}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{customerData?.secondaryPhone && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.phone")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{customerData?.secondaryPhone}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Otros Contactos */}
|
||||
{(customerData?.website || customerData?.fax) && (
|
||||
<div className="space-y-4 md:col-span-2">
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{t("view.sections.other_contacts")}
|
||||
</h3>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{customerData?.website && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Globe className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.website")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-primary hover:underline">
|
||||
<a
|
||||
href={customerData?.website}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{customerData?.website}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{customerData?.fax && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.fax")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.fax}</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{viewCtrl.customerData?.primaryPhone && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.phone")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{viewCtrl.customerData?.primaryPhone}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Preferencias */}
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Languages className="size-5 text-primary" />
|
||||
{t("view.sections.preferences")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<Languages className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.language_code")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.languageCode}</dd>
|
||||
{/* Contacto Secundario */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{t("view.sections.secondary_contact")}
|
||||
</h3>
|
||||
{viewCtrl.customerData?.secondaryEmail && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.email")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{viewCtrl.customerData?.secondaryEmail}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{viewCtrl.customerData?.secondaryMobile && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.mobile")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{viewCtrl.customerData?.secondaryMobile}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{viewCtrl.customerData?.secondaryPhone && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.phone")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{viewCtrl.customerData?.secondaryPhone}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Otros Contactos */}
|
||||
{(viewCtrl.customerData?.website || viewCtrl.customerData?.fax) && (
|
||||
<div className="space-y-4 md:col-span-2">
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{t("view.sections.other_contacts")}
|
||||
</h3>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{viewCtrl.customerData?.website && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Globe className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.website")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-primary hover:underline">
|
||||
<a
|
||||
href={viewCtrl.customerData?.website}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{viewCtrl.customerData?.website}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{viewCtrl.customerData?.fax && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.fax")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{viewCtrl.customerData?.fax}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Banknote className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.currency_code")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">{customerData?.currencyCode}</dd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Preferencias */}
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Languages className="size-5 text-primary" />
|
||||
{t("view.sections.preferences")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<Languages className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.language_code")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{viewCtrl.customerData?.languageCode}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
<div className="flex items-start gap-3">
|
||||
<Banknote className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
{t("view.fields.currency_code")}
|
||||
</dt>
|
||||
<dd className="mt-1 text-base text-foreground">
|
||||
{viewCtrl.customerData?.currencyCode}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/factuges",
|
||||
"version": "0.6.9",
|
||||
"version": "0.7.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
@ -23,6 +23,7 @@
|
||||
"dependencies": {
|
||||
"@erp/auth": "workspace:*",
|
||||
"@erp/core": "workspace:*",
|
||||
"@erp/catalogs": "workspace:*",
|
||||
"@erp/customer-invoices": "workspace:*",
|
||||
"@erp/customers": "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