Facturas de cliente y clientes
This commit is contained in:
parent
c260f64007
commit
5b7ee437ff
@ -38,6 +38,7 @@
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
|
||||
@ -3,16 +3,19 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import { UnsavedWarnProvider } from "@/lib/hooks";
|
||||
import { i18n } from "@/locales";
|
||||
|
||||
import { AuthProvider, createAuthService } from "@erp/auth/client";
|
||||
import { createAxiosDataSource, createAxiosInstance } from "@erp/core/client";
|
||||
import { DataSourceProvider } from "@erp/core/hooks";
|
||||
import { DataSourceProvider, UnsavedWarnProvider } from "@erp/core/hooks";
|
||||
import DineroFactory from "dinero.js";
|
||||
import "./app.css";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
import { clearAccessToken, getAccessToken, setAccessToken } from "./lib";
|
||||
import { AppRoutes } from "./routes";
|
||||
import { getAppRouter } from "./routes";
|
||||
|
||||
import { LoadingOverlay } from "@repo/rdx-ui/components";
|
||||
import { Suspense } from "react";
|
||||
import "./app.css";
|
||||
|
||||
export const App = () => {
|
||||
DineroFactory.globalLocale = "es-ES";
|
||||
@ -37,6 +40,7 @@ export const App = () => {
|
||||
});
|
||||
|
||||
const dataSource = createAxiosDataSource(axiosInstance);
|
||||
const appRouter = getAppRouter();
|
||||
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
@ -52,7 +56,10 @@ export const App = () => {
|
||||
>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<UnsavedWarnProvider>
|
||||
<AppRoutes />
|
||||
{/* Fallback Route */}
|
||||
<Suspense fallback={<LoadingOverlay />}>
|
||||
<RouterProvider router={appRouter} />
|
||||
</Suspense>
|
||||
</UnsavedWarnProvider>
|
||||
</TooltipProvider>
|
||||
<Toaster />
|
||||
|
||||
214
apps/web/src/components/error-fallbacks.tsx
Normal file
214
apps/web/src/components/error-fallbacks.tsx
Normal file
@ -0,0 +1,214 @@
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import * as React from "react";
|
||||
import { FallbackProps } from "react-error-boundary";
|
||||
|
||||
/**
|
||||
* 1) Fallback simple
|
||||
*/
|
||||
export function SimpleFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
return (
|
||||
<div role='alert' className='p-4 rounded-md border bg-red-50 text-red-700'>
|
||||
<p className='font-medium'>⚠️ Algo salió mal</p>
|
||||
<pre className='mt-2 text-sm whitespace-pre-wrap'>{error?.message}</pre>
|
||||
<Button
|
||||
onClick={resetErrorBoundary}
|
||||
className='mt-3 px-3 py-1.5 rounded-md bg-red-600 text-white'
|
||||
>
|
||||
Reintentar
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 2) Fallback de sección (tarjeta compacta)
|
||||
*/
|
||||
export function SectionCardFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
return (
|
||||
<div role='alert' className='rounded-2xl border shadow-sm p-5 bg-white'>
|
||||
<div className='flex items-start gap-3'>
|
||||
<div className='shrink-0 rounded-full p-2 bg-red-100'>❗</div>
|
||||
<div className='grow'>
|
||||
<h3 className='font-semibold text-gray-900'>No se pudo cargar esta sección</h3>
|
||||
<p className='mt-1 text-sm text-gray-600'>{error?.message}</p>
|
||||
<div className='mt-3 flex gap-2'>
|
||||
<Button
|
||||
onClick={resetErrorBoundary}
|
||||
className='px-3 py-1.5 rounded-md bg-gray-900 text-white'
|
||||
>
|
||||
Reintentar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => window.location.reload()}
|
||||
className='px-3 py-1.5 rounded-md border'
|
||||
>
|
||||
Refrescar página
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 3) Fallback pantalla completa
|
||||
*/
|
||||
export function FullPageFallback({ error }: FallbackProps) {
|
||||
return (
|
||||
<div className='min-h-screen flex flex-col items-center justify-center px-6 bg-gray-50 text-center'>
|
||||
<div className='text-5xl'>😵</div>
|
||||
<h1 className='mt-4 text-2xl font-bold text-gray-900'>Ocurrió un error inesperado</h1>
|
||||
<p className='mt-2 text-gray-600'>{error?.message}</p>
|
||||
<div className='mt-6 flex flex-wrap items-center justify-center gap-3'>
|
||||
<a href='/' className='px-4 py-2 rounded-md bg-blue-600 text-white'>
|
||||
Volver al inicio
|
||||
</a>
|
||||
<Button onClick={() => window.location.reload()} className='px-4 py-2 rounded-md border'>
|
||||
Recargar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 4) Fallback para listas
|
||||
*/
|
||||
export function ListFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
return (
|
||||
<div role='alert' className='p-4 rounded-md border bg-amber-50'>
|
||||
<div className='flex items-start gap-3'>
|
||||
<span className='text-xl'>🗂️</span>
|
||||
<div>
|
||||
<p className='font-medium text-amber-900'>No pudimos cargar la lista.</p>
|
||||
<p className='text-sm text-amber-800 mt-1'>{error?.message}</p>
|
||||
<div className='mt-3 flex gap-2'>
|
||||
<Button
|
||||
onClick={resetErrorBoundary}
|
||||
className='px-3 py-1.5 rounded-md bg-amber-700 text-white'
|
||||
>
|
||||
Reintentar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => window.location.reload()}
|
||||
className='px-3 py-1.5 rounded-md border'
|
||||
>
|
||||
Recargar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 5) Fallback para formularios
|
||||
*/
|
||||
export function FormFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
return (
|
||||
<div role='alert' className='rounded-md border p-4 bg-red-50'>
|
||||
<h4 className='font-semibold text-red-800'>No se pudo mostrar el formulario</h4>
|
||||
<p className='mt-1 text-sm text-red-700'>{error?.message}</p>
|
||||
<div className='mt-3 flex gap-2'>
|
||||
<Button
|
||||
onClick={resetErrorBoundary}
|
||||
className='px-3 py-1.5 rounded-md bg-red-600 text-white'
|
||||
>
|
||||
Reintentar
|
||||
</Button>
|
||||
<Button onClick={() => history.back()} className='px-3 py-1.5 rounded-md border'>
|
||||
Volver atrás
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 6) Fallback para problemas de red
|
||||
*/
|
||||
export function NetworkFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
const note = navigator.onLine
|
||||
? "La conexión parece estar activa."
|
||||
: "Estás sin conexión. Revisa tu red y reintenta.";
|
||||
|
||||
return (
|
||||
<div role='alert' className='rounded-md border p-4 bg-blue-50 text-blue-900'>
|
||||
<div className='font-medium'>No pudimos obtener los datos</div>
|
||||
<p className='mt-1 text-sm'>{error?.message}</p>
|
||||
<p className='mt-1 text-xs opacity-80'>{note}</p>
|
||||
<div className='mt-3 flex gap-2'>
|
||||
<Button
|
||||
onClick={resetErrorBoundary}
|
||||
className='px-3 py-1.5 rounded-md bg-blue-700 text-white'
|
||||
>
|
||||
Reintentar
|
||||
</Button>
|
||||
<Button onClick={() => window.location.reload()} className='px-3 py-1.5 rounded-md border'>
|
||||
Recargar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 7) Fallback modo desarrollador (detalle de stack)
|
||||
*/
|
||||
export function DevDetailsFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
return (
|
||||
<div role='alert' className='rounded-md border p-4 bg-gray-50'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<p className='font-medium text-gray-900'>Algo falló</p>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
onClick={resetErrorBoundary}
|
||||
className='px-3 py-1.5 rounded-md bg-gray-900 text-white'
|
||||
>
|
||||
Reintentar
|
||||
</Button>
|
||||
<Button onClick={() => setOpen((v) => !v)} className='px-3 py-1.5 rounded-md border'>
|
||||
{open ? "Ocultar detalles" : "Ver detalles"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{open && (
|
||||
<pre className='mt-3 text-xs whitespace-pre-wrap bg-white p-3 rounded-md border overflow-auto'>
|
||||
{error?.stack || error?.message}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 8) Fallback con soporte (link a ayuda)
|
||||
*/
|
||||
export function SupportFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
return (
|
||||
<div role='alert' className='rounded-md border p-4 bg-white'>
|
||||
<h4 className='font-semibold text-gray-900'>No pudimos completar la acción</h4>
|
||||
<p className='mt-1 text-gray-700'>{error?.message}</p>
|
||||
<div className='mt-3 flex flex-wrap gap-2'>
|
||||
<Button
|
||||
onClick={resetErrorBoundary}
|
||||
className='px-3 py-1.5 rounded-md bg-gray-900 text-white'
|
||||
>
|
||||
Intentar de nuevo
|
||||
</Button>
|
||||
<a href='/ayuda' className='px-3 py-1.5 rounded-md border'>
|
||||
Ir a Ayuda
|
||||
</a>
|
||||
<a
|
||||
href='mailto:soporte@tuapp.com?subject=Error%20en%20la%20aplicaci%C3%B3n'
|
||||
className='px-3 py-1.5 rounded-md border'
|
||||
>
|
||||
Contactar soporte
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
export * from "./error-fallbacks";
|
||||
export * from "./slider-demo";
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export * from "./use-theme";
|
||||
export * from "./use-unsaved-changes-notifier";
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./warn-about-change-provider";
|
||||
@ -1,8 +1,7 @@
|
||||
import { ModuleRoutes } from "@/components/module-routes";
|
||||
import { IModuleClient } from "@erp/core/client";
|
||||
import { AppLayout, LoadingOverlay, ScrollToTop } from "@repo/rdx-ui/components";
|
||||
import { JSX, Suspense } from "react";
|
||||
import { Navigate, Route, BrowserRouter as Router, Routes } from "react-router-dom";
|
||||
import { AppLayout } from "@repo/rdx-ui/components";
|
||||
import { Navigate, Route, createBrowserRouter, createRoutesFromElements } from "react-router-dom";
|
||||
import { ErrorPage } from "../pages";
|
||||
import { modules } from "../register-modules"; // Aquí ca
|
||||
|
||||
@ -25,41 +24,36 @@ function groupModulesByLayout(modules: IModuleClient[]) {
|
||||
return groups;
|
||||
}
|
||||
|
||||
export const AppRoutes = (): JSX.Element => {
|
||||
export const getAppRouter = () => {
|
||||
const params = {
|
||||
...import.meta.env,
|
||||
};
|
||||
|
||||
const grouped = groupModulesByLayout(modules);
|
||||
|
||||
console.log(grouped);
|
||||
console.debug(grouped);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<ScrollToTop />
|
||||
return createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route path='/'>
|
||||
{/* Auth Layout */}
|
||||
<Route path='/auth'>
|
||||
<Route index element={<Navigate to='login' />} />
|
||||
<Route path='*' element={<ModuleRoutes modules={grouped.auth} params={params} />} />
|
||||
</Route>
|
||||
|
||||
{/* Fallback Route */}
|
||||
<Suspense fallback={<LoadingOverlay />}>
|
||||
<Routes>
|
||||
{/* Auth Layout */}
|
||||
<Route path='/auth'>
|
||||
<Route index element={<Navigate to='login' />} />
|
||||
<Route path='*' element={<ModuleRoutes modules={grouped.auth} params={params} />} />
|
||||
</Route>
|
||||
{/* App Layout */}
|
||||
<Route element={<AppLayout />}>
|
||||
{/* Dynamic Module Routes */}
|
||||
<Route path='*' element={<ModuleRoutes modules={grouped.app} params={params} />} />
|
||||
|
||||
{/* App Layout */}
|
||||
<Route element={<AppLayout />}>
|
||||
{/* Dynamic Module Routes */}
|
||||
<Route path='*' element={<ModuleRoutes modules={grouped.app} params={params} />} />
|
||||
|
||||
{/* Main Layout */}
|
||||
<Route path='/dashboard' element={<ErrorPage />} />
|
||||
<Route path='/settings' element={<ErrorPage />} />
|
||||
<Route path='/catalog' element={<ErrorPage />} />
|
||||
<Route path='/quotes' element={<ErrorPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Router>
|
||||
{/* Main Layout */}
|
||||
<Route path='/dashboard' element={<ErrorPage />} />
|
||||
<Route path='/settings' element={<ErrorPage />} />
|
||||
<Route path='/catalog' element={<ErrorPage />} />
|
||||
<Route path='/quotes' element={<ErrorPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@ -27,6 +27,6 @@
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"allowUnreachableCode": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src", "../../modules/core/src/web/hooks/use-unsaved-changes-notifier"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"common": {},
|
||||
"common": {
|
||||
"required": "required"
|
||||
},
|
||||
"components": {
|
||||
"taxes_multi_select": {
|
||||
"label": "Taxes",
|
||||
@ -7,5 +9,13 @@
|
||||
"description": "Select the taxes to apply to the invoice items",
|
||||
"invalid_tax_selection": "Invalid tax selection. Please select a valid tax."
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"common": {},
|
||||
"common": {
|
||||
"required": "requerido"
|
||||
},
|
||||
"components": {
|
||||
"taxes_multi_select": {
|
||||
"label": "Impuestos",
|
||||
@ -7,5 +9,13 @@
|
||||
"description": "Seleccionar los impuestos a aplicar a los artículos de la factura",
|
||||
"invalid_tax_selection": "Selección de impuestos no válida. Por favor, seleccione un impuesto válido."
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,3 +2,4 @@ export * from "./use-datasource";
|
||||
export * from "./use-pagination";
|
||||
export * from "./use-query-key";
|
||||
export * from "./use-toggle";
|
||||
export * from "./use-unsaved-changes-notifier";
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./use-unsaved-changes-notifier";
|
||||
export * from "./warn-about-change-provider";
|
||||
@ -1,9 +1,10 @@
|
||||
import { t } from "i18next";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useBlocker } from "react-router-dom";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { useWarnAboutChange } from "./use-warn-about-change";
|
||||
|
||||
export type UnsavedChangesNotifierProps = {
|
||||
isDirty?: boolean;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
confirmText?: string;
|
||||
@ -15,26 +16,37 @@ export type UnsavedChangesNotifierProps = {
|
||||
|
||||
export const useUnsavedChangesNotifier = ({
|
||||
isDirty = false,
|
||||
title = t("hooks.use_unsaved_changes_notifier.title"),
|
||||
subtitle = t("hooks.use_unsaved_changes_notifier.subtitle"),
|
||||
confirmText = t("hooks.use_unsaved_changes_notifier.confirm_text"),
|
||||
cancelText = t("hooks.use_unsaved_changes_notifier.cancel_text"),
|
||||
title,
|
||||
subtitle,
|
||||
confirmText,
|
||||
cancelText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
type = "warning",
|
||||
}: UnsavedChangesNotifierProps & { isDirty?: boolean }) => {
|
||||
}: UnsavedChangesNotifierProps = {}) => {
|
||||
const { t } = useTranslation();
|
||||
const blocker = useBlocker(isDirty);
|
||||
const { show } = useWarnAboutChange();
|
||||
|
||||
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]
|
||||
);
|
||||
|
||||
const confirm = useCallback(() => {
|
||||
if (!isDirty) return Promise.resolve(true);
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
show({
|
||||
title,
|
||||
subtitle,
|
||||
confirmText,
|
||||
cancelText,
|
||||
title: texts.title,
|
||||
subtitle: texts.subtitle,
|
||||
confirmText: texts.confirmText,
|
||||
cancelText: texts.cancelText,
|
||||
type,
|
||||
onConfirm: () => {
|
||||
resolve(true);
|
||||
@ -46,7 +58,7 @@ export const useUnsavedChangesNotifier = ({
|
||||
},
|
||||
});
|
||||
});
|
||||
}, [cancelText, confirmText, isDirty, onCancel, onConfirm, show, subtitle, title, type]);
|
||||
}, [isDirty, show, texts, type, onConfirm, onCancel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (blocker.state === "blocked") {
|
||||
@ -59,13 +71,13 @@ export const useUnsavedChangesNotifier = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (isDirty) {
|
||||
window.onbeforeunload = () => subtitle;
|
||||
window.onbeforeunload = () => texts.subtitle;
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, [isDirty, subtitle]);
|
||||
}, [isDirty, texts.subtitle]);
|
||||
|
||||
return {
|
||||
confirm,
|
||||
@ -1,5 +1,5 @@
|
||||
import { NullOr } from "@repo/rdx-utils";
|
||||
import { createContext } from "react";
|
||||
import type { NullOr } from "../../types";
|
||||
import type { UnsavedChangesNotifierProps } from "./use-unsaved-changes-notifier";
|
||||
|
||||
export interface IUnsavedWarnContextState {
|
||||
@ -1,6 +1,6 @@
|
||||
import { CustomDialog } from "@repo/rdx-ui/components";
|
||||
import { NullOr } from "@repo/rdx-utils";
|
||||
import { type PropsWithChildren, useCallback, useMemo, useState } from "react";
|
||||
import type { NullOr } from "../../types";
|
||||
import type { UnsavedChangesNotifierProps } from "./use-unsaved-changes-notifier";
|
||||
import { UnsavedWarnContext } from "./warn-about-change-context";
|
||||
|
||||
@ -40,12 +40,8 @@ export const createAxiosInstance = ({
|
||||
getAccessToken,
|
||||
onAuthError,
|
||||
}: AxiosFactoryConfig): AxiosInstance => {
|
||||
console.log({ baseURL, getAccessToken, onAuthError });
|
||||
|
||||
const instance = axios.create(defaultAxiosRequestConfig);
|
||||
|
||||
instance.defaults.baseURL = baseURL;
|
||||
|
||||
setupInterceptors(instance, getAccessToken, onAuthError);
|
||||
|
||||
return instance;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { AppBreadcrumb, AppContent } from "@repo/rdx-ui/components";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useBlocker, useNavigate } from "react-router-dom";
|
||||
|
||||
import { useCreateCustomerMutation } from "../../hooks/use-create-customer-mutation";
|
||||
import { useTranslation } from "../../i18n";
|
||||
@ -9,6 +9,7 @@ import { CustomerEditForm } from "./customer-edit-form";
|
||||
export const CustomerCreate = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { block, unblock } = useBlocker(1);
|
||||
|
||||
const { mutate, isPending, isError, error } = useCreateCustomerMutation();
|
||||
|
||||
|
||||
@ -14,6 +14,8 @@ import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
|
||||
import { useUnsavedChangesNotifier } from "@erp/core/hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerData, CustomerDataFormSchema } from "./customer.schema";
|
||||
|
||||
@ -45,6 +47,11 @@ export const CustomerEditForm = ({
|
||||
const form = useForm<CustomerData>({
|
||||
resolver: zodResolver(CustomerDataFormSchema),
|
||||
defaultValues: initialData,
|
||||
disabled: isPending,
|
||||
});
|
||||
|
||||
useUnsavedChangesNotifier({
|
||||
isDirty: form.formState.isDirty,
|
||||
});
|
||||
|
||||
const handleSubmit = (data: CustomerData) => {
|
||||
|
||||
@ -170,7 +170,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))(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)
|
||||
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)
|
||||
tsconfig-paths:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@ -228,6 +228,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^19.1.0
|
||||
version: 19.1.0(react@19.1.0)
|
||||
react-error-boundary:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(react@19.1.0)
|
||||
react-hook-form:
|
||||
specifier: ^7.56.4
|
||||
version: 7.58.1(react@19.1.0)
|
||||
@ -5335,6 +5338,11 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^19.1.0
|
||||
|
||||
react-error-boundary@6.0.0:
|
||||
resolution: {integrity: sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==}
|
||||
peerDependencies:
|
||||
react: '>=16.13.1'
|
||||
|
||||
react-hook-form@7.58.1:
|
||||
resolution: {integrity: sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@ -11044,6 +11052,11 @@ snapshots:
|
||||
react: 19.1.0
|
||||
scheduler: 0.26.0
|
||||
|
||||
react-error-boundary@6.0.0(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
react: 19.1.0
|
||||
|
||||
react-hook-form@7.58.1(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@ -11655,7 +11668,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))(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):
|
||||
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):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
ejs: 3.1.10
|
||||
@ -11673,7 +11686,6 @@ 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