Clientes
This commit is contained in:
parent
633f7cf6bd
commit
01ead56fff
@ -7,7 +7,7 @@ import { i18n } from "@/locales";
|
||||
|
||||
import { AuthProvider, createAuthService } from "@erp/auth/client";
|
||||
import { createAxiosDataSource, createAxiosInstance } from "@erp/core/client";
|
||||
import { DataSourceProvider, UnsavedWarnProvider } from "@erp/core/hooks";
|
||||
import { DataSourceProvider } from "@erp/core/hooks";
|
||||
import DineroFactory from "dinero.js";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
import { clearAccessToken, getAccessToken, setAccessToken } from "./lib";
|
||||
@ -55,12 +55,10 @@ export const App = () => {
|
||||
}}
|
||||
>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<UnsavedWarnProvider>
|
||||
{/* Fallback Route */}
|
||||
<Suspense fallback={<LoadingOverlay />}>
|
||||
<RouterProvider router={appRouter} />
|
||||
</Suspense>
|
||||
</UnsavedWarnProvider>
|
||||
{/* Fallback Route */}
|
||||
<Suspense fallback={<LoadingOverlay />}>
|
||||
<RouterProvider router={appRouter} />
|
||||
</Suspense>
|
||||
</TooltipProvider>
|
||||
<Toaster
|
||||
toastOptions={
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
"express": "^4.18.2",
|
||||
"http-status": "^2.1.0",
|
||||
"i18next": "^25.1.1",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"required": "•"
|
||||
},
|
||||
"components": {
|
||||
@ -12,11 +13,11 @@
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"use_unsaved_changes_notifier": {
|
||||
"unsaved_changes": "You have unsaved changes",
|
||||
"unsaved_changes_explanation": "If you leave, your changes will be lost.",
|
||||
"discard_changes": "Discard changes",
|
||||
"stay_on_page": "Stay on page"
|
||||
"unsaved_changes_dialog": {
|
||||
"title": "You have unsaved changes",
|
||||
"description": "If you leave, your changes will be lost.",
|
||||
"confirm": "Discard changes",
|
||||
"cancel": "Stay on page"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"cancel": "Cancelar",
|
||||
"save": "Guardar",
|
||||
"required": "•"
|
||||
},
|
||||
"components": {
|
||||
@ -12,11 +13,11 @@
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"use_unsaved_changes_notifier": {
|
||||
"unsaved_changes": "Tienes cambios no guardados",
|
||||
"unsaved_changes_explanation": "Si sales, tus cambios se perderán.",
|
||||
"discard_changes": "Descartar cambios",
|
||||
"stay_on_page": "Permanecer en la página"
|
||||
"unsaved_changes_dialog": {
|
||||
"title": "Tienes cambios no guardados",
|
||||
"description": "Si sales, tus cambios se perderán.",
|
||||
"confirm": "Descartar cambios",
|
||||
"cancel": "Permanecer en esta página"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import * as React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "../../../i18n.ts";
|
||||
import { useUnsavedChangesContext } from "../use-unsaved-changes-notifier";
|
||||
|
||||
export type CancelFormButtonProps = {
|
||||
to?: string; /// Ruta a la que navegar si no se pasa onCancel
|
||||
onCancel?: () => void | Promise<void>; // Prioritaria sobre "to"
|
||||
label?: string;
|
||||
variant?: React.ComponentProps<typeof Button>["variant"];
|
||||
size?: React.ComponentProps<typeof Button>["size"];
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
"data-testid"?: string;
|
||||
};
|
||||
|
||||
export const CancelFormButton = ({
|
||||
to,
|
||||
onCancel,
|
||||
label,
|
||||
variant = "outline",
|
||||
size = "default",
|
||||
className,
|
||||
disabled,
|
||||
"data-testid": dataTestId = "cancel-button",
|
||||
}: CancelFormButtonProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const defaultLabel = t ? t("common.cancel") : "Cancel";
|
||||
const { requestConfirm } = useUnsavedChangesContext();
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
const ok = requestConfirm ? await requestConfirm() : true;
|
||||
console.log("ok => ", ok);
|
||||
if (!ok) return;
|
||||
|
||||
if (onCancel) {
|
||||
await onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (to) {
|
||||
console.log("navego => ", to);
|
||||
navigate(to);
|
||||
}
|
||||
// si no hay ni onCancel ni to → no hace nada
|
||||
}, [requestConfirm, onCancel, to, navigate]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
type='button'
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={className}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
aria-disabled={disabled}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<span>{label ?? defaultLabel}</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,47 @@
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { CancelFormButton, CancelFormButtonProps } from "./cancel-form-button";
|
||||
import { SubmitButtonProps, SubmitFormButton } from "./submit-form-button";
|
||||
|
||||
type Align = "start" | "center" | "end" | "between";
|
||||
|
||||
export type FormCommitButtonGroupProps = {
|
||||
className?: string;
|
||||
align?: Align; // default "end"
|
||||
gap?: string; // default "gap-2"
|
||||
reverseOrderOnMobile?: boolean; // default true (Cancel debajo en móvil)
|
||||
cancel?: CancelFormButtonProps & { show?: boolean };
|
||||
submit?: SubmitButtonProps; // props directas a SubmitButton
|
||||
};
|
||||
|
||||
const alignToJustify: Record<Align, string> = {
|
||||
start: "justify-start",
|
||||
center: "justify-center",
|
||||
end: "justify-end",
|
||||
between: "justify-between",
|
||||
};
|
||||
|
||||
export const FormCommitButtonGroup = ({
|
||||
className,
|
||||
align = "end",
|
||||
gap = "gap-2",
|
||||
reverseOrderOnMobile = true,
|
||||
cancel,
|
||||
submit,
|
||||
}: FormCommitButtonGroupProps) => {
|
||||
const showCancel = cancel?.show ?? true;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
reverseOrderOnMobile ? "flex-col-reverse sm:flex-row" : "flex-row",
|
||||
alignToJustify[align],
|
||||
gap,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{showCancel && <CancelFormButton {...cancel} />}
|
||||
{submit && <SubmitFormButton {...submit} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
export * from "./cancel-form-button";
|
||||
export * from "./form-commit-button-group";
|
||||
export * from "./submit-form-button";
|
||||
export * from "./unsaved-changes-dialog";
|
||||
@ -0,0 +1,86 @@
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { LoaderCircleIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "../../../i18n.ts";
|
||||
|
||||
export type SubmitButtonProps = {
|
||||
formId?: string;
|
||||
isLoading?: boolean;
|
||||
label?: string;
|
||||
|
||||
variant?: React.ComponentProps<typeof Button>["variant"];
|
||||
size?: React.ComponentProps<typeof Button>["size"];
|
||||
className?: string;
|
||||
|
||||
preventDoubleSubmit?: boolean; // Evita múltiples submits mientras loading
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
disabled?: boolean;
|
||||
children?: React.ReactNode;
|
||||
"data-testid"?: string;
|
||||
};
|
||||
|
||||
export const SubmitFormButton = ({
|
||||
formId,
|
||||
isLoading,
|
||||
label,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
className,
|
||||
preventDoubleSubmit = true,
|
||||
onClick,
|
||||
disabled,
|
||||
children,
|
||||
"data-testid": dataTestId = "submit-button",
|
||||
}: SubmitButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultLabel = t ? t("common.save") : "Save";
|
||||
|
||||
// ⛳️ 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));
|
||||
const dataState = busy ? "loading" : "idle";
|
||||
|
||||
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
if (preventDoubleSubmit && busy) {
|
||||
// Evitar submits duplicados durante loading
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
onClick?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
type='submit'
|
||||
form={formId}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={className}
|
||||
disabled={computedDisabled}
|
||||
aria-busy={busy}
|
||||
aria-disabled={computedDisabled}
|
||||
data-state={dataState}
|
||||
onClick={handleClick}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
{children ? (
|
||||
children
|
||||
) : (
|
||||
<span className='inline-flex items-center gap-2'>
|
||||
{busy && <LoaderCircleIcon className='h-4 w-4 animate-spin' aria-hidden='true' />}
|
||||
<span>{label ?? defaultLabel}</span>
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import { CustomDialog } from "@repo/rdx-ui/components";
|
||||
import { useTranslation } from "../../../i18n";
|
||||
|
||||
type UnsavedChangesDialogProps = {
|
||||
open: boolean;
|
||||
onConfirm: (ok: boolean) => void;
|
||||
};
|
||||
|
||||
export function UnsavedChangesDialog({ open, onConfirm }: UnsavedChangesDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const defaultTitle = t ? t("hooks.unsaved_changes_dialog.title") : "¿Descartar cambios?";
|
||||
const defaultDescription = t
|
||||
? t("hooks.unsaved_changes_dialog.description")
|
||||
: "Tienes cambios sin guardar. Si sales de esta página, se perderán.";
|
||||
const defaultCancelLabel = t ? t("hooks.unsaved_changes_dialog.cancel") : "Seguir en esta página";
|
||||
const defaultConfirmLabel = t ? t("hooks.unsaved_changes_dialog.confirm") : "Descartar cambios";
|
||||
|
||||
return (
|
||||
<CustomDialog
|
||||
open={open}
|
||||
onConfirm={onConfirm}
|
||||
title={defaultTitle}
|
||||
description={defaultDescription}
|
||||
confirmLabel={defaultConfirmLabel}
|
||||
cancelLabel={defaultCancelLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,4 +1,2 @@
|
||||
export * from "./components";
|
||||
export * from "./use-unsaved-changes-notifier";
|
||||
export * from "./use-warn-about-change";
|
||||
export * from "./warn-about-change-context";
|
||||
export * from "./warn-about-change-provider";
|
||||
|
||||
@ -1,89 +1,94 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useBlocker } from "react-router-dom";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { useWarnAboutChange } from "./use-warn-about-change";
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||
import { UNSAFE_NavigationContext, useBeforeUnload, useBlocker } from "react-router-dom";
|
||||
import { UnsavedChangesDialog } from "./components";
|
||||
|
||||
export type UnsavedChangesNotifierProps = {
|
||||
isDirty?: boolean;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
onConfirm?: () => void;
|
||||
onCancel?: () => void;
|
||||
type?: "success" | "error" | "warning" | "info";
|
||||
type ContextValue = {
|
||||
requestConfirm: () => Promise<boolean>;
|
||||
};
|
||||
|
||||
export const useUnsavedChangesNotifier = ({
|
||||
isDirty = false,
|
||||
title,
|
||||
subtitle,
|
||||
confirmText,
|
||||
cancelText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
type = "warning",
|
||||
}: UnsavedChangesNotifierProps = {}) => {
|
||||
const { t } = useTranslation();
|
||||
const blocker = useBlocker(isDirty);
|
||||
const { show } = useWarnAboutChange();
|
||||
export const UnsavedChangesContext = createContext<ContextValue | null>(null);
|
||||
|
||||
const texts = useMemo(
|
||||
() => ({
|
||||
title: title ?? t("hooks.use_unsaved_changes_notifier.unsaved_changes"),
|
||||
subtitle: subtitle ?? t("hooks.use_unsaved_changes_notifier.unsaved_changes_explanation"),
|
||||
confirmText: confirmText ?? t("hooks.use_unsaved_changes_notifier.discard_changes"),
|
||||
cancelText: cancelText ?? t("hooks.use_unsaved_changes_notifier.stay_on_page"),
|
||||
}),
|
||||
[t, title, subtitle, confirmText, cancelText]
|
||||
export const useUnsavedChangesContext = () => {
|
||||
const context = useContext(UnsavedChangesContext);
|
||||
if (context === null) {
|
||||
throw new Error("useUnsavedChangesContext must be used within an UnsavedChangesProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export function UnsavedChangesProvider({
|
||||
isDirty,
|
||||
children,
|
||||
}: {
|
||||
isDirty: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [resolver, setResolver] = useState<(ok: boolean) => void>();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// requestConfirm() devuelve una promesa que resuelve true/false
|
||||
// según lo que haya pulsado el usuario
|
||||
const requestConfirm = useCallback(() => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
setResolver(() => resolve);
|
||||
setOpen(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConfirm = (ok: boolean) => {
|
||||
resolver?.(ok);
|
||||
setResolver(undefined);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
// 🔹 bloquea F5/cierre de pestaña
|
||||
useBeforeUnload(
|
||||
(event) => {
|
||||
if (isDirty) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
|
||||
const confirm = useCallback(() => {
|
||||
if (!isDirty) return Promise.resolve(true);
|
||||
// 🔹 bloquea navegación interna (react-router)
|
||||
const navigator = useContext(UNSAFE_NavigationContext).navigator as any;
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
show({
|
||||
title: texts.title,
|
||||
subtitle: texts.subtitle,
|
||||
confirmText: texts.confirmText,
|
||||
cancelText: texts.cancelText,
|
||||
type,
|
||||
onConfirm: () => {
|
||||
resolve(true);
|
||||
onConfirm?.();
|
||||
window.onbeforeunload = null; // limpirar de cambios
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(false);
|
||||
onCancel?.();
|
||||
},
|
||||
});
|
||||
});
|
||||
}, [isDirty, show, texts, type, onConfirm, onCancel]);
|
||||
useEffect(() => {
|
||||
if (!isDirty) return;
|
||||
|
||||
const push = navigator.push;
|
||||
navigator.push = async (...args: any[]) => {
|
||||
const ok = await requestConfirm();
|
||||
if (ok) push.apply(navigator, args);
|
||||
};
|
||||
|
||||
return () => {
|
||||
navigator.push = push;
|
||||
};
|
||||
}, [isDirty, requestConfirm, navigator]);
|
||||
|
||||
// 🔹 Bloquea navegación entre rutas
|
||||
const blocker = useBlocker(() => isDirty);
|
||||
|
||||
useEffect(() => {
|
||||
if (blocker.state === "blocked") {
|
||||
confirm().then((result) => {
|
||||
if (result) blocker.proceed();
|
||||
else blocker.reset();
|
||||
});
|
||||
(async () => {
|
||||
const ok = await requestConfirm();
|
||||
if (ok) {
|
||||
blocker.proceed();
|
||||
} else {
|
||||
blocker.reset();
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [blocker, confirm]);
|
||||
}, [blocker, requestConfirm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDirty) {
|
||||
window.onbeforeunload = (e) => {
|
||||
e.preventDefault();
|
||||
return texts.subtitle;
|
||||
};
|
||||
} else {
|
||||
window.onbeforeunload = null;
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, [isDirty, texts.subtitle]);
|
||||
|
||||
return { confirm };
|
||||
};
|
||||
return (
|
||||
<UnsavedChangesContext.Provider value={{ requestConfirm }}>
|
||||
<h1>hola</h1>
|
||||
{children}
|
||||
<UnsavedChangesDialog open={open} onConfirm={handleConfirm} />
|
||||
</UnsavedChangesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
import { UnsavedWarnContext } from "./warn-about-change-context";
|
||||
|
||||
export const useWarnAboutChange = () => {
|
||||
const context = useContext(UnsavedWarnContext);
|
||||
if (context === null) {
|
||||
throw new Error("useWarnAboutChange must be used within an UnsavedWarnProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
import { NullOr } from "@repo/rdx-utils";
|
||||
import { createContext } from "react";
|
||||
import type { UnsavedChangesNotifierProps } from "./use-unsaved-changes-notifier";
|
||||
|
||||
export interface IUnsavedWarnContextState {
|
||||
show: (options: NullOr<UnsavedChangesNotifierProps>) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
export const UnsavedWarnContext = createContext<NullOr<IUnsavedWarnContextState>>(null);
|
||||
@ -1,52 +0,0 @@
|
||||
import { CustomDialog } from "@repo/rdx-ui/components";
|
||||
import { NullOr } from "@repo/rdx-utils";
|
||||
import { type PropsWithChildren, useCallback, useMemo, useState } from "react";
|
||||
import type { UnsavedChangesNotifierProps } from "./use-unsaved-changes-notifier";
|
||||
import { UnsavedWarnContext } from "./warn-about-change-context";
|
||||
|
||||
// Aseguramos que las props mínimas para el diálogo siempre estén presentes
|
||||
type ConfirmOptions = Required<
|
||||
Pick<UnsavedChangesNotifierProps, "title" | "subtitle" | "confirmText" | "cancelText">
|
||||
> &
|
||||
Omit<UnsavedChangesNotifierProps, "title" | "subtitle" | "confirmText" | "cancelText">;
|
||||
|
||||
export const UnsavedWarnProvider = ({ children }: PropsWithChildren) => {
|
||||
const [confirm, setConfirm] = useState<NullOr<ConfirmOptions>>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const show = useCallback((confirmOptions: NullOr<UnsavedChangesNotifierProps>) => {
|
||||
if (confirmOptions) {
|
||||
setConfirm(confirmOptions as ConfirmOptions);
|
||||
setOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const hide = useCallback(() => setOpen(false), []);
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
confirm?.onConfirm?.();
|
||||
hide();
|
||||
}, [confirm, hide]);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
confirm?.onCancel?.();
|
||||
hide();
|
||||
}, [confirm, hide]);
|
||||
|
||||
const value = useMemo(() => ({ show, hide }), [show, hide]);
|
||||
|
||||
return (
|
||||
<UnsavedWarnContext.Provider value={value}>
|
||||
{children}
|
||||
<CustomDialog
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
title={confirm?.title}
|
||||
description={confirm?.subtitle}
|
||||
confirmLabel={confirm?.confirmText}
|
||||
cancelLabel={confirm?.cancelText}
|
||||
isOpen={open}
|
||||
/>
|
||||
</UnsavedWarnContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -1,8 +1,7 @@
|
||||
import { AppBreadcrumb, AppContent, ButtonGroup } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useUnsavedChangesNotifier } from "@erp/core/hooks";
|
||||
import { FormCommitButtonGroup, UnsavedChangesProvider } from "@erp/core/hooks";
|
||||
import { showErrorToast, showSuccessToast } from "@repo/shadcn-ui/lib/utils";
|
||||
import { useState } from "react";
|
||||
import { FieldErrors } from "react-hook-form";
|
||||
@ -24,10 +23,6 @@ export const CustomerCreate = () => {
|
||||
error: createError,
|
||||
} = useCreateCustomerMutation();
|
||||
|
||||
const { confirm: confirmCancel } = useUnsavedChangesNotifier({
|
||||
isDirty,
|
||||
});
|
||||
|
||||
// 3) Submit con navegación condicionada por éxito
|
||||
const handleSubmit = async (formData: CustomerFormData) => {
|
||||
/*const changedData: Record<string, string> = {};
|
||||
@ -56,68 +51,54 @@ export const CustomerCreate = () => {
|
||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||
};
|
||||
|
||||
console.log("render");
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBreadcrumb />
|
||||
<AppContent>
|
||||
<div className='flex items-center justify-between space-y-4 px-6'>
|
||||
<div className='space-y-2'>
|
||||
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
|
||||
{t("pages.create.title")}
|
||||
</h2>
|
||||
<p className='text-muted-foreground scroll-m-20 tracking-tight text-balance'>
|
||||
{t("pages.create.description")}
|
||||
</p>
|
||||
</div>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className='cursor-pointer'
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const ok = await confirmCancel();
|
||||
if (ok) {
|
||||
console.log("Cambios descartados");
|
||||
navigate("/customers/list");
|
||||
}
|
||||
<UnsavedChangesProvider isDirty={isDirty}>
|
||||
<div className='flex items-center justify-between space-y-4 px-6'>
|
||||
<div className='space-y-2'>
|
||||
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
|
||||
{t("pages.create.title")}
|
||||
</h2>
|
||||
<p className='text-muted-foreground scroll-m-20 tracking-tight text-balance'>
|
||||
{t("pages.create.description")}
|
||||
</p>
|
||||
</div>
|
||||
<FormCommitButtonGroup
|
||||
cancel={{
|
||||
to: "/customers/list",
|
||||
}}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
submit={{
|
||||
formId: "customer-create-form",
|
||||
disabled: isCreating,
|
||||
isLoading: isCreating,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
||||
{isCreateError && (
|
||||
<ErrorAlert
|
||||
title={t("pages.create.errorTitle", "No se pudo guardar los cambios")}
|
||||
message={
|
||||
(createError as Error)?.message ??
|
||||
t("pages.create.errorMsg", "Revisa los datos e inténtalo de nuevo.")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
form='customer-create-form'
|
||||
className='cursor-pointer'
|
||||
disabled={isCreating}
|
||||
aria-busy={isCreating}
|
||||
aria-disabled={isCreating}
|
||||
data-state={isCreating ? "loading" : "idle"}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
||||
{isCreateError && (
|
||||
<ErrorAlert
|
||||
title={t("pages.create.errorTitle", "No se pudo guardar los cambios")}
|
||||
message={
|
||||
(createError as Error)?.message ??
|
||||
t("pages.create.errorMsg", "Revisa los datos e inténtalo de nuevo.")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='flex flex-1 flex-col gap-4 p-4'>
|
||||
<CustomerEditForm
|
||||
formId={"customer-create-form"} // para que el botón del header pueda hacer submit
|
||||
initialValues={defaultCustomerFormData}
|
||||
onSubmit={handleSubmit}
|
||||
onError={handleError}
|
||||
onDirtyChange={setIsDirty}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-1 flex-col gap-4 p-4'>
|
||||
<CustomerEditForm
|
||||
formId={"customer-create-form"} // para que el botón del header pueda hacer submit
|
||||
initialValues={defaultCustomerFormData}
|
||||
onSubmit={handleSubmit}
|
||||
onError={handleError}
|
||||
onDirtyChange={setIsDirty}
|
||||
/>
|
||||
</div>
|
||||
</UnsavedChangesProvider>
|
||||
</AppContent>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -10,18 +10,16 @@ import {
|
||||
} from "@repo/shadcn-ui/components";
|
||||
|
||||
interface CustomDialogProps {
|
||||
isOpen: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
title: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
cancelLabel: React.ReactNode;
|
||||
confirmLabel: React.ReactNode;
|
||||
open: boolean;
|
||||
onConfirm: (ok: boolean) => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
cancelLabel?: string;
|
||||
confirmLabel?: string;
|
||||
}
|
||||
|
||||
export const CustomDialog = ({
|
||||
isOpen,
|
||||
onCancel,
|
||||
open,
|
||||
onConfirm,
|
||||
title,
|
||||
description,
|
||||
@ -29,15 +27,15 @@ export const CustomDialog = ({
|
||||
confirmLabel,
|
||||
}: CustomDialogProps) => {
|
||||
return (
|
||||
<AlertDialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
|
||||
<AlertDialog open={open} onOpenChange={(open) => !open && onConfirm(false)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={onCancel}>{cancelLabel}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirm}>{confirmLabel}</AlertDialogAction>
|
||||
<AlertDialogCancel onClick={() => onConfirm(false)}>{cancelLabel}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => onConfirm(true)}>{confirmLabel}</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
@ -179,7 +179,7 @@ importers:
|
||||
version: 29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3))
|
||||
ts-jest:
|
||||
specifier: ^29.2.5
|
||||
version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
tsconfig-paths:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@ -395,6 +395,9 @@ importers:
|
||||
i18next:
|
||||
specifier: ^25.1.1
|
||||
version: 25.2.1(typescript@5.8.3)
|
||||
lucide-react:
|
||||
specifier: ^0.503.0
|
||||
version: 0.503.0(react@19.1.0)
|
||||
react:
|
||||
specifier: ^19.1.0
|
||||
version: 19.1.0
|
||||
@ -12295,7 +12298,7 @@ snapshots:
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
ejs: 3.1.10
|
||||
@ -12313,6 +12316,7 @@ snapshots:
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.27.4)
|
||||
esbuild: 0.25.5
|
||||
jest-util: 29.7.0
|
||||
|
||||
ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user