Skip to content

Multiple fixes#44

Merged
MasterMarcoHD merged 14 commits intomainfrom
lpa-43RR
Oct 2, 2025
Merged

Multiple fixes#44
MasterMarcoHD merged 14 commits intomainfrom
lpa-43RR

Conversation

@MasterMarcoHD
Copy link
Contributor

No description provided.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added Slots View Switcher in the title bar for quick capability-based navigation.
    • Introduced popovers to reveal additional supervisors and course–vintage mappings.
    • New concise course–vintage mapping display.
  • UI/UX

    • Improved spacing, flexible text, and overflow handling; cleaner timetable and overview layouts.
    • Title bars now show contextual trailing actions.
    • Web URLs now use hash-based routing.
  • Refactor

    • Converted key slot screens to stateful for dynamic title bar actions.
    • Navigation updated to push-based routing; faster direct course lookup and enhanced slot querying.
  • Style

    • Dropdown menus now feature a bordered rounded shape.

Walkthrough

Navigation calls switched from navigate to pushNamed; several slot screens became stateful and set title-bar trailing widgets. New widgets (MappingWidget, SlotDataPopOver, SlotsViewSwitcher) and exports added. Repositories gained getById/query and extra dependencies. Layout tweaks (greedy(), flexible, removed carousels), date normalization, and a dropdown border were applied.

Changes

Cohort / File(s) Summary
Navigation & startup
lib/main.dart, lib/src/app/presentation/widgets/title_bar.dart, lib/src/app/utils/no_mobile_utils.dart
Commented out setPathUrlStrategy(); replaced Modular.to.navigate(...) with Modular.to.pushNamed(...) for back/dashboard navigation; added spacing and joined capability text in title bar.
Layout adjustments (greedy / clip)
lib/src/course_overview/presentation/screens/course_overview_screen.dart, lib/src/course_overview/presentation/screens/courses_overview_screen.dart
Wrapped returned widgets with .greedy() and set SingleChildScrollView.clipBehavior = Clip.none.
User widget API & usage
lib/src/moodle/presentation/widgets/user_widget.dart, lib/src/slots/presentation/widgets/slot_widget.dart
Renamed expandflexible; replaced conditional Expanded usage with .flexible() extension; updated call sites to flexible: true.
Slots repositories: additions & query
lib/src/slots/presentation/repositories/slot_master_courses_repository.dart, lib/src/slots/presentation/repositories/slot_master_slots_repository.dart
Added MoodleCourse? getById(int id); SlotMasterSlotsRepository now depends on courses and users repos, constructor updated, added List<Slot> query(String, {Iterable<Slot>?}), and a deprecated Iterable<Slot>.query extension.
Domain: date normalization
lib/src/slots/domain/models/slot.dart
Weekday.nextDate now returns dates normalized to midnight (time components zeroed).
Screens: title-bar integration & stateful conversions
lib/src/slots/presentation/screens/slot_details_screen.dart, lib/src/slots/presentation/screens/slot_master_screen.dart, lib/src/slots/presentation/screens/slot_overview_screen.dart, lib/src/slots/presentation/screens/slot_reservation_screen.dart
Screens now set parent route and trailing widgets via TitleBarState; slotTimeTable signature gains SlotMasterSlotsRepository repo and uses repo.query; SlotOverviewScreen and SlotReservationScreen converted to stateful with didChangeDependencies to install trailing widgets; formatter references updated.
New widgets & exports
lib/src/slots/presentation/widgets/mapping_widget.dart, lib/src/slots/presentation/widgets/slot_data_pop_over.dart, lib/src/slots/presentation/widgets/slots_view_switcher.dart, lib/src/slots/presentation/widgets/widgets.dart
Added MappingWidget, SlotDataPopOver, SlotsViewSwitcher and a UserCapability extension; re-exported new widgets via widgets.dart.
Remove carousels → popovers
lib/src/slots/presentation/widgets/slot_master_widget.dart, lib/src/slots/presentation/widgets/slot_overview_widget.dart
Replaced CarouselSlider usages with SlotDataPopOver lists and MappingWidget; switched course lookup to getById; removed carousel import.
Small UI/theming tweaks
lib/src/theming/infra/services/material_theme_generator_service.dart
DropdownMenuThemeData menu shape updated to squircle(side: BorderSide(color: themeBase.tertiaryColor)) (adds border).

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Switcher as SlotsViewSwitcher
  participant UserRepo as UserRepository
  participant Router as Modular.to

  User->>Switcher: Open dropdown
  Switcher->>UserRepo: getCapabilities()
  Switcher-->>User: Show options (icons, labels)
  User->>Switcher: Select capability
  Switcher->>Router: pushNamed(selectedCapability.slotRoute)
Loading
sequenceDiagram
  participant UI as Slot UI
  participant SlotsRepo as SlotMasterSlotsRepository
  participant Courses as SlotMasterCoursesRepository
  participant Users as UsersRepository

  UI->>SlotsRepo: query("text", {slots?})
  alt slots provided
    SlotsRepo-->>SlotsRepo: use provided slots
  else no slots
    SlotsRepo-->>SlotsRepo: use current state data
  end
  SlotsRepo->>Courses: getById(...) / course names
  SlotsRepo->>Users: supervisor names
  SlotsRepo-->>UI: filtered list of slots
Loading
sequenceDiagram
  actor User
  participant Item as SlotDataPopOver
  participant Pop as Popover

  User->>Item: Hover / Click
  alt Multiple items
    Item->>Pop: showPopover(remaining items)
    User-->>Pop: Hover content
    Pop-->>Item: Close on delayed mouseout if not hovered
  else Single item
    Item-->>User: Render primary only
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

In burrows of code I nudge the route,
pushNamed hops in where navigate once stood.
Popovers bloom where carousels used to spin,
Mappings and switches snugly tuck within.
A rabbit's wink — query, trim, and run — carrot done. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title “Multiple fixes” is a vague summary that does not clearly convey the primary changes or focus of this pull request, which includes navigation updates, new widgets, layout adjustments, and API extensions. It fails to highlight any specific change or the main purpose of the PR. Rename the pull request to a concise, descriptive title that captures the main change or objective, such as “Replace navigate with pushNamed and add SlotDataPopOver widget.”
Description Check ⚠️ Warning There is no pull request description, so there is no information about the changes or intent, making it impossible for reviewers to understand the scope and rationale of this PR. Add a summary describing the changes made, their purpose, and any relevant context to help reviewers understand what this PR accomplishes.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch lpa-43RR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@necodeit
Copy link

necodeit bot commented Oct 2, 2025

Analysis Report for db9b314

  • Infos: 11
  • Warnings: 0
  • Errors: 2
Click to see the full report
error • The method 'greedy' isn't defined for the type 'Card' • lib/src/course_overview/presentation/screens/course_overview_screen.dart:165:9 • undefined_method
error • The method 'greedy' isn't defined for the type 'SingleChildScrollView' • lib/src/course_overview/presentation/screens/courses_overview_screen.dart:55:9 • undefined_method
info • The value of the argument is redundant because it matches the default value • lib/src/slots/presentation/screens/slot_reservation_screen.dart:51:38 • avoid_redundant_argument_values
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/mapping_widget.dart:6:7 • public_member_api_docs
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/mapping_widget.dart:7:9 • public_member_api_docs
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/mapping_widget.dart:9:22 • public_member_api_docs
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/mapping_widget.dart:11:17 • public_member_api_docs
info • The value of the argument is redundant because it matches the default value • lib/src/slots/presentation/widgets/mapping_widget.dart:22:85 • avoid_redundant_argument_values
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/slot_data_pop_over.dart:6:7 • public_member_api_docs
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/slot_data_pop_over.dart:7:9 • public_member_api_docs
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/slot_data_pop_over.dart:9:22 • public_member_api_docs
info • The value of the argument is redundant because it matches the default value • lib/src/slots/presentation/widgets/slot_data_pop_over.dart:84:38 • avoid_redundant_argument_values
info • The value of the argument is redundant because it matches the default value • lib/src/slots/presentation/widgets/slot_data_pop_over.dart:87:47 • avoid_redundant_argument_values

View annotated files

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (1)
lib/src/app/presentation/widgets/title_bar.dart (1)

371-371: Verify the intended navigation behavior for the back button.

Same concern as in the desktop build path (line 218): the back button uses pushNamed(_parentRoute!), which may not be the intended behavior for a back action. Please verify whether pushing or replacing is the correct navigation pattern here.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1dc6bf and db9b314.

📒 Files selected for processing (21)
  • lib/main.dart (1 hunks)
  • lib/src/app/presentation/widgets/title_bar.dart (3 hunks)
  • lib/src/app/utils/no_mobile_utils.dart (1 hunks)
  • lib/src/course_overview/presentation/screens/course_overview_screen.dart (1 hunks)
  • lib/src/course_overview/presentation/screens/courses_overview_screen.dart (2 hunks)
  • lib/src/moodle/presentation/widgets/user_widget.dart (2 hunks)
  • lib/src/slots/domain/models/slot.dart (1 hunks)
  • lib/src/slots/presentation/repositories/slot_master_courses_repository.dart (1 hunks)
  • lib/src/slots/presentation/repositories/slot_master_slots_repository.dart (3 hunks)
  • lib/src/slots/presentation/screens/slot_details_screen.dart (5 hunks)
  • lib/src/slots/presentation/screens/slot_master_screen.dart (3 hunks)
  • lib/src/slots/presentation/screens/slot_overview_screen.dart (2 hunks)
  • lib/src/slots/presentation/screens/slot_reservation_screen.dart (4 hunks)
  • lib/src/slots/presentation/widgets/mapping_widget.dart (1 hunks)
  • lib/src/slots/presentation/widgets/slot_data_pop_over.dart (1 hunks)
  • lib/src/slots/presentation/widgets/slot_master_widget.dart (3 hunks)
  • lib/src/slots/presentation/widgets/slot_overview_widget.dart (2 hunks)
  • lib/src/slots/presentation/widgets/slot_widget.dart (1 hunks)
  • lib/src/slots/presentation/widgets/slots_view_switcher.dart (1 hunks)
  • lib/src/slots/presentation/widgets/widgets.dart (1 hunks)
  • lib/src/theming/infra/services/material_theme_generator_service.dart (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Testing 🧪
  • GitHub Check: Analysis 🔍
🔇 Additional comments (11)
lib/src/course_overview/presentation/screens/courses_overview_screen.dart (1)

42-55: Verify the layout behavior with clipBehavior: Clip.none and greedy().

The combination of clipBehavior: Clip.none on the SingleChildScrollView and the .greedy() extension on the outer Padding is unusual:

  • Clip.none allows content to render outside the scroll view's bounds, which can cause visual overflow issues if child widgets extend beyond the viewport.
  • .greedy() forces the Padding to consume all available space from its parent, which may conflict with SingleChildScrollView's intrinsic content sizing, potentially leading to layout warnings or unexpected scroll behavior.

Please verify that this combination produces the intended layout without overflow or scroll issues across different screen sizes and content amounts. If this is a workaround for a specific layout problem, consider documenting the reasoning with a comment.

lib/src/slots/presentation/widgets/slot_widget.dart (1)

286-289: LGTM! Parameter rename aligns with UserWidget API update.

The parameter name change from expand: true to flexible: true correctly reflects the updated UserWidget API. This change maintains the same layout behavior while using the new parameter name.

lib/src/course_overview/presentation/screens/course_overview_screen.dart (1)

76-166: Verify the layout behavior with greedy().

The .greedy() extension forces the outer Padding to consume all available space. While this may be intentional to ensure the Card and DataTable fill the screen, verify that this doesn't cause layout issues on smaller screens or when the table content is minimal.

lib/src/slots/presentation/screens/slot_master_screen.dart (2)

44-46: LGTM! Trailing widget enhancement improves UI flexibility.

Setting the trailing widget to SlotsViewSwitcher in didChangeDependencies enhances the title bar with dynamic controls, aligning with the broader pattern of stateful title bar customization introduced in this PR.


93-136: LGTM! Refactoring filtering logic to the repository improves separation of concerns.

The updated slotTimeTable signature now accepts SlotMasterSlotsRepository repo and delegates filtering to repo.query(...) instead of performing inline filtering. This refactor:

  • Centralizes filtering logic in the repository, making it reusable and testable.
  • Reduces code duplication and improves maintainability.
  • Aligns with the new repository query capabilities introduced in this PR.
lib/src/slots/presentation/widgets/slot_overview_widget.dart (2)

27-35: LGTM! Using getById improves clarity and performance.

Switching from courseRepository.filter(id: m.courseId).firstOrNull to courseRepository.getById(m.courseId) is a cleaner and more efficient approach for single-object retrieval. The subsequent .where((cv) => cv.$1 != null) filter ensures only valid courses are processed, preventing null reference issues downstream.


75-77: LGTM! Replacing carousel with SlotDataPopOver improves UI consistency.

The new SlotDataPopOver widget with MappingWidget provides a more consistent and reusable approach for displaying slot mappings, aligning with the PR's goal to move away from CarouselSlider-based presentation. The non-null assertion course! is safe because the preceding filter removes null courses.

lib/src/app/presentation/widgets/title_bar.dart (3)

218-218: Verify the intended navigation behavior for the back button.

The back button uses pushNamed(_parentRoute!), which pushes a new route onto the navigation stack. Typical back button behavior is to pop the current route or navigate to the parent route without adding to the history. Using pushNamed may result in users navigating through multiple instances of the same parent route.

If the intent is to replace the current route (standard back button behavior), consider using Modular.to.navigate(_parentRoute!) (which replaces) or Modular.to.pop() with conditional logic. If pushing is intentional, please confirm the reasoning.

This is the same concern raised in lib/src/app/utils/no_mobile_utils.dart (line 22).


318-318: Verify the layout impact of displaying all user capabilities.

The change from displaying only the highest capability (user.capabilities.highest.translate(context)) to displaying all capabilities (user.capabilities.map((c) => c.translate(context)).join(', ')) provides more information to the user, which is generally beneficial. However, if a user has multiple capabilities, the resulting string may become quite long and affect the layout or cause overflow in the title bar.

Please verify that the title bar layout handles longer capability strings gracefully across different screen sizes and capability counts. Consider adding overflow handling (e.g., TextOverflow.ellipsis) or a tooltip to display the full list if it's too long.


213-213: LGTM! Spacing enhancement improves visual layout.

Adding spacing: Spacing.mediumSpacing to the leading Row improves the horizontal spacing between elements in the title bar, enhancing readability and visual balance.

lib/src/app/utils/no_mobile_utils.dart (1)

22-22: Verify "Go Back" navigation behavior (no_mobile_utils.dart:22)
The button label implies returning to the previous screen, but it currently uses Modular.to.pushNamed('/dashboard/'), which pushes a new route. If you intended to replace or pop, use Modular.to.navigate('/dashboard/') or Modular.to.pop(). Otherwise confirm that pushing is desired.

Comment on lines +32 to 37
Text(
user.fullname,
style: style,
overflow: TextOverflow.ellipsis,
).flexible(),
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore optional flex behavior

flexible is never read now—the text is always wrapped in .flexible(), so callers setting flexible: false still get a Flexible child, altering layouts (and regressing previous behavior). Gate the modifier on the flag instead of forcing it.

-        Text(
-          user.fullname,
-          style: style,
-          overflow: TextOverflow.ellipsis,
-        ).flexible(),
+        final name = Text(
+          user.fullname,
+          style: style,
+          overflow: TextOverflow.ellipsis,
+        );
+
+        return Row(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            UserProfileImage(
+              userId: user.id,
+              size: size,
+            ),
+            Spacing.smallHorizontal(),
+            flexible ? name.flexible() : name,
+          ],
+        );

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In lib/src/moodle/presentation/widgets/user_widget.dart around lines 32 to 37,
the Text widget is always wrapped with .flexible() which ignores the flexible
flag and alters layouts; change the code to conditionally wrap the Text: if the
widget's flexible flag is true wrap the Text with .flexible(), otherwise return
the plain Text widget so callers passing flexible: false get an unwrapped child.

Comment on lines +22 to +28
class _SlotOverviewScreenState extends State<SlotOverviewScreen> with AdaptiveState, NoMobile {
@override
void didChangeDependencies() {
super.didChangeDependencies();

Data.of<TitleBarState>(context).setTrailingWidget(const SlotsViewSwitcher());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clear the TitleBar trailing widget when this screen disposes

We set a SlotsViewSwitcher on the shared TitleBarState, but never clear it. After navigating away, any screen that doesn’t explicitly override the trailing slot inherits the leftover switcher, leading to a stale UI. Cache the TitleBarState and reset the trailing widget (e.g. setTrailingWidget(null) or the dedicated clear API) in dispose() so the header returns to its default state when this screen is popped.

 class _SlotOverviewScreenState extends State<SlotOverviewScreen> with AdaptiveState, NoMobile {
+  TitleBarState? _titleBarState;
+
   @override
   void didChangeDependencies() {
     super.didChangeDependencies();
-
-    Data.of<TitleBarState>(context).setTrailingWidget(const SlotsViewSwitcher());
+    _titleBarState = Data.of<TitleBarState>(context);
+    _titleBarState!.setTrailingWidget(const SlotsViewSwitcher());
   }
+
+  @override
+  void dispose() {
+    _titleBarState?.setTrailingWidget(null);
+    super.dispose();
+  }
 }
🤖 Prompt for AI Agents
In lib/src/slots/presentation/screens/slot_overview_screen.dart around lines 22
to 28, the TitleBarState trailing widget is set to SlotsViewSwitcher in
didChangeDependencies but never cleared; cache the TitleBarState instance when
you call Data.of<TitleBarState>(context) and then override dispose() to call the
TitleBarState clear API (e.g. setTrailingWidget(null) or the dedicated clear
method) to reset the header before calling super.dispose().

Comment on lines +20 to +48
final currentRoute = Modular.to.path;
final capabilities = user.state.data?.capabilities.toList() ?? [];

if (capabilities.length <= 1) return const SizedBox.shrink();

return DropdownMenu<UserCapability>(
inputDecorationTheme: context.theme.dropdownMenuTheme.inputDecorationTheme
?.copyWith(fillColor: context.theme.cardColor, contentPadding: PaddingHorizontal(Spacing.smallSpacing)),
dropdownMenuEntries: [
for (final r in capabilities)
DropdownMenuEntry(
value: r,
label: r.translateSlotRoute(context),
labelWidget: Text(r.translateSlotRoute(context)),
leadingIcon: Icon(r.slotIcon, size: 16),
),
],

trailingIcon: const Icon(
FontAwesome5Solid.chevron_down,
size: 13,
),
leadingIcon: Icon(
SlotUserCapabilityX.capabilityFromRoute(currentRoute).slotIcon,
size: 16,
),
requestFocusOnTap: false, // disable text input
initialSelection: SlotUserCapabilityX.capabilityFromRoute(currentRoute),
onSelected: (r) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard against unmatched current routes

SlotUserCapabilityX.capabilityFromRoute throws when the current path is empty or '/'. Modular.to.path is known to briefly expose those values (see _preventEmptyRoute in AppWidget), so the switcher can crash while building. Capture the capability via a nullable lookup and bail out early when it’s not available before wiring it into leadingIcon/initialSelection.

-    final currentRoute = Modular.to.path;
-    final capabilities = user.state.data?.capabilities.toList() ?? [];
-
-    if (capabilities.length <= 1) return const SizedBox.shrink();
+    final currentRoute = Modular.to.path;
+    final capabilities = user.state.data?.capabilities.toList() ?? [];
+    final currentCapability = SlotUserCapabilityX.maybeCapabilityFromRoute(currentRoute);
+
+    if (capabilities.length <= 1 || currentCapability == null) return const SizedBox.shrink();
 ...
-      leadingIcon: Icon(
-        SlotUserCapabilityX.capabilityFromRoute(currentRoute).slotIcon,
+      leadingIcon: Icon(
+        currentCapability.slotIcon,
         size: 16,
       ),
 ...
-      initialSelection: SlotUserCapabilityX.capabilityFromRoute(currentRoute),
+      initialSelection: currentCapability,

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In lib/src/slots/presentation/widgets/slots_view_switcher.dart around lines 20
to 48, calling SlotUserCapabilityX.capabilityFromRoute(currentRoute) can throw
for empty or '/' paths; obtain the capability via a nullable-safe lookup (or
wrap the call in a try/catch returning null) into a local variable, and if that
variable is null return SizedBox.shrink() before building the DropdownMenu so
you don't wire a thrown value into leadingIcon or initialSelection; then use the
safely-obtained non-null capability for leadingIcon and initialSelection.

Comment on lines +70 to +85
///
/// If no matching capability is found this throws.
static UserCapability capabilityFromRoute(String route) {
if (route.startsWith(UserCapability.teacher.slotRoute)) {
return UserCapability.teacher;
}
if (route.startsWith(UserCapability.student.slotRoute)) {
return UserCapability.student;
}
if (route.startsWith(UserCapability.slotMaster.slotRoute)) {
return UserCapability.slotMaster;
}

throw ArgumentError.value(route, 'route', 'Unkown route');
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Expose a safe route→capability lookup

Provide a non-throwing helper so the widget can avoid crashing when the path is temporarily empty or unrecognised, and have the throwing variant delegate to it. Also fix the typo in the error message.

+  /// Returns the [UserCapability] linked to the given [route] or `null` if none matches.
+  static UserCapability? maybeCapabilityFromRoute(String route) {
+    if (route.startsWith(UserCapability.teacher.slotRoute)) {
+      return UserCapability.teacher;
+    }
+    if (route.startsWith(UserCapability.student.slotRoute)) {
+      return UserCapability.student;
+    }
+    if (route.startsWith(UserCapability.slotMaster.slotRoute)) {
+      return UserCapability.slotMaster;
+    }
+
+    return null;
+  }
+
   /// Returns the [UserCapability] linked to the given [route].
   ///
   /// If no matching capability is found this throws.
   static UserCapability capabilityFromRoute(String route) {
-    if (route.startsWith(UserCapability.teacher.slotRoute)) {
-      return UserCapability.teacher;
-    }
-    if (route.startsWith(UserCapability.student.slotRoute)) {
-      return UserCapability.student;
-    }
-    if (route.startsWith(UserCapability.slotMaster.slotRoute)) {
-      return UserCapability.slotMaster;
-    }
-
-    throw ArgumentError.value(route, 'route', 'Unkown route');
+    return maybeCapabilityFromRoute(route) ?? (throw ArgumentError.value(route, 'route', 'Unknown route'));
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
///
/// If no matching capability is found this throws.
static UserCapability capabilityFromRoute(String route) {
if (route.startsWith(UserCapability.teacher.slotRoute)) {
return UserCapability.teacher;
}
if (route.startsWith(UserCapability.student.slotRoute)) {
return UserCapability.student;
}
if (route.startsWith(UserCapability.slotMaster.slotRoute)) {
return UserCapability.slotMaster;
}
throw ArgumentError.value(route, 'route', 'Unkown route');
}
/// Returns the [UserCapability] linked to the given [route] or `null` if none matches.
static UserCapability? maybeCapabilityFromRoute(String route) {
if (route.startsWith(UserCapability.teacher.slotRoute)) {
return UserCapability.teacher;
}
if (route.startsWith(UserCapability.student.slotRoute)) {
return UserCapability.student;
}
if (route.startsWith(UserCapability.slotMaster.slotRoute)) {
return UserCapability.slotMaster;
}
return null;
}
/// Returns the [UserCapability] linked to the given [route].
///
/// If no matching capability is found this throws.
static UserCapability capabilityFromRoute(String route) {
return maybeCapabilityFromRoute(route)
?? (throw ArgumentError.value(route, 'route', 'Unknown route'));
}
🤖 Prompt for AI Agents
In lib/src/slots/presentation/widgets/slots_view_switcher.dart around lines 70
to 85, add a non-throwing helper (e.g. UserCapability?
tryCapabilityFromRoute(String? route)) that returns null for
null/empty/unrecognised routes and keeps the same prefix checks, then change the
existing static capabilityFromRoute to call that helper and throw
ArgumentError.value(...) only if the helper returned null; also fix the typo in
the error message from 'Unkown route' to 'Unknown route'.

@necodeit
Copy link

necodeit bot commented Oct 2, 2025

Analysis Report for e96b07d

  • Infos: 11
  • Warnings: 0
  • Errors: 0
Click to see the full report
info • The value of the argument is redundant because it matches the default value • lib/src/slots/presentation/screens/slot_reservation_screen.dart:51:38 • avoid_redundant_argument_values
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/mapping_widget.dart:6:7 • public_member_api_docs
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/mapping_widget.dart:7:9 • public_member_api_docs
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/mapping_widget.dart:9:22 • public_member_api_docs
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/mapping_widget.dart:11:17 • public_member_api_docs
info • The value of the argument is redundant because it matches the default value • lib/src/slots/presentation/widgets/mapping_widget.dart:22:85 • avoid_redundant_argument_values
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/slot_data_pop_over.dart:6:7 • public_member_api_docs
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/slot_data_pop_over.dart:7:9 • public_member_api_docs
info • Missing documentation for a public member • lib/src/slots/presentation/widgets/slot_data_pop_over.dart:9:22 • public_member_api_docs
info • The value of the argument is redundant because it matches the default value • lib/src/slots/presentation/widgets/slot_data_pop_over.dart:88:38 • avoid_redundant_argument_values
info • The value of the argument is redundant because it matches the default value • lib/src/slots/presentation/widgets/slot_data_pop_over.dart:91:47 • avoid_redundant_argument_values

View annotated files

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db9b314 and e96b07d.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • lib/src/course_overview/presentation/screens/course_overview_screen.dart (1 hunks)
  • lib/src/course_overview/presentation/screens/courses_overview_screen.dart (2 hunks)
  • lib/src/slots/presentation/widgets/slot_data_pop_over.dart (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analysis 🔍
  • GitHub Check: Testing 🧪
🔇 Additional comments (3)
lib/src/course_overview/presentation/screens/course_overview_screen.dart (1)

165-165: LGTM! Layout expansion applied correctly.

The .greedy() extension (from awesome_extensions) makes the Padding widget expand to fill all available space in its parent, which is appropriate for a desktop layout where the course overview table should occupy the full viewport. This change preserves all existing functionality while improving the layout behavior.

lib/src/course_overview/presentation/screens/courses_overview_screen.dart (2)

43-43: Clarify the need for Clip.none on the scroll view.

Setting clipBehavior: Clip.none on a SingleChildScrollView allows child content to render outside the scroll viewport, which is unusual and can cause visual artifacts. Course cards may render outside the intended scroll area and interfere with parent layout constraints.

Unless there's a specific requirement for content to overflow the scroll bounds (e.g., shadows, tooltips, or popovers that need to escape), the default Clip.hardEdge is typically preferred for scroll views to ensure content is properly clipped.

Please clarify:

  1. Is there a specific visual element (shadow, popover, tooltip) that requires rendering outside the scroll bounds?
  2. Have you tested this on different screen sizes to ensure no unintended overflow occurs?

If overflow is not required, consider removing this property to use the default clipping behavior.


55-55: LGTM! Appropriate use of .greedy() for main content.

The .greedy() extension (from awesome_extensions) makes the scroll view expand to fill available space, which is the correct pattern for a main content area. This ensures the course overview takes up the full viewport even when content is minimal.

Based on learnings.

Comment on lines +20 to +109
void closePopUp({bool forceClose = false}) {
if (popupContext == null) return;

if ((parentHover || childHover) && !forceClose) return;

Navigator.of(popupContext!).pop();

popupContext = null;
}

@override
Widget build(BuildContext context) {
return HoverBuilder(
cursor: SystemMouseCursors.basic,
builder: (context, isHovering) {
parentHover = isHovering;
Future.delayed(const Duration(milliseconds: 300), closePopUp);
if (widget.contentList.isEmpty) {
closePopUp(forceClose: true);
return const SizedBox.shrink();
}
return Row(
children: [
Expanded(
child: widget.contentList[0],
),
if (widget.contentList.length > 1) ...[
Spacing.xsHorizontal(),
GestureDetector(
// ignore: sort_child_properties_last
child: Chip(
label: Text('+${widget.contentList.length - 1}'),
),
onTap: widget.contentList.length > 1
? () {
if (popupContext != null) {
closePopUp(forceClose: true);
return;
}
showPopover(
context: context,
bodyBuilder: (context) {
popupContext = context;
return HoverBuilder(
cursor: SystemMouseCursors.basic,
builder: (context, isHovering) {
childHover = isHovering;
Future.delayed(const Duration(milliseconds: 300), closePopUp);
return Card(
elevation: 16,
shape: squircle(side: BorderSide(color: context.theme.dividerColor)),
color: context.theme.cardColor,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widget.contentList.spaced(Spacing.smallSpacing),
),
),
),
);
},
);
},
arrowHeight: 0,
arrowWidth: 0,
allowClicksOnBackground: true,
direction: PopoverDirection.bottom,
transition: PopoverTransition.other,
// contentDxOffset: -width + (context.size?.width ?? 0),
barrierDismissible: true,
barrierColor: Colors.transparent,
backgroundColor: Colors.transparent,
transitionDuration: const Duration(milliseconds: 300),
popoverTransitionBuilder: (animation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: animation.drive(
Tween<Offset>(
begin: const Offset(0, -0.02),
end: Offset.zero,
),
),
child: child,
),
);
},
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle popover lifecycle when dismissed externally

When the popover is closed via the barrier/back navigation, popupContext stays set. The next closePopUp (from the delayed timer or the guard on tap) runs against a deactivated context, throwing FlutterError: Looking up a deactivated widget's ancestor… and preventing the popover from opening again. Reset the reference once the route completes and guard against unmounted contexts before calling Navigator.pop().

   void closePopUp({bool forceClose = false}) {
-    if (popupContext == null) return;
+    final localPopupContext = popupContext;
+    if (localPopupContext == null) return;
+
+    if (localPopupContext is Element && !localPopupContext.mounted) {
+      popupContext = null;
+      return;
+    }
 
-    if ((parentHover || childHover) && !forceClose) return;
+    if ((parentHover || childHover) && !forceClose) return;
 
-    Navigator.of(popupContext!).pop();
+    Navigator.of(localPopupContext).pop();
 
     popupContext = null;
   }
@@
-                        showPopover(
+                        showPopover(
                           context: context,
                           bodyBuilder: (context) {
                             popupContext = context;
@@
-                        );
+                        ).whenComplete(() {
+                          popupContext = null;
+                        });
                       }
                     : null,
               ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void closePopUp({bool forceClose = false}) {
if (popupContext == null) return;
if ((parentHover || childHover) && !forceClose) return;
Navigator.of(popupContext!).pop();
popupContext = null;
}
@override
Widget build(BuildContext context) {
return HoverBuilder(
cursor: SystemMouseCursors.basic,
builder: (context, isHovering) {
parentHover = isHovering;
Future.delayed(const Duration(milliseconds: 300), closePopUp);
if (widget.contentList.isEmpty) {
closePopUp(forceClose: true);
return const SizedBox.shrink();
}
return Row(
children: [
Expanded(
child: widget.contentList[0],
),
if (widget.contentList.length > 1) ...[
Spacing.xsHorizontal(),
GestureDetector(
// ignore: sort_child_properties_last
child: Chip(
label: Text('+${widget.contentList.length - 1}'),
),
onTap: widget.contentList.length > 1
? () {
if (popupContext != null) {
closePopUp(forceClose: true);
return;
}
showPopover(
context: context,
bodyBuilder: (context) {
popupContext = context;
return HoverBuilder(
cursor: SystemMouseCursors.basic,
builder: (context, isHovering) {
childHover = isHovering;
Future.delayed(const Duration(milliseconds: 300), closePopUp);
return Card(
elevation: 16,
shape: squircle(side: BorderSide(color: context.theme.dividerColor)),
color: context.theme.cardColor,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widget.contentList.spaced(Spacing.smallSpacing),
),
),
),
);
},
);
},
arrowHeight: 0,
arrowWidth: 0,
allowClicksOnBackground: true,
direction: PopoverDirection.bottom,
transition: PopoverTransition.other,
// contentDxOffset: -width + (context.size?.width ?? 0),
barrierDismissible: true,
barrierColor: Colors.transparent,
backgroundColor: Colors.transparent,
transitionDuration: const Duration(milliseconds: 300),
popoverTransitionBuilder: (animation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: animation.drive(
Tween<Offset>(
begin: const Offset(0, -0.02),
end: Offset.zero,
),
),
child: child,
),
);
},
);
// lib/src/slots/presentation/widgets/slot_data_pop_over.dart
void closePopUp({bool forceClose = false}) {
final localPopupContext = popupContext;
if (localPopupContext == null) return;
// If the route was dismissed externally, the context is unmounted – clear it.
if (localPopupContext is Element && !localPopupContext.mounted) {
popupContext = null;
return;
}
if ((parentHover || childHover) && !forceClose) return;
Navigator.of(localPopupContext).pop();
popupContext = null;
}
@override
Widget build(BuildContext context) {
return HoverBuilder(
cursor: SystemMouseCursors.basic,
builder: (context, isHovering) {
parentHover = isHovering;
Future.delayed(const Duration(milliseconds: 300), closePopUp);
if (widget.contentList.isEmpty) {
closePopUp(forceClose: true);
return const SizedBox.shrink();
}
return Row(
children: [
Expanded(
child: widget.contentList[0],
),
if (widget.contentList.length > 1) ...[
Spacing.xsHorizontal(),
GestureDetector(
// ignore: sort_child_properties_last
child: Chip(
label: Text('+${widget.contentList.length - 1}'),
),
onTap: widget.contentList.length > 1
? () {
if (popupContext != null) {
closePopUp(forceClose: true);
return;
}
showPopover(
context: context,
bodyBuilder: (context) {
popupContext = context;
return HoverBuilder(
cursor: SystemMouseCursors.basic,
builder: (context, isHovering) {
childHover = isHovering;
Future.delayed(const Duration(milliseconds: 300), closePopUp);
return Card(
elevation: 16,
shape: squircle(side: BorderSide(color: context.theme.dividerColor)),
color: context.theme.cardColor,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widget.contentList.spaced(Spacing.smallSpacing),
),
),
),
);
},
);
},
).whenComplete(() {
// Ensure we clear the context when the popover route finishes.
popupContext = null;
});
}
: null,
),

Comment on lines +115 to +118
);
},
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Dispose must close any open popover

If this widget is removed from the tree while the popover is open, the overlay route keeps hanging around because nothing calls closePopUp. Cleaning it up in dispose avoids orphaned UI and stale references.

   }
 
+  @override
+  void dispose() {
+    closePopUp(forceClose: true);
+    super.dispose();
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
);
},
);
}
);
},
);
}
@override
void dispose() {
closePopUp(forceClose: true);
super.dispose();
}
}
🤖 Prompt for AI Agents
In lib/src/slots/presentation/widgets/slot_data_pop_over.dart around lines 115
to 118, the widget never closes the popover when removed which leaves the
overlay route orphaned; add an override of dispose that calls closePopUp()
(guarded if necessary, e.g. if the popover/controller is non-null or if mounted
checks are required) before calling super.dispose() so any open popover is
closed and resources cleaned up.

@MasterMarcoHD MasterMarcoHD merged commit 7db9c89 into main Oct 2, 2025
2 of 4 checks passed
@MasterMarcoHD MasterMarcoHD deleted the lpa-43RR branch October 2, 2025 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants