.
This commit is contained in:
parent
a8ea434ce1
commit
6a19698adc
@ -1,3 +1,5 @@
|
|||||||
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EmailAddress,
|
EmailAddress,
|
||||||
PhoneNumber,
|
PhoneNumber,
|
||||||
@ -5,10 +7,11 @@ import {
|
|||||||
TINNumber,
|
TINNumber,
|
||||||
UniqueID,
|
UniqueID,
|
||||||
} from "@/core/common/domain";
|
} from "@/core/common/domain";
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
|
||||||
import { Account, IAccountProps } from "../aggregates";
|
import { Account, type IAccountProps } from "../aggregates";
|
||||||
import { IAccountRepository } from "../repositories";
|
import type { IAccountRepository } from "../repositories";
|
||||||
import { AccountStatus } from "../value-objects";
|
import { AccountStatus } from "../value-objects";
|
||||||
|
|
||||||
import { AccountService } from "./account.service";
|
import { AccountService } from "./account.service";
|
||||||
|
|
||||||
const mockAccountRepository: IAccountRepository = {
|
const mockAccountRepository: IAccountRepository = {
|
||||||
@ -113,8 +116,6 @@ describe("AccountService - Integración", () => {
|
|||||||
|
|
||||||
const result = await accountService.activateAccount(existingAccount.id);
|
const result = await accountService.activateAccount(existingAccount.id);
|
||||||
|
|
||||||
console.log(result);
|
|
||||||
|
|
||||||
expect(result.isSuccess).toBe(true);
|
expect(result.isSuccess).toBe(true);
|
||||||
expect(result.data.isActive).toBeTruthy();
|
expect(result.data.isActive).toBeTruthy();
|
||||||
expect(mockAccountRepository.update).toHaveBeenCalledWith(result.data);
|
expect(mockAccountRepository.update).toHaveBeenCalledWith(result.data);
|
||||||
|
|||||||
@ -40,7 +40,6 @@ function listarDirectorio(directorio: string): string[] {
|
|||||||
|
|
||||||
// Retornar rutas absolutas (opcional)
|
// Retornar rutas absolutas (opcional)
|
||||||
const result = archivos.map((nombre) => path.join(directorio, nombre));
|
const result = archivos.map((nombre) => path.join(directorio, nombre));
|
||||||
console.log(result);
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(`Error al listar el directorio: ${error.message}`);
|
throw new Error(`Error al listar el directorio: ${error.message}`);
|
||||||
|
|||||||
@ -218,8 +218,6 @@ function validateModuleDependencies() {
|
|||||||
const declared = new Set(pkg.dependencies ?? []);
|
const declared = new Set(pkg.dependencies ?? []);
|
||||||
const used = usedDependenciesByModule.get(moduleName) ?? new Set<string>();
|
const used = usedDependenciesByModule.get(moduleName) ?? new Set<string>();
|
||||||
|
|
||||||
console.log(declared, used);
|
|
||||||
|
|
||||||
// ❌ usadas pero no declaradas
|
// ❌ usadas pero no declaradas
|
||||||
const undeclaredUsed = [...used].filter((d) => !declared.has(d));
|
const undeclaredUsed = [...used].filter((d) => !declared.has(d));
|
||||||
|
|
||||||
|
|||||||
@ -5,14 +5,13 @@ export const LoginPage = () => {
|
|||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
|
|
||||||
const handleOnSubmit = (data) => {
|
const handleOnSubmit = (data) => {
|
||||||
console.log(data);
|
|
||||||
const { email, password } = data;
|
const { email, password } = data;
|
||||||
login(email, password);
|
login(email, password);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-svh w-full items-center justify-center p-6 md:p-10'>
|
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
<div className='w-full max-w-sm'>
|
<div className="w-full max-w-sm">
|
||||||
<LoginForm onSubmit={handleOnSubmit} />
|
<LoginForm onSubmit={handleOnSubmit} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export const FormCommitButtonGroup = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const ctx = useFormContext();
|
const ctx = useFormContext();
|
||||||
const rhfIsSubmitting = !!ctx.formState.isSubmitting;
|
const rhfIsSubmitting = ctx.formState.isSubmitting;
|
||||||
const busy = isLoading ?? rhfIsSubmitting;
|
const busy = isLoading ?? rhfIsSubmitting;
|
||||||
|
|
||||||
const showCancel = cancel?.show ?? true;
|
const showCancel = cancel?.show ?? true;
|
||||||
|
|||||||
@ -72,11 +72,6 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
|
|||||||
): Promise<R> => {
|
): Promise<R> => {
|
||||||
const url = `${resource}/${id}`;
|
const url = `${resource}/${id}`;
|
||||||
|
|
||||||
console.log("Axios updateOne => ", {
|
|
||||||
url,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await client.put<R, AxiosResponse<R>, TData>(
|
const res = await client.put<R, AxiosResponse<R>, TData>(
|
||||||
url,
|
url,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@ -533,12 +533,16 @@ export default (database: Sequelize) => {
|
|||||||
|
|
||||||
{ name: "uq_invoice_proforma_id", fields: ["proforma_id"], unique: true }, // <- para asegurar que una proforma solo tenga una factura vinculada
|
{ name: "uq_invoice_proforma_id", fields: ["proforma_id"], unique: true }, // <- para asegurar que una proforma solo tenga una factura vinculada
|
||||||
|
|
||||||
// Para búsquedas simples
|
// Para búsquedas simples => se hace con el "CriteriaToSequelizeConverter"
|
||||||
{
|
/*{
|
||||||
name: "ft_customer_invoice",
|
name: "ft_customer_invoice",
|
||||||
type: "FULLTEXT",
|
type: "FULLTEXT",
|
||||||
fields: ["invoice_number", "reference", "description"],
|
fields: [
|
||||||
},
|
"CustomerInvoiceModel.invoice_number",
|
||||||
|
"CustomerInvoiceModel.reference",
|
||||||
|
"CustomerInvoiceModel.description",
|
||||||
|
],
|
||||||
|
},*/
|
||||||
],
|
],
|
||||||
|
|
||||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|||||||
@ -220,7 +220,9 @@ export class IssuedInvoiceRepository
|
|||||||
const query = criteriaConverter.convert(criteria, {
|
const query = criteriaConverter.convert(criteria, {
|
||||||
searchableFields: ["invoice_number", "reference", "description"],
|
searchableFields: ["invoice_number", "reference", "description"],
|
||||||
mappings: {
|
mappings: {
|
||||||
|
invoice_number: "CustomerInvoiceModel.invoice_number",
|
||||||
reference: "CustomerInvoiceModel.reference",
|
reference: "CustomerInvoiceModel.reference",
|
||||||
|
description: "CustomerInvoiceModel.description",
|
||||||
},
|
},
|
||||||
allowedFields: ["invoice_date", "id", "created_at"],
|
allowedFields: ["invoice_date", "id", "created_at"],
|
||||||
enableFullText: true,
|
enableFullText: true,
|
||||||
|
|||||||
@ -437,7 +437,9 @@ export class ProformaRepository
|
|||||||
const query = criteriaConverter.convert(criteria, {
|
const query = criteriaConverter.convert(criteria, {
|
||||||
searchableFields: ["invoice_number", "reference", "description"],
|
searchableFields: ["invoice_number", "reference", "description"],
|
||||||
mappings: {
|
mappings: {
|
||||||
|
invoice_number: "CustomerInvoiceModel.invoice_number",
|
||||||
reference: "CustomerInvoiceModel.reference",
|
reference: "CustomerInvoiceModel.reference",
|
||||||
|
description: "CustomerInvoiceModel.description",
|
||||||
},
|
},
|
||||||
allowedFields: ["invoice_date", "id", "created_at"],
|
allowedFields: ["invoice_date", "id", "created_at"],
|
||||||
enableFullText: true,
|
enableFullText: true,
|
||||||
|
|||||||
@ -10,6 +10,6 @@ export const mapProformaFormToCommercialDocumentLines = (
|
|||||||
unitAmount: item.unitAmount,
|
unitAmount: item.unitAmount,
|
||||||
itemDiscountPercentage: item.itemDiscountPercentage,
|
itemDiscountPercentage: item.itemDiscountPercentage,
|
||||||
taxPercentage: item.taxPercentage,
|
taxPercentage: item.taxPercentage,
|
||||||
equivalenceSurchargePercentage: item.equivalenceSurchargePercentage,
|
recPercentage: item.recPercentage,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,6 +24,6 @@ export const mapProformaItemsToProformaItemsUpdateForm = (
|
|||||||
itemDiscountPercentage: item.itemDiscountPercentage ?? null,
|
itemDiscountPercentage: item.itemDiscountPercentage ?? null,
|
||||||
|
|
||||||
taxPercentage: item.ivaPercentage ?? null,
|
taxPercentage: item.ivaPercentage ?? null,
|
||||||
equivalenceSurchargePercentage: item.recPercentage ?? null,
|
recPercentage: item.recPercentage ?? null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import { PercentageHelper } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { Proforma, ProformaItem } from "../../shared";
|
import type { Proforma, ProformaItem } from "../../shared";
|
||||||
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
|
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
|
||||||
|
import { buildProformaUpdateDefault } from "../utils";
|
||||||
|
|
||||||
import { mapProformaItemsToProformaItemsUpdateForm } from "./map-proforma-items-to-proforma-items-update-form.adapter";
|
import { mapProformaItemsToProformaItemsUpdateForm } from "./map-proforma-items-to-proforma-items-update-form.adapter";
|
||||||
|
|
||||||
@ -7,40 +10,46 @@ import { mapProformaItemsToProformaItemsUpdateForm } from "./map-proforma-items-
|
|||||||
* Mapea una proforma a un formulario de actualización de proforma.
|
* Mapea una proforma a un formulario de actualización de proforma.
|
||||||
*/
|
*/
|
||||||
export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpdateForm => {
|
export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpdateForm => {
|
||||||
|
const proformaDefaults = buildProformaUpdateDefault();
|
||||||
|
|
||||||
const taxMode = inferProformaTaxMode(proforma.items);
|
const taxMode = inferProformaTaxMode(proforma.items);
|
||||||
|
|
||||||
const defaultTaxSummary = proforma.taxes[0];
|
const defaultTaxSummary = proforma.taxes[0];
|
||||||
const firstTaxableItem = getFirstTaxableItem(proforma.items);
|
const firstTaxableItem = getFirstTaxableItem(proforma.items);
|
||||||
|
|
||||||
const defaultTaxPercentage =
|
const defaultTaxPercentage =
|
||||||
defaultTaxSummary?.ivaPercentage ?? firstTaxableItem?.ivaPercentage ?? null;
|
defaultTaxSummary?.ivaPercentage ??
|
||||||
|
firstTaxableItem?.ivaPercentage ??
|
||||||
|
proformaDefaults.defaultTaxPercentage;
|
||||||
|
|
||||||
const defaultEquivalenceSurchargePercentage =
|
const defaultRecPercentage =
|
||||||
defaultTaxSummary?.recPercentage ?? firstTaxableItem?.recPercentage ?? null;
|
defaultTaxSummary?.recPercentage ?? firstTaxableItem?.recPercentage ?? null;
|
||||||
|
|
||||||
const defaultRetentionPercentage =
|
const defaultRetentionPercentage =
|
||||||
defaultTaxSummary?.retentionPercentage ?? firstTaxableItem?.retentionPercentage ?? null;
|
defaultTaxSummary?.retentionPercentage ?? firstTaxableItem?.retentionPercentage ?? null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
series: proforma.series ?? "",
|
series: proforma.series ?? proformaDefaults.series,
|
||||||
|
|
||||||
invoiceDate: proforma.invoiceDate ?? "",
|
invoiceDate: proforma.invoiceDate ?? proformaDefaults.invoiceDate,
|
||||||
operationDate: proforma.operationDate ?? "",
|
operationDate: proforma.operationDate ?? proformaDefaults.operationDate,
|
||||||
|
|
||||||
customerId: proforma.customerId ?? "",
|
customerId: proforma.customerId ?? proformaDefaults.customerId,
|
||||||
|
|
||||||
description: proforma.description ?? "",
|
description: proforma.description ?? proformaDefaults.description,
|
||||||
reference: proforma.reference ?? "",
|
reference: proforma.reference ?? proformaDefaults.reference,
|
||||||
notes: proforma.notes ?? "",
|
notes: proforma.notes ?? proformaDefaults.notes,
|
||||||
|
|
||||||
languageCode: proforma.languageCode ?? "es",
|
languageCode: proforma.languageCode ?? proformaDefaults.languageCode,
|
||||||
currencyCode: proforma.currencyCode ?? "EUR",
|
currencyCode: proforma.currencyCode ?? proformaDefaults.currencyCode,
|
||||||
|
|
||||||
globalDiscountPercentage: proforma.globalDiscountPercentage ?? 0,
|
globalDiscountPercentage:
|
||||||
|
proforma.globalDiscountPercentage ?? proformaDefaults.globalDiscountPercentage,
|
||||||
|
|
||||||
taxMode,
|
taxMode,
|
||||||
|
taxRegimeCode: proformaDefaults.taxRegimeCode,
|
||||||
defaultTaxPercentage,
|
defaultTaxPercentage,
|
||||||
hasEquivalenceSurcharge: hasPositivePercentage(defaultEquivalenceSurchargePercentage),
|
hasRecPercentage: hasPositivePercentage(defaultRecPercentage),
|
||||||
hasRetention: hasPositivePercentage(defaultRetentionPercentage),
|
hasRetention: hasPositivePercentage(defaultRetentionPercentage),
|
||||||
retentionPercentage: defaultRetentionPercentage,
|
retentionPercentage: defaultRetentionPercentage,
|
||||||
|
|
||||||
@ -84,5 +93,5 @@ const normalizePercentage = (value: number): number => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasPositivePercentage = (value: number | null | undefined): boolean => {
|
const hasPositivePercentage = (value: number | null | undefined): boolean => {
|
||||||
return value !== null && value !== undefined && value > 0;
|
return PercentageHelper.hasPositivePercentage(value);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { type UseFormReturn, useWatch } from "react-hook-form";
|
import { type UseFormReturn, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
|
import type { ProformaTaxMode, ProformaUpdateForm } from "../entities";
|
||||||
@ -11,7 +11,7 @@ export interface UseUpdateProformaTaxControllerResult {
|
|||||||
taxMode: ProformaTaxMode;
|
taxMode: ProformaTaxMode;
|
||||||
|
|
||||||
defaultTaxPercentage: number | null;
|
defaultTaxPercentage: number | null;
|
||||||
hasEquivalenceSurcharge: boolean;
|
hasRecPercentage: boolean;
|
||||||
hasRetention: boolean;
|
hasRetention: boolean;
|
||||||
retentionPercentage: number | null;
|
retentionPercentage: number | null;
|
||||||
|
|
||||||
@ -22,9 +22,7 @@ export interface UseUpdateProformaTaxControllerResult {
|
|||||||
disablePerLineTaxes: () => void;
|
disablePerLineTaxes: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEquivalenceSurchargePercentage = (
|
const getRecPercentage = (taxPercentage: number | null | undefined): number | null => {
|
||||||
taxPercentage: number | null | undefined
|
|
||||||
): number | null => {
|
|
||||||
if (taxPercentage === 21) return 5.2;
|
if (taxPercentage === 21) return 5.2;
|
||||||
if (taxPercentage === 10) return 1.4;
|
if (taxPercentage === 10) return 1.4;
|
||||||
if (taxPercentage === 4) return 0.5;
|
if (taxPercentage === 4) return 0.5;
|
||||||
@ -38,33 +36,40 @@ export const useUpdateProformaTaxController = ({
|
|||||||
const { control, getValues, setValue } = form;
|
const { control, getValues, setValue } = form;
|
||||||
|
|
||||||
const taxMode = useWatch({ control, name: "taxMode" });
|
const taxMode = useWatch({ control, name: "taxMode" });
|
||||||
const hasEquivalenceSurcharge = useWatch({ control, name: "hasEquivalenceSurcharge" }) ?? false;
|
const hasRecPercentage = useWatch({ control, name: "hasRecPercentage" }) ?? false;
|
||||||
const hasRetention = useWatch({ control, name: "hasRetention" }) ?? false;
|
const hasRetention = useWatch({ control, name: "hasRetention" }) ?? false;
|
||||||
const retentionPercentage = useWatch({ control, name: "retentionPercentage" }) ?? null;
|
const retentionPercentage = useWatch({ control, name: "retentionPercentage" }) ?? null;
|
||||||
const defaultTaxPercentage = useWatch({ control, name: "defaultTaxPercentage" }) ?? null;
|
const defaultTaxPercentage = useWatch({ control, name: "defaultTaxPercentage" }) ?? null;
|
||||||
|
const hasMountedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (taxMode !== "single") return;
|
if (taxMode !== "single") return;
|
||||||
|
|
||||||
const currentItems = getValues("items") ?? [];
|
const currentItems = getValues("items") ?? [];
|
||||||
const equivalenceSurchargePercentage = hasEquivalenceSurcharge
|
const recPercentage = hasRecPercentage ? getRecPercentage(defaultTaxPercentage) : null;
|
||||||
? getEquivalenceSurchargePercentage(defaultTaxPercentage)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
currentItems.forEach((_, index) => {
|
const shouldMarkDirty = hasMountedRef.current;
|
||||||
setValue(`items.${index}.taxPercentage`, defaultTaxPercentage ?? null, {
|
|
||||||
shouldDirty: true,
|
currentItems.forEach((item, index) => {
|
||||||
shouldTouch: true,
|
if (item.taxPercentage !== defaultTaxPercentage) {
|
||||||
|
setValue(`items.${index}.taxPercentage`, defaultTaxPercentage, {
|
||||||
|
shouldDirty: shouldMarkDirty,
|
||||||
|
shouldTouch: false,
|
||||||
shouldValidate: true,
|
shouldValidate: true,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setValue(`items.${index}.equivalenceSurchargePercentage`, equivalenceSurchargePercentage, {
|
if (item.recPercentage !== recPercentage) {
|
||||||
shouldDirty: true,
|
setValue(`items.${index}.recPercentage`, recPercentage, {
|
||||||
shouldTouch: true,
|
shouldDirty: shouldMarkDirty,
|
||||||
|
shouldTouch: false,
|
||||||
shouldValidate: true,
|
shouldValidate: true,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [defaultTaxPercentage, getValues, hasEquivalenceSurcharge, setValue, taxMode]);
|
|
||||||
|
hasMountedRef.current = true;
|
||||||
|
}, [defaultTaxPercentage, getValues, hasRecPercentage, setValue, taxMode]);
|
||||||
|
|
||||||
const enablePerLineTaxes = useCallback(() => {
|
const enablePerLineTaxes = useCallback(() => {
|
||||||
setValue("taxMode", "perLine", {
|
setValue("taxMode", "perLine", {
|
||||||
@ -86,7 +91,7 @@ export const useUpdateProformaTaxController = ({
|
|||||||
taxMode,
|
taxMode,
|
||||||
|
|
||||||
defaultTaxPercentage,
|
defaultTaxPercentage,
|
||||||
hasEquivalenceSurcharge,
|
hasRecPercentage,
|
||||||
hasRetention,
|
hasRetention,
|
||||||
retentionPercentage,
|
retentionPercentage,
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@ export interface CommercialDocumentLineInput {
|
|||||||
unitAmount: number | null;
|
unitAmount: number | null;
|
||||||
itemDiscountPercentage: number | null;
|
itemDiscountPercentage: number | null;
|
||||||
taxPercentage: number | null;
|
taxPercentage: number | null;
|
||||||
|
|
||||||
|
recPercentage: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommercialDocumentLineAmounts {
|
export interface CommercialDocumentLineAmounts {
|
||||||
|
|||||||
@ -11,5 +11,5 @@ export interface ProformaItemUpdateForm {
|
|||||||
itemDiscountPercentage: number | null;
|
itemDiscountPercentage: number | null;
|
||||||
|
|
||||||
taxPercentage: number | null;
|
taxPercentage: number | null;
|
||||||
equivalenceSurchargePercentage: number | null;
|
recPercentage: number | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export const ProformaItemUpdateFormSchema = z.object({
|
|||||||
itemDiscountPercentage: z.number().nullable(),
|
itemDiscountPercentage: z.number().nullable(),
|
||||||
|
|
||||||
taxPercentage: z.number().nullable(),
|
taxPercentage: z.number().nullable(),
|
||||||
equivalenceSurchargePercentage: z.number().nullable(),
|
recPercentage: z.number().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ProformaItemUpdateFormSchemaType = z.infer<typeof ProformaItemUpdateFormSchema>;
|
export type ProformaItemUpdateFormSchemaType = z.infer<typeof ProformaItemUpdateFormSchema>;
|
||||||
|
|||||||
@ -35,9 +35,10 @@ export interface ProformaUpdateForm {
|
|||||||
globalDiscountPercentage: number;
|
globalDiscountPercentage: number;
|
||||||
|
|
||||||
taxMode: ProformaTaxMode;
|
taxMode: ProformaTaxMode;
|
||||||
|
taxRegimeCode: string | null;
|
||||||
defaultTaxPercentage: number | null;
|
defaultTaxPercentage: number | null;
|
||||||
|
|
||||||
hasEquivalenceSurcharge: boolean;
|
hasRecPercentage: boolean;
|
||||||
hasRetention: boolean;
|
hasRetention: boolean;
|
||||||
retentionPercentage: number | null;
|
retentionPercentage: number | null;
|
||||||
|
|
||||||
|
|||||||
@ -34,9 +34,10 @@ export const ProformaUpdateFormSchema = z.object({
|
|||||||
globalDiscountPercentage: z.number().min(0).max(100),
|
globalDiscountPercentage: z.number().min(0).max(100),
|
||||||
|
|
||||||
taxMode: z.enum(["single", "perLine"]),
|
taxMode: z.enum(["single", "perLine"]),
|
||||||
|
taxRegimeCode: z.string(),
|
||||||
defaultTaxPercentage: z.number().nullable(),
|
defaultTaxPercentage: z.number().nullable(),
|
||||||
|
|
||||||
hasEquivalenceSurcharge: z.boolean(),
|
hasRecPercentage: z.boolean(),
|
||||||
hasRetention: z.boolean(),
|
hasRetention: z.boolean(),
|
||||||
retentionPercentage: z.number().nullable(),
|
retentionPercentage: z.number().nullable(),
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export interface ProformaTaxBreakdownLine {
|
|||||||
taxPercentage: number;
|
taxPercentage: number;
|
||||||
taxableBase: number;
|
taxableBase: number;
|
||||||
taxAmount: number;
|
taxAmount: number;
|
||||||
equivalenceSurchargePercentage: number | null;
|
recPercentage: number | null;
|
||||||
equivalenceSurchargeAmount: number;
|
equivalenceSurchargeAmount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -104,13 +104,13 @@ export const ProformaTotalsSummary = ({
|
|||||||
value={formatMoney(tax.taxAmount)}
|
value={formatMoney(tax.taxAmount)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showEquivalenceSurcharge && tax.equivalenceSurchargePercentage !== null ? (
|
{showEquivalenceSurcharge && tax.recPercentage !== null ? (
|
||||||
<TotalsRow
|
<TotalsRow
|
||||||
description={formatMoney(tax.taxableBase)}
|
description={formatMoney(tax.taxableBase)}
|
||||||
label={`${t(
|
label={`${t(
|
||||||
"proformas.update.totals.equivalenceSurcharge",
|
"proformas.update.totals.equivalenceSurcharge",
|
||||||
"Recargo equivalencia"
|
"Recargo equivalencia"
|
||||||
)} ${formatPercent(tax.equivalenceSurchargePercentage)}`}
|
)} ${formatPercent(tax.recPercentage)}`}
|
||||||
value={formatMoney(tax.equivalenceSurchargeAmount)}
|
value={formatMoney(tax.equivalenceSurchargeAmount)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
className="md:col-span-6"
|
className="md:col-span-6"
|
||||||
currency={currencyCode}
|
currency={currencyCode}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
showEquivalenceSurcharge={taxCtrl.hasEquivalenceSurcharge}
|
showEquivalenceSurcharge={taxCtrl.hasRecPercentage}
|
||||||
showRetention={taxCtrl.hasRetention}
|
showRetention={taxCtrl.hasRetention}
|
||||||
totals={totalsCtrl.totals}
|
totals={totalsCtrl.totals}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import {
|
|||||||
SelectField,
|
SelectField,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
@ -14,6 +16,7 @@ import {
|
|||||||
FieldSet,
|
FieldSet,
|
||||||
Label,
|
Label,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { ReceiptTextIcon } from "lucide-react";
|
import { ReceiptTextIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
@ -47,29 +50,34 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
icon={<ReceiptTextIcon className="size-5" />}
|
icon={<ReceiptTextIcon className="size-5" />}
|
||||||
title={t("form_groups.proformas.taxes.title", "Impuestos")}
|
title={t("form_groups.proformas.taxes.title", "Impuestos")}
|
||||||
>
|
>
|
||||||
|
<FieldSet>
|
||||||
|
<FieldLegend className="text-primary">1. Régimen fiscal</FieldLegend>
|
||||||
|
<FieldDescription>
|
||||||
|
Selecciona el régimen fiscal que aplica a esta proforma.
|
||||||
|
</FieldDescription>
|
||||||
<FormSectionGrid>
|
<FormSectionGrid>
|
||||||
<Field className="md:col-span-12 md:col-start-1" orientation="horizontal">
|
<Field className="md:col-span-12 md:col-start-1" orientation="horizontal">
|
||||||
<SelectField
|
<SelectField
|
||||||
className="md:col-span-4 md:col-start-1"
|
className="md:col-span-4 md:col-start-1"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
items={[
|
items={[
|
||||||
{ value: "1", label: "01: Operación de régimen general." },
|
{ value: "01", label: "01: Operación de régimen general." },
|
||||||
{ value: "2", label: "02: Exportación." },
|
{ value: "02", label: "02: Exportación." },
|
||||||
{
|
{
|
||||||
value: "3",
|
value: "03",
|
||||||
label:
|
label:
|
||||||
"03: Operaciones a las que se aplique el régimen especial de bienes usados, objetos de arte, antigüedades y objetos de colección.",
|
"03: Operaciones a las que se aplique el régimen especial de bienes usados, objetos de arte, antigüedades y objetos de colección.",
|
||||||
},
|
},
|
||||||
{ value: "4", label: "04: Régimen especial del oro de inversión." },
|
{ value: "04", label: "04: Régimen especial del oro de inversión." },
|
||||||
{ value: "5", label: "05: Régimen especial de las agencias de viajes." },
|
{ value: "05", label: "05: Régimen especial de las agencias de viajes." },
|
||||||
{
|
{
|
||||||
value: "6",
|
value: "06",
|
||||||
label: "06: Régimen especial grupo de entidades en IVA o IGIC (Nivel Avanzado)",
|
label: "06: Régimen especial grupo de entidades en IVA o IGIC (Nivel Avanzado)",
|
||||||
},
|
},
|
||||||
{ value: "7", label: "07: Régimen especial del criterio de caja." },
|
{ value: "07", label: "07: Régimen especial del criterio de caja." },
|
||||||
{ value: "8", label: "08: Operaciones sujetas al IPSI/IVA o IGIC." },
|
{ value: "08", label: "08: Operaciones sujetas al IPSI/IVA o IGIC." },
|
||||||
{
|
{
|
||||||
value: "9",
|
value: "09",
|
||||||
label:
|
label:
|
||||||
"09: Facturación de las prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena (D.A.4ª RD1619/2012)",
|
"09: Facturación de las prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena (D.A.4ª RD1619/2012)",
|
||||||
},
|
},
|
||||||
@ -106,21 +114,29 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
},
|
},
|
||||||
{ value: "20", label: "20: Régimen simplificado" },
|
{ value: "20", label: "20: Régimen simplificado" },
|
||||||
]}
|
]}
|
||||||
label={t("form_fields.proformas.tax_regime_code.label", "Régimen de IVA")}
|
label={t("form_fields.proformas.tax_regime_code.label", "Régimen fiscal")}
|
||||||
name="taxRegimeCode"
|
name="taxRegimeCode"
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
"form_fields.proformas.tax_regime_code.placeholder",
|
"form_fields.proformas.tax_regime_code.placeholder",
|
||||||
"Selecciona el régimen de IVA de esta proforma"
|
"Selecciona el régimen fiscal para esta proforma"
|
||||||
)}
|
)}
|
||||||
readOnly={readOnly || taxCtrl.usesPerLineTax}
|
readOnly={readOnly || taxCtrl.usesPerLineTax}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</FormSectionGrid>
|
</FormSectionGrid>
|
||||||
|
</FieldSet>
|
||||||
<FieldSeparator className="my-4" />
|
<FieldSeparator className="my-4" />
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={cn(disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary")}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
<FieldSet>
|
<FieldSet>
|
||||||
<FieldLegend>Billing Address</FieldLegend>
|
<FieldLegend className="font-semibold">Configuración de IVA</FieldLegend>
|
||||||
<FieldDescription>The billing address associated with your payment method</FieldDescription>
|
<FieldDescription>
|
||||||
|
Puedes usar un tipo único para todos las líneas de detalle o permitir que cada línea
|
||||||
|
tenga su propio IVA.
|
||||||
|
</FieldDescription>
|
||||||
|
|
||||||
<FormSectionGrid>
|
<FormSectionGrid>
|
||||||
<Field className="md:col-span-12 md:col-start-1" orientation="horizontal">
|
<Field className="md:col-span-12 md:col-start-1" orientation="horizontal">
|
||||||
@ -134,13 +150,14 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
<Label htmlFor="terms-checkbox">
|
<Label htmlFor="terms-checkbox">
|
||||||
{t(
|
{t(
|
||||||
"proformas.update.taxes.disable_per_line",
|
"proformas.update.taxes.disable_per_line",
|
||||||
"Usar el mismo impuesto en toda la proforma"
|
"Aplicar el mismo IVA a todas las líneas de la proforma"
|
||||||
)}
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
className="md:col-span-4 md:col-start-1"
|
className="md:col-span-4 md:col-start-1"
|
||||||
|
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
items={[
|
items={[
|
||||||
{ value: "0", label: "0%" },
|
{ value: "0", label: "0%" },
|
||||||
@ -155,8 +172,22 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
"Selecciona IVA"
|
"Selecciona IVA"
|
||||||
)}
|
)}
|
||||||
readOnly={readOnly || taxCtrl.usesPerLineTax}
|
readOnly={readOnly || taxCtrl.usesPerLineTax}
|
||||||
|
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
||||||
/>
|
/>
|
||||||
|
</FormSectionGrid>
|
||||||
|
</FieldSet>{" "}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<FieldSeparator className="my-4" />
|
||||||
|
<FieldSet>
|
||||||
|
<FieldLegend>Impuestos adicionales</FieldLegend>
|
||||||
|
<FieldDescription>
|
||||||
|
Activa opciones fiscales adicionales como recargo de equivalencia o retenciones (IRPF),
|
||||||
|
según el tipo de cliente y normativa aplicable.
|
||||||
|
</FieldDescription>
|
||||||
|
|
||||||
|
<FormSectionGrid>
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
className="md:col-span-12 md:col-start-1"
|
className="md:col-span-12 md:col-start-1"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -164,7 +195,7 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
"form_fields.proformas.has_equivalence_surcharge.label",
|
"form_fields.proformas.has_equivalence_surcharge.label",
|
||||||
"Recargo de equivalencia"
|
"Recargo de equivalencia"
|
||||||
)}
|
)}
|
||||||
name="hasEquivalenceSurcharge"
|
name="hasRecPercentage"
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { ErrorAlert, NotFoundCard, PageHeader } from "@erp/core/components";
|
|||||||
import { FormCommitButtonGroup, UnsavedChangesProvider } from "@erp/core/hooks";
|
import { FormCommitButtonGroup, UnsavedChangesProvider } from "@erp/core/hooks";
|
||||||
import { SelectCustomerDialog } from "@erp/customers";
|
import { SelectCustomerDialog } from "@erp/customers";
|
||||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
@ -19,12 +19,6 @@ export const ProformaUpdatePage = () => {
|
|||||||
|
|
||||||
const { updateCtrl, selectCustomerCtrl } = useUpdateProformaPageController();
|
const { updateCtrl, selectCustomerCtrl } = useUpdateProformaPageController();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("[ProformaUpdatePage] isDirty:", updateCtrl.form.formState.isDirty);
|
|
||||||
console.log("[ProformaUpdatePage] dirtyFields:", updateCtrl.form.formState.dirtyFields);
|
|
||||||
console.log("[ProformaUpdatePage] values:", updateCtrl.form.getValues());
|
|
||||||
}, [updateCtrl.form.formState.isDirty, updateCtrl.form.formState.dirtyFields, updateCtrl.form]);
|
|
||||||
|
|
||||||
if (updateCtrl.isLoading) {
|
if (updateCtrl.isLoading) {
|
||||||
return <ProformaUpdateSkeleton />;
|
return <ProformaUpdateSkeleton />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,5 +13,8 @@ export const buildProformaItemUpdateDefault = (position: number): ProformaItemUp
|
|||||||
quantity: null,
|
quantity: null,
|
||||||
unitAmount: null,
|
unitAmount: null,
|
||||||
itemDiscountPercentage: null,
|
itemDiscountPercentage: null,
|
||||||
|
|
||||||
|
taxPercentage: null,
|
||||||
|
recPercentage: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,10 +16,19 @@ export const buildProformaUpdateDefault = (): ProformaUpdateForm => {
|
|||||||
languageCode: "es",
|
languageCode: "es",
|
||||||
currencyCode: "EUR",
|
currencyCode: "EUR",
|
||||||
|
|
||||||
paymentMethod: "",
|
|
||||||
|
|
||||||
globalDiscountPercentage: 0,
|
globalDiscountPercentage: 0,
|
||||||
|
|
||||||
|
taxMode: "single",
|
||||||
|
taxRegimeCode: "01",
|
||||||
|
defaultTaxPercentage: 21,
|
||||||
|
|
||||||
|
hasRecPercentage: false,
|
||||||
|
hasRetention: false,
|
||||||
|
|
||||||
|
retentionPercentage: null,
|
||||||
|
|
||||||
|
paymentMethod: "",
|
||||||
|
|
||||||
items: [],
|
items: [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -140,8 +140,6 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Mapped CustomerPatchProps:", customerPatchProps);
|
|
||||||
|
|
||||||
return Result.ok(customerPatchProps);
|
return Result.ok(customerPatchProps);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return Result.fail(new DomainError("Customer props mapping failed (update)", { cause: err }));
|
return Result.fail(new DomainError("Customer props mapping failed (update)", { cause: err }));
|
||||||
|
|||||||
@ -242,11 +242,12 @@ export default (database: Sequelize) => {
|
|||||||
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
|
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
|
||||||
{ name: "idx_factuges", fields: ["factuges_id"], unique: true }, // <- para el proceso python
|
{ name: "idx_factuges", fields: ["factuges_id"], unique: true }, // <- para el proceso python
|
||||||
|
|
||||||
{
|
// Para búsquedas simples => se hace con el "CriteriaToSequelizeConverter"
|
||||||
|
/*{
|
||||||
name: "ft_customer",
|
name: "ft_customer",
|
||||||
type: "FULLTEXT",
|
type: "FULLTEXT",
|
||||||
fields: ["name", "trade_name", "reference", "tin", "email_primary", "mobile_primary"],
|
fields: ["name", "trade_name", "reference", "tin", "email_primary", "mobile_primary"],
|
||||||
},
|
},*/
|
||||||
],
|
],
|
||||||
|
|
||||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|||||||
@ -48,7 +48,6 @@ export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicIn
|
|||||||
control={control}
|
control={control}
|
||||||
name="isCompany"
|
name="isCompany"
|
||||||
render={({ field, fieldState }) => {
|
render={({ field, fieldState }) => {
|
||||||
console.log(field.value);
|
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
className="gap-1 lg:col-span-1 lg:col-start-1"
|
className="gap-1 lg:col-span-1 lg:col-start-1"
|
||||||
@ -61,7 +60,6 @@ export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicIn
|
|||||||
disabled={field.disabled}
|
disabled={field.disabled}
|
||||||
name={field.name}
|
name={field.name}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
console.log("Pongo ", value);
|
|
||||||
field.onChange(value === "true");
|
field.onChange(value === "true");
|
||||||
}}
|
}}
|
||||||
required
|
required
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { ErrorAlert, PageHeader } from "@erp/core/components";
|
|||||||
import { FormCommitButtonGroup, UnsavedChangesProvider } from "@erp/core/hooks";
|
import { FormCommitButtonGroup, UnsavedChangesProvider } from "@erp/core/hooks";
|
||||||
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
|
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { useCustomerCreatePageController } from "../../controllers";
|
import { useCustomerCreatePageController } from "../../controllers";
|
||||||
@ -9,6 +10,7 @@ import { CustomerCreateEditorForm } from "../editor";
|
|||||||
|
|
||||||
export const CustomerCreatePage = () => {
|
export const CustomerCreatePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { createCtrl } = useCustomerCreatePageController();
|
const { createCtrl } = useCustomerCreatePageController();
|
||||||
|
|
||||||
const { form, formId, onSubmit, resetForm, isCreating, isCreateError, createError } = createCtrl;
|
const { form, formId, onSubmit, resetForm, isCreating, isCreateError, createError } = createCtrl;
|
||||||
@ -17,15 +19,19 @@ export const CustomerCreatePage = () => {
|
|||||||
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
|
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
|
||||||
<AppHeader>
|
<AppHeader>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
backIcon
|
|
||||||
description={t("pages.create.description")}
|
description={t("pages.create.description")}
|
||||||
|
onBackClick={() => navigate("/customers/list")}
|
||||||
rightSlot={
|
rightSlot={
|
||||||
<FormCommitButtonGroup
|
<FormCommitButtonGroup
|
||||||
cancel={{ formId, to: "/customers/list", disabled: isCreating }}
|
cancel={{
|
||||||
disabled={isCreating}
|
to: "/customers/list",
|
||||||
isLoading={isCreating}
|
}}
|
||||||
onReset={resetForm}
|
disabled={createCtrl.isCreating}
|
||||||
submit={{ formId, disabled: isCreating }}
|
isLoading={createCtrl.isCreating}
|
||||||
|
onReset={createCtrl.form.formState.isDirty ? createCtrl.resetForm : undefined}
|
||||||
|
submit={{
|
||||||
|
formId: createCtrl.formId,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
title={t("pages.create.title")}
|
title={t("pages.create.title")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { DateHelper } from "@erp/core";
|
import { DateHelper } from "@repo/rdx-utils";
|
||||||
import { Badge, Button } from "@repo/shadcn-ui/components";
|
import { Badge, Button } from "@repo/shadcn-ui/components";
|
||||||
import { FileTextIcon } from "lucide-react";
|
import { FileTextIcon } from "lucide-react";
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { Customer } from "../../shared";
|
import type { Customer } from "../../shared";
|
||||||
import { type CustomerUpdateForm, defaultCustomerUpdateForm } from "../entities"; /**
|
import type { CustomerUpdateForm } from "../entities"; /**
|
||||||
* Mapea un cliente a un formulario de actualización de cliente.
|
* Mapea un cliente a un formulario de actualización de cliente.
|
||||||
*
|
*
|
||||||
* Reglas:
|
* Reglas:
|
||||||
@ -11,10 +11,11 @@ import { type CustomerUpdateForm, defaultCustomerUpdateForm } from "../entities"
|
|||||||
* @param customer
|
* @param customer
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
import { buildCustomerUpdateDefault } from "../utils";
|
||||||
|
|
||||||
export const mapCustomerToCustomerUpdateForm = (customer: Customer): CustomerUpdateForm => ({
|
export const mapCustomerToCustomerUpdateForm = (customer: Customer): CustomerUpdateForm => ({
|
||||||
reference: customer.reference ?? "",
|
reference: customer.reference ?? "",
|
||||||
isCompany: customer.isCompany ?? defaultCustomerUpdateForm.isCompany,
|
isCompany: customer.isCompany ?? buildCustomerUpdateDefault().isCompany,
|
||||||
name: customer.name,
|
name: customer.name,
|
||||||
tradeName: customer.tradeName ?? "",
|
tradeName: customer.tradeName ?? "",
|
||||||
tin: customer.tin ?? "",
|
tin: customer.tin ?? "",
|
||||||
@ -26,7 +27,7 @@ export const mapCustomerToCustomerUpdateForm = (customer: Customer): CustomerUpd
|
|||||||
city: customer.address.city ?? "",
|
city: customer.address.city ?? "",
|
||||||
province: customer.address.province ?? "",
|
province: customer.address.province ?? "",
|
||||||
postalCode: customer.address.postalCode ?? "",
|
postalCode: customer.address.postalCode ?? "",
|
||||||
country: customer.address.country ?? defaultCustomerUpdateForm.country,
|
country: customer.address.country ?? buildCustomerUpdateDefault().country,
|
||||||
|
|
||||||
primaryEmail: customer.contact.primaryEmail ?? "",
|
primaryEmail: customer.contact.primaryEmail ?? "",
|
||||||
secondaryEmail: customer.contact.secondaryEmail ?? "",
|
secondaryEmail: customer.contact.secondaryEmail ?? "",
|
||||||
@ -40,6 +41,6 @@ export const mapCustomerToCustomerUpdateForm = (customer: Customer): CustomerUpd
|
|||||||
|
|
||||||
legalRecord: customer.legalRecord ?? "",
|
legalRecord: customer.legalRecord ?? "",
|
||||||
|
|
||||||
languageCode: customer.languageCode ?? defaultCustomerUpdateForm.languageCode,
|
languageCode: customer.languageCode ?? buildCustomerUpdateDefault().languageCode,
|
||||||
currencyCode: customer.currencyCode ?? defaultCustomerUpdateForm.currencyCode,
|
currencyCode: customer.currencyCode ?? buildCustomerUpdateDefault().currencyCode,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -196,7 +196,6 @@ export const useCustomerUpdateController = (
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
(errors: FieldErrors<CustomerUpdateForm>) => {
|
(errors: FieldErrors<CustomerUpdateForm>) => {
|
||||||
console.log(errors);
|
|
||||||
focusFirstInputFormError(form);
|
focusFirstInputFormError(form);
|
||||||
|
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { FormCommitButtonGroup, UnsavedChangesProvider } from "@erp/core/hooks";
|
|||||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import { Spinner } from "@repo/shadcn-ui/components";
|
import { Spinner } from "@repo/shadcn-ui/components";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { useCustomerUpdatePageController } from "../../controllers";
|
import { useCustomerUpdatePageController } from "../../controllers";
|
||||||
@ -11,7 +12,7 @@ import { CustomerUpdateEditorForm } from "../editor";
|
|||||||
|
|
||||||
export const CustomerUpdatePage = () => {
|
export const CustomerUpdatePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { updateCtrl } = useCustomerUpdatePageController();
|
const { updateCtrl } = useCustomerUpdatePageController();
|
||||||
|
|
||||||
if (updateCtrl.isLoading) {
|
if (updateCtrl.isLoading) {
|
||||||
@ -48,24 +49,22 @@ export const CustomerUpdatePage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<FormProvider {...updateCtrl.form}>
|
||||||
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
||||||
<AppHeader className="space-y-4 max-w-5xl mx-auto">
|
<AppHeader className="space-y-4 max-w-5xl mx-auto">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
backIcon
|
|
||||||
description={t("pages.update.description")}
|
description={t("pages.update.description")}
|
||||||
|
onBackClick={() => navigate("/customers/list")}
|
||||||
rightSlot={
|
rightSlot={
|
||||||
<FormCommitButtonGroup
|
<FormCommitButtonGroup
|
||||||
cancel={{
|
cancel={{
|
||||||
formId: updateCtrl.formId,
|
|
||||||
to: "/customers/list",
|
to: "/customers/list",
|
||||||
disabled: updateCtrl.isUpdating,
|
|
||||||
}}
|
}}
|
||||||
disabled={updateCtrl.isUpdating}
|
disabled={updateCtrl.isUpdating}
|
||||||
isLoading={updateCtrl.isUpdating}
|
isLoading={updateCtrl.isUpdating}
|
||||||
onReset={updateCtrl.resetForm}
|
onReset={updateCtrl.form.formState.isDirty ? updateCtrl.resetForm : undefined}
|
||||||
submit={{
|
submit={{
|
||||||
formId: updateCtrl.formId,
|
formId: updateCtrl.formId,
|
||||||
disabled: updateCtrl.isUpdating,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -87,16 +86,15 @@ export const CustomerUpdatePage = () => {
|
|||||||
{updateCtrl.isLoading && <Spinner />}
|
{updateCtrl.isLoading && <Spinner />}
|
||||||
|
|
||||||
{!updateCtrl.isLoading && (
|
{!updateCtrl.isLoading && (
|
||||||
<FormProvider {...updateCtrl.form}>
|
|
||||||
<CustomerUpdateEditorForm
|
<CustomerUpdateEditorForm
|
||||||
formId={updateCtrl.formId}
|
formId={updateCtrl.formId}
|
||||||
isSubmitting={updateCtrl.isUpdating}
|
isSubmitting={updateCtrl.isUpdating}
|
||||||
onReset={updateCtrl.resetForm}
|
onReset={updateCtrl.resetForm}
|
||||||
onSubmit={updateCtrl.onSubmit}
|
onSubmit={updateCtrl.onSubmit}
|
||||||
/>
|
/>
|
||||||
</FormProvider>
|
|
||||||
)}
|
)}
|
||||||
</AppContent>
|
</AppContent>
|
||||||
</UnsavedChangesProvider>
|
</UnsavedChangesProvider>
|
||||||
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -300,11 +300,13 @@ export default (database: Sequelize) => {
|
|||||||
name: "idx_supplier_invoice_document_id",
|
name: "idx_supplier_invoice_document_id",
|
||||||
fields: ["document_id"],
|
fields: ["document_id"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
|
// Para búsquedas simples => se hace con el "CriteriaToSequelizeConverter"
|
||||||
|
/*{
|
||||||
name: "ft_supplier_invoice",
|
name: "ft_supplier_invoice",
|
||||||
type: "FULLTEXT",
|
type: "FULLTEXT",
|
||||||
fields: ["invoice_number", "description", "notes"],
|
fields: ["invoice_number", "description", "notes"],
|
||||||
},
|
},*/
|
||||||
],
|
],
|
||||||
|
|
||||||
whereMergeStrategy: "and",
|
whereMergeStrategy: "and",
|
||||||
|
|||||||
@ -311,7 +311,11 @@ export class SupplierInvoiceRepository
|
|||||||
|
|
||||||
const query = criteriaConverter.convert(criteria, {
|
const query = criteriaConverter.convert(criteria, {
|
||||||
searchableFields: ["invoice_number", "description", "internal_notes"],
|
searchableFields: ["invoice_number", "description", "internal_notes"],
|
||||||
mappings: {},
|
mappings: {
|
||||||
|
invoice_number: "SupplierInvoiceModel.invoice_number",
|
||||||
|
reference: "SupplierInvoiceModel.description",
|
||||||
|
description: "SupplierInvoiceModel.internal_notes",
|
||||||
|
},
|
||||||
allowedFields: ["invoice_date", "due_date", "id", "created_at"],
|
allowedFields: ["invoice_date", "due_date", "id", "created_at"],
|
||||||
enableFullText: true,
|
enableFullText: true,
|
||||||
database: this.database,
|
database: this.database,
|
||||||
|
|||||||
@ -218,11 +218,12 @@ export default (database: Sequelize) => {
|
|||||||
{ name: "idx_tin", fields: ["tin"] }, // <- para servicios externos
|
{ name: "idx_tin", fields: ["tin"] }, // <- para servicios externos
|
||||||
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
|
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
|
||||||
|
|
||||||
{
|
// Para búsquedas simples => se hace con el "CriteriaToSequelizeConverter"
|
||||||
|
/*{
|
||||||
name: "ft_supplier",
|
name: "ft_supplier",
|
||||||
type: "FULLTEXT",
|
type: "FULLTEXT",
|
||||||
fields: ["name", "trade_name", "reference", "tin", "email_primary", "mobile_primary"],
|
fields: ["name", "trade_name", "reference", "tin", "email_primary", "mobile_primary"],
|
||||||
},
|
},*/
|
||||||
],
|
],
|
||||||
|
|
||||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|||||||
@ -61,12 +61,15 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
|||||||
searchableFields = [],
|
searchableFields = [],
|
||||||
database,
|
database,
|
||||||
enableFullText = false,
|
enableFullText = false,
|
||||||
} = params as ConvertParams & { database?: Sequelize };
|
fullTextTableAlias,
|
||||||
|
} = params as ConvertParams & {
|
||||||
|
database?: Sequelize;
|
||||||
|
fullTextTableAlias?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const term = typeof criteria.quickSearch === "string" ? criteria.quickSearch.trim() : "";
|
const term = typeof criteria.quickSearch === "string" ? criteria.quickSearch.trim() : "";
|
||||||
if (!term || searchableFields.length === 0 || !enableFullText) return;
|
if (!term || searchableFields.length === 0 || !enableFullText) return;
|
||||||
|
|
||||||
// Validación defensiva
|
|
||||||
if (!database) {
|
if (!database) {
|
||||||
const msg = `[CriteriaToSequelizeConverter] enableFullText=true pero falta 'database' en params.`;
|
const msg = `[CriteriaToSequelizeConverter] enableFullText=true pero falta 'database' en params.`;
|
||||||
if (params.strictMode) throw new Error(msg);
|
if (params.strictMode) throw new Error(msg);
|
||||||
@ -80,13 +83,19 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
|||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
const mappedFields = searchableFields.map((f) => mappings[f] || f);
|
const mappedFields = searchableFields.map((f) => mappings[f] || f);
|
||||||
const matchExpr = `MATCH(${mappedFields.join(", ")}) AGAINST (${database.escape(
|
|
||||||
|
const qualifiedFields = mappedFields.map((field) =>
|
||||||
|
this.qualifyField(field, fullTextTableAlias)
|
||||||
|
);
|
||||||
|
|
||||||
|
const matchExpr = `MATCH(${qualifiedFields.join(", ")}) AGAINST (${database.escape(
|
||||||
booleanTerm
|
booleanTerm
|
||||||
)} IN BOOLEAN MODE)`;
|
)} IN BOOLEAN MODE)`;
|
||||||
|
|
||||||
const matchLiteral = Sequelize.literal(matchExpr);
|
const matchLiteral = Sequelize.literal(matchExpr);
|
||||||
|
|
||||||
// Añadir campo virtual "score"
|
|
||||||
if (!options.attributes) options.attributes = { include: [] };
|
if (!options.attributes) options.attributes = { include: [] };
|
||||||
|
|
||||||
if (Array.isArray(options.attributes)) {
|
if (Array.isArray(options.attributes)) {
|
||||||
options.attributes.push([matchLiteral, "score"]);
|
options.attributes.push([matchLiteral, "score"]);
|
||||||
} else {
|
} else {
|
||||||
@ -94,13 +103,12 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
|||||||
options.attributes.include.push([matchLiteral, "score"]);
|
options.attributes.include.push([matchLiteral, "score"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WHERE score > 0
|
|
||||||
const scoreCondition = Sequelize.where(matchLiteral, { [Op.gt]: 0 });
|
const scoreCondition = Sequelize.where(matchLiteral, { [Op.gt]: 0 });
|
||||||
|
|
||||||
options.where = options.where
|
options.where = options.where
|
||||||
? { [Op.and]: [options.where, scoreCondition] }
|
? { [Op.and]: [options.where, scoreCondition] }
|
||||||
: { [Op.and]: [scoreCondition] };
|
: { [Op.and]: [scoreCondition] };
|
||||||
|
|
||||||
// Ordenar por relevancia
|
|
||||||
prependOrder(options, [Sequelize.literal("score"), "DESC"]);
|
prependOrder(options, [Sequelize.literal("score"), "DESC"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,4 +175,11 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
|||||||
if (operator === Op.like || operator === Op.notLike) return `%${value}%`;
|
if (operator === Op.like || operator === Op.notLike) return `%${value}%`;
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private qualifyField(field: string, tableAlias?: string): string {
|
||||||
|
if (field.includes(".") || field.includes("`")) return field;
|
||||||
|
if (!tableAlias) return field;
|
||||||
|
|
||||||
|
return `\`${tableAlias}\`.\`${field}\``;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export const FormSectionCard = ({
|
|||||||
{icon ? (
|
{icon ? (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-10 shrink-0 items-center justify-center rounded-md",
|
"flex size-12 shrink-0 items-center justify-center rounded-md",
|
||||||
disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary"
|
disabled ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -50,8 +50,10 @@ export const FormSectionCard = ({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{title ? <FieldLegend>{title}</FieldLegend> : null}
|
{title ? <FieldLegend className="font-semibold">{title}</FieldLegend> : null}
|
||||||
{description ? <FieldDescription>{description}</FieldDescription> : null}
|
{description ? (
|
||||||
|
<FieldDescription className="font-medium">{description}</FieldDescription>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@ -19,6 +18,8 @@ interface SelectFieldItem {
|
|||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelectFieldValue = string | null;
|
||||||
|
|
||||||
type SelectFieldProps<TFormValues extends FieldValues> = {
|
type SelectFieldProps<TFormValues extends FieldValues> = {
|
||||||
name: FieldPath<TFormValues>;
|
name: FieldPath<TFormValues>;
|
||||||
|
|
||||||
@ -35,6 +36,9 @@ type SelectFieldProps<TFormValues extends FieldValues> = {
|
|||||||
|
|
||||||
orientation?: "vertical" | "horizontal" | "responsive";
|
orientation?: "vertical" | "horizontal" | "responsive";
|
||||||
|
|
||||||
|
serialize?: (value: unknown) => string;
|
||||||
|
deserialize?: (value: SelectFieldValue) => unknown;
|
||||||
|
|
||||||
className?: string;
|
className?: string;
|
||||||
inputClassName?: string;
|
inputClassName?: string;
|
||||||
};
|
};
|
||||||
@ -55,6 +59,9 @@ export function SelectField<TFormValues extends FieldValues>({
|
|||||||
|
|
||||||
orientation = "vertical",
|
orientation = "vertical",
|
||||||
|
|
||||||
|
serialize,
|
||||||
|
deserialize,
|
||||||
|
|
||||||
className,
|
className,
|
||||||
inputClassName,
|
inputClassName,
|
||||||
}: SelectFieldProps<TFormValues>) {
|
}: SelectFieldProps<TFormValues>) {
|
||||||
@ -62,18 +69,25 @@ export function SelectField<TFormValues extends FieldValues>({
|
|||||||
const { control, formState } = useFormContext<TFormValues>();
|
const { control, formState } = useFormContext<TFormValues>();
|
||||||
const isDisabled = Boolean(disabled || readOnly || formState.isSubmitting);
|
const isDisabled = Boolean(disabled || readOnly || formState.isSubmitting);
|
||||||
|
|
||||||
|
const serializeValue =
|
||||||
|
serialize ?? ((value: unknown): string => (typeof value === "string" ? value : ""));
|
||||||
|
|
||||||
|
const deserializeValue = deserialize ?? ((value: SelectFieldValue): string => value ?? "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field, fieldState }) => {
|
render={({ field, fieldState }) => {
|
||||||
const fieldValue = typeof field.value === "string" ? field.value.trim() : "";
|
const fieldValue = serializeValue(field.value);
|
||||||
|
|
||||||
const normalizedItems =
|
const normalizedItems =
|
||||||
fieldValue && !items.some((item) => item.value === fieldValue)
|
fieldValue && !items.some((item) => item.value === fieldValue)
|
||||||
? [{ value: fieldValue, label: fieldValue }, ...items]
|
? [{ value: fieldValue, label: fieldValue }, ...items]
|
||||||
: items;
|
: items;
|
||||||
|
|
||||||
|
const selectedItem = normalizedItems.find((item) => item.value === fieldValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
className={cn("gap-1", className)}
|
className={cn("gap-1", className)}
|
||||||
@ -88,8 +102,8 @@ export function SelectField<TFormValues extends FieldValues>({
|
|||||||
|
|
||||||
<Select
|
<Select
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onValueChange={field.onChange}
|
onValueChange={(value) => field.onChange(deserializeValue(value))}
|
||||||
value={field.value ?? undefined}
|
value={fieldValue}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
aria-invalid={fieldState.invalid}
|
aria-invalid={fieldState.invalid}
|
||||||
@ -103,11 +117,16 @@ export function SelectField<TFormValues extends FieldValues>({
|
|||||||
)}
|
)}
|
||||||
id={triggerId}
|
id={triggerId}
|
||||||
>
|
>
|
||||||
<SelectValue
|
<span
|
||||||
className={"placeholder:font-normal placeholder:italic"}
|
className={cn(
|
||||||
placeholder={placeholder}
|
"truncate text-left",
|
||||||
/>
|
selectedItem ? "" : "text-muted-foreground font-normal italic"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedItem?.label ?? placeholder}
|
||||||
|
</span>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{normalizedItems.map((item) => (
|
{normalizedItems.map((item) => (
|
||||||
<SelectItem key={`key-${item.value}`} value={item.value}>
|
<SelectItem key={`key-${item.value}`} value={item.value}>
|
||||||
|
|||||||
@ -5,6 +5,12 @@ const formatPercent = (value: number): string => {
|
|||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
}).format(value / 100);
|
}).format(value / 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasPositivePercentage = (value: number | null | undefined): boolean => {
|
||||||
|
return value !== null && value !== undefined && value > 0;
|
||||||
|
};
|
||||||
|
|
||||||
export const PercentageHelper = {
|
export const PercentageHelper = {
|
||||||
formatPercent,
|
formatPercent,
|
||||||
|
hasPositivePercentage,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user