91045a1897
- Rename "Project" → "Client" across UI (labels + /projects→/clients routes with redirect); normalize company-meaning labels to "Company" - Finance tabs: merge dual cards into one per tab, add header column filters (expenses/subs/invoices), overview as 3-section card, move +Expense/+Invoice to card right edge - Tasks: responsive mobile stats grid, progressive column hiding (min Status+Name+Assigned), sidebar mobile footer (profile/theme/signout) - Hide Company column for single-company users; +Task shows disabled Company field instead of hiding - Sticky table headers site-wide with opaque card background - Pin dev server to port 5173 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
167 lines
9.3 KiB
React
167 lines
9.3 KiB
React
import { lazy, Suspense, Component } from 'react';
|
|
import { BrowserRouter, Routes, Route, Navigate, useParams } from 'react-router-dom';
|
|
import { AuthProvider, useAuth } from './context/AuthContext';
|
|
import ProtectedRoute from './components/ProtectedRoute';
|
|
import PageLoader from './components/PageLoader';
|
|
|
|
class ChunkErrorBoundary extends Component {
|
|
state = { error: null };
|
|
static getDerivedStateFromError(error) { return { error }; }
|
|
componentDidCatch(error) {
|
|
const isChunkError = error?.name === 'ChunkLoadError'
|
|
|| error?.message?.includes('dynamically imported module')
|
|
|| error?.message?.includes('Failed to fetch');
|
|
if (isChunkError) {
|
|
const key = 'chunk_reload_attempted';
|
|
if (!sessionStorage.getItem(key)) {
|
|
sessionStorage.setItem(key, '1');
|
|
window.location.reload();
|
|
return;
|
|
}
|
|
sessionStorage.removeItem(key);
|
|
}
|
|
}
|
|
render() {
|
|
if (this.state.error) {
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '60vh', gap: 16 }}>
|
|
<div style={{ fontSize: 14, color: 'var(--text-secondary)' }}>Page failed to load.</div>
|
|
<button className="btn btn-primary" onClick={() => { sessionStorage.removeItem('chunk_reload_attempted'); this.setState({ error: null }); window.location.reload(); }}>
|
|
Reload
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
import Login from './pages/Login';
|
|
import PayInvoice from './pages/PayInvoice';
|
|
|
|
const ProfilePage = lazy(() => import('./pages/Profile'));
|
|
const CompaniesPage = lazy(() => import('./pages/Companies'));
|
|
const CompanyDetail = lazy(() => import('./pages/CompanyDetail'));
|
|
const TeamInvoices = lazy(() => import('./pages/team/TeamInvoices'));
|
|
const TaskDetail = lazy(() => import('./pages/TaskDetail'));
|
|
const TeamCreateSubcontractorPO = lazy(() => import('./pages/team/TeamCreateSubcontractorPO'));
|
|
const TeamInvoiceDetail = lazy(() => import('./pages/team/TeamInvoiceDetail'));
|
|
const TeamSubcontractorPODetail = lazy(() => import('./pages/team/TeamSubcontractorPODetail'));
|
|
const TeamSubInvoiceDetail = lazy(() => import('./pages/team/TeamSubInvoiceDetail'));
|
|
const SurveyMaker = lazy(() => import('./pages/SurveyMaker'));
|
|
const BrandBook = lazy(() => import('./pages/BrandBook'));
|
|
const Converters = lazy(() => import('./pages/Converters'));
|
|
const TeamFourgePasswords = lazy(() => import('./pages/team/TeamFourgePasswords'));
|
|
const TeamReports = lazy(() => import('./pages/team/TeamReports'));
|
|
const ExternalMyPurchaseOrders = lazy(() => import('./pages/external/ExternalMyPurchaseOrders'));
|
|
const ExternalMyInvoices = lazy(() => import('./pages/external/ExternalMyInvoices'));
|
|
const ExternalMyInvoiceDetail = lazy(() => import('./pages/external/ExternalMyInvoiceDetail'));
|
|
const ExternalMyInvoiceCreate = lazy(() => import('./pages/external/ExternalMyInvoiceCreate'));
|
|
const ProjectDetailPage = lazy(() => import('./pages/ProjectDetail'));
|
|
const TeamDashboard = lazy(() => import('./pages/team/TeamDashboard'));
|
|
const RequestsPage = lazy(() => import('./pages/Tasks'));
|
|
const ClientMyInvoices = lazy(() => import('./pages/client/ClientMyInvoices'));
|
|
|
|
function RedirectProjectDetail() {
|
|
const { id } = useParams();
|
|
return <Navigate to={`/clients/${id}`} replace />;
|
|
}
|
|
|
|
function RedirectClientDetail() {
|
|
const { id } = useParams();
|
|
return <Navigate to={`/clients/${id}`} replace />;
|
|
}
|
|
|
|
function RedirectToTask() {
|
|
const { id } = useParams();
|
|
return <Navigate to={`/tasks/${id}`} replace />;
|
|
}
|
|
|
|
function RedirectTeamInvoiceDetail() {
|
|
const { id } = useParams();
|
|
return <Navigate to={`/finances/${id}`} replace />;
|
|
}
|
|
|
|
function RedirectSubsInvoiceDetail() {
|
|
const { id } = useParams();
|
|
return <Navigate to={`/subs-invoices/${id}`} replace />;
|
|
}
|
|
|
|
function NavigateCompanyDetail() {
|
|
const { id } = useParams();
|
|
return <Navigate to={`/company/${id}`} replace />;
|
|
}
|
|
|
|
|
|
|
|
export default function App() {
|
|
return (
|
|
<AuthProvider>
|
|
<BrowserRouter>
|
|
<ChunkErrorBoundary>
|
|
<Suspense fallback={<PageLoader />}>
|
|
<Routes>
|
|
<Route path="/" element={<Login />} />
|
|
|
|
<Route path="/dashboard" element={<ProtectedRoute role={['team', 'external', 'client']}><TeamDashboard /></ProtectedRoute>} />
|
|
<Route path="/clients/:id" element={<ProtectedRoute role={['team', 'external', 'client']}><ProjectDetailPage /></ProtectedRoute>} />
|
|
<Route path="/projects/:id" element={<RedirectClientDetail />} />
|
|
<Route path="/tasks/:id" element={<ProtectedRoute role={['team', 'external', 'client']}><TaskDetail /></ProtectedRoute>} />
|
|
<Route path="/company" element={<ProtectedRoute role={['team', 'client']}><CompaniesPage /></ProtectedRoute>} />
|
|
<Route path="/company/:id" element={<ProtectedRoute role={['team', 'client']}><CompanyDetail /></ProtectedRoute>} />
|
|
<Route path="/companies" element={<Navigate to="/company" replace />} />
|
|
<Route path="/companies/:id" element={<NavigateCompanyDetail />} />
|
|
<Route path="/team/tasks" element={<Navigate to="/tasks" replace />} />
|
|
<Route path="/tasks" element={<ProtectedRoute role={['team', 'external', 'client']}><RequestsPage /></ProtectedRoute>} />
|
|
<Route path="/requests" element={<Navigate to="/tasks" replace />} />
|
|
<Route path="/team-projects" element={<Navigate to="/tasks" replace />} />
|
|
<Route path="/finances" element={<ProtectedRoute role="team"><TeamInvoices /></ProtectedRoute>} />
|
|
<Route path="/finances/new" element={<Navigate to="/finances" replace />} />
|
|
<Route path="/subcontractor-pos/new" element={<ProtectedRoute role="team"><TeamCreateSubcontractorPO /></ProtectedRoute>} />
|
|
<Route path="/finances/:id" element={<ProtectedRoute role="team"><TeamInvoiceDetail /></ProtectedRoute>} />
|
|
<Route path="/subcontractor-pos/:id" element={<ProtectedRoute role="team"><TeamSubcontractorPODetail /></ProtectedRoute>} />
|
|
<Route path="/sub-invoices/:id" element={<ProtectedRoute role="team"><TeamSubInvoiceDetail /></ProtectedRoute>} />
|
|
<Route path="/invoices" element={<Navigate to="/finances" replace />} />
|
|
<Route path="/invoices/new" element={<Navigate to="/finances" replace />} />
|
|
<Route path="/invoices/:id" element={<RedirectTeamInvoiceDetail />} />
|
|
<Route path="/survey-maker" element={<ProtectedRoute role={['team', 'external']}><SurveyMaker /></ProtectedRoute>} />
|
|
<Route path="/brand-book" element={<ProtectedRoute role={['team', 'external']}><BrandBook /></ProtectedRoute>} />
|
|
<Route path="/converters" element={<ProtectedRoute role={['team', 'external']}><Converters /></ProtectedRoute>} />
|
|
<Route path="/fourge-passwords" element={<ProtectedRoute role="team"><TeamFourgePasswords /></ProtectedRoute>} />
|
|
<Route path="/reports" element={<ProtectedRoute role="team"><TeamReports /></ProtectedRoute>} />
|
|
<Route path="/server-status" element={<Navigate to="/dashboard" replace />} />
|
|
<Route path="/assigned-requests" element={<Navigate to="/tasks" replace />} />
|
|
<Route path="/my-purchase-orders" element={<ProtectedRoute role="external"><ExternalMyPurchaseOrders /></ProtectedRoute>} />
|
|
<Route path="/my-projects-sub" element={<Navigate to="/tasks" replace />} />
|
|
<Route path="/subs-invoices" element={<ProtectedRoute role="external"><ExternalMyInvoices /></ProtectedRoute>} />
|
|
<Route path="/subs-invoices/new" element={<ProtectedRoute role="external"><ExternalMyInvoiceCreate /></ProtectedRoute>} />
|
|
<Route path="/subs-invoices/:id" element={<ProtectedRoute role="external"><ExternalMyInvoiceDetail /></ProtectedRoute>} />
|
|
<Route path="/my-invoices-sub" element={<Navigate to="/subs-invoices" replace />} />
|
|
<Route path="/my-invoices-sub/new" element={<Navigate to="/subs-invoices/new" replace />} />
|
|
<Route path="/my-invoices-sub/:id" element={<RedirectSubsInvoiceDetail />} />
|
|
|
|
<Route path="/profile" element={<ProtectedRoute><ProfilePage /></ProtectedRoute>} />
|
|
<Route path="/profile/:id" element={<ProtectedRoute><ProfilePage /></ProtectedRoute>} />
|
|
<Route path="/settings" element={<Navigate to="/profile" replace />} />
|
|
|
|
<Route path="/my-dashboard" element={<Navigate to="/dashboard" replace />} />
|
|
<Route path="/my-company" element={<Navigate to="/company" replace />} />
|
|
<Route path="/my-requests" element={<Navigate to="/tasks" replace />} />
|
|
<Route path="/my-requests/:id" element={<RedirectToTask />} />
|
|
<Route path="/my-projects" element={<Navigate to="/tasks" replace />} />
|
|
<Route path="/my-projects/:id" element={<RedirectProjectDetail />} />
|
|
<Route path="/client-invoices" element={<ProtectedRoute role="client"><ClientMyInvoices /></ProtectedRoute>} />
|
|
<Route path="/my-invoices" element={<Navigate to="/client-invoices" replace />} />
|
|
<Route path="/new-request" element={<Navigate to="/tasks" replace />} />
|
|
<Route path="/new-project" element={<Navigate to="/tasks" replace />} />
|
|
|
|
<Route path="/pay/:id" element={<PayInvoice />} />
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</Suspense>
|
|
</ChunkErrorBoundary>
|
|
</BrowserRouter>
|
|
</AuthProvider>
|
|
);
|
|
}
|