1use crate::collaboration::CollaborationManager;
7use anyhow::Result;
8use chrono::{DateTime, Duration, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use uuid::Uuid;
12
13#[derive(Debug, Clone)]
15pub struct TeamDashboard {
16 config: DashboardConfig,
18 activity_feed: Vec<ActivityEvent>,
20 metrics: TeamMetrics,
22 active_sessions: HashMap<Uuid, SessionActivity>,
24 notifications: NotificationSystem,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct DashboardConfig {
31 pub refresh_interval: u64,
33 pub max_activities: usize,
35 pub real_time_updates: bool,
37 pub show_detailed_metrics: bool,
39 pub widgets: Vec<WidgetConfig>,
41 pub theme: DashboardTheme,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct WidgetConfig {
48 pub id: String,
50 pub widget_type: WidgetType,
52 pub position: WidgetPosition,
54 pub size: WidgetSize,
56 pub settings: HashMap<String, serde_json::Value>,
58 pub visible: bool,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct DashboardTheme {
65 pub primary_color: String,
67 pub secondary_color: String,
69 pub background_color: String,
71 pub text_color: String,
73 pub dark_mode: bool,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ActivityEvent {
80 pub id: Uuid,
82 pub event_type: ActivityType,
84 pub actor: Uuid,
86 pub target: ActivityTarget,
88 pub timestamp: DateTime<Utc>,
90 pub description: String,
92 pub metadata: HashMap<String, serde_json::Value>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct TeamMetrics {
99 pub active_members: usize,
101 pub reports_today: usize,
103 pub annotations_today: usize,
105 pub comments_today: usize,
107 pub avg_response_time: Option<f64>,
114 pub collaboration_score: f64,
116 pub top_contributors: Vec<ContributorMetric>,
118 pub activity_trends: ActivityTrends,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ContributorMetric {
125 pub member_id: Uuid,
127 pub name: String,
129 pub reports_count: usize,
131 pub annotations_count: usize,
133 pub comments_count: usize,
135 pub activity_score: f64,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ActivityTrends {
142 pub daily_activity: Vec<DailyActivity>,
144 pub weekly_summary: WeeklyActivity,
146 pub growth_rate: Option<f64>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct DailyActivity {
158 pub date: DateTime<Utc>,
160 pub reports: usize,
162 pub annotations: usize,
164 pub comments: usize,
166 pub active_users: usize,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct WeeklyActivity {
173 pub total_reports: usize,
175 pub total_annotations: usize,
177 pub total_comments: usize,
179 pub peak_day: Option<String>,
186 pub avg_daily_active_users: f64,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct SessionActivity {
193 pub session_id: Uuid,
195 pub session_type: String,
197 pub participants: Vec<Uuid>,
199 pub started_at: DateTime<Utc>,
201 pub last_activity: DateTime<Utc>,
203 pub activity_count: usize,
205 pub status: String,
207}
208
209#[derive(Debug, Clone)]
211pub struct NotificationSystem {
212 notifications: Vec<DashboardNotification>,
214 settings: NotificationSettings,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct DashboardNotification {
221 pub id: Uuid,
223 pub notification_type: NotificationType,
225 pub title: String,
227 pub message: String,
229 pub priority: NotificationPriority,
231 pub timestamp: DateTime<Utc>,
233 pub target_users: Vec<Uuid>,
235 pub read_by: Vec<Uuid>,
237 pub actions: Vec<NotificationAction>,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct NotificationSettings {
244 pub browser_notifications: bool,
246 pub sound_alerts: bool,
248 pub auto_dismiss_time: u64,
250 pub max_notifications: usize,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub enum ActivityType {
257 ReportShared,
258 ReportUpdated,
259 AnnotationAdded,
260 CommentPosted,
261 SessionStarted,
262 SessionEnded,
263 MemberJoined,
264 MemberLeft,
265 IssueResolved,
266 Custom(String),
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub enum ActivityTarget {
272 Report(Uuid),
273 Annotation(Uuid),
274 Comment(Uuid),
275 Session(Uuid),
276 Member(Uuid),
277 Custom(String),
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282pub enum WidgetType {
283 ActivityFeed,
284 TeamMetrics,
285 ActiveSessions,
286 RecentReports,
287 TopContributors,
288 ActivityChart,
289 NotificationCenter,
290 QuickActions,
291 Custom(String),
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct WidgetPosition {
297 pub x: u32,
299 pub y: u32,
301 pub col: u32,
303 pub row: u32,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct WidgetSize {
310 pub width: u32,
312 pub height: u32,
314 pub min_width: u32,
316 pub min_height: u32,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322pub enum NotificationType {
323 NewReport,
324 NewAnnotation,
325 NewComment,
326 SessionInvite,
327 IssueAssigned,
328 SystemAlert,
329 Custom(String),
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
334pub enum NotificationPriority {
335 Low,
336 Normal,
337 High,
338 Urgent,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct NotificationAction {
344 pub label: String,
346 pub action_type: String,
348 pub data: HashMap<String, serde_json::Value>,
350}
351
352impl TeamDashboard {
353 pub fn new(config: DashboardConfig) -> Self {
355 Self {
356 config,
357 activity_feed: Vec::new(),
358 metrics: TeamMetrics::default(),
359 active_sessions: HashMap::new(),
360 notifications: NotificationSystem::new(),
361 }
362 }
363
364 pub fn update_from_collaboration(
366 &mut self,
367 collaboration: &CollaborationManager,
368 ) -> Result<()> {
369 self.update_metrics(collaboration)?;
371
372 self.update_activity_feed(collaboration)?;
374
375 self.update_active_sessions(collaboration)?;
377
378 Ok(())
379 }
380
381 pub fn add_activity_event(
383 &mut self,
384 event_type: ActivityType,
385 actor: Uuid,
386 target: ActivityTarget,
387 description: String,
388 ) -> Uuid {
389 let event_id = Uuid::new_v4();
390 let event = ActivityEvent {
391 id: event_id,
392 event_type,
393 actor,
394 target,
395 timestamp: Utc::now(),
396 description,
397 metadata: HashMap::new(),
398 };
399
400 self.activity_feed.insert(0, event);
401
402 if self.activity_feed.len() > self.config.max_activities {
404 self.activity_feed.truncate(self.config.max_activities);
405 }
406
407 event_id
408 }
409
410 pub fn get_dashboard_data(&self) -> DashboardData {
412 DashboardData {
413 config: self.config.clone(),
414 activity_feed: self.activity_feed.clone(),
415 metrics: self.metrics.clone(),
416 active_sessions: self.active_sessions.values().cloned().collect(),
417 notifications: self.notifications.get_unread_notifications(),
418 last_updated: Utc::now(),
419 }
420 }
421
422 pub fn send_notification(
424 &mut self,
425 notification_type: NotificationType,
426 title: String,
427 message: String,
428 priority: NotificationPriority,
429 target_users: Vec<Uuid>,
430 ) -> Uuid {
431 self.notifications.send_notification(
432 notification_type,
433 title,
434 message,
435 priority,
436 target_users,
437 )
438 }
439
440 pub fn mark_notification_read(&mut self, notification_id: Uuid, user_id: Uuid) -> Result<()> {
442 self.notifications.mark_read(notification_id, user_id)
443 }
444
445 pub fn get_activity_summary(&self, days: u32) -> ActivitySummary {
447 let cutoff_date = Utc::now() - Duration::days(days as i64);
448
449 let recent_activities: Vec<_> = self
450 .activity_feed
451 .iter()
452 .filter(|activity| activity.timestamp >= cutoff_date)
453 .collect();
454
455 let total_activities = recent_activities.len();
456 let unique_contributors: std::collections::HashSet<_> =
457 recent_activities.iter().map(|a| a.actor).collect();
458
459 ActivitySummary {
460 total_activities,
461 unique_contributors: unique_contributors.len(),
462 activity_by_type: self.count_activities_by_type(&recent_activities),
463 most_active_day: self.find_most_active_day(&recent_activities),
464 period_days: days,
465 }
466 }
467
468 fn update_metrics(&mut self, collaboration: &CollaborationManager) -> Result<()> {
470 let stats = collaboration.get_collaboration_stats();
471
472 let trends = self.calculate_activity_trends();
474
475 let collaboration_score = self.calculate_collaboration_score(&stats);
477
478 self.metrics = TeamMetrics {
479 active_members: stats.team_size,
480 reports_today: self.count_today_activities(ActivityType::ReportShared),
481 annotations_today: self.count_today_activities(ActivityType::AnnotationAdded),
482 comments_today: self.count_today_activities(ActivityType::CommentPosted),
483 avg_response_time: self.calculate_avg_response_time(),
484 collaboration_score,
485 top_contributors: self.calculate_top_contributors(),
486 activity_trends: trends,
487 };
488
489 Ok(())
490 }
491
492 fn update_activity_feed(&mut self, _collaboration: &CollaborationManager) -> Result<()> {
502 Ok(())
503 }
504
505 fn update_active_sessions(&mut self, _collaboration: &CollaborationManager) -> Result<()> {
507 let now = Utc::now();
509
510 let stale_cutoff = now - Duration::hours(1);
512 self.active_sessions.retain(|_, session| session.last_activity >= stale_cutoff);
513
514 Ok(())
515 }
516
517 fn calculate_activity_trends(&self) -> ActivityTrends {
519 let now = Utc::now();
520 let mut daily_activity = Vec::new();
521
522 for i in 0..7 {
524 let date = now - Duration::days(i);
525 let day_start = date.date_naive().and_time(chrono::NaiveTime::MIN).and_utc();
526 let day_end = day_start + Duration::days(1);
527
528 let day_activities: Vec<_> = self
529 .activity_feed
530 .iter()
531 .filter(|a| a.timestamp >= day_start && a.timestamp < day_end)
532 .collect();
533
534 let daily = DailyActivity {
535 date: day_start,
536 reports: day_activities
537 .iter()
538 .filter(|a| matches!(a.event_type, ActivityType::ReportShared))
539 .count(),
540 annotations: day_activities
541 .iter()
542 .filter(|a| matches!(a.event_type, ActivityType::AnnotationAdded))
543 .count(),
544 comments: day_activities
545 .iter()
546 .filter(|a| matches!(a.event_type, ActivityType::CommentPosted))
547 .count(),
548 active_users: day_activities
549 .iter()
550 .map(|a| a.actor)
551 .collect::<std::collections::HashSet<_>>()
552 .len(),
553 };
554
555 daily_activity.push(daily);
556 }
557
558 let total_reports = daily_activity.iter().map(|d| d.reports).sum();
560 let total_annotations = daily_activity.iter().map(|d| d.annotations).sum();
561 let total_comments = daily_activity.iter().map(|d| d.comments).sum();
562 let avg_daily_active_users =
563 daily_activity.iter().map(|d| d.active_users as f64).sum::<f64>() / 7.0;
564
565 let peak_day = daily_activity
569 .iter()
570 .map(|d| (d, d.reports + d.annotations + d.comments))
571 .filter(|(_, total)| *total > 0)
572 .fold(None::<(&DailyActivity, usize)>, |best, cur| match best {
573 Some((_, best_total)) if best_total >= cur.1 => best,
574 _ => Some(cur),
575 })
576 .map(|(day, _)| day.date.format("%A").to_string());
577
578 let weekly_summary = WeeklyActivity {
579 total_reports,
580 total_annotations,
581 total_comments,
582 peak_day,
583 avg_daily_active_users,
584 };
585
586 let week_ago = now - Duration::days(7);
589 let two_weeks_ago = now - Duration::days(14);
590 let recent = self.activity_feed.iter().filter(|a| a.timestamp >= week_ago).count();
591 let prior = self
592 .activity_feed
593 .iter()
594 .filter(|a| a.timestamp >= two_weeks_ago && a.timestamp < week_ago)
595 .count();
596 let growth_rate = if prior == 0 {
597 None
598 } else {
599 Some((recent as f64 - prior as f64) / prior as f64 * 100.0)
600 };
601
602 ActivityTrends {
603 daily_activity,
604 weekly_summary,
605 growth_rate,
606 }
607 }
608
609 fn calculate_collaboration_score(
626 &self,
627 stats: &crate::collaboration::CollaborationStats,
628 ) -> f64 {
629 const DISCUSSION_TARGET: f64 = 3.0;
632 const BREADTH_TARGET: f64 = 5.0;
634
635 let contributors = self
636 .activity_feed
637 .iter()
638 .map(|a| a.actor)
639 .collect::<std::collections::HashSet<_>>()
640 .len();
641 let participation = if stats.team_size == 0 {
642 0.0
643 } else {
644 (contributors as f64 / stats.team_size as f64).min(1.0)
645 };
646
647 let responses = (stats.total_comments + stats.total_annotations) as f64;
648 let discussion = if stats.total_reports == 0 {
649 0.0
650 } else {
651 (responses / stats.total_reports as f64 / DISCUSSION_TARGET).min(1.0)
652 };
653
654 let breadth = (stats.reports_per_member / BREADTH_TARGET).clamp(0.0, 1.0);
655
656 (participation + discussion + breadth) / 3.0 * 100.0
657 }
658
659 fn calculate_top_contributors(&self) -> Vec<ContributorMetric> {
660 let mut contributor_map: HashMap<Uuid, (usize, usize, usize)> = HashMap::new();
661
662 for activity in &self.activity_feed {
663 let entry = contributor_map.entry(activity.actor).or_insert((0, 0, 0));
664 match activity.event_type {
665 ActivityType::ReportShared => entry.0 += 1,
666 ActivityType::AnnotationAdded => entry.1 += 1,
667 ActivityType::CommentPosted => entry.2 += 1,
668 _ => {},
669 }
670 }
671
672 contributor_map
673 .into_iter()
674 .map(|(member_id, (reports, annotations, comments))| {
675 let activity_score =
676 reports as f64 * 3.0 + annotations as f64 * 2.0 + comments as f64;
677 ContributorMetric {
678 member_id,
679 name: format!("User {}", member_id.to_string()[0..8].to_uppercase()),
680 reports_count: reports,
681 annotations_count: annotations,
682 comments_count: comments,
683 activity_score,
684 }
685 })
686 .collect()
687 }
688
689 fn count_today_activities(&self, activity_type: ActivityType) -> usize {
690 let today = Utc::now().date_naive();
691 self.activity_feed
692 .iter()
693 .filter(|a| {
694 a.timestamp.date_naive() == today
695 && std::mem::discriminant(&a.event_type)
696 == std::mem::discriminant(&activity_type)
697 })
698 .count()
699 }
700
701 fn calculate_avg_response_time(&self) -> Option<f64> {
705 None
706 }
707
708 fn count_activities_by_type(&self, activities: &[&ActivityEvent]) -> HashMap<String, usize> {
709 let mut counts = HashMap::new();
710 for activity in activities {
711 let type_name = match &activity.event_type {
712 ActivityType::ReportShared => "ReportShared",
713 ActivityType::AnnotationAdded => "AnnotationAdded",
714 ActivityType::CommentPosted => "CommentPosted",
715 ActivityType::SessionStarted => "SessionStarted",
716 ActivityType::Custom(name) => name,
717 _ => "Other",
718 };
719 *counts.entry(type_name.to_string()).or_insert(0) += 1;
720 }
721 counts
722 }
723
724 fn find_most_active_day(&self, activities: &[&ActivityEvent]) -> String {
725 let mut day_counts = HashMap::new();
726 for activity in activities {
727 let day = activity.timestamp.format("%A").to_string();
728 *day_counts.entry(day).or_insert(0) += 1;
729 }
730
731 day_counts
732 .into_iter()
733 .max_by_key(|(_, count)| *count)
734 .map(|(day, _)| day)
735 .unwrap_or_else(|| "Monday".to_string())
736 }
737}
738
739#[derive(Debug, Clone, Serialize, Deserialize)]
741pub struct DashboardData {
742 pub config: DashboardConfig,
743 pub activity_feed: Vec<ActivityEvent>,
744 pub metrics: TeamMetrics,
745 pub active_sessions: Vec<SessionActivity>,
746 pub notifications: Vec<DashboardNotification>,
747 pub last_updated: DateTime<Utc>,
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
752pub struct ActivitySummary {
753 pub total_activities: usize,
754 pub unique_contributors: usize,
755 pub activity_by_type: HashMap<String, usize>,
756 pub most_active_day: String,
757 pub period_days: u32,
758}
759
760impl Default for NotificationSystem {
761 fn default() -> Self {
762 Self::new()
763 }
764}
765
766impl NotificationSystem {
767 pub fn new() -> Self {
768 Self {
769 notifications: Vec::new(),
770 settings: NotificationSettings {
771 browser_notifications: true,
772 sound_alerts: false,
773 auto_dismiss_time: 5,
774 max_notifications: 50,
775 },
776 }
777 }
778
779 pub fn send_notification(
780 &mut self,
781 notification_type: NotificationType,
782 title: String,
783 message: String,
784 priority: NotificationPriority,
785 target_users: Vec<Uuid>,
786 ) -> Uuid {
787 let notification_id = Uuid::new_v4();
788 let notification = DashboardNotification {
789 id: notification_id,
790 notification_type,
791 title,
792 message,
793 priority,
794 timestamp: Utc::now(),
795 target_users,
796 read_by: Vec::new(),
797 actions: Vec::new(),
798 };
799
800 self.notifications.insert(0, notification);
801
802 if self.notifications.len() > self.settings.max_notifications {
804 self.notifications.truncate(self.settings.max_notifications);
805 }
806
807 notification_id
808 }
809
810 pub fn mark_read(&mut self, notification_id: Uuid, user_id: Uuid) -> Result<()> {
811 if let Some(notification) = self.notifications.iter_mut().find(|n| n.id == notification_id)
812 {
813 if !notification.read_by.contains(&user_id) {
814 notification.read_by.push(user_id);
815 }
816 Ok(())
817 } else {
818 Err(anyhow::anyhow!("Notification not found"))
819 }
820 }
821
822 pub fn get_unread_notifications(&self) -> Vec<DashboardNotification> {
823 self.notifications.clone()
824 }
825}
826
827impl Default for TeamMetrics {
828 fn default() -> Self {
829 Self {
830 active_members: 0,
831 reports_today: 0,
832 annotations_today: 0,
833 comments_today: 0,
834 avg_response_time: None,
835 collaboration_score: 0.0,
836 top_contributors: Vec::new(),
837 activity_trends: ActivityTrends {
838 daily_activity: Vec::new(),
839 weekly_summary: WeeklyActivity {
840 total_reports: 0,
841 total_annotations: 0,
842 total_comments: 0,
843 peak_day: None,
846 avg_daily_active_users: 0.0,
847 },
848 growth_rate: None,
849 },
850 }
851 }
852}
853
854impl Default for DashboardConfig {
855 fn default() -> Self {
856 Self {
857 refresh_interval: 30,
858 max_activities: 100,
859 real_time_updates: true,
860 show_detailed_metrics: true,
861 widgets: vec![
862 WidgetConfig {
863 id: "activity-feed".to_string(),
864 widget_type: WidgetType::ActivityFeed,
865 position: WidgetPosition {
866 x: 0,
867 y: 0,
868 col: 0,
869 row: 0,
870 },
871 size: WidgetSize {
872 width: 6,
873 height: 8,
874 min_width: 4,
875 min_height: 6,
876 },
877 settings: HashMap::new(),
878 visible: true,
879 },
880 WidgetConfig {
881 id: "team-metrics".to_string(),
882 widget_type: WidgetType::TeamMetrics,
883 position: WidgetPosition {
884 x: 6,
885 y: 0,
886 col: 6,
887 row: 0,
888 },
889 size: WidgetSize {
890 width: 6,
891 height: 4,
892 min_width: 4,
893 min_height: 3,
894 },
895 settings: HashMap::new(),
896 visible: true,
897 },
898 ],
899 theme: DashboardTheme {
900 primary_color: "#007acc".to_string(),
901 secondary_color: "#6c757d".to_string(),
902 background_color: "#ffffff".to_string(),
903 text_color: "#333333".to_string(),
904 dark_mode: false,
905 },
906 }
907 }
908}
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913
914 fn push_event_at(
917 dashboard: &mut TeamDashboard,
918 event_type: ActivityType,
919 actor: Uuid,
920 timestamp: DateTime<Utc>,
921 ) {
922 dashboard.activity_feed.push(ActivityEvent {
923 id: Uuid::new_v4(),
924 event_type,
925 actor,
926 target: ActivityTarget::Report(Uuid::new_v4()),
927 timestamp,
928 description: String::new(),
929 metadata: HashMap::new(),
930 });
931 }
932
933 #[test]
934 fn peak_day_is_the_real_busiest_day_not_the_literal_monday() {
935 let mut dashboard = TeamDashboard::new(DashboardConfig::default());
936 let actor = Uuid::new_v4();
937 let now = Utc::now();
938 let busy = now - Duration::days(2);
940 for _ in 0..3 {
941 push_event_at(&mut dashboard, ActivityType::ReportShared, actor, busy);
942 }
943 push_event_at(&mut dashboard, ActivityType::CommentPosted, actor, now);
944
945 let trends = dashboard.calculate_activity_trends();
946 let expected = busy.format("%A").to_string();
947 assert_eq!(
948 trends.weekly_summary.peak_day,
949 Some(expected.clone()),
950 "peak day must be the real argmax ({expected}), not a constant"
951 );
952 }
953
954 #[test]
955 fn peak_day_is_absent_when_nothing_happened() {
956 let dashboard = TeamDashboard::new(DashboardConfig::default());
957 let trends = dashboard.calculate_activity_trends();
958 assert_eq!(
959 trends.weekly_summary.peak_day, None,
960 "no activity => no peak day"
961 );
962 assert_eq!(
963 trends.growth_rate, None,
964 "no prior week => growth is undefined"
965 );
966 }
967
968 #[test]
969 fn growth_rate_compares_this_week_against_the_previous_week() {
970 let mut dashboard = TeamDashboard::new(DashboardConfig::default());
971 let actor = Uuid::new_v4();
972 let now = Utc::now();
973 for _ in 0..2 {
975 push_event_at(
976 &mut dashboard,
977 ActivityType::ReportShared,
978 actor,
979 now - Duration::days(10),
980 );
981 }
982 for _ in 0..3 {
983 push_event_at(
984 &mut dashboard,
985 ActivityType::ReportShared,
986 actor,
987 now - Duration::days(2),
988 );
989 }
990 let trends = dashboard.calculate_activity_trends();
991 let rate = trends.growth_rate.expect("a prior week exists, so growth is defined");
992 assert!((rate - 50.0).abs() < 1e-9, "expected +50%, got {rate}");
993 }
994
995 #[test]
996 fn growth_rate_can_be_negative() {
997 let mut dashboard = TeamDashboard::new(DashboardConfig::default());
998 let actor = Uuid::new_v4();
999 let now = Utc::now();
1000 for _ in 0..4 {
1001 push_event_at(
1002 &mut dashboard,
1003 ActivityType::ReportShared,
1004 actor,
1005 now - Duration::days(9),
1006 );
1007 }
1008 push_event_at(
1009 &mut dashboard,
1010 ActivityType::ReportShared,
1011 actor,
1012 now - Duration::days(1),
1013 );
1014 let rate = dashboard.calculate_activity_trends().growth_rate.expect("prior week non-empty");
1015 assert!((rate + 75.0).abs() < 1e-9, "1 vs 4 is -75%, got {rate}");
1016 }
1017
1018 #[test]
1019 fn collaboration_score_responds_to_stats_and_stays_bounded() {
1020 let mut dashboard = TeamDashboard::new(DashboardConfig::default());
1021 let empty = crate::collaboration::CollaborationStats {
1022 total_reports: 0,
1023 total_annotations: 0,
1024 total_comments: 0,
1025 active_sessions: 0,
1026 team_size: 0,
1027 reports_per_member: 0.0,
1028 annotations_per_report: 0.0,
1029 };
1030 assert_eq!(dashboard.calculate_collaboration_score(&empty), 0.0);
1033
1034 let actor = Uuid::new_v4();
1035 push_event_at(
1036 &mut dashboard,
1037 ActivityType::ReportShared,
1038 actor,
1039 Utc::now(),
1040 );
1041 let healthy = crate::collaboration::CollaborationStats {
1042 total_reports: 10,
1043 total_annotations: 20,
1044 total_comments: 20,
1045 active_sessions: 2,
1046 team_size: 1,
1047 reports_per_member: 10.0,
1048 annotations_per_report: 2.0,
1049 };
1050 let score = dashboard.calculate_collaboration_score(&healthy);
1051 assert!(
1052 (score - 100.0).abs() < 1e-9,
1053 "all three sub-scores saturate: {score}"
1054 );
1055
1056 for _ in 0..600 {
1058 push_event_at(
1059 &mut dashboard,
1060 ActivityType::CommentPosted,
1061 actor,
1062 Utc::now(),
1063 );
1064 }
1065 let score = dashboard.calculate_collaboration_score(&healthy);
1066 assert!(
1067 (0.0..=100.0).contains(&score),
1068 "score must stay bounded: {score}"
1069 );
1070 }
1071
1072 #[test]
1073 fn test_dashboard_creation() {
1074 let config = DashboardConfig::default();
1075 let dashboard = TeamDashboard::new(config);
1076
1077 assert_eq!(dashboard.activity_feed.len(), 0);
1078 assert_eq!(dashboard.metrics.active_members, 0);
1079 }
1080
1081 #[test]
1082 fn test_activity_event_addition() {
1083 let config = DashboardConfig::default();
1084 let mut dashboard = TeamDashboard::new(config);
1085
1086 let actor = Uuid::new_v4();
1087 let target = ActivityTarget::Report(Uuid::new_v4());
1088
1089 let event_id = dashboard.add_activity_event(
1090 ActivityType::ReportShared,
1091 actor,
1092 target,
1093 "Shared debugging report".to_string(),
1094 );
1095
1096 assert_eq!(dashboard.activity_feed.len(), 1);
1097 assert_eq!(dashboard.activity_feed[0].id, event_id);
1098 }
1099
1100 #[test]
1101 fn test_notification_system() {
1102 let mut notification_system = NotificationSystem::new();
1103
1104 let notification_id = notification_system.send_notification(
1105 NotificationType::NewReport,
1106 "New Report".to_string(),
1107 "A new debugging report has been shared".to_string(),
1108 NotificationPriority::Normal,
1109 vec![Uuid::new_v4()],
1110 );
1111
1112 assert_eq!(notification_system.notifications.len(), 1);
1113 assert_eq!(notification_system.notifications[0].id, notification_id);
1114 }
1115
1116 #[test]
1117 fn test_activity_summary() {
1118 let config = DashboardConfig::default();
1119 let mut dashboard = TeamDashboard::new(config);
1120
1121 for i in 0..5 {
1123 dashboard.add_activity_event(
1124 ActivityType::ReportShared,
1125 Uuid::new_v4(),
1126 ActivityTarget::Report(Uuid::new_v4()),
1127 format!("Test activity {}", i),
1128 );
1129 }
1130
1131 let summary = dashboard.get_activity_summary(7);
1132 assert_eq!(summary.total_activities, 5);
1133 assert_eq!(summary.period_days, 7);
1134 }
1135}