create Teams tab in projectdetails page and display team managment section, removed from projects lists page
This commit is contained in:
@@ -12,12 +12,13 @@ import {
|
|||||||
ArrowLeft, CheckCircle, Clock, AlertTriangle, PauseCircle, ShieldAlert,
|
ArrowLeft, CheckCircle, Clock, AlertTriangle, PauseCircle, ShieldAlert,
|
||||||
FileText, DollarSign, GitPullRequest, AlertCircle, Activity, Milestone,
|
FileText, DollarSign, GitPullRequest, AlertCircle, Activity, Milestone,
|
||||||
Shield, ChevronRight, Phone, MessageSquare, Mail, AlertOctagon, TrendingUp, Key, Zap, Camera, Plus, Map, User, Crosshair, Image as ImageIcon, MapPin,
|
Shield, ChevronRight, Phone, MessageSquare, Mail, AlertOctagon, TrendingUp, Key, Zap, Camera, Plus, Map, User, Crosshair, Image as ImageIcon, MapPin,
|
||||||
Upload, Trash2, Eye, Pencil, X as XIcon, FolderOpen, Download, File, StickyNote, Banknote, ChevronLeft, ChevronDown as ChevronDownIcon, Info, Settings, Building2, User as UserIcon, Briefcase
|
Upload, Trash2, Eye, Pencil, X as XIcon, FolderOpen, Download, File, StickyNote, Banknote, ChevronLeft, ChevronDown as ChevronDownIcon, Info, Settings, Building2, User as UserIcon, Briefcase, Users
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer';
|
import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer';
|
||||||
import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal';
|
import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal';
|
||||||
import CreateItemModal from '../../components/owner/CreateItemModal';
|
import CreateItemModal from '../../components/owner/CreateItemModal';
|
||||||
import CommissionSettingsModal from '../../components/owner/CommissionSettingsModal';
|
import CommissionSettingsModal from '../../components/owner/CommissionSettingsModal';
|
||||||
|
import TeamManagementPanel from '../../components/owner/TeamManagementPanel';
|
||||||
import { usePermissions } from '../../hooks/usePermissions';
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
import { resolveCommission, findAssignedUserRoleKey } from '../../utils/commissionResolver';
|
import { resolveCommission, findAssignedUserRoleKey } from '../../utils/commissionResolver';
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ const statusConfig = {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'overview', label: 'Overview', icon: Activity },
|
{ id: 'overview', label: 'Overview', icon: Activity },
|
||||||
{ id: 'budget', label: 'Budget & Costs', icon: DollarSign },
|
{ id: 'budget', label: 'Budget & Costs', icon: DollarSign },
|
||||||
|
{ id: 'team', label: 'Team', icon: Users },
|
||||||
{ id: 'changeOrders', label: 'Change Orders', icon: GitPullRequest },
|
{ id: 'changeOrders', label: 'Change Orders', icon: GitPullRequest },
|
||||||
{ id: 'rfis', label: 'RFIs', icon: FileText },
|
{ id: 'rfis', label: 'RFIs', icon: FileText },
|
||||||
{ id: 'milestones', label: 'Milestones', icon: Milestone },
|
{ id: 'milestones', label: 'Milestones', icon: Milestone },
|
||||||
@@ -137,11 +139,12 @@ const OwnerProjectDetail = () => {
|
|||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const {
|
const {
|
||||||
projects, vendors, personnel,
|
projects, vendors, personnel, users,
|
||||||
orgCommissionDefaults, userCommissionOverrides,
|
orgCommissionDefaults, userCommissionOverrides,
|
||||||
roleCommissionOverrides, orgMembers, orgRoles,
|
roleCommissionOverrides, orgMembers, orgRoles,
|
||||||
updateOrgCommissionDefaults, setUserCommissionOverride,
|
updateOrgCommissionDefaults, setUserCommissionOverride,
|
||||||
removeUserCommissionOverride, updateProject,
|
removeUserCommissionOverride, updateProject,
|
||||||
|
addJobTeamMember, removeJobTeamMember, updateJobTeamMember,
|
||||||
} = useMockStore();
|
} = useMockStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { can } = usePermissions();
|
const { can } = usePermissions();
|
||||||
@@ -245,6 +248,16 @@ const OwnerProjectDetail = () => {
|
|||||||
projects.find(p => p.id === projectId && p.ownerId === user?.id)
|
projects.find(p => p.id === projectId && p.ownerId === user?.id)
|
||||||
, [projects, projectId, user]);
|
, [projects, projectId, user]);
|
||||||
|
|
||||||
|
const teamFinancials = useMemo(() => {
|
||||||
|
if (!project) return { totalReceived: 0, grossProfit: 0 };
|
||||||
|
const paid = (project.paymentSchedule || [])
|
||||||
|
.filter(ps => ps.status === 'paid')
|
||||||
|
.reduce((s, ps) => s + (ps.amount || 0), 0);
|
||||||
|
const tr = paid || (project.budget || 0);
|
||||||
|
const totalCosts = Number(project.actualCost ?? project.spent) || 0;
|
||||||
|
return { totalReceived: tr, grossProfit: tr - totalCosts };
|
||||||
|
}, [project]);
|
||||||
|
|
||||||
// ── Commission hierarchy: Job → Individual → Role → Org ──────────────
|
// ── Commission hierarchy: Job → Individual → Role → Org ──────────────
|
||||||
const jobCommissionOverride = useMemo(() => {
|
const jobCommissionOverride = useMemo(() => {
|
||||||
if (commissionConfig) return commissionConfig;
|
if (commissionConfig) return commissionConfig;
|
||||||
@@ -1620,6 +1633,27 @@ const OwnerProjectDetail = () => {
|
|||||||
)}
|
)}
|
||||||
</NeoCard>
|
</NeoCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'team' && (
|
||||||
|
<NeoCard spotlightColor="rgba(139, 92, 246, 0.1)">
|
||||||
|
<TeamManagementPanel
|
||||||
|
project={project}
|
||||||
|
teamMembers={project.teamMembers || []}
|
||||||
|
allUsers={users}
|
||||||
|
personnel={personnel}
|
||||||
|
userRole={user?.role}
|
||||||
|
orgCommissionDefaults={orgCommissionDefaults}
|
||||||
|
userCommissionOverrides={userCommissionOverrides}
|
||||||
|
roleCommissionOverrides={roleCommissionOverrides}
|
||||||
|
orgMembers={orgMembers}
|
||||||
|
totalReceived={teamFinancials.totalReceived}
|
||||||
|
grossProfit={teamFinancials.grossProfit}
|
||||||
|
onAddMember={addJobTeamMember}
|
||||||
|
onRemoveMember={removeJobTeamMember}
|
||||||
|
onUpdateMember={updateJobTeamMember}
|
||||||
|
/>
|
||||||
|
</NeoCard>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { useMockStore } from '../../data/mockStore';
|
import { useMockStore } from '../../data/mockStore';
|
||||||
import { SpotlightCard } from '../../components/SpotlightCard';
|
import { SpotlightCard } from '../../components/SpotlightCard';
|
||||||
import TeamManagementPanel from '../../components/owner/TeamManagementPanel';
|
|
||||||
import {
|
import {
|
||||||
Search, ChevronRight, ArrowUpDown, Briefcase, LayoutGrid,
|
Search, ChevronRight, ArrowUpDown, Briefcase, LayoutGrid,
|
||||||
CheckCircle, Clock, AlertTriangle, PauseCircle, ShieldAlert, User, Users, ChevronDown
|
CheckCircle, Clock, AlertTriangle, PauseCircle, ShieldAlert, User
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
@@ -50,12 +49,7 @@ function getPipelineHealth(lead) {
|
|||||||
|
|
||||||
const OwnerProjectList = () => {
|
const OwnerProjectList = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const {
|
const { projects, vendors, kanbanLeads, kanbanColumns } = useMockStore();
|
||||||
projects, vendors, kanbanLeads, kanbanColumns,
|
|
||||||
users, personnel, orgCommissionDefaults, userCommissionOverrides,
|
|
||||||
roleCommissionOverrides, orgMembers,
|
|
||||||
addJobTeamMember, removeJobTeamMember, updateJobTeamMember,
|
|
||||||
} = useMockStore();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [activeSection, setActiveSection] = useState('construction'); // 'construction' | 'pipeline'
|
const [activeSection, setActiveSection] = useState('construction'); // 'construction' | 'pipeline'
|
||||||
@@ -75,23 +69,6 @@ const OwnerProjectList = () => {
|
|||||||
projects.filter(p => p.ownerId === user?.id)
|
projects.filter(p => p.ownerId === user?.id)
|
||||||
, [projects, user]);
|
, [projects, user]);
|
||||||
|
|
||||||
// ── Team Management ──
|
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState(null);
|
|
||||||
const selectedProject = useMemo(() => {
|
|
||||||
const id = selectedProjectId || ownerProjects.find(p => p.status === 'active')?.id;
|
|
||||||
return ownerProjects.find(p => p.id === id) || ownerProjects[0] || null;
|
|
||||||
}, [selectedProjectId, ownerProjects]);
|
|
||||||
|
|
||||||
const teamFinancials = useMemo(() => {
|
|
||||||
if (!selectedProject) return { totalReceived: 0, grossProfit: 0 };
|
|
||||||
const paid = (selectedProject.paymentSchedule || [])
|
|
||||||
.filter(ps => ps.status === 'paid')
|
|
||||||
.reduce((s, ps) => s + (ps.amount || 0), 0);
|
|
||||||
const totalReceived = paid || (selectedProject.budget || 0);
|
|
||||||
const totalCosts = Number(selectedProject.actualCost ?? selectedProject.spent) || 0;
|
|
||||||
return { totalReceived, grossProfit: totalReceived - totalCosts };
|
|
||||||
}, [selectedProject]);
|
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
const filteredProjects = useMemo(() => {
|
const filteredProjects = useMemo(() => {
|
||||||
return ownerProjects.filter(p => {
|
return ownerProjects.filter(p => {
|
||||||
@@ -658,50 +635,6 @@ const OwnerProjectList = () => {
|
|||||||
</SpotlightCard>
|
</SpotlightCard>
|
||||||
</>)}
|
</>)}
|
||||||
|
|
||||||
{/* ── Owner's Box — Team Management ── */}
|
|
||||||
{selectedProject && (
|
|
||||||
<section>
|
|
||||||
<div className="flex items-center justify-between mb-4 flex-wrap gap-3">
|
|
||||||
<h2 className="text-xl font-bold text-zinc-900 dark:text-white/90 flex items-center">
|
|
||||||
<span className="w-2 h-8 bg-violet-500 rounded-full mr-3"></span>
|
|
||||||
Team Management
|
|
||||||
</h2>
|
|
||||||
<div className="relative">
|
|
||||||
<select
|
|
||||||
value={selectedProject.id}
|
|
||||||
onChange={e => setSelectedProjectId(e.target.value)}
|
|
||||||
className="appearance-none pl-3 pr-8 py-2 text-sm font-semibold rounded-xl bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-zinc-900 dark:text-white outline-none focus:border-violet-500 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
{ownerProjects.map(p => (
|
|
||||||
<option key={p.id} value={p.id}>
|
|
||||||
{p.projectType} — {p.address?.split(',')[0] || p.id}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<ChevronDown size={14} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SpotlightCard className="p-5 lg:p-6" spotlightColor="rgba(139, 92, 246, 0.06)">
|
|
||||||
<TeamManagementPanel
|
|
||||||
project={selectedProject}
|
|
||||||
teamMembers={selectedProject.teamMembers || []}
|
|
||||||
allUsers={users}
|
|
||||||
personnel={personnel}
|
|
||||||
userRole={user?.role}
|
|
||||||
orgCommissionDefaults={orgCommissionDefaults}
|
|
||||||
userCommissionOverrides={userCommissionOverrides}
|
|
||||||
roleCommissionOverrides={roleCommissionOverrides}
|
|
||||||
orgMembers={orgMembers}
|
|
||||||
totalReceived={teamFinancials.totalReceived}
|
|
||||||
grossProfit={teamFinancials.grossProfit}
|
|
||||||
onAddMember={addJobTeamMember}
|
|
||||||
onRemoveMember={removeJobTeamMember}
|
|
||||||
onUpdateMember={updateJobTeamMember}
|
|
||||||
/>
|
|
||||||
</SpotlightCard>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<span className="attribution-ghost">igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?</span>
|
<span className="attribution-ghost">igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user