1use crate::error::{OptimError, Result};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::PathBuf;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct CollaborativeWorkspace {
15 pub id: String,
17 pub name: String,
19 pub members: Vec<ProjectMember>,
21 pub documents: Vec<SharedDocument>,
23 pub channels: Vec<CommunicationChannel>,
25 pub tasks: Vec<Task>,
27 pub version_control: VersionControl,
29 pub settings: WorkspaceSettings,
31 pub access_control: AccessControl,
33 pub activity_log: Vec<Activity>,
35 pub created_at: DateTime<Utc>,
37 pub modified_at: DateTime<Utc>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ProjectMember {
44 pub id: String,
46 pub user: UserInfo,
48 pub role: MemberRole,
50 pub permissions: Vec<Permission>,
52 pub joined_at: DateTime<Utc>,
54 pub last_active: DateTime<Utc>,
56 pub status: MemberStatus,
58 pub contributions: ContributionStats,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct UserInfo {
65 pub name: String,
67 pub email: String,
69 pub institution: String,
71 pub avatar_url: Option<String>,
73 pub timezone: String,
75 pub language: String,
77 pub research_interests: Vec<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83pub enum MemberRole {
84 Owner,
86 Admin,
88 PrincipalInvestigator,
90 SeniorResearcher,
92 Researcher,
94 PhDStudent,
96 MastersStudent,
98 ResearchAssistant,
100 Collaborator,
102 Guest,
104 Observer,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110pub enum Permission {
111 Read,
113 Write,
115 Delete,
117 ManageMembers,
119 ManagePermissions,
121 ManageSettings,
123 CreateExperiments,
125 RunExperiments,
127 PublishResults,
129 AccessSensitiveData,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
135pub enum MemberStatus {
136 Active,
138 Inactive,
140 OnLeave,
142 Suspended,
144 Former,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ContributionStats {
151 pub experiments_created: usize,
153 pub experiments_run: usize,
155 pub lines_of_code: usize,
157 pub documents_authored: usize,
159 pub comments_posted: usize,
161 pub reviews_conducted: usize,
163 pub contribution_score: f64,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct SharedDocument {
170 pub id: String,
172 pub name: String,
174 pub document_type: DocumentType,
176 pub content: String,
178 pub owner_id: String,
180 pub collaborators: Vec<String>,
182 pub version: u32,
184 pub version_history: Vec<DocumentVersion>,
186 pub access_permissions: DocumentPermissions,
188 pub metadata: DocumentMetadata,
190 pub created_at: DateTime<Utc>,
192 pub modified_at: DateTime<Utc>,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
198pub enum DocumentType {
199 Manuscript,
201 ExperimentNotes,
203 MeetingNotes,
205 LiteratureReview,
207 ResearchProposal,
209 DataAnalysis,
211 CodeDocumentation,
213 Presentation,
215 Other(String),
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct DocumentVersion {
222 pub version: u32,
224 pub author_id: String,
226 pub content: String,
228 pub change_summary: String,
230 pub timestamp: DateTime<Utc>,
232 pub content_hash: String,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct DocumentPermissions {
239 pub public: bool,
241 pub read_access: Vec<String>,
243 pub write_access: Vec<String>,
245 pub admin_access: Vec<String>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct DocumentMetadata {
252 pub tags: Vec<String>,
254 pub word_count: usize,
256 pub character_count: usize,
258 pub collaborator_count: usize,
260 pub version_count: usize,
262 pub last_editor_id: String,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct CommunicationChannel {
269 pub id: String,
271 pub name: String,
273 pub description: String,
275 pub channel_type: ChannelType,
277 pub members: Vec<String>,
279 pub messages: Vec<Message>,
281 pub settings: ChannelSettings,
283 pub created_by: String,
289 pub created_at: DateTime<Utc>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
295pub enum ChannelType {
296 General,
298 Experiments,
300 PaperWriting,
302 CodeReview,
304 Announcements,
306 Random,
308 Private,
310 DirectMessage,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct Message {
317 pub id: String,
319 pub author_id: String,
321 pub content: String,
323 pub message_type: MessageType,
325 pub attachments: Vec<Attachment>,
327 pub replies: Vec<Message>,
329 pub reactions: Vec<Reaction>,
331 pub mentions: Vec<String>,
333 pub thread_id: Option<String>,
335 pub timestamp: DateTime<Utc>,
337 pub edit_history: Vec<MessageEdit>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
343pub enum MessageType {
344 Text,
346 Code,
348 File,
350 System,
352 ExperimentResult,
354 TaskAssignment,
356 MeetingInvitation,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct Attachment {
363 pub filename: String,
365 pub size: usize,
367 pub mime_type: String,
369 pub file_path: String,
371 pub file_hash: String,
373 pub uploaded_at: DateTime<Utc>,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct Reaction {
380 pub emoji: String,
382 pub users: Vec<String>,
384 pub count: usize,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct MessageEdit {
391 pub original_content: String,
393 pub edited_at: DateTime<Utc>,
395 pub edit_reason: Option<String>,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct ChannelSettings {
402 pub notifications: bool,
404 pub auto_archive: bool,
406 pub archive_after_days: u32,
408 pub allow_external_invites: bool,
410 pub moderation: ModerationSettings,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct ModerationSettings {
417 pub require_approval: bool,
419 pub auto_delete_inappropriate: bool,
421 pub spam_filtering: bool,
423 pub moderators: Vec<String>,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct Task {
430 pub id: String,
432 pub title: String,
434 pub description: String,
436 pub task_type: TaskType,
438 pub status: TaskStatus,
440 pub priority: TaskPriority,
442 pub assigned_to: Vec<String>,
444 pub created_by: String,
446 pub due_date: Option<DateTime<Utc>>,
448 pub estimated_hours: Option<f64>,
450 pub actual_hours: Option<f64>,
452 pub dependencies: Vec<String>,
454 pub subtasks: Vec<Task>,
456 pub comments: Vec<TaskComment>,
458 pub attachments: Vec<Attachment>,
460 pub labels: Vec<String>,
462 pub created_at: DateTime<Utc>,
464 pub completed_at: Option<DateTime<Utc>>,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
470pub enum TaskType {
471 ExperimentDesign,
473 DataCollection,
475 DataAnalysis,
477 CodeDevelopment,
479 Documentation,
481 LiteratureReview,
483 PaperWriting,
485 Review,
487 Meeting,
489 Administrative,
491 Other(String),
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
497pub enum TaskStatus {
498 NotStarted,
500 InProgress,
502 OnHold,
504 Completed,
506 Cancelled,
508 NeedsReview,
510 Approved,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
516pub enum TaskPriority {
517 Critical,
519 High,
521 Medium,
523 Low,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct TaskComment {
530 pub id: String,
532 pub author_id: String,
534 pub content: String,
536 pub timestamp: DateTime<Utc>,
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct VersionControl {
543 pub repository_url: Option<String>,
545 pub current_branch: String,
547 pub branches: Vec<String>,
549 pub commits: Vec<Commit>,
551 pub merge_requests: Vec<MergeRequest>,
553 pub settings: VersionControlSettings,
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct Commit {
560 pub hash: String,
562 pub author: String,
564 pub message: String,
566 pub timestamp: DateTime<Utc>,
568 pub modified_files: Vec<String>,
570 pub parents: Vec<String>,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize)]
576pub struct MergeRequest {
577 pub id: String,
579 pub title: String,
581 pub description: String,
583 pub source_branch: String,
585 pub target_branch: String,
587 pub author: String,
589 pub reviewers: Vec<String>,
591 pub status: MergeRequestStatus,
593 pub created_at: DateTime<Utc>,
595 pub merged_at: Option<DateTime<Utc>>,
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
601pub enum MergeRequestStatus {
602 Open,
604 UnderReview,
606 Approved,
608 Merged,
610 Closed,
612 Draft,
614}
615
616#[derive(Debug, Clone, Serialize, Deserialize)]
618pub struct VersionControlSettings {
619 pub auto_commit: bool,
621 pub auto_commit_frequency: u32,
623 pub require_review: bool,
625 pub protected_branches: Vec<String>,
627 pub automatic_backups: bool,
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct WorkspaceSettings {
634 pub timezone: String,
636 pub default_language: String,
638 pub collaboration: CollaborationSettings,
640 pub notifications: NotificationSettings,
642 pub integrations: IntegrationSettings,
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct CollaborationSettings {
649 pub real_time_editing: bool,
651 pub auto_save_frequency: u32,
653 pub conflict_resolution: ConflictResolution,
655 pub max_simultaneous_editors: u32,
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
661pub enum ConflictResolution {
662 Manual,
664 LastWriterWins,
666 FirstWriterWins,
668 Merge,
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize)]
674pub struct NotificationSettings {
675 pub email: bool,
677 pub in_app: bool,
679 pub desktop: bool,
681 pub mobile_push: bool,
683 pub frequency: NotificationFrequency,
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
689pub enum NotificationFrequency {
690 Immediate,
692 Hourly,
694 Daily,
696 Weekly,
698 None,
700}
701
702#[derive(Debug, Clone, Serialize, Deserialize, Default)]
704pub struct IntegrationSettings {
705 pub slack: Option<SlackIntegration>,
707 pub email: Option<EmailIntegration>,
709 pub calendar: Option<CalendarIntegration>,
711 pub cloud_storage: Option<CloudStorageIntegration>,
713}
714
715#[derive(Debug, Clone, Serialize, Deserialize)]
717pub struct SlackIntegration {
718 pub webhook_url: String,
720 pub default_channel: String,
722 pub experiment_notifications: bool,
724 pub task_notifications: bool,
726}
727
728#[derive(Debug, Clone, Serialize, Deserialize)]
730pub struct EmailIntegration {
731 pub smtp_server: String,
733 pub smtp_port: u16,
735 pub email_address: String,
737 pub auth_credentials: Option<String>,
739}
740
741#[derive(Debug, Clone, Serialize, Deserialize)]
743pub struct CalendarIntegration {
744 pub provider: CalendarProvider,
746 pub calendar_id: String,
748 pub sync_meetings: bool,
750 pub sync_deadlines: bool,
752}
753
754#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
756pub enum CalendarProvider {
757 Google,
759 Outlook,
761 Apple,
763 CalDAV,
765}
766
767#[derive(Debug, Clone, Serialize, Deserialize)]
769pub struct CloudStorageIntegration {
770 pub provider: CloudStorageProvider,
772 pub storage_path: String,
774 pub auto_sync: bool,
776 pub sync_frequency: u32,
778}
779
780#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
782pub enum CloudStorageProvider {
783 GoogleDrive,
785 Dropbox,
787 OneDrive,
789 AmazonS3,
791 Custom(String),
793}
794
795#[derive(Debug, Clone, Serialize, Deserialize)]
797pub struct AccessControl {
798 pub acls: Vec<AccessControlEntry>,
800 pub default_permissions: Vec<Permission>,
802 pub guest_access: bool,
804 pub public_visibility: bool,
806 pub invitation_settings: InvitationSettings,
808}
809
810#[derive(Debug, Clone, Serialize, Deserialize)]
812pub struct AccessControlEntry {
813 pub principal: Principal,
815 pub permissions: Vec<Permission>,
817 pub expires_at: Option<DateTime<Utc>>,
819}
820
821#[derive(Debug, Clone, Serialize, Deserialize)]
823pub enum Principal {
824 User(String),
826 Group(String),
828 Role(MemberRole),
830 Everyone,
832}
833
834#[derive(Debug, Clone, Serialize, Deserialize)]
836pub struct InvitationSettings {
837 pub require_approval: bool,
839 pub allow_external: bool,
841 pub expiration_days: u32,
843 pub max_invitations_per_user: u32,
845}
846
847#[derive(Debug, Clone, Serialize, Deserialize)]
849pub struct Activity {
850 pub id: String,
852 pub user_id: String,
854 pub activity_type: ActivityType,
856 pub description: String,
858 pub resources: Vec<String>,
860 pub metadata: HashMap<String, serde_json::Value>,
862 pub timestamp: DateTime<Utc>,
864}
865
866#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
868pub enum ActivityType {
869 UserJoined,
871 UserLeft,
873 DocumentCreated,
875 DocumentEdited,
877 DocumentDeleted,
879 ExperimentCreated,
881 ExperimentStarted,
883 ExperimentCompleted,
885 TaskCreated,
887 TaskAssigned,
889 TaskCompleted,
891 MessagePosted,
893 FileUploaded,
895 MergeRequestCreated,
897 SettingsChanged,
899}
900
901#[derive(Debug)]
903pub struct CollaborationManager {
904 workspaces: HashMap<String, CollaborativeWorkspace>,
906 storage_dir: PathBuf,
908 settings: CollaborationManagerSettings,
910}
911
912#[derive(Debug, Clone, Serialize, Deserialize)]
914pub struct CollaborationManagerSettings {
915 pub max_workspaces_per_user: u32,
917 pub default_workspace_settings: WorkspaceSettings,
919 pub backup_settings: BackupSettings,
921}
922
923#[derive(Debug, Clone, Serialize, Deserialize)]
925pub struct BackupSettings {
926 pub enabled: bool,
928 pub frequency_hours: u32,
930 pub retention_days: u32,
932 pub backup_location: PathBuf,
934}
935
936impl CollaborativeWorkspace {
937 pub fn new(name: &str, owner: UserInfo) -> Self {
939 let now = Utc::now();
940 let workspace_id = uuid::Uuid::new_v4().to_string();
941 let owner_id = uuid::Uuid::new_v4().to_string();
942
943 let owner_member = ProjectMember {
944 id: owner_id.clone(),
945 user: owner,
946 role: MemberRole::Owner,
947 permissions: vec![
948 Permission::Read,
949 Permission::Write,
950 Permission::Delete,
951 Permission::ManageMembers,
952 Permission::ManagePermissions,
953 Permission::ManageSettings,
954 Permission::CreateExperiments,
955 Permission::RunExperiments,
956 Permission::PublishResults,
957 Permission::AccessSensitiveData,
958 ],
959 joined_at: now,
960 last_active: now,
961 status: MemberStatus::Active,
962 contributions: ContributionStats::default(),
963 };
964
965 Self {
966 id: workspace_id,
967 name: name.to_string(),
968 members: vec![owner_member],
969 documents: Vec::new(),
970 channels: Vec::new(),
971 tasks: Vec::new(),
972 version_control: VersionControl::default(),
973 settings: WorkspaceSettings::default(),
974 access_control: AccessControl::default(),
975 activity_log: Vec::new(),
976 created_at: now,
977 modified_at: now,
978 }
979 }
980
981 pub fn add_member(&mut self, user: UserInfo, role: MemberRole) -> Result<()> {
983 let member_id = uuid::Uuid::new_v4().to_string();
984 let permissions = self.get_default_permissions_for_role(&role);
985
986 let member = ProjectMember {
987 id: member_id.clone(),
988 user,
989 role,
990 permissions,
991 joined_at: Utc::now(),
992 last_active: Utc::now(),
993 status: MemberStatus::Active,
994 contributions: ContributionStats::default(),
995 };
996
997 self.members.push(member);
998 self.log_activity(
999 &member_id,
1000 ActivityType::UserJoined,
1001 "User joined the workspace".to_string(),
1002 vec![],
1003 );
1004
1005 Ok(())
1006 }
1007
1008 pub fn create_document(
1010 &mut self,
1011 name: &str,
1012 document_type: DocumentType,
1013 ownerid: &str,
1014 ) -> Result<String> {
1015 if !self.has_permission(ownerid, &Permission::Write) {
1016 return Err(OptimError::InvalidConfig(
1017 "Insufficient permissions to create document".to_string(),
1018 ));
1019 }
1020
1021 let document_id = uuid::Uuid::new_v4().to_string();
1022 let document = SharedDocument {
1023 id: document_id.clone(),
1024 name: name.to_string(),
1025 document_type,
1026 content: String::new(),
1027 owner_id: ownerid.to_string(),
1028 collaborators: Vec::new(),
1029 version: 1,
1030 version_history: Vec::new(),
1031 access_permissions: DocumentPermissions {
1032 public: false,
1033 read_access: self.members.iter().map(|m| m.id.clone()).collect(),
1034 write_access: vec![ownerid.to_string()],
1035 admin_access: vec![ownerid.to_string()],
1036 },
1037 metadata: DocumentMetadata {
1038 tags: Vec::new(),
1039 word_count: 0,
1040 character_count: 0,
1041 collaborator_count: 1,
1042 version_count: 1,
1043 last_editor_id: ownerid.to_string(),
1044 },
1045 created_at: Utc::now(),
1046 modified_at: Utc::now(),
1047 };
1048
1049 self.documents.push(document);
1050 self.log_activity(
1051 ownerid,
1052 ActivityType::DocumentCreated,
1053 format!("Created document: {name}"),
1054 vec![document_id.clone()],
1055 );
1056
1057 Ok(document_id)
1058 }
1059
1060 pub fn create_channel(
1062 &mut self,
1063 name: &str,
1064 channel_type: ChannelType,
1065 creatorid: &str,
1066 ) -> Result<String> {
1067 let channel_id = uuid::Uuid::new_v4().to_string();
1068 let mut members: Vec<String> = self.members.iter().map(|m| m.id.clone()).collect();
1069 if !members.iter().any(|id| id == creatorid) {
1070 members.push(creatorid.to_string());
1071 }
1072 let channel = CommunicationChannel {
1073 id: channel_id.clone(),
1074 name: name.to_string(),
1075 description: String::new(),
1076 channel_type,
1077 members,
1078 messages: Vec::new(),
1079 settings: ChannelSettings::default(),
1080 created_by: creatorid.to_string(),
1081 created_at: Utc::now(),
1082 };
1083
1084 self.channels.push(channel);
1085 Ok(channel_id)
1086 }
1087
1088 pub fn create_task(
1090 &mut self,
1091 title: &str,
1092 task_type: TaskType,
1093 creatorid: &str,
1094 ) -> Result<String> {
1095 let task_id = uuid::Uuid::new_v4().to_string();
1096 let task = Task {
1097 id: task_id.clone(),
1098 title: title.to_string(),
1099 description: String::new(),
1100 task_type,
1101 status: TaskStatus::NotStarted,
1102 priority: TaskPriority::Medium,
1103 assigned_to: Vec::new(),
1104 created_by: creatorid.to_string(),
1105 due_date: None,
1106 estimated_hours: None,
1107 actual_hours: None,
1108 dependencies: Vec::new(),
1109 subtasks: Vec::new(),
1110 comments: Vec::new(),
1111 attachments: Vec::new(),
1112 labels: Vec::new(),
1113 created_at: Utc::now(),
1114 completed_at: None,
1115 };
1116
1117 self.tasks.push(task);
1118 self.log_activity(
1119 creatorid,
1120 ActivityType::TaskCreated,
1121 format!("Created task: {title}"),
1122 vec![task_id.clone()],
1123 );
1124
1125 Ok(task_id)
1126 }
1127
1128 pub fn has_permission(&self, userid: &str, permission: &Permission) -> bool {
1130 if let Some(member) = self.members.iter().find(|m| m.id == userid) {
1131 member.permissions.contains(permission)
1132 } else {
1133 false
1134 }
1135 }
1136
1137 pub fn log_activity(
1139 &mut self,
1140 user_id: &str,
1141 activitytype: ActivityType,
1142 description: String,
1143 resources: Vec<String>,
1144 ) {
1145 let activity = Activity {
1146 id: uuid::Uuid::new_v4().to_string(),
1147 user_id: user_id.to_string(),
1148 activity_type: activitytype,
1149 description,
1150 resources,
1151 metadata: HashMap::new(),
1152 timestamp: Utc::now(),
1153 };
1154
1155 self.activity_log.push(activity);
1156 self.modified_at = Utc::now();
1157 }
1158
1159 fn get_default_permissions_for_role(&self, role: &MemberRole) -> Vec<Permission> {
1160 match role {
1161 MemberRole::Owner | MemberRole::Admin => vec![
1162 Permission::Read,
1163 Permission::Write,
1164 Permission::Delete,
1165 Permission::ManageMembers,
1166 Permission::ManagePermissions,
1167 Permission::ManageSettings,
1168 Permission::CreateExperiments,
1169 Permission::RunExperiments,
1170 Permission::PublishResults,
1171 Permission::AccessSensitiveData,
1172 ],
1173 MemberRole::PrincipalInvestigator | MemberRole::SeniorResearcher => vec![
1174 Permission::Read,
1175 Permission::Write,
1176 Permission::CreateExperiments,
1177 Permission::RunExperiments,
1178 Permission::PublishResults,
1179 Permission::AccessSensitiveData,
1180 ],
1181 MemberRole::Researcher | MemberRole::PhDStudent => vec![
1182 Permission::Read,
1183 Permission::Write,
1184 Permission::CreateExperiments,
1185 Permission::RunExperiments,
1186 ],
1187 MemberRole::MastersStudent | MemberRole::ResearchAssistant => vec![
1188 Permission::Read,
1189 Permission::Write,
1190 Permission::CreateExperiments,
1191 ],
1192 MemberRole::Collaborator => vec![Permission::Read, Permission::Write],
1193 MemberRole::Guest | MemberRole::Observer => vec![Permission::Read],
1194 }
1195 }
1196
1197 pub fn generate_statistics(&self) -> WorkspaceStatistics {
1199 let active_members = self
1200 .members
1201 .iter()
1202 .filter(|m| m.status == MemberStatus::Active)
1203 .count();
1204
1205 let total_documents = self.documents.len();
1206 let total_tasks = self.tasks.len();
1207 let completed_tasks = self
1208 .tasks
1209 .iter()
1210 .filter(|t| t.status == TaskStatus::Completed)
1211 .count();
1212
1213 let total_messages = self.channels.iter().map(|c| c.messages.len()).sum();
1214
1215 let activity_last_30_days = self
1216 .activity_log
1217 .iter()
1218 .filter(|a| {
1219 let thirty_days_ago = Utc::now() - chrono::Duration::days(30);
1220 a.timestamp > thirty_days_ago
1221 })
1222 .count();
1223
1224 WorkspaceStatistics {
1225 total_members: self.members.len(),
1226 active_members,
1227 total_documents,
1228 total_tasks,
1229 completed_tasks,
1230 task_completion_rate: if total_tasks > 0 {
1231 completed_tasks as f64 / total_tasks as f64
1232 } else {
1233 0.0
1234 },
1235 total_messages,
1236 total_channels: self.channels.len(),
1237 activity_last_30_days,
1238 creation_date: self.created_at,
1239 last_activity: self.modified_at,
1240 }
1241 }
1242}
1243
1244#[derive(Debug, Clone, Serialize, Deserialize)]
1246pub struct WorkspaceStatistics {
1247 pub total_members: usize,
1249 pub active_members: usize,
1251 pub total_documents: usize,
1253 pub total_tasks: usize,
1255 pub completed_tasks: usize,
1257 pub task_completion_rate: f64,
1259 pub total_messages: usize,
1261 pub total_channels: usize,
1263 pub activity_last_30_days: usize,
1265 pub creation_date: DateTime<Utc>,
1267 pub last_activity: DateTime<Utc>,
1269}
1270
1271impl Default for ContributionStats {
1272 fn default() -> Self {
1273 Self {
1274 experiments_created: 0,
1275 experiments_run: 0,
1276 lines_of_code: 0,
1277 documents_authored: 0,
1278 comments_posted: 0,
1279 reviews_conducted: 0,
1280 contribution_score: 0.0,
1281 }
1282 }
1283}
1284
1285impl Default for VersionControl {
1286 fn default() -> Self {
1287 Self {
1288 repository_url: None,
1289 current_branch: "main".to_string(),
1290 branches: vec!["main".to_string()],
1291 commits: Vec::new(),
1292 merge_requests: Vec::new(),
1293 settings: VersionControlSettings::default(),
1294 }
1295 }
1296}
1297
1298impl Default for VersionControlSettings {
1299 fn default() -> Self {
1300 Self {
1301 auto_commit: false,
1302 auto_commit_frequency: 60, require_review: true,
1304 protected_branches: vec!["main".to_string(), "master".to_string()],
1305 automatic_backups: true,
1306 }
1307 }
1308}
1309
1310impl Default for WorkspaceSettings {
1311 fn default() -> Self {
1312 Self {
1313 timezone: "UTC".to_string(),
1314 default_language: "en".to_string(),
1315 collaboration: CollaborationSettings::default(),
1316 notifications: NotificationSettings::default(),
1317 integrations: IntegrationSettings::default(),
1318 }
1319 }
1320}
1321
1322impl Default for CollaborationSettings {
1323 fn default() -> Self {
1324 Self {
1325 real_time_editing: true,
1326 auto_save_frequency: 30, conflict_resolution: ConflictResolution::Manual,
1328 max_simultaneous_editors: 10,
1329 }
1330 }
1331}
1332
1333impl Default for NotificationSettings {
1334 fn default() -> Self {
1335 Self {
1336 email: true,
1337 in_app: true,
1338 desktop: false,
1339 mobile_push: false,
1340 frequency: NotificationFrequency::Daily,
1341 }
1342 }
1343}
1344
1345impl Default for AccessControl {
1346 fn default() -> Self {
1347 Self {
1348 acls: Vec::new(),
1349 default_permissions: vec![Permission::Read],
1350 guest_access: false,
1351 public_visibility: false,
1352 invitation_settings: InvitationSettings::default(),
1353 }
1354 }
1355}
1356
1357impl Default for InvitationSettings {
1358 fn default() -> Self {
1359 Self {
1360 require_approval: true,
1361 allow_external: false,
1362 expiration_days: 7,
1363 max_invitations_per_user: 10,
1364 }
1365 }
1366}
1367
1368impl Default for ChannelSettings {
1369 fn default() -> Self {
1370 Self {
1371 notifications: true,
1372 auto_archive: false,
1373 archive_after_days: 365,
1374 allow_external_invites: false,
1375 moderation: ModerationSettings::default(),
1376 }
1377 }
1378}
1379
1380impl Default for ModerationSettings {
1381 fn default() -> Self {
1382 Self {
1383 require_approval: false,
1384 auto_delete_inappropriate: false,
1385 spam_filtering: true,
1386 moderators: Vec::new(),
1387 }
1388 }
1389}
1390
1391impl Default for BackupSettings {
1392 fn default() -> Self {
1393 Self {
1394 enabled: false,
1395 frequency_hours: 24,
1396 retention_days: 30,
1397 backup_location: PathBuf::from("./backups"),
1398 }
1399 }
1400}
1401
1402impl Default for CollaborationManagerSettings {
1403 fn default() -> Self {
1404 Self {
1405 max_workspaces_per_user: 10,
1406 default_workspace_settings: WorkspaceSettings::default(),
1407 backup_settings: BackupSettings::default(),
1408 }
1409 }
1410}
1411
1412impl CollaborationManager {
1413 pub fn new(storage_dir: PathBuf, settings: CollaborationManagerSettings) -> Result<Self> {
1416 std::fs::create_dir_all(&storage_dir).map_err(|e| {
1417 OptimError::InvalidConfig(format!(
1418 "failed to create collaboration storage directory {}: {e}",
1419 storage_dir.display()
1420 ))
1421 })?;
1422
1423 Ok(Self {
1424 workspaces: HashMap::new(),
1425 storage_dir,
1426 settings,
1427 })
1428 }
1429
1430 pub fn create_workspace(&mut self, name: &str, owner: UserInfo) -> Result<String> {
1433 let owned_count = self
1434 .workspaces
1435 .values()
1436 .filter(|ws| {
1437 ws.members
1438 .iter()
1439 .any(|m| m.role == MemberRole::Owner && m.user.email == owner.email)
1440 })
1441 .count();
1442
1443 if owned_count as u32 >= self.settings.max_workspaces_per_user {
1444 return Err(OptimError::InvalidConfig(format!(
1445 "user '{}' already owns the maximum of {} workspaces",
1446 owner.email, self.settings.max_workspaces_per_user
1447 )));
1448 }
1449
1450 let workspace = CollaborativeWorkspace::new(name, owner);
1451 let id = workspace.id.clone();
1452 self.workspaces.insert(id.clone(), workspace);
1453 Ok(id)
1454 }
1455
1456 pub fn get_workspace(&self, id: &str) -> Option<&CollaborativeWorkspace> {
1458 self.workspaces.get(id)
1459 }
1460
1461 pub fn get_workspace_mut(&mut self, id: &str) -> Option<&mut CollaborativeWorkspace> {
1463 self.workspaces.get_mut(id)
1464 }
1465
1466 pub fn list_workspaces(&self) -> impl Iterator<Item = &CollaborativeWorkspace> {
1468 self.workspaces.values()
1469 }
1470
1471 pub fn remove_workspace(&mut self, id: &str) -> Option<CollaborativeWorkspace> {
1473 self.workspaces.remove(id)
1474 }
1475
1476 pub fn save_workspace(&self, id: &str) -> Result<PathBuf> {
1478 let workspace = self
1479 .workspaces
1480 .get(id)
1481 .ok_or_else(|| OptimError::InvalidConfig(format!("workspace '{id}' not found")))?;
1482
1483 let path = self.storage_dir.join(format!("{id}.json"));
1484 let json = serde_json::to_string_pretty(workspace).map_err(|e| {
1485 OptimError::InvalidConfig(format!("failed to serialize workspace '{id}': {e}"))
1486 })?;
1487 std::fs::write(&path, json).map_err(|e| {
1488 OptimError::InvalidConfig(format!("failed to write {}: {e}", path.display()))
1489 })?;
1490
1491 Ok(path)
1492 }
1493
1494 pub fn load_workspace(&mut self, id: &str) -> Result<()> {
1497 let path = self.storage_dir.join(format!("{id}.json"));
1498 let json = std::fs::read_to_string(&path).map_err(|e| {
1499 OptimError::InvalidConfig(format!("failed to read {}: {e}", path.display()))
1500 })?;
1501 let workspace: CollaborativeWorkspace = serde_json::from_str(&json).map_err(|e| {
1502 OptimError::InvalidConfig(format!("failed to parse workspace '{id}': {e}"))
1503 })?;
1504
1505 self.workspaces.insert(id.to_string(), workspace);
1506 Ok(())
1507 }
1508
1509 pub fn settings(&self) -> &CollaborationManagerSettings {
1511 &self.settings
1512 }
1513
1514 pub fn storage_dir(&self) -> &std::path::Path {
1516 &self.storage_dir
1517 }
1518}
1519
1520#[cfg(test)]
1521mod tests {
1522 use super::*;
1523
1524 #[test]
1525 fn test_workspace_creation() {
1526 let owner = UserInfo {
1527 name: "Dr. Test".to_string(),
1528 email: "test@example.com".to_string(),
1529 institution: "Test University".to_string(),
1530 avatar_url: None,
1531 timezone: "UTC".to_string(),
1532 language: "en".to_string(),
1533 research_interests: vec!["machine learning".to_string()],
1534 };
1535
1536 let workspace = CollaborativeWorkspace::new("Test Workspace", owner);
1537
1538 assert_eq!(workspace.name, "Test Workspace");
1539 assert_eq!(workspace.members.len(), 1);
1540 assert_eq!(workspace.members[0].role, MemberRole::Owner);
1541 assert!(workspace.members[0]
1542 .permissions
1543 .contains(&Permission::ManageMembers));
1544 }
1545
1546 #[test]
1547 fn test_document_creation() {
1548 let owner = UserInfo {
1549 name: "Dr. Test".to_string(),
1550 email: "test@example.com".to_string(),
1551 institution: "Test University".to_string(),
1552 avatar_url: None,
1553 timezone: "UTC".to_string(),
1554 language: "en".to_string(),
1555 research_interests: vec![],
1556 };
1557
1558 let mut workspace = CollaborativeWorkspace::new("Test Workspace", owner);
1559 let owner_id = workspace.members[0].id.clone();
1560
1561 let doc_id = workspace
1562 .create_document("Test Document", DocumentType::Manuscript, &owner_id)
1563 .expect("create_document");
1564
1565 assert_eq!(workspace.documents.len(), 1);
1566 assert_eq!(workspace.documents[0].id, doc_id);
1567 assert_eq!(workspace.documents[0].name, "Test Document");
1568 assert_eq!(
1569 workspace.documents[0].document_type,
1570 DocumentType::Manuscript
1571 );
1572 assert_eq!(workspace.activity_log.len(), 1);
1573 assert_eq!(
1574 workspace.activity_log[0].activity_type,
1575 ActivityType::DocumentCreated
1576 );
1577 }
1578
1579 #[test]
1580 fn test_permission_checking() {
1581 let owner = UserInfo {
1582 name: "Dr. Test".to_string(),
1583 email: "test@example.com".to_string(),
1584 institution: "Test University".to_string(),
1585 avatar_url: None,
1586 timezone: "UTC".to_string(),
1587 language: "en".to_string(),
1588 research_interests: vec![],
1589 };
1590
1591 let workspace = CollaborativeWorkspace::new("Test Workspace", owner);
1592 let owner_id = &workspace.members[0].id;
1593
1594 assert!(workspace.has_permission(owner_id, &Permission::Read));
1595 assert!(workspace.has_permission(owner_id, &Permission::Write));
1596 assert!(workspace.has_permission(owner_id, &Permission::ManageMembers));
1597
1598 assert!(!workspace.has_permission("non-existent", &Permission::Read));
1600 }
1601
1602 fn test_owner(email: &str) -> UserInfo {
1603 UserInfo {
1604 name: "Dr. Test".to_string(),
1605 email: email.to_string(),
1606 institution: "Test University".to_string(),
1607 avatar_url: None,
1608 timezone: "UTC".to_string(),
1609 language: "en".to_string(),
1610 research_interests: vec![],
1611 }
1612 }
1613
1614 #[test]
1619 fn test_collaboration_manager_is_constructible_and_functional() {
1620 let dir = std::env::temp_dir().join(format!("optirs_collab_test_{}", uuid::Uuid::new_v4()));
1621
1622 let mut manager =
1623 CollaborationManager::new(dir.clone(), CollaborationManagerSettings::default())
1624 .expect("manager creation should succeed");
1625 assert!(dir.exists());
1626
1627 let workspace_id = manager
1628 .create_workspace("Test Workspace", test_owner("owner@example.com"))
1629 .expect("workspace creation should succeed");
1630
1631 assert!(manager.get_workspace(&workspace_id).is_some());
1632 assert_eq!(manager.list_workspaces().count(), 1);
1633
1634 let saved_path = manager
1635 .save_workspace(&workspace_id)
1636 .expect("save should succeed");
1637 assert!(saved_path.exists());
1638
1639 manager.remove_workspace(&workspace_id);
1640 assert!(manager.get_workspace(&workspace_id).is_none());
1641
1642 manager
1643 .load_workspace(&workspace_id)
1644 .expect("load should succeed");
1645 assert!(manager.get_workspace(&workspace_id).is_some());
1646 assert_eq!(
1647 manager.get_workspace(&workspace_id).unwrap().name,
1648 "Test Workspace"
1649 );
1650
1651 let _ = std::fs::remove_dir_all(&dir);
1652 }
1653
1654 #[test]
1655 fn test_collaboration_manager_enforces_workspace_limit() {
1656 let dir =
1657 std::env::temp_dir().join(format!("optirs_collab_limit_test_{}", uuid::Uuid::new_v4()));
1658 let settings = CollaborationManagerSettings {
1659 max_workspaces_per_user: 1,
1660 ..CollaborationManagerSettings::default()
1661 };
1662
1663 let mut manager =
1664 CollaborationManager::new(dir.clone(), settings).expect("manager creation");
1665
1666 manager
1667 .create_workspace("First", test_owner("limited@example.com"))
1668 .expect("first workspace should succeed");
1669
1670 let second = manager.create_workspace("Second", test_owner("limited@example.com"));
1671 assert!(second.is_err(), "second workspace should exceed the limit");
1672
1673 let _ = std::fs::remove_dir_all(&dir);
1674 }
1675}