Add client filter tabs to team Projects page
This commit is contained in:
@@ -1254,3 +1254,19 @@ select option { background: #222; color: #fff; }
|
|||||||
/* Version timeline */
|
/* Version timeline */
|
||||||
.version-item { padding: 12px; }
|
.version-item { padding: 12px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tab bar */
|
||||||
|
.tab-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tab-btn:hover { border-color: var(--interactive-hover-border); color: var(--text-secondary); }
|
||||||
|
.tab-btn.active { background: var(--accent); border-color: var(--accent); color: #000; font-weight: 600; }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import Layout from '../../components/Layout';
|
import Layout from '../../components/Layout';
|
||||||
import StatusBadge from '../../components/StatusBadge';
|
import StatusBadge from '../../components/StatusBadge';
|
||||||
@@ -9,6 +9,7 @@ export default function TeamProjects() {
|
|||||||
const [projects, setProjects] = useState([]);
|
const [projects, setProjects] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [activeTab, setActiveTab] = useState('all');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
supabase
|
supabase
|
||||||
@@ -21,11 +22,21 @@ export default function TeamProjects() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filtered = projects.filter(p =>
|
const companies = useMemo(() => {
|
||||||
!search ||
|
const seen = new Map();
|
||||||
|
projects.forEach(p => {
|
||||||
|
if (p.company?.id && !seen.has(p.company.id)) seen.set(p.company.id, p.company.name);
|
||||||
|
});
|
||||||
|
return [...seen.entries()].sort((a, b) => a[1].localeCompare(b[1]));
|
||||||
|
}, [projects]);
|
||||||
|
|
||||||
|
const filtered = projects.filter(p => {
|
||||||
|
const matchesTab = activeTab === 'all' || p.company?.id === activeTab;
|
||||||
|
const matchesSearch = !search ||
|
||||||
p.name.toLowerCase().includes(search.toLowerCase()) ||
|
p.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
p.company?.name?.toLowerCase().includes(search.toLowerCase())
|
p.company?.name?.toLowerCase().includes(search.toLowerCase());
|
||||||
);
|
return matchesTab && matchesSearch;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
@@ -36,13 +47,34 @@ export default function TeamProjects() {
|
|||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search projects or clients..."
|
placeholder="Search projects..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
style={{ width: 240 }}
|
style={{ width: 220 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="tab-bar" style={{ marginBottom: 20, display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||||
|
<button
|
||||||
|
className={`tab-btn${activeTab === 'all' ? ' active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('all')}
|
||||||
|
>
|
||||||
|
All ({projects.length})
|
||||||
|
</button>
|
||||||
|
{companies.map(([id, name]) => {
|
||||||
|
const count = projects.filter(p => p.company?.id === id).length;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
className={`tab-btn${activeTab === id ? ' active' : ''}`}
|
||||||
|
onClick={() => setActiveTab(id)}
|
||||||
|
>
|
||||||
|
{name} ({count})
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p>
|
<p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p>
|
||||||
) : filtered.length === 0 ? (
|
) : filtered.length === 0 ? (
|
||||||
@@ -56,7 +88,7 @@ export default function TeamProjects() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Project</th>
|
<th>Project</th>
|
||||||
<th>Client</th>
|
{activeTab === 'all' && <th>Client</th>}
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Started</th>
|
<th>Started</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -65,7 +97,7 @@ export default function TeamProjects() {
|
|||||||
{filtered.map(p => (
|
{filtered.map(p => (
|
||||||
<tr key={p.id} style={{ cursor: 'pointer' }} onClick={() => navigate(`/projects/${p.id}`)}>
|
<tr key={p.id} style={{ cursor: 'pointer' }} onClick={() => navigate(`/projects/${p.id}`)}>
|
||||||
<td style={{ fontWeight: 600 }}>{p.name}</td>
|
<td style={{ fontWeight: 600 }}>{p.name}</td>
|
||||||
<td style={{ color: 'var(--text-muted)' }}>{p.company?.name || '—'}</td>
|
{activeTab === 'all' && <td style={{ color: 'var(--text-muted)' }}>{p.company?.name || '—'}</td>}
|
||||||
<td><StatusBadge status={p.status} /></td>
|
<td><StatusBadge status={p.status} /></td>
|
||||||
<td style={{ color: 'var(--text-muted)' }}>{new Date(p.created_at).toLocaleDateString()}</td>
|
<td style={{ color: 'var(--text-muted)' }}>{new Date(p.created_at).toLocaleDateString()}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user