koprogo_api/infrastructure/web/handlers/
gamification_handlers.rs

1use crate::application::dto::{
2    CreateAchievementDto, CreateChallengeDto, UpdateAchievementDto, UpdateChallengeDto,
3};
4use crate::domain::entities::{AchievementCategory, ChallengeStatus};
5use crate::infrastructure::web::app_state::AppState;
6use crate::infrastructure::web::middleware::AuthenticatedUser;
7use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
8use serde::Deserialize;
9use uuid::Uuid;
10
11// ============================================================================
12// ACHIEVEMENT HANDLERS
13// ============================================================================
14
15/// Create a new achievement (admin only)
16///
17/// POST /achievements
18///
19/// # Request Body
20/// - organization_id: UUID
21/// - category: AchievementCategory
22/// - tier: AchievementTier
23/// - name: String (3-100 chars)
24/// - description: String (10-500 chars)
25/// - icon: String (emoji or URL)
26/// - points_value: i32 (0-1000)
27/// - requirements: String (JSON criteria)
28/// - is_secret: bool
29/// - is_repeatable: bool
30/// - display_order: i32
31///
32/// # Responses
33/// - 201 Created: Achievement created successfully
34/// - 400 Bad Request: Validation error
35#[post("/achievements")]
36pub async fn create_achievement(
37    data: web::Data<AppState>,
38    auth: AuthenticatedUser,
39    request: web::Json<CreateAchievementDto>,
40) -> impl Responder {
41    if auth.role != "superadmin" && auth.role != "syndic" {
42        return HttpResponse::Forbidden().json(serde_json::json!({
43            "error": "Only superadmin or syndic can create achievements"
44        }));
45    }
46    match data
47        .achievement_use_cases
48        .create_achievement(request.into_inner())
49        .await
50    {
51        Ok(achievement) => HttpResponse::Created().json(achievement),
52        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
53    }
54}
55
56/// Get achievement by ID
57///
58/// GET /achievements/:id
59///
60/// # Responses
61/// - 200 OK: Achievement details
62/// - 404 Not Found: Achievement not found
63#[get("/achievements/{id}")]
64pub async fn get_achievement(
65    data: web::Data<AppState>,
66    _auth: AuthenticatedUser,
67    id: web::Path<Uuid>,
68) -> impl Responder {
69    match data
70        .achievement_use_cases
71        .get_achievement(id.into_inner())
72        .await
73    {
74        Ok(achievement) => HttpResponse::Ok().json(achievement),
75        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
76    }
77}
78
79/// List all achievements for an organization
80///
81/// GET /organizations/:organization_id/achievements
82///
83/// # Responses
84/// - 200 OK: List of achievements
85#[get("/organizations/{organization_id}/achievements")]
86pub async fn list_achievements(
87    data: web::Data<AppState>,
88    _auth: AuthenticatedUser,
89    organization_id: web::Path<Uuid>,
90) -> impl Responder {
91    match data
92        .achievement_use_cases
93        .list_achievements(organization_id.into_inner())
94        .await
95    {
96        Ok(achievements) => HttpResponse::Ok().json(achievements),
97        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
98    }
99}
100
101/// List achievements by category
102///
103/// GET /organizations/:organization_id/achievements/category/:category
104///
105/// # Responses
106/// - 200 OK: List of achievements in category
107#[get("/organizations/{organization_id}/achievements/category/{category}")]
108pub async fn list_achievements_by_category(
109    data: web::Data<AppState>,
110    _auth: AuthenticatedUser,
111    path: web::Path<(Uuid, String)>,
112) -> impl Responder {
113    let (organization_id, category_str) = path.into_inner();
114
115    // Parse category
116    let category: AchievementCategory = match serde_json::from_str(&format!("\"{}\"", category_str))
117    {
118        Ok(cat) => cat,
119        Err(_) => {
120            return HttpResponse::BadRequest()
121                .json(serde_json::json!({"error": "Invalid category"}))
122        }
123    };
124
125    match data
126        .achievement_use_cases
127        .list_achievements_by_category(organization_id, category)
128        .await
129    {
130        Ok(achievements) => HttpResponse::Ok().json(achievements),
131        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
132    }
133}
134
135/// List visible achievements for current user
136///
137/// GET /organizations/:organization_id/achievements/visible
138///
139/// # Responses
140/// - 200 OK: List of visible achievements (non-secret or already earned)
141#[get("/organizations/{organization_id}/achievements/visible")]
142pub async fn list_visible_achievements(
143    data: web::Data<AppState>,
144    auth: AuthenticatedUser,
145    organization_id: web::Path<Uuid>,
146) -> impl Responder {
147    match data
148        .achievement_use_cases
149        .list_visible_achievements(organization_id.into_inner(), auth.user_id)
150        .await
151    {
152        Ok(achievements) => HttpResponse::Ok().json(achievements),
153        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
154    }
155}
156
157/// Update achievement (admin only)
158///
159/// PUT /achievements/:id
160///
161/// # Responses
162/// - 200 OK: Achievement updated successfully
163/// - 400 Bad Request: Validation error
164/// - 404 Not Found: Achievement not found
165#[put("/achievements/{id}")]
166pub async fn update_achievement(
167    data: web::Data<AppState>,
168    auth: AuthenticatedUser,
169    id: web::Path<Uuid>,
170    request: web::Json<UpdateAchievementDto>,
171) -> impl Responder {
172    if auth.role != "superadmin" && auth.role != "syndic" {
173        return HttpResponse::Forbidden().json(serde_json::json!({
174            "error": "Only superadmin or syndic can update achievements"
175        }));
176    }
177    match data
178        .achievement_use_cases
179        .update_achievement(id.into_inner(), request.into_inner())
180        .await
181    {
182        Ok(achievement) => HttpResponse::Ok().json(achievement),
183        Err(e) if e.contains("not found") => {
184            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
185        }
186        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
187    }
188}
189
190/// Delete achievement (admin only)
191///
192/// DELETE /achievements/:id
193///
194/// # Responses
195/// - 204 No Content: Achievement deleted successfully
196/// - 404 Not Found: Achievement not found
197#[delete("/achievements/{id}")]
198pub async fn delete_achievement(
199    data: web::Data<AppState>,
200    auth: AuthenticatedUser,
201    id: web::Path<Uuid>,
202) -> impl Responder {
203    if auth.role != "superadmin" && auth.role != "syndic" {
204        return HttpResponse::Forbidden().json(serde_json::json!({
205            "error": "Only superadmin or syndic can delete achievements"
206        }));
207    }
208    match data
209        .achievement_use_cases
210        .delete_achievement(id.into_inner())
211        .await
212    {
213        Ok(_) => HttpResponse::NoContent().finish(),
214        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
215    }
216}
217
218// ============================================================================
219// USER ACHIEVEMENT HANDLERS
220// ============================================================================
221
222#[derive(Debug, Deserialize)]
223pub struct AwardAchievementRequest {
224    pub achievement_id: Uuid,
225    pub progress_data: Option<String>,
226}
227
228/// Award achievement to user
229///
230/// POST /users/achievements
231///
232/// # Request Body
233/// - achievement_id: UUID
234/// - progress_data: `Option<String>` (JSON)
235///
236/// # Responses
237/// - 201 Created: Achievement awarded successfully
238/// - 400 Bad Request: Already earned (non-repeatable) or validation error
239/// - 404 Not Found: Achievement not found
240#[post("/users/achievements")]
241pub async fn award_achievement(
242    data: web::Data<AppState>,
243    auth: AuthenticatedUser,
244    request: web::Json<AwardAchievementRequest>,
245) -> impl Responder {
246    let req = request.into_inner();
247    match data
248        .achievement_use_cases
249        .award_achievement(auth.user_id, req.achievement_id, req.progress_data)
250        .await
251    {
252        Ok(user_achievement) => HttpResponse::Created().json(user_achievement),
253        Err(e) if e.contains("not found") => {
254            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
255        }
256        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
257    }
258}
259
260/// Get all achievements earned by current user
261///
262/// GET /users/achievements
263///
264/// # Responses
265/// - 200 OK: List of user achievements with enriched achievement data
266#[get("/users/achievements")]
267pub async fn get_user_achievements(
268    data: web::Data<AppState>,
269    auth: AuthenticatedUser,
270) -> impl Responder {
271    match data
272        .achievement_use_cases
273        .get_user_achievements(auth.user_id)
274        .await
275    {
276        Ok(achievements) => HttpResponse::Ok().json(achievements),
277        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
278    }
279}
280
281/// Get recent achievements for current user
282///
283/// GET /users/achievements/recent?limit=5
284///
285/// # Query Parameters
286/// - limit: i64 (default: 5)
287///
288/// # Responses
289/// - 200 OK: List of recent achievements
290#[get("/users/achievements/recent")]
291pub async fn get_recent_achievements(
292    data: web::Data<AppState>,
293    auth: AuthenticatedUser,
294    query: web::Query<serde_json::Value>,
295) -> impl Responder {
296    let limit = query.get("limit").and_then(|v| v.as_i64()).unwrap_or(5);
297
298    match data
299        .achievement_use_cases
300        .get_recent_achievements(auth.user_id, limit)
301        .await
302    {
303        Ok(achievements) => HttpResponse::Ok().json(achievements),
304        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
305    }
306}
307
308// ============================================================================
309// CHALLENGE HANDLERS
310// ============================================================================
311
312/// Create a new challenge (admin only)
313///
314/// POST /challenges
315///
316/// # Request Body
317/// - organization_id: UUID
318/// - building_id: `Option<UUID>` (null = organization-wide)
319/// - challenge_type: ChallengeType (Individual, Team, Building)
320/// - title: String (3-100 chars)
321/// - description: String (10-1000 chars)
322/// - icon: String
323/// - start_date: DateTime<Utc>
324/// - end_date: DateTime<Utc>
325/// - target_metric: String (e.g., "bookings_created")
326/// - target_value: i32
327/// - reward_points: i32 (0-10000)
328///
329/// # Responses
330/// - 201 Created: Challenge created successfully (Draft status)
331/// - 400 Bad Request: Validation error
332#[post("/challenges")]
333pub async fn create_challenge(
334    data: web::Data<AppState>,
335    auth: AuthenticatedUser,
336    request: web::Json<CreateChallengeDto>,
337) -> impl Responder {
338    if auth.role != "superadmin" && auth.role != "syndic" {
339        return HttpResponse::Forbidden().json(serde_json::json!({
340            "error": "Only superadmin or syndic can create challenges"
341        }));
342    }
343    match data
344        .challenge_use_cases
345        .create_challenge(request.into_inner())
346        .await
347    {
348        Ok(challenge) => HttpResponse::Created().json(challenge),
349        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
350    }
351}
352
353/// Get challenge by ID
354///
355/// GET /challenges/:id
356///
357/// # Responses
358/// - 200 OK: Challenge details
359/// - 404 Not Found: Challenge not found
360#[get("/challenges/{id}")]
361pub async fn get_challenge(
362    data: web::Data<AppState>,
363    _auth: AuthenticatedUser,
364    id: web::Path<Uuid>,
365) -> impl Responder {
366    match data
367        .challenge_use_cases
368        .get_challenge(id.into_inner())
369        .await
370    {
371        Ok(challenge) => HttpResponse::Ok().json(challenge),
372        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
373    }
374}
375
376/// List all challenges for an organization
377///
378/// GET /organizations/:organization_id/challenges
379///
380/// # Responses
381/// - 200 OK: List of challenges
382#[get("/organizations/{organization_id}/challenges")]
383pub async fn list_challenges(
384    data: web::Data<AppState>,
385    _auth: AuthenticatedUser,
386    organization_id: web::Path<Uuid>,
387) -> impl Responder {
388    match data
389        .challenge_use_cases
390        .list_challenges(organization_id.into_inner())
391        .await
392    {
393        Ok(challenges) => HttpResponse::Ok().json(challenges),
394        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
395    }
396}
397
398/// List challenges by status
399///
400/// GET /organizations/:organization_id/challenges/status/:status
401///
402/// # Responses
403/// - 200 OK: List of challenges with specified status
404#[get("/organizations/{organization_id}/challenges/status/{status}")]
405pub async fn list_challenges_by_status(
406    data: web::Data<AppState>,
407    _auth: AuthenticatedUser,
408    path: web::Path<(Uuid, String)>,
409) -> impl Responder {
410    let (organization_id, status_str) = path.into_inner();
411
412    // Parse status
413    let status: ChallengeStatus = match serde_json::from_str(&format!("\"{}\"", status_str)) {
414        Ok(s) => s,
415        Err(_) => {
416            return HttpResponse::BadRequest().json(serde_json::json!({"error": "Invalid status"}))
417        }
418    };
419
420    match data
421        .challenge_use_cases
422        .list_challenges_by_status(organization_id, status)
423        .await
424    {
425        Ok(challenges) => HttpResponse::Ok().json(challenges),
426        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
427    }
428}
429
430/// List challenges for a building
431///
432/// GET /buildings/:building_id/challenges
433///
434/// # Responses
435/// - 200 OK: List of building challenges
436#[get("/buildings/{building_id}/challenges")]
437pub async fn list_building_challenges(
438    data: web::Data<AppState>,
439    _auth: AuthenticatedUser,
440    building_id: web::Path<Uuid>,
441) -> impl Responder {
442    match data
443        .challenge_use_cases
444        .list_building_challenges(building_id.into_inner())
445        .await
446    {
447        Ok(challenges) => HttpResponse::Ok().json(challenges),
448        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
449    }
450}
451
452/// List active challenges (Active status + date range)
453///
454/// GET /organizations/:organization_id/challenges/active
455///
456/// # Responses
457/// - 200 OK: List of currently active challenges
458#[get("/organizations/{organization_id}/challenges/active")]
459pub async fn list_active_challenges(
460    data: web::Data<AppState>,
461    _auth: AuthenticatedUser,
462    organization_id: web::Path<Uuid>,
463) -> impl Responder {
464    match data
465        .challenge_use_cases
466        .list_active_challenges(organization_id.into_inner())
467        .await
468    {
469        Ok(challenges) => HttpResponse::Ok().json(challenges),
470        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
471    }
472}
473
474/// Update challenge (Draft only, admin only)
475///
476/// PUT /challenges/:id
477///
478/// # Responses
479/// - 200 OK: Challenge updated successfully
480/// - 400 Bad Request: Validation error or not Draft status
481/// - 404 Not Found: Challenge not found
482#[put("/challenges/{id}")]
483pub async fn update_challenge(
484    data: web::Data<AppState>,
485    auth: AuthenticatedUser,
486    id: web::Path<Uuid>,
487    request: web::Json<UpdateChallengeDto>,
488) -> impl Responder {
489    if auth.role != "superadmin" && auth.role != "syndic" {
490        return HttpResponse::Forbidden().json(serde_json::json!({
491            "error": "Only superadmin or syndic can update challenges"
492        }));
493    }
494    match data
495        .challenge_use_cases
496        .update_challenge(id.into_inner(), request.into_inner())
497        .await
498    {
499        Ok(challenge) => HttpResponse::Ok().json(challenge),
500        Err(e) if e.contains("not found") => {
501            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
502        }
503        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
504    }
505}
506
507/// Activate challenge (Draft → Active, admin only)
508///
509/// PUT /challenges/:id/activate
510///
511/// # Responses
512/// - 200 OK: Challenge activated successfully
513/// - 400 Bad Request: Invalid state transition
514/// - 404 Not Found: Challenge not found
515#[put("/challenges/{id}/activate")]
516pub async fn activate_challenge(
517    data: web::Data<AppState>,
518    auth: AuthenticatedUser,
519    id: web::Path<Uuid>,
520) -> impl Responder {
521    if auth.role != "superadmin" && auth.role != "syndic" {
522        return HttpResponse::Forbidden().json(serde_json::json!({
523            "error": "Only superadmin or syndic can activate challenges"
524        }));
525    }
526    match data
527        .challenge_use_cases
528        .activate_challenge(id.into_inner())
529        .await
530    {
531        Ok(challenge) => HttpResponse::Ok().json(challenge),
532        Err(e) if e.contains("not found") => {
533            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
534        }
535        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
536    }
537}
538
539/// Complete challenge (Active → Completed, admin only)
540///
541/// PUT /challenges/:id/complete
542///
543/// # Responses
544/// - 200 OK: Challenge completed successfully
545/// - 400 Bad Request: Invalid state transition
546/// - 404 Not Found: Challenge not found
547#[put("/challenges/{id}/complete")]
548pub async fn complete_challenge(
549    data: web::Data<AppState>,
550    auth: AuthenticatedUser,
551    id: web::Path<Uuid>,
552) -> impl Responder {
553    if auth.role != "superadmin" && auth.role != "syndic" {
554        return HttpResponse::Forbidden().json(serde_json::json!({
555            "error": "Only superadmin or syndic can complete challenges"
556        }));
557    }
558    match data
559        .challenge_use_cases
560        .complete_challenge(id.into_inner())
561        .await
562    {
563        Ok(challenge) => HttpResponse::Ok().json(challenge),
564        Err(e) if e.contains("not found") => {
565            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
566        }
567        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
568    }
569}
570
571/// Cancel challenge (Draft/Active → Cancelled, admin only)
572///
573/// PUT /challenges/:id/cancel
574///
575/// # Responses
576/// - 200 OK: Challenge cancelled successfully
577/// - 400 Bad Request: Invalid state transition
578/// - 404 Not Found: Challenge not found
579#[put("/challenges/{id}/cancel")]
580pub async fn cancel_challenge(
581    data: web::Data<AppState>,
582    auth: AuthenticatedUser,
583    id: web::Path<Uuid>,
584) -> impl Responder {
585    if auth.role != "superadmin" && auth.role != "syndic" {
586        return HttpResponse::Forbidden().json(serde_json::json!({
587            "error": "Only superadmin or syndic can cancel challenges"
588        }));
589    }
590    match data
591        .challenge_use_cases
592        .cancel_challenge(id.into_inner())
593        .await
594    {
595        Ok(challenge) => HttpResponse::Ok().json(challenge),
596        Err(e) if e.contains("not found") => {
597            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
598        }
599        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
600    }
601}
602
603/// Delete challenge (admin only)
604///
605/// DELETE /challenges/:id
606///
607/// # Responses
608/// - 204 No Content: Challenge deleted successfully
609/// - 404 Not Found: Challenge not found
610#[delete("/challenges/{id}")]
611pub async fn delete_challenge(
612    data: web::Data<AppState>,
613    auth: AuthenticatedUser,
614    id: web::Path<Uuid>,
615) -> impl Responder {
616    if auth.role != "superadmin" && auth.role != "syndic" {
617        return HttpResponse::Forbidden().json(serde_json::json!({
618            "error": "Only superadmin or syndic can delete challenges"
619        }));
620    }
621    match data
622        .challenge_use_cases
623        .delete_challenge(id.into_inner())
624        .await
625    {
626        Ok(_) => HttpResponse::NoContent().finish(),
627        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
628    }
629}
630
631// ============================================================================
632// CHALLENGE PROGRESS HANDLERS
633// ============================================================================
634
635/// Get user progress for a challenge
636///
637/// GET /challenges/:challenge_id/progress
638///
639/// # Responses
640/// - 200 OK: User progress details
641/// - 404 Not Found: Progress or challenge not found
642#[get("/challenges/{challenge_id}/progress")]
643pub async fn get_challenge_progress(
644    data: web::Data<AppState>,
645    auth: AuthenticatedUser,
646    challenge_id: web::Path<Uuid>,
647) -> impl Responder {
648    match data
649        .challenge_use_cases
650        .get_challenge_progress(auth.user_id, challenge_id.into_inner())
651        .await
652    {
653        Ok(progress) => HttpResponse::Ok().json(progress),
654        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
655    }
656}
657
658/// List all progress for a challenge
659///
660/// GET /challenges/:challenge_id/all-progress
661///
662/// # Responses
663/// - 200 OK: List of all user progress for challenge
664#[get("/challenges/{challenge_id}/all-progress")]
665pub async fn list_challenge_progress(
666    data: web::Data<AppState>,
667    _auth: AuthenticatedUser,
668    challenge_id: web::Path<Uuid>,
669) -> impl Responder {
670    match data
671        .challenge_use_cases
672        .list_challenge_progress(challenge_id.into_inner())
673        .await
674    {
675        Ok(progress_list) => HttpResponse::Ok().json(progress_list),
676        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
677    }
678}
679
680/// List active challenges for current user with progress
681///
682/// GET /users/challenges/active
683///
684/// # Responses
685/// - 200 OK: List of active challenges with user progress
686#[get("/users/challenges/active")]
687pub async fn list_user_active_challenges(
688    data: web::Data<AppState>,
689    auth: AuthenticatedUser,
690) -> impl Responder {
691    match data
692        .challenge_use_cases
693        .list_user_active_progress(auth.user_id)
694        .await
695    {
696        Ok(progress_list) => HttpResponse::Ok().json(progress_list),
697        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
698    }
699}
700
701#[derive(Debug, Deserialize)]
702pub struct IncrementProgressRequest {
703    pub increment: i32,
704}
705
706/// Increment user progress for a challenge
707///
708/// POST /challenges/:challenge_id/progress/increment
709///
710/// # Request Body
711/// - increment: i32 (value to add to current progress)
712///
713/// # Responses
714/// - 200 OK: Progress incremented successfully (auto-completes if target reached)
715/// - 400 Bad Request: Validation error
716/// - 404 Not Found: Challenge not found
717#[post("/challenges/{challenge_id}/progress/increment")]
718pub async fn increment_progress(
719    data: web::Data<AppState>,
720    auth: AuthenticatedUser,
721    challenge_id: web::Path<Uuid>,
722    request: web::Json<IncrementProgressRequest>,
723) -> impl Responder {
724    match data
725        .challenge_use_cases
726        .increment_progress(auth.user_id, challenge_id.into_inner(), request.increment)
727        .await
728    {
729        Ok(progress) => HttpResponse::Ok().json(progress),
730        Err(e) if e.contains("not found") => {
731            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
732        }
733        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
734    }
735}
736
737// ============================================================================
738// GAMIFICATION STATS HANDLERS
739// ============================================================================
740
741/// Get comprehensive gamification stats for current user
742///
743/// GET /organizations/:organization_id/gamification/stats
744///
745/// # Responses
746/// - 200 OK: User gamification statistics
747#[get("/organizations/{organization_id}/gamification/stats")]
748pub async fn get_gamification_user_stats(
749    data: web::Data<AppState>,
750    auth: AuthenticatedUser,
751    organization_id: web::Path<Uuid>,
752) -> impl Responder {
753    match data
754        .gamification_stats_use_cases
755        .get_user_stats(auth.user_id, organization_id.into_inner())
756        .await
757    {
758        Ok(stats) => HttpResponse::Ok().json(stats),
759        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
760    }
761}
762
763/// Get leaderboard for organization or building
764///
765/// GET /organizations/:organization_id/gamification/leaderboard?building_id=<uuid>&limit=10
766///
767/// # Query Parameters
768/// - building_id: `Option<UUID>` (filter by building)
769/// - limit: `i64` (default: 10)
770///
771/// # Responses
772/// - 200 OK: Leaderboard with top users
773#[get("/organizations/{organization_id}/gamification/leaderboard")]
774pub async fn get_gamification_leaderboard(
775    data: web::Data<AppState>,
776    _auth: AuthenticatedUser,
777    organization_id: web::Path<Uuid>,
778    query: web::Query<serde_json::Value>,
779) -> impl Responder {
780    let building_id = query
781        .get("building_id")
782        .and_then(|v| v.as_str())
783        .and_then(|s| Uuid::parse_str(s).ok());
784
785    let limit = query.get("limit").and_then(|v| v.as_i64()).unwrap_or(10);
786
787    match data
788        .gamification_stats_use_cases
789        .get_leaderboard(organization_id.into_inner(), building_id, limit)
790        .await
791    {
792        Ok(leaderboard) => HttpResponse::Ok().json(leaderboard),
793        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
794    }
795}