use leptos::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Clone, Debug)]
struct OrdersInProgress {
count: usize,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
struct WeeklyComparison {
last_7_days_total: f64,
previous_7_days_total: f64,
percentage_change: f64,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
struct MonthlyComparison {
last_month_name: String,
last_month_total: f64,
previous_month_total: f64,
percentage_change: f64,
}
#[server]
async fn fetch_in_progress_orders() -> Result<OrdersInProgress, ServerFnError> {
let user = crate::session::get_user().await?;
if user.is_admin != Some(true) {
return Err(ServerFnError::ServerError("Unauthorized".into()));
}
let db = crate::db::db_init().await?;
let mut result = db
.query("SELECT count() as count FROM order WHERE paid = true and done != true GROUP ALL;")
.await?;
let count: Option<usize> = result.take("count")?;
Ok(OrdersInProgress {
count: count.unwrap_or(0),
})
}
#[server]
async fn fetch_weekly_comparison() -> Result<WeeklyComparison, ServerFnError> {
let user = crate::session::get_user().await?;
if user.is_admin != Some(true) {
return Err(ServerFnError::ServerError("Unauthorized".into()));
}
let db = crate::db::db_init().await?;
let mut orders_req = db
.query(
"SELECT created_at, total FROM order WHERE paid = true AND created_at > time::now() - 14d ORDER BY created_at ASC;",
)
.await?;
#[derive(Deserialize, Serialize, Clone, Debug)]
struct OrderData {
created_at: crate::surrealtypes::Datetime,
total: String,
}
let orders: Vec<OrderData> = orders_req.take(0)?;
let now = chrono::Utc::now();
let seven_days_ago = now - chrono::Duration::days(7);
let mut last_7_days_total = 0.0;
let mut previous_7_days_total = 0.0;
for order in orders {
let order_date = chrono::DateTime::parse_from_rfc3339(&order.created_at.to_string())
.map_err(|e| ServerFnError::new(format!("Date parse error: {}", e)))?
.with_timezone(&chrono::Utc);
let total: f64 = order.total.parse().unwrap_or(0.0);
if order_date >= seven_days_ago {
last_7_days_total += total;
} else {
previous_7_days_total += total;
}
}
let percentage_change = if previous_7_days_total > 0.0 {
((last_7_days_total - previous_7_days_total) / previous_7_days_total) * 100.0
} else if last_7_days_total > 0.0 {
100.0
} else {
0.0
};
Ok(WeeklyComparison {
last_7_days_total,
previous_7_days_total,
percentage_change,
})
}
#[server]
async fn fetch_monthly_comparison() -> Result<MonthlyComparison, ServerFnError> {
let user = crate::session::get_user().await?;
if user.is_admin != Some(true) {
return Err(ServerFnError::ServerError("Unauthorized".into()));
}
let db = crate::db::db_init().await?;
let mut orders_req = db
.query("SELECT created_at, total FROM order WHERE paid = true ORDER BY created_at ASC;")
.await?;
#[derive(Deserialize, Serialize, Clone, Debug)]
struct OrderData {
created_at: crate::surrealtypes::Datetime,
total: String,
}
let orders: Vec<OrderData> = orders_req.take(0)?;
let mut monthly_totals: std::collections::HashMap<String, f64> =
std::collections::HashMap::new();
for order in orders {
let month = order.created_at.format("%Y-%m");
let total: f64 = order.total.parse().unwrap_or(0.0);
*monthly_totals.entry(month).or_insert(0.0) += total;
}
let mut months: Vec<(String, f64)> = monthly_totals.into_iter().collect();
months.sort_by(|a, b| a.0.cmp(&b.0));
let now = chrono::Utc::now();
let current_month = now.format("%Y-%m").to_string();
let complete_months: Vec<(String, f64)> = months
.into_iter()
.filter(|(month, _)| month != ¤t_month)
.collect();
let len = complete_months.len();
if len < 2 {
return Ok(MonthlyComparison {
last_month_name: if len == 1 {
format_month_name(&complete_months[0].0)
} else {
"N/A".to_string()
},
last_month_total: if len == 1 { complete_months[0].1 } else { 0.0 },
previous_month_total: 0.0,
percentage_change: 0.0,
});
}
let last_month = &complete_months[len - 1];
let previous_month = &complete_months[len - 2];
let percentage_change = if previous_month.1 > 0.0 {
((last_month.1 - previous_month.1) / previous_month.1) * 100.0
} else if last_month.1 > 0.0 {
100.0
} else {
0.0
};
Ok(MonthlyComparison {
last_month_name: format_month_name(&last_month.0),
last_month_total: last_month.1,
previous_month_total: previous_month.1,
percentage_change,
})
}
fn format_month_name(month_str: &str) -> String {
if let Some(parts) = month_str.split('-').collect::<Vec<_>>().get(0..2) {
let year = parts[0];
let month_num = parts[1];
let month_name = match month_num {
"01" => "Jan",
"02" => "Feb",
"03" => "Mar",
"04" => "Apr",
"05" => "May",
"06" => "Jun",
"07" => "Jul",
"08" => "Aug",
"09" => "Sep",
"10" => "Oct",
"11" => "Nov",
"12" => "Dec",
_ => month_num,
};
format!("{} {}", month_name, year)
} else {
month_str.to_string()
}
}
#[component]
fn WidgetCard(title: String, children: Children) -> impl IntoView {
view! {
<div class="bg-white dark:bg-neutral-900 p-2 md:p-6 md:rounded-lg shadow">
<div class="text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-2">
{title}
</div>
{children()}
</div>
}
}
#[component]
pub fn MetricsWidgets() -> impl IntoView {
let in_progress_resource =
Resource::new(|| (), |_| async move { fetch_in_progress_orders().await });
let weekly_resource = Resource::new(|| (), |_| async move { fetch_weekly_comparison().await });
let monthly_resource =
Resource::new(|| (), |_| async move { fetch_monthly_comparison().await });
view! {
<div class="grid grid-cols-3 md:grid-cols-3 gap-1">
<WidgetCard title="Orders".to_string()>
<Suspense fallback=move || {
view! { <div class="text-2xl font-bold">"..."</div> }
}>
{move || {
in_progress_resource
.get()
.map(|result| match result {
Ok(data) => {
view! {
<div class="md:text-3xl font-bold text-neutral-900 dark:text-neutral-100">
{data.count.to_string()}
</div>
}
.into_any()
}
Err(err) => {
view! {
<div class="text-sm text-red-500">{err.to_string()}</div>
}
.into_any()
}
})
}}
</Suspense>
</WidgetCard>
<WidgetCard title="Last 7 Days".to_string()>
<Suspense fallback=move || {
view! { <div class="text-2xl font-bold">"..."</div> }
}>
{move || {
weekly_resource
.get()
.map(|result| match result {
Ok(data) => {
let is_positive = data.percentage_change >= 0.0;
let color_class = if is_positive {
"text-green-600 dark:text-green-400"
} else {
"text-red-600 dark:text-red-400"
};
let arrow = if is_positive { "\u{2191}" } else { "\u{2193}" };
view! {
<div class="flex items-baseline gap-2">
<div class="md:text-3xl font-bold text-neutral-900 dark:text-neutral-100">
"R " {data.last_7_days_total.round().to_string()}
</div>
</div>
<div class=format!(
"text-sm font-semibold mt-1 {}",
color_class,
)>
{arrow} " "
{format!("{:.1}%", data.percentage_change.abs())}
</div>
}
.into_any()
}
Err(err) => {
view! {
<div class="text-sm text-red-500">{err.to_string()}</div>
}
.into_any()
}
})
}}
</Suspense>
</WidgetCard>
<Suspense fallback=move || {
view! {
<WidgetCard title="Loading...".to_string()>
<div class="text-2xl font-bold">"..."</div>
</WidgetCard>
}
}>
{move || {
monthly_resource
.get()
.map(|result| match result {
Ok(data) => {
let is_positive = data.percentage_change >= 0.0;
let color_class = if is_positive {
"text-green-600 dark:text-green-400"
} else {
"text-red-600 dark:text-red-400"
};
let arrow = if is_positive { "\u{2191}" } else { "\u{2193}" };
view! {
<WidgetCard title=data.last_month_name.clone()>
<div class="flex items-baseline gap-2">
<div class="md:text-3xl font-bold text-neutral-900 dark:text-neutral-100">
"R " {data.last_month_total.round().to_string()}
</div>
</div>
<div class=format!(
"text-sm font-semibold mt-1 {}",
color_class,
)>
{arrow} " "
{format!("{:.1}%", data.percentage_change.abs())}
</div>
</WidgetCard>
}
.into_any()
}
Err(err) => {
view! {
<WidgetCard title="Error".to_string()>
<div class="text-sm text-red-500">{err.to_string()}</div>
</WidgetCard>
}
.into_any()
}
})
}}
</Suspense>
</div>
}
}