From 67c9b0e0cc90f5c975fb72a4ead44f61595791d5 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Mon, 12 Jan 2026 14:35:03 -0300 Subject: [PATCH] feat(api): add CRM, billing, products stub UI routes - Add crm_ui.rs with stub handlers for pipeline, leads, contacts, accounts, stats - Add billing_ui.rs with stub handlers for invoices, payments, quotes, stats - Add products module with stub handlers for items, services, pricelists, stats - Register routes in main.rs These stubs return empty data/HTML to prevent 404 errors in UI. Full CRUD implementation to follow. --- src/billing/billing_ui.rs | 112 +++++++++++++++++++++++++++ src/billing/mod.rs | 1 + src/contacts/crm_ui.rs | 154 ++++++++++++++++++++++++++++++++++++++ src/contacts/mod.rs | 1 + src/lib.rs | 1 + src/main.rs | 3 + src/products/mod.rs | 117 +++++++++++++++++++++++++++++ 7 files changed, 389 insertions(+) create mode 100644 src/billing/billing_ui.rs create mode 100644 src/contacts/crm_ui.rs create mode 100644 src/products/mod.rs diff --git a/src/billing/billing_ui.rs b/src/billing/billing_ui.rs new file mode 100644 index 000000000..799d469c2 --- /dev/null +++ b/src/billing/billing_ui.rs @@ -0,0 +1,112 @@ +use axum::{ + extract::{Query, State}, + response::{Html, IntoResponse}, + routing::get, + Router, +}; +use serde::Deserialize; +use std::sync::Arc; + +use crate::shared::state::AppState; + +#[derive(Debug, Deserialize)] +pub struct StatusQuery { + pub status: Option, +} + +#[derive(Debug, Deserialize)] +pub struct SearchQuery { + pub q: Option, +} + +pub fn configure_billing_routes() -> Router> { + Router::new() + .route("/api/billing/invoices", get(handle_invoices)) + .route("/api/billing/payments", get(handle_payments)) + .route("/api/billing/quotes", get(handle_quotes)) + .route("/api/billing/stats/pending", get(handle_stats_pending)) + .route("/api/billing/stats/revenue-month", get(handle_revenue_month)) + .route("/api/billing/stats/paid-month", get(handle_paid_month)) + .route("/api/billing/stats/overdue", get(handle_overdue)) + .route("/api/billing/search", get(handle_billing_search)) +} + +async fn handle_invoices( + State(_state): State>, + Query(_query): Query, +) -> impl IntoResponse { + Html( + r#" + +
📄
+

No invoices yet

+

Create your first invoice to get started

+ + "# + .to_string(), + ) +} + +async fn handle_payments( + State(_state): State>, + Query(_query): Query, +) -> impl IntoResponse { + Html( + r#" + +
💳
+

No payments recorded

+

Payments will appear here when invoices are paid

+ + "# + .to_string(), + ) +} + +async fn handle_quotes( + State(_state): State>, + Query(_query): Query, +) -> impl IntoResponse { + Html( + r#" + +
📝
+

No quotes yet

+

Create quotes for your prospects

+ + "# + .to_string(), + ) +} + +async fn handle_stats_pending(State(_state): State>) -> impl IntoResponse { + Html("$0".to_string()) +} + +async fn handle_revenue_month(State(_state): State>) -> impl IntoResponse { + Html("$0".to_string()) +} + +async fn handle_paid_month(State(_state): State>) -> impl IntoResponse { + Html("$0".to_string()) +} + +async fn handle_overdue(State(_state): State>) -> impl IntoResponse { + Html("$0".to_string()) +} + +async fn handle_billing_search( + State(_state): State>, + Query(query): Query, +) -> impl IntoResponse { + let q = query.q.unwrap_or_default(); + if q.is_empty() { + return Html(String::new()); + } + Html(format!( + r#"
+

No results for "{}"

+
"#, + q + )) +} diff --git a/src/billing/mod.rs b/src/billing/mod.rs index b1a6dbb24..ac51e759e 100644 --- a/src/billing/mod.rs +++ b/src/billing/mod.rs @@ -6,6 +6,7 @@ use tokio::sync::RwLock; use uuid::Uuid; pub mod alerts; +pub mod billing_ui; pub mod invoice; pub mod lifecycle; pub mod meters; diff --git a/src/contacts/crm_ui.rs b/src/contacts/crm_ui.rs new file mode 100644 index 000000000..df6bc361b --- /dev/null +++ b/src/contacts/crm_ui.rs @@ -0,0 +1,154 @@ +use axum::{ + extract::{Query, State}, + response::{Html, IntoResponse}, + routing::get, + Json, Router, +}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +use crate::shared::state::AppState; + +#[derive(Debug, Deserialize)] +pub struct StageQuery { + pub stage: Option, +} + +#[derive(Debug, Deserialize)] +pub struct SearchQuery { + pub q: Option, +} + +#[derive(Debug, Serialize)] +pub struct CountResponse { + pub count: i64, +} + +#[derive(Debug, Serialize)] +pub struct StatsResponse { + pub value: String, +} + +pub fn configure_crm_routes() -> Router> { + Router::new() + .route("/api/crm/count", get(handle_crm_count)) + .route("/api/crm/pipeline", get(handle_crm_pipeline)) + .route("/api/crm/leads", get(handle_crm_leads)) + .route("/api/crm/opportunities", get(handle_crm_opportunities)) + .route("/api/crm/contacts", get(handle_crm_contacts)) + .route("/api/crm/accounts", get(handle_crm_accounts)) + .route("/api/crm/search", get(handle_crm_search)) + .route("/api/crm/stats/conversion-rate", get(handle_conversion_rate)) + .route("/api/crm/stats/pipeline-value", get(handle_pipeline_value)) + .route("/api/crm/stats/avg-deal", get(handle_avg_deal)) + .route("/api/crm/stats/won-month", get(handle_won_month)) +} + +async fn handle_crm_count( + State(_state): State>, + Query(query): Query, +) -> impl IntoResponse { + let _stage = query.stage.unwrap_or_else(|| "all".to_string()); + Html("0".to_string()) +} + +async fn handle_crm_pipeline( + State(_state): State>, + Query(query): Query, +) -> impl IntoResponse { + let stage = query.stage.unwrap_or_else(|| "lead".to_string()); + Html(format!( + r#"
+

No {} items yet

+
"#, + stage + )) +} + +async fn handle_crm_leads( + State(_state): State>, +) -> impl IntoResponse { + Html(r#" + +
📋
+

No leads yet

+

Create your first lead to get started

+ + "#.to_string()) +} + +async fn handle_crm_opportunities( + State(_state): State>, +) -> impl IntoResponse { + Html(r#" + +
💼
+

No opportunities yet

+

Qualify leads to create opportunities

+ + "#.to_string()) +} + +async fn handle_crm_contacts( + State(_state): State>, +) -> impl IntoResponse { + Html(r#" + +
👥
+

No contacts yet

+

Add contacts to your CRM

+ + "#.to_string()) +} + +async fn handle_crm_accounts( + State(_state): State>, +) -> impl IntoResponse { + Html(r#" + +
🏢
+

No accounts yet

+

Add company accounts to your CRM

+ + "#.to_string()) +} + +async fn handle_crm_search( + State(_state): State>, + Query(query): Query, +) -> impl IntoResponse { + let q = query.q.unwrap_or_default(); + if q.is_empty() { + return Html(String::new()); + } + Html(format!( + r#"
+

No results for "{}"

+
"#, + q + )) +} + +async fn handle_conversion_rate( + State(_state): State>, +) -> impl IntoResponse { + Html("0%".to_string()) +} + +async fn handle_pipeline_value( + State(_state): State>, +) -> impl IntoResponse { + Html("$0".to_string()) +} + +async fn handle_avg_deal( + State(_state): State>, +) -> impl IntoResponse { + Html("$0".to_string()) +} + +async fn handle_won_month( + State(_state): State>, +) -> impl IntoResponse { + Html("0".to_string()) +} diff --git a/src/contacts/mod.rs b/src/contacts/mod.rs index ee6defa7b..226dc329b 100644 --- a/src/contacts/mod.rs +++ b/src/contacts/mod.rs @@ -1,4 +1,5 @@ pub mod calendar_integration; +pub mod crm_ui; pub mod external_sync; pub mod tasks_integration; diff --git a/src/lib.rs b/src/lib.rs index 6cc87cb8c..5bcc81dd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub mod embedded_ui; pub mod maintenance; pub mod multimodal; pub mod player; +pub mod products; pub mod search; pub mod security; diff --git a/src/main.rs b/src/main.rs index 124e09f1c..d73a26ede 100644 --- a/src/main.rs +++ b/src/main.rs @@ -384,6 +384,9 @@ async fn run_axum_server( api_router = api_router.merge(botserver::player::configure_player_routes()); api_router = api_router.merge(botserver::canvas::configure_canvas_routes()); api_router = api_router.merge(botserver::social::configure_social_routes()); + api_router = api_router.merge(botserver::contacts::crm_ui::configure_crm_routes()); + api_router = api_router.merge(botserver::billing::billing_ui::configure_billing_routes()); + api_router = api_router.merge(botserver::products::configure_products_routes()); #[cfg(feature = "whatsapp")] { diff --git a/src/products/mod.rs b/src/products/mod.rs new file mode 100644 index 000000000..dfbc43e4c --- /dev/null +++ b/src/products/mod.rs @@ -0,0 +1,117 @@ +use axum::{ + extract::{Query, State}, + response::{Html, IntoResponse}, + routing::get, + Router, +}; +use serde::Deserialize; +use std::sync::Arc; + +use crate::shared::state::AppState; + +#[derive(Debug, Deserialize)] +pub struct ProductQuery { + pub category: Option, + pub status: Option, +} + +#[derive(Debug, Deserialize)] +pub struct SearchQuery { + pub q: Option, +} + +pub fn configure_products_routes() -> Router> { + Router::new() + .route("/api/products/items", get(handle_products_items)) + .route("/api/products/services", get(handle_products_services)) + .route("/api/products/pricelists", get(handle_products_pricelists)) + .route( + "/api/products/stats/total-products", + get(handle_total_products), + ) + .route( + "/api/products/stats/total-services", + get(handle_total_services), + ) + .route("/api/products/stats/pricelists", get(handle_total_pricelists)) + .route("/api/products/stats/active", get(handle_active_products)) + .route("/api/products/search", get(handle_products_search)) +} + +async fn handle_products_items( + State(_state): State>, + Query(_query): Query, +) -> impl IntoResponse { + Html( + r#"
+
📦
+

No products yet

+

Add your first product to get started

+
"# + .to_string(), + ) +} + +async fn handle_products_services( + State(_state): State>, + Query(_query): Query, +) -> impl IntoResponse { + Html( + r#" + +
🔧
+

No services yet

+

Add services to your catalog

+ + "# + .to_string(), + ) +} + +async fn handle_products_pricelists( + State(_state): State>, + Query(_query): Query, +) -> impl IntoResponse { + Html( + r#" + +
💰
+

No price lists yet

+

Create price lists for different customer segments

+ + "# + .to_string(), + ) +} + +async fn handle_total_products(State(_state): State>) -> impl IntoResponse { + Html("0".to_string()) +} + +async fn handle_total_services(State(_state): State>) -> impl IntoResponse { + Html("0".to_string()) +} + +async fn handle_total_pricelists(State(_state): State>) -> impl IntoResponse { + Html("0".to_string()) +} + +async fn handle_active_products(State(_state): State>) -> impl IntoResponse { + Html("0".to_string()) +} + +async fn handle_products_search( + State(_state): State>, + Query(query): Query, +) -> impl IntoResponse { + let q = query.q.unwrap_or_default(); + if q.is_empty() { + return Html(String::new()); + } + Html(format!( + r#"
+

No results for "{}"

+
"#, + q + )) +}