Files
iistwin/client/src/App.tsx

270 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;