2 Commits

Author SHA1 Message Date
tusuii
892a2ceba1 feat: MySQL integration, Docker setup, drag-and-drop kanban 2026-02-16 10:20:27 +05:30
tusuii
5d8af1f173 feat: add mobile responsive support (768px breakpoint)
- Add CSS media queries for all sections: sidebar overlay, navbar,
  calendar, kanban, dashboard, list view, drawer, modal, reports,
  team tasks, members
- Add hamburger menu button (hidden on desktop, visible on mobile)
- Add sidebar slide-in overlay with backdrop for mobile
- Auto-close sidebar on navigation for mobile UX
- Login card, drawer, and modal go full-width on mobile
- Dashboard stats grid collapses to 2-column on mobile
- Report charts stack to single column on mobile
2026-02-15 13:10:39 +05:30
24 changed files with 3204 additions and 463 deletions

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

52
docker-compose.yml Normal file
View File

@@ -0,0 +1,52 @@
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: scrum-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: scrumpass
MYSQL_DATABASE: scrum_manager
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pscrumpass"]
interval: 5s
timeout: 5s
retries: 10
backend:
build:
context: ./server
dockerfile: Dockerfile
container_name: scrum-backend
restart: unless-stopped
ports:
- "3001:3001"
environment:
DB_HOST: mysql
DB_PORT: 3306
DB_USER: root
DB_PASSWORD: scrumpass
DB_NAME: scrum_manager
PORT: 3001
depends_on:
mysql:
condition: service_healthy
frontend:
build:
context: .
dockerfile: Dockerfile
container_name: scrum-frontend
restart: unless-stopped
ports:
- "5173:5173"
depends_on:
- backend
volumes:
mysql_data:

12
server/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
EXPOSE 3001
CMD ["node", "index.js"]

94
server/db.js Normal file
View File

@@ -0,0 +1,94 @@
import mysql from 'mysql2/promise';
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306'),
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || 'scrumpass',
database: process.env.DB_NAME || 'scrum_manager',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
});
export async function initDB() {
const conn = await pool.getConnection();
try {
await conn.query(`
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'employee',
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
color VARCHAR(20) DEFAULT '#818cf8',
avatar VARCHAR(10) DEFAULT '',
dept VARCHAR(100) DEFAULT ''
)
`);
await conn.query(`
CREATE TABLE IF NOT EXISTS tasks (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(500) NOT NULL,
description TEXT,
status ENUM('todo','inprogress','review','done') NOT NULL DEFAULT 'todo',
priority ENUM('critical','high','medium','low') NOT NULL DEFAULT 'medium',
assignee_id VARCHAR(36),
reporter_id VARCHAR(36),
due_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (reporter_id) REFERENCES users(id) ON DELETE SET NULL
)
`);
await conn.query(`
CREATE TABLE IF NOT EXISTS subtasks (
id VARCHAR(36) PRIMARY KEY,
task_id VARCHAR(36) NOT NULL,
title VARCHAR(500) NOT NULL,
done BOOLEAN DEFAULT FALSE,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
)
`);
await conn.query(`
CREATE TABLE IF NOT EXISTS comments (
id VARCHAR(36) PRIMARY KEY,
task_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36),
text TEXT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
)
`);
await conn.query(`
CREATE TABLE IF NOT EXISTS activities (
id VARCHAR(36) PRIMARY KEY,
task_id VARCHAR(36) NOT NULL,
text TEXT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
)
`);
await conn.query(`
CREATE TABLE IF NOT EXISTS task_tags (
id INT AUTO_INCREMENT PRIMARY KEY,
task_id VARCHAR(36) NOT NULL,
tag VARCHAR(100) NOT NULL,
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
UNIQUE KEY unique_task_tag (task_id, tag)
)
`);
console.log('✅ Database tables initialized');
} finally {
conn.release();
}
}
export default pool;

35
server/index.js Normal file
View File

@@ -0,0 +1,35 @@
import express from 'express';
import cors from 'cors';
import { initDB } from './db.js';
import authRoutes from './routes/auth.js';
import taskRoutes from './routes/tasks.js';
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/tasks', taskRoutes);
// Health check
app.get('/api/health', (_req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Initialize DB and start server
async function start() {
try {
await initDB();
app.listen(PORT, () => {
console.log(`🚀 Backend server running on port ${PORT}`);
});
} catch (err) {
console.error('❌ Failed to start server:', err);
process.exit(1);
}
}
start();

15
server/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "scrum-manager-backend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^5.1.0",
"mysql2": "^3.14.1",
"cors": "^2.8.5",
"bcryptjs": "^3.0.2"
}
}

79
server/routes/auth.js Normal file
View File

@@ -0,0 +1,79 @@
import { Router } from 'express';
import bcrypt from 'bcryptjs';
import pool from '../db.js';
import { randomUUID } from 'crypto';
const router = Router();
// POST /api/auth/login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
if (rows.length === 0) {
return res.status(401).json({ error: 'Invalid email or password' });
}
const user = rows[0];
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) {
return res.status(401).json({ error: 'Invalid email or password' });
}
res.json({
id: user.id, name: user.name, role: user.role, email: user.email,
color: user.color, avatar: user.avatar, dept: user.dept,
});
} catch (err) {
console.error('Login error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// POST /api/auth/register
router.post('/register', async (req, res) => {
try {
const { name, email, password, role, dept } = req.body;
if (!name || !email || !password) {
return res.status(400).json({ error: 'Name, email and password required' });
}
const [existing] = await pool.query('SELECT id FROM users WHERE email = ?', [email]);
if (existing.length > 0) {
return res.status(409).json({ error: 'Email already registered' });
}
const id = randomUUID();
const password_hash = await bcrypt.hash(password, 10);
const avatar = name.split(' ').map(w => w[0]).join('').substring(0, 2).toUpperCase();
const colors = ['#818cf8', '#f59e0b', '#34d399', '#f472b6', '#fb923c', '#60a5fa', '#a78bfa'];
const color = colors[Math.floor(Math.random() * colors.length)];
await pool.query(
'INSERT INTO users (id, name, role, email, password_hash, color, avatar, dept) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[id, name, role || 'employee', email, password_hash, color, avatar, dept || '']
);
res.status(201).json({ id, name, role: role || 'employee', email, color, avatar, dept: dept || '' });
} catch (err) {
console.error('Register error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// GET /api/auth/users
router.get('/users', async (_req, res) => {
try {
const [rows] = await pool.query('SELECT id, name, role, email, color, avatar, dept FROM users');
res.json(rows);
} catch (err) {
console.error('Get users error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;

202
server/routes/tasks.js Normal file
View File

@@ -0,0 +1,202 @@
import { Router } from 'express';
import pool from '../db.js';
import { randomUUID } from 'crypto';
const router = Router();
// Helper: fetch full task with subtasks, comments, activities, tags
async function getFullTask(taskId) {
const [taskRows] = await pool.query('SELECT * FROM tasks WHERE id = ?', [taskId]);
if (taskRows.length === 0) return null;
const task = taskRows[0];
const [subtasks] = await pool.query('SELECT id, title, done FROM subtasks WHERE task_id = ? ORDER BY id', [taskId]);
const [comments] = await pool.query('SELECT id, user_id AS userId, text, timestamp FROM comments WHERE task_id = ? ORDER BY timestamp', [taskId]);
const [activities] = await pool.query('SELECT id, text, timestamp FROM activities WHERE task_id = ? ORDER BY timestamp', [taskId]);
const [tagRows] = await pool.query('SELECT tag FROM task_tags WHERE task_id = ?', [taskId]);
return {
id: task.id,
title: task.title,
description: task.description || '',
status: task.status,
priority: task.priority,
assignee: task.assignee_id || '',
reporter: task.reporter_id || '',
dueDate: task.due_date ? task.due_date.toISOString().split('T')[0] : '',
tags: tagRows.map(r => r.tag),
subtasks: subtasks.map(s => ({ id: s.id, title: s.title, done: !!s.done })),
comments: comments.map(c => ({ id: c.id, userId: c.userId, text: c.text, timestamp: c.timestamp?.toISOString() || '' })),
activity: activities.map(a => ({ id: a.id, text: a.text, timestamp: a.timestamp?.toISOString() || '' })),
};
}
// GET /api/tasks
router.get('/', async (_req, res) => {
try {
const [taskRows] = await pool.query('SELECT id FROM tasks ORDER BY created_at DESC');
const tasks = await Promise.all(taskRows.map(t => getFullTask(t.id)));
res.json(tasks.filter(Boolean));
} catch (err) {
console.error('Get tasks error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// POST /api/tasks
router.post('/', async (req, res) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const { title, description, status, priority, assignee, reporter, dueDate, tags, subtasks } = req.body;
const id = randomUUID();
await conn.query(
'INSERT INTO tasks (id, title, description, status, priority, assignee_id, reporter_id, due_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[id, title, description || '', status || 'todo', priority || 'medium', assignee || null, reporter || null, dueDate || null]
);
// Insert tags
if (tags && tags.length > 0) {
const tagValues = tags.map(tag => [id, tag]);
await conn.query('INSERT INTO task_tags (task_id, tag) VALUES ?', [tagValues]);
}
// Insert subtasks
if (subtasks && subtasks.length > 0) {
for (const st of subtasks) {
await conn.query('INSERT INTO subtasks (id, task_id, title, done) VALUES (?, ?, ?, ?)',
[st.id || randomUUID(), id, st.title, st.done || false]);
}
}
// Add creation activity
const actId = randomUUID();
await conn.query('INSERT INTO activities (id, task_id, text) VALUES (?, ?, ?)',
[actId, id, '📝 Task created']);
await conn.commit();
const task = await getFullTask(id);
res.status(201).json(task);
} catch (err) {
await conn.rollback();
console.error('Create task error:', err);
res.status(500).json({ error: 'Internal server error' });
} finally {
conn.release();
}
});
// PUT /api/tasks/:id
router.put('/:id', async (req, res) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const { title, description, status, priority, assignee, reporter, dueDate, tags } = req.body;
const taskId = req.params.id;
// Check task exists
const [existing] = await conn.query('SELECT id FROM tasks WHERE id = ?', [taskId]);
if (existing.length === 0) {
await conn.rollback();
return res.status(404).json({ error: 'Task not found' });
}
await conn.query(
`UPDATE tasks SET title = COALESCE(?, title), description = COALESCE(?, description),
status = COALESCE(?, status), priority = COALESCE(?, priority),
assignee_id = COALESCE(?, assignee_id), reporter_id = COALESCE(?, reporter_id),
due_date = COALESCE(?, due_date) WHERE id = ?`,
[title, description, status, priority, assignee, reporter, dueDate, taskId]
);
// Update tags if provided
if (tags !== undefined) {
await conn.query('DELETE FROM task_tags WHERE task_id = ?', [taskId]);
if (tags.length > 0) {
const tagValues = tags.map(tag => [taskId, tag]);
await conn.query('INSERT INTO task_tags (task_id, tag) VALUES ?', [tagValues]);
}
}
await conn.commit();
const task = await getFullTask(taskId);
res.json(task);
} catch (err) {
await conn.rollback();
console.error('Update task error:', err);
res.status(500).json({ error: 'Internal server error' });
} finally {
conn.release();
}
});
// POST /api/tasks/:id/subtasks
router.post('/:id/subtasks', async (req, res) => {
try {
const { title } = req.body;
const id = randomUUID();
await pool.query('INSERT INTO subtasks (id, task_id, title, done) VALUES (?, ?, ?, ?)',
[id, req.params.id, title, false]);
res.status(201).json({ id, title, done: false });
} catch (err) {
console.error('Add subtask error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// PUT /api/tasks/:id/subtasks/:sid
router.put('/:id/subtasks/:sid', async (req, res) => {
try {
const { done } = req.body;
await pool.query('UPDATE subtasks SET done = ? WHERE id = ? AND task_id = ?',
[done, req.params.sid, req.params.id]);
res.json({ success: true });
} catch (err) {
console.error('Toggle subtask error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// POST /api/tasks/:id/comments
router.post('/:id/comments', async (req, res) => {
try {
const { userId, text } = req.body;
const id = randomUUID();
const timestamp = new Date();
await pool.query('INSERT INTO comments (id, task_id, user_id, text, timestamp) VALUES (?, ?, ?, ?, ?)',
[id, req.params.id, userId, text, timestamp]);
res.status(201).json({ id, userId, text, timestamp: timestamp.toISOString() });
} catch (err) {
console.error('Add comment error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// POST /api/tasks/:id/activity
router.post('/:id/activity', async (req, res) => {
try {
const { text } = req.body;
const id = randomUUID();
const timestamp = new Date();
await pool.query('INSERT INTO activities (id, task_id, text, timestamp) VALUES (?, ?, ?, ?)',
[id, req.params.id, text, timestamp]);
res.status(201).json({ id, text, timestamp: timestamp.toISOString() });
} catch (err) {
console.error('Add activity error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// DELETE /api/tasks/:id
router.delete('/:id', async (req, res) => {
try {
await pool.query('DELETE FROM tasks WHERE id = ?', [req.params.id]);
res.json({ success: true });
} catch (err) {
console.error('Delete task error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { SEED_TASKS } from './data'; import { apiFetchTasks, apiFetchUsers, apiCreateTask, apiUpdateTask, apiAddActivity } from './api';
import type { Task, User, Status } from './data'; import type { Task, User, Status } from './data';
import { STATUS_LABELS } from './data';
import { LoginPage } from './Login'; import { LoginPage } from './Login';
import { Sidebar } from './Sidebar'; import { Sidebar } from './Sidebar';
import { TopNavbar, BottomToggleBar } from './NavBars'; import { TopNavbar, BottomToggleBar } from './NavBars';
@@ -24,7 +25,8 @@ const VIEW_PAGES = ['calendar', 'kanban', 'list'];
export default function App() { export default function App() {
const now = new Date(); const now = new Date();
const [currentUser, setCurrentUser] = useState<User | null>(null); const [currentUser, setCurrentUser] = useState<User | null>(null);
const [tasks, setTasks] = useState<Task[]>(SEED_TASKS); const [users, setUsers] = useState<User[]>([]);
const [tasks, setTasks] = useState<Task[]>([]);
const [activePage, setActivePage] = useState('calendar'); const [activePage, setActivePage] = useState('calendar');
const [activeView, setActiveView] = useState('calendar'); const [activeView, setActiveView] = useState('calendar');
const [activeTask, setActiveTask] = useState<Task | null>(null); const [activeTask, setActiveTask] = useState<Task | null>(null);
@@ -34,13 +36,29 @@ export default function App() {
const [calView, setCalView] = useState('month'); const [calView, setCalView] = useState('month');
const [filterUser, setFilterUser] = useState<string | null>(null); const [filterUser, setFilterUser] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [sidebarOpen, setSidebarOpen] = useState(false);
const [quickAddDay, setQuickAddDay] = useState<{ date: string; rect: { top: number; left: number } } | null>(null); const [quickAddDay, setQuickAddDay] = useState<{ date: string; rect: { top: number; left: number } } | null>(null);
const [loading, setLoading] = useState(false);
// Load data from API when user logs in
useEffect(() => {
if (!currentUser) return;
setLoading(true);
Promise.all([apiFetchTasks(), apiFetchUsers()])
.then(([fetchedTasks, fetchedUsers]) => {
setTasks(fetchedTasks);
setUsers(fetchedUsers);
})
.catch(err => console.error('Failed to load data:', err))
.finally(() => setLoading(false));
}, [currentUser]);
if (!currentUser) return <LoginPage onLogin={u => { setCurrentUser(u); setActivePage('calendar'); setActiveView('calendar'); }} />; if (!currentUser) return <LoginPage onLogin={u => { setCurrentUser(u); setActivePage('calendar'); setActiveView('calendar'); }} />;
const handleNavigate = (page: string) => { const handleNavigate = (page: string) => {
setActivePage(page); setActivePage(page);
if (VIEW_PAGES.includes(page)) setActiveView(page); if (VIEW_PAGES.includes(page)) setActiveView(page);
setSidebarOpen(false);
}; };
const handleViewChange = (view: string) => { const handleViewChange = (view: string) => {
@@ -55,29 +73,91 @@ export default function App() {
setQuickAddDay({ date, rect: { top: rect.bottom, left: rect.left } }); setQuickAddDay({ date, rect: { top: rect.bottom, left: rect.left } });
}; };
const handleQuickAdd = (partial: Partial<Task>) => { const handleQuickAdd = async (partial: Partial<Task>) => {
const task: Task = { try {
id: `t${Date.now()}`, title: partial.title || '', description: partial.description || '', const created = await apiCreateTask({
status: (partial.status || 'todo') as Status, priority: partial.priority || 'medium', title: partial.title || '',
assignee: partial.assignee || 'u1', reporter: currentUser.id, dueDate: partial.dueDate || '', description: partial.description || '',
tags: partial.tags || [], subtasks: partial.subtasks || [], comments: partial.comments || [], status: partial.status || 'todo',
activity: [{ id: `a${Date.now()}`, text: '📝 Task created', timestamp: new Date().toISOString() }], priority: partial.priority || 'medium',
}; assignee: partial.assignee || currentUser.id,
setTasks(prev => [...prev, task]); reporter: currentUser.id,
setQuickAddDay(null); dueDate: partial.dueDate || '',
tags: partial.tags || [],
});
setTasks(prev => [...prev, created]);
setQuickAddDay(null);
} catch (err) {
console.error('Failed to quick-add task:', err);
}
}; };
const handleAddTask = (task: Task) => setTasks(prev => [...prev, { ...task, reporter: currentUser.id }]); const handleAddTask = async (task: Task) => {
try {
const created = await apiCreateTask({
title: task.title,
description: task.description,
status: task.status,
priority: task.priority,
assignee: task.assignee,
reporter: currentUser.id,
dueDate: task.dueDate,
tags: task.tags,
});
setTasks(prev => [...prev, created]);
} catch (err) {
console.error('Failed to add task:', err);
}
};
const handleUpdateTask = (updated: Task) => { const handleUpdateTask = async (updated: Task) => {
setTasks(prev => prev.map(t => t.id === updated.id ? updated : t)); try {
setActiveTask(updated); const result = await apiUpdateTask(updated.id, {
title: updated.title,
description: updated.description,
status: updated.status,
priority: updated.priority,
assignee: updated.assignee,
reporter: updated.reporter,
dueDate: updated.dueDate,
tags: updated.tags,
});
setTasks(prev => prev.map(t => t.id === result.id ? result : t));
setActiveTask(result);
} catch (err) {
console.error('Failed to update task:', err);
}
}; };
const handleNewTask = () => { setAddModalDefaults({}); setShowAddModal(true); }; const handleNewTask = () => { setAddModalDefaults({}); setShowAddModal(true); };
const handleKanbanAdd = (status: Status) => { setAddModalDefaults({ status }); setShowAddModal(true); }; const handleKanbanAdd = (status: Status) => { setAddModalDefaults({ status }); setShowAddModal(true); };
const handleToggleDone = (taskId: string) => { const handleToggleDone = async (taskId: string) => {
setTasks(prev => prev.map(t => t.id === taskId ? { ...t, status: t.status === 'done' ? 'todo' : 'done' as Status } : t)); const task = tasks.find(t => t.id === taskId);
if (!task) return;
const newStatus = task.status === 'done' ? 'todo' : 'done';
try {
const result = await apiUpdateTask(taskId, { status: newStatus });
await apiAddActivity(taskId, `🔄 ${currentUser.name} changed status to ${newStatus}`);
setTasks(prev => prev.map(t => t.id === taskId ? result : t));
} catch (err) {
console.error('Failed to toggle done:', err);
}
};
const handleMoveTask = async (taskId: string, newStatus: Status) => {
const task = tasks.find(t => t.id === taskId);
if (!task || task.status === newStatus) return;
// Optimistic update
setTasks(prev => prev.map(t => t.id === taskId ? { ...t, status: newStatus } : t));
try {
const result = await apiUpdateTask(taskId, { status: newStatus });
await apiAddActivity(taskId, `🔄 ${currentUser.name} moved task to ${STATUS_LABELS[newStatus]}`);
setTasks(prev => prev.map(t => t.id === taskId ? result : t));
} catch (err) {
console.error('Failed to move task:', err);
// Revert on failure
setTasks(prev => prev.map(t => t.id === taskId ? task : t));
}
}; };
const displayPage = VIEW_PAGES.includes(activePage) ? activeView : activePage; const displayPage = VIEW_PAGES.includes(activePage) ? activeView : activePage;
@@ -85,35 +165,45 @@ export default function App() {
const pageTitle = PAGE_TITLES[displayPage] || 'Calendar'; const pageTitle = PAGE_TITLES[displayPage] || 'Calendar';
if (loading) {
return (
<div className="app-shell" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
<p style={{ color: '#818cf8', fontSize: 18 }}>Loading...</p>
</div>
);
}
return ( return (
<div className="app-shell"> <div className="app-shell">
<TopNavbar title={pageTitle} filterUser={filterUser} onFilterChange={setFilterUser} <TopNavbar title={pageTitle} filterUser={filterUser} onFilterChange={setFilterUser}
searchQuery={searchQuery} onSearch={setSearchQuery} onNewTask={handleNewTask} /> searchQuery={searchQuery} onSearch={setSearchQuery} onNewTask={handleNewTask}
onOpenSidebar={() => setSidebarOpen(true)} users={users} />
<div className="app-body"> <div className="app-body">
<Sidebar currentUser={currentUser} activePage={activePage} onNavigate={handleNavigate} <Sidebar currentUser={currentUser} activePage={activePage} onNavigate={handleNavigate}
onSignOut={() => { setCurrentUser(null); setActivePage('calendar'); setActiveView('calendar'); }} /> onSignOut={() => { setCurrentUser(null); setActivePage('calendar'); setActiveView('calendar'); setSidebarOpen(false); }}
isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} users={users} />
<div className="main-content"> <div className="main-content">
{displayPage === 'calendar' && ( {displayPage === 'calendar' && (
<CalendarView tasks={tasks} currentUser={currentUser} calMonth={calMonth} calView={calView} <CalendarView tasks={tasks} currentUser={currentUser} calMonth={calMonth} calView={calView}
onMonthChange={setCalMonth} onViewChange={setCalView} onTaskClick={handleTaskClick} onMonthChange={setCalMonth} onViewChange={setCalView} onTaskClick={handleTaskClick}
onDayClick={handleDayClick} filterUser={filterUser} searchQuery={searchQuery} /> onDayClick={handleDayClick} filterUser={filterUser} searchQuery={searchQuery} users={users} />
)} )}
{displayPage === 'kanban' && ( {displayPage === 'kanban' && (
<KanbanBoard tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick} <KanbanBoard tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
onAddTask={handleKanbanAdd} filterUser={filterUser} searchQuery={searchQuery} /> onAddTask={handleKanbanAdd} onMoveTask={handleMoveTask} filterUser={filterUser} searchQuery={searchQuery} users={users} />
)} )}
{displayPage === 'list' && ( {displayPage === 'list' && (
<ListView tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick} <ListView tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
filterUser={filterUser} searchQuery={searchQuery} onToggleDone={handleToggleDone} /> filterUser={filterUser} searchQuery={searchQuery} onToggleDone={handleToggleDone} users={users} />
)} )}
{displayPage === 'dashboard' && <DashboardPage tasks={tasks} currentUser={currentUser} />} {displayPage === 'dashboard' && <DashboardPage tasks={tasks} currentUser={currentUser} users={users} />}
{displayPage === 'mytasks' && ( {displayPage === 'mytasks' && (
<ListView tasks={filteredMyTasks} currentUser={currentUser} onTaskClick={handleTaskClick} <ListView tasks={filteredMyTasks} currentUser={currentUser} onTaskClick={handleTaskClick}
filterUser={null} searchQuery={searchQuery} onToggleDone={handleToggleDone} /> filterUser={null} searchQuery={searchQuery} onToggleDone={handleToggleDone} users={users} />
)} )}
{displayPage === 'teamtasks' && <TeamTasksPage tasks={tasks} currentUser={currentUser} />} {displayPage === 'teamtasks' && <TeamTasksPage tasks={tasks} currentUser={currentUser} users={users} />}
{displayPage === 'reports' && <ReportsPage tasks={tasks} />} {displayPage === 'reports' && <ReportsPage tasks={tasks} users={users} />}
{displayPage === 'members' && <MembersPage tasks={tasks} />} {displayPage === 'members' && <MembersPage tasks={tasks} users={users} />}
</div> </div>
</div> </div>
@@ -121,8 +211,8 @@ export default function App() {
<BottomToggleBar activeView={activeView} onViewChange={handleViewChange} /> <BottomToggleBar activeView={activeView} onViewChange={handleViewChange} />
)} )}
{activeTask && <TaskDrawer task={activeTask} currentUser={currentUser} onClose={() => setActiveTask(null)} onUpdate={handleUpdateTask} />} {activeTask && <TaskDrawer task={activeTask} currentUser={currentUser} onClose={() => setActiveTask(null)} onUpdate={handleUpdateTask} users={users} />}
{showAddModal && <AddTaskModal onClose={() => setShowAddModal(false)} onAdd={handleAddTask} defaultDate={addModalDefaults.date} defaultStatus={addModalDefaults.status} />} {showAddModal && <AddTaskModal onClose={() => setShowAddModal(false)} onAdd={handleAddTask} defaultDate={addModalDefaults.date} defaultStatus={addModalDefaults.status} users={users} currentUser={currentUser} />}
{quickAddDay && ( {quickAddDay && (
<div style={{ position: 'fixed', inset: 0, zIndex: 199 }} onClick={() => setQuickAddDay(null)}> <div style={{ position: 'fixed', inset: 0, zIndex: 199 }} onClick={() => setQuickAddDay(null)}>
@@ -130,7 +220,7 @@ export default function App() {
onClick={e => e.stopPropagation()}> onClick={e => e.stopPropagation()}>
<QuickAddPanel date={quickAddDay.date} onAdd={handleQuickAdd} <QuickAddPanel date={quickAddDay.date} onAdd={handleQuickAdd}
onOpenFull={() => { setAddModalDefaults({ date: quickAddDay.date }); setShowAddModal(true); setQuickAddDay(null); }} onOpenFull={() => { setAddModalDefaults({ date: quickAddDay.date }); setShowAddModal(true); setQuickAddDay(null); }}
onClose={() => setQuickAddDay(null)} /> onClose={() => setQuickAddDay(null)} users={users} />
</div> </div>
</div> </div>
)} )}

View File

@@ -1,13 +1,13 @@
import { useState } from 'react'; import { useState } from 'react';
import type { Task, User } from './data'; import type { Task, User } from './data';
import { USERS, PRIORITY_COLORS } from './data'; import { PRIORITY_COLORS } from './data';
import { Avatar } from './Shared'; import { Avatar } from './Shared';
interface CalendarProps { interface CalendarProps {
tasks: Task[]; currentUser: User; calMonth: { year: number; month: number }; calView: string; tasks: Task[]; currentUser: User; calMonth: { year: number; month: number }; calView: string;
onMonthChange: (m: { year: number; month: number }) => void; onViewChange: (v: string) => void; onMonthChange: (m: { year: number; month: number }) => void; onViewChange: (v: string) => void;
onTaskClick: (t: Task) => void; onDayClick: (date: string, el: HTMLElement) => void; onTaskClick: (t: Task) => void; onDayClick: (date: string, el: HTMLElement) => void;
filterUser: string | null; searchQuery: string; filterUser: string | null; searchQuery: string; users: User[];
} }
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
@@ -47,29 +47,29 @@ function getWeekDays(_year: number, _month: number) {
function dateStr(d: Date) { return d.toISOString().split('T')[0]; } 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 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 }) { function TaskChip({ task, onClick, users }: { task: Task; onClick: () => void; users: User[] }) {
const p = PRIORITY_COLORS[task.priority]; const p = PRIORITY_COLORS[task.priority];
return ( return (
<div className="task-chip" style={{ background: p.bg, borderLeftColor: p.color }} onClick={e => { e.stopPropagation(); onClick(); }}> <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-dot" style={{ background: p.color }} />
<span className="task-chip-title">{task.title}</span> <span className="task-chip-title">{task.title}</span>
<Avatar userId={task.assignee} size={14} /> <Avatar userId={task.assignee} size={14} users={users} />
</div> </div>
); );
} }
function MorePopover({ tasks, onTaskClick, onClose }: { tasks: Task[]; onTaskClick: (t: Task) => void; onClose: () => void }) { function MorePopover({ tasks, onTaskClick, onClose, users }: { tasks: Task[]; onTaskClick: (t: Task) => void; onClose: () => void; users: User[] }) {
return ( return (
<div className="more-popover" onClick={e => e.stopPropagation()}> <div className="more-popover" onClick={e => e.stopPropagation()}>
<div className="more-popover-title">{tasks.length} tasks</div> <div className="more-popover-title">{tasks.length} tasks</div>
{tasks.map(t => <TaskChip key={t.id} task={t} onClick={() => { onTaskClick(t); onClose(); }} />)} {tasks.map(t => <TaskChip key={t.id} task={t} onClick={() => { onTaskClick(t); onClose(); }} users={users} />)}
</div> </div>
); );
} }
export function QuickAddPanel({ date, onAdd, onOpenFull, onClose }: { date: string; onAdd: (t: Partial<Task>) => void; onOpenFull: () => void; onClose: () => void }) { export function QuickAddPanel({ date, onAdd, onOpenFull, onClose, users }: { date: string; onAdd: (t: Partial<Task>) => void; onOpenFull: () => void; onClose: () => void; users: User[] }) {
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [assignee, setAssignee] = useState('u1'); const [assignee, setAssignee] = useState(users[0]?.id || '');
const [priority, setPriority] = useState<'medium' | 'low' | 'high' | 'critical'>('medium'); const [priority, setPriority] = useState<'medium' | 'low' | 'high' | 'critical'>('medium');
const submit = () => { const submit = () => {
@@ -86,7 +86,7 @@ export function QuickAddPanel({ date, onAdd, onOpenFull, onClose }: { date: stri
onChange={e => setTitle(e.target.value)} onKeyDown={e => e.key === 'Enter' && submit()} /> onChange={e => setTitle(e.target.value)} onKeyDown={e => e.key === 'Enter' && submit()} />
<div className="quick-add-row"> <div className="quick-add-row">
<select className="quick-add-select" value={assignee} onChange={e => setAssignee(e.target.value)}> <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>)} {users.map(u => <option key={u.id} value={u.id}>{u.avatar} {u.name}</option>)}
</select> </select>
<select className="quick-add-select" value={priority} onChange={e => setPriority(e.target.value as any)}> <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>)} {['critical', 'high', 'medium', 'low'].map(p => <option key={p} value={p}>{p}</option>)}
@@ -100,7 +100,7 @@ export function QuickAddPanel({ date, onAdd, onOpenFull, onClose }: { date: stri
); );
} }
export function CalendarView({ tasks, currentUser, calMonth, calView, onMonthChange, onViewChange, onTaskClick, onDayClick, filterUser, searchQuery }: CalendarProps) { export function CalendarView({ tasks, currentUser, calMonth, calView, onMonthChange, onViewChange, onTaskClick, onDayClick, filterUser, searchQuery, users }: CalendarProps) {
const [morePopover, setMorePopover] = useState<{ date: string; tasks: Task[] } | null>(null); const [morePopover, setMorePopover] = useState<{ date: string; tasks: Task[] } | null>(null);
const filtered = filterTasks(tasks, currentUser, filterUser, searchQuery); const filtered = filterTasks(tasks, currentUser, filterUser, searchQuery);
@@ -147,14 +147,14 @@ export function CalendarView({ tasks, currentUser, calMonth, calView, onMonthCha
{cell.date.getDate()} {cell.date.getDate()}
</div> </div>
<div className="day-tasks"> <div className="day-tasks">
{show.map(t => <TaskChip key={t.id} task={t} onClick={() => onTaskClick(t)} />)} {show.map(t => <TaskChip key={t.id} task={t} onClick={() => onTaskClick(t)} users={users} />)}
{extra > 0 && ( {extra > 0 && (
<span className="more-tasks-link" onClick={e => { e.stopPropagation(); setMorePopover({ date: ds, tasks: dayTasks }); }}> <span className="more-tasks-link" onClick={e => { e.stopPropagation(); setMorePopover({ date: ds, tasks: dayTasks }); }}>
+{extra} more +{extra} more
</span> </span>
)} )}
</div> </div>
{morePopover?.date === ds && <MorePopover tasks={morePopover.tasks} onTaskClick={onTaskClick} onClose={() => setMorePopover(null)} />} {morePopover?.date === ds && <MorePopover tasks={morePopover.tasks} onTaskClick={onTaskClick} onClose={() => setMorePopover(null)} users={users} />}
</div> </div>
); );
})} })}

View File

@@ -1,8 +1,8 @@
import type { Task, User } from './data'; import type { Task, User } from './data';
import { USERS, STATUS_COLORS, PRIORITY_COLORS } from './data'; import { STATUS_COLORS, PRIORITY_COLORS } from './data';
import { Avatar } from './Shared'; import { Avatar } from './Shared';
export function DashboardPage({ tasks, currentUser }: { tasks: Task[]; currentUser: User }) { export function DashboardPage({ tasks, currentUser, users }: { tasks: Task[]; currentUser: User; users: User[] }) {
const total = tasks.length; const total = tasks.length;
const completed = tasks.filter(t => t.status === 'done').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 overdue = tasks.filter(t => new Date(t.dueDate + 'T00:00:00') < new Date() && t.status !== 'done').length;
@@ -32,13 +32,13 @@ export function DashboardPage({ tasks, currentUser }: { tasks: Task[]; currentUs
<> <>
<div className="workload-card"> <div className="workload-card">
<div className="workload-card-title">Team Workload</div> <div className="workload-card-title">Team Workload</div>
{USERS.filter(u => u.id !== currentUser.id || true).map(u => { {users.map(u => {
const ut = tasks.filter(t => t.assignee === u.id); const ut = tasks.filter(t => t.assignee === u.id);
const done = ut.filter(t => t.status === 'done').length; const done = ut.filter(t => t.status === 'done').length;
const pct = ut.length ? Math.round((done / ut.length) * 100) : 0; const pct = ut.length ? Math.round((done / ut.length) * 100) : 0;
return ( return (
<div key={u.id} className="workload-row"> <div key={u.id} className="workload-row">
<Avatar userId={u.id} size={28} /> <Avatar userId={u.id} size={28} users={users} />
<span className="workload-name">{u.name}</span> <span className="workload-name">{u.name}</span>
<span className="workload-dept">{u.dept}</span> <span className="workload-dept">{u.dept}</span>
<div className="workload-bar"> <div className="workload-bar">

View File

@@ -1,17 +1,27 @@
import { useState } from 'react';
import type { Task, User, Status } from './data'; import type { Task, User, Status } from './data';
import { PRIORITY_COLORS, STATUS_COLORS, STATUS_LABELS } from './data'; import { PRIORITY_COLORS, STATUS_COLORS, STATUS_LABELS } from './data';
import { Avatar, PriorityBadge, StatusBadge, ProgressBar } from './Shared'; import { Avatar, PriorityBadge, StatusBadge, ProgressBar } from './Shared';
function TaskCard({ task, onClick }: { task: Task; onClick: () => void }) { function TaskCard({ task, onClick, users }: { task: Task; onClick: () => void; users: User[] }) {
const p = PRIORITY_COLORS[task.priority]; const p = PRIORITY_COLORS[task.priority];
const due = new Date(task.dueDate + 'T00:00:00'); const due = new Date(task.dueDate + 'T00:00:00');
const overdue = due < new Date() && task.status !== 'done'; const overdue = due < new Date() && task.status !== 'done';
const commCount = task.comments.length; const commCount = task.comments.length;
return ( return (
<div className="task-card" style={{ borderLeftColor: p.color }} onClick={onClick}> <div className="task-card" style={{ borderLeftColor: p.color }}
draggable
onDragStart={e => {
e.dataTransfer.setData('text/plain', task.id);
e.dataTransfer.effectAllowed = 'move';
(e.currentTarget as HTMLElement).classList.add('dragging');
}}
onDragEnd={e => (e.currentTarget as HTMLElement).classList.remove('dragging')}
onClick={onClick}
>
<div className="task-card-row"> <div className="task-card-row">
<span className="task-card-title">{task.title}</span> <span className="task-card-title">{task.title}</span>
<Avatar userId={task.assignee} size={24} /> <Avatar userId={task.assignee} size={24} users={users} />
</div> </div>
<div className="task-card-badges"> <div className="task-card-badges">
<PriorityBadge level={task.priority} /> <PriorityBadge level={task.priority} />
@@ -27,12 +37,29 @@ function TaskCard({ task, onClick }: { task: Task; onClick: () => void }) {
); );
} }
function KanbanColumn({ status, statusLabel, tasks, color, onTaskClick, onAddTask }: { function KanbanColumn({ status, statusLabel, tasks, color, onTaskClick, onAddTask, onMoveTask, users }: {
status: Status; statusLabel: string; tasks: Task[]; color: string; status: Status; statusLabel: string; tasks: Task[]; color: string;
onTaskClick: (t: Task) => void; onAddTask: (s: Status) => void; onTaskClick: (t: Task) => void; onAddTask: (s: Status) => void;
onMoveTask: (taskId: string, newStatus: Status) => void; users: User[];
}) { }) {
const [dragOver, setDragOver] = useState(false);
return ( return (
<div className="kanban-column"> <div
className={`kanban-column ${dragOver ? 'kanban-column-dragover' : ''}`}
onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOver(true); }}
onDragEnter={e => { e.preventDefault(); setDragOver(true); }}
onDragLeave={e => {
// Only set false if leaving the column (not entering a child)
if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragOver(false);
}}
onDrop={e => {
e.preventDefault();
setDragOver(false);
const taskId = e.dataTransfer.getData('text/plain');
if (taskId) onMoveTask(taskId, status);
}}
>
<div className="kanban-col-header"> <div className="kanban-col-header">
<div className="kanban-col-dot" style={{ background: color }} /> <div className="kanban-col-dot" style={{ background: color }} />
<span className="kanban-col-label">{statusLabel}</span> <span className="kanban-col-label">{statusLabel}</span>
@@ -41,9 +68,11 @@ function KanbanColumn({ status, statusLabel, tasks, color, onTaskClick, onAddTas
</div> </div>
<div className="kanban-col-body"> <div className="kanban-col-body">
{tasks.length === 0 ? ( {tasks.length === 0 ? (
<div className="kanban-empty">No tasks here · Click + to add one</div> <div className="kanban-empty">
{dragOver ? '⬇ Drop here' : 'No tasks here · Click + to add one'}
</div>
) : ( ) : (
tasks.map(t => <TaskCard key={t.id} task={t} onClick={() => onTaskClick(t)} />) tasks.map(t => <TaskCard key={t.id} task={t} onClick={() => onTaskClick(t)} users={users} />)
)} )}
</div> </div>
</div> </div>
@@ -52,10 +81,11 @@ function KanbanColumn({ status, statusLabel, tasks, color, onTaskClick, onAddTas
interface KanbanProps { interface KanbanProps {
tasks: Task[]; currentUser: User; onTaskClick: (t: Task) => void; tasks: Task[]; currentUser: User; onTaskClick: (t: Task) => void;
onAddTask: (s: Status) => void; filterUser: string | null; searchQuery: string; onAddTask: (s: Status) => void; onMoveTask: (taskId: string, newStatus: Status) => void;
filterUser: string | null; searchQuery: string; users: User[];
} }
export function KanbanBoard({ tasks, currentUser, onTaskClick, onAddTask, filterUser, searchQuery }: KanbanProps) { export function KanbanBoard({ tasks, currentUser, onTaskClick, onAddTask, onMoveTask, filterUser, searchQuery, users }: KanbanProps) {
let filtered = tasks; let filtered = tasks;
if (currentUser.role === 'employee') filtered = filtered.filter(t => t.assignee === currentUser.id); if (currentUser.role === 'employee') filtered = filtered.filter(t => t.assignee === currentUser.id);
if (filterUser) filtered = filtered.filter(t => t.assignee === filterUser); if (filterUser) filtered = filtered.filter(t => t.assignee === filterUser);
@@ -66,7 +96,8 @@ export function KanbanBoard({ tasks, currentUser, onTaskClick, onAddTask, filter
<div className="kanban-board"> <div className="kanban-board">
{statuses.map(s => ( {statuses.map(s => (
<KanbanColumn key={s} status={s} statusLabel={STATUS_LABELS[s]} color={STATUS_COLORS[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} /> tasks={filtered.filter(t => t.status === s)} onTaskClick={onTaskClick}
onAddTask={onAddTask} onMoveTask={onMoveTask} users={users} />
))} ))}
</div> </div>
); );

View File

@@ -7,12 +7,13 @@ interface ListProps {
tasks: Task[]; currentUser: User; onTaskClick: (t: Task) => void; tasks: Task[]; currentUser: User; onTaskClick: (t: Task) => void;
filterUser: string | null; searchQuery: string; filterUser: string | null; searchQuery: string;
onToggleDone: (taskId: string) => void; onToggleDone: (taskId: string) => void;
users: User[];
} }
type SortKey = 'dueDate' | 'priority' | 'status' | 'assignee'; type SortKey = 'dueDate' | 'priority' | 'status' | 'assignee';
const PRIO_ORDER = { critical: 0, high: 1, medium: 2, low: 3 }; const PRIO_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
export function ListView({ tasks, currentUser, onTaskClick, filterUser, searchQuery, onToggleDone }: ListProps) { export function ListView({ tasks, currentUser, onTaskClick, filterUser, searchQuery, onToggleDone, users }: ListProps) {
const [sortBy, setSortBy] = useState<SortKey>('dueDate'); const [sortBy, setSortBy] = useState<SortKey>('dueDate');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
const [menuOpen, setMenuOpen] = useState<string | null>(null); const [menuOpen, setMenuOpen] = useState<string | null>(null);
@@ -53,14 +54,14 @@ export function ListView({ tasks, currentUser, onTaskClick, filterUser, searchQu
</thead> </thead>
<tbody> <tbody>
{sorted.map(t => { {sorted.map(t => {
const u = getUserById(t.assignee); const u = getUserById(users, t.assignee);
const due = new Date(t.dueDate + 'T00:00:00'); const due = new Date(t.dueDate + 'T00:00:00');
const overdue = due < new Date() && t.status !== 'done'; const overdue = due < new Date() && t.status !== 'done';
return ( return (
<tr key={t.id}> <tr key={t.id}>
<td><input type="checkbox" checked={t.status === 'done'} onChange={() => onToggleDone(t.id)} /></td> <td><input type="checkbox" checked={t.status === 'done'} onChange={() => onToggleDone(t.id)} /></td>
<td onClick={() => onTaskClick(t)} style={{ cursor: 'pointer' }}>{t.title}</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><div style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Avatar userId={t.assignee} size={20} users={users} />{u?.name}</div></td>
<td><PriorityBadge level={t.priority} /></td> <td><PriorityBadge level={t.priority} /></td>
<td><StatusBadge status={t.status} /></td> <td><StatusBadge status={t.status} /></td>
<td style={{ color: overdue ? '#ef4444' : undefined }}>{due.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</td> <td style={{ color: overdue ? '#ef4444' : undefined }}>{due.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</td>

View File

@@ -1,29 +1,70 @@
import { useState } from 'react'; import { useState } from 'react';
import { USERS } from './data';
import type { User } from './data'; import type { User } from './data';
import { apiLogin, apiRegister } from './api';
export function LoginPage({ onLogin }: { onLogin: (u: User) => void }) { export function LoginPage({ onLogin }: { onLogin: (u: User) => void }) {
const [mode, setMode] = useState<'login' | 'register'>('login');
const [name, setName] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [pass, setPass] = useState(''); const [pass, setPass] = useState('');
const [role, setRole] = useState('employee');
const [dept, setDept] = useState('');
const [showPass, setShowPass] = useState(false); const [showPass, setShowPass] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const user = USERS.find(u => u.email === email && u.pass === pass); setLoading(true);
if (user) { onLogin(user); } setError('');
else { setError('Invalid email or password'); } try {
const user = await apiLogin(email, pass);
onLogin(user);
} catch (err: any) {
setError(err.message || 'Invalid email or password');
} finally {
setLoading(false);
}
};
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim() || !email.trim() || !pass.trim()) {
setError('All fields are required');
return;
}
setLoading(true);
setError('');
try {
const user = await apiRegister({ name, email, password: pass, role, dept });
onLogin(user);
} catch (err: any) {
setError(err.message || 'Registration failed');
} finally {
setLoading(false);
}
}; };
return ( return (
<div className="login-bg"> <div className="login-bg">
<form className="login-card" onSubmit={handleSubmit}> <form className="login-card" onSubmit={mode === 'login' ? handleLogin : handleRegister}>
<div className="login-logo"> <div className="login-logo">
<div className="login-logo-icon"></div> <div className="login-logo-icon"></div>
<span className="login-title">Scrum-manager</span> <span className="login-title">Scrum-manager</span>
</div> </div>
<p className="login-tagline">Your team's command center</p> <p className="login-tagline">Your team's command center</p>
<div className="login-divider" /> <div className="login-divider" />
{mode === 'register' && (
<>
<label className="login-label">Name</label>
<div className="login-input-wrap">
<input className={`login-input ${error ? 'error' : ''}`} type="text" placeholder="Your full name"
value={name} onChange={e => { setName(e.target.value); setError(''); }} />
</div>
</>
)}
<label className="login-label">Email</label> <label className="login-label">Email</label>
<div className="login-input-wrap"> <div className="login-input-wrap">
<input className={`login-input ${error ? 'error' : ''}`} type="email" placeholder="you@company.io" <input className={`login-input ${error ? 'error' : ''}`} type="email" placeholder="you@company.io"
@@ -35,9 +76,32 @@ export function LoginPage({ onLogin }: { onLogin: (u: User) => void }) {
value={pass} onChange={e => { setPass(e.target.value); setError(''); }} /> value={pass} onChange={e => { setPass(e.target.value); setError(''); }} />
<button type="button" className="login-eye" onClick={() => setShowPass(!showPass)}>{showPass ? '🙈' : '👁'}</button> <button type="button" className="login-eye" onClick={() => setShowPass(!showPass)}>{showPass ? '🙈' : '👁'}</button>
</div> </div>
<button type="submit" className="login-btn">Sign In</button>
{mode === 'register' && (
<>
<label className="login-label">Role</label>
<div className="login-input-wrap">
<select className="login-input" value={role} onChange={e => setRole(e.target.value)}>
<option value="employee">Employee</option>
<option value="manager">Manager</option>
<option value="cto">CTO</option>
</select>
</div>
<label className="login-label">Department</label>
<div className="login-input-wrap">
<input className="login-input" type="text" placeholder="e.g. Backend, Frontend, DevOps"
value={dept} onChange={e => setDept(e.target.value)} />
</div>
</>
)}
<button type="submit" className="login-btn" disabled={loading}>
{loading ? '...' : mode === 'login' ? 'Sign In' : 'Create Account'}
</button>
{error && <p className="login-error">{error}</p>} {error && <p className="login-error">{error}</p>}
<div className="login-hint">💡 Try: subodh@corp.io / cto123</div> <div className="login-hint" style={{ cursor: 'pointer' }} onClick={() => { setMode(mode === 'login' ? 'register' : 'login'); setError(''); }}>
{mode === 'login' ? '📝 No account? Register here' : '🔑 Already have an account? Sign in'}
</div>
</form> </form>
</div> </div>
); );

View File

@@ -1,5 +1,4 @@
import { USERS } from './data'; import type { User } from './data';
interface TopNavbarProps { interface TopNavbarProps {
title: string; title: string;
@@ -8,11 +7,14 @@ interface TopNavbarProps {
searchQuery: string; searchQuery: string;
onSearch: (q: string) => void; onSearch: (q: string) => void;
onNewTask: () => void; onNewTask: () => void;
onOpenSidebar: () => void;
users: User[];
} }
export function TopNavbar({ title, filterUser, onFilterChange, searchQuery, onSearch, onNewTask }: TopNavbarProps) { export function TopNavbar({ title, filterUser, onFilterChange, searchQuery, onSearch, onNewTask, onOpenSidebar, users }: TopNavbarProps) {
return ( return (
<div className="top-navbar"> <div className="top-navbar">
<button className="hamburger-btn" onClick={onOpenSidebar}></button>
<span className="navbar-title">{title}</span> <span className="navbar-title">{title}</span>
<div className="navbar-search"> <div className="navbar-search">
<span className="navbar-search-icon">🔍</span> <span className="navbar-search-icon">🔍</span>
@@ -21,7 +23,7 @@ export function TopNavbar({ title, filterUser, onFilterChange, searchQuery, onSe
<div className="navbar-right"> <div className="navbar-right">
<div className="filter-chips"> <div className="filter-chips">
<span className={`filter-chip filter-chip-all ${!filterUser ? 'active' : ''}`} onClick={() => onFilterChange(null)}>All</span> <span className={`filter-chip filter-chip-all ${!filterUser ? 'active' : ''}`} onClick={() => onFilterChange(null)}>All</span>
{USERS.map(u => ( {users.map(u => (
<div key={u.id} className={`filter-chip ${filterUser === u.id ? 'active' : ''}`} <div key={u.id} className={`filter-chip ${filterUser === u.id ? 'active' : ''}`}
style={{ background: u.color, borderColor: filterUser === u.id ? u.color : 'transparent' }} style={{ background: u.color, borderColor: filterUser === u.id ? u.color : 'transparent' }}
title={u.name} onClick={() => onFilterChange(u.id === filterUser ? null : u.id)}> title={u.name} onClick={() => onFilterChange(u.id === filterUser ? null : u.id)}>

View File

@@ -1,22 +1,21 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import type { Task, User } from './data'; import type { Task, User } from './data';
import { USERS, PRIORITY_COLORS } from './data'; import { PRIORITY_COLORS } from './data';
import { Avatar, StatusBadge } from './Shared'; import { Avatar, StatusBadge } from './Shared';
export function TeamTasksPage({ tasks }: { tasks: Task[]; currentUser: User }) { export function TeamTasksPage({ tasks, users }: { tasks: Task[]; currentUser: User; users: User[] }) {
const [expanded, setExpanded] = useState<Record<string, boolean>>({}); const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const members = USERS;
return ( return (
<div className="team-tasks"> <div className="team-tasks">
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16 }}>Team Tasks</h2> <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16 }}>Team Tasks</h2>
{members.map(m => { {users.map(m => {
const mTasks = tasks.filter(t => t.assignee === m.id); const mTasks = tasks.filter(t => t.assignee === m.id);
const isOpen = expanded[m.id] !== false; const isOpen = expanded[m.id] !== false;
return ( return (
<div key={m.id} className="team-group"> <div key={m.id} className="team-group">
<div className="team-group-header" onClick={() => setExpanded(e => ({ ...e, [m.id]: !isOpen }))}> <div className="team-group-header" onClick={() => setExpanded(e => ({ ...e, [m.id]: !isOpen }))}>
<Avatar userId={m.id} size={28} /> <Avatar userId={m.id} size={28} users={users} />
<span className="team-group-name">{m.name}</span> <span className="team-group-name">{m.name}</span>
<span className="team-group-count">({mTasks.length} tasks)</span> <span className="team-group-count">({mTasks.length} tasks)</span>
<span style={{ color: '#64748b' }}>{isOpen ? '▼' : '▶'}</span> <span style={{ color: '#64748b' }}>{isOpen ? '▼' : '▶'}</span>
@@ -43,7 +42,7 @@ export function TeamTasksPage({ tasks }: { tasks: Task[]; currentUser: User }) {
); );
} }
export function MembersPage({ tasks }: { tasks: Task[] }) { export function MembersPage({ tasks, users }: { tasks: Task[]; users: User[] }) {
const [expanded, setExpanded] = useState<string | null>(null); const [expanded, setExpanded] = useState<string | null>(null);
const [showInvite, setShowInvite] = useState(false); const [showInvite, setShowInvite] = useState(false);
@@ -56,7 +55,7 @@ export function MembersPage({ tasks }: { tasks: Task[] }) {
<table className="members-table"> <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> <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> <tbody>
{USERS.map(u => { {users.map(u => {
const ut = tasks.filter(t => t.assignee === u.id); const ut = tasks.filter(t => t.assignee === u.id);
const done = ut.filter(t => t.status === 'done').length; const done = ut.filter(t => t.status === 'done').length;
const active = ut.filter(t => t.status !== 'done').length; const active = ut.filter(t => t.status !== 'done').length;
@@ -64,7 +63,7 @@ export function MembersPage({ tasks }: { tasks: Task[] }) {
return ( return (
<React.Fragment key={u.id}> <React.Fragment key={u.id}>
<tr onClick={() => setExpanded(expanded === u.id ? null : u.id)}> <tr onClick={() => setExpanded(expanded === u.id ? null : u.id)}>
<td><Avatar userId={u.id} size={28} /></td> <td><Avatar userId={u.id} size={28} users={users} /></td>
<td>{u.name}</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><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>{u.dept}</td>
@@ -110,4 +109,3 @@ export function MembersPage({ tasks }: { tasks: Task[] }) {
</div> </div>
); );
} }

View File

@@ -1,5 +1,5 @@
import type { Task } from './data'; import type { Task, User } from './data';
import { USERS, STATUS_COLORS, PRIORITY_COLORS } from './data'; import { STATUS_COLORS, PRIORITY_COLORS } from './data';
import { BarChart, Bar, PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts'; import { BarChart, Bar, PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
const tooltipStyle = { const tooltipStyle = {
@@ -8,14 +8,14 @@ const tooltipStyle = {
labelStyle: { color: '#94a3b8' }, labelStyle: { color: '#94a3b8' },
}; };
export function ReportsPage({ tasks }: { tasks: Task[] }) { export function ReportsPage({ tasks, users }: { tasks: Task[]; users: User[] }) {
const total = tasks.length; const total = tasks.length;
const completed = tasks.filter(t => t.status === 'done').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 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 critical = tasks.filter(t => t.priority === 'critical' && t.status !== 'done').length;
// Tasks per member (stacked by status) // Tasks per member (stacked by status)
const memberData = USERS.map(u => { const memberData = users.map(u => {
const ut = tasks.filter(t => t.assignee === u.id); const ut = tasks.filter(t => t.assignee === u.id);
return { return {
name: u.name.split(' ')[0], name: u.name.split(' ')[0],
@@ -36,7 +36,7 @@ export function ReportsPage({ tasks }: { tasks: Task[] }) {
const completionData = days.map((d, i) => ({ name: d, completed: [1, 0, 2, 1, 3, 0, 1][i] })); const completionData = days.map((d, i) => ({ name: d, completed: [1, 0, 2, 1, 3, 0, 1][i] }));
// Overdue by member // Overdue by member
const overdueData = USERS.map(u => ({ const overdueData = users.map(u => ({
name: u.name.split(' ')[0], 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, 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); })).filter(d => d.overdue > 0);

View File

@@ -1,8 +1,9 @@
import { PRIORITY_COLORS, STATUS_COLORS, STATUS_LABELS, getUserById } from './data'; import { PRIORITY_COLORS, STATUS_COLORS, STATUS_LABELS, getUserById } from './data';
import type { Priority, Status, Subtask } from './data'; import type { Priority, Status, Subtask, User } from './data';
export function Avatar({ userId, size = 28 }: { userId: string; size?: number }) { export function Avatar({ userId, size = 28, users }: { userId: string; size?: number; users: User[] }) {
const u = getUserById(userId); if (!users || !users.length) return null;
const u = getUserById(users, userId);
if (!u) return null; if (!u) return null;
return ( return (
<div className="avatar" style={{ width: size, height: size, fontSize: size * 0.36, background: u.color }}> <div className="avatar" style={{ width: size, height: size, fontSize: size * 0.36, background: u.color }}>

View File

@@ -17,36 +17,42 @@ interface SidebarProps {
activePage: string; activePage: string;
onNavigate: (page: string) => void; onNavigate: (page: string) => void;
onSignOut: () => void; onSignOut: () => void;
isOpen: boolean;
onClose: () => void;
users: User[];
} }
export function Sidebar({ currentUser, activePage, onNavigate, onSignOut }: SidebarProps) { export function Sidebar({ currentUser, activePage, onNavigate, onSignOut, isOpen, onClose, users }: SidebarProps) {
const filteredNav = NAV_ITEMS.filter(n => n.roles.includes(currentUser.role)); const filteredNav = NAV_ITEMS.filter(n => n.roles.includes(currentUser.role));
return ( return (
<div className="sidebar"> <>
<div className="sidebar-logo"> {isOpen && <div className="sidebar-backdrop visible" onClick={onClose} />}
<div className="sidebar-logo-icon"></div> <div className={`sidebar ${isOpen ? 'sidebar-open' : ''}`}>
<span className="sidebar-logo-text">Scrum-manager</span> <div className="sidebar-logo">
</div> <div className="sidebar-logo-icon"></div>
<div className="sidebar-divider" /> <span className="sidebar-logo-text">Scrum-manager</span>
<div className="sidebar-section-label">Navigate</div> </div>
<nav className="sidebar-nav"> <div className="sidebar-divider" />
{filteredNav.map(n => ( <div className="sidebar-section-label">Navigate</div>
<div key={n.id} className={`sidebar-item ${activePage === n.id ? 'active' : ''}`} onClick={() => onNavigate(n.id)}> <nav className="sidebar-nav">
<span className="sidebar-item-icon">{n.icon}</span> {filteredNav.map(n => (
{n.label} <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} users={users} />
<div className="sidebar-profile-info">
<div className="sidebar-profile-name">{currentUser.name}</div>
<RoleBadge role={currentUser.role} />
</div> </div>
))} </div>
</nav> <div style={{ padding: '0 16px 12px' }}>
<div className="sidebar-profile"> <button className="sidebar-signout" onClick={onSignOut}>Sign Out</button>
<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> </div>
<div style={{ padding: '0 16px 12px' }}> </>
<button className="sidebar-signout" onClick={onSignOut}>Sign Out</button>
</div>
</div>
); );
} }

View File

@@ -1,14 +1,15 @@
import { useState } from 'react'; import { useState } from 'react';
import type { Task, User, Status, Priority } from './data'; import type { Task, User, Status, Priority } from './data';
import { USERS, STATUS_LABELS, getUserById } from './data'; import { STATUS_LABELS, getUserById } from './data';
import { Avatar, Tag, ProgressBar } from './Shared'; import { Avatar, Tag, ProgressBar } from './Shared';
interface DrawerProps { interface DrawerProps {
task: Task; currentUser: User; onClose: () => void; task: Task; currentUser: User; onClose: () => void;
onUpdate: (updated: Task) => void; onUpdate: (updated: Task) => void;
users: User[];
} }
export function TaskDrawer({ task, currentUser, onClose, onUpdate }: DrawerProps) { export function TaskDrawer({ task, currentUser, onClose, onUpdate, users }: DrawerProps) {
const [commentText, setCommentText] = useState(''); const [commentText, setCommentText] = useState('');
const [subtaskText, setSubtaskText] = useState(''); const [subtaskText, setSubtaskText] = useState('');
@@ -48,7 +49,7 @@ export function TaskDrawer({ task, currentUser, onClose, onUpdate }: DrawerProps
setCommentText(''); setCommentText('');
}; };
const reporter = getUserById(task.reporter); const reporter = getUserById(users, task.reporter);
const doneCount = task.subtasks.filter(s => s.done).length; const doneCount = task.subtasks.filter(s => s.done).length;
return ( return (
@@ -67,15 +68,15 @@ export function TaskDrawer({ task, currentUser, onClose, onUpdate }: DrawerProps
<div> <div>
<div className="drawer-meta-label">Assignee</div> <div className="drawer-meta-label">Assignee</div>
<div className="drawer-meta-val"> <div className="drawer-meta-val">
<Avatar userId={task.assignee} size={20} /> <Avatar userId={task.assignee} size={20} users={users} />
<select className="drawer-select" value={task.assignee} onChange={e => updateField('assignee', e.target.value)}> <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>)} {users.map(u => <option key={u.id} value={u.id}>{u.name}</option>)}
</select> </select>
</div> </div>
</div> </div>
<div> <div>
<div className="drawer-meta-label">Reporter</div> <div className="drawer-meta-label">Reporter</div>
<div className="drawer-meta-val"><Avatar userId={task.reporter} size={20} /> {reporter?.name}</div> <div className="drawer-meta-val"><Avatar userId={task.reporter} size={20} users={users} /> {reporter?.name}</div>
</div> </div>
<div> <div>
<div className="drawer-meta-label">Status</div> <div className="drawer-meta-label">Status</div>
@@ -117,10 +118,10 @@ export function TaskDrawer({ task, currentUser, onClose, onUpdate }: DrawerProps
<div className="drawer-section"> <div className="drawer-section">
<div className="drawer-section-title">Comments</div> <div className="drawer-section-title">Comments</div>
{task.comments.map(c => { {task.comments.map(c => {
const cu = getUserById(c.userId); const cu = getUserById(users, c.userId);
return ( return (
<div key={c.id} className="comment-item"> <div key={c.id} className="comment-item">
<Avatar userId={c.userId} size={26} /> <Avatar userId={c.userId} size={26} users={users} />
<div className="comment-bubble"> <div className="comment-bubble">
<div className="comment-header"> <div className="comment-header">
<span className="comment-name">{cu?.name}</span> <span className="comment-name">{cu?.name}</span>
@@ -132,7 +133,7 @@ export function TaskDrawer({ task, currentUser, onClose, onUpdate }: DrawerProps
); );
})} })}
<div className="comment-input-row"> <div className="comment-input-row">
<Avatar userId={currentUser.id} size={26} /> <Avatar userId={currentUser.id} size={26} users={users} />
<input placeholder="Add a comment..." value={commentText} onChange={e => setCommentText(e.target.value)} onKeyDown={e => e.key === 'Enter' && addComment()} /> <input placeholder="Add a comment..." value={commentText} onChange={e => setCommentText(e.target.value)} onKeyDown={e => e.key === 'Enter' && addComment()} />
<button onClick={addComment}>Post</button> <button onClick={addComment}>Post</button>
</div> </div>
@@ -157,12 +158,14 @@ interface ModalProps {
onAdd: (task: Task) => void; onAdd: (task: Task) => void;
defaultDate?: string; defaultDate?: string;
defaultStatus?: Status; defaultStatus?: Status;
users: User[];
currentUser: User;
} }
export function AddTaskModal({ onClose, onAdd, defaultDate, defaultStatus }: ModalProps) { export function AddTaskModal({ onClose, onAdd, defaultDate, defaultStatus, users, currentUser }: ModalProps) {
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [desc, setDesc] = useState(''); const [desc, setDesc] = useState('');
const [assignee, setAssignee] = useState('u1'); const [assignee, setAssignee] = useState(users[0]?.id || '');
const [priority, setPriority] = useState<Priority>('medium'); const [priority, setPriority] = useState<Priority>('medium');
const [status, setStatus] = useState<Status>(defaultStatus || 'todo'); const [status, setStatus] = useState<Status>(defaultStatus || 'todo');
const [dueDate, setDueDate] = useState(defaultDate || new Date().toISOString().split('T')[0]); const [dueDate, setDueDate] = useState(defaultDate || new Date().toISOString().split('T')[0]);
@@ -173,7 +176,7 @@ export function AddTaskModal({ onClose, onAdd, defaultDate, defaultStatus }: Mod
if (!title.trim()) { setError(true); return; } if (!title.trim()) { setError(true); return; }
const task: Task = { const task: Task = {
id: `t${Date.now()}`, title, description: desc, status, priority, id: `t${Date.now()}`, title, description: desc, status, priority,
assignee, reporter: 'u1', dueDate, assignee, reporter: currentUser.id, dueDate,
tags: tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [], tags: tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [],
subtasks: [], comments: [], subtasks: [], comments: [],
activity: [{ id: `a${Date.now()}`, text: `📝 Task created`, timestamp: new Date().toISOString() }], activity: [{ id: `a${Date.now()}`, text: `📝 Task created`, timestamp: new Date().toISOString() }],
@@ -199,7 +202,7 @@ export function AddTaskModal({ onClose, onAdd, defaultDate, defaultStatus }: Mod
<div className="modal-field"> <div className="modal-field">
<label>Assignee</label> <label>Assignee</label>
<select className="modal-input" value={assignee} onChange={e => setAssignee(e.target.value)}> <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>)} {users.map(u => <option key={u.id} value={u.id}>{u.avatar} {u.name}</option>)}
</select> </select>
</div> </div>
<div className="modal-field"> <div className="modal-field">

85
src/api.ts Normal file
View File

@@ -0,0 +1,85 @@
const API_BASE = '/api';
async function request(url: string, options?: RequestInit) {
const res = await fetch(`${API_BASE}${url}`, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(err.error || 'Request failed');
}
return res.json();
}
// Auth
export async function apiLogin(email: string, password: string) {
return request('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
}
export async function apiRegister(data: {
name: string; email: string; password: string; role?: string; dept?: string;
}) {
return request('/auth/register', {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function apiFetchUsers() {
return request('/auth/users');
}
// Tasks
export async function apiFetchTasks() {
return request('/tasks');
}
export async function apiCreateTask(task: {
title: string; description?: string; status?: string; priority?: string;
assignee?: string; reporter?: string; dueDate?: string; tags?: string[];
subtasks?: { title: string; done: boolean }[];
}) {
return request('/tasks', {
method: 'POST',
body: JSON.stringify(task),
});
}
export async function apiUpdateTask(id: string, updates: Record<string, unknown>) {
return request(`/tasks/${id}`, {
method: 'PUT',
body: JSON.stringify(updates),
});
}
export async function apiAddSubtask(taskId: string, title: string) {
return request(`/tasks/${taskId}/subtasks`, {
method: 'POST',
body: JSON.stringify({ title }),
});
}
export async function apiToggleSubtask(taskId: string, subtaskId: string, done: boolean) {
return request(`/tasks/${taskId}/subtasks/${subtaskId}`, {
method: 'PUT',
body: JSON.stringify({ done }),
});
}
export async function apiAddComment(taskId: string, userId: string, text: string) {
return request(`/tasks/${taskId}/comments`, {
method: 'POST',
body: JSON.stringify({ userId, text }),
});
}
export async function apiAddActivity(taskId: string, text: string) {
return request(`/tasks/${taskId}/activity`, {
method: 'POST',
body: JSON.stringify({ text }),
});
}

View File

@@ -1,22 +1,11 @@
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 Priority = 'critical' | 'high' | 'medium' | 'low';
export type Status = 'todo' | 'inprogress' | 'review' | 'done'; export type Status = 'todo' | 'inprogress' | 'review' | 'done';
export interface User {
id: string; name: string; role: string; email: string;
color: string; avatar: string; dept: string;
}
export interface Subtask { id: string; title: string; done: boolean } export interface Subtask { id: string; title: string; done: boolean }
export interface Comment { id: string; userId: string; text: string; timestamp: string } export interface Comment { id: string; userId: string; text: string; timestamp: string }
export interface Activity { id: string; text: string; timestamp: string } export interface Activity { id: string; text: string; timestamp: string }
@@ -41,77 +30,4 @@ export const STATUS_LABELS: Record<Status, string> = {
todo: 'To Do', inprogress: 'In Progress', review: 'Review', done: 'Done', todo: 'To Do', inprogress: 'In Progress', review: 'Review', done: 'Done',
}; };
export const SEED_TASKS: Task[] = [ export function getUserById(users: User[], id: string) { return users.find(u => u.id === id); }
{
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); }

File diff suppressed because it is too large Load Diff

View File

@@ -4,4 +4,13 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: {
host: '0.0.0.0',
proxy: {
'/api': {
target: 'http://backend:3001',
changeOrigin: true,
},
},
},
}) })