lariv-rs 0.1.0

Compile-time plugin web application framework built on Axum, SeaORM, Maud, and HTMX
Documentation
use axum::{
    extract::Path,
    http::{StatusCode, header},
    response::{IntoResponse, Redirect, Response},
};
use chrono::Utc;
use sea_orm::{
    ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter, QueryOrder,
};

use crate::{
    components::{SharedChromeFolder, SlotCtx, SwapKey},
    html_form::HtmlFormBody,
    http::Cap,
    plugins::users::{middleware::RequireAuth, state::AuthContext},
    web::{
        Htmx, html_built_page_or_app_layout, html_built_page_with_slots, respond_edit_modal_done,
    },
};

use crate::plugins::crm::{
    entities::lead_update::{self, Entity as LeadUpdateEntity},
    forms::{LeadUpdateForm, LeadUpdateQuickForm},
    handlers::ModalNameQuery,
    keys::{LEAD_UPDATE_SAVED_EVENT, LeadUpdateDeleteModalKey, LeadUpdateEditModalKey},
    routes::LeadDetailRouteTag,
    scope::{
        find_lead_scoped, find_lead_update_scoped, lead_display_name, scope_superuser,
        user_display_label, user_exists,
    },
    state::CrmState,
    templates::{
        ConfirmDeletePage, LeadUpdateDetailPage, LeadUpdateEditModalPage, LeadUpdateItem,
        LeadUpdatesPanel,
    },
};

use axum::extract::Query;

fn lead_url(lead_id: i64) -> String {
    LeadDetailRouteTag::new(lead_id).url()
}

pub(crate) async fn load_updates_panel(
    db: &sea_orm::DatabaseConnection,
    auth: &AuthContext,
    lead_id: i64,
    can_edit: bool,
) -> LeadUpdatesPanel {
    let mut query = LeadUpdateEntity::find().filter(lead_update::Column::LeadId.eq(lead_id));
    query = scope_superuser(query, auth);
    let models = query
        .order_by_desc(lead_update::Column::Datetime)
        .order_by_desc(lead_update::Column::Id)
        .all(db)
        .await
        .unwrap_or_default();
    let items = models
        .into_iter()
        .map(|u| LeadUpdateItem {
            id: u.id,
            datetime: auth.format_datetime(u.datetime).into_string(),
            description: u.description,
        })
        .collect();
    LeadUpdatesPanel {
        lead_id,
        items,
        can_edit,
        default_datetime: auth.datetime_local_input(Utc::now()).into_string(),
    }
}

pub async fn detail(
    Cap(state): Cap<CrmState>,
    Cap(chrome): Cap<SharedChromeFolder>,
    RequireAuth(ctx): RequireAuth,
    htmx: Htmx,
    Path(id): Path<i64>,
) -> Response {
    let Some(update) = find_lead_update_scoped(&state.db, id, &ctx).await else {
        return Redirect::to("/crm/leads").into_response();
    };
    let Some(lead) = find_lead_scoped(&state.db, update.lead_id, &ctx).await else {
        return Redirect::to("/crm/leads").into_response();
    };
    let page = LeadUpdateDetailPage {
        id: update.id,
        lead_id: update.lead_id,
        display_name: lead_display_name(&state.db, &lead).await,
        created_by: user_display_label(&state.db, update.created_by_id).await,
        datetime: ctx.format_datetime(update.datetime).into_string(),
        description: update.description,
        can_edit: ctx.user.is_superuser,
    };
    html_built_page_or_app_layout(&page, &htmx, &chrome, &SlotCtx::from_auth(&ctx)).into_response()
}

fn created_by_id(form: &LeadUpdateForm, fallback: i64) -> i64 {
    if form.created_by_id <= 0 {
        fallback
    } else {
        form.created_by_id
    }
}

pub async fn add_post(
    Cap(state): Cap<CrmState>,
    RequireAuth(ctx): RequireAuth,
    htmx: Htmx,
    Path(lead_id): Path<i64>,
    HtmlFormBody(form): HtmlFormBody<LeadUpdateQuickForm>,
) -> Response {
    if !ctx.user.is_superuser {
        return Redirect::to(&lead_url(lead_id)).into_response();
    }
    if find_lead_scoped(&state.db, lead_id, &ctx).await.is_none() {
        return Redirect::to("/crm/leads").into_response();
    }
    let description = form.description.trim();
    if description.is_empty() {
        return StatusCode::UNPROCESSABLE_ENTITY.into_response();
    }
    let Some(datetime) = ctx.parse_datetime_local_input(&form.datetime) else {
        return StatusCode::UNPROCESSABLE_ENTITY.into_response();
    };
    let now = Utc::now();
    let model = lead_update::ActiveModel {
        id: Default::default(),
        created_at: Set(Some(now)),
        updated_at: Set(Some(now)),
        lead_id: Set(lead_id),
        created_by_id: Set(ctx.user.id),
        datetime: Set(datetime),
        description: Set(description.to_string()),
    };
    if model.insert(&state.db).await.is_err() {
        return StatusCode::INTERNAL_SERVER_ERROR.into_response();
    }
    if !htmx.request {
        return Redirect::to(&lead_url(lead_id)).into_response();
    }
    let panel = load_updates_panel(&state.db, &ctx, lead_id, true).await;
    let body = panel.render_list().into_string();
    let trigger = format!(r#"{{"{LEAD_UPDATE_SAVED_EVENT}":true}}"#);
    Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
        .header("HX-Trigger", trigger)
        .body(body.into())
        .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
}

pub async fn edit_get(
    Cap(state): Cap<CrmState>,
    Cap(chrome): Cap<SharedChromeFolder>,
    RequireAuth(ctx): RequireAuth,
    Path(id): Path<i64>,
    Query(q): Query<ModalNameQuery>,
) -> Response {
    if !ctx.user.is_superuser {
        return Redirect::to("/crm/leads").into_response();
    }
    let Some(update) = find_lead_update_scoped(&state.db, id, &ctx).await else {
        return Redirect::to("/crm/leads").into_response();
    };
    let page = LeadUpdateEditModalPage {
        id: update.id,
        form_name: q.form_name(),
        created_by_id: update.created_by_id,
        created_by_display: user_display_label(&state.db, update.created_by_id).await,
        datetime: ctx.datetime_local_input(update.datetime).into_string(),
        description: update.description,
        error: String::new(),
    };
    html_built_page_with_slots(&page, &chrome, &SlotCtx::from_auth(&ctx)).into_response()
}

async fn edit_modal_error(
    db: &sea_orm::DatabaseConnection,
    chrome: &SharedChromeFolder,
    ctx: &AuthContext,
    id: i64,
    q: &ModalNameQuery,
    form: &LeadUpdateForm,
    created_by_id: i64,
    error: &str,
) -> Response {
    let page = LeadUpdateEditModalPage {
        id,
        form_name: q.form_name(),
        created_by_id,
        created_by_display: user_display_label(db, created_by_id).await,
        datetime: form.datetime.clone(),
        description: form.description.clone(),
        error: error.to_string(),
    };
    html_built_page_with_slots(&page, chrome, &SlotCtx::from_auth(ctx)).into_response()
}

pub async fn edit_post(
    Cap(state): Cap<CrmState>,
    Cap(chrome): Cap<SharedChromeFolder>,
    RequireAuth(ctx): RequireAuth,
    htmx: Htmx,
    Path(id): Path<i64>,
    Query(q): Query<ModalNameQuery>,
    HtmlFormBody(form): HtmlFormBody<LeadUpdateForm>,
) -> Response {
    if !ctx.user.is_superuser {
        return Redirect::to("/crm/leads").into_response();
    }
    let Some(existing) = find_lead_update_scoped(&state.db, id, &ctx).await else {
        return Redirect::to("/crm/leads").into_response();
    };
    let created_by_id = created_by_id(&form, existing.created_by_id);
    if !user_exists(&state.db, created_by_id).await {
        return edit_modal_error(
            &state.db,
            &chrome,
            &ctx,
            id,
            &q,
            &form,
            created_by_id,
            "created by is required",
        )
        .await;
    }
    if form.description.trim().is_empty() {
        return edit_modal_error(
            &state.db,
            &chrome,
            &ctx,
            id,
            &q,
            &form,
            created_by_id,
            "description is required",
        )
        .await;
    }
    let Some(datetime) = ctx.parse_datetime_local_input(&form.datetime) else {
        return edit_modal_error(
            &state.db,
            &chrome,
            &ctx,
            id,
            &q,
            &form,
            created_by_id,
            "invalid date & time",
        )
        .await;
    };
    let lead_id = existing.lead_id;
    let now = Utc::now();
    let mut am: lead_update::ActiveModel = existing.into();
    am.updated_at = Set(Some(now));
    am.created_by_id = Set(created_by_id);
    am.datetime = Set(datetime);
    am.description = Set(form.description.trim().to_string());
    match am.update(&state.db).await {
        Ok(_) => respond_edit_modal_done::<LeadUpdateEditModalKey>(&htmx, &lead_url(lead_id)),
        Err(e) => {
            edit_modal_error(
                &state.db,
                &chrome,
                &ctx,
                id,
                &q,
                &form,
                created_by_id,
                &e.to_string(),
            )
            .await
        }
    }
}

pub async fn delete_get(
    Cap(chrome): Cap<SharedChromeFolder>,
    RequireAuth(ctx): RequireAuth,
    Query(q): Query<ModalNameQuery>,
    Path(id): Path<i64>,
) -> maud::Markup {
    let page = ConfirmDeletePage {
        modal_uid: LeadUpdateDeleteModalKey::ID.to_string(),
        message: "Are you sure you want to delete this update?".into(),
        form_name: q
            .name
            .clone()
            .unwrap_or_else(|| "p_crm.LeadUpdateDeleteForm".into()),
        id,
        error: String::new(),
    };
    html_built_page_with_slots(&page, &chrome, &SlotCtx::from_auth(&ctx))
}

pub async fn delete_post(
    Cap(state): Cap<CrmState>,
    Cap(chrome): Cap<SharedChromeFolder>,
    RequireAuth(ctx): RequireAuth,
    htmx: Htmx,
    Path(id): Path<i64>,
) -> Response {
    if !ctx.user.is_superuser {
        return Redirect::to("/crm/leads").into_response();
    }
    let Some(update) = find_lead_update_scoped(&state.db, id, &ctx).await else {
        return Redirect::to("/crm/leads").into_response();
    };
    let lead_id = update.lead_id;
    match LeadUpdateEntity::delete_by_id(id).exec(&state.db).await {
        Ok(_) => htmx.redirect(&lead_url(lead_id)),
        Err(e) => {
            tracing::error!(error = %e, id, "failed to delete lead update");
            let page = ConfirmDeletePage {
                modal_uid: LeadUpdateDeleteModalKey::ID.to_string(),
                message: "Are you sure you want to delete this update?".into(),
                form_name: "p_crm.LeadUpdateDeleteForm".into(),
                id,
                error: e.to_string(),
            };
            html_built_page_with_slots(&page, &chrome, &SlotCtx::from_auth(&ctx)).into_response()
        }
    }
}