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' &&