Skip to content

Develop#23

Open
yohann69 wants to merge 19 commits intomainfrom
develop
Open

Develop#23
yohann69 wants to merge 19 commits intomainfrom
develop

Conversation

@yohann69
Copy link
Member

@yohann69 yohann69 commented Jan 13, 2026

cf pr front

  • amélioration du dashboard admin avec la gestion des éléments de resa
    ajout de notifs pour les events

Summary by CodeRabbit

  • New Features

    • Full admin UI: login, dashboard, users, events, clubs, reservations, menus, reviews, games (scores & history), command palette, data tables, modals, charts, sidebar.
    • Event reminders: scheduled, localized push notifications.
    • Reservations: hierarchical tree with per-item custom warning/confirmation messages.
    • Menu: allergen support and dish details.
    • Bassine: score editing and history views.
  • Configuration

    • Admin scaffold: env example (NEXT_PUBLIC_API_URL), README, TypeScript, linting/formatting, Tailwind/PostCSS, package manifest.
  • Database

    • Migrations: notification tokens, reservation messages, event notifications, allergens, sequence fixes.

✏️ Tip: You can customize this high-level summary in your review settings.

luclu7 and others added 13 commits October 2, 2025 11:34
* Interface web admin

* ajout .env.example

* Meilleure gestion des rôles, ajout bouton vérifier un utilisateur, fix quelques comportements relous

* mobile "friendly"

* fix users verified

* refacto et ajout recherche

* Ajout menu RU

* Menu mieux

* fix viewport

* ajout bassine

* Biome fix

* oups

* rabbit
Introduces a modal in the admin UI for managing club responsibles, allowing admins to add or remove responsibles for a club. Adds supporting React hooks and API methods, updates backend with a new handler and route for removing responsibles, and updates dependencies and dev server port.
Introduces support for custom warning and confirmation messages on reservation items. Adds database columns, admin UI modal for editing messages, API endpoints for fetching and updating messages, and updates backend models and queries to handle these fields. Enables club owners and admins to manage reservation dialog messages per item.
Introduces a full reservation management interface in the admin panel, including a reservation tree page, modals for category and item creation/editing, and message customization. Adds React components, API types, React hooks, and Go backend handlers for CRUD operations on reservation categories and items. Updates the sidebar to include a link to the new reservations section and extends the API and routes to support the new features.
Moved Club, Event, and User type imports from './api' to './types' in relevant files for better type organization and separation. This change improves code clarity and maintains a cleaner type structure.
Replaces Plus icons with FolderPlus and PackagePlus for category and item actions to improve clarity. Adds display of item descriptions in the reservation list and updates folder toggling for club items to use icons instead of a spacer.
Improves the construction of SQL INSERT queries in CreateCategory and CreateItem by dynamically building column lists and parameter placeholders. This change ensures correct parameter numbering and simplifies handling of optional fields, reducing potential errors and improving maintainability.
Introduces a migration that resets the sequences for reservation_category and reservation_element tables to prevent duplicate key errors by aligning them with the current maximum IDs.
Introduces push notifications for new events and event reminders, with localized messages based on user language. Adds a migration for tracking sent notifications to prevent duplicates, updates the event handler to send notifications on event creation, and implements an event scheduler to send reminders 1 hour before events. Updates i18n files for notification translations and extends the notification service with new user/token retrieval methods.
@yohann69 yohann69 requested review from Copilot and luclu7 January 13, 2026 18:04
@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a complete admin app (Next.js frontend, components, hooks, Zustand stores), a centralized Axios client with React Query hooks, backend admin handlers/routes, notification service and scheduler, DB migrations (notifications, reservations, allergens), and project configuration files.

Changes

Cohort / File(s) Summary
Admin project scaffold & config
admin/.env.example, admin/.gitignore, admin/README.md, admin/package.json, admin/next.config.ts, admin/tsconfig.json, admin/biome.json, admin/postcss.config.mjs, admin/postcss.config.mjs
New admin app metadata, lint/format configs, scripts, .gitignore, and .env.example with NEXT_PUBLIC_API_URL.
App layout & styling
admin/app/layout.tsx, admin/app/globals.css, admin/app/page.tsx
Root layout, fonts, QueryProvider, Toaster, ReactQueryDevtools, global CSS and redirect home page.
Pages (client)
admin/app/*/page.tsx (e.g. dashboard, users, events, clubs, games, menu, reservations, login)
New client pages implementing dashboards and resource management (create/edit/delete flows, filters, skeletons).
Core shell & UX components
admin/components/Layout.tsx, admin/components/Sidebar.tsx, admin/components/CommandPalette.tsx, admin/components/ErrorBoundary.tsx, admin/components/QueryProvider.tsx, admin/components/LazyComponents.tsx
App shell, sidebar, command palette, error boundary, query provider, lazy-loading wrappers.
Reusable UI primitives
admin/components/DataTable.tsx, admin/components/Combobox.tsx, admin/components/TagAutocomplete.tsx, admin/components/Modal.tsx, admin/components/LoadingSpinner.tsx, admin/components/Combobox.tsx
Generic data table, combobox, tag-autocomplete, modal and loading components.
Domain components & modals
admin/components/*Modal.tsx, admin/components/* (Bassine, MenuManager, ReviewsManager, Reservation* , MenuManager, etc.)
Domain-specific managers and modals for users, events, clubs, reservations, menu, reviews, and game scores (create/edit/history flows).
Client API, hooks & types
admin/lib/api.ts, admin/lib/hooks.ts, admin/lib/hooks/*, admin/lib/performance.ts, admin/lib/schemas.ts, admin/lib/types.ts, admin/lib/utils.ts, admin/lib/router.tsx, admin/lib/stores/*
Axios client configured from NEXT_PUBLIC_API_URL, React Query hooks, utility hooks (debounce, click-outside, keyboard), performance helpers, Zod schemas, TS types, router helpers, and Zustand stores (app, auth).
Small helpers & charts
admin/components/LanguageFlag.tsx, admin/components/UserGrowthChart.tsx, admin/components/Combobox.tsx
Language flag, user growth chart and other small UI helpers.
Backend: admin handlers & routes
handlers/admin/admin_handlers.go, routes/admin.go
New AdminHandler with wide admin CRUD surface and route registration under /admin.
Backend: auth/club/event/reservation updates
handlers/auth/auth_handlers.go, handlers/club/club_handler.go, handlers/event/event_handler.go, handlers/reservation/repository/reservation_repository.go
GraduationYear nullability, multi-responsible clubs and removal endpoint, EventHandler now accepts NotificationService and triggers localized push notifications, reservation repo supports custom messages and dynamic inserts.
Notification service & schedulers
services/notification.go, scheduler/event_scheduler.go, scheduler/scheduler.go, main.go
NotificationService reads tokens from new table, new EventScheduler sends localized reminders and integrated into scheduler lifecycle; main and scheduler wiring updated.
Middleware & routing updates
middlewares/adminAuth.go, routes/statistics.go, routes/club.go, main.go
New AdminAuthMiddleware enforcing ADMIN role, admin statistics route, club responsible removal route, and admin routes wiring.
DB migrations & model changes
db/migrations/00019-00025_*, models/*.go
Migrations add user_notification_tokens, reservation message columns, event notification tracking, allergens (seed/markers), sequence fix; models updated for notification tokens, reservation messages, allergens, nullable graduation year, and admin bassine types.
Restaurant allergen & menu pipeline
handlers/restaurant/repository/menu_repository.go, handlers/restaurant/service/menu_service.go, handlers/restaurant/scheduler.go, models/restaurant.go
Preserve allergen codes from raw fetch, persist/link allergens, include allergens in dish details, and adapt service to raw-item processing.

Sequence Diagram(s)

sequenceDiagram
    participant AdminUI as Admin UI
    participant API as Backend Admin API
    participant DB as Database
    participant Notif as Notification Service

    AdminUI->>API: POST /admin/events (create event)
    API->>DB: INSERT event
    DB-->>API: returns event id
    API->>API: spawn goroutine to process notifications
    API->>DB: query users + tokens grouped by language
    DB-->>API: users with tokens
    API->>Notif: send localized push per language group
    Notif-->>API: send results
    API->>DB: INSERT event_notification_sent (mark sent)
Loading
sequenceDiagram
    participant AdminUI as Admin UI
    participant API as Backend Admin API
    participant DB as Database

    AdminUI->>API: GET /admin/reservation/tree
    API->>DB: fetch categories and items (include messages)
    DB-->>API: nested reservation tree
    API-->>AdminUI: reservation tree
    AdminUI->>API: PATCH /admin/reservation-items/:id (update messages)
    API->>DB: UPDATE reservation_element (warning_message, confirmation_message)
    DB-->>API: update result
    API-->>AdminUI: success
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Interface web admin #20: Overlaps admin frontend scaffold and config additions (includes admin/.env.example with NEXT_PUBLIC_API_URL and many admin files).

Poem

🐰 I hopped through code and left a trail,

Panels and modals in a dainty veil.
Events that ring, tokens that sing,
Schedules that chirp at the hour they bring.
A bunny’s cheer for this admin tale!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Develop' is vague and does not convey meaningful information about the changeset. It does not describe the main changes, which include admin dashboard improvements, reservation element management, event notifications, allergen/marker support, and database migrations. Use a more descriptive title that summarizes the primary changes, such as 'Add admin dashboard features, event notifications, and allergen/marker support' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
handlers/restaurant/service/menu_service.go (2)

136-143: Consider extracting the duplicated categoryToID map to a package-level constant.

The categoryToID map is duplicated between BuildFetchedItems (lines 136-143) and ConvertMenuDataToFetchedItems (lines 254-261). Extracting this to a package-level variable would reduce duplication and ensure consistency.

♻️ Suggested refactor
+// CategoryToMenuTypeID maps category names to restaurant IDs
+var CategoryToMenuTypeID = map[string]int{
+	"grilladesMidi": 1,
+	"migrateurs":    2,
+	"cibo":          3,
+	"accompMidi":    4,
+	"grilladesSoir": 5,
+	"accompSoir":    6,
+}
+
 func (s *MenuService) BuildFetchedItems(items []models.MenuItemAPI) []models.FetchedItem {
 	var fetchedItems []models.FetchedItem
-
-	categoryToID := map[string]int{
-		"grilladesMidi": 1,
-		"migrateurs":    2,
-		"cibo":          3,
-		"accompMidi":    4,
-		"grilladesSoir": 5,
-		"accompSoir":    6,
-	}

Then use CategoryToMenuTypeID in both functions.

Also applies to: 254-261


200-209: Redundant special-case check for weightWatcher.

The conditional on lines 203-207 is unnecessary. Since markerName is already "weightWatcher" (from the map key on line 195), the else branch on line 206 would produce the same result as line 204.

♻️ Suggested simplification
 		for markerName, markerValue := range markerMap {
 			if strings.EqualFold(markerValue, "TRUE") || strings.EqualFold(markerValue, "VRAI") {
-				// Special case: API uses "ww" but DB uses "weightWatcher"
-				if markerName == "weightWatcher" {
-					markerCodes = append(markerCodes, "weightWatcher")
-				} else {
-					markerCodes = append(markerCodes, markerName)
-				}
+				markerCodes = append(markerCodes, markerName)
 			}
 		}

The comment on line 195 already documents the API-to-DB mapping, so the special case in the loop body is redundant.

handlers/restaurant/repository/menu_repository.go (1)

501-581: Consider extracting duplicated allergen/marker sync logic into a helper function.

The allergen sync block (lines 502-540) and marker sync block (lines 543-581) are nearly identical, differing only in the is_marker boolean value and log message text. Extracting a helper function would reduce duplication and make future maintenance easier.

♻️ Suggested refactor
// syncAllergenOrMarker links an allergen or marker code to an article, creating it if needed.
func (r *MenuRepository) syncAllergenOrMarker(articleID int, code string, isMarker bool) {
	var id int
	entityType := "allergen"
	if isMarker {
		entityType = "marker"
	}

	err := r.DB.QueryRow(`
		SELECT id_allergen
		FROM allergens
		WHERE name = $1 AND is_marker = $2
	`, code, isMarker).Scan(&id)

	if err != nil {
		if err == sql.ErrNoRows {
			err = r.DB.QueryRow(`
				INSERT INTO allergens (name, is_marker)
				VALUES ($1, $2)
				RETURNING id_allergen
			`, code, isMarker).Scan(&id)
			if err != nil {
				utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to insert %s '%s': %v", entityType, code, err))
				return
			}
		} else {
			utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to lookup %s '%s': %v", entityType, code, err))
			return
		}
	}

	_, err = r.DB.Exec(`
		INSERT INTO restaurant_article_allergens (id_restaurant_articles, id_allergen)
		VALUES ($1, $2)
		ON CONFLICT (id_restaurant_articles, id_allergen) DO NOTHING
	`, articleID, id)

	if err != nil {
		utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to link %s '%s' to article %d: %v", entityType, code, articleID, err))
	}
}

Then replace the inline blocks:

-		// Sync allergens for this article based on fetched allergen codes
-		if len(item.AllergenCodes) > 0 {
-			for _, code := range item.AllergenCodes {
-				// ... existing logic ...
-			}
-		}
-
-		// Sync markers for this article based on fetched marker codes
-		if len(item.MarkerCodes) > 0 {
-			for _, code := range item.MarkerCodes {
-				// ... existing logic ...
-			}
-		}
+		// Sync allergens for this article
+		for _, code := range item.AllergenCodes {
+			r.syncAllergenOrMarker(articleID, code, false)
+		}
+
+		// Sync markers for this article
+		for _, code := range item.MarkerCodes {
+			r.syncAllergenOrMarker(articleID, code, true)
+		}

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9baa3b and 7ce2507.

📒 Files selected for processing (3)
  • handlers/restaurant/repository/menu_repository.go
  • handlers/restaurant/service/menu_service.go
  • models/restaurant.go
🧰 Additional context used
🧬 Code graph analysis (2)
handlers/restaurant/service/menu_service.go (1)
models/restaurant.go (3)
  • MenuItemAPI (46-75)
  • MenuData (87-94)
  • FetchedItem (78-84)
handlers/restaurant/repository/menu_repository.go (2)
utils/logManager.go (2)
  • LogMessage (62-64)
  • LevelWarn (17-17)
models/restaurant.go (3)
  • Allergen (35-42)
  • DishDetailsResponse (160-167)
  • RestaurantArticle (12-17)
🔇 Additional comments (9)
models/restaurant.go (4)

34-42: LGTM!

The Allergen struct is well-defined with appropriate JSON and DB tags. The IsMarker field provides a clean way to differentiate allergens from markers within the same table.


53-74: LGTM!

The extension of MenuItemAPI with allergen fields (Allergene1-10) and marker boolean fields aligns with the external API structure. Good documentation comments for the expected values.


78-84: LGTM!

The FetchedItem struct is properly extended with AllergenCodes and MarkerCodes slices to carry the processed codes for downstream synchronization.


158-167: LGTM!

The DishDetailsResponse struct correctly embeds RestaurantArticle and includes the additional fields for ratings, allergens, and reviews. This provides a clean, typed response structure for the dish details endpoint.

handlers/restaurant/service/menu_service.go (3)

31-71: LGTM!

The FetchRawMenuItems() function properly handles HTTP errors, response body cleanup, and JSON parsing. Good error messages with context.


73-80: LGTM!

Clean refactoring to preserve backward compatibility by wrapping the new FetchRawMenuItems() for existing callers.


162-182: LGTM!

Good allergen code processing with proper trimming, placeholder filtering ("vide", "undefined"), and deduplication.

handlers/restaurant/repository/menu_repository.go (2)

196-209: LGTM!

Good graceful degradation - returning partial dish details with allergens when reviews fetch fails, rather than failing the entire request.


231-244: LGTM!

The response construction correctly uses the typed models.DishDetailsResponse struct with allergens and reviews.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a comprehensive admin panel for the Transat backend application along with several backend improvements. The changes include a complete Next.js-based admin dashboard, event notification system, multi-device notification token support, and enhanced reservation management.

Changes:

  • Migrated notification token storage to support multiple devices per user via user_notification_tokens table
  • Added automated event reminder notifications (1 hour before events) via a new scheduler
  • Created a full-featured admin panel with user, event, club, menu, and reservation management
  • Enhanced club responsible management to support multiple owners per club
  • Added custom warning/confirmation messages for reservation items

Reviewed changes

Copilot reviewed 87 out of 91 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
services/notification.go Updated to use new user_notification_tokens table, added methods for event/club interest queries
scheduler/event_scheduler.go New scheduler for sending event reminder notifications 1h before start
scheduler/scheduler.go Integrated EventScheduler into main scheduler
handlers/user/user_handlers.go Changed notification token storage to new table, updated GraduationYear to pointer
handlers/club/club_handler.go Enhanced to support multiple club responsibles, added RemoveClubRespo endpoint
handlers/admin/admin_handlers.go New comprehensive admin handlers for user, event, club, menu, reservation, and bassine management
models/*.go Updated models for nullable GraduationYear, reservation messages, notification tokens
middlewares/adminAuth.go New middleware for admin authentication checks
routes/*.go Added admin routes and new club/statistics endpoints
main.go Updated CORS config for admin panel, integrated schedulers
db/migrations/*.sql New migrations for user_notification_tokens, reservation messages, event notifications
admin/* Complete Next.js admin panel with components, hooks, stores, and pages

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +117 to +120
// Build query dynamically with correct parameter numbering
columns := []string{"name", "id_clubs"}
placeholders := []string{"$1", "$2"}
args := []interface{}{category.Name, *category.IDClubParent}
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

Potential nil pointer dereference when dereferencing category.IDClubParent. The field is a pointer and should be checked for nil before dereferencing. If it's guaranteed to be non-nil at this point, add validation earlier in the function.

Copilot uses AI. Check for mistakes.
// Build query dynamically with correct parameter numbering
columns := []string{"name", "slot", "id_clubs"}
placeholders := []string{"$1", "$2", "$3"}
args := []interface{}{item.Name, item.Slot, *item.IDClubParent}
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

Potential nil pointer dereference when dereferencing item.IDClubParent. Similar to the category creation, this should be validated before use or the pointer should be checked for nil.

Copilot uses AI. Check for mistakes.
AllowMethods: "GET, POST, PUT, DELETE, OPTIONS",
AllowHeaders: "Origin, Content-Type, Accept, Authorization, X-Requested-With",
AllowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
AllowOrigins: "http://localhost:3001", // pour le pannel admin en dev
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

Corrected spelling of 'pannel' to 'panel' in the comment.

Copilot uses AI. Check for mistakes.
Comment on lines +210 to +212
// Create table if it doesn't exist (should be done via migration, but this is a safety check)
createTableQuery := `
CREATE TABLE IF NOT EXISTS event_notification_sent (
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

Creating tables in application code is not recommended. While the comment acknowledges this should be done via migration, the CREATE TABLE IF NOT EXISTS logic should be removed and rely solely on migrations. This could cause issues in production environments where the application user may not have CREATE TABLE permissions.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +16
const findItemInTree = (
tree: any[],
itemId: number,
): { warning_message?: string; confirmation_message?: string } | null => {
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

Using any type defeats the purpose of TypeScript's type safety. Define proper types for the tree structure or use the existing ReservationTreeItem type from the API module.

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +67
} catch (error: any) {
alert(error?.response?.data?.error || "Erreur lors de l'enregistrement");
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

Using alert() for error messages is not ideal for user experience. Consider using the toast notification system that's already being used elsewhere in the application (react-hot-toast).

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +76
} catch (error: any) {
alert(error?.response?.data?.error || "Erreur lors de l'enregistrement");
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

Using alert() for error messages is not ideal. Consider using the toast notification system that's already being used elsewhere in the application (react-hot-toast).

Copilot uses AI. Check for mistakes.
const deleteItem = useDeleteReservationItem();

const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
const [selectedCategory, setSelectedCategory] = useState<ReservationTreeItem | null>(null);
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

Unused variable selectedCategory.

Suggested change
const [selectedCategory, setSelectedCategory] = useState<ReservationTreeItem | null>(null);

Copilot uses AI. Check for mistakes.
const deleteItem = useDeleteReservationItem();

const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
const [selectedCategory, setSelectedCategory] = useState<ReservationTreeItem | null>(null);
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

Unused variable setSelectedCategory.

Suggested change
const [selectedCategory, setSelectedCategory] = useState<ReservationTreeItem | null>(null);
const [selectedCategory] = useState<ReservationTreeItem | null>(null);

Copilot uses AI. Check for mistakes.
Copy link

@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: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
handlers/club/club_handler.go (1)

754-757: Remove debug logging of raw request body.

Logging raw request bodies can expose sensitive data (e.g., user emails) in production logs, which may raise privacy/compliance concerns. This appears to be debug code that should be removed before merging.

Suggested fix
-	// Debug: Log the raw body content
-	bodyBytes := c.Body()
-	utils.LogLineKeyValue(utils.LevelInfo, "Raw Body", string(bodyBytes))
-	utils.LogLineKeyValue(utils.LevelInfo, "Content-Type", c.Get("Content-Type"))
-
 	var req models.AddRespoRequest
 	if err := c.BodyParser(&req); err != nil {
 		utils.LogMessage(utils.LevelError, "Failed to parse request body")
 		utils.LogLineKeyValue(utils.LevelError, "Error", err)
-		utils.LogLineKeyValue(utils.LevelError, "Raw Body", string(bodyBytes))
-		utils.LogLineKeyValue(utils.LevelError, "Content-Type", c.Get("Content-Type"))
 		utils.LogFooter()
handlers/auth/auth_handlers.go (2)

148-154: Transaction handling after duplicate user has potential issues.

After rolling back the transaction on duplicate key error, a new transaction is started but the error from h.DB.Begin() at line 151 is only logged via fmt.Println (inconsistent with other logging) and execution continues regardless. If this transaction start fails, subsequent operations using tx will fail unexpectedly.

Consider either returning early on duplicate user or properly handling the transaction error.

Suggested improvement
 			commitOrRollback(tx, err) // Rollback
-			tx, err = h.DB.Begin()
-			if err != nil {
-				fmt.Println("Failed to begin database transaction")
-			}
-
+			// User already exists, we'll just update their verification code
+			tx, err = h.DB.Begin()
+			if err != nil {
+				utils.LogMessage(utils.LevelError, "Failed to begin new transaction after duplicate user")
+				utils.LogLineKeyValue(utils.LevelError, "Error", err)
+				utils.LogFooter()
+				return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Database transaction error"})
+			}
 			alreadyExists = true

179-184: Same transaction error handling issue.

This is the same pattern as lines 148-154 - if the new transaction fails to start, execution continues with an invalid tx handle.

services/notification.go (3)

70-122: Prepared statement not closed on all error paths.

The statement stmt created at line 79 is only closed at line 117-121, but if stmt.Query() fails (line 86-91), the statement is not closed before returning. This causes a resource leak.

Proposed fix using defer
 stmt, err := ns.db.Prepare(query)
 if err != nil {
   log.Println("║ 💥 Failed to prepare statement: ", err)
   log.Println("╚=========================================╝")
   return nil, err
 }
+defer stmt.Close()

 rows, err := stmt.Query(pq.Array(groups))
 if err != nil {
   log.Println("║ 💥 Failed to query groups: ", err)
   log.Println("╚=========================================╝")
   return nil, err
 }
-
-defer func(rows *sql.Rows) {
-  err := rows.Close()
-  if err != nil {
-    log.Println("║ 💥 Failed to close rows: ", err)
-    log.Println("╚=========================================╝")
-  }
-}(rows)
+defer rows.Close()
 
 // ... rest of code ...

-if err := stmt.Close(); err != nil {
-  log.Println("║ 💥 Failed to close statement: ", err)
-  log.Println("╚=========================================╝")
-  return nil, err
-}

39-67: Missing rows.Err() check after iteration in the first query block.

The email-based query (lines 39-68) iterates over rows but does not check rows.Err() after the loop. This could miss errors that occurred during iteration.

Proposed fix
       targets = append(targets, target)
     }
+    if err := rows.Err(); err != nil {
+      log.Println("║ 💥 Failed to iterate rows: ", err)
+      log.Println("╚=========================================╝")
+      return nil, err
+    }
   }

283-291: HTTP client lacks timeout configuration.

Creating &http.Client{} without a timeout means requests to the Expo API could hang indefinitely if the server is unresponsive, potentially exhausting resources.

Proposed fix
-client := &http.Client{}
+client := &http.Client{
+  Timeout: 30 * time.Second,
+}

You'll also need to add "time" to the imports.

🤖 Fix all issues with AI agents
In @admin/app/login/page.tsx:
- Around line 9-18: The useSearchParams() call inside LoginPage requires a React
Suspense boundary in Next.js 15; either wrap the component that calls
useSearchParams in a <Suspense> or move the search-param logic into a small
client subcomponent. Fix by extracting the search param read into a new client
component (e.g., SearchParamsReader) that uses useSearchParams to read the
"verified" param and calls a prop callback (e.g., onVerified) or returns the
verified UI, then render <Suspense fallback={null}><SearchParamsReader
onVerified={setShowVerifiedMessage} /></Suspense> from LoginPage (or
alternatively wrap the parent that renders LoginPage in <Suspense>), ensuring
you remove direct useSearchParams() from LoginPage and keep state setters like
setShowVerifiedMessage intact.

In @admin/components/Combobox.tsx:
- Around line 37-38: The generated buttonId using Math.random in Combobox.tsx is
unstable across renders; replace that line to produce a stable id (e.g., use
React's useId() or create a stable ref/memo) so the htmlFor/id association and
SSR hydration remain consistent; update the variable named buttonId (and any
places referencing it) to derive its value from useId() (or useRef/useMemo)
inside the Combobox component and remove the Math.random usage.

In @admin/components/ReservationItemsModal.tsx:
- Around line 41-52: The handleSave function currently calls
updateMessages.mutateAsync without error handling and always resets local state;
wrap the mutateAsync call in a try/catch and use toast from react-hot-toast to
notify the user: call toast.success(...) on successful mutation and only then
call setEditingItem(null), setWarningMessage(""), and
setConfirmationMessage(""); in the catch block call toast.error(...) with the
error message so failures are surfaced; reference handleSave and
updateMessages.mutateAsync when implementing the try/catch and toast calls.

In @admin/lib/performance.ts:
- Around line 74-99: The hook lacks unmount cleanup and treats options as
unstable; fix by stabilizing the callback with a ref (create callbackRef in
useIntersectionObserver, set callbackRef.current = callback in an effect) and
use callbackRef.current when constructing the IntersectionObserver inside
observe, add a useEffect that returns a cleanup which disconnects
observer.current on unmount (and when options change), and address the unstable
options by deriving a stable dependency (e.g., memoize or serialize options with
useMemo/JSON.stringify and use that stable value in your observe/effect
dependencies) so observe/disconnect no longer recreate the observer every
render.
- Around line 55-71: The function useStableCallback mutates refs during render
which violates React purity; change the ref-update logic to run inside a
useEffect (add useEffect to imports) so depsRef.current and callbackRef.current
are updated after render, and in that effect compare both array lengths and
elements (e.g., check length mismatch or any dep !== depsRef.current[i]) to
detect changes including shorter arrays before assigning the new dependencies
and callback; keep returning the stable useCallback wrapper (empty deps) so the
callback identity remains stable while avoiding render-side effects.

In @admin/lib/router.tsx:
- Around line 7-12: Router.tsx duplicates the lazy-loaded page components
already defined in admin/components/LazyComponents.tsx (DashboardPage,
UsersPage, EventsPage, ClubsPage, LoginPage); consolidate by moving the lazy
import + loading/error wrapper into a single module (e.g., keep
LazyComponents.tsx as the source of truth) and have admin/lib/router.tsx
re-export or import those named components instead of redefining them so
maintenance and behavior stay consistent.

In @admin/next.config.ts:
- Around line 7-18: The images.remotePatterns configuration is too permissive
(uses hostname: "**") which allows Next.js to proxy images from any host;
replace the wildcard entries in images.remotePatterns with an explicit allowlist
of trusted hostnames (e.g., add objects with protocol "https"/"http" and
hostname set to each trusted domain) and remove the wildcard hostname entries so
that only those trusted domains are permitted by the images configuration;
update the images object in next.config.ts (the remotePatterns array) to contain
only those specific hostnames and protocols, or alternatively implement
server-side URL validation before passing URLs to Next.js Image if dynamic
sources are required.

In @handlers/reservation/repository/reservation_repository.go:
- Around line 160-164: The code dereferences *item.IDClubParent when building
args (columns/placeholders/args/paramNum) which can panic; in the CreateItem
function add an explicit nil check for item.IDClubParent before using
*item.IDClubParent and handle it (e.g., return a descriptive error from
CreateItem such as "missing IDClubParent" or validate earlier) rather than
dereferencing; update any logic that builds columns/placeholders/args (variables
columns, placeholders, args, paramNum) to only use *item.IDClubParent after the
nil check so the function cannot panic.
- Around line 117-128: The code dereferences *category.IDClubParent when
building args (columns/placeholders/args/paramNum) which can panic if
IDClubParent is nil; update the logic in the block that initializes
columns/placeholders/args so you first check if category.IDClubParent != nil
before appending and using *category.IDClubParent, and if that field is required
return a validation error (or if optional, append a nil value or omit the column
entirely) so that functions using columns/placeholders/args (and variables
paramNum, category.Name) never perform a direct dereference of a potentially nil
pointer.

In @scheduler/event_scheduler.go:
- Around line 208-224: Remove the defensive CREATE TABLE logic from
markNotificationSent on EventScheduler: the inline createTableQuery and the
es.db.Exec call that attempts to create event_notification_sent should be
deleted so schema is managed only via migrations (migration
00022_event_notification_sent.sql); ensure markNotificationSent only performs
the insert/update logic for recording a sent notification and does not attempt
any DDL, and remove or adjust any related error logging in that function that
referenced the table creation attempt.
- Around line 58-64: In EventScheduler.checkAndSendEventNotifications replace
time.Now() with time.Now().UTC() when computing
oneHourFromNow/windowStart/windowEnd (variables oneHourFromNow, windowStart,
windowEnd) so Go-side times are in UTC, and update the DB side to use UTC-aware
comparisons (either ensure events.start_date is stored in UTC or migrate
start_date to TIMESTAMP WITH TIME ZONE and use timezone-aware NOW() in queries)
to avoid server vs DB timezone mismatches.
🟡 Minor comments (13)
handlers/statistics/statistics_handlers.go-87-117 (1)

87-117: Errors are logged but not surfaced—partial data with zero values may mislead consumers.

When individual queries fail, the code logs the error but continues, returning zero values for failed metrics. This could be intentional (best-effort dashboard), but the caller cannot distinguish between "zero users" and "query failed."

Consider either:

  1. Return an error response if any critical query fails, or
  2. Include per-field error indicators in the response, or
  3. Document this best-effort behavior for consumers.
handlers/statistics/statistics_handlers.go-134-145 (1)

134-145: Missing rows.Err() check after iteration.

After iterating through rows.Next(), you should check rows.Err() to catch any errors that occurred during iteration (e.g., network issues mid-stream).

🔧 Proposed fix
       if err := rows.Scan(&date, &count, &cumulativeCount); err == nil {
         stats.UserGrowth = append(stats.UserGrowth, map[string]interface{}{
           "date":            date,
           "count":           count,
           "cumulativeCount": cumulativeCount,
         })
       }
     }
+    if err := rows.Err(); err != nil {
+      utils.LogMessage(utils.LevelError, "Error during user growth rows iteration")
+      utils.LogLineKeyValue(utils.LevelError, "Error", err)
+    }
   }
admin/lib/schemas.ts-21-38 (1)

21-38: Graduation year validation mismatch with backend.

The frontend schema allows currentYear + 10, but the backend (handlers/user/user_handlers.go line 187) validates against currentYear + 5. This inconsistency will cause valid frontend submissions to fail on the backend.

🔧 Align with backend validation
     .refine(
       (val) => {
         if (val === undefined) return true;
         const currentYear = new Date().getFullYear();
-        return val >= 1900 && val <= currentYear + 10;
+        return val >= 1900 && val <= currentYear + 5;
       },
       {
         message: "Année de diplôme invalide",
       },
     ),
admin/app/clubs/page.tsx-220-231 (1)

220-231: Potential issue: ClubOwnersModal receives nullable clubId.

selectedClubId can be null, but ClubOwnersModal is always rendered (not conditionally like ReservationItemsModal on line 232). If the modal component doesn't handle null gracefully, this could cause issues.

Consider applying the same conditional rendering pattern:

Suggested fix
-      <ClubOwnersModal
-        isOpen={ownersModalOpen}
-        onClose={() => setOwnersModalOpen(false)}
-        clubId={selectedClubId}
-        clubName={selectedClubName}
-      />
+      {selectedClubId && (
+        <ClubOwnersModal
+          isOpen={ownersModalOpen}
+          onClose={() => setOwnersModalOpen(false)}
+          clubId={selectedClubId}
+          clubName={selectedClubName}
+        />
+      )}
admin/components/ErrorBoundary.tsx-20-30 (1)

20-30: retryCount persists after successful recovery, which may cause unexpected page reloads.

The retryCount instance property is never reset when children render successfully after a retry. If an error occurs, the user retries successfully, and then a different error occurs later, retryCount retains its previous value. This could cause an unexpected page reload if the user has already used retries on previous errors.

🔧 Suggested fix - reset retryCount on successful render
   render() {
     if (this.state.hasError) {
       return this.props.fallback || this.getErrorUI();
     }

+    // Reset retry count on successful render
+    if (this.retryCount > 0) {
+      this.retryCount = 0;
+    }
+
     return this.props.children;
   }
admin/app/reservations/page.tsx-152-153 (1)

152-153: Non-null assertion on node.id may cause runtime issues.

If node.id is ever undefined, the non-null assertion (!) will pass undefined to handleDeleteCategory, which expects a number.

🔧 Add a guard or handle undefined case
                 <button
-                    onClick={() => handleDeleteCategory(node.id!)}
+                    onClick={() => node.id && handleDeleteCategory(node.id)}
                     className="p-1 text-red-600 hover:bg-red-50 rounded"
                     title="Supprimer"
                   >
admin/components/EventModal.tsx-98-102 (1)

98-102: Handle empty id_club to prevent NaN.

If formData.id_club is an empty string, parseInt returns NaN, which could cause backend issues. Consider adding validation or a fallback.

🐛 Suggested fix
     try {
+      if (!formData.id_club) {
+        setError("Veuillez sélectionner un club");
+        return;
+      }
+
       const eventData = {
         ...formData,
         id_club: parseInt(formData.id_club, 10),
       };
admin/components/CategoryModal.tsx-52-68 (1)

52-68: Non-null assertion on category.id may cause runtime error.

When category exists but id is undefined (which is possible per the ReservationTreeItem type where id?: number), the non-null assertion on line 55 will pass undefined to the API, potentially causing unexpected behavior.

🛡️ Suggested defensive check
     try {
       if (category) {
+        if (category.id === undefined) {
+          alert("Impossible de modifier une catégorie sans ID");
+          return;
+        }
         await updateCategory.mutateAsync({
-          id: category.id!,
+          id: category.id,
           data: { name: name.trim() },
         });
       } else {
admin/lib/stores/appStore.ts-94-99 (1)

94-99: Potential race condition when rapidly reopening modals.

The 150ms timeout to clear editingUser is not cancelled if the modal is reopened before the timeout fires. This could result in the editing state being cleared while the modal is open with a new user.

🔧 Consider storing timeout IDs to cancel on reopen

One approach is to store timeout IDs and clear them in the open actions:

+let userModalTimeout: ReturnType<typeof setTimeout> | null = null;

 openUserModal: (user) => {
+  if (userModalTimeout) {
+    clearTimeout(userModalTimeout);
+    userModalTimeout = null;
+  }
   set({
     userModalOpen: true,
     editingUser: user || null,
   });
 },

 closeUserModal: () => {
   set({ userModalOpen: false });
-  setTimeout(() => {
+  userModalTimeout = setTimeout(() => {
     set({ editingUser: null });
+    userModalTimeout = null;
   }, 150);
 },

The same pattern would apply to club and event modals.

admin/components/UserModal.tsx-53-80 (1)

53-80: Form state not reset when modal reopens in create mode.

The useEffect depends only on [user]. If a user opens the modal in create mode, enters data, closes without saving, then reopens to create another user, the form retains the previous values because user remains null and the effect doesn't re-run.

🔧 Proposed fix
   useEffect(() => {
     if (user) {
       setFormData({
         email: user.email,
         // ... existing user fields
       });
     } else {
       setFormData({
         email: "",
         // ... reset all fields
       });
     }
     setError("");
-  }, [user]);
+  }, [user, isOpen]);
scheduler/event_scheduler.go-37-40 (1)

37-40: Potential blocking send on stop channel.

If Stop() is called when the goroutine is not currently blocked on the select (e.g., during checkAndSendEventNotifications()), the send to stopChan will block until the next select iteration. Consider using a buffered channel or a non-blocking send pattern.

Proposed fix using buffered channel
 func NewEventScheduler(db *sql.DB, notificationService *services.NotificationService) *EventScheduler {
   return &EventScheduler{
     db:                 db,
     notificationService: notificationService,
-    stopChan:           make(chan bool),
+    stopChan:           make(chan bool, 1),
   }
 }
admin/components/LazyComponents.tsx-31-43 (1)

31-43: withPageLazyLoading lacks ErrorBoundary unlike router.tsx's version.

The withRouteLoading in router.tsx wraps components with both ErrorBoundary and Suspense, but withPageLazyLoading here only uses Suspense. This inconsistency means errors in lazy-loaded pages might not be caught gracefully.

Proposed fix to add ErrorBoundary
+import ErrorBoundary from "./ErrorBoundary";
+
 export function withPageLazyLoading<P extends object>(
   Component: React.LazyExoticComponent<React.ComponentType<P>>,
   loadingText?: string,
 ) {
   return function LazyPageWrapper(props: P) {
     return (
+      <ErrorBoundary level="page">
         <Suspense fallback={<FullPageLoading text={loadingText || "Chargement de la page..."} />}>
           <Component {...props} />
         </Suspense>
+      </ErrorBoundary>
     );
   };
 }
admin/lib/hooks.ts-281-290 (1)

281-290: Missing cache invalidation for user-specific history after score update.

When useUpdateBassineScore succeeds, it invalidates ["bassine-scores"] but not ["bassine-history", userEmail]. If a user is viewing history while a score update occurs, the history view will show stale data until manually refreshed.

Consider invalidating the affected user's history query as well.

🔧 Proposed fix
 export const useUpdateBassineScore = () => {
   const queryClient = useQueryClient();
 
   return useMutation({
     mutationFn: bassineApi.updateScore,
-    onSuccess: () => {
+    onSuccess: (_data, variables) => {
       queryClient.invalidateQueries({ queryKey: ["bassine-scores"] });
+      queryClient.invalidateQueries({ queryKey: ["bassine-history", variables.userEmail] });
     },
   });
 };

Comment on lines +9 to +18
export default function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [showVerifiedMessage, setShowVerifiedMessage] = useState(false);
const router = useRouter();
const searchParams = useSearchParams();
const emailId = useId();
const passwordId = useId();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wrap useSearchParams usage in a Suspense boundary.

In Next.js 15, useSearchParams() requires the component to be wrapped in a <Suspense> boundary. Without it, the page will throw an error during static rendering. Since this component reads the verified query parameter, consider either:

  1. Wrapping the component in <Suspense> in a parent, or
  2. Splitting the search params logic into a separate client component wrapped with Suspense.
Suggested fix: Create a wrapper with Suspense
+"use client";
+
+import { Suspense } from "react";
+
+function LoginPageContent() {
+  // ... existing component code
+}
+
 export default function LoginPage() {
-  const [email, setEmail] = useState("");
-  // ... rest of component
+  return (
+    <Suspense fallback={<div className="min-h-screen bg-gray-50 flex items-center justify-center">Loading...</div>}>
+      <LoginPageContent />
+    </Suspense>
+  );
 }
🤖 Prompt for AI Agents
In @admin/app/login/page.tsx around lines 9 - 18, The useSearchParams() call
inside LoginPage requires a React Suspense boundary in Next.js 15; either wrap
the component that calls useSearchParams in a <Suspense> or move the
search-param logic into a small client subcomponent. Fix by extracting the
search param read into a new client component (e.g., SearchParamsReader) that
uses useSearchParams to read the "verified" param and calls a prop callback
(e.g., onVerified) or returns the verified UI, then render <Suspense
fallback={null}><SearchParamsReader onVerified={setShowVerifiedMessage}
/></Suspense> from LoginPage (or alternatively wrap the parent that renders
LoginPage in <Suspense>), ensuring you remove direct useSearchParams() from
LoginPage and keep state setters like setShowVerifiedMessage intact.

Comment on lines +37 to +38
// Generate unique ID for accessibility
const buttonId = `combobox-${Math.random().toString(36).substr(2, 9)}`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unique ID generated on every render breaks accessibility.

Math.random() is called on every render, producing a new buttonId each time. This breaks the htmlFor/id association and can cause React hydration mismatches in SSR.

Use useId() (React 18+) or useMemo with a stable reference:

🐛 Recommended fix using useId
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useId, useRef, useState } from "react";
 
 // Inside the component:
-  const buttonId = `combobox-${Math.random().toString(36).substr(2, 9)}`;
+  const buttonId = useId();
📝 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
// Generate unique ID for accessibility
const buttonId = `combobox-${Math.random().toString(36).substr(2, 9)}`;
// Generate unique ID for accessibility
const buttonId = useId();
🤖 Prompt for AI Agents
In @admin/components/Combobox.tsx around lines 37 - 38, The generated buttonId
using Math.random in Combobox.tsx is unstable across renders; replace that line
to produce a stable id (e.g., use React's useId() or create a stable ref/memo)
so the htmlFor/id association and SSR hydration remain consistent; update the
variable named buttonId (and any places referencing it) to derive its value from
useId() (or useRef/useMemo) inside the Combobox component and remove the
Math.random usage.

Comment on lines +41 to +52
const handleSave = async (itemId: number) => {
await updateMessages.mutateAsync({
itemId,
messages: {
warning_message: warningMessage.trim() || null,
confirmation_message: confirmationMessage.trim() || null,
},
});
setEditingItem(null);
setWarningMessage("");
setConfirmationMessage("");
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing error handling in save operation.

The handleSave function uses mutateAsync without a try/catch block. If the mutation fails, the error is silently swallowed and the user receives no feedback about the failure. Additionally, the state reset on lines 49-51 won't execute on error.

🔧 Proposed fix
 const handleSave = async (itemId: number) => {
+  try {
     await updateMessages.mutateAsync({
       itemId,
       messages: {
         warning_message: warningMessage.trim() || null,
         confirmation_message: confirmationMessage.trim() || null,
       },
     });
     setEditingItem(null);
     setWarningMessage("");
     setConfirmationMessage("");
+  } catch (error) {
+    // Error is already handled by react-query, but optionally show inline feedback
+    console.error("Failed to save messages:", error);
+  }
 };

Consider using toast from react-hot-toast (as used in UserModal.tsx) to provide user feedback on success/failure.

📝 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
const handleSave = async (itemId: number) => {
await updateMessages.mutateAsync({
itemId,
messages: {
warning_message: warningMessage.trim() || null,
confirmation_message: confirmationMessage.trim() || null,
},
});
setEditingItem(null);
setWarningMessage("");
setConfirmationMessage("");
};
const handleSave = async (itemId: number) => {
try {
await updateMessages.mutateAsync({
itemId,
messages: {
warning_message: warningMessage.trim() || null,
confirmation_message: confirmationMessage.trim() || null,
},
});
setEditingItem(null);
setWarningMessage("");
setConfirmationMessage("");
} catch (error) {
// Error is already handled by react-query, but optionally show inline feedback
console.error("Failed to save messages:", error);
}
};
🤖 Prompt for AI Agents
In @admin/components/ReservationItemsModal.tsx around lines 41 - 52, The
handleSave function currently calls updateMessages.mutateAsync without error
handling and always resets local state; wrap the mutateAsync call in a try/catch
and use toast from react-hot-toast to notify the user: call toast.success(...)
on successful mutation and only then call setEditingItem(null),
setWarningMessage(""), and setConfirmationMessage(""); in the catch block call
toast.error(...) with the error message so failures are surfaced; reference
handleSave and updateMessages.mutateAsync when implementing the try/catch and
toast calls.

Comment on lines +55 to +71
export function useStableCallback<T extends (...args: unknown[]) => unknown>(
callback: T,
dependencies: unknown[],
): T {
const callbackRef = useRef<T>(callback);
const depsRef = useRef(dependencies);

// Update callback if dependencies changed
if (dependencies.some((dep, i) => dep !== depsRef.current[i])) {
callbackRef.current = callback;
depsRef.current = dependencies;
}

return useCallback((...args: Parameters<T>) => {
return callbackRef.current(...args);
}, []) as T;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Side effect during render violates React's purity rules.

Mutating refs based on dependency comparison during render (lines 63-66) is a side effect that breaks React's assumption that render is pure. This can cause issues with:

  • StrictMode double-renders
  • Concurrent rendering features
  • Future React versions

Additionally, the dependency comparison doesn't handle array length changes (if new array is shorter, extra old deps aren't detected).

Recommended fix using useEffect for ref updates
 export function useStableCallback<T extends (...args: unknown[]) => unknown>(
   callback: T,
-  dependencies: unknown[],
 ): T {
   const callbackRef = useRef<T>(callback);
-  const depsRef = useRef(dependencies);

-  // Update callback if dependencies changed
-  if (dependencies.some((dep, i) => dep !== depsRef.current[i])) {
-    callbackRef.current = callback;
-    depsRef.current = dependencies;
-  }
+  // Update ref in effect to avoid side effects during render
+  useEffect(() => {
+    callbackRef.current = callback;
+  });

   return useCallback((...args: Parameters<T>) => {
     return callbackRef.current(...args);
   }, []) as T;
 }

Note: You'll need to add useEffect to the imports. This pattern always keeps the ref up-to-date after each render while maintaining a stable callback identity.

🤖 Prompt for AI Agents
In @admin/lib/performance.ts around lines 55 - 71, The function
useStableCallback mutates refs during render which violates React purity; change
the ref-update logic to run inside a useEffect (add useEffect to imports) so
depsRef.current and callbackRef.current are updated after render, and in that
effect compare both array lengths and elements (e.g., check length mismatch or
any dep !== depsRef.current[i]) to detect changes including shorter arrays
before assigning the new dependencies and callback; keep returning the stable
useCallback wrapper (empty deps) so the callback identity remains stable while
avoiding render-side effects.

Comment on lines +74 to +99
export function useIntersectionObserver(
callback: (entries: IntersectionObserverEntry[]) => void,
options?: IntersectionObserverInit,
) {
const observer = useRef<IntersectionObserver | null>(null);

const observe = useCallback(
(element: Element) => {
if (observer.current) {
observer.current.disconnect();
}

observer.current = new IntersectionObserver(callback, options);
observer.current.observe(element);
},
[callback, options],
);

const disconnect = useCallback(() => {
if (observer.current) {
observer.current.disconnect();
}
}, []);

return { observe, disconnect };
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing cleanup on unmount and unstable options dependency.

Two issues with this hook:

  1. Missing unmount cleanup: If the component unmounts without calling disconnect(), the observer continues running, potentially causing memory leaks or errors when the callback fires.

  2. Unstable options dependency: Object reference comparison means if the caller doesn't memoize options, every render creates a new observer.

Recommended fix with proper cleanup
 export function useIntersectionObserver(
   callback: (entries: IntersectionObserverEntry[]) => void,
   options?: IntersectionObserverInit,
 ) {
   const observer = useRef<IntersectionObserver | null>(null);
+  const callbackRef = useRef(callback);
+  callbackRef.current = callback;

   const observe = useCallback(
     (element: Element) => {
       if (observer.current) {
         observer.current.disconnect();
       }

-      observer.current = new IntersectionObserver(callback, options);
+      observer.current = new IntersectionObserver(
+        (entries) => callbackRef.current(entries),
+        options
+      );
       observer.current.observe(element);
     },
-    [callback, options],
+    [options?.root, options?.rootMargin, options?.threshold],
   );

   const disconnect = useCallback(() => {
     if (observer.current) {
       observer.current.disconnect();
     }
   }, []);

+  // Cleanup on unmount
+  useEffect(() => {
+    return () => {
+      if (observer.current) {
+        observer.current.disconnect();
+      }
+    };
+  }, []);

   return { observe, disconnect };
 }

This stabilizes the callback via ref and adds automatic cleanup on unmount. You'll need to add useEffect to imports.

🤖 Prompt for AI Agents
In @admin/lib/performance.ts around lines 74 - 99, The hook lacks unmount
cleanup and treats options as unstable; fix by stabilizing the callback with a
ref (create callbackRef in useIntersectionObserver, set callbackRef.current =
callback in an effect) and use callbackRef.current when constructing the
IntersectionObserver inside observe, add a useEffect that returns a cleanup
which disconnects observer.current on unmount (and when options change), and
address the unstable options by deriving a stable dependency (e.g., memoize or
serialize options with useMemo/JSON.stringify and use that stable value in your
observe/effect dependencies) so observe/disconnect no longer recreate the
observer every render.

Comment on lines +781 to +858
// Send notification to all users about the new event (with translations)
go func() {
users, err := h.notificationService.GetAllUsersWithLanguage()
if err != nil {
log.Printf("Failed to get all users with language for new event notification: %v", err)
return
}

if len(users) == 0 {
log.Println("No users found for new event notification")
return
}

// Group users by language
languageGroups := make(map[string][]models.NotificationTargetWithLanguage)
for _, user := range users {
if user.NotificationToken != "" {
langCode := user.LanguageCode
if langCode == "" {
langCode = "fr" // Default to French
}
languageGroups[langCode] = append(languageGroups[langCode], user)
}
}

totalSent := 0
// Send notifications to each language group
for langCode, langUsers := range languageGroups {
localizer := appI18n.GetLocalizer(langCode)

title := localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "event_notification.new_event_title",
DefaultMessage: &i18n.Message{
ID: "event_notification.new_event_title",
Other: "New event",
},
})

message := localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "event_notification.new_event_message",
TemplateData: map[string]interface{}{
"EventName": req.Name,
"Location": req.Location,
},
DefaultMessage: &i18n.Message{
ID: "event_notification.new_event_message",
Other: "{{.EventName}} - {{.Location}}",
},
})

var tokens []string
for _, user := range langUsers {
tokens = append(tokens, user.NotificationToken)
}

payload := models.NotificationPayload{
NotificationTokens: tokens,
Title: title,
Message: message,
Sound: "default",
ChannelID: "default",
Data: map[string]interface{}{
"screen": "Events",
"eventId": eventID,
},
}

if err := h.notificationService.SendPushNotification(payload); err != nil {
log.Printf("Failed to send new event notification to %s users: %v", langCode, err)
continue
}

log.Printf("Successfully sent new event notification to %d users in %s", len(tokens), langCode)
totalSent += len(tokens)
}

log.Printf("Successfully sent new event notification to %d users across %d languages", totalSent, len(languageGroups))
}()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Goroutine lacks panic recovery and timeout control.

The notification goroutine could panic (e.g., if MustLocalize fails unexpectedly or if the notification service has an unhandled error). Additionally, there's no timeout or context, so the goroutine could run indefinitely if SendPushNotification hangs.

🔒️ Add panic recovery and timeout
 // Send notification to all users about the new event (with translations)
 go func() {
+	defer func() {
+		if r := recover(); r != nil {
+			log.Printf("Panic in event notification goroutine: %v", r)
+		}
+	}()
+
 	users, err := h.notificationService.GetAllUsersWithLanguage()
 	if err != nil {
 		log.Printf("Failed to get all users with language for new event notification: %v", err)
 		return
 	}
 	// ... rest of the code
 }()

Consider also adding a context with timeout for the notification sending operations.

📝 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
// Send notification to all users about the new event (with translations)
go func() {
users, err := h.notificationService.GetAllUsersWithLanguage()
if err != nil {
log.Printf("Failed to get all users with language for new event notification: %v", err)
return
}
if len(users) == 0 {
log.Println("No users found for new event notification")
return
}
// Group users by language
languageGroups := make(map[string][]models.NotificationTargetWithLanguage)
for _, user := range users {
if user.NotificationToken != "" {
langCode := user.LanguageCode
if langCode == "" {
langCode = "fr" // Default to French
}
languageGroups[langCode] = append(languageGroups[langCode], user)
}
}
totalSent := 0
// Send notifications to each language group
for langCode, langUsers := range languageGroups {
localizer := appI18n.GetLocalizer(langCode)
title := localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "event_notification.new_event_title",
DefaultMessage: &i18n.Message{
ID: "event_notification.new_event_title",
Other: "New event",
},
})
message := localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "event_notification.new_event_message",
TemplateData: map[string]interface{}{
"EventName": req.Name,
"Location": req.Location,
},
DefaultMessage: &i18n.Message{
ID: "event_notification.new_event_message",
Other: "{{.EventName}} - {{.Location}}",
},
})
var tokens []string
for _, user := range langUsers {
tokens = append(tokens, user.NotificationToken)
}
payload := models.NotificationPayload{
NotificationTokens: tokens,
Title: title,
Message: message,
Sound: "default",
ChannelID: "default",
Data: map[string]interface{}{
"screen": "Events",
"eventId": eventID,
},
}
if err := h.notificationService.SendPushNotification(payload); err != nil {
log.Printf("Failed to send new event notification to %s users: %v", langCode, err)
continue
}
log.Printf("Successfully sent new event notification to %d users in %s", len(tokens), langCode)
totalSent += len(tokens)
}
log.Printf("Successfully sent new event notification to %d users across %d languages", totalSent, len(languageGroups))
}()
// Send notification to all users about the new event (with translations)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in event notification goroutine: %v", r)
}
}()
users, err := h.notificationService.GetAllUsersWithLanguage()
if err != nil {
log.Printf("Failed to get all users with language for new event notification: %v", err)
return
}
if len(users) == 0 {
log.Println("No users found for new event notification")
return
}
// Group users by language
languageGroups := make(map[string][]models.NotificationTargetWithLanguage)
for _, user := range users {
if user.NotificationToken != "" {
langCode := user.LanguageCode
if langCode == "" {
langCode = "fr" // Default to French
}
languageGroups[langCode] = append(languageGroups[langCode], user)
}
}
totalSent := 0
// Send notifications to each language group
for langCode, langUsers := range languageGroups {
localizer := appI18n.GetLocalizer(langCode)
title := localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "event_notification.new_event_title",
DefaultMessage: &i18n.Message{
ID: "event_notification.new_event_title",
Other: "New event",
},
})
message := localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "event_notification.new_event_message",
TemplateData: map[string]interface{}{
"EventName": req.Name,
"Location": req.Location,
},
DefaultMessage: &i18n.Message{
ID: "event_notification.new_event_message",
Other: "{{.EventName}} - {{.Location}}",
},
})
var tokens []string
for _, user := range langUsers {
tokens = append(tokens, user.NotificationToken)
}
payload := models.NotificationPayload{
NotificationTokens: tokens,
Title: title,
Message: message,
Sound: "default",
ChannelID: "default",
Data: map[string]interface{}{
"screen": "Events",
"eventId": eventID,
},
}
if err := h.notificationService.SendPushNotification(payload); err != nil {
log.Printf("Failed to send new event notification to %s users: %v", langCode, err)
continue
}
log.Printf("Successfully sent new event notification to %d users in %s", len(tokens), langCode)
totalSent += len(tokens)
}
log.Printf("Successfully sent new event notification to %d users across %d languages", totalSent, len(languageGroups))
}()

Comment on lines +117 to 128
// Build query dynamically with correct parameter numbering
columns := []string{"name", "id_clubs"}
placeholders := []string{"$1", "$2"}
args := []interface{}{category.Name, *category.IDClubParent}
paramNum := 3

if category.IDCategoryParent != nil {
extraCols = ", id_parent_category"
extraVals = ", $3"
columns = append(columns, "id_parent_category")
placeholders = append(placeholders, fmt.Sprintf("$%d", paramNum))
args = append(args, *category.IDCategoryParent)
paramNum++
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Potential nil pointer dereference on IDClubParent.

Line 120 dereferences *category.IDClubParent without checking if it's nil. If the caller passes a request without IDClubParent, this will panic.

🐛 Add nil check
 func (r *ReservationRepository) CreateCategory(category models.ReservationCreateCategoryRequest) (models.ReservationCategoryComplete, error) {
 	var res models.ReservationCategoryComplete
 	if category.Name == "" {
 		utils.LogMessage(utils.LevelError, "Category name is required")
 		return res, fmt.Errorf("category name is required")
 	}
+	if category.IDClubParent == nil {
+		utils.LogMessage(utils.LevelError, "Club parent ID is required")
+		return res, fmt.Errorf("club parent ID is required")
+	}

 	// Build query dynamically with correct parameter numbering
 	columns := []string{"name", "id_clubs"}
 	placeholders := []string{"$1", "$2"}
 	args := []interface{}{category.Name, *category.IDClubParent}
🤖 Prompt for AI Agents
In @handlers/reservation/repository/reservation_repository.go around lines 117 -
128, The code dereferences *category.IDClubParent when building args
(columns/placeholders/args/paramNum) which can panic if IDClubParent is nil;
update the logic in the block that initializes columns/placeholders/args so you
first check if category.IDClubParent != nil before appending and using
*category.IDClubParent, and if that field is required return a validation error
(or if optional, append a nil value or omit the column entirely) so that
functions using columns/placeholders/args (and variables paramNum,
category.Name) never perform a direct dereference of a potentially nil pointer.

Comment on lines +160 to +164
// Build query dynamically with correct parameter numbering
columns := []string{"name", "slot", "id_clubs"}
placeholders := []string{"$1", "$2", "$3"}
args := []interface{}{item.Name, item.Slot, *item.IDClubParent}
extraCols, extraVals := "", ""
paramNum := 4
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Same nil pointer dereference risk for IDClubParent in CreateItem.

Line 163 dereferences *item.IDClubParent without a nil check.

🐛 Add nil check
 func (r *ReservationRepository) CreateItem(item models.ReservationCreateItemRequest) (models.ReservationItemComplete, error) {
 	var res models.ReservationItemComplete

 	if item.Name == "" {
 		utils.LogMessage(utils.LevelError, "Item name is required")
 		return res, fmt.Errorf("item name is required")
 	}
+	if item.IDClubParent == nil {
+		utils.LogMessage(utils.LevelError, "Club parent ID is required")
+		return res, fmt.Errorf("club parent ID is required")
+	}

 	// Build query dynamically with correct parameter numbering
 	columns := []string{"name", "slot", "id_clubs"}
📝 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
// Build query dynamically with correct parameter numbering
columns := []string{"name", "slot", "id_clubs"}
placeholders := []string{"$1", "$2", "$3"}
args := []interface{}{item.Name, item.Slot, *item.IDClubParent}
extraCols, extraVals := "", ""
paramNum := 4
if item.Name == "" {
utils.LogMessage(utils.LevelError, "Item name is required")
return res, fmt.Errorf("item name is required")
}
if item.IDClubParent == nil {
utils.LogMessage(utils.LevelError, "Club parent ID is required")
return res, fmt.Errorf("club parent ID is required")
}
// Build query dynamically with correct parameter numbering
columns := []string{"name", "slot", "id_clubs"}
placeholders := []string{"$1", "$2", "$3"}
args := []interface{}{item.Name, item.Slot, *item.IDClubParent}
paramNum := 4
🤖 Prompt for AI Agents
In @handlers/reservation/repository/reservation_repository.go around lines 160 -
164, The code dereferences *item.IDClubParent when building args
(columns/placeholders/args/paramNum) which can panic; in the CreateItem function
add an explicit nil check for item.IDClubParent before using *item.IDClubParent
and handle it (e.g., return a descriptive error from CreateItem such as "missing
IDClubParent" or validate earlier) rather than dereferencing; update any logic
that builds columns/placeholders/args (variables columns, placeholders, args,
paramNum) to only use *item.IDClubParent after the nil check so the function
cannot panic.

Comment on lines +58 to +64
func (es *EventScheduler) checkAndSendEventNotifications() {
// Get current time + 1 hour
oneHourFromNow := time.Now().Add(1 * time.Hour)
// Use a 2-minute window to account for timing variations
windowStart := oneHourFromNow.Add(-1 * time.Minute)
windowEnd := oneHourFromNow.Add(1 * time.Minute)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the repository structure
git ls-files | grep -E "(migration|db)" | head -20

Repository: PlugImt/transat-backend

Length of output: 923


🏁 Script executed:

# Check for timestamp definitions in migrations
find . -type f -name "*migration*" -o -name "*sql" | head -20

Repository: PlugImt/transat-backend

Length of output: 994


🏁 Script executed:

# Search for how start_date is used in database queries
rg -n 'start_date' --type-list

Repository: PlugImt/transat-backend

Length of output: 456


🏁 Script executed:

# Check the events migration to see how start_date is defined
cat db/migrations/00014_events.sql

Repository: PlugImt/transat-backend

Length of output: 574


🏁 Script executed:

# Also check the end_date migration
cat db/migrations/00018_non_nullable_event_end_date.sql

Repository: PlugImt/transat-backend

Length of output: 486


🏁 Script executed:

# Search for start_date usage in the codebase
rg -n 'start_date' --type go

Repository: PlugImt/transat-backend

Length of output: 4260


🏁 Script executed:

# Look at the full EventScheduler implementation
cat -n scheduler/event_scheduler.go

Repository: PlugImt/transat-backend

Length of output: 8595


🏁 Script executed:

# Check the initial schema for the events table definition
cat db/migrations/00001_initial_schema.sql | grep -A 20 "CREATE TABLE events"

Repository: PlugImt/transat-backend

Length of output: 1845


🏁 Script executed:

# Search for any other timezone-related configurations or migrations
rg -i 'timezone|time zone' db/migrations/

Repository: PlugImt/transat-backend

Length of output: 178


🏁 Script executed:

# Check how event timestamps are inserted into the database to understand timezone handling
rg -n 'INSERT INTO events' -A 5 handlers/

Repository: PlugImt/transat-backend

Length of output: 1934


Use UTC consistently for server and database time comparisons.

time.Now() uses the server's local timezone, but start_date is stored as TIMESTAMP (timezone-naive) in the database. If the server timezone differs from the database server's timezone, events will be missed or notified at incorrect times. The query also uses PostgreSQL's NOW(), creating a mismatch between Go's local time values and database comparisons.

Change time.Now() to time.Now().UTC() and ensure the database stores event timestamps in UTC or migrates start_date to TIMESTAMP WITH TIME ZONE for consistency.

Proposed fix
 func (es *EventScheduler) checkAndSendEventNotifications() {
   // Get current time + 1 hour
-  oneHourFromNow := time.Now().Add(1 * time.Hour)
+  oneHourFromNow := time.Now().UTC().Add(1 * time.Hour)
   // Use a 2-minute window to account for timing variations
   windowStart := oneHourFromNow.Add(-1 * time.Minute)
   windowEnd := oneHourFromNow.Add(1 * time.Minute)

Additionally, consider migrating the events.start_date column from TIMESTAMP to TIMESTAMP WITH TIME ZONE to eliminate ambiguity.

📝 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
func (es *EventScheduler) checkAndSendEventNotifications() {
// Get current time + 1 hour
oneHourFromNow := time.Now().Add(1 * time.Hour)
// Use a 2-minute window to account for timing variations
windowStart := oneHourFromNow.Add(-1 * time.Minute)
windowEnd := oneHourFromNow.Add(1 * time.Minute)
func (es *EventScheduler) checkAndSendEventNotifications() {
// Get current time + 1 hour
oneHourFromNow := time.Now().UTC().Add(1 * time.Hour)
// Use a 2-minute window to account for timing variations
windowStart := oneHourFromNow.Add(-1 * time.Minute)
windowEnd := oneHourFromNow.Add(1 * time.Minute)
🤖 Prompt for AI Agents
In @scheduler/event_scheduler.go around lines 58 - 64, In
EventScheduler.checkAndSendEventNotifications replace time.Now() with
time.Now().UTC() when computing oneHourFromNow/windowStart/windowEnd (variables
oneHourFromNow, windowStart, windowEnd) so Go-side times are in UTC, and update
the DB side to use UTC-aware comparisons (either ensure events.start_date is
stored in UTC or migrate start_date to TIMESTAMP WITH TIME ZONE and use
timezone-aware NOW() in queries) to avoid server vs DB timezone mismatches.

Comment on lines +208 to +224
// markNotificationSent marks that a notification has been sent for an event
func (es *EventScheduler) markNotificationSent(eventID int, notificationType string) {
// Create table if it doesn't exist (should be done via migration, but this is a safety check)
createTableQuery := `
CREATE TABLE IF NOT EXISTS event_notification_sent (
id SERIAL PRIMARY KEY,
id_events INTEGER NOT NULL,
notification_type VARCHAR(50) NOT NULL,
sent_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(id_events, notification_type),
FOREIGN KEY (id_events) REFERENCES events(id_events) ON DELETE CASCADE
)
`
_, err := es.db.Exec(createTableQuery)
if err != nil {
log.Printf("Error creating event_notification_sent table: %v", err)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "*.sql" -o -name "*migration*" -type f | head -20

Repository: PlugImt/transat-backend

Length of output: 994


🏁 Script executed:

rg 'event_notification_sent' --type sql --type go

Repository: PlugImt/transat-backend

Length of output: 1127


🏁 Script executed:

cat db/migrations/00022_event_notification_sent.sql

Repository: PlugImt/transat-backend

Length of output: 1029


Remove defensive CREATE TABLE IF NOT EXISTS from application code.

Schema changes should only be managed via migrations. The migration 00022_event_notification_sent.sql already handles complete table creation with indexes and proper rollback logic. This defensive check in the application code is redundant and creates several issues:

  1. Race conditions if multiple instances run simultaneously
  2. Schema drift if the inline definition diverges from migrations
  3. Bypasses migration tracking and rollback capabilities
Proposed fix
 func (es *EventScheduler) markNotificationSent(eventID int, notificationType string) {
-  // Create table if it doesn't exist (should be done via migration, but this is a safety check)
-  createTableQuery := `
-    CREATE TABLE IF NOT EXISTS event_notification_sent (
-      id SERIAL PRIMARY KEY,
-      id_events INTEGER NOT NULL,
-      notification_type VARCHAR(50) NOT NULL,
-      sent_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-      UNIQUE(id_events, notification_type),
-      FOREIGN KEY (id_events) REFERENCES events(id_events) ON DELETE CASCADE
-    )
-  `
-  _, err := es.db.Exec(createTableQuery)
-  if err != nil {
-    log.Printf("Error creating event_notification_sent table: %v", err)
-  }
-
   // Insert or update the notification record
   insertQuery := `
📝 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
// markNotificationSent marks that a notification has been sent for an event
func (es *EventScheduler) markNotificationSent(eventID int, notificationType string) {
// Create table if it doesn't exist (should be done via migration, but this is a safety check)
createTableQuery := `
CREATE TABLE IF NOT EXISTS event_notification_sent (
id SERIAL PRIMARY KEY,
id_events INTEGER NOT NULL,
notification_type VARCHAR(50) NOT NULL,
sent_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(id_events, notification_type),
FOREIGN KEY (id_events) REFERENCES events(id_events) ON DELETE CASCADE
)
`
_, err := es.db.Exec(createTableQuery)
if err != nil {
log.Printf("Error creating event_notification_sent table: %v", err)
}
// markNotificationSent marks that a notification has been sent for an event
func (es *EventScheduler) markNotificationSent(eventID int, notificationType string) {
// Insert or update the notification record
insertQuery := `
🤖 Prompt for AI Agents
In @scheduler/event_scheduler.go around lines 208 - 224, Remove the defensive
CREATE TABLE logic from markNotificationSent on EventScheduler: the inline
createTableQuery and the es.db.Exec call that attempts to create
event_notification_sent should be deleted so schema is managed only via
migrations (migration 00022_event_notification_sent.sql); ensure
markNotificationSent only performs the insert/update logic for recording a sent
notification and does not attempt any DDL, and remove or adjust any related
error logging in that function that referenced the table creation attempt.

* Add allergens support to restaurant menu

Introduces database tables for allergens and their association with restaurant articles, including initial allergen data. Adds example HTML pages and updates Go backend models, repositories, and services to support allergen information for menu items.

* Update restaurant_scheduler.go
Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scheduler/restaurant_scheduler.go (1)

75-76: Remove debug print statements.

These fmt.Println calls appear to be debug artifacts and should be removed or converted to proper logging using utils.LogMessage(utils.LevelDebug, ...) for production code.

🧹 Proposed fix
 func (s *RestaurantScheduler) isScheduledTimeAllowed(t time.Time) bool {
 	parisTime := utils.ToParisTime(t)
 	weekday := parisTime.Weekday()
-	fmt.Println("-------> Weekday:", weekday)
-	fmt.Println("-------> Hour Paris time:", parisTime.Hour())
 	if weekday == time.Saturday || weekday == time.Sunday {
 		return false
 	}
🤖 Fix all issues with AI agents
In `@handlers/restaurant/repository/menu_repository.go`:
- Around line 145-182: After iterating allergenRows in the function that builds
the allergens slice, add a post-iteration error check by calling
allergenRows.Err() and logging/handling any error returned (similar to existing
logging pattern using utils.LogMessage and fmt.Sprintf). Specifically, after the
for allergenRows.Next() loop for models.Allergen, check if err :=
allergenRows.Err(); err != nil { utils.LogMessage(utils.LevelWarn,
fmt.Sprintf("Failed during allergen rows iteration for dish %d: %v", dishID,
err)) } so any iteration errors on allergenRows are surfaced and handled.

In `@scheduler/restaurant_scheduler.go`:
- Line 82: Log messages still say "9h-14h" while the service window was changed
to 8h-15h; update every hardcoded occurrence of the string "9h-14h" in
restaurant_scheduler.go to "8h-15h" (the two occurrences near the scheduler
start/stop logs) so logs match the actual logic that returns hour >= 8 && hour <
15; ensure any surrounding text remains grammatically correct and update any
related test or comment strings if present.
🧹 Nitpick comments (2)
models/restaurant.go (1)

52-61: Consider using a slice for allergen fields if the API structure permits.

The 10 individual Allergene1 through Allergene10 fields work but require repetitive handling. If the external API cannot be changed, this is acceptable; otherwise, an array would simplify processing.

handlers/restaurant/service/menu_service.go (1)

136-143: Consider extracting categoryToID map to avoid duplication.

This map is defined in three places:

  • BuildFetchedItems (lines 136-143)
  • ConvertMenuDataToFetchedItems (lines 226-233)
  • Implicitly in processRawMenuItems switch statement

Extracting it to a package-level constant would reduce maintenance burden.

Example extraction
// At package level
var categoryToID = map[string]int{
    "grilladesMidi": 1,
    "migrateurs":    2,
    "cibo":          3,
    "accompMidi":    4,
    "grilladesSoir": 5,
    "accompSoir":    6,
}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 58f141c and e352fca.

📒 Files selected for processing (8)
  • db/migrations/00023_allergens.sql
  • exemple_restaurant_page.html
  • exemple_restaurant_page_wrapped.html
  • handlers/restaurant/repository/menu_repository.go
  • handlers/restaurant/scheduler.go
  • handlers/restaurant/service/menu_service.go
  • models/restaurant.go
  • scheduler/restaurant_scheduler.go
🧰 Additional context used
🧬 Code graph analysis (3)
handlers/restaurant/repository/menu_repository.go (2)
utils/logManager.go (2)
  • LogMessage (62-64)
  • LevelWarn (17-17)
models/restaurant.go (3)
  • Allergen (35-41)
  • DishDetailsResponse (146-153)
  • RestaurantArticle (12-17)
handlers/restaurant/scheduler.go (2)
handlers/restaurant/service/menu_service.go (1)
  • MenuService (15-18)
utils/logManager.go (3)
  • LogMessage (62-64)
  • LevelError (16-16)
  • LogFooter (70-72)
handlers/restaurant/service/menu_service.go (1)
models/restaurant.go (3)
  • MenuItemAPI (45-62)
  • MenuData (73-80)
  • FetchedItem (65-70)
🔇 Additional comments (11)
handlers/restaurant/scheduler.go (1)

29-57: LGTM! Clean refactor to support allergen data flow.

The updated CheckAndUpdateMenuCron correctly chains:

  1. FetchRawMenuItems() to get raw API data with allergen fields
  2. BuildFetchedItems() to transform and preserve allergen codes
  3. SyncTodaysMenu() for database synchronization

Error handling and logging remain consistent with the previous implementation.

db/migrations/00023_allergens.sql (3)

5-12: Schema design looks solid.

The allergens table structure with name as UNIQUE constraint enables safe ON CONFLICT handling during seeding and runtime lookups by code.


14-28: FK cascade strategy is appropriate.

  • ON DELETE CASCADE on id_restaurant_articles: removes allergen links when an article is deleted
  • ON DELETE RESTRICT on id_allergen: prevents accidental deletion of allergens that are still referenced

This protects referential integrity while allowing article cleanup.


39-102: Clarify where pictosAllergenes assets are located and served.

The picture_url values store relative paths like views/assets/img/pictosAllergenes/Arachide.svg, but these assets don't exist in this backend repository. These are metadata references that the backend returns via the API for clients to consume. Ensure:

  1. Frontend/client applications know where these assets are actually hosted (separate frontend repo, CDN, etc.)
  2. The path format aligns with how the frontend or deployment infrastructure serves static assets
  3. Asset availability is verified in the frontend deployment, not the backend
models/restaurant.go (2)

34-41: LGTM! Allergen model correctly maps to DB schema.

The struct fields and tags align with the migration schema (id_allergen, name, description, description_en, picture_url).


144-153: LGTM! Well-structured response type.

DishDetailsResponse cleanly embeds RestaurantArticle and extends it with computed fields (AverageRating, TotalRatings, TimesServed) and related data (Allergens, Reviews). The JSON field name recent_reviews for Reviews appropriately describes the content.

handlers/restaurant/repository/menu_repository.go (2)

501-540: Allergen sync logic handles edge cases well.

The implementation:

  • Creates missing allergens on-the-fly (with minimal data)
  • Uses ON CONFLICT DO NOTHING for idempotent article-allergen linking
  • Logs warnings without blocking the sync process

One consideration: dynamically inserted allergens (line 515-523) will lack description, description_en, and picture_url. This is acceptable if all expected allergens are pre-seeded, but could result in incomplete data for unknown codes.


196-209: Good graceful degradation on review fetch failure.

Returning partial data with allergens but nil reviews when the reviews query fails is a reasonable approach that maintains API availability.

handlers/restaurant/service/menu_service.go (3)

31-71: LGTM! Clean extraction of raw API fetching.

FetchRawMenuItems properly separates the HTTP fetch and JSON parsing from the data transformation, enabling the new allergen-preserving flow while maintaining backward compatibility via FetchMenuFromAPI.


162-182: Allergen code collection is well-implemented.

The logic correctly:

  • Trims whitespace from each code
  • Filters out empty strings and placeholder values (vide, undefined)
  • Deduplicates codes using a seen map
  • Preserves original casing for DB lookup

131-193: Note: BuildFetchedItems allows duplicate item names unlike processRawMenuItems.

processRawMenuItems uses an itemMap to deduplicate items by category+name, but BuildFetchedItems does not. This is acceptable since SyncTodaysMenu uses ON CONFLICT at the database level, but be aware that duplicate API entries will result in redundant processing and allergen sync operations.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +145 to +182
// Fetch allergens linked to this article
allergensQuery := `
SELECT a.id_allergen,
a.name,
COALESCE(a.description, ''),
COALESCE(a.description_en, ''),
COALESCE(a.picture_url, '')
FROM restaurant_article_allergens raa
JOIN allergens a ON raa.id_allergen = a.id_allergen
WHERE raa.id_restaurant_articles = $1
ORDER BY a.name;
`

allergenRows, err := r.DB.Query(allergensQuery, dishID)
if err != nil {
utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to fetch allergens for dish %d: %v", dishID, err))
}

var allergens []models.Allergen
if allergenRows != nil {
defer func(rows *sql.Rows) {
if rows == nil {
return
}
if err := rows.Close(); err != nil {
utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to close allergen rows: %v", err))
}
}(allergenRows)

for allergenRows.Next() {
var a models.Allergen
if err := allergenRows.Scan(&a.ID, &a.Name, &a.Description, &a.DescriptionEn, &a.PictureURL); err != nil {
utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to scan allergen: %v", err))
continue
}
allergens = append(allergens, a)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing allergenRows.Err() check after iteration.

After the for allergenRows.Next() loop, you should check allergenRows.Err() to catch any errors that occurred during iteration. This is a common Go database/sql best practice.

Proposed fix
 		for allergenRows.Next() {
 			var a models.Allergen
 			if err := allergenRows.Scan(&a.ID, &a.Name, &a.Description, &a.DescriptionEn, &a.PictureURL); err != nil {
 				utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to scan allergen: %v", err))
 				continue
 			}
 			allergens = append(allergens, a)
 		}
+		if err := allergenRows.Err(); err != nil {
+			utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Error iterating allergen rows: %v", err))
+		}
 	}
📝 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
// Fetch allergens linked to this article
allergensQuery := `
SELECT a.id_allergen,
a.name,
COALESCE(a.description, ''),
COALESCE(a.description_en, ''),
COALESCE(a.picture_url, '')
FROM restaurant_article_allergens raa
JOIN allergens a ON raa.id_allergen = a.id_allergen
WHERE raa.id_restaurant_articles = $1
ORDER BY a.name;
`
allergenRows, err := r.DB.Query(allergensQuery, dishID)
if err != nil {
utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to fetch allergens for dish %d: %v", dishID, err))
}
var allergens []models.Allergen
if allergenRows != nil {
defer func(rows *sql.Rows) {
if rows == nil {
return
}
if err := rows.Close(); err != nil {
utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to close allergen rows: %v", err))
}
}(allergenRows)
for allergenRows.Next() {
var a models.Allergen
if err := allergenRows.Scan(&a.ID, &a.Name, &a.Description, &a.DescriptionEn, &a.PictureURL); err != nil {
utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to scan allergen: %v", err))
continue
}
allergens = append(allergens, a)
}
}
// Fetch allergens linked to this article
allergensQuery := `
SELECT a.id_allergen,
a.name,
COALESCE(a.description, ''),
COALESCE(a.description_en, ''),
COALESCE(a.picture_url, '')
FROM restaurant_article_allergens raa
JOIN allergens a ON raa.id_allergen = a.id_allergen
WHERE raa.id_restaurant_articles = $1
ORDER BY a.name;
`
allergenRows, err := r.DB.Query(allergensQuery, dishID)
if err != nil {
utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to fetch allergens for dish %d: %v", dishID, err))
}
var allergens []models.Allergen
if allergenRows != nil {
defer func(rows *sql.Rows) {
if rows == nil {
return
}
if err := rows.Close(); err != nil {
utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to close allergen rows: %v", err))
}
}(allergenRows)
for allergenRows.Next() {
var a models.Allergen
if err := allergenRows.Scan(&a.ID, &a.Name, &a.Description, &a.DescriptionEn, &a.PictureURL); err != nil {
utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Failed to scan allergen: %v", err))
continue
}
allergens = append(allergens, a)
}
if err := allergenRows.Err(); err != nil {
utils.LogMessage(utils.LevelWarn, fmt.Sprintf("Error iterating allergen rows: %v", err))
}
}
🤖 Prompt for AI Agents
In `@handlers/restaurant/repository/menu_repository.go` around lines 145 - 182,
After iterating allergenRows in the function that builds the allergens slice,
add a post-iteration error check by calling allergenRows.Err() and
logging/handling any error returned (similar to existing logging pattern using
utils.LogMessage and fmt.Sprintf). Specifically, after the for
allergenRows.Next() loop for models.Allergen, check if err :=
allergenRows.Err(); err != nil { utils.LogMessage(utils.LevelWarn,
fmt.Sprintf("Failed during allergen rows iteration for dish %d: %v", dishID,
err)) } so any iteration errors on allergenRows are surfaced and handled.


hour := parisTime.Hour()
return hour >= 8 && hour < 14
return hour >= 8 && hour < 15
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Time window extended; update log messages for consistency.

The time window is now 8h-15h (Paris time), but log messages on lines 50 and 89 still reference "9h-14h". Update them to reflect the actual window.

🔧 Proposed fix for log messages
@@ -47,7 +47,7 @@
 			case <-ticker.C:
 				if s.isScheduledTimeAllowed(time.Now()) {
 					go s.checkAndUpdateMenu()
 				} else {
-					utils.LogMessage(utils.LevelDebug, "Scheduler: Outside allowed time window (weekdays 9h-14h), skipping check")
+					utils.LogMessage(utils.LevelDebug, "Scheduler: Outside allowed time window (weekdays 8h-15h), skipping check")
 				}
@@ -86,7 +86,7 @@
 func (s *RestaurantScheduler) checkAndUpdateMenu() {
 	now := time.Now()

 	if !s.isScheduledTimeAllowed(now) {
-		utils.LogMessage(utils.LevelInfo, "Scheduler: Outside allowed time window (weekdays 9h-14h), skipping menu check")
+		utils.LogMessage(utils.LevelInfo, "Scheduler: Outside allowed time window (weekdays 8h-15h), skipping menu check")
 		return
 	}
📝 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
return hour >= 8 && hour < 15
} else {
utils.LogMessage(utils.LevelDebug, "Scheduler: Outside allowed time window (weekdays 8h-15h), skipping check")
}
Suggested change
return hour >= 8 && hour < 15
if !s.isScheduledTimeAllowed(now) {
utils.LogMessage(utils.LevelInfo, "Scheduler: Outside allowed time window (weekdays 8h-15h), skipping menu check")
return
}
🤖 Prompt for AI Agents
In `@scheduler/restaurant_scheduler.go` at line 82, Log messages still say
"9h-14h" while the service window was changed to 8h-15h; update every hardcoded
occurrence of the string "9h-14h" in restaurant_scheduler.go to "8h-15h" (the
two occurrences near the scheduler start/stop logs) so logs match the actual
logic that returns hour >= 8 && hour < 15; ensure any surrounding text remains
grammatically correct and update any related test or comment strings if present.

Updated the isScheduledTimeAllowed function to allow scheduling until 16:00 instead of 15:00. Also made minor formatting adjustments in models/restaurant.go for consistency.
Introduces an is_marker boolean column to the allergens table to distinguish between allergens and label/marker entries. Updates the Allergen model to include the is_marker field and migrates marker pictograms into the allergens table with full absolute URLs for picture_url.
Introduces handling of marker fields (e.g., bio, local, vegetarien) from the API in the menu sync process. Updates models to include marker fields, extracts marker codes in the service layer, and syncs marker associations in the repository. This allows markers to be stored and linked similarly to allergens.
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.

3 participants