//! Admin waitlist dashboard, filtering, approval, and lottery. use axum::{ extract::{Path, Query, State}, response::{IntoResponse, Response}, Form, }; use serde::Deserialize; use crate::{ auth::AdminUser, db::{self, SelectionMethod, WaitlistEntryId, WaitlistStatus}, error::{AppError, Result}, helpers::get_csrf_token, templates::*, types::*, AppState, }; #[derive(Debug, Deserialize)] pub(super) struct WaitlistFilterQuery { pub status: Option, } /// Render the admin waitlist dashboard with stats and entries. #[tracing::instrument(skip_all, name = "admin::admin_waitlist")] pub(super) async fn admin_waitlist( State(state): State, session: tower_sessions::Session, AdminUser(user): AdminUser, Query(query): Query, ) -> Result { let csrf_token = get_csrf_token(&session).await; let current_filter = query.status.clone().unwrap_or_default(); let db_stats = db::waitlist::get_waitlist_stats(&state.db).await?; let total_creators = db::waitlist::count_active_creators(&state.db).await?; let stats = WaitlistStats { total_pending: db_stats.pending as u32, total_approved: db_stats.approved as u32, total_spam: db_stats.spam as u32, total_creators: total_creators as u32, }; let db_entries = db::waitlist::get_admin_waitlist(&state.db, query.status.as_deref()).await?; let entries: Vec = db_entries.iter().map(AdminWaitlistRow::from).collect(); Ok(AdminWaitlistTemplate { csrf_token, session_user: Some(user), stats, entries, current_filter, admin_active_page: "waitlist", }) } /// Return filtered waitlist entries as an HTMX partial. #[tracing::instrument(skip_all, name = "admin::admin_waitlist_entries")] pub(super) async fn admin_waitlist_entries( State(state): State, AdminUser(_user): AdminUser, Query(query): Query, ) -> Result { let db_entries = db::waitlist::get_admin_waitlist(&state.db, query.status.as_deref()).await?; let entries: Vec = db_entries.iter().map(AdminWaitlistRow::from).collect(); Ok(AdminWaitlistEntriesTemplate { entries }) } /// Approve a waitlist entry and grant creator access to the user. #[tracing::instrument(skip_all, name = "admin::admin_approve")] pub(super) async fn admin_approve( State(state): State, AdminUser(_user): AdminUser, Path(id): Path, ) -> Result { // Get entry to find user_id let entry = db::waitlist::update_waitlist_status(&state.db, id, WaitlistStatus::Approved, Some(SelectionMethod::HandPicked), None).await?; // Grant creator access db::waitlist::grant_creator_access(&state.db, entry.user_id).await?; tracing::info!(entry_id = %id, user_id = %entry.user_id, "admin approved waitlist entry"); // Return updated entries partial let db_entries = db::waitlist::get_admin_waitlist(&state.db, Some("pending")).await?; let entries: Vec = db_entries.iter().map(AdminWaitlistRow::from).collect(); Ok(AdminWaitlistEntriesTemplate { entries }.into_response()) } /// Flag a waitlist entry as spam. #[tracing::instrument(skip_all, name = "admin::admin_spam")] pub(super) async fn admin_spam( State(state): State, AdminUser(_user): AdminUser, Path(id): Path, ) -> Result { db::waitlist::update_waitlist_status(&state.db, id, WaitlistStatus::Spam, None, None).await?; tracing::info!(entry_id = %id, "admin flagged waitlist entry as spam"); // Return updated entries partial let db_entries = db::waitlist::get_admin_waitlist(&state.db, Some("pending")).await?; let entries: Vec = db_entries.iter().map(AdminWaitlistRow::from).collect(); Ok(AdminWaitlistEntriesTemplate { entries }.into_response()) } #[derive(Debug, Deserialize)] pub(super) struct LotteryForm { pub count: i32, } /// Run a creator lottery: create a wave, select random winners, and grant access. #[tracing::instrument(skip_all, name = "admin::admin_lottery")] pub(super) async fn admin_lottery( State(state): State, AdminUser(_user): AdminUser, Form(form): Form, ) -> Result { if form.count < 1 { return Err(AppError::validation("Count must be at least 1".to_string())); } // Use a transaction for atomicity let mut tx = state.db.begin().await?; // Count hand-picks not yet assigned to a wave let hand_picked_count = db::waitlist::count_unassigned_handpicks(&mut *tx).await?; // Get next wave number let wave_number = db::waitlist::get_next_wave_number(&mut *tx).await?; // Count eligible pool let eligible = db::waitlist::get_lottery_eligible_count(&mut *tx).await?; // Create the wave let wave = db::waitlist::create_wave( &mut *tx, wave_number, hand_picked_count as i32, form.count, eligible as i32, None, ).await?; // Assign wave to unassigned hand-picks db::waitlist::assign_wave_to_handpicks(&mut *tx, wave.id).await?; // Run the lottery let winners = db::waitlist::run_lottery(&mut *tx, wave.id, form.count).await?; // Grant creator access to all lottery winners (batch) let winner_ids: Vec<_> = winners.iter().map(|w| w.user_id).collect(); db::waitlist::grant_creator_access_batch(&mut *tx, &winner_ids).await?; tx.commit().await?; tracing::info!( wave_number = wave_number, hand_picked = hand_picked_count, lottery_winners = winners.len(), eligible = eligible, "wave created" ); // Redirect back to admin waitlist Ok(( axum::http::StatusCode::OK, [("HX-Redirect", "/admin/waitlist")], "", ).into_response()) }