5b3060e190
- Tasks table: added R#, Assigned To (avatar), Priority (HOT/NO) columns; removed Status column; sortable columns; adjustable widths - FileBrowser: full visual rewrite to match FileSharing page (table layout, colored ext badges, SVG folder icon, sortable columns, multi-select, context menu, drag-drop) - TaskDetail folder tab: fix path with safeName(), rootPath set to project folder level - filebrowserFolders: export safeName for reuse Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
146 lines
8.5 KiB
React
146 lines
8.5 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 TeamCreateInvoice = lazy(() => import('./pages/team/TeamCreateInvoice'));
|
|
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 FileSharing = lazy(() => import('./pages/FileSharing'));
|
|
const TeamFourgePasswords = lazy(() => import('./pages/team/TeamFourgePasswords'));
|
|
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={`/projects/${id}`} replace />;
|
|
}
|
|
|
|
function RedirectToTask() {
|
|
const { id } = useParams();
|
|
return <Navigate to={`/tasks/${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="/projects/:id" element={<ProtectedRoute role={['team', 'external', 'client']}><ProjectDetailPage /></ProtectedRoute>} />
|
|
<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="/invoices" element={<ProtectedRoute role="team"><TeamInvoices /></ProtectedRoute>} />
|
|
<Route path="/invoices/new" element={<ProtectedRoute role="team"><TeamCreateInvoice /></ProtectedRoute>} />
|
|
<Route path="/subcontractor-pos/new" element={<ProtectedRoute role="team"><TeamCreateSubcontractorPO /></ProtectedRoute>} />
|
|
<Route path="/invoices/: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="/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="/file-sharing" element={<ProtectedRoute role={['team', 'external', 'client']}><FileSharing /></ProtectedRoute>} />
|
|
<Route path="/file-uploads" element={<Navigate to="/file-sharing" replace />} />
|
|
<Route path="/fourge-passwords" element={<ProtectedRoute role="team"><TeamFourgePasswords /></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="/my-invoices-sub" element={<ProtectedRoute role="external"><ExternalMyInvoices /></ProtectedRoute>} />
|
|
<Route path="/my-invoices-sub/new" element={<ProtectedRoute role="external"><ExternalMyInvoiceCreate /></ProtectedRoute>} />
|
|
<Route path="/my-invoices-sub/:id" element={<ProtectedRoute role="external"><ExternalMyInvoiceDetail /></ProtectedRoute>} />
|
|
|
|
<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="/my-invoices" element={<ProtectedRoute role="client"><ClientMyInvoices /></ProtectedRoute>} />
|
|
<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>
|
|
);
|
|
}
|