1use axum::{
2 extract::{Query, State},
3 http::StatusCode,
4 response::IntoResponse,
5 Json,
6};
7use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use validator::Validate;
11use crate::{
12 auth::claims::AuthUser,
13 response::ApiResponse,
14};
15use common::format_validation_errors;
16use db::models::{assignment, module, user_module_role};
17use sea_orm::{ColumnTrait, Condition, EntityTrait, JoinType, QueryFilter, RelationTrait, QuerySelect};
18use util::state::AppState;
19
20#[derive(Debug, Deserialize, Validate)]
22pub struct GetEventsQuery {
23 #[serde(default)]
25 pub from: Option<String>,
26
27 #[serde(default)]
29 pub to: Option<String>,
30}
31
32#[derive(Debug, Serialize)]
34pub struct EventItem {
35 pub r#type: String,
36 pub content: String,
37}
38
39#[derive(Debug, Serialize)]
41pub struct EventsResponse {
42 pub events: HashMap<String, Vec<EventItem>>,
43}
44
45fn format_iso_datetime(dt: DateTime<Utc>) -> String {
47 dt.format("%Y-%m-%dT%H:%M:%S").to_string()
48}
49
50fn parse_optional_datetime(s: Option<&str>) -> Result<Option<DateTime<Utc>>, String> {
52 let Some(s) = s else {
53 return Ok(None);
54 };
55
56 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
57 return Ok(Some(dt.with_timezone(&Utc)));
58 }
59
60 if let Ok(date) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
61 let datetime = date.and_time(NaiveTime::MIN).and_utc();
62 return Ok(Some(datetime));
63 }
64
65 if let Ok(naive_dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
66 return Ok(Some(Utc.from_utc_datetime(&naive_dt)));
67 }
68
69 Err("Invalid date format. Use YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss".to_string())
70}
71
72pub async fn get_my_events(
88 State(state): State<AppState>,
89 axum::Extension(user): axum::Extension<AuthUser>,
90 Query(query): Query<GetEventsQuery>,
91) -> impl IntoResponse {
92 if let Err(e) = query.validate() {
93 return (
94 StatusCode::BAD_REQUEST,
95 Json(ApiResponse::<()>::error(format_validation_errors(&e))),
96 ).into_response();
97 }
98
99 let user_id = user.0.sub;
100
101 let from_dt = match parse_optional_datetime(query.from.as_deref()) {
102 Ok(dt) => dt,
103 Err(msg) => {
104 return (
105 StatusCode::BAD_REQUEST,
106 Json(ApiResponse::<()>::error(msg)),
107 ).into_response();
108 }
109 };
110
111 let to_dt = match parse_optional_datetime(query.to.as_deref()) {
112 Ok(dt) => dt,
113 Err(msg) => {
114 return (
115 StatusCode::BAD_REQUEST,
116 Json(ApiResponse::<()>::error(msg)),
117 ).into_response();
118 }
119 };
120
121 if let (Some(from), Some(to)) = (from_dt, to_dt) {
122 if from > to {
123 return (
124 StatusCode::BAD_REQUEST,
125 Json(ApiResponse::<()>::error(
126 "'from' date must be before 'to' date".to_string()
127 )),
128 ).into_response();
129 }
130 }
131
132 let mut events_map: HashMap<String, Vec<EventItem>> = HashMap::new();
133
134 let mut assignment_query = assignment::Entity::find()
135 .join(JoinType::InnerJoin, assignment::Relation::Module.def())
136 .join(
137 JoinType::InnerJoin,
138 module::Relation::UserModuleRole.def(),
139 )
140 .filter(user_module_role::Column::UserId.eq(user_id));
141
142 let mut date_conditions = Condition::all();
143
144 if let Some(from) = from_dt {
145 date_conditions = date_conditions.add(
146 Condition::any()
147 .add(assignment::Column::AvailableFrom.gte(from))
148 .add(assignment::Column::DueDate.gte(from))
149 );
150 }
151
152 if let Some(to) = to_dt {
153 date_conditions = date_conditions.add(
154 Condition::any()
155 .add(assignment::Column::AvailableFrom.lte(to))
156 .add(assignment::Column::DueDate.lte(to))
157 );
158 }
159
160 assignment_query = assignment_query.filter(date_conditions);
161
162 let assignments = match assignment_query.all(state.db()).await {
163 Ok(assignments) => assignments,
164 Err(e) => {
165 return (
166 StatusCode::INTERNAL_SERVER_ERROR,
167 Json(ApiResponse::<()>::error(format!("Database error: {}", e))),
168 ).into_response();
169 }
170 };
171
172 for assignment in assignments {
173 let available_in_range = match (from_dt, to_dt) {
174 (Some(from), Some(to)) => assignment.available_from >= from && assignment.available_from <= to,
175 (Some(from), None) => assignment.available_from >= from,
176 (None, Some(to)) => assignment.available_from <= to,
177 (None, None) => true,
178 };
179
180 if available_in_range {
181 let available_key = format_iso_datetime(assignment.available_from);
182 events_map
183 .entry(available_key)
184 .or_default()
185 .push(EventItem {
186 r#type: "warning".to_string(),
187 content: format!("{} available", assignment.name),
188 });
189 }
190
191 let due_in_range = match (from_dt, to_dt) {
192 (Some(from), Some(to)) => assignment.due_date >= from && assignment.due_date <= to,
193 (Some(from), None) => assignment.due_date >= from,
194 (None, Some(to)) => assignment.due_date <= to,
195 (None, None) => true,
196 };
197
198 if due_in_range {
199 let due_key = format_iso_datetime(assignment.due_date);
200 events_map
201 .entry(due_key)
202 .or_default()
203 .push(EventItem {
204 r#type: "error".to_string(),
205 content: format!("{} due", assignment.name),
206 });
207 }
208 }
209
210 (
211 StatusCode::OK,
212 Json(ApiResponse::success(
213 EventsResponse { events: events_map },
214 "Events retrieved successfully",
215 )),
216 ).into_response()
217}