use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct CollaborationManager {
shared_reports: HashMap<Uuid, SharedReport>,
annotation_sessions: HashMap<Uuid, AnnotationSession>,
team_members: HashMap<Uuid, TeamMember>,
comment_threads: HashMap<Uuid, CommentThread>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SharedReport {
pub id: Uuid,
pub title: String,
pub content: String,
pub format: ReportFormat,
pub author: TeamMember,
pub created_at: DateTime<Utc>,
pub last_modified: DateTime<Utc>,
pub permissions: ReportPermissions,
pub tags: Vec<String>,
pub version: u32,
pub sharing_status: SharingStatus,
pub annotations: Vec<Uuid>,
pub view_count: u64,
pub collaborators: Vec<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeamMember {
pub id: Uuid,
pub name: String,
pub email: String,
pub avatar_url: Option<String>,
pub role: TeamRole,
pub timezone: String,
pub last_active: DateTime<Utc>,
pub notification_preferences: NotificationPreferences,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnnotationSession {
pub id: Uuid,
pub report_id: Uuid,
pub participants: Vec<Uuid>,
pub creator: Uuid,
pub started_at: DateTime<Utc>,
pub ended_at: Option<DateTime<Utc>>,
pub status: SessionStatus,
pub annotations: Vec<Annotation>,
pub session_type: AnnotationType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Annotation {
pub id: Uuid,
pub author: Uuid,
pub timestamp: DateTime<Utc>,
pub annotation_type: AnnotationType,
pub target: AnnotationTarget,
pub content: String,
pub importance: ImportanceLevel,
pub status: AnnotationStatus,
pub related_annotations: Vec<Uuid>,
pub attachments: Vec<Attachment>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommentThread {
pub id: Uuid,
pub report_id: Uuid,
pub subject: String,
pub creator: Uuid,
pub created_at: DateTime<Utc>,
pub status: ThreadStatus,
pub comments: Vec<Comment>,
pub participants: Vec<Uuid>,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Comment {
pub id: Uuid,
pub author: Uuid,
pub content: String,
pub timestamp: DateTime<Utc>,
pub edited_at: Option<DateTime<Utc>>,
pub parent_id: Option<Uuid>,
pub reactions: HashMap<String, Vec<Uuid>>,
pub attachments: Vec<Attachment>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attachment {
pub id: Uuid,
pub filename: String,
pub content_type: String,
pub size: u64,
pub url: String,
pub uploaded_at: DateTime<Utc>,
pub uploader: Uuid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReportFormat {
Json,
Markdown,
Html,
Pdf,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportPermissions {
pub is_public: bool,
pub team_access: bool,
pub user_access: Vec<Uuid>,
pub edit_permissions: Vec<Uuid>,
pub comment_permissions: Vec<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SharingStatus {
Private,
TeamShared,
PublicShared,
LinkShared(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TeamRole {
Owner,
Admin,
Developer,
Reviewer,
Viewer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationPreferences {
pub email_enabled: bool,
pub comment_notifications: bool,
pub annotation_notifications: bool,
pub share_notifications: bool,
pub digest_frequency: DigestFrequency,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DigestFrequency {
Immediate,
Hourly,
Daily,
Weekly,
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SessionStatus {
Active,
Paused,
Completed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AnnotationType {
Note,
Issue,
Question,
Improvement,
Bug,
Performance,
Security,
Documentation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AnnotationTarget {
Line(u32),
LineRange(u32, u32),
Section(String),
Element(String),
Custom(HashMap<String, String>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ImportanceLevel {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AnnotationStatus {
Open,
InProgress,
Resolved,
Dismissed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ThreadStatus {
Open,
Resolved,
Locked,
Archived,
}
impl CollaborationManager {
pub fn new() -> Self {
Self {
shared_reports: HashMap::new(),
annotation_sessions: HashMap::new(),
team_members: HashMap::new(),
comment_threads: HashMap::new(),
}
}
pub fn share_report(
&mut self,
title: String,
content: String,
format: ReportFormat,
author: TeamMember,
permissions: ReportPermissions,
tags: Vec<String>,
) -> Result<Uuid> {
let report_id = Uuid::new_v4();
let now = Utc::now();
let shared_report = SharedReport {
id: report_id,
title,
content,
format,
author: author.clone(),
created_at: now,
last_modified: now,
permissions,
tags,
version: 1,
sharing_status: SharingStatus::Private,
annotations: Vec::new(),
view_count: 0,
collaborators: vec![author.id],
};
self.shared_reports.insert(report_id, shared_report);
self.team_members.insert(author.id, author);
Ok(report_id)
}
pub fn get_report(&mut self, report_id: Uuid) -> Option<&mut SharedReport> {
if let Some(report) = self.shared_reports.get_mut(&report_id) {
report.view_count += 1;
Some(report)
} else {
None
}
}
pub fn start_annotation_session(
&mut self,
report_id: Uuid,
creator: Uuid,
participants: Vec<Uuid>,
session_type: AnnotationType,
) -> Result<Uuid> {
let session_id = Uuid::new_v4();
let now = Utc::now();
let session = AnnotationSession {
id: session_id,
report_id,
participants,
creator,
started_at: now,
ended_at: None,
status: SessionStatus::Active,
annotations: Vec::new(),
session_type,
};
self.annotation_sessions.insert(session_id, session);
Ok(session_id)
}
pub fn add_annotation(
&mut self,
session_id: Uuid,
author: Uuid,
annotation_type: AnnotationType,
target: AnnotationTarget,
content: String,
importance: ImportanceLevel,
) -> Result<Uuid> {
let annotation_id = Uuid::new_v4();
let now = Utc::now();
let annotation = Annotation {
id: annotation_id,
author,
timestamp: now,
annotation_type,
target,
content,
importance,
status: AnnotationStatus::Open,
related_annotations: Vec::new(),
attachments: Vec::new(),
};
if let Some(session) = self.annotation_sessions.get_mut(&session_id) {
session.annotations.push(annotation);
if let Some(report) = self.shared_reports.get_mut(&session.report_id) {
report.annotations.push(annotation_id);
report.last_modified = now;
}
Ok(annotation_id)
} else {
Err(anyhow::anyhow!("Annotation session not found"))
}
}
pub fn create_comment_thread(
&mut self,
report_id: Uuid,
subject: String,
creator: Uuid,
tags: Vec<String>,
) -> Result<Uuid> {
let thread_id = Uuid::new_v4();
let now = Utc::now();
let thread = CommentThread {
id: thread_id,
report_id,
subject,
creator,
created_at: now,
status: ThreadStatus::Open,
comments: Vec::new(),
participants: vec![creator],
tags,
};
self.comment_threads.insert(thread_id, thread);
Ok(thread_id)
}
pub fn add_comment(
&mut self,
thread_id: Uuid,
author: Uuid,
content: String,
parent_id: Option<Uuid>,
) -> Result<Uuid> {
let comment_id = Uuid::new_v4();
let now = Utc::now();
let comment = Comment {
id: comment_id,
author,
content,
timestamp: now,
edited_at: None,
parent_id,
reactions: HashMap::new(),
attachments: Vec::new(),
};
if let Some(thread) = self.comment_threads.get_mut(&thread_id) {
thread.comments.push(comment);
if !thread.participants.contains(&author) {
thread.participants.push(author);
}
Ok(comment_id)
} else {
Err(anyhow::anyhow!("Comment thread not found"))
}
}
pub fn add_team_member(&mut self, member: TeamMember) -> Uuid {
let member_id = member.id;
self.team_members.insert(member_id, member);
member_id
}
pub fn update_sharing_status(&mut self, report_id: Uuid, status: SharingStatus) -> Result<()> {
if let Some(report) = self.shared_reports.get_mut(&report_id) {
report.sharing_status = status;
report.last_modified = Utc::now();
Ok(())
} else {
Err(anyhow::anyhow!("Report not found"))
}
}
pub fn get_user_reports(&self, user_id: Uuid) -> Vec<&SharedReport> {
self.shared_reports
.values()
.filter(|report| {
report.author.id == user_id
|| report.collaborators.contains(&user_id)
|| report.permissions.user_access.contains(&user_id)
|| report.permissions.is_public
|| (report.permissions.team_access && self.team_members.contains_key(&user_id))
})
.collect()
}
pub fn get_report_annotations(&self, report_id: Uuid) -> Vec<&Annotation> {
let mut annotations = Vec::new();
for session in self.annotation_sessions.values() {
if session.report_id == report_id {
annotations.extend(session.annotations.iter());
}
}
annotations
}
pub fn get_report_threads(&self, report_id: Uuid) -> Vec<&CommentThread> {
self.comment_threads
.values()
.filter(|thread| thread.report_id == report_id)
.collect()
}
pub fn export_collaboration_data(&self) -> Result<String> {
#[derive(Serialize)]
struct CollaborationExport<'a> {
reports: Vec<&'a SharedReport>,
sessions: Vec<&'a AnnotationSession>,
threads: Vec<&'a CommentThread>,
members: Vec<&'a TeamMember>,
export_timestamp: DateTime<Utc>,
}
let export_data = CollaborationExport {
reports: self.shared_reports.values().collect(),
sessions: self.annotation_sessions.values().collect(),
threads: self.comment_threads.values().collect(),
members: self.team_members.values().collect(),
export_timestamp: Utc::now(),
};
serde_json::to_string_pretty(&export_data)
.map_err(|e| anyhow::anyhow!("Failed to export collaboration data: {}", e))
}
pub fn get_collaboration_stats(&self) -> CollaborationStats {
let total_reports = self.shared_reports.len();
let total_annotations =
self.annotation_sessions.values().map(|s| s.annotations.len()).sum();
let total_comments = self.comment_threads.values().map(|t| t.comments.len()).sum();
let active_sessions = self
.annotation_sessions
.values()
.filter(|s| matches!(s.status, SessionStatus::Active))
.count();
let team_size = self.team_members.len();
CollaborationStats {
total_reports,
total_annotations,
total_comments,
active_sessions,
team_size,
reports_per_member: if team_size > 0 {
total_reports as f64 / team_size as f64
} else {
0.0
},
annotations_per_report: if total_reports > 0 {
total_annotations as f64 / total_reports as f64
} else {
0.0
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollaborationStats {
pub total_reports: usize,
pub total_annotations: usize,
pub total_comments: usize,
pub active_sessions: usize,
pub team_size: usize,
pub reports_per_member: f64,
pub annotations_per_report: f64,
}
impl Default for CollaborationManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_member() -> TeamMember {
TeamMember {
id: Uuid::new_v4(),
name: "Test User".to_string(),
email: "test@example.com".to_string(),
avatar_url: None,
role: TeamRole::Developer,
timezone: "UTC".to_string(),
last_active: Utc::now(),
notification_preferences: NotificationPreferences {
email_enabled: true,
comment_notifications: true,
annotation_notifications: true,
share_notifications: true,
digest_frequency: DigestFrequency::Daily,
},
}
}
#[test]
fn test_share_report() {
let mut manager = CollaborationManager::new();
let author = create_test_member();
let permissions = ReportPermissions {
is_public: false,
team_access: true,
user_access: vec![],
edit_permissions: vec![author.id],
comment_permissions: vec![author.id],
};
let report_id = manager
.share_report(
"Test Report".to_string(),
"Report content".to_string(),
ReportFormat::Markdown,
author,
permissions,
vec!["test".to_string()],
)
.expect("operation failed in test");
assert!(manager.shared_reports.contains_key(&report_id));
}
#[test]
fn test_annotation_session() {
let mut manager = CollaborationManager::new();
let author = create_test_member();
let permissions = ReportPermissions {
is_public: false,
team_access: true,
user_access: vec![],
edit_permissions: vec![author.id],
comment_permissions: vec![author.id],
};
let report_id = manager
.share_report(
"Test Report".to_string(),
"Report content".to_string(),
ReportFormat::Markdown,
author.clone(),
permissions,
vec![],
)
.expect("operation failed in test");
let session_id = manager
.start_annotation_session(report_id, author.id, vec![author.id], AnnotationType::Note)
.expect("operation failed in test");
let _annotation_id = manager
.add_annotation(
session_id,
author.id,
AnnotationType::Issue,
AnnotationTarget::Line(10),
"This looks like a bug".to_string(),
ImportanceLevel::High,
)
.expect("operation failed in test");
assert!(manager.annotation_sessions.contains_key(&session_id));
let report = manager.shared_reports.get(&report_id).expect("expected value not found");
assert!(!report.annotations.is_empty());
}
#[test]
fn test_comment_thread() {
let mut manager = CollaborationManager::new();
let author = create_test_member();
let permissions = ReportPermissions {
is_public: false,
team_access: true,
user_access: vec![],
edit_permissions: vec![author.id],
comment_permissions: vec![author.id],
};
let report_id = manager
.share_report(
"Test Report".to_string(),
"Report content".to_string(),
ReportFormat::Markdown,
author.clone(),
permissions,
vec![],
)
.expect("operation failed in test");
let thread_id = manager
.create_comment_thread(
report_id,
"Discussion about results".to_string(),
author.id,
vec!["discussion".to_string()],
)
.expect("operation failed in test");
let _comment_id = manager
.add_comment(thread_id, author.id, "Great analysis!".to_string(), None)
.expect("operation failed in test");
assert!(manager.comment_threads.contains_key(&thread_id));
let thread = manager.comment_threads.get(&thread_id).expect("expected value not found");
assert_eq!(thread.comments.len(), 1);
}
#[test]
fn test_collaboration_stats() {
let mut manager = CollaborationManager::new();
let author = create_test_member();
manager.add_team_member(author.clone());
let permissions = ReportPermissions {
is_public: false,
team_access: true,
user_access: vec![],
edit_permissions: vec![author.id],
comment_permissions: vec![author.id],
};
manager
.share_report(
"Test Report".to_string(),
"Content".to_string(),
ReportFormat::Markdown,
author,
permissions,
vec![],
)
.expect("operation failed in test");
let stats = manager.get_collaboration_stats();
assert_eq!(stats.total_reports, 1);
assert_eq!(stats.team_size, 1);
assert_eq!(stats.reports_per_member, 1.0);
}
}