Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import DocumentIntelligence from '@azure-rest/ai-document-intelligence';
import { AzureKeyCredential } from '@azure/core-auth';
import { getLongRunningPoller, isUnexpected } from '@azure-rest/ai-document-intelligence';
import { AzureOpenAI } from 'openai';
import { getPointsForPriority } from '../../../../lib/gamification';

/**
* Azure Document Intelligence + Foundry (GPT-5) Pipeline
Expand Down Expand Up @@ -295,6 +296,7 @@ export async function POST(req: NextRequest) {
completed: false,
task_type: t.type || 'other', // Use task_type instead of topic
topic: t.type || null, // Also set topic for compatibility
points: getPointsForPriority(t.priority),
}));


Expand Down
172 changes: 156 additions & 16 deletions taskmaster-client/src/client-pages/dashboard/StatsWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
import React, { useState, useEffect } from "react";
import { CheckSquare, Zap, TrendingUp, X } from "lucide-react";
import { CheckSquare, Trophy, Zap, TrendingUp, X } from "lucide-react";
import { apiService } from "../../services/api";
import { useUser } from "../../context/UserContext";
import type { TasksData } from "../../services/types";
import { RANKS, getRankProgress } from "../../lib/gamification";

interface StatsWidgetProps {
tasks: TasksData[];
isLoading?: boolean;
user?: any;
}

type StatItem = {
label: string;
value: string;
change: string;
icon: React.ElementType;
color: string;
onClick?: () => void;
changeClass?: string;
progress?: number;
progressLabel?: string;
progressLeftLabel?: string;
progressRightLabel?: string;
progressBarColor?: string;
};

export const StatsWidget: React.FC<StatsWidgetProps> = ({ tasks = [], isLoading = false, user }) => {
const { user: contextUser } = useUser();
const currentUser = user || contextUser;
const [showStreakModal, setShowStreakModal] = useState(false);
const [showRankModal, setShowRankModal] = useState(false);
const [loginDates, setLoginDates] = useState<string[]>([]);
const [streak, setStreak] = useState(0);
const [isLoadingStreak, setIsLoadingStreak] = useState(false);
Expand Down Expand Up @@ -88,10 +105,38 @@ export const StatsWidget: React.FC<StatsWidgetProps> = ({ tasks = [], isLoading
const last30Days = getLast30Days();
const loginDatesSet = new Set(loginDates.map(d => new Date(d).toISOString().split('T')[0]));

const userPoints = currentUser?.points || 0;
const userLevel = currentUser?.level || 1;
const taskPoints = tasks.reduce((total, task) => {
const isComplete = task.completed || task.status === "completed";
if (!isComplete) return total;
const points = task.earnedPoints ?? task.points ?? 0;
return total + (Number.isFinite(points) ? points : 0);
}, 0);
const userPoints = Math.max(taskPoints, currentUser?.points || 0);
const { currentRank, nextRank, progress, targetPoints, pointsToNext } = getRankProgress(userPoints);
const getRankStyle = (rankName?: string) => {
switch (rankName) {
case "Bronze":
return { text: "text-amber-700", bar: "bg-amber-700/70", chip: "bg-amber-700/10 text-amber-700 border-amber-700/30" };
case "Silver":
return { text: "text-gray-400", bar: "bg-gray-400/70", chip: "bg-gray-400/10 text-gray-400 border-gray-400/30" };
case "Gold":
return { text: "text-yellow-500", bar: "bg-yellow-500/70", chip: "bg-yellow-500/10 text-yellow-500 border-yellow-500/30" };
case "Diamond":
return { text: "text-blue-400", bar: "bg-blue-400/70", chip: "bg-blue-400/10 text-blue-400 border-blue-400/30" };
case "Study Expert":
return { text: "text-emerald-500", bar: "bg-emerald-500/70", chip: "bg-emerald-500/10 text-emerald-500 border-emerald-500/30" };
case "Task Master":
return { text: "text-rose-500", bar: "bg-rose-500/70", chip: "bg-rose-500/10 text-rose-500 border-rose-500/30" };
default:
return { text: "text-yellow-500", bar: "bg-yellow-500/70", chip: "bg-yellow-500/10 text-yellow-500 border-yellow-500/30" };
}
};
const rankStyle = getRankStyle(currentRank.name);
const pointsValue = userPoints.toString();
const pointsTarget = nextRank ? `${targetPoints}` : `${userPoints}`;
const modalPointsLabel = nextRank ? `${userPoints} / ${targetPoints}` : `${userPoints}`;

const stats = [
const stats: StatItem[] = [
{
label: "Due Today",
value: isLoading ? "..." : dueTodayTotal.toString(),
Expand All @@ -110,11 +155,16 @@ export const StatsWidget: React.FC<StatsWidgetProps> = ({ tasks = [], isLoading
},
{
label: "Points",
value: isLoading ? "..." : userPoints.toString(),
change: `Level ${userLevel}`,
icon: Zap,
color: "text-yellow-500",
onClick: undefined
value: isLoading ? "..." : pointsValue,
change: `Rank ${currentRank.name}`,
icon: Trophy,
color: rankStyle.text,
onClick: () => setShowRankModal(true),
changeClass: "text-muted-foreground",
progress: isLoading ? undefined : progress,
progressLeftLabel: `${userPoints}`,
progressRightLabel: pointsTarget,
progressBarColor: rankStyle.bar,
},
{
label: "Streak",
Expand All @@ -130,7 +180,8 @@ export const StatsWidget: React.FC<StatsWidgetProps> = ({ tasks = [], isLoading
change: dueTodayTotal > 0 ? `${dueTodayCompleted}/${dueTodayTotal} due today` : "No tasks due today",
icon: TrendingUp,
color: "text-purple-500",
onClick: undefined
onClick: undefined,
changeClass: dueTodayTotal > 0 ? "text-green-500" : "text-muted-foreground",
},
];

Expand All @@ -139,20 +190,43 @@ export const StatsWidget: React.FC<StatsWidgetProps> = ({ tasks = [], isLoading
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
{stats.map((stat) => {
const Icon = stat.icon;
const isPoints = stat.label === "Points";
return (
<div
key={stat.label}
className={`bg-card border border-border rounded-md p-4 flex items-center justify-between hover:border-primary/50 transition-colors ${stat.onClick ? 'cursor-pointer' : ''}`}
className={`bg-card border border-border rounded-md p-4 ${isPoints ? 'relative' : 'flex items-center justify-between'} hover:border-primary/50 transition-colors ${stat.onClick ? 'cursor-pointer' : ''}`}
onClick={stat.onClick}
>
<div>
<div className={isPoints ? "w-full pr-12" : ""}>
<p className="text-sm text-muted-foreground font-medium">{stat.label}</p>
<h3 className="text-2xl font-bold text-foreground mt-1">{stat.value}</h3>
<span className={`text-xs mt-1 block ${stat.change.includes("pending") || stat.change.includes("overdue") || stat.change.includes("Keep it going") ? "text-muted-foreground" : "text-green-500"}`}>
{stat.change}
</span>
{isPoints && (
<div className={`text-lg font-semibold mt-1 ${stat.color}`}>
{stat.change}
</div>
)}
{typeof stat.progress === "number" && (
<div className="mt-2">
{stat.progressRightLabel && (
<div className="text-[11px] text-muted-foreground flex justify-end">
<span>{stat.progressRightLabel}</span>
</div>
)}
<div className="mt-1 h-2 rounded-full bg-secondary overflow-hidden border border-border relative">
<div
className={`h-full ${stat.progressBarColor}`}
style={{ width: `${Math.round(stat.progress * 100)}%` }}
/>
</div>
</div>
)}
{!isPoints && (
<span className={`text-xs mt-2 block ${stat.changeClass || "text-muted-foreground"}`}>
{stat.change}
</span>
)}
</div>
<div className={`p-3 rounded-full bg-secondary ${stat.color}`}>
<div className={`p-3 rounded-full bg-secondary ${stat.color} ${isPoints ? "absolute right-4 top-4" : ""}`}>
<Icon size={24} />
</div>
</div>
Expand Down Expand Up @@ -235,6 +309,72 @@ export const StatsWidget: React.FC<StatsWidgetProps> = ({ tasks = [], isLoading
</div>
</div>
)}

{/* Rank Modal */}
{showRankModal && (
<div className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center px-4">
<div className="w-full max-w-2xl bg-card border border-border rounded-md p-6 shadow-xl">
<div className="flex justify-between items-center mb-4">
<div className="flex-1 pr-3">
<h2 className="text-2xl font-bold text-foreground">Rank Progress</h2>
<p className="text-sm text-muted-foreground">
{nextRank ? `${pointsToNext} points to reach ${nextRank.name}` : "You reached the top rank"}
</p>
</div>
<button
onClick={() => setShowRankModal(false)}
className="text-muted-foreground hover:text-foreground"
>
<X size={24} />
</button>
</div>

<div className="mb-5">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>{modalPointsLabel} points</span>
<span>{currentRank.name}</span>
</div>
<div className="mt-2 h-3 w-full rounded-full bg-secondary overflow-hidden border border-border">
<div
className={`h-full ${rankStyle.bar}`}
style={{ width: `${Math.round(progress * 100)}%` }}
/>
</div>
</div>

<div className="space-y-2">
{RANKS.map((rank, index) => {
const next = RANKS[index + 1];
const maxPoints = next ? next.minPoints - 1 : null;
const rangeLabel = maxPoints !== null ? `${rank.minPoints} - ${maxPoints}` : `${rank.minPoints}+`;
const isCurrent = rank.name === currentRank.name;
const style = getRankStyle(rank.name);
return (
<div
key={rank.name}
className={`flex items-center justify-between rounded-md border px-4 py-3 ${
isCurrent ? "border-primary/50 bg-primary/5" : "border-border"
}`}
>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-full bg-secondary ${style.text}`}>
<Trophy size={18} />
</div>
<div>
<div className="text-sm font-semibold text-foreground">{rank.name}</div>
<div className="text-xs text-muted-foreground">{rangeLabel} pts</div>
</div>
</div>
{isCurrent && (
<span className={`text-xs border rounded-full px-2 py-1 ${style.chip}`}>Current</span>
)}
</div>
);
})}
</div>
</div>
</div>
)}
</>
);
};
62 changes: 47 additions & 15 deletions taskmaster-client/src/client-pages/tasks/TasksPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@ import { TaskTimeline } from "../../components/tasks/TaskTimeline";
import { TaskFilters } from "../../components/tasks/TaskFilters";
import { TaskViewToggle } from "../../components/tasks/TaskViewToggle";
import type { TasksData, ClassData } from "../../services/types";
import { calculateEarnedPoints } from "../../lib/gamification";

const TasksPage: React.FC = () => {
const { user, isLoadingUser } = useUser();
const { user, isLoadingUser, setUserState } = useUser();
const router = useRouter();
const searchParams = useSearchParams();
const [tasks, setTasks] = useState<TasksData[]>([]);
const [classes, setClasses] = useState<ClassData[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [editingTaskId, setEditingTaskId] = useState<string | null>(null);
const [filter, setFilter] = useState<"today" | "upcoming" | "history">("today");
const [filter, setFilter] = useState<"today" | "upcoming" | "overdue" | "unscheduled" | "history">("today");
const [viewMode, setViewMode] = useState<"list" | "timeline">("list");
const [error, setError] = useState<string | null>(null);

Expand Down Expand Up @@ -92,12 +93,37 @@ const TasksPage: React.FC = () => {
};

const handleToggleComplete = async (taskId: string, completed: boolean) => {
const targetTask = tasks.find((task) => task._id === taskId);
if (!targetTask) return;
const taskPoints = Number.isFinite(targetTask.points ?? 0) ? (targetTask.points ?? 0) : 0;
const earnedPoints = completed
? calculateEarnedPoints(taskPoints, targetTask.deadline)
: 0;
const status = completed ? 'completed' : 'pending';
const previousTasks = tasks;

setTasks(prev => prev.map(t =>
t._id === taskId ? { ...t, completed, status, earnedPoints } : t
));

try {
const status = completed ? 'completed' : 'pending';
await apiService.updateTask(taskId, { completed, status });
setTasks(prev => prev.map(t =>
t._id === taskId ? { ...t, completed, status } : t
));
await apiService.updateTask(taskId, { completed, status, earnedPoints });

if (taskPoints > 0 && targetTask) {
const previousEarned = Number.isFinite(targetTask.earnedPoints ?? taskPoints)
? targetTask.earnedPoints ?? taskPoints
: taskPoints;
const pointsDelta = completed ? earnedPoints : -1 * previousEarned;

if (pointsDelta !== 0) {
try {
const nextPoints = await apiService.updateUserPoints(pointsDelta);
setUserState({ points: nextPoints });
} catch (pointsError) {
console.error("Error updating points:", pointsError);
}
}
}

// Notify other components
const { taskEvents } = await import('../../lib/taskEvents');
Expand All @@ -109,6 +135,7 @@ const TasksPage: React.FC = () => {
}
} catch (error: any) {
console.error("Error updating task:", error);
setTasks(previousTasks);
setError(error.message || "Failed to update task");
}
};
Expand All @@ -131,20 +158,25 @@ const TasksPage: React.FC = () => {
return date >= endOfDay;
};

const isOverdue = (deadline?: string) => {
if (!deadline) return false;
const date = new Date(deadline);
const isOverdueTask = (task: TasksData) => {
const isComplete = task.completed || task.status === "completed";
if (isComplete) return false;
if (task.status === "overdue") return true;
if (!task.deadline) return false;
const date = new Date(task.deadline);
return date < startOfDay;
};

const dueTodayCount = tasks.filter(t => t.deadline && isToday(t.deadline)).length;
const overdueCount = tasks.filter(
t => !t.completed && t.deadline && isOverdue(t.deadline)
).length;
const isUnscheduledTask = (task: TasksData) => {
const isComplete = task.completed || task.status === "completed";
return !task.deadline && !isComplete;
};

return {
today: dueTodayCount + overdueCount,
today: tasks.filter(t => t.deadline && isToday(t.deadline)).length,
upcoming: tasks.filter(t => !t.completed && isUpcoming(t.deadline)).length,
overdue: tasks.filter(t => isOverdueTask(t)).length,
unscheduled: tasks.filter(t => isUnscheduledTask(t)).length,
history: tasks.filter(t => t.completed || t.status === "completed").length,
};
}, [tasks]);
Expand Down
10 changes: 7 additions & 3 deletions taskmaster-client/src/components/tasks/TaskFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@
import React from 'react';

interface TaskFiltersProps {
currentFilter: "today" | "upcoming" | "history";
onFilterChange: (filter: "today" | "upcoming" | "history") => void;
currentFilter: "today" | "upcoming" | "overdue" | "unscheduled" | "history";
onFilterChange: (filter: "today" | "upcoming" | "overdue" | "unscheduled" | "history") => void;
counts: {
today: number;
upcoming: number;
overdue: number;
unscheduled: number;
history: number;
};
}

export const TaskFilters: React.FC<TaskFiltersProps> = ({ currentFilter, onFilterChange, counts }) => {
const filters: Array<{ value: "today" | "upcoming" | "history"; label: string }> = [
const filters: Array<{ value: "today" | "upcoming" | "overdue" | "unscheduled" | "history"; label: string }> = [
{ value: "today", label: "Today" },
{ value: "upcoming", label: "Upcoming" },
{ value: "overdue", label: "Overdue" },
{ value: "unscheduled", label: "Unscheduled" },
{ value: "history", label: "History" },
];

Expand Down
Loading