o134's picture
Upload full TeamTasker system with all fixes
8314cf4 verified
Raw
History Blame Contribute Delete
11.3 kB
import { Router } from "express";
import { db } from "@workspace/db";
import { tasksTable, clientsTable } from "@workspace/db";
import { sql, eq, and, isNotNull, lt, ne, desc, gte, count, inArray } from "drizzle-orm";
import { GetRecentActivityQueryParams } from "@workspace/api-zod";
import { AuthRequest } from "../middleware/auth.js";
import { getTaskVisibilityFilter, User, isAdmin, canAccessAllTeams } from "../lib/auth-utils.js";
const router = Router();
interface CacheEntry<T> {
data: T;
timestamp: number;
}
const cache = new Map<string, CacheEntry<any>>();
const CACHE_TTL_MS = 60 * 1000; // 1 minute TTL
export function invalidateDashboardCache() {
cache.clear();
}
function getCached<T>(key: string): T | null {
const entry = cache.get(key);
if (!entry) return null;
if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
cache.delete(key);
return null;
}
return entry.data;
}
function setCached<T>(key: string, data: T): void {
cache.set(key, { data, timestamp: Date.now() });
}
function serializeTask(task: typeof tasksTable.$inferSelect, extras: Record<string, unknown> = {}) {
return {
...task,
dueDate: task.dueDate ? task.dueDate.toISOString() : null,
completedAt: task.completedAt ? task.completedAt.toISOString() : null,
createdAt: task.createdAt.toISOString(),
updatedAt: task.updatedAt.toISOString(),
...extras,
};
}
router.get("/dashboard/stats", async (req: AuthRequest, res) => {
try {
const user = req.user as User;
const cacheKey = `stats_${user.id}`;
const cachedData = getCached(cacheKey);
if (cachedData) {
res.json(cachedData);
return;
}
const visibilityFilter = getTaskVisibilityFilter(user);
const now = new Date();
const [total, notStarted, inProgress, inReview, completed, overdue] = await Promise.all([
db.select({ cnt: count() }).from(tasksTable).where(visibilityFilter).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(and(eq(tasksTable.status, "not_started"), visibilityFilter)).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(and(eq(tasksTable.status, "in_progress"), visibilityFilter)).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(and(eq(tasksTable.status, "review"), visibilityFilter)).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(and(eq(tasksTable.status, "completed"), visibilityFilter)).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(
and(isNotNull(tasksTable.dueDate), lt(tasksTable.dueDate, now), ne(tasksTable.status, "completed"), visibilityFilter)
).then(r => Number(r[0].cnt)),
]);
const result = {
totalTasks: total,
notStarted,
inProgress,
inReview,
completed,
overdue,
completionRate: total > 0 ? Math.round((completed / total) * 100) : 0,
};
setCached(cacheKey, result);
res.json(result);
} catch (err) {
req.log.error({ err }, "Failed to get dashboard stats");
res.status(500).json({ error: "Internal server error" });
}
});
router.get("/dashboard/team-stats", async (req: AuthRequest, res) => {
try {
const user = req.user as User;
const cacheKey = `team_stats_${user.id}`;
const cachedData = getCached(cacheKey);
if (cachedData) {
res.json(cachedData);
return;
}
const now = new Date();
let teams = ["Account Manager", "Creative", "SEO Specialist", "Media Buying", "Social Media", "Operation"];
// Only Admins and Head of Team see all teams' stats
if (!canAccessAllTeams(user)) {
teams = teams.filter(t => t === user.team);
}
const statsPromises = teams.map(async (team) => {
const [total, notStarted, inProgress, completed, overdue] = await Promise.all([
db.select({ cnt: count() }).from(tasksTable).where(eq(tasksTable.assignedTeam, team)).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(and(eq(tasksTable.assignedTeam, team), eq(tasksTable.status, "not_started"))).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(and(eq(tasksTable.assignedTeam, team), eq(tasksTable.status, "in_progress"))).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(and(eq(tasksTable.assignedTeam, team), eq(tasksTable.status, "completed"))).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(
and(eq(tasksTable.assignedTeam, team), isNotNull(tasksTable.dueDate), lt(tasksTable.dueDate, now), ne(tasksTable.status, "completed"))
).then(r => Number(r[0].cnt)),
]);
return { team, totalTasks: total, notStarted, inProgress, completed, overdue };
});
const stats = await Promise.all(statsPromises);
setCached(cacheKey, stats);
res.json(stats);
} catch (err) {
req.log.error({ err }, "Failed to get team stats");
res.status(500).json({ error: "Internal server error" });
}
});
router.get("/dashboard/overdue-tasks", async (req: AuthRequest, res) => {
try {
const user = req.user as User;
const visibilityFilter = getTaskVisibilityFilter(user);
const now = new Date();
const tasks = await db.select().from(tasksTable).where(
and(isNotNull(tasksTable.dueDate), lt(tasksTable.dueDate, now), ne(tasksTable.status, "completed"), visibilityFilter)
).orderBy(tasksTable.dueDate);
res.json(tasks.map(t => serializeTask(t, {
createdByUser: null,
assignedUser: null,
commentCount: 0,
})));
} catch (err) {
req.log.error({ err }, "Failed to get overdue tasks");
res.status(500).json({ error: "Internal server error" });
}
});
router.get("/dashboard/recent-activity", async (req: AuthRequest, res) => {
try {
const user = req.user as User;
const visibilityFilter = getTaskVisibilityFilter(user);
const params = GetRecentActivityQueryParams.parse(req.query);
const limit = params.limit ? Number(params.limit) : 20;
const tasks = await db.select().from(tasksTable)
.where(visibilityFilter)
.orderBy(desc(tasksTable.updatedAt))
.limit(limit);
const activity = tasks.map((t, i) => ({
id: i + 1,
taskId: t.id,
taskTitle: t.title,
action: t.status === "completed" ? "completed" : t.status === "in_progress" ? "started" : "created",
fromTeam: t.fromTeam,
assignedTeam: t.assignedTeam,
status: t.status,
createdAt: t.updatedAt.toISOString(),
}));
res.json(activity);
} catch (err) {
req.log.error({ err }, "Failed to get recent activity");
res.status(500).json({ error: "Internal server error" });
}
});
router.get("/dashboard/analytics", async (req: AuthRequest, res) => {
try {
const user = req.user as User;
const cacheKey = `analytics_${user.id}`;
const cachedData = getCached(cacheKey);
if (cachedData) {
res.json(cachedData);
return;
}
const visibilityFilter = getTaskVisibilityFilter(user);
const now = new Date();
const startOfThisWeek = new Date(now);
startOfThisWeek.setDate(now.getDate() - now.getDay());
startOfThisWeek.setHours(0, 0, 0, 0);
const startOfLastWeek = new Date(startOfThisWeek);
startOfLastWeek.setDate(startOfLastWeek.getDate() - 7);
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const weekly = await Promise.all(days.map(async (day, i) => {
const thisStart = new Date(startOfThisWeek);
thisStart.setDate(thisStart.getDate() + i);
const thisEnd = new Date(thisStart);
thisEnd.setDate(thisEnd.getDate() + 1);
const lastStart = new Date(startOfLastWeek);
lastStart.setDate(lastStart.getDate() + i);
const lastEnd = new Date(lastStart);
lastEnd.setDate(lastEnd.getDate() + 1);
const [thisWeekRow] = await db.select({ cnt: count() }).from(tasksTable)
.where(and(gte(tasksTable.createdAt, thisStart), lt(tasksTable.createdAt, thisEnd), visibilityFilter));
const [lastWeekRow] = await db.select({ cnt: count() }).from(tasksTable)
.where(and(gte(tasksTable.createdAt, lastStart), lt(tasksTable.createdAt, lastEnd), visibilityFilter));
return { day, thisWeek: Number(thisWeekRow.cnt), lastWeek: Number(lastWeekRow.cnt) };
}));
let teams = ["Account Manager", "Creative", "SEO Specialist", "Media Buying", "Social Media", "Operation"];
if (!canAccessAllTeams(user)) {
teams = teams.filter(t => t === user.team);
}
const byTeam = await Promise.all(teams.map(async (team) => {
const [total, notStarted, inProgress, completed, overdue] = await Promise.all([
db.select({ cnt: count() }).from(tasksTable).where(and(eq(tasksTable.assignedTeam, team), visibilityFilter)).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(and(eq(tasksTable.assignedTeam, team), eq(tasksTable.status, "not_started"), visibilityFilter)).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(and(eq(tasksTable.assignedTeam, team), eq(tasksTable.status, "in_progress"), visibilityFilter)).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(and(eq(tasksTable.assignedTeam, team), eq(tasksTable.status, "completed"), visibilityFilter)).then(r => Number(r[0].cnt)),
db.select({ cnt: count() }).from(tasksTable).where(and(eq(tasksTable.assignedTeam, team), isNotNull(tasksTable.dueDate), lt(tasksTable.dueDate, now), ne(tasksTable.status, "completed"), visibilityFilter)).then(r => Number(r[0].cnt)),
]);
return { team, totalTasks: total, notStarted, inProgress, completed, overdue };
}));
const clientTaskCounts = await db.select({
clientId: tasksTable.clientId,
total: count(),
}).from(tasksTable)
.where(and(isNotNull(tasksTable.clientId), visibilityFilter))
.groupBy(tasksTable.clientId)
.orderBy(desc(count()))
.limit(5);
const topClientIds = clientTaskCounts.map(r => r.clientId).filter(Boolean) as number[];
const topClientRows = topClientIds.length > 0
? await db.select({ id: clientsTable.id, name: clientsTable.name }).from(clientsTable).where(inArray(clientsTable.id, topClientIds))
: [];
const clientNameMap = Object.fromEntries(topClientRows.map(c => [c.id, c.name]));
const topClients = await Promise.all(clientTaskCounts.map(async (row) => {
if (!row.clientId) return null;
const [{ cnt: completedCnt }] = await db.select({ cnt: count() }).from(tasksTable)
.where(and(eq(tasksTable.clientId, row.clientId), eq(tasksTable.status, "completed"), visibilityFilter));
return {
id: row.clientId,
name: clientNameMap[row.clientId] ?? "Unknown",
totalTasks: Number(row.total),
completedTasks: Number(completedCnt),
};
}));
const result = {
weekly,
byTeam,
topClients: topClients.filter(Boolean),
};
setCached(cacheKey, result);
res.json(result);
} catch (err) {
req.log.error({ err }, "Failed to get analytics");
res.status(500).json({ error: "Internal server error" });
}
});
export default router;