generalbots/src/people/mod.rs
Rodrigo Rodriguez 5ea171d126
Some checks failed
BotServer CI / build (push) Failing after 1m34s
Refactor: Split large files into modular subdirectories
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>
2026-02-12 21:09:30 +00:00

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))
}