11use aide:: NoApi ;
2+ use axum:: Json ;
23use axum:: extract:: ConnectInfo ;
3- use axum:: extract:: { Json , Path , State } ;
4+ use axum:: extract:: { Path , State } ;
45use axum:: http:: StatusCode ;
56use axum:: response:: { IntoResponse , Response } ;
67use chrono:: Utc ;
78use diesel:: prelude:: * ;
89use ipnetwork:: IpNetwork ;
9- use serde_json:: json;
10+ use schemars:: JsonSchema ;
11+ use serde:: Serialize ;
1012use std:: net:: IpAddr ;
1113use std:: net:: SocketAddr ;
1214
@@ -16,15 +18,47 @@ use crate::models::heartbeat::*;
1618use crate :: models:: project:: get_or_create_project_id;
1719use crate :: schema:: heartbeats;
1820use crate :: state:: AppState ;
19- use crate :: utils:: auth:: { get_user_id_from_api_key, get_valid_api_key} ;
21+ use crate :: utils:: auth:: { get_user_from_api_key , get_user_id_from_api_key, get_valid_api_key} ;
2022use crate :: utils:: extractors:: DbConnection ;
2123use crate :: utils:: http:: extract_client_ip_from_headers;
22- use crate :: utils:: time:: { TimeFormat , human_readable_duration} ;
24+ use crate :: utils:: time:: {
25+ TimeFormat , get_day_end_utc, get_day_start_utc, get_today_in_timezone, human_readable_duration,
26+ parse_timezone,
27+ } ;
2328use std:: collections:: { HashMap , hash_map} ;
2429
2530const MAX_HEARTBEATS_PER_REQUEST : usize = 100 ;
2631const HEARTBEAT_INSERT_BATCH_SIZE : usize = 1_000 ; // avoids hitting Postgres' 65k parameter limit
2732
33+ #[ derive( Serialize , JsonSchema ) ]
34+ pub struct StatusBarResponse {
35+ data : StatusBarResponseData ,
36+ }
37+
38+ #[ derive( Serialize , JsonSchema ) ]
39+ pub struct StatusBarResponseData {
40+ grand_total : StatusBarResponseDataGrandTotal ,
41+ range : StatusBarResponseDataRange ,
42+ }
43+
44+ #[ derive( Serialize , JsonSchema ) ]
45+ pub struct StatusBarResponseDataGrandTotal {
46+ decimal : String ,
47+ digital : String ,
48+ hours : i64 ,
49+ minutes : i64 ,
50+ human_readable : String ,
51+ total_seconds : i64 ,
52+ }
53+
54+ #[ derive( Serialize , JsonSchema ) ]
55+ pub struct StatusBarResponseDataRange {
56+ date : String ,
57+ end : String ,
58+ start : String ,
59+ timezone : String ,
60+ }
61+
2862/// Process heartbeat request and store in the database
2963async fn process_heartbeat_request (
3064 app_state : & AppState ,
@@ -132,14 +166,15 @@ pub async fn create_heartbeats(
132166/// Handler to get today's status bar data
133167pub async fn get_statusbar_today (
134168 State ( app_state) : State < AppState > ,
135- Path ( id) : Path < String > ,
136169 NoApi ( DbConnection ( mut conn) ) : NoApi < DbConnection > ,
170+ Path ( id) : Path < String > ,
137171 headers : axum:: http:: HeaderMap ,
138172 uri : axum:: http:: Uri ,
139- ) -> Result < Json < serde_json:: Value > , Response > {
140- let user_id: i32 = if id != "current" {
173+ ) -> Result < Json < StatusBarResponse > , Response > {
174+ let ( user_id, timezone) = if id != "current" {
175+ // not implemented
141176 match id. parse :: < i32 > ( ) {
142- Ok ( id) => id ,
177+ Ok ( id) => ( id , None ) ,
143178 Err ( _) => return Err ( ( StatusCode :: BAD_REQUEST , "Bad request" ) . into_response ( ) ) ,
144179 }
145180 } else {
@@ -149,22 +184,20 @@ pub async fn get_statusbar_today(
149184 None => return Err ( ( StatusCode :: BAD_REQUEST , "Bad request" ) . into_response ( ) ) ,
150185 } ;
151186
152- let user_result = get_user_id_from_api_key ( & app_state. db_pool , & api_key) . await ;
153- match user_result {
154- Some ( id ) => id ,
187+ let user_result = get_user_from_api_key ( & app_state. db_pool , & api_key) . await ;
188+ let user = match user_result {
189+ Some ( user ) => user ,
155190 None => return Err ( ( StatusCode :: BAD_REQUEST , "Bad request" ) . into_response ( ) ) ,
156- }
191+ } ;
192+
193+ ( user. id , Some ( user. timezone ) )
157194 } ;
158195
159- // calculate today's date range
160- let today = Utc :: now ( ) . date_naive ( ) ;
161- let start_of_day = today. and_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) . and_utc ( ) ;
162- let end_of_day = today
163- . succ_opt ( )
164- . unwrap_or ( today)
165- . and_hms_opt ( 0 , 0 , 0 )
166- . unwrap ( )
167- . and_utc ( ) ;
196+ // calculate today's date range in user's timezone
197+ let user_tz = parse_timezone ( timezone. as_deref ( ) . unwrap_or ( "UTC" ) ) ;
198+ let today = get_today_in_timezone ( user_tz) ;
199+ let start_of_day = get_day_start_utc ( today, user_tz) ;
200+ let end_of_day = get_day_end_utc ( today, user_tz) ;
168201
169202 match Heartbeat :: get_user_duration_seconds (
170203 & mut conn,
@@ -181,18 +214,24 @@ pub async fn get_statusbar_today(
181214 Ok ( total_seconds) => {
182215 let time_obj = human_readable_duration ( total_seconds, TimeFormat :: HourMinute ) ;
183216
184- Ok ( Json ( json ! ( {
185- "data" : {
186- "grand_total" : {
187- "decimal" : format!( "{:.2}" , total_seconds as f64 / 3600.0 ) ,
188- "digital" : format!( "{:02}:{:02}" , time_obj. hours, time_obj. minutes) ,
189- "hours" : time_obj. hours,
190- "minutes" : time_obj. minutes,
191- "text" : time_obj. human_readable,
192- "total_seconds" : total_seconds
193- }
194- }
195- } ) ) )
217+ Ok ( Json ( StatusBarResponse {
218+ data : StatusBarResponseData {
219+ grand_total : StatusBarResponseDataGrandTotal {
220+ decimal : format ! ( "{:.2}" , total_seconds as f64 / 3600.0 ) ,
221+ digital : format ! ( "{:02}:{:02}" , time_obj. hours, time_obj. minutes) ,
222+ hours : time_obj. hours ,
223+ minutes : time_obj. minutes ,
224+ human_readable : time_obj. human_readable ,
225+ total_seconds,
226+ } ,
227+ range : StatusBarResponseDataRange {
228+ date : today. format ( "%Y-%m-%d" ) . to_string ( ) ,
229+ start : start_of_day. to_rfc3339 ( ) ,
230+ end : end_of_day. to_rfc3339 ( ) ,
231+ timezone : timezone. unwrap_or_else ( || "UTC" . to_string ( ) ) ,
232+ } ,
233+ } ,
234+ } ) )
196235 }
197236 Err ( err) => {
198237 eprintln ! ( "❌ Error calculating duration: {}" , err) ;
0 commit comments