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