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 />
|
<OwnerProjectDetail />
|
||||||
</ProtectedRoute>
|
</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={
|
<Route path="/owner/maps" element={
|
||||||
<ProtectedRoute allowedRoles={['OWNER']}>
|
<ProtectedRoute allowedRoles={['OWNER']}>
|
||||||
<Maps />
|
<Maps />
|
||||||
|
|||||||
@@ -299,7 +299,9 @@ export default function LeadInfoDrawer({ lead, columns, isOpen, onClose }) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose();
|
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"
|
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 totalReceived = payments.reduce((sum, p) => sum + p.amount, 0);
|
||||||
|
|
||||||
const project = useMemo(() =>
|
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]);
|
, [projects, projectId, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ const OwnerProjectList = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={project.id}
|
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"
|
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">
|
<div className="flex justify-between items-start mb-2">
|
||||||
@@ -363,7 +363,7 @@ const OwnerProjectList = () => {
|
|||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={project.id}
|
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"
|
className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors cursor-pointer group"
|
||||||
>
|
>
|
||||||
{/* Project Name */}
|
{/* Project Name */}
|
||||||
@@ -496,7 +496,7 @@ const OwnerProjectList = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={lead.id}
|
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"
|
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">
|
<div className="flex justify-between items-start mb-2">
|
||||||
@@ -562,7 +562,7 @@ const OwnerProjectList = () => {
|
|||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={lead.id}
|
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"
|
className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors cursor-pointer group"
|
||||||
>
|
>
|
||||||
<td className="px-5 py-4">
|
<td className="px-5 py-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user