1use std::collections::HashMap;
7use super::advancement::{RankProgress, MeritBadgeProgress, ScoutRank};
8use super::person::Youth;
9
10#[derive(Debug, Clone)]
16pub struct RankGroupEntry {
17 pub user_id: i64,
18 pub display_name: String,
19 pub rank: Option<RankProgress>,
21}
22
23#[derive(Debug, Clone)]
25pub struct RankGroup {
26 pub rank_name: String,
27 pub rank_order: usize,
28 pub scouts: Vec<RankGroupEntry>,
29}
30
31pub fn group_youth_by_rank(
40 youth: &[Youth],
41 all_ranks: &HashMap<i64, Vec<RankProgress>>,
42) -> Vec<RankGroup> {
43 let mut by_rank: HashMap<String, Vec<RankGroupEntry>> = HashMap::new();
44 let mut crossover: Vec<RankGroupEntry> = Vec::new();
45
46 for y in youth {
47 let uid = match y.user_id {
48 Some(id) => id,
49 None => {
50 crossover.push(RankGroupEntry {
51 user_id: 0,
52 display_name: y.display_name(),
53 rank: None,
54 });
55 continue;
56 }
57 };
58
59 let ranks = match all_ranks.get(&uid) {
60 Some(r) => r,
61 None => {
62 crossover.push(RankGroupEntry {
63 user_id: uid,
64 display_name: y.display_name(),
65 rank: None,
66 });
67 continue;
68 }
69 };
70
71 let current_rank = ranks
73 .iter()
74 .filter(|r| r.is_completed() || r.is_awarded())
75 .max_by_key(|r| r.level);
76
77 if let Some(rank) = current_rank {
78 by_rank
79 .entry(rank.rank_name.clone())
80 .or_default()
81 .push(RankGroupEntry {
82 user_id: uid,
83 display_name: y.display_name(),
84 rank: Some(rank.clone()),
85 });
86 } else {
87 let lowest_rank = ranks
91 .iter()
92 .min_by_key(|r| r.sort_order())
93 .cloned();
94 crossover.push(RankGroupEntry {
95 user_id: uid,
96 display_name: y.display_name(),
97 rank: lowest_rank,
98 });
99 }
100 }
101
102 if !crossover.is_empty() {
103 by_rank.insert(ScoutRank::Unknown.display_name().to_string(), crossover);
104 }
105
106 for scouts in by_rank.values_mut() {
108 scouts.sort_by(|a, b| {
109 match (&a.rank, &b.rank) {
110 (None, None) => a.display_name.cmp(&b.display_name),
111 (None, Some(_)) => std::cmp::Ordering::Greater,
112 (Some(_), None) => std::cmp::Ordering::Less,
113 (Some(ar), Some(br)) => {
114 let status_order = |r: &RankProgress| -> u8 {
115 if r.is_awarded() { 2 } else if r.is_completed() { 1 } else { 0 }
116 };
117 let sa = status_order(ar);
118 let sb = status_order(br);
119 if sa != sb {
120 return sb.cmp(&sa); }
122 if sa == 0 {
123 return a.display_name.cmp(&b.display_name);
125 }
126 let da = ar.date_awarded.as_deref()
128 .or(ar.date_completed.as_deref())
129 .unwrap_or("");
130 let db = br.date_awarded.as_deref()
131 .or(br.date_completed.as_deref())
132 .unwrap_or("");
133 db.cmp(da)
134 }
135 }
136 });
137 }
138
139 let mut groups: Vec<RankGroup> = by_rank
141 .into_iter()
142 .map(|(name, scouts)| {
143 let order = ScoutRank::parse(Some(&name)).order();
144 RankGroup {
145 rank_name: name,
146 rank_order: order,
147 scouts,
148 }
149 })
150 .collect();
151 groups.sort_by_key(|g| g.rank_order);
152 groups
153}
154
155#[derive(Debug, Clone)]
161pub struct BadgeGroupEntry {
162 pub user_id: i64,
163 pub display_name: String,
164 pub badge: MeritBadgeProgress,
165}
166
167#[derive(Debug, Clone)]
169pub struct BadgeGroup {
170 pub badge_name: String,
171 pub is_eagle_required: bool,
172 pub scouts: Vec<BadgeGroupEntry>,
173}
174
175pub fn group_youth_by_badge(
181 youth: &[Youth],
182 all_badges: &HashMap<i64, Vec<MeritBadgeProgress>>,
183) -> Vec<BadgeGroup> {
184 let mut by_badge: HashMap<String, (bool, Vec<BadgeGroupEntry>)> = HashMap::new();
185
186 for y in youth {
187 let uid = match y.user_id {
188 Some(id) => id,
189 None => continue,
190 };
191
192 let badges = match all_badges.get(&uid) {
193 Some(b) => b,
194 None => continue,
195 };
196
197 for badge in badges {
198 if badge.name.is_empty() {
199 continue;
200 }
201 let entry = by_badge
202 .entry(badge.name.clone())
203 .or_insert_with(|| (badge.is_eagle_required.unwrap_or(false), Vec::new()));
204 if badge.is_eagle_required.unwrap_or(false) {
205 entry.0 = true;
206 }
207 entry.1.push(BadgeGroupEntry {
208 user_id: uid,
209 display_name: y.display_name(),
210 badge: badge.clone(),
211 });
212 }
213 }
214
215 for (_, scouts) in by_badge.values_mut() {
217 scouts.sort_by(|a, b| {
218 let status_order = |s: &BadgeGroupEntry| -> u8 {
219 if s.badge.is_awarded() { 2 } else if s.badge.is_completed() { 1 } else { 0 }
220 };
221 let sa = status_order(a);
222 let sb = status_order(b);
223 if sa != sb {
224 return sb.cmp(&sa); }
226 if sa == 0 {
227 let pa = a.badge.percent_completed.unwrap_or(0.0);
229 let pb = b.badge.percent_completed.unwrap_or(0.0);
230 pb.partial_cmp(&pa).unwrap_or(std::cmp::Ordering::Equal)
231 } else {
232 let da = a.badge.awarded_date.as_deref()
234 .or(a.badge.date_completed.as_deref())
235 .unwrap_or("");
236 let db = b.badge.awarded_date.as_deref()
237 .or(b.badge.date_completed.as_deref())
238 .unwrap_or("");
239 db.cmp(da)
240 }
241 });
242 }
243
244 let mut groups: Vec<BadgeGroup> = by_badge
245 .into_iter()
246 .map(|(name, (eagle, scouts))| BadgeGroup {
247 badge_name: name,
248 is_eagle_required: eagle,
249 scouts,
250 })
251 .collect();
252 groups.sort_by(|a, b| a.badge_name.to_lowercase().cmp(&b.badge_name.to_lowercase()));
253 groups
254}
255
256#[derive(Debug, Clone)]
262pub struct BadgeListEntry {
263 pub name: String,
264 pub is_eagle_required: bool,
265 pub count: usize,
266}
267
268#[derive(Debug, Clone)]
270pub struct RankListEntry {
271 pub name: String,
272 pub count: usize,
273}
274
275pub fn badge_list(
279 youth: &[Youth],
280 all_badges: &std::collections::HashMap<i64, Vec<MeritBadgeProgress>>,
281 sort_by_count: bool,
282) -> Vec<BadgeListEntry> {
283 let grouped = group_youth_by_badge(youth, all_badges);
284 let mut result: Vec<BadgeListEntry> = grouped
285 .into_iter()
286 .map(|g| BadgeListEntry {
287 name: g.badge_name,
288 is_eagle_required: g.is_eagle_required,
289 count: g.scouts.len(),
290 })
291 .collect();
292
293 if sort_by_count {
294 result.sort_by(|a, b| {
295 b.count.cmp(&a.count)
296 .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
297 });
298 } else {
299 result.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
300 }
301
302 result
303}
304
305pub fn rank_list(
309 youth: &[Youth],
310 all_ranks: &std::collections::HashMap<i64, Vec<RankProgress>>,
311 sort_by_count: bool,
312) -> Vec<RankListEntry> {
313 let grouped = group_youth_by_rank(youth, all_ranks);
314 let mut result: Vec<RankListEntry> = grouped
315 .into_iter()
316 .map(|g| RankListEntry {
317 name: g.rank_name,
318 count: g.scouts.len(),
319 })
320 .collect();
321
322 if sort_by_count {
323 result.sort_by(|a, b| {
324 b.count.cmp(&a.count)
325 .then_with(|| {
326 ScoutRank::parse(Some(&a.name)).order()
327 .cmp(&ScoutRank::parse(Some(&b.name)).order())
328 })
329 });
330 } else {
331 result.sort_by(|a, b| {
332 ScoutRank::parse(Some(&a.name)).order()
333 .cmp(&ScoutRank::parse(Some(&b.name)).order())
334 });
335 }
336
337 result
338}