lariv-rs 0.1.0

Compile-time plugin web application framework built on Axum, SeaORM, Maud, and HTMX
Documentation
use frunk::Generic;
use maud::{Markup, html};

use crate::{
    components::{
        Crumb, FieldText, FieldTitle, ObjectList, PaginationPage, ShellChrome, SlotCapability,
        SlotRegistrar, SwapKey, TableButtonFilter, TableColumnHeader, TablePagination, TableRow,
        breadcrumbs, column_sort_url, container_column, data_table_list_refresh, detail,
        field_text, field_title, label, page_size_only_filter_form, pagination_pages,
        row_attr_navigate_route, sort_indicator, table_button_filter, table_pagination,
    },
    http::ProvideRequestCaps,
    template::{RenderAppPane, RenderTemplate, TemplateCapability, TemplateOf, TemplateRegistrar},
};

use crate::plugins::finance_accounts::accounting_detail_menu::{
    DetailMenuNavItem, detail_sidebar_menu,
};
use crate::plugins::finance_accounts::routes::JournalEntryDetailRouteTag;
use crate::plugins::finance_accounts::templates::{
    app_scaffold, app_scaffold_with_sidebar, layout_main_with_crumbs,
    layout_with_entity_sidebar_crumbs, layout_with_sidebar_crumbs,
};

use super::keys::CreditNoteTableKey;
use super::routes::{CreditNoteDefaultRouteTag, CreditNoteDetailRouteTag};

fn credit_notes_list_crumbs() -> Markup {
    breadcrumbs(&[Crumb {
        label: "Credit Notes",
        href: None,
    }])
}

fn credit_note_crumbs(label: &str) -> Markup {
    let list_url = CreditNoteDefaultRouteTag.url();
    breadcrumbs(&[
        Crumb {
            label: "Credit Notes",
            href: Some(&list_url),
        },
        Crumb {
            label: label,
            href: None,
        },
    ])
}

fn credit_note_detail_menu(id: i64) -> Markup {
    detail_sidebar_menu(
        format!("Credit note #{id}"),
        &[DetailMenuNavItem {
            title: "Credit Note Detail",
            url: CreditNoteDetailRouteTag::new(id).url(),
            active: true,
        }],
        None,
        html! {},
    )
}

crate::define_register_items! {
    plugin: FinanceCreditnotesTag;
    capability: TemplateCapability;
    trait: TemplateRegistrar;
    method: register_templates;
    wrapper: TemplateOf;
    bounds: [Clone, ProvideRequestCaps, Send, Sync];
    hook: Hook;
    items: [
        CreditNoteListIdx: CreditNoteListPageTag => CreditNoteListPage,
        CreditNoteDetailIdx: CreditNoteDetailPageTag => CreditNoteDetailPage,
    ]
}

crate::define_register_items! {
    plugin: FinanceCreditnotesTag;
    capability: SlotCapability;
    trait: SlotRegistrar;
    method: register_slots;
    bounds: [];
    items: [];
    hook: SlotsHook;
}

fn render_pagination<K: SwapKey>(path_and_query: &str, number: u32, num_pages: u32) -> Markup {
    let owned = pagination_pages(path_and_query, number, num_pages, true);
    let pages: Vec<PaginationPage<'_>> = owned
        .iter()
        .map(|(ellipsis, url, push_url, active, label)| PaginationPage {
            ellipsis: *ellipsis,
            url: url.as_str(),
            push_url: *push_url,
            active: *active,
            label: label.as_str(),
        })
        .collect();
    table_pagination(TablePagination {
        pages: &pages,
        hx_target: K::SELECTOR,
    })
}

#[derive(Clone)]
pub struct CreditNoteRow {
    pub id: i64,
    pub datetime: String,
    pub reason: String,
    pub original_entry_label: String,
    pub reversal_entry_label: String,
}

#[derive(Generic)]
pub struct CreditNoteListPage {
    pub credit_notes: ObjectList<CreditNoteRow>,
    pub sort: String,
    pub path_and_query: String,
    pub page_size: u32,
}

impl CreditNoteListPage {
    pub fn render_table(&self) -> Markup {
        let date_sort = column_sort_url(&self.path_and_query, "Date", &self.sort);
        let reason_sort = column_sort_url(&self.path_and_query, "Reason", &self.sort);
        let date_label = format!("Date{}", sort_indicator(&self.sort, "Date"));
        let reason_label = format!("Reason{}", sort_indicator(&self.sort, "Reason"));
        let headers = [
            TableColumnHeader {
                key: "Date",
                label: &date_label,
                sort_url: Some(&date_sort),
                push_url: true,
            },
            TableColumnHeader {
                key: "Reason",
                label: &reason_label,
                sort_url: Some(&reason_sort),
                push_url: true,
            },
            TableColumnHeader {
                key: "OriginalEntry",
                label: "Original entry",
                sort_url: None,
                push_url: true,
            },
            TableColumnHeader {
                key: "ReversalEntry",
                label: "Reversal entry",
                sort_url: None,
                push_url: true,
            },
        ];
        let rows: Vec<TableRow> = self
            .credit_notes
            .items
            .iter()
            .map(|c| TableRow {
                attrs: row_attr_navigate_route(CreditNoteDetailRouteTag::new(c.id)),
                cells: vec![
                    field_text(FieldText {
                        value: &c.datetime,
                        classes: "",
                    }),
                    field_text(FieldText {
                        value: &c.reason,
                        classes: "",
                    }),
                    field_text(FieldText {
                        value: &c.original_entry_label,
                        classes: "",
                    }),
                    field_text(FieldText {
                        value: &c.reversal_entry_label,
                        classes: "",
                    }),
                ],
            })
            .collect();
        let pagination = render_pagination::<CreditNoteTableKey>(
            &self.path_and_query,
            self.credit_notes.number,
            self.credit_notes.num_pages,
        );
        let actions = table_button_filter(TableButtonFilter {
            panel: page_size_only_filter_form::<CreditNoteTableKey, CreditNoteDefaultRouteTag>(
                self.page_size,
            ),
            ..Default::default()
        });
        data_table_list_refresh::<CreditNoteTableKey>(
            "Credit Notes",
            actions,
            &headers,
            &rows,
            pagination,
            &self.path_and_query,
        )
    }

    fn body(&self) -> Markup {
        self.render_table()
    }
}

impl RenderAppPane for CreditNoteListPage {
    fn render_pane(&self) -> crate::components::AppLayoutHtml {
        layout_with_sidebar_crumbs(
            &self.path_and_query,
            credit_notes_list_crumbs(),
            self.body(),
        )
    }
    fn render_main(&self) -> crate::components::MainContentHtml {
        layout_main_with_crumbs(credit_notes_list_crumbs(), self.body())
    }
}

impl RenderTemplate for CreditNoteListPage {
    fn render(&self, chrome: &ShellChrome) -> Markup {
        app_scaffold(
            "Credit Notes",
            chrome,
            credit_notes_list_crumbs(),
            self.body(),
            &self.path_and_query,
        )
    }
}

#[derive(Generic)]
pub struct CreditNoteDetailPage {
    pub id: i64,
    pub datetime: String,
    pub reason: String,
    pub journal_entry_id: i64,
    pub reversed_journal_entry_id: i64,
}

impl CreditNoteDetailPage {
    fn body(&self) -> Markup {
        html! {
            (detail(html! {
                (container_column("", html! {
                    (field_title(FieldTitle {
                        value: &format!("Credit note #{}", self.id),
                        classes: "",
                    }))
                    (label("Date", field_text(FieldText { value: &self.datetime, classes: "" })))
                    (label("Reason", field_text(FieldText { value: &self.reason, classes: "" })))
                    (label("Original journal entry", html! {
                        a href=(JournalEntryDetailRouteTag::new(self.journal_entry_id).url()) {
                            "Entry #" (self.journal_entry_id)
                        }
                    }))
                    (label("Reversal journal entry", html! {
                        a href=(JournalEntryDetailRouteTag::new(self.reversed_journal_entry_id).url()) {
                            "Entry #" (self.reversed_journal_entry_id)
                        }
                    }))
                }))
            }))
        }
    }

    fn menu(&self) -> Markup {
        credit_note_detail_menu(self.id)
    }
}

impl RenderAppPane for CreditNoteDetailPage {
    fn render_pane(&self) -> crate::components::AppLayoutHtml {
        let label = format!("#{}", self.id);
        let crumbs = credit_note_crumbs(&label);
        layout_with_entity_sidebar_crumbs(self.menu(), crumbs, self.body())
    }
    fn render_main(&self) -> crate::components::MainContentHtml {
        let label = format!("#{}", self.id);
        layout_main_with_crumbs(credit_note_crumbs(&label), self.body())
    }
}

impl RenderTemplate for CreditNoteDetailPage {
    fn render(&self, chrome: &ShellChrome) -> Markup {
        let label = format!("#{}", self.id);
        let crumbs = credit_note_crumbs(&label);
        app_scaffold_with_sidebar("Credit Note", chrome, self.menu(), crumbs, self.body())
    }
}