Facturas de cliente y clientes
This commit is contained in:
parent
c260f64007
commit
5b7ee437ff
@ -38,6 +38,7 @@
|
|||||||
"i18next-browser-languagedetector": "^8.1.0",
|
"i18next-browser-languagedetector": "^8.1.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hook-form": "^7.56.4",
|
"react-hook-form": "^7.56.4",
|
||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^15.0.1",
|
||||||
"react-router-dom": "^6.26.0",
|
"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 { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
|
|
||||||
import { UnsavedWarnProvider } from "@/lib/hooks";
|
|
||||||
import { i18n } from "@/locales";
|
import { i18n } from "@/locales";
|
||||||
|
|
||||||
import { AuthProvider, createAuthService } from "@erp/auth/client";
|
import { AuthProvider, createAuthService } from "@erp/auth/client";
|
||||||
import { createAxiosDataSource, createAxiosInstance } from "@erp/core/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 DineroFactory from "dinero.js";
|
||||||
import "./app.css";
|
import { RouterProvider } from "react-router-dom";
|
||||||
import { clearAccessToken, getAccessToken, setAccessToken } from "./lib";
|
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 = () => {
|
export const App = () => {
|
||||||
DineroFactory.globalLocale = "es-ES";
|
DineroFactory.globalLocale = "es-ES";
|
||||||
@ -37,6 +40,7 @@ export const App = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const dataSource = createAxiosDataSource(axiosInstance);
|
const dataSource = createAxiosDataSource(axiosInstance);
|
||||||
|
const appRouter = getAppRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
@ -52,7 +56,10 @@ export const App = () => {
|
|||||||
>
|
>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<UnsavedWarnProvider>
|
<UnsavedWarnProvider>
|
||||||
<AppRoutes />
|
{/* Fallback Route */}
|
||||||
|
<Suspense fallback={<LoadingOverlay />}>
|
||||||
|
<RouterProvider router={appRouter} />
|
||||||
|
</Suspense>
|
||||||
</UnsavedWarnProvider>
|
</UnsavedWarnProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<Toaster />
|
<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";
|
export * from "./slider-demo";
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export * from "./use-theme";
|
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 { ModuleRoutes } from "@/components/module-routes";
|
||||||
import { IModuleClient } from "@erp/core/client";
|
import { IModuleClient } from "@erp/core/client";
|
||||||
import { AppLayout, LoadingOverlay, ScrollToTop } from "@repo/rdx-ui/components";
|
import { AppLayout } from "@repo/rdx-ui/components";
|
||||||
import { JSX, Suspense } from "react";
|
import { Navigate, Route, createBrowserRouter, createRoutesFromElements } from "react-router-dom";
|
||||||
import { Navigate, Route, BrowserRouter as Router, Routes } from "react-router-dom";
|
|
||||||
import { ErrorPage } from "../pages";
|
import { ErrorPage } from "../pages";
|
||||||
import { modules } from "../register-modules"; // Aquí ca
|
import { modules } from "../register-modules"; // Aquí ca
|
||||||
|
|
||||||
@ -25,41 +24,36 @@ function groupModulesByLayout(modules: IModuleClient[]) {
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppRoutes = (): JSX.Element => {
|
export const getAppRouter = () => {
|
||||||
const params = {
|
const params = {
|
||||||
...import.meta.env,
|
...import.meta.env,
|
||||||
};
|
};
|
||||||
|
|
||||||
const grouped = groupModulesByLayout(modules);
|
const grouped = groupModulesByLayout(modules);
|
||||||
|
|
||||||
console.log(grouped);
|
console.debug(grouped);
|
||||||
|
|
||||||
return (
|
return createBrowserRouter(
|
||||||
<Router>
|
createRoutesFromElements(
|
||||||
<ScrollToTop />
|
<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 */}
|
{/* App Layout */}
|
||||||
<Suspense fallback={<LoadingOverlay />}>
|
<Route element={<AppLayout />}>
|
||||||
<Routes>
|
{/* Dynamic Module Routes */}
|
||||||
{/* Auth Layout */}
|
<Route path='*' element={<ModuleRoutes modules={grouped.app} params={params} />} />
|
||||||
<Route path='/auth'>
|
|
||||||
<Route index element={<Navigate to='login' />} />
|
|
||||||
<Route path='*' element={<ModuleRoutes modules={grouped.auth} params={params} />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* App Layout */}
|
{/* Main Layout */}
|
||||||
<Route element={<AppLayout />}>
|
<Route path='/dashboard' element={<ErrorPage />} />
|
||||||
{/* Dynamic Module Routes */}
|
<Route path='/settings' element={<ErrorPage />} />
|
||||||
<Route path='*' element={<ModuleRoutes modules={grouped.app} params={params} />} />
|
<Route path='/catalog' element={<ErrorPage />} />
|
||||||
|
<Route path='/quotes' element={<ErrorPage />} />
|
||||||
{/* Main Layout */}
|
</Route>
|
||||||
<Route path='/dashboard' element={<ErrorPage />} />
|
</Route>
|
||||||
<Route path='/settings' element={<ErrorPage />} />
|
)
|
||||||
<Route path='/catalog' element={<ErrorPage />} />
|
|
||||||
<Route path='/quotes' element={<ErrorPage />} />
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
</Suspense>
|
|
||||||
</Router>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -27,6 +27,6 @@
|
|||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
"allowUnreachableCode": true
|
"allowUnreachableCode": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src", "../../modules/core/src/web/hooks/use-unsaved-changes-notifier"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"common": {},
|
"common": {
|
||||||
|
"required": "required"
|
||||||
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"taxes_multi_select": {
|
"taxes_multi_select": {
|
||||||
"label": "Taxes",
|
"label": "Taxes",
|
||||||
@ -7,5 +9,13 @@
|
|||||||
"description": "Select the taxes to apply to the invoice items",
|
"description": "Select the taxes to apply to the invoice items",
|
||||||
"invalid_tax_selection": "Invalid tax selection. Please select a valid tax."
|
"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": {
|
"components": {
|
||||||
"taxes_multi_select": {
|
"taxes_multi_select": {
|
||||||
"label": "Impuestos",
|
"label": "Impuestos",
|
||||||
@ -7,5 +9,13 @@
|
|||||||
"description": "Seleccionar los impuestos a aplicar a los artículos de la factura",
|
"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."
|
"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-pagination";
|
||||||
export * from "./use-query-key";
|
export * from "./use-query-key";
|
||||||
export * from "./use-toggle";
|
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, useMemo } from "react";
|
||||||
import { useCallback, useEffect } from "react";
|
|
||||||
import { useBlocker } from "react-router-dom";
|
import { useBlocker } from "react-router-dom";
|
||||||
|
import { useTranslation } from "../../i18n";
|
||||||
import { useWarnAboutChange } from "./use-warn-about-change";
|
import { useWarnAboutChange } from "./use-warn-about-change";
|
||||||
|
|
||||||
export type UnsavedChangesNotifierProps = {
|
export type UnsavedChangesNotifierProps = {
|
||||||
|
isDirty?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
@ -15,26 +16,37 @@ export type UnsavedChangesNotifierProps = {
|
|||||||
|
|
||||||
export const useUnsavedChangesNotifier = ({
|
export const useUnsavedChangesNotifier = ({
|
||||||
isDirty = false,
|
isDirty = false,
|
||||||
title = t("hooks.use_unsaved_changes_notifier.title"),
|
title,
|
||||||
subtitle = t("hooks.use_unsaved_changes_notifier.subtitle"),
|
subtitle,
|
||||||
confirmText = t("hooks.use_unsaved_changes_notifier.confirm_text"),
|
confirmText,
|
||||||
cancelText = t("hooks.use_unsaved_changes_notifier.cancel_text"),
|
cancelText,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel,
|
onCancel,
|
||||||
type = "warning",
|
type = "warning",
|
||||||
}: UnsavedChangesNotifierProps & { isDirty?: boolean }) => {
|
}: UnsavedChangesNotifierProps = {}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const blocker = useBlocker(isDirty);
|
const blocker = useBlocker(isDirty);
|
||||||
const { show } = useWarnAboutChange();
|
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(() => {
|
const confirm = useCallback(() => {
|
||||||
if (!isDirty) return Promise.resolve(true);
|
if (!isDirty) return Promise.resolve(true);
|
||||||
|
|
||||||
return new Promise<boolean>((resolve) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
show({
|
show({
|
||||||
title,
|
title: texts.title,
|
||||||
subtitle,
|
subtitle: texts.subtitle,
|
||||||
confirmText,
|
confirmText: texts.confirmText,
|
||||||
cancelText,
|
cancelText: texts.cancelText,
|
||||||
type,
|
type,
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
resolve(true);
|
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(() => {
|
useEffect(() => {
|
||||||
if (blocker.state === "blocked") {
|
if (blocker.state === "blocked") {
|
||||||
@ -59,13 +71,13 @@ export const useUnsavedChangesNotifier = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDirty) {
|
if (isDirty) {
|
||||||
window.onbeforeunload = () => subtitle;
|
window.onbeforeunload = () => texts.subtitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.onbeforeunload = null;
|
window.onbeforeunload = null;
|
||||||
};
|
};
|
||||||
}, [isDirty, subtitle]);
|
}, [isDirty, texts.subtitle]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
confirm,
|
confirm,
|
||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { NullOr } from "@repo/rdx-utils";
|
||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
import type { NullOr } from "../../types";
|
|
||||||
import type { UnsavedChangesNotifierProps } from "./use-unsaved-changes-notifier";
|
import type { UnsavedChangesNotifierProps } from "./use-unsaved-changes-notifier";
|
||||||
|
|
||||||
export interface IUnsavedWarnContextState {
|
export interface IUnsavedWarnContextState {
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { CustomDialog } from "@repo/rdx-ui/components";
|
import { CustomDialog } from "@repo/rdx-ui/components";
|
||||||
|
import { NullOr } from "@repo/rdx-utils";
|
||||||
import { type PropsWithChildren, useCallback, useMemo, useState } from "react";
|
import { type PropsWithChildren, useCallback, useMemo, useState } from "react";
|
||||||
import type { NullOr } from "../../types";
|
|
||||||
import type { UnsavedChangesNotifierProps } from "./use-unsaved-changes-notifier";
|
import type { UnsavedChangesNotifierProps } from "./use-unsaved-changes-notifier";
|
||||||
import { UnsavedWarnContext } from "./warn-about-change-context";
|
import { UnsavedWarnContext } from "./warn-about-change-context";
|
||||||
|
|
||||||
@ -40,12 +40,8 @@ export const createAxiosInstance = ({
|
|||||||
getAccessToken,
|
getAccessToken,
|
||||||
onAuthError,
|
onAuthError,
|
||||||
}: AxiosFactoryConfig): AxiosInstance => {
|
}: AxiosFactoryConfig): AxiosInstance => {
|
||||||
console.log({ baseURL, getAccessToken, onAuthError });
|
|
||||||
|
|
||||||
const instance = axios.create(defaultAxiosRequestConfig);
|
const instance = axios.create(defaultAxiosRequestConfig);
|
||||||
|
|
||||||
instance.defaults.baseURL = baseURL;
|
instance.defaults.baseURL = baseURL;
|
||||||
|
|
||||||
setupInterceptors(instance, getAccessToken, onAuthError);
|
setupInterceptors(instance, getAccessToken, onAuthError);
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
|||||||
@ -1,6 +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 { useNavigate } from "react-router-dom";
|
import { useBlocker, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useCreateCustomerMutation } from "../../hooks/use-create-customer-mutation";
|
import { useCreateCustomerMutation } from "../../hooks/use-create-customer-mutation";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
@ -9,6 +9,7 @@ import { CustomerEditForm } from "./customer-edit-form";
|
|||||||
export const CustomerCreate = () => {
|
export const CustomerCreate = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { block, unblock } = useBlocker(1);
|
||||||
|
|
||||||
const { mutate, isPending, isError, error } = useCreateCustomerMutation();
|
const { mutate, isPending, isError, error } = useCreateCustomerMutation();
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import {
|
|||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioGroupItem,
|
RadioGroupItem,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
|
import { useUnsavedChangesNotifier } from "@erp/core/hooks";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { CustomerData, CustomerDataFormSchema } from "./customer.schema";
|
import { CustomerData, CustomerDataFormSchema } from "./customer.schema";
|
||||||
|
|
||||||
@ -45,6 +47,11 @@ export const CustomerEditForm = ({
|
|||||||
const form = useForm<CustomerData>({
|
const form = useForm<CustomerData>({
|
||||||
resolver: zodResolver(CustomerDataFormSchema),
|
resolver: zodResolver(CustomerDataFormSchema),
|
||||||
defaultValues: initialData,
|
defaultValues: initialData,
|
||||||
|
disabled: isPending,
|
||||||
|
});
|
||||||
|
|
||||||
|
useUnsavedChangesNotifier({
|
||||||
|
isDirty: form.formState.isDirty,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (data: CustomerData) => {
|
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))
|
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:
|
ts-jest:
|
||||||
specifier: ^29.2.5
|
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:
|
tsconfig-paths:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
@ -228,6 +228,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.1.0(react@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:
|
react-hook-form:
|
||||||
specifier: ^7.56.4
|
specifier: ^7.56.4
|
||||||
version: 7.58.1(react@19.1.0)
|
version: 7.58.1(react@19.1.0)
|
||||||
@ -5335,6 +5338,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.1.0
|
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:
|
react-hook-form@7.58.1:
|
||||||
resolution: {integrity: sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==}
|
resolution: {integrity: sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@ -11044,6 +11052,11 @@ snapshots:
|
|||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
scheduler: 0.26.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):
|
react-hook-form@7.58.1(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@ -11655,7 +11668,7 @@ snapshots:
|
|||||||
|
|
||||||
ts-interface-checker@0.1.13: {}
|
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:
|
dependencies:
|
||||||
bs-logger: 0.2.6
|
bs-logger: 0.2.6
|
||||||
ejs: 3.1.10
|
ejs: 3.1.10
|
||||||
@ -11673,7 +11686,6 @@ snapshots:
|
|||||||
'@jest/transform': 29.7.0
|
'@jest/transform': 29.7.0
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
babel-jest: 29.7.0(@babel/core@7.27.4)
|
babel-jest: 29.7.0(@babel/core@7.27.4)
|
||||||
esbuild: 0.25.5
|
|
||||||
jest-util: 29.7.0
|
jest-util: 29.7.0
|
||||||
|
|
||||||
ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3):
|
ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user