diff --git a/api/filebrowser.js b/api/filebrowser.js index a929073..f7ab8c2 100644 --- a/api/filebrowser.js +++ b/api/filebrowser.js @@ -1,6 +1,6 @@ import { createClient } from '@supabase/supabase-js'; -const FB_SOURCE = 'files'; +const FB_SOURCE = 'srv'; function json(res, status, body) { res.status(status).setHeader('Content-Type', 'application/json'); @@ -263,7 +263,8 @@ function toListResponse(vPath, entries, { readOnly = false } = {}) { export default async function handler(req, res) { try { - const authHeader = req.headers.authorization || ''; + const queryToken = String(req.query?.sb_access_token || req.body?.sb_access_token || '').trim(); + const authHeader = req.headers.authorization || (queryToken ? `Bearer ${queryToken}` : ''); if (!authHeader) return json(res, 401, { error: 'No authorization header' }); const auth = await requirePortalUser(authHeader); @@ -333,6 +334,25 @@ export default async function handler(req, res) { return json(res, 200, { url: downloadUrl, token }); } + if (req.method === 'GET' && action === 'download-blob') { + const resolved = toFbPath(); + if (resolved.virtual) return json(res, 400, { error: 'Cannot download virtual directory' }); + const token = getToken(config); + const downloadUrl = `${config.url}/api/resources/download?source=${FB_SOURCE}&file=${encodeURIComponent(resolved.fbPath)}&auth=${encodeURIComponent(token)}`; + const upstream = await fetch(downloadUrl); + if (!upstream.ok) { + const text = await upstream.text(); + return json(res, upstream.status, { error: text || `Download failed (${upstream.status})` }); + } + + res.status(upstream.status); + res.setHeader('Cache-Control', 'no-store'); + res.setHeader('Content-Type', upstream.headers.get('content-type') || 'application/octet-stream'); + res.setHeader('Content-Disposition', upstream.headers.get('content-disposition') || `attachment; filename="${basename(resolved.fbPath) || 'download'}"`); + const arrayBuffer = await upstream.arrayBuffer(); + return res.send(Buffer.from(arrayBuffer)); + } + if (req.method === 'POST' && action === 'upload-token') { const resolved = toFbPath(); if (resolved.virtual) return json(res, 400, { error: 'Cannot upload to virtual directory' }); diff --git a/src/App.jsx b/src/App.jsx index 5d4969d..0611517 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -39,31 +39,31 @@ class ChunkErrorBoundary extends Component { import Login from './pages/Login'; import PayInvoice from './pages/PayInvoice'; -const ProfilePage = lazy(() => import('./pages/Settings')); -const CompaniesPage = lazy(() => import('./pages/CompaniesPage')); +const ProfilePage = lazy(() => import('./pages/Profile')); +const CompaniesPage = lazy(() => import('./pages/Companies')); const CompanyDetail = lazy(() => import('./pages/CompanyDetail')); -const Invoices = lazy(() => import('./pages/team/Invoices')); +const TeamInvoices = lazy(() => import('./pages/team/TeamInvoices')); const RequestDetail = lazy(() => import('./pages/RequestDetail')); -const CreateInvoice = lazy(() => import('./pages/team/CreateInvoice')); -const CreateSubcontractorPO = lazy(() => import('./pages/team/CreateSubcontractorPO')); -const InvoiceDetail = lazy(() => import('./pages/team/InvoiceDetail')); -const SubcontractorPODetail = lazy(() => import('./pages/team/SubcontractorPODetail')); -const SubInvoiceDetail = lazy(() => import('./pages/team/SubInvoiceDetail')); +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 FourgePasswords = lazy(() => import('./pages/team/FourgePasswords')); -const MyPurchaseOrders = lazy(() => import('./pages/external/MyPurchaseOrders')); -const ExternalMyInvoices = lazy(() => import('./pages/external/MyInvoices')); -const ExternalMyInvoiceDetail = lazy(() => import('./pages/external/MyInvoiceDetail')); -const ExternalMyInvoiceCreate = lazy(() => import('./pages/external/MyInvoiceCreate')); -const ProjectDetailPage = lazy(() => import('./pages/ProjectDetailPage')); +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/RequestsPage')); -const MyInvoices = lazy(() => import('./pages/client/MyInvoices')); -const NewRequest = lazy(() => import('./pages/client/NewRequest')); -const NewProject = lazy(() => import('./pages/client/NewProject')); +const RequestsPage = lazy(() => import('./pages/Tasks')); +const ClientMyInvoices = lazy(() => import('./pages/client/ClientMyInvoices')); +const ClientNewRequest = lazy(() => import('./pages/client/ClientNewRequest')); +const ClientNewProject = lazy(() => import('./pages/client/ClientNewProject')); function RedirectProjectDetail() { const { id } = useParams(); @@ -103,21 +103,21 @@ export default function App() { } /> } /> } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> } /> } /> } /> } /> - } /> + } /> } /> } /> - } /> + } /> } /> } /> } /> @@ -133,9 +133,9 @@ export default function App() { } /> } /> } /> - } /> - } /> - } /> + } /> + } /> + } /> } /> } /> diff --git a/src/components/PageLoader.jsx b/src/components/PageLoader.jsx index 94601ad..cf0a78a 100644 --- a/src/components/PageLoader.jsx +++ b/src/components/PageLoader.jsx @@ -1,32 +1,43 @@ -export default function PageLoader() { +export default function PageLoader({ label = 'Loading', progress = null, scope = 'page' }) { + const hasProgress = typeof progress === 'number' && Number.isFinite(progress); + const isContainerScope = scope === 'container'; return (
-
- Loading -
-
-
+
+ {label}
+ <> +
+
+
+ {hasProgress && ( +
{progress}%
+ )} +
); diff --git a/src/components/RequestForm.jsx b/src/components/RequestForm.jsx index a0b1c84..d002261 100644 --- a/src/components/RequestForm.jsx +++ b/src/components/RequestForm.jsx @@ -5,6 +5,9 @@ import FileAttachment from './FileAttachment'; import { addDaysToDateOnly, getTodayDateOnlyEST } from '../lib/dates'; const defaultDeadline = () => addDaysToDateOnly(getTodayDateOnlyEST(), 3); +const modalLabelStyle = { fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: 0.8, marginBottom: 6 }; +const modalInputStyle = { fontSize: 13, padding: '0 5px', textAlign: 'left', lineHeight: 1 }; +const modalTextAreaStyle = { ...modalInputStyle, minHeight: 100, padding: '8px 5px', lineHeight: 1.4 }; const emptyForm = (companyId = '') => ({ companyId, project: '', @@ -123,11 +126,12 @@ export default function RequestForm({
{showCompanySelect && (
- + setNewProjectName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleAddProject(); } }} autoFocus - style={{ flex: 1 }} + style={{ ...modalInputStyle, flex: 1 }} /> - - + +
) : ( - {allProjectNames.map(name => )} {(!showCompanySelect || companyId) && } @@ -162,20 +166,20 @@ export default function RequestForm({
- - {serviceTypes.map(s => )}
- - + +
-