Files
zero2prod/src/routes/admin/dashboard.rs
2025-10-03 18:30:09 +02:00

117 lines
3.5 KiB
Rust

use crate::routes::{POSTS_PER_PAGE, SUBS_PER_PAGE, get_posts_count, get_posts_page, get_users};
use crate::{
authentication::AuthenticatedUser,
routes::{AppError, get_max_page, get_subs, get_total_subs},
startup::AppState,
templates::DashboardTemplate,
};
use anyhow::Context;
use askama::Template;
use axum::{
Extension,
extract::State,
response::{Html, IntoResponse, Response},
};
use sqlx::PgPool;
use uuid::Uuid;
pub struct DashboardStats {
pub subscribers: i64,
pub posts: i64,
pub notifications_sent: i64,
pub open_rate: f64,
}
impl DashboardStats {
pub fn formatted_rate(&self) -> String {
format!("{:.1}%", self.open_rate)
}
}
pub async fn admin_dashboard(
State(AppState {
connection_pool, ..
}): State<AppState>,
Extension(user): Extension<AuthenticatedUser>,
) -> Result<Response, AppError> {
let stats = get_stats(&connection_pool).await?;
let idempotency_key_1 = Uuid::new_v4().to_string();
let idempotency_key_2 = Uuid::new_v4().to_string();
let current_page = 1;
let subscribers = get_subs(&connection_pool, current_page)
.await
.context("Could not fetch subscribers from database.")
.map_err(AppError::unexpected_message)?;
let count = get_total_subs(&connection_pool)
.await
.context("Could not fetch total subscribers count from the database.")?;
let max_page = get_max_page(count, SUBS_PER_PAGE);
let users = get_users(&connection_pool)
.await
.context("Could not fetch users")?;
let posts = get_posts_page(&connection_pool, 1)
.await
.context("Could not fetch posts.")?;
let posts_current_page = 1;
let count = get_posts_count(&connection_pool)
.await
.context("Could not fetch posts count.")?;
let posts_max_page = get_max_page(count, POSTS_PER_PAGE);
let template = DashboardTemplate {
user,
idempotency_key_1,
idempotency_key_2,
stats,
subscribers,
current_page,
max_page,
users,
posts,
posts_current_page,
posts_max_page,
};
Ok(Html(template.render().unwrap()).into_response())
}
#[tracing::instrument("Computing dashboard stats", skip_all)]
async fn get_stats(connection_pool: &PgPool) -> Result<DashboardStats, anyhow::Error> {
let subscribers =
sqlx::query_scalar!("SELECT count(*) FROM subscriptions WHERE status = 'confirmed'")
.fetch_one(connection_pool)
.await
.context("Failed to fetch subscribers count.")?
.unwrap_or(0);
let posts = sqlx::query_scalar!("SELECT count(*) FROM posts")
.fetch_one(connection_pool)
.await
.context("Failed to fetch posts count.")?
.unwrap_or(0);
let notifications_sent = sqlx::query_scalar!("SELECT count(*) FROM notifications_delivered")
.fetch_one(connection_pool)
.await
.context("Failed to fetch notifications sent count.")?
.unwrap_or(0);
let opened =
sqlx::query_scalar!("SELECT count(*) FROM notifications_delivered WHERE opened = TRUE")
.fetch_one(connection_pool)
.await
.context("Failed to fetch notifications sent count.")?
.unwrap_or(0);
let open_rate = if notifications_sent == 0 {
0.0
} else {
(opened as f64) / (notifications_sent as f64) * 100.0
};
Ok(DashboardStats {
subscribers,
posts,
notifications_sent,
open_rate,
})
}