Add dedicated New Project page for clients
Route /new-project: name + company selector → creates project, redirects to project detail. My Projects buttons now link there instead of new-request. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+49
-21
@@ -1,34 +1,48 @@
|
|||||||
|
import { lazy, Suspense } from 'react';
|
||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { AuthProvider } from './context/AuthContext';
|
import { AuthProvider } from './context/AuthContext';
|
||||||
import ProtectedRoute from './components/ProtectedRoute';
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
|
import PageLoader from './components/PageLoader';
|
||||||
|
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
|
import PayInvoice from './pages/PayInvoice';
|
||||||
|
|
||||||
import Dashboard from './pages/team/Dashboard';
|
const Settings = lazy(() => import('./pages/Settings'));
|
||||||
import Companies from './pages/team/Companies';
|
const Dashboard = lazy(() => import('./pages/team/Dashboard'));
|
||||||
import CompanyDetail from './pages/team/CompanyDetail';
|
const Companies = lazy(() => import('./pages/team/Companies'));
|
||||||
import ProjectDetail from './pages/team/ProjectDetail';
|
const CompanyDetail = lazy(() => import('./pages/team/CompanyDetail'));
|
||||||
import TaskDetail from './pages/team/TaskDetail';
|
const ProjectDetail = lazy(() => import('./pages/team/ProjectDetail'));
|
||||||
import Requests from './pages/team/Requests';
|
const Requests = lazy(() => import('./pages/team/Requests'));
|
||||||
import Invoices from './pages/team/Invoices';
|
const Invoices = lazy(() => import('./pages/team/Invoices'));
|
||||||
import CreateInvoice from './pages/team/CreateInvoice';
|
const MeetingNotes = lazy(() => import('./pages/team/MeetingNotes'));
|
||||||
import InvoiceDetail from './pages/team/InvoiceDetail';
|
const TaskDetail = lazy(() => import('./pages/team/TaskDetail'));
|
||||||
import BrandBook from './pages/team/BrandBook';
|
const CreateInvoice = lazy(() => import('./pages/team/CreateInvoice'));
|
||||||
|
const CreateSubcontractorPO = lazy(() => import('./pages/team/CreateSubcontractorPO'));
|
||||||
import Settings from './pages/Settings';
|
const InvoiceDetail = lazy(() => import('./pages/team/InvoiceDetail'));
|
||||||
import ClientDashboard from './pages/client/ClientDashboard';
|
const SubcontractorPODetail = lazy(() => import('./pages/team/SubcontractorPODetail'));
|
||||||
import MyCompany from './pages/client/MyCompany';
|
const SurveyMaker = lazy(() => import('./pages/team/SurveyMaker'));
|
||||||
import MyRequests from './pages/client/MyRequests';
|
const BrandBook = lazy(() => import('./pages/team/BrandBook'));
|
||||||
import MyProjects from './pages/client/MyProjects';
|
const Converters = lazy(() => import('./pages/team/Converters'));
|
||||||
import MyProjectDetail from './pages/client/MyProjectDetail';
|
const ServerStatus = lazy(() => import('./pages/team/ServerStatus'));
|
||||||
import MyInvoices from './pages/client/MyInvoices';
|
const FileSharing = lazy(() => import('./pages/team/FileSharing'));
|
||||||
import RequestDetail from './pages/client/RequestDetail';
|
const FourgePasswords = lazy(() => import('./pages/team/FourgePasswords'));
|
||||||
import NewRequest from './pages/client/NewRequest';
|
const ExternalMyRequests = lazy(() => import('./pages/external/MyRequests'));
|
||||||
|
const MyPurchaseOrders = lazy(() => import('./pages/external/MyPurchaseOrders'));
|
||||||
|
const ClientDashboard = lazy(() => import('./pages/client/ClientDashboard'));
|
||||||
|
const MyCompany = lazy(() => import('./pages/client/MyCompany'));
|
||||||
|
const MyRequests = lazy(() => import('./pages/client/MyRequests'));
|
||||||
|
const MyProjects = lazy(() => import('./pages/client/MyProjects'));
|
||||||
|
const MyProjectDetail = lazy(() => import('./pages/client/MyProjectDetail'));
|
||||||
|
const MyInvoices = lazy(() => import('./pages/client/MyInvoices'));
|
||||||
|
const RequestDetail = lazy(() => import('./pages/client/RequestDetail'));
|
||||||
|
const NewRequest = lazy(() => import('./pages/client/NewRequest'));
|
||||||
|
const NewProject = lazy(() => import('./pages/client/NewProject'));
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<Suspense fallback={<PageLoader />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Login />} />
|
<Route path="/" element={<Login />} />
|
||||||
|
|
||||||
@@ -38,10 +52,21 @@ export default function App() {
|
|||||||
<Route path="/companies" element={<ProtectedRoute role="team"><Companies /></ProtectedRoute>} />
|
<Route path="/companies" element={<ProtectedRoute role="team"><Companies /></ProtectedRoute>} />
|
||||||
<Route path="/companies/:id" element={<ProtectedRoute role="team"><CompanyDetail /></ProtectedRoute>} />
|
<Route path="/companies/:id" element={<ProtectedRoute role="team"><CompanyDetail /></ProtectedRoute>} />
|
||||||
<Route path="/requests" element={<ProtectedRoute role="team"><Requests /></ProtectedRoute>} />
|
<Route path="/requests" element={<ProtectedRoute role="team"><Requests /></ProtectedRoute>} />
|
||||||
|
<Route path="/meeting-notes" element={<ProtectedRoute role="team"><MeetingNotes /></ProtectedRoute>} />
|
||||||
<Route path="/invoices" element={<ProtectedRoute role="team"><Invoices /></ProtectedRoute>} />
|
<Route path="/invoices" element={<ProtectedRoute role="team"><Invoices /></ProtectedRoute>} />
|
||||||
<Route path="/invoices/new" element={<ProtectedRoute role="team"><CreateInvoice /></ProtectedRoute>} />
|
<Route path="/invoices/new" element={<ProtectedRoute role="team"><CreateInvoice /></ProtectedRoute>} />
|
||||||
|
<Route path="/subcontractor-pos/new" element={<ProtectedRoute role="team"><CreateSubcontractorPO /></ProtectedRoute>} />
|
||||||
<Route path="/invoices/:id" element={<ProtectedRoute role="team"><InvoiceDetail /></ProtectedRoute>} />
|
<Route path="/invoices/:id" element={<ProtectedRoute role="team"><InvoiceDetail /></ProtectedRoute>} />
|
||||||
<Route path="/brand-book" element={<ProtectedRoute role="team"><BrandBook /></ProtectedRoute>} />
|
<Route path="/subcontractor-pos/:id" element={<ProtectedRoute role="team"><SubcontractorPODetail /></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"><FourgePasswords /></ProtectedRoute>} />
|
||||||
|
<Route path="/server-status" element={<ProtectedRoute role="team"><ServerStatus /></ProtectedRoute>} />
|
||||||
|
<Route path="/assigned-requests" element={<ProtectedRoute role="external"><ExternalMyRequests /></ProtectedRoute>} />
|
||||||
|
<Route path="/my-purchase-orders" element={<ProtectedRoute role="external"><MyPurchaseOrders /></ProtectedRoute>} />
|
||||||
|
|
||||||
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
|
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
|
||||||
|
|
||||||
@@ -53,9 +78,12 @@ export default function App() {
|
|||||||
<Route path="/my-projects/:id" element={<ProtectedRoute role="client"><MyProjectDetail /></ProtectedRoute>} />
|
<Route path="/my-projects/:id" element={<ProtectedRoute role="client"><MyProjectDetail /></ProtectedRoute>} />
|
||||||
<Route path="/my-invoices" element={<ProtectedRoute role="client"><MyInvoices /></ProtectedRoute>} />
|
<Route path="/my-invoices" element={<ProtectedRoute role="client"><MyInvoices /></ProtectedRoute>} />
|
||||||
<Route path="/new-request" element={<ProtectedRoute role="client"><NewRequest /></ProtectedRoute>} />
|
<Route path="/new-request" element={<ProtectedRoute role="client"><NewRequest /></ProtectedRoute>} />
|
||||||
|
<Route path="/new-project" element={<ProtectedRoute role="client"><NewProject /></ProtectedRoute>} />
|
||||||
|
|
||||||
|
<Route path="/pay/:id" element={<PayInvoice />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ function ProjectGroup({ project, tasks, submissions, currentUserId, filter }) {
|
|||||||
)}
|
)}
|
||||||
<div style={{ padding: '10px 16px', borderTop: filteredTasks.length > 0 ? '1px solid var(--border)' : 'none' }}>
|
<div style={{ padding: '10px 16px', borderTop: filteredTasks.length > 0 ? '1px solid var(--border)' : 'none' }}>
|
||||||
<Link
|
<Link
|
||||||
to={`/new-request?project=${encodeURIComponent(project.name)}`}
|
to={`/new-project?project=${encodeURIComponent(project.name)}`}
|
||||||
className="btn btn-outline btn-sm"
|
className="btn btn-outline btn-sm"
|
||||||
>
|
>
|
||||||
+ Add Request to {project.name}
|
+ Add Request to {project.name}
|
||||||
@@ -169,7 +169,7 @@ export default function MyProjects() {
|
|||||||
<div className="page-title">Projects</div>
|
<div className="page-title">Projects</div>
|
||||||
<div className="page-subtitle">All work for your company.</div>
|
<div className="page-subtitle">All work for your company.</div>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/new-request" className="btn btn-primary">+ New Project</Link>
|
<Link to="/new-project" className="btn btn-primary">+ New Project</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card page-toolbar">
|
<div className="card page-toolbar">
|
||||||
@@ -199,7 +199,7 @@ export default function MyProjects() {
|
|||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
<h3>No projects yet</h3>
|
<h3>No projects yet</h3>
|
||||||
<p>Submit a request and a project will be created automatically.</p>
|
<p>Submit a request and a project will be created automatically.</p>
|
||||||
<Link to="/new-request" className="btn btn-primary" style={{ marginTop: 16 }}>+ New Project</Link>
|
<Link to="/new-project" className="btn btn-primary" style={{ marginTop: 16 }}>+ New Project</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
projects.map(project => (
|
projects.map(project => (
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import Layout from '../../components/Layout';
|
||||||
|
import { supabase } from '../../lib/supabase';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
|
|
||||||
|
export default function NewProject() {
|
||||||
|
const { currentUser } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const companyOptions = currentUser.companies?.length
|
||||||
|
? currentUser.companies
|
||||||
|
: (currentUser.company ? [currentUser.company] : []);
|
||||||
|
|
||||||
|
const [selectedCompanyId, setSelectedCompanyId] = useState(companyOptions[0]?.id || '');
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
if (!companyOptions.length) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div style={{ maxWidth: 480, margin: '0 auto', textAlign: 'center', paddingTop: 48 }}>
|
||||||
|
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 8 }}>Account Not Yet Active</h2>
|
||||||
|
<p style={{ color: 'var(--text-secondary)', fontSize: 14 }}>
|
||||||
|
Your account hasn't been linked to a company yet. Please contact the Fourge team to get set up.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (saving) return;
|
||||||
|
const trimmedName = name.trim();
|
||||||
|
if (!trimmedName) return;
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const { data, error: insertError } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.insert({ company_id: selectedCompanyId, name: trimmedName })
|
||||||
|
.select('id')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (insertError) {
|
||||||
|
setError(insertError.message);
|
||||||
|
setSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(`/my-projects/${data.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="page-header">
|
||||||
|
<div>
|
||||||
|
<div className="page-title">New Project</div>
|
||||||
|
<div className="page-subtitle">Create a project to organise your work.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card" style={{ maxWidth: 480 }}>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{companyOptions.length > 1 && (
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Company *</label>
|
||||||
|
<select
|
||||||
|
value={selectedCompanyId}
|
||||||
|
onChange={e => setSelectedCompanyId(e.target.value)}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
{companyOptions.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Project Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. Brand Refresh 2026"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="notification notification-error" style={{ marginBottom: 16 }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="action-buttons">
|
||||||
|
<button type="submit" className="btn btn-primary btn-lg" disabled={saving || !name.trim()}>
|
||||||
|
{saving ? 'Creating...' : 'Create Project'}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-outline" onClick={() => navigate('/my-projects')}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user