From cd8b3c5606d1dc4fccee181689f07848bf8189b9 Mon Sep 17 00:00:00 2001 From: Ken D Date: Sat, 22 Nov 2025 11:39:46 -0500 Subject: [PATCH 01/23] feat(operations): add Operations Dashboard page and navigation - New page: src/pages/OperationsDashboard.tsx per HANDOFF specs - SVG district map with patrol/incident markers and legend - Live service request feed with filters and expandable details - Performance metrics bar (teams, open requests, response time, activity) - Ask Plexi integration bar at bottom - Added route /operations and sidebar link --- src/App.tsx | 2 + src/components/NavigationSidebar.tsx | 1 + src/pages/OperationsDashboard.tsx | 274 +++++++++++++++++++++++++++ 3 files changed, 277 insertions(+) create mode 100644 src/pages/OperationsDashboard.tsx diff --git a/src/App.tsx b/src/App.tsx index bdff614..cd52667 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import PlaceholderPage from './components/PlaceholderPage'; import AskPlexiInterface from './components/AskPlexiInterface'; import ExecutiveFeed from './features/executive/ExecutiveFeed'; import FieldView from './features/field/FieldView'; +import OperationsDashboard from './pages/OperationsDashboard'; /** * Main App Component - Phase 1 Navigation @@ -24,6 +25,7 @@ const App: React.FC = () => { } /> } /> + } /> } /> } /> diff --git a/src/components/NavigationSidebar.tsx b/src/components/NavigationSidebar.tsx index fcf2deb..6e54081 100644 --- a/src/components/NavigationSidebar.tsx +++ b/src/components/NavigationSidebar.tsx @@ -25,6 +25,7 @@ const NavigationSidebar: React.FC = () => { title: 'AI REPORTS', items: [ { path: '/home', label: 'Home', icon: Home }, + { path: '/operations', label: 'Operations', icon: Activity }, { path: '/ask-plexi', label: 'Ask Plexi', diff --git a/src/pages/OperationsDashboard.tsx b/src/pages/OperationsDashboard.tsx new file mode 100644 index 0000000..a1833aa --- /dev/null +++ b/src/pages/OperationsDashboard.tsx @@ -0,0 +1,274 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { MapPin, AlertTriangle, Wrench, Calendar, Filter } from 'lucide-react'; + +type RequestType = 'Security' | 'Maintenance' | 'Events' | 'Other'; +type Priority = 'high' | 'medium' | 'low'; + +interface ServiceRequest { + id: number; + type: RequestType; + priority: Priority; + title: string; + location: string; + timestamp: Date; + assignedTo: string; + status: 'New' | 'Dispatched' | 'In Progress' | 'Completed'; + details?: string; +} + +interface PatrolMarker { + id: string; + x: number; // 0-100 as percentage of width + y: number; // 0-100 as percentage of height + label: string; + type: 'patrol' | 'incident'; +} + +const nowMinus = (mins: number) => new Date(Date.now() - mins * 60 * 1000); + +const initialRequests: ServiceRequest[] = [ + { id: 1, type: 'Security', priority: 'high', title: 'Unattended package reported', location: 'K St NW & 16th St', timestamp: nowMinus(12), assignedTo: 'Unit 3', status: 'In Progress' }, + { id: 2, type: 'Maintenance', priority: 'medium', title: 'Sidewalk damage - trip hazard', location: '1800 block of I St NW', timestamp: nowMinus(65), assignedTo: 'Maintenance Team B', status: 'Dispatched' }, + { id: 3, type: 'Security', priority: 'low', title: 'Wellness check requested', location: 'Connecticut Ave & L St NW', timestamp: nowMinus(5), assignedTo: 'Unit 1', status: 'In Progress' }, + { id: 4, type: 'Events', priority: 'low', title: 'Street performance permit inquiry', location: 'Farragut Square', timestamp: nowMinus(140), assignedTo: 'Events Team', status: 'Completed' }, + { id: 5, type: 'Maintenance', priority: 'medium', title: 'Graffiti reported on utility box', location: '1700 K St NW', timestamp: nowMinus(28), assignedTo: 'Maintenance Team A', status: 'Dispatched' }, + { id: 6, type: 'Security', priority: 'high', title: 'Traffic obstruction - delivery truck', location: '19th St NW & I St', timestamp: nowMinus(9), assignedTo: 'Unit 2', status: 'In Progress' }, + { id: 7, type: 'Other', priority: 'low', title: 'Streetlight flickering', location: 'Pennsylvania Ave NW & 20th St', timestamp: nowMinus(210), assignedTo: 'DC DDOT', status: 'Dispatched' }, + { id: 8, type: 'Events', priority: 'medium', title: 'Stage setup coordination', location: 'Golden Triangle Plaza', timestamp: nowMinus(33), assignedTo: 'Events Team', status: 'In Progress' }, + { id: 9, type: 'Maintenance', priority: 'low', title: 'Litter pickup request', location: 'Connecticut Ave median', timestamp: nowMinus(7), assignedTo: 'Maintenance Team C', status: 'In Progress' }, + { id: 10, type: 'Security', priority: 'medium', title: 'Loitering complaint', location: '1800 Penn Ave NW', timestamp: nowMinus(52), assignedTo: 'Unit 4', status: 'Dispatched' }, +]; + +const patrolMarkers: PatrolMarker[] = [ + { id: 'p1', x: 25, y: 35, label: 'Unit 1', type: 'patrol' }, + { id: 'p2', x: 60, y: 40, label: 'Unit 2', type: 'patrol' }, + { id: 'p3', x: 45, y: 65, label: 'Unit 3', type: 'patrol' }, + { id: 'i1', x: 52, y: 48, label: 'Incident', type: 'incident' }, + { id: 'i2', x: 30, y: 58, label: 'Incident', type: 'incident' }, +]; + +const classNames = (...parts: Array) => parts.filter(Boolean).join(' '); + +const OperationsDashboard: React.FC = () => { + const navigate = useNavigate(); + const [requests, setRequests] = useState(initialRequests); + const [filter, setFilter] = useState('All'); + const [expandedId, setExpandedId] = useState(null); + + // Simulate live timestamps by forcing a re-render every 60s + useEffect(() => { + const t = setInterval(() => setRequests(r => [...r]), 60000); + return () => clearInterval(t); + }, []); + + const filtered = useMemo(() => { + return filter === 'All' ? requests : requests.filter(r => r.type === filter); + }, [requests, filter]); + + const stat = { + activeTeams: 15, + openRequests: requests.filter(r => r.status !== 'Completed').length, + responseMins: 12, + todaysActivity: 47, + }; + + const timeAgo = (d: Date) => { + const mins = Math.max(0, Math.floor((Date.now() - d.getTime()) / 60000)); + if (mins < 1) return 'just now'; + if (mins < 60) return `${mins} min ago`; + const hrs = Math.floor(mins / 60); + return `${hrs} hr${hrs > 1 ? 's' : ''} ago`; + }; + + return ( +
+ {/* Header */} +
+

Operations Dashboard

+
+
+ + Live +
+
+ +
+ {(['All','Security','Maintenance','Events'] as const).map(t => ( + + ))} +
+
+
+
+ + {/* Main Grid */} +
+ {/* Map + Metrics (span 2) */} +
+ {/* Map Card */} +
+
+
+

Golden Triangle BID – District Map

+

Boundaries: Connecticut Ave ↔ 16th St NW, K St ↔ Pennsylvania Ave

+
+
+ Patrol + Incident +
+
+
+ + {/* Background grid for subtle map feel */} + + + + + + + {/* Golden Triangle-ish polygon */} + + {/* Simple patrol routes */} + + + {/* Markers */} + {patrolMarkers.map(m => ( + + + {m.label} + + ))} + +
+
+ + {/* Metrics */} +
+
+
Active Field Teams
+
{stat.activeTeams}
+
+
+
Open Service Requests
+
{stat.openRequests}
+
+
+
Avg Response Time
+
{stat.responseMins} min
+
+
+
Today's Activity
+
{stat.todaysActivity}
+
+
+
+ + {/* Service Request Feed */} +
+
+
+

Service Request Feed

+ +
+
    + {filtered.map(req => { + const color = req.priority === 'high' ? 'bg-red-500' : req.priority === 'medium' ? 'bg-amber-500' : 'bg-green-500'; + const icon = req.type === 'Security' ? AlertTriangle : req.type === 'Maintenance' ? Wrench : Calendar; + const Icon = icon; + const isExpanded = expandedId === req.id; + return ( +
  • + +
  • + ); + })} +
+
+ + {/* Ask Plexi integration */} +
+
+

Ask Plexi

+ +
+
{ + e.preventDefault(); + const form = e.currentTarget as HTMLFormElement; + const fd = new FormData(form); + const q = String(fd.get('q') || '').trim(); + navigate(q ? `/ask-plexi?q=${encodeURIComponent(q)}` : '/ask-plexi'); + }} + className="flex gap-2" + > + + +
+
+
+
+
+ ); +}; + +export default OperationsDashboard; From 12d4992c98116a314a10740eb03318148caa50ed Mon Sep 17 00:00:00 2001 From: Ken D Date: Sat, 22 Nov 2025 11:58:18 -0500 Subject: [PATCH 02/23] feat(assessments): add Assessment Management page, route, and nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Top metrics (billed, collected, outstanding) with progress bar - Search, filter (All/Paid/Pending/Overdue), sortable columns - 25‑row demo table with realistic Golden Triangle BID data - Row click opens payment history detail modal - Assessment Calculator modal (.18/sq ft) with type selection - Added /assessments route and sidebar item --- src/App.tsx | 2 + src/components/NavigationSidebar.tsx | 3 +- src/pages/AssessmentManagement.tsx | 318 +++++++++++++++++++++++++++ 3 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 src/pages/AssessmentManagement.tsx diff --git a/src/App.tsx b/src/App.tsx index cd52667..401195c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import AskPlexiInterface from './components/AskPlexiInterface'; import ExecutiveFeed from './features/executive/ExecutiveFeed'; import FieldView from './features/field/FieldView'; import OperationsDashboard from './pages/OperationsDashboard'; +import AssessmentManagement from './pages/AssessmentManagement'; /** * Main App Component - Phase 1 Navigation @@ -26,6 +27,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> diff --git a/src/components/NavigationSidebar.tsx b/src/components/NavigationSidebar.tsx index 6e54081..d12d194 100644 --- a/src/components/NavigationSidebar.tsx +++ b/src/components/NavigationSidebar.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useLocation, Link } from 'react-router-dom'; import { Home, Upload, Folder, Grid, Settings, - BarChart, Bell, Activity, ChevronLeft, Menu + BarChart, Bell, Activity, ChevronLeft, Menu, Table } from 'lucide-react'; const NavigationSidebar: React.FC = () => { @@ -26,6 +26,7 @@ const NavigationSidebar: React.FC = () => { items: [ { path: '/home', label: 'Home', icon: Home }, { path: '/operations', label: 'Operations', icon: Activity }, + { path: '/assessments', label: 'Assessments', icon: Table }, { path: '/ask-plexi', label: 'Ask Plexi', diff --git a/src/pages/AssessmentManagement.tsx b/src/pages/AssessmentManagement.tsx new file mode 100644 index 0000000..191016c --- /dev/null +++ b/src/pages/AssessmentManagement.tsx @@ -0,0 +1,318 @@ +import React, { useMemo, useState } from 'react'; + +type PropertyType = 'Office' | 'Retail' | 'Mixed'; +type Status = 'Paid' | 'Pending' | 'Overdue'; + +interface PropertyRow { + id: number; + address: string; + owner: string; + sqft: number; + assessedAmount: number; // USD + collectedAmount: number; // USD + dueDate: string; // ISO date + paidDate?: string; // ISO date + type: PropertyType; +} + +const DOLLAR = (v: number) => v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }); + +const RATE_PER_SQFT = 0.18; // Golden Triangle BID rate + +const sample: PropertyRow[] = [ + { id: 1, address: '1850 K Street NW', owner: 'Capitol Properties LLC', sqft: 125000, assessedAmount: 125000, collectedAmount: 125000, dueDate: '2025-10-01', paidDate: '2025-09-28', type: 'Office' }, + { id: 2, address: '1800 K Street NW', owner: 'Midtown Holdings Inc.', sqft: 98500, assessedAmount: 98500, collectedAmount: 98500, dueDate: '2025-10-01', paidDate: '2025-09-25', type: 'Office' }, + { id: 3, address: '1776 K Street NW', owner: 'National Liberty Partners', sqft: 112000, assessedAmount: 112000, collectedAmount: 0, dueDate: '2025-10-01', type: 'Office' }, + { id: 4, address: '1900 L Street NW', owner: 'Beacon Square REIT', sqft: 87500, assessedAmount: 87500, collectedAmount: 87500, dueDate: '2025-10-01', paidDate: '2025-09-29', type: 'Office' }, + { id: 5, address: '1700 K Street NW', owner: 'Cityline Retail Group', sqft: 54000, assessedAmount: 54000, collectedAmount: 27000, dueDate: '2025-10-15', type: 'Retail' }, + { id: 6, address: '1600 I Street NW', owner: 'District Mixed Ventures', sqft: 102000, assessedAmount: 102000, collectedAmount: 102000, dueDate: '2025-10-01', paidDate: '2025-09-27', type: 'Mixed' }, + { id: 7, address: '1500 I Street NW', owner: 'Federal Square Estates', sqft: 96000, assessedAmount: 96000, collectedAmount: 96000, dueDate: '2025-10-01', paidDate: '2025-09-28', type: 'Office' }, + { id: 8, address: '2000 Pennsylvania Ave NW', owner: 'Penn Ave Holdings', sqft: 130000, assessedAmount: 130000, collectedAmount: 0, dueDate: '2025-10-01', type: 'Office' }, + { id: 9, address: 'Connecticut Ave NW 1701', owner: 'Northeast Capital', sqft: 88000, assessedAmount: 88000, collectedAmount: 44000, dueDate: '2025-10-10', type: 'Office' }, + { id: 10, address: '1901 L Street NW', owner: 'Beacon Square REIT', sqft: 74000, assessedAmount: 74000, collectedAmount: 74000, dueDate: '2025-10-01', paidDate: '2025-09-30', type: 'Office' }, + // Duplicate patterns to reach 25 demo rows + { id: 11, address: '1725 K Street NW', owner: 'Capital Ridge Partners', sqft: 82000, assessedAmount: 82000, collectedAmount: 82000, dueDate: '2025-10-01', paidDate: '2025-09-29', type: 'Office' }, + { id: 12, address: '1101 19th St NW', owner: 'Urban Retail Trust', sqft: 51000, assessedAmount: 51000, collectedAmount: 25500, dueDate: '2025-10-15', type: 'Retail' }, + { id: 13, address: '1717 Pennsylvania Ave NW', owner: 'Penn Ave Holdings', sqft: 120000, assessedAmount: 120000, collectedAmount: 120000, dueDate: '2025-10-01', paidDate: '2025-09-24', type: 'Office' }, + { id: 14, address: '1601 K Street NW', owner: 'Midtown Holdings Inc.', sqft: 91000, assessedAmount: 91000, collectedAmount: 91000, dueDate: '2025-10-01', paidDate: '2025-09-28', type: 'Office' }, + { id: 15, address: '1401 I Street NW', owner: 'Cityline Retail Group', sqft: 47000, assessedAmount: 47000, collectedAmount: 0, dueDate: '2025-10-20', type: 'Retail' }, + { id: 16, address: '2001 K Street NW', owner: 'District Mixed Ventures', sqft: 101000, assessedAmount: 101000, collectedAmount: 101000, dueDate: '2025-10-01', paidDate: '2025-09-29', type: 'Mixed' }, + { id: 17, address: '1999 K Street NW', owner: 'Beacon Square REIT', sqft: 88000, assessedAmount: 88000, collectedAmount: 88000, dueDate: '2025-10-01', paidDate: '2025-09-28', type: 'Office' }, + { id: 18, address: '1501 K Street NW', owner: 'Northeast Capital', sqft: 86000, assessedAmount: 86000, collectedAmount: 43000, dueDate: '2025-10-10', type: 'Office' }, + { id: 19, address: '1750 Pennsylvania Ave NW', owner: 'Penn Ave Holdings', sqft: 135000, assessedAmount: 135000, collectedAmount: 0, dueDate: '2025-10-01', type: 'Office' }, + { id: 20, address: '1099 18th St NW', owner: 'Urban Retail Trust', sqft: 60000, assessedAmount: 60000, collectedAmount: 60000, dueDate: '2025-10-01', paidDate: '2025-09-26', type: 'Retail' }, + { id: 21, address: '1750 K Street NW', owner: 'Capital Ridge Partners', sqft: 90000, assessedAmount: 90000, collectedAmount: 90000, dueDate: '2025-10-01', paidDate: '2025-09-30', type: 'Office' }, + { id: 22, address: '1001 17th St NW', owner: 'Cityline Retail Group', sqft: 45000, assessedAmount: 45000, collectedAmount: 22500, dueDate: '2025-10-15', type: 'Retail' }, + { id: 23, address: '1701 L Street NW', owner: 'District Mixed Ventures', sqft: 99000, assessedAmount: 99000, collectedAmount: 99000, dueDate: '2025-10-01', paidDate: '2025-09-27', type: 'Mixed' }, + { id: 24, address: '1200 18th St NW', owner: 'Midtown Holdings Inc.', sqft: 78000, assessedAmount: 78000, collectedAmount: 78000, dueDate: '2025-10-01', paidDate: '2025-09-29', type: 'Office' }, + { id: 25, address: '1100 19th St NW', owner: 'Northeast Capital', sqft: 83000, assessedAmount: 83000, collectedAmount: 0, dueDate: '2025-10-01', type: 'Office' }, +]; + +type SortKey = keyof Pick; + +const AssessmentManagement: React.FC = () => { + const [query, setQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState<'All' | Status>('All'); + const [sortKey, setSortKey] = useState('address'); + const [sortAsc, setSortAsc] = useState(true); + const [calcOpen, setCalcOpen] = useState(false); + const [calcSqft, setCalcSqft] = useState(100000); + const [calcType, setCalcType] = useState('Office'); + const [detailId, setDetailId] = useState(null); + + const today = new Date(); + + const statusOf = (row: PropertyRow): Status => { + if (row.collectedAmount >= row.assessedAmount) return 'Paid'; + const due = new Date(row.dueDate); + return due.getTime() < today.getTime() ? 'Overdue' : 'Pending'; + }; + + const rows = useMemo(() => { + const base = sample + .filter(r => + [r.address, r.owner].some(x => x.toLowerCase().includes(query.toLowerCase())) + ) + .filter(r => (statusFilter === 'All' ? true : statusOf(r) === statusFilter)); + const sorted = [...base].sort((a, b) => { + const A = a[sortKey]; + const B = b[sortKey]; + const res = typeof A === 'number' && typeof B === 'number' + ? A - B + : String(A).localeCompare(String(B)); + return sortAsc ? res : -res; + }); + return sorted; + }, [query, statusFilter, sortKey, sortAsc]); + + const totals = useMemo(() => { + const billed = 8200000; // Q4 billed (from spec) + const collected = 7800000; + const outstanding = billed - collected; + const rate = (collected / billed) * 100; + return { billed, collected, outstanding, rate }; + }, []); + + const changeSort = (key: SortKey) => { + if (key === sortKey) setSortAsc(!sortAsc); + else { setSortKey(key); setSortAsc(true); } + }; + + const calcAmount = Math.max(0, Math.round(calcSqft * RATE_PER_SQFT)); + + return ( +
+ {/* Header */} +
+

Assessment Management

+
+ +
+
+ + {/* Top cards */} +
+
+
Total Billed (Q4 2025)
+
{DOLLAR(totals.billed)}
+
+
+
Collected
+
{DOLLAR(totals.collected)}
+
+
+
+
+
{totals.rate.toFixed(1)}% collection rate
+
+
+
+
Outstanding
+
{DOLLAR(totals.outstanding)}
+
4.9% outstanding
+
+
+ + {/* Search & Filters */} +
+
+
+ setQuery(e.target.value)} + placeholder="Search by address or owner..." + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" + /> +
+
+ + +
+
+
+ + {/* Table */} +
+
+ + + + + + + + + + + + + + {rows.map((r, idx) => { + const status = statusOf(r); + const statusClass = status === 'Paid' ? 'text-green-700 bg-green-100' : status === 'Pending' ? 'text-amber-700 bg-amber-100' : 'text-red-700 bg-red-100'; + return ( + setDetailId(r.id)}> + + + + + + + + + ); + })} + +
# changeSort('address')}>Property {sortKey==='address' && (sortAsc ? '▲' : '▼')} changeSort('owner')}>Owner {sortKey==='owner' && (sortAsc ? '▲' : '▼')}Sq Ft changeSort('assessedAmount')}>Assessed {sortKey==='assessedAmount' && (sortAsc ? '▲' : '▼')} changeSort('collectedAmount')}>Collected {sortKey==='collectedAmount' && (sortAsc ? '▲' : '▼')}Status
{idx + 1}{r.address}{r.owner}{r.sqft.toLocaleString()}{DOLLAR(r.assessedAmount)}{DOLLAR(r.collectedAmount)} + {status} +
+
+
+
Showing 1–25 of 800 properties
+
+ + 1 + + + +
+
+
+ + {/* Detail modal */} + {detailId !== null && (() => { + const r = sample.find(x => x.id === detailId)!; + const status = statusOf(r); + return ( +
setDetailId(null)}> +
e.stopPropagation()}> +
+
+

{r.address}

+
{r.owner}
+
+ +
+
+
+
+
Assessed
+
{DOLLAR(r.assessedAmount)}
+
+
+
Collected
+
{DOLLAR(r.collectedAmount)}
+
+
+
Due Date
+
{new Date(r.dueDate).toLocaleDateString()}
+
+
+
Status
+
{status}
+
+
+
+
Payment History (demo)
+
    + {r.paidDate ? ( +
  • Payment received on {new Date(r.paidDate).toLocaleDateString()} — {DOLLAR(r.collectedAmount)}
  • + ) : ( +
  • No payments recorded yet.
  • + )} +
+
+
+
+ +
+
+
+ ); + })()} + + {/* Calculator modal */} + {calcOpen && ( +
setCalcOpen(false)}> +
e.stopPropagation()}> +
+

Assessment Calculator

+ +
+
+
+
+ + setCalcSqft(Number(e.target.value))} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" + min={0} + /> +
+
+ + +
+
+
+
Estimated Annual Assessment
+
{DOLLAR(calcAmount)}
+
Rate: ${RATE_PER_SQFT.toFixed(2)}/sq ft • Type: {calcType}
+
+
+
+ + +
+
+
+ )} +
+ ); +}; + +export default AssessmentManagement; From 66dc402fcb3e974f167fcd392b06696ed77fd96a Mon Sep 17 00:00:00 2001 From: Ken D Date: Sat, 22 Nov 2025 12:08:08 -0500 Subject: [PATCH 03/23] feat(board): add Board Reporting page, route, and nav item; build validated (Droid-assisted) --- src/App.tsx | 2 + src/components/NavigationSidebar.tsx | 3 +- src/pages/BoardReporting.tsx | 246 +++++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 src/pages/BoardReporting.tsx diff --git a/src/App.tsx b/src/App.tsx index 401195c..bd1e2d7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import ExecutiveFeed from './features/executive/ExecutiveFeed'; import FieldView from './features/field/FieldView'; import OperationsDashboard from './pages/OperationsDashboard'; import AssessmentManagement from './pages/AssessmentManagement'; +import BoardReporting from './pages/BoardReporting'; /** * Main App Component - Phase 1 Navigation @@ -28,6 +29,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> diff --git a/src/components/NavigationSidebar.tsx b/src/components/NavigationSidebar.tsx index d12d194..b2d363b 100644 --- a/src/components/NavigationSidebar.tsx +++ b/src/components/NavigationSidebar.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useLocation, Link } from 'react-router-dom'; import { Home, Upload, Folder, Grid, Settings, - BarChart, Bell, Activity, ChevronLeft, Menu, Table + BarChart, Bell, Activity, ChevronLeft, Menu, Table, FileText } from 'lucide-react'; const NavigationSidebar: React.FC = () => { @@ -27,6 +27,7 @@ const NavigationSidebar: React.FC = () => { { path: '/home', label: 'Home', icon: Home }, { path: '/operations', label: 'Operations', icon: Activity }, { path: '/assessments', label: 'Assessments', icon: Table }, + { path: '/board-reports', label: 'Board Reports', icon: FileText }, { path: '/ask-plexi', label: 'Ask Plexi', diff --git a/src/pages/BoardReporting.tsx b/src/pages/BoardReporting.tsx new file mode 100644 index 0000000..d5a7ed4 --- /dev/null +++ b/src/pages/BoardReporting.tsx @@ -0,0 +1,246 @@ +import React, { useMemo, useState } from 'react'; + +type Period = 'Q3 2025' | 'Q4 2025' | 'Year to Date'; + +const boardMetrics = { + q3: { + safetyIncidents: 23, + revenue: 7900000, + expenses: 7650000, + serviceRequests: 892, + events: 42, + attendance: 11200, + mix: { security: 0.45, maintenance: 0.3, events: 0.15, other: 0.1 }, + responseMins: 13, + }, + q4: { + safetyIncidents: 18, + revenue: 8200000, + expenses: 7900000, + serviceRequests: 924, + events: 47, + attendance: 12500, + mix: { security: 0.45, maintenance: 0.3, events: 0.15, other: 0.1 }, + responseMins: 12, + }, +}; + +const DOLLAR = (v: number) => v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }); + +const Slide: React.FC<{ children: React.ReactNode; title: string }> = ({ children, title }) => ( +
+
+

{title}

+
+
+ {children} +
+
+); + +const BoardReporting: React.FC = () => { + const [period, setPeriod] = useState('Q4 2025'); + const [present, setPresent] = useState(false); + const m = useMemo(() => (period === 'Q3 2025' ? boardMetrics.q3 : boardMetrics.q4), [period]); + + const barPct = (value: number, max: number) => `${Math.min(100, Math.max(0, (value / max) * 100))}%`; + + const pieStyle = { + background: `conic-gradient(#3b82f6 0 ${m.mix.security * 360}deg, #10b981 ${m.mix.security * 360}deg ${(m.mix.security + m.mix.maintenance) * 360}deg, #f59e0b ${(m.mix.security + m.mix.maintenance) * 360}deg ${(m.mix.security + m.mix.maintenance + m.mix.events) * 360}deg, #94a3b8 ${(m.mix.security + m.mix.maintenance + m.mix.events) * 360}deg 360deg)` + } as React.CSSProperties; + + return ( +
+ {/* Top controls */} +
+

Board Reporting

+
+ {(['Q3 2025','Q4 2025','Year to Date'] as Period[]).map(p => ( + + ))} +
+ + + +
+
+ + {/* Slides container */} +
+ {/* Slide 1: Executive Summary */} + +
+
+

Golden Triangle BID — {period}

+

+ Q4 performance remained strong across operations, programming, and financials. + Safety incidents declined 15% quarter-over-quarter, events and attendance grew, + and assessment collections achieved a 95.1% rate. +

+
    +
  • Safety Incidents: ↓ 15%
  • +
  • Property Values: ↑ 8% (market trend)
  • +
  • Events Hosted: {m.events}
  • +
  • Assessment Collection: 95.1%
  • +
+
+
+
+
Service Requests
+
{m.serviceRequests}
+
Quarter total
+
+
+
Avg Response Time
+
{m.responseMins} min
+
Quarter average
+
+
+
Events
+
{m.events}
+
Hosted
+
+
+
Attendance
+
{m.attendance.toLocaleString()}
+
Total
+
+
+
+
+ + {/* Slide 2: Financial Overview */} + +
+
+
Revenue and Expenses
+
+ {/* Revenue Bar */} +
+
+ Revenue + {DOLLAR(m.revenue)} (102%) +
+
+
+
+
+ {/* Expenses Bar */} +
+
+ Expenses + {DOLLAR(m.expenses)} (97%) +
+
+
+
+
+ {/* Surplus */} +
+ Surplus + {DOLLAR(m.revenue - m.expenses)} +
+
+
Simple bars for demo; replace with charting lib later if needed.
+
+
+

Legend

+
    +
  • Revenue (Actual vs Budget)
  • +
  • Expenses (Actual vs Budget)
  • +
  • Budget baseline
  • +
+
+
+ + + {/* Slide 3: Operations Metrics */} + +
+
+
+
+
Security 45%
+
Maintenance 30%
+
Events 15%
+
Other 10%
+
+
+
+
+
Avg Response Time (minutes)
+
+
+
+
Lower is better. Current: {m.responseMins} min
+
+
+
+ + + {/* Slide 4: Events & Programming */} + +
+
+
Mini Calendar (demo)
+
+ {Array.from({ length: 28 }).map((_, i) => ( +
+ {i + 1} + {[2,5,9,14,20,26].includes(i) && ( +
Event
+ )} +
+ ))} +
+
+
+
+
Quarter Attendance
+
{m.attendance.toLocaleString()}
+
+

Top Events

+
    +
  • Farragut Fridays Concert — 2,300
  • +
  • Holiday Lighting Ceremony — 3,100
  • +
  • Outdoor Fitness Series — 1,800
  • +
+
+
+
+
+
+ + {/* Slide 5: Strategic Initiatives */} + +
+ {[{label:'K Street Beautification',pct:75,color:'bg-green-500'}, + {label:'Public WiFi Expansion',pct:100,color:'bg-blue-500'}, + {label:'Wayfinding Signage',pct:40,color:'bg-amber-500'}].map(item => ( +
+
+ {item.label} + {item.pct}% +
+
+
+
+
+ ))} +
Estimated completion timelines are illustrative for demo.
+
+ +
+
+ ); +}; + +export default BoardReporting; From b43c205d47e3829feddf0940616fdb3b09c0bb32 Mon Sep 17 00:00:00 2001 From: Ken D Date: Sat, 22 Nov 2025 18:44:49 -0500 Subject: [PATCH 04/23] style(nav): change sidebar background to #1f367d (Droid-assisted) --- src/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.css b/src/index.css index 2ade447..15eff00 100644 --- a/src/index.css +++ b/src/index.css @@ -150,7 +150,7 @@ .sidebar { @apply fixed left-0 top-0 h-full z-40 transition-all duration-300 ease-in-out flex flex-col; width: 252px; - background-color: #1e3a8a; + background-color: #1f367d; } .sidebar.collapsed { From 8043b782f2b2b8af7c90cc02f40923d2365cdb32 Mon Sep 17 00:00:00 2001 From: Ken D Date: Thu, 11 Dec 2025 15:19:05 -0500 Subject: [PATCH 05/23] feat(workspace): Phase 0 - wire project cards to Report Editor Workspace via zustand store and app-level overlay (Droid-assisted) --- src/components/ReportEditorWorkspace.tsx | 42 ++++++++++++++++++++++++ src/features/executive/ExecutiveFeed.tsx | 5 ++- src/store/workspaceStore.ts | 26 +++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/components/ReportEditorWorkspace.tsx create mode 100644 src/store/workspaceStore.ts diff --git a/src/components/ReportEditorWorkspace.tsx b/src/components/ReportEditorWorkspace.tsx new file mode 100644 index 0000000..ea8e2cd --- /dev/null +++ b/src/components/ReportEditorWorkspace.tsx @@ -0,0 +1,42 @@ +import React, { useEffect } from 'react'; + +interface ReportEditorWorkspaceProps { + projectId: string; + isOpen: boolean; + onClose: () => void; +} + +const ReportEditorWorkspace: React.FC = ({ projectId, isOpen, onClose }) => { + if (!isOpen) return null; + + // Close on ESC + useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, [onClose]); + + return ( +
+
+

Report Editor Workspace

+

Project ID: {projectId}

+

+ Placeholder for Phase 1 shell. This verifies project-specific opening. +

+
+ +
+
+
+ ); +}; + +export default ReportEditorWorkspace; \ No newline at end of file diff --git a/src/features/executive/ExecutiveFeed.tsx b/src/features/executive/ExecutiveFeed.tsx index 065b8cc..3ec6e04 100644 --- a/src/features/executive/ExecutiveFeed.tsx +++ b/src/features/executive/ExecutiveFeed.tsx @@ -3,8 +3,9 @@ import { useNavigate } from 'react-router-dom'; import { UnifiedDailyIntelligence } from '../../types'; import useReportStore from '../../store/reportStore'; import AudioPlayer from '../../components/AudioPlayer'; -import AudioNarrationService from '../../services/AudioNarrationService'; +import { useWorkspaceStore } from '../../store/workspaceStore'; +import AudioNarrationService from '../../services/AudioNarrationService'; /** * ExecutiveFeed Component * @@ -27,6 +28,8 @@ const ExecutiveFeed: React.FC = () => { const [lastUpdate, setLastUpdate] = useState(new Date()); const [audioService] = useState(() => new AudioNarrationService()); + const openWorkspace = useWorkspaceStore(state => state.openWorkspace); + // Refresh local reports whenever the store publishes new executive data useEffect(() => { console.log('📋 Operations Dashboard: Store updated with', executiveReports.length, 'reports'); diff --git a/src/store/workspaceStore.ts b/src/store/workspaceStore.ts new file mode 100644 index 0000000..e6f2090 --- /dev/null +++ b/src/store/workspaceStore.ts @@ -0,0 +1,26 @@ +import { create } from 'zustand'; + +interface WorkspaceState { + isOpen: boolean; + currentProjectId: string | null; + + openWorkspace: (projectId: string) => void; + closeWorkspace: () => void; +} + +export const useWorkspaceStore = create((set, get) => ({ + isOpen: false, + currentProjectId: null, + + openWorkspace: (projectId: string) => { + const current = get(); + if (current.isOpen && current.currentProjectId !== projectId) { + // Minimal handling for Phase 0; upgrade in later phases + // eslint-disable-next-line no-console + console.log(`Switching from project ${current.currentProjectId} to ${projectId}`); + } + set({ isOpen: true, currentProjectId: projectId }); + }, + + closeWorkspace: () => set({ isOpen: false, currentProjectId: null }), +})); \ No newline at end of file From 31379eac9e0328c7ceb929203638b861d6d1c6c7 Mon Sep 17 00:00:00 2001 From: Ken D Date: Thu, 11 Dec 2025 15:31:33 -0500 Subject: [PATCH 06/23] =?UTF-8?q?feat(workspace):=20Phase=201=20shell=20?= =?UTF-8?q?=E2=80=93=20header=20+=203-column=20layout=20(25/50/25),=20inde?= =?UTF-8?q?pendent=20scroll;=20wire=20View=20Full=20Report=20to=20open=20w?= =?UTF-8?q?orkspace=20(Droid-assisted)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ReportEditorWorkspace.tsx | 139 ++++++++++++++++++++--- src/features/executive/ExecutiveFeed.tsx | 2 +- 2 files changed, 126 insertions(+), 15 deletions(-) diff --git a/src/components/ReportEditorWorkspace.tsx b/src/components/ReportEditorWorkspace.tsx index ea8e2cd..142b433 100644 --- a/src/components/ReportEditorWorkspace.tsx +++ b/src/components/ReportEditorWorkspace.tsx @@ -19,20 +19,131 @@ const ReportEditorWorkspace: React.FC = ({ projectId }, [onClose]); return ( -
-
-

Report Editor Workspace

-

Project ID: {projectId}

-

- Placeholder for Phase 1 shell. This verifies project-specific opening. -

-
- +
+ {/* Workspace Container */} +
+ {/* Header Bar */} +
+
+
P
+

PlexifyBID Report Editor Workspace

+
+
+ + + + +
+
+ + {/* 3-Column Grid */} +
+ {/* LEFT PANEL - Inputs & Media Sources */} + + + {/* CENTER PANEL - Block Editor Canvas */} +
+

Project {projectId}: Executive Project Report

+
+
+ + + + + + +
+
+
+
+

Executive Summary

+

Block editor coming in Phase 3. Use this space to draft the executive summary

+
+
+

Critical Path Progress

+
    +
  • North Wing structural steel at 65% completion
  • +
  • MEP rough-in scheduled to begin next week
  • +
  • All quality inspections passed
  • +
+
+
+
Auto-save: Ready
+
+ + {/* RIGHT PANEL - AI Research Assistant */} +
diff --git a/src/features/executive/ExecutiveFeed.tsx b/src/features/executive/ExecutiveFeed.tsx index 3ec6e04..db171ad 100644 --- a/src/features/executive/ExecutiveFeed.tsx +++ b/src/features/executive/ExecutiveFeed.tsx @@ -388,7 +388,7 @@ const ExecutiveFeed: React.FC = () => {
From 34154b7be1783ee25df98804b28b25de20271df0 Mon Sep 17 00:00:00 2001 From: Ken D Date: Thu, 11 Dec 2025 15:38:03 -0500 Subject: [PATCH 07/23] feat(workspace:phase2): Add AudioBriefingCard, VideoSummaryCard, and draggable SourceMaterialsList; integrate in left panel (Droid-assisted) --- package.json | 10 +- src/components/ReportEditorWorkspace.tsx | 78 +++++-------- .../workspace/AudioBriefingCard.tsx | 106 ++++++++++++++++++ .../workspace/SourceMaterialsList.tsx | 71 ++++++++++++ src/components/workspace/VideoSummaryCard.tsx | 29 +++++ 5 files changed, 243 insertions(+), 51 deletions(-) create mode 100644 src/components/workspace/AudioBriefingCard.tsx create mode 100644 src/components/workspace/SourceMaterialsList.tsx create mode 100644 src/components/workspace/VideoSummaryCard.tsx diff --git a/package.json b/package.json index c74d0d2..daaf431 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,12 @@ "preview": "vite preview", "test": "echo \"No tests yet\"" }, - "keywords": ["BID", "business-improvement-district", "operations", "management"], + "keywords": [ + "BID", + "business-improvement-district", + "operations", + "management" + ], "author": "Ken D'Amato ", "license": "MIT", "description": "PlexifyBID - Operations Management Platform for Business Improvement Districts", @@ -17,6 +22,9 @@ "url": "https://github.com/Plexify-AI/plexifybid.git" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", "@types/uuid": "^10.0.0", diff --git a/src/components/ReportEditorWorkspace.tsx b/src/components/ReportEditorWorkspace.tsx index 142b433..c7971dd 100644 --- a/src/components/ReportEditorWorkspace.tsx +++ b/src/components/ReportEditorWorkspace.tsx @@ -1,4 +1,7 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; +import AudioBriefingCard from './workspace/AudioBriefingCard'; +import VideoSummaryCard from './workspace/VideoSummaryCard'; +import SourceMaterialsList, { MaterialItem } from './workspace/SourceMaterialsList'; interface ReportEditorWorkspaceProps { projectId: string; @@ -18,6 +21,13 @@ const ReportEditorWorkspace: React.FC = ({ projectId return () => window.removeEventListener('keydown', handleEsc); }, [onClose]); + const [materials, setMaterials] = useState([ + { id: 'm1', label: 'Daily Logs - Oct 9', meta: 'PDF' }, + { id: 'm2', label: 'RFI-H-042 Details', meta: 'RFI' }, + { id: 'm3', label: 'Site Photos (14)', meta: 'Images' }, + { id: 'm4', label: 'Centennial Tower Schedule Data', meta: 'XLSX' }, + ]); + return (
{/* Workspace Container */} @@ -42,55 +52,23 @@ const ReportEditorWorkspace: React.FC = ({ projectId @@ -110,7 +88,7 @@ const ReportEditorWorkspace: React.FC = ({ projectId

Executive Summary

-

Block editor coming in Phase 3. Use this space to draft the executive summary

+

Block editor coming in Phase 3. Use this space to draft the executive summary�

Critical Path Progress

@@ -129,7 +107,7 @@ const ReportEditorWorkspace: React.FC = ({ projectId

Plexify AI Assistant

- Ive drafted the executive summary based on todays field reports and the new video feed. Would you like me to expand on the steel erection delays? + I�ve drafted the executive summary based on today�s field reports and the new video feed. Would you like me to expand on the steel erection delays?
Yes, adjust the tone to be more urgent for external stakeholders. diff --git a/src/components/workspace/AudioBriefingCard.tsx b/src/components/workspace/AudioBriefingCard.tsx new file mode 100644 index 0000000..09d8c81 --- /dev/null +++ b/src/components/workspace/AudioBriefingCard.tsx @@ -0,0 +1,106 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; + +export interface AudioBriefingCardProps { + audioUrl: string; + duration: number; // seconds + chapters: Array<{ label: string; timestamp: number }>; // seconds +} + +const formatTime = (s: number) => { + const m = Math.floor(s / 60) + .toString() + .padStart(1, '0'); + const ss = Math.floor(s % 60) + .toString() + .padStart(2, '0'); + return `${m}:${ss}`; +}; + +const AudioBriefingCard: React.FC = ({ audioUrl, duration, chapters }) => { + const audioRef = useRef(null); + const [playing, setPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const [time, setTime] = useState(0); + + useEffect(() => { + const el = audioRef.current; + if (!el) return; + el.playbackRate = speed; + }, [speed]); + + useEffect(() => { + const el = audioRef.current; + if (!el) return; + const onTime = () => setTime(el.currentTime); + el.addEventListener('timeupdate', onTime); + return () => el.removeEventListener('timeupdate', onTime); + }, []); + + const percent = useMemo(() => (duration ? Math.min(100, (time / duration) * 100) : 0), [time, duration]); + + const toggle = () => { + const el = audioRef.current; + if (!el) return; + if (playing) { + el.pause(); + setPlaying(false); + } else { + el.play().then(() => setPlaying(true)).catch(() => setPlaying(false)); + } + }; + + const seek = (t: number) => { + const el = audioRef.current; + if (!el) return; + el.currentTime = Math.max(0, Math.min(duration, t)); + }; + + return ( +
+
Audio Briefing
+ +
diff --git a/src/index.css b/src/index.css index 15eff00..92ce277 100644 --- a/src/index.css +++ b/src/index.css @@ -687,4 +687,11 @@ .intelligence-card-footer { @apply p-3; } +} + +/* Print styles */ +@media print { + .sidebar { display: none !important; } + .main-content { margin: 0 !important; } + .no-print { display: none !important; } } \ No newline at end of file diff --git a/src/pages/ReportPrintView.tsx b/src/pages/ReportPrintView.tsx new file mode 100644 index 0000000..415e58a --- /dev/null +++ b/src/pages/ReportPrintView.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; + +type BlockType = 'h1' | 'h2' | 'p'; + +type Block = { + id: string; + type: BlockType; + text: string; + bold?: boolean; + italic?: boolean; + strike?: boolean; + citationIds?: string[]; +}; + +type Citation = { + id: string; + title: string; + source: string; + url?: string; +}; + +const ReportPrintView: React.FC = () => { + const { projectId } = useParams<{ projectId: string }>(); + const storageKey = useMemo(() => `workspace:project:${projectId}:blocks`, [projectId]); + const [blocks, setBlocks] = useState([]); + const [citations, setCitations] = useState([]); + + useEffect(() => { + try { + const raw = localStorage.getItem(storageKey); + if (raw) setBlocks(JSON.parse(raw) as Block[]); + const rc = localStorage.getItem(storageKey + ':citations'); + if (rc) setCitations(JSON.parse(rc) as Citation[]); + } catch {} + }, [storageKey]); + + const onPrint = () => window.print(); + const onCopy = async () => { + try { + await navigator.clipboard.writeText(window.location.href); + alert('Share link copied to clipboard'); + } catch {} + }; + + return ( +
+ {/* Controls (hidden in print) */} +
+ Exit + + +
+ +
+ {/* Cover/Header */} +
+
+

Executive Project Report

+
+
Project: {projectId}
+
{new Date().toLocaleDateString()}
+
+
+
+
+ + {/* Content */} +
+ {blocks.map((b) => { + const Tag: any = b.type === 'h1' ? 'h1' : b.type === 'h2' ? 'h2' : 'p'; + const style = `${b.bold ? 'font-semibold ' : ''}${b.italic ? 'italic ' : ''}${b.strike ? 'line-through ' : ''}`; + return ( + + {b.text} + {b.citationIds && b.citationIds.length > 0 ? ( + + {b.citationIds.map((cid) => { + const idx = citations.findIndex((c) => c.id === cid); + return ( + [{idx + 1}] + ); + })} + + ) : null} + + ); + })} +
+ + {/* References */} + {citations.length > 0 && ( +
+

References

+
    + {citations.map((c, i) => ( +
  1. + [{i + 1}] {c.title} — {c.source} + {c.url ? ( + <> + {' '} + + source + + + ) : null} +
  2. + ))} +
+
+ )} +
+
+ ); +}; + +export default ReportPrintView; From 7e06ff04802b578486c4b44cbac404a9230670d9 Mon Sep 17 00:00:00 2001 From: Ken D Date: Thu, 11 Dec 2025 16:35:01 -0500 Subject: [PATCH 12/23] fix(workspace): mount ReportEditorWorkspace in App so 'View Full Report' opens overlay (Droid-assisted) --- src/App.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index aadabc6..07889e9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,8 @@ import OperationsDashboard from './pages/OperationsDashboard'; import AssessmentManagement from './pages/AssessmentManagement'; import BoardReporting from './pages/BoardReporting'; import ReportPrintView from './pages/ReportPrintView'; +import ReportEditorWorkspace from './components/ReportEditorWorkspace'; +import { useWorkspaceStore } from './store/workspaceStore'; /** * Main App Component - Phase 1 Navigation @@ -17,6 +19,10 @@ import ReportPrintView from './pages/ReportPrintView'; * with professional sidebar navigation system */ const App: React.FC = () => { + const isOpen = useWorkspaceStore(s => s.isOpen); + const currentProjectId = useWorkspaceStore(s => s.currentProjectId); + const closeWorkspace = useWorkspaceStore(s => s.closeWorkspace); + return (
@@ -81,6 +87,13 @@ const App: React.FC = () => { } /> + + {/* Workspace Overlay */} +
); From 2aa23a7cf4ba8084a4da0f7d0ae7f4fb62af49a5 Mon Sep 17 00:00:00 2001 From: Ken D Date: Thu, 11 Dec 2025 17:08:35 -0500 Subject: [PATCH 13/23] style: add shared .card and .btn-sm utilities (Droid-assisted) --- src/index.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/index.css b/src/index.css index 92ce277..6976301 100644 --- a/src/index.css +++ b/src/index.css @@ -25,6 +25,16 @@ /* Custom components */ @layer components { + /* Generic card wrapper for consistent panels */ + .card { + @apply bg-white rounded-xl border border-gray-200 shadow-sm; + } + + /* Small button size helper */ + .btn-sm { + @apply px-2 py-1 text-sm; + } + /* Intelligence Card styling */ .intelligence-card { @apply bg-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 border border-gray-100 overflow-hidden; From 8fda2f39c9bc9720e2649b3920d73aaf6cb8f569 Mon Sep 17 00:00:00 2001 From: Ken D Date: Thu, 11 Dec 2025 17:11:11 -0500 Subject: [PATCH 14/23] style(workspace): apply brand styles across workspace (header #1f367d, shared .card, btn-*; lucide icons; consistent borders) (Droid-assisted) --- src/components/ReportEditorWorkspace.tsx | 43 +++++++++++-------- .../workspace/AudioBriefingCard.tsx | 18 ++++---- .../workspace/SourceMaterialsList.tsx | 6 +-- src/components/workspace/VideoSummaryCard.tsx | 2 +- .../workspace/editor/BlockEditor.tsx | 21 ++++----- 5 files changed, 50 insertions(+), 40 deletions(-) diff --git a/src/components/ReportEditorWorkspace.tsx b/src/components/ReportEditorWorkspace.tsx index 4d8a04d..89a9df0 100644 --- a/src/components/ReportEditorWorkspace.tsx +++ b/src/components/ReportEditorWorkspace.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { HelpCircle, FileText, Save, Share2, Download, X, Paperclip, Image as ImageIcon, Send } from 'lucide-react'; import AudioBriefingCard from './workspace/AudioBriefingCard'; import VideoSummaryCard from './workspace/VideoSummaryCard'; import SourceMaterialsList, { MaterialItem } from './workspace/SourceMaterialsList'; @@ -36,18 +37,24 @@ const ReportEditorWorkspace: React.FC = ({ projectId {/* Workspace Container */}
{/* Header Bar */} -
+
P

PlexifyBID Report Editor Workspace

- - - + + + + -
{/* 3-Column Grid */}
{/* LEFT PANEL - Inputs & Media Sources */} -