.
This commit is contained in:
parent
c813081ce1
commit
9f870bbd76
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@ -1,12 +1,10 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"mfeckies.handlebars-formatter",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"biomejs.biome",
|
||||
"cweijan.vscode-mysql-client2",
|
||||
"ms-vscode.vscode-json",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"cweijan.dbclient-jdbc",
|
||||
"nabous.handlebars-preview-plus"
|
||||
"cweijan.dbclient-jdbc"
|
||||
]
|
||||
}
|
||||
|
||||
@ -44,9 +44,8 @@
|
||||
"axios": "^1.9.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"express": "^4.18.2",
|
||||
"handlebars": "^4.7.8",
|
||||
"http-status": "^2.1.0",
|
||||
"lucide-react": "^0.503.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"mime-types": "^3.0.1",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { IRendererTemplateResolver } from "../../../application";
|
||||
|
||||
import { FastReportTemplateNotFoundError } from "./fastreport";
|
||||
|
||||
/**
|
||||
* Resuelve rutas de plantillas para desarrollo y producción.
|
||||
*/
|
||||
export abstract class RendererTemplateResolver implements IRendererTemplateResolver {
|
||||
constructor(protected readonly rootPath: string) {}
|
||||
|
||||
/** Une partes de ruta relativas al rootPath */
|
||||
protected resolveJoin(parts: string[]): string {
|
||||
return join(this.rootPath, ...parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve el directorio donde residen las plantillas de un módulo/empresa
|
||||
* según el entorno (dev/prod).
|
||||
*/
|
||||
protected resolveTemplateDirectory(module: string, companySlug: string): string {
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
if (isDev) {
|
||||
// <root>/<module>/templates/<companySlug>/
|
||||
return this.resolveJoin([module, "templates", companySlug]);
|
||||
}
|
||||
|
||||
// <root>/templates/<module>/<companySlug>/
|
||||
//return this.resolveJoin(["templates", module, companySlug]);
|
||||
return this.resolveJoin([module]);
|
||||
}
|
||||
|
||||
/** Resuelve una ruta de recurso relativa al directorio de plantilla */
|
||||
protected resolveAssetPath(templateDir: string, relative: string): string {
|
||||
return join(templateDir, relative);
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve la ruta absoluta del fichero de plantilla.
|
||||
*/
|
||||
public resolveTemplatePath(module: string, companySlug: string, templateName: string): string {
|
||||
const dir = this.resolveTemplateDirectory(module, companySlug);
|
||||
const filePath = this.resolveAssetPath(dir, templateName);
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
throw new FastReportTemplateNotFoundError(
|
||||
`Template not found: module=${module} company=${companySlug} name=${templateName}`
|
||||
);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/** Lee el contenido de un fichero plantilla */
|
||||
protected readTemplateFile(templatePath: string): string {
|
||||
return readFileSync(templatePath, "utf8");
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,44 @@
|
||||
import { DevTool } from '@hookform/devtools';
|
||||
import { useState } from "react";
|
||||
import { Suspense, lazy, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
const HookFormDevTool = lazy(async () => {
|
||||
const mod = await import("@hookform/devtools");
|
||||
return { default: mod.DevTool };
|
||||
});
|
||||
|
||||
type FormDebugProps = {
|
||||
enabled?: boolean;
|
||||
placement?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
||||
};
|
||||
|
||||
export function FormDebug({ enabled = false, placement = "top-right" }: FormDebugProps) {
|
||||
return enabled ? <FormDebugInner placement={placement} /> : null;
|
||||
}
|
||||
|
||||
function FormDebugInner({
|
||||
placement,
|
||||
}: {
|
||||
placement: "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
||||
}) {
|
||||
const { control } = useFormContext();
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<HookFormDevTool control={control} placement={placement} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// Renderiza una propiedad recursivamente con expansión
|
||||
function DebugField({ label, oldValue, newValue }: { label?: string; oldValue: any; newValue: any }) {
|
||||
function DebugField({
|
||||
label,
|
||||
oldValue,
|
||||
newValue,
|
||||
}: {
|
||||
label?: string;
|
||||
oldValue: any;
|
||||
newValue: any;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const isObject = typeof newValue === "object" && newValue !== null;
|
||||
@ -12,8 +47,8 @@ function DebugField({ label, oldValue, newValue }: { label?: string; oldValue: a
|
||||
return (
|
||||
<li className="ml-4">
|
||||
{label && <span className="font-medium">{label}: </span>}
|
||||
<span className="text-gray-500 line-through">{String(oldValue)}</span>{" "}
|
||||
➝ <span className="text-green-600">{String(newValue)}</span>
|
||||
<span className="text-gray-500 line-through">{String(oldValue)}</span> ➝{" "}
|
||||
<span className="text-green-600">{String(newValue)}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@ -21,58 +56,19 @@ function DebugField({ label, oldValue, newValue }: { label?: string; oldValue: a
|
||||
return (
|
||||
<li className="ml-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="text-left font-medium text-blue-600 hover:underline focus:outline-none"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
type="button"
|
||||
>
|
||||
{open ? "▼" : "▶"} {label}
|
||||
</button>
|
||||
{open && (
|
||||
<ul className="ml-4 border-l pl-2 mt-1 space-y-1">
|
||||
{Object.keys(newValue).map((key) => (
|
||||
<DebugField
|
||||
key={key}
|
||||
label={key}
|
||||
oldValue={oldValue?.[key]}
|
||||
newValue={newValue[key]}
|
||||
/>
|
||||
<DebugField key={key} label={key} newValue={newValue[key]} oldValue={oldValue?.[key]} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export const FormDebug = () => {
|
||||
const { control } = useFormContext();
|
||||
//const { watch, formState } = useFormContext();
|
||||
//const { isDirty, dirtyFields, defaultValues } = formState;
|
||||
//const currentValues = watch();
|
||||
|
||||
return <DevTool control={control} placement="top-right" />
|
||||
|
||||
/*return (
|
||||
<div className="absolute right-4 bottom-4 z-50 p-4 border rounded bg-red-50">
|
||||
<p>
|
||||
<strong>¿Formulario modificado?</strong> {isDirty ? "Sí" : "No"}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<strong>Campos modificados:</strong>
|
||||
{Object.keys(dirtyFields).length > 0 ? (
|
||||
<ul className="list-disc list-inside mt-1">
|
||||
{Object.keys(dirtyFields).map((key) => (
|
||||
<DebugField
|
||||
key={key}
|
||||
label={key}
|
||||
oldValue={defaultValues?.[key]}
|
||||
newValue={currentValues[key]}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>Ninguno</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);*/
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { FieldValues, UseFormProps, UseFormReturn, useForm } from "react-hook-form";
|
||||
import * as z4 from "zod/v4/core";
|
||||
import { type FieldValues, type UseFormProps, type UseFormReturn, useForm } from "react-hook-form";
|
||||
import type * as z4 from "zod/v4/core";
|
||||
|
||||
type UseHookFormProps<TFields extends FieldValues = FieldValues, TContext = any> = UseFormProps<
|
||||
TFields,
|
||||
@ -24,6 +24,9 @@ export function useHookForm<TFields extends FieldValues = FieldValues, TContext
|
||||
resolver: zodResolver(resolverSchema),
|
||||
defaultValues: initialValues,
|
||||
disabled,
|
||||
|
||||
mode: "onBlur",
|
||||
reValidateMode: "onBlur",
|
||||
});
|
||||
|
||||
const {
|
||||
@ -57,7 +60,9 @@ export function useHookForm<TFields extends FieldValues = FieldValues, TContext
|
||||
lastAppliedRef.current = next;
|
||||
}
|
||||
};
|
||||
void apply();
|
||||
|
||||
apply();
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
|
||||
@ -52,9 +52,8 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"express": "^4.18.2",
|
||||
"handlebars": "^4.7.8",
|
||||
"libphonenumber-js": "^1.12.7",
|
||||
"lucide-react": "^0.503.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||
import { ErrorAlert } from "@erp/customers/components";
|
||||
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||
import { AppContent, AppHeader, BackHistoryButton, LogoVerifactu } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Alert,
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
"@tanstack/react-query": "^5.90.6",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"express": "^4.18.2",
|
||||
"lucide-react": "^0.503.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react-data-table-component": "^7.7.0",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-i18next": "^16.2.4",
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import type { ICatalogs } from "@erp/core/api";
|
||||
|
||||
import { CreateCustomerInputMapper, type ICreateCustomerInputMapper } from "../mappers";
|
||||
import {
|
||||
CreateCustomerInputMapper,
|
||||
type ICreateCustomerInputMapper,
|
||||
type IUpdateCustomerInputMapper,
|
||||
UpdateCustomerInputMapper,
|
||||
} from "../mappers";
|
||||
|
||||
export interface ICustomerInputMappers {
|
||||
createInputMapper: ICreateCustomerInputMapper;
|
||||
updateInputMapper: IUpdateCustomerInputMapper;
|
||||
}
|
||||
|
||||
export const buildCustomerInputMappers = (catalogs: ICatalogs): ICustomerInputMappers => {
|
||||
@ -11,9 +17,10 @@ export const buildCustomerInputMappers = (catalogs: ICatalogs): ICustomerInputMa
|
||||
|
||||
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
|
||||
const createInputMapper = new CreateCustomerInputMapper({ taxCatalog });
|
||||
//const updateCustomerInputMapper = new UpdateCustomerInputMapper();
|
||||
const updateInputMapper = new UpdateCustomerInputMapper();
|
||||
|
||||
return {
|
||||
createInputMapper,
|
||||
updateInputMapper,
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import type { ICustomerRepository } from "../repositories";
|
||||
import { CustomerUpdater, type ICustomerUpdater } from "../services";
|
||||
|
||||
export const buildCustomerUpdater = (params: {
|
||||
repository: ICustomerRepository;
|
||||
}): ICustomerUpdater => {
|
||||
const { repository } = params;
|
||||
|
||||
return new CustomerUpdater({
|
||||
repository,
|
||||
});
|
||||
};
|
||||
@ -2,4 +2,5 @@ export * from "./customer-creator.di";
|
||||
export * from "./customer-finder.di";
|
||||
export * from "./customer-input-mappers.di";
|
||||
export * from "./customer-snapshot-builders.di";
|
||||
export * from "./customer-updater.di";
|
||||
export * from "./customer-use-cases.di";
|
||||
|
||||
@ -65,14 +65,7 @@ export class UpdateCustomerUseCase {
|
||||
return Result.fail(updateResult.error);
|
||||
}
|
||||
|
||||
const customerOrError = await this.service.updateCustomerInCompany(
|
||||
companyId,
|
||||
updateResult.data,
|
||||
transaction
|
||||
);
|
||||
const customer = customerOrError.data;
|
||||
const dto = presenter.toOutput(customer);
|
||||
return Result.ok(dto);
|
||||
return Result.ok(this.fullSnapshotBuilder.toOutput(updateResult.data));
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
|
||||
@ -10,8 +10,10 @@ import {
|
||||
buildCustomerFinder,
|
||||
buildCustomerInputMappers,
|
||||
buildCustomerSnapshotBuilders,
|
||||
buildCustomerUpdater,
|
||||
buildGetCustomerByIdUseCase,
|
||||
buildListCustomersUseCase,
|
||||
buildUpdateCustomerUseCase,
|
||||
} from "../../application";
|
||||
|
||||
import { buildCustomerPersistenceMappers } from "./customer-persistence-mappers.di";
|
||||
@ -49,6 +51,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
|
||||
const inputMappers = buildCustomerInputMappers(catalogs);
|
||||
const finder = buildCustomerFinder({ repository });
|
||||
const creator = buildCustomerCreator({ repository });
|
||||
const updater = buildCustomerUpdater({ repository });
|
||||
|
||||
const snapshotBuilders = buildCustomerSnapshotBuilders();
|
||||
//const documentGeneratorPipeline = buildCustomerDocumentService(params);
|
||||
@ -80,7 +83,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
|
||||
|
||||
updateCustomer: () =>
|
||||
buildUpdateCustomerUseCase({
|
||||
creator,
|
||||
updater,
|
||||
dtoMapper: inputMappers.updateInputMapper,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
transactionManager,
|
||||
|
||||
@ -4,6 +4,7 @@ import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-
|
||||
|
||||
import { UpdateCustomerByIdRequestSchema } from "../../../common";
|
||||
import type { Customer } from "../api";
|
||||
import type { CustomerData } from "../types";
|
||||
|
||||
import { toValidationErrors } from "./toValidationErrors";
|
||||
|
||||
@ -13,7 +14,7 @@ type UpdateCustomerContext = {};
|
||||
|
||||
type UpdateCustomerPayload = {
|
||||
id: string;
|
||||
data: Partial<CustomerFormData>;
|
||||
data: Partial<CustomerData>;
|
||||
};
|
||||
|
||||
export const useCustomerUpdateMutation = () => {
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./api";
|
||||
export * from "./hooks";
|
||||
export * from "./types";
|
||||
|
||||
3
modules/customers/src/web/common/types.ts
Normal file
3
modules/customers/src/web/common/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import type { Customer } from "./api";
|
||||
|
||||
export type CustomerData = Customer;
|
||||
@ -1,4 +1,3 @@
|
||||
import { FormDebug } from "@erp/core/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
|
||||
import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields";
|
||||
@ -16,8 +15,7 @@ type CustomerFormProps = {
|
||||
export const CustomerEditForm = ({ formId, onSubmit, className, focusRef }: CustomerFormProps) => {
|
||||
return (
|
||||
<form id={formId} noValidate onSubmit={onSubmit}>
|
||||
<FormDebug />
|
||||
<section className={cn("space-y-6 p-6", className)}>
|
||||
<section className={cn("space-y-12 p-6", className)}>
|
||||
<CustomerBasicInfoFields focusRef={focusRef} />
|
||||
<CustomerAddressFields />
|
||||
<CustomerContactFields />
|
||||
|
||||
@ -45,8 +45,8 @@ export const CustomerTaxesMultiSelect = (props: CustomerTaxesMultiSelect) => {
|
||||
animation={0}
|
||||
autoFilter={true}
|
||||
className={cn(
|
||||
"flex w-full -mt-0.5 px-1 py-0.5 rounded-md border min-h-8 h-auto items-center justify-between bg-background hover:bg-inherit [&_svg]:pointer-events-auto",
|
||||
"hover:border-ring hover:ring-ring/50 hover:ring-[2px]",
|
||||
"flex w-full -mt-0.5 px-1 py-0.5 rounded-md border border-input min-h-8 h-auto items-center justify-between hover:bg-inherit [&_svg]:pointer-events-auto",
|
||||
"hover:border-ring hover:ring-ring/50 hover:ring-[2px] font-medium bg-muted/50",
|
||||
className
|
||||
)}
|
||||
defaultValue={value}
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
export * from "./customer.api.schema";
|
||||
export * from "./customer.form.schema";
|
||||
export * from "./customer-resume.form.schema";
|
||||
|
||||
@ -6,12 +6,8 @@ import { type FieldErrors, FormProvider } from "react-hook-form";
|
||||
|
||||
import { useCustomerGetQuery, useCustomerUpdateMutation } from "../../common";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import {
|
||||
type Customer,
|
||||
type CustomerFormData,
|
||||
CustomerFormSchema,
|
||||
defaultCustomerFormData,
|
||||
} from "../../schemas";
|
||||
import { type Customer, CustomerFormSchema, defaultCustomerFormData } from "../../schemas";
|
||||
import type { CustomerFormData } from "../types";
|
||||
|
||||
export interface UseCustomerUpdateControllerOptions {
|
||||
onUpdated?(updated: Customer): void;
|
||||
|
||||
@ -1,3 +1,89 @@
|
||||
import type { Customer } from "../api";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export type CustomerData = Customer;
|
||||
export const CustomerFormSchema = z.object({
|
||||
reference: z.string().optional(),
|
||||
|
||||
is_company: z.string().default("true"),
|
||||
name: z
|
||||
.string({
|
||||
error: "El nombre es obligatorio",
|
||||
})
|
||||
.min(1, "El nombre no puede estar vacío"),
|
||||
trade_name: z.string().optional(),
|
||||
tin: z.string().optional(),
|
||||
default_taxes: z.array(z.string()).default([]),
|
||||
|
||||
street: z.string().optional(),
|
||||
street2: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
province: z.string().optional(),
|
||||
postal_code: z.string().optional(),
|
||||
country: z
|
||||
.string({
|
||||
error: "El país es obligatorio",
|
||||
})
|
||||
.min(1, "El país no puede estar vacío")
|
||||
.toLowerCase() // asegura minúsculas
|
||||
.default("es"),
|
||||
|
||||
email_primary: z.string().optional(),
|
||||
email_secondary: z.string().optional(),
|
||||
phone_primary: z.string().optional(),
|
||||
phone_secondary: z.string().optional(),
|
||||
mobile_primary: z.string().optional(),
|
||||
mobile_secondary: z.string().optional(),
|
||||
|
||||
fax: z.string().optional(),
|
||||
website: z.string().optional(),
|
||||
|
||||
legal_record: z.string().optional(),
|
||||
|
||||
language_code: z
|
||||
.string({
|
||||
error: "El idioma es obligatorio",
|
||||
})
|
||||
.min(1, "Debe indicar un idioma")
|
||||
.toUpperCase() // asegura mayúsculas
|
||||
.default("es"),
|
||||
|
||||
currency_code: z
|
||||
.string({
|
||||
error: "La moneda es obligatoria",
|
||||
})
|
||||
.min(1, "La moneda no puede estar vacía")
|
||||
.toUpperCase() // asegura mayúsculas
|
||||
.default("EUR"),
|
||||
});
|
||||
|
||||
export type CustomerFormData = z.infer<typeof CustomerFormSchema>;
|
||||
|
||||
export const defaultCustomerFormData: CustomerFormData = {
|
||||
reference: "",
|
||||
|
||||
is_company: "true",
|
||||
name: "",
|
||||
trade_name: "",
|
||||
tin: "",
|
||||
default_taxes: ["iva_21"],
|
||||
|
||||
street: "",
|
||||
street2: "",
|
||||
city: "",
|
||||
province: "",
|
||||
postal_code: "",
|
||||
country: "es",
|
||||
|
||||
email_primary: "",
|
||||
email_secondary: "",
|
||||
phone_primary: "",
|
||||
phone_secondary: "",
|
||||
mobile_primary: "",
|
||||
mobile_secondary: "",
|
||||
|
||||
fax: "",
|
||||
website: "",
|
||||
|
||||
legal_record: "",
|
||||
language_code: "es",
|
||||
currency_code: "EUR",
|
||||
};
|
||||
|
||||
@ -31,6 +31,8 @@ export const CustomerUpdatePage = () => {
|
||||
FormProvider,
|
||||
} = useCustomerUpdateController(initialCustomerId, {});
|
||||
|
||||
console.log("venga!!!");
|
||||
|
||||
if (isLoading) {
|
||||
return <CustomerEditorSkeleton />;
|
||||
}
|
||||
@ -68,7 +70,7 @@ export const CustomerUpdatePage = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
|
||||
<UnsavedChangesProvider isDirty={false}>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
backIcon
|
||||
@ -106,7 +108,7 @@ export const CustomerUpdatePage = () => {
|
||||
|
||||
<FormProvider {...form}>
|
||||
<CustomerEditForm
|
||||
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto mt-6" // para que el botón del header pueda hacer submit
|
||||
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto mt-6 " // para que el botón del header pueda hacer submit
|
||||
formId={formId}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
|
||||
@ -2,9 +2,9 @@ import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth
|
||||
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api";
|
||||
import type { ProformaPublicServices } from "@erp/customer-invoices/api";
|
||||
import type { CustomerPublicServices } from "@erp/customers/api";
|
||||
import { CreateProformaFromFactugesRequestSchema } from "@erp/factuges/common";
|
||||
import { type NextFunction, type Request, type Response, Router } from "express";
|
||||
|
||||
import { CreateProformaFromFactugesRequestSchema } from "../../../common/dto/request/create-proforma-from-factuges.request.dto";
|
||||
import type { FactugesInternalDeps } from "../di/factuges.di";
|
||||
|
||||
import { CreateProformaFromFactugesController } from "./controllers";
|
||||
|
||||
@ -25,10 +25,11 @@
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"baseline-browser-mapping": "^2.10.8",
|
||||
"change-case": "^5.4.4",
|
||||
"inquirer": "^12.10.0",
|
||||
"plop": "^4.0.4",
|
||||
"rimraf": "^5.0.5",
|
||||
"plop": "^4.0.5",
|
||||
"rimraf": "^6.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "5.9.3"
|
||||
|
||||
@ -1,32 +1,43 @@
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { Control, FieldPath, FieldValues, useController, useFormState } from "react-hook-form";
|
||||
import {
|
||||
type Control,
|
||||
Controller,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
useController,
|
||||
useFormState,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../locales/i18n.ts";
|
||||
|
||||
type SelectFieldProps<TFormValues extends FieldValues> = {
|
||||
control: Control<TFormValues>;
|
||||
name: FieldPath<TFormValues>;
|
||||
items: Array<{ value: string; label: string }>;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
|
||||
placeholder?: string;
|
||||
items: Array<{ value: string; label: string }>;
|
||||
|
||||
orientation?: "vertical" | "horizontal" | "responsive";
|
||||
|
||||
className?: string;
|
||||
inputClassName?: string;
|
||||
};
|
||||
|
||||
export function SelectField<TFormValues extends FieldValues>({
|
||||
@ -39,61 +50,61 @@ export function SelectField<TFormValues extends FieldValues>({
|
||||
disabled = false,
|
||||
required = false,
|
||||
readOnly = false,
|
||||
|
||||
orientation = "vertical",
|
||||
|
||||
className,
|
||||
inputClassName,
|
||||
}: SelectFieldProps<TFormValues>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { isSubmitting, isValidating } = useFormState({ control, name });
|
||||
const { isSubmitting } = useFormState({ control, name });
|
||||
const { field, fieldState } = useController({ control, name });
|
||||
|
||||
const isDisabled = disabled || readOnly;
|
||||
|
||||
return (
|
||||
<FormField
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className={cn("space-y-0", className)}>
|
||||
{label && (
|
||||
<div className='mb-1 flex justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<FormLabel htmlFor={name} className='m-0'>
|
||||
{label}
|
||||
</FormLabel>
|
||||
{required && (
|
||||
<span className='text-xs text-destructive'>{t("common.required")}</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Punto “unsaved” */}
|
||||
{fieldState.isDirty && (
|
||||
<span className='text-[10px] text-muted-foreground'>{t("common.modified")}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value} disabled={isDisabled}>
|
||||
<FormControl>
|
||||
<SelectTrigger className='w-full bg-background h-8'>
|
||||
<SelectValue
|
||||
placeholder={placeholder}
|
||||
className=' placeholder:font-normal placeholder:italic '
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{items.map((item) => (
|
||||
<SelectItem key={`key-${item.value}`} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<Field
|
||||
className={cn("gap-1", className)}
|
||||
data-invalid={fieldState.invalid}
|
||||
orientation={orientation}
|
||||
>
|
||||
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
|
||||
|
||||
<FormDescription className={cn("text-xs truncate", !description && "invisible")}>
|
||||
{description || "\u00A0"}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
<Select defaultValue={field.value} disabled={isDisabled} onValueChange={field.onChange}>
|
||||
<FormControl>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"w-full h-8",
|
||||
"font-medium bg-muted/50 hover:bg-inherit hover:border-ring hover:ring-ring/50 hover:ring-[2px]",
|
||||
inputClassName
|
||||
)}
|
||||
>
|
||||
<SelectValue
|
||||
className={"placeholder:font-normal placeholder:italic"}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{items.map((item) => (
|
||||
<SelectItem key={`key-${item.value}`} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<FieldDescription>{description || "\u00A0"}</FieldDescription>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// DatePickerField.tsx
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@ -7,19 +5,26 @@ import {
|
||||
FieldLabel,
|
||||
Textarea,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import {
|
||||
type Control,
|
||||
Controller,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
useFormState,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
import { Control, Controller, FieldPath, FieldValues, useFormState } from "react-hook-form";
|
||||
import { CommonInputProps } from "./types.js";
|
||||
import type { CommonInputProps } from "./types.js";
|
||||
|
||||
type TextAreaFieldProps<TFormValues extends FieldValues> = CommonInputProps & {
|
||||
control: Control<TFormValues>;
|
||||
name: FieldPath<TFormValues>;
|
||||
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
description?: string;
|
||||
|
||||
orientation?: "vertical" | "horizontal" | "responsive",
|
||||
orientation?: "vertical" | "horizontal" | "responsive";
|
||||
|
||||
inputClassName?: string;
|
||||
};
|
||||
@ -32,14 +37,14 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
||||
required = false,
|
||||
readOnly = false,
|
||||
|
||||
orientation = 'vertical',
|
||||
orientation = "vertical",
|
||||
|
||||
className,
|
||||
inputClassName,
|
||||
|
||||
...inputRest
|
||||
}: TextAreaFieldProps<TFormValues>) {
|
||||
const { isSubmitting, isValidating } = useFormState({ control, name });
|
||||
const { isSubmitting } = useFormState({ control, name });
|
||||
const disabled = isSubmitting || inputRest.disabled;
|
||||
|
||||
return (
|
||||
@ -48,25 +53,30 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
||||
name={name}
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<Field data-invalid={fieldState.invalid} orientation={orientation} className={cn("gap-1", className)}>
|
||||
{label && <FieldLabel className='text-xs text-muted-foreground text-nowrap' htmlFor={name}>{label}</FieldLabel>}
|
||||
<Field
|
||||
className={cn("gap-1", className)}
|
||||
data-invalid={fieldState.invalid}
|
||||
orientation={orientation}
|
||||
>
|
||||
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
|
||||
|
||||
<Textarea
|
||||
ref={field.ref}
|
||||
id={name}
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
aria-invalid={fieldState.invalid}
|
||||
aria-busy={isValidating}
|
||||
id={name}
|
||||
onBlur={field.onBlur}
|
||||
onChange={field.onChange}
|
||||
ref={field.ref}
|
||||
value={field.value ?? ""}
|
||||
{...inputRest}
|
||||
disabled={disabled}
|
||||
aria-disabled={disabled}
|
||||
className={cn("bg-background", inputClassName)}
|
||||
className={cn(
|
||||
"font-medium bg-muted/50 hover:bg-inherit hover:border-ring hover:ring-ring/50 hover:ring-[2px]",
|
||||
inputClassName
|
||||
)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
|
||||
{false && <FieldDescription className='text-xs'>{description || "\u00A0"}</FieldDescription>}
|
||||
<FieldDescription>{description || "\u00A0"}</FieldDescription>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
);
|
||||
@ -74,4 +84,3 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,17 +1,14 @@
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
Input
|
||||
} from "@repo/shadcn-ui/components";
|
||||
|
||||
import { Field, FieldDescription, FieldError, FieldLabel, Input } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { Control, Controller, FieldPath, FieldValues, useFormState } from "react-hook-form";
|
||||
import { CommonInputProps } from "./types.js";
|
||||
import {
|
||||
type Control,
|
||||
Controller,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
useFormState,
|
||||
} from "react-hook-form";
|
||||
|
||||
|
||||
type Normalizer = (value: string) => string;
|
||||
import type { CommonInputProps } from "./types.js";
|
||||
|
||||
type TextFieldProps<TFormValues extends FieldValues> = CommonInputProps & {
|
||||
control: Control<TFormValues>;
|
||||
@ -20,7 +17,7 @@ type TextFieldProps<TFormValues extends FieldValues> = CommonInputProps & {
|
||||
label?: string;
|
||||
description?: string;
|
||||
|
||||
orientation?: "vertical" | "horizontal" | "responsive",
|
||||
orientation?: "vertical" | "horizontal" | "responsive";
|
||||
|
||||
inputClassName?: string;
|
||||
};
|
||||
@ -33,14 +30,14 @@ export function TextField<TFormValues extends FieldValues>({
|
||||
required = false,
|
||||
readOnly = false,
|
||||
|
||||
orientation = 'vertical',
|
||||
orientation = "vertical",
|
||||
|
||||
className,
|
||||
inputClassName,
|
||||
|
||||
...inputRest
|
||||
}: TextFieldProps<TFormValues>) {
|
||||
const { isSubmitting, isValidating } = useFormState({ control, name });
|
||||
const { isSubmitting } = useFormState({ control, name });
|
||||
const disabled = isSubmitting || inputRest.disabled;
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||
@ -56,24 +53,30 @@ export function TextField<TFormValues extends FieldValues>({
|
||||
name={name}
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<Field data-invalid={fieldState.invalid} orientation={orientation} className={cn("gap-1", className)}>
|
||||
{label && <FieldLabel className='text-xs text-muted-foreground text-nowrap' htmlFor={name}>{label}</FieldLabel>}
|
||||
<Field
|
||||
className={cn("gap-1", className)}
|
||||
data-invalid={fieldState.invalid}
|
||||
orientation={orientation}
|
||||
>
|
||||
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
|
||||
|
||||
<Input
|
||||
ref={field.ref}
|
||||
id={name}
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-invalid={fieldState.invalid}
|
||||
aria-busy={isValidating}
|
||||
id={name}
|
||||
onBlur={field.onBlur}
|
||||
onChange={field.onChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
ref={field.ref}
|
||||
value={field.value ?? ""}
|
||||
{...inputRest}
|
||||
disabled={disabled}
|
||||
aria-disabled={disabled}
|
||||
className={cn("bg-background", inputClassName)}
|
||||
className={cn(
|
||||
"font-medium bg-muted/50 hover:bg-inherit hover:border-ring hover:ring-ring/50 hover:ring-[2px]",
|
||||
inputClassName
|
||||
)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{false && <FieldDescription className='text-xs'>{description || "\u00A0"}</FieldDescription>}
|
||||
<FieldDescription>{description || "\u00A0"}</FieldDescription>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
);
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils"
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type * as React from "react";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
data-slot="input"
|
||||
type={type}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
|
||||
@ -125,12 +125,12 @@
|
||||
--secondary-foreground: oklch(0.9543 0.0166 250.8425);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--accent: oklch(0.6427 0.1407 253.94);
|
||||
--accent-foreground: oklch(0.97 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--ring: oklch(0.575 0.1533 256.4357); /* oklch(0.708 0 0); */
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
|
||||
1894
pnpm-lock.yaml
1894
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -12,4 +12,5 @@ onlyBuiltDependencies:
|
||||
- '@tailwindcss/oxide'
|
||||
- bcrypt
|
||||
- core-js-pure
|
||||
- msw
|
||||
- sharp
|
||||
|
||||
Loading…
Reference in New Issue
Block a user