1use tracing::warn;
8
9use crate::api::ApiClient;
10use crate::cache::CacheManager;
11
12#[derive(Debug, Clone)]
14pub struct CacheProgress {
15 pub current: u32,
16 pub total: u32,
17 pub description: String,
18}
19
20pub async fn cache_all_for_offline(
28 api: &ApiClient,
29 cache: &CacheManager,
30 org_guid: &str,
31 user_id: i64,
32 on_progress: impl Fn(CacheProgress),
33) -> anyhow::Result<String> {
34 let base = super::refresh::refresh_base_data(api, cache, org_guid, user_id, &on_progress).await;
36 let successes = base.successes;
37 let _errors = base.errors;
38
39 let youth_ids: Vec<i64> = base.youth
40 .as_ref()
41 .map(|list| list.iter().filter_map(|y| y.user_id).collect())
42 .unwrap_or_default();
43
44 let events = base.events;
45
46 let event_ids: Vec<i64> = events
48 .as_ref()
49 .map(|list| list.iter().map(|e| e.id).collect())
50 .unwrap_or_default();
51
52 if !event_ids.is_empty() {
53 use futures::future::join_all;
54 use std::collections::HashMap;
55
56 on_progress(CacheProgress {
57 current: 0,
58 total: event_ids.len() as u32,
59 description: "Caching event RSVP data...".into(),
60 });
61
62 const MAX_CONCURRENT_EVENTS: usize = 10;
64 let mut rsvp_map: HashMap<i64, Vec<crate::models::event::InvitedUser>> = HashMap::new();
65
66 let mut completed = 0u32;
67 for chunk in event_ids.chunks(MAX_CONCURRENT_EVENTS) {
68 let futures: Vec<_> = chunk
69 .iter()
70 .map(|&eid| {
71 let api = api.clone();
72 async move {
73 let detail = api.fetch_event_detail(eid).await.ok();
74 (eid, detail)
75 }
76 })
77 .collect();
78
79 let results = join_all(futures).await;
80 for (eid, detail) in results {
81 if let Some(detail) = detail {
82 rsvp_map.insert(eid, detail.invited_users);
83 }
84 completed += 1;
85 }
86
87 on_progress(CacheProgress {
88 current: completed,
89 total: event_ids.len() as u32,
90 description: "Caching event RSVP data...".into(),
91 });
92 }
93
94 let mut cached_events = events.unwrap_or_default();
96 for ev in &mut cached_events {
97 if let Some(users) = rsvp_map.remove(&ev.id) {
98 ev.invited_users = users;
99 }
100 }
101 if let Err(e) = cache.save_events(&cached_events) {
102 warn!("Failed to save events to cache: {e}");
103 }
104 }
105
106 if !youth_ids.is_empty() {
114 use futures::future::join_all;
115
116 let youth_total = youth_ids.len() as u32;
117 const MAX_CONCURRENT_YOUTH: usize = 5;
118
119 let mut completed = 0u32;
120 for chunk in youth_ids.chunks(MAX_CONCURRENT_YOUTH) {
121 let futures: Vec<_> = chunk
122 .iter()
123 .map(|&uid| {
124 let api = api.clone();
125 async move {
126 let (ranks_result, badges_result) = futures::future::join(
128 api.fetch_youth_ranks(uid),
129 api.fetch_youth_merit_badges(uid),
130 )
131 .await;
132
133 let ranks = ranks_result.unwrap_or_default();
134 let badges = badges_result.unwrap_or_default();
135
136 let rank_req_futures: Vec<_> = ranks
138 .iter()
139 .map(|r| {
140 let api = api.clone();
141 let rank_id = r.rank_id;
142 async move {
143 let reqs = api.fetch_rank_requirements(uid, rank_id).await.ok();
144 (rank_id, reqs)
145 }
146 })
147 .collect();
148
149 let badge_req_futures: Vec<_> = badges
150 .iter()
151 .map(|b| {
152 let api = api.clone();
153 let badge_id = b.id;
154 async move {
155 let reqs = api.fetch_badge_requirements_only(uid, badge_id).await.ok();
156 (badge_id, reqs)
157 }
158 })
159 .collect();
160
161 let (rank_reqs, badge_reqs) = futures::future::join(
162 join_all(rank_req_futures),
163 join_all(badge_req_futures),
164 )
165 .await;
166
167 (uid, ranks, badges, rank_reqs, badge_reqs)
168 }
169 })
170 .collect();
171
172 let results = join_all(futures).await;
173
174 for (uid, ranks, badges, rank_reqs, badge_reqs) in results {
176 if let Err(e) = cache.save_youth_ranks(uid, &ranks) {
177 warn!("Failed to save ranks for user {uid}: {e}");
178 }
179 if let Err(e) = cache.save_youth_merit_badges(uid, &badges) {
180 warn!("Failed to save badges for user {uid}: {e}");
181 }
182
183 for (rank_id, reqs) in rank_reqs {
184 if let Some(reqs) = reqs {
185 if let Err(e) = cache.save_rank_requirements(uid, rank_id, &reqs) {
186 warn!("Failed to save rank requirements for user {uid}, rank {rank_id}: {e}");
187 }
188 }
189 }
190
191 for (badge_id, reqs) in badge_reqs {
192 if let Some((reqs, version)) = reqs {
193 if let Err(e) = cache.save_badge_requirements(uid, badge_id, &reqs, &version) {
194 warn!("Failed to save badge requirements for user {uid}, badge {badge_id}: {e}");
195 }
196 }
197 }
198
199 completed += 1;
200 on_progress(CacheProgress {
201 current: completed,
202 total: youth_total,
203 description: format!("Caching scout advancement ({}/{})...", completed, youth_total),
204 });
205 }
206 }
207 }
208
209 on_progress(CacheProgress {
211 current: 1,
212 total: 1,
213 description: "Verifying cache...".into(),
214 });
215
216 let gaps = verify_cache(cache, &youth_ids);
217
218 on_progress(CacheProgress {
219 current: 1,
220 total: 1,
221 description: if gaps.is_empty() {
222 "Caching complete".into()
223 } else {
224 format!("Caching complete ({} gaps)", gaps.len())
225 },
226 });
227
228 if gaps.is_empty() {
229 Ok(format!("Cached all {} data sources + requirements + RSVP — verified complete", successes))
230 } else {
231 let summary = gaps.join("; ");
232 Ok(format!(
233 "Caching complete with gaps: {}",
234 summary
235 ))
236 }
237}
238
239fn verify_cache(cache: &CacheManager, youth_ids: &[i64]) -> Vec<String> {
242 let mut gaps = Vec::new();
243
244 macro_rules! check {
246 ($label:expr, $load:expr) => {
247 match $load {
248 Ok(Some(_)) => {}
249 Ok(None) => gaps.push(format!("{}: missing", $label)),
250 Err(_) => gaps.push(format!("{}: unreadable", $label)),
251 }
252 };
253 }
254
255 check!("Scouts", cache.load_youth());
256 check!("Adults", cache.load_adults());
257 check!("Events", cache.load_events());
258 check!("Patrols", cache.load_patrols());
259 check!("Unit info", cache.load_unit_info());
260 check!("Key 3", cache.load_key3());
261 check!("Commissioners", cache.load_commissioners());
262 check!("Org profile", cache.load_org_profile());
263 check!("Parents", cache.load_parents());
264 check!("Advancement", cache.load_advancement_dashboard());
265
266 let mut missing_ranks = 0u32;
268 let mut missing_badges = 0u32;
269 let mut missing_rank_reqs = 0u32;
270 let mut missing_badge_reqs = 0u32;
271
272 for &uid in youth_ids {
273 let ranks = match cache.load_youth_ranks(uid) {
275 Ok(Some(cached)) => cached.data,
276 _ => {
277 missing_ranks += 1;
278 continue;
279 }
280 };
281
282 for rank in &ranks {
284 if cache.load_rank_requirements(uid, rank.rank_id).ok().flatten().is_none() {
285 missing_rank_reqs += 1;
286 }
287 }
288
289 let badges = match cache.load_youth_merit_badges(uid) {
291 Ok(Some(cached)) => cached.data,
292 _ => {
293 missing_badges += 1;
294 continue;
295 }
296 };
297
298 for badge in &badges {
300 if cache.load_badge_requirements(uid, badge.id).ok().flatten().is_none() {
301 missing_badge_reqs += 1;
302 }
303 }
304 }
305
306 if missing_ranks > 0 {
307 gaps.push(format!("{} scouts missing rank data", missing_ranks));
308 }
309 if missing_badges > 0 {
310 gaps.push(format!("{} scouts missing badge data", missing_badges));
311 }
312 if missing_rank_reqs > 0 {
313 gaps.push(format!("{} rank requirements missing", missing_rank_reqs));
314 }
315 if missing_badge_reqs > 0 {
316 gaps.push(format!("{} badge requirements missing", missing_badge_reqs));
317 }
318
319 gaps
320}