fix(owner): replace native Team Management select with custom dark-mode-aware project picker

This commit is contained in:
Satyam Rastogi
2026-05-29 18:59:50 +05:30
parent 6a5b857de3
commit 94ddc1b030
2 changed files with 104 additions and 16 deletions
+43
View File
@@ -101,3 +101,46 @@ export function stormAttribution(kanbanLeads = [], projects = []) {
revenuePct: totalRevenue ? Math.round((stormRevenue / totalRevenue) * 100) : 0, revenuePct: totalRevenue ? Math.round((stormRevenue / totalRevenue) * 100) : 0,
}; };
} }
// Owner Financial-Overview metrics, each carrying the underlying line items so the dashboard
// cards can drill down to the real records. Pass `today` (ISO date) to flag payouts due soon.
// - collected: cash received from clients (paid paymentSchedule entries)
// - outstandingAR: client money still owed (unpaid paymentSchedule entries)
// - pendingPayouts: money owed to vendors (pending invoices); dueSoon = within 7 days of `today`
// - vendorSpend: cash paid out to vendors (paid invoices)
export function financialOverview(projects = [], today) {
const now = today ? new Date(today) : null;
const within7 = (d) => {
if (!now || !d) return false;
const diff = (new Date(d) - now) / 86400000;
return diff >= 0 && diff <= 7;
};
const collected = { total: 0, items: [] };
const outstandingAR = { total: 0, count: 0, items: [] };
const pendingPayouts = { total: 0, dueSoonCount: 0, dueSoonTotal: 0, items: [] };
const vendorSpend = { total: 0, items: [] };
for (const p of projects) {
const project = p.customerName || p.projectType || p.id;
for (const ps of p.paymentSchedule || []) {
if (ps.status === 'paid') {
collected.total += ps.amount || 0;
collected.items.push({ projectId: p.id, project, label: ps.milestone, amount: ps.amount || 0, date: ps.paidDate || ps.dueDate });
} else {
outstandingAR.total += ps.amount || 0;
outstandingAR.count += 1;
outstandingAR.items.push({ projectId: p.id, project, label: ps.milestone, amount: ps.amount || 0, dueDate: ps.dueDate });
}
}
for (const inv of p.invoices || []) {
if (inv.status === 'pending') {
pendingPayouts.total += inv.amount || 0;
pendingPayouts.items.push({ projectId: p.id, project, label: inv.submittedBy, amount: inv.amount || 0, dueDate: inv.dueDate });
if (within7(inv.dueDate)) { pendingPayouts.dueSoonCount += 1; pendingPayouts.dueSoonTotal += inv.amount || 0; }
} else if (inv.status === 'paid') {
vendorSpend.total += inv.amount || 0;
vendorSpend.items.push({ projectId: p.id, project, label: inv.submittedBy, amount: inv.amount || 0, date: inv.datePaid });
}
}
}
return { collected, outstandingAR, pendingPayouts, vendorSpend };
}
+61 -16
View File
@@ -1,4 +1,4 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; 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';
@@ -15,9 +15,63 @@ import {
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend
} from 'recharts'; } from 'recharts';
import { import {
Briefcase, TrendingUp, AlertTriangle, Shield, ChevronRight, ArrowUpRight, Users, ChevronDown Briefcase, TrendingUp, AlertTriangle, Shield, ChevronRight, ArrowUpRight, Users, ChevronDown, Check
} from 'lucide-react'; } from 'lucide-react';
// Custom dark-mode-aware project picker (replaces the native <select> whose option list
// renders white-on-white in dark mode). Mirrors the FilterPopover pattern used elsewhere.
function ProjectPicker({ projects, value, onChange }) {
const [open, setOpen] = useState(false);
const boxRef = useRef(null);
useEffect(() => {
if (!open) return;
const onClick = (e) => { if (boxRef.current && !boxRef.current.contains(e.target)) setOpen(false); };
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
document.addEventListener('mousedown', onClick);
document.addEventListener('keydown', onKey);
return () => { document.removeEventListener('mousedown', onClick); document.removeEventListener('keydown', onKey); };
}, [open]);
const labelFor = (p) => `${p.projectType}${p.address?.split(',')[0] || p.id}`;
const selected = projects.find(p => p.id === value);
return (
<div className="relative" ref={boxRef}>
<button
type="button"
aria-haspopup="listbox"
aria-expanded={open}
aria-label="Select project"
onClick={() => setOpen(o => !o)}
className="w-full sm:w-auto min-w-[220px] max-w-[300px] flex items-center justify-between gap-2 pl-3 pr-2 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:ring-2 focus:ring-violet-500/20 focus:border-violet-500/40 transition-colors"
>
<span className="truncate text-left">{selected ? labelFor(selected) : 'Select project'}</span>
<ChevronDown size={14} className={`shrink-0 text-zinc-400 transition-transform ${open ? 'rotate-180' : ''}`} />
</button>
{open && (
<div role="listbox" className="absolute z-30 right-0 mt-1 w-[300px] max-w-[80vw] rounded-xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-white/10 shadow-xl shadow-black/10 dark:shadow-black/50 overflow-hidden">
<div className="max-h-72 overflow-y-auto custom-scrollbar py-1">
{projects.map(p => {
const isSelected = p.id === value;
return (
<button
key={p.id}
type="button"
role="option"
aria-selected={isSelected}
onClick={() => { onChange(p.id); setOpen(false); }}
className={`w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-left transition-colors ${isSelected ? 'text-violet-600 dark:text-violet-400 bg-violet-50 dark:bg-violet-500/10' : 'text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-white/5'}`}
>
<span className="truncate">{labelFor(p)}</span>
{isSelected && <Check size={12} strokeWidth={2.5} className="shrink-0 opacity-70" />}
</button>
);
})}
</div>
</div>
)}
</div>
);
}
const OwnerSnapshot = () => { const OwnerSnapshot = () => {
const { user } = useAuth(); const { user } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -491,20 +545,11 @@ const OwnerSnapshot = () => {
<span className="w-2 h-8 bg-violet-500 rounded-full mr-3"></span> <span className="w-2 h-8 bg-violet-500 rounded-full mr-3"></span>
Team Management Team Management
</h2> </h2>
<div className="relative"> <ProjectPicker
<select projects={ownerProjects}
value={selectedProject.id} value={selectedProject.id}
onChange={e => setSelectedProjectId(e.target.value)} onChange={setSelectedProjectId}
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> </div>
<SpotlightCard className="p-5 lg:p-6" spotlightColor="rgba(139, 92, 246, 0.06)"> <SpotlightCard className="p-5 lg:p-6" spotlightColor="rgba(139, 92, 246, 0.06)">
<TeamManagementPanel <TeamManagementPanel