feat: complete Scrum-manager MVP — dark-themed multi-user task manager

- Login with role-based auth (CTO/Manager/Employee)
- Calendar view (month/week) with task chips and quick-add
- Kanban board with status columns
- Sortable list view with action menus
- Task detail drawer with subtasks, comments, activity
- Add task modal with validation
- Dashboard with stats, workload, priority breakdown
- Team tasks grouped by assignee
- Reports page with recharts (bar, pie, line, horizontal bar)
- Members page with invite modal
- Search and assignee filter across views
- ErrorBoundary for production error handling
- Full dark design system via index.css
This commit is contained in:
tusuii
2026-02-15 11:36:38 +05:30
commit e46d8773ee
26 changed files with 5410 additions and 0 deletions

139
src/App.tsx Normal file
View File

@@ -0,0 +1,139 @@
import { useState } from 'react';
import { SEED_TASKS } from './data';
import type { Task, User, Status } from './data';
import { LoginPage } from './Login';
import { Sidebar } from './Sidebar';
import { TopNavbar, BottomToggleBar } from './NavBars';
import { CalendarView, QuickAddPanel } from './Calendar';
import { KanbanBoard } from './Kanban';
import { ListView } from './ListView';
import { TaskDrawer, AddTaskModal } from './TaskDrawer';
import { DashboardPage } from './Dashboard';
import { TeamTasksPage, MembersPage } from './Pages';
import { ReportsPage } from './Reports';
import './index.css';
const PAGE_TITLES: Record<string, string> = {
dashboard: 'Dashboard', calendar: 'Calendar', kanban: 'Kanban Board',
mytasks: 'My Tasks', teamtasks: 'Team Tasks', reports: 'Reports', members: 'Members',
list: 'List View',
};
const VIEW_PAGES = ['calendar', 'kanban', 'list'];
export default function App() {
const now = new Date();
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [tasks, setTasks] = useState<Task[]>(SEED_TASKS);
const [activePage, setActivePage] = useState('calendar');
const [activeView, setActiveView] = useState('calendar');
const [activeTask, setActiveTask] = useState<Task | null>(null);
const [showAddModal, setShowAddModal] = useState(false);
const [addModalDefaults, setAddModalDefaults] = useState<{ date?: string; status?: Status }>({});
const [calMonth, setCalMonth] = useState({ year: now.getFullYear(), month: now.getMonth() });
const [calView, setCalView] = useState('month');
const [filterUser, setFilterUser] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [quickAddDay, setQuickAddDay] = useState<{ date: string; rect: { top: number; left: number } } | null>(null);
if (!currentUser) return <LoginPage onLogin={u => { setCurrentUser(u); setActivePage('calendar'); setActiveView('calendar'); }} />;
const handleNavigate = (page: string) => {
setActivePage(page);
if (VIEW_PAGES.includes(page)) setActiveView(page);
};
const handleViewChange = (view: string) => {
setActiveView(view);
if (VIEW_PAGES.includes(view)) setActivePage(view);
};
const handleTaskClick = (t: Task) => setActiveTask(t);
const handleDayClick = (date: string, el: HTMLElement) => {
const rect = el.getBoundingClientRect();
setQuickAddDay({ date, rect: { top: rect.bottom, left: rect.left } });
};
const handleQuickAdd = (partial: Partial<Task>) => {
const task: Task = {
id: `t${Date.now()}`, title: partial.title || '', description: partial.description || '',
status: (partial.status || 'todo') as Status, priority: partial.priority || 'medium',
assignee: partial.assignee || 'u1', reporter: currentUser.id, dueDate: partial.dueDate || '',
tags: partial.tags || [], subtasks: partial.subtasks || [], comments: partial.comments || [],
activity: [{ id: `a${Date.now()}`, text: '📝 Task created', timestamp: new Date().toISOString() }],
};
setTasks(prev => [...prev, task]);
setQuickAddDay(null);
};
const handleAddTask = (task: Task) => setTasks(prev => [...prev, { ...task, reporter: currentUser.id }]);
const handleUpdateTask = (updated: Task) => {
setTasks(prev => prev.map(t => t.id === updated.id ? updated : t));
setActiveTask(updated);
};
const handleNewTask = () => { setAddModalDefaults({}); setShowAddModal(true); };
const handleKanbanAdd = (status: Status) => { setAddModalDefaults({ status }); setShowAddModal(true); };
const handleToggleDone = (taskId: string) => {
setTasks(prev => prev.map(t => t.id === taskId ? { ...t, status: t.status === 'done' ? 'todo' : 'done' as Status } : t));
};
const displayPage = VIEW_PAGES.includes(activePage) ? activeView : activePage;
const filteredMyTasks = tasks.filter(t => t.assignee === currentUser.id);
const pageTitle = PAGE_TITLES[displayPage] || 'Calendar';
return (
<div className="app-shell">
<TopNavbar title={pageTitle} filterUser={filterUser} onFilterChange={setFilterUser}
searchQuery={searchQuery} onSearch={setSearchQuery} onNewTask={handleNewTask} />
<div className="app-body">
<Sidebar currentUser={currentUser} activePage={activePage} onNavigate={handleNavigate}
onSignOut={() => { setCurrentUser(null); setActivePage('calendar'); setActiveView('calendar'); }} />
<div className="main-content">
{displayPage === 'calendar' && (
<CalendarView tasks={tasks} currentUser={currentUser} calMonth={calMonth} calView={calView}
onMonthChange={setCalMonth} onViewChange={setCalView} onTaskClick={handleTaskClick}
onDayClick={handleDayClick} filterUser={filterUser} searchQuery={searchQuery} />
)}
{displayPage === 'kanban' && (
<KanbanBoard tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
onAddTask={handleKanbanAdd} filterUser={filterUser} searchQuery={searchQuery} />
)}
{displayPage === 'list' && (
<ListView tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
filterUser={filterUser} searchQuery={searchQuery} onToggleDone={handleToggleDone} />
)}
{displayPage === 'dashboard' && <DashboardPage tasks={tasks} currentUser={currentUser} />}
{displayPage === 'mytasks' && (
<ListView tasks={filteredMyTasks} currentUser={currentUser} onTaskClick={handleTaskClick}
filterUser={null} searchQuery={searchQuery} onToggleDone={handleToggleDone} />
)}
{displayPage === 'teamtasks' && <TeamTasksPage tasks={tasks} currentUser={currentUser} />}
{displayPage === 'reports' && <ReportsPage tasks={tasks} />}
{displayPage === 'members' && <MembersPage tasks={tasks} />}
</div>
</div>
{VIEW_PAGES.includes(activePage) && (
<BottomToggleBar activeView={activeView} onViewChange={handleViewChange} />
)}
{activeTask && <TaskDrawer task={activeTask} currentUser={currentUser} onClose={() => setActiveTask(null)} onUpdate={handleUpdateTask} />}
{showAddModal && <AddTaskModal onClose={() => setShowAddModal(false)} onAdd={handleAddTask} defaultDate={addModalDefaults.date} defaultStatus={addModalDefaults.status} />}
{quickAddDay && (
<div style={{ position: 'fixed', inset: 0, zIndex: 199 }} onClick={() => setQuickAddDay(null)}>
<div style={{ position: 'absolute', top: Math.min(quickAddDay.rect.top, window.innerHeight - 280), left: Math.min(quickAddDay.rect.left, window.innerWidth - 340) }}
onClick={e => e.stopPropagation()}>
<QuickAddPanel date={quickAddDay.date} onAdd={handleQuickAdd}
onOpenFull={() => { setAddModalDefaults({ date: quickAddDay.date }); setShowAddModal(true); setQuickAddDay(null); }}
onClose={() => setQuickAddDay(null)} />
</div>
</div>
)}
</div>
);
}

190
src/Calendar.tsx Normal file
View File

@@ -0,0 +1,190 @@
import { useState } from 'react';
import type { Task, User } from './data';
import { USERS, PRIORITY_COLORS } from './data';
import { Avatar } from './Shared';
interface CalendarProps {
tasks: Task[]; currentUser: User; calMonth: { year: number; month: number }; calView: string;
onMonthChange: (m: { year: number; month: number }) => void; onViewChange: (v: string) => void;
onTaskClick: (t: Task) => void; onDayClick: (date: string, el: HTMLElement) => void;
filterUser: string | null; searchQuery: string;
}
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const DAYS = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
function filterTasks(tasks: Task[], user: User, filterUser: string | null, search: string) {
let t = tasks;
if (user.role === 'employee') t = t.filter(x => x.assignee === user.id);
if (filterUser) t = t.filter(x => x.assignee === filterUser);
if (search) t = t.filter(x => x.title.toLowerCase().includes(search.toLowerCase()));
return t;
}
function getMonthDays(year: number, month: number) {
const first = new Date(year, month, 1);
const startDay = first.getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const prevMonthDays = new Date(year, month, 0).getDate();
const cells: { date: Date; isCurrentMonth: boolean }[] = [];
for (let i = startDay - 1; i >= 0; i--) cells.push({ date: new Date(year, month - 1, prevMonthDays - i), isCurrentMonth: false });
for (let i = 1; i <= daysInMonth; i++) cells.push({ date: new Date(year, month, i), isCurrentMonth: true });
const rem = 42 - cells.length;
for (let i = 1; i <= rem; i++) cells.push({ date: new Date(year, month + 1, i), isCurrentMonth: false });
return cells;
}
function getWeekDays(_year: number, _month: number) {
const today = new Date();
const dayOfWeek = today.getDay();
const start = new Date(today);
start.setDate(today.getDate() - dayOfWeek);
const cells: Date[] = [];
for (let i = 0; i < 7; i++) { const d = new Date(start); d.setDate(start.getDate() + i); cells.push(d); }
return cells;
}
function dateStr(d: Date) { return d.toISOString().split('T')[0]; }
function isToday(d: Date) { const t = new Date(); return d.getDate() === t.getDate() && d.getMonth() === t.getMonth() && d.getFullYear() === t.getFullYear(); }
function TaskChip({ task, onClick }: { task: Task; onClick: () => void }) {
const p = PRIORITY_COLORS[task.priority];
return (
<div className="task-chip" style={{ background: p.bg, borderLeftColor: p.color }} onClick={e => { e.stopPropagation(); onClick(); }}>
<span className="task-chip-dot" style={{ background: p.color }} />
<span className="task-chip-title">{task.title}</span>
<Avatar userId={task.assignee} size={14} />
</div>
);
}
function MorePopover({ tasks, onTaskClick, onClose }: { tasks: Task[]; onTaskClick: (t: Task) => void; onClose: () => void }) {
return (
<div className="more-popover" onClick={e => e.stopPropagation()}>
<div className="more-popover-title">{tasks.length} tasks</div>
{tasks.map(t => <TaskChip key={t.id} task={t} onClick={() => { onTaskClick(t); onClose(); }} />)}
</div>
);
}
export function QuickAddPanel({ date, onAdd, onOpenFull, onClose }: { date: string; onAdd: (t: Partial<Task>) => void; onOpenFull: () => void; onClose: () => void }) {
const [title, setTitle] = useState('');
const [assignee, setAssignee] = useState('u1');
const [priority, setPriority] = useState<'medium' | 'low' | 'high' | 'critical'>('medium');
const submit = () => {
if (!title.trim()) return;
onAdd({ title, assignee, priority, dueDate: date, status: 'todo', description: '', tags: [], subtasks: [], comments: [], activity: [] });
setTitle('');
};
return (
<div className="quick-add-panel" onClick={e => e.stopPropagation()}>
<button className="quick-add-close" onClick={onClose}></button>
<div className="quick-add-header">📅 Add Task {new Date(date + 'T00:00:00').toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })}</div>
<input className="quick-add-input" placeholder="Task title..." value={title} autoFocus
onChange={e => setTitle(e.target.value)} onKeyDown={e => e.key === 'Enter' && submit()} />
<div className="quick-add-row">
<select className="quick-add-select" value={assignee} onChange={e => setAssignee(e.target.value)}>
{USERS.map(u => <option key={u.id} value={u.id}>{u.avatar} {u.name}</option>)}
</select>
<select className="quick-add-select" value={priority} onChange={e => setPriority(e.target.value as any)}>
{['critical', 'high', 'medium', 'low'].map(p => <option key={p} value={p}>{p}</option>)}
</select>
</div>
<div className="quick-add-actions">
<button className="quick-add-submit" onClick={submit}>Add Task</button>
<button className="quick-add-link" onClick={onOpenFull}>Open Full Form</button>
</div>
</div>
);
}
export function CalendarView({ tasks, currentUser, calMonth, calView, onMonthChange, onViewChange, onTaskClick, onDayClick, filterUser, searchQuery }: CalendarProps) {
const [morePopover, setMorePopover] = useState<{ date: string; tasks: Task[] } | null>(null);
const filtered = filterTasks(tasks, currentUser, filterUser, searchQuery);
const prevMonth = () => {
if (calView === 'week') { const d = new Date(); d.setDate(d.getDate() - 7); onMonthChange({ year: d.getFullYear(), month: d.getMonth() }); return; }
const m = calMonth.month === 0 ? 11 : calMonth.month - 1;
const y = calMonth.month === 0 ? calMonth.year - 1 : calMonth.year;
onMonthChange({ year: y, month: m });
};
const nextMonth = () => {
if (calView === 'week') { const d = new Date(); d.setDate(d.getDate() + 7); onMonthChange({ year: d.getFullYear(), month: d.getMonth() }); return; }
const m = calMonth.month === 11 ? 0 : calMonth.month + 1;
const y = calMonth.month === 11 ? calMonth.year + 1 : calMonth.year;
onMonthChange({ year: y, month: m });
};
const goToday = () => { const n = new Date(); onMonthChange({ year: n.getFullYear(), month: n.getMonth() }); };
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div className="calendar-toolbar">
<div className="cal-nav">
<button className="cal-nav-btn" onClick={prevMonth}></button>
<span className="cal-month-label">{MONTHS[calMonth.month]} {calMonth.year}</span>
<button className="cal-nav-btn" onClick={nextMonth}></button>
<button className="cal-today-btn" onClick={goToday}>Today</button>
</div>
<div className="cal-view-toggle">
<button className={`cal-view-btn ${calView === 'month' ? 'active' : ''}`} onClick={() => onViewChange('month')}>Month</button>
<button className={`cal-view-btn ${calView === 'week' ? 'active' : ''}`} onClick={() => onViewChange('week')}>Week</button>
</div>
</div>
{calView === 'month' ? (
<div className="month-grid" style={{ gridTemplateRows: 'auto repeat(6, 1fr)' }}>
{DAYS.map(d => <div key={d} className="month-grid-header">{d}</div>)}
{getMonthDays(calMonth.year, calMonth.month).map((cell, i) => {
const ds = dateStr(cell.date);
const dayTasks = filtered.filter(t => t.dueDate === ds);
const show = dayTasks.slice(0, 3);
const extra = dayTasks.length - 3;
return (
<div key={i} className="day-cell" onClick={e => { if (!(e.target as HTMLElement).closest('.task-chip,.more-tasks-link,.more-popover')) onDayClick(ds, e.currentTarget); }}>
<div className={`day-number ${isToday(cell.date) ? 'today' : ''} ${!cell.isCurrentMonth ? 'other-month' : ''}`}>
{cell.date.getDate()}
</div>
<div className="day-tasks">
{show.map(t => <TaskChip key={t.id} task={t} onClick={() => onTaskClick(t)} />)}
{extra > 0 && (
<span className="more-tasks-link" onClick={e => { e.stopPropagation(); setMorePopover({ date: ds, tasks: dayTasks }); }}>
+{extra} more
</span>
)}
</div>
{morePopover?.date === ds && <MorePopover tasks={morePopover.tasks} onTaskClick={onTaskClick} onClose={() => setMorePopover(null)} />}
</div>
);
})}
</div>
) : (
<div className="week-grid" style={{ gridTemplateRows: 'auto 1fr' }}>
{getWeekDays(calMonth.year, calMonth.month).map((d, i) => (
<div key={i} className={`week-header-cell ${isToday(d) ? 'today' : ''}`}>
{DAYS[d.getDay()]} {d.getDate()}
</div>
))}
{getWeekDays(calMonth.year, calMonth.month).map((d, i) => {
const ds = dateStr(d);
const dayTasks = filtered.filter(t => t.dueDate === ds);
return (
<div key={i} className="week-day-cell" onClick={e => { if (!(e.target as HTMLElement).closest('.task-chip,.week-chip')) onDayClick(ds, e.currentTarget); }}>
{dayTasks.map(t => {
const p = PRIORITY_COLORS[t.priority];
return (
<div key={t.id} className="week-chip" style={{ background: p.bg, borderLeftColor: p.color }} onClick={e => { e.stopPropagation(); onTaskClick(t); }}>
<span className="task-chip-dot" style={{ background: p.color }} />
<span className="task-chip-title">{t.title}</span>
</div>
);
})}
</div>
);
})}
</div>
)}
</div>
);
}

122
src/Dashboard.tsx Normal file
View File

@@ -0,0 +1,122 @@
import type { Task, User } from './data';
import { USERS, STATUS_COLORS, PRIORITY_COLORS } from './data';
import { Avatar } from './Shared';
export function DashboardPage({ tasks, currentUser }: { tasks: Task[]; currentUser: User }) {
const total = tasks.length;
const completed = tasks.filter(t => t.status === 'done').length;
const overdue = tasks.filter(t => new Date(t.dueDate + 'T00:00:00') < new Date() && t.status !== 'done').length;
const critical = tasks.filter(t => t.priority === 'critical' && t.status !== 'done').length;
const isLeader = currentUser.role === 'cto' || currentUser.role === 'manager';
const myTasks = tasks.filter(t => t.assignee === currentUser.id);
const myDone = myTasks.filter(t => t.status === 'done').length;
return (
<div className="dashboard">
<div className="stats-row">
{[
{ label: 'Total Tasks', num: total, border: '#6366f1' },
{ label: 'Completed', num: completed, border: '#22c55e' },
{ label: 'Overdue', num: overdue, border: '#ef4444' },
{ label: 'Critical Open', num: critical, border: '#f97316' },
].map(s => (
<div key={s.label} className="stat-card" style={{ borderTop: `3px solid ${s.border}` }}>
<div className="stat-card-num">{s.num}</div>
<div className="stat-card-label">{s.label}</div>
</div>
))}
</div>
{isLeader ? (
<>
<div className="workload-card">
<div className="workload-card-title">Team Workload</div>
{USERS.filter(u => u.id !== currentUser.id || true).map(u => {
const ut = tasks.filter(t => t.assignee === u.id);
const done = ut.filter(t => t.status === 'done').length;
const pct = ut.length ? Math.round((done / ut.length) * 100) : 0;
return (
<div key={u.id} className="workload-row">
<Avatar userId={u.id} size={28} />
<span className="workload-name">{u.name}</span>
<span className="workload-dept">{u.dept}</span>
<div className="workload-bar">
<div className="progress-bar" style={{ height: 6, borderRadius: 3 }}>
<div className="progress-bar-fill" style={{ width: `${pct}%`, background: '#6366f1', borderRadius: 3 }} />
</div>
</div>
<div className="workload-badges">
{(['todo', 'inprogress', 'review', 'done'] as const).map(s => {
const cnt = ut.filter(t => t.status === s).length;
return cnt > 0 ? (
<span key={s} style={{ background: `${STATUS_COLORS[s]}22`, color: STATUS_COLORS[s], padding: '1px 6px', borderRadius: 8, fontSize: 10, fontWeight: 700 }}>
{cnt}
</span>
) : null;
})}
</div>
</div>
);
})}
</div>
<div className="workload-card">
<div className="workload-card-title">Priority Breakdown</div>
<div className="priority-bar" style={{ height: 28 }}>
{(['critical', 'high', 'medium', 'low'] as const).map(p => {
const cnt = tasks.filter(t => t.priority === p).length;
const pct = total ? Math.round((cnt / total) * 100) : 0;
if (!pct) return null;
return (
<div key={p} className="priority-segment" style={{ width: `${pct}%`, background: PRIORITY_COLORS[p].color }}>
{pct}%
</div>
);
})}
</div>
<div style={{ display: 'flex', gap: 16, marginTop: 10 }}>
{(['critical', 'high', 'medium', 'low'] as const).map(p => (
<span key={p} style={{ fontSize: 11, color: PRIORITY_COLORS[p].color, display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: PRIORITY_COLORS[p].color, display: 'inline-block' }} />
{p}
</span>
))}
</div>
</div>
</>
) : (
<>
<div className="workload-card">
<div className="workload-card-title">My Upcoming Deadlines</div>
{myTasks.filter(t => t.status !== 'done').sort((a, b) => a.dueDate.localeCompare(b.dueDate)).slice(0, 5).map(t => {
const overdue = new Date(t.dueDate + 'T00:00:00') < new Date();
return (
<div key={t.id} className={`deadline-item ${overdue ? 'overdue' : ''}`}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: PRIORITY_COLORS[t.priority].color }} />
<span style={{ flex: 1, fontSize: 13 }}>{t.title}</span>
<span style={{ fontSize: 11, color: overdue ? '#ef4444' : '#64748b' }}>
{new Date(t.dueDate + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
</div>
);
})}
</div>
<div className="workload-card" style={{ textAlign: 'center' }}>
<div className="workload-card-title">My Progress</div>
<div style={{ fontSize: 32, fontWeight: 800 }}>{myDone} / {myTasks.length}</div>
<div style={{ fontSize: 12, color: '#64748b', marginBottom: 16 }}>tasks done</div>
<svg className="progress-ring" width="120" height="120" viewBox="0 0 120 120">
<circle cx="60" cy="60" r="50" fill="none" stroke="#1e293b" strokeWidth="10" />
<circle cx="60" cy="60" r="50" fill="none" stroke="#6366f1" strokeWidth="10"
strokeDasharray={`${(myDone / (myTasks.length || 1)) * 314} 314`}
strokeLinecap="round" transform="rotate(-90 60 60)" />
<text x="60" y="65" textAnchor="middle" fill="#f1f5f9" fontSize="18" fontWeight="800">
{myTasks.length ? Math.round((myDone / myTasks.length) * 100) : 0}%
</text>
</svg>
</div>
</>
)}
</div>
);
}

48
src/ErrorBoundary.tsx Normal file
View File

@@ -0,0 +1,48 @@
import React from 'react';
interface Props { children: React.ReactNode; }
interface State { hasError: boolean; error: Error | null; }
export class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
minHeight: '100vh', background: '#0a0f1a', color: '#e2e8f0',
fontFamily: '"DM Sans", sans-serif', flexDirection: 'column', gap: 16,
}}>
<div style={{ fontSize: 48 }}></div>
<h1 style={{ fontSize: 24, fontWeight: 700 }}>Something went wrong</h1>
<p style={{ color: '#94a3b8', maxWidth: 400, textAlign: 'center', lineHeight: 1.6 }}>
{this.state.error?.message || 'An unexpected error occurred.'}
</p>
<button
onClick={() => { this.setState({ hasError: false, error: null }); window.location.reload(); }}
style={{
background: '#6366f1', color: '#fff', border: 'none', borderRadius: 8,
padding: '10px 24px', fontSize: 14, fontWeight: 600, cursor: 'pointer',
marginTop: 8,
}}
>
Reload App
</button>
</div>
);
}
return this.props.children;
}
}

73
src/Kanban.tsx Normal file
View File

@@ -0,0 +1,73 @@
import type { Task, User, Status } from './data';
import { PRIORITY_COLORS, STATUS_COLORS, STATUS_LABELS } from './data';
import { Avatar, PriorityBadge, StatusBadge, ProgressBar } from './Shared';
function TaskCard({ task, onClick }: { task: Task; onClick: () => void }) {
const p = PRIORITY_COLORS[task.priority];
const due = new Date(task.dueDate + 'T00:00:00');
const overdue = due < new Date() && task.status !== 'done';
const commCount = task.comments.length;
return (
<div className="task-card" style={{ borderLeftColor: p.color }} onClick={onClick}>
<div className="task-card-row">
<span className="task-card-title">{task.title}</span>
<Avatar userId={task.assignee} size={24} />
</div>
<div className="task-card-badges">
<PriorityBadge level={task.priority} />
<StatusBadge status={task.status} />
{task.tags.slice(0, 2).map(t => <span key={t} className="tag-pill" style={{ fontSize: 9 }}>{t}</span>)}
</div>
{task.subtasks.length > 0 && <ProgressBar subtasks={task.subtasks} />}
<div className="task-card-meta">
<span className={overdue ? 'task-card-overdue' : ''}>📅 {due.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</span>
{commCount > 0 && <span>💬 {commCount}</span>}
</div>
</div>
);
}
function KanbanColumn({ status, statusLabel, tasks, color, onTaskClick, onAddTask }: {
status: Status; statusLabel: string; tasks: Task[]; color: string;
onTaskClick: (t: Task) => void; onAddTask: (s: Status) => void;
}) {
return (
<div className="kanban-column">
<div className="kanban-col-header">
<div className="kanban-col-dot" style={{ background: color }} />
<span className="kanban-col-label">{statusLabel}</span>
<span className="kanban-col-count">{tasks.length}</span>
<button className="kanban-col-add" onClick={() => onAddTask(status)}>+</button>
</div>
<div className="kanban-col-body">
{tasks.length === 0 ? (
<div className="kanban-empty">No tasks here · Click + to add one</div>
) : (
tasks.map(t => <TaskCard key={t.id} task={t} onClick={() => onTaskClick(t)} />)
)}
</div>
</div>
);
}
interface KanbanProps {
tasks: Task[]; currentUser: User; onTaskClick: (t: Task) => void;
onAddTask: (s: Status) => void; filterUser: string | null; searchQuery: string;
}
export function KanbanBoard({ tasks, currentUser, onTaskClick, onAddTask, filterUser, searchQuery }: KanbanProps) {
let filtered = tasks;
if (currentUser.role === 'employee') filtered = filtered.filter(t => t.assignee === currentUser.id);
if (filterUser) filtered = filtered.filter(t => t.assignee === filterUser);
if (searchQuery) filtered = filtered.filter(t => t.title.toLowerCase().includes(searchQuery.toLowerCase()));
const statuses: Status[] = ['todo', 'inprogress', 'review', 'done'];
return (
<div className="kanban-board">
{statuses.map(s => (
<KanbanColumn key={s} status={s} statusLabel={STATUS_LABELS[s]} color={STATUS_COLORS[s]}
tasks={filtered.filter(t => t.status === s)} onTaskClick={onTaskClick} onAddTask={onAddTask} />
))}
</div>
);
}

88
src/ListView.tsx Normal file
View File

@@ -0,0 +1,88 @@
import { useState } from 'react';
import type { Task, User } from './data';
import { getUserById } from './data';
import { Avatar, PriorityBadge, StatusBadge } from './Shared';
interface ListProps {
tasks: Task[]; currentUser: User; onTaskClick: (t: Task) => void;
filterUser: string | null; searchQuery: string;
onToggleDone: (taskId: string) => void;
}
type SortKey = 'dueDate' | 'priority' | 'status' | 'assignee';
const PRIO_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
export function ListView({ tasks, currentUser, onTaskClick, filterUser, searchQuery, onToggleDone }: ListProps) {
const [sortBy, setSortBy] = useState<SortKey>('dueDate');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
const [menuOpen, setMenuOpen] = useState<string | null>(null);
let filtered = tasks;
if (currentUser.role === 'employee') filtered = filtered.filter(t => t.assignee === currentUser.id);
if (filterUser) filtered = filtered.filter(t => t.assignee === filterUser);
if (searchQuery) filtered = filtered.filter(t => t.title.toLowerCase().includes(searchQuery.toLowerCase()));
const sorted = [...filtered].sort((a, b) => {
let cmp = 0;
if (sortBy === 'dueDate') cmp = a.dueDate.localeCompare(b.dueDate);
else if (sortBy === 'priority') cmp = PRIO_ORDER[a.priority] - PRIO_ORDER[b.priority];
else if (sortBy === 'status') cmp = a.status.localeCompare(b.status);
else if (sortBy === 'assignee') cmp = a.assignee.localeCompare(b.assignee);
return sortDir === 'asc' ? cmp : -cmp;
});
const toggleSort = (key: SortKey) => {
if (sortBy === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
else { setSortBy(key); setSortDir('asc'); }
};
return (
<div className="list-view">
<div className="list-sort-row">
Sort by:
{(['dueDate', 'priority', 'status', 'assignee'] as SortKey[]).map(k => (
<button key={k} className={`list-sort-btn ${sortBy === k ? 'active' : ''}`} onClick={() => toggleSort(k)}>
{k === 'dueDate' ? 'Due Date' : k.charAt(0).toUpperCase() + k.slice(1)}
{sortBy === k && (sortDir === 'asc' ? ' ↑' : ' ↓')}
</button>
))}
</div>
<table className="list-table">
<thead>
<tr><th></th><th>Title</th><th>Assignee</th><th>Priority</th><th>Status</th><th>Due Date</th><th>Tags</th><th>Actions</th></tr>
</thead>
<tbody>
{sorted.map(t => {
const u = getUserById(t.assignee);
const due = new Date(t.dueDate + 'T00:00:00');
const overdue = due < new Date() && t.status !== 'done';
return (
<tr key={t.id}>
<td><input type="checkbox" checked={t.status === 'done'} onChange={() => onToggleDone(t.id)} /></td>
<td onClick={() => onTaskClick(t)} style={{ cursor: 'pointer' }}>{t.title}</td>
<td><div style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Avatar userId={t.assignee} size={20} />{u?.name}</div></td>
<td><PriorityBadge level={t.priority} /></td>
<td><StatusBadge status={t.status} /></td>
<td style={{ color: overdue ? '#ef4444' : undefined }}>{due.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</td>
<td>
{t.tags.slice(0, 2).map(tag => <span key={tag} className="tag-pill" style={{ marginRight: 4 }}>{tag}</span>)}
{t.tags.length > 2 && <span className="tag-pill">+{t.tags.length - 2}</span>}
</td>
<td style={{ position: 'relative' }}>
<button className="list-actions-btn" onClick={e => { e.stopPropagation(); setMenuOpen(menuOpen === t.id ? null : t.id); }}></button>
{menuOpen === t.id && (
<div className="list-dropdown">
<button className="list-dropdown-item" onClick={() => { onTaskClick(t); setMenuOpen(null); }}>Edit</button>
<button className="list-dropdown-item" onClick={() => setMenuOpen(null)}>Delete</button>
<button className="list-dropdown-item" onClick={() => setMenuOpen(null)}>Copy Link</button>
</div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}

44
src/Login.tsx Normal file
View File

@@ -0,0 +1,44 @@
import { useState } from 'react';
import { USERS } from './data';
import type { User } from './data';
export function LoginPage({ onLogin }: { onLogin: (u: User) => void }) {
const [email, setEmail] = useState('');
const [pass, setPass] = useState('');
const [showPass, setShowPass] = useState(false);
const [error, setError] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const user = USERS.find(u => u.email === email && u.pass === pass);
if (user) { onLogin(user); }
else { setError('Invalid email or password'); }
};
return (
<div className="login-bg">
<form className="login-card" onSubmit={handleSubmit}>
<div className="login-logo">
<div className="login-logo-icon"></div>
<span className="login-title">Scrum-manager</span>
</div>
<p className="login-tagline">Your team's command center</p>
<div className="login-divider" />
<label className="login-label">Email</label>
<div className="login-input-wrap">
<input className={`login-input ${error ? 'error' : ''}`} type="email" placeholder="you@company.io"
value={email} onChange={e => { setEmail(e.target.value); setError(''); }} />
</div>
<label className="login-label">Password</label>
<div className="login-input-wrap">
<input className={`login-input ${error ? 'error' : ''}`} type={showPass ? 'text' : 'password'} placeholder="••••••••"
value={pass} onChange={e => { setPass(e.target.value); setError(''); }} />
<button type="button" className="login-eye" onClick={() => setShowPass(!showPass)}>{showPass ? '🙈' : '👁'}</button>
</div>
<button type="submit" className="login-btn">Sign In</button>
{error && <p className="login-error">{error}</p>}
<div className="login-hint">💡 Try: subodh@corp.io / cto123</div>
</form>
</div>
);
}

56
src/NavBars.tsx Normal file
View File

@@ -0,0 +1,56 @@
import { USERS } from './data';
interface TopNavbarProps {
title: string;
filterUser: string | null;
onFilterChange: (uid: string | null) => void;
searchQuery: string;
onSearch: (q: string) => void;
onNewTask: () => void;
}
export function TopNavbar({ title, filterUser, onFilterChange, searchQuery, onSearch, onNewTask }: TopNavbarProps) {
return (
<div className="top-navbar">
<span className="navbar-title">{title}</span>
<div className="navbar-search">
<span className="navbar-search-icon">🔍</span>
<input placeholder="Search tasks..." value={searchQuery} onChange={e => onSearch(e.target.value)} />
</div>
<div className="navbar-right">
<div className="filter-chips">
<span className={`filter-chip filter-chip-all ${!filterUser ? 'active' : ''}`} onClick={() => onFilterChange(null)}>All</span>
{USERS.map(u => (
<div key={u.id} className={`filter-chip ${filterUser === u.id ? 'active' : ''}`}
style={{ background: u.color, borderColor: filterUser === u.id ? u.color : 'transparent' }}
title={u.name} onClick={() => onFilterChange(u.id === filterUser ? null : u.id)}>
{u.avatar}
</div>
))}
</div>
<button className="notif-btn">🔔<span className="notif-badge">3</span></button>
<button className="new-task-btn" onClick={onNewTask}>+ New Task</button>
</div>
</div>
);
}
interface BottomToggleBarProps {
activeView: string;
onViewChange: (v: string) => void;
}
export function BottomToggleBar({ activeView, onViewChange }: BottomToggleBarProps) {
return (
<div className="bottom-bar">
<div className="toggle-pill">
{[{ id: 'calendar', icon: '📅', label: 'Calendar' }, { id: 'kanban', icon: '▦', label: 'Kanban' }, { id: 'list', icon: '☰', label: 'List' }].map(v => (
<button key={v.id} className={`toggle-btn ${activeView === v.id ? 'active' : ''}`} onClick={() => onViewChange(v.id)}>
{v.icon} {v.label}
</button>
))}
</div>
</div>
);
}

113
src/Pages.tsx Normal file
View File

@@ -0,0 +1,113 @@
import React, { useState } from 'react';
import type { Task, User } from './data';
import { USERS, PRIORITY_COLORS } from './data';
import { Avatar, StatusBadge } from './Shared';
export function TeamTasksPage({ tasks }: { tasks: Task[]; currentUser: User }) {
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const members = USERS;
return (
<div className="team-tasks">
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16 }}>Team Tasks</h2>
{members.map(m => {
const mTasks = tasks.filter(t => t.assignee === m.id);
const isOpen = expanded[m.id] !== false;
return (
<div key={m.id} className="team-group">
<div className="team-group-header" onClick={() => setExpanded(e => ({ ...e, [m.id]: !isOpen }))}>
<Avatar userId={m.id} size={28} />
<span className="team-group-name">{m.name}</span>
<span className="team-group-count">({mTasks.length} tasks)</span>
<span style={{ color: '#64748b' }}>{isOpen ? '▼' : '▶'}</span>
</div>
{isOpen && (
<div className="team-group-tasks">
{mTasks.map(t => (
<div key={t.id} className="team-task-row">
<span style={{ width: 8, height: 8, borderRadius: '50%', background: PRIORITY_COLORS[t.priority].color }} />
<span className="team-task-title">{t.title}</span>
<StatusBadge status={t.status} />
<span style={{ fontSize: 11, color: '#64748b' }}>
{new Date(t.dueDate + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
{t.subtasks.length > 0 && <span style={{ fontSize: 10, color: '#64748b' }}>📋 {t.subtasks.filter(s => s.done).length}/{t.subtasks.length}</span>}
</div>
))}
</div>
)}
</div>
);
})}
</div>
);
}
export function MembersPage({ tasks }: { tasks: Task[] }) {
const [expanded, setExpanded] = useState<string | null>(null);
const [showInvite, setShowInvite] = useState(false);
return (
<div className="members-page">
<div className="members-header">
<h2>Team Members</h2>
<button className="btn-ghost" onClick={() => setShowInvite(true)}>+ Invite Member</button>
</div>
<table className="members-table">
<thead><tr><th>Avatar</th><th>Full Name</th><th>Role</th><th>Dept</th><th>Assigned</th><th>Done</th><th>Active</th></tr></thead>
<tbody>
{USERS.map(u => {
const ut = tasks.filter(t => t.assignee === u.id);
const done = ut.filter(t => t.status === 'done').length;
const active = ut.filter(t => t.status !== 'done').length;
const roleColors: Record<string, string> = { cto: '#818cf8', manager: '#fb923c', employee: '#22c55e' };
return (
<React.Fragment key={u.id}>
<tr onClick={() => setExpanded(expanded === u.id ? null : u.id)}>
<td><Avatar userId={u.id} size={28} /></td>
<td>{u.name}</td>
<td><span style={{ background: `${roleColors[u.role]}22`, color: roleColors[u.role], padding: '2px 8px', borderRadius: 10, fontSize: 10, fontWeight: 600 }}>{u.role.toUpperCase()}</span></td>
<td>{u.dept}</td>
<td>{ut.length}</td>
<td>{done}</td>
<td>{active}</td>
</tr>
{expanded === u.id && (
<tr><td colSpan={7}>
<div className="member-expand">
{ut.map(t => (
<div key={t.id} className="team-task-row">
<span style={{ width: 8, height: 8, borderRadius: '50%', background: PRIORITY_COLORS[t.priority].color }} />
<span className="team-task-title">{t.title}</span>
<StatusBadge status={t.status} />
</div>
))}
{ut.length === 0 && <span style={{ color: '#64748b', fontSize: 12 }}>No tasks assigned</span>}
</div>
</td></tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
{showInvite && (
<div className="modal-backdrop" onClick={() => setShowInvite(false)}>
<div className="modal invite-modal" onClick={e => e.stopPropagation()}>
<div className="modal-header"><h2>Invite Member</h2><button className="drawer-close" onClick={() => setShowInvite(false)}></button></div>
<div className="modal-body">
<div className="modal-field"><label>Email</label><input className="modal-input" placeholder="member@company.io" /></div>
<div className="modal-field">
<label>Role</label>
<select className="modal-input"><option value="employee">Employee</option><option value="manager">Manager</option></select>
</div>
</div>
<div className="modal-footer"><button className="btn-ghost" onClick={() => setShowInvite(false)}>Cancel</button><button className="btn-primary" onClick={() => setShowInvite(false)}>Send Invite</button></div>
</div>
</div>
)}
</div>
);
}

118
src/Reports.tsx Normal file
View File

@@ -0,0 +1,118 @@
import type { Task } from './data';
import { USERS, STATUS_COLORS, PRIORITY_COLORS } from './data';
import { BarChart, Bar, PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
const tooltipStyle = {
contentStyle: { background: '#0f172a', border: '1px solid #334155', borderRadius: 8, color: '#e2e8f0', fontSize: 12 },
itemStyle: { color: '#e2e8f0' },
labelStyle: { color: '#94a3b8' },
};
export function ReportsPage({ tasks }: { tasks: Task[] }) {
const total = tasks.length;
const completed = tasks.filter(t => t.status === 'done').length;
const overdue = tasks.filter(t => new Date(t.dueDate + 'T00:00:00') < new Date() && t.status !== 'done').length;
const critical = tasks.filter(t => t.priority === 'critical' && t.status !== 'done').length;
// Tasks per member (stacked by status)
const memberData = USERS.map(u => {
const ut = tasks.filter(t => t.assignee === u.id);
return {
name: u.name.split(' ')[0],
todo: ut.filter(t => t.status === 'todo').length,
inprogress: ut.filter(t => t.status === 'inprogress').length,
review: ut.filter(t => t.status === 'review').length,
done: ut.filter(t => t.status === 'done').length,
};
});
// Priority distribution
const prioData = (['critical', 'high', 'medium', 'low'] as const).map(p => ({
name: p, value: tasks.filter(t => t.priority === p).length, color: PRIORITY_COLORS[p].color,
}));
// Completions mock
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const completionData = days.map((d, i) => ({ name: d, completed: [1, 0, 2, 1, 3, 0, 1][i] }));
// Overdue by member
const overdueData = USERS.map(u => ({
name: u.name.split(' ')[0],
overdue: tasks.filter(t => t.assignee === u.id && new Date(t.dueDate + 'T00:00:00') < new Date() && t.status !== 'done').length,
})).filter(d => d.overdue > 0);
return (
<div className="reports">
<div className="stats-row">
{[
{ label: 'Total Tasks', num: total, border: '#6366f1' },
{ label: 'Completed', num: completed, border: '#22c55e' },
{ label: 'Overdue', num: overdue, border: '#ef4444' },
{ label: 'Critical Open', num: critical, border: '#f97316' },
].map(s => (
<div key={s.label} className="stat-card" style={{ borderTop: `3px solid ${s.border}` }}>
<div className="stat-card-num">{s.num}</div>
<div className="stat-card-label">{s.label}</div>
</div>
))}
</div>
<div className="charts-grid">
<div className="chart-card">
<div className="chart-card-title">Tasks per Member</div>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={memberData}>
<XAxis dataKey="name" tick={{ fill: '#64748b', fontSize: 11 }} />
<YAxis tick={{ fill: '#64748b', fontSize: 11 }} />
<Tooltip {...tooltipStyle} />
<Legend wrapperStyle={{ fontSize: 11, color: '#94a3b8' }} />
<Bar dataKey="todo" stackId="a" fill={STATUS_COLORS.todo} name="To Do" />
<Bar dataKey="inprogress" stackId="a" fill={STATUS_COLORS.inprogress} name="In Progress" />
<Bar dataKey="review" stackId="a" fill={STATUS_COLORS.review} name="Review" />
<Bar dataKey="done" stackId="a" fill={STATUS_COLORS.done} name="Done" />
</BarChart>
</ResponsiveContainer>
</div>
<div className="chart-card">
<div className="chart-card-title">Priority Distribution</div>
<ResponsiveContainer width="100%" height={250}>
<PieChart>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<Pie data={prioData} cx="50%" cy="50%" innerRadius={55} outerRadius={85} paddingAngle={3} dataKey="value" label={((entry: any) => `${entry.name} ${((entry.percent ?? 0) * 100).toFixed(0)}%`) as any}>
{prioData.map(d => <Cell key={d.name} fill={d.color} />)}
</Pie>
<Tooltip {...tooltipStyle} />
</PieChart>
</ResponsiveContainer>
<div style={{ textAlign: 'center', fontSize: 22, fontWeight: 800, marginTop: -140, position: 'relative', pointerEvents: 'none', color: '#f1f5f9' }}>{total}</div>
<div style={{ height: 100 }} />
</div>
<div className="chart-card">
<div className="chart-card-title">Completions This Week</div>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={completionData}>
<XAxis dataKey="name" tick={{ fill: '#64748b', fontSize: 11 }} />
<YAxis tick={{ fill: '#64748b', fontSize: 11 }} />
<Tooltip {...tooltipStyle} />
<Line type="monotone" dataKey="completed" stroke="#6366f1" strokeWidth={2} dot={{ fill: '#22c55e', r: 4 }} />
</LineChart>
</ResponsiveContainer>
</div>
<div className="chart-card">
<div className="chart-card-title">Overdue by Member</div>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={overdueData} layout="vertical">
<XAxis type="number" tick={{ fill: '#64748b', fontSize: 11 }} />
<YAxis dataKey="name" type="category" tick={{ fill: '#64748b', fontSize: 11 }} width={60} />
<Tooltip {...tooltipStyle} />
<Bar dataKey="overdue" fill="#ef4444" radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
);
}

56
src/Shared.tsx Normal file
View File

@@ -0,0 +1,56 @@
import { PRIORITY_COLORS, STATUS_COLORS, STATUS_LABELS, getUserById } from './data';
import type { Priority, Status, Subtask } from './data';
export function Avatar({ userId, size = 28 }: { userId: string; size?: number }) {
const u = getUserById(userId);
if (!u) return null;
return (
<div className="avatar" style={{ width: size, height: size, fontSize: size * 0.36, background: u.color }}>
{u.avatar}
</div>
);
}
export function PriorityBadge({ level }: { level: Priority }) {
const p = PRIORITY_COLORS[level];
return (
<span className="priority-badge" style={{ background: p.bg, color: p.color }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: p.color, display: 'inline-block' }} />
{level}
</span>
);
}
export function StatusBadge({ status }: { status: Status }) {
const color = STATUS_COLORS[status];
return (
<span className="status-badge" style={{ background: `${color}22`, color }}>
{STATUS_LABELS[status]}
</span>
);
}
export function Tag({ label }: { label: string }) {
return <span className="tag-pill">🏷 {label}</span>;
}
export function ProgressBar({ subtasks }: { subtasks: Subtask[] }) {
if (!subtasks.length) return null;
const done = subtasks.filter(s => s.done).length;
const pct = Math.round((done / subtasks.length) * 100);
const color = pct === 100 ? '#22c55e' : '#6366f1';
return (
<div className="progress-bar-wrap">
<div className="progress-bar-text">{done}/{subtasks.length}</div>
<div className="progress-bar">
<div className="progress-bar-fill" style={{ width: `${pct}%`, background: color }} />
</div>
</div>
);
}
export function RoleBadge({ role }: { role: string }) {
const colors: Record<string, string> = { cto: '#818cf8', manager: '#fb923c', employee: '#22c55e' };
const c = colors[role] || '#64748b';
return <span className="role-badge" style={{ background: `${c}22`, color: c }}>{role.toUpperCase()}</span>;
}

52
src/Sidebar.tsx Normal file
View File

@@ -0,0 +1,52 @@
import type { User } from './data';
import { Avatar } from './Shared';
import { RoleBadge } from './Shared';
const NAV_ITEMS = [
{ id: 'dashboard', icon: '⊞', label: 'Dashboard', roles: ['cto', 'manager', 'employee'] },
{ id: 'calendar', icon: '📅', label: 'Calendar', roles: ['cto', 'manager', 'employee'] },
{ id: 'kanban', icon: '▦', label: 'Kanban Board', roles: ['cto', 'manager', 'employee'] },
{ id: 'mytasks', icon: '✓', label: 'My Tasks', roles: ['employee'] },
{ id: 'teamtasks', icon: '👥', label: 'Team Tasks', roles: ['cto', 'manager'] },
{ id: 'reports', icon: '📊', label: 'Reports', roles: ['cto', 'manager'] },
{ id: 'members', icon: '👤', label: 'Members', roles: ['cto'] },
];
interface SidebarProps {
currentUser: User;
activePage: string;
onNavigate: (page: string) => void;
onSignOut: () => void;
}
export function Sidebar({ currentUser, activePage, onNavigate, onSignOut }: SidebarProps) {
const filteredNav = NAV_ITEMS.filter(n => n.roles.includes(currentUser.role));
return (
<div className="sidebar">
<div className="sidebar-logo">
<div className="sidebar-logo-icon"></div>
<span className="sidebar-logo-text">Scrum-manager</span>
</div>
<div className="sidebar-divider" />
<div className="sidebar-section-label">Navigate</div>
<nav className="sidebar-nav">
{filteredNav.map(n => (
<div key={n.id} className={`sidebar-item ${activePage === n.id ? 'active' : ''}`} onClick={() => onNavigate(n.id)}>
<span className="sidebar-item-icon">{n.icon}</span>
{n.label}
</div>
))}
</nav>
<div className="sidebar-profile">
<Avatar userId={currentUser.id} size={36} />
<div className="sidebar-profile-info">
<div className="sidebar-profile-name">{currentUser.name}</div>
<RoleBadge role={currentUser.role} />
</div>
</div>
<div style={{ padding: '0 16px 12px' }}>
<button className="sidebar-signout" onClick={onSignOut}>Sign Out</button>
</div>
</div>
);
}

234
src/TaskDrawer.tsx Normal file
View File

@@ -0,0 +1,234 @@
import { useState } from 'react';
import type { Task, User, Status, Priority } from './data';
import { USERS, STATUS_LABELS, getUserById } from './data';
import { Avatar, Tag, ProgressBar } from './Shared';
interface DrawerProps {
task: Task; currentUser: User; onClose: () => void;
onUpdate: (updated: Task) => void;
}
export function TaskDrawer({ task, currentUser, onClose, onUpdate }: DrawerProps) {
const [commentText, setCommentText] = useState('');
const [subtaskText, setSubtaskText] = useState('');
const updateField = (field: string, value: any) => {
const now = new Date().toISOString();
const updated = {
...task, [field]: value,
activity: [...task.activity, { id: `a${Date.now()}`, text: `🔄 ${currentUser.name} changed ${field} to ${value}`, timestamp: now }]
};
onUpdate(updated);
};
const toggleSubtask = (sid: string) => {
const now = new Date().toISOString();
const subtasks = task.subtasks.map(s => s.id === sid ? { ...s, done: !s.done } : s);
const st = subtasks.find(s => s.id === sid)!;
onUpdate({
...task, subtasks,
activity: [...task.activity, { id: `a${Date.now()}`, text: `${st.done ? '✅' : '↩️'} ${currentUser.name} ${st.done ? 'completed' : 'unchecked'} subtask "${st.title}"`, timestamp: now }]
});
};
const addSubtask = () => {
if (!subtaskText.trim()) return;
onUpdate({ ...task, subtasks: [...task.subtasks, { id: `s${Date.now()}`, title: subtaskText, done: false }] });
setSubtaskText('');
};
const addComment = () => {
if (!commentText.trim()) return;
const now = new Date().toISOString();
onUpdate({
...task,
comments: [...task.comments, { id: `c${Date.now()}`, userId: currentUser.id, text: commentText, timestamp: now }],
activity: [...task.activity, { id: `a${Date.now()}`, text: `💬 ${currentUser.name} added a comment`, timestamp: now }]
});
setCommentText('');
};
const reporter = getUserById(task.reporter);
const doneCount = task.subtasks.filter(s => s.done).length;
return (
<>
<div className="drawer-backdrop" onClick={onClose} />
<div className="drawer">
<div className="drawer-header">
<span className="drawer-header-label">Task Detail</span>
<button className="drawer-close" onClick={onClose}></button>
</div>
<div className="drawer-body">
<h2 className="drawer-title">{task.title}</h2>
<p className="drawer-desc">{task.description}</p>
<div className="drawer-meta">
<div>
<div className="drawer-meta-label">Assignee</div>
<div className="drawer-meta-val">
<Avatar userId={task.assignee} size={20} />
<select className="drawer-select" value={task.assignee} onChange={e => updateField('assignee', e.target.value)}>
{USERS.map(u => <option key={u.id} value={u.id}>{u.name}</option>)}
</select>
</div>
</div>
<div>
<div className="drawer-meta-label">Reporter</div>
<div className="drawer-meta-val"><Avatar userId={task.reporter} size={20} /> {reporter?.name}</div>
</div>
<div>
<div className="drawer-meta-label">Status</div>
<select className="drawer-select" value={task.status} onChange={e => updateField('status', e.target.value)}>
{Object.entries(STATUS_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
</div>
<div>
<div className="drawer-meta-label">Priority</div>
<select className="drawer-select" value={task.priority} onChange={e => updateField('priority', e.target.value)}>
{['critical', 'high', 'medium', 'low'].map(p => <option key={p} value={p}>{p}</option>)}
</select>
</div>
<div>
<div className="drawer-meta-label">Due Date</div>
<input type="date" className="drawer-select" value={task.dueDate} onChange={e => updateField('dueDate', e.target.value)} />
</div>
<div>
<div className="drawer-meta-label">Tags</div>
<div className="drawer-tags">{task.tags.map(t => <Tag key={t} label={t} />)}</div>
</div>
</div>
<div className="drawer-section">
<div className="drawer-section-title">Subtasks <span style={{ color: '#64748b', fontWeight: 400, fontSize: 12 }}>{doneCount} of {task.subtasks.length} complete</span></div>
{task.subtasks.length > 0 && <ProgressBar subtasks={task.subtasks} />}
{task.subtasks.map(s => (
<div key={s.id} className="subtask-row" onClick={() => toggleSubtask(s.id)}>
<input type="checkbox" className="subtask-checkbox" checked={s.done} readOnly />
<span className={`subtask-text ${s.done ? 'done' : ''}`}>{s.title}</span>
</div>
))}
<div className="subtask-add">
<input placeholder="Add a subtask..." value={subtaskText} onChange={e => setSubtaskText(e.target.value)} onKeyDown={e => e.key === 'Enter' && addSubtask()} />
<button onClick={addSubtask}>Add</button>
</div>
</div>
<div className="drawer-section">
<div className="drawer-section-title">Comments</div>
{task.comments.map(c => {
const cu = getUserById(c.userId);
return (
<div key={c.id} className="comment-item">
<Avatar userId={c.userId} size={26} />
<div className="comment-bubble">
<div className="comment-header">
<span className="comment-name">{cu?.name}</span>
<span className="comment-time">{new Date(c.timestamp).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</span>
</div>
<div className="comment-text">{c.text}</div>
</div>
</div>
);
})}
<div className="comment-input-row">
<Avatar userId={currentUser.id} size={26} />
<input placeholder="Add a comment..." value={commentText} onChange={e => setCommentText(e.target.value)} onKeyDown={e => e.key === 'Enter' && addComment()} />
<button onClick={addComment}>Post</button>
</div>
</div>
<div className="drawer-section">
<div className="drawer-section-title">Activity</div>
{task.activity.map(a => (
<div key={a.id} className="activity-item">
<span className="activity-text">{a.text} · {new Date(a.timestamp).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</span>
</div>
))}
</div>
</div>
</div>
</>
);
}
interface ModalProps {
onClose: () => void;
onAdd: (task: Task) => void;
defaultDate?: string;
defaultStatus?: Status;
}
export function AddTaskModal({ onClose, onAdd, defaultDate, defaultStatus }: ModalProps) {
const [title, setTitle] = useState('');
const [desc, setDesc] = useState('');
const [assignee, setAssignee] = useState('u1');
const [priority, setPriority] = useState<Priority>('medium');
const [status, setStatus] = useState<Status>(defaultStatus || 'todo');
const [dueDate, setDueDate] = useState(defaultDate || new Date().toISOString().split('T')[0]);
const [tags, setTags] = useState('');
const [error, setError] = useState(false);
const submit = () => {
if (!title.trim()) { setError(true); return; }
const task: Task = {
id: `t${Date.now()}`, title, description: desc, status, priority,
assignee, reporter: 'u1', dueDate,
tags: tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [],
subtasks: [], comments: [],
activity: [{ id: `a${Date.now()}`, text: `📝 Task created`, timestamp: new Date().toISOString() }],
};
onAdd(task);
onClose();
};
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal" onClick={e => e.stopPropagation()}>
<div className="modal-header"><h2>New Task</h2><button className="drawer-close" onClick={onClose}></button></div>
<div className="modal-body">
<div className="modal-field">
<label>Title *</label>
<input className={`modal-input ${error && !title.trim() ? 'error' : ''}`} placeholder="Task title" value={title} onChange={e => { setTitle(e.target.value); setError(false); }} />
</div>
<div className="modal-field">
<label>Description</label>
<textarea className="modal-input modal-input-textarea" placeholder="Describe the task..." rows={3} value={desc} onChange={e => setDesc(e.target.value)} />
</div>
<div className="modal-grid">
<div className="modal-field">
<label>Assignee</label>
<select className="modal-input" value={assignee} onChange={e => setAssignee(e.target.value)}>
{USERS.map(u => <option key={u.id} value={u.id}>{u.avatar} {u.name}</option>)}
</select>
</div>
<div className="modal-field">
<label>Priority</label>
<select className="modal-input" value={priority} onChange={e => setPriority(e.target.value as Priority)}>
{['critical', 'high', 'medium', 'low'].map(p => <option key={p} value={p}>{p}</option>)}
</select>
</div>
<div className="modal-field">
<label>Status</label>
<select className="modal-input" value={status} onChange={e => setStatus(e.target.value as Status)}>
{Object.entries(STATUS_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
</div>
<div className="modal-field">
<label>Due Date</label>
<input className="modal-input" type="date" value={dueDate} onChange={e => setDueDate(e.target.value)} />
</div>
</div>
<div className="modal-field">
<label>Tags (comma separated)</label>
<input className="modal-input" placeholder="devops, backend, ..." value={tags} onChange={e => setTags(e.target.value)} />
</div>
</div>
<div className="modal-footer">
<button className="btn-ghost" onClick={onClose}>Cancel</button>
<button className="btn-primary" onClick={submit}>Create Task</button>
</div>
</div>
</div>
);
}

117
src/data.ts Normal file
View File

@@ -0,0 +1,117 @@
const now = new Date();
const d = (offset: number) => {
const dt = new Date(now);
dt.setDate(dt.getDate() + offset);
return dt.toISOString().split('T')[0];
};
export const USERS = [
{ id: 'u1', name: 'Subodh Pawar', role: 'cto', email: 'subodh@corp.io', pass: 'cto123', color: '#818cf8', avatar: 'SP', dept: 'Leadership' },
{ id: 'u2', name: 'Ankit Sharma', role: 'employee', email: 'ankit@corp.io', pass: 'emp123', color: '#f59e0b', avatar: 'AS', dept: 'DevOps' },
{ id: 'u3', name: 'Priya Nair', role: 'employee', email: 'priya@corp.io', pass: 'emp123', color: '#34d399', avatar: 'PN', dept: 'Backend' },
{ id: 'u4', name: 'Rahul Mehta', role: 'employee', email: 'rahul@corp.io', pass: 'emp123', color: '#f472b6', avatar: 'RM', dept: 'Frontend' },
{ id: 'u5', name: 'Deepa Iyer', role: 'manager', email: 'deepa@corp.io', pass: 'mgr123', color: '#fb923c', avatar: 'DI', dept: 'QA' },
];
export type User = typeof USERS[number];
export type Priority = 'critical' | 'high' | 'medium' | 'low';
export type Status = 'todo' | 'inprogress' | 'review' | 'done';
export interface Subtask { id: string; title: string; done: boolean }
export interface Comment { id: string; userId: string; text: string; timestamp: string }
export interface Activity { id: string; text: string; timestamp: string }
export interface Task {
id: string; title: string; description: string; status: Status; priority: Priority;
assignee: string; reporter: string; dueDate: string; tags: string[];
subtasks: Subtask[]; comments: Comment[]; activity: Activity[];
}
export const PRIORITY_COLORS: Record<Priority, { color: string; bg: string }> = {
critical: { color: '#ef4444', bg: 'rgba(239,68,68,0.13)' },
high: { color: '#f97316', bg: 'rgba(249,115,22,0.13)' },
medium: { color: '#eab308', bg: 'rgba(234,179,8,0.13)' },
low: { color: '#22c55e', bg: 'rgba(34,197,94,0.13)' },
};
export const STATUS_COLORS: Record<Status, string> = {
todo: '#64748b', inprogress: '#818cf8', review: '#f59e0b', done: '#22c55e',
};
export const STATUS_LABELS: Record<Status, string> = {
todo: 'To Do', inprogress: 'In Progress', review: 'Review', done: 'Done',
};
export const SEED_TASKS: Task[] = [
{
id: 't1', title: 'ArgoCD pipeline for staging', description: 'Set up ArgoCD GitOps pipeline for the staging environment with automatic sync and rollback capabilities.',
status: 'inprogress', priority: 'critical', assignee: 'u2', reporter: 'u1', dueDate: d(3), tags: ['devops', 'ci-cd'],
subtasks: [{ id: 's1', title: 'Configure ArgoCD manifest', done: true }, { id: 's2', title: 'Setup GitOps repo structure', done: false }, { id: 's3', title: 'Test rollback workflow', done: false }],
comments: [{ id: 'c1', userId: 'u1', text: 'Make sure we use Helm charts for this.', timestamp: '2026-02-14T10:22:00' }],
activity: [{ id: 'a1', text: '🔄 Subodh moved to In Progress', timestamp: '2026-02-14T10:22:00' }, { id: 'a2', text: '✅ Ankit completed subtask "Configure ArgoCD manifest"', timestamp: '2026-02-14T14:30:00' }],
},
{
id: 't2', title: 'Harbor registry cleanup script', description: 'Write a cron-based cleanup script to remove stale Docker images older than 30 days from Harbor.',
status: 'todo', priority: 'medium', assignee: 'u3', reporter: 'u5', dueDate: d(8), tags: ['backend', 'devops'],
subtasks: [{ id: 's4', title: 'Draft retention policy', done: false }, { id: 's5', title: 'Write cleanup script', done: false }],
comments: [{ id: 'c2', userId: 'u5', text: 'Coordinate with DevOps for registry credentials.', timestamp: '2026-02-13T09:00:00' }],
activity: [{ id: 'a3', text: '📝 Deepa created task', timestamp: '2026-02-13T09:00:00' }, { id: 'a4', text: '👤 Assigned to Priya', timestamp: '2026-02-13T09:01:00' }],
},
{
id: 't3', title: 'SonarQube quality gate fix', description: 'Fix failing quality gates in SonarQube for the API microservice — coverage is below threshold.',
status: 'review', priority: 'high', assignee: 'u2', reporter: 'u1', dueDate: d(1), tags: ['quality', 'testing'],
subtasks: [{ id: 's6', title: 'Identify uncovered code paths', done: true }, { id: 's7', title: 'Write missing unit tests', done: true }, { id: 's8', title: 'Verify gate passes', done: false }],
comments: [{ id: 'c3', userId: 'u1', text: 'Coverage must be above 80%.', timestamp: '2026-02-12T11:00:00' }, { id: 'c4', userId: 'u2', text: 'Currently at 76%, adding tests now.', timestamp: '2026-02-13T15:00:00' }],
activity: [{ id: 'a5', text: '🔄 Ankit moved to Review', timestamp: '2026-02-14T16:00:00' }, { id: 'a6', text: '✅ Ankit completed 2 subtasks', timestamp: '2026-02-14T15:30:00' }],
},
{
id: 't4', title: 'MinIO bucket lifecycle policy', description: 'Configure lifecycle policies for MinIO buckets to auto-expire temporary uploads after 7 days.',
status: 'done', priority: 'low', assignee: 'u5', reporter: 'u1', dueDate: d(-2), tags: ['infrastructure'],
subtasks: [{ id: 's9', title: 'Define lifecycle rules', done: true }, { id: 's10', title: 'Apply and test policy', done: true }],
comments: [{ id: 'c5', userId: 'u5', text: 'Done and verified on staging.', timestamp: '2026-02-13T17:00:00' }],
activity: [{ id: 'a7', text: '✅ Deepa moved to Done', timestamp: '2026-02-13T17:00:00' }, { id: 'a8', text: '💬 Deepa added a comment', timestamp: '2026-02-13T17:01:00' }],
},
{
id: 't5', title: 'Jenkins shared library refactor', description: 'Refactor Jenkins shared libraries to use declarative pipeline syntax and reduce duplication.',
status: 'inprogress', priority: 'high', assignee: 'u2', reporter: 'u5', dueDate: d(6), tags: ['devops', 'refactor'],
subtasks: [{ id: 's11', title: 'Audit existing shared libs', done: true }, { id: 's12', title: 'Migrate to declarative syntax', done: false }, { id: 's13', title: 'Update pipeline docs', done: false }],
comments: [{ id: 'c6', userId: 'u2', text: 'Found 12 redundant pipeline stages.', timestamp: '2026-02-14T10:00:00' }],
activity: [{ id: 'a9', text: '🔄 Ankit started working', timestamp: '2026-02-13T11:00:00' }, { id: 'a10', text: '✅ Completed audit subtask', timestamp: '2026-02-14T10:00:00' }],
},
{
id: 't6', title: 'Grafana k8s dashboard', description: 'Create comprehensive Grafana dashboards for Kubernetes cluster monitoring including pod health and resource usage.',
status: 'todo', priority: 'medium', assignee: 'u4', reporter: 'u1', dueDate: d(12), tags: ['monitoring', 'frontend'],
subtasks: [{ id: 's14', title: 'Design dashboard layout', done: false }, { id: 's15', title: 'Configure Prometheus data sources', done: false }],
comments: [{ id: 'c7', userId: 'u1', text: 'Use the standard k8s mixin as a starting point.', timestamp: '2026-02-12T14:00:00' }],
activity: [{ id: 'a11', text: '📝 Subodh created task', timestamp: '2026-02-12T14:00:00' }, { id: 'a12', text: '👤 Assigned to Rahul', timestamp: '2026-02-12T14:01:00' }],
},
{
id: 't7', title: 'React component audit', description: 'Audit all React components for accessibility compliance and performance optimizations.',
status: 'inprogress', priority: 'medium', assignee: 'u4', reporter: 'u5', dueDate: d(5), tags: ['frontend', 'a11y'],
subtasks: [{ id: 's16', title: 'Run Lighthouse audit', done: true }, { id: 's17', title: 'Fix critical a11y issues', done: false }, { id: 's18', title: 'Document findings', done: false }],
comments: [{ id: 'c8', userId: 'u4', text: 'Initial Lighthouse score is 72.', timestamp: '2026-02-14T08:00:00' }],
activity: [{ id: 'a13', text: '🔄 Rahul moved to In Progress', timestamp: '2026-02-13T09:00:00' }, { id: 'a14', text: '✅ Completed Lighthouse audit', timestamp: '2026-02-14T08:00:00' }],
},
{
id: 't8', title: 'PostgreSQL backup strategy', description: 'Implement automated daily backups for PostgreSQL with point-in-time recovery and off-site storage.',
status: 'todo', priority: 'critical', assignee: 'u3', reporter: 'u1', dueDate: d(2), tags: ['database', 'infrastructure'],
subtasks: [{ id: 's19', title: 'Setup pg_basebackup cron', done: false }, { id: 's20', title: 'Configure WAL archiving', done: false }, { id: 's21', title: 'Test restore procedure', done: false }],
comments: [{ id: 'c9', userId: 'u1', text: 'This is critical — we need backups before the release.', timestamp: '2026-02-14T09:00:00' }],
activity: [{ id: 'a15', text: '📝 Subodh created task', timestamp: '2026-02-14T09:00:00' }, { id: 'a16', text: '⚠️ Marked as Critical priority', timestamp: '2026-02-14T09:01:00' }],
},
{
id: 't9', title: 'API rate limiting middleware', description: 'Add rate limiting middleware to the Express API with configurable thresholds per endpoint.',
status: 'review', priority: 'high', assignee: 'u3', reporter: 'u5', dueDate: d(4), tags: ['backend', 'security'],
subtasks: [{ id: 's22', title: 'Research rate limiting libraries', done: true }, { id: 's23', title: 'Implement middleware', done: true }, { id: 's24', title: 'Add integration tests', done: false }],
comments: [{ id: 'c10', userId: 'u3', text: 'Using express-rate-limit with Redis store.', timestamp: '2026-02-14T16:00:00' }, { id: 'c11', userId: 'u5', text: 'Please add tests before moving to done.', timestamp: '2026-02-15T09:00:00' }],
activity: [{ id: 'a17', text: '🔄 Priya moved to Review', timestamp: '2026-02-14T16:00:00' }, { id: 'a18', text: '💬 Deepa added a comment', timestamp: '2026-02-15T09:00:00' }],
},
{
id: 't10', title: 'Mobile responsive QA sweep', description: 'Complete QA sweep of all pages for mobile responsiveness across iOS and Android devices.',
status: 'done', priority: 'low', assignee: 'u5', reporter: 'u1', dueDate: d(-5), tags: ['qa', 'mobile'],
subtasks: [{ id: 's25', title: 'Test on iOS Safari', done: true }, { id: 's26', title: 'Test on Android Chrome', done: true }, { id: 's27', title: 'File bug reports', done: true }],
comments: [{ id: 'c12', userId: 'u5', text: 'All pages pass on both platforms. 3 minor bugs filed.', timestamp: '2026-02-10T15:00:00' }],
activity: [{ id: 'a19', text: '✅ Deepa moved to Done', timestamp: '2026-02-10T15:00:00' }, { id: 'a20', text: '🐛 3 bugs filed in tracker', timestamp: '2026-02-10T15:30:00' }],
},
];
export function getUserById(id: string) { return USERS.find(u => u.id === id); }

317
src/index.css Normal file
View File

@@ -0,0 +1,317 @@
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&display=swap');
:root {
--bg-base: #060b18;
--bg-surface: #0a0f1e;
--bg-card: #0f172a;
--border: #1e293b;
--border-muted: #334155;
--text-primary: #f1f5f9;
--text-muted: #64748b;
--text-secondary: #94a3b8;
--accent: #6366f1;
--accent-hover: #4f46e5;
--accent-glow: rgba(99,102,241,0.3);
--accent-bg: rgba(99,102,241,0.15);
--critical: #ef4444; --critical-bg: rgba(239,68,68,0.13);
--high: #f97316; --high-bg: rgba(249,115,22,0.13);
--medium: #eab308; --medium-bg: rgba(234,179,8,0.13);
--low: #22c55e; --low-bg: rgba(34,197,94,0.13);
--status-todo: #64748b;
--status-inprogress: #818cf8;
--status-review: #f59e0b;
--status-done: #22c55e;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'DM Sans', sans-serif;
background: var(--bg-base);
color: var(--text-primary);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg-surface); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
@keyframes slideIn { from { transform: translateX(40px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes fadeUp { from { transform: translateY(24px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
/* LOGIN */
.login-bg { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: radial-gradient(ellipse at center, #0d1a3a 0%, #060b18 70%); }
.login-card { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 16px; padding: 40px; width: 400px; animation: fadeUp 0.4s ease; }
.login-logo { display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 8px; }
.login-logo-icon { width: 44px; height: 44px; background: var(--accent); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 22px; }
.login-title { font-size: 22px; font-weight: 800; }
.login-tagline { text-align: center; color: var(--text-muted); font-size: 13px; margin-bottom: 20px; }
.login-divider { height: 1px; background: var(--border); margin: 16px 0; }
.login-label { font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; display: block; }
.login-input-wrap { position: relative; margin-bottom: 16px; }
.login-input { width: 100%; padding: 10px 14px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; color: var(--text-primary); font-size: 14px; font-family: inherit; outline: none; transition: border-color 0.15s; }
.login-input:focus { border-color: var(--accent); }
.login-input.error { border-color: var(--critical); }
.login-eye { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 16px; }
.login-btn { width: 100%; padding: 12px; background: var(--accent); color: #fff; border: none; border-radius: 10px; font-size: 14px; font-weight: 700; font-family: inherit; cursor: pointer; box-shadow: 0 0 20px var(--accent-glow); transition: background 0.15s; }
.login-btn:hover { background: var(--accent-hover); }
.login-hint { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; font-size: 12px; color: var(--text-muted); margin-top: 16px; text-align: center; }
.login-error { color: var(--critical); font-size: 12px; margin-top: 8px; text-align: center; }
/* APP SHELL */
.app-shell { display: flex; flex-direction: column; height: 100vh; background: var(--bg-base); }
.app-body { display: flex; flex: 1; overflow: hidden; }
.main-content { flex: 1; overflow-y: auto; padding: 0; }
/* SIDEBAR */
.sidebar { width: 240px; min-width: 240px; background: var(--bg-surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; height: 100%; }
.sidebar-logo { padding: 16px 20px; display: flex; align-items: center; gap: 10px; }
.sidebar-logo-icon { width: 32px; height: 32px; background: var(--accent); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; }
.sidebar-logo-text { font-size: 16px; font-weight: 800; }
.sidebar-divider { height: 1px; background: var(--border); margin: 0 16px; }
.sidebar-section-label { font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.1em; padding: 16px 20px 8px; }
.sidebar-nav { flex: 1; display: flex; flex-direction: column; }
.sidebar-item { display: flex; align-items: center; gap: 10px; padding: 10px 16px; margin: 2px 8px; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 600; color: var(--text-muted); transition: all 0.15s; border-left: 3px solid transparent; text-decoration: none; }
.sidebar-item:hover { color: #e2e8f0; background: rgba(255,255,255,0.04); }
.sidebar-item.active { background: var(--accent-bg); color: #818cf8; border-left-color: var(--accent); }
.sidebar-item-icon { font-size: 18px; width: 20px; text-align: center; }
.sidebar-profile { padding: 16px; border-top: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
.sidebar-profile-info { flex: 1; }
.sidebar-profile-name { font-size: 13px; font-weight: 600; color: #e2e8f0; }
.sidebar-signout { background: none; border: none; color: var(--text-muted); font-size: 11px; cursor: pointer; font-family: inherit; padding: 0; transition: color 0.15s; }
.sidebar-signout:hover { color: var(--critical); }
/* NAVBAR */
.top-navbar { height: 56px; min-height: 56px; background: var(--bg-surface); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 20px; gap: 16px; position: sticky; top: 0; z-index: 50; }
.navbar-title { font-size: 16px; font-weight: 700; white-space: nowrap; }
.navbar-search { flex: 0 0 220px; margin: 0 auto; position: relative; }
.navbar-search input { width: 100%; padding: 7px 12px 7px 32px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; color: var(--text-primary); font-size: 12px; font-family: inherit; outline: none; }
.navbar-search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-muted); font-size: 13px; }
.navbar-right { display: flex; align-items: center; gap: 12px; margin-left: auto; }
.filter-chips { display: flex; align-items: center; gap: 6px; }
.filter-chip { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; font-weight: 700; cursor: pointer; border: 2px solid transparent; transition: all 0.15s; }
.filter-chip.active { box-shadow: 0 0 0 2px var(--accent); }
.filter-chip-all { padding: 4px 12px; border-radius: 14px; font-size: 11px; font-weight: 600; cursor: pointer; background: var(--bg-card); border: 1px solid var(--border); color: var(--text-muted); transition: all 0.15s; width: auto; height: auto; }
.filter-chip-all.active { background: var(--accent-bg); color: var(--accent); border-color: var(--accent); }
.notif-btn { position: relative; background: none; border: none; color: var(--text-muted); font-size: 18px; cursor: pointer; transition: color 0.15s; }
.notif-btn:hover { color: var(--text-primary); }
.notif-badge { position: absolute; top: -4px; right: -6px; background: var(--critical); color: #fff; font-size: 9px; font-weight: 700; width: 16px; height: 16px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.new-task-btn { padding: 8px 16px; background: var(--accent); color: #fff; border: none; border-radius: 8px; font-size: 13px; font-weight: 700; font-family: inherit; cursor: pointer; box-shadow: 0 0 20px var(--accent-glow); transition: background 0.15s; white-space: nowrap; }
.new-task-btn:hover { background: var(--accent-hover); }
/* BOTTOM TOGGLE */
.bottom-bar { height: 48px; min-height: 48px; background: var(--bg-surface); border-top: 1px solid var(--border); display: flex; align-items: center; justify-content: center; }
.toggle-pill { display: flex; background: var(--bg-card); border: 1px solid var(--border); border-radius: 40px; padding: 4px; }
.toggle-btn { width: 120px; height: 40px; border: none; border-radius: 36px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; cursor: pointer; font-family: inherit; transition: all 0.15s; background: transparent; color: var(--text-muted); display: flex; align-items: center; justify-content: center; gap: 6px; }
.toggle-btn.active { background: var(--accent); color: #fff; box-shadow: 0 0 16px rgba(99,102,241,0.4); }
.toggle-btn:not(.active):hover { color: #e2e8f0; }
/* AVATAR */
.avatar { border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-weight: 700; color: #fff; flex-shrink: 0; }
/* BADGES */
.priority-badge, .status-badge { padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; display: inline-flex; align-items: center; gap: 4px; }
.role-badge { padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; }
.tag-pill { padding: 2px 8px; border-radius: 6px; font-size: 10px; background: var(--bg-card); border: 1px solid var(--border); color: var(--text-secondary); }
/* CALENDAR */
.calendar-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; }
.cal-nav { display: flex; align-items: center; gap: 12px; }
.cal-nav-btn { background: none; border: 1px solid var(--border-muted); color: var(--text-secondary); width: 32px; height: 32px; border-radius: 8px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: all 0.15s; }
.cal-nav-btn:hover { border-color: var(--accent); color: var(--accent); }
.cal-month-label { font-size: 18px; font-weight: 700; min-width: 180px; text-align: center; }
.cal-today-btn { padding: 6px 14px; background: none; border: 1px solid var(--border-muted); color: var(--text-secondary); border-radius: 8px; font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all 0.15s; }
.cal-today-btn:hover { border-color: var(--accent); color: var(--accent); }
.cal-view-toggle { display: flex; background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.cal-view-btn { padding: 6px 14px; background: none; border: none; color: var(--text-muted); font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all 0.15s; }
.cal-view-btn.active { background: var(--accent); color: #fff; }
.month-grid { display: grid; grid-template-columns: repeat(7, 1fr); flex: 1; }
.month-grid-header { font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: #475569; text-align: center; padding: 8px; border-bottom: 1px solid var(--border); }
.day-cell { background: var(--bg-surface); border: 1px solid var(--border); min-height: 120px; padding: 4px; cursor: pointer; transition: background 0.15s; position: relative; }
.day-cell:hover { background: #0d1629; }
.day-number { font-size: 13px; padding: 4px 8px; color: var(--text-secondary); }
.day-number.today { background: var(--accent); color: #fff; font-weight: 700; width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; padding: 0; }
.day-number.other-month { color: var(--border); }
.day-tasks { display: flex; flex-direction: column; gap: 3px; padding: 2px 4px; }
/* TASK CHIP */
.task-chip { height: 22px; border-radius: 4px; padding: 0 8px; display: flex; align-items: center; gap: 6px; cursor: pointer; transition: all 0.15s; animation: fadeIn 0.15s ease; border-left: 3px solid; }
.task-chip:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.4); filter: brightness(1.2); }
.task-chip-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
.task-chip-title { font-size: 11px; color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
.task-chip-avatar { width: 14px; height: 14px; font-size: 6px; flex-shrink: 0; }
.more-tasks-link { font-size: 11px; color: var(--accent); cursor: pointer; padding: 2px 8px; }
.more-tasks-link:hover { text-decoration: underline; }
/* QUICK ADD */
.quick-add-panel { position: absolute; z-index: 200; background: var(--bg-card); border: 1px solid var(--border-muted); border-radius: 12px; padding: 16px; width: 320px; box-shadow: 0 12px 40px rgba(0,0,0,0.6); animation: fadeUp 0.15s ease; }
.quick-add-header { font-size: 12px; color: var(--text-muted); margin-bottom: 10px; }
.quick-add-close { position: absolute; top: 8px; right: 8px; background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 16px; }
.quick-add-input { width: 100%; padding: 8px 10px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary); font-size: 13px; font-family: inherit; outline: none; margin-bottom: 10px; }
.quick-add-row { display: flex; gap: 8px; margin-bottom: 10px; }
.quick-add-select { flex: 1; padding: 6px 8px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary); font-size: 11px; font-family: inherit; }
.quick-add-actions { display: flex; align-items: center; gap: 10px; }
.quick-add-submit { padding: 6px 14px; background: var(--accent); color: #fff; border: none; border-radius: 6px; font-size: 12px; font-weight: 700; cursor: pointer; font-family: inherit; }
.quick-add-link { font-size: 11px; color: var(--accent); cursor: pointer; background: none; border: none; font-family: inherit; }
/* WEEK VIEW */
.week-grid { display: grid; grid-template-columns: repeat(7, 1fr); flex: 1; }
.week-header-cell { text-align: center; padding: 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: #475569; border-bottom: 1px solid var(--border); }
.week-header-cell.today { background: var(--accent); color: #fff; border-radius: 8px 8px 0 0; }
.week-day-cell { background: var(--bg-surface); border: 1px solid var(--border); min-height: 200px; padding: 8px; cursor: pointer; transition: background 0.15s; position: relative; }
.week-day-cell:hover { background: #0d1629; }
.week-chip { height: 28px; border-radius: 4px; padding: 4px 8px; display: flex; align-items: center; gap: 6px; cursor: pointer; transition: all 0.15s; border-left: 3px solid; margin-bottom: 4px; }
/* KANBAN */
.kanban-board { display: flex; gap: 16px; padding: 16px 20px; flex: 1; overflow-x: auto; }
.kanban-column { flex: 1; min-width: 260px; background: var(--bg-surface); border-radius: 12px; border: 1px solid var(--border); display: flex; flex-direction: column; }
.kanban-col-header { display: flex; align-items: center; gap: 8px; padding: 14px 16px; border-bottom: 1px solid var(--border); }
.kanban-col-dot { width: 8px; height: 8px; border-radius: 50%; }
.kanban-col-label { font-size: 12px; font-weight: 700; text-transform: uppercase; flex: 1; }
.kanban-col-count { font-size: 11px; color: var(--text-muted); background: var(--bg-card); padding: 1px 8px; border-radius: 10px; }
.kanban-col-add { background: none; border: 1px solid var(--border); color: var(--text-muted); width: 24px; height: 24px; border-radius: 6px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: all 0.15s; }
.kanban-col-add:hover { border-color: var(--accent); color: var(--accent); }
.kanban-col-body { padding: 10px; flex: 1; overflow-y: auto; }
.kanban-empty { border: 1px dashed var(--border-muted); border-radius: 8px; padding: 20px; text-align: center; color: var(--text-muted); font-size: 12px; }
/* TASK CARD */
.task-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; cursor: pointer; transition: all 0.15s; border-left: 3px solid; }
.task-card:hover { background: var(--border); transform: translateY(-1px); box-shadow: 0 6px 24px rgba(0,0,0,0.4); }
.task-card-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.task-card-title { font-size: 13px; font-weight: 600; color: #e2e8f0; flex: 1; }
.task-card-badges { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
.task-card-meta { display: flex; align-items: center; gap: 10px; font-size: 11px; color: var(--text-muted); }
.task-card-overdue { color: var(--critical); }
.progress-bar-wrap { margin-bottom: 4px; }
.progress-bar-text { font-size: 10px; color: var(--text-muted); margin-bottom: 2px; }
.progress-bar { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
.progress-bar-fill { height: 100%; border-radius: 2px; transition: width 0.3s; }
/* LIST VIEW */
.list-view { padding: 16px 20px; }
.list-sort-row { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; font-size: 12px; color: var(--text-muted); }
.list-sort-btn { padding: 4px 10px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px; color: var(--text-muted); font-size: 11px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all 0.15s; }
.list-sort-btn.active { border-color: var(--accent); color: var(--accent); }
.list-table { width: 100%; border-collapse: collapse; }
.list-table th { background: var(--bg-card); border-bottom: 1px solid var(--border); padding: 10px 12px; font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--text-muted); text-align: left; position: sticky; top: 0; }
.list-table td { padding: 10px 12px; font-size: 13px; border-bottom: 1px solid var(--border); }
.list-table tr:nth-child(odd) td { background: var(--bg-surface); }
.list-table tr:nth-child(even) td { background: #0d1120; }
.list-table tr:hover td { background: var(--border) !important; cursor: pointer; }
.list-actions-btn { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 16px; position: relative; }
.list-dropdown { position: absolute; right: 0; top: 100%; background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 4px; min-width: 120px; z-index: 100; box-shadow: 0 8px 24px rgba(0,0,0,0.4); }
.list-dropdown-item { display: block; width: 100%; padding: 6px 12px; background: none; border: none; color: var(--text-secondary); font-size: 12px; text-align: left; cursor: pointer; border-radius: 4px; font-family: inherit; }
.list-dropdown-item:hover { background: var(--accent-bg); color: var(--accent); }
/* DRAWER */
.drawer-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); z-index: 100; animation: fadeIn 0.15s; }
.drawer { position: fixed; right: 0; top: 0; bottom: 0; width: 520px; background: var(--bg-surface); border-left: 1px solid var(--border); z-index: 101; animation: slideIn 0.2s ease; overflow-y: auto; display: flex; flex-direction: column; }
.drawer-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--border); }
.drawer-header-label { font-size: 11px; text-transform: uppercase; color: var(--text-muted); letter-spacing: 0.05em; }
.drawer-close { background: var(--border); border: none; color: var(--text-secondary); width: 28px; height: 28px; border-radius: 6px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; }
.drawer-body { padding: 20px; flex: 1; }
.drawer-title { font-size: 18px; font-weight: 700; margin-bottom: 6px; }
.drawer-desc { font-size: 13px; color: var(--text-muted); line-height: 1.6; margin-bottom: 16px; }
.drawer-meta { background: var(--bg-card); border-radius: 10px; padding: 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; }
.drawer-meta-label { font-size: 10px; text-transform: uppercase; color: var(--text-muted); margin-bottom: 4px; }
.drawer-meta-val { font-size: 13px; display: flex; align-items: center; gap: 6px; }
.drawer-select { padding: 4px 8px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary); font-size: 12px; font-family: inherit; }
.drawer-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; }
.drawer-section { margin-bottom: 20px; }
.drawer-section-title { font-size: 13px; font-weight: 700; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.subtask-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; cursor: pointer; transition: background 0.15s; border-radius: 4px; }
.subtask-row:hover { background: rgba(255,255,255,0.03); }
.subtask-checkbox { width: 16px; height: 16px; border: 1.5px solid var(--border-muted); border-radius: 4px; cursor: pointer; accent-color: var(--status-done); }
.subtask-text { font-size: 13px; }
.subtask-text.done { text-decoration: line-through; color: var(--text-muted); }
.subtask-add { display: flex; gap: 8px; margin-top: 8px; }
.subtask-add input { flex: 1; padding: 6px 10px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary); font-size: 12px; font-family: inherit; outline: none; }
.subtask-add button { padding: 6px 12px; background: var(--accent); color: #fff; border: none; border-radius: 6px; font-size: 11px; font-weight: 700; cursor: pointer; font-family: inherit; }
.comment-item { display: flex; gap: 10px; margin-bottom: 14px; }
.comment-bubble { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; flex: 1; }
.comment-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.comment-name { font-size: 12px; font-weight: 700; }
.comment-time { font-size: 10px; color: var(--text-muted); }
.comment-text { font-size: 12px; color: var(--text-secondary); }
.comment-input-row { display: flex; gap: 8px; align-items: center; }
.comment-input-row input { flex: 1; padding: 8px 10px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary); font-size: 12px; font-family: inherit; outline: none; }
.comment-input-row button { padding: 8px 14px; background: var(--accent); color: #fff; border: none; border-radius: 6px; font-size: 12px; font-weight: 700; cursor: pointer; font-family: inherit; }
.activity-item { display: flex; gap: 10px; position: relative; padding-left: 16px; margin-bottom: 10px; }
.activity-item::before { content: ''; position: absolute; left: 3px; top: 8px; width: 6px; height: 6px; border-radius: 50%; background: var(--border-muted); }
.activity-item::after { content: ''; position: absolute; left: 5px; top: 16px; width: 2px; height: calc(100% + 4px); background: var(--border); }
.activity-item:last-child::after { display: none; }
.activity-text { font-size: 11px; color: #475569; }
/* MODAL */
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); z-index: 200; display: flex; align-items: center; justify-content: center; animation: fadeIn 0.15s; }
.modal { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 16px; width: 480px; max-height: 90vh; overflow-y: auto; animation: fadeUp 0.2s ease; }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px; border-bottom: 1px solid var(--border); }
.modal-header h2 { font-size: 16px; font-weight: 700; }
.modal-body { padding: 24px; }
.modal-field { margin-bottom: 14px; }
.modal-field label { font-size: 12px; font-weight: 600; color: var(--text-secondary); display: block; margin-bottom: 4px; }
.modal-input { width: 100%; padding: 8px 12px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; color: var(--text-primary); font-size: 13px; font-family: inherit; outline: none; }
.modal-input:focus { border-color: var(--accent); }
.modal-input.error { border-color: var(--critical); }
.modal-input-textarea { resize: vertical; min-height: 60px; }
.modal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.modal-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 16px 24px; border-top: 1px solid var(--border); }
.btn-ghost { padding: 8px 18px; background: none; border: 1px solid var(--border); border-radius: 8px; color: var(--text-secondary); font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all 0.15s; }
.btn-ghost:hover { border-color: var(--text-muted); }
.btn-primary { padding: 8px 18px; background: var(--accent); border: none; border-radius: 8px; color: #fff; font-size: 13px; font-weight: 700; cursor: pointer; font-family: inherit; box-shadow: 0 0 16px var(--accent-glow); transition: background 0.15s; }
.btn-primary:hover { background: var(--accent-hover); }
/* DASHBOARD */
.dashboard { padding: 20px; }
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
.stat-card { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px; padding: 20px; }
.stat-card-num { font-size: 32px; font-weight: 800; }
.stat-card-label { font-size: 11px; text-transform: uppercase; color: var(--text-muted); margin-top: 4px; }
.workload-card { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; }
.workload-card-title { font-size: 14px; font-weight: 700; margin-bottom: 16px; }
.workload-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--border); }
.workload-row:last-child { border-bottom: none; }
.workload-name { font-size: 13px; font-weight: 600; min-width: 120px; }
.workload-dept { font-size: 11px; color: var(--text-muted); min-width: 80px; }
.workload-bar { flex: 1; }
.workload-badges { display: flex; gap: 4px; }
.priority-bar-wrap { margin-bottom: 8px; }
.priority-bar { height: 24px; border-radius: 6px; display: flex; overflow: hidden; }
.priority-segment { display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; color: #fff; }
.deadline-item { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); }
.deadline-item.overdue { background: rgba(239,68,68,0.08); border-radius: 6px; padding: 8px; }
.progress-ring { margin: 20px auto; display: block; }
/* TEAM TASKS */
.team-tasks { padding: 20px; }
.team-group { margin-bottom: 16px; }
.team-group-header { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px; cursor: pointer; transition: background 0.15s; }
.team-group-header:hover { background: var(--bg-card); }
.team-group-name { font-size: 14px; font-weight: 600; flex: 1; }
.team-group-count { font-size: 12px; color: var(--text-muted); }
.team-group-tasks { padding: 8px 0 8px 42px; }
.team-task-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
.team-task-title { font-size: 13px; flex: 1; }
/* REPORTS */
.reports { padding: 20px; }
.charts-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.chart-card { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px; padding: 20px; }
.chart-card-title { font-size: 14px; font-weight: 700; margin-bottom: 16px; }
/* MEMBERS */
.members-page { padding: 20px; }
.members-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.members-header h2 { font-size: 18px; font-weight: 700; }
.members-table { width: 100%; border-collapse: collapse; }
.members-table th { background: var(--bg-card); border-bottom: 1px solid var(--border); padding: 10px 12px; font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--text-muted); text-align: left; }
.members-table td { padding: 10px 12px; border-bottom: 1px solid var(--border); font-size: 13px; }
.members-table tr:hover td { background: var(--bg-card); cursor: pointer; }
.member-expand { padding: 12px 16px; background: var(--bg-card); }
.invite-modal { width: 380px; }
/* MORE TASKS POPOVER */
.more-popover { position: absolute; z-index: 150; background: var(--bg-card); border: 1px solid var(--border-muted); border-radius: 10px; padding: 10px; width: 260px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); animation: fadeUp 0.15s ease; }
.more-popover-title { font-size: 11px; color: var(--text-muted); margin-bottom: 8px; font-weight: 600; }

14
src/main.tsx Normal file
View File

@@ -0,0 +1,14 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { ErrorBoundary } from './ErrorBoundary'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</StrictMode>,
)