Compare commits

..

No commits in common. "327756413d19a9e81c1f07aa76a5783eef2db2db" and "8d0c0b88de24d62134e081bcfdc798d9e97b3a5b" have entirely different histories.

54 changed files with 364 additions and 425 deletions

View File

@ -45,7 +45,6 @@
"@erp/core": "workspace:*", "@erp/core": "workspace:*",
"@erp/customer-invoices": "workspace:*", "@erp/customer-invoices": "workspace:*",
"@erp/customers": "workspace:*", "@erp/customers": "workspace:*",
"@erp/verifactu": "workspace:*",
"@repo/rdx-logger": "workspace:*", "@repo/rdx-logger": "workspace:*",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cls-rtracer": "^2.6.3", "cls-rtracer": "^2.6.3",

View File

@ -1,6 +1,6 @@
import customerInvoicesAPIModule from "@erp/customer-invoices/api"; import customerInvoicesAPIModule from "@erp/customer-invoices/api";
import customersAPIModule from "@erp/customers/api"; import customersAPIModule from "@erp/customers/api";
//import verifactuAPIModule from "@erp/verifactu/api"; import verifactuAPIModule from "@erp/verifactu/api";
import { registerModule } from "./lib"; import { registerModule } from "./lib";
@ -8,5 +8,5 @@ export const registerModules = () => {
//registerModule(authAPIModule); //registerModule(authAPIModule);
registerModule(customersAPIModule); registerModule(customersAPIModule);
registerModule(customerInvoicesAPIModule); registerModule(customerInvoicesAPIModule);
//registerModule(verifactuAPIModule); registerModule(verifactuAPIModule);
}; };

View File

@ -2,7 +2,6 @@
"common": { "common": {
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save", "save": "Save",
"saving": "Saving...",
"required": "•" "required": "•"
}, },
"components": { "components": {

View File

@ -47,7 +47,7 @@ export function TaxesMultiSelectField<TFormValues extends FieldValues>({
render={({ field }) => ( render={({ field }) => (
<FormItem className={cn("space-y-0", className)}> <FormItem className={cn("space-y-0", className)}>
{label && ( {label && (
<div className='mb-1 flex justify-between'> <div className='mb-1 flex justify-between gap-2'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<FormLabel htmlFor={name} className='m-0'> <FormLabel htmlFor={name} className='m-0'>
{label} {label}

View File

@ -1,5 +1,4 @@
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { XIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { useCallback } from "react"; import { useCallback } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@ -35,6 +34,7 @@ export const CancelFormButton = ({
const handleClick = useCallback(async () => { const handleClick = useCallback(async () => {
const ok = requestConfirm ? await requestConfirm() : true; const ok = requestConfirm ? await requestConfirm() : true;
console.log("ok => ", ok);
if (!ok) return; if (!ok) return;
if (onCancel) { if (onCancel) {
@ -43,6 +43,7 @@ export const CancelFormButton = ({
} }
if (to) { if (to) {
console.log("navego => ", to);
navigate(to); navigate(to);
} }
// si no hay ni onCancel ni to → no hace nada // si no hay ni onCancel ni to → no hace nada
@ -59,7 +60,6 @@ export const CancelFormButton = ({
aria-disabled={disabled} aria-disabled={disabled}
data-testid={dataTestId} data-testid={dataTestId}
> >
<XIcon className='mr-2 h-3 w-3' />
<span>{label ?? defaultLabel}</span> <span>{label ?? defaultLabel}</span>
</Button> </Button>
); );

View File

@ -1,46 +1,16 @@
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import {
ArrowLeftIcon,
CopyIcon,
EyeIcon,
MoreHorizontalIcon,
RotateCcwIcon,
Trash2Icon,
} from "lucide-react";
import { useFormContext } from "react-hook-form";
import { CancelFormButton, CancelFormButtonProps } from "./cancel-form-button"; import { CancelFormButton, CancelFormButtonProps } from "./cancel-form-button";
import { SubmitButtonProps, SubmitFormButton } from "./submit-form-button"; import { SubmitButtonProps, SubmitFormButton } from "./submit-form-button";
type Align = "start" | "center" | "end" | "between"; type Align = "start" | "center" | "end" | "between";
type GroupSubmitButtonProps = Omit<SubmitButtonProps, "isLoading" | "preventDoubleSubmit">;
export type FormCommitButtonGroupProps = { export type FormCommitButtonGroupProps = {
className?: string; className?: string;
align?: Align; // default "end" align?: Align; // default "end"
gap?: string; // default "gap-2" gap?: string; // default "gap-2"
reverseOrderOnMobile?: boolean; // default true (Cancel debajo en móvil) reverseOrderOnMobile?: boolean; // default true (Cancel debajo en móvil)
isLoading?: boolean;
disabled?: boolean;
preventDoubleSubmit?: boolean; // Evita múltiples submits mientras loading
cancel?: CancelFormButtonProps & { show?: boolean }; cancel?: CancelFormButtonProps & { show?: boolean };
submit?: GroupSubmitButtonProps; // props directas a SubmitButton submit?: SubmitButtonProps; // props directas a SubmitButton
onReset?: () => void;
onDelete?: () => void;
onPreview?: () => void;
onDuplicate?: () => void;
onBack?: () => void;
}; };
const alignToJustify: Record<Align, string> = { const alignToJustify: Record<Align, string> = {
@ -55,33 +25,10 @@ export const FormCommitButtonGroup = ({
align = "end", align = "end",
gap = "gap-2", gap = "gap-2",
reverseOrderOnMobile = true, reverseOrderOnMobile = true,
isLoading,
disabled = false,
preventDoubleSubmit = true,
cancel, cancel,
submit, submit,
onReset,
onDelete,
onPreview,
onDuplicate,
onBack,
}: FormCommitButtonGroupProps) => { }: FormCommitButtonGroupProps) => {
const showCancel = cancel?.show ?? true; const showCancel = cancel?.show ?? true;
const hasSecondaryActions = onReset || onPreview || onDuplicate || onBack || onDelete;
// ⛳️ RHF opcional: auto-detectar isSubmitting si no se pasó isLoading
let rhfIsSubmitting = false;
try {
const ctx = useFormContext();
rhfIsSubmitting = !!ctx?.formState?.isSubmitting;
} catch {
// No hay provider de RHF; ignorar
}
const busy = isLoading ?? rhfIsSubmitting;
const computedDisabled = !!(disabled || (preventDoubleSubmit && busy));
return ( return (
<div <div
@ -93,62 +40,8 @@ export const FormCommitButtonGroup = ({
className className
)} )}
> >
{submit && <SubmitFormButton {...submit} />}
{showCancel && <CancelFormButton {...cancel} />} {showCancel && <CancelFormButton {...cancel} />}
{submit && <SubmitFormButton {...submit} />}
{/* Menú de acciones adicionales */}
{hasSecondaryActions && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost' size='sm' disabled={computedDisabled} className='px-2'>
<MoreHorizontalIcon className='h-4 w-4' />
<span className='sr-only'>Más acciones</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-48'>
{onReset && (
<DropdownMenuItem
onClick={onReset}
disabled={computedDisabled}
className='text-muted-foreground'
>
<RotateCcwIcon className='mr-2 h-4 w-4' />
Deshacer cambios
</DropdownMenuItem>
)}
{onPreview && (
<DropdownMenuItem onClick={onPreview} className='text-muted-foreground'>
<EyeIcon className='mr-2 h-4 w-4' />
Vista previa
</DropdownMenuItem>
)}
{onDuplicate && (
<DropdownMenuItem onClick={onDuplicate} className='text-muted-foreground'>
<CopyIcon className='mr-2 h-4 w-4' />
Duplicar
</DropdownMenuItem>
)}
{onBack && (
<DropdownMenuItem onClick={onBack} className='text-muted-foreground'>
<ArrowLeftIcon className='mr-2 h-4 w-4' />
Volver
</DropdownMenuItem>
)}
{onDelete && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={onDelete}
className='text-destructive focus:text-destructive'
>
<Trash2Icon className='mr-2 h-4 w-4' />
Eliminar
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div> </div>
); );
}; };

View File

@ -1,6 +1,5 @@
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { LoaderCircleIcon } from "lucide-react";
import { LoaderCircleIcon, SaveIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { useTranslation } from "../../../i18n.ts"; import { useTranslation } from "../../../i18n.ts";
@ -9,15 +8,12 @@ export type SubmitButtonProps = {
formId?: string; formId?: string;
isLoading?: boolean; isLoading?: boolean;
label?: string; label?: string;
labelIsLoading?: string;
variant?: React.ComponentProps<typeof Button>["variant"]; variant?: React.ComponentProps<typeof Button>["variant"];
size?: React.ComponentProps<typeof Button>["size"]; size?: React.ComponentProps<typeof Button>["size"];
className?: string; className?: string;
preventDoubleSubmit?: boolean; // Evita múltiples submits mientras loading preventDoubleSubmit?: boolean; // Evita múltiples submits mientras loading
hasChanges?: boolean;
onClick?: React.MouseEventHandler<HTMLButtonElement>; onClick?: React.MouseEventHandler<HTMLButtonElement>;
disabled?: boolean; disabled?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
@ -28,12 +24,10 @@ export const SubmitFormButton = ({
formId, formId,
isLoading, isLoading,
label, label,
labelIsLoading,
variant = "default", variant = "default",
size = "default", size = "default",
className, className,
preventDoubleSubmit = true, preventDoubleSubmit = true,
hasChanges = false,
onClick, onClick,
disabled, disabled,
children, children,
@ -41,7 +35,6 @@ export const SubmitFormButton = ({
}: SubmitButtonProps) => { }: SubmitButtonProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const defaultLabel = t ? t("common.save") : "Save"; const defaultLabel = t ? t("common.save") : "Save";
const defaultLabelIsLoading = t ? t("common.saving") : "Saving...";
// ⛳️ RHF opcional: auto-detectar isSubmitting si no se pasó isLoading // ⛳️ RHF opcional: auto-detectar isSubmitting si no se pasó isLoading
let rhfIsSubmitting = false; let rhfIsSubmitting = false;
@ -72,30 +65,20 @@ export const SubmitFormButton = ({
form={formId} form={formId}
variant={variant} variant={variant}
size={size} size={size}
className={className}
disabled={computedDisabled} disabled={computedDisabled}
aria-busy={busy} aria-busy={busy}
aria-disabled={computedDisabled} aria-disabled={computedDisabled}
data-state={dataState} data-state={dataState}
onClick={handleClick} onClick={handleClick}
data-testid={dataTestId} data-testid={dataTestId}
className={cn("min-w-[100px] font-medium", hasChanges && "ring-2 ring-primary/20", className)}
> >
{children ? ( {children ? (
children children
) : ( ) : (
<span className='inline-flex items-center gap-2'> <span className='inline-flex items-center gap-2'>
{busy && ( {busy && <LoaderCircleIcon className='h-4 w-4 animate-spin' aria-hidden='true' />}
<> <span>{label ?? defaultLabel}</span>
<LoaderCircleIcon className='mr-2 h-3 w-3 animate-spin' aria-hidden='true' />
<span>{labelIsLoading ?? defaultLabelIsLoading}</span>
</>
)}
{!busy && (
<>
<SaveIcon className='mr-2 h-3 w-3' />
<span>{label ?? defaultLabel}</span>
</>
)}
</span> </span>
)} )}
</Button> </Button>

View File

@ -1,6 +1,5 @@
import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api"; import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
import { ModuleParams, validateRequest } from "@erp/core/api"; import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
import { ILogger } from "@repo/rdx-logger";
import { Application, NextFunction, Request, Response, Router } from "express"; import { Application, NextFunction, Request, Response, Router } from "express";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import { import {
@ -116,5 +115,5 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
} }
); );
app.use(`${baseRoutePath}/customer-invoices`, router); app.use(`${baseRoutePath}/proforma-invoices`, router);
}; };

View File

@ -10,7 +10,6 @@ import { useMemo, useState } from "react";
import { MoneyDTO } from "@erp/core"; import { MoneyDTO } from "@erp/core";
import { formatDate, formatMoney } from "@erp/core/client"; import { formatDate, formatMoney } from "@erp/core/client";
import { ErrorOverlay } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { AgGridReact } from "ag-grid-react"; import { AgGridReact } from "ag-grid-react";
import { ChevronRightIcon } from "lucide-react"; import { ChevronRightIcon } from "lucide-react";
@ -24,6 +23,7 @@ ModuleRegistry.registerModules([AllCommunityModule]);
// Create new GridExample component // Create new GridExample component
export const CustomerInvoicesListGrid = () => { export const CustomerInvoicesListGrid = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
@ -141,19 +141,6 @@ export const CustomerInvoicesListGrid = () => {
[autoSizeStrategy, colDefs] [autoSizeStrategy, colDefs]
); );
if (isLoadError) {
return (
<>
<ErrorOverlay
errorMessage={
(loadError as Error)?.message ??
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")
}
/>
</>
);
}
// Container: Defines the grid's theme & dimensions. // Container: Defines the grid's theme & dimensions.
return ( return (
<div <div

View File

@ -15,6 +15,25 @@ const CustomerInvoiceAdd = lazy(() =>
import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate })) import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate }))
); );
//const LogoutPage = lazy(() => import("./app").then((m) => ({ default: m.LogoutPage })));
/*const DealerLayout = lazy(() => import("./app").then((m) => ({ default: m.DealerLayout })));
const DealersList = lazy(() => import("./app").then((m) => ({ default: m.DealersList })));
const LoginPageWithLanguageSelector = lazy(() =>
import("./app").then((m) => ({ default: m.LoginPageWithLanguageSelector }))
);
const CustomerInvoiceEdit = lazy(() => import("./app").then((m) => ({ default: m.CustomerInvoiceEdit })));
const SettingsEditor = lazy(() => import("./app").then((m) => ({ default: m.SettingsEditor })));
const SettingsLayout = lazy(() => import("./app").then((m) => ({ default: m.SettingsLayout })));
const CatalogLayout = lazy(() => import("./app").then((m) => ({ default: m.CatalogLayout })));
const CatalogList = lazy(() => import("./app").then((m) => ({ default: m.CatalogList })));
const DashboardPage = lazy(() => import("./app").then((m) => ({ default: m.DashboardPage })));
const CustomerInvoicesLayout = lazy(() => import("./app").then((m) => ({ default: m.CustomerInvoicesLayout })));
const CustomerInvoicesList = lazy(() => import("./app").then((m) => ({ default: m.CustomerInvoicesList })));*/
export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => { export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => {
return [ return [
{ {

View File

@ -1,2 +1,2 @@
export * from "./create"; export * from "./create";
export * from "./customer-invoices-list"; export * from "./list";

View File

@ -1,6 +1,7 @@
import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components"; import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { CustomerInvoicesListGrid } from "../components"; import { CustomerInvoicesListGrid } from "../components";
import { useTranslation } from "../i18n"; import { useTranslation } from "../i18n";
@ -8,6 +9,17 @@ import { useTranslation } from "../i18n";
export const CustomerInvoicesList = () => { export const CustomerInvoicesList = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [status, setStatus] = useState("all");
/*const CustomerInvoiceStatuses = [
{ value: "all", label: t("customerInvoices.list.tabs.all") },
{ value: "draft", label: t("customerInvoices.list.tabs.draft") },
{ value: "ready", label: t("customerInvoices.list.tabs.ready") },
{ value: "delivered", label: t("customerInvoices.list.tabs.delivered") },
{ value: "accepted", label: t("customerInvoices.list.tabs.accepted") },
{ value: "rejected", label: t("customerInvoices.list.tabs.rejected") },
{ value: "archived", label: t("customerInvoices.list.tabs.archived") },
];*/
return ( return (
<> <>

View File

@ -1,13 +0,0 @@
//import * as z from "zod/v4";
import { ListCustomerInvoicesResponseDTO } from "@erp/customer-invoices/common";
/*export const CustomerCreateSchema = CreateCustomerRequestSchema;
export const CustomerUpdateSchema = UpdateCustomerByIdRequestSchema;
export const CustomerSchema = GetCustomerByIdResponseSchema.omit({
metadata: true,
});
export type CustomerData = z.infer<typeof CustomerSchema>;*/
export type CustomerInvoicesListData = ListCustomerInvoicesResponseDTO;

View File

@ -1,2 +0,0 @@
export * from "./customer-invoices.api.schema";
export * from "./customer-invoices.form.schema";

View File

@ -1,7 +1,6 @@
import { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { CustomerListDTO } from "../../infrastructure/mappers";
import { Customer } from "../aggregates"; import { Customer } from "../aggregates";
/** /**
@ -43,7 +42,7 @@ export interface ICustomerRepository {
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: any transaction?: any
): Promise<Result<Collection<CustomerListDTO>, Error>>; ): Promise<Result<Collection<CustomerListDTO>>>;
/** /**
* Elimina un Customer por su ID, dentro de una empresa. * Elimina un Customer por su ID, dentro de una empresa.

View File

@ -14,10 +14,10 @@ export class CustomerAddressType extends ValueObject<ICustomerAddressTypeProps>
private static readonly ALLOWED_TYPES = ["shipping", "billing"]; private static readonly ALLOWED_TYPES = ["shipping", "billing"];
static create(value: string): Result<CustomerAddressType, Error> { static create(value: string): Result<CustomerAddressType, Error> {
if (!CustomerAddressType.ALLOWED_TYPES.includes(value)) { if (!this.ALLOWED_TYPES.includes(value)) {
return Result.fail( return Result.fail(
new Error( new Error(
`Invalid address type: ${value}. Allowed types are: ${CustomerAddressType.ALLOWED_TYPES.join(", ")}` `Invalid address type: ${value}. Allowed types are: ${this.ALLOWED_TYPES.join(", ")}`
) )
); );
} }

View File

@ -1,4 +1,5 @@
import { DomainValidationError, ValueObject } from "@repo/rdx-ddd"; import { DomainValidationError } from "@erp/core/api";
import { ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import * as z from "zod/v4"; import * as z from "zod/v4";

View File

@ -1,4 +1,5 @@
import { DomainValidationError, ValueObject } from "@repo/rdx-ddd"; import { DomainValidationError } from "@erp/core/api";
import { ValueObject } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import * as z from "zod/v4"; import * as z from "zod/v4";

View File

@ -1,4 +1,5 @@
import { DomainValidationError, ValueObject } from "@repo/rdx-ddd"; import { DomainValidationError } from "@erp/core/api";
import { ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
interface ICustomerStatusProps { interface ICustomerStatusProps {

View File

@ -5,12 +5,6 @@
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save" "save": "Save"
}, },
"catalog": {
"status": {
"active": "active",
"inactive": "inactive"
}
},
"pages": { "pages": {
"title": "Customers", "title": "Customers",
"description": "Manage your customers", "description": "Manage your customers",

View File

@ -5,12 +5,6 @@
"cancel": "Cancelar", "cancel": "Cancelar",
"save": "Guardar" "save": "Guardar"
}, },
"catalog": {
"status": {
"active": "activo",
"inactive": "inactivo"
}
},
"pages": { "pages": {
"title": "Clientes", "title": "Clientes",
"description": "Gestiona tus clientes", "description": "Gestiona tus clientes",
@ -102,7 +96,6 @@
"placeholder": "Ingrese el correo electrónico", "placeholder": "Ingrese el correo electrónico",
"description": "La dirección de correo electrónico principal del cliente" "description": "La dirección de correo electrónico principal del cliente"
}, },
"email_secondary": { "email_secondary": {
"label": "Email secundario", "label": "Email secundario",
"placeholder": "Ingrese el correo electrónico", "placeholder": "Ingrese el correo electrónico",
@ -114,7 +107,6 @@
"placeholder": "Ingrese el número de teléfono", "placeholder": "Ingrese el número de teléfono",
"description": "El número de teléfono del cliente" "description": "El número de teléfono del cliente"
}, },
"phone_secondary": { "phone_secondary": {
"label": "Teléfono secundario", "label": "Teléfono secundario",
"placeholder": "Ingrese el número de teléfono secundario", "placeholder": "Ingrese el número de teléfono secundario",
@ -126,7 +118,6 @@
"placeholder": "Ingrese el número de teléfono", "placeholder": "Ingrese el número de teléfono",
"description": "El número de teléfono del cliente" "description": "El número de teléfono del cliente"
}, },
"mobile_secondary": { "mobile_secondary": {
"label": "Teléfono secundario", "label": "Teléfono secundario",
"placeholder": "Ingrese el número de teléfono secundario", "placeholder": "Ingrese el número de teléfono secundario",
@ -138,25 +129,21 @@
"placeholder": "Ingrese el número de fax", "placeholder": "Ingrese el número de fax",
"description": "El número de fax del cliente" "description": "El número de fax del cliente"
}, },
"website": { "website": {
"label": "Sitio web", "label": "Sitio web",
"placeholder": "Ingrese la URL del sitio web", "placeholder": "Ingrese la URL del sitio web",
"description": "El sitio web del cliente" "description": "El sitio web del cliente"
}, },
"default_taxes": { "default_taxes": {
"label": "Impuesto por defecto", "label": "Impuesto por defecto",
"placeholder": "Seleccione el impuesto por defecto", "placeholder": "Seleccione el impuesto por defecto",
"description": "La tasa de impuesto por defecto para el cliente" "description": "La tasa de impuesto por defecto para el cliente"
}, },
"language_code": { "language_code": {
"label": "Idioma", "label": "Idioma",
"placeholder": "Seleccione el idioma", "placeholder": "Seleccione el idioma",
"description": "El idioma preferido del cliente" "description": "El idioma preferido del cliente"
}, },
"currency_code": { "currency_code": {
"label": "Moneda", "label": "Moneda",
"placeholder": "Seleccione la moneda", "placeholder": "Seleccione la moneda",

View File

@ -42,7 +42,7 @@ export const CustomerStatusBadge = forwardRef<HTMLDivElement, CustomerStatusBadg
return ( return (
<Badge className={cn(commonClassName, config.badge, className)} {...props}> <Badge className={cn(commonClassName, config.badge, className)} {...props}>
<div className={cn("h-1.5 w-1.5 rounded-full mr-2", config.dot)} /> <div className={cn("h-1.5 w-1.5 rounded-full mr-2", config.dot)} />
{t(`catalog.status.${status}`)} {t(`status.${status}`)}
</Badge> </Badge>
); );
} }

View File

@ -67,7 +67,7 @@ export const CustomersListGrid = () => {
{ {
field: "status", field: "status",
headerName: t("pages.list.grid_columns.status"), headerName: t("pages.list.grid_columns.status"),
maxWidth: 135, maxWidth: 125,
cellRenderer: (params: ValueFormatterParams) => { cellRenderer: (params: ValueFormatterParams) => {
return <CustomerStatusBadge status={params.value} />; return <CustomerStatusBadge status={params.value} />;
}, },

View File

@ -1,6 +1,11 @@
import { Description, Field, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/components";
import { SelectField } from "@repo/rdx-ui/components"; import { SelectField } from "@repo/rdx-ui/components";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@repo/shadcn-ui/components";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants"; import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants";
import { useTranslation } from "../../i18n"; import { useTranslation } from "../../i18n";
@ -11,12 +16,15 @@ export const CustomerAdditionalConfigFields = () => {
const { control } = useFormContext<CustomerFormData>(); const { control } = useFormContext<CustomerFormData>();
return ( return (
<Fieldset> <Card className='border-0 shadow-none'>
<Legend>{t("form_groups.preferences.title")}</Legend> <CardHeader>
<Description>{t("form_groups.preferences.description")}</Description> <CardTitle>{t("form_groups.preferences.title")}</CardTitle>
<FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'> <CardDescription>{t("form_groups.preferences.description")}</CardDescription>
<Field className='lg:col-span-2'> </CardHeader>
<CardContent>
<div className='grid grid-cols-1 gap-8 lg:grid-cols-4 mb-12 '>
<SelectField <SelectField
className='lg:col-span-2'
control={control} control={control}
name='language_code' name='language_code'
required required
@ -25,8 +33,6 @@ export const CustomerAdditionalConfigFields = () => {
description={t("form_fields.language_code.description")} description={t("form_fields.language_code.description")}
items={[...LANGUAGE_OPTIONS]} items={[...LANGUAGE_OPTIONS]}
/> />
</Field>
<Field className='lg:col-span-2'>
<SelectField <SelectField
className='lg:col-span-2' className='lg:col-span-2'
control={control} control={control}
@ -37,8 +43,8 @@ export const CustomerAdditionalConfigFields = () => {
description={t("form_fields.currency_code.description")} description={t("form_fields.currency_code.description")}
items={[...CURRENCY_OPTIONS]} items={[...CURRENCY_OPTIONS]}
/> />
</Field> </div>
</FieldGroup> </CardContent>
</Fieldset> </Card>
); );
}; };

View File

@ -1,12 +1,18 @@
import { import {
Description, Description,
Field,
FieldGroup, FieldGroup,
Fieldset, Fieldset,
Legend, Legend,
SelectField, SelectField,
TextField, TextField,
} from "@repo/rdx-ui/components"; } from "@repo/rdx-ui/components";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@repo/shadcn-ui/components";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { COUNTRY_OPTIONS } from "../../constants"; import { COUNTRY_OPTIONS } from "../../constants";
import { useTranslation } from "../../i18n"; import { useTranslation } from "../../i18n";
@ -20,7 +26,7 @@ export const CustomerAddressFields = () => {
<Fieldset> <Fieldset>
<Legend>{t("form_groups.address.title")}</Legend> <Legend>{t("form_groups.address.title")}</Legend>
<Description>{t("form_groups.address.description")}</Description> <Description>{t("form_groups.address.description")}</Description>
<FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'> <FieldGroup className='grid grid-cols-1 gap-8 lg:grid-cols-4'>
<TextField <TextField
className='lg:col-span-2' className='lg:col-span-2'
control={control} control={control}
@ -54,16 +60,77 @@ export const CustomerAddressFields = () => {
description={t("form_fields.postal_code.description")} description={t("form_fields.postal_code.description")}
/> />
<Field className='lg:col-span-2 lg:col-start-1'> <TextField
className='lg:col-span-2 lg:col-start-1'
control={control}
name='province'
label={t("form_fields.province.label")}
placeholder={t("form_fields.province.placeholder")}
description={t("form_fields.province.description")}
/>
<SelectField
control={control}
name='country'
required
label={t("form_fields.country.label")}
placeholder={t("form_fields.country.placeholder")}
description={t("form_fields.country.description")}
items={[...COUNTRY_OPTIONS]}
/>
</FieldGroup>
</Fieldset>
);
return (
<Card className='border-0 shadow-none'>
<CardHeader>
<CardTitle>{t("form_groups.address.title")}</CardTitle>
<CardDescription>{t("form_groups.address.description")}</CardDescription>
</CardHeader>
<CardContent>
<div className='grid grid-cols-1 gap-8 lg:grid-cols-4 mb-6 '>
<TextField <TextField
className='lg:col-span-2'
control={control}
name='street'
label={t("form_fields.street.label")}
placeholder={t("form_fields.street.placeholder")}
description={t("form_fields.street.description")}
/>
<TextField
className='lg:col-span-2'
control={control}
name='street2'
label={t("form_fields.street2.label")}
placeholder={t("form_fields.street2.placeholder")}
description={t("form_fields.street2.description")}
/>
<TextField
className='lg:col-span-2'
control={control}
name='city'
label={t("form_fields.city.label")}
placeholder={t("form_fields.city.placeholder")}
description={t("form_fields.city.description")}
/>
<TextField
control={control}
name='postal_code'
label={t("form_fields.postal_code.label")}
placeholder={t("form_fields.postal_code.placeholder")}
description={t("form_fields.postal_code.description")}
/>
</div>
<div className='grid grid-cols-1 gap-8 lg:grid-cols-4 mb-0 '>
<TextField
className='lg:col-span-2'
control={control} control={control}
name='province' name='province'
label={t("form_fields.province.label")} label={t("form_fields.province.label")}
placeholder={t("form_fields.province.placeholder")} placeholder={t("form_fields.province.placeholder")}
description={t("form_fields.province.description")} description={t("form_fields.province.description")}
/> />
</Field>
<Field className='lg:col-span-2'>
<SelectField <SelectField
control={control} control={control}
name='country' name='country'
@ -73,8 +140,8 @@ export const CustomerAddressFields = () => {
description={t("form_fields.country.description")} description={t("form_fields.country.description")}
items={[...COUNTRY_OPTIONS]} items={[...COUNTRY_OPTIONS]}
/> />
</Field> </div>
</FieldGroup> </CardContent>
</Fieldset> </Card>
); );
}; };

View File

@ -33,9 +33,9 @@ export const CustomerBasicInfoFields = () => {
return ( return (
<Fieldset> <Fieldset>
<Legend>{t("form_groups.basic_info.title")}</Legend> <Legend>Identificación</Legend>
<Description>{t("form_groups.basic_info.description")}</Description> <Description>descripción</Description>
<FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'> <FieldGroup className='grid grid-cols-1 gap-8 lg:grid-cols-4'>
<Field className='lg:col-span-2'> <Field className='lg:col-span-2'>
<TextField <TextField
control={control} control={control}
@ -59,7 +59,7 @@ export const CustomerBasicInfoFields = () => {
field.onChange(value === "false" ? "false" : "true"); field.onChange(value === "false" ? "false" : "true");
}} }}
defaultValue={field.value ? "true" : "false"} defaultValue={field.value ? "true" : "false"}
className='flex items-center gap-6' className='flex items-center gap-8'
> >
<FormItem className='flex items-center space-x-2'> <FormItem className='flex items-center space-x-2'>
<FormControl> <FormControl>
@ -106,16 +106,16 @@ export const CustomerBasicInfoFields = () => {
placeholder={t("form_fields.reference.placeholder")} placeholder={t("form_fields.reference.placeholder")}
description={t("form_fields.reference.description")} description={t("form_fields.reference.description")}
/> />
<Field className='lg:col-span-2'> <TaxesMultiSelectField
<TaxesMultiSelectField className='lg:col-span-2'
control={control} control={control}
name='default_taxes' name='default_taxes'
required required
label={t("form_fields.default_taxes.label")} label={t("form_fields.default_taxes.label")}
placeholder={t("form_fields.default_taxes.placeholder")} placeholder={t("form_fields.default_taxes.placeholder")}
description={t("form_fields.default_taxes.description")} description={t("form_fields.default_taxes.description")}
/> />
</Field>
<TextAreaField <TextAreaField
className='lg:col-span-full' className='lg:col-span-full'
control={control} control={control}

View File

@ -1,11 +1,4 @@
import { import { Description, FieldGroup, Fieldset, Legend, TextField } from "@repo/rdx-ui/components";
Description,
Field,
FieldGroup,
Fieldset,
Legend,
TextField,
} from "@repo/rdx-ui/components";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@repo/shadcn-ui/components"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@repo/shadcn-ui/components";
import { ChevronDown, MailIcon, PhoneIcon, SmartphoneIcon } from "lucide-react"; import { ChevronDown, MailIcon, PhoneIcon, SmartphoneIcon } from "lucide-react";
@ -22,7 +15,7 @@ export const CustomerContactFields = () => {
<Fieldset> <Fieldset>
<Legend>{t("form_groups.contact_info.title")}</Legend> <Legend>{t("form_groups.contact_info.title")}</Legend>
<Description>{t("form_groups.contact_info.description")}</Description> <Description>{t("form_groups.contact_info.description")}</Description>
<FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'> <FieldGroup className='grid grid-cols-1 gap-8 lg:grid-cols-4'>
<TextField <TextField
className='lg:col-span-2' className='lg:col-span-2'
control={control} control={control}
@ -34,7 +27,6 @@ export const CustomerContactFields = () => {
typePreset='email' typePreset='email'
required required
/> />
<TextField <TextField
className='lg:col-span-2' className='lg:col-span-2'
control={control} control={control}
@ -98,37 +90,29 @@ export const CustomerContactFields = () => {
} }
/> />
<Collapsible <Collapsible open={open} onOpenChange={setOpen} className='space-y-4'>
open={open}
onOpenChange={setOpen}
className='space-y-8 col-start-1 col-span-full'
>
<CollapsibleTrigger className='inline-flex items-center gap-1 text-sm text-primary hover:underline'> <CollapsibleTrigger className='inline-flex items-center gap-1 text-sm text-primary hover:underline'>
{t("common.more_details")}{" "} {t("common.more_details")}{" "}
<ChevronDown className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`} /> <ChevronDown className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`} />
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'> <FieldGroup className='grid grid-cols-1 gap-8 lg:grid-cols-4'>
<Field className='lg:col-span-2'> <TextField
<TextField className='lg:col-span-2'
className='lg:col-span-2' control={control}
control={control} name='website'
name='website' label={t("form_fields.website.label")}
label={t("form_fields.website.label")} placeholder={t("form_fields.website.placeholder")}
placeholder={t("form_fields.website.placeholder")} description={t("form_fields.website.description")}
description={t("form_fields.website.description")} />
/> <TextField
</Field> className='lg:col-span-2'
<Field className='lg:col-span-2'> control={control}
<TextField name='fax'
className='lg:col-span-2' label={t("form_fields.fax.label")}
control={control} placeholder={t("form_fields.fax.placeholder")}
name='fax' description={t("form_fields.fax.description")}
label={t("form_fields.fax.label")} />
placeholder={t("form_fields.fax.placeholder")}
description={t("form_fields.fax.description")}
/>
</Field>
</FieldGroup> </FieldGroup>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>

View File

@ -22,7 +22,7 @@ export const CustomerEditForm = ({ formId, onSubmit, onError }: CustomerFormProp
<div className='w-full xl:w-6/12'> <div className='w-full xl:w-6/12'>
<FormDebug /> <FormDebug />
</div> </div>
<div className='w-full xl:grow space-y-6'> <div className='w-full xl:grow'>
<CustomerBasicInfoFields /> <CustomerBasicInfoFields />
<CustomerContactFields /> <CustomerContactFields />
<CustomerAddressFields /> <CustomerAddressFields />

View File

@ -12,6 +12,25 @@ const CustomersList = lazy(() => import("./pages").then((m) => ({ default: m.Cus
const CustomerAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerCreate }))); const CustomerAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerCreate })));
//const LogoutPage = lazy(() => import("./app").then((m) => ({ default: m.LogoutPage })));
/*const DealerLayout = lazy(() => import("./app").then((m) => ({ default: m.DealerLayout })));
const DealersList = lazy(() => import("./app").then((m) => ({ default: m.DealersList })));
const LoginPageWithLanguageSelector = lazy(() =>
import("./app").then((m) => ({ default: m.LoginPageWithLanguageSelector }))
);
const CustomerEdit = lazy(() => import("./app").then((m) => ({ default: m.CustomerEdit })));
const SettingsEditor = lazy(() => import("./app").then((m) => ({ default: m.SettingsEditor })));
const SettingsLayout = lazy(() => import("./app").then((m) => ({ default: m.SettingsLayout })));
const CatalogLayout = lazy(() => import("./app").then((m) => ({ default: m.CatalogLayout })));
const CatalogList = lazy(() => import("./app").then((m) => ({ default: m.CatalogList })));
const DashboardPage = lazy(() => import("./app").then((m) => ({ default: m.DashboardPage })));
const CustomersLayout = lazy(() => import("./app").then((m) => ({ default: m.CustomersLayout })));
const CustomersList = lazy(() => import("./app").then((m) => ({ default: m.CustomersList })));*/
export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => { export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
return [ return [
{ {

View File

@ -1,13 +1,13 @@
import { useDataSource, useQueryKey } from "@erp/core/hooks"; import { useDataSource, useQueryKey } from "@erp/core/hooks";
import { ListCustomersResponseDTO } from "@erp/customer-invoices/common";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { CustomersListData } from "../schemas";
// Obtener todas las facturas // Obtener todas las facturas
export const useCustomersQuery = (params?: any) => { export const useCustomersQuery = (params?: any) => {
const dataSource = useDataSource(); const dataSource = useDataSource();
const keys = useQueryKey(); const keys = useQueryKey();
return useQuery<CustomersListData>({ return useQuery<ListCustomersResponseDTO>({
queryKey: keys().data().resource("customers").action("list").params(params).get(), queryKey: keys().data().resource("customers").action("list").params(params).get(),
queryFn: async (context) => { queryFn: async (context) => {
const { signal } = context; const { signal } = context;
@ -16,7 +16,7 @@ export const useCustomersQuery = (params?: any) => {
...params, ...params,
}); });
return customers as CustomersListData; return customers as ListCustomersResponseDTO;
}, },
}); });
}; };

View File

@ -54,16 +54,12 @@ export const CustomerCreate = () => {
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario // Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
}; };
const handleBack = () => {
navigate(-1);
};
return ( return (
<> <>
<AppBreadcrumb /> <AppBreadcrumb />
<AppContent> <AppContent>
<UnsavedChangesProvider isDirty={form.formState.isDirty}> <UnsavedChangesProvider isDirty={form.formState.isDirty}>
<div className='flex items-center justify-between space-y-6'> <div className='flex items-center justify-between space-y-4 px-6'>
<div className='space-y-2'> <div className='space-y-2'>
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'> <h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
{t("pages.create.title")} {t("pages.create.title")}
@ -73,17 +69,14 @@ export const CustomerCreate = () => {
</p> </p>
</div> </div>
<FormCommitButtonGroup <FormCommitButtonGroup
isLoading={isCreating}
disabled={isCreating}
cancel={{ cancel={{
to: "/customers/list", to: "/customers/list",
disabled: isCreating,
}} }}
submit={{ submit={{
formId: "customer-create-form", formId: "customer-create-form",
disabled: isCreating, disabled: isCreating,
isLoading: isCreating,
}} }}
onBack={() => handleBack()}
/> />
</div> </div>
{/* Alerta de error de actualización (si ha fallado el último intento) */} {/* Alerta de error de actualización (si ha fallado el último intento) */}
@ -97,13 +90,15 @@ export const CustomerCreate = () => {
/> />
)} )}
<FormProvider {...form}> <div className='flex flex-1 flex-col gap-4 p-4'>
<CustomerEditForm <FormProvider {...form}>
formId='customer-create-form' <CustomerEditForm
onSubmit={handleSubmit} formId='customer-create-form'
onError={handleError} onSubmit={handleSubmit}
/> onError={handleError}
</FormProvider> />
</FormProvider>
</div>
</UnsavedChangesProvider> </UnsavedChangesProvider>
</AppContent> </AppContent>
</> </>

View File

@ -1,2 +1,2 @@
export * from "./create"; export * from "./create";
export * from "./customer-list"; export * from "./list";

View File

@ -13,7 +13,7 @@ export const CustomersList = () => {
<> <>
<AppBreadcrumb /> <AppBreadcrumb />
<AppContent> <AppContent>
<div className='flex items-center justify-between space-y-6'> <div className='flex items-center justify-between space-y-2'>
<div> <div>
<h2 className='text-2xl font-bold tracking-tight'>{t("pages.list.title")}</h2> <h2 className='text-2xl font-bold tracking-tight'>{t("pages.list.title")}</h2>
<p className='text-muted-foreground'>{t("pages.list.description")}</p> <p className='text-muted-foreground'>{t("pages.list.description")}</p>
@ -21,7 +21,7 @@ export const CustomersList = () => {
<div className='flex items-center space-x-2'> <div className='flex items-center space-x-2'>
<Button onClick={() => navigate("/customers/create")} className='cursor-pointer'> <Button onClick={() => navigate("/customers/create")} className='cursor-pointer'>
<PlusIcon className='w-4 h-4 mr-2' /> <PlusIcon className='w-4 h-4 mr-2' />
{t("pages.list.title")} {t("pages.create.title")}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -67,12 +67,6 @@ export const CustomerUpdate = () => {
); );
}; };
const handleReset = () => form.reset(customerData ?? defaultCustomerFormData);
const handleBack = () => {
navigate(-1);
};
const handleError = (errors: FieldErrors<CustomerFormData>) => { const handleError = (errors: FieldErrors<CustomerFormData>) => {
console.error("Errores en el formulario:", errors); console.error("Errores en el formulario:", errors);
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario // Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
@ -121,7 +115,7 @@ export const CustomerUpdate = () => {
<AppBreadcrumb /> <AppBreadcrumb />
<AppContent> <AppContent>
<UnsavedChangesProvider isDirty={form.formState.isDirty}> <UnsavedChangesProvider isDirty={form.formState.isDirty}>
<div className='flex items-center justify-between space-y-6'> <div className='flex items-center justify-between space-y-4 px-6'>
<div className='space-y-2'> <div className='space-y-2'>
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'> <h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
{t("pages.update.title")} {t("pages.update.title")}
@ -131,18 +125,14 @@ export const CustomerUpdate = () => {
</p> </p>
</div> </div>
<FormCommitButtonGroup <FormCommitButtonGroup
isLoading={isUpdating}
disabled={isUpdating}
cancel={{ cancel={{
to: "/customers/list", to: "/customers/list",
disabled: isUpdating,
}} }}
submit={{ submit={{
formId: "customer-update-form", formId: "customer-update-form",
disabled: isUpdating, disabled: isUpdating,
isLoading: isUpdating,
}} }}
onBack={() => handleBack()}
onReset={() => handleReset()}
/> />
</div> </div>
{/* Alerta de error de actualización (si ha fallado el último intento) */} {/* Alerta de error de actualización (si ha fallado el último intento) */}
@ -156,13 +146,15 @@ export const CustomerUpdate = () => {
/> />
)} )}
<FormProvider {...form}> <div className='flex flex-1 flex-col gap-4 p-4'>
<CustomerEditForm <FormProvider {...form}>
formId={"customer-update-form"} // para que el botón del header pueda hacer submit <CustomerEditForm
onSubmit={handleSubmit} formId={"customer-update-form"} // para que el botón del header pueda hacer submit
onError={handleError} onSubmit={handleSubmit}
/> onError={handleError}
</FormProvider> />
</FormProvider>
</div>
</UnsavedChangesProvider> </UnsavedChangesProvider>
</AppContent> </AppContent>
</> </>

View File

@ -0,0 +1,15 @@
import * as z from "zod/v4";
import { CustomerFormSchema } from "./customer.form.schema";
export const UpdateCustomerFormSchema = CustomerFormSchema.extend({
is_company: CustomerFormSchema.shape.is_company.optional(),
name: CustomerFormSchema.shape.name.optional(),
default_taxes: z.array(z.string()).optional(),
country: CustomerFormSchema.shape.country.optional(),
language_code: CustomerFormSchema.shape.language_code.optional(),
currency_code: CustomerFormSchema.shape.currency_code.optional(),
});
export type UpdateCustomerFormData = z.infer<typeof UpdateCustomerFormSchema>;

View File

@ -3,7 +3,6 @@ import * as z from "zod/v4";
import { import {
CreateCustomerRequestSchema, CreateCustomerRequestSchema,
GetCustomerByIdResponseSchema, GetCustomerByIdResponseSchema,
ListCustomersResponseDTO,
UpdateCustomerByIdRequestSchema, UpdateCustomerByIdRequestSchema,
} from "@erp/customers"; } from "@erp/customers";
@ -14,5 +13,3 @@ export const CustomerSchema = GetCustomerByIdResponseSchema.omit({
}); });
export type CustomerData = z.infer<typeof CustomerSchema>; export type CustomerData = z.infer<typeof CustomerSchema>;
export type CustomersListData = ListCustomersResponseDTO;

View File

@ -3,14 +3,12 @@
"version": "0.0.1", "version": "0.0.1",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",
"exports": { "./api": "./src/api/index.ts" }, "exports": {},
"peerDependencies": { "peerDependencies": {
"sequelize": "^6.37.5", "sequelize": "^6.37.5"
"express": "^4.18.2"
}, },
"devDependencies": { "@types/express": "^4.17.21" }, "devDependencies": {},
"dependencies": { "dependencies": {
"@erp/auth": "workspace:*",
"@erp/core": "workspace:*", "@erp/core": "workspace:*",
"@repo/rdx-ddd": "workspace:*", "@repo/rdx-ddd": "workspace:*",
"@repo/rdx-utils": "workspace:*", "@repo/rdx-utils": "workspace:*",

View File

@ -8,7 +8,6 @@ export const verifactuAPIModule: IModuleServer = {
async init(params: ModuleParams) { async init(params: ModuleParams) {
// const contacts = getService<ContactsService>("contacts"); // const contacts = getService<ContactsService>("contacts");
console.log("111111111111111111111111111A>>>>>>>>>>>>>>>>>>>");
const { logger } = params; const { logger } = params;
verifactuRouter(params); verifactuRouter(params);
logger.info("🚀 Verifactu module initialized", { label: this.name }); logger.info("🚀 Verifactu module initialized", { label: this.name });

View File

@ -14,7 +14,6 @@ export const verifactuRouter = (params: ModuleParams) => {
logger: ILogger; logger: ILogger;
}; };
console.log("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>>>>>>>>>>>>>>>>>>>");
const deps = buildVerifactuDependencies(params); const deps = buildVerifactuDependencies(params);
const router: Router = Router({ mergeParams: true }); const router: Router = Router({ mergeParams: true });

View File

@ -1,3 +1,3 @@
//export * from "./mappers"; //export * from "./mappers";
//export * from "./sequelize";
export * from "./express"; export * from "./express";
export * from "./sequelize";

View File

@ -20,7 +20,7 @@ interface ErrorOverlayProps {
export const ErrorOverlay = ({ export const ErrorOverlay = ({
title = "Se ha producido un error", title = "Se ha producido un error",
subtitle = "Inténtalo de nuevo más tarde", subtitle = undefined,
description = undefined, description = undefined,
errorMessage = undefined, errorMessage = undefined,
}: //errorStatusCode = undefined, }: //errorStatusCode = undefined,
@ -32,7 +32,7 @@ ErrorOverlayProps): JSX.Element => {
: _DrawByStatusCode['0'];*/ : _DrawByStatusCode['0'];*/
return ( return (
<div className='h-fit place-items-center-safe mt-10 '> <div className='grid h-screen place-items-center '>
<div className='text-center'> <div className='text-center'>
<h2 className='mt-2 text-xl font-semibold text-center text-slate-900'>{title}</h2> <h2 className='mt-2 text-xl font-semibold text-center text-slate-900'>{title}</h2>
<p className='mt-1 font-medium text-slate-500'> <p className='mt-1 font-medium text-slate-500'>

View File

@ -1,7 +1,6 @@
import { import {
Calendar, Calendar,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -167,9 +166,16 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
</p> </p>
)} )}
<FormDescription className={cn("text-xs truncate", !description && "invisible")}> {(inputError || description) && (
{description || "\u00A0"} <p
</FormDescription> className={cn(
"text-xs mt-1",
inputError ? "text-destructive" : "text-muted-foreground"
)}
>
{inputError || description}
</p>
)}
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@ -2,7 +2,6 @@
import { import {
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -54,9 +53,9 @@ export function NumberField<TFormValues extends FieldValues>({
<Input disabled={isDisabled} placeholder={placeholder} {...field} /> <Input disabled={isDisabled} placeholder={placeholder} {...field} />
</FormControl> </FormControl>
<FormDescription className={cn("text-xs truncate", !description && "invisible")}> <p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
{description || "\u00A0"} {description || "\u00A0"}
</FormDescription> </p>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

@ -55,7 +55,7 @@ export function SelectField<TFormValues extends FieldValues>({
render={({ field }) => ( render={({ field }) => (
<FormItem className={cn("space-y-0", className)}> <FormItem className={cn("space-y-0", className)}>
{label && ( {label && (
<div className='mb-1 flex justify-between'> <div className='mb-1 flex justify-between gap-2'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<FormLabel htmlFor={name} className='m-0'> <FormLabel htmlFor={name} className='m-0'>
{label} {label}
@ -72,7 +72,7 @@ export function SelectField<TFormValues extends FieldValues>({
)} )}
<Select onValueChange={field.onChange} defaultValue={field.value} disabled={isDisabled}> <Select onValueChange={field.onChange} defaultValue={field.value} disabled={isDisabled}>
<FormControl> <FormControl>
<SelectTrigger className='w-full bg-background h-8'> <SelectTrigger className='w-full'>
<SelectValue placeholder={placeholder} /> <SelectValue placeholder={placeholder} />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
@ -85,7 +85,9 @@ export function SelectField<TFormValues extends FieldValues>({
</SelectContent> </SelectContent>
</Select> </Select>
<FormDescription className={cn("text-xs truncate", !description && "invisible")}> <FormDescription
className={cn("text-xs text-muted-foreground", !description && "invisible")}
>
{description || "\u00A0"} {description || "\u00A0"}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />

View File

@ -2,7 +2,6 @@
import { import {
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -59,17 +58,12 @@ export function TextAreaField<TFormValues extends FieldValues>({
</div> </div>
)} )}
<FormControl> <FormControl>
<Textarea <Textarea disabled={isDisabled} placeholder={placeholder} {...field} />
disabled={isDisabled}
placeholder={placeholder}
className={"bg-background"}
{...field}
/>
</FormControl> </FormControl>
<FormDescription className={cn("text-xs truncate", !description && "invisible")}> <p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
{description || "\u00A0"} {description || "\u00A0"}
</FormDescription> </p>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

@ -1,6 +1,5 @@
import { import {
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -394,7 +393,7 @@ export function TextField<TFormValues extends FieldValues>({
maxLength={maxLength} maxLength={maxLength}
{...rest} {...rest}
className={cn( className={cn(
"placeholder:font-normal placeholder:italic bg-background", "placeholder:font-normal placeholder:italic",
inputPadding, inputPadding,
invalid && "border-destructive focus-visible:ring-destructive", invalid && "border-destructive focus-visible:ring-destructive",
valid && showSuccessWhenValid && "border-green-500 focus-visible:ring-green-500", valid && showSuccessWhenValid && "border-green-500 focus-visible:ring-green-500",
@ -449,12 +448,12 @@ export function TextField<TFormValues extends FieldValues>({
</FormControl> </FormControl>
<div className='mt-1 flex items-start justify-between'> <div className='mt-1 flex items-start justify-between'>
<FormDescription <p
id={describedById} id={describedById}
className={cn("text-xs truncate", !description && "invisible")} className={cn("text-xs text-muted-foreground", !description && "invisible")}
> >
{description || "\u00A0"} {description || "\u00A0"}
</FormDescription> </p>
{showCounter && typeof maxLength === "number" && ( {showCounter && typeof maxLength === "number" && (
<p className='text-xs text-muted-foreground'> <p className='text-xs text-muted-foreground'>

View File

@ -4,10 +4,7 @@ import * as React from "react";
export const Fieldset = ({ className, children, ...props }: React.ComponentProps<"fieldset">) => ( export const Fieldset = ({ className, children, ...props }: React.ComponentProps<"fieldset">) => (
<fieldset <fieldset
data-slot='fieldset' data-slot='fieldset'
className={cn( className={cn("*:data-[slot=text]:mt-1 [&>*+[data-slot=control]]:mt-6", className)}
"*:data-[slot=text]:mt-1 [&>*+[data-slot=control]]:mt-6 bg-gray-50/50 rounded-xl p-6",
className
)}
{...props} {...props}
> >
{children} {children}
@ -15,7 +12,7 @@ export const Fieldset = ({ className, children, ...props }: React.ComponentProps
); );
export const FieldGroup = ({ className, children, ...props }: React.ComponentProps<"div">) => ( export const FieldGroup = ({ className, children, ...props }: React.ComponentProps<"div">) => (
<div data-slot='control' className={cn("space-y-6", className)} {...props}> <div data-slot='control' className={cn("space-y-8", className)} {...props}>
{children} {children}
</div> </div>
); );

View File

@ -0,0 +1,11 @@
import { cn } from "@repo/shadcn-ui/lib/utils";
export function FormContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot='form-content'
className={cn("grid grid-cols-1 gap-6 md:grid-cols-4 space-y-6", className)}
{...props}
/>
);
}

View File

@ -1,6 +1,7 @@
export * from "./DatePickerField.tsx"; export * from "./DatePickerField.tsx";
export * from "./DatePickerInputField.tsx"; export * from "./DatePickerInputField.tsx";
export * from "./fieldset.tsx"; export * from "./fieldset.tsx";
export * from "./form-content.tsx";
export * from "./multi-select-field.tsx"; export * from "./multi-select-field.tsx";
export * from "./SelectField.tsx"; export * from "./SelectField.tsx";
export * from "./TextAreaField.tsx"; export * from "./TextAreaField.tsx";

View File

@ -9,7 +9,6 @@ import {
CommandList, CommandList,
CommandSeparator, CommandSeparator,
FormControl, FormControl,
FormDescription,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
@ -439,12 +438,12 @@ export const MultiSelectFieldInner = React.forwardRef(
</FormControl> </FormControl>
<div className='mt-1 flex items-start justify-between'> <div className='mt-1 flex items-start justify-between'>
<FormDescription <p
id={describedById} id={describedById}
className={cn("text-xs truncate", !description && "invisible")} className={cn("text-xs text-muted-foreground", !description && "invisible")}
> >
{description || "\u00A0"} {description || "\u00A0"}
</FormDescription> </p>
</div> </div>
<FormMessage id={errorId} /> <FormMessage id={errorId} />

View File

@ -1,5 +1,5 @@
import { type VariantProps, cva } from "class-variance-authority"; import { type VariantProps, cva } from "class-variance-authority";
import { CheckIcon, ChevronDown, WandSparkles, XCircleIcon } from "lucide-react"; import { CheckIcon, ChevronDown, WandSparkles, XCircle } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { import {
@ -219,7 +219,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
{...props} {...props}
onClick={handleTogglePopover} onClick={handleTogglePopover}
className={cn( 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", "flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto",
className className
)} )}
> >
@ -260,7 +260,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
style={{ animationDuration: `${animation}s` }} style={{ animationDuration: `${animation}s` }}
> >
{`+ ${selectedValues.length - maxCount} more`} {`+ ${selectedValues.length - maxCount} more`}
<XCircleIcon <XCircle
className='ml-2 h-4 w-4 cursor-pointer' className='ml-2 h-4 w-4 cursor-pointer'
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();

View File

@ -1,31 +1,33 @@
"use client"; "use client"
import * as LabelPrimitive from "@radix-ui/react-label"; import * as React from "react"
import { Slot } from "@radix-ui/react-slot"; import * as LabelPrimitive from "@radix-ui/react-label"
import * as React from "react"; import { Slot } from "@radix-ui/react-slot"
import { import {
Controller, Controller,
type ControllerProps,
type FieldPath,
type FieldValues,
FormProvider, FormProvider,
useFormContext, useFormContext,
useFormState, useFormState,
} from "react-hook-form"; type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { Label } from "@repo/shadcn-ui/components/label"; import { cn } from "@repo/shadcn-ui/lib/utils"
import { cn } from "@repo/shadcn-ui/lib/utils"; import { Label } from "@repo/shadcn-ui/components/label"
const Form = FormProvider; const Form = FormProvider
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = { > = {
name: TName; name: TName
}; }
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue); const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
@ -37,21 +39,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
); )
}; }
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext); const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext); const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext(); const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name }); const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState); const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>"); throw new Error("useFormField should be used within <FormField>")
} }
const { id } = itemContext; const { id } = itemContext
return { return {
id, id,
@ -60,93 +62,106 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState,
}; }
}; }
type FormItemContextValue = { type FormItemContextValue = {
id: string; id: string
}; }
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue); const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) { function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId(); const id = React.useId()
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div data-slot='form-item' className={cn("grid gap-2", className)} {...props} /> <div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider> </FormItemContext.Provider>
); )
} }
function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) { function FormLabel({
const { error, formItemId } = useFormField(); className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return ( return (
<Label <Label
data-slot='form-label' data-slot="form-label"
data-error={!!error} data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)} className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
); )
} }
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField(); const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return ( return (
<Slot <Slot
data-slot='form-control' data-slot="form-control"
id={formItemId} id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`} aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
); )
} }
function FormDescription({ className, ...props }: React.ComponentProps<"p">) { function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField(); const { formDescriptionId } = useFormField()
return ( return (
<p <p
data-slot='form-description' data-slot="form-description"
id={formDescriptionId} id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
); )
} }
function FormMessage({ className, ...props }: React.ComponentProps<"p">) { function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField(); const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children; const body = error ? String(error?.message ?? "") : props.children
if (!body) { if (!body) {
return null; return null
} }
return ( return (
<p <p
data-slot='form-message' data-slot="form-message"
id={formMessageId} id={formMessageId}
className={cn("text-destructive text-sm", className)} className={cn("text-destructive text-sm", className)}
{...props} {...props}
> >
{body} {body}
</p> </p>
); )
} }
export { export {
useFormField,
Form, Form,
FormControl,
FormDescription,
FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormControl,
FormDescription,
FormMessage, FormMessage,
useFormField, FormField,
}; }

View File

@ -41,9 +41,6 @@ importers:
'@erp/customers': '@erp/customers':
specifier: workspace:* specifier: workspace:*
version: link:../../modules/customers version: link:../../modules/customers
'@erp/verifactu':
specifier: workspace:*
version: link:../../modules/verifactu
'@repo/rdx-logger': '@repo/rdx-logger':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/rdx-logger version: link:../../packages/rdx-logger
@ -662,9 +659,6 @@ importers:
modules/verifactu: modules/verifactu:
dependencies: dependencies:
'@erp/auth':
specifier: workspace:*
version: link:../auth
'@erp/core': '@erp/core':
specifier: workspace:* specifier: workspace:*
version: link:../core version: link:../core
@ -677,16 +671,9 @@ importers:
'@repo/rdx-utils': '@repo/rdx-utils':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/rdx-utils version: link:../../packages/rdx-utils
express:
specifier: ^4.18.2
version: 4.21.2
sequelize: sequelize:
specifier: ^6.37.5 specifier: ^6.37.5
version: 6.37.7(mysql2@3.14.1) version: 6.37.7(mysql2@3.14.1)
devDependencies:
'@types/express':
specifier: ^4.17.21
version: 4.17.23
packages/rdx-criteria: packages/rdx-criteria:
dependencies: dependencies: