270 lines
13 KiB
TypeScript
270 lines
13 KiB
TypeScript
import { Switch, Route } from "wouter";
|
||
import { queryClient } from "./lib/queryClient";
|
||
import { QueryClientProvider } from "@tanstack/react-query";
|
||
import { Toaster } from "@/components/ui/toaster";
|
||
import { ToastAction } from "@/components/ui/toast";
|
||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||
import { useAuthProvider, AuthContext } from "@/hooks/useAuth";
|
||
import { ThemeProvider } from "@/themes/ThemeProvider";
|
||
import { OfflineSyncProvider } from "@/contexts/OfflineSyncContext";
|
||
import { useEffect, lazy, Suspense, useCallback, type ComponentType, type LazyExoticComponent } from "react";
|
||
import { initializePushNotifications } from "@/lib/push-notifications";
|
||
import { initPWA, initWebPush } from "@/lib/pwa";
|
||
import { useToast } from "@/hooks/use-toast";
|
||
import { usePrefetchOfflineTasks } from "@/hooks/usePrefetchOfflineTasks";
|
||
import { useEvents } from "@/hooks/useEvents";
|
||
import { usePwaFontSize } from "@/hooks/usePwaFontSize";
|
||
import { GlobalErrorBoundary } from "@/components/GlobalErrorBoundary";
|
||
import { Loader2 } from "lucide-react";
|
||
|
||
type LazyModule<T = any> = { default: ComponentType<T> };
|
||
|
||
function safeLazy<T>(factory: () => Promise<LazyModule<T>>): LazyExoticComponent<ComponentType<T>> {
|
||
return lazy(() =>
|
||
factory().catch((error) => {
|
||
console.error('[safeLazy] chunk load failed, reloading page', error);
|
||
window.location.reload();
|
||
return { default: (() => null) as unknown as ComponentType<T> };
|
||
})
|
||
);
|
||
}
|
||
|
||
const Login = safeLazy(() => import("@/pages/Login"));
|
||
const Register = safeLazy(() => import("@/pages/Register"));
|
||
const InviteRegister = safeLazy(() => import("@/pages/InviteRegister"));
|
||
const UsersPage = safeLazy(() => import("@/pages/Users"));
|
||
const UserProfilePage = safeLazy(() => import("@/pages/UserProfilePage"));
|
||
const UserFieldsEditor = safeLazy(() => import("@/pages/UserFieldsEditor"));
|
||
const Forms = safeLazy(() => import("@/pages/Forms"));
|
||
const FormEditor = safeLazy(() => import("@/pages/FormEditor"));
|
||
const Tables = safeLazy(() => import("@/pages/Tables"));
|
||
const TableEditor = safeLazy(() => import("@/pages/TableEditor"));
|
||
const TaskView = safeLazy(() => import("@/pages/TaskView"));
|
||
const TaskDetail = safeLazy(() => import("@/pages/TaskDetail"));
|
||
const Settings = safeLazy(() => import("@/pages/Settings"));
|
||
const ProfileSettings = safeLazy(() => import("@/pages/ProfileSettings"));
|
||
const Reports = safeLazy(() => import("@/pages/Reports"));
|
||
const Projects = safeLazy(() => import("@/pages/Projects"));
|
||
const Bots = safeLazy(() => import("@/pages/Bots"));
|
||
const FieldTemplates = safeLazy(() => import("@/pages/GlobalFields"));
|
||
const CustomPageView = safeLazy(() => import("@/pages/CustomPageView"));
|
||
const PrivacyPolicy = safeLazy(() => import("@/pages/PrivacyPolicy"));
|
||
const ForgotPassword = safeLazy(() => import("@/pages/ForgotPassword"));
|
||
const ResetPassword = safeLazy(() => import("@/pages/ResetPassword"));
|
||
const SuperAdminLogin = safeLazy(() => import("@/pages/SuperAdminLogin"));
|
||
const SuperAdminForgotPassword = safeLazy(() => import("@/pages/SuperAdminForgotPassword"));
|
||
const SuperAdminResetPassword = safeLazy(() => import("@/pages/SuperAdminResetPassword"));
|
||
const SuperAdminDashboard = safeLazy(() => import("@/pages/SuperAdminDashboard"));
|
||
const SuperAdminOrgDetail = safeLazy(() => import("@/pages/SuperAdminOrgDetail"));
|
||
const Billing = safeLazy(() => import("@/pages/Billing"));
|
||
const FinanceDashboard = safeLazy(() => import("@/pages/finance/dashboard"));
|
||
const IncomeOverview = safeLazy(() => import("@/pages/finance/income-overview"));
|
||
const ExpensesOverview = safeLazy(() => import("@/pages/finance/expenses-overview"));
|
||
const IncomeDetail = safeLazy(() => import("@/pages/finance/income"));
|
||
const ExpensesDetail = safeLazy(() => import("@/pages/finance/expenses"));
|
||
const IncomeCategories = safeLazy(() => import("@/pages/finance/income-categories"));
|
||
const ExpenseCategories = safeLazy(() => import("@/pages/finance/expense-categories"));
|
||
|
||
const FinanceSettingsPage = safeLazy(() => import("@/pages/finance/settings"));
|
||
const DocumentsPage = safeLazy(() => import("@/pages/documents/DocumentsPage").then(m => ({ default: m.DocumentsPage })));
|
||
const TemplateEditorPage = safeLazy(() => import("@/pages/documents/TemplateEditorPage").then(m => ({ default: m.TemplateEditorPage })));
|
||
const GenerationHistoryPage = safeLazy(() => import("@/pages/documents/GenerationHistoryPage").then(m => ({ default: m.GenerationHistoryPage })));
|
||
const AssetsPage = safeLazy(() => import("@/pages/documents/AssetsPage").then(m => ({ default: m.AssetsPage })));
|
||
const PackagesPage = safeLazy(() => import("@/pages/documents/PackagesPage").then(m => ({ default: m.PackagesPage })));
|
||
const SalaryPage = safeLazy(() => import("@/pages/salary"));
|
||
const ChatPage = safeLazy(() => import("@/pages/Chat"));
|
||
const HomePage = safeLazy(() => import("@/pages/Home"));
|
||
const DashboardPage = safeLazy(() => import("@/pages/DashboardPage"));
|
||
const CalendarPage = safeLazy(() => import("@/pages/CalendarPage"));
|
||
const GanttPage = safeLazy(() => import("@/pages/GanttPage"));
|
||
const MedSchedulePage = safeLazy(() => import("@/pages/medschedule"));
|
||
const ShareTarget = safeLazy(() => import("@/pages/ShareTarget"));
|
||
const QrHandler = safeLazy(() => import("@/pages/QrHandler"));
|
||
const NotFound = safeLazy(() => import("@/pages/not-found"));
|
||
|
||
function PageLoader() {
|
||
return (
|
||
<div className="flex items-center justify-center min-h-screen">
|
||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Router() {
|
||
return (
|
||
<Suspense fallback={<PageLoader />}>
|
||
<Switch>
|
||
<Route path="/" component={Login} />
|
||
<Route path="/login" component={Login} />
|
||
<Route path="/register" component={Register} />
|
||
<Route path="/invite/:token" component={InviteRegister} />
|
||
<Route path="/users" component={UsersPage} />
|
||
<Route path="/users/fields/edit" component={UserFieldsEditor} />
|
||
<Route path="/users/:id" component={UserProfilePage} />
|
||
<Route path="/forms" component={Forms} />
|
||
<Route path="/forms/:id/edit" component={FormEditor} />
|
||
<Route path="/directories" component={Tables} />
|
||
<Route path="/directories/:id" component={TableEditor} />
|
||
<Route path="/tasks/:taskId" component={TaskDetail} />
|
||
<Route path="/forms/:id/tasks" component={TaskView} />
|
||
<Route path="/forms/:id/tasks/:taskId" component={TaskDetail} />
|
||
|
||
{/* Organization-specific task routes */}
|
||
<Route path="/:org/tasks/:taskId" component={TaskDetail} />
|
||
<Route path="/:org/forms/:id/tasks" component={TaskView} />
|
||
<Route path="/:org/forms/:id/tasks/:taskId" component={TaskDetail} />
|
||
|
||
<Route path="/projects" component={Projects} />
|
||
<Route path="/reports" component={Reports} />
|
||
<Route path="/settings" component={Settings} />
|
||
<Route path="/profile" component={ProfileSettings} />
|
||
<Route path="/bots" component={Bots} />
|
||
<Route path="/field-templates" component={FieldTemplates} />
|
||
<Route path="/global-fields">
|
||
{() => { window.location.href = '/field-templates'; return null; }}
|
||
</Route>
|
||
<Route path="/pages/:slug" component={CustomPageView} />
|
||
<Route path="/home" component={HomePage} />
|
||
<Route path="/dashboard" component={DashboardPage} />
|
||
<Route path="/calendar" component={CalendarPage} />
|
||
<Route path="/gantt" component={GanttPage} />
|
||
<Route path="/med" component={MedSchedulePage} />
|
||
<Route path="/med/:tab" component={MedSchedulePage} />
|
||
<Route path="/billing" component={Billing} />
|
||
<Route path="/finance" component={FinanceDashboard} />
|
||
<Route path="/income" component={IncomeOverview} />
|
||
<Route path="/income/uncategorized" component={IncomeDetail} />
|
||
<Route path="/income/categories" component={IncomeCategories} />
|
||
<Route path="/income/:categoryId" component={IncomeDetail} />
|
||
<Route path="/expenses" component={ExpensesOverview} />
|
||
<Route path="/expenses/uncategorized" component={ExpensesDetail} />
|
||
<Route path="/expenses/categories" component={ExpenseCategories} />
|
||
<Route path="/expenses/:categoryId" component={ExpensesDetail} />
|
||
|
||
<Route path="/finance/settings" component={FinanceSettingsPage} />
|
||
<Route path="/documents" component={DocumentsPage} />
|
||
<Route path="/documents/templates/new" component={TemplateEditorPage} />
|
||
<Route path="/documents/templates/:id/edit" component={TemplateEditorPage} />
|
||
<Route path="/documents/history" component={GenerationHistoryPage} />
|
||
<Route path="/documents/assets" component={AssetsPage} />
|
||
<Route path="/documents/packages" component={PackagesPage} />
|
||
<Route path="/salary" component={SalaryPage} />
|
||
<Route path="/chat/:conversationId" component={ChatPage} />
|
||
<Route path="/chat" component={ChatPage} />
|
||
<Route path="/share-target" component={ShareTarget} />
|
||
<Route path="/qr/:templateKey/:objectId?" component={QrHandler} />
|
||
<Route path="/forgot-password" component={ForgotPassword} />
|
||
<Route path="/reset-password" component={ResetPassword} />
|
||
<Route path="/privacy-policy" component={PrivacyPolicy} />
|
||
<Route path="/superadmin/login" component={SuperAdminLogin} />
|
||
<Route path="/superadmin/forgot-password" component={SuperAdminForgotPassword} />
|
||
<Route path="/superadmin/reset-password" component={SuperAdminResetPassword} />
|
||
<Route path="/superadmin/organizations/:id" component={SuperAdminOrgDetail} />
|
||
<Route path="/superadmin" component={SuperAdminDashboard} />
|
||
<Route component={NotFound} />
|
||
</Switch>
|
||
</Suspense>
|
||
);
|
||
}
|
||
|
||
function PrefetchWrapper({ children }: { children: React.ReactNode }) {
|
||
usePrefetchOfflineTasks();
|
||
return <>{children}</>;
|
||
}
|
||
|
||
function GlobalEventListener() {
|
||
useEvents({
|
||
onNotification: useCallback(async () => {
|
||
queryClient.invalidateQueries({ queryKey: ['/api/notifications'] });
|
||
await Promise.all([
|
||
queryClient.refetchQueries({ queryKey: ['home'] }),
|
||
queryClient.refetchQueries({ queryKey: ['/api/home/inbox'] }),
|
||
queryClient.refetchQueries({ queryKey: ['/api/home/inbox/count'] }),
|
||
]);
|
||
}, []),
|
||
onTaskUpdated: useCallback(async () => {
|
||
await Promise.all([
|
||
queryClient.refetchQueries({ queryKey: ['home'] }),
|
||
queryClient.refetchQueries({ queryKey: ['/api/home/inbox'] }),
|
||
queryClient.refetchQueries({ queryKey: ['/api/home/inbox/count'] }),
|
||
]);
|
||
}, []),
|
||
});
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Displays in-app toast notifications when a push arrives while the app tab
|
||
* is active. The service worker forwards these instead of showing a system
|
||
* notification so the user isn't interrupted with an OS-level alert.
|
||
*/
|
||
function ForegroundPushHandler() {
|
||
const { toast } = useToast();
|
||
|
||
useEffect(() => {
|
||
if (!('serviceWorker' in navigator)) return;
|
||
|
||
const handler = (event: MessageEvent) => {
|
||
if (event.data?.type !== 'FOREGROUND_NOTIFICATION') return;
|
||
const payload = event.data.payload || {};
|
||
const isChat = payload.data?.type === 'chat' || payload.data?.type === 'messenger';
|
||
const url = payload.url || payload.data?.url || '/';
|
||
|
||
toast({
|
||
title: payload.title || 'Уведомление',
|
||
description: payload.body || payload.message || '',
|
||
duration: 6000,
|
||
action: isChat ? (
|
||
<ToastAction altText="Ответить" onClick={() => { window.location.href = url; }}>
|
||
Ответить
|
||
</ToastAction>
|
||
) : undefined,
|
||
});
|
||
};
|
||
|
||
navigator.serviceWorker.addEventListener('message', handler);
|
||
return () => navigator.serviceWorker.removeEventListener('message', handler);
|
||
}, [toast]);
|
||
|
||
return null;
|
||
}
|
||
|
||
function App() {
|
||
const auth = useAuthProvider();
|
||
usePwaFontSize(auth.user?.id);
|
||
|
||
useEffect(() => {
|
||
initPWA();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (auth.isAuthenticated) {
|
||
initializePushNotifications().catch(console.error);
|
||
initWebPush().catch(console.error);
|
||
}
|
||
}, [auth.isAuthenticated]);
|
||
|
||
return (
|
||
<QueryClientProvider client={queryClient}>
|
||
<AuthContext.Provider value={auth}>
|
||
<ThemeProvider>
|
||
<OfflineSyncProvider>
|
||
<PrefetchWrapper>
|
||
<TooltipProvider>
|
||
<Toaster />
|
||
<GlobalEventListener />
|
||
<ForegroundPushHandler />
|
||
<GlobalErrorBoundary>
|
||
<Router />
|
||
</GlobalErrorBoundary>
|
||
</TooltipProvider>
|
||
</PrefetchWrapper>
|
||
</OfflineSyncProvider>
|
||
</ThemeProvider>
|
||
</AuthContext.Provider>
|
||
</QueryClientProvider>
|
||
);
|
||
}
|
||
|
||
export default App;
|