117 lines
3.5 KiB
Rust
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,
|
|
})
|
|
}
|