Progress Tracking
+Monitor your learning journey and achieve your goals
+ + +0
+Study Hours
+0
+Goals Completed
+0
+Day Streak
+0
+Points Earned
+diff --git a/.gitignore b/.gitignore index 32e0c029..767de96c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,6 @@ Thumbs.db # Windows # Build and output directories /dist /build -/out \ No newline at end of file +/out + +db.sqlite3 \ No newline at end of file diff --git a/core/admin.py b/core/admin.py index 8e640ecb..28513871 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from .models import FAQ -from .models import Contact, SuccessStory, StoryReaction, Question, Answer, QuestionUpvote, AnswerUpvote +from .models import Contact, SuccessStory, StoryReaction, Question, Answer, QuestionUpvote, AnswerUpvote,Goal,Milestone,StudySession,Achievement,UserStats,WeeklyGoal @admin.register(Contact) class ContactAdmin(admin.ModelAdmin): @@ -140,4 +140,153 @@ def answer_author(self, obj): @admin.register(FAQ) class FAQAdmin(admin.ModelAdmin): - list_display = ('question', 'answer') \ No newline at end of file + list_display = ('question', 'answer') + +# Add these admin configurations to your core/admin.py file + +@admin.register(Goal) +class GoalAdmin(admin.ModelAdmin): + list_display = ('title', 'user', 'category', 'priority', 'status', 'progress_percentage', 'target_date', 'is_completed', 'created_at') + list_filter = ('category', 'priority', 'status', 'is_completed', 'created_at') + search_fields = ('title', 'description', 'user__username', 'user__first_name', 'user__last_name') + readonly_fields = ('created_at', 'updated_at', 'completed_at') + list_editable = ('status', 'progress_percentage') + ordering = ('-created_at',) + + fieldsets = ( + ('Goal Information', { + 'fields': ('user', 'title', 'description', 'category', 'priority') + }), + ('Progress', { + 'fields': ('status', 'progress_percentage', 'is_completed', 'target_date') + }), + ('Timestamps', { + 'fields': ('created_at', 'updated_at', 'completed_at'), + 'classes': ('collapse',) + }), + ) + +@admin.register(Milestone) +class MilestoneAdmin(admin.ModelAdmin): + list_display = ('title', 'goal', 'goal_user', 'is_completed', 'due_date', 'created_at') + list_filter = ('is_completed', 'due_date', 'created_at') + search_fields = ('title', 'description', 'goal__title', 'goal__user__username') + readonly_fields = ('created_at', 'completed_at') + list_editable = ('is_completed',) + ordering = ('-created_at',) + + def goal_user(self, obj): + return obj.goal.user.username + goal_user.short_description = 'Goal Owner' + + fieldsets = ( + ('Milestone Information', { + 'fields': ('goal', 'title', 'description', 'due_date') + }), + ('Status', { + 'fields': ('is_completed',) + }), + ('Timestamps', { + 'fields': ('created_at', 'completed_at'), + 'classes': ('collapse',) + }), + ) + +@admin.register(StudySession) +class StudySessionAdmin(admin.ModelAdmin): + list_display = ('title', 'user', 'subject', 'duration_minutes', 'productivity_rating', 'date', 'created_at') + list_filter = ('subject', 'date', 'productivity_rating', 'created_at') + search_fields = ('title', 'description', 'notes', 'user__username', 'user__first_name', 'user__last_name') + readonly_fields = ('created_at',) + ordering = ('-date', '-created_at') + + fieldsets = ( + ('Session Information', { + 'fields': ('user', 'subject', 'title', 'description', 'date') + }), + ('Performance', { + 'fields': ('duration_minutes', 'productivity_rating') + }), + ('Additional', { + 'fields': ('goals_related', 'notes'), + 'classes': ('collapse',) + }), + ('Timestamps', { + 'fields': ('created_at',), + 'classes': ('collapse',) + }), + ) + +@admin.register(Achievement) +class AchievementAdmin(admin.ModelAdmin): + list_display = ('title', 'user', 'achievement_type', 'points', 'is_earned', 'earned_at') + list_filter = ('achievement_type', 'is_earned', 'earned_at') + search_fields = ('title', 'description', 'user__username', 'user__first_name', 'user__last_name') + readonly_fields = ('earned_at',) + list_editable = ('is_earned',) + ordering = ('-earned_at',) + + fieldsets = ( + ('Achievement Information', { + 'fields': ('user', 'title', 'description', 'achievement_type', 'icon') + }), + ('Rewards', { + 'fields': ('points', 'is_earned', 'related_goal') + }), + ('Timestamps', { + 'fields': ('earned_at',) + }), + ) + +@admin.register(UserStats) +class UserStatsAdmin(admin.ModelAdmin): + list_display = ('user', 'total_study_hours', 'total_goals_completed', 'current_streak_days', 'achievement_points', 'last_activity_date') + search_fields = ('user__username', 'user__first_name', 'user__last_name') + readonly_fields = ('created_at', 'updated_at') + ordering = ('-total_study_hours',) + + fieldsets = ( + ('User', { + 'fields': ('user',) + }), + ('Study Statistics', { + 'fields': ('total_study_hours', 'current_streak_days', 'longest_streak_days', 'last_activity_date') + }), + ('Community Statistics', { + 'fields': ('total_questions_asked', 'total_answers_given', 'total_success_stories') + }), + ('Achievements', { + 'fields': ('total_goals_completed', 'achievement_points') + }), + ('Timestamps', { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + +@admin.register(WeeklyGoal) +class WeeklyGoalAdmin(admin.ModelAdmin): + list_display = ('user', 'week_start', 'target_study_hours', 'actual_study_hours', 'target_sessions', 'actual_sessions', 'progress_percentage', 'is_completed') + list_filter = ('week_start', 'is_completed', 'created_at') + search_fields = ('user__username', 'user__first_name', 'user__last_name') + readonly_fields = ('created_at', 'progress_percentage') + ordering = ('-week_start',) + + def progress_percentage(self, obj): + return f"{obj.progress_percentage():.1f}%" + progress_percentage.short_description = 'Progress %' + + fieldsets = ( + ('Weekly Goal', { + 'fields': ('user', 'week_start') + }), + ('Targets', { + 'fields': ('target_study_hours', 'target_sessions') + }), + ('Actual Progress', { + 'fields': ('actual_study_hours', 'actual_sessions', 'is_completed') + }), + ('Timestamps', { + 'fields': ('created_at',) + }), + ) \ No newline at end of file diff --git a/core/forms.py b/core/forms.py index 777ce230..d742c56b 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,6 +1,8 @@ from django import forms -from .models import Contact, SuccessStory, Question, Answer +from .models import Contact, SuccessStory, Question, Answer,Goal, Milestone, StudySession, WeeklyGoal from .models import FAQ +from django.utils import timezone +from datetime import datetime, timedelta class ContactForm(forms.ModelForm): class Meta: @@ -125,4 +127,174 @@ def clean_content(self): class FAQForm(forms.ModelForm): class Meta: model = FAQ - fields = ['question', 'answer'] \ No newline at end of file + fields = ['question', 'answer'] + +class GoalForm(forms.ModelForm): + class Meta: + model = Goal + fields = ['title', 'description', 'category', 'priority', 'target_date'] + widgets = { + 'title': forms.TextInput(attrs={ + 'class': 'form-input', + 'placeholder': 'e.g., Master Python Programming', + 'required': True + }), + 'description': forms.Textarea(attrs={ + 'class': 'form-textarea', + 'placeholder': 'Describe your goal in detail...', + 'rows': 3 + }), + 'category': forms.Select(attrs={ + 'class': 'form-input', + 'required': True + }), + 'priority': forms.Select(attrs={ + 'class': 'form-input', + 'required': True + }), + 'target_date': forms.DateInput(attrs={ + 'class': 'form-input', + 'type': 'date' + }), + } + + def clean_target_date(self): + target_date = self.cleaned_data.get('target_date') + if target_date and target_date <= timezone.now().date(): + raise forms.ValidationError("Target date must be in the future.") + return target_date + +class MilestoneForm(forms.ModelForm): + class Meta: + model = Milestone + fields = ['title', 'description', 'due_date'] + widgets = { + 'title': forms.TextInput(attrs={ + 'class': 'form-input', + 'placeholder': 'e.g., Complete Chapter 1', + 'required': True + }), + 'description': forms.Textarea(attrs={ + 'class': 'form-textarea', + 'placeholder': 'Describe this milestone...', + 'rows': 2 + }), + 'due_date': forms.DateInput(attrs={ + 'class': 'form-input', + 'type': 'date' + }), + } + +class StudySessionForm(forms.ModelForm): + class Meta: + model = StudySession + fields = ['subject', 'title', 'description', 'duration_minutes', 'productivity_rating', 'notes', 'date'] + widgets = { + 'subject': forms.Select(attrs={ + 'class': 'form-input', + 'required': True + }), + 'title': forms.TextInput(attrs={ + 'class': 'form-input', + 'placeholder': 'e.g., Algebra Practice Session', + 'required': True + }), + 'description': forms.Textarea(attrs={ + 'class': 'form-textarea', + 'placeholder': 'What did you study?', + 'rows': 2 + }), + 'duration_minutes': forms.NumberInput(attrs={ + 'class': 'form-input', + 'placeholder': '60', + 'min': '1', + 'max': '480', + 'required': True + }), + 'productivity_rating': forms.NumberInput(attrs={ + 'class': 'form-input', + 'min': '1', + 'max': '10', + 'value': '5', + 'required': True + }), + 'notes': forms.Textarea(attrs={ + 'class': 'form-textarea', + 'placeholder': 'Any notes or reflections...', + 'rows': 3 + }), + 'date': forms.DateInput(attrs={ + 'class': 'form-input', + 'type': 'date', + 'value': timezone.now().date() + }), + } + + def clean_duration_minutes(self): + duration = self.cleaned_data.get('duration_minutes') + if duration and (duration < 1 or duration > 480): + raise forms.ValidationError("Duration must be between 1 and 480 minutes (8 hours).") + return duration + + def clean_productivity_rating(self): + rating = self.cleaned_data.get('productivity_rating') + if rating and (rating < 1 or rating > 10): + raise forms.ValidationError("Productivity rating must be between 1 and 10.") + return rating + + def clean_date(self): + date = self.cleaned_data.get('date') + if date and date > timezone.now().date(): + raise forms.ValidationError("Study session date cannot be in the future.") + return date + +class WeeklyGoalForm(forms.ModelForm): + class Meta: + model = WeeklyGoal + fields = ['target_study_hours', 'target_sessions'] + widgets = { + 'target_study_hours': forms.NumberInput(attrs={ + 'class': 'form-input', + 'placeholder': '10.0', + 'step': '0.5', + 'min': '0.5', + 'max': '60', + 'required': True + }), + 'target_sessions': forms.NumberInput(attrs={ + 'class': 'form-input', + 'placeholder': '5', + 'min': '1', + 'max': '20', + 'required': True + }), + } + + def clean_target_study_hours(self): + hours = self.cleaned_data.get('target_study_hours') + if hours and (hours < 0.5 or hours > 60): + raise forms.ValidationError("Target study hours must be between 0.5 and 60 hours per week.") + return hours + + def clean_target_sessions(self): + sessions = self.cleaned_data.get('target_sessions') + if sessions and (sessions < 1 or sessions > 20): + raise forms.ValidationError("Target sessions must be between 1 and 20 sessions per week.") + return sessions + +class GoalUpdateForm(forms.ModelForm): + class Meta: + model = Goal + fields = ['progress_percentage', 'status'] + widgets = { + 'progress_percentage': forms.NumberInput(attrs={ + 'class': 'form-input', + 'min': '0', + 'max': '100', + 'required': True + }), + 'status': forms.Select(attrs={ + 'class': 'form-input', + 'required': True + }), + } \ No newline at end of file diff --git a/core/migrations/0003_goal_milestone_studysession_userstats_achievement_and_more.py b/core/migrations/0003_goal_milestone_studysession_userstats_achievement_and_more.py new file mode 100644 index 00000000..41ff2da0 --- /dev/null +++ b/core/migrations/0003_goal_milestone_studysession_userstats_achievement_and_more.py @@ -0,0 +1,132 @@ +# Generated by Django 5.2.4 on 2025-09-05 08:39 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_faq'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Goal', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('description', models.TextField(blank=True)), + ('category', models.CharField(choices=[('academic', 'Academic'), ('skill', 'Skill Development'), ('career', 'Career'), ('personal', 'Personal Growth'), ('health', 'Health & Fitness'), ('creative', 'Creative'), ('other', 'Other')], max_length=20)), + ('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('urgent', 'Urgent')], default='medium', max_length=10)), + ('status', models.CharField(choices=[('not_started', 'Not Started'), ('in_progress', 'In Progress'), ('completed', 'Completed'), ('paused', 'Paused'), ('cancelled', 'Cancelled')], default='not_started', max_length=20)), + ('target_date', models.DateField(blank=True, null=True)), + ('progress_percentage', models.PositiveIntegerField(default=0, help_text='0-100')), + ('is_completed', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('completed_at', models.DateTimeField(blank=True, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='goals', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-priority', '-created_at'], + }, + ), + migrations.CreateModel( + name='Milestone', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('description', models.TextField(blank=True)), + ('is_completed', models.BooleanField(default=False)), + ('due_date', models.DateField(blank=True, null=True)), + ('completed_at', models.DateTimeField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('goal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='milestones', to='core.goal')), + ], + options={ + 'ordering': ['due_date', 'created_at'], + }, + ), + migrations.CreateModel( + name='StudySession', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subject', models.CharField(choices=[('math', 'Mathematics'), ('science', 'Science'), ('physics', 'Physics'), ('chemistry', 'Chemistry'), ('biology', 'Biology'), ('computer_science', 'Computer Science'), ('programming', 'Programming'), ('english', 'English'), ('history', 'History'), ('geography', 'Geography'), ('economics', 'Economics'), ('psychology', 'Psychology'), ('philosophy', 'Philosophy'), ('engineering', 'Engineering'), ('other', 'Other')], max_length=50)), + ('title', models.CharField(max_length=200)), + ('description', models.TextField(blank=True)), + ('duration_minutes', models.PositiveIntegerField(help_text='Duration in minutes')), + ('productivity_rating', models.PositiveIntegerField(default=5, help_text='1-10 scale')), + ('notes', models.TextField(blank=True)), + ('date', models.DateField(default=django.utils.timezone.now)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('goals_related', models.ManyToManyField(blank=True, related_name='study_sessions', to='core.goal')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='study_sessions', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-date', '-created_at'], + }, + ), + migrations.CreateModel( + name='UserStats', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('total_study_hours', models.FloatField(default=0.0)), + ('total_goals_completed', models.PositiveIntegerField(default=0)), + ('current_streak_days', models.PositiveIntegerField(default=0)), + ('longest_streak_days', models.PositiveIntegerField(default=0)), + ('total_questions_asked', models.PositiveIntegerField(default=0)), + ('total_answers_given', models.PositiveIntegerField(default=0)), + ('total_success_stories', models.PositiveIntegerField(default=0)), + ('achievement_points', models.PositiveIntegerField(default=0)), + ('last_activity_date', models.DateField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='stats', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'User Statistics', + 'verbose_name_plural': 'User Statistics', + }, + ), + migrations.CreateModel( + name='Achievement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('description', models.TextField()), + ('achievement_type', models.CharField(choices=[('goal_completed', 'Goal Completed'), ('streak', 'Study Streak'), ('milestone', 'Milestone Reached'), ('participation', 'Community Participation'), ('helping_others', 'Helping Others'), ('consistency', 'Consistency'), ('improvement', 'Improvement'), ('special', 'Special Achievement')], max_length=20)), + ('icon', models.CharField(default='🏆', max_length=10)), + ('points', models.PositiveIntegerField(default=10)), + ('is_earned', models.BooleanField(default=True)), + ('earned_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='achievements', to=settings.AUTH_USER_MODEL)), + ('related_goal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.goal')), + ], + options={ + 'ordering': ['-earned_at'], + 'unique_together': {('user', 'title', 'achievement_type')}, + }, + ), + migrations.CreateModel( + name='WeeklyGoal', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('week_start', models.DateField()), + ('target_study_hours', models.FloatField(default=10.0)), + ('target_sessions', models.PositiveIntegerField(default=5)), + ('actual_study_hours', models.FloatField(default=0.0)), + ('actual_sessions', models.PositiveIntegerField(default=0)), + ('is_completed', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='weekly_goals', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-week_start'], + 'unique_together': {('user', 'week_start')}, + }, + ), + ] diff --git a/core/models.py b/core/models.py index 9478c5ac..61ca22d2 100644 --- a/core/models.py +++ b/core/models.py @@ -1,6 +1,7 @@ from django.db import models from django.contrib.auth.models import User from django.utils import timezone +from datetime import timedelta class Contact(models.Model): first_name = models.CharField(max_length=50) @@ -210,4 +211,224 @@ class FAQ(models.Model): answer = models.TextField() def __str__(self): - return self.question \ No newline at end of file + return self.question + +class Goal(models.Model): + CATEGORY_CHOICES = [ + ('academic', 'Academic'), + ('skill', 'Skill Development'), + ('career', 'Career'), + ('personal', 'Personal Growth'), + ('health', 'Health & Fitness'), + ('creative', 'Creative'), + ('other', 'Other'), + ] + + PRIORITY_CHOICES = [ + ('low', 'Low'), + ('medium', 'Medium'), + ('high', 'High'), + ('urgent', 'Urgent'), + ] + + STATUS_CHOICES = [ + ('not_started', 'Not Started'), + ('in_progress', 'In Progress'), + ('completed', 'Completed'), + ('paused', 'Paused'), + ('cancelled', 'Cancelled'), + ] + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='goals') + title = models.CharField(max_length=200) + description = models.TextField(blank=True) + category = models.CharField(max_length=20, choices=CATEGORY_CHOICES) + priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default='medium') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='not_started') + target_date = models.DateField(null=True, blank=True) + progress_percentage = models.PositiveIntegerField(default=0, help_text="0-100") + is_completed = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + completed_at = models.DateTimeField(null=True, blank=True) + + class Meta: + ordering = ['-priority', '-created_at'] + + def __str__(self): + return self.title + + def is_overdue(self): + if self.target_date and not self.is_completed: + return timezone.now().date() > self.target_date + return False + + def days_remaining(self): + if self.target_date and not self.is_completed: + delta = self.target_date - timezone.now().date() + return delta.days + return None + + def save(self, *args, **kwargs): + if self.progress_percentage >= 100 and not self.is_completed: + self.is_completed = True + self.status = 'completed' + self.completed_at = timezone.now() + elif self.progress_percentage < 100 and self.is_completed: + self.is_completed = False + self.status = 'in_progress' + self.completed_at = None + super().save(*args, **kwargs) + +class Milestone(models.Model): + goal = models.ForeignKey(Goal, on_delete=models.CASCADE, related_name='milestones') + title = models.CharField(max_length=200) + description = models.TextField(blank=True) + is_completed = models.BooleanField(default=False) + due_date = models.DateField(null=True, blank=True) + completed_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['due_date', 'created_at'] + + def __str__(self): + return f"{self.goal.title} - {self.title}" + +class StudySession(models.Model): + SUBJECT_CHOICES = [ + ('math', 'Mathematics'), + ('science', 'Science'), + ('physics', 'Physics'), + ('chemistry', 'Chemistry'), + ('biology', 'Biology'), + ('computer_science', 'Computer Science'), + ('programming', 'Programming'), + ('english', 'English'), + ('history', 'History'), + ('geography', 'Geography'), + ('economics', 'Economics'), + ('psychology', 'Psychology'), + ('philosophy', 'Philosophy'), + ('engineering', 'Engineering'), + ('other', 'Other'), + ] + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='study_sessions') + subject = models.CharField(max_length=50, choices=SUBJECT_CHOICES) + title = models.CharField(max_length=200) + description = models.TextField(blank=True) + duration_minutes = models.PositiveIntegerField(help_text="Duration in minutes") + productivity_rating = models.PositiveIntegerField( + default=5, + help_text="1-10 scale" + ) + goals_related = models.ManyToManyField(Goal, blank=True, related_name='study_sessions') + notes = models.TextField(blank=True) + date = models.DateField(default=timezone.now) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-date', '-created_at'] + + def __str__(self): + return f"{self.subject} - {self.title} ({self.duration_minutes}min)" + +class Achievement(models.Model): + ACHIEVEMENT_TYPES = [ + ('goal_completed', 'Goal Completed'), + ('streak', 'Study Streak'), + ('milestone', 'Milestone Reached'), + ('participation', 'Community Participation'), + ('helping_others', 'Helping Others'), + ('consistency', 'Consistency'), + ('improvement', 'Improvement'), + ('special', 'Special Achievement'), + ] + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='achievements') + title = models.CharField(max_length=200) + description = models.TextField() + achievement_type = models.CharField(max_length=20, choices=ACHIEVEMENT_TYPES) + icon = models.CharField(max_length=10, default='🏆') + points = models.PositiveIntegerField(default=10) + is_earned = models.BooleanField(default=True) + earned_at = models.DateTimeField(auto_now_add=True) + related_goal = models.ForeignKey(Goal, on_delete=models.SET_NULL, null=True, blank=True) + + class Meta: + ordering = ['-earned_at'] + unique_together = ('user', 'title', 'achievement_type') + + def __str__(self): + return f"{self.user.username} - {self.title}" + +class UserStats(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='stats') + total_study_hours = models.FloatField(default=0.0) + total_goals_completed = models.PositiveIntegerField(default=0) + current_streak_days = models.PositiveIntegerField(default=0) + longest_streak_days = models.PositiveIntegerField(default=0) + total_questions_asked = models.PositiveIntegerField(default=0) + total_answers_given = models.PositiveIntegerField(default=0) + total_success_stories = models.PositiveIntegerField(default=0) + achievement_points = models.PositiveIntegerField(default=0) + last_activity_date = models.DateField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = 'User Statistics' + verbose_name_plural = 'User Statistics' + + def __str__(self): + return f"{self.user.username} - Stats" + + def update_streak(self): + """Update study streak based on recent activity""" + today = timezone.now().date() + yesterday = today - timedelta(days=1) + + # Check if user has activity today or yesterday + recent_sessions = StudySession.objects.filter( + user=self.user, + date__in=[today, yesterday] + ).exists() + + if recent_sessions: + if self.last_activity_date == yesterday: + self.current_streak_days += 1 + elif self.last_activity_date != today: + self.current_streak_days = 1 + + self.last_activity_date = today + + if self.current_streak_days > self.longest_streak_days: + self.longest_streak_days = self.current_streak_days + else: + # Reset streak if no recent activity + self.current_streak_days = 0 + + self.save() + +class WeeklyGoal(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='weekly_goals') + week_start = models.DateField() + target_study_hours = models.FloatField(default=10.0) + target_sessions = models.PositiveIntegerField(default=5) + actual_study_hours = models.FloatField(default=0.0) + actual_sessions = models.PositiveIntegerField(default=0) + is_completed = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('user', 'week_start') + ordering = ['-week_start'] + + def __str__(self): + return f"{self.user.username} - Week of {self.week_start}" + + def progress_percentage(self): + hours_progress = min((self.actual_study_hours / self.target_study_hours) * 100, 100) if self.target_study_hours > 0 else 100 + sessions_progress = min((self.actual_sessions / self.target_sessions) * 100, 100) if self.target_sessions > 0 else 100 + return (hours_progress + sessions_progress) / 2 \ No newline at end of file diff --git a/core/static/css/progress.css b/core/static/css/progress.css new file mode 100644 index 00000000..8bdc823d --- /dev/null +++ b/core/static/css/progress.css @@ -0,0 +1,1036 @@ +/* Progress Tracking Styles - Add to core/static/css/progress.css */ + +.progress-container { + max-width: 1400px; + margin: 0 auto; + padding: 0 2rem; +} + +.progress-header { + text-align: center; + margin-bottom: 3rem; +} + +.progress-header .main-title { + font-size: 3.5rem; + font-weight: 800; + background: linear-gradient(135deg, #ffffff 0%, #3b82f6 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 1rem; +} + +.progress-header .subtitle { + font-size: 1.2rem; + color: rgba(255, 255, 255, 0.8); + margin-bottom: 2rem; +} + +/* Quick Actions */ +.quick-actions { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; +} + +.action-btn { + background: linear-gradient(135deg, #3b82f6, #1d4ed8); + color: white; + border: none; + border-radius: 12px; + padding: 0.75rem 1.5rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + transition: all 0.3s ease; +} + +.action-btn:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3); +} + +/* Statistics Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 3rem; +} + +.stat-card { + background: rgba(30, 41, 59, 0.4); + backdrop-filter: blur(15px); + border: 1px solid rgba(59, 130, 246, 0.2); + border-radius: 16px; + padding: 2rem; + display: flex; + align-items: center; + gap: 1.5rem; + transition: all 0.3s ease; +} + +.stat-card:hover { + transform: translateY(-3px); + border-color: rgba(59, 130, 246, 0.4); + box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2); +} + +.stat-icon { + width: 60px; + height: 60px; + background: linear-gradient(135deg, #3b82f6, #1d4ed8); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + color: white; +} + +.stat-content h3 { + font-size: 2rem; + font-weight: 800; + color: white; + margin-bottom: 0.5rem; +} + +.stat-content p { + color: rgba(255, 255, 255, 0.7); + font-size: 0.9rem; + margin: 0; +} + +/* Content Grid */ +.content-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; +} + +.left-column, .right-column { + display: flex; + flex-direction: column; + gap: 2rem; +} + +/* Progress Cards */ +.progress-card { + background: rgba(30, 41, 59, 0.4); + backdrop-filter: blur(15px); + border: 1px solid rgba(59, 130, 246, 0.2); + border-radius: 16px; + padding: 2rem; + transition: all 0.3s ease; +} + +.progress-card:hover { + border-color: rgba(59, 130, 246, 0.4); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.card-header h3 { + font-size: 1.3rem; + font-weight: 600; + color: white; + margin: 0; +} + +.time-period { + color: rgba(255, 255, 255, 0.6); + font-size: 0.85rem; +} + +.header-btn { + background: rgba(59, 130, 246, 0.2); + border: 1px solid rgba(59, 130, 246, 0.3); + border-radius: 8px; + color: #3b82f6; + padding: 0.5rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.header-btn:hover { + background: rgba(59, 130, 246, 0.3); + transform: scale(1.1); +} + +/* Weekly Progress */ +.weekly-stats { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.weekly-stat { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.stat-label { + color: rgba(255, 255, 255, 0.8); + font-weight: 500; + font-size: 0.9rem; +} + +.progress-bar { + background: rgba(15, 23, 42, 0.6); + border-radius: 8px; + height: 8px; + overflow: hidden; + position: relative; +} + +.progress-fill { + background: linear-gradient(135deg, #10b981, #059669); + height: 100%; + border-radius: 8px; + transition: width 0.8s ease; + position: relative; +} + +.progress-fill::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); + animation: shimmer 2s infinite; +} + +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +.stat-value { + color: white; + font-weight: 600; + font-size: 0.9rem; +} + +/* Chart Container */ +.chart-container { + position: relative; + height: 250px; + margin-top: 1rem; +} + +/* Goals Container */ +.goals-container { + display: flex; + flex-direction: column; + gap: 1rem; + max-height: 400px; + overflow-y: auto; +} + +.goal-item { + background: rgba(15, 23, 42, 0.6); + border: 1px solid rgba(59, 130, 246, 0.2); + border-radius: 12px; + padding: 1.5rem; + transition: all 0.3s ease; + cursor: pointer; +} + +.goal-item:hover { + border-color: rgba(59, 130, 246, 0.4); + transform: translateX(5px); +} + +.goal-header { + display: flex; + justify-content: between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1rem; +} + +.goal-info { + flex: 1; +} + +.goal-title { + font-size: 1.1rem; + font-weight: 600; + color: white; + margin-bottom: 0.5rem; +} + +.goal-meta { + display: flex; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 1rem; +} + +.goal-badge { + padding: 0.2rem 0.6rem; + border-radius: 12px; + font-size: 0.7rem; + font-weight: 500; +} + +.priority-low { background: rgba(107, 114, 128, 0.3); color: #9ca3af; } +.priority-medium { background: rgba(251, 191, 36, 0.3); color: #fbbf24; } +.priority-high { background: rgba(239, 68, 68, 0.3); color: #ef4444; } +.priority-urgent { background: rgba(220, 38, 127, 0.3); color: #dc2626; } + +.status-not_started { background: rgba(107, 114, 128, 0.3); color: #9ca3af; } +.status-in_progress { background: rgba(59, 130, 246, 0.3); color: #3b82f6; } +.status-completed { background: rgba(16, 185, 129, 0.3); color: #10b981; } +.status-paused { background: rgba(251, 191, 36, 0.3); color: #fbbf24; } + +.goal-progress { + margin-bottom: 1rem; +} + +.goal-progress-bar { + background: rgba(15, 23, 42, 0.8); + border-radius: 6px; + height: 6px; + overflow: hidden; + margin-top: 0.5rem; +} + +.goal-progress-fill { + background: linear-gradient(135deg, #3b82f6, #1d4ed8); + height: 100%; + border-radius: 6px; + transition: width 0.8s ease; +} + +.goal-actions { + display: flex; + gap: 0.5rem; +} + +.goal-btn { + background: transparent; + border: 1px solid rgba(59, 130, 246, 0.3); + color: #3b82f6; + border-radius: 6px; + padding: 0.4rem 0.8rem; + font-size: 0.8rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.goal-btn:hover { + background: rgba(59, 130, 246, 0.2); +} + +/* Activity Feed */ +.activity-feed { + display: flex; + flex-direction: column; + gap: 1rem; + max-height: 400px; + overflow-y: auto; +} + +.activity-item { + background: rgba(15, 23, 42, 0.6); + border-left: 3px solid #3b82f6; + border-radius: 8px; + padding: 1rem; + transition: all 0.3s ease; +} + +.activity-item:hover { + background: rgba(15, 23, 42, 0.8); + transform: translateX(3px); +} + +.activity-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.5rem; +} + +.activity-icon { + font-size: 1.2rem; +} + +.activity-title { + font-weight: 600; + color: white; + font-size: 0.95rem; +} + +.activity-description { + color: rgba(255, 255, 255, 0.7); + font-size: 0.85rem; + margin-bottom: 0.5rem; +} + +.activity-time { + color: rgba(255, 255, 255, 0.5); + font-size: 0.75rem; +} + +/* Achievements */ +.achievements-container { + display: flex; + flex-direction: column; + gap: 1rem; + max-height: 300px; + overflow-y: auto; +} + +.achievement-item { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(16, 185, 129, 0.1)); + border: 1px solid rgba(59, 130, 246, 0.3); + border-radius: 12px; + padding: 1rem; + display: flex; + align-items: center; + gap: 1rem; + transition: all 0.3s ease; +} + +.achievement-item:hover { + transform: scale(1.02); + border-color: rgba(59, 130, 246, 0.5); +} + +.achievement-icon { + font-size: 2rem; + width: 50px; + text-align: center; +} + +.achievement-content { + flex: 1; +} + +.achievement-title { + font-weight: 600; + color: white; + font-size: 0.95rem; + margin-bottom: 0.3rem; +} + +.achievement-description { + color: rgba(255, 255, 255, 0.7); + font-size: 0.8rem; +} + +.achievement-points { + background: linear-gradient(135deg, #10b981, #059669); + color: white; + padding: 0.3rem 0.6rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; +} + +/* Modals */ +.modal { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(5px); + overflow-y: auto; +} + +.modal-content { + background: rgba(30, 41, 59, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(59, 130, 246, 0.3); + border-radius: 20px; + margin: 2% auto; /* Reduced top margin */ + padding: 0; + width: 90%; + max-width: 600px; + max-height: 90vh; /* Limit max height */ + animation: modalSlideIn 0.3s ease; + position: relative; + display: flex; + flex-direction: column; +} + +.modal-content.large { + max-width: 800px; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-50px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.modal-header { + background: linear-gradient(135deg, #3b82f6, #1d4ed8); + padding: 1.5rem 2rem; + border-radius: 20px 20px 0 0; + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; /* Don't shrink header */ +} + +.modal-header h2 { + color: white; + font-size: 1.5rem; + font-weight: 700; + margin: 0; +} + +.close { + color: white; + font-size: 2rem; + font-weight: bold; + cursor: pointer; + line-height: 1; + transition: all 0.3s ease; + padding: 0; + margin: 0; + background: none; + border: none; +} + +.close:hover { + transform: scale(1.2); +} +.modal-form-container { + padding: 2rem; + flex: 1; + overflow-y: auto; /* Allow form content to scroll */ + max-height: calc(90vh - 100px); /* Account for header height */ +} + +.modal-content form { + padding: 0; +} +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; + margin-bottom: 1.5rem; +} + +.form-group { + display: flex; + flex-direction: column; +} + +.form-group.full-width { + grid-column: 1 / -1; +} + +.form-label { + font-size: 0.9rem; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); + margin-bottom: 0.5rem; +} + +.required { + color: #3b82f6; +} + +.form-input, .form-textarea { + background: rgba(15, 23, 42, 0.8); + border: 1px solid rgba(59, 130, 246, 0.3); + border-radius: 12px; + padding: 1rem 1.2rem; + color: white; + font-size: 1rem; + transition: all 0.3s ease; + outline: none; + resize: vertical; +} + +.form-input:focus, .form-textarea:focus { + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + transform: translateY(-1px); +} + +.form-input::placeholder, .form-textarea::placeholder { + color: rgba(255, 255, 255, 0.4); +} + +.form-textarea { + min-height: 80px; + resize: vertical; +} +.modal-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + flex-shrink: 0; /* Don't shrink buttons */ + background: rgba(30, 41, 59, 0.95); /* Ensure buttons are visible */ +} + +.btn-secondary { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.3); + color: rgba(255, 255, 255, 0.8); + border-radius: 8px; + padding: 0.75rem 1.5rem; + cursor: pointer; + transition: all 0.3s ease; + font-weight: 500; +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.1); + color: white; +} +.submit-btn { + background: linear-gradient(135deg, #3b82f6, #1d4ed8); + color: white; + border: none; + border-radius: 8px; + padding: 0.75rem 1.5rem; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + min-width: 120px; +} + +.submit-btn:hover { + transform: translateY(-1px); + box-shadow: 0 5px 15px rgba(59, 130, 246, 0.3); +} + +.submit-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} +.view-all { + text-align: center; + margin-top: 1rem; +} + +.view-all button { + background: transparent; + border: 1px solid rgba(59, 130, 246, 0.3); + color: #3b82f6; + border-radius: 8px; + padding: 0.5rem 1rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.view-all button:hover { + background: rgba(59, 130, 246, 0.2); +} + +/* Goal Detail Styles */ +.goal-detail-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 2rem; + gap: 2rem; +} + +.goal-detail-info { + flex: 1; +} + +.goal-detail-title { + font-size: 1.5rem; + font-weight: 700; + color: white; + margin-bottom: 1rem; +} + +.goal-detail-meta { + display: flex; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 1rem; +} + +.goal-detail-description { + color: rgba(255, 255, 255, 0.8); + line-height: 1.6; + margin-bottom: 1.5rem; +} + +.goal-detail-actions { + display: flex; + flex-direction: column; + gap: 1rem; + min-width: 200px; +} + +.progress-update { + background: rgba(15, 23, 42, 0.6); + border: 1px solid rgba(59, 130, 246, 0.3); + border-radius: 12px; + padding: 1.5rem; +} + +.progress-update h4 { + color: white; + font-size: 1rem; + margin-bottom: 1rem; +} + +.progress-input { + width: 100%; + margin-bottom: 1rem; +} + +.milestones-section { + margin-top: 2rem; +} + +.milestones-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.milestones-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.milestone-item { + background: rgba(15, 23, 42, 0.4); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 1rem; + display: flex; + align-items: center; + gap: 1rem; + transition: all 0.3s ease; +} + +.milestone-item:hover { + background: rgba(15, 23, 42, 0.6); +} + +.milestone-checkbox { + width: 20px; + height: 20px; + border: 2px solid #3b82f6; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.milestone-checkbox.completed { + background: #3b82f6; + color: white; +} + +.milestone-content { + flex: 1; +} + +.milestone-title { + color: white; + font-weight: 500; + margin-bottom: 0.3rem; +} + +.milestone-due { + color: rgba(255, 255, 255, 0.6); + font-size: 0.8rem; +} + +/* Light Mode Styles */ +body.light-mode .progress-header .main-title { + background: linear-gradient(135deg, #1F5F4A 0%, #328E6E 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +body.light-mode .progress-header .subtitle { + color: #19624D; +} + +body.light-mode .action-btn { + background: linear-gradient(135deg, #328E6E, #1F5F4A); +} + +body.light-mode .stat-card { + background: #90C67C; + border: 1px solid rgba(31, 95, 74, 0.3); +} + +body.light-mode .stat-icon { + background: linear-gradient(135deg, #1F5F4A, #328E6E); +} + +body.light-mode .stat-content h3 { + color: #2F4F2F; +} + +body.light-mode .stat-content p { + color: #19624D; +} + +body.light-mode .progress-card { + background: #90C67C; + border: 1px solid rgba(31, 95, 74, 0.3); +} + +body.light-mode .card-header h3 { + color: #2F4F2F; +} + +body.light-mode .time-period { + color: #19624D; +} + +body.light-mode .header-btn { + background: rgba(31, 95, 74, 0.2); + border: 1px solid rgba(31, 95, 74, 0.4); + color: #1F5F4A; +} + +body.light-mode .stat-label { + color: #12241f; +} + +body.light-mode .progress-bar { + background: rgba(31, 95, 74, 0.2); +} + +body.light-mode .stat-value { + color: #2F4F2F; +} + +body.light-mode .goal-item { + background: rgba(31, 95, 74, 0.1); + border: 1px solid rgba(31, 95, 74, 0.3); +} + +body.light-mode .goal-title { + color: #2F4F2F; +} + +body.light-mode .modal-content { + background: #90C67C; + border: 1px solid rgba(31, 95, 74, 0.4); +} + +body.light-mode .modal-header { + background: linear-gradient(135deg, #328E6E, #1F5F4A); +} +body.light-mode .form-input, +body.light-mode .form-textarea { + background: #E1EEBC; + border: 1px solid #328E6E; + color: #2F4F2F; +} + +body.light-mode .form-input:focus, +body.light-mode .form-textarea:focus { + border-color: #328E6E; + box-shadow: 0 0 0 3px rgba(50, 142, 110, 0.2); +} + +body.light-mode .form-input::placeholder, +body.light-mode .form-textarea::placeholder { + color: rgba(47, 79, 47, 0.6); +} + +body.light-mode .form-label { + color: #2F4F2F; +} + +body.light-mode .btn-secondary { + border: 1px solid rgba(31, 95, 74, 0.4); + color: #1F5F4A; +} + +body.light-mode .btn-secondary:hover { + background: rgba(31, 95, 74, 0.2); +} + +body.light-mode .submit-btn { + background: linear-gradient(135deg, #328E6E, #1F5F4A); +} + +body.light-mode .modal-actions { + background: #90C67C; + border-top: 1px solid rgba(31, 95, 74, 0.2); +} +body.light-mode .btn-secondary { + border: 1px solid rgba(31, 95, 74, 0.4); + color: #1F5F4A; +} + +body.light-mode .activity-item { + background: rgba(31, 95, 74, 0.1); + border-left-color: #328E6E; +} + +body.light-mode .activity-title { + color: #2F4F2F; +} + +body.light-mode .activity-description { + color: #19624D; +} + +body.light-mode .activity-time { + color: #67AE6E; +} + +body.light-mode .achievement-item { + background: linear-gradient(135deg, rgba(31, 95, 74, 0.1), rgba(50, 142, 110, 0.1)); + border: 1px solid rgba(31, 95, 74, 0.4); +} + +body.light-mode .achievement-title { + color: #2F4F2F; +} + +body.light-mode .achievement-description { + color: #19624D; +} + +body.light-mode .milestone-item { + background: rgba(31, 95, 74, 0.1); + border: 1px solid rgba(31, 95, 74, 0.2); +} + +body.light-mode .milestone-title { + color: #2F4F2F; +} + +body.light-mode .milestone-due { + color: #19624D; +} + +body.light-mode .milestone-checkbox { + border-color: #328E6E; +} + +body.light-mode .milestone-checkbox.completed { + background: #328E6E; +} + +body.light-mode .view-all button { + border: 1px solid rgba(31, 95, 74, 0.4); + color: #1F5F4A; +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .content-grid { + grid-template-columns: 1fr; + } + + .stats-grid { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + } +} + +@media (max-width: 768px) { + .progress-container { + padding: 0 1rem; + } + + .progress-header .main-title { + font-size: 2.5rem; + } + + .quick-actions { + flex-direction: column; + align-items: center; + } + + .action-btn { + width: 200px; + justify-content: center; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .modal-content { + width: 95%; + margin: 2% auto; + } + + .goal-header { + flex-direction: column; + align-items: flex-start; + } + + .goal-detail-header { + flex-direction: column; + } + + .goal-detail-actions { + width: 100%; + } +} +@media (max-width: 768px) { + .modal-content { + width: 95%; + margin: 1% auto; + max-height: 95vh; + } + + .form-grid { + grid-template-columns: 1fr; + } + + .modal-header { + padding: 1rem 1.5rem; + } + + .modal-form-container { + padding: 1.5rem; + } + + .modal-actions { + flex-direction: column; + gap: 0.75rem; + } + + .btn-secondary, .submit-btn { + width: 100%; + justify-content: center; + } +} \ No newline at end of file diff --git a/core/static/js/progress.js b/core/static/js/progress.js new file mode 100644 index 00000000..c0934c15 --- /dev/null +++ b/core/static/js/progress.js @@ -0,0 +1,858 @@ +// Progress Tracking JavaScript - Create as core/static/js/progress.js + +let charts = {}; +let currentGoals = []; + +document.addEventListener('DOMContentLoaded', function() { + loadProgressData(); + setupEventListeners(); +}); + +function setupEventListeners() { + // Goal Form + document.getElementById('goalForm').addEventListener('submit', handleGoalSubmission); + + // Session Form + document.getElementById('sessionForm').addEventListener('submit', handleSessionSubmission); + + // Weekly Goals Form + document.getElementById('weeklyForm').addEventListener('submit', handleWeeklyGoalSubmission); + + // Modal close on outside click + window.addEventListener('click', function(e) { + if (e.target.classList.contains('modal')) { + closeModal(e.target.id); + } + }); +} + +async function loadProgressData() { + try { + showLoading(); + + // Load dashboard data + const response = await fetch('/api/progress-dashboard/', { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + } + }); + + const data = await response.json(); + + if (data.success) { + updateStatistics(data.stats); + updateWeeklyProgress(data.weekly_progress); + renderDailyChart(data.daily_data); + renderSubjectChart(data.subject_data); + displayRecentActivity(data.recent_sessions); + displayAchievements(data.recent_achievements); + } + + // Load goals + await loadGoals(); + + hideLoading(); + + } catch (error) { + console.error('Error loading progress data:', error); + showNotification('Failed to load progress data', 'error'); + hideLoading(); + } +} + +async function loadGoals() { + try { + const response = await fetch('/api/goals/', { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + } + }); + + const data = await response.json(); + + if (data.success) { + currentGoals = data.goals; + displayGoals(data.goals); + } + + } catch (error) { + console.error('Error loading goals:', error); + } +} + +function updateStatistics(stats) { + document.getElementById('totalHours').textContent = stats.total_study_hours; + document.getElementById('completedGoals').textContent = stats.total_goals_completed; + document.getElementById('currentStreak').textContent = stats.current_streak_days; + document.getElementById('achievementPoints').textContent = stats.achievement_points; +} + +function updateWeeklyProgress(weekly) { + const hoursProgress = Math.min((weekly.actual_hours / weekly.target_hours) * 100, 100); + const sessionsProgress = Math.min((weekly.actual_sessions / weekly.target_sessions) * 100, 100); + + document.getElementById('weeklyHoursProgress').style.width = hoursProgress + '%'; + document.getElementById('weeklySessionsProgress').style.width = sessionsProgress + '%'; + document.getElementById('weeklyHoursText').textContent = `${weekly.actual_hours} / ${weekly.target_hours} hrs`; + document.getElementById('weeklySessionsText').textContent = `${weekly.actual_sessions} / ${weekly.target_sessions} sessions`; +} + +function renderDailyChart(dailyData) { + const ctx = document.getElementById('dailyChart').getContext('2d'); + + if (charts.daily) { + charts.daily.destroy(); + } + + charts.daily = new Chart(ctx, { + type: 'line', + data: { + labels: dailyData.map(d => d.day), + datasets: [{ + label: 'Study Hours', + data: dailyData.map(d => d.hours), + borderColor: '#3b82f6', + backgroundColor: 'rgba(59, 130, 246, 0.1)', + borderWidth: 3, + fill: true, + tension: 0.4, + pointBackgroundColor: '#3b82f6', + pointBorderColor: '#ffffff', + pointBorderWidth: 2, + pointRadius: 6, + pointHoverRadius: 8, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + } + }, + scales: { + y: { + beginAtZero: true, + grid: { + color: 'rgba(255, 255, 255, 0.1)' + }, + ticks: { + color: 'rgba(255, 255, 255, 0.7)' + } + }, + x: { + grid: { + color: 'rgba(255, 255, 255, 0.1)' + }, + ticks: { + color: 'rgba(255, 255, 255, 0.7)' + } + } + } + } + }); +} + +function renderSubjectChart(subjectData) { + const ctx = document.getElementById('subjectChart').getContext('2d'); + + if (charts.subject) { + charts.subject.destroy(); + } + + const colors = [ + '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', + '#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1' + ]; + + charts.subject = new Chart(ctx, { + type: 'doughnut', + data: { + labels: subjectData.map(d => d.subject_display), + datasets: [{ + data: subjectData.map(d => d.hours), + backgroundColor: colors.slice(0, subjectData.length), + borderColor: colors.slice(0, subjectData.length), + borderWidth: 2, + hoverOffset: 4 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + labels: { + color: 'rgba(255, 255, 255, 0.8)', + usePointStyle: true, + padding: 15 + } + }, + tooltip: { + callbacks: { + label: function(context) { + const label = context.label || ''; + const value = context.parsed; + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const percentage = ((value / total) * 100).toFixed(1); + return `${label}: ${value}h (${percentage}%)`; + } + } + } + } + } + }); +} + +function displayGoals(goals) { + const container = document.getElementById('goalsContainer'); + + if (goals.length === 0) { + container.innerHTML = ` +
No goals yet. Create your first goal to start tracking progress!
+No recent activity
+No achievements yet
+Target Date: ${formatDate(goal.target_date)}
` : ''} + ${goal.days_remaining !== null ? `Days Remaining: ${goal.days_remaining}
` : ''} +Monitor your learning progress and achievements.
- +Monitor your learning progress, set goals, and track achievements with detailed analytics.
+ + +Monitor your learning journey and achieve your goals
+ + +Study Hours
+Goals Completed
+Day Streak
+Points Earned
+