Session 2026-05-29: tasks page redesign, projects sidebar, invoicing fix
- Tasks page: tabs (All/To Do/In Progress/In Review/Completed) in header, removed grid view, 70/30 split with projects card - Projects card: sortable table, dynamic height fills viewport - Removed Projects page and nav links; redirects → /tasks - Invoice dedup: prevent double-charge when multiple submissions per revision version - Dashboard table style applied to tasks table (10px headers, 5px cell padding, transparent cells) - stat cards: removed "Tasks" header, moved Total Tasks to far right Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+8
-11
@@ -58,12 +58,9 @@ const MyPurchaseOrders = lazy(() => import('./pages/external/MyPurchaseOrders'))
|
|||||||
const ExternalMyInvoices = lazy(() => import('./pages/external/MyInvoices'));
|
const ExternalMyInvoices = lazy(() => import('./pages/external/MyInvoices'));
|
||||||
const ExternalMyInvoiceDetail = lazy(() => import('./pages/external/MyInvoiceDetail'));
|
const ExternalMyInvoiceDetail = lazy(() => import('./pages/external/MyInvoiceDetail'));
|
||||||
const ExternalMyInvoiceCreate = lazy(() => import('./pages/external/MyInvoiceCreate'));
|
const ExternalMyInvoiceCreate = lazy(() => import('./pages/external/MyInvoiceCreate'));
|
||||||
const Projects = lazy(() => import('./pages/Projects'));
|
|
||||||
const ProjectDetailPage = lazy(() => import('./pages/ProjectDetailPage'));
|
const ProjectDetailPage = lazy(() => import('./pages/ProjectDetailPage'));
|
||||||
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
|
|
||||||
const TeamDashboard = lazy(() => import('./pages/team/TeamDashboard'));
|
const TeamDashboard = lazy(() => import('./pages/team/TeamDashboard'));
|
||||||
const RequestsPage = lazy(() => import('./pages/RequestsPage'));
|
const RequestsPage = lazy(() => import('./pages/RequestsPage'));
|
||||||
const TeamTasksPage = lazy(() => import('./pages/team/TeamTasksPage'));
|
|
||||||
const MyInvoices = lazy(() => import('./pages/client/MyInvoices'));
|
const MyInvoices = lazy(() => import('./pages/client/MyInvoices'));
|
||||||
const NewRequest = lazy(() => import('./pages/client/NewRequest'));
|
const NewRequest = lazy(() => import('./pages/client/NewRequest'));
|
||||||
const NewProject = lazy(() => import('./pages/client/NewProject'));
|
const NewProject = lazy(() => import('./pages/client/NewProject'));
|
||||||
@@ -95,7 +92,6 @@ export default function App() {
|
|||||||
<Route path="/" element={<Login />} />
|
<Route path="/" element={<Login />} />
|
||||||
|
|
||||||
<Route path="/dashboard" element={<ProtectedRoute role={['team', 'external', 'client']}><TeamDashboard /></ProtectedRoute>} />
|
<Route path="/dashboard" element={<ProtectedRoute role={['team', 'external', 'client']}><TeamDashboard /></ProtectedRoute>} />
|
||||||
<Route path="/projects" element={<ProtectedRoute role={['team', 'external', 'client']}><Projects /></ProtectedRoute>} />
|
|
||||||
<Route path="/projects/:id" element={<ProtectedRoute role={['team', 'external', 'client']}><ProjectDetailPage /></ProtectedRoute>} />
|
<Route path="/projects/:id" element={<ProtectedRoute role={['team', 'external', 'client']}><ProjectDetailPage /></ProtectedRoute>} />
|
||||||
<Route path="/tasks/:id" element={<RedirectRequestDetail />} />
|
<Route path="/tasks/:id" element={<RedirectRequestDetail />} />
|
||||||
<Route path="/requests/:id" element={<ProtectedRoute role={['team', 'external', 'client']}><RequestDetail /></ProtectedRoute>} />
|
<Route path="/requests/:id" element={<ProtectedRoute role={['team', 'external', 'client']}><RequestDetail /></ProtectedRoute>} />
|
||||||
@@ -103,9 +99,10 @@ export default function App() {
|
|||||||
<Route path="/company/:id" element={<ProtectedRoute role={['team', 'client']}><CompanyDetail /></ProtectedRoute>} />
|
<Route path="/company/:id" element={<ProtectedRoute role={['team', 'client']}><CompanyDetail /></ProtectedRoute>} />
|
||||||
<Route path="/companies" element={<Navigate to="/company" replace />} />
|
<Route path="/companies" element={<Navigate to="/company" replace />} />
|
||||||
<Route path="/companies/:id" element={<NavigateCompanyDetail />} />
|
<Route path="/companies/:id" element={<NavigateCompanyDetail />} />
|
||||||
<Route path="/team/tasks" element={<ProtectedRoute role="team"><TeamTasksPage /></ProtectedRoute>} />
|
<Route path="/team/tasks" element={<Navigate to="/tasks" replace />} />
|
||||||
<Route path="/requests" element={<ProtectedRoute role={['external', 'client']}><RequestsPage /></ProtectedRoute>} />
|
<Route path="/tasks" element={<ProtectedRoute role={['team', 'external', 'client']}><RequestsPage /></ProtectedRoute>} />
|
||||||
<Route path="/team-projects" element={<Navigate to="/projects" replace />} />
|
<Route path="/requests" element={<Navigate to="/tasks" replace />} />
|
||||||
|
<Route path="/team-projects" element={<Navigate to="/tasks" replace />} />
|
||||||
<Route path="/invoices" element={<ProtectedRoute role="team"><Invoices /></ProtectedRoute>} />
|
<Route path="/invoices" element={<ProtectedRoute role="team"><Invoices /></ProtectedRoute>} />
|
||||||
<Route path="/invoices/new" element={<ProtectedRoute role="team"><CreateInvoice /></ProtectedRoute>} />
|
<Route path="/invoices/new" element={<ProtectedRoute role="team"><CreateInvoice /></ProtectedRoute>} />
|
||||||
<Route path="/subcontractor-pos/new" element={<ProtectedRoute role="team"><CreateSubcontractorPO /></ProtectedRoute>} />
|
<Route path="/subcontractor-pos/new" element={<ProtectedRoute role="team"><CreateSubcontractorPO /></ProtectedRoute>} />
|
||||||
@@ -119,9 +116,9 @@ export default function App() {
|
|||||||
<Route path="/file-uploads" element={<Navigate to="/file-sharing" replace />} />
|
<Route path="/file-uploads" element={<Navigate to="/file-sharing" replace />} />
|
||||||
<Route path="/fourge-passwords" element={<ProtectedRoute role="team"><FourgePasswords /></ProtectedRoute>} />
|
<Route path="/fourge-passwords" element={<ProtectedRoute role="team"><FourgePasswords /></ProtectedRoute>} />
|
||||||
<Route path="/server-status" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/server-status" element={<Navigate to="/dashboard" replace />} />
|
||||||
<Route path="/assigned-requests" element={<Navigate to="/requests" replace />} />
|
<Route path="/assigned-requests" element={<Navigate to="/tasks" replace />} />
|
||||||
<Route path="/my-purchase-orders" element={<ProtectedRoute role="external"><MyPurchaseOrders /></ProtectedRoute>} />
|
<Route path="/my-purchase-orders" element={<ProtectedRoute role="external"><MyPurchaseOrders /></ProtectedRoute>} />
|
||||||
<Route path="/my-projects-sub" element={<Navigate to="/projects" replace />} />
|
<Route path="/my-projects-sub" element={<Navigate to="/tasks" replace />} />
|
||||||
<Route path="/my-invoices-sub" element={<ProtectedRoute role="external"><ExternalMyInvoices /></ProtectedRoute>} />
|
<Route path="/my-invoices-sub" element={<ProtectedRoute role="external"><ExternalMyInvoices /></ProtectedRoute>} />
|
||||||
<Route path="/my-invoices-sub/new" element={<ProtectedRoute role="external"><ExternalMyInvoiceCreate /></ProtectedRoute>} />
|
<Route path="/my-invoices-sub/new" element={<ProtectedRoute role="external"><ExternalMyInvoiceCreate /></ProtectedRoute>} />
|
||||||
<Route path="/my-invoices-sub/:id" element={<ProtectedRoute role="external"><ExternalMyInvoiceDetail /></ProtectedRoute>} />
|
<Route path="/my-invoices-sub/:id" element={<ProtectedRoute role="external"><ExternalMyInvoiceDetail /></ProtectedRoute>} />
|
||||||
@@ -132,9 +129,9 @@ export default function App() {
|
|||||||
|
|
||||||
<Route path="/my-dashboard" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/my-dashboard" element={<Navigate to="/dashboard" replace />} />
|
||||||
<Route path="/my-company" element={<Navigate to="/company" replace />} />
|
<Route path="/my-company" element={<Navigate to="/company" replace />} />
|
||||||
<Route path="/my-requests" element={<Navigate to="/requests" replace />} />
|
<Route path="/my-requests" element={<Navigate to="/tasks" replace />} />
|
||||||
<Route path="/my-requests/:id" element={<RedirectRequestDetail />} />
|
<Route path="/my-requests/:id" element={<RedirectRequestDetail />} />
|
||||||
<Route path="/my-projects" element={<Navigate to="/projects" replace />} />
|
<Route path="/my-projects" element={<Navigate to="/tasks" replace />} />
|
||||||
<Route path="/my-projects/:id" element={<RedirectProjectDetail />} />
|
<Route path="/my-projects/:id" element={<RedirectProjectDetail />} />
|
||||||
<Route path="/my-invoices" element={<ProtectedRoute role="client"><MyInvoices /></ProtectedRoute>} />
|
<Route path="/my-invoices" element={<ProtectedRoute role="client"><MyInvoices /></ProtectedRoute>} />
|
||||||
<Route path="/new-request" element={<ProtectedRoute role="client"><NewRequest /></ProtectedRoute>} />
|
<Route path="/new-request" element={<ProtectedRoute role="client"><NewRequest /></ProtectedRoute>} />
|
||||||
|
|||||||
+68
-52
@@ -27,8 +27,7 @@ function TeamNav({ onNav }) {
|
|||||||
|
|
||||||
const primaryLinks = [
|
const primaryLinks = [
|
||||||
{ to: '/dashboard', label: 'Dashboard', icon: ICONS.dashboard },
|
{ to: '/dashboard', label: 'Dashboard', icon: ICONS.dashboard },
|
||||||
{ to: '/team/tasks', label: 'Tasks', icon: ICONS.requests },
|
{ to: '/tasks', label: 'Tasks', icon: ICONS.requests },
|
||||||
{ to: '/projects', label: 'Projects', icon: ICONS.projects },
|
|
||||||
{ to: '/invoices', label: 'Finances', icon: ICONS.invoices },
|
{ to: '/invoices', label: 'Finances', icon: ICONS.invoices },
|
||||||
{ to: '/file-sharing', label: 'File Sharing', icon: ICONS.fileSharing },
|
{ to: '/file-sharing', label: 'File Sharing', icon: ICONS.fileSharing },
|
||||||
];
|
];
|
||||||
@@ -50,10 +49,8 @@ function TeamNav({ onNav }) {
|
|||||||
<NI icon={icon} /><span className="nav-label">{label}</span>
|
<NI icon={icon} /><span className="nav-label">{label}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
<div className="sidebar-tools-divider" style={{ height: 1, margin: '10px 12px', background: 'var(--border)' }} />
|
<div className="sidebar-tools-divider" />
|
||||||
<div className="sidebar-tools-label" style={{ padding: '0 12px 8px', fontSize: 11, fontWeight: 400, letterSpacing: 0.8, textTransform: 'uppercase', color: 'var(--text-muted)' }}>
|
<div className="sidebar-tools-label">Tools</div>
|
||||||
Tools
|
|
||||||
</div>
|
|
||||||
{utilityLinks.map(({ to, label, icon }) => (
|
{utilityLinks.map(({ to, label, icon }) => (
|
||||||
<NavLink key={to} to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
<NavLink key={to} to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||||
<NI icon={icon} /><span className="nav-label">{label}</span>
|
<NI icon={icon} /><span className="nav-label">{label}</span>
|
||||||
@@ -72,8 +69,7 @@ function TeamNav({ onNav }) {
|
|||||||
function ClientNav({ onNav }) {
|
function ClientNav({ onNav }) {
|
||||||
const links = [
|
const links = [
|
||||||
{ to: '/dashboard', label: 'Dashboard', icon: ICONS.dashboard },
|
{ to: '/dashboard', label: 'Dashboard', icon: ICONS.dashboard },
|
||||||
{ to: '/requests', label: 'Requests', icon: ICONS.requests },
|
{ to: '/tasks', label: 'Tasks', icon: ICONS.requests },
|
||||||
{ to: '/projects', label: 'Projects', icon: ICONS.projects },
|
|
||||||
{ to: '/file-sharing', label: 'File Sharing', icon: ICONS.fileSharing },
|
{ to: '/file-sharing', label: 'File Sharing', icon: ICONS.fileSharing },
|
||||||
{ to: '/my-invoices', label: 'Invoices', icon: ICONS.invoices },
|
{ to: '/my-invoices', label: 'Invoices', icon: ICONS.invoices },
|
||||||
];
|
];
|
||||||
@@ -92,8 +88,7 @@ function ClientNav({ onNav }) {
|
|||||||
function ExternalNav({ onNav }) {
|
function ExternalNav({ onNav }) {
|
||||||
const links = [
|
const links = [
|
||||||
{ to: '/dashboard', label: 'Dashboard', icon: ICONS.dashboard },
|
{ to: '/dashboard', label: 'Dashboard', icon: ICONS.dashboard },
|
||||||
{ to: '/requests', label: 'Requests', icon: ICONS.requests },
|
{ to: '/tasks', label: 'Tasks', icon: ICONS.requests },
|
||||||
{ to: '/projects', label: 'Projects', icon: ICONS.projects },
|
|
||||||
{ to: '/my-invoices-sub', label: 'Invoices', icon: ICONS.invoices },
|
{ to: '/my-invoices-sub', label: 'Invoices', icon: ICONS.invoices },
|
||||||
{ to: '/file-sharing', label: 'File Sharing', icon: ICONS.fileSharing },
|
{ to: '/file-sharing', label: 'File Sharing', icon: ICONS.fileSharing },
|
||||||
{ to: '/survey-maker', label: 'Survey Maker', icon: ICONS.survey },
|
{ to: '/survey-maker', label: 'Survey Maker', icon: ICONS.survey },
|
||||||
@@ -107,10 +102,8 @@ function ExternalNav({ onNav }) {
|
|||||||
<div key={to}>
|
<div key={to}>
|
||||||
{index === 4 && (
|
{index === 4 && (
|
||||||
<>
|
<>
|
||||||
<div className="sidebar-tools-divider" style={{ height: 1, margin: '10px 12px', background: 'var(--border)' }} />
|
<div className="sidebar-tools-divider" />
|
||||||
<div className="sidebar-tools-label" style={{ padding: '0 12px 8px', fontSize: 11, fontWeight: 400, letterSpacing: 0.8, textTransform: 'uppercase', color: 'var(--text-muted)' }}>
|
<div className="sidebar-tools-label">Tools</div>
|
||||||
Tools
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<NavLink to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
<NavLink to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||||
@@ -122,16 +115,22 @@ function ExternalNav({ onNav }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getInitialTheme() {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
return document.documentElement.getAttribute('data-theme') || localStorage.getItem('theme') || 'dark';
|
||||||
|
}
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
export default function Layout({ children }) {
|
export default function Layout({ children }) {
|
||||||
const { currentUser, logout } = useAuth();
|
const { currentUser, logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'dark');
|
const [theme, setTheme] = useState(getInitialTheme);
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [searchResults, setSearchResults] = useState({ projects: [], tasks: [] });
|
const [searchResults, setSearchResults] = useState({ projects: [], tasks: [], companies: [], people: [] });
|
||||||
const [searchOpen, setSearchOpen] = useState(false);
|
const [searchOpen, setSearchOpen] = useState(false);
|
||||||
const [searchExpanded, setSearchExpanded] = useState(false);
|
|
||||||
const searchInputRef = useRef(null);
|
const searchInputRef = useRef(null);
|
||||||
const [avatarOpen, setAvatarOpen] = useState(false);
|
const [avatarOpen, setAvatarOpen] = useState(false);
|
||||||
const avatarRef = useRef(null);
|
const avatarRef = useRef(null);
|
||||||
@@ -141,33 +140,33 @@ export default function Layout({ children }) {
|
|||||||
const firstName = currentUser?.name?.split(' ')[0] || '';
|
const firstName = currentUser?.name?.split(' ')[0] || '';
|
||||||
const isProfileRoute = location.pathname === '/profile' || location.pathname.startsWith('/profile/');
|
const isProfileRoute = location.pathname === '/profile' || location.pathname.startsWith('/profile/');
|
||||||
const isFileSharingRoute = location.pathname === '/file-sharing';
|
const isFileSharingRoute = location.pathname === '/file-sharing';
|
||||||
const isRequestsRoute = location.pathname === '/requests';
|
const isRequestsRoute = location.pathname === '/tasks' || location.pathname === '/requests' || location.pathname === '/team/tasks';
|
||||||
const isTasksRoute = location.pathname === '/team/tasks';
|
const headerTitle = isProfileRoute ? 'Profile' : isFileSharingRoute ? 'File Sharing' : isRequestsRoute ? 'Tasks & Projects' : `Good ${timeOfDay}${firstName ? `, ${firstName}` : ''}`;
|
||||||
const headerTitle = isProfileRoute ? 'Profile' : isFileSharingRoute ? 'File Sharing' : isTasksRoute ? 'Tasks & Projects' : isRequestsRoute ? 'Requests' : `Good ${timeOfDay}${firstName ? `, ${firstName}` : ''}`;
|
|
||||||
const headerSubtitle = isProfileRoute
|
const headerSubtitle = isProfileRoute
|
||||||
? 'Account details and security settings.'
|
? 'Account details and security settings.'
|
||||||
: isFileSharingRoute
|
: isFileSharingRoute
|
||||||
? 'Browse, share and manage files.'
|
? 'Browse, share and manage files.'
|
||||||
: isTasksRoute
|
: isRequestsRoute
|
||||||
? 'Browse and manage all tasks and projects.'
|
? 'Browse and manage all tasks and projects.'
|
||||||
: isRequestsRoute
|
|
||||||
? 'Browse and manage all your requests.'
|
|
||||||
: "Here's what's happening today.";
|
: "Here's what's happening today.";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
document.documentElement.style.colorScheme = theme === 'light' ? 'light' : 'dark';
|
||||||
localStorage.setItem('theme', theme);
|
localStorage.setItem('theme', theme);
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchQuery.trim()) { setSearchResults({ projects: [], tasks: [] }); return; }
|
if (!searchQuery.trim()) { setSearchResults({ projects: [], tasks: [], companies: [], people: [] }); return; }
|
||||||
const t = setTimeout(async () => {
|
const t = setTimeout(async () => {
|
||||||
const { supabase } = await import('../lib/supabase');
|
const { supabase } = await import('../lib/supabase');
|
||||||
const [{ data: projects }, { data: tasks }] = await Promise.all([
|
const [{ data: projects }, { data: tasks }, { data: companies }, { data: people }] = await Promise.all([
|
||||||
supabase.from('projects').select('id, name').ilike('name', `%${searchQuery}%`).limit(6),
|
supabase.from('projects').select('id, name').ilike('name', `%${searchQuery}%`).limit(6),
|
||||||
supabase.from('tasks').select('id, title').ilike('title', `%${searchQuery}%`).limit(6),
|
supabase.from('tasks').select('id, title').ilike('title', `%${searchQuery}%`).limit(6),
|
||||||
|
supabase.from('companies').select('id, name').ilike('name', `%${searchQuery}%`).limit(6),
|
||||||
|
supabase.from('profiles').select('id, name').ilike('name', `%${searchQuery}%`).limit(6),
|
||||||
]);
|
]);
|
||||||
setSearchResults({ projects: projects || [], tasks: tasks || [] });
|
setSearchResults({ projects: projects || [], tasks: tasks || [], companies: companies || [], people: people || [] });
|
||||||
}, 280);
|
}, 280);
|
||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
@@ -201,7 +200,11 @@ export default function Layout({ children }) {
|
|||||||
const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark');
|
const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark');
|
||||||
const handleLogout = async () => { await logout(); navigate('/'); };
|
const handleLogout = async () => { await logout(); navigate('/'); };
|
||||||
|
|
||||||
const hasResults = searchResults.projects.length > 0 || searchResults.tasks.length > 0;
|
const hasResults =
|
||||||
|
searchResults.projects.length > 0 ||
|
||||||
|
searchResults.tasks.length > 0 ||
|
||||||
|
searchResults.companies.length > 0 ||
|
||||||
|
searchResults.people.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-layout">
|
<div className="app-layout">
|
||||||
@@ -238,29 +241,20 @@ export default function Layout({ children }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="site-header-right">
|
<div className="site-header-right">
|
||||||
<div className="site-header-search-wrap">
|
<div className="site-header-search-wrap">
|
||||||
{!searchExpanded ? (
|
<span className="site-header-search-icon" aria-hidden="true">
|
||||||
<button
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><circle cx="6.5" cy="6.5" r="4"/><line x1="10" y1="10" x2="14" y2="14"/></svg>
|
||||||
className="site-header-search-btn"
|
</span>
|
||||||
onClick={() => { setSearchExpanded(true); setTimeout(() => searchInputRef.current?.focus(), 50); }}
|
<input
|
||||||
aria-label="Search"
|
ref={searchInputRef}
|
||||||
>
|
className="site-header-search"
|
||||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><circle cx="6.5" cy="6.5" r="4"/><line x1="10" y1="10" x2="14" y2="14"/></svg>
|
type="text"
|
||||||
</button>
|
placeholder="Search"
|
||||||
) : (
|
value={searchQuery}
|
||||||
<>
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
<input
|
onFocus={() => setSearchOpen(true)}
|
||||||
ref={searchInputRef}
|
onBlur={() => setTimeout(() => { setSearchOpen(false); setSearchQuery(''); }, 150)}
|
||||||
className="site-header-search"
|
onKeyDown={e => { if (e.key === 'Escape') { setSearchOpen(false); setSearchQuery(''); } }}
|
||||||
type="text"
|
/>
|
||||||
placeholder="Search projects & tasks..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={e => setSearchQuery(e.target.value)}
|
|
||||||
onFocus={() => setSearchOpen(true)}
|
|
||||||
onBlur={() => setTimeout(() => { setSearchOpen(false); setSearchExpanded(false); setSearchQuery(''); }, 150)}
|
|
||||||
onKeyDown={e => { if (e.key === 'Escape') { setSearchOpen(false); setSearchExpanded(false); setSearchQuery(''); } }}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{searchOpen && searchQuery.trim() && (
|
{searchOpen && searchQuery.trim() && (
|
||||||
<div className="site-header-dropdown">
|
<div className="site-header-dropdown">
|
||||||
{!hasResults && <div className="site-header-dropdown-empty">No results for "{searchQuery}"</div>}
|
{!hasResults && <div className="site-header-dropdown-empty">No results for "{searchQuery}"</div>}
|
||||||
@@ -268,7 +262,7 @@ export default function Layout({ children }) {
|
|||||||
<>
|
<>
|
||||||
<div className="site-header-dropdown-group">Projects</div>
|
<div className="site-header-dropdown-group">Projects</div>
|
||||||
{searchResults.projects.map(p => (
|
{searchResults.projects.map(p => (
|
||||||
<div key={p.id} className="site-header-dropdown-item" onMouseDown={() => { navigate(`/projects/${p.id}`); setSearchQuery(''); setSearchOpen(false); setSearchExpanded(false); }}>
|
<div key={p.id} className="site-header-dropdown-item" onMouseDown={() => { navigate(`/projects/${p.id}`); setSearchQuery(''); setSearchOpen(false); }}>
|
||||||
<span className="nav-icon" style={{ opacity: 0.6 }}>{ICONS.projects}</span>
|
<span className="nav-icon" style={{ opacity: 0.6 }}>{ICONS.projects}</span>
|
||||||
{p.name}
|
{p.name}
|
||||||
</div>
|
</div>
|
||||||
@@ -279,13 +273,35 @@ export default function Layout({ children }) {
|
|||||||
<>
|
<>
|
||||||
<div className="site-header-dropdown-group">Tasks</div>
|
<div className="site-header-dropdown-group">Tasks</div>
|
||||||
{searchResults.tasks.map(r => (
|
{searchResults.tasks.map(r => (
|
||||||
<div key={r.id} className="site-header-dropdown-item" onMouseDown={() => { navigate(`/requests/${r.id}`); setSearchQuery(''); setSearchOpen(false); setSearchExpanded(false); }}>
|
<div key={r.id} className="site-header-dropdown-item" onMouseDown={() => { navigate(`/requests/${r.id}`); setSearchQuery(''); setSearchOpen(false); }}>
|
||||||
<span className="nav-icon" style={{ opacity: 0.6 }}>{ICONS.requests}</span>
|
<span className="nav-icon" style={{ opacity: 0.6 }}>{ICONS.requests}</span>
|
||||||
{r.title}
|
{r.title}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{searchResults.companies.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="site-header-dropdown-group">Companies</div>
|
||||||
|
{searchResults.companies.map(c => (
|
||||||
|
<div key={c.id} className="site-header-dropdown-item" onMouseDown={() => { navigate(`/company/${c.id}`); setSearchQuery(''); setSearchOpen(false); }}>
|
||||||
|
<span className="nav-icon" style={{ opacity: 0.6 }}>{ICONS.companies}</span>
|
||||||
|
{c.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{searchResults.people.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="site-header-dropdown-group">People</div>
|
||||||
|
{searchResults.people.map(p => (
|
||||||
|
<div key={p.id} className="site-header-dropdown-item" onMouseDown={() => { navigate(`/profile/${p.id}`); setSearchQuery(''); setSearchOpen(false); }}>
|
||||||
|
<span className="nav-icon" style={{ opacity: 0.6 }}>{ICONS.users}</span>
|
||||||
|
{p.name || 'Unknown User'}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+124
-14
@@ -24,6 +24,7 @@
|
|||||||
linear-gradient(180deg, #111111 0%, #0d0d0d 42%, #0a0a0a 100%);
|
linear-gradient(180deg, #111111 0%, #0d0d0d 42%, #0a0a0a 100%);
|
||||||
--card-bg: rgba(255, 255, 255, 0.02);
|
--card-bg: rgba(255, 255, 255, 0.02);
|
||||||
--card-bg-2: rgba(255, 255, 255, 0.08);
|
--card-bg-2: rgba(255, 255, 255, 0.08);
|
||||||
|
--popup-bg: rgba(13,13,13,0.88);
|
||||||
--text-primary: #ffffff;
|
--text-primary: #ffffff;
|
||||||
--text-secondary: #a8a8a8;
|
--text-secondary: #a8a8a8;
|
||||||
--text-muted: #666666;
|
--text-muted: #666666;
|
||||||
@@ -31,6 +32,9 @@
|
|||||||
--interactive-hover-border: rgba(245, 165, 35, 0.3);
|
--interactive-hover-border: rgba(245, 165, 35, 0.3);
|
||||||
--interactive-row-hover: rgba(255,255,255,0.03);
|
--interactive-row-hover: rgba(255,255,255,0.03);
|
||||||
--file-row-alt-bg: rgba(255,255,255,0.02);
|
--file-row-alt-bg: rgba(255,255,255,0.02);
|
||||||
|
--overlay-scrim: rgba(0,0,0,0.58);
|
||||||
|
--popup-shadow: 0 24px 64px rgba(0,0,0,0.5);
|
||||||
|
--avatar-inner-ring: #111111;
|
||||||
--danger: #ef4444;
|
--danger: #ef4444;
|
||||||
--success: #22c55e;
|
--success: #22c55e;
|
||||||
}
|
}
|
||||||
@@ -51,6 +55,7 @@
|
|||||||
linear-gradient(180deg, #ffffff 0%, #ffffff 42%, #ffffff 100%);
|
linear-gradient(180deg, #ffffff 0%, #ffffff 42%, #ffffff 100%);
|
||||||
--card-bg: rgba(0,0,0,0.02);
|
--card-bg: rgba(0,0,0,0.02);
|
||||||
--card-bg-2: rgba(0,0,0,0.08);
|
--card-bg-2: rgba(0,0,0,0.08);
|
||||||
|
--popup-bg: rgba(255,255,255,0.92);
|
||||||
--text-primary: #0d0d0d;
|
--text-primary: #0d0d0d;
|
||||||
--text-secondary: rgba(0,0,0,0.6);
|
--text-secondary: rgba(0,0,0,0.6);
|
||||||
--text-muted: rgba(0,0,0,0.38);
|
--text-muted: rgba(0,0,0,0.38);
|
||||||
@@ -58,6 +63,9 @@
|
|||||||
--interactive-hover-border: rgba(0,0,0,0.2);
|
--interactive-hover-border: rgba(0,0,0,0.2);
|
||||||
--interactive-row-hover: rgba(0,0,0,0.025);
|
--interactive-row-hover: rgba(0,0,0,0.025);
|
||||||
--file-row-alt-bg: rgba(0,0,0,0.02);
|
--file-row-alt-bg: rgba(0,0,0,0.02);
|
||||||
|
--overlay-scrim: rgba(255,255,255,0.72);
|
||||||
|
--popup-shadow: 0 16px 42px rgba(0,0,0,0.18);
|
||||||
|
--avatar-inner-ring: #ffffff;
|
||||||
}
|
}
|
||||||
[data-theme="light"] input[type="text"],
|
[data-theme="light"] input[type="text"],
|
||||||
[data-theme="light"] input[type="email"],
|
[data-theme="light"] input[type="email"],
|
||||||
@@ -169,7 +177,7 @@ body {
|
|||||||
.sidebar-link {
|
.sidebar-link {
|
||||||
display: flex; align-items: center; gap: 10px;
|
display: flex; align-items: center; gap: 10px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 10px 8px; border-radius: 4px;
|
padding: 10px 8px; border-radius: 8px;
|
||||||
color: var(--sidebar-text); text-decoration: none;
|
color: var(--sidebar-text); text-decoration: none;
|
||||||
font-size: 13px; font-weight: 500;
|
font-size: 13px; font-weight: 500;
|
||||||
transition: background 0.15s, color 0.15s;
|
transition: background 0.15s, color 0.15s;
|
||||||
@@ -190,7 +198,7 @@ body {
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
background: var(--sidebar-bg);
|
background: var(--sidebar-bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -204,7 +212,20 @@ body {
|
|||||||
.sidebar-link:hover .nav-label { opacity: 1; }
|
.sidebar-link:hover .nav-label { opacity: 1; }
|
||||||
[data-theme="light"] .nav-label { color: #0d0d0d; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
[data-theme="light"] .nav-label { color: #0d0d0d; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||||
.sidebar-tools-divider,
|
.sidebar-tools-divider,
|
||||||
.sidebar-tools-label { display: none; }
|
.sidebar-tools-label { display: block; }
|
||||||
|
.sidebar-tools-divider {
|
||||||
|
height: 1px;
|
||||||
|
margin: 10px 12px;
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
.sidebar-tools-label {
|
||||||
|
padding: 0 12px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
.grid-card { background: var(--card-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); cursor: pointer; transition: background 0.15s; }
|
.grid-card { background: var(--card-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); cursor: pointer; transition: background 0.15s; }
|
||||||
.grid-card:hover { background: var(--card-bg-2); }
|
.grid-card:hover { background: var(--card-bg-2); }
|
||||||
[data-theme="light"] .grid-card:hover { background: #fafafa; }
|
[data-theme="light"] .grid-card:hover { background: #fafafa; }
|
||||||
@@ -251,7 +272,8 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
display: block;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
@@ -261,28 +283,55 @@ body {
|
|||||||
.hot-items-row { cursor: pointer; }
|
.hot-items-row { cursor: pointer; }
|
||||||
.hot-items-row:hover td { background: rgba(255,255,255,0.04); }
|
.hot-items-row:hover td { background: rgba(255,255,255,0.04); }
|
||||||
.site-header-right { display: flex; align-items: center; gap: 0; }
|
.site-header-right { display: flex; align-items: center; gap: 0; }
|
||||||
.site-header-search-wrap { position: relative; margin-right: 7px; }
|
.site-header-search-wrap { position: relative; margin-right: 7px; width: 300px; flex: 0 0 300px; }
|
||||||
.site-header-search-btn { background: none; border: 1px solid transparent; cursor: pointer; color: var(--text-muted); display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 4px; }
|
.site-header-search-btn { background: none; border: 1px solid transparent; cursor: pointer; color: var(--text-muted); display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; }
|
||||||
.site-header-search-btn:hover { color: var(--accent); }
|
.site-header-search-btn:hover { color: var(--accent); }
|
||||||
.site-header-search-btn svg { width: 16px; height: 16px; }
|
.site-header-search-btn svg { width: 16px; height: 16px; }
|
||||||
.site-header-search { width: 240px; height: 34px; background: var(--card-bg-2); border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary); font-size: 13px; padding: 0 12px 0 34px; outline: none; font-family: inherit; transition: border-color 0.15s, width 0.2s; }
|
.site-header-search,
|
||||||
.site-header-search:focus { border-color: var(--accent); width: 300px; }
|
input.site-header-search[type="text"] {
|
||||||
.site-header-search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-muted); pointer-events: none; display: flex; }
|
width: 300px;
|
||||||
|
height: var(--h-control);
|
||||||
|
background: var(--card-bg-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 3px 12px 0 38px;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
.site-header-search::placeholder,
|
||||||
|
input.site-header-search[type="text"]::placeholder {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.site-header-search:focus,
|
||||||
|
input.site-header-search[type="text"]:focus { border-color: var(--accent); }
|
||||||
|
.site-header-search-icon { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); color: var(--text-muted); pointer-events: none; display: flex; }
|
||||||
.site-header-search-icon svg { width: 14px; height: 14px; }
|
.site-header-search-icon svg { width: 14px; height: 14px; }
|
||||||
.site-header-dropdown { position: absolute; top: calc(100% + 6px); right: 0; width: 340px; background: var(--sidebar-bg); border: 1px solid var(--border); border-radius: 4px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); z-index: 300; overflow: hidden; }
|
.site-header-dropdown { position: absolute; top: calc(100% + 6px); right: 0; width: 340px; background: var(--sidebar-bg); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); z-index: 300; overflow: hidden; }
|
||||||
.site-header-dropdown { animation: ui-fade-up var(--motion-fast) var(--motion-ease); }
|
.site-header-dropdown { animation: ui-fade-up var(--motion-fast) var(--motion-ease); }
|
||||||
.site-header-dropdown-group { padding: 8px 12px 4px; font-size: 10px; font-weight: 500; letter-spacing: 0.8px; text-transform: uppercase; color: var(--text-muted); }
|
.site-header-dropdown-group { padding: 8px 12px 4px; font-size: 10px; font-weight: 500; letter-spacing: 0.8px; text-transform: uppercase; color: var(--text-muted); }
|
||||||
.site-header-dropdown-item { display: flex; align-items: center; gap: 10px; padding: 9px 12px; cursor: pointer; font-size: 13px; color: var(--text-primary); border: 1px solid transparent; }
|
.site-header-dropdown-item { display: flex; align-items: center; gap: 10px; padding: 9px 12px; cursor: pointer; font-size: 13px; color: var(--text-primary); border: 1px solid transparent; }
|
||||||
.site-header-dropdown-item:hover { color: #fff; }
|
.site-header-dropdown-item:hover { color: #fff; }
|
||||||
.site-header-dropdown-empty { padding: 12px; font-size: 13px; color: var(--text-muted); text-align: center; }
|
.site-header-dropdown-empty { padding: 12px; font-size: 13px; color: var(--text-muted); text-align: center; }
|
||||||
.site-header-avatar-wrap { position: relative; margin-left: 14px; }
|
.site-header-avatar-wrap { position: relative; margin-left: 14px; }
|
||||||
.site-header-theme-toggle { background: none; border: 1px solid transparent; border-radius: 4px; cursor: pointer; color: var(--text-muted); display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; flex-shrink: 0; }
|
.site-header-theme-toggle { background: none; border: 1px solid transparent; border-radius: 8px; cursor: pointer; color: var(--text-muted); display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; flex-shrink: 0; }
|
||||||
.site-header-theme-toggle:hover { color: var(--accent); }
|
.site-header-theme-toggle:hover { color: var(--accent); }
|
||||||
.site-header-theme-toggle svg { width: 16px; height: 16px; display: block; flex-shrink: 0; }
|
.site-header-theme-toggle svg { width: 16px; height: 16px; display: block; flex-shrink: 0; }
|
||||||
.site-header-avatar-btn { width: 49px; height: 49px; border-radius: 50%; background: var(--card-bg-2); border: 2px solid #111; outline: 2px solid var(--accent); outline-offset: 0; cursor: pointer; color: var(--text-muted); display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: opacity 0.15s; padding: 0; }
|
.site-header-avatar-btn { width: 49px; height: 49px; border-radius: 50%; background: var(--card-bg-2); border: 2px solid #111; outline: 2px solid var(--accent); outline-offset: 0; cursor: pointer; color: var(--text-muted); display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: opacity 0.15s; padding: 0; }
|
||||||
.site-header-avatar-btn:hover { opacity: 0.85; }
|
.site-header-avatar-btn:hover { opacity: 0.85; }
|
||||||
[data-theme="light"] .site-header-avatar-btn { border-color: #fff; }
|
[data-theme="light"] .site-header-avatar-btn { border-color: #fff; }
|
||||||
.site-header-avatar-menu { position: absolute; top: calc(100% + 8px); right: 0; background: var(--sidebar-bg); border: 1px solid var(--border); border-radius: 4px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); z-index: 300; min-width: 160px; overflow: hidden; }
|
.site-header-avatar-menu { position: absolute; top: calc(100% + 8px); right: 0; background: var(--sidebar-bg); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); z-index: 300; min-width: 160px; overflow: hidden; }
|
||||||
.site-header-avatar-menu { animation: ui-fade-up var(--motion-fast) var(--motion-ease); }
|
.site-header-avatar-menu { animation: ui-fade-up var(--motion-fast) var(--motion-ease); }
|
||||||
.site-header-avatar-item { padding: 10px 16px; cursor: pointer; font-size: 13px; color: var(--text-primary); display: block; width: 100%; text-align: left; background: none; border: 1px solid transparent; font-family: inherit; }
|
.site-header-avatar-item { padding: 10px 16px; cursor: pointer; font-size: 13px; color: var(--text-primary); display: block; width: 100%; text-align: left; background: none; border: 1px solid transparent; font-family: inherit; }
|
||||||
.site-header-avatar-item:hover { color: #fff; }
|
.site-header-avatar-item:hover { color: #fff; }
|
||||||
@@ -292,6 +341,39 @@ body {
|
|||||||
.main-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
.main-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||||||
.main-content::-webkit-scrollbar-thumb:hover { background: var(--interactive-hover-border); }
|
.main-content::-webkit-scrollbar-thumb:hover { background: var(--interactive-hover-border); }
|
||||||
|
|
||||||
|
.scrollbar-thin-theme {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: transparent transparent;
|
||||||
|
}
|
||||||
|
.scrollbar-thin-theme::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
.scrollbar-thin-theme::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.scrollbar-thin-theme::-webkit-scrollbar-thumb {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
.scrollbar-thin-theme:hover {
|
||||||
|
scrollbar-color: var(--interactive-hover-border) transparent;
|
||||||
|
}
|
||||||
|
.scrollbar-thin-theme:focus-within {
|
||||||
|
scrollbar-color: var(--interactive-hover-border) transparent;
|
||||||
|
}
|
||||||
|
.scrollbar-thin-theme:hover::-webkit-scrollbar-thumb,
|
||||||
|
.scrollbar-thin-theme:focus-within::-webkit-scrollbar-thumb,
|
||||||
|
.scrollbar-thin-theme::-webkit-scrollbar-thumb:active {
|
||||||
|
background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 62%, var(--border) 38%), color-mix(in srgb, var(--accent) 42%, var(--border) 58%));
|
||||||
|
}
|
||||||
|
.scrollbar-thin-theme::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 72%, var(--border) 28%), color-mix(in srgb, var(--accent) 52%, var(--border) 48%));
|
||||||
|
}
|
||||||
|
|
||||||
/* Page header */
|
/* Page header */
|
||||||
.page-header {
|
.page-header {
|
||||||
margin-bottom: 24px; display: flex;
|
margin-bottom: 24px; display: flex;
|
||||||
@@ -987,6 +1069,14 @@ textarea {
|
|||||||
|
|
||||||
/* Badges */
|
/* Badges */
|
||||||
.badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 4px; font-size: 11px; font-weight: 400; white-space: nowrap; letter-spacing: 0.3px; }
|
.badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 4px; font-size: 11px; font-weight: 400; white-space: nowrap; letter-spacing: 0.3px; }
|
||||||
|
.badge-status {
|
||||||
|
min-width: 78px;
|
||||||
|
height: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
.badge-not_started { background: #222; color: #888; border: 1px solid #333; }
|
.badge-not_started { background: #222; color: #888; border: 1px solid #333; }
|
||||||
.badge-in_progress { background: rgba(37,99,235,0.15); color: #60a5fa; border: 1px solid rgba(37,99,235,0.3); }
|
.badge-in_progress { background: rgba(37,99,235,0.15); color: #60a5fa; border: 1px solid rgba(37,99,235,0.3); }
|
||||||
.badge-on_hold { background: rgba(217,119,6,0.15); color: #fbbf24; border: 1px solid rgba(217,119,6,0.3); }
|
.badge-on_hold { background: rgba(217,119,6,0.15); color: #fbbf24; border: 1px solid rgba(217,119,6,0.3); }
|
||||||
@@ -1015,8 +1105,28 @@ th { text-align: left; padding: 12px 16px; font-size: 10px; font-weight: 400; te
|
|||||||
td { padding: 14px 16px; border-bottom: 1px solid var(--border); font-size: 13px; color: var(--text-primary); }
|
td { padding: 14px 16px; border-bottom: 1px solid var(--border); font-size: 13px; color: var(--text-primary); }
|
||||||
|
|
||||||
tr:hover td { background: rgba(255,255,255,0.02); }
|
tr:hover td { background: rgba(255,255,255,0.02); }
|
||||||
.table-link { color: var(--accent); text-decoration: none; font-weight: 400; }
|
.table-link {
|
||||||
.table-link:hover { text-decoration: underline; }
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 400;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.table-link:hover,
|
||||||
|
.table-link:focus-visible {
|
||||||
|
color: var(--accent) !important;
|
||||||
|
text-decoration: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-sticky-head thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 3;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
.table-scroll-fade {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
.dashboard-inline-link {
|
.dashboard-inline-link {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|||||||
+579
-320
File diff suppressed because it is too large
Load Diff
@@ -117,7 +117,13 @@ export default function CreateInvoice() {
|
|||||||
return { ...t, service_type: initial?.service_type || t.title };
|
return { ...t, service_type: initial?.service_type || t.title };
|
||||||
});
|
});
|
||||||
setUninvoicedTasks(tasksWithService);
|
setUninvoicedTasks(tasksWithService);
|
||||||
setUninvoicedRevisions(revisions || []);
|
// Deduplicate by (task_id, version_number) — multiple submission rows per version (e.g. "Add Files") must not produce multiple invoice charges
|
||||||
|
const revMap = new Map();
|
||||||
|
for (const rev of (revisions || [])) {
|
||||||
|
const key = `${rev.task_id}:${rev.version_number}`;
|
||||||
|
if (!revMap.has(key)) revMap.set(key, rev);
|
||||||
|
}
|
||||||
|
setUninvoicedRevisions([...revMap.values()]);
|
||||||
} else {
|
} else {
|
||||||
setUninvoicedTasks([]);
|
setUninvoicedTasks([]);
|
||||||
setUninvoicedRevisions([]);
|
setUninvoicedRevisions([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user