From 892a2ceba1f23dbd7976e05eec8988ae91b4a2aa Mon Sep 17 00:00:00 2001 From: tusuii Date: Mon, 16 Feb 2026 10:20:27 +0530 Subject: [PATCH] feat: MySQL integration, Docker setup, drag-and-drop kanban --- Dockerfile | 12 +++ docker-compose.yml | 52 +++++++++++ server/Dockerfile | 12 +++ server/db.js | 94 +++++++++++++++++++ server/index.js | 35 +++++++ server/package.json | 15 +++ server/routes/auth.js | 79 ++++++++++++++++ server/routes/tasks.js | 202 +++++++++++++++++++++++++++++++++++++++++ src/App.tsx | 150 +++++++++++++++++++++++------- src/Calendar.tsx | 24 ++--- src/Dashboard.tsx | 8 +- src/Kanban.tsx | 51 +++++++++-- src/ListView.tsx | 7 +- src/Login.tsx | 80 ++++++++++++++-- src/NavBars.tsx | 8 +- src/Pages.tsx | 16 ++-- src/Reports.tsx | 10 +- src/Shared.tsx | 7 +- src/Sidebar.tsx | 6 +- src/TaskDrawer.tsx | 29 +++--- src/api.ts | 85 +++++++++++++++++ src/data.ts | 96 ++------------------ src/index.css | 28 ++++++ vite.config.ts | 9 ++ 24 files changed, 919 insertions(+), 196 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 server/Dockerfile create mode 100644 server/db.js create mode 100644 server/index.js create mode 100644 server/package.json create mode 100644 server/routes/auth.js create mode 100644 server/routes/tasks.js create mode 100644 src/api.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..324adcf --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a2676d5 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..02512db --- /dev/null +++ b/server/Dockerfile @@ -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"] diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..cdf3e6b --- /dev/null +++ b/server/db.js @@ -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; diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..fa3e24e --- /dev/null +++ b/server/index.js @@ -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(); diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..d3bc8a9 --- /dev/null +++ b/server/package.json @@ -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" + } +} \ No newline at end of file diff --git a/server/routes/auth.js b/server/routes/auth.js new file mode 100644 index 0000000..fa7d88d --- /dev/null +++ b/server/routes/auth.js @@ -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; diff --git a/server/routes/tasks.js b/server/routes/tasks.js new file mode 100644 index 0000000..679f74e --- /dev/null +++ b/server/routes/tasks.js @@ -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; diff --git a/src/App.tsx b/src/App.tsx index d97d3a1..76674eb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ -import { useState } from 'react'; -import { SEED_TASKS } from './data'; +import { useState, useEffect } from 'react'; +import { apiFetchTasks, apiFetchUsers, apiCreateTask, apiUpdateTask, apiAddActivity } from './api'; import type { Task, User, Status } from './data'; +import { STATUS_LABELS } from './data'; import { LoginPage } from './Login'; import { Sidebar } from './Sidebar'; import { TopNavbar, BottomToggleBar } from './NavBars'; @@ -24,7 +25,8 @@ const VIEW_PAGES = ['calendar', 'kanban', 'list']; export default function App() { const now = new Date(); const [currentUser, setCurrentUser] = useState(null); - const [tasks, setTasks] = useState(SEED_TASKS); + const [users, setUsers] = useState([]); + const [tasks, setTasks] = useState([]); const [activePage, setActivePage] = useState('calendar'); const [activeView, setActiveView] = useState('calendar'); const [activeTask, setActiveTask] = useState(null); @@ -36,6 +38,20 @@ export default function App() { const [searchQuery, setSearchQuery] = useState(''); const [sidebarOpen, setSidebarOpen] = useState(false); 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 { setCurrentUser(u); setActivePage('calendar'); setActiveView('calendar'); }} />; @@ -57,29 +73,91 @@ export default function App() { setQuickAddDay({ date, rect: { top: rect.bottom, left: rect.left } }); }; - const handleQuickAdd = (partial: Partial) => { - 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 handleQuickAdd = async (partial: Partial) => { + try { + const created = await apiCreateTask({ + title: partial.title || '', + description: partial.description || '', + status: partial.status || 'todo', + priority: partial.priority || 'medium', + assignee: partial.assignee || currentUser.id, + reporter: currentUser.id, + 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) => { - setTasks(prev => prev.map(t => t.id === updated.id ? updated : t)); - setActiveTask(updated); + const handleUpdateTask = async (updated: Task) => { + try { + 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 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 handleToggleDone = async (taskId: string) => { + 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; @@ -87,37 +165,45 @@ export default function App() { const pageTitle = PAGE_TITLES[displayPage] || 'Calendar'; + if (loading) { + return ( +
+

Loading...

+
+ ); + } + return (
setSidebarOpen(true)} /> + onOpenSidebar={() => setSidebarOpen(true)} users={users} />
{ setCurrentUser(null); setActivePage('calendar'); setActiveView('calendar'); setSidebarOpen(false); }} - isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} /> + isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} users={users} />
{displayPage === 'calendar' && ( + onDayClick={handleDayClick} filterUser={filterUser} searchQuery={searchQuery} users={users} /> )} {displayPage === 'kanban' && ( + onAddTask={handleKanbanAdd} onMoveTask={handleMoveTask} filterUser={filterUser} searchQuery={searchQuery} users={users} /> )} {displayPage === 'list' && ( + filterUser={filterUser} searchQuery={searchQuery} onToggleDone={handleToggleDone} users={users} /> )} - {displayPage === 'dashboard' && } + {displayPage === 'dashboard' && } {displayPage === 'mytasks' && ( + filterUser={null} searchQuery={searchQuery} onToggleDone={handleToggleDone} users={users} /> )} - {displayPage === 'teamtasks' && } - {displayPage === 'reports' && } - {displayPage === 'members' && } + {displayPage === 'teamtasks' && } + {displayPage === 'reports' && } + {displayPage === 'members' && }
@@ -125,8 +211,8 @@ export default function App() { )} - {activeTask && setActiveTask(null)} onUpdate={handleUpdateTask} />} - {showAddModal && setShowAddModal(false)} onAdd={handleAddTask} defaultDate={addModalDefaults.date} defaultStatus={addModalDefaults.status} />} + {activeTask && setActiveTask(null)} onUpdate={handleUpdateTask} users={users} />} + {showAddModal && setShowAddModal(false)} onAdd={handleAddTask} defaultDate={addModalDefaults.date} defaultStatus={addModalDefaults.status} users={users} currentUser={currentUser} />} {quickAddDay && (
setQuickAddDay(null)}> @@ -134,7 +220,7 @@ export default function App() { onClick={e => e.stopPropagation()}> { setAddModalDefaults({ date: quickAddDay.date }); setShowAddModal(true); setQuickAddDay(null); }} - onClose={() => setQuickAddDay(null)} /> + onClose={() => setQuickAddDay(null)} users={users} />
)} diff --git a/src/Calendar.tsx b/src/Calendar.tsx index 0c6ad06..0c8f2fb 100644 --- a/src/Calendar.tsx +++ b/src/Calendar.tsx @@ -1,13 +1,13 @@ import { useState } from 'react'; import type { Task, User } from './data'; -import { USERS, PRIORITY_COLORS } from './data'; +import { 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; + filterUser: string | null; searchQuery: string; users: User[]; } 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 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]; return (
{ e.stopPropagation(); onClick(); }}> {task.title} - +
); } -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 (
e.stopPropagation()}>
{tasks.length} tasks
- {tasks.map(t => { onTaskClick(t); onClose(); }} />)} + {tasks.map(t => { onTaskClick(t); onClose(); }} users={users} />)}
); } -export function QuickAddPanel({ date, onAdd, onOpenFull, onClose }: { date: string; onAdd: (t: Partial) => void; onOpenFull: () => void; onClose: () => void }) { +export function QuickAddPanel({ date, onAdd, onOpenFull, onClose, users }: { date: string; onAdd: (t: Partial) => void; onOpenFull: () => void; onClose: () => void; users: User[] }) { 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 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()} />
onToggleDone(t.id)} /> onTaskClick(t)} style={{ cursor: 'pointer' }}>{t.title} -
{u?.name}
+
{u?.name}
{due.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} diff --git a/src/Login.tsx b/src/Login.tsx index 2e6b34b..103a033 100644 --- a/src/Login.tsx +++ b/src/Login.tsx @@ -1,29 +1,70 @@ import { useState } from 'react'; -import { USERS } from './data'; import type { User } from './data'; +import { apiLogin, apiRegister } from './api'; export function LoginPage({ onLogin }: { onLogin: (u: User) => void }) { + const [mode, setMode] = useState<'login' | 'register'>('login'); + const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [pass, setPass] = useState(''); + const [role, setRole] = useState('employee'); + const [dept, setDept] = useState(''); const [showPass, setShowPass] = useState(false); const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); - const handleSubmit = (e: React.FormEvent) => { + const handleLogin = async (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'); } + setLoading(true); + setError(''); + 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 (
-
+
โšก
Scrum-manager

Your team's command center

+ + {mode === 'register' && ( + <> + +
+ { setName(e.target.value); setError(''); }} /> +
+ + )} +
void }) { value={pass} onChange={e => { setPass(e.target.value); setError(''); }} />
- + + {mode === 'register' && ( + <> + +
+ +
+ +
+ setDept(e.target.value)} /> +
+ + )} + + {error &&

{error}

} -
๐Ÿ’ก Try: subodh@corp.io / cto123
+
{ setMode(mode === 'login' ? 'register' : 'login'); setError(''); }}> + {mode === 'login' ? '๐Ÿ“ No account? Register here' : '๐Ÿ”‘ Already have an account? Sign in'} +
); diff --git a/src/NavBars.tsx b/src/NavBars.tsx index f7c5ecd..440f16f 100644 --- a/src/NavBars.tsx +++ b/src/NavBars.tsx @@ -1,5 +1,4 @@ -import { USERS } from './data'; - +import type { User } from './data'; interface TopNavbarProps { title: string; @@ -9,9 +8,10 @@ interface TopNavbarProps { onSearch: (q: string) => void; onNewTask: () => void; onOpenSidebar: () => void; + users: User[]; } -export function TopNavbar({ title, filterUser, onFilterChange, searchQuery, onSearch, onNewTask, onOpenSidebar }: TopNavbarProps) { +export function TopNavbar({ title, filterUser, onFilterChange, searchQuery, onSearch, onNewTask, onOpenSidebar, users }: TopNavbarProps) { return (
@@ -23,7 +23,7 @@ export function TopNavbar({ title, filterUser, onFilterChange, searchQuery, onSe
onFilterChange(null)}>All - {USERS.map(u => ( + {users.map(u => (
onFilterChange(u.id === filterUser ? null : u.id)}> diff --git a/src/Pages.tsx b/src/Pages.tsx index 805dbde..42ba0c0 100644 --- a/src/Pages.tsx +++ b/src/Pages.tsx @@ -1,22 +1,21 @@ import React, { useState } from 'react'; import type { Task, User } from './data'; -import { USERS, PRIORITY_COLORS } from './data'; +import { PRIORITY_COLORS } from './data'; 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>({}); - const members = USERS; return (

Team Tasks

- {members.map(m => { + {users.map(m => { const mTasks = tasks.filter(t => t.assignee === m.id); const isOpen = expanded[m.id] !== false; return (
setExpanded(e => ({ ...e, [m.id]: !isOpen }))}> - + {m.name} ({mTasks.length} tasks) {isOpen ? 'โ–ผ' : 'โ–ถ'} @@ -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(null); const [showInvite, setShowInvite] = useState(false); @@ -56,7 +55,7 @@ export function MembersPage({ tasks }: { tasks: Task[] }) { - {USERS.map(u => { + {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; @@ -64,7 +63,7 @@ export function MembersPage({ tasks }: { tasks: Task[] }) { return ( setExpanded(expanded === u.id ? null : u.id)}> - + @@ -110,4 +109,3 @@ export function MembersPage({ tasks }: { tasks: Task[] }) { ); } - diff --git a/src/Reports.tsx b/src/Reports.tsx index 7a352c3..0dd103b 100644 --- a/src/Reports.tsx +++ b/src/Reports.tsx @@ -1,5 +1,5 @@ -import type { Task } from './data'; -import { USERS, STATUS_COLORS, PRIORITY_COLORS } from './data'; +import type { Task, User } from './data'; +import { STATUS_COLORS, PRIORITY_COLORS } from './data'; import { BarChart, Bar, PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts'; const tooltipStyle = { @@ -8,14 +8,14 @@ const tooltipStyle = { labelStyle: { color: '#94a3b8' }, }; -export function ReportsPage({ tasks }: { tasks: Task[] }) { +export function ReportsPage({ tasks, users }: { tasks: Task[]; users: 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; // 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); return { 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] })); // Overdue by member - const overdueData = USERS.map(u => ({ + 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); diff --git a/src/Shared.tsx b/src/Shared.tsx index ce2821c..b2ec3f9 100644 --- a/src/Shared.tsx +++ b/src/Shared.tsx @@ -1,8 +1,9 @@ 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 }) { - const u = getUserById(userId); +export function Avatar({ userId, size = 28, users }: { userId: string; size?: number; users: User[] }) { + if (!users || !users.length) return null; + const u = getUserById(users, userId); if (!u) return null; return (
diff --git a/src/Sidebar.tsx b/src/Sidebar.tsx index 25de40e..5c2f8c2 100644 --- a/src/Sidebar.tsx +++ b/src/Sidebar.tsx @@ -19,9 +19,10 @@ interface SidebarProps { onSignOut: () => void; isOpen: boolean; onClose: () => void; + users: User[]; } -export function Sidebar({ currentUser, activePage, onNavigate, onSignOut, isOpen, onClose }: SidebarProps) { +export function Sidebar({ currentUser, activePage, onNavigate, onSignOut, isOpen, onClose, users }: SidebarProps) { const filteredNav = NAV_ITEMS.filter(n => n.roles.includes(currentUser.role)); return ( <> @@ -42,7 +43,7 @@ export function Sidebar({ currentUser, activePage, onNavigate, onSignOut, isOpen ))}
- +
{currentUser.name}
@@ -55,4 +56,3 @@ export function Sidebar({ currentUser, activePage, onNavigate, onSignOut, isOpen ); } - diff --git a/src/TaskDrawer.tsx b/src/TaskDrawer.tsx index 6c86949..b8c381e 100644 --- a/src/TaskDrawer.tsx +++ b/src/TaskDrawer.tsx @@ -1,14 +1,15 @@ import { useState } from 'react'; 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'; interface DrawerProps { task: Task; currentUser: User; onClose: () => 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 [subtaskText, setSubtaskText] = useState(''); @@ -48,7 +49,7 @@ export function TaskDrawer({ task, currentUser, onClose, onUpdate }: DrawerProps setCommentText(''); }; - const reporter = getUserById(task.reporter); + const reporter = getUserById(users, task.reporter); const doneCount = task.subtasks.filter(s => s.done).length; return ( @@ -67,15 +68,15 @@ export function TaskDrawer({ task, currentUser, onClose, onUpdate }: DrawerProps
Assignee
- +
Reporter
-
{reporter?.name}
+
{reporter?.name}
Status
@@ -117,10 +118,10 @@ export function TaskDrawer({ task, currentUser, onClose, onUpdate }: DrawerProps
Comments
{task.comments.map(c => { - const cu = getUserById(c.userId); + const cu = getUserById(users, c.userId); return (
- +
{cu?.name} @@ -132,7 +133,7 @@ export function TaskDrawer({ task, currentUser, onClose, onUpdate }: DrawerProps ); })}
- + setCommentText(e.target.value)} onKeyDown={e => e.key === 'Enter' && addComment()} />
@@ -157,12 +158,14 @@ interface ModalProps { onAdd: (task: Task) => void; defaultDate?: string; 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 [desc, setDesc] = useState(''); - const [assignee, setAssignee] = useState('u1'); + const [assignee, setAssignee] = useState(users[0]?.id || ''); const [priority, setPriority] = useState('medium'); const [status, setStatus] = useState(defaultStatus || 'todo'); 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; } const task: Task = { 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) : [], subtasks: [], comments: [], activity: [{ id: `a${Date.now()}`, text: `๐Ÿ“ Task created`, timestamp: new Date().toISOString() }], @@ -199,7 +202,7 @@ export function AddTaskModal({ onClose, onAdd, defaultDate, defaultStatus }: Mod
diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..d1f5a97 --- /dev/null +++ b/src/api.ts @@ -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) { + 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 }), + }); +} diff --git a/src/data.ts b/src/data.ts index 7558c41..ed4222c 100644 --- a/src/data.ts +++ b/src/data.ts @@ -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 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 Comment { id: string; userId: string; text: string; timestamp: string } export interface Activity { id: string; text: string; timestamp: string } @@ -41,77 +30,4 @@ export const STATUS_LABELS: Record = { 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); } +export function getUserById(users: User[], id: string) { return users.find(u => u.id === id); } diff --git a/src/index.css b/src/index.css index 76eafdb..2b2acd3 100644 --- a/src/index.css +++ b/src/index.css @@ -1023,6 +1023,34 @@ body { text-align: center; color: var(--text-muted); font-size: 12px; + transition: all 0.2s; +} + +/* DRAG AND DROP */ +.kanban-column-dragover { + background: rgba(99, 102, 241, 0.08); + border-color: var(--accent); + box-shadow: inset 0 0 0 1px var(--accent), 0 0 20px rgba(99, 102, 241, 0.15); +} + +.kanban-column-dragover .kanban-empty { + border-color: var(--accent); + color: var(--accent); + background: rgba(99, 102, 241, 0.06); +} + +.task-card.dragging { + opacity: 0.4; + transform: scale(0.95); + box-shadow: 0 8px 32px rgba(99, 102, 241, 0.3); +} + +.task-card[draggable="true"] { + cursor: grab; +} + +.task-card[draggable="true"]:active { + cursor: grabbing; } /* TASK CARD */ diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..d692594 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,4 +4,13 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + host: '0.0.0.0', + proxy: { + '/api': { + target: 'http://backend:3001', + changeOrigin: true, + }, + }, + }, })
AvatarFull NameRoleDeptAssignedDoneActive
{u.name} {u.role.toUpperCase()} {u.dept}