feat: Add Time Tracking page with analytics and time distribution charts
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Add TimeTrackingPage with pie/bar charts for time spent per label - Add task_time_logs schema for tracking time entries - Add /api/analytics/time-distribution endpoint - Update sidebar navigation with time tracking link - Update App routing for new pages - Fix routine blocker and settings card issues - Add German translations for analytics Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+42
-40
@@ -95,8 +95,11 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
await storage.setSystemSettings("smtp_pass", "");
|
||||
await storage.setSystemSettings("smtp_from", "noreply@example.com");
|
||||
await storage.setSystemSettings("smtp_secure", "false");
|
||||
// Ensure daily routine time is fixed to 22:00
|
||||
await storage.setSystemSettings("evening_routine_time", "22:00");
|
||||
await storage.setSystemSettings("morning_routine_time", "09:00");
|
||||
// Also mark setup as NOT completed if no admin exists, or just ensure registration is open
|
||||
res.json({ message: "Settings fixed, registration enabled" });
|
||||
res.json({ message: "Settings fixed, registration enabled, routine times reset" });
|
||||
});
|
||||
|
||||
app.post("/api/debug/force-enable-registration", async (req, res) => {
|
||||
@@ -927,43 +930,7 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
}
|
||||
});
|
||||
|
||||
app.patch("/api/tasks/:id", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
// Validate request body against schema
|
||||
const cleanBody = insertTaskSchema.partial().safeParse(req.body);
|
||||
if (!cleanBody.success) {
|
||||
return res.status(400).json({ error: cleanBody.error });
|
||||
}
|
||||
|
||||
try {
|
||||
const { hasAccess, task } = await checkTaskAccess(req.user as User, req.params.id, 'write');
|
||||
if (!task) return res.status(404).json({ error: "Task not found" });
|
||||
if (!hasAccess) return res.status(403).json({ error: "Access denied" });
|
||||
|
||||
const updatedTask = await storage.updateTask(task.id, cleanBody.data);
|
||||
|
||||
if (cleanBody.data.status === 'done' && task.status !== 'done') {
|
||||
if (req.user) {
|
||||
// Check if late
|
||||
const now = new Date();
|
||||
const isLate = task.dueDate && new Date(task.dueDate) < now;
|
||||
const xpSource = isLate ? "complete_task_late" : "complete_task";
|
||||
|
||||
// Award XP for completion with Task Title context
|
||||
await gamificationService.awardXP(
|
||||
(req.user as User).id,
|
||||
xpSource,
|
||||
undefined,
|
||||
{ taskId: task.id, taskTitle: task.title }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
res.json(updatedTask);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Failed to update task" });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch("/api/labels/:id", async (req, res) => {
|
||||
try {
|
||||
@@ -1226,7 +1193,13 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
app.patch("/api/tasks/:id", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const previousTask = await storage.getTask(req.params.id);
|
||||
// 1. Check Access (Security Fix + Fetch)
|
||||
// We use checkTaskAccess to ensure the user has write permissions (ownership or shared write access)
|
||||
const { hasAccess, task: previousTask } = await checkTaskAccess(req.user as User, req.params.id, 'write');
|
||||
|
||||
if (!previousTask) return res.status(404).json({ error: "Task not found" });
|
||||
if (!hasAccess) return res.status(403).json({ error: "Access denied" });
|
||||
|
||||
const updates = insertTaskSchema.partial().safeParse(req.body);
|
||||
|
||||
if (!updates.success) {
|
||||
@@ -1234,12 +1207,12 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
}
|
||||
|
||||
// Award XP using GamificationService
|
||||
if (req.user && previousTask) { // Ensure previousTask exists for comparison
|
||||
if (req.user) {
|
||||
if (updates.data.status === 'done' && previousTask.status !== 'done') {
|
||||
const isLate = previousTask.dueDate && new Date(previousTask.dueDate) < new Date();
|
||||
const source = isLate ? 'complete_task_late' : 'complete_task';
|
||||
await gamificationService.awardXP((req.user as User).id, source, undefined, { taskId: previousTask.id, taskTitle: previousTask.title });
|
||||
} else if (Object.keys(updates.data).length > 0) { // Only award if there are actual updates
|
||||
} else if (Object.keys(updates.data).length > 0) {
|
||||
// Small points for any other update (title, description, etc)
|
||||
await gamificationService.awardXP((req.user as User).id, 'update_task', undefined, { taskId: previousTask.id, taskTitle: previousTask.title });
|
||||
}
|
||||
@@ -1250,6 +1223,17 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
return res.status(404).json({ error: "Task not found" });
|
||||
}
|
||||
|
||||
// 2. Time Tracking Logic
|
||||
// If timeTracked has increased, log the delta
|
||||
if (updates.data.timeTracked && updatedTask.timeTracked > previousTask.timeTracked) {
|
||||
const delta = updatedTask.timeTracked - previousTask.timeTracked;
|
||||
await storage.logTaskTime({
|
||||
taskId: updatedTask.id,
|
||||
userId: (req.user as User).id,
|
||||
timeSpent: delta
|
||||
});
|
||||
}
|
||||
|
||||
await storage.createAuditLog({
|
||||
userId: (req.user as User).id,
|
||||
action: "UPDATE",
|
||||
@@ -1266,6 +1250,24 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
}
|
||||
});
|
||||
|
||||
// Time Analytics Endpoint
|
||||
app.get("/api/analytics/time-distribution", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
|
||||
const period = req.query.period as 'day' | 'week' | 'month' | 'year';
|
||||
if (!['day', 'week', 'month', 'year'].includes(period)) {
|
||||
return res.status(400).json({ error: "Invalid period. Must be day, week, month, or year." });
|
||||
}
|
||||
|
||||
try {
|
||||
const distribution = await storage.getAnalyticsTimeDistribution((req.user as User).id, period);
|
||||
res.json(distribution);
|
||||
} catch (error) {
|
||||
console.error("Analytics Error:", error);
|
||||
res.status(500).json({ error: "Failed to fetch time distribution" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/tasks/:id", async (req, res) => {
|
||||
try {
|
||||
const success = await storage.deleteTask(req.params.id);
|
||||
|
||||
Reference in New Issue
Block a user