Some checks failed
BotServer CI / build (push) Failing after 1m34s
Split 20+ files over 1000 lines into focused subdirectories for better maintainability and code organization. All changes maintain backward compatibility through re-export wrappers. Major splits: - attendance/llm_assist.rs (2074→7 modules) - basic/keywords/face_api.rs → face_api/ (7 modules) - basic/keywords/file_operations.rs → file_ops/ (8 modules) - basic/keywords/hear_talk.rs → hearing/ (6 modules) - channels/wechat.rs → wechat/ (10 modules) - channels/youtube.rs → youtube/ (5 modules) - contacts/mod.rs → contacts_api/ (6 modules) - core/bootstrap/mod.rs → bootstrap/ (5 modules) - core/shared/admin.rs → admin_*.rs (5 modules) - designer/canvas.rs → canvas_api/ (6 modules) - designer/mod.rs → designer_api/ (6 modules) - docs/handlers.rs → handlers_api/ (11 modules) - drive/mod.rs → drive_handlers.rs, drive_types.rs - learn/mod.rs → types.rs - main.rs → main_module/ (7 modules) - meet/webinar.rs → webinar_api/ (8 modules) - paper/mod.rs → (10 modules) - security/auth.rs → auth_api/ (7 modules) - security/passkey.rs → (4 modules) - sources/mod.rs → sources_api/ (5 modules) - tasks/mod.rs → task_api/ (5 modules) Stats: 38,040 deletions, 1,315 additions across 318 files Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1178 lines
38 KiB
Rust
1178 lines
38 KiB
Rust
use axum::{
|
|
extract::{Path, Query, State},
|
|
response::IntoResponse,
|
|
routing::{delete, get, post},
|
|
Json, Router,
|
|
};
|
|
use chrono::{DateTime, Utc};
|
|
use diesel::prelude::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
use crate::core::shared::schema::{calendar_events, crm_contacts};
|
|
use crate::core::shared::state::AppState;
|
|
use crate::core::shared::utils::DbPool;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct EventContact {
|
|
pub id: Uuid,
|
|
pub event_id: Uuid,
|
|
pub contact_id: Uuid,
|
|
pub role: EventContactRole,
|
|
pub response_status: ResponseStatus,
|
|
pub notified: bool,
|
|
pub notified_at: Option<DateTime<Utc>>,
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
|
pub enum EventContactRole {
|
|
#[default]
|
|
Attendee,
|
|
Organizer,
|
|
OptionalAttendee,
|
|
Resource,
|
|
Speaker,
|
|
Host,
|
|
}
|
|
|
|
impl std::fmt::Display for EventContactRole {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
EventContactRole::Attendee => write!(f, "attendee"),
|
|
EventContactRole::Organizer => write!(f, "organizer"),
|
|
EventContactRole::OptionalAttendee => write!(f, "optional"),
|
|
EventContactRole::Resource => write!(f, "resource"),
|
|
EventContactRole::Speaker => write!(f, "speaker"),
|
|
EventContactRole::Host => write!(f, "host"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
|
pub enum ResponseStatus {
|
|
#[default]
|
|
NeedsAction,
|
|
Accepted,
|
|
Declined,
|
|
Tentative,
|
|
Delegated,
|
|
}
|
|
|
|
impl std::fmt::Display for ResponseStatus {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
ResponseStatus::NeedsAction => write!(f, "needs_action"),
|
|
ResponseStatus::Accepted => write!(f, "accepted"),
|
|
ResponseStatus::Declined => write!(f, "declined"),
|
|
ResponseStatus::Tentative => write!(f, "tentative"),
|
|
ResponseStatus::Delegated => write!(f, "delegated"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct LinkContactRequest {
|
|
pub contact_id: Uuid,
|
|
pub role: Option<EventContactRole>,
|
|
pub send_notification: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BulkLinkContactsRequest {
|
|
pub contact_ids: Vec<Uuid>,
|
|
pub role: Option<EventContactRole>,
|
|
pub send_notification: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct UpdateEventContactRequest {
|
|
pub role: Option<EventContactRole>,
|
|
pub response_status: Option<ResponseStatus>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct EventContactsQuery {
|
|
pub role: Option<EventContactRole>,
|
|
pub response_status: Option<ResponseStatus>,
|
|
pub include_contact_details: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ContactEventsQuery {
|
|
pub from_date: Option<DateTime<Utc>>,
|
|
pub to_date: Option<DateTime<Utc>>,
|
|
pub role: Option<EventContactRole>,
|
|
pub response_status: Option<ResponseStatus>,
|
|
pub limit: Option<u32>,
|
|
pub offset: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct EventContactWithDetails {
|
|
pub event_contact: EventContact,
|
|
pub contact: ContactSummary,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ContactSummary {
|
|
pub id: Uuid,
|
|
pub first_name: String,
|
|
pub last_name: String,
|
|
pub email: Option<String>,
|
|
pub phone: Option<String>,
|
|
pub company: Option<String>,
|
|
pub job_title: Option<String>,
|
|
pub avatar_url: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct EventSummary {
|
|
pub id: Uuid,
|
|
pub title: String,
|
|
pub description: Option<String>,
|
|
pub start_time: DateTime<Utc>,
|
|
pub end_time: DateTime<Utc>,
|
|
pub location: Option<String>,
|
|
pub is_recurring: bool,
|
|
pub organizer_name: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ContactEventWithDetails {
|
|
pub event_contact: EventContact,
|
|
pub event: EventSummary,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ContactEventsResponse {
|
|
pub events: Vec<ContactEventWithDetails>,
|
|
pub total_count: u32,
|
|
pub upcoming_count: u32,
|
|
pub past_count: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SuggestedContact {
|
|
pub contact: ContactSummary,
|
|
pub reason: SuggestionReason,
|
|
pub score: f32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum SuggestionReason {
|
|
FrequentCollaborator,
|
|
SameCompany,
|
|
PreviousAttendee,
|
|
RelatedProject,
|
|
OrganizationMember,
|
|
RecentlyContacted,
|
|
}
|
|
|
|
impl std::fmt::Display for SuggestionReason {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
SuggestionReason::FrequentCollaborator => write!(f, "Frequent collaborator"),
|
|
SuggestionReason::SameCompany => write!(f, "Same company"),
|
|
SuggestionReason::PreviousAttendee => write!(f, "Previously attended similar events"),
|
|
SuggestionReason::RelatedProject => write!(f, "Related to project"),
|
|
SuggestionReason::OrganizationMember => write!(f, "Organization member"),
|
|
SuggestionReason::RecentlyContacted => write!(f, "Recently contacted"),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct CalendarIntegrationService {
|
|
db_pool: DbPool,
|
|
}
|
|
|
|
impl CalendarIntegrationService {
|
|
pub fn new(pool: DbPool) -> Self {
|
|
Self { db_pool: pool }
|
|
}
|
|
|
|
pub async fn link_contact_to_event(
|
|
&self,
|
|
organization_id: Uuid,
|
|
event_id: Uuid,
|
|
request: &LinkContactRequest,
|
|
) -> Result<EventContact, CalendarIntegrationError> {
|
|
// Verify contact exists and belongs to organization
|
|
self.verify_contact(organization_id, request.contact_id).await?;
|
|
|
|
// Verify event exists
|
|
self.verify_event(organization_id, event_id).await?;
|
|
|
|
// Check if already linked
|
|
if self.is_contact_linked(event_id, request.contact_id).await? {
|
|
return Err(CalendarIntegrationError::AlreadyLinked);
|
|
}
|
|
|
|
let id = Uuid::new_v4();
|
|
let now = Utc::now();
|
|
let role = request.role.clone().unwrap_or_default();
|
|
|
|
// Create link in database
|
|
self.create_event_contact_link(id, event_id, request.contact_id, &role, now)
|
|
.await?;
|
|
|
|
// Send notification if requested
|
|
let notified = if request.send_notification.unwrap_or(true) {
|
|
self.send_event_invitation(event_id, request.contact_id).await.is_ok()
|
|
} else {
|
|
false
|
|
};
|
|
|
|
// Log activity
|
|
self.log_contact_activity(
|
|
request.contact_id,
|
|
"linked_to_event",
|
|
&format!("Linked to event {}", event_id),
|
|
Some(event_id),
|
|
)
|
|
.await?;
|
|
|
|
Ok(EventContact {
|
|
id,
|
|
event_id,
|
|
contact_id: request.contact_id,
|
|
role,
|
|
response_status: ResponseStatus::NeedsAction,
|
|
notified,
|
|
notified_at: if notified { Some(now) } else { None },
|
|
created_at: now,
|
|
})
|
|
}
|
|
|
|
pub async fn bulk_link_contacts(
|
|
&self,
|
|
organization_id: Uuid,
|
|
event_id: Uuid,
|
|
request: &BulkLinkContactsRequest,
|
|
) -> Result<Vec<EventContact>, CalendarIntegrationError> {
|
|
let mut results = Vec::new();
|
|
|
|
for contact_id in &request.contact_ids {
|
|
let link_request = LinkContactRequest {
|
|
contact_id: *contact_id,
|
|
role: request.role.clone(),
|
|
send_notification: request.send_notification,
|
|
};
|
|
|
|
match self.link_contact_to_event(organization_id, event_id, &link_request).await {
|
|
Ok(event_contact) => results.push(event_contact),
|
|
Err(CalendarIntegrationError::AlreadyLinked) => {
|
|
// Skip already linked contacts
|
|
continue;
|
|
}
|
|
Err(e) => return Err(e),
|
|
}
|
|
}
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
pub async fn unlink_contact_from_event(
|
|
&self,
|
|
organization_id: Uuid,
|
|
event_id: Uuid,
|
|
contact_id: Uuid,
|
|
) -> Result<(), CalendarIntegrationError> {
|
|
// Verify ownership
|
|
self.verify_contact(organization_id, contact_id).await?;
|
|
self.verify_event(organization_id, event_id).await?;
|
|
|
|
// Delete link
|
|
self.delete_event_contact_link(event_id, contact_id).await?;
|
|
|
|
// Log activity
|
|
self.log_contact_activity(
|
|
contact_id,
|
|
"unlinked_from_event",
|
|
&format!("Unlinked from event {}", event_id),
|
|
Some(event_id),
|
|
)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn update_event_contact(
|
|
&self,
|
|
organization_id: Uuid,
|
|
event_id: Uuid,
|
|
contact_id: Uuid,
|
|
request: &UpdateEventContactRequest,
|
|
) -> Result<EventContact, CalendarIntegrationError> {
|
|
self.verify_contact(organization_id, contact_id).await?;
|
|
self.verify_event(organization_id, event_id).await?;
|
|
|
|
let mut event_contact = self.get_event_contact(event_id, contact_id).await?;
|
|
|
|
if let Some(role) = &request.role {
|
|
event_contact.role = role.clone();
|
|
}
|
|
|
|
if let Some(status) = &request.response_status {
|
|
event_contact.response_status = status.clone();
|
|
}
|
|
|
|
self.update_event_contact_in_db(&event_contact).await?;
|
|
|
|
Ok(event_contact)
|
|
}
|
|
|
|
pub async fn get_event_contacts(
|
|
&self,
|
|
organization_id: Uuid,
|
|
event_id: Uuid,
|
|
query: &EventContactsQuery,
|
|
) -> Result<Vec<EventContactWithDetails>, CalendarIntegrationError> {
|
|
self.verify_event(organization_id, event_id).await?;
|
|
|
|
let contacts = self.fetch_event_contacts(event_id, query).await?;
|
|
|
|
if query.include_contact_details.unwrap_or(true) {
|
|
let mut results = Vec::new();
|
|
for event_contact in contacts {
|
|
if let Ok(contact) = self.get_contact_summary(event_contact.contact_id).await {
|
|
results.push(EventContactWithDetails {
|
|
event_contact,
|
|
contact,
|
|
});
|
|
}
|
|
}
|
|
Ok(results)
|
|
} else {
|
|
Ok(contacts
|
|
.into_iter()
|
|
.map(|ec| EventContactWithDetails {
|
|
contact: ContactSummary {
|
|
id: ec.contact_id,
|
|
first_name: String::new(),
|
|
last_name: String::new(),
|
|
email: None,
|
|
phone: None,
|
|
company: None,
|
|
job_title: None,
|
|
avatar_url: None,
|
|
},
|
|
event_contact: ec,
|
|
})
|
|
.collect())
|
|
}
|
|
}
|
|
|
|
pub async fn get_contact_events(
|
|
&self,
|
|
organization_id: Uuid,
|
|
contact_id: Uuid,
|
|
query: &ContactEventsQuery,
|
|
) -> Result<ContactEventsResponse, CalendarIntegrationError> {
|
|
self.verify_contact(organization_id, contact_id).await?;
|
|
|
|
let events = self.fetch_contact_events(contact_id, query).await?;
|
|
let total_count = events.len() as u32;
|
|
let now = Utc::now();
|
|
|
|
let upcoming_count = events
|
|
.iter()
|
|
.filter(|e| e.event.start_time > now)
|
|
.count() as u32;
|
|
let past_count = total_count - upcoming_count;
|
|
|
|
Ok(ContactEventsResponse {
|
|
events,
|
|
total_count,
|
|
upcoming_count,
|
|
past_count,
|
|
})
|
|
}
|
|
|
|
pub async fn get_suggested_contacts(
|
|
&self,
|
|
organization_id: Uuid,
|
|
event_id: Uuid,
|
|
limit: Option<u32>,
|
|
) -> Result<Vec<SuggestedContact>, CalendarIntegrationError> {
|
|
self.verify_event(organization_id, event_id).await?;
|
|
|
|
let limit = limit.unwrap_or(10);
|
|
let mut suggestions: Vec<SuggestedContact> = Vec::new();
|
|
|
|
// Get event details for context
|
|
let event = self.get_event_details(event_id).await?;
|
|
|
|
// Get already linked contacts to exclude
|
|
let linked_contacts = self.get_linked_contact_ids(event_id).await?;
|
|
|
|
// Find frequent collaborators of the organizer
|
|
if let Some(organizer_id) = self.get_event_organizer_contact_id(event_id).await? {
|
|
let collaborators = self
|
|
.find_frequent_collaborators(organizer_id, &linked_contacts, 5)
|
|
.await?;
|
|
for contact in collaborators {
|
|
suggestions.push(SuggestedContact {
|
|
contact,
|
|
reason: SuggestionReason::FrequentCollaborator,
|
|
score: 0.9,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Find contacts from same company as existing attendees
|
|
let company_contacts = self
|
|
.find_same_company_contacts(event_id, &linked_contacts, 5)
|
|
.await?;
|
|
for contact in company_contacts {
|
|
suggestions.push(SuggestedContact {
|
|
contact,
|
|
reason: SuggestionReason::SameCompany,
|
|
score: 0.7,
|
|
});
|
|
}
|
|
|
|
// Find contacts who attended similar events
|
|
let similar_attendees = self
|
|
.find_similar_event_attendees(&event.title, &linked_contacts, 5)
|
|
.await?;
|
|
for contact in similar_attendees {
|
|
suggestions.push(SuggestedContact {
|
|
contact,
|
|
reason: SuggestionReason::PreviousAttendee,
|
|
score: 0.6,
|
|
});
|
|
}
|
|
|
|
// Sort by score and limit
|
|
suggestions.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
|
|
suggestions.truncate(limit as usize);
|
|
|
|
Ok(suggestions)
|
|
}
|
|
|
|
pub async fn find_contacts_for_event(
|
|
&self,
|
|
organization_id: Uuid,
|
|
emails: &[String],
|
|
) -> Result<HashMap<String, Option<ContactSummary>>, CalendarIntegrationError> {
|
|
let mut results = HashMap::new();
|
|
|
|
for email in emails {
|
|
let contact = self.find_contact_by_email(organization_id, email).await.ok();
|
|
results.insert(email.clone(), contact);
|
|
}
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
pub async fn create_contacts_from_attendees(
|
|
&self,
|
|
organization_id: Uuid,
|
|
user_id: Uuid,
|
|
attendees: &[AttendeeInfo],
|
|
) -> Result<Vec<ContactSummary>, CalendarIntegrationError> {
|
|
let mut created_contacts = Vec::new();
|
|
|
|
for attendee in attendees {
|
|
// Check if contact already exists
|
|
if self
|
|
.find_contact_by_email(organization_id, &attendee.email)
|
|
.await
|
|
.is_ok()
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Create new contact
|
|
let contact_id = Uuid::new_v4();
|
|
let now = Utc::now();
|
|
|
|
self.create_contact_from_attendee(
|
|
contact_id,
|
|
organization_id,
|
|
user_id,
|
|
attendee,
|
|
now,
|
|
)
|
|
.await?;
|
|
|
|
created_contacts.push(ContactSummary {
|
|
id: contact_id,
|
|
first_name: attendee.name.split_whitespace().next().unwrap_or("").to_string(),
|
|
last_name: attendee
|
|
.name
|
|
.split_whitespace()
|
|
.skip(1)
|
|
.collect::<Vec<_>>()
|
|
.join(" "),
|
|
email: Some(attendee.email.clone()),
|
|
phone: None,
|
|
company: attendee.company.clone(),
|
|
job_title: None,
|
|
avatar_url: None,
|
|
});
|
|
}
|
|
|
|
Ok(created_contacts)
|
|
}
|
|
|
|
// Helper methods (database operations - stubs for implementation)
|
|
|
|
async fn verify_contact(
|
|
&self,
|
|
_organization_id: Uuid,
|
|
_contact_id: Uuid,
|
|
) -> Result<(), CalendarIntegrationError> {
|
|
// Verify contact exists and belongs to organization
|
|
Ok(())
|
|
}
|
|
|
|
async fn verify_event(
|
|
&self,
|
|
_organization_id: Uuid,
|
|
_event_id: Uuid,
|
|
) -> Result<(), CalendarIntegrationError> {
|
|
// Verify event exists and belongs to organization
|
|
Ok(())
|
|
}
|
|
|
|
async fn is_contact_linked(
|
|
&self,
|
|
_event_id: Uuid,
|
|
_contact_id: Uuid,
|
|
) -> Result<bool, CalendarIntegrationError> {
|
|
// Check if contact is already linked to event
|
|
Ok(false)
|
|
}
|
|
|
|
async fn create_event_contact_link(
|
|
&self,
|
|
_id: Uuid,
|
|
_event_id: Uuid,
|
|
_contact_id: Uuid,
|
|
_role: &EventContactRole,
|
|
_created_at: DateTime<Utc>,
|
|
) -> Result<(), CalendarIntegrationError> {
|
|
// Insert into event_contacts table
|
|
Ok(())
|
|
}
|
|
|
|
async fn delete_event_contact_link(
|
|
&self,
|
|
_event_id: Uuid,
|
|
_contact_id: Uuid,
|
|
) -> Result<(), CalendarIntegrationError> {
|
|
// Delete from event_contacts table
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_event_contact(
|
|
&self,
|
|
event_id: Uuid,
|
|
contact_id: Uuid,
|
|
) -> Result<EventContact, CalendarIntegrationError> {
|
|
// Query event_contacts table
|
|
Ok(EventContact {
|
|
id: Uuid::new_v4(),
|
|
event_id,
|
|
contact_id,
|
|
role: EventContactRole::Attendee,
|
|
response_status: ResponseStatus::NeedsAction,
|
|
notified: false,
|
|
notified_at: None,
|
|
created_at: Utc::now(),
|
|
})
|
|
}
|
|
|
|
async fn update_event_contact_in_db(
|
|
&self,
|
|
_event_contact: &EventContact,
|
|
) -> Result<(), CalendarIntegrationError> {
|
|
// Update event_contacts table
|
|
Ok(())
|
|
}
|
|
|
|
async fn fetch_event_contacts(
|
|
&self,
|
|
event_id: Uuid,
|
|
_query: &EventContactsQuery,
|
|
) -> Result<Vec<EventContact>, CalendarIntegrationError> {
|
|
// Return mock data for contacts linked to this event
|
|
// In production, this would query an event_contacts junction table
|
|
Ok(vec![
|
|
EventContact {
|
|
id: Uuid::new_v4(),
|
|
event_id,
|
|
contact_id: Uuid::new_v4(),
|
|
role: EventContactRole::Attendee,
|
|
response_status: ResponseStatus::Accepted,
|
|
notified: true,
|
|
notified_at: Some(Utc::now()),
|
|
created_at: Utc::now(),
|
|
}
|
|
])
|
|
}
|
|
|
|
async fn fetch_contact_events(
|
|
&self,
|
|
contact_id: Uuid,
|
|
query: &ContactEventsQuery,
|
|
) -> Result<Vec<ContactEventWithDetails>, CalendarIntegrationError> {
|
|
let pool = self.db_pool.clone();
|
|
let from_date = query.from_date;
|
|
let to_date = query.to_date;
|
|
|
|
tokio::task::spawn_blocking(move || -> Result<Vec<ContactEventWithDetails>, CalendarIntegrationError> {
|
|
let mut conn = pool.get().map_err(|_| CalendarIntegrationError::DatabaseError)?;
|
|
|
|
// Get events for the contact's organization in the date range
|
|
let rows: Vec<(Uuid, String, Option<String>, DateTime<Utc>, DateTime<Utc>, Option<String>)> = calendar_events::table
|
|
.filter(calendar_events::start_time.ge(from_date.unwrap_or(Utc::now())))
|
|
.filter(calendar_events::start_time.le(to_date.unwrap_or(Utc::now() + chrono::Duration::days(30))))
|
|
.filter(calendar_events::status.ne("cancelled"))
|
|
.order(calendar_events::start_time.asc())
|
|
.select((
|
|
calendar_events::id,
|
|
calendar_events::title,
|
|
calendar_events::description,
|
|
calendar_events::start_time,
|
|
calendar_events::end_time,
|
|
calendar_events::location,
|
|
))
|
|
.limit(50)
|
|
.load(&mut conn)
|
|
.map_err(|_| CalendarIntegrationError::DatabaseError)?;
|
|
|
|
let events = rows.into_iter().map(|row| {
|
|
ContactEventWithDetails {
|
|
event_contact: EventContact {
|
|
id: Uuid::new_v4(),
|
|
event_id: row.0,
|
|
contact_id,
|
|
role: EventContactRole::Attendee,
|
|
response_status: ResponseStatus::Accepted,
|
|
notified: false,
|
|
notified_at: None,
|
|
created_at: Utc::now(),
|
|
},
|
|
event: EventSummary {
|
|
id: row.0,
|
|
title: row.1,
|
|
description: row.2,
|
|
start_time: row.3,
|
|
end_time: row.4,
|
|
location: row.5,
|
|
is_recurring: false,
|
|
organizer_name: None,
|
|
},
|
|
}
|
|
}).collect();
|
|
|
|
Ok(events)
|
|
})
|
|
.await
|
|
.map_err(|e: tokio::task::JoinError| {
|
|
log::error!("Spawn blocking error: {}", e);
|
|
CalendarIntegrationError::DatabaseError
|
|
})?
|
|
}
|
|
|
|
async fn get_contact_summary(
|
|
&self,
|
|
contact_id: Uuid,
|
|
) -> Result<ContactSummary, CalendarIntegrationError> {
|
|
// Query contacts table for summary
|
|
Ok(ContactSummary {
|
|
id: contact_id,
|
|
first_name: "John".to_string(),
|
|
last_name: "Doe".to_string(),
|
|
email: Some("john@example.com".to_string()),
|
|
phone: None,
|
|
company: None,
|
|
job_title: None,
|
|
avatar_url: None,
|
|
})
|
|
}
|
|
|
|
async fn get_event_details(
|
|
&self,
|
|
event_id: Uuid,
|
|
) -> Result<EventSummary, CalendarIntegrationError> {
|
|
// Query events table
|
|
Ok(EventSummary {
|
|
id: event_id,
|
|
title: "Meeting".to_string(),
|
|
description: None,
|
|
start_time: Utc::now(),
|
|
end_time: Utc::now(),
|
|
location: None,
|
|
is_recurring: false,
|
|
organizer_name: None,
|
|
})
|
|
}
|
|
|
|
async fn get_linked_contact_ids(
|
|
&self,
|
|
event_id: Uuid,
|
|
) -> Result<Vec<Uuid>, CalendarIntegrationError> {
|
|
// In production, query event_contacts junction table
|
|
// For now return empty - would need junction table to be created
|
|
let _ = event_id;
|
|
Ok(vec![])
|
|
}
|
|
|
|
async fn get_event_organizer_contact_id(
|
|
&self,
|
|
_event_id: Uuid,
|
|
) -> Result<Option<Uuid>, CalendarIntegrationError> {
|
|
// Get organizer's contact ID if exists
|
|
Ok(None)
|
|
}
|
|
|
|
async fn find_frequent_collaborators(
|
|
&self,
|
|
contact_id: Uuid,
|
|
exclude: &[Uuid],
|
|
limit: usize,
|
|
) -> Result<Vec<ContactSummary>, CalendarIntegrationError> {
|
|
let pool = self.db_pool.clone();
|
|
let exclude = exclude.to_vec();
|
|
|
|
tokio::task::spawn_blocking(move || -> Result<Vec<ContactSummary>, CalendarIntegrationError> {
|
|
let mut conn = pool.get().map_err(|_| CalendarIntegrationError::DatabaseError)?;
|
|
|
|
// Find other contacts in the same organization, excluding specified ones
|
|
let mut query = crm_contacts::table
|
|
.filter(crm_contacts::id.ne(contact_id))
|
|
.filter(crm_contacts::status.eq("active"))
|
|
.into_boxed();
|
|
|
|
for exc in &exclude {
|
|
query = query.filter(crm_contacts::id.ne(*exc));
|
|
}
|
|
|
|
let rows: Vec<(Uuid, Option<String>, Option<String>, Option<String>, Option<String>, Option<String>)> = query
|
|
.select((
|
|
crm_contacts::id,
|
|
crm_contacts::first_name,
|
|
crm_contacts::last_name,
|
|
crm_contacts::email,
|
|
crm_contacts::company,
|
|
crm_contacts::job_title,
|
|
))
|
|
.limit(limit as i64)
|
|
.load(&mut conn)
|
|
.map_err(|_| CalendarIntegrationError::DatabaseError)?;
|
|
|
|
let contacts = rows.into_iter().map(|row| {
|
|
ContactSummary {
|
|
id: row.0,
|
|
first_name: row.1.unwrap_or_default(),
|
|
last_name: row.2.unwrap_or_default(),
|
|
email: row.3,
|
|
phone: None,
|
|
company: row.4,
|
|
job_title: row.5,
|
|
avatar_url: None,
|
|
}
|
|
}).collect();
|
|
|
|
Ok(contacts)
|
|
})
|
|
.await
|
|
.map_err(|e: tokio::task::JoinError| {
|
|
log::error!("Spawn blocking error: {}", e);
|
|
CalendarIntegrationError::DatabaseError
|
|
})?
|
|
}
|
|
|
|
async fn find_same_company_contacts(
|
|
&self,
|
|
_event_id: Uuid,
|
|
exclude: &[Uuid],
|
|
limit: usize,
|
|
) -> Result<Vec<ContactSummary>, CalendarIntegrationError> {
|
|
let pool = self.db_pool.clone();
|
|
let exclude = exclude.to_vec();
|
|
|
|
tokio::task::spawn_blocking(move || -> Result<Vec<ContactSummary>, CalendarIntegrationError> {
|
|
let mut conn = pool.get().map_err(|_| CalendarIntegrationError::DatabaseError)?;
|
|
|
|
// Find contacts with company field set
|
|
let mut query = crm_contacts::table
|
|
.filter(crm_contacts::company.is_not_null())
|
|
.filter(crm_contacts::status.eq("active"))
|
|
.into_boxed();
|
|
|
|
for exc in &exclude {
|
|
query = query.filter(crm_contacts::id.ne(*exc));
|
|
}
|
|
|
|
let rows: Vec<(Uuid, Option<String>, Option<String>, Option<String>, Option<String>, Option<String>)> = query
|
|
.select((
|
|
crm_contacts::id,
|
|
crm_contacts::first_name,
|
|
crm_contacts::last_name,
|
|
crm_contacts::email,
|
|
crm_contacts::company,
|
|
crm_contacts::job_title,
|
|
))
|
|
.limit(limit as i64)
|
|
.load(&mut conn)
|
|
.map_err(|_| CalendarIntegrationError::DatabaseError)?;
|
|
|
|
let contacts = rows.into_iter().map(|row| {
|
|
ContactSummary {
|
|
id: row.0,
|
|
first_name: row.1.unwrap_or_default(),
|
|
last_name: row.2.unwrap_or_default(),
|
|
email: row.3,
|
|
phone: None,
|
|
company: row.4,
|
|
job_title: row.5,
|
|
avatar_url: None,
|
|
}
|
|
}).collect();
|
|
|
|
Ok(contacts)
|
|
})
|
|
.await
|
|
.map_err(|_| CalendarIntegrationError::DatabaseError)?
|
|
}
|
|
|
|
async fn find_similar_event_attendees(
|
|
&self,
|
|
_event_title: &str,
|
|
exclude: &[Uuid],
|
|
limit: usize,
|
|
) -> Result<Vec<ContactSummary>, CalendarIntegrationError> {
|
|
let pool = self.db_pool.clone();
|
|
let exclude = exclude.to_vec();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let mut conn = pool.get().map_err(|_| CalendarIntegrationError::DatabaseError)?;
|
|
|
|
// Find active contacts
|
|
let mut query = crm_contacts::table
|
|
.filter(crm_contacts::status.eq("active"))
|
|
.into_boxed();
|
|
|
|
for exc in &exclude {
|
|
query = query.filter(crm_contacts::id.ne(*exc));
|
|
}
|
|
|
|
let rows: Vec<(Uuid, Option<String>, Option<String>, Option<String>, Option<String>, Option<String>)> = query
|
|
.select((
|
|
crm_contacts::id,
|
|
crm_contacts::first_name,
|
|
crm_contacts::last_name,
|
|
crm_contacts::email,
|
|
crm_contacts::company,
|
|
crm_contacts::job_title,
|
|
))
|
|
.limit(limit as i64)
|
|
.load(&mut conn)
|
|
.map_err(|_| CalendarIntegrationError::DatabaseError)?;
|
|
|
|
let contacts = rows.into_iter().map(|row| {
|
|
ContactSummary {
|
|
id: row.0,
|
|
first_name: row.1.unwrap_or_default(),
|
|
last_name: row.2.unwrap_or_default(),
|
|
email: row.3,
|
|
phone: None,
|
|
company: row.4,
|
|
job_title: row.5,
|
|
avatar_url: None,
|
|
}
|
|
}).collect();
|
|
|
|
Ok(contacts)
|
|
})
|
|
.await
|
|
.map_err(|_| CalendarIntegrationError::DatabaseError)?
|
|
}
|
|
|
|
async fn find_contact_by_email(
|
|
&self,
|
|
_organization_id: Uuid,
|
|
_email: &str,
|
|
) -> Result<ContactSummary, CalendarIntegrationError> {
|
|
Err(CalendarIntegrationError::ContactNotFound)
|
|
}
|
|
|
|
async fn create_contact_from_attendee(
|
|
&self,
|
|
_contact_id: Uuid,
|
|
_organization_id: Uuid,
|
|
_user_id: Uuid,
|
|
_attendee: &AttendeeInfo,
|
|
_created_at: DateTime<Utc>,
|
|
) -> Result<(), CalendarIntegrationError> {
|
|
// Insert into contacts table
|
|
Ok(())
|
|
}
|
|
|
|
async fn send_event_invitation(
|
|
&self,
|
|
_event_id: Uuid,
|
|
_contact_id: Uuid,
|
|
) -> Result<(), CalendarIntegrationError> {
|
|
// Send email invitation
|
|
Ok(())
|
|
}
|
|
|
|
async fn log_contact_activity(
|
|
&self,
|
|
_contact_id: Uuid,
|
|
_activity_type: &str,
|
|
_description: &str,
|
|
_related_id: Option<Uuid>,
|
|
) -> Result<(), CalendarIntegrationError> {
|
|
// Log activity to contact_activities table
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AttendeeInfo {
|
|
pub email: String,
|
|
pub name: String,
|
|
pub company: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum CalendarIntegrationError {
|
|
DatabaseError,
|
|
ContactNotFound,
|
|
EventNotFound,
|
|
AlreadyLinked,
|
|
NotLinked,
|
|
Unauthorized,
|
|
InvalidInput(String),
|
|
}
|
|
|
|
impl std::fmt::Display for CalendarIntegrationError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
CalendarIntegrationError::DatabaseError => write!(f, "Database error"),
|
|
CalendarIntegrationError::ContactNotFound => write!(f, "Contact not found"),
|
|
CalendarIntegrationError::EventNotFound => write!(f, "Event not found"),
|
|
CalendarIntegrationError::AlreadyLinked => {
|
|
write!(f, "Contact is already linked to this event")
|
|
}
|
|
CalendarIntegrationError::NotLinked => {
|
|
write!(f, "Contact is not linked to this event")
|
|
}
|
|
CalendarIntegrationError::Unauthorized => write!(f, "Unauthorized"),
|
|
CalendarIntegrationError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for CalendarIntegrationError {}
|
|
|
|
impl IntoResponse for CalendarIntegrationError {
|
|
fn into_response(self) -> axum::response::Response {
|
|
use axum::http::StatusCode;
|
|
|
|
let (status, message) = match self {
|
|
CalendarIntegrationError::DatabaseError => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
|
|
}
|
|
CalendarIntegrationError::ContactNotFound => (StatusCode::NOT_FOUND, self.to_string()),
|
|
CalendarIntegrationError::EventNotFound => (StatusCode::NOT_FOUND, self.to_string()),
|
|
CalendarIntegrationError::AlreadyLinked => (StatusCode::CONFLICT, self.to_string()),
|
|
CalendarIntegrationError::NotLinked => (StatusCode::NOT_FOUND, self.to_string()),
|
|
CalendarIntegrationError::Unauthorized => (StatusCode::FORBIDDEN, self.to_string()),
|
|
CalendarIntegrationError::InvalidInput(_) => {
|
|
(StatusCode::BAD_REQUEST, self.to_string())
|
|
}
|
|
};
|
|
|
|
(status, Json(serde_json::json!({ "error": message }))).into_response()
|
|
}
|
|
}
|
|
|
|
pub fn create_calendar_integration_tables_migration() -> String {
|
|
r#"
|
|
CREATE TABLE IF NOT EXISTS event_contacts (
|
|
id UUID PRIMARY KEY,
|
|
event_id UUID NOT NULL REFERENCES calendar_events(id) ON DELETE CASCADE,
|
|
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
role VARCHAR(50) NOT NULL DEFAULT 'attendee',
|
|
response_status VARCHAR(50) NOT NULL DEFAULT 'needs_action',
|
|
notified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
notified_at TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
|
|
UNIQUE(event_id, contact_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_event_contacts_event_id ON event_contacts(event_id);
|
|
CREATE INDEX IF NOT EXISTS idx_event_contacts_contact_id ON event_contacts(contact_id);
|
|
CREATE INDEX IF NOT EXISTS idx_event_contacts_role ON event_contacts(role);
|
|
CREATE INDEX IF NOT EXISTS idx_event_contacts_response_status ON event_contacts(response_status);
|
|
"#
|
|
.to_string()
|
|
}
|
|
|
|
pub fn calendar_integration_routes() -> Router<Arc<AppState>> {
|
|
Router::new()
|
|
// Event contacts
|
|
.route("/events/:event_id/contacts", get(get_event_contacts_handler))
|
|
.route("/events/:event_id/contacts", post(link_contact_handler))
|
|
.route(
|
|
"/events/:event_id/contacts/bulk",
|
|
post(bulk_link_contacts_handler),
|
|
)
|
|
.route(
|
|
"/events/:event_id/contacts/:contact_id",
|
|
delete(unlink_contact_handler),
|
|
)
|
|
.route(
|
|
"/events/:event_id/contacts/:contact_id",
|
|
post(update_event_contact_handler),
|
|
)
|
|
.route(
|
|
"/events/:event_id/contacts/suggestions",
|
|
get(get_suggestions_handler),
|
|
)
|
|
// Contact events
|
|
.route("/contacts/:contact_id/events", get(get_contact_events_handler))
|
|
// Utilities
|
|
.route("/events/:event_id/find-contacts", post(find_contacts_handler))
|
|
.route(
|
|
"/events/:event_id/create-contacts",
|
|
post(create_contacts_from_attendees_handler),
|
|
)
|
|
}
|
|
|
|
// Route handlers
|
|
|
|
async fn get_event_contacts_handler(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(event_id): Path<Uuid>,
|
|
Query(query): Query<EventContactsQuery>,
|
|
) -> impl IntoResponse {
|
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
|
let org_id = Uuid::new_v4();
|
|
|
|
match service.get_event_contacts(org_id, event_id, &query).await {
|
|
Ok(contacts) => Json(contacts).into_response(),
|
|
Err(e) => e.into_response(),
|
|
}
|
|
}
|
|
|
|
async fn link_contact_handler(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(event_id): Path<Uuid>,
|
|
Json(request): Json<LinkContactRequest>,
|
|
) -> impl IntoResponse {
|
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
|
let org_id = Uuid::new_v4();
|
|
|
|
match service.link_contact_to_event(org_id, event_id, &request).await {
|
|
Ok(event_contact) => Json(event_contact).into_response(),
|
|
Err(e) => e.into_response(),
|
|
}
|
|
}
|
|
|
|
async fn bulk_link_contacts_handler(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(event_id): Path<Uuid>,
|
|
Json(request): Json<BulkLinkContactsRequest>,
|
|
) -> impl IntoResponse {
|
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
|
let org_id = Uuid::new_v4();
|
|
|
|
match service.bulk_link_contacts(org_id, event_id, &request).await {
|
|
Ok(contacts) => Json(contacts).into_response(),
|
|
Err(e) => e.into_response(),
|
|
}
|
|
}
|
|
|
|
async fn unlink_contact_handler(
|
|
State(state): State<Arc<AppState>>,
|
|
Path((event_id, contact_id)): Path<(Uuid, Uuid)>,
|
|
) -> impl IntoResponse {
|
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
|
let org_id = Uuid::new_v4();
|
|
|
|
match service.unlink_contact_from_event(org_id, event_id, contact_id).await {
|
|
Ok(_) => Json(serde_json::json!({ "success": true })).into_response(),
|
|
Err(e) => e.into_response(),
|
|
}
|
|
}
|
|
|
|
async fn update_event_contact_handler(
|
|
State(state): State<Arc<AppState>>,
|
|
Path((event_id, contact_id)): Path<(Uuid, Uuid)>,
|
|
Json(request): Json<UpdateEventContactRequest>,
|
|
) -> impl IntoResponse {
|
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
|
let org_id = Uuid::new_v4();
|
|
|
|
match service.update_event_contact(org_id, event_id, contact_id, &request).await {
|
|
Ok(contact) => Json(contact).into_response(),
|
|
Err(e) => e.into_response(),
|
|
}
|
|
}
|
|
|
|
async fn get_suggestions_handler(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(event_id): Path<Uuid>,
|
|
) -> impl IntoResponse {
|
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
|
let org_id = Uuid::new_v4();
|
|
|
|
match service.get_suggested_contacts(org_id, event_id, None).await {
|
|
Ok(suggestions) => Json(suggestions).into_response(),
|
|
Err(e) => e.into_response(),
|
|
}
|
|
}
|
|
|
|
async fn get_contact_events_handler(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(contact_id): Path<Uuid>,
|
|
Query(query): Query<ContactEventsQuery>,
|
|
) -> impl IntoResponse {
|
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
|
let org_id = Uuid::new_v4();
|
|
|
|
match service.get_contact_events(org_id, contact_id, &query).await {
|
|
Ok(events) => Json(events).into_response(),
|
|
Err(e) => e.into_response(),
|
|
}
|
|
}
|
|
|
|
async fn find_contacts_handler(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(event_id): Path<Uuid>,
|
|
) -> impl IntoResponse {
|
|
log::debug!("Finding contacts for event {event_id}");
|
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
|
let org_id = Uuid::new_v4();
|
|
|
|
match service.find_contacts_for_event(org_id, &[]).await {
|
|
Ok(contacts) => Json(contacts).into_response(),
|
|
Err(e) => e.into_response(),
|
|
}
|
|
}
|
|
|
|
async fn create_contacts_from_attendees_handler(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(event_id): Path<Uuid>,
|
|
Json(attendees): Json<Vec<AttendeeInfo>>,
|
|
) -> impl IntoResponse {
|
|
let service = CalendarIntegrationService::new(state.conn.clone());
|
|
let org_id = Uuid::new_v4();
|
|
|
|
match service.create_contacts_from_attendees(org_id, event_id, &attendees).await {
|
|
Ok(contacts) => Json(contacts).into_response(),
|
|
Err(e) => e.into_response(),
|
|
}
|
|
}
|