Compare commits

...

2 Commits

Author SHA1 Message Date
327756413d Clientes y facturas de cliente 2025-09-24 17:09:37 +02:00
7e700bdf22 Clientes y facturas de cliente 2025-09-24 13:49:17 +02:00
54 changed files with 425 additions and 364 deletions

View File

@ -45,6 +45,7 @@
"@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,6 +2,7 @@
"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 gap-2'> <div className='mb-1 flex justify-between'>
<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,4 +1,5 @@
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";
@ -34,7 +35,6 @@ 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,7 +43,6 @@ 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
@ -60,6 +59,7 @@ 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,16 +1,46 @@
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?: SubmitButtonProps; // props directas a SubmitButton submit?: GroupSubmitButtonProps; // props directas a SubmitButton
onReset?: () => void;
onDelete?: () => void;
onPreview?: () => void;
onDuplicate?: () => void;
onBack?: () => void;
}; };
const alignToJustify: Record<Align, string> = { const alignToJustify: Record<Align, string> = {
@ -25,10 +55,33 @@ 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
@ -40,8 +93,62 @@ export const FormCommitButtonGroup = ({
className className
)} )}
> >
{showCancel && <CancelFormButton {...cancel} />}
{submit && <SubmitFormButton {...submit} />} {submit && <SubmitFormButton {...submit} />}
{showCancel && <CancelFormButton {...cancel} />}
{/* 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,5 +1,6 @@
import { Button } from "@repo/shadcn-ui/components"; import { Button } from "@repo/shadcn-ui/components";
import { LoaderCircleIcon } from "lucide-react"; import { cn } from "@repo/shadcn-ui/lib/utils";
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";
@ -8,12 +9,15 @@ 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;
@ -24,10 +28,12 @@ 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,
@ -35,6 +41,7 @@ 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;
@ -65,20 +72,30 @@ 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 && <LoaderCircleIcon className='h-4 w-4 animate-spin' aria-hidden='true' />} {busy && (
<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,5 +1,6 @@
import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api"; import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api"; import { 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 {
@ -115,5 +116,5 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
} }
); );
app.use(`${baseRoutePath}/proforma-invoices`, router); app.use(`${baseRoutePath}/customer-invoices`, router);
}; };

View File

@ -10,6 +10,7 @@ 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";
@ -23,7 +24,6 @@ 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,6 +141,19 @@ 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,25 +15,6 @@ 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,7 +1,6 @@
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";
@ -9,17 +8,6 @@ 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,2 +1,2 @@
export * from "./create"; export * from "./create";
export * from "./list"; export * from "./customer-invoices-list";

View File

@ -0,0 +1,13 @@
//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

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

View File

@ -1,6 +1,7 @@
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";
/** /**
@ -42,7 +43,7 @@ export interface ICustomerRepository {
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: any transaction?: any
): Promise<Result<Collection<CustomerListDTO>>>; ): Promise<Result<Collection<CustomerListDTO>, Error>>;
/** /**
* 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 (!this.ALLOWED_TYPES.includes(value)) { if (!CustomerAddressType.ALLOWED_TYPES.includes(value)) {
return Result.fail( return Result.fail(
new Error( new Error(
`Invalid address type: ${value}. Allowed types are: ${this.ALLOWED_TYPES.join(", ")}` `Invalid address type: ${value}. Allowed types are: ${CustomerAddressType.ALLOWED_TYPES.join(", ")}`
) )
); );
} }

View File

@ -1,5 +1,4 @@
import { DomainValidationError } from "@erp/core/api"; import { DomainValidationError, ValueObject } from "@repo/rdx-ddd";
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,5 +1,4 @@
import { DomainValidationError } from "@erp/core/api"; import { DomainValidationError, ValueObject } from "@repo/rdx-ddd";
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,5 +1,4 @@
import { DomainValidationError } from "@erp/core/api"; import { DomainValidationError, ValueObject } from "@repo/rdx-ddd";
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,6 +5,12 @@
"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,6 +5,12 @@
"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",
@ -96,6 +102,7 @@
"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",
@ -107,6 +114,7 @@
"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",
@ -118,6 +126,7 @@
"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",
@ -129,21 +138,25 @@
"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(`status.${status}`)} {t(`catalog.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: 125, maxWidth: 135,
cellRenderer: (params: ValueFormatterParams) => { cellRenderer: (params: ValueFormatterParams) => {
return <CustomerStatusBadge status={params.value} />; return <CustomerStatusBadge status={params.value} />;
}, },

View File

@ -1,11 +1,6 @@
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";
@ -16,15 +11,12 @@ export const CustomerAdditionalConfigFields = () => {
const { control } = useFormContext<CustomerFormData>(); const { control } = useFormContext<CustomerFormData>();
return ( return (
<Card className='border-0 shadow-none'> <Fieldset>
<CardHeader> <Legend>{t("form_groups.preferences.title")}</Legend>
<CardTitle>{t("form_groups.preferences.title")}</CardTitle> <Description>{t("form_groups.preferences.description")}</Description>
<CardDescription>{t("form_groups.preferences.description")}</CardDescription> <FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'>
</CardHeader> <Field className='lg:col-span-2'>
<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
@ -33,6 +25,8 @@ 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}
@ -43,8 +37,8 @@ export const CustomerAdditionalConfigFields = () => {
description={t("form_fields.currency_code.description")} description={t("form_fields.currency_code.description")}
items={[...CURRENCY_OPTIONS]} items={[...CURRENCY_OPTIONS]}
/> />
</div> </Field>
</CardContent> </FieldGroup>
</Card> </Fieldset>
); );
}; };

View File

@ -1,18 +1,12 @@
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";
@ -26,7 +20,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-8 lg:grid-cols-4'> <FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'>
<TextField <TextField
className='lg:col-span-2' className='lg:col-span-2'
control={control} control={control}
@ -60,77 +54,16 @@ export const CustomerAddressFields = () => {
description={t("form_fields.postal_code.description")} description={t("form_fields.postal_code.description")}
/> />
<TextField <Field className='lg:col-span-2 lg:col-start-1'>
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'
@ -140,8 +73,8 @@ export const CustomerAddressFields = () => {
description={t("form_fields.country.description")} description={t("form_fields.country.description")}
items={[...COUNTRY_OPTIONS]} items={[...COUNTRY_OPTIONS]}
/> />
</div> </Field>
</CardContent> </FieldGroup>
</Card> </Fieldset>
); );
}; };

View File

@ -33,9 +33,9 @@ export const CustomerBasicInfoFields = () => {
return ( return (
<Fieldset> <Fieldset>
<Legend>Identificación</Legend> <Legend>{t("form_groups.basic_info.title")}</Legend>
<Description>descripción</Description> <Description>{t("form_groups.basic_info.description")}</Description>
<FieldGroup className='grid grid-cols-1 gap-8 lg:grid-cols-4'> <FieldGroup className='grid grid-cols-1 gap-6 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-8' className='flex items-center gap-6'
> >
<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")}
/> />
<TaxesMultiSelectField <Field className='lg:col-span-2'>
className='lg:col-span-2' <TaxesMultiSelectField
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,4 +1,11 @@
import { Description, FieldGroup, Fieldset, Legend, TextField } from "@repo/rdx-ui/components"; import {
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";
@ -15,7 +22,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-8 lg:grid-cols-4'> <FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'>
<TextField <TextField
className='lg:col-span-2' className='lg:col-span-2'
control={control} control={control}
@ -27,6 +34,7 @@ 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}
@ -90,29 +98,37 @@ export const CustomerContactFields = () => {
} }
/> />
<Collapsible open={open} onOpenChange={setOpen} className='space-y-4'> <Collapsible
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-8 lg:grid-cols-4'> <FieldGroup className='grid grid-cols-1 gap-6 lg:grid-cols-4'>
<TextField <Field className='lg:col-span-2'>
className='lg:col-span-2' <TextField
control={control} className='lg:col-span-2'
name='website' control={control}
label={t("form_fields.website.label")} name='website'
placeholder={t("form_fields.website.placeholder")} label={t("form_fields.website.label")}
description={t("form_fields.website.description")} placeholder={t("form_fields.website.placeholder")}
/> description={t("form_fields.website.description")}
<TextField />
className='lg:col-span-2' </Field>
control={control} <Field className='lg:col-span-2'>
name='fax' <TextField
label={t("form_fields.fax.label")} className='lg:col-span-2'
placeholder={t("form_fields.fax.placeholder")} control={control}
description={t("form_fields.fax.description")} name='fax'
/> 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'> <div className='w-full xl:grow space-y-6'>
<CustomerBasicInfoFields /> <CustomerBasicInfoFields />
<CustomerContactFields /> <CustomerContactFields />
<CustomerAddressFields /> <CustomerAddressFields />

View File

@ -12,25 +12,6 @@ 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<ListCustomersResponseDTO>({ return useQuery<CustomersListData>({
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 ListCustomersResponseDTO; return customers as CustomersListData;
}, },
}); });
}; };

View File

@ -54,12 +54,16 @@ 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-4 px-6'> <div className='flex items-center justify-between space-y-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")}
@ -69,14 +73,17 @@ 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) */}
@ -90,15 +97,13 @@ export const CustomerCreate = () => {
/> />
)} )}
<div className='flex flex-1 flex-col gap-4 p-4'> <FormProvider {...form}>
<FormProvider {...form}> <CustomerEditForm
<CustomerEditForm formId='customer-create-form'
formId='customer-create-form' onSubmit={handleSubmit}
onSubmit={handleSubmit} onError={handleError}
onError={handleError} />
/> </FormProvider>
</FormProvider>
</div>
</UnsavedChangesProvider> </UnsavedChangesProvider>
</AppContent> </AppContent>
</> </>

View File

@ -13,7 +13,7 @@ export const CustomersList = () => {
<> <>
<AppBreadcrumb /> <AppBreadcrumb />
<AppContent> <AppContent>
<div className='flex items-center justify-between space-y-2'> <div className='flex items-center justify-between space-y-6'>
<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.create.title")} {t("pages.list.title")}
</Button> </Button>
</div> </div>
</div> </div>

View File

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

View File

@ -67,6 +67,12 @@ 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
@ -115,7 +121,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-4 px-6'> <div className='flex items-center justify-between space-y-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")}
@ -125,14 +131,18 @@ 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) */}
@ -146,15 +156,13 @@ export const CustomerUpdate = () => {
/> />
)} )}
<div className='flex flex-1 flex-col gap-4 p-4'> <FormProvider {...form}>
<FormProvider {...form}> <CustomerEditForm
<CustomerEditForm formId={"customer-update-form"} // para que el botón del header pueda hacer submit
formId={"customer-update-form"} // para que el botón del header pueda hacer submit onSubmit={handleSubmit}
onSubmit={handleSubmit} onError={handleError}
onError={handleError} />
/> </FormProvider>
</FormProvider>
</div>
</UnsavedChangesProvider> </UnsavedChangesProvider>
</AppContent> </AppContent>
</> </>

View File

@ -1,15 +0,0 @@
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,6 +3,7 @@ import * as z from "zod/v4";
import { import {
CreateCustomerRequestSchema, CreateCustomerRequestSchema,
GetCustomerByIdResponseSchema, GetCustomerByIdResponseSchema,
ListCustomersResponseDTO,
UpdateCustomerByIdRequestSchema, UpdateCustomerByIdRequestSchema,
} from "@erp/customers"; } from "@erp/customers";
@ -13,3 +14,5 @@ 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,12 +3,14 @@
"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": {}, "exports": { "./api": "./src/api/index.ts" },
"peerDependencies": { "peerDependencies": {
"sequelize": "^6.37.5" "sequelize": "^6.37.5",
"express": "^4.18.2"
}, },
"devDependencies": {}, "devDependencies": { "@types/express": "^4.17.21" },
"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,6 +8,7 @@ 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,6 +14,7 @@ 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 = undefined, subtitle = "Inténtalo de nuevo más tarde",
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='grid h-screen place-items-center '> <div className='h-fit place-items-center-safe mt-10 '>
<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,6 +1,7 @@
import { import {
Calendar, Calendar,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -166,16 +167,9 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
</p> </p>
)} )}
{(inputError || description) && ( <FormDescription className={cn("text-xs truncate", !description && "invisible")}>
<p {description || "\u00A0"}
className={cn( </FormDescription>
"text-xs mt-1",
inputError ? "text-destructive" : "text-muted-foreground"
)}
>
{inputError || description}
</p>
)}
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@ -2,6 +2,7 @@
import { import {
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -53,9 +54,9 @@ export function NumberField<TFormValues extends FieldValues>({
<Input disabled={isDisabled} placeholder={placeholder} {...field} /> <Input disabled={isDisabled} placeholder={placeholder} {...field} />
</FormControl> </FormControl>
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}> <FormDescription className={cn("text-xs truncate", !description && "invisible")}>
{description || "\u00A0"} {description || "\u00A0"}
</p> </FormDescription>
<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 gap-2'> <div className='mb-1 flex justify-between'>
<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'> <SelectTrigger className='w-full bg-background h-8'>
<SelectValue placeholder={placeholder} /> <SelectValue placeholder={placeholder} />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
@ -85,9 +85,7 @@ export function SelectField<TFormValues extends FieldValues>({
</SelectContent> </SelectContent>
</Select> </Select>
<FormDescription <FormDescription className={cn("text-xs truncate", !description && "invisible")}>
className={cn("text-xs text-muted-foreground", !description && "invisible")}
>
{description || "\u00A0"} {description || "\u00A0"}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />

View File

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

View File

@ -1,5 +1,6 @@
import { import {
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -393,7 +394,7 @@ export function TextField<TFormValues extends FieldValues>({
maxLength={maxLength} maxLength={maxLength}
{...rest} {...rest}
className={cn( className={cn(
"placeholder:font-normal placeholder:italic", "placeholder:font-normal placeholder:italic bg-background",
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",
@ -448,12 +449,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'>
<p <FormDescription
id={describedById} id={describedById}
className={cn("text-xs text-muted-foreground", !description && "invisible")} className={cn("text-xs truncate", !description && "invisible")}
> >
{description || "\u00A0"} {description || "\u00A0"}
</p> </FormDescription>
{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,7 +4,10 @@ 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("*:data-[slot=text]:mt-1 [&>*+[data-slot=control]]:mt-6", className)} className={cn(
"*:data-[slot=text]:mt-1 [&>*+[data-slot=control]]:mt-6 bg-gray-50/50 rounded-xl p-6",
className
)}
{...props} {...props}
> >
{children} {children}
@ -12,7 +15,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-8", className)} {...props}> <div data-slot='control' className={cn("space-y-6", className)} {...props}>
{children} {children}
</div> </div>
); );

View File

@ -1,11 +0,0 @@
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,7 +1,6 @@
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,6 +9,7 @@ import {
CommandList, CommandList,
CommandSeparator, CommandSeparator,
FormControl, FormControl,
FormDescription,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
@ -438,12 +439,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'>
<p <FormDescription
id={describedById} id={describedById}
className={cn("text-xs text-muted-foreground", !description && "invisible")} className={cn("text-xs truncate", !description && "invisible")}
> >
{description || "\u00A0"} {description || "\u00A0"}
</p> </FormDescription>
</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, XCircle } from "lucide-react"; import { CheckIcon, ChevronDown, WandSparkles, XCircleIcon } 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 p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto", "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",
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`}
<XCircle <XCircleIcon
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,33 +1,31 @@
"use client" "use client";
import * as React from "react" import * as LabelPrimitive from "@radix-ui/react-label";
import * as LabelPrimitive from "@radix-ui/react-label" import { Slot } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot" import * as React from "react";
import { import {
Controller, Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps, type ControllerProps,
type FieldPath, type FieldPath,
type FieldValues, type FieldValues,
} from "react-hook-form" FormProvider,
useFormContext,
useFormState,
} from "react-hook-form";
import { cn } from "@repo/shadcn-ui/lib/utils" import { Label } from "@repo/shadcn-ui/components/label";
import { Label } from "@repo/shadcn-ui/components/label" import { cn } from "@repo/shadcn-ui/lib/utils";
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>( const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
{} as FormFieldContextValue
)
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
@ -39,21 +37,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,
@ -62,106 +60,93 @@ 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>( const FormItemContext = React.createContext<FormItemContextValue>({} as 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 <div data-slot='form-item' className={cn("grid gap-2", className)} {...props} />
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider> </FormItemContext.Provider>
) );
} }
function FormLabel({ function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
className, const { error, formItemId } = useFormField();
...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={ aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
!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,
FormItem,
FormLabel,
FormControl, FormControl,
FormDescription, FormDescription,
FormMessage,
FormField, FormField,
} FormItem,
FormLabel,
FormMessage,
useFormField,
};

View File

@ -41,6 +41,9 @@ 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
@ -659,6 +662,9 @@ 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
@ -671,9 +677,16 @@ 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: