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>
1088 lines
33 KiB
Rust
1088 lines
33 KiB
Rust
pub mod ui;
|
|
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
routing::{delete, get, post, put},
|
|
Json, Router,
|
|
};
|
|
|
|
use bigdecimal::BigDecimal;
|
|
use chrono::{DateTime, Datelike, NaiveDate, Utc};
|
|
use diesel::prelude::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
use crate::core::bot::get_default_bot;
|
|
use crate::core::shared::schema::{
|
|
people_departments, people_org_chart, people_person_skills, people_skills,
|
|
people_team_members, people_teams, people_time_off,
|
|
};
|
|
use crate::core::shared::schema::people::people as people_table;
|
|
use crate::core::shared::state::AppState;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)]
|
|
#[diesel(table_name = people_table)]
|
|
pub struct Person {
|
|
pub id: Uuid,
|
|
pub org_id: Uuid,
|
|
pub bot_id: Uuid,
|
|
pub user_id: Option<Uuid>,
|
|
pub first_name: String,
|
|
pub last_name: Option<String>,
|
|
pub email: Option<String>,
|
|
pub phone: Option<String>,
|
|
pub mobile: Option<String>,
|
|
pub job_title: Option<String>,
|
|
pub department: Option<String>,
|
|
pub manager_id: Option<Uuid>,
|
|
pub office_location: Option<String>,
|
|
pub hire_date: Option<NaiveDate>,
|
|
pub birthday: Option<NaiveDate>,
|
|
pub avatar_url: Option<String>,
|
|
pub bio: Option<String>,
|
|
pub skills: Vec<String>,
|
|
pub social_links: serde_json::Value,
|
|
pub custom_fields: serde_json::Value,
|
|
pub timezone: Option<String>,
|
|
pub locale: Option<String>,
|
|
pub is_active: bool,
|
|
pub last_seen_at: Option<DateTime<Utc>>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)]
|
|
#[diesel(table_name = people_teams)]
|
|
pub struct Team {
|
|
pub id: Uuid,
|
|
pub org_id: Uuid,
|
|
pub bot_id: Uuid,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub leader_id: Option<Uuid>,
|
|
pub parent_team_id: Option<Uuid>,
|
|
pub color: Option<String>,
|
|
pub icon: Option<String>,
|
|
pub is_active: bool,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)]
|
|
#[diesel(table_name = people_team_members)]
|
|
pub struct TeamMember {
|
|
pub id: Uuid,
|
|
pub team_id: Uuid,
|
|
pub person_id: Uuid,
|
|
pub role: Option<String>,
|
|
pub is_primary: bool,
|
|
pub joined_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)]
|
|
#[diesel(table_name = people_org_chart)]
|
|
pub struct OrgChartEntry {
|
|
pub id: Uuid,
|
|
pub org_id: Uuid,
|
|
pub bot_id: Uuid,
|
|
pub person_id: Uuid,
|
|
pub reports_to_id: Option<Uuid>,
|
|
pub position_title: Option<String>,
|
|
pub position_level: i32,
|
|
pub position_order: i32,
|
|
pub effective_from: Option<NaiveDate>,
|
|
pub effective_until: Option<NaiveDate>,
|
|
pub is_active: bool,
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)]
|
|
#[diesel(table_name = people_departments)]
|
|
pub struct Department {
|
|
pub id: Uuid,
|
|
pub org_id: Uuid,
|
|
pub bot_id: Uuid,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub code: Option<String>,
|
|
pub parent_id: Option<Uuid>,
|
|
pub head_id: Option<Uuid>,
|
|
pub cost_center: Option<String>,
|
|
pub is_active: bool,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)]
|
|
#[diesel(table_name = people_skills)]
|
|
pub struct Skill {
|
|
pub id: Uuid,
|
|
pub org_id: Uuid,
|
|
pub bot_id: Uuid,
|
|
pub name: String,
|
|
pub category: Option<String>,
|
|
pub description: Option<String>,
|
|
pub is_active: bool,
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)]
|
|
#[diesel(table_name = people_person_skills)]
|
|
pub struct PersonSkill {
|
|
pub id: Uuid,
|
|
pub person_id: Uuid,
|
|
pub skill_id: Uuid,
|
|
pub proficiency_level: i32,
|
|
pub years_experience: Option<BigDecimal>,
|
|
pub verified_by: Option<Uuid>,
|
|
pub verified_at: Option<DateTime<Utc>>,
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)]
|
|
#[diesel(table_name = people_time_off)]
|
|
pub struct TimeOff {
|
|
pub id: Uuid,
|
|
pub org_id: Uuid,
|
|
pub bot_id: Uuid,
|
|
pub person_id: Uuid,
|
|
pub time_off_type: String,
|
|
pub status: String,
|
|
pub start_date: NaiveDate,
|
|
pub end_date: NaiveDate,
|
|
pub hours_requested: Option<BigDecimal>,
|
|
pub reason: Option<String>,
|
|
pub approved_by: Option<Uuid>,
|
|
pub approved_at: Option<DateTime<Utc>>,
|
|
pub notes: Option<String>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreatePersonRequest {
|
|
pub first_name: String,
|
|
pub last_name: Option<String>,
|
|
pub email: Option<String>,
|
|
pub phone: Option<String>,
|
|
pub mobile: Option<String>,
|
|
pub job_title: Option<String>,
|
|
pub department: Option<String>,
|
|
pub manager_id: Option<Uuid>,
|
|
pub office_location: Option<String>,
|
|
pub hire_date: Option<String>,
|
|
pub birthday: Option<String>,
|
|
pub avatar_url: Option<String>,
|
|
pub bio: Option<String>,
|
|
pub skills: Option<Vec<String>>,
|
|
pub timezone: Option<String>,
|
|
pub locale: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdatePersonRequest {
|
|
pub first_name: Option<String>,
|
|
pub last_name: Option<String>,
|
|
pub email: Option<String>,
|
|
pub phone: Option<String>,
|
|
pub mobile: Option<String>,
|
|
pub job_title: Option<String>,
|
|
pub department: Option<String>,
|
|
pub manager_id: Option<Uuid>,
|
|
pub office_location: Option<String>,
|
|
pub bio: Option<String>,
|
|
pub skills: Option<Vec<String>>,
|
|
pub is_active: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateTeamRequest {
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub leader_id: Option<Uuid>,
|
|
pub parent_team_id: Option<Uuid>,
|
|
pub color: Option<String>,
|
|
pub icon: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AddTeamMemberRequest {
|
|
pub person_id: Uuid,
|
|
pub role: Option<String>,
|
|
pub is_primary: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateDepartmentRequest {
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub code: Option<String>,
|
|
pub parent_id: Option<Uuid>,
|
|
pub head_id: Option<Uuid>,
|
|
pub cost_center: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateSkillRequest {
|
|
pub name: String,
|
|
pub category: Option<String>,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AddPersonSkillRequest {
|
|
pub skill_id: Uuid,
|
|
pub proficiency_level: Option<i32>,
|
|
pub years_experience: Option<f64>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateTimeOffRequest {
|
|
pub person_id: Uuid,
|
|
pub time_off_type: String,
|
|
pub start_date: String,
|
|
pub end_date: String,
|
|
pub hours_requested: Option<f64>,
|
|
pub reason: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ApproveTimeOffRequest {
|
|
pub approved: bool,
|
|
pub notes: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ListQuery {
|
|
pub search: Option<String>,
|
|
pub department: Option<String>,
|
|
pub team_id: Option<Uuid>,
|
|
pub manager_id: Option<Uuid>,
|
|
pub is_active: Option<bool>,
|
|
pub limit: Option<i64>,
|
|
pub offset: Option<i64>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct PeopleStats {
|
|
pub total_people: i64,
|
|
pub active_people: i64,
|
|
pub total_teams: i64,
|
|
pub total_departments: i64,
|
|
pub pending_time_off: i64,
|
|
pub new_hires_this_month: i64,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct PersonWithDetails {
|
|
pub person: Person,
|
|
pub manager: Option<Person>,
|
|
pub direct_reports: Vec<Person>,
|
|
pub teams: Vec<Team>,
|
|
pub skills: Vec<Skill>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct TeamWithMembers {
|
|
pub team: Team,
|
|
pub members: Vec<Person>,
|
|
pub leader: Option<Person>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct OrgChartNode {
|
|
pub person: Person,
|
|
pub position_title: Option<String>,
|
|
pub position_level: i32,
|
|
pub reports: Vec<OrgChartNode>,
|
|
}
|
|
|
|
fn get_bot_context(state: &AppState) -> (Uuid, Uuid) {
|
|
let Ok(mut conn) = state.conn.get() else {
|
|
return (Uuid::nil(), Uuid::nil());
|
|
};
|
|
let (bot_id, _bot_name) = get_default_bot(&mut conn);
|
|
let org_id = Uuid::nil();
|
|
(org_id, bot_id)
|
|
}
|
|
|
|
fn bd(val: f64) -> BigDecimal {
|
|
use std::str::FromStr;
|
|
BigDecimal::from_str(&val.to_string()).unwrap_or_else(|_| BigDecimal::from(0))
|
|
}
|
|
|
|
pub async fn create_person(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<CreatePersonRequest>,
|
|
) -> Result<Json<Person>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
let id = Uuid::new_v4();
|
|
let now = Utc::now();
|
|
|
|
let hire_date = req
|
|
.hire_date
|
|
.and_then(|d| NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
|
|
|
|
let birthday = req
|
|
.birthday
|
|
.and_then(|d| NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
|
|
|
|
let person = Person {
|
|
id,
|
|
org_id,
|
|
bot_id,
|
|
user_id: None,
|
|
first_name: req.first_name,
|
|
last_name: req.last_name,
|
|
email: req.email,
|
|
phone: req.phone,
|
|
mobile: req.mobile,
|
|
job_title: req.job_title,
|
|
department: req.department,
|
|
manager_id: req.manager_id,
|
|
office_location: req.office_location,
|
|
hire_date,
|
|
birthday,
|
|
avatar_url: req.avatar_url,
|
|
bio: req.bio,
|
|
skills: req.skills.unwrap_or_default(),
|
|
social_links: serde_json::json!({}),
|
|
custom_fields: serde_json::json!({}),
|
|
timezone: req.timezone,
|
|
locale: req.locale,
|
|
is_active: true,
|
|
last_seen_at: None,
|
|
created_at: now,
|
|
updated_at: now,
|
|
};
|
|
|
|
diesel::insert_into(people_table::table)
|
|
.values(&person)
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?;
|
|
|
|
Ok(Json(person))
|
|
}
|
|
|
|
pub async fn list_people(
|
|
State(state): State<Arc<AppState>>,
|
|
Query(query): Query<ListQuery>,
|
|
) -> Result<Json<Vec<Person>>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
let limit = query.limit.unwrap_or(50);
|
|
let offset = query.offset.unwrap_or(0);
|
|
|
|
let mut q = people_table::table
|
|
.filter(people_table::org_id.eq(org_id))
|
|
.filter(people_table::bot_id.eq(bot_id))
|
|
.into_boxed();
|
|
|
|
if let Some(is_active) = query.is_active {
|
|
q = q.filter(people_table::is_active.eq(is_active));
|
|
}
|
|
|
|
if let Some(department) = query.department {
|
|
q = q.filter(people_table::department.eq(department));
|
|
}
|
|
|
|
if let Some(manager_id) = query.manager_id {
|
|
q = q.filter(people_table::manager_id.eq(manager_id));
|
|
}
|
|
|
|
if let Some(search) = query.search {
|
|
let pattern = format!("%{search}%");
|
|
q = q.filter(
|
|
people_table::first_name
|
|
.ilike(pattern.clone())
|
|
.or(people_table::last_name.ilike(pattern.clone()))
|
|
.or(people_table::email.ilike(pattern.clone()))
|
|
.or(people_table::job_title.ilike(pattern)),
|
|
);
|
|
}
|
|
|
|
let persons: Vec<Person> = q
|
|
.order((people_table::last_name.asc(), people_table::first_name.asc()))
|
|
.limit(limit)
|
|
.offset(offset)
|
|
.load(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?;
|
|
|
|
Ok(Json(persons))
|
|
}
|
|
|
|
pub async fn get_person(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<Json<PersonWithDetails>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let person: Person = people_table::table
|
|
.filter(people_table::id.eq(id))
|
|
.first(&mut conn)
|
|
.map_err(|_| (StatusCode::NOT_FOUND, "Person not found".to_string()))?;
|
|
|
|
let manager: Option<Person> = person
|
|
.manager_id
|
|
.and_then(|mid| {
|
|
people_table::table
|
|
.filter(people_table::id.eq(mid))
|
|
.first(&mut conn)
|
|
.ok()
|
|
});
|
|
|
|
let direct_reports: Vec<Person> = people_table::table
|
|
.filter(people_table::manager_id.eq(id))
|
|
.filter(people_table::is_active.eq(true))
|
|
.order(people_table::first_name.asc())
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
let team_ids: Vec<Uuid> = people_team_members::table
|
|
.filter(people_team_members::person_id.eq(id))
|
|
.select(people_team_members::team_id)
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
let teams: Vec<Team> = if team_ids.is_empty() {
|
|
vec![]
|
|
} else {
|
|
people_teams::table
|
|
.filter(people_teams::id.eq_any(&team_ids))
|
|
.load(&mut conn)
|
|
.unwrap_or_default()
|
|
};
|
|
|
|
let skill_ids: Vec<Uuid> = people_person_skills::table
|
|
.filter(people_person_skills::person_id.eq(id))
|
|
.select(people_person_skills::skill_id)
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
let skills: Vec<Skill> = if skill_ids.is_empty() {
|
|
vec![]
|
|
} else {
|
|
people_skills::table
|
|
.filter(people_skills::id.eq_any(&skill_ids))
|
|
.load(&mut conn)
|
|
.unwrap_or_default()
|
|
};
|
|
|
|
Ok(Json(PersonWithDetails {
|
|
person,
|
|
manager,
|
|
direct_reports,
|
|
teams,
|
|
skills,
|
|
}))
|
|
}
|
|
|
|
pub async fn update_person(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
Json(req): Json<UpdatePersonRequest>,
|
|
) -> Result<Json<Person>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let now = Utc::now();
|
|
|
|
diesel::update(people_table::table.filter(people_table::id.eq(id)))
|
|
.set(people_table::updated_at.eq(now))
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?;
|
|
|
|
if let Some(first_name) = req.first_name {
|
|
diesel::update(people_table::table.filter(people_table::id.eq(id)))
|
|
.set(people_table::first_name.eq(first_name))
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?;
|
|
}
|
|
|
|
if let Some(last_name) = req.last_name {
|
|
diesel::update(people_table::table.filter(people_table::id.eq(id)))
|
|
.set(people_table::last_name.eq(last_name))
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?;
|
|
}
|
|
|
|
if let Some(email) = req.email {
|
|
diesel::update(people_table::table.filter(people_table::id.eq(id)))
|
|
.set(people_table::email.eq(email))
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?;
|
|
}
|
|
|
|
if let Some(job_title) = req.job_title {
|
|
diesel::update(people_table::table.filter(people_table::id.eq(id)))
|
|
.set(people_table::job_title.eq(job_title))
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?;
|
|
}
|
|
|
|
if let Some(department) = req.department {
|
|
diesel::update(people_table::table.filter(people_table::id.eq(id)))
|
|
.set(people_table::department.eq(department))
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?;
|
|
}
|
|
|
|
if let Some(manager_id) = req.manager_id {
|
|
diesel::update(people_table::table.filter(people_table::id.eq(id)))
|
|
.set(people_table::manager_id.eq(Some(manager_id)))
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?;
|
|
}
|
|
|
|
if let Some(is_active) = req.is_active {
|
|
diesel::update(people_table::table.filter(people_table::id.eq(id)))
|
|
.set(people_table::is_active.eq(is_active))
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?;
|
|
}
|
|
|
|
if let Some(skills) = req.skills {
|
|
diesel::update(people_table::table.filter(people_table::id.eq(id)))
|
|
.set(people_table::skills.eq(skills))
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?;
|
|
}
|
|
|
|
let person: Person = people_table::table
|
|
.filter(people_table::id.eq(id))
|
|
.first(&mut conn)
|
|
.map_err(|_| (StatusCode::NOT_FOUND, "Person not found".to_string()))?;
|
|
|
|
Ok(Json(person))
|
|
}
|
|
|
|
pub async fn delete_person(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<StatusCode, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
diesel::delete(people_table::table.filter(people_table::id.eq(id)))
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Delete error: {e}")))?;
|
|
|
|
Ok(StatusCode::NO_CONTENT)
|
|
}
|
|
|
|
pub async fn get_direct_reports(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<Json<Vec<Person>>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let reports: Vec<Person> = people_table::table
|
|
.filter(people_table::manager_id.eq(id))
|
|
.filter(people_table::is_active.eq(true))
|
|
.order(people_table::first_name.asc())
|
|
.load(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?;
|
|
|
|
Ok(Json(reports))
|
|
}
|
|
|
|
pub async fn create_team(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<CreateTeamRequest>,
|
|
) -> Result<Json<Team>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
let id = Uuid::new_v4();
|
|
let now = Utc::now();
|
|
|
|
let team = Team {
|
|
id,
|
|
org_id,
|
|
bot_id,
|
|
name: req.name,
|
|
description: req.description,
|
|
leader_id: req.leader_id,
|
|
parent_team_id: req.parent_team_id,
|
|
color: req.color,
|
|
icon: req.icon,
|
|
is_active: true,
|
|
created_at: now,
|
|
updated_at: now,
|
|
};
|
|
|
|
diesel::insert_into(people_teams::table)
|
|
.values(&team)
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?;
|
|
|
|
Ok(Json(team))
|
|
}
|
|
|
|
pub async fn list_teams(
|
|
State(state): State<Arc<AppState>>,
|
|
) -> Result<Json<Vec<Team>>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
|
|
let teams: Vec<Team> = people_teams::table
|
|
.filter(people_teams::org_id.eq(org_id))
|
|
.filter(people_teams::bot_id.eq(bot_id))
|
|
.filter(people_teams::is_active.eq(true))
|
|
.order(people_teams::name.asc())
|
|
.load(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?;
|
|
|
|
Ok(Json(teams))
|
|
}
|
|
|
|
pub async fn get_team(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<Json<TeamWithMembers>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let team: Team = people_teams::table
|
|
.filter(people_teams::id.eq(id))
|
|
.first(&mut conn)
|
|
.map_err(|_| (StatusCode::NOT_FOUND, "Team not found".to_string()))?;
|
|
|
|
let member_ids: Vec<Uuid> = people_team_members::table
|
|
.filter(people_team_members::team_id.eq(id))
|
|
.select(people_team_members::person_id)
|
|
.load(&mut conn)
|
|
.unwrap_or_default();
|
|
|
|
let members: Vec<Person> = if member_ids.is_empty() {
|
|
vec![]
|
|
} else {
|
|
people_table::table
|
|
.filter(people_table::id.eq_any(&member_ids))
|
|
.load(&mut conn)
|
|
.unwrap_or_default()
|
|
};
|
|
|
|
let leader: Option<Person> = team
|
|
.leader_id
|
|
.and_then(|lid| {
|
|
people_table::table
|
|
.filter(people_table::id.eq(lid))
|
|
.first(&mut conn)
|
|
.ok()
|
|
});
|
|
|
|
Ok(Json(TeamWithMembers {
|
|
team,
|
|
members,
|
|
leader,
|
|
}))
|
|
}
|
|
|
|
pub async fn add_team_member(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(team_id): Path<Uuid>,
|
|
Json(req): Json<AddTeamMemberRequest>,
|
|
) -> Result<Json<TeamMember>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let id = Uuid::new_v4();
|
|
let now = Utc::now();
|
|
|
|
let member = TeamMember {
|
|
id,
|
|
team_id,
|
|
person_id: req.person_id,
|
|
role: req.role,
|
|
is_primary: req.is_primary.unwrap_or(false),
|
|
joined_at: now,
|
|
};
|
|
|
|
diesel::insert_into(people_team_members::table)
|
|
.values(&member)
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?;
|
|
|
|
Ok(Json(member))
|
|
}
|
|
|
|
pub async fn remove_team_member(
|
|
State(state): State<Arc<AppState>>,
|
|
Path((team_id, person_id)): Path<(Uuid, Uuid)>,
|
|
) -> Result<StatusCode, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
diesel::delete(
|
|
people_team_members::table
|
|
.filter(people_team_members::team_id.eq(team_id))
|
|
.filter(people_team_members::person_id.eq(person_id)),
|
|
)
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Delete error: {e}")))?;
|
|
|
|
Ok(StatusCode::NO_CONTENT)
|
|
}
|
|
|
|
pub async fn delete_team(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<StatusCode, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
diesel::delete(people_teams::table.filter(people_teams::id.eq(id)))
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Delete error: {e}")))?;
|
|
|
|
Ok(StatusCode::NO_CONTENT)
|
|
}
|
|
|
|
pub async fn create_department(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<CreateDepartmentRequest>,
|
|
) -> Result<Json<Department>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
let id = Uuid::new_v4();
|
|
let now = Utc::now();
|
|
|
|
let department = Department {
|
|
id,
|
|
org_id,
|
|
bot_id,
|
|
name: req.name,
|
|
description: req.description,
|
|
code: req.code,
|
|
parent_id: req.parent_id,
|
|
head_id: req.head_id,
|
|
cost_center: req.cost_center,
|
|
is_active: true,
|
|
created_at: now,
|
|
updated_at: now,
|
|
};
|
|
|
|
diesel::insert_into(people_departments::table)
|
|
.values(&department)
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?;
|
|
|
|
Ok(Json(department))
|
|
}
|
|
|
|
pub async fn list_departments(
|
|
State(state): State<Arc<AppState>>,
|
|
) -> Result<Json<Vec<Department>>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
|
|
let depts: Vec<Department> = people_departments::table
|
|
.filter(people_departments::org_id.eq(org_id))
|
|
.filter(people_departments::bot_id.eq(bot_id))
|
|
.filter(people_departments::is_active.eq(true))
|
|
.order(people_departments::name.asc())
|
|
.load(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?;
|
|
|
|
Ok(Json(depts))
|
|
}
|
|
|
|
pub async fn list_skills(
|
|
State(state): State<Arc<AppState>>,
|
|
) -> Result<Json<Vec<Skill>>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
|
|
let skills: Vec<Skill> = people_skills::table
|
|
.filter(people_skills::org_id.eq(org_id))
|
|
.filter(people_skills::bot_id.eq(bot_id))
|
|
.filter(people_skills::is_active.eq(true))
|
|
.order(people_skills::name.asc())
|
|
.load(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?;
|
|
|
|
Ok(Json(skills))
|
|
}
|
|
|
|
pub async fn create_skill(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<CreateSkillRequest>,
|
|
) -> Result<Json<Skill>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
let id = Uuid::new_v4();
|
|
let now = Utc::now();
|
|
|
|
let skill = Skill {
|
|
id,
|
|
org_id,
|
|
bot_id,
|
|
name: req.name,
|
|
category: req.category,
|
|
description: req.description,
|
|
is_active: true,
|
|
created_at: now,
|
|
};
|
|
|
|
diesel::insert_into(people_skills::table)
|
|
.values(&skill)
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?;
|
|
|
|
Ok(Json(skill))
|
|
}
|
|
|
|
pub async fn add_person_skill(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(person_id): Path<Uuid>,
|
|
Json(req): Json<AddPersonSkillRequest>,
|
|
) -> Result<Json<PersonSkill>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let id = Uuid::new_v4();
|
|
let now = Utc::now();
|
|
|
|
let person_skill = PersonSkill {
|
|
id,
|
|
person_id,
|
|
skill_id: req.skill_id,
|
|
proficiency_level: req.proficiency_level.unwrap_or(1),
|
|
years_experience: req.years_experience.map(bd),
|
|
verified_by: None,
|
|
verified_at: None,
|
|
created_at: now,
|
|
};
|
|
|
|
diesel::insert_into(people_person_skills::table)
|
|
.values(&person_skill)
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?;
|
|
|
|
Ok(Json(person_skill))
|
|
}
|
|
|
|
pub async fn create_time_off(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<CreateTimeOffRequest>,
|
|
) -> Result<Json<TimeOff>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
let id = Uuid::new_v4();
|
|
let now = Utc::now();
|
|
|
|
let start_date = NaiveDate::parse_from_str(&req.start_date, "%Y-%m-%d")
|
|
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid start_date".to_string()))?;
|
|
|
|
let end_date = NaiveDate::parse_from_str(&req.end_date, "%Y-%m-%d")
|
|
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid end_date".to_string()))?;
|
|
|
|
let time_off = TimeOff {
|
|
id,
|
|
org_id,
|
|
bot_id,
|
|
person_id: req.person_id,
|
|
time_off_type: req.time_off_type,
|
|
status: "pending".to_string(),
|
|
start_date,
|
|
end_date,
|
|
hours_requested: req.hours_requested.map(bd),
|
|
reason: req.reason,
|
|
approved_by: None,
|
|
approved_at: None,
|
|
notes: None,
|
|
created_at: now,
|
|
updated_at: now,
|
|
};
|
|
|
|
diesel::insert_into(people_time_off::table)
|
|
.values(&time_off)
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?;
|
|
|
|
Ok(Json(time_off))
|
|
}
|
|
|
|
pub async fn list_time_off(
|
|
State(state): State<Arc<AppState>>,
|
|
Query(query): Query<ListQuery>,
|
|
) -> Result<Json<Vec<TimeOff>>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
let limit = query.limit.unwrap_or(50);
|
|
let offset = query.offset.unwrap_or(0);
|
|
|
|
let time_offs: Vec<TimeOff> = people_time_off::table
|
|
.filter(people_time_off::org_id.eq(org_id))
|
|
.filter(people_time_off::bot_id.eq(bot_id))
|
|
.order(people_time_off::start_date.desc())
|
|
.limit(limit)
|
|
.offset(offset)
|
|
.load(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?;
|
|
|
|
Ok(Json(time_offs))
|
|
}
|
|
|
|
pub async fn approve_time_off(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(id): Path<Uuid>,
|
|
Json(req): Json<ApproveTimeOffRequest>,
|
|
) -> Result<Json<TimeOff>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let now = Utc::now();
|
|
let status = if req.approved { "approved" } else { "rejected" };
|
|
|
|
diesel::update(people_time_off::table.filter(people_time_off::id.eq(id)))
|
|
.set((
|
|
people_time_off::status.eq(status),
|
|
people_time_off::approved_at.eq(Some(now)),
|
|
people_time_off::notes.eq(req.notes),
|
|
people_time_off::updated_at.eq(now),
|
|
))
|
|
.execute(&mut conn)
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?;
|
|
|
|
let time_off: TimeOff = people_time_off::table
|
|
.filter(people_time_off::id.eq(id))
|
|
.first(&mut conn)
|
|
.map_err(|_| (StatusCode::NOT_FOUND, "Time off request not found".to_string()))?;
|
|
|
|
Ok(Json(time_off))
|
|
}
|
|
|
|
pub async fn get_people_stats(
|
|
State(state): State<Arc<AppState>>,
|
|
) -> Result<Json<PeopleStats>, (StatusCode, String)> {
|
|
let mut conn = state.conn.get().map_err(|e| {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
|
})?;
|
|
|
|
let (org_id, bot_id) = get_bot_context(&state);
|
|
|
|
let total_people: i64 = people_table::table
|
|
.filter(people_table::org_id.eq(org_id))
|
|
.filter(people_table::bot_id.eq(bot_id))
|
|
.count()
|
|
.get_result(&mut conn)
|
|
.unwrap_or(0);
|
|
|
|
let active_people: i64 = people_table::table
|
|
.filter(people_table::org_id.eq(org_id))
|
|
.filter(people_table::bot_id.eq(bot_id))
|
|
.filter(people_table::is_active.eq(true))
|
|
.count()
|
|
.get_result(&mut conn)
|
|
.unwrap_or(0);
|
|
|
|
let total_teams: i64 = people_teams::table
|
|
.filter(people_teams::org_id.eq(org_id))
|
|
.filter(people_teams::bot_id.eq(bot_id))
|
|
.filter(people_teams::is_active.eq(true))
|
|
.count()
|
|
.get_result(&mut conn)
|
|
.unwrap_or(0);
|
|
|
|
let total_departments: i64 = people_departments::table
|
|
.filter(people_departments::org_id.eq(org_id))
|
|
.filter(people_departments::bot_id.eq(bot_id))
|
|
.filter(people_departments::is_active.eq(true))
|
|
.count()
|
|
.get_result(&mut conn)
|
|
.unwrap_or(0);
|
|
|
|
let pending_time_off: i64 = people_time_off::table
|
|
.filter(people_time_off::org_id.eq(org_id))
|
|
.filter(people_time_off::bot_id.eq(bot_id))
|
|
.filter(people_time_off::status.eq("pending"))
|
|
.count()
|
|
.get_result(&mut conn)
|
|
.unwrap_or(0);
|
|
|
|
let today = Utc::now().date_naive();
|
|
let month_start = NaiveDate::from_ymd_opt(today.year(), today.month(), 1)
|
|
.unwrap_or(today);
|
|
|
|
let new_hires_this_month: i64 = people_table::table
|
|
.filter(people_table::org_id.eq(org_id))
|
|
.filter(people_table::bot_id.eq(bot_id))
|
|
.filter(people_table::hire_date.ge(month_start))
|
|
.count()
|
|
.get_result(&mut conn)
|
|
.unwrap_or(0);
|
|
|
|
let stats = PeopleStats {
|
|
total_people,
|
|
active_people,
|
|
total_teams,
|
|
total_departments,
|
|
pending_time_off,
|
|
new_hires_this_month,
|
|
};
|
|
|
|
Ok(Json(stats))
|
|
}
|
|
|
|
pub fn configure_people_routes() -> Router<Arc<AppState>> {
|
|
Router::new()
|
|
.route("/api/people", get(list_people).post(create_person))
|
|
.route("/api/people/stats", get(get_people_stats))
|
|
.route("/api/people/:id", get(get_person).put(update_person).delete(delete_person))
|
|
.route("/api/people/:id/reports", get(get_direct_reports))
|
|
.route("/api/people/:id/skills", post(add_person_skill))
|
|
.route("/api/people/teams", get(list_teams).post(create_team))
|
|
.route("/api/people/teams/:id", get(get_team).delete(delete_team))
|
|
.route("/api/people/teams/:id/members", post(add_team_member))
|
|
.route("/api/people/teams/:team_id/members/:person_id", delete(remove_team_member))
|
|
.route("/api/people/departments", get(list_departments).post(create_department))
|
|
.route("/api/people/skills", get(list_skills).post(create_skill))
|
|
.route("/api/people/time-off", get(list_time_off).post(create_time_off))
|
|
.route("/api/people/time-off/:id/approve", put(approve_time_off))
|
|
}
|