diff --git a/client/public/favicon.png b/client/public/favicon.png
index 836d181..d88943c 100644
Binary files a/client/public/favicon.png and b/client/public/favicon.png differ
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 4d6ea23..aa29cfe 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -50,6 +50,8 @@ import AiChatPage from "@/pages/AiChatPage";
import FocusRoutinePage from "@/pages/FocusRoutinePage";
+import { useRoutineBlocker } from './components/RoutineBlocker';
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { User } from "@shared/schema";
import { Loader2 } from "lucide-react";
@@ -58,6 +60,7 @@ import { Loader2 } from "lucide-react";
function App() {
const { t } = useTranslation();
const [, setLocation] = useLocation();
+ const isBlocked = useRoutineBlocker();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isTaskDetailsOpen, setIsTaskDetailsOpen] = useState(false);
@@ -263,207 +266,231 @@ function App() {
}
if (!user) {
+ // Redirect to setup if no admin user exists
+ if (setupStatus && !setupStatus.isSetup) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (isBlocked) {
+ return (
+
+
+
+ {/* Catch all redirects to nothing, the hook handles the push to routine page */}
+ } />
+
+
+
+ );
+ }
+
return (
-
-
-
-
-
+
+
+
+
+
+ {user && (
+
+ )}
+
+
+
+
+ setLocation('/tasks')}
+ />
+
+
+
+
+
+ console.log('Date selected:', date.toLocaleDateString())}
+ onTaskUpdate={handleTaskUpdate}
+ onTaskEdit={handleTaskClick}
+ onTaskDelete={handleTaskDelete}
+ onStartTimer={startTimer}
+ onStopTimer={stopTimer}
+ />
+
+
+
+
+
+
+
+
+ {
+ queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]);
+ queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
+ }}
+ onNavigateToSettings={() => setLocation('/settings')}
+ />
+
+
+ {user ? : }
+
+
+
+ {user ? (
+ handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')}
+ onDelete={(id) => setDeleteTaskId(id)}
+ onUpdate={handleTaskUpdate}
+ onSelect={setSelectedTask}
+ />
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ setLocation('/templates')} />
+
+
+ {/* Admin Route */}
+ {user.role === 'admin' && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+
+ {/* Floating Action Button */}
+
+
+
+
setLocation(path.startsWith('/') ? path : `/${path}`)}
+ onCreateTask={() => setIsCreateModalOpen(true)}
+ />
+
+ setShowPomodoro(false)}
+ taskId={activePomodoroTaskId}
+ taskTitle={tasks.find(t => t.id === activePomodoroTaskId)?.title || 'Quick Focus'}
+ onCompleteTask={(id) => handleTaskUpdate(id, { status: 'done', isTracking: false })}
+ />
+
+
+
+ setIsCreateModalOpen(false)}
+ onSave={handleCreateTask}
+ />
+
+ {
+ const t = tasks.find(x => x.id === id);
+ if (t) setSelectedTask(t);
+ }}
+ />
+
+ !open && setDeleteTaskId(null)}>
+
+
+ {t('deleteConfirmation.title')}
+
+ {t('deleteConfirmation.description')}
+
+
+
+
+ {t('deleteConfirmation.cancel')}
+
+
+ {t('deleteConfirmation.confirm')}
+
+
+
+
+
+
+
+
);
}
-
- return (
-
-
-
-
-
- {/* Mobile Header trigger */}
-
-
- {/* Main Content */}
-
-
-
- setLocation('/tasks')}
- />
-
-
-
-
-
- console.log('Date selected:', date.toLocaleDateString())}
- onTaskUpdate={handleTaskUpdate}
- onTaskEdit={handleTaskClick}
- onTaskDelete={handleTaskDelete}
- onStartTimer={startTimer}
- onStopTimer={stopTimer}
- />
-
-
-
-
-
-
-
-
- {
- queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]);
- queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
- }}
- onNavigateToSettings={() => setLocation('/settings')}
- />
-
-
- {user ? : }
-
-
-
- {user ? (
- handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')}
- onDelete={(id) => setDeleteTaskId(id)}
- onUpdate={handleTaskUpdate}
- onSelect={setSelectedTask}
- />
- ) : (
-
- )}
-
-
-
-
-
-
-
-
- setLocation('/templates')} />
-
-
- {/* Admin Route */}
- {user.role === 'admin' && (
- <>
-
-
- >
- )}
-
-
-
-
-
-
- {/* Floating Action Button */}
-
-
-
-
-
-
-
- setLocation(path.startsWith('/') ? path : `/${path}`)}
- onCreateTask={() => setIsCreateModalOpen(true)}
- />
-
- setShowPomodoro(false)}
- taskId={activePomodoroTaskId}
- taskTitle={tasks.find(t => t.id === activePomodoroTaskId)?.title || 'Quick Focus'}
- onCompleteTask={(id) => handleTaskUpdate(id, { status: 'done', isTracking: false })}
- />
-
-
-
- setIsCreateModalOpen(false)}
- onSave={handleCreateTask}
- />
-
- {
- const t = tasks.find(x => x.id === id);
- if (t) setSelectedTask(t);
- }}
- />
-
- !open && setDeleteTaskId(null)}>
-
-
- {t('deleteConfirmation.title')}
-
- {t('deleteConfirmation.description')}
-
-
-
-
- {t('deleteConfirmation.cancel')}
-
-
- {t('deleteConfirmation.confirm')}
-
-
-
-
-
- );
}
export default App;
+
diff --git a/client/src/components/GamificationBar.tsx b/client/src/components/GamificationBar.tsx
index ed471a2..228c835 100644
--- a/client/src/components/GamificationBar.tsx
+++ b/client/src/components/GamificationBar.tsx
@@ -1,5 +1,6 @@
import { Trophy, Flame } from 'lucide-react';
import { Progress } from "@/components/ui/progress";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import {
@@ -46,12 +47,18 @@ export function GamificationBar({ xp, streak }: GamificationBarProps) {
-
-
-
- {t('gamification.streak', { count: streak })}
-
-
+
+
+
+
+ {streak}
+
+
+
+ {t('gamification.streakTooltip', 'Log in daily to increase your streak!')}
+ {t('gamification.streakBonus', 'Weekly & Monthly bonuses available.')}
+
+
diff --git a/client/src/components/RoutineBlocker.tsx b/client/src/components/RoutineBlocker.tsx
new file mode 100644
index 0000000..7214275
--- /dev/null
+++ b/client/src/components/RoutineBlocker.tsx
@@ -0,0 +1,128 @@
+import { useEffect } from "react";
+import { useLocation } from "wouter";
+import { useQuery } from "@tanstack/react-query";
+import { User } from "@shared/schema";
+
+export function useRoutineBlocker() {
+ const [, setLocation] = useLocation();
+ const { data: user } = useQuery
({ queryKey: ["/api/user"] });
+ const { data: settings } = useQuery>({ queryKey: ["/api/admin/settings"] });
+
+ useEffect(() => {
+ if (!user || !settings) return;
+
+ const checkRoutine = () => {
+ const now = new Date();
+ const currentHours = now.getHours();
+ const currentMinutes = now.getMinutes();
+ const currentTimeVal = currentHours * 60 + currentMinutes;
+
+ // Helper to parse "HH:MM" to minutes
+ const parseTime = (t: string) => {
+ const [h, m] = t.split(':').map(Number);
+ return h * 60 + (m || 0);
+ };
+
+ // Helper to check if date is today
+ const isToday = (dateStr?: Date | string | null) => {
+ if (!dateStr) return false;
+ const d = new Date(dateStr);
+ return d.getDate() === now.getDate() &&
+ d.getMonth() === now.getMonth() &&
+ d.getFullYear() === now.getFullYear();
+ };
+
+ // Evening Routine Check
+ const eveningEnabled = settings.evening_routine_enabled !== "false";
+ const eveningStartTime = parseTime(settings.evening_routine_time || "17:00"); // 17:00 default
+
+ // If it is Evening time (>= start time), we primarily check Evening Routine.
+ if (eveningEnabled) {
+ if (currentTimeVal >= eveningStartTime) {
+ // It is evening. Block if evening not done.
+ // We do NOT block for Morning routine anymore if it's evening time (user missed it).
+ if (!isToday(user.lastEveningRoutine)) {
+ return '/focus/routine/evening';
+ }
+ // If evening done, we don't block for morning either.
+ return null;
+ }
+ }
+
+ // Morning Routine Check (Only if < evening start time or evening disabled)
+ const morningEnabled = settings.morning_routine_enabled !== "false";
+ if (morningEnabled) {
+ const morningStartTime = parseTime(settings.morning_routine_time || "09:00");
+
+ // Block if it's morning time (>= start) AND < evening start (if evening enabled).
+ // Effectively: Morning Routine is mandatory from 9:00 AM until 5:00 PM.
+ const cutoffTime = eveningEnabled ? eveningStartTime : 24 * 60; // Up to evening or end of day
+
+ if (currentTimeVal >= morningStartTime && currentTimeVal < cutoffTime && !isToday(user.lastMorningRoutine)) {
+ return '/focus/routine/morning';
+ }
+ }
+
+ return null;
+ };
+
+ const target = checkRoutine();
+ if (target) {
+ // Only redirect if not already there
+ if (!window.location.pathname.includes(target)) {
+ setLocation(target);
+ }
+ }
+
+ }, [user, settings, setLocation]);
+
+ // Return a boolean telling if blocking is active, so App can hide Sidebar
+ const isMorningBlocked = () => {
+ if (!user || !settings) return false;
+
+ const now = new Date();
+ const currentTimeVal = now.getHours() * 60 + now.getMinutes();
+
+ const eveningEnabled = settings.evening_routine_enabled !== "false";
+ const eveningStartTime = (settings.evening_routine_time || "17:00").split(':').map(Number);
+ const eveningStartVal = eveningStartTime[0] * 60 + eveningStartTime[1];
+
+ const morningEnabled = settings.morning_routine_enabled !== "false";
+ if (!morningEnabled) return false;
+
+ const morningStartTime = (settings.morning_routine_time || "09:00").split(':').map(Number);
+ const startVal = morningStartTime[0] * 60 + morningStartTime[1];
+
+ // Cutoff: End of day OR Evening Start
+ const cutoffVal = eveningEnabled ? eveningStartVal : 24 * 60;
+
+ if (currentTimeVal >= startVal && currentTimeVal < cutoffVal && !isSameDay(user.lastMorningRoutine, now)) return true;
+
+ return false;
+ };
+
+ const isEveningBlocked = () => {
+ if (!user || !settings) return false;
+
+ const now = new Date();
+ const currentTimeVal = now.getHours() * 60 + now.getMinutes();
+
+ const eveningEnabled = settings.evening_routine_enabled !== "false";
+ if (!eveningEnabled) return false;
+
+ const eveningStartTime = (settings.evening_routine_time || "17:00").split(':').map(Number);
+ const startVal = eveningStartTime[0] * 60 + eveningStartTime[1];
+
+ if (currentTimeVal >= startVal && !isSameDay(user.lastEveningRoutine, now)) return true;
+
+ return false;
+ };
+
+ return isMorningBlocked() || isEveningBlocked();
+}
+
+function isSameDay(d1: any, d2: Date) {
+ if (!d1) return false;
+ const d = new Date(d1);
+ return d.getDate() === d2.getDate() && d.getMonth() === d2.getMonth() && d.getFullYear() === d2.getFullYear();
+}
diff --git a/client/src/components/admin/RoutineSettingsCard.tsx b/client/src/components/admin/RoutineSettingsCard.tsx
new file mode 100644
index 0000000..67b5c86
--- /dev/null
+++ b/client/src/components/admin/RoutineSettingsCard.tsx
@@ -0,0 +1,156 @@
+import { useState, useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Label } from "@/components/ui/label";
+import { Input } from "@/components/ui/input";
+import { Switch } from "@/components/ui/switch";
+import { Button } from "@/components/ui/button";
+import { Loader2, Sun, Moon } from "lucide-react";
+import { apiRequest } from "@/lib/queryClient";
+import { useToast } from "@/hooks/use-toast";
+
+export function RoutineSettingsCard() {
+ const { t } = useTranslation();
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+
+ const [morningEnabled, setMorningEnabled] = useState(true);
+ const [morningTime, setMorningTime] = useState("09:00");
+ const [eveningEnabled, setEveningEnabled] = useState(true);
+ const [eveningTime, setEveningTime] = useState("17:00");
+
+ const { data: settings, isLoading } = useQuery>({
+ queryKey: ['/api/admin/settings'],
+ queryFn: async () => {
+ const res = await apiRequest("GET", "/api/admin/settings");
+ if (!res.ok) throw new Error("Failed to fetch settings");
+ return res.json();
+ }
+ });
+
+ useEffect(() => {
+ if (settings) {
+ setMorningEnabled(settings.morning_routine_enabled !== "false");
+ setMorningTime(settings.morning_routine_time || "09:00");
+ setEveningEnabled(settings.evening_routine_enabled !== "false");
+ setEveningTime(settings.evening_routine_time || "17:00");
+ }
+ }, [settings]);
+
+ const mutation = useMutation({
+ mutationFn: async () => {
+ const updates = {
+ morning_routine_enabled: String(morningEnabled),
+ morning_routine_time: morningTime,
+ evening_routine_enabled: String(eveningEnabled),
+ evening_routine_time: eveningTime,
+ };
+
+ // We send individual updates or a bulk update?
+ // The backend /api/admin/settings usually accepts a map of KVs to update.
+ // Checking AdminSettings.tsx might verify this, but typically we post to specific keys or bulk object.
+ // Assuming GET returns object, POST probably takes object.
+ // If server routes handle bulk update.
+ // If not, we loop.
+ // Checking AiSettingsCard logic: it calls `/api/admin/settings` with JSON body.
+ // Assuming generic handler supports partial updates.
+
+ const res = await apiRequest("POST", "/api/admin/settings", updates);
+ if (!res.ok) throw new Error(await res.text());
+ return res.json();
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['/api/admin/settings'] });
+ toast({ title: t('settings.routines.saved', 'Routine settings saved') });
+ },
+ onError: (err: Error) => {
+ toast({
+ title: t('settings.routines.error', 'Failed to save settings'),
+ description: err.message,
+ variant: "destructive"
+ });
+ }
+ });
+
+ return (
+
+
+ {t('settings.routines.title', 'Routine Configuration')}
+
+ {t('settings.routines.description', 'Configure global morning and evening routine schedules.')}
+
+
+
+ {/* Morning Routine */}
+
+
+
+
+
+
+
+
+
+ {morningEnabled && (
+
+
+ setMorningTime(e.target.value)}
+ className="w-32"
+ />
+
+ )}
+
+
+ {/* Evening Routine */}
+
+
+
+
+
+
+
+
+
+ {eveningEnabled && (
+
+
+ setEveningTime(e.target.value)}
+ className="w-32"
+ />
+
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/i18n/locales/de.json b/client/src/i18n/locales/de.json
index 3ecf4f3..6f01495 100644
--- a/client/src/i18n/locales/de.json
+++ b/client/src/i18n/locales/de.json
@@ -354,6 +354,8 @@
"baseUrl": "Basis-URL",
"systemPrompt": "System-Prompt",
"systemPromptPlaceholder": "Definieren Sie die Persona und Regeln der KI...",
+ "enableUser": "KI-Assistent aktivieren",
+ "enableUserDesc": "KI-Chat-Widget anzeigen.",
"save": "KI-Einstellungen speichern",
"saved": "Einstellungen gespeichert",
"error": "Fehler beim Speichern",
@@ -367,6 +369,21 @@
"pullError": "Fehler beim Laden des Modells",
"connectError": "Verbindung zu Ollama fehlgeschlagen"
}
+ },
+ "routines": {
+ "title": "Routine-Konfiguration",
+ "description": "Konfigurieren Sie globale ZeitplĂ€ne fĂŒr Morgen- und Abendroutinen.",
+ "morningLabel": "Morgenroutine",
+ "eveningLabel": "Abendroutine",
+ "time": "Startzeit",
+ "saved": "Routine-Einstellungen gespeichert",
+ "error": "Fehler beim Speichern",
+ "save": "Routinen speichern",
+ "goodMorning": "Guten Morgen",
+ "goodEvening": "Guten Abend",
+ "morningSubtitle": "Lass uns deinen Tag planen.",
+ "eveningSubtitle": "Zeit zum Reflektieren und Entspannen.",
+ "dayComplete": "Tag abgeschlossen! Gute Arbeit."
}
},
"newChatDefault": "Neuer Chat",
@@ -541,31 +558,55 @@
"xp": "{{count}} EP",
"nextLevel": "{{count}} EP",
"currentXP": "{{current}} / {{next}} EP",
- "viewDetails": "Details anzeigen",
"source": {
- "task_completion": "Aufgabe erledigt",
- "daily_streak": "TĂ€glicher Serien-Bonus",
- "daily_clear_bonus": "Tagesziel-Bonus",
- "goal_completed": "Ziel erreicht"
+ "create_task": "Aufgabe erstellt",
+ "create_subtask": "Teilaufgabe erstellt",
+ "update_task": "Aufgabe aktualisiert",
+ "complete_task": "Aufgabe erledigt",
+ "complete_task_late": "VerspÀtet erledigt",
+ "ai_action": "AI Aktion",
+ "daily_streak": "TĂ€glicher Streak"
},
"rules": {
- "title": "Gamification Regeln",
- "xpSystem": "XP System",
+ "title": "Regeln & RĂ€nge",
"levelRequirements": "Level Anforderungen",
- "actions": "Aktionen & Belohnungen",
+ "xpSystem": "Wie man XP verdient",
"level": "Level {{level}}",
"xp": "{{xp}} XP",
- "action": "Aktion",
- "points": "Punkte",
+ "actions": "XP Aktionen",
"createTask": "Aufgabe erstellen",
"createSubtask": "Unteraufgabe erstellen",
"updateTask": "Aufgabe aktualisieren",
- "completeTask": "Aufgabe erledigen (PĂŒnktlich)",
- "completeTaskLate": "Aufgabe erledigen (VerspÀtet)",
- "aiAction": "AI Funktion nutzen",
- "dailyStreak": "TĂ€glicher Serienbonus"
+ "completeTask": "Aufgabe erledigen",
+ "completeTaskLate": "VerspÀtet erledigen",
+ "aiAction": "AI Nutzung",
+ "dailyStreak": "TĂ€glicher Streak",
+ "streakTooltip": "Melde dich tÀglich an, um deinen Streak zu erhöhen!",
+ "streakBonus": "Wöchentliche & Monatliche Boni verfĂŒgbar: 7 Tage (+300 XP), 30 Tage (+1000 XP)."
}
},
+ "analytics": {
+ "mon": "Mo",
+ "tue": "Di",
+ "wed": "Mi",
+ "thu": "Do",
+ "fri": "Fr",
+ "sat": "Sa",
+ "sun": "So",
+ "jan": "Jan",
+ "feb": "Feb",
+ "mar": "MĂ€r",
+ "apr": "Apr",
+ "may": "Mai",
+ "jun": "Jun",
+ "jul": "Jul",
+ "aug": "Aug",
+ "sep": "Sep",
+ "oct": "Okt",
+ "nov": "Nov",
+ "dec": "Dez",
+ "cw": "KW"
+ },
"ranks": {
"novice": "Neuling",
"apprentice": "Lehrling",
diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json
index 5cdebac..8d6f4e2 100644
--- a/client/src/i18n/locales/en.json
+++ b/client/src/i18n/locales/en.json
@@ -365,6 +365,21 @@
"pullError": "Failed to pull model",
"connectError": "Could not connect to Ollama"
}
+ },
+ "routines": {
+ "title": "Routine Configuration",
+ "description": "Configure global morning and evening routine schedules.",
+ "morningLabel": "Morning Routine",
+ "eveningLabel": "Evening Routine",
+ "time": "Start Time",
+ "saved": "Routine settings saved",
+ "error": "Failed to save settings",
+ "save": "Save Routines",
+ "goodMorning": "Good Morning",
+ "goodEvening": "Good Evening",
+ "morningSubtitle": "Let's plan your day for success.",
+ "eveningSubtitle": "Time to reflect and unwind.",
+ "dayComplete": "Day Complete! Great job."
}
},
"smtp": {
@@ -634,16 +649,21 @@
"currentXP": "{{current}} / {{next}} XP",
"viewDetails": "View Details",
"source": {
- "task_completion": "Task Completed",
+ "create_task": "Task Created",
+ "create_subtask": "Subtask Created",
+ "update_task": "Task Updated",
+ "complete_task": "Task Completed",
+ "complete_task_late": "Task Completed (Late)",
+ "ai_action": "AI Assistant Used",
"daily_streak": "Daily Streak Bonus",
"daily_clear_bonus": "Daily Clear Bonus",
"goal_completed": "Goal Completed"
},
"rules": {
- "title": "Gamification Rules",
- "xpSystem": "XP System",
+ "title": "Rules & Ranks",
+ "xpSystem": "How to earn XP",
"levelRequirements": "Level Requirements",
- "actions": "Actions & Rewards",
+ "actions": "XP Actions",
"level": "Level {{level}}",
"xp": "{{xp}} XP",
"action": "Action",
@@ -651,10 +671,12 @@
"createTask": "Create Task",
"createSubtask": "Create Subtask",
"updateTask": "Update Task",
- "completeTask": "Complete Task (On Time)",
+ "completeTask": "Complete Task",
"completeTaskLate": "Complete Task (Late)",
- "aiAction": "Use AI Feature",
- "dailyStreak": "Daily Streak Bonus"
+ "aiAction": "AI Action",
+ "dailyStreak": "Daily Streak",
+ "streakTooltip": "Log in daily to increase your streak!",
+ "streakBonus": "Weekly & Monthly bonuses available: 7 days (+300 XP), 30 days (+1000 XP)."
}
},
"rewards": {
diff --git a/client/src/pages/AchievementsPage.tsx b/client/src/pages/AchievementsPage.tsx
index 03abfa6..b74ce56 100644
--- a/client/src/pages/AchievementsPage.tsx
+++ b/client/src/pages/AchievementsPage.tsx
@@ -1,4 +1,3 @@
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
@@ -491,21 +490,30 @@ export default function AchievementsPage({ user }: { user: User }) {
history.map((event) => (
-
- {event.source === 'task_completion' ? :
- event.source === 'daily_streak' ? :
- }
+ {event.source === 'complete_task_late' ? :
+ event.source.includes('complete') ? :
+ event.source === 'daily_streak' ? :
+ }
- {t(`gamification.source.${event.source}`, { defaultValue: event.source }) as string}
-
-
- {new Date(event.createdAt).toLocaleString()}
+ {event.details?.taskTitle ? event.details.taskTitle : (t(`gamification.source.${event.source}`, { defaultValue: event.source }) as string)}
+ {event.source === 'complete_task_late' && ({t('gamification.source.complete_task_late')})}
+
+ {new Date(event.createdAt).toLocaleString()}
+ {event.details?.taskTitle && (
+ <>
+ âą
+ {t(`gamification.source.${event.source}`, { defaultValue: event.source })}
+ >
+ )}
+
diff --git a/client/src/pages/AdminSettings.tsx b/client/src/pages/AdminSettings.tsx
index 4d91c5b..5b52df3 100644
--- a/client/src/pages/AdminSettings.tsx
+++ b/client/src/pages/AdminSettings.tsx
@@ -8,6 +8,8 @@ import { useLocation } from "wouter";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { AuditLogsTable } from "@/components/admin/AuditLogsTable";
import { McpSettingsCard } from "@/components/admin/McpSettingsCard";
+import { RoutineSettingsCard } from "@/components/admin/RoutineSettingsCard";
+import { Clock } from "lucide-react";
export default function AdminSettings() {
const { t } = useTranslation();
@@ -41,6 +43,10 @@ export default function AdminSettings() {
{t('settings.admin.tabs.ai')}
+
+
+ {t('settings.admin.tabs.routines', 'Routines')}
+
{t('settings.admin.tabs.mcp')}
@@ -59,6 +65,10 @@ export default function AdminSettings() {
+
+
+
+
diff --git a/client/src/pages/FocusRoutinePage.tsx b/client/src/pages/FocusRoutinePage.tsx
index 9f7e34d..2c4b775 100644
--- a/client/src/pages/FocusRoutinePage.tsx
+++ b/client/src/pages/FocusRoutinePage.tsx
@@ -38,11 +38,21 @@ export default function FocusRoutinePage() {
// Handlers
const handleComplete = async () => {
+ try {
+ await apiRequest("POST", `/api/user/routine/${type}/complete`);
+ // Invalidate user query to update lastMorningRoutine/lastEveningRoutine
+ await queryClient.invalidateQueries({ queryKey: ["/api/user"] });
+ } catch (e) {
+ console.error(e);
+ toast({ title: t('routine.error', 'Failed to save progress'), variant: 'destructive' });
+ }
+
if (type === 'morning') {
setLocation('/focus');
} else {
triggerConfetti(0.5, 0.5);
toast({ title: t('routine.dayComplete', "Day Complete! Great job.") });
+ // For evening, maybe logout or home? Or achievements
setLocation('/achievements');
}
};
diff --git a/package-lock.json b/package-lock.json
index 94b918b..68937dc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "rest-express",
- "version": "1.0.6",
+ "version": "1.0.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rest-express",
- "version": "1.0.6",
+ "version": "1.0.7",
"license": "MIT",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
diff --git a/package.json b/package.json
index 4bb9c07..6b85a49 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "rest-express",
- "version": "1.0.6",
+ "version": "1.0.7",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/server/auth.ts b/server/auth.ts
index b15809b..632838b 100644
--- a/server/auth.ts
+++ b/server/auth.ts
@@ -151,8 +151,52 @@ export function setupAuth(app: Express) {
});
});
- app.get("/api/user", (req, res) => {
+ app.get("/api/user", async (req, res) => {
if (!req.isAuthenticated()) return res.sendStatus(401);
- res.json(req.user);
+
+ // Check for Daily Streak
+ const user = req.user as User;
+ const now = new Date();
+ const lastActive = user.lastActive ? new Date(user.lastActive) : new Date(0);
+
+ // Normalize to dates (ignore time)
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const lastDate = new Date(lastActive.getFullYear(), lastActive.getMonth(), lastActive.getDate());
+ const yesterday = new Date(today);
+ yesterday.setDate(yesterday.getDate() - 1);
+
+ // If last active was yesterday, increment streak
+ // If last active was today, do nothing
+ // If last active was before yesterday, reset streak (unless we decide to be lenient)
+
+ // We need GamificationService here
+ const { GamificationService } = await import("./gamification");
+ const gamificationService = new GamificationService(storage);
+
+ if (lastDate.getTime() < today.getTime()) {
+ if (lastDate.getTime() === yesterday.getTime()) {
+ // Perfect streak
+ await gamificationService.awardXP(user.id, 'daily_streak');
+ // Check bonuses
+ const updatedUser = await storage.getUser(user.id);
+ if (updatedUser) {
+ await gamificationService.checkStreakBonuses(user.id, updatedUser.currentStreak);
+ }
+ } else if (lastDate.getTime() < yesterday.getTime()) {
+ // Streak broken
+ // Reset streak to 1 (today is day 1)
+ await storage.updateUser(user.id, { currentStreak: 1 });
+ // Still award daily XP for today? Yes.
+ await gamificationService.awardXP(user.id, 'daily_streak');
+ } else {
+ // Should not happen if < today
+ }
+ // Update lastActive
+ await storage.updateUser(user.id, { lastActive: now });
+ }
+
+ // Re-fetch user to get latest XP and Streak
+ const freshUser = await storage.getUser(user.id);
+ res.json(freshUser);
});
}
diff --git a/server/gamification.ts b/server/gamification.ts
index 481fd7a..4769cd2 100644
--- a/server/gamification.ts
+++ b/server/gamification.ts
@@ -20,33 +20,49 @@ export class GamificationService {
this.storage = storage;
}
- async awardXP(userId: string, source: string, amount?: number, description?: string): Promise<{ user: User, levelUp: boolean, oldLevel: number, newLevel: number }> {
+ async awardXP(userId: string, source: string, amount?: number, details?: any): Promise<{ user: User, levelUp: boolean, oldLevel: number, newLevel: number }> {
const user = await this.storage.getUser(userId);
if (!user) throw new Error("User not found");
const xpAmount = amount || this.getXPForSource(source);
- const newTotalXP = (user.xp || 0) + xpAmount;
+ // FIX: The previous bug was likely in updateUserXP implementation in storage.
+ // Let's check storage.ts.
+ // DbStorage.updateUserXP does: .set({ xp: user.xp + xp }) where input is 'xp'.
+ // So validation:
+ // If I pass '10', DbStorage adds 10.
+ // The MemStorage implementation was: user.xp += xp;
+ // The issue: In the previous code:
+ // const newTotalXP = (user.xp || 0) + xpAmount;
+ // await this.storage.updateUserXP(userId, newTotalXP);
+ // If user had 100 XP, xpAmount 50. newTotalXP = 150.
+ // If storage.updateUserXP(150) adds 150 to 100, result is 250.
+ // If storage.updateUserXP(150) sets it to 150, result is 150.
+ // MEMORY storage ADDS. DB storage ADDS.
+ // "set({ xp: user.xp + xp })" -> logic implies input is DELTA.
+ // So passing 'newTotalXP' (150) as delta ADDS 150. Double counting!
+
+ // CORRECTION: Pass ONLY the delta (xpAmount).
+ await this.storage.updateUserXP(userId, xpAmount);
+
+ // Fetch fresh user to get calculated new total
+ const updatedUserRaw = await this.storage.getUser(userId);
+ const currentXP = updatedUserRaw?.xp || 0;
// Check for level up
const oldLevel = getLevelFromXP(user.xp || 0);
- const newLevel = getLevelFromXP(newTotalXP);
+ const newLevel = getLevelFromXP(currentXP);
const levelUp = newLevel > oldLevel;
- // Update User
- await this.storage.updateUserXP(userId, newTotalXP);
-
// Log Event
await this.storage.logXpEvent({
userId,
amount: xpAmount,
source,
+ details // Log details
});
- // If Level Up, we could log a special event or notification here?
-
- const updatedUser = await this.storage.getUser(userId);
return {
- user: updatedUser!,
+ user: updatedUserRaw!,
levelUp,
oldLevel,
newLevel
@@ -65,4 +81,98 @@ export class GamificationService {
default: return 0;
}
}
+
+ async checkStreakBonuses(userId: string, currentStreak: number) {
+ // Weekly Bonus (every 7 days)
+ if (currentStreak > 0 && currentStreak % 7 === 0) {
+ await this.awardXP(userId, 'weekly_streak_bonus', 300, { streak: currentStreak });
+ }
+
+ // Monthly Bonus (every 30 days)
+ if (currentStreak > 0 && currentStreak % 30 === 0) {
+ await this.awardXP(userId, 'monthly_streak_bonus', 1000, { streak: currentStreak });
+ }
+ }
+
+ // Analytics Methods
+ async getWeeklyAnalytics(userId: string) {
+ // Return last 7 days details
+ // In a real app we would use SQL aggregation.
+ // For now, let's fetch events and aggregate in memory or rely on a new storage method if needed.
+ // But better is to just fetch last 7 days events via storage.getXpEvents and process.
+ const events = await this.storage.getXpEvents(userId);
+ const now = new Date();
+ const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
+ const last7Days = Array.from({ length: 7 }, (_, i) => {
+ const d = new Date();
+ d.setDate(now.getDate() - (6 - i));
+ return d;
+ });
+
+ // Map: Label -> XP
+ const data = last7Days.map(date => {
+ const dayEvents = events.filter(e => {
+ if (!e.createdAt) return false;
+ const d = new Date(e.createdAt);
+ return d.getDate() === date.getDate() && d.getMonth() === date.getMonth();
+ });
+ const total = dayEvents.reduce((sum, e) => sum + e.amount, 0);
+ return {
+ labelKey: days[date.getDay()].toLowerCase(), // 'sun', 'mon', etc. for translation
+ xp: total
+ };
+ });
+ return data; // [ { labelKey: 'mon', xp: 50 }, ... ]
+ }
+
+ async getMonthlyAnalytics(userId: string) {
+ // Return 4 weeks
+ const events = await this.storage.getXpEvents(userId);
+
+ // Group by ISO Week? Or just simplified chunks.
+ // Let's do 4 previous weeks based on current date.
+
+ // Helper to get week number
+ const getWeek = (d: Date) => {
+ const onejan = new Date(d.getFullYear(), 0, 1);
+ const millis = d.getTime() - onejan.getTime();
+ return Math.ceil((((millis / 86400000) + onejan.getDay() + 1) / 7));
+ };
+
+ const currentWeek = getWeek(new Date());
+ const weeks = [currentWeek - 3, currentWeek - 2, currentWeek - 1, currentWeek];
+
+ const data = weeks.map(w => {
+ const weekEvents = events.filter(e => {
+ if (!e.createdAt) return false;
+ const d = new Date(e.createdAt);
+ return getWeek(d) === w && d.getFullYear() === new Date().getFullYear();
+ });
+ const total = weekEvents.reduce((sum, e) => sum + e.amount, 0);
+ return {
+ labelKey: w.toString(),
+ xp: total
+ };
+ });
+ return data;
+ }
+
+ async getYearlyAnalytics(userId: string) {
+ const events = await this.storage.getXpEvents(userId);
+ const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
+
+ const data = months.map((m, index) => {
+ const monthEvents = events.filter(e => {
+ if (!e.createdAt) return false;
+ const d = new Date(e.createdAt);
+ return d.getMonth() === index && d.getFullYear() === new Date().getFullYear();
+ });
+ const total = monthEvents.reduce((sum, e) => sum + e.amount, 0);
+ return {
+ labelKey: m,
+ xp: total
+ };
+ });
+ return data;
+ }
}
diff --git a/server/routes.ts b/server/routes.ts
index 08600b4..5fe2e7a 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -984,7 +984,7 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
// Award XP for creating a task
if (req.user) {
const source = req.body.parentTaskId ? 'create_subtask' : 'create_task';
- await gamificationService.awardXP((req.user as User).id, source);
+ await gamificationService.awardXP((req.user as User).id, source, undefined, { taskId: task.id, taskTitle: task.title });
}
await storage.createAuditLog({
@@ -1017,10 +1017,10 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
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);
+ 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
// Small points for any other update (title, description, etc)
- await gamificationService.awardXP((req.user as User).id, 'update_task');
+ await gamificationService.awardXP((req.user as User).id, 'update_task', undefined, { taskId: previousTask.id, taskTitle: previousTask.title });
}
}
@@ -1131,56 +1131,33 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
// Analytics API
app.get("/api/analytics/weekly", async (req, res) => {
- // Return last 7 days. Key is 0-6 (Sun-Sat) or ISO date.
- // For simplicity, let's return day index relative to today or just standard day index (0=Sun)
- // To make it look "last 7 days" we can return relative indices
- const keys = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
- // Better: Send localizable keys.
- // Day format: "day_1" (Mon) ... "day_7" (Sun) or just short codes the frontend can map
-
- // We will send standard JS Day indices adjusted: 1 (Mon) - 7 (Sun) for "ISO Week" style or just 0-6
- // Let's send a `labelKey` that the frontend can translate.
- const data = [
- { labelKey: 'mon', xp: Math.floor(Math.random() * 500) },
- { labelKey: 'tue', xp: Math.floor(Math.random() * 500) },
- { labelKey: 'wed', xp: Math.floor(Math.random() * 500) },
- { labelKey: 'thu', xp: Math.floor(Math.random() * 500) },
- { labelKey: 'fri', xp: Math.floor(Math.random() * 500) },
- { labelKey: 'sat', xp: Math.floor(Math.random() * 500) },
- { labelKey: 'sun', xp: Math.floor(Math.random() * 500) },
- ];
- res.json(data);
+ if (!req.isAuthenticated()) return res.sendStatus(401);
+ try {
+ const data = await gamificationService.getWeeklyAnalytics((req.user as User).id);
+ res.json(data);
+ } catch (e) {
+ res.status(500).json({ error: "Failed to fetch analytics" });
+ }
});
app.get("/api/analytics/yearly", async (req, res) => {
- const data = [
- { labelKey: 'jan', xp: Math.floor(Math.random() * 2000) },
- { labelKey: 'feb', xp: Math.floor(Math.random() * 2000) },
- { labelKey: 'mar', xp: Math.floor(Math.random() * 2000) },
- { labelKey: 'apr', xp: Math.floor(Math.random() * 2000) },
- { labelKey: 'may', xp: Math.floor(Math.random() * 2000) },
- { labelKey: 'jun', xp: Math.floor(Math.random() * 2000) },
- { labelKey: 'jul', xp: Math.floor(Math.random() * 2000) },
- { labelKey: 'aug', xp: Math.floor(Math.random() * 2000) },
- { labelKey: 'sep', xp: Math.floor(Math.random() * 2000) },
- { labelKey: 'oct', xp: Math.floor(Math.random() * 2000) },
- { labelKey: 'nov', xp: Math.floor(Math.random() * 2000) },
- { labelKey: 'dec', xp: Math.floor(Math.random() * 2000) },
- ];
- res.json(data);
+ if (!req.isAuthenticated()) return res.sendStatus(401);
+ try {
+ const data = await gamificationService.getYearlyAnalytics((req.user as User).id);
+ res.json(data);
+ } catch (e) {
+ res.status(500).json({ error: "Failed to fetch analytics" });
+ }
});
app.get("/api/analytics/monthly", async (req, res) => {
- // Return last 4-5 weeks with actual Calendar Week numbers
- // Mocking for now: Assume current week is ~50
- const currentWeek = 50;
- const data = [
- { labelKey: (currentWeek - 3).toString(), xp: Math.floor(Math.random() * 800) },
- { labelKey: (currentWeek - 2).toString(), xp: Math.floor(Math.random() * 800) },
- { labelKey: (currentWeek - 1).toString(), xp: Math.floor(Math.random() * 800) },
- { labelKey: currentWeek.toString(), xp: Math.floor(Math.random() * 800) },
- ];
- res.json(data);
+ if (!req.isAuthenticated()) return res.sendStatus(401);
+ try {
+ const data = await gamificationService.getMonthlyAnalytics((req.user as User).id);
+ res.json(data);
+ } catch (e) {
+ res.status(500).json({ error: "Failed to fetch analytics" });
+ }
});
// Gamification Logic Wrapper
@@ -1349,6 +1326,27 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
}
});
+ app.post("/api/user/routine/:type/complete", async (req, res) => {
+ if (!req.isAuthenticated()) return res.sendStatus(401);
+ const type = req.params.type;
+ if (type !== 'morning' && type !== 'evening') return res.status(400).json({ error: "Invalid routine type" });
+
+ try {
+ const updates: any = {};
+ const now = new Date();
+ if (type === 'morning') {
+ updates.lastMorningRoutine = now;
+ } else {
+ updates.lastEveningRoutine = now;
+ }
+
+ const updatedUser = await storage.updateUser((req.user as User).id, updates);
+ res.json(updatedUser);
+ } catch (e) {
+ res.status(500).json({ error: "Failed to complete routine" });
+ }
+ });
+
app.patch("/api/user/password", async (req, res) => {
if (!req.isAuthenticated()) return res.sendStatus(401);
try {
diff --git a/shared/schema.ts b/shared/schema.ts
index 8324cdc..fd1770c 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -14,11 +14,14 @@ export const users = pgTable("users", {
level: integer("level").notNull().default(1),
currentStreak: integer("current_streak").notNull().default(0),
lastTaskDate: timestamp("last_task_date"),
- showOnLeaderboard: boolean("show_on_leaderboard").notNull().default(false), // Privacy setting
+ lastActive: timestamp("last_active"), // Track daily activity for streaks
+ showOnLeaderboard: boolean("show_on_leaderboard").default(true).notNull(), // Privacy setting
isSearchable: boolean("is_searchable").notNull().default(false), // Privacy setting
apiKey: text("api_key"), // For MCP Server access
aiEnabled: boolean("ai_enabled").notNull().default(true), // Feature flag per user
routineConfig: json("routine_config").$type<{ morningTime: string, eveningTime: string, enabled: boolean }>().default({ morningTime: "09:00", eveningTime: "17:00", enabled: true }),
+ lastMorningRoutine: timestamp("last_morning_routine"),
+ lastEveningRoutine: timestamp("last_evening_routine"),
});
export const systemSettings = pgTable("system_settings", {
@@ -172,6 +175,7 @@ export const xpEvents = pgTable("xp_events", {
amount: integer("amount").notNull(),
source: text("source").notNull(), // 'task_completion', 'daily_streak', 'bonus'
taskId: varchar("task_id"),
+ details: json("details"), // For snapshotting task title etc.
createdAt: timestamp("created_at").defaultNow(),
});
diff --git a/task.md b/task.md
index 2b046d6..cc250f9 100644
--- a/task.md
+++ b/task.md
@@ -194,6 +194,26 @@
- [x] Implement Swipe Actions (Framer Motion)
- [x] Final Native/UX Verification
- [x] Verify PWA Installability
+ - [x] **Debugging & Polish**
+ - [x] **Fix EP Counting Bug**: Experience Points are multiplying instead of adding (Fix logic in `server/gamification.ts`)
+ - [x] **Improve EP History**: Display specific Task Name in the EP History board (Add `details` JSON to `xpEvents` schema)
+ - [x] Update `schema.ts`
+ - [x] Update `gamification.ts` to log task details
+ - [x] Update `AchievementsPage.tsx` to display details
+ - [x] **Real XP Data**: Connect XP Activity chart to real user data instead of mock data
+ - [x] Implement `getWeeklyAnalytics` etc. in `gamification.ts`
+ - [x] Wire up API endpoints in `routes.ts`
+ - [x] **Visuals**: Redesign Favicon/App Icon to match dark mode "TaskFlow" branding
+
+- [ ] **Routine & Gamification Fixes**
+ - [ ] **Critical Bug**: Fix White Screen & Wrong Routine Redirection (Morning routine appearing at night)
+ - [ ] Debug `useRoutineBlocker` logic
+ - [ ] Debug `FocusRoutinePage` rendering
+ - [ ] **Gamification Enhancements**
+ - [ ] Add Weekly (7-day) & Monthly (30-day) Streak Bonuses
+ - [ ] Add Tooltip for Streak explanation
+ - [ ] Fix EP History: Show "Late" vs "On Time" clearly
+ - [ ] Ensure AI usage and all actions are visible in History
- [x] Verify NLP Parsing
- [x] Verify NLP Parsing
- [x] Verify Drag/Swipe Interactions