use axum::{ extract::{Query, State}, response::Html, routing::get, Router, }; use bigdecimal::ToPrimitive; use chrono::{DateTime, Utc}; use diesel::prelude::*; use serde::Deserialize; use std::sync::Arc; use uuid::Uuid; use crate::bot::get_default_bot; use crate::core::shared::schema::{okr_checkins, okr_objectives}; use crate::shared::state::AppState; #[derive(Debug, Deserialize, Default)] pub struct ObjectivesQuery { pub status: Option, pub period: Option, pub owner_id: Option, pub limit: Option, } pub async fn objectives_list( State(state): State>, Query(query): Query, ) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; let mut db_query = okr_objectives::table .filter(okr_objectives::bot_id.eq(bot_id)) .into_boxed(); if let Some(status) = query.status { db_query = db_query.filter(okr_objectives::status.eq(status)); } if let Some(period) = query.period { db_query = db_query.filter(okr_objectives::period.eq(period)); } if let Some(owner_id) = query.owner_id { db_query = db_query.filter(okr_objectives::owner_id.eq(owner_id)); } db_query = db_query.order(okr_objectives::created_at.desc()); if let Some(limit) = query.limit { db_query = db_query.limit(limit); } else { db_query = db_query.limit(50); } db_query .select(( okr_objectives::id, okr_objectives::title, okr_objectives::description, okr_objectives::period, okr_objectives::status, okr_objectives::progress, okr_objectives::visibility, okr_objectives::created_at, )) .load::<(Uuid, String, Option, String, String, bigdecimal::BigDecimal, String, DateTime)>(&mut conn) .ok() }) .await .ok() .flatten(); match result { Some(objectives) if !objectives.is_empty() => { let items: String = objectives .iter() .map(|(id, title, _desc, period, status, progress, visibility, _created)| { let progress_val = progress.to_f32().unwrap_or(0.0); let progress_pct = (progress_val * 100.0) as i32; let status_class = match status.as_str() { "active" => "status-active", "on_track" => "status-on-track", "at_risk" => "status-at-risk", "behind" => "status-behind", "completed" => "status-completed", _ => "status-draft", }; let progress_class = if progress_val >= 0.7 { "progress-good" } else if progress_val >= 0.4 { "progress-medium" } else { "progress-low" }; format!( r##"

{title}

{status}
{period} {visibility}
{progress_pct}%
"## ) }) .collect(); Html(format!(r##"
{items}
"##)) } _ => Html( r##"

No objectives found

"##.to_string(), ), } } pub async fn objectives_count(State(state): State>) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; okr_objectives::table .filter(okr_objectives::bot_id.eq(bot_id)) .count() .get_result::(&mut conn) .ok() }) .await .ok() .flatten(); Html(format!("{}", result.unwrap_or(0))) } pub async fn active_objectives_count(State(state): State>) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; okr_objectives::table .filter(okr_objectives::bot_id.eq(bot_id)) .filter(okr_objectives::status.eq("active")) .count() .get_result::(&mut conn) .ok() }) .await .ok() .flatten(); Html(format!("{}", result.unwrap_or(0))) } pub async fn at_risk_count(State(state): State>) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; okr_objectives::table .filter(okr_objectives::bot_id.eq(bot_id)) .filter(okr_objectives::status.eq("at_risk")) .count() .get_result::(&mut conn) .ok() }) .await .ok() .flatten(); Html(format!("{}", result.unwrap_or(0))) } pub async fn average_progress(State(state): State>) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; let objectives = okr_objectives::table .filter(okr_objectives::bot_id.eq(bot_id)) .filter(okr_objectives::status.ne("draft")) .filter(okr_objectives::status.ne("cancelled")) .select(okr_objectives::progress) .load::(&mut conn) .ok()?; if objectives.is_empty() { return Some(0.0f32); } let sum: f32 = objectives.iter().map(|p| p.to_f32().unwrap_or(0.0)).sum(); Some(sum / objectives.len() as f32) }) .await .ok() .flatten(); let avg = result.unwrap_or(0.0); let pct = (avg * 100.0) as i32; Html(format!("{pct}%")) } pub async fn dashboard_stats(State(state): State>) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; let total: i64 = okr_objectives::table .filter(okr_objectives::bot_id.eq(bot_id)) .count() .get_result(&mut conn) .unwrap_or(0); let active: i64 = okr_objectives::table .filter(okr_objectives::bot_id.eq(bot_id)) .filter(okr_objectives::status.eq("active")) .count() .get_result(&mut conn) .unwrap_or(0); let at_risk: i64 = okr_objectives::table .filter(okr_objectives::bot_id.eq(bot_id)) .filter(okr_objectives::status.eq("at_risk")) .count() .get_result(&mut conn) .unwrap_or(0); let completed: i64 = okr_objectives::table .filter(okr_objectives::bot_id.eq(bot_id)) .filter(okr_objectives::status.eq("completed")) .count() .get_result(&mut conn) .unwrap_or(0); Some((total, active, at_risk, completed)) }) .await .ok() .flatten(); match result { Some((total, active, at_risk, completed)) => Html(format!( r##"
{total} Total Objectives
{active} Active
{at_risk} At Risk
{completed} Completed
"## )), None => Html(r##"
-
"##.to_string()), } } pub async fn new_objective_form() -> Html { Html(r##"
"##.to_string()) } pub async fn recent_checkins(State(state): State>) -> Html { let pool = state.pool.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (_, bot_id) = get_default_bot(&mut conn).ok()?; okr_checkins::table .filter(okr_checkins::bot_id.eq(bot_id)) .order(okr_checkins::created_at.desc()) .limit(10) .select(( okr_checkins::id, okr_checkins::new_value, okr_checkins::note, okr_checkins::confidence, okr_checkins::created_at, )) .load::<(Uuid, bigdecimal::BigDecimal, Option, Option, DateTime)>(&mut conn) .ok() }) .await .ok() .flatten(); match result { Some(checkins) if !checkins.is_empty() => { let items: String = checkins .iter() .map(|(id, value, note, confidence, created)| { let val = value.to_f64().unwrap_or(0.0); let note_text = note.clone().unwrap_or_else(|| "No note".to_string()); let conf = confidence.clone().unwrap_or_else(|| "medium".to_string()); let conf_class = match conf.as_str() { "high" => "confidence-high", "low" => "confidence-low", _ => "confidence-medium", }; let time_str = created.format("%b %d, %H:%M").to_string(); format!( r##"
{val:.2} {conf}

{note_text}

{time_str}
"## ) }) .collect(); Html(format!(r##"
{items}
"##)) } _ => Html(r##"

No recent check-ins

"##.to_string()), } } pub fn configure_goals_ui_routes() -> Router> { Router::new() .route("/api/ui/goals/objectives", get(objectives_list)) .route("/api/ui/goals/objectives/count", get(objectives_count)) .route("/api/ui/goals/objectives/active", get(active_objectives_count)) .route("/api/ui/goals/objectives/at-risk", get(at_risk_count)) .route("/api/ui/goals/dashboard", get(dashboard_stats)) .route("/api/ui/goals/progress", get(average_progress)) .route("/api/ui/goals/checkins/recent", get(recent_checkins)) .route("/api/ui/goals/new-objective", get(new_objective_form)) }