1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::io::Write;
10use uuid::Uuid;
11
12use super::dashboard::{TeamDashboard, AnalyticsPeriod, MemberStats, ProviderStats};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum ReportFormat {
22 Pdf,
23 Csv,
24 Json,
25 Html,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum ReportType {
32 TeamAnalytics,
34 MemberActivity,
36 ProviderUsage,
38 SessionSummary,
40 Collaboration,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ReportRequest {
47 pub team_id: Uuid,
49 pub report_type: ReportType,
51 pub format: ReportFormat,
53 pub period: AnalyticsPeriod,
55 pub start_date: Option<DateTime<Utc>>,
57 pub end_date: Option<DateTime<Utc>>,
59 pub include_details: bool,
61 pub requested_by: Uuid,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct Report {
68 pub id: Uuid,
70 pub report_type: ReportType,
72 pub format: ReportFormat,
74 pub team_id: Uuid,
76 pub period: AnalyticsPeriod,
78 pub generated_at: DateTime<Utc>,
80 pub title: String,
82 pub filename: String,
84 pub content: String,
86 pub size_bytes: usize,
88}
89
90pub struct ReportGenerator;
96
97impl ReportGenerator {
98 pub fn new() -> Self {
100 Self
101 }
102
103 pub fn generate(&self, request: &ReportRequest, dashboard: &TeamDashboard) -> Report {
105 let content = match request.format {
106 ReportFormat::Csv => self.generate_csv(request, dashboard),
107 ReportFormat::Json => self.generate_json(request, dashboard),
108 ReportFormat::Html => self.generate_html(request, dashboard),
109 ReportFormat::Pdf => self.generate_pdf_placeholder(request, dashboard),
110 };
111
112 let title = self.get_report_title(request);
113 let filename = self.get_filename(request);
114
115 Report {
116 id: Uuid::new_v4(),
117 report_type: request.report_type,
118 format: request.format,
119 team_id: request.team_id,
120 period: request.period,
121 generated_at: Utc::now(),
122 title,
123 filename,
124 size_bytes: content.len(),
125 content,
126 }
127 }
128
129 fn generate_csv(&self, request: &ReportRequest, dashboard: &TeamDashboard) -> String {
131 let mut csv = String::new();
132
133 match request.report_type {
134 ReportType::TeamAnalytics => {
135 csv.push_str("Team Analytics Report\n\n");
137 csv.push_str("Metric,Value,Change\n");
138 csv.push_str(&format!(
139 "Total Sessions,{},{:.1}%\n",
140 dashboard.overview.total_sessions,
141 dashboard.overview.sessions_change
142 ));
143 csv.push_str(&format!(
144 "Total Messages,{},{:.1}%\n",
145 dashboard.overview.total_messages,
146 dashboard.overview.messages_change
147 ));
148 csv.push_str(&format!(
149 "Total Tokens,{},{:.1}%\n",
150 dashboard.overview.total_tokens,
151 dashboard.overview.tokens_change
152 ));
153 csv.push_str(&format!(
154 "Active Members,{},{:.1}%\n",
155 dashboard.overview.active_members,
156 dashboard.overview.active_members_change
157 ));
158 csv.push_str(&format!(
159 "Avg Sessions/Member,{:.2}\n",
160 dashboard.overview.avg_sessions_per_member
161 ));
162 csv.push_str(&format!(
163 "Avg Messages/Session,{:.2}\n",
164 dashboard.overview.avg_messages_per_session
165 ));
166
167 if request.include_details {
168 csv.push_str("\nProvider Breakdown\n");
170 csv.push_str("Provider,Sessions,Percentage,Messages,Tokens\n");
171 for provider in &dashboard.provider_breakdown {
172 csv.push_str(&format!(
173 "{},{},{:.1}%,{},{}\n",
174 provider.provider,
175 provider.sessions,
176 provider.session_percentage,
177 provider.messages,
178 provider.tokens
179 ));
180 }
181 }
182 }
183 ReportType::MemberActivity => {
184 csv.push_str("Member Activity Report\n\n");
185 csv.push_str("Member,Sessions,Messages,Tokens,Avg Session Length,Activity Score,Last Active\n");
186 for member in &dashboard.member_stats {
187 csv.push_str(&format!(
188 "{},{},{},{},{:.2},{},{}\n",
189 member.display_name,
190 member.sessions,
191 member.messages,
192 member.tokens,
193 member.avg_session_length,
194 member.activity_score,
195 member.last_active.map(|d| d.to_rfc3339()).unwrap_or_default()
196 ));
197 }
198 }
199 ReportType::ProviderUsage => {
200 csv.push_str("Provider Usage Report\n\n");
201 csv.push_str("Provider,Sessions,Percentage,Messages,Tokens\n");
202 for provider in &dashboard.provider_breakdown {
203 csv.push_str(&format!(
204 "{},{},{:.1}%,{},{}\n",
205 provider.provider,
206 provider.sessions,
207 provider.session_percentage,
208 provider.messages,
209 provider.tokens
210 ));
211 }
212 }
213 ReportType::SessionSummary => {
214 csv.push_str("Session Summary Report\n\n");
215 csv.push_str("Metric,Value\n");
216 csv.push_str(&format!(
217 "Average Messages,{:.2}\n",
218 dashboard.session_analytics.avg_messages
219 ));
220 csv.push_str(&format!(
221 "Average Tokens,{:.2}\n",
222 dashboard.session_analytics.avg_tokens
223 ));
224 csv.push_str(&format!(
225 "Short Sessions (1-5 msgs),{}\n",
226 dashboard.session_analytics.length_distribution.short
227 ));
228 csv.push_str(&format!(
229 "Medium Sessions (6-20 msgs),{}\n",
230 dashboard.session_analytics.length_distribution.medium
231 ));
232 csv.push_str(&format!(
233 "Long Sessions (21-50 msgs),{}\n",
234 dashboard.session_analytics.length_distribution.long
235 ));
236 csv.push_str(&format!(
237 "Very Long Sessions (51+ msgs),{}\n",
238 dashboard.session_analytics.length_distribution.very_long
239 ));
240
241 csv.push_str("\nTop Tags\n");
242 csv.push_str("Tag,Count,Percentage\n");
243 for tag in &dashboard.session_analytics.top_tags {
244 csv.push_str(&format!(
245 "{},{},{:.1}%\n",
246 tag.tag, tag.count, tag.percentage
247 ));
248 }
249 }
250 ReportType::Collaboration => {
251 csv.push_str("Collaboration Report\n\n");
252 csv.push_str("Metric,Value\n");
253 csv.push_str(&format!(
254 "Shared Sessions,{}\n",
255 dashboard.collaboration.shared_sessions
256 ));
257 csv.push_str(&format!(
258 "Total Comments,{}\n",
259 dashboard.collaboration.total_comments
260 ));
261 csv.push_str(&format!(
262 "Active Collaborations,{}\n",
263 dashboard.collaboration.active_collaborations
264 ));
265 }
266 }
267
268 csv
269 }
270
271 fn generate_json(&self, request: &ReportRequest, dashboard: &TeamDashboard) -> String {
273 match request.report_type {
274 ReportType::TeamAnalytics => {
275 serde_json::to_string_pretty(dashboard).unwrap_or_default()
276 }
277 ReportType::MemberActivity => {
278 serde_json::to_string_pretty(&dashboard.member_stats).unwrap_or_default()
279 }
280 ReportType::ProviderUsage => {
281 serde_json::to_string_pretty(&dashboard.provider_breakdown).unwrap_or_default()
282 }
283 ReportType::SessionSummary => {
284 serde_json::to_string_pretty(&dashboard.session_analytics).unwrap_or_default()
285 }
286 ReportType::Collaboration => {
287 serde_json::to_string_pretty(&dashboard.collaboration).unwrap_or_default()
288 }
289 }
290 }
291
292 fn generate_html(&self, request: &ReportRequest, dashboard: &TeamDashboard) -> String {
294 let title = self.get_report_title(request);
295 let mut html = String::new();
296
297 html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
298 html.push_str(&format!("<title>{}</title>\n", title));
299 html.push_str("<style>\n");
300 html.push_str("body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; color: #333; }\n");
301 html.push_str("h1 { color: #2563eb; border-bottom: 2px solid #2563eb; padding-bottom: 10px; }\n");
302 html.push_str("h2 { color: #1f2937; margin-top: 30px; }\n");
303 html.push_str("table { border-collapse: collapse; width: 100%; margin: 20px 0; }\n");
304 html.push_str("th, td { border: 1px solid #e5e7eb; padding: 12px; text-align: left; }\n");
305 html.push_str("th { background: #f3f4f6; font-weight: 600; }\n");
306 html.push_str("tr:nth-child(even) { background: #f9fafb; }\n");
307 html.push_str(".metric-card { display: inline-block; background: #f3f4f6; padding: 20px; margin: 10px; border-radius: 8px; min-width: 150px; }\n");
308 html.push_str(".metric-value { font-size: 32px; font-weight: bold; color: #2563eb; }\n");
309 html.push_str(".metric-label { color: #6b7280; margin-top: 5px; }\n");
310 html.push_str(".change-positive { color: #059669; }\n");
311 html.push_str(".change-negative { color: #dc2626; }\n");
312 html.push_str(".footer { margin-top: 40px; color: #9ca3af; font-size: 12px; }\n");
313 html.push_str("</style>\n</head>\n<body>\n");
314
315 html.push_str(&format!("<h1>{}</h1>\n", title));
316 html.push_str(&format!(
317 "<p>Generated: {} | Period: {:?}</p>\n",
318 dashboard.generated_at.format("%Y-%m-%d %H:%M UTC"),
319 dashboard.period
320 ));
321
322 match request.report_type {
323 ReportType::TeamAnalytics => {
324 html.push_str("<h2>Overview</h2>\n<div>\n");
326 html.push_str(&self.metric_card(
327 "Total Sessions",
328 &dashboard.overview.total_sessions.to_string(),
329 dashboard.overview.sessions_change,
330 ));
331 html.push_str(&self.metric_card(
332 "Total Messages",
333 &dashboard.overview.total_messages.to_string(),
334 dashboard.overview.messages_change,
335 ));
336 html.push_str(&self.metric_card(
337 "Active Members",
338 &dashboard.overview.active_members.to_string(),
339 dashboard.overview.active_members_change,
340 ));
341 html.push_str("</div>\n");
342
343 if request.include_details {
344 html.push_str("<h2>Provider Breakdown</h2>\n");
346 html.push_str(
347 "<table>\n<tr><th>Provider</th><th>Sessions</th><th>%</th><th>Messages</th><th>Tokens</th></tr>\n"
348 );
349 for p in &dashboard.provider_breakdown {
350 html.push_str(&format!(
351 "<tr><td>{}</td><td>{}</td><td>{:.1}%</td><td>{}</td><td>{}</td></tr>\n",
352 p.provider, p.sessions, p.session_percentage, p.messages, p.tokens
353 ));
354 }
355 html.push_str("</table>\n");
356 }
357 }
358 ReportType::MemberActivity => {
359 html.push_str("<h2>Member Activity</h2>\n");
360 html.push_str(
361 "<table>\n<tr><th>Member</th><th>Sessions</th><th>Messages</th><th>Tokens</th><th>Avg Length</th><th>Score</th></tr>\n"
362 );
363 for m in &dashboard.member_stats {
364 html.push_str(&format!(
365 "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{:.1}</td><td>{}</td></tr>\n",
366 m.display_name, m.sessions, m.messages, m.tokens, m.avg_session_length, m.activity_score
367 ));
368 }
369 html.push_str("</table>\n");
370 }
371 ReportType::ProviderUsage => {
372 html.push_str("<h2>Provider Usage</h2>\n");
373 html.push_str(
374 "<table>\n<tr><th>Provider</th><th>Sessions</th><th>%</th><th>Messages</th><th>Tokens</th></tr>\n"
375 );
376 for p in &dashboard.provider_breakdown {
377 html.push_str(&format!(
378 "<tr><td>{}</td><td>{}</td><td>{:.1}%</td><td>{}</td><td>{}</td></tr>\n",
379 p.provider, p.sessions, p.session_percentage, p.messages, p.tokens
380 ));
381 }
382 html.push_str("</table>\n");
383 }
384 ReportType::SessionSummary => {
385 html.push_str("<h2>Session Statistics</h2>\n<div>\n");
386 html.push_str(&self.metric_card(
387 "Avg Messages",
388 &format!("{:.1}", dashboard.session_analytics.avg_messages),
389 0.0,
390 ));
391 html.push_str(&self.metric_card(
392 "Avg Tokens",
393 &format!("{:.0}", dashboard.session_analytics.avg_tokens),
394 0.0,
395 ));
396 html.push_str("</div>\n");
397
398 html.push_str("<h2>Session Length Distribution</h2>\n");
399 html.push_str("<table>\n<tr><th>Length</th><th>Count</th></tr>\n");
400 html.push_str(&format!(
401 "<tr><td>Short (1-5)</td><td>{}</td></tr>\n",
402 dashboard.session_analytics.length_distribution.short
403 ));
404 html.push_str(&format!(
405 "<tr><td>Medium (6-20)</td><td>{}</td></tr>\n",
406 dashboard.session_analytics.length_distribution.medium
407 ));
408 html.push_str(&format!(
409 "<tr><td>Long (21-50)</td><td>{}</td></tr>\n",
410 dashboard.session_analytics.length_distribution.long
411 ));
412 html.push_str(&format!(
413 "<tr><td>Very Long (51+)</td><td>{}</td></tr>\n",
414 dashboard.session_analytics.length_distribution.very_long
415 ));
416 html.push_str("</table>\n");
417 }
418 ReportType::Collaboration => {
419 html.push_str("<h2>Collaboration Metrics</h2>\n<div>\n");
420 html.push_str(&self.metric_card(
421 "Shared Sessions",
422 &dashboard.collaboration.shared_sessions.to_string(),
423 0.0,
424 ));
425 html.push_str(&self.metric_card(
426 "Total Comments",
427 &dashboard.collaboration.total_comments.to_string(),
428 0.0,
429 ));
430 html.push_str("</div>\n");
431 }
432 }
433
434 html.push_str("<div class=\"footer\">Generated by Chasm Analytics</div>\n");
435 html.push_str("</body>\n</html>");
436
437 html
438 }
439
440 fn generate_pdf_placeholder(&self, request: &ReportRequest, dashboard: &TeamDashboard) -> String {
442 self.generate_html(request, dashboard)
445 }
446
447 fn metric_card(&self, label: &str, value: &str, change: f64) -> String {
449 let change_class = if change >= 0.0 {
450 "change-positive"
451 } else {
452 "change-negative"
453 };
454 let change_str = if change != 0.0 {
455 format!(
456 " <span class=\"{}\">{:+.1}%</span>",
457 change_class, change
458 )
459 } else {
460 String::new()
461 };
462
463 format!(
464 "<div class=\"metric-card\"><div class=\"metric-value\">{}{}</div><div class=\"metric-label\">{}</div></div>\n",
465 value, change_str, label
466 )
467 }
468
469 fn get_report_title(&self, request: &ReportRequest) -> String {
471 match request.report_type {
472 ReportType::TeamAnalytics => "Team Analytics Report".to_string(),
473 ReportType::MemberActivity => "Member Activity Report".to_string(),
474 ReportType::ProviderUsage => "Provider Usage Report".to_string(),
475 ReportType::SessionSummary => "Session Summary Report".to_string(),
476 ReportType::Collaboration => "Collaboration Report".to_string(),
477 }
478 }
479
480 fn get_filename(&self, request: &ReportRequest) -> String {
482 let type_str = match request.report_type {
483 ReportType::TeamAnalytics => "team-analytics",
484 ReportType::MemberActivity => "member-activity",
485 ReportType::ProviderUsage => "provider-usage",
486 ReportType::SessionSummary => "session-summary",
487 ReportType::Collaboration => "collaboration",
488 };
489
490 let ext = match request.format {
491 ReportFormat::Csv => "csv",
492 ReportFormat::Json => "json",
493 ReportFormat::Html => "html",
494 ReportFormat::Pdf => "pdf",
495 };
496
497 let timestamp = Utc::now().format("%Y%m%d-%H%M%S");
498 format!("chasm-{}-{}.{}", type_str, timestamp, ext)
499 }
500}
501
502impl Default for ReportGenerator {
503 fn default() -> Self {
504 Self::new()
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use crate::analytics::dashboard::*;
512
513 fn create_test_dashboard() -> TeamDashboard {
514 TeamDashboard {
515 team_id: Uuid::new_v4(),
516 generated_at: Utc::now(),
517 period: AnalyticsPeriod::Last7Days,
518 overview: OverviewMetrics {
519 total_sessions: 100,
520 sessions_change: 10.5,
521 total_messages: 1000,
522 messages_change: 15.2,
523 total_tokens: 50000,
524 tokens_change: 8.3,
525 active_members: 5,
526 active_members_change: 0.0,
527 avg_sessions_per_member: 20.0,
528 avg_messages_per_session: 10.0,
529 },
530 trends: UsageTrends {
531 daily_sessions: vec![],
532 daily_messages: vec![],
533 daily_tokens: vec![],
534 hourly_distribution: vec![0; 24],
535 weekday_distribution: vec![0; 7],
536 },
537 member_stats: vec![MemberStats {
538 member_id: Uuid::new_v4(),
539 display_name: "Test User".to_string(),
540 sessions: 50,
541 messages: 500,
542 tokens: 25000,
543 favorite_provider: Some("copilot".to_string()),
544 avg_session_length: 10.0,
545 last_active: Some(Utc::now()),
546 activity_score: 85,
547 }],
548 provider_breakdown: vec![ProviderStats {
549 provider: "copilot".to_string(),
550 sessions: 60,
551 session_percentage: 60.0,
552 messages: 600,
553 tokens: 30000,
554 top_models: vec![],
555 }],
556 session_analytics: SessionAnalytics {
557 avg_duration_minutes: 15.0,
558 avg_messages: 10.0,
559 avg_tokens: 500.0,
560 length_distribution: SessionLengthDistribution {
561 short: 20,
562 medium: 50,
563 long: 25,
564 very_long: 5,
565 },
566 top_tags: vec![],
567 quality_distribution: QualityDistribution {
568 excellent: 30,
569 good: 40,
570 average: 25,
571 below_average: 5,
572 },
573 },
574 collaboration: CollaborationMetrics {
575 shared_sessions: 20,
576 total_comments: 100,
577 active_collaborations: 5,
578 top_collaborators: vec![],
579 },
580 }
581 }
582
583 #[test]
584 fn test_generate_csv_report() {
585 let generator = ReportGenerator::new();
586 let dashboard = create_test_dashboard();
587 let request = ReportRequest {
588 team_id: dashboard.team_id,
589 report_type: ReportType::TeamAnalytics,
590 format: ReportFormat::Csv,
591 period: AnalyticsPeriod::Last7Days,
592 start_date: None,
593 end_date: None,
594 include_details: true,
595 requested_by: Uuid::new_v4(),
596 };
597
598 let report = generator.generate(&request, &dashboard);
599 assert!(report.content.contains("Total Sessions,100"));
600 assert!(report.filename.ends_with(".csv"));
601 }
602
603 #[test]
604 fn test_generate_html_report() {
605 let generator = ReportGenerator::new();
606 let dashboard = create_test_dashboard();
607 let request = ReportRequest {
608 team_id: dashboard.team_id,
609 report_type: ReportType::MemberActivity,
610 format: ReportFormat::Html,
611 period: AnalyticsPeriod::Last7Days,
612 start_date: None,
613 end_date: None,
614 include_details: true,
615 requested_by: Uuid::new_v4(),
616 };
617
618 let report = generator.generate(&request, &dashboard);
619 assert!(report.content.contains("<html>"));
620 assert!(report.content.contains("Test User"));
621 assert!(report.filename.ends_with(".html"));
622 }
623}