use axum::{ extract::{Path, Query, State}, response::Html, routing::get, Router, }; use chrono::{DateTime, Utc}; use diesel::prelude::*; use serde::Deserialize; use std::sync::Arc; use uuid::Uuid; use crate::core::bot::get_default_bot; use crate::core::shared::schema::{ attendant_agent_status, attendant_queues, attendant_sessions, }; use crate::core::shared::state::AppState; #[derive(Debug, Deserialize, Default)] pub struct SessionListQuery { pub status: Option, pub queue_id: Option, pub agent_id: Option, pub limit: Option, } pub async fn sessions_table( State(state): State>, Query(query): Query, ) -> Html { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (bot_id, _) = get_default_bot(&mut conn); let mut db_query = attendant_sessions::table .filter(attendant_sessions::bot_id.eq(bot_id)) .into_boxed(); if let Some(status) = query.status { db_query = db_query.filter(attendant_sessions::status.eq(status)); } if let Some(queue_id) = query.queue_id { db_query = db_query.filter(attendant_sessions::queue_id.eq(queue_id)); } if let Some(agent_id) = query.agent_id { db_query = db_query.filter(attendant_sessions::agent_id.eq(agent_id)); } db_query = db_query.order(attendant_sessions::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(( attendant_sessions::id, attendant_sessions::session_number, attendant_sessions::customer_name, attendant_sessions::customer_email, attendant_sessions::channel, attendant_sessions::status, attendant_sessions::priority, attendant_sessions::subject, attendant_sessions::created_at, )) .load::<(Uuid, String, Option, Option, String, String, i32, Option, DateTime)>(&mut conn) .ok() }) .await .ok() .flatten(); match result { Some(sessions) if !sessions.is_empty() => { let rows: String = sessions .iter() .map(|(id, number, name, email, channel, status, priority, subject, created)| { let customer = name.clone().unwrap_or_else(|| email.clone().unwrap_or_else(|| "Unknown".to_string())); let subj = subject.clone().unwrap_or_else(|| "No subject".to_string()); let status_class = match status.as_str() { "waiting" => "status-waiting", "active" => "status-active", "ended" => "status-ended", _ => "status-default", }; let priority_badge = match priority { p if *p >= 2 => r##"High"##, p if *p == 1 => r##"Medium"##, _ => r##"Low"##, }; let time = created.format("%Y-%m-%d %H:%M").to_string(); format!( r##" {} {} {} {} {} {} {} "##, id, id, number, customer, channel, channel, subj, priority_badge, status_class, status, time ) }) .collect(); Html(format!( r##"{}
Session # Customer Channel Subject Priority Status Created
"##, rows )) } _ => Html( r##"

No sessions found

"## .to_string(), ), } } pub async fn sessions_count(State(state): State>) -> Html { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (bot_id, _) = get_default_bot(&mut conn); attendant_sessions::table .filter(attendant_sessions::bot_id.eq(bot_id)) .count() .get_result::(&mut conn) .ok() }) .await .ok() .flatten(); Html(result.unwrap_or(0).to_string()) } pub async fn waiting_count(State(state): State>) -> Html { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (bot_id, _) = get_default_bot(&mut conn); attendant_sessions::table .filter(attendant_sessions::bot_id.eq(bot_id)) .filter(attendant_sessions::status.eq("waiting")) .count() .get_result::(&mut conn) .ok() }) .await .ok() .flatten(); Html(result.unwrap_or(0).to_string()) } pub async fn active_count(State(state): State>) -> Html { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (bot_id, _) = get_default_bot(&mut conn); attendant_sessions::table .filter(attendant_sessions::bot_id.eq(bot_id)) .filter(attendant_sessions::status.eq("active")) .count() .get_result::(&mut conn) .ok() }) .await .ok() .flatten(); Html(result.unwrap_or(0).to_string()) } pub async fn agents_online_count(State(state): State>) -> Html { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (bot_id, _) = get_default_bot(&mut conn); attendant_agent_status::table .filter(attendant_agent_status::bot_id.eq(bot_id)) .filter(attendant_agent_status::status.eq("online")) .count() .get_result::(&mut conn) .ok() }) .await .ok() .flatten(); Html(result.unwrap_or(0).to_string()) } pub async fn session_detail( State(state): State>, Path(id): Path, ) -> Html { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; attendant_sessions::table .find(id) .select(( attendant_sessions::id, attendant_sessions::session_number, attendant_sessions::customer_name, attendant_sessions::customer_email, attendant_sessions::customer_phone, attendant_sessions::channel, attendant_sessions::status, attendant_sessions::priority, attendant_sessions::subject, attendant_sessions::initial_message, attendant_sessions::notes, attendant_sessions::created_at, attendant_sessions::assigned_at, attendant_sessions::ended_at, )) .first::<( Uuid, String, Option, Option, Option, String, String, i32, Option, Option, Option, DateTime, Option>, Option>, )>(&mut conn) .ok() }) .await .ok() .flatten(); match result { Some((id, number, name, email, phone, channel, status, priority, subject, message, notes, created, assigned, ended)) => { let customer_name = name.unwrap_or_else(|| "Unknown".to_string()); let customer_email = email.unwrap_or_else(|| "-".to_string()); let customer_phone = phone.unwrap_or_else(|| "-".to_string()); let subject_text = subject.unwrap_or_else(|| "No subject".to_string()); let message_text = message.unwrap_or_else(|| "-".to_string()); let notes_text = notes.unwrap_or_else(|| "-".to_string()); let created_time = created.format("%Y-%m-%d %H:%M:%S").to_string(); let assigned_time = assigned.map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string()).unwrap_or_else(|| "-".to_string()); let ended_time = ended.map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string()).unwrap_or_else(|| "-".to_string()); let priority_text = match priority { p if p >= 2 => "High", p if p == 1 => "Medium", _ => "Low", }; Html(format!( r##"

Session #{}

{}

Customer Information

{}
{}
{}
{}

Session Details

{}
{}
{}
{}
{}

Initial Message

{}

Notes

{}

"##, number, status, status, customer_name, customer_email, customer_phone, channel, channel, subject_text, priority_text, created_time, assigned_time, ended_time, message_text, notes_text, id, id, id )) } None => Html( r##"

Session not found

"## .to_string(), ), } } pub async fn queues_list(State(state): State>) -> Html { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (bot_id, _) = get_default_bot(&mut conn); attendant_queues::table .filter(attendant_queues::bot_id.eq(bot_id)) .filter(attendant_queues::is_active.eq(true)) .order(attendant_queues::priority.desc()) .select(( attendant_queues::id, attendant_queues::name, attendant_queues::description, attendant_queues::priority, )) .load::<(Uuid, String, Option, i32)>(&mut conn) .ok() }) .await .ok() .flatten(); match result { Some(queues) if !queues.is_empty() => { let items: String = queues .iter() .map(|(id, name, desc, priority)| { let description = desc.clone().unwrap_or_default(); format!( r##"
{} Priority: {}

{}

"##, id, name, priority, description, id ) }) .collect(); Html(format!(r##"
{}
"##, items)) } _ => Html( r##"

No queues configured

"## .to_string(), ), } } pub async fn queue_stats( State(state): State>, Path(queue_id): Path, ) -> Html { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let waiting: i64 = attendant_sessions::table .filter(attendant_sessions::queue_id.eq(queue_id)) .filter(attendant_sessions::status.eq("waiting")) .count() .get_result(&mut conn) .unwrap_or(0); let active: i64 = attendant_sessions::table .filter(attendant_sessions::queue_id.eq(queue_id)) .filter(attendant_sessions::status.eq("active")) .count() .get_result(&mut conn) .unwrap_or(0); Some((waiting, active)) }) .await .ok() .flatten(); match result { Some((waiting, active)) => Html(format!( r##"Waiting: {} Active: {}"##, waiting, active )), None => Html(r##"-"##.to_string()), } } pub async fn agent_status_list(State(state): State>) -> Html { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (bot_id, _) = get_default_bot(&mut conn); attendant_agent_status::table .filter(attendant_agent_status::bot_id.eq(bot_id)) .order(attendant_agent_status::status.asc()) .select(( attendant_agent_status::id, attendant_agent_status::agent_id, attendant_agent_status::status, attendant_agent_status::current_sessions, attendant_agent_status::max_sessions, attendant_agent_status::last_activity_at, )) .load::<(Uuid, Uuid, String, i32, i32, DateTime)>(&mut conn) .ok() }) .await .ok() .flatten(); match result { Some(agents) if !agents.is_empty() => { let items: String = agents .iter() .map(|(id, agent_id, status, current, max, last_activity)| { let status_class = match status.as_str() { "online" => "status-online", "busy" => "status-busy", "break" => "status-break", _ => "status-offline", }; let last_time = last_activity.format("%H:%M").to_string(); format!( r##"
Agent {} {}
{}/{} sessions Last: {}
"##, id, status_class, &agent_id.to_string()[..8], status, current, max, last_time ) }) .collect(); Html(format!(r##"
{}
"##, items)) } _ => Html( r##"

No agents found

"## .to_string(), ), } } pub async fn dashboard_stats(State(state): State>) -> Html { let pool = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut conn = pool.get().ok()?; let (bot_id, _) = get_default_bot(&mut conn); let today = Utc::now().date_naive(); let today_start = today.and_hms_opt(0, 0, 0)?; let total_today: i64 = attendant_sessions::table .filter(attendant_sessions::bot_id.eq(bot_id)) .filter(attendant_sessions::created_at.ge(today_start)) .count() .get_result(&mut conn) .unwrap_or(0); let waiting: i64 = attendant_sessions::table .filter(attendant_sessions::bot_id.eq(bot_id)) .filter(attendant_sessions::status.eq("waiting")) .count() .get_result(&mut conn) .unwrap_or(0); let active: i64 = attendant_sessions::table .filter(attendant_sessions::bot_id.eq(bot_id)) .filter(attendant_sessions::status.eq("active")) .count() .get_result(&mut conn) .unwrap_or(0); let agents_online: i64 = attendant_agent_status::table .filter(attendant_agent_status::bot_id.eq(bot_id)) .filter(attendant_agent_status::status.eq("online")) .count() .get_result(&mut conn) .unwrap_or(0); Some((total_today, waiting, active, agents_online)) }) .await .ok() .flatten(); match result { Some((total, waiting, active, agents)) => Html(format!( r##"
{} Sessions Today
{} Waiting
{} Active
{} Agents Online
"##, total, waiting, active, agents )), None => Html( r##"
-
"## .to_string(), ), } } pub fn configure_attendant_ui_routes() -> Router> { Router::new() .route("/api/ui/attendant/sessions", get(sessions_table)) .route("/api/ui/attendant/sessions/count", get(sessions_count)) .route("/api/ui/attendant/sessions/waiting", get(waiting_count)) .route("/api/ui/attendant/sessions/active", get(active_count)) .route("/api/ui/attendant/sessions/:id", get(session_detail)) .route("/api/ui/attendant/queues", get(queues_list)) .route("/api/ui/attendant/queues/:id/stats", get(queue_stats)) .route("/api/ui/attendant/agents", get(agent_status_list)) .route("/api/ui/attendant/agents/online", get(agents_online_count)) .route("/api/ui/attendant/dashboard", get(dashboard_stats)) }