From a6029a97a76b60303727f42d2fc447a6299c6b8a Mon Sep 17 00:00:00 2001 From: TR Tajim Date: Tue, 13 Oct 2026 22:08:04 +0600 Subject: [PATCH 1/4] fix(forum): filter subjects and chapters by is_trackable --- app/Http/Controllers/ForumController.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/ForumController.php b/app/Http/Controllers/ForumController.php index c4b24077..9b5ccba7 100644 --- a/app/Http/Controllers/ForumController.php +++ b/app/Http/Controllers/ForumController.php @@ -65,8 +65,9 @@ public function index(Request $request): Response } $subjects = Cache::remember('forum_filter_subjects', now()->addDay(), function () { - return Subject::select('id', 'name', 'course', 'slug') - ->with(['nodes' => fn ($q) => $q->whereNull('parent_id')->select('id', 'subject_id', 'name', 'slug')->orderBy('sort_order')]) + return Subject::where('is_trackable', true) + ->select('id', 'name', 'course', 'slug') + ->with(['nodes' => fn ($q) => $q->where('is_trackable', true)->select('id', 'subject_id', 'name', 'slug')->orderBy('sort_order')]) ->orderBy('sort_order') ->get() ->toArray(); @@ -182,8 +183,9 @@ public function create(): Response|RedirectResponse } $subjects = Cache::remember('forum_filter_subjects', now()->addDay(), function () { - return Subject::select('id', 'name', 'course', 'slug') - ->with(['nodes' => fn ($q) => $q->whereNull('parent_id')->select('id', 'subject_id', 'name', 'slug')->orderBy('sort_order')]) + return Subject::where('is_trackable', true) + ->select('id', 'name', 'course', 'slug') + ->with(['nodes' => fn ($q) => $q->where('is_trackable', true)->select('id', 'subject_id', 'name', 'slug')->orderBy('sort_order')]) ->orderBy('sort_order') ->get() ->toArray(); From d40ea5eafe128835b05d3d12a46a9adeab77da27 Mon Sep 17 00:00:00 2001 From: TR Tajim Date: Tue, 13 Oct 2026 22:08:16 +0600 Subject: [PATCH 2/4] feat(tracker): manage optional subjects and excluded subjects in tracker and profile --- .../Controllers/StudyTrackerController.php | 35 +- .../Controllers/UserProfileController.php | 21 +- app/Models/Subject.php | 1 + app/Models/User.php | 2 + ...ubjects_and_excluded_subjects_to_users.php | 54 +++ .../components/tracker/SubjectChecklist.vue | 409 +++++++++++++++--- .../tracker/SyllabusBreakdownModal.vue | 281 +++++++++--- resources/js/pages/Tracker/Index.vue | 10 + 8 files changed, 687 insertions(+), 126 deletions(-) create mode 100644 database/migrations/2026_10_13_155150_add_is_optional_to_subjects_and_excluded_subjects_to_users.php diff --git a/app/Http/Controllers/StudyTrackerController.php b/app/Http/Controllers/StudyTrackerController.php index 17f647cf..518ac2e2 100644 --- a/app/Http/Controllers/StudyTrackerController.php +++ b/app/Http/Controllers/StudyTrackerController.php @@ -18,17 +18,32 @@ public function index(Request $request): Response $user = $request->user(); $course = $user?->curriculum ?: 'hsc'; $group = $user?->group ?: 'science'; + $excludedSubjectIds = (array) ($user?->excluded_subject_ids ?? []); - $subjects = Subject::where('course', $course) + $subjectsQuery = Subject::where('course', $course) ->where('is_trackable', true) ->whereIn('group', [$group, 'common']) - ->orderBy('sort_order', 'asc') + ->orderBy('sort_order', 'asc'); + + if (! empty($excludedSubjectIds)) { + $subjectsQuery->whereNotIn('id', $excludedSubjectIds); + } + + $subjects = $subjectsQuery ->with(['nodes' => function ($query) { $query->where('is_trackable', true) + ->with('parent:id,name,sort_order') ->orderBy('sort_order', 'asc') - ->select('id', 'subject_id', 'name', 'slug', 'sort_order', 'weight'); + ->select('id', 'subject_id', 'parent_id', 'name', 'slug', 'sort_order', 'weight'); }]) - ->get(['id', 'name', 'slug', 'course', 'group', 'sort_order']) + ->get(['id', 'name', 'slug', 'course', 'group', 'sort_order', 'is_optional']) + ->toArray(); + + // All trackable subjects for curriculum modal (so user can toggle optional subjects on/off) + $groupSubjects = Subject::where('is_trackable', true) + ->where('is_optional', true) + ->orderBy('sort_order', 'asc') + ->get(['id', 'name', 'course', 'group', 'is_optional']) ->toArray(); if (! $user) { @@ -36,6 +51,8 @@ public function index(Request $request): Response 'course' => $course, 'group' => $group, 'subjects' => $subjects, + 'excludedSubjectIds' => [], + 'optionalSubjects' => $groupSubjects, 'completedNodeIds' => [], 'todaySeconds' => 0, 'stats' => [ @@ -64,6 +81,8 @@ public function index(Request $request): Response 'course' => $course, 'group' => $group, 'subjects' => $subjects, + 'excludedSubjectIds' => $excludedSubjectIds, + 'optionalSubjects' => $groupSubjects, 'completedNodeIds' => $completedNodeIds, 'todaySeconds' => $todaySeconds, 'stats' => $studyData['stats'], @@ -147,9 +166,15 @@ public function updateCurriculum(Request $request) $validated = $request->validate([ 'curriculum' => 'required|string|in:hsc,ssc', 'group' => 'required|string|in:science,humanities,commerce', + 'excluded_subject_ids' => 'nullable|array', + 'excluded_subject_ids.*' => 'integer|exists:subjects,id', ]); - $request->user()->update($validated); + $request->user()->update([ + 'curriculum' => $validated['curriculum'], + 'group' => $validated['group'], + 'excluded_subject_ids' => $validated['excluded_subject_ids'] ?? [], + ]); return redirect()->route('tracker.index') ->with('success', 'Curriculum updated to '.strtoupper($validated['curriculum']).' ('.ucfirst($validated['group']).').'); diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php index 2e78090a..44e6d087 100644 --- a/app/Http/Controllers/UserProfileController.php +++ b/app/Http/Controllers/UserProfileController.php @@ -393,14 +393,23 @@ private function getSyllabusProgress(User $user): array { $course = $user->curriculum ?: 'hsc'; $group = $user->group ?: 'science'; - $trackableSubjects = Subject::where('course', $course) + $excludedSubjectIds = (array) ($user->excluded_subject_ids ?? []); + + $subjectsQuery = Subject::where('course', $course) ->where('is_trackable', true) ->whereIn('group', [$group, 'common']) - ->orderBy('sort_order', 'asc') + ->orderBy('sort_order', 'asc'); + + if (! empty($excludedSubjectIds)) { + $subjectsQuery->whereNotIn('id', $excludedSubjectIds); + } + + $trackableSubjects = $subjectsQuery ->with(['nodes' => function ($query) { $query->where('is_trackable', true) + ->with('parent:id,name,sort_order') ->orderBy('sort_order', 'asc') - ->select('id', 'subject_id', 'name', 'slug', 'sort_order', 'weight'); + ->select('id', 'subject_id', 'parent_id', 'name', 'slug', 'sort_order', 'weight'); }]) ->get(['id', 'name', 'slug', 'course', 'tailwind_format', 'icon', 'sort_order']); @@ -439,6 +448,12 @@ private function getSyllabusProgress(User $user): array 'name' => $node->name, 'weight' => $nodeWeight, 'is_completed' => $isDone, + 'parent_id' => $node->parent_id, + 'parent' => $node->parent ? [ + 'id' => $node->parent->id, + 'name' => $node->parent->name, + 'sort_order' => $node->parent->sort_order, + ] : null, ]; } diff --git a/app/Models/Subject.php b/app/Models/Subject.php index 32be626d..41691ec6 100644 --- a/app/Models/Subject.php +++ b/app/Models/Subject.php @@ -12,6 +12,7 @@ protected function casts(): array { return [ 'is_trackable' => 'boolean', + 'is_optional' => 'boolean', ]; } diff --git a/app/Models/User.php b/app/Models/User.php index d008dffa..40ff8926 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -59,6 +59,7 @@ class User extends Authenticatable 'facebook', 'instagram', 'github', + 'excluded_subject_ids', 'last_active_at', 'last_seen_at', ]; @@ -121,6 +122,7 @@ protected function casts(): array 'is_verified' => 'boolean', 'priority' => 'integer', 'allow_pokes' => 'boolean', + 'excluded_subject_ids' => 'array', ]; } diff --git a/database/migrations/2026_10_13_155150_add_is_optional_to_subjects_and_excluded_subjects_to_users.php b/database/migrations/2026_10_13_155150_add_is_optional_to_subjects_and_excluded_subjects_to_users.php new file mode 100644 index 00000000..25cde5c6 --- /dev/null +++ b/database/migrations/2026_10_13_155150_add_is_optional_to_subjects_and_excluded_subjects_to_users.php @@ -0,0 +1,54 @@ +boolean('is_optional')->default(false)->after('is_trackable'); + }); + + Schema::table('users', function (Blueprint $table) { + $table->json('excluded_subject_ids')->nullable()->after('group'); + }); + + // Seed initial known optional subjects: + // HSC Science: Higher Math 1st & 2nd (9, 10), Biology 1st & 2nd (11, 12) + // SSC Science: Biology (24), Higher Math (25) + // HSC Commerce: Production Management 1st & 2nd (48, 49), Finance 1st & 2nd (46, 47) + // HSC Humanities: Islamic History 1st & 2nd (56, 57), Logic 1st & 2nd (54, 55), Economics 1st & 2nd (52, 53) + $optionalIds = [ + // HSC Science + 9, 10, 11, 12, + // SSC Science + 24, 25, + // HSC Commerce + 46, 47, 48, 49, + // HSC Humanities + 52, 53, 54, 55, 56, 57, + ]; + + DB::table('subjects')->whereIn('id', $optionalIds)->update(['is_optional' => true]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('excluded_subject_ids'); + }); + + Schema::table('subjects', function (Blueprint $table) { + $table->dropColumn('is_optional'); + }); + } +}; diff --git a/resources/js/components/tracker/SubjectChecklist.vue b/resources/js/components/tracker/SubjectChecklist.vue index be2da76a..6854f7bf 100644 --- a/resources/js/components/tracker/SubjectChecklist.vue +++ b/resources/js/components/tracker/SubjectChecklist.vue @@ -1,25 +1,33 @@ diff --git a/resources/js/components/tracker/SyllabusBreakdownModal.vue b/resources/js/components/tracker/SyllabusBreakdownModal.vue index 8a0b5a45..2bc85d99 100644 --- a/resources/js/components/tracker/SyllabusBreakdownModal.vue +++ b/resources/js/components/tracker/SyllabusBreakdownModal.vue @@ -17,6 +17,12 @@ export interface SyllabusChapter { name: string; weight: number; is_completed: boolean; + parent_id?: number | null; + parent?: { + id: number; + name: string; + sort_order?: number; + } | null; } export interface SyllabusSubject { @@ -32,6 +38,11 @@ export interface SyllabusSubject { chapters?: SyllabusChapter[]; } +export interface SyllabusChapterGroup { + name: string | null; + chapters: SyllabusChapter[]; +} + export interface SyllabusProgressData { course: string; group?: string; @@ -67,6 +78,102 @@ const toggleSubjectExpanded = (subjId: number) => { } }; +function getSubjectChapterGroups( + subj: SyllabusSubject, +): SyllabusChapterGroup[] { + const chapters = subj.chapters || []; + const hasAnyParent = chapters.some((c) => c.parent_id && c.parent?.name); + + if (!hasAnyParent) { + return [{ name: null, chapters }]; + } + + const groupsMap = new Map< + string, + { sort: number; chapters: SyllabusChapter[] } + >(); + const noParentChapters: SyllabusChapter[] = []; + + for (const chap of chapters) { + if (chap.parent?.name) { + const parentName = chap.parent.name; + const parentSort = chap.parent.sort_order ?? 999; + + if (!groupsMap.has(parentName)) { + groupsMap.set(parentName, { sort: parentSort, chapters: [] }); + } + + groupsMap.get(parentName)!.chapters.push(chap); + } else { + noParentChapters.push(chap); + } + } + + const result: SyllabusChapterGroup[] = Array.from(groupsMap.entries()) + .sort((a, b) => a[1].sort - b[1].sort) + .map(([name, data]) => ({ name, chapters: data.chapters })); + + if (noParentChapters.length > 0) { + result.push({ name: 'অন্যান্য অধ্যায়', chapters: noParentChapters }); + } + + return result; +} + +// Collapsed state for sub-groups with names (default: 1st group expanded (index 0), others collapsed) +const collapsedModalSubgroups = ref>(new Set()); + +function isModalSubgroupExpanded( + subjId: number, + groupIndex: number, + groupName: string | null, +): boolean { + if (!groupName) { + return true; + } + + const key = `${subjId}_${groupName}`; + + if (collapsedModalSubgroups.value.has(key)) { + return false; + } + + // Initially expand 1st group (index 0), others collapsed + if (groupIndex === 0) { + return true; + } + + const explicitExpandedKey = `exp_${key}`; + + return collapsedModalSubgroups.value.has(explicitExpandedKey); +} + +function toggleModalSubgroup( + subjId: number, + groupIndex: number, + groupName: string | null, +) { + if (!groupName) { + return; + } + + const key = `${subjId}_${groupName}`; + const explicitExpandedKey = `exp_${key}`; + const currentlyExpanded = isModalSubgroupExpanded( + subjId, + groupIndex, + groupName, + ); + + if (currentlyExpanded) { + collapsedModalSubgroups.value.add(key); + collapsedModalSubgroups.value.delete(explicitExpandedKey); + } else { + collapsedModalSubgroups.value.delete(key); + collapsedModalSubgroups.value.add(explicitExpandedKey); + } +} + const formatGroupName = (group?: string) => { if (!group) { return ''; @@ -242,84 +349,136 @@ function toggleChapterCompletion(subj: SyllabusSubject, chap: SyllabusChapter) { /> - +
-
+ +
+
+ + {{ group.name }} +
- -
+ {{ + group.chapters.filter((c) => c.is_completed) + .length + }}/{{ group.chapters.length }} +
- +
+
- {{ chap.name }} - -
+
+ + +
+ - - {{ - chap.weight === 3 - ? '(Large Chapter)' - : chap.weight === 1 - ? '(Small Chapter)' - : '(Normal Chapter)' - }} - + + {{ chap.name }} + +
+ + + {{ + chap.weight === 3 + ? '(Large Chapter)' + : chap.weight === 1 + ? '(Small Chapter)' + : '(Normal Chapter)' + }} + +
+
diff --git a/resources/js/pages/Tracker/Index.vue b/resources/js/pages/Tracker/Index.vue index df293c61..4863732b 100644 --- a/resources/js/pages/Tracker/Index.vue +++ b/resources/js/pages/Tracker/Index.vue @@ -12,6 +12,14 @@ interface Props { course: 'hsc' | 'ssc'; group: 'science' | 'humanities' | 'commerce'; subjects: SubjectItem[]; + excludedSubjectIds?: number[]; + optionalSubjects?: Array<{ + id: number; + name: string; + course: string; + group: string; + is_optional: boolean; + }>; completedNodeIds: number[]; todaySeconds: number; } @@ -107,6 +115,8 @@ function handleAuthRequired( :course="course" :group="group" :subjects="subjects" + :excluded-subject-ids="excludedSubjectIds" + :optional-subjects="optionalSubjects" :completed-node-ids="completedNodeIds" :is-authenticated="isAuthenticated" @require-auth="handleAuthRequired" From 7639c2b43c09a0ec434de7e0dea3313015e8065d Mon Sep 17 00:00:00 2001 From: TR Tajim Date: Tue, 13 Oct 2026 22:27:04 +0600 Subject: [PATCH 3/4] refactor(tracker): switch to include-first optional subjects selection and seed by slugs --- .../Controllers/StudyTrackerController.php | 33 +++++++++------ .../Controllers/UserProfileController.php | 21 +++++++--- app/Models/User.php | 4 +- ...cts_and_optional_subject_ids_to_users.php} | 35 ++++++++++------ .../components/tracker/SubjectChecklist.vue | 42 ++++++++----------- resources/js/pages/Tracker/Index.vue | 4 +- 6 files changed, 80 insertions(+), 59 deletions(-) rename database/migrations/{2026_10_13_155150_add_is_optional_to_subjects_and_excluded_subjects_to_users.php => 2026_10_13_155150_add_is_optional_to_subjects_and_optional_subject_ids_to_users.php} (52%) diff --git a/app/Http/Controllers/StudyTrackerController.php b/app/Http/Controllers/StudyTrackerController.php index 518ac2e2..77f165d8 100644 --- a/app/Http/Controllers/StudyTrackerController.php +++ b/app/Http/Controllers/StudyTrackerController.php @@ -18,17 +18,26 @@ public function index(Request $request): Response $user = $request->user(); $course = $user?->curriculum ?: 'hsc'; $group = $user?->group ?: 'science'; - $excludedSubjectIds = (array) ($user?->excluded_subject_ids ?? []); + $optionalSubjectIds = (array) ($user?->optional_subject_ids ?? []); $subjectsQuery = Subject::where('course', $course) ->where('is_trackable', true) - ->whereIn('group', [$group, 'common']) + ->where(function ($query) use ($group, $optionalSubjectIds) { + $query->where(function ($q) use ($group) { + $q->where('is_optional', false) + ->whereIn('group', [$group, 'common']); + }); + + if (! empty($optionalSubjectIds)) { + $query->orWhere(function ($q) use ($group, $optionalSubjectIds) { + $q->where('is_optional', true) + ->whereIn('group', [$group, 'common']) + ->whereIn('id', $optionalSubjectIds); + }); + } + }) ->orderBy('sort_order', 'asc'); - if (! empty($excludedSubjectIds)) { - $subjectsQuery->whereNotIn('id', $excludedSubjectIds); - } - $subjects = $subjectsQuery ->with(['nodes' => function ($query) { $query->where('is_trackable', true) @@ -39,7 +48,7 @@ public function index(Request $request): Response ->get(['id', 'name', 'slug', 'course', 'group', 'sort_order', 'is_optional']) ->toArray(); - // All trackable subjects for curriculum modal (so user can toggle optional subjects on/off) + // All trackable optional subjects for curriculum modal (so user can toggle optional subjects on/off) $groupSubjects = Subject::where('is_trackable', true) ->where('is_optional', true) ->orderBy('sort_order', 'asc') @@ -51,7 +60,7 @@ public function index(Request $request): Response 'course' => $course, 'group' => $group, 'subjects' => $subjects, - 'excludedSubjectIds' => [], + 'optionalSubjectIds' => [], 'optionalSubjects' => $groupSubjects, 'completedNodeIds' => [], 'todaySeconds' => 0, @@ -81,7 +90,7 @@ public function index(Request $request): Response 'course' => $course, 'group' => $group, 'subjects' => $subjects, - 'excludedSubjectIds' => $excludedSubjectIds, + 'optionalSubjectIds' => $optionalSubjectIds, 'optionalSubjects' => $groupSubjects, 'completedNodeIds' => $completedNodeIds, 'todaySeconds' => $todaySeconds, @@ -166,14 +175,14 @@ public function updateCurriculum(Request $request) $validated = $request->validate([ 'curriculum' => 'required|string|in:hsc,ssc', 'group' => 'required|string|in:science,humanities,commerce', - 'excluded_subject_ids' => 'nullable|array', - 'excluded_subject_ids.*' => 'integer|exists:subjects,id', + 'optional_subject_ids' => 'nullable|array', + 'optional_subject_ids.*' => 'integer|exists:subjects,id', ]); $request->user()->update([ 'curriculum' => $validated['curriculum'], 'group' => $validated['group'], - 'excluded_subject_ids' => $validated['excluded_subject_ids'] ?? [], + 'optional_subject_ids' => $validated['optional_subject_ids'] ?? [], ]); return redirect()->route('tracker.index') diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php index 44e6d087..49cbe3c3 100644 --- a/app/Http/Controllers/UserProfileController.php +++ b/app/Http/Controllers/UserProfileController.php @@ -393,17 +393,26 @@ private function getSyllabusProgress(User $user): array { $course = $user->curriculum ?: 'hsc'; $group = $user->group ?: 'science'; - $excludedSubjectIds = (array) ($user->excluded_subject_ids ?? []); + $optionalSubjectIds = (array) ($user->optional_subject_ids ?? []); $subjectsQuery = Subject::where('course', $course) ->where('is_trackable', true) - ->whereIn('group', [$group, 'common']) + ->where(function ($query) use ($group, $optionalSubjectIds) { + $query->where(function ($q) use ($group) { + $q->where('is_optional', false) + ->whereIn('group', [$group, 'common']); + }); + + if (! empty($optionalSubjectIds)) { + $query->orWhere(function ($q) use ($group, $optionalSubjectIds) { + $q->where('is_optional', true) + ->whereIn('group', [$group, 'common']) + ->whereIn('id', $optionalSubjectIds); + }); + } + }) ->orderBy('sort_order', 'asc'); - if (! empty($excludedSubjectIds)) { - $subjectsQuery->whereNotIn('id', $excludedSubjectIds); - } - $trackableSubjects = $subjectsQuery ->with(['nodes' => function ($query) { $query->where('is_trackable', true) diff --git a/app/Models/User.php b/app/Models/User.php index 40ff8926..99e9c875 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -59,7 +59,7 @@ class User extends Authenticatable 'facebook', 'instagram', 'github', - 'excluded_subject_ids', + 'optional_subject_ids', 'last_active_at', 'last_seen_at', ]; @@ -122,7 +122,7 @@ protected function casts(): array 'is_verified' => 'boolean', 'priority' => 'integer', 'allow_pokes' => 'boolean', - 'excluded_subject_ids' => 'array', + 'optional_subject_ids' => 'array', ]; } diff --git a/database/migrations/2026_10_13_155150_add_is_optional_to_subjects_and_excluded_subjects_to_users.php b/database/migrations/2026_10_13_155150_add_is_optional_to_subjects_and_optional_subject_ids_to_users.php similarity index 52% rename from database/migrations/2026_10_13_155150_add_is_optional_to_subjects_and_excluded_subjects_to_users.php rename to database/migrations/2026_10_13_155150_add_is_optional_to_subjects_and_optional_subject_ids_to_users.php index 25cde5c6..7ef66a88 100644 --- a/database/migrations/2026_10_13_155150_add_is_optional_to_subjects_and_excluded_subjects_to_users.php +++ b/database/migrations/2026_10_13_155150_add_is_optional_to_subjects_and_optional_subject_ids_to_users.php @@ -2,6 +2,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -16,26 +17,34 @@ public function up(): void }); Schema::table('users', function (Blueprint $table) { - $table->json('excluded_subject_ids')->nullable()->after('group'); + $table->json('optional_subject_ids')->nullable()->after('group'); }); - // Seed initial known optional subjects: - // HSC Science: Higher Math 1st & 2nd (9, 10), Biology 1st & 2nd (11, 12) - // SSC Science: Biology (24), Higher Math (25) - // HSC Commerce: Production Management 1st & 2nd (48, 49), Finance 1st & 2nd (46, 47) - // HSC Humanities: Islamic History 1st & 2nd (56, 57), Logic 1st & 2nd (54, 55), Economics 1st & 2nd (52, 53) - $optionalIds = [ + // Seed initial known optional subjects by unique slug: + $optionalSlugs = [ // HSC Science - 9, 10, 11, 12, + 'hsc-hmath-1st', + 'hsc-hmath-2nd', + 'hsc-biology-1st', + 'hsc-biology-2nd', // SSC Science - 24, 25, + 'ssc-biology', + 'ssc-higher-math', // HSC Commerce - 46, 47, 48, 49, + 'hsc-finance-1st', + 'hsc-finance-2nd', + 'hsc-marketing-1st', + 'hsc-marketing-2nd', // HSC Humanities - 52, 53, 54, 55, 56, 57, + 'hsc-economics-1st', + 'hsc-economics-2nd', + 'hsc-logic-1st', + 'hsc-logic-2nd', + 'hsc-islamic-history-1st', + 'hsc-islamic-history-2nd', ]; - DB::table('subjects')->whereIn('id', $optionalIds)->update(['is_optional' => true]); + DB::table('subjects')->whereIn('slug', $optionalSlugs)->update(['is_optional' => true]); } /** @@ -44,7 +53,7 @@ public function up(): void public function down(): void { Schema::table('users', function (Blueprint $table) { - $table->dropColumn('excluded_subject_ids'); + $table->dropColumn('optional_subject_ids'); }); Schema::table('subjects', function (Blueprint $table) { diff --git a/resources/js/components/tracker/SubjectChecklist.vue b/resources/js/components/tracker/SubjectChecklist.vue index 6854f7bf..4edb0ee1 100644 --- a/resources/js/components/tracker/SubjectChecklist.vue +++ b/resources/js/components/tracker/SubjectChecklist.vue @@ -51,7 +51,7 @@ interface Props { course: 'hsc' | 'ssc'; group?: 'science' | 'humanities' | 'commerce'; subjects: SubjectItem[]; - excludedSubjectIds?: number[]; + optionalSubjectIds?: number[]; optionalSubjects?: OptionalSubjectItem[]; completedNodeIds: number[]; isAuthenticated: boolean; @@ -59,7 +59,7 @@ interface Props { const props = withDefaults(defineProps(), { group: 'science', - excludedSubjectIds: () => [], + optionalSubjectIds: () => [], optionalSubjects: () => [], }); @@ -92,17 +92,17 @@ function toggleExpand(subjectId: number) { const showSwitchConfirmModal = ref(false); const pendingCurriculum = ref<'hsc' | 'ssc'>(props.course); const pendingGroup = ref<'science' | 'humanities' | 'commerce'>(props.group); -const pendingExcludedSubjectIds = ref>( - new Set(props.excludedSubjectIds), +const pendingOptionalSubjectIds = ref>( + new Set(props.optionalSubjectIds), ); const isSwitching = ref(false); watch( - () => [props.course, props.group, props.excludedSubjectIds] as const, - ([newCourse, newGroup, newExcluded]) => { + () => [props.course, props.group, props.optionalSubjectIds] as const, + ([newCourse, newGroup, newOptionals]) => { pendingCurriculum.value = newCourse; pendingGroup.value = newGroup; - pendingExcludedSubjectIds.value = new Set(newExcluded); + pendingOptionalSubjectIds.value = new Set(newOptionals); }, ); @@ -116,10 +116,10 @@ const relevantOptionalSubjects = computed(() => { }); function toggleOptionalSubject(subjId: number) { - if (pendingExcludedSubjectIds.value.has(subjId)) { - pendingExcludedSubjectIds.value.delete(subjId); + if (pendingOptionalSubjectIds.value.has(subjId)) { + pendingOptionalSubjectIds.value.delete(subjId); } else { - pendingExcludedSubjectIds.value.add(subjId); + pendingOptionalSubjectIds.value.add(subjId); } } @@ -132,7 +132,7 @@ function openSwitchModal() { pendingCurriculum.value = props.course; pendingGroup.value = props.group; - pendingExcludedSubjectIds.value = new Set(props.excludedSubjectIds); + pendingOptionalSubjectIds.value = new Set(props.optionalSubjectIds); showSwitchConfirmModal.value = true; } @@ -154,7 +154,7 @@ function confirmSwitchCurriculum() { { curriculum: pendingCurriculum.value, group: pendingGroup.value, - excluded_subject_ids: Array.from(pendingExcludedSubjectIds.value), + optional_subject_ids: Array.from(pendingOptionalSubjectIds.value), }, { preserveScroll: true, @@ -887,13 +887,12 @@ onUnmounted(() => {

- যে বিষয়গুলো আপনার সিলেবাসে নেই, সেগুলোর টিক চিহ্ন - তুলে দিন + যে বিষয়গুলো আপনার গ্রুপে আছে সেগুলো নির্বাচন করুন

@@ -904,33 +903,28 @@ onUnmounted(() => { type="button" @click="toggleOptionalSubject(subj.id)" :class="[ - !pendingExcludedSubjectIds.has(subj.id) + pendingOptionalSubjectIds.has(subj.id) ? 'border-indigo-600 bg-indigo-50/60 text-indigo-950 ring-1 ring-indigo-600 dark:border-indigo-500 dark:bg-indigo-950/40 dark:text-indigo-200' - : 'border-slate-200 bg-white text-slate-400 opacity-60 hover:bg-slate-50 hover:text-slate-600 dark:border-gray-800 dark:bg-gray-800/80 dark:text-gray-500 dark:hover:bg-gray-800 dark:hover:text-gray-400', + : 'border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50 dark:border-gray-800 dark:bg-gray-800/80 dark:text-gray-300 dark:hover:bg-gray-800', ]" class="flex cursor-pointer items-center justify-between rounded-xl border p-2.5 px-3 text-left transition" > {{ subj.name }} diff --git a/resources/js/pages/Tracker/Index.vue b/resources/js/pages/Tracker/Index.vue index 4863732b..a71fb908 100644 --- a/resources/js/pages/Tracker/Index.vue +++ b/resources/js/pages/Tracker/Index.vue @@ -12,7 +12,7 @@ interface Props { course: 'hsc' | 'ssc'; group: 'science' | 'humanities' | 'commerce'; subjects: SubjectItem[]; - excludedSubjectIds?: number[]; + optionalSubjectIds?: number[]; optionalSubjects?: Array<{ id: number; name: string; @@ -115,7 +115,7 @@ function handleAuthRequired( :course="course" :group="group" :subjects="subjects" - :excluded-subject-ids="excludedSubjectIds" + :optional-subject-ids="optionalSubjectIds" :optional-subjects="optionalSubjects" :completed-node-ids="completedNodeIds" :is-authenticated="isAuthenticated" From cad2883ee6258ecec1faf152ec8cab01c85e8396 Mon Sep 17 00:00:00 2001 From: TR Tajim Date: Wed, 14 Oct 2026 00:22:53 +0600 Subject: [PATCH 4/4] feat(tracker): add eye-catching optional subjects callout banner and status bar --- .../components/tracker/SubjectChecklist.vue | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/resources/js/components/tracker/SubjectChecklist.vue b/resources/js/components/tracker/SubjectChecklist.vue index 4edb0ee1..e0d224a0 100644 --- a/resources/js/components/tracker/SubjectChecklist.vue +++ b/resources/js/components/tracker/SubjectChecklist.vue @@ -10,6 +10,7 @@ import { ExternalLink, Loader2, Share2, + SlidersHorizontal, } from 'lucide-vue-next'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; import BaseModal from '@/components/BaseModal.vue'; @@ -115,6 +116,29 @@ const relevantOptionalSubjects = computed(() => { ); }); +const selectedOptionalSubjectsCount = computed(() => { + return (props.optionalSubjectIds || []).length; +}); + +const selectedOptionalSubjectNames = computed(() => { + const set = new Set(props.optionalSubjectIds || []); + + return props.optionalSubjects + .filter( + (s) => + s.course === props.course && + s.group === props.group && + set.has(s.id), + ) + .map((s) => s.name); +}); + +const hasAvailableOptionalSubjects = computed(() => { + return props.optionalSubjects.some( + (s) => s.course === props.course && s.group === props.group, + ); +}); + function toggleOptionalSubject(subjId: number) { if (pendingOptionalSubjectIds.value.has(subjId)) { pendingOptionalSubjectIds.value.delete(subjId); @@ -537,6 +561,75 @@ onUnmounted(() => { + +
+
+ + + +
+

+ অতিরিক্ত বিষয় নির্বাচন করুন +

+

+ আপনার গ্রুপের ঐচ্ছিক বিষয়গুলো সিলেবাসে যুক্ত করতে + নির্বাচন করুন +

+
+
+ + +
+ +
+
+ + + + + অতিরিক্ত বিষয়: + {{ selectedOptionalSubjectNames.join(', ') }} + +
+ +
+