api/routes/me/
announcements.rs

1//! # My Announcements Handlers
2//!
3//! Provides endpoints to fetch announcements for the currently authenticated user.
4//!
5//! Users can retrieve a paginated list of announcements filtered by role, year, pinned status,
6//! search query, and sorted by various fields. Only announcements in modules the user
7//! is associated with are returned.
8
9use axum::{
10    Extension, Json,
11    extract::{Query, State},
12    http::StatusCode,
13    response::IntoResponse,
14};
15use db::models::{announcements, module, user, user_module_role};
16use migration::Expr;
17use sea_orm::{
18    ColumnTrait, Condition, EntityTrait, JoinType, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, RelationTrait
19};
20use serde::{Deserialize, Serialize};
21use util::state::AppState;
22
23use crate::{auth::AuthUser, response::ApiResponse};
24
25/// Query parameters for filtering, sorting, and pagination of announcements
26#[derive(Debug, Deserialize)]
27pub struct FilterReq {
28    /// Page number (default: 1)
29    pub page: Option<i32>,
30    /// Items per page (default: 20)
31    pub per_page: Option<i32>,
32    /// Search query (matches announcement title, module code, or username)
33    pub query: Option<String>,
34    /// Filter announcements by role (lecturer, assistant_lecturer, tutor, student)
35    pub role: Option<String>,
36    /// Filter by module year
37    pub year: Option<i32>,
38    /// Filter by pinned status
39    pub pinned: Option<bool>,
40    /// Sort fields (comma-separated, prefix with `-` for descending)
41    pub sort: Option<String>,
42}
43
44/// Response object for a user
45#[derive(Serialize)]
46pub struct UserResponse {
47    pub id: i64,
48    pub username: String,
49}
50
51/// Response object for a module
52#[derive(Serialize)]
53pub struct ModuleResponse {
54    pub id: i64,
55    pub code: String,
56}
57
58/// Response object for an announcement
59#[derive(Serialize)]
60pub struct AnnouncementResponse {
61    pub id: i64,
62    pub title: String,
63    pub content: String,
64    pub pinned: bool,
65    pub created_at: String,
66    pub updated_at: String,
67    pub module: ModuleResponse,
68    pub user: UserResponse,
69}
70
71/// Response for a paginated list of announcements
72#[derive(Serialize)]
73pub struct FilterResponse {
74    pub announcements: Vec<AnnouncementResponse>,
75    pub page: i32,
76    pub per_page: i32,
77    pub total: i32,
78}
79
80impl FilterResponse {
81    fn new(announcements: Vec<AnnouncementResponse>, page: i32, per_page: i32, total: i32) -> Self {
82        Self { announcements, page, per_page, total }
83    }
84}
85
86/// Retrieves announcements for the currently authenticated user.
87///
88/// **Endpoint:** `GET /my/announcements`  
89/// **Permissions:** User must be associated with at least one module (student, tutor, lecturer, assistant)
90///
91/// ### Query parameters
92/// - `page` → Page number (default: 1)
93/// - `per_page` → Number of items per page (default: 20, max: 100)
94/// - `query` → Search query in announcement title, module code, or username
95/// - `role` → Filter announcements by user role
96/// - `year` → Filter announcements by module year
97/// - `pinned` → Filter by pinned status
98/// - `sort` → Sort announcements by fields (e.g., `created_at,-updated_at`)
99///
100/// ### Responses
101/// - `200 OK` → Announcements retrieved successfully
102/// ```json
103/// {
104///   "success": true,
105///   "data": {
106///     "announcements": [ /* Announcement objects */ ],
107///     "page": 1,
108///     "per_page": 20,
109///     "total": 42
110///   },
111///   "message": "Announcements retrieved"
112/// }
113/// ```
114/// - `500 Internal Server Error` → Failed to retrieve announcements
115/// ```json
116/// {
117///   "success": false,
118///   "data": null,
119///   "message": "Failed to retrieve announcements"
120/// }
121/// ```
122pub async fn get_my_announcements(
123    State(state): State<AppState>,
124    Extension(AuthUser(claims)): Extension<AuthUser>,
125    Query(params): Query<FilterReq>,
126) -> impl IntoResponse {
127    let db = state.db();
128    let user_id = claims.sub;
129    let page = params.page.unwrap_or(1).max(1);
130    let per_page = params.per_page.unwrap_or(20).min(100);
131
132    let allowed_roles = vec!["lecturer", "assistant_lecturer", "tutor", "student"];
133    let requested_role = params.role.clone().filter(|r| allowed_roles.contains(&r.as_str()));
134
135    let memberships = user_module_role::Entity::find()
136        .filter(user_module_role::Column::UserId.eq(user_id))
137        .filter(user_module_role::Column::Role.is_in(allowed_roles.clone()))
138        .all(db)
139        .await
140        .unwrap_or_default();
141
142    if memberships.is_empty() {
143        let response = FilterResponse::new(vec![], page, per_page, 0);
144        return (StatusCode::OK, Json(ApiResponse::success(response, "Announcements retrieved"))).into_response();
145    }
146
147    let module_ids: Vec<i64> = memberships.iter()
148        .filter(|m| requested_role.as_ref().map_or(true, |r| &m.role.to_string() == r))
149        .map(|m| m.module_id)
150        .collect();
151
152    if module_ids.is_empty() {
153        let response = FilterResponse::new(vec![], page, per_page, 0);
154        return (StatusCode::OK, Json(ApiResponse::success(response, "Announcements retrieved"))).into_response();
155    }
156
157    let mut condition = Condition::all().add(announcements::Column::ModuleId.is_in(module_ids));
158
159    if let Some(year) = params.year {
160        condition = condition.add(Expr::col((module::Entity, module::Column::Year)).eq(year));
161    }
162
163    if let Some(pinned) = params.pinned {
164        condition = condition.add(announcements::Column::Pinned.eq(pinned));
165    }
166
167    if let Some(ref q) = params.query {
168        let pattern = format!("%{}%", q.to_lowercase());
169        condition = condition.add(
170            Condition::any()
171                .add(Expr::cust("LOWER(announcements.title)").like(&pattern))
172                .add(Expr::cust("LOWER(module.code)").like(&pattern))
173                .add(Expr::cust("LOWER(user.username)").like(&pattern))
174        );
175    }
176
177    let mut query = announcements::Entity::find()
178        .join(JoinType::InnerJoin, announcements::Relation::Module.def())
179        .join(JoinType::InnerJoin, announcements::Relation::User.def())
180        .filter(condition);
181
182    if let Some(sort_param) = &params.sort {
183        for sort in sort_param.split(',') {
184            let (field, asc) = if sort.starts_with('-') { (&sort[1..], false) } else { (sort, true) };
185            query = match field {
186                "created_at" => if asc { query.order_by_asc(announcements::Column::CreatedAt) } else { query.order_by_desc(announcements::Column::CreatedAt) },
187                "updated_at" => if asc { query.order_by_asc(announcements::Column::UpdatedAt) } else { query.order_by_desc(announcements::Column::UpdatedAt) },
188                _ => query,
189            };
190        }
191    } else {
192        query = query.order_by_desc(announcements::Column::CreatedAt).order_by_asc(announcements::Column::Id);
193    }
194
195    let paginator = query.clone().paginate(db, per_page as u64);
196    let total = match paginator.num_items().await {
197        Ok(n) => n as i32,
198        Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<FilterResponse>::error("Error counting announcements"))).into_response(),
199    };
200
201    match paginator.fetch_page((page - 1) as u64).await {
202        Ok(results) => {
203            let mut announcements_vec = Vec::new();
204            for a in results {
205                let m = module::Entity::find_by_id(a.module_id).one(db).await.unwrap_or(None);
206                if m.is_none() { continue; }
207                let m = m.unwrap();
208
209                let u = user::Entity::find_by_id(a.user_id).one(db).await.unwrap_or(None);
210
211                announcements_vec.push(AnnouncementResponse {
212                    id: a.id,
213                    title: a.title,
214                    content: a.body,
215                    pinned: a.pinned,
216                    created_at: a.created_at.to_string(),
217                    updated_at: a.updated_at.to_string(),
218                    module: ModuleResponse { id: m.id, code: m.code },
219                    user: UserResponse { id: a.user_id, username: u.map(|uu| uu.username).unwrap_or_default() },
220                });
221            }
222
223            let response = FilterResponse::new(announcements_vec, page, per_page, total);
224            (StatusCode::OK, Json(ApiResponse::success(response, "Announcements retrieved"))).into_response()
225        }
226        Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<FilterResponse>::error("Failed to retrieve announcements"))).into_response(),
227    }
228}