1use anyhow::Result;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use uuid::Uuid;
11
12#[derive(Debug, Clone)]
14pub struct CollaborationManager {
15 shared_reports: HashMap<Uuid, SharedReport>,
17 annotation_sessions: HashMap<Uuid, AnnotationSession>,
19 team_members: HashMap<Uuid, TeamMember>,
21 comment_threads: HashMap<Uuid, CommentThread>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SharedReport {
28 pub id: Uuid,
30 pub title: String,
32 pub content: String,
34 pub format: ReportFormat,
36 pub author: TeamMember,
38 pub created_at: DateTime<Utc>,
40 pub last_modified: DateTime<Utc>,
42 pub permissions: ReportPermissions,
44 pub tags: Vec<String>,
46 pub version: u32,
48 pub sharing_status: SharingStatus,
50 pub annotations: Vec<Uuid>,
52 pub view_count: u64,
54 pub collaborators: Vec<Uuid>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct TeamMember {
61 pub id: Uuid,
63 pub name: String,
65 pub email: String,
67 pub avatar_url: Option<String>,
69 pub role: TeamRole,
71 pub timezone: String,
73 pub last_active: DateTime<Utc>,
75 pub notification_preferences: NotificationPreferences,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct AnnotationSession {
82 pub id: Uuid,
84 pub report_id: Uuid,
86 pub participants: Vec<Uuid>,
88 pub creator: Uuid,
90 pub started_at: DateTime<Utc>,
92 pub ended_at: Option<DateTime<Utc>>,
94 pub status: SessionStatus,
96 pub annotations: Vec<Annotation>,
98 pub session_type: AnnotationType,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct Annotation {
105 pub id: Uuid,
107 pub author: Uuid,
109 pub timestamp: DateTime<Utc>,
111 pub annotation_type: AnnotationType,
113 pub target: AnnotationTarget,
115 pub content: String,
117 pub importance: ImportanceLevel,
119 pub status: AnnotationStatus,
121 pub related_annotations: Vec<Uuid>,
123 pub attachments: Vec<Attachment>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct CommentThread {
130 pub id: Uuid,
132 pub report_id: Uuid,
134 pub subject: String,
136 pub creator: Uuid,
138 pub created_at: DateTime<Utc>,
140 pub status: ThreadStatus,
142 pub comments: Vec<Comment>,
144 pub participants: Vec<Uuid>,
146 pub tags: Vec<String>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct Comment {
153 pub id: Uuid,
155 pub author: Uuid,
157 pub content: String,
159 pub timestamp: DateTime<Utc>,
161 pub edited_at: Option<DateTime<Utc>>,
163 pub parent_id: Option<Uuid>,
165 pub reactions: HashMap<String, Vec<Uuid>>,
167 pub attachments: Vec<Attachment>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct Attachment {
174 pub id: Uuid,
176 pub filename: String,
178 pub content_type: String,
180 pub size: u64,
182 pub url: String,
184 pub uploaded_at: DateTime<Utc>,
186 pub uploader: Uuid,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub enum ReportFormat {
193 Json,
194 Markdown,
195 Html,
196 Pdf,
197 Custom(String),
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct ReportPermissions {
203 pub is_public: bool,
205 pub team_access: bool,
207 pub user_access: Vec<Uuid>,
209 pub edit_permissions: Vec<Uuid>,
211 pub comment_permissions: Vec<Uuid>,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub enum SharingStatus {
218 Private,
219 TeamShared,
220 PublicShared,
221 LinkShared(String),
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub enum TeamRole {
227 Owner,
228 Admin,
229 Developer,
230 Reviewer,
231 Viewer,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct NotificationPreferences {
237 pub email_enabled: bool,
239 pub comment_notifications: bool,
241 pub annotation_notifications: bool,
243 pub share_notifications: bool,
245 pub digest_frequency: DigestFrequency,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub enum DigestFrequency {
252 Immediate,
253 Hourly,
254 Daily,
255 Weekly,
256 Disabled,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub enum SessionStatus {
262 Active,
263 Paused,
264 Completed,
265 Cancelled,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub enum AnnotationType {
271 Note,
272 Issue,
273 Question,
274 Improvement,
275 Bug,
276 Performance,
277 Security,
278 Documentation,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub enum AnnotationTarget {
284 Line(u32),
286 LineRange(u32, u32),
288 Section(String),
290 Element(String),
292 Custom(HashMap<String, String>),
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298pub enum ImportanceLevel {
299 Low,
300 Medium,
301 High,
302 Critical,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub enum AnnotationStatus {
308 Open,
309 InProgress,
310 Resolved,
311 Dismissed,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub enum ThreadStatus {
317 Open,
318 Resolved,
319 Locked,
320 Archived,
321}
322
323impl CollaborationManager {
324 pub fn new() -> Self {
326 Self {
327 shared_reports: HashMap::new(),
328 annotation_sessions: HashMap::new(),
329 team_members: HashMap::new(),
330 comment_threads: HashMap::new(),
331 }
332 }
333
334 pub fn share_report(
336 &mut self,
337 title: String,
338 content: String,
339 format: ReportFormat,
340 author: TeamMember,
341 permissions: ReportPermissions,
342 tags: Vec<String>,
343 ) -> Result<Uuid> {
344 let report_id = Uuid::new_v4();
345 let now = Utc::now();
346
347 let shared_report = SharedReport {
348 id: report_id,
349 title,
350 content,
351 format,
352 author: author.clone(),
353 created_at: now,
354 last_modified: now,
355 permissions,
356 tags,
357 version: 1,
358 sharing_status: SharingStatus::Private,
359 annotations: Vec::new(),
360 view_count: 0,
361 collaborators: vec![author.id],
362 };
363
364 self.shared_reports.insert(report_id, shared_report);
365 self.team_members.insert(author.id, author);
366
367 Ok(report_id)
368 }
369
370 pub fn get_report(&mut self, report_id: Uuid) -> Option<&mut SharedReport> {
372 if let Some(report) = self.shared_reports.get_mut(&report_id) {
373 report.view_count += 1;
374 Some(report)
375 } else {
376 None
377 }
378 }
379
380 pub fn start_annotation_session(
382 &mut self,
383 report_id: Uuid,
384 creator: Uuid,
385 participants: Vec<Uuid>,
386 session_type: AnnotationType,
387 ) -> Result<Uuid> {
388 let session_id = Uuid::new_v4();
389 let now = Utc::now();
390
391 let session = AnnotationSession {
392 id: session_id,
393 report_id,
394 participants,
395 creator,
396 started_at: now,
397 ended_at: None,
398 status: SessionStatus::Active,
399 annotations: Vec::new(),
400 session_type,
401 };
402
403 self.annotation_sessions.insert(session_id, session);
404 Ok(session_id)
405 }
406
407 pub fn add_annotation(
409 &mut self,
410 session_id: Uuid,
411 author: Uuid,
412 annotation_type: AnnotationType,
413 target: AnnotationTarget,
414 content: String,
415 importance: ImportanceLevel,
416 ) -> Result<Uuid> {
417 let annotation_id = Uuid::new_v4();
418 let now = Utc::now();
419
420 let annotation = Annotation {
421 id: annotation_id,
422 author,
423 timestamp: now,
424 annotation_type,
425 target,
426 content,
427 importance,
428 status: AnnotationStatus::Open,
429 related_annotations: Vec::new(),
430 attachments: Vec::new(),
431 };
432
433 if let Some(session) = self.annotation_sessions.get_mut(&session_id) {
434 session.annotations.push(annotation);
435
436 if let Some(report) = self.shared_reports.get_mut(&session.report_id) {
438 report.annotations.push(annotation_id);
439 report.last_modified = now;
440 }
441
442 Ok(annotation_id)
443 } else {
444 Err(anyhow::anyhow!("Annotation session not found"))
445 }
446 }
447
448 pub fn create_comment_thread(
450 &mut self,
451 report_id: Uuid,
452 subject: String,
453 creator: Uuid,
454 tags: Vec<String>,
455 ) -> Result<Uuid> {
456 let thread_id = Uuid::new_v4();
457 let now = Utc::now();
458
459 let thread = CommentThread {
460 id: thread_id,
461 report_id,
462 subject,
463 creator,
464 created_at: now,
465 status: ThreadStatus::Open,
466 comments: Vec::new(),
467 participants: vec![creator],
468 tags,
469 };
470
471 self.comment_threads.insert(thread_id, thread);
472 Ok(thread_id)
473 }
474
475 pub fn add_comment(
477 &mut self,
478 thread_id: Uuid,
479 author: Uuid,
480 content: String,
481 parent_id: Option<Uuid>,
482 ) -> Result<Uuid> {
483 let comment_id = Uuid::new_v4();
484 let now = Utc::now();
485
486 let comment = Comment {
487 id: comment_id,
488 author,
489 content,
490 timestamp: now,
491 edited_at: None,
492 parent_id,
493 reactions: HashMap::new(),
494 attachments: Vec::new(),
495 };
496
497 if let Some(thread) = self.comment_threads.get_mut(&thread_id) {
498 thread.comments.push(comment);
499
500 if !thread.participants.contains(&author) {
502 thread.participants.push(author);
503 }
504
505 Ok(comment_id)
506 } else {
507 Err(anyhow::anyhow!("Comment thread not found"))
508 }
509 }
510
511 pub fn add_team_member(&mut self, member: TeamMember) -> Uuid {
513 let member_id = member.id;
514 self.team_members.insert(member_id, member);
515 member_id
516 }
517
518 pub fn update_sharing_status(&mut self, report_id: Uuid, status: SharingStatus) -> Result<()> {
520 if let Some(report) = self.shared_reports.get_mut(&report_id) {
521 report.sharing_status = status;
522 report.last_modified = Utc::now();
523 Ok(())
524 } else {
525 Err(anyhow::anyhow!("Report not found"))
526 }
527 }
528
529 pub fn get_user_reports(&self, user_id: Uuid) -> Vec<&SharedReport> {
531 self.shared_reports
532 .values()
533 .filter(|report| {
534 report.author.id == user_id
536 || report.collaborators.contains(&user_id)
537 || report.permissions.user_access.contains(&user_id)
538 || report.permissions.is_public
539 || (report.permissions.team_access && self.team_members.contains_key(&user_id))
540 })
541 .collect()
542 }
543
544 pub fn get_report_annotations(&self, report_id: Uuid) -> Vec<&Annotation> {
546 let mut annotations = Vec::new();
547
548 for session in self.annotation_sessions.values() {
549 if session.report_id == report_id {
550 annotations.extend(session.annotations.iter());
551 }
552 }
553
554 annotations
555 }
556
557 pub fn get_report_threads(&self, report_id: Uuid) -> Vec<&CommentThread> {
559 self.comment_threads
560 .values()
561 .filter(|thread| thread.report_id == report_id)
562 .collect()
563 }
564
565 pub fn export_collaboration_data(&self) -> Result<String> {
567 #[derive(Serialize)]
568 struct CollaborationExport<'a> {
569 reports: Vec<&'a SharedReport>,
570 sessions: Vec<&'a AnnotationSession>,
571 threads: Vec<&'a CommentThread>,
572 members: Vec<&'a TeamMember>,
573 export_timestamp: DateTime<Utc>,
574 }
575
576 let export_data = CollaborationExport {
577 reports: self.shared_reports.values().collect(),
578 sessions: self.annotation_sessions.values().collect(),
579 threads: self.comment_threads.values().collect(),
580 members: self.team_members.values().collect(),
581 export_timestamp: Utc::now(),
582 };
583
584 serde_json::to_string_pretty(&export_data)
585 .map_err(|e| anyhow::anyhow!("Failed to export collaboration data: {}", e))
586 }
587
588 pub fn get_collaboration_stats(&self) -> CollaborationStats {
590 let total_reports = self.shared_reports.len();
591 let total_annotations =
592 self.annotation_sessions.values().map(|s| s.annotations.len()).sum();
593 let total_comments = self.comment_threads.values().map(|t| t.comments.len()).sum();
594 let active_sessions = self
595 .annotation_sessions
596 .values()
597 .filter(|s| matches!(s.status, SessionStatus::Active))
598 .count();
599 let team_size = self.team_members.len();
600
601 CollaborationStats {
602 total_reports,
603 total_annotations,
604 total_comments,
605 active_sessions,
606 team_size,
607 reports_per_member: if team_size > 0 {
608 total_reports as f64 / team_size as f64
609 } else {
610 0.0
611 },
612 annotations_per_report: if total_reports > 0 {
613 total_annotations as f64 / total_reports as f64
614 } else {
615 0.0
616 },
617 }
618 }
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize)]
623pub struct CollaborationStats {
624 pub total_reports: usize,
625 pub total_annotations: usize,
626 pub total_comments: usize,
627 pub active_sessions: usize,
628 pub team_size: usize,
629 pub reports_per_member: f64,
630 pub annotations_per_report: f64,
631}
632
633impl Default for CollaborationManager {
634 fn default() -> Self {
635 Self::new()
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642
643 fn create_test_member() -> TeamMember {
644 TeamMember {
645 id: Uuid::new_v4(),
646 name: "Test User".to_string(),
647 email: "test@example.com".to_string(),
648 avatar_url: None,
649 role: TeamRole::Developer,
650 timezone: "UTC".to_string(),
651 last_active: Utc::now(),
652 notification_preferences: NotificationPreferences {
653 email_enabled: true,
654 comment_notifications: true,
655 annotation_notifications: true,
656 share_notifications: true,
657 digest_frequency: DigestFrequency::Daily,
658 },
659 }
660 }
661
662 #[test]
663 fn test_share_report() {
664 let mut manager = CollaborationManager::new();
665 let author = create_test_member();
666
667 let permissions = ReportPermissions {
668 is_public: false,
669 team_access: true,
670 user_access: vec![],
671 edit_permissions: vec![author.id],
672 comment_permissions: vec![author.id],
673 };
674
675 let report_id = manager
676 .share_report(
677 "Test Report".to_string(),
678 "Report content".to_string(),
679 ReportFormat::Markdown,
680 author,
681 permissions,
682 vec!["test".to_string()],
683 )
684 .expect("operation failed in test");
685
686 assert!(manager.shared_reports.contains_key(&report_id));
687 }
688
689 #[test]
690 fn test_annotation_session() {
691 let mut manager = CollaborationManager::new();
692 let author = create_test_member();
693
694 let permissions = ReportPermissions {
696 is_public: false,
697 team_access: true,
698 user_access: vec![],
699 edit_permissions: vec![author.id],
700 comment_permissions: vec![author.id],
701 };
702
703 let report_id = manager
704 .share_report(
705 "Test Report".to_string(),
706 "Report content".to_string(),
707 ReportFormat::Markdown,
708 author.clone(),
709 permissions,
710 vec![],
711 )
712 .expect("operation failed in test");
713
714 let session_id = manager
716 .start_annotation_session(report_id, author.id, vec![author.id], AnnotationType::Note)
717 .expect("operation failed in test");
718
719 let _annotation_id = manager
721 .add_annotation(
722 session_id,
723 author.id,
724 AnnotationType::Issue,
725 AnnotationTarget::Line(10),
726 "This looks like a bug".to_string(),
727 ImportanceLevel::High,
728 )
729 .expect("operation failed in test");
730
731 assert!(manager.annotation_sessions.contains_key(&session_id));
732
733 let report = manager.shared_reports.get(&report_id).expect("expected value not found");
735 assert!(!report.annotations.is_empty());
736 }
737
738 #[test]
739 fn test_comment_thread() {
740 let mut manager = CollaborationManager::new();
741 let author = create_test_member();
742
743 let permissions = ReportPermissions {
745 is_public: false,
746 team_access: true,
747 user_access: vec![],
748 edit_permissions: vec![author.id],
749 comment_permissions: vec![author.id],
750 };
751
752 let report_id = manager
753 .share_report(
754 "Test Report".to_string(),
755 "Report content".to_string(),
756 ReportFormat::Markdown,
757 author.clone(),
758 permissions,
759 vec![],
760 )
761 .expect("operation failed in test");
762
763 let thread_id = manager
765 .create_comment_thread(
766 report_id,
767 "Discussion about results".to_string(),
768 author.id,
769 vec!["discussion".to_string()],
770 )
771 .expect("operation failed in test");
772
773 let _comment_id = manager
775 .add_comment(thread_id, author.id, "Great analysis!".to_string(), None)
776 .expect("operation failed in test");
777
778 assert!(manager.comment_threads.contains_key(&thread_id));
779
780 let thread = manager.comment_threads.get(&thread_id).expect("expected value not found");
781 assert_eq!(thread.comments.len(), 1);
782 }
783
784 #[test]
785 fn test_collaboration_stats() {
786 let mut manager = CollaborationManager::new();
787 let author = create_test_member();
788
789 manager.add_team_member(author.clone());
791
792 let permissions = ReportPermissions {
793 is_public: false,
794 team_access: true,
795 user_access: vec![],
796 edit_permissions: vec![author.id],
797 comment_permissions: vec![author.id],
798 };
799
800 manager
801 .share_report(
802 "Test Report".to_string(),
803 "Content".to_string(),
804 ReportFormat::Markdown,
805 author,
806 permissions,
807 vec![],
808 )
809 .expect("operation failed in test");
810
811 let stats = manager.get_collaboration_stats();
812 assert_eq!(stats.total_reports, 1);
813 assert_eq!(stats.team_size, 1);
814 assert_eq!(stats.reports_per_member, 1.0);
815 }
816}