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;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub mod alerts;
|
pub mod alerts;
|
||||||
|
pub mod billing_ui;
|
||||||
pub mod invoice;
|
pub mod invoice;
|
||||||
pub mod lifecycle;
|
pub mod lifecycle;
|
||||||
pub mod meters;
|
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 calendar_integration;
|
||||||
|
pub mod crm_ui;
|
||||||
pub mod external_sync;
|
pub mod external_sync;
|
||||||
pub mod tasks_integration;
|
pub mod tasks_integration;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub mod embedded_ui;
|
||||||
pub mod maintenance;
|
pub mod maintenance;
|
||||||
pub mod multimodal;
|
pub mod multimodal;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
|
pub mod products;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod security;
|
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::player::configure_player_routes());
|
||||||
api_router = api_router.merge(botserver::canvas::configure_canvas_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::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")]
|
#[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