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.
This commit is contained in:
parent
4ed05f3f19
commit
67c9b0e0cc
7 changed files with 389 additions and 0 deletions
112
src/billing/billing_ui.rs
Normal file
112
src/billing/billing_ui.rs
Normal file
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchQuery {
|
||||
pub q: Option<String>,
|
||||
}
|
||||
|
||||
pub fn configure_billing_routes() -> Router<Arc<AppState>> {
|
||||
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<Arc<AppState>>,
|
||||
Query(_query): Query<StatusQuery>,
|
||||
) -> impl IntoResponse {
|
||||
Html(
|
||||
r#"<tr class="empty-row">
|
||||
<td colspan="7" class="empty-state">
|
||||
<div class="empty-icon">📄</div>
|
||||
<p>No invoices yet</p>
|
||||
<p class="empty-hint">Create your first invoice to get started</p>
|
||||
</td>
|
||||
</tr>"#
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_payments(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Query(_query): Query<StatusQuery>,
|
||||
) -> impl IntoResponse {
|
||||
Html(
|
||||
r#"<tr class="empty-row">
|
||||
<td colspan="6" class="empty-state">
|
||||
<div class="empty-icon">💳</div>
|
||||
<p>No payments recorded</p>
|
||||
<p class="empty-hint">Payments will appear here when invoices are paid</p>
|
||||
</td>
|
||||
</tr>"#
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_quotes(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Query(_query): Query<StatusQuery>,
|
||||
) -> impl IntoResponse {
|
||||
Html(
|
||||
r#"<tr class="empty-row">
|
||||
<td colspan="6" class="empty-state">
|
||||
<div class="empty-icon">📝</div>
|
||||
<p>No quotes yet</p>
|
||||
<p class="empty-hint">Create quotes for your prospects</p>
|
||||
</td>
|
||||
</tr>"#
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_stats_pending(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
Html("$0".to_string())
|
||||
}
|
||||
|
||||
async fn handle_revenue_month(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
Html("$0".to_string())
|
||||
}
|
||||
|
||||
async fn handle_paid_month(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
Html("$0".to_string())
|
||||
}
|
||||
|
||||
async fn handle_overdue(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
Html("$0".to_string())
|
||||
}
|
||||
|
||||
async fn handle_billing_search(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Query(query): Query<SearchQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let q = query.q.unwrap_or_default();
|
||||
if q.is_empty() {
|
||||
return Html(String::new());
|
||||
}
|
||||
Html(format!(
|
||||
r#"<div class="search-results-empty">
|
||||
<p>No results for "{}"</p>
|
||||
</div>"#,
|
||||
q
|
||||
))
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
154
src/contacts/crm_ui.rs
Normal file
154
src/contacts/crm_ui.rs
Normal file
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchQuery {
|
||||
pub q: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CountResponse {
|
||||
pub count: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct StatsResponse {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
pub fn configure_crm_routes() -> Router<Arc<AppState>> {
|
||||
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<Arc<AppState>>,
|
||||
Query(query): Query<StageQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let _stage = query.stage.unwrap_or_else(|| "all".to_string());
|
||||
Html("0".to_string())
|
||||
}
|
||||
|
||||
async fn handle_crm_pipeline(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Query(query): Query<StageQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let stage = query.stage.unwrap_or_else(|| "lead".to_string());
|
||||
Html(format!(
|
||||
r#"<div class="pipeline-empty">
|
||||
<p>No {} items yet</p>
|
||||
</div>"#,
|
||||
stage
|
||||
))
|
||||
}
|
||||
|
||||
async fn handle_crm_leads(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
Html(r#"<tr class="empty-row">
|
||||
<td colspan="7" class="empty-state">
|
||||
<div class="empty-icon">📋</div>
|
||||
<p>No leads yet</p>
|
||||
<p class="empty-hint">Create your first lead to get started</p>
|
||||
</td>
|
||||
</tr>"#.to_string())
|
||||
}
|
||||
|
||||
async fn handle_crm_opportunities(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
Html(r#"<tr class="empty-row">
|
||||
<td colspan="7" class="empty-state">
|
||||
<div class="empty-icon">💼</div>
|
||||
<p>No opportunities yet</p>
|
||||
<p class="empty-hint">Qualify leads to create opportunities</p>
|
||||
</td>
|
||||
</tr>"#.to_string())
|
||||
}
|
||||
|
||||
async fn handle_crm_contacts(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
Html(r#"<tr class="empty-row">
|
||||
<td colspan="6" class="empty-state">
|
||||
<div class="empty-icon">👥</div>
|
||||
<p>No contacts yet</p>
|
||||
<p class="empty-hint">Add contacts to your CRM</p>
|
||||
</td>
|
||||
</tr>"#.to_string())
|
||||
}
|
||||
|
||||
async fn handle_crm_accounts(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
Html(r#"<tr class="empty-row">
|
||||
<td colspan="6" class="empty-state">
|
||||
<div class="empty-icon">🏢</div>
|
||||
<p>No accounts yet</p>
|
||||
<p class="empty-hint">Add company accounts to your CRM</p>
|
||||
</td>
|
||||
</tr>"#.to_string())
|
||||
}
|
||||
|
||||
async fn handle_crm_search(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Query(query): Query<SearchQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let q = query.q.unwrap_or_default();
|
||||
if q.is_empty() {
|
||||
return Html(String::new());
|
||||
}
|
||||
Html(format!(
|
||||
r#"<div class="search-results-empty">
|
||||
<p>No results for "{}"</p>
|
||||
</div>"#,
|
||||
q
|
||||
))
|
||||
}
|
||||
|
||||
async fn handle_conversion_rate(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
Html("0%".to_string())
|
||||
}
|
||||
|
||||
async fn handle_pipeline_value(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
Html("$0".to_string())
|
||||
}
|
||||
|
||||
async fn handle_avg_deal(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
Html("$0".to_string())
|
||||
}
|
||||
|
||||
async fn handle_won_month(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
Html("0".to_string())
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod calendar_integration;
|
||||
pub mod crm_ui;
|
||||
pub mod external_sync;
|
||||
pub mod tasks_integration;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
{
|
||||
|
|
|
|||
117
src/products/mod.rs
Normal file
117
src/products/mod.rs
Normal file
|
|
@ -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<String>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchQuery {
|
||||
pub q: Option<String>,
|
||||
}
|
||||
|
||||
pub fn configure_products_routes() -> Router<Arc<AppState>> {
|
||||
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<Arc<AppState>>,
|
||||
Query(_query): Query<ProductQuery>,
|
||||
) -> impl IntoResponse {
|
||||
Html(
|
||||
r#"<div class="products-empty">
|
||||
<div class="empty-icon">📦</div>
|
||||
<p>No products yet</p>
|
||||
<p class="empty-hint">Add your first product to get started</p>
|
||||
</div>"#
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_products_services(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Query(_query): Query<ProductQuery>,
|
||||
) -> impl IntoResponse {
|
||||
Html(
|
||||
r#"<tr class="empty-row">
|
||||
<td colspan="6" class="empty-state">
|
||||
<div class="empty-icon">🔧</div>
|
||||
<p>No services yet</p>
|
||||
<p class="empty-hint">Add services to your catalog</p>
|
||||
</td>
|
||||
</tr>"#
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_products_pricelists(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Query(_query): Query<ProductQuery>,
|
||||
) -> impl IntoResponse {
|
||||
Html(
|
||||
r#"<tr class="empty-row">
|
||||
<td colspan="5" class="empty-state">
|
||||
<div class="empty-icon">💰</div>
|
||||
<p>No price lists yet</p>
|
||||
<p class="empty-hint">Create price lists for different customer segments</p>
|
||||
</td>
|
||||
</tr>"#
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_total_products(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
Html("0".to_string())
|
||||
}
|
||||
|
||||
async fn handle_total_services(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
Html("0".to_string())
|
||||
}
|
||||
|
||||
async fn handle_total_pricelists(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
Html("0".to_string())
|
||||
}
|
||||
|
||||
async fn handle_active_products(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
Html("0".to_string())
|
||||
}
|
||||
|
||||
async fn handle_products_search(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Query(query): Query<SearchQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let q = query.q.unwrap_or_default();
|
||||
if q.is_empty() {
|
||||
return Html(String::new());
|
||||
}
|
||||
Html(format!(
|
||||
r#"<div class="search-results-empty">
|
||||
<p>No results for "{}"</p>
|
||||
</div>"#,
|
||||
q
|
||||
))
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue