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: f64,
109 pub collaboration_score: f64,
111 pub top_contributors: Vec<ContributorMetric>,
113 pub activity_trends: ActivityTrends,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ContributorMetric {
120 pub member_id: Uuid,
122 pub name: String,
124 pub reports_count: usize,
126 pub annotations_count: usize,
128 pub comments_count: usize,
130 pub activity_score: f64,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ActivityTrends {
137 pub daily_activity: Vec<DailyActivity>,
139 pub weekly_summary: WeeklyActivity,
141 pub growth_rate: f64,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct DailyActivity {
148 pub date: DateTime<Utc>,
150 pub reports: usize,
152 pub annotations: usize,
154 pub comments: usize,
156 pub active_users: usize,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct WeeklyActivity {
163 pub total_reports: usize,
165 pub total_annotations: usize,
167 pub total_comments: usize,
169 pub peak_day: String,
171 pub avg_daily_active_users: f64,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct SessionActivity {
178 pub session_id: Uuid,
180 pub session_type: String,
182 pub participants: Vec<Uuid>,
184 pub started_at: DateTime<Utc>,
186 pub last_activity: DateTime<Utc>,
188 pub activity_count: usize,
190 pub status: String,
192}
193
194#[derive(Debug, Clone)]
196pub struct NotificationSystem {
197 notifications: Vec<DashboardNotification>,
199 settings: NotificationSettings,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct DashboardNotification {
206 pub id: Uuid,
208 pub notification_type: NotificationType,
210 pub title: String,
212 pub message: String,
214 pub priority: NotificationPriority,
216 pub timestamp: DateTime<Utc>,
218 pub target_users: Vec<Uuid>,
220 pub read_by: Vec<Uuid>,
222 pub actions: Vec<NotificationAction>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct NotificationSettings {
229 pub browser_notifications: bool,
231 pub sound_alerts: bool,
233 pub auto_dismiss_time: u64,
235 pub max_notifications: usize,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub enum ActivityType {
242 ReportShared,
243 ReportUpdated,
244 AnnotationAdded,
245 CommentPosted,
246 SessionStarted,
247 SessionEnded,
248 MemberJoined,
249 MemberLeft,
250 IssueResolved,
251 Custom(String),
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub enum ActivityTarget {
257 Report(Uuid),
258 Annotation(Uuid),
259 Comment(Uuid),
260 Session(Uuid),
261 Member(Uuid),
262 Custom(String),
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub enum WidgetType {
268 ActivityFeed,
269 TeamMetrics,
270 ActiveSessions,
271 RecentReports,
272 TopContributors,
273 ActivityChart,
274 NotificationCenter,
275 QuickActions,
276 Custom(String),
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct WidgetPosition {
282 pub x: u32,
284 pub y: u32,
286 pub col: u32,
288 pub row: u32,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct WidgetSize {
295 pub width: u32,
297 pub height: u32,
299 pub min_width: u32,
301 pub min_height: u32,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub enum NotificationType {
308 NewReport,
309 NewAnnotation,
310 NewComment,
311 SessionInvite,
312 IssueAssigned,
313 SystemAlert,
314 Custom(String),
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub enum NotificationPriority {
320 Low,
321 Normal,
322 High,
323 Urgent,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct NotificationAction {
329 pub label: String,
331 pub action_type: String,
333 pub data: HashMap<String, serde_json::Value>,
335}
336
337impl TeamDashboard {
338 pub fn new(config: DashboardConfig) -> Self {
340 Self {
341 config,
342 activity_feed: Vec::new(),
343 metrics: TeamMetrics::default(),
344 active_sessions: HashMap::new(),
345 notifications: NotificationSystem::new(),
346 }
347 }
348
349 pub fn update_from_collaboration(
351 &mut self,
352 collaboration: &CollaborationManager,
353 ) -> Result<()> {
354 self.update_metrics(collaboration)?;
356
357 self.update_activity_feed(collaboration)?;
359
360 self.update_active_sessions(collaboration)?;
362
363 Ok(())
364 }
365
366 pub fn add_activity_event(
368 &mut self,
369 event_type: ActivityType,
370 actor: Uuid,
371 target: ActivityTarget,
372 description: String,
373 ) -> Uuid {
374 let event_id = Uuid::new_v4();
375 let event = ActivityEvent {
376 id: event_id,
377 event_type,
378 actor,
379 target,
380 timestamp: Utc::now(),
381 description,
382 metadata: HashMap::new(),
383 };
384
385 self.activity_feed.insert(0, event);
386
387 if self.activity_feed.len() > self.config.max_activities {
389 self.activity_feed.truncate(self.config.max_activities);
390 }
391
392 event_id
393 }
394
395 pub fn get_dashboard_data(&self) -> DashboardData {
397 DashboardData {
398 config: self.config.clone(),
399 activity_feed: self.activity_feed.clone(),
400 metrics: self.metrics.clone(),
401 active_sessions: self.active_sessions.values().cloned().collect(),
402 notifications: self.notifications.get_unread_notifications(),
403 last_updated: Utc::now(),
404 }
405 }
406
407 pub fn send_notification(
409 &mut self,
410 notification_type: NotificationType,
411 title: String,
412 message: String,
413 priority: NotificationPriority,
414 target_users: Vec<Uuid>,
415 ) -> Uuid {
416 self.notifications.send_notification(
417 notification_type,
418 title,
419 message,
420 priority,
421 target_users,
422 )
423 }
424
425 pub fn mark_notification_read(&mut self, notification_id: Uuid, user_id: Uuid) -> Result<()> {
427 self.notifications.mark_read(notification_id, user_id)
428 }
429
430 pub fn get_activity_summary(&self, days: u32) -> ActivitySummary {
432 let cutoff_date = Utc::now() - Duration::days(days as i64);
433
434 let recent_activities: Vec<_> = self
435 .activity_feed
436 .iter()
437 .filter(|activity| activity.timestamp >= cutoff_date)
438 .collect();
439
440 let total_activities = recent_activities.len();
441 let unique_contributors: std::collections::HashSet<_> =
442 recent_activities.iter().map(|a| a.actor).collect();
443
444 ActivitySummary {
445 total_activities,
446 unique_contributors: unique_contributors.len(),
447 activity_by_type: self.count_activities_by_type(&recent_activities),
448 most_active_day: self.find_most_active_day(&recent_activities),
449 period_days: days,
450 }
451 }
452
453 fn update_metrics(&mut self, collaboration: &CollaborationManager) -> Result<()> {
455 let stats = collaboration.get_collaboration_stats();
456
457 let trends = self.calculate_activity_trends();
459
460 let collaboration_score = self.calculate_collaboration_score(&stats);
462
463 self.metrics = TeamMetrics {
464 active_members: stats.team_size,
465 reports_today: self.count_today_activities(ActivityType::ReportShared),
466 annotations_today: self.count_today_activities(ActivityType::AnnotationAdded),
467 comments_today: self.count_today_activities(ActivityType::CommentPosted),
468 avg_response_time: self.calculate_avg_response_time(),
469 collaboration_score,
470 top_contributors: self.calculate_top_contributors(),
471 activity_trends: trends,
472 };
473
474 Ok(())
475 }
476
477 fn update_activity_feed(&mut self, _collaboration: &CollaborationManager) -> Result<()> {
479 Ok(())
482 }
483
484 fn update_active_sessions(&mut self, _collaboration: &CollaborationManager) -> Result<()> {
486 let now = Utc::now();
488
489 let stale_cutoff = now - Duration::hours(1);
491 self.active_sessions.retain(|_, session| session.last_activity >= stale_cutoff);
492
493 Ok(())
494 }
495
496 fn calculate_activity_trends(&self) -> ActivityTrends {
498 let now = Utc::now();
499 let mut daily_activity = Vec::new();
500
501 for i in 0..7 {
503 let date = now - Duration::days(i);
504 let day_start = date
505 .date_naive()
506 .and_hms_opt(0, 0, 0)
507 .expect("0:0:0 is always a valid time")
508 .and_utc();
509 let day_end = day_start + Duration::days(1);
510
511 let day_activities: Vec<_> = self
512 .activity_feed
513 .iter()
514 .filter(|a| a.timestamp >= day_start && a.timestamp < day_end)
515 .collect();
516
517 let daily = DailyActivity {
518 date: day_start,
519 reports: day_activities
520 .iter()
521 .filter(|a| matches!(a.event_type, ActivityType::ReportShared))
522 .count(),
523 annotations: day_activities
524 .iter()
525 .filter(|a| matches!(a.event_type, ActivityType::AnnotationAdded))
526 .count(),
527 comments: day_activities
528 .iter()
529 .filter(|a| matches!(a.event_type, ActivityType::CommentPosted))
530 .count(),
531 active_users: day_activities
532 .iter()
533 .map(|a| a.actor)
534 .collect::<std::collections::HashSet<_>>()
535 .len(),
536 };
537
538 daily_activity.push(daily);
539 }
540
541 let total_reports = daily_activity.iter().map(|d| d.reports).sum();
543 let total_annotations = daily_activity.iter().map(|d| d.annotations).sum();
544 let total_comments = daily_activity.iter().map(|d| d.comments).sum();
545 let avg_daily_active_users =
546 daily_activity.iter().map(|d| d.active_users as f64).sum::<f64>() / 7.0;
547
548 let weekly_summary = WeeklyActivity {
549 total_reports,
550 total_annotations,
551 total_comments,
552 peak_day: "Monday".to_string(), avg_daily_active_users,
554 };
555
556 ActivityTrends {
557 daily_activity,
558 weekly_summary,
559 growth_rate: 0.0, }
561 }
562
563 fn calculate_collaboration_score(
564 &self,
565 _stats: &crate::collaboration::CollaborationStats,
566 ) -> f64 {
567 50.0 + (self.activity_feed.len() as f64 * 0.1)
570 }
571
572 fn calculate_top_contributors(&self) -> Vec<ContributorMetric> {
573 let mut contributor_map: HashMap<Uuid, (usize, usize, usize)> = HashMap::new();
574
575 for activity in &self.activity_feed {
576 let entry = contributor_map.entry(activity.actor).or_insert((0, 0, 0));
577 match activity.event_type {
578 ActivityType::ReportShared => entry.0 += 1,
579 ActivityType::AnnotationAdded => entry.1 += 1,
580 ActivityType::CommentPosted => entry.2 += 1,
581 _ => {},
582 }
583 }
584
585 contributor_map
586 .into_iter()
587 .map(|(member_id, (reports, annotations, comments))| {
588 let activity_score =
589 reports as f64 * 3.0 + annotations as f64 * 2.0 + comments as f64;
590 ContributorMetric {
591 member_id,
592 name: format!("User {}", member_id.to_string()[0..8].to_uppercase()),
593 reports_count: reports,
594 annotations_count: annotations,
595 comments_count: comments,
596 activity_score,
597 }
598 })
599 .collect()
600 }
601
602 fn count_today_activities(&self, activity_type: ActivityType) -> usize {
603 let today = Utc::now().date_naive();
604 self.activity_feed
605 .iter()
606 .filter(|a| {
607 a.timestamp.date_naive() == today
608 && std::mem::discriminant(&a.event_type)
609 == std::mem::discriminant(&activity_type)
610 })
611 .count()
612 }
613
614 fn calculate_avg_response_time(&self) -> f64 {
615 15.0 }
618
619 fn count_activities_by_type(&self, activities: &[&ActivityEvent]) -> HashMap<String, usize> {
620 let mut counts = HashMap::new();
621 for activity in activities {
622 let type_name = match &activity.event_type {
623 ActivityType::ReportShared => "ReportShared",
624 ActivityType::AnnotationAdded => "AnnotationAdded",
625 ActivityType::CommentPosted => "CommentPosted",
626 ActivityType::SessionStarted => "SessionStarted",
627 ActivityType::Custom(name) => name,
628 _ => "Other",
629 };
630 *counts.entry(type_name.to_string()).or_insert(0) += 1;
631 }
632 counts
633 }
634
635 fn find_most_active_day(&self, activities: &[&ActivityEvent]) -> String {
636 let mut day_counts = HashMap::new();
637 for activity in activities {
638 let day = activity.timestamp.format("%A").to_string();
639 *day_counts.entry(day).or_insert(0) += 1;
640 }
641
642 day_counts
643 .into_iter()
644 .max_by_key(|(_, count)| *count)
645 .map(|(day, _)| day)
646 .unwrap_or_else(|| "Monday".to_string())
647 }
648}
649
650#[derive(Debug, Clone, Serialize, Deserialize)]
652pub struct DashboardData {
653 pub config: DashboardConfig,
654 pub activity_feed: Vec<ActivityEvent>,
655 pub metrics: TeamMetrics,
656 pub active_sessions: Vec<SessionActivity>,
657 pub notifications: Vec<DashboardNotification>,
658 pub last_updated: DateTime<Utc>,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize)]
663pub struct ActivitySummary {
664 pub total_activities: usize,
665 pub unique_contributors: usize,
666 pub activity_by_type: HashMap<String, usize>,
667 pub most_active_day: String,
668 pub period_days: u32,
669}
670
671impl Default for NotificationSystem {
672 fn default() -> Self {
673 Self::new()
674 }
675}
676
677impl NotificationSystem {
678 pub fn new() -> Self {
679 Self {
680 notifications: Vec::new(),
681 settings: NotificationSettings {
682 browser_notifications: true,
683 sound_alerts: false,
684 auto_dismiss_time: 5,
685 max_notifications: 50,
686 },
687 }
688 }
689
690 pub fn send_notification(
691 &mut self,
692 notification_type: NotificationType,
693 title: String,
694 message: String,
695 priority: NotificationPriority,
696 target_users: Vec<Uuid>,
697 ) -> Uuid {
698 let notification_id = Uuid::new_v4();
699 let notification = DashboardNotification {
700 id: notification_id,
701 notification_type,
702 title,
703 message,
704 priority,
705 timestamp: Utc::now(),
706 target_users,
707 read_by: Vec::new(),
708 actions: Vec::new(),
709 };
710
711 self.notifications.insert(0, notification);
712
713 if self.notifications.len() > self.settings.max_notifications {
715 self.notifications.truncate(self.settings.max_notifications);
716 }
717
718 notification_id
719 }
720
721 pub fn mark_read(&mut self, notification_id: Uuid, user_id: Uuid) -> Result<()> {
722 if let Some(notification) = self.notifications.iter_mut().find(|n| n.id == notification_id)
723 {
724 if !notification.read_by.contains(&user_id) {
725 notification.read_by.push(user_id);
726 }
727 Ok(())
728 } else {
729 Err(anyhow::anyhow!("Notification not found"))
730 }
731 }
732
733 pub fn get_unread_notifications(&self) -> Vec<DashboardNotification> {
734 self.notifications.clone()
735 }
736}
737
738impl Default for TeamMetrics {
739 fn default() -> Self {
740 Self {
741 active_members: 0,
742 reports_today: 0,
743 annotations_today: 0,
744 comments_today: 0,
745 avg_response_time: 0.0,
746 collaboration_score: 0.0,
747 top_contributors: Vec::new(),
748 activity_trends: ActivityTrends {
749 daily_activity: Vec::new(),
750 weekly_summary: WeeklyActivity {
751 total_reports: 0,
752 total_annotations: 0,
753 total_comments: 0,
754 peak_day: "Monday".to_string(),
755 avg_daily_active_users: 0.0,
756 },
757 growth_rate: 0.0,
758 },
759 }
760 }
761}
762
763impl Default for DashboardConfig {
764 fn default() -> Self {
765 Self {
766 refresh_interval: 30,
767 max_activities: 100,
768 real_time_updates: true,
769 show_detailed_metrics: true,
770 widgets: vec![
771 WidgetConfig {
772 id: "activity-feed".to_string(),
773 widget_type: WidgetType::ActivityFeed,
774 position: WidgetPosition {
775 x: 0,
776 y: 0,
777 col: 0,
778 row: 0,
779 },
780 size: WidgetSize {
781 width: 6,
782 height: 8,
783 min_width: 4,
784 min_height: 6,
785 },
786 settings: HashMap::new(),
787 visible: true,
788 },
789 WidgetConfig {
790 id: "team-metrics".to_string(),
791 widget_type: WidgetType::TeamMetrics,
792 position: WidgetPosition {
793 x: 6,
794 y: 0,
795 col: 6,
796 row: 0,
797 },
798 size: WidgetSize {
799 width: 6,
800 height: 4,
801 min_width: 4,
802 min_height: 3,
803 },
804 settings: HashMap::new(),
805 visible: true,
806 },
807 ],
808 theme: DashboardTheme {
809 primary_color: "#007acc".to_string(),
810 secondary_color: "#6c757d".to_string(),
811 background_color: "#ffffff".to_string(),
812 text_color: "#333333".to_string(),
813 dark_mode: false,
814 },
815 }
816 }
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
822
823 #[test]
824 fn test_dashboard_creation() {
825 let config = DashboardConfig::default();
826 let dashboard = TeamDashboard::new(config);
827
828 assert_eq!(dashboard.activity_feed.len(), 0);
829 assert_eq!(dashboard.metrics.active_members, 0);
830 }
831
832 #[test]
833 fn test_activity_event_addition() {
834 let config = DashboardConfig::default();
835 let mut dashboard = TeamDashboard::new(config);
836
837 let actor = Uuid::new_v4();
838 let target = ActivityTarget::Report(Uuid::new_v4());
839
840 let event_id = dashboard.add_activity_event(
841 ActivityType::ReportShared,
842 actor,
843 target,
844 "Shared debugging report".to_string(),
845 );
846
847 assert_eq!(dashboard.activity_feed.len(), 1);
848 assert_eq!(dashboard.activity_feed[0].id, event_id);
849 }
850
851 #[test]
852 fn test_notification_system() {
853 let mut notification_system = NotificationSystem::new();
854
855 let notification_id = notification_system.send_notification(
856 NotificationType::NewReport,
857 "New Report".to_string(),
858 "A new debugging report has been shared".to_string(),
859 NotificationPriority::Normal,
860 vec![Uuid::new_v4()],
861 );
862
863 assert_eq!(notification_system.notifications.len(), 1);
864 assert_eq!(notification_system.notifications[0].id, notification_id);
865 }
866
867 #[test]
868 fn test_activity_summary() {
869 let config = DashboardConfig::default();
870 let mut dashboard = TeamDashboard::new(config);
871
872 for i in 0..5 {
874 dashboard.add_activity_event(
875 ActivityType::ReportShared,
876 Uuid::new_v4(),
877 ActivityTarget::Report(Uuid::new_v4()),
878 format!("Test activity {}", i),
879 );
880 }
881
882 let summary = dashboard.get_activity_summary(7);
883 assert_eq!(summary.total_activities, 5);
884 assert_eq!(summary.period_days, 7);
885 }
886}