diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f576176..96c43d2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,7 +6,6 @@ import StatsBar from './components/StatsBar'; import ChatToggle from './components/ChatToggle'; import Loading from './components/Loading'; import Login from './pages/Login'; -import { DataProvider } from './hooks/useDataContext'; import { canAccess, useAuth } from './hooks/useAuth'; const Department = lazy(() => import('./pages/Department')); @@ -28,18 +27,16 @@ function RoleGate({ slug, children }: { slug: string; children: React.ReactNode function ProtectedShell({ children, slug }: { children: React.ReactNode; slug: string }) { return ( - - -
- - -
- }>{children} -
- -
-
-
+ +
+ + +
+ }>{children} +
+ +
+
); } diff --git a/frontend/src/components/HeaderUploads.tsx b/frontend/src/components/HeaderUploads.tsx new file mode 100644 index 0000000..d32446d --- /dev/null +++ b/frontend/src/components/HeaderUploads.tsx @@ -0,0 +1,128 @@ +import { useRef } from 'react'; +import { Upload, FileSpreadsheet, X, Loader2 } from 'lucide-react'; +import { useDataContext } from '../hooks/useDataContext'; + +const ACCEPT = '.xlsx,.csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv'; + +interface SlotProps { + label: string; + filename: string | null; + rows: number; + uploading: boolean; + error: string | null; + onFile: (f: File) => void; + onClear?: () => void; +} + +function Slot({ label, filename, rows, uploading, error, onFile, onClear }: SlotProps) { + const inputRef = useRef(null); + + const onChange = (e: React.ChangeEvent) => { + const f = e.target.files?.[0]; + if (f) onFile(f); + e.target.value = ''; + }; + + const status = uploading + ? 'uploading' + : error + ? 'error' + : filename + ? 'loaded' + : 'empty'; + + return ( +
+ {uploading ? ( + + ) : ( + + )} + +
+ {label} + {status === 'loaded' && ( + + {filename} · {rows.toLocaleString()} rows + + )} + {status === 'uploading' && Parsing…} + {status === 'empty' && Not uploaded} + {status === 'error' && {error}} +
+ + + + {filename && onClear && ( + + )} +
+ ); +} + +export default function HeaderUploads() { + const { + timelog, deliverable, projectSummary, + uploadTimelog, uploadDeliverable, uploadProjectSummary, + clearTimelog, + } = useDataContext(); + + return ( +
+ void uploadTimelog(f)} + onClear={clearTimelog} + /> + void uploadDeliverable(f)} + /> + void uploadProjectSummary(f)} + /> +
+ ); +} diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 5ac8562..124f865 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,6 +1,7 @@ import { NavLink, useNavigate } from 'react-router-dom'; import { LogOut, BarChart3 } from 'lucide-react'; import { canAccess, useAuth } from '../hooks/useAuth'; +import HeaderUploads from './HeaderUploads'; interface Tab { to: string; @@ -74,6 +75,12 @@ export default function Navbar() { + {/* Compact upload tray — always visible so every data page can feed it. */} +
+
+ +
+
); } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index cc4488c..1881355 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,15 +3,21 @@ import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; import { AuthProvider } from './hooks/useAuth'; +import { DataProvider } from './hooks/useDataContext'; import './index.css'; const basename = import.meta.env.BASE_URL.replace(/\/$/, ''); +// DataProvider lives at the root so the uploaded timelog / deliverable / +// project-summary state survives navigation between tabs. (Previously it +// was per-route, which silently wiped your upload on every nav click.) ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - + + + , diff --git a/frontend/src/pages/Department.tsx b/frontend/src/pages/Department.tsx index c96fab6..eb2f11a 100644 --- a/frontend/src/pages/Department.tsx +++ b/frontend/src/pages/Department.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { Download, RefreshCw } from 'lucide-react'; import FilterBar from '../components/FilterBar'; -import UploadButton from '../components/UploadButton'; import KpiTiles from '../components/KpiTiles'; import PeriodToggle from '../components/PeriodToggle'; import HourBreakdown from '../components/HourBreakdown'; @@ -21,7 +20,7 @@ import type { UtilisationSummaryRow, UtilisationTotals } from '../api/types'; export default function Department() { const airtable = useAirtableData(false); - const { filters, dispatch, timelog, dimensions, uploadTimelog, clearTimelog } = useDataContext(); + const { filters, dispatch, timelog, dimensions } = useDataContext(); const [summary, setSummary] = useState([]); const [totals, setTotals] = useState(null); const [summaryLoading, setSummaryLoading] = useState(false); @@ -97,29 +96,23 @@ export default function Department() { -
- void uploadTimelog(f)} - onClear={clearTimelog} - /> - - -
+ + {timelog.unrecognised.length > 0 && ( +
+ Time log is missing expected column{timelog.unrecognised.length > 1 ? 's' : ''}:{' '} + {timelog.unrecognised.join(', ')} — charts may be incomplete. +
+ )}