diff --git a/taskmaster-client/src/app/api/documents/analyze-azure/route.ts b/taskmaster-client/src/app/api/documents/analyze-azure/route.ts index 9fc4e2d..892ffe0 100644 --- a/taskmaster-client/src/app/api/documents/analyze-azure/route.ts +++ b/taskmaster-client/src/app/api/documents/analyze-azure/route.ts @@ -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 @@ -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), })); diff --git a/taskmaster-client/src/client-pages/dashboard/StatsWidget.tsx b/taskmaster-client/src/client-pages/dashboard/StatsWidget.tsx index a356c71..f33d2cd 100644 --- a/taskmaster-client/src/client-pages/dashboard/StatsWidget.tsx +++ b/taskmaster-client/src/client-pages/dashboard/StatsWidget.tsx @@ -1,8 +1,9 @@ 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[]; @@ -10,10 +11,26 @@ interface StatsWidgetProps { 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 = ({ 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([]); const [streak, setStreak] = useState(0); const [isLoadingStreak, setIsLoadingStreak] = useState(false); @@ -88,10 +105,38 @@ export const StatsWidget: React.FC = ({ 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(), @@ -110,11 +155,16 @@ export const StatsWidget: React.FC = ({ 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", @@ -130,7 +180,8 @@ export const StatsWidget: React.FC = ({ 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", }, ]; @@ -139,20 +190,43 @@ export const StatsWidget: React.FC = ({ tasks = [], isLoading
{stats.map((stat) => { const Icon = stat.icon; + const isPoints = stat.label === "Points"; return (
-
+

{stat.label}

{stat.value}

- - {stat.change} - + {isPoints && ( +
+ {stat.change} +
+ )} + {typeof stat.progress === "number" && ( +
+ {stat.progressRightLabel && ( +
+ {stat.progressRightLabel} +
+ )} +
+
+
+
+ )} + {!isPoints && ( + + {stat.change} + + )}
-
+
@@ -235,6 +309,72 @@ export const StatsWidget: React.FC = ({ tasks = [], isLoading
)} + + {/* Rank Modal */} + {showRankModal && ( +
+
+
+
+

Rank Progress

+

+ {nextRank ? `${pointsToNext} points to reach ${nextRank.name}` : "You reached the top rank"} +

+
+ +
+ +
+
+ {modalPointsLabel} points + {currentRank.name} +
+
+
+
+
+ +
+ {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 ( +
+
+
+ +
+
+
{rank.name}
+
{rangeLabel} pts
+
+
+ {isCurrent && ( + Current + )} +
+ ); + })} +
+
+
+ )} ); }; diff --git a/taskmaster-client/src/client-pages/tasks/TasksPage.tsx b/taskmaster-client/src/client-pages/tasks/TasksPage.tsx index 821dd2c..3644fd4 100644 --- a/taskmaster-client/src/client-pages/tasks/TasksPage.tsx +++ b/taskmaster-client/src/client-pages/tasks/TasksPage.tsx @@ -14,9 +14,10 @@ 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([]); @@ -24,7 +25,7 @@ const TasksPage: React.FC = () => { const [isLoading, setIsLoading] = useState(true); const [showModal, setShowModal] = useState(false); const [editingTaskId, setEditingTaskId] = useState(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(null); @@ -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'); @@ -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"); } }; @@ -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]); diff --git a/taskmaster-client/src/components/tasks/TaskFilters.tsx b/taskmaster-client/src/components/tasks/TaskFilters.tsx index b2cd00c..80c0b70 100644 --- a/taskmaster-client/src/components/tasks/TaskFilters.tsx +++ b/taskmaster-client/src/components/tasks/TaskFilters.tsx @@ -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 = ({ 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" }, ]; diff --git a/taskmaster-client/src/components/tasks/TaskList.tsx b/taskmaster-client/src/components/tasks/TaskList.tsx index 8c5e7a1..a97aba9 100644 --- a/taskmaster-client/src/components/tasks/TaskList.tsx +++ b/taskmaster-client/src/components/tasks/TaskList.tsx @@ -1,12 +1,12 @@ import React from "react"; import { Edit2, Trash2 } from "lucide-react"; import type { TasksData, ClassData } from "../../services/types"; -import { getClassColor } from "../../utils/classColors"; +import { calculateEarnedPoints } from "../../lib/gamification"; interface TaskListProps { tasks: TasksData[]; classes: ClassData[]; - filter: "today" | "upcoming" | "history"; + filter: "today" | "upcoming" | "overdue" | "unscheduled" | "history"; onEdit: (task: TasksData) => void; onDelete: (taskId: string) => void; onToggleComplete?: (taskId: string, completed: boolean) => void; @@ -22,6 +22,9 @@ export const TaskList: React.FC = ({ }) => { const [completingTaskId, setCompletingTaskId] = React.useState(null); const [selectedHistoryDate, setSelectedHistoryDate] = React.useState(null); + const [rewardedTaskId, setRewardedTaskId] = React.useState(null); + const [rewardedPoints, setRewardedPoints] = React.useState(0); + const rewardTimeoutRef = React.useRef | null>(null); const now = new Date(); const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); @@ -39,24 +42,36 @@ export const TaskList: React.FC = ({ return date >= endOfDay; }; - const isOverdue = (deadline?: string) => { - if (!deadline) return false; - const date = new Date(deadline); + const isOverdue = (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 isUnscheduled = (task: TasksData) => { + const isComplete = task.completed || task.status === "completed"; + return !task.deadline && !isComplete; + }; + const todayTasks = tasks .filter((task) => task.deadline && isToday(task.deadline)) .sort((a, b) => new Date(a.deadline!).getTime() - new Date(b.deadline!).getTime()); - const overdueTasks = tasks - .filter((task) => !task.completed && task.deadline && isOverdue(task.deadline)) - .sort((a, b) => new Date(a.deadline!).getTime() - new Date(b.deadline!).getTime()); - const upcomingTasks = tasks .filter((task) => !task.completed && isUpcoming(task.deadline)) .sort((a, b) => new Date(a.deadline!).getTime() - new Date(b.deadline!).getTime()); + const overdueTasks = tasks + .filter(isOverdue) + .sort((a, b) => taskDateSortValue(a.deadline) - taskDateSortValue(b.deadline)); + + const unscheduledTasks = tasks + .filter(isUnscheduled) + .sort((a, b) => a.title.localeCompare(b.title)); + const historyTasks = tasks .filter((task) => task.completed || task.status === "completed") .sort((a, b) => { @@ -66,7 +81,25 @@ export const TaskList: React.FC = ({ }); const filteredTasks = - filter === "today" ? [...overdueTasks, ...todayTasks] : filter === "upcoming" ? upcomingTasks : historyTasks; + filter === "today" + ? todayTasks + : filter === "upcoming" + ? upcomingTasks + : filter === "overdue" + ? overdueTasks + : filter === "unscheduled" + ? unscheduledTasks + : historyTasks; + + const displayTasks = (() => { + if (!rewardedTaskId) return filteredTasks; + const rewardedTask = tasks.find((task) => task._id === rewardedTaskId); + if (!rewardedTask) return filteredTasks; + if (filteredTasks.some((task) => task._id === rewardedTask._id)) { + return filteredTasks; + } + return [rewardedTask, ...filteredTasks]; + })(); function taskDateSortValue(deadline?: string) { if (!deadline) return 0; @@ -75,11 +108,30 @@ export const TaskList: React.FC = ({ const handleToggle = async (task: TasksData) => { if (!onToggleComplete) return; + const wasComplete = task.completed || task.status === "completed"; + const points = Number.isFinite(task.points ?? 0) ? (task.points ?? 0) : 0; + const earnedPoints = calculateEarnedPoints(points, task.deadline); + if (!wasComplete && earnedPoints > 0) { + if (rewardTimeoutRef.current) { + clearTimeout(rewardTimeoutRef.current); + } + setRewardedTaskId(task._id); + setRewardedPoints(earnedPoints); + rewardTimeoutRef.current = setTimeout(() => { + setRewardedTaskId(null); + }, 1100); + } setCompletingTaskId(task._id); await onToggleComplete(task._id, !task.completed); setTimeout(() => setCompletingTaskId(null), 600); }; + React.useEffect(() => { + return () => { + if (rewardTimeoutRef.current) clearTimeout(rewardTimeoutRef.current); + }; + }, []); + React.useEffect(() => { if (filter !== "history") return; if (selectedHistoryDate) return; @@ -98,7 +150,7 @@ export const TaskList: React.FC = ({ if (next) setSelectedHistoryDate(next); }, [filter, historyTasks, selectedHistoryDate]); - if (filteredTasks.length === 0) { + if (displayTasks.length === 0) { return (

No tasks found. Create your first task!

@@ -164,15 +216,9 @@ export const TaskList: React.FC = ({ return (
-
- -

- {taskClass?.name || "Personal"} -

-
+

+ {taskClass?.name || "Personal"} +

{classTasks.map((task) => (
= ({ className="flex items-center justify-between p-3 bg-background rounded-md border border-border" >
-
+

{task.title} @@ -222,121 +265,89 @@ export const TaskList: React.FC = ({ ); } - const renderTaskRow = (task: TasksData, highlightOverdue: boolean = false) => { - const taskClass = task.class ? classes.find((c) => c._id === task.class) : null; - const isTaskOverdue = !task.completed && task.deadline && isOverdue(task.deadline); - return ( -

-
- -
- {isTaskOverdue && ( - - OVERDUE - - )} -

- {task.title} -

-

- - {taskClass?.name || "Personal"} • Due{" "} - {task.deadline - ? new Date(task.deadline).toLocaleDateString() - : "No deadline"} -

-
-
-
- - -
-
- ); - }; - - if (filter === "today") { - return ( -
-
-

Due Today

- {todayTasks.length === 0 ? ( -
- No tasks due today. + {task.completed ? ( + + + + ) : ( +
+ )} + +
+ {/* Overdue badge */} + {!task.completed && task.deadline && new Date(task.deadline) < new Date() && ( + + OVERDUE + + )} +

+ {task.title} + {rewardedTaskId === task._id && rewardedPoints > 0 && ( + + +{rewardedPoints} pts + + )} +

+

+ {taskClass?.name || "Personal"} • Due{" "} + {task.deadline + ? new Date(task.deadline).toLocaleDateString() + : "No deadline"} +

+
- ) : ( - todayTasks.map((task) => renderTaskRow(task)) - )} -
-
-

Overdue

- {overdueTasks.length === 0 ? ( -
- No overdue tasks. +
+ +
- ) : ( - overdueTasks.map((task) => renderTaskRow(task, true)) - )} -
-
- ); - } - - return ( -
- {filteredTasks.map((task) => renderTaskRow(task))} +
+ ); + })}
); }; diff --git a/taskmaster-client/src/lib/gamification.ts b/taskmaster-client/src/lib/gamification.ts new file mode 100644 index 0000000..c34227c --- /dev/null +++ b/taskmaster-client/src/lib/gamification.ts @@ -0,0 +1,112 @@ +export type TaskPriority = "high" | "medium" | "low"; + +export const PRIORITY_POINTS: Record = { + high: 50, + medium: 30, + low: 10, +}; + +export const RANKS = [ + { name: "Bronze", minPoints: 0 }, + { name: "Silver", minPoints: 1500 }, + { name: "Gold", minPoints: 4000 }, + { name: "Diamond", minPoints: 8000 }, + { name: "Study Expert", minPoints: 15000 }, + { name: "Task Master", minPoints: 25000 }, +]; + +export const getPointsForPriority = (priority?: string | null): number => { + if (!priority) return PRIORITY_POINTS.low; + if (priority === "high" || priority === "medium" || priority === "low") { + return PRIORITY_POINTS[priority]; + } + return PRIORITY_POINTS.low; +}; + +export const getRankForPoints = (points: number) => { + const safePoints = Number.isFinite(points) ? Math.max(0, points) : 0; + let current = RANKS[0]; + + for (const rank of RANKS) { + if (safePoints >= rank.minPoints) { + current = rank; + } else { + break; + } + } + + const currentIndex = RANKS.findIndex((rank) => rank.name === current.name); + const nextRank = currentIndex >= 0 ? RANKS[currentIndex + 1] : undefined; + const maxPoints = nextRank ? nextRank.minPoints - 1 : null; + + return { ...current, maxPoints }; +}; + +export const getRankProgress = (points: number) => { + const safePoints = Number.isFinite(points) ? Math.max(0, points) : 0; + const currentRank = getRankForPoints(safePoints); + const currentIndex = RANKS.findIndex((rank) => rank.name === currentRank.name); + const nextRank = currentIndex >= 0 ? RANKS[currentIndex + 1] : undefined; + const targetPoints = nextRank ? nextRank.minPoints : safePoints; + const rankSpan = nextRank ? nextRank.minPoints - currentRank.minPoints : 0; + const progress = nextRank && rankSpan > 0 + ? Math.min(1, (safePoints - currentRank.minPoints) / rankSpan) + : 1; + + return { + currentRank, + nextRank, + progress, + targetPoints, + pointsToNext: nextRank ? Math.max(0, nextRank.minPoints - safePoints) : 0, + }; +}; + +const hasTimezoneInfo = (deadlineStr: string) => /Z$|[+-]\d{2}:\d{2}$/.test(deadlineStr); + +export const parseTaskDeadline = (deadlineStr: string) => { + if (deadlineStr.includes("T") && !hasTimezoneInfo(deadlineStr)) { + const [datePart, timePart = "00:00"] = deadlineStr.split("T"); + const [year, month, day] = datePart.split("-").map(Number); + const [hours, minutes] = timePart.split(":").map(Number); + return new Date(year, month - 1, day, hours || 0, minutes || 0, 0, 0); + } + + if (!deadlineStr.includes("T")) { + const [year, month, day] = deadlineStr.split("-").map(Number); + return new Date(year, month - 1, day, 0, 0, 0, 0); + } + + return new Date(deadlineStr); +}; + +const startOfDay = (date: Date) => + new Date(date.getFullYear(), date.getMonth(), date.getDate()); + +export const calculateEarnedPoints = ( + basePoints: number, + deadline?: string | null, + completedAt: Date = new Date(), +): number => { + const safeBase = Number.isFinite(basePoints) ? Math.max(0, basePoints) : 0; + if (!deadline || safeBase === 0) return safeBase; + + const dueDate = parseTaskDeadline(deadline); + const completedDate = startOfDay(completedAt); + const dueDay = startOfDay(dueDate); + const msPerDay = 24 * 60 * 60 * 1000; + const dayDiff = Math.floor((dueDay.getTime() - completedDate.getTime()) / msPerDay); + + if (dayDiff > 0) { + const bonusMultiplier = Math.min(1.5, 1 + dayDiff * 0.05); + return Math.round(safeBase * bonusMultiplier); + } + + if (dayDiff < 0) { + const overdueDays = Math.abs(dayDiff); + const penaltyMultiplier = Math.max(0.5, 1 - overdueDays * 0.1); + return Math.round(safeBase * penaltyMultiplier); + } + + return safeBase; +}; diff --git a/taskmaster-client/src/services/api/index.ts b/taskmaster-client/src/services/api/index.ts index cbf6d0c..b8e8882 100644 --- a/taskmaster-client/src/services/api/index.ts +++ b/taskmaster-client/src/services/api/index.ts @@ -100,6 +100,7 @@ class ApiService { // Users/Friends findUsers = userService.findUsers; getFriendsFromUserService = userService.getFriends; + updateUserPoints = userService.updatePoints; sendFriendRequest = userService.sendFriendRequest; getIncomingRequests = userService.getIncomingRequests; getOutgoingRequests = userService.getOutgoingRequests; diff --git a/taskmaster-client/src/services/api/taskService.ts b/taskmaster-client/src/services/api/taskService.ts index 5220193..c65a1c3 100644 --- a/taskmaster-client/src/services/api/taskService.ts +++ b/taskmaster-client/src/services/api/taskService.ts @@ -151,6 +151,7 @@ export const taskService = { if (updates.deadline !== undefined) updateData.deadline = updates.deadline; if (updates.textbook !== undefined) updateData.textbook = updates.textbook; if (updates.completed !== undefined) updateData.completed = updates.completed; + if (updates.earnedPoints !== undefined) updateData.earned_points = updates.earnedPoints; const { data, error } = await supabaseClient .from('tasks') diff --git a/taskmaster-client/src/services/api/userService.ts b/taskmaster-client/src/services/api/userService.ts index 63287ee..2b884eb 100644 --- a/taskmaster-client/src/services/api/userService.ts +++ b/taskmaster-client/src/services/api/userService.ts @@ -125,6 +125,37 @@ async function findSortYears( export const userService = { + async updatePoints(delta: number): Promise { + const userId = await getCachedUserId(); + + const { data: current, error: fetchError } = await supabase + .from('users') + .select('points') + .eq('id', userId) + .single(); + + if (fetchError) throw new Error(fetchError.message); + + const nextPoints = Math.max(0, (current?.points || 0) + delta); + + const { data: updated, error: updateError } = await supabase + .from('users') + .update({ points: nextPoints }) + .eq('id', userId) + .select('points') + .single(); + + if (updateError) throw new Error(updateError.message); + + return updated?.points ?? nextPoints; + }, + + async matchFriends(userId: string): Promise<{ users: string[] }> { + // Find users with similar preferences for friend matching + const { data: currentUser, error: userError } = await supabase + .from('users') + .select('personality, time_preference, in_person, private_space') + .eq('id', userId) async findUsers(userId: string): Promise<{ users: { _id: string; displayName: string; requestSent?: boolean }[] }> { console.log("🎯 findUsers called for userId:", userId);