Files
fourge-portal/src/App.jsx
T
Krao Hasanee 91045a1897 redesign: rename Project→Client, finance single-card + filters, mobile tasks, sticky headers
- 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>
2026-07-02 09:39:54 -04:00

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>
);
}