redesign: rename Project→Client, finance single-card + filters, mobile tasks, sticky headers
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<div className="sidebar-section">
|
||||
@@ -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 }) {
|
||||
: <ClientNav onNav={() => setMenuOpen(false)} />
|
||||
}
|
||||
|
||||
<div className="sidebar-mobile-footer">
|
||||
<button className="sidebar-link" onClick={() => { navigate('/profile'); setMenuOpen(false); }} title="Profile">
|
||||
<ProfileAvatar profile={currentUser} name={currentUser?.name} size={26} fontSize={10} />
|
||||
<span className="nav-label">Profile</span>
|
||||
</button>
|
||||
<button className="sidebar-link" onClick={toggleTheme} title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}>
|
||||
<span className="nav-icon">
|
||||
{theme === 'dark'
|
||||
? <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="8" cy="8" r="3"/><line x1="8" y1="1" x2="8" y2="2.5"/><line x1="8" y1="13.5" x2="8" y2="15"/><line x1="1" y1="8" x2="2.5" y2="8"/><line x1="13.5" y1="8" x2="15" y2="8"/><line x1="3" y1="3" x2="4.1" y2="4.1"/><line x1="11.9" y1="11.9" x2="13" y2="13"/><line x1="13" y1="3" x2="11.9" y2="4.1"/><line x1="4.1" y1="11.9" x2="3" y2="13"/></svg>
|
||||
: <svg viewBox="0 0 16 16" fill="currentColor" stroke="none"><path d="M14 8.53A6 6 0 1 1 7.47 2 4.67 4.67 0 0 0 14 8.53Z"/></svg>}
|
||||
</span>
|
||||
<span className="nav-label">{theme === 'dark' ? 'Light mode' : 'Dark mode'}</span>
|
||||
</button>
|
||||
<button className="sidebar-link" onClick={handleLogout} title="Sign Out">
|
||||
<span className="nav-icon">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M6 14H3a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h3"/><polyline points="10 11 13 8 10 5"/><line x1="13" y1="8" x2="6" y2="8"/></svg>
|
||||
</span>
|
||||
<span className="nav-label">Sign Out</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<div className="main-wrapper">
|
||||
@@ -313,9 +334,9 @@ export default function Layout({ children, loading = false }) {
|
||||
{!hasResults && <div className="site-header-dropdown-empty">No results for "{searchQuery}"</div>}
|
||||
{searchResults.projects.length > 0 && (
|
||||
<>
|
||||
<div className="site-header-dropdown-group">Projects</div>
|
||||
<div className="site-header-dropdown-group">Clients</div>
|
||||
{searchResults.projects.map(p => (
|
||||
<div key={p.id} className="site-header-dropdown-item" onMouseDown={() => { navigate(`/projects/${p.id}`); setSearchQuery(''); setSearchOpen(false); }}>
|
||||
<div key={p.id} className="site-header-dropdown-item" onMouseDown={() => { navigate(`/clients/${p.id}`); setSearchQuery(''); setSearchOpen(false); }}>
|
||||
<span className="nav-icon" style={{ opacity: 0.6 }}>{ICONS.projects}</span>
|
||||
{p.name}
|
||||
</div>
|
||||
|
||||
@@ -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 => <option key={co.id} value={co.id}>{co.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
) : companies.length === 1 ? (
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>Company</label>
|
||||
<input value={companies[0].name} disabled style={{ ...modalInputStyle, opacity: 0.5, cursor: 'not-allowed' }} />
|
||||
</div>
|
||||
) : <div />}
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>Project {!lockedFields.includes('project') && '*'}</label>
|
||||
<label style={modalLabelStyle}>Client {!lockedFields.includes('project') && '*'}</label>
|
||||
{lockedFields.includes('project') ? (
|
||||
<input value={form.project} disabled style={{ ...modalInputStyle, opacity: 0.5, cursor: 'not-allowed' }} />
|
||||
) : isTypingProject ? (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<input type="text" placeholder="Enter project name..." value={newProjectName} onChange={e => setNewProjectName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleAddProject(); } }} autoFocus style={{ ...modalInputStyle, flex: 1 }} />
|
||||
<input type="text" placeholder="Enter client name..." value={newProjectName} onChange={e => setNewProjectName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleAddProject(); } }} autoFocus style={{ ...modalInputStyle, flex: 1 }} />
|
||||
<button type="button" className="btn btn-outline" onClick={handleAddProject} disabled={!newProjectName.trim()}>Add</button>
|
||||
<button type="button" className="btn btn-outline" onClick={() => { setIsTypingProject(false); setNewProjectName(''); }}>Cancel</button>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<th
|
||||
className={className}
|
||||
onClick={() => onSort(col)}
|
||||
style={{ cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap', ...style }}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user