From 91045a1897f0f7c0eb2e5883801792d26e9f3173 Mon Sep 17 00:00:00 2001 From: Krao Hasanee Date: Thu, 2 Jul 2026 09:39:54 -0400 Subject: [PATCH] =?UTF-8?q?redesign:=20rename=20Project=E2=86=92Client,=20?= =?UTF-8?q?finance=20single-card=20+=20filters,=20mobile=20tasks,=20sticky?= =?UTF-8?q?=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename "Project" → "Client" across UI (labels + /projects→/clients routes with redirect); normalize company-meaning labels to "Company" - Finance tabs: merge dual cards into one per tab, add header column filters (expenses/subs/invoices), overview as 3-section card, move +Expense/+Invoice to card right edge - Tasks: responsive mobile stats grid, progressive column hiding (min Status+Name+Assigned), sidebar mobile footer (profile/theme/signout) - Hide Company column for single-company users; +Task shows disabled Company field instead of hiding - Sticky table headers site-wide with opaque card background - Pin dev server to port 5173 Co-Authored-By: Claude Opus 4.8 --- src/App.jsx | 10 +- src/components/Layout.jsx | 35 ++- src/components/RequestForm.jsx | 16 +- src/components/SortTh.jsx | 3 +- src/index.css | 26 ++- src/pages/BrandBook.jsx | 16 +- src/pages/Companies.jsx | 14 +- src/pages/CompanyDetail.jsx | 16 +- src/pages/Profile.jsx | 6 +- src/pages/ProjectDetail.jsx | 209 ++++++++++------- src/pages/TaskDetail.jsx | 4 +- src/pages/Tasks.jsx | 91 +++++--- src/pages/team/TeamCreateSubcontractorPO.jsx | 6 +- src/pages/team/TeamDashboard.jsx | 8 +- src/pages/team/TeamInvoiceDetail.jsx | 2 +- src/pages/team/TeamInvoices.jsx | 226 ++++++++++--------- src/pages/team/TeamReports.jsx | 10 +- src/pages/team/TeamSubcontractorPODetail.jsx | 10 +- vite.config.js | 2 +- 19 files changed, 432 insertions(+), 278 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index b5fd035..7e108b3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -64,7 +64,12 @@ const ClientMyInvoices = lazy(() => import('./pages/client/ClientMyInvoices')); function RedirectProjectDetail() { const { id } = useParams(); - return ; + return ; +} + +function RedirectClientDetail() { + const { id } = useParams(); + return ; } function RedirectToTask() { @@ -99,7 +104,8 @@ export default function App() { } /> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx index c86c485..708a0af 100644 --- a/src/components/Layout.jsx +++ b/src/components/Layout.jsx @@ -42,7 +42,7 @@ function TeamNav({ onNav }) { const isCompaniesActive = location.pathname === '/company' && !location.search.includes('tab=users'); const isUsersActive = location.pathname === '/company' && location.search.includes('tab=users'); - const isTasksActive = location.pathname === '/tasks' || location.pathname.startsWith('/tasks/') || location.pathname.startsWith('/projects/') || location.pathname.startsWith('/requests/'); + const isTasksActive = location.pathname === '/tasks' || location.pathname.startsWith('/tasks/') || location.pathname.startsWith('/projects/') || location.pathname.startsWith('/clients/') || location.pathname.startsWith('/requests/'); return (
@@ -70,7 +70,7 @@ function TeamNav({ onNav }) { function ClientNav({ onNav }) { const location = useLocation(); - const isTasksActive = location.pathname === '/tasks' || location.pathname.startsWith('/tasks/') || location.pathname.startsWith('/projects/') || location.pathname.startsWith('/requests/'); + const isTasksActive = location.pathname === '/tasks' || location.pathname.startsWith('/tasks/') || location.pathname.startsWith('/projects/') || location.pathname.startsWith('/clients/') || location.pathname.startsWith('/requests/'); const links = [ { to: '/dashboard', label: 'Dashboard', icon: ICONS.dashboard }, { to: '/tasks', label: 'Tasks', icon: ICONS.requests }, @@ -90,7 +90,7 @@ function ClientNav({ onNav }) { function ExternalNav({ onNav }) { const location = useLocation(); - const isTasksActive = location.pathname === '/tasks' || location.pathname.startsWith('/tasks/') || location.pathname.startsWith('/projects/') || location.pathname.startsWith('/requests/'); + const isTasksActive = location.pathname === '/tasks' || location.pathname.startsWith('/tasks/') || location.pathname.startsWith('/projects/') || location.pathname.startsWith('/clients/') || location.pathname.startsWith('/requests/'); const links = [ { to: '/dashboard', label: 'Dashboard', icon: ICONS.dashboard }, { to: '/tasks', label: 'Tasks', icon: ICONS.requests }, @@ -143,7 +143,7 @@ export default function Layout({ children, loading = false }) { const timeOfDay = hour < 12 ? 'morning' : hour < 17 ? 'afternoon' : 'evening'; const firstName = currentUser?.name?.split(' ')[0] || ''; const isProfileRoute = location.pathname === '/profile' || location.pathname.startsWith('/profile/'); - const isTaskDetailRoute = location.pathname.startsWith('/tasks/') || location.pathname.startsWith('/projects/') || location.pathname.startsWith('/requests/'); + const isTaskDetailRoute = location.pathname.startsWith('/tasks/') || location.pathname.startsWith('/projects/') || location.pathname.startsWith('/clients/') || location.pathname.startsWith('/requests/'); const isInvoiceDetailRoute = location.pathname.startsWith('/finances/') || location.pathname.startsWith('/invoices/') || @@ -181,7 +181,7 @@ export default function Layout({ children, loading = false }) { : isReportsRoute ? 'Track task versions, billing state and exportable summaries.' : isRequestsRoute && !isTaskDetailRoute - ? 'Browse and manage all tasks and projects.' + ? 'Browse and manage all tasks and clients.' : isTaskDetailRoute || isInvoiceDetailRoute ? null : "Here's what's happening today."; const detailBackTarget = isTaskDetailRoute ? '/tasks' @@ -265,6 +265,27 @@ export default function Layout({ children, loading = false }) { : setMenuOpen(false)} /> } +
+ + + +
+
@@ -313,9 +334,9 @@ export default function Layout({ children, loading = false }) { {!hasResults &&
No results for "{searchQuery}"
} {searchResults.projects.length > 0 && ( <> -
Projects
+
Clients
{searchResults.projects.map(p => ( -
{ navigate(`/projects/${p.id}`); setSearchQuery(''); setSearchOpen(false); }}> +
{ navigate(`/clients/${p.id}`); setSearchQuery(''); setSearchOpen(false); }}> {ICONS.projects} {p.name}
diff --git a/src/components/RequestForm.jsx b/src/components/RequestForm.jsx index 028f607..94e394c 100644 --- a/src/components/RequestForm.jsx +++ b/src/components/RequestForm.jsx @@ -85,6 +85,13 @@ export default function RequestForm({ const companyId = form.companyId || initialCompanyId; + // Single-company user: lock selection to their one company. + useEffect(() => { + if (companies.length === 1 && !form.companyId) { + setForm(f => ({ ...f, companyId: companies[0].id })); + } + }, [companies, form.companyId]); + useEffect(() => { if (!companyId) { setExistingProjects([]); setCompanyUsers([]); return; } Promise.all([ @@ -228,14 +235,19 @@ export default function RequestForm({ {companies.map(co => )}
+ ) : companies.length === 1 ? ( +
+ + +
) :
}
- + {lockedFields.includes('project') ? ( ) : isTypingProject ? (
- setNewProjectName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleAddProject(); } }} autoFocus style={{ ...modalInputStyle, flex: 1 }} /> + setNewProjectName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleAddProject(); } }} autoFocus style={{ ...modalInputStyle, flex: 1 }} />
diff --git a/src/components/SortTh.jsx b/src/components/SortTh.jsx index 50b9a2d..da0d272 100644 --- a/src/components/SortTh.jsx +++ b/src/components/SortTh.jsx @@ -1,7 +1,8 @@ -export default function SortTh({ col, children, sortKey, sortDir, onSort, style }) { +export default function SortTh({ col, children, sortKey, sortDir, onSort, style, className }) { const active = sortKey === col; return ( onSort(col)} style={{ cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap', ...style }} > diff --git a/src/index.css b/src/index.css index 89dca0c..c8b110c 100644 --- a/src/index.css +++ b/src/index.css @@ -30,6 +30,7 @@ --accent-hover: #e09510; /* Layout2: solid, no transparency. bg 90% K, cards 88% K */ --bg: #1a1a1a; + --page-bg-solid: #1a1a1a; --card-bg: #1f1f1f; --card-bg-2: #262626; /* Single source of truth for all card frames across the site. @@ -135,6 +136,7 @@ --bg: radial-gradient(560px circle at 58% -6%, rgba(78,78,78,0.42) 0%, rgba(78,78,78,0.29) 24%, rgba(78,78,78,0.16) 48%, rgba(78,78,78,0.06) 72%, rgba(78,78,78,0) 100%), linear-gradient(180deg, #ffffff 0%, #ffffff 42%, #ffffff 100%); + --page-bg-solid: #ffffff; --card-bg: rgba(0,0,0,0.02); --card-bg-2: rgba(0,0,0,0.08); --popup-bg: rgba(255,255,255,0.92); @@ -374,6 +376,8 @@ body::before, body::after { display: none; } .grid-card:hover { background: var(--card-bg-2); } [data-theme="light"] .grid-card:hover { background: #fafafa; } +.sidebar-mobile-footer { display: none; } + .sidebar-bottom { margin-top: auto; padding: 16px 8px 0; border-top: 1px solid var(--border); @@ -1082,7 +1086,7 @@ tbody tr:hover .table-link { position: sticky; top: 0; z-index: 3; - background: transparent !important; + background: linear-gradient(var(--card-bg), var(--card-bg)), var(--page-bg-solid) !important; } .table-scroll-fade { position: static; @@ -1659,6 +1663,13 @@ select option { background: #222; color: #fff; } } .sidebar.sidebar-open { left: 0; } + /* Account actions pinned to bottom of mobile sidebar */ + .sidebar-mobile-footer { + display: flex; flex-direction: column; gap: 4px; + margin-top: auto; padding: 12px 8px; + border-top: 1px solid var(--border); + } + /* Show overlay when menu open */ .sidebar-overlay { display: block; } @@ -1808,3 +1819,16 @@ button.section-tab-btn:focus-visible { @media (max-width: 560px) { .dash-stat-grid { grid-template-columns: 1fr; } } + +/* Tasks stats row — responsive 6→3→2→1 */ +.tasks-stat-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 24px; margin-bottom: 24px; } +@media (max-width: 1200px) { .tasks-stat-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; } } +@media (max-width: 768px) { .tasks-stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; } } +@media (max-width: 480px) { .tasks-stat-grid { grid-template-columns: 1fr; } } + +/* Tasks table progressive column hiding — keep Status, Name, Assigned at smallest */ +@media (max-width: 1200px) { .tcol-company { display: none; } } +@media (max-width: 1040px) { .tcol-due { display: none; } } +@media (max-width: 920px) { .tcol-rev { display: none; } } +@media (max-width: 820px) { .tcol-type { display: none; } } +@media (max-width: 720px) { .tcol-project { display: none; } } diff --git a/src/pages/BrandBook.jsx b/src/pages/BrandBook.jsx index d1b5e3e..18d0666 100644 --- a/src/pages/BrandBook.jsx +++ b/src/pages/BrandBook.jsx @@ -452,7 +452,7 @@ export default function BrandBook() { const handleSave = async () => { if (!bookInfo.clientName.trim()) { - setNotification({ type: 'error', msg: 'Please select a client.' }); + setNotification({ type: 'error', msg: 'Please select a company.' }); return; } setSaving(true); @@ -614,7 +614,7 @@ export default function BrandBook() { const handleGenerate = async () => { if (!bookInfo.clientName.trim()) { - setNotification({ type: 'error', msg: 'Please select a client.' }); + setNotification({ type: 'error', msg: 'Please select a company.' }); return; } setGenerating(true); @@ -682,7 +682,7 @@ export default function BrandBook() { const handleClientLogoUpload = async (e) => { const file = e.target.files[0]; if (!file) return; - if (!bookInfo.clientId) { setNotification({ type: 'error', msg: 'Select a client first.' }); return; } + if (!bookInfo.clientId) { setNotification({ type: 'error', msg: 'Select a company first.' }); return; } setUploadingClientLogo(true); const ext = file.name.split('.').pop().toLowerCase(); const path = `${bookInfo.clientId}/logo.${ext}`; @@ -801,13 +801,13 @@ export default function BrandBook() {
) : (
- +
NameRevisionSign Count - Client + CompanyUpdated @@ -893,7 +893,7 @@ export default function BrandBook() {
Brand Book Info
- +
+
Company @@ -396,7 +396,7 @@ function TeamCompanies() {
No users
) : (
-
+
Name @@ -450,7 +450,7 @@ function TeamCompanies() {
No subcontractors
) : (
-
+
Name @@ -517,7 +517,7 @@ function ClientCompanyList() {
No companies
) : (
-
+
Company diff --git a/src/pages/CompanyDetail.jsx b/src/pages/CompanyDetail.jsx index b357ae4..0bf7ab7 100644 --- a/src/pages/CompanyDetail.jsx +++ b/src/pages/CompanyDetail.jsx @@ -154,7 +154,7 @@ export default function CompanyDetail() { }; const handleDeleteCompany = async () => { - if (!window.confirm(`Delete "${company.name}"? This will permanently delete all projects, jobs, files, and data. This cannot be undone.`)) return; + if (!window.confirm(`Delete "${company.name}"? This will permanently delete all clients, jobs, files, and data. This cannot be undone.`)) return; await deleteCompanyData(id); navigate('/company'); }; @@ -309,7 +309,7 @@ export default function CompanyDetail() {
📁
{projects.length}
-
Projects
+
Clients
@@ -477,16 +477,16 @@ export default function CompanyDetail() {
{isTeam &&
} {showNewProject && (
-
New Project
+
New Client
- + No projects
+
No clients
) : (
{projects.map(project => { @@ -513,7 +513,7 @@ export default function CompanyDetail() { const done = projectTasks.filter(t => t.status === 'client_approved').length; return (
- +
{project.name}
@@ -529,7 +529,7 @@ export default function CompanyDetail() { type="button" onClick={() => handleDeleteProject(project)} style={{ background: 'none', border: 'none', borderLeft: '1px solid var(--border)', color: 'var(--danger, var(--danger-strong))', cursor: 'pointer', fontSize: 16, padding: '0 14px', alignSelf: 'stretch', display: 'flex', alignItems: 'center' }} - title="Delete project" + title="Delete client" >✕}
); diff --git a/src/pages/Profile.jsx b/src/pages/Profile.jsx index 3d7374a..a65e3db 100644 --- a/src/pages/Profile.jsx +++ b/src/pages/Profile.jsx @@ -392,7 +392,7 @@ export default function ProfilePage() { const ACTION_LABEL = { task_created: 'Task created', task_started: 'Task started', task_on_hold: 'Task put on hold', task_resumed: 'Task resumed', task_submitted: 'Task submitted', task_approved: 'Task approved', - project_created: 'Project created', request_submitted: 'Request submitted', revision_requested: 'Task rejected', + project_created: 'Client created', request_submitted: 'Request submitted', revision_requested: 'Task rejected', }; const toSentenceTitle = (value = '') => { @@ -443,7 +443,7 @@ export default function ProfilePage() { {rows.length === 0 ? (
No tasks in progress
) : ( -
+
@@ -616,7 +616,7 @@ export default function ProfilePage() {
-
Active Projects
+
Active Clients
{profileStats.activeProjects}
diff --git a/src/pages/ProjectDetail.jsx b/src/pages/ProjectDetail.jsx index 9ee287d..65b46bf 100644 --- a/src/pages/ProjectDetail.jsx +++ b/src/pages/ProjectDetail.jsx @@ -5,6 +5,7 @@ import PageLoader from '../components/PageLoader'; import ProfileAvatar from '../components/ProfileAvatar'; import StatusBadge from '../components/StatusBadge'; import SortTh from '../components/SortTh'; +import FilterDropdown from '../components/FilterDropdown'; import { supabase } from '../lib/supabase'; import { useAuth } from '../context/AuthContext'; import { logActivity } from '../lib/activityLog'; @@ -49,7 +50,11 @@ export default function ProjectDetailPage() { const [loading, setLoading] = useState(true); const [submissions, setSubmissions] = useState([]); const [deliveries, setDeliveries] = useState([]); - const [activeTab, setActiveTab] = useState('all'); + const [activeTab, setActiveTab] = useState('tasks'); + const [filterStatus, setFilterStatus] = useState('all'); + const [filterRevision, setFilterRevision] = useState('all'); + const [filterType, setFilterType] = useState('all'); + const [filterAssigned, setFilterAssigned] = useState('all'); const [editingName, setEditingName] = useState(false); const [nameVal, setNameVal] = useState(''); @@ -227,7 +232,7 @@ export default function ProjectDetailPage() { }; if (loading) return ; - if (!project) return

Project not found.

; + if (!project) return

Client not found.

; const rows = tasks.map(task => { const derived = getTaskDerivedState(task, submissions, deliveries); @@ -254,21 +259,51 @@ export default function ProjectDetailPage() { const doneStatuses = new Set(['client_approved', 'invoiced', 'paid']); const taskTabs = [ - { id: 'all', label: 'All Tasks' }, - { id: 'new_requests', label: 'New Requests' }, - { id: 'revisions', label: 'Revisions' }, - { id: 'in_progress', label: 'In Progress' }, - { id: 'on_hold', label: 'On Hold' }, - { id: 'client_review', label: 'In Review' }, - { id: 'completed', label: 'Completed' }, + { id: 'tasks', label: 'Tasks' }, + { id: 'completed', label: 'Completed' }, ...(isTeam ? [{ id: 'members', label: 'Subcontractors' }] : []), ]; - const visibleRows = activeTab === 'all' ? sortedRows - : activeTab === 'completed' ? sortedRows.filter(r => doneStatuses.has(r.status)) - : activeTab === 'new_requests' ? sortedRows.filter(r => r.status === 'not_started' && Number(r.version || 0) === 0) - : activeTab === 'revisions' ? sortedRows.filter(r => r.status === 'not_started' && Number(r.version || 0) >= 1) - : activeTab === 'members' ? [] - : sortedRows.filter(r => r.status === activeTab); + const tabBaseRows = activeTab === 'completed' + ? sortedRows.filter(r => doneStatuses.has(r.status)) + : sortedRows.filter(r => !doneStatuses.has(r.status)); + let visibleRows = tabBaseRows; + if (filterStatus !== 'all') visibleRows = visibleRows.filter(r => r.status === filterStatus); + if (filterRevision === 'new') visibleRows = visibleRows.filter(r => Number(r.version || 0) === 0); + else if (filterRevision === 'revision') visibleRows = visibleRows.filter(r => Number(r.version || 0) >= 1); + if (filterType !== 'all') visibleRows = visibleRows.filter(r => (r.serviceType || '') === filterType); + if (filterAssigned === 'unassigned') visibleRows = visibleRows.filter(r => !r.assignedTo); + else if (filterAssigned !== 'all') visibleRows = visibleRows.filter(r => r.assignedTo === filterAssigned); + + const STATUS_FILTER_OPTIONS = activeTab === 'completed' + ? [ + { value: 'all', label: 'All Statuses' }, + { value: 'client_approved', label: 'Approved' }, + { value: 'invoiced', label: 'Invoiced' }, + { value: 'paid', label: 'Paid' }, + ] + : [ + { value: 'all', label: 'All Statuses' }, + { value: 'not_started', label: 'Not Started' }, + { value: 'in_progress', label: 'In Progress' }, + { value: 'on_hold', label: 'On Hold' }, + { value: 'client_review', label: 'In Review' }, + ]; + const REVISION_FILTER_OPTIONS = [ + { value: 'all', label: 'All' }, + { value: 'new', label: 'New (R00)' }, + { value: 'revision', label: 'Revisions (R1+)' }, + ]; + const TYPE_FILTER_OPTIONS = [ + { value: 'all', label: 'All Types' }, + ...[...new Set(tabBaseRows.map(r => r.serviceType).filter(Boolean))].sort((a, b) => a.localeCompare(b)).map(t => ({ value: t, label: t })), + ]; + const ASSIGNED_FILTER_OPTIONS = [ + { value: 'all', label: 'All Assignees' }, + { value: 'unassigned', label: 'Unassigned' }, + ...[...new Map(tabBaseRows.filter(r => r.assignedTo).map(r => [r.assignedTo, r.assignedName || '—'])).entries()] + .sort((a, b) => a[1].localeCompare(b[1])) + .map(([id, name]) => ({ value: id, label: name })), + ]; const fmtDate = (d) => d ? new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'; @@ -345,13 +380,8 @@ export default function ProjectDetailPage() {
{(() => { const tabCounts = { - all: rows.length, - new_requests: rows.filter(r => r.status === 'not_started' && Number(r.version || 0) === 0).length, - revisions: rows.filter(r => r.status === 'not_started' && Number(r.version || 0) >= 1).length, - in_progress: rows.filter(r => r.status === 'in_progress').length, - on_hold: rows.filter(r => r.status === 'on_hold').length, - client_review: rows.filter(r => r.status === 'client_review').length, - completed: rows.filter(r => ['client_approved','invoiced','paid'].includes(r.status)).length, + tasks: rows.filter(r => !doneStatuses.has(r.status)).length, + completed: rows.filter(r => doneStatuses.has(r.status)).length, members: members.filter(m => m.profile?.role === 'external').length, }; return ( @@ -360,7 +390,7 @@ export default function ProjectDetailPage() { const count = tabCounts[tab.id] ?? 0; const active = activeTab === tab.id; return ( - @@ -373,67 +403,82 @@ export default function ProjectDetailPage() { ); })()} -
+
{activeTab !== 'members' && ( - visibleRows.length === 0 - ?
No tasks
- :
-
- - - - - - - - - - - - R# - Name - Assigned - Priority - Task Type - Deadline - Status +
+
+ + + + + + + + + + + + + Name + + + Due + + + + {visibleRows.length === 0 ? ( + + ) : visibleRows.map(row => { + const avatarStyle = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 26, height: 26, borderRadius: '50%', overflow: 'hidden', verticalAlign: 'middle', flexShrink: 0 }; + return ( + + + + + + + - - - {visibleRows.map(row => { - const avatarStyle = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 26, height: 26, borderRadius: '50%', overflow: 'hidden', verticalAlign: 'middle', flexShrink: 0 }; - return ( - - - - - - - - - - ); - })} - -
+ + toggle('status')} style={{ cursor: 'pointer', userSelect: 'none' }}>Status{sortKey === 'status' ? (sortDir === 'asc' ? '▲' : '▼') : '▲▼'} + + + + + toggle('revision')} style={{ cursor: 'pointer', userSelect: 'none' }}>R#{sortKey === 'revision' ? (sortDir === 'asc' ? '▲' : '▼') : '▲▼'} + + + + + toggle('assigned')} style={{ cursor: 'pointer', userSelect: 'none' }}>Assigned{sortKey === 'assigned' ? (sortDir === 'asc' ? '▲' : '▼') : '▲▼'} + + + + + toggle('serviceType')} style={{ cursor: 'pointer', userSelect: 'none' }}>Task Type{sortKey === 'serviceType' ? (sortDir === 'asc' ? '▲' : '▼') : '▲▼'} + + +
No tasks
+ {`R${String(row.version).padStart(2, '0')}`} + + {row.title || '—'} + + {row.assignedName && row.assignedTo + ? + + + :
+ } +
{row.serviceType}{fmtShortDate(row.deadline, 'Not specified')}
- {`R${String(row.version).padStart(2, '0')}`} - - {row.title || '—'} - - {row.assignedName && row.assignedTo - ? - - - :
- } -
- {row.isHot ? 'HOT' : 'NO'} - {row.serviceType}{fmtShortDate(row.deadline, 'Not specified')}
-
+ ); + })} + + +
)} {activeTab === 'members' && isTeam && ( -
+
{(() => { const subs = members.filter(m => m.profile?.role === 'external'); if (subs.length === 0) return
No subcontractors
; @@ -448,7 +493,7 @@ export default function ProjectDetailPage() { return subSortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av); }); return ( - +
@@ -461,7 +506,7 @@ export default function ProjectDetailPage() { @@ -582,7 +627,7 @@ export default function ProjectDetailPage() {
{externalProfiles.length === 0 ?
No subcontractors
- :
Name Email - Projects + Clients
+ :
diff --git a/src/pages/TaskDetail.jsx b/src/pages/TaskDetail.jsx index cf2243d..05a514e 100644 --- a/src/pages/TaskDetail.jsx +++ b/src/pages/TaskDetail.jsx @@ -757,9 +757,9 @@ export default function TaskDetail() {
-
Project
+
Client
{project - ? {project.name} + ? {project.name} :
}
diff --git a/src/pages/Tasks.jsx b/src/pages/Tasks.jsx index df27760..ed051e1 100644 --- a/src/pages/Tasks.jsx +++ b/src/pages/Tasks.jsx @@ -68,7 +68,7 @@ function TasksStatsRow({ tasks = [] }) { const open = Math.max(total - completed, 0); const pct = (count) => total > 0 ? `${Math.round((count / total) * 100)}% of total` : '0% of total'; return ( -
+
@@ -124,10 +124,10 @@ function TasksPageShell({ statsTasks = [], projects = [], onAddProject = null, c return 35; }; return ( -
+
-
-
+
+
{children}
@@ -140,6 +140,8 @@ export default function RequestsPage() { const isTeam = currentUser?.role === 'team'; const isExternal = currentUser?.role === 'external'; const isClient = currentUser?.role === 'client'; + // Company column only meaningful when the user spans multiple companies (team sees all). + const showCompanyCol = isTeam || (currentUser?.companies?.length || 0) > 1; const { refreshKey } = useLiveRefresh(['tasks', 'projects', 'submissions']); @@ -163,6 +165,7 @@ export default function RequestsPage() { const [filterStatus, setFilterStatus] = useState('all'); const [filterRevision, setFilterRevision] = useState('all'); const [filterType, setFilterType] = useState('all'); + const [filterAssigned, setFilterAssigned] = useState('all'); const [filterProject, setFilterProject] = useState(''); const { sortKey, sortDir, toggle, sort } = useSortable('status', 'asc'); @@ -432,7 +435,7 @@ export default function RequestsPage() { setAddProjectSaving(true); setAddProjectError(''); try { const name = addProjectForm.name.trim(); - if (!name) throw new Error('Project name required.'); + if (!name) throw new Error('Client name required.'); if (!addProjectForm.companyId) throw new Error('Company required.'); const { data: newProject, error: insertError } = await supabase .from('projects').insert({ name, company_id: addProjectForm.companyId, status: 'active' }) @@ -517,6 +520,8 @@ export default function RequestsPage() { if (filterRevision === 'new') tabRows = tabRows.filter(r => Number(r.version || 0) === 0); else if (filterRevision === 'revision') tabRows = tabRows.filter(r => Number(r.version || 0) >= 1); if (filterType !== 'all') tabRows = tabRows.filter(r => (r.serviceType || '') === filterType); + if (filterAssigned === 'unassigned') tabRows = tabRows.filter(r => !r.assignedTo); + else if (filterAssigned !== 'all') tabRows = tabRows.filter(r => r.assignedTo === filterAssigned); const REVISION_FILTER_OPTIONS = [ { value: 'all', label: 'All' }, @@ -525,7 +530,7 @@ export default function RequestsPage() { ]; const PROJECT_FILTER_OPTIONS = [ - { value: '', label: 'All Projects' }, + { value: '', label: 'All Clients' }, ...projects .filter(p => !filterCompany || p.company_id === filterCompany) .slice() @@ -545,6 +550,14 @@ export default function RequestsPage() { ...filterableCompanies.map(c => ({ value: c.id, label: c.name })), ]; + const ASSIGNED_FILTER_OPTIONS = [ + { value: 'all', label: 'All Assignees' }, + { value: 'unassigned', label: 'Unassigned' }, + ...[...new Map(tabRowsBase.filter(r => r.assignedTo).map(r => [r.assignedTo, r.assignedName || '—'])).entries()] + .sort((a, b) => a[1].localeCompare(b[1])) + .map(([id, name]) => ({ value: id, label: name })), + ]; + const STATUS_FILTER_OPTIONS = activeTab === 'completed' ? [ { value: 'all', label: 'All Statuses' }, @@ -589,14 +602,14 @@ export default function RequestsPage() {
- - - - - + {showCompanyCol && ( + + )} ); }; @@ -641,7 +656,7 @@ export default function RequestsPage() {
{ setShowAddProjectForm(false); setAddProjectForm({ name: '', companyId: '' }); setAddProjectError(''); }}>
e.stopPropagation()}>
-
Add Project
+
Add Client
@@ -652,7 +667,7 @@ export default function RequestsPage() {
- + setAddProjectForm(f => ({ ...f, name: e.target.value }))} required autoFocus style={TASK_MODAL_INPUT_STYLE} />
{addProjectError &&
{addProjectError}
} @@ -689,9 +704,9 @@ export default function RequestsPage() { )} {/* Controls bar: tabs left, filters + actions right */} -
+
{tabs.map(t => ( -
{/* Task table */} -
-
+
+
+ {`R${String(row.version).padStart(2, '0')}`} {row.title || '—'} - {project ? {project.name} : '—'} + + {project ? {project.name} : '—'} {(() => { @@ -607,15 +620,17 @@ export default function RequestsPage() { return
; })()}
+ {row.serviceType} + {fmtShortDate(row.deadline, 'Not specified')} - {project?.company?.name || '—'} - + {project?.company?.name || '—'} +
- + - + - - - + + + {showCompanyCol && } @@ -734,7 +749,7 @@ export default function RequestsPage() { - Name - - Assigned + - Due - + )} {sortedRows.length === 0 ? ( - + ) : sortedRows.map(renderRow)}
+ toggle('revision')} style={{ cursor: 'pointer', userSelect: 'none' }}> R# @@ -746,10 +761,10 @@ export default function RequestsPage() { + toggle('project')} style={{ cursor: 'pointer', userSelect: 'none' }}> - Project + Client {sortKey === 'project' ? (sortDir === 'asc' ? '▲' : '▼') : '▲▼'} @@ -757,8 +772,18 @@ export default function RequestsPage() { + + toggle('assigned')} style={{ cursor: 'pointer', userSelect: 'none' }}> + Assigned + + {sortKey === 'assigned' ? (sortDir === 'asc' ? '▲' : '▼') : '▲▼'} + + + + + toggle('serviceType')} style={{ cursor: 'pointer', userSelect: 'none' }}> Task Type @@ -769,8 +794,9 @@ export default function RequestsPage() { + Due + {showCompanyCol && ( + toggle('company')} style={{ cursor: 'pointer', userSelect: 'none' }}> Company @@ -781,11 +807,12 @@ export default function RequestsPage() { { setFilterCompany(v); setFilterProject(''); }} options={COMPANY_FILTER_OPTIONS} />
No tasks
No tasks
diff --git a/src/pages/team/TeamCreateSubcontractorPO.jsx b/src/pages/team/TeamCreateSubcontractorPO.jsx index 01256c4..fbabd5d 100644 --- a/src/pages/team/TeamCreateSubcontractorPO.jsx +++ b/src/pages/team/TeamCreateSubcontractorPO.jsx @@ -208,9 +208,9 @@ export default function CreateSubcontractorPO() {
- +
-
Project
+
Client