feat(routes): unify won leads and projects on a single /projects/:id page
- add role-prefix-free /projects/:id route (OWNER/ADMIN/FIELD_AGENT; ABAC guard lands in Phase 8) - relax detail lookup so admin/field-agent open any project, owners only their own - kanban drawer + project list route won leads (with projectId) to /projects/:id; pre-sale leads stay on the lead page
This commit is contained in:
@@ -188,6 +188,12 @@ function App() {
|
||||
<OwnerProjectDetail />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
{/* Unified project detail — won leads and projects share this page (interim coarse guard; ABAC policy guard lands in Phase 8) */}
|
||||
<Route path="/projects/:projectId" element={
|
||||
<ProtectedRoute allowedRoles={['OWNER', 'ADMIN', 'FIELD_AGENT']}>
|
||||
<OwnerProjectDetail />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/owner/maps" element={
|
||||
<ProtectedRoute allowedRoles={['OWNER']}>
|
||||
<Maps />
|
||||
|
||||
@@ -299,7 +299,9 @@ export default function LeadInfoDrawer({ lead, columns, isOpen, onClose }) {
|
||||
<button
|
||||
onClick={() => {
|
||||
onClose();
|
||||
navigate(`${basePath}/leads/${lead.id}`);
|
||||
// Won leads are projects now — open the unified project page; pre-sale leads stay on the lead page.
|
||||
if (lead.projectId) navigate(`/projects/${lead.projectId}`);
|
||||
else navigate(`${basePath}/leads/${lead.id}`);
|
||||
}}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold transition-colors shadow-sm"
|
||||
>
|
||||
|
||||
@@ -294,7 +294,8 @@ const OwnerProjectDetail = () => {
|
||||
const totalReceived = payments.reduce((sum, p) => sum + p.amount, 0);
|
||||
|
||||
const project = useMemo(() =>
|
||||
projects.find(p => p.id === projectId && p.ownerId === user?.id)
|
||||
// Owners only see their own projects; admin/field-agent can open any project.
|
||||
projects.find(p => p.id === projectId && (user?.role !== 'OWNER' || p.ownerId === user?.id))
|
||||
, [projects, projectId, user]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -277,7 +277,7 @@ const OwnerProjectList = () => {
|
||||
return (
|
||||
<div
|
||||
key={project.id}
|
||||
onClick={() => navigate(`/owner/projects/${project.id}`)}
|
||||
onClick={() => navigate(`/projects/${project.id}`)}
|
||||
className="p-4 active:bg-zinc-50 dark:active:bg-white/5 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
@@ -363,7 +363,7 @@ const OwnerProjectList = () => {
|
||||
return (
|
||||
<tr
|
||||
key={project.id}
|
||||
onClick={() => navigate(`/owner/projects/${project.id}`)}
|
||||
onClick={() => navigate(`/projects/${project.id}`)}
|
||||
className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors cursor-pointer group"
|
||||
>
|
||||
{/* Project Name */}
|
||||
@@ -496,7 +496,7 @@ const OwnerProjectList = () => {
|
||||
return (
|
||||
<div
|
||||
key={lead.id}
|
||||
onClick={() => navigate(`/owner/leads/${lead.id}`)}
|
||||
onClick={() => navigate(lead.projectId ? `/projects/${lead.projectId}` : `/owner/leads/${lead.id}`)}
|
||||
className="p-4 active:bg-zinc-50 dark:active:bg-white/5 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
@@ -562,7 +562,7 @@ const OwnerProjectList = () => {
|
||||
return (
|
||||
<tr
|
||||
key={lead.id}
|
||||
onClick={() => navigate(`/owner/leads/${lead.id}`)}
|
||||
onClick={() => navigate(lead.projectId ? `/projects/${lead.projectId}` : `/owner/leads/${lead.id}`)}
|
||||
className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors cursor-pointer group"
|
||||
>
|
||||
<td className="px-5 py-4">
|
||||
|
||||
Reference in New Issue
Block a user