sidebar in the admin
This commit is contained in:
@@ -74,3 +74,85 @@ pub fn sidebar_groups(categories: &[categories::Model]) -> Vec<Value> {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Admin category-filter tree for product listings: like [`sidebar_groups`] but
|
||||
/// each node carries its `id` (links filter by `?category=<id>`) and a `count`
|
||||
/// of matching products — the node's own products plus every descendant's, so a
|
||||
/// parent's count covers its whole subtree. `category_ids` is each product's
|
||||
/// `category_id` (`None` = uncategorized), taken over the full unfiltered list.
|
||||
pub fn admin_category_groups(
|
||||
categories: &[categories::Model],
|
||||
category_ids: &[Option<i32>],
|
||||
) -> Vec<Value> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut direct: HashMap<i32, usize> = HashMap::new();
|
||||
for id in category_ids.iter().flatten() {
|
||||
*direct.entry(*id).or_default() += 1;
|
||||
}
|
||||
let subtree_count = |id: i32| -> usize {
|
||||
let mut n = direct.get(&id).copied().unwrap_or(0);
|
||||
for d in crate::models::categories::descendant_ids(categories, id) {
|
||||
n += direct.get(&d).copied().unwrap_or(0);
|
||||
}
|
||||
n
|
||||
};
|
||||
|
||||
let mut top: Vec<&categories::Model> = categories
|
||||
.iter()
|
||||
.filter(|c| c.parent_id.is_none())
|
||||
.collect();
|
||||
top.sort_by(|a, b| a.position.cmp(&b.position).then_with(|| a.name.cmp(&b.name)));
|
||||
|
||||
top.into_iter()
|
||||
.map(|category| {
|
||||
let children: Vec<Value> = crate::models::categories::children_of(categories, category.id)
|
||||
.into_iter()
|
||||
.map(|child| {
|
||||
json!({ "id": child.id, "name": child.name, "count": subtree_count(child.id) })
|
||||
})
|
||||
.collect();
|
||||
json!({
|
||||
"id": category.id,
|
||||
"name": category.name,
|
||||
"count": subtree_count(category.id),
|
||||
"children": children,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Resolve the `?category=` filter param against the category forest into the
|
||||
/// set of `category_id`s a product may have to be shown. Returns:
|
||||
/// - `None` for "all" (no filtering) or an unknown value,
|
||||
/// - `Some(empty set)` for "none" (uncategorized — match products with no
|
||||
/// category; callers treat an empty set as "uncategorized only"),
|
||||
/// - `Some({id} ∪ descendants)` for a numeric category id.
|
||||
pub fn category_filter_ids(
|
||||
categories: &[categories::Model],
|
||||
selected: &str,
|
||||
) -> Option<std::collections::HashSet<i32>> {
|
||||
match selected {
|
||||
"all" => None,
|
||||
"none" => Some(std::collections::HashSet::new()),
|
||||
s => s.parse::<i32>().ok().map(|id| {
|
||||
let mut set = crate::models::categories::descendant_ids(categories, id);
|
||||
set.insert(id);
|
||||
set
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a product with `category_id` passes the filter from
|
||||
/// [`category_filter_ids`]: `None` keeps everything, an empty set keeps only
|
||||
/// uncategorized products, a non-empty set keeps products in those categories.
|
||||
pub fn category_filter_keep(
|
||||
filter: &Option<std::collections::HashSet<i32>>,
|
||||
category_id: Option<i32>,
|
||||
) -> bool {
|
||||
match filter {
|
||||
None => true,
|
||||
Some(set) if set.is_empty() => category_id.is_none(),
|
||||
Some(set) => category_id.is_some_and(|id| set.contains(&id)),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user