fix(routine): resolove routine blocker logic bug and white screen
continuous-integration/drone/push Build is passing

feat(gamification): add streak bonuses, tooltip, and improved history details

fix(ep): resolve double counting ep bug

ui: update app icon and translations
This commit is contained in:
2025-12-15 23:17:59 +01:00
parent 9736864425
commit 7b79015ac2
17 changed files with 884 additions and 299 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

After

Width:  |  Height:  |  Size: 419 KiB

+225 -198
View File
@@ -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 (
<Switch>
<Route path="/setup" component={SetupWizard} />
<Route component={SetupWizard} />
</Switch>
);
}
if (isBlocked) {
return (
<div className="min-h-screen bg-background w-full">
<Switch>
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
{/* Catch all redirects to nothing, the hook handles the push to routine page */}
<Route component={() => <div />} />
</Switch>
<Toaster />
</div>
);
}
return (
<Switch>
<Route path="/forgot-password" component={ForgotPasswordPage} />
<Route path="/reset-password" component={ResetPasswordPage} />
<Route component={AuthPage} />
</Switch>
<TooltipProvider>
<SidebarProvider>
<AppSidebar user={user} />
<SidebarInset>
<div className="min-h-screen bg-background flex flex-col">
{user && (
<header className="flex h-16 shrink-0 items-center gap-2 border-b pl-4 pr-6">
<SidebarTrigger />
<div className="flex-1" />
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => setLocation('/notifications')}>
{/* Bell icon would go here */}
</Button>
<div className="flex items-center gap-2">
<span className="text-sm text-yellow-500 font-bold"> {user.xp}</span>
<span className="text-sm text-orange-500 font-bold">🔥 {user.currentStreak}</span>
</div>
</div>
</header>
)}
<main className="flex-1 p-4 md:p-6 max-w-screen-2xl mx-auto w-full relative">
<Switch>
<Route path="/">
<FocusMode
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
onNavigateToTasks={() => setLocation('/tasks')}
/>
</Route>
<Route path="/tasks">
<TasksWithCalendar
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/calendar">
<CalendarView
tasks={tasks}
onTaskDrop={handleTaskDrop}
onTaskClick={handleTaskClick}
onDateSelect={(date) => console.log('Date selected:', date.toLocaleDateString())}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/kanban">
<KanbanBoard
tasks={tasks}
onTaskStatusChange={handleTaskStatusChange}
onTaskUpdate={handleTaskUpdate}
onTaskClick={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/weeklist">
<WeekListView
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/templates">
<ProjectTemplate
onCreateFromTemplate={handleCreateFromTemplate}
onCreateTasks={(newTasks) => {
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]);
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
}}
onNavigateToSettings={() => setLocation('/settings')}
/>
</Route>
<Route path="/achievements">
{user ? <AchievementsPage user={user} /> : <AuthPage />}
</Route>
<Route path="/unscheduled">
{user ? (
<UnscheduledTasksPage
user={user}
onToggleCompletion={(taskId, currentStatus) => handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')}
onDelete={(id) => setDeleteTaskId(id)}
onUpdate={handleTaskUpdate}
onSelect={setSelectedTask}
/>
) : (
<AuthPage />
)}
</Route>
<Route path="/leaderboard">
<LeaderboardPage />
</Route>
<Route path="/ai">
<AiChatPage />
</Route>
<Route path="/settings">
<Settings onNavigateToTemplates={() => setLocation('/templates')} />
</Route>
{/* Admin Route */}
{user.role === 'admin' && (
<>
<Route path="/admin/users" component={AdminUserManagement} />
<Route path="/admin/settings" component={AdminSettings} />
</>
)}
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
<Route component={NotFound} />
</Switch>
</main>
{/* Floating Action Button */}
<div className="fixed bottom-8 right-8 z-[100]">
<Button
size="icon"
className="h-14 w-14 rounded-full shadow-2xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:scale-110 transition-transform duration-200"
onClick={() => setIsCreateModalOpen(true)}
data-testid="fab-create-task"
>
<Plus className="h-6 w-6 text-white" />
</Button>
</div>
<CommandPalette
onNavigate={(path) => setLocation(path.startsWith('/') ? path : `/${path}`)}
onCreateTask={() => setIsCreateModalOpen(true)}
/>
<PomodoroOverlay
isOpen={showPomodoro}
onClose={() => setShowPomodoro(false)}
taskId={activePomodoroTaskId}
taskTitle={tasks.find(t => t.id === activePomodoroTaskId)?.title || 'Quick Focus'}
onCompleteTask={(id) => handleTaskUpdate(id, { status: 'done', isTracking: false })}
/>
<Toaster />
<TaskCreationModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSave={handleCreateTask}
/>
<TaskDetailsModal
isOpen={isTaskDetailsOpen}
onClose={handleTaskDetailsClose}
task={selectedTask}
onSave={handleTaskDetailsSave}
labels={labels}
onNavigate={(id) => {
const t = tasks.find(x => x.id === id);
if (t) setSelectedTask(t);
}}
/>
<AlertDialog open={deleteTaskId !== null} onOpenChange={(open) => !open && setDeleteTaskId(null)}>
<AlertDialogContent data-testid="dialog-delete-confirmation">
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteConfirmation.title')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteConfirmation.description')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel data-testid="button-cancel-delete">
{t('deleteConfirmation.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmTaskDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
data-testid="button-confirm-delete"
>
{t('deleteConfirmation.confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</SidebarInset>
</SidebarProvider>
</TooltipProvider>
);
}
return (
<TooltipProvider>
<SidebarProvider>
<AppSidebar user={user} />
<SidebarInset>
<div className="flex flex-col min-h-screen bg-background">
{/* Mobile Header trigger */}
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 md:hidden">
<SidebarTrigger className="-ml-1" />
<div className="font-semibold">{t('app.title')}</div>
</header>
{/* Main Content */}
<main className="flex-1 p-4 md:p-6 max-w-screen-2xl mx-auto w-full relative">
<Switch>
<Route path="/">
<FocusMode
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
onNavigateToTasks={() => setLocation('/tasks')}
/>
</Route>
<Route path="/tasks">
<TasksWithCalendar
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/calendar">
<CalendarView
tasks={tasks}
onTaskDrop={handleTaskDrop}
onTaskClick={handleTaskClick}
onDateSelect={(date) => console.log('Date selected:', date.toLocaleDateString())}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/kanban">
<KanbanBoard
tasks={tasks}
onTaskStatusChange={handleTaskStatusChange}
onTaskUpdate={handleTaskUpdate}
onTaskClick={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/weeklist">
<WeekListView
tasks={tasks}
onTaskUpdate={handleTaskUpdate}
onTaskEdit={handleTaskClick}
onTaskDelete={handleTaskDelete}
onStartTimer={startTimer}
onStopTimer={stopTimer}
/>
</Route>
<Route path="/templates">
<ProjectTemplate
onCreateFromTemplate={handleCreateFromTemplate}
onCreateTasks={(newTasks) => {
queryClient.setQueryData(['/api/tasks'], (old: Task[] = []) => [...old, ...newTasks]);
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
}}
onNavigateToSettings={() => setLocation('/settings')}
/>
</Route>
<Route path="/achievements">
{user ? <AchievementsPage user={user} /> : <AuthPage />}
</Route>
<Route path="/unscheduled">
{user ? (
<UnscheduledTasksPage
user={user}
onToggleCompletion={(taskId, currentStatus) => handleTaskStatusChange(taskId, currentStatus === 'done' ? 'todo' : 'done')}
onDelete={(id) => setDeleteTaskId(id)}
onUpdate={handleTaskUpdate}
onSelect={setSelectedTask}
/>
) : (
<AuthPage />
)}
</Route>
<Route path="/leaderboard">
<LeaderboardPage />
</Route>
<Route path="/ai">
<AiChatPage />
</Route>
<Route path="/settings">
<Settings onNavigateToTemplates={() => setLocation('/templates')} />
</Route>
{/* Admin Route */}
{user.role === 'admin' && (
<>
<Route path="/admin/users" component={AdminUserManagement} />
<Route path="/admin/settings" component={AdminSettings} />
</>
)}
<Route path="/focus/routine/:type" component={FocusRoutinePage} />
<Route component={NotFound} />
</Switch>
</main>
{/* Floating Action Button */}
<div className="fixed bottom-8 right-8 z-[100]">
<Button
size="icon"
className="h-14 w-14 rounded-full shadow-2xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:scale-110 transition-transform duration-200"
onClick={() => setIsCreateModalOpen(true)}
data-testid="fab-create-task"
>
<Plus className="h-6 w-6 text-white" />
</Button>
</div>
</div>
</SidebarInset>
</SidebarProvider>
<CommandPalette
onNavigate={(path) => setLocation(path.startsWith('/') ? path : `/${path}`)}
onCreateTask={() => setIsCreateModalOpen(true)}
/>
<PomodoroOverlay
isOpen={showPomodoro}
onClose={() => setShowPomodoro(false)}
taskId={activePomodoroTaskId}
taskTitle={tasks.find(t => t.id === activePomodoroTaskId)?.title || 'Quick Focus'}
onCompleteTask={(id) => handleTaskUpdate(id, { status: 'done', isTracking: false })}
/>
<Toaster />
<TaskCreationModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSave={handleCreateTask}
/>
<TaskDetailsModal
isOpen={isTaskDetailsOpen}
onClose={handleTaskDetailsClose}
task={selectedTask}
onSave={handleTaskDetailsSave}
labels={labels}
onNavigate={(id) => {
const t = tasks.find(x => x.id === id);
if (t) setSelectedTask(t);
}}
/>
<AlertDialog open={deleteTaskId !== null} onOpenChange={(open) => !open && setDeleteTaskId(null)}>
<AlertDialogContent data-testid="dialog-delete-confirmation">
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteConfirmation.title')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteConfirmation.description')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel data-testid="button-cancel-delete">
{t('deleteConfirmation.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmTaskDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
data-testid="button-confirm-delete"
>
{t('deleteConfirmation.confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</TooltipProvider>
);
}
export default App;
+13 -6
View File
@@ -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) {
</div>
</div>
</div>
<div className="flex items-center gap-1 text-orange-600 bg-orange-500/10 px-2 py-1 rounded-full">
<Flame className="size-3 fill-orange-600" />
<span className="text-xs font-bold">
{t('gamification.streak', { count: streak })}
</span>
</div>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5 px-2 py-1 bg-orange-500/10 rounded-md border border-orange-500/20 cursor-help">
<Flame className="w-4 h-4 text-orange-500 fill-orange-500" />
<span className="text-sm font-bold text-orange-600 dark:text-orange-400">{streak}</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('gamification.streakTooltip', 'Log in daily to increase your streak!')}</p>
<p className="text-xs text-muted-foreground mt-1">{t('gamification.streakBonus', 'Weekly & Monthly bonuses available.')}</p>
</TooltipContent>
</Tooltip>
</div>
<div className="space-y-1">
+128
View File
@@ -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<User>({ queryKey: ["/api/user"] });
const { data: settings } = useQuery<Record<string, string>>({ 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();
}
@@ -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<Record<string, string>>({
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 (
<Card>
<CardHeader>
<CardTitle>{t('settings.routines.title', 'Routine Configuration')}</CardTitle>
<CardDescription>
{t('settings.routines.description', 'Configure global morning and evening routine schedules.')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Morning Routine */}
<div className="flex flex-col gap-4 border-b pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Sun className="h-5 w-5 text-orange-500" />
<Label htmlFor="morning_enabled" className="text-base font-medium">
{t('settings.routines.morningLabel', 'Morning Routine')}
</Label>
</div>
<Switch
id="morning_enabled"
checked={morningEnabled}
onCheckedChange={setMorningEnabled}
disabled={isLoading}
/>
</div>
{morningEnabled && (
<div className="pl-7">
<Label htmlFor="morning_time" className="mb-2 block text-sm text-muted-foreground">{t('settings.routines.time', 'Start Time')}</Label>
<Input
id="morning_time"
type="time"
value={morningTime}
onChange={(e) => setMorningTime(e.target.value)}
className="w-32"
/>
</div>
)}
</div>
{/* Evening Routine */}
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Moon className="h-5 w-5 text-indigo-500" />
<Label htmlFor="evening_enabled" className="text-base font-medium">
{t('settings.routines.eveningLabel', 'Evening Routine')}
</Label>
</div>
<Switch
id="evening_enabled"
checked={eveningEnabled}
onCheckedChange={setEveningEnabled}
disabled={isLoading}
/>
</div>
{eveningEnabled && (
<div className="pl-7">
<Label htmlFor="evening_time" className="mb-2 block text-sm text-muted-foreground">{t('settings.routines.time', 'Start Time')}</Label>
<Input
id="evening_time"
type="time"
value={eveningTime}
onChange={(e) => setEveningTime(e.target.value)}
className="w-32"
/>
</div>
)}
</div>
<div className="pt-4 flex justify-end">
<Button onClick={() => mutation.mutate()} disabled={isLoading || mutation.isPending}>
{mutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{t('settings.routines.save', 'Save Routines')}
</Button>
</div>
</CardContent>
</Card>
);
}
+55 -14
View File
@@ -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",
+29 -7
View File
@@ -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": {
+19 -11
View File
@@ -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) => (
<div key={event.id} className="flex items-center justify-between py-3 border-b last:border-0 hover:bg-muted/50 px-2 rounded-md transition-colors">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-full ${event.source === 'task_completion' ? 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' :
event.source === 'daily_streak' ? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400' :
'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
<div className={`p-2 rounded-full ${event.source === 'complete_task_late' ? 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400' :
(event.source === 'task_completion' || event.source === 'complete_task') ? 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' :
event.source === 'daily_streak' ? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400' :
'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
}`}>
{event.source === 'task_completion' ? <CheckCircle2 className="h-4 w-4" /> :
event.source === 'daily_streak' ? <Flame className="h-4 w-4" /> :
<Trophy className="h-4 w-4" />}
{event.source === 'complete_task_late' ? <Clock className="h-4 w-4" /> :
event.source.includes('complete') ? <CheckCircle2 className="h-4 w-4" /> :
event.source === 'daily_streak' ? <Flame className="h-4 w-4" /> :
<Trophy className="h-4 w-4" />}
</div>
<div>
<p className="font-medium text-sm">
{t(`gamification.source.${event.source}`, { defaultValue: event.source }) as string}
</p>
<p className="text-xs text-muted-foreground">
{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' && <span className="ml-2 text-xs text-red-500 font-normal">({t('gamification.source.complete_task_late')})</span>}
</p>
<div className="text-xs text-muted-foreground flex items-center gap-1">
<span>{new Date(event.createdAt).toLocaleString()}</span>
{event.details?.taskTitle && (
<>
<span></span>
<span>{t(`gamification.source.${event.source}`, { defaultValue: event.source })}</span>
</>
)}
</div>
</div>
</div>
<span className="font-bold text-green-600 dark:text-green-400">
+10
View File
@@ -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() {
<Sparkles className="h-4 w-4" />
{t('settings.admin.tabs.ai')}
</TabsTrigger>
<TabsTrigger value="routines" className="flex items-center gap-2">
<Clock className="h-4 w-4" />
{t('settings.admin.tabs.routines', 'Routines')}
</TabsTrigger>
<TabsTrigger value="mcp" className="flex items-center gap-2">
<Bot className="h-4 w-4" />
{t('settings.admin.tabs.mcp')}
@@ -59,6 +65,10 @@ export default function AdminSettings() {
<AiSettingsCard />
</TabsContent>
<TabsContent value="routines">
<RoutineSettingsCard />
</TabsContent>
<TabsContent value="mcp">
<McpSettingsCard />
</TabsContent>
+10
View File
@@ -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');
}
};