From 6aec1445e987ebaf32bb1c30bc0ba68633f887fc Mon Sep 17 00:00:00 2001 From: tusuii Date: Mon, 16 Feb 2026 13:26:36 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20data=20export=20=E2=80=94=20CSV=20expor?= =?UTF-8?q?t=20for=20tasks,=20users,=20activities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: GET /api/export/{tasks,users,activities}?month=YYYY-MM - Frontend: Export panel on Reports page (CEO/CTO/Manager only) - API: apiExportCsv helper for browser download --- server/index.js | 2 + server/routes/export.js | 131 ++++++++++++++++++++++++++++++++++++++++ src/App.tsx | 2 +- src/Reports.tsx | 47 +++++++++++++- src/api.ts | 15 +++++ 5 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 server/routes/export.js diff --git a/server/index.js b/server/index.js index b377cc9..80372ca 100644 --- a/server/index.js +++ b/server/index.js @@ -3,6 +3,7 @@ import cors from 'cors'; import { initDB } from './db.js'; import authRoutes from './routes/auth.js'; import taskRoutes from './routes/tasks.js'; +import exportRoutes from './routes/export.js'; const app = express(); const PORT = process.env.PORT || 3001; @@ -13,6 +14,7 @@ app.use(express.json()); // Routes app.use('/api/auth', authRoutes); app.use('/api/tasks', taskRoutes); +app.use('/api/export', exportRoutes); // Health check app.get('/api/health', (_req, res) => { diff --git a/server/routes/export.js b/server/routes/export.js new file mode 100644 index 0000000..7f542b5 --- /dev/null +++ b/server/routes/export.js @@ -0,0 +1,131 @@ +import { Router } from 'express'; +import pool from '../db.js'; + +const router = Router(); + +// Helper: escape CSV field +function csvEscape(val) { + if (val == null) return ''; + const str = String(val); + if (str.includes(',') || str.includes('"') || str.includes('\n')) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; +} + +// Helper: convert rows to CSV string +function toCsv(headers, rows) { + const headerLine = headers.map(csvEscape).join(','); + const dataLines = rows.map(row => headers.map(h => csvEscape(row[h])).join(',')); + return [headerLine, ...dataLines].join('\n'); +} + +// Helper: build month filter clause for a date column +function monthFilter(column, month) { + if (!month || !/^\d{4}-\d{2}$/.test(month)) return { clause: '', params: [] }; + const [year, mon] = month.split('-'); + const start = `${year}-${mon}-01`; + // Last day of month + const nextMonth = parseInt(mon) === 12 ? `${parseInt(year) + 1}-01-01` : `${year}-${String(parseInt(mon) + 1).padStart(2, '0')}-01`; + return { clause: ` AND ${column} >= ? AND ${column} < ?`, params: [start, nextMonth] }; +} + +// GET /api/export/tasks?month=YYYY-MM +router.get('/tasks', async (req, res) => { + try { + const { month } = req.query; + const mf = monthFilter('t.due_date', month); + + const [rows] = await pool.query(` + SELECT t.id, t.title, t.description, t.status, t.priority, + t.due_date, t.created_at, + a.name AS assignee_name, a.email AS assignee_email, + r.name AS reporter_name, + GROUP_CONCAT(tt.tag SEPARATOR '; ') AS tags + FROM tasks t + LEFT JOIN users a ON t.assignee_id = a.id + LEFT JOIN users r ON t.reporter_id = r.id + LEFT JOIN task_tags tt ON tt.task_id = t.id + WHERE 1=1 ${mf.clause} + GROUP BY t.id + ORDER BY t.created_at DESC + `, mf.params); + + const csv = toCsv( + ['id', 'title', 'description', 'status', 'priority', 'due_date', 'created_at', 'assignee_name', 'assignee_email', 'reporter_name', 'tags'], + rows + ); + + const filename = month ? `tasks_${month}.csv` : 'tasks_all.csv'; + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); + res.send(csv); + } catch (err) { + console.error('Export tasks error:', err); + res.status(500).json({ error: 'Export failed' }); + } +}); + +// GET /api/export/users?month=YYYY-MM +router.get('/users', async (req, res) => { + try { + const { month } = req.query; + const mf = monthFilter('t.due_date', month); + + const [rows] = await pool.query(` + SELECT u.id, u.name, u.email, u.role, u.dept, + COUNT(t.id) AS total_tasks, + SUM(CASE WHEN t.status = 'done' THEN 1 ELSE 0 END) AS completed_tasks, + SUM(CASE WHEN t.status != 'done' AND t.due_date < CURDATE() THEN 1 ELSE 0 END) AS overdue_tasks + FROM users u + LEFT JOIN tasks t ON t.assignee_id = u.id ${mf.clause ? 'AND' + mf.clause.replace(' AND', '') : ''} + GROUP BY u.id + ORDER BY u.name + `, mf.params); + + const csv = toCsv( + ['id', 'name', 'email', 'role', 'dept', 'total_tasks', 'completed_tasks', 'overdue_tasks'], + rows + ); + + const filename = month ? `users_${month}.csv` : 'users_all.csv'; + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); + res.send(csv); + } catch (err) { + console.error('Export users error:', err); + res.status(500).json({ error: 'Export failed' }); + } +}); + +// GET /api/export/activities?month=YYYY-MM +router.get('/activities', async (req, res) => { + try { + const { month } = req.query; + const mf = monthFilter('a.timestamp', month); + + const [rows] = await pool.query(` + SELECT a.id, a.text AS activity, a.timestamp, + t.title AS task_title, t.status AS task_status + FROM activities a + LEFT JOIN tasks t ON a.task_id = t.id + WHERE 1=1 ${mf.clause} + ORDER BY a.timestamp DESC + `, mf.params); + + const csv = toCsv( + ['id', 'activity', 'timestamp', 'task_title', 'task_status'], + rows + ); + + const filename = month ? `activities_${month}.csv` : 'activities_all.csv'; + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); + res.send(csv); + } catch (err) { + console.error('Export activities error:', err); + res.status(500).json({ error: 'Export failed' }); + } +}); + +export default router; diff --git a/src/App.tsx b/src/App.tsx index 9bce370..bb80aa9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -240,7 +240,7 @@ export default function App() { filterUser={null} searchQuery={searchQuery} onToggleDone={handleToggleDone} users={users} /> )} {displayPage === 'teamtasks' && } - {displayPage === 'reports' && } + {displayPage === 'reports' && } {displayPage === 'members' && } diff --git a/src/Reports.tsx b/src/Reports.tsx index 0dd103b..b300ccc 100644 --- a/src/Reports.tsx +++ b/src/Reports.tsx @@ -1,5 +1,7 @@ +import { useState } from 'react'; import type { Task, User } from './data'; import { STATUS_COLORS, PRIORITY_COLORS } from './data'; +import { apiExportCsv } from './api'; import { BarChart, Bar, PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts'; const tooltipStyle = { @@ -8,7 +10,12 @@ const tooltipStyle = { labelStyle: { color: '#94a3b8' }, }; -export function ReportsPage({ tasks, users }: { tasks: Task[]; users: User[] }) { +export function ReportsPage({ tasks, users, currentUser }: { tasks: Task[]; users: User[]; currentUser: User }) { + const [exportType, setExportType] = useState<'tasks' | 'users' | 'activities'>('tasks'); + const [exportMonth, setExportMonth] = useState(''); + const [exporting, setExporting] = useState(false); + + const canExport = ['ceo', 'cto', 'manager'].includes(currentUser.role); 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; @@ -113,6 +120,44 @@ export function ReportsPage({ tasks, users }: { tasks: Task[]; users: User[] }) + + {canExport && ( +
+
+ 📥 Export Data +
+
+
+ + +
+
+ + setExportMonth(e.target.value)} /> +
+ +
+
+ )} ); } diff --git a/src/api.ts b/src/api.ts index f2e73f8..6dc595b 100644 --- a/src/api.ts +++ b/src/api.ts @@ -48,6 +48,21 @@ export async function apiDeleteUser(id: string) { }); } +export async function apiExportCsv(type: 'tasks' | 'users' | 'activities', month?: string) { + const params = month ? `?month=${month}` : ''; + const res = await fetch(`${API_BASE}/export/${type}${params}`); + if (!res.ok) throw new Error('Export failed'); + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = month ? `${type}_${month}.csv` : `${type}_all.csv`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + // Tasks export async function apiFetchTasks() { return request('/tasks');