From 357a2ed423536cba82cef85dec1dbee794e1d964 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Mon, 11 May 2026 13:43:16 +0100 Subject: [PATCH] Added timezone support --- Cargo.lock | 3 +- Cargo.toml | 3 +- src/admin.rs | 70 +++++++++++++++++++---------------- src/i18n.rs | 3 ++ src/main.rs | 1 + src/public.rs | 11 +++--- templates/admin/schedule.html | 1 + templates/admin/settings.html | 6 +++ 8 files changed, 60 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10a180a..a43a0c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3255,9 +3255,10 @@ dependencies = [ [[package]] name = "web-petting" -version = "0.1.2" +version = "0.1.3" dependencies = [ "chrono", + "chrono-tz", "cot", "futures", "multer", diff --git a/Cargo.toml b/Cargo.toml index 4f0223e..aff6532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "web-petting" -version = "0.1.3" +version = "0.1.4" edition = "2024" [dependencies] cot = { version = "0.6.0", features = ["sqlite"] } chrono = "0.4" +chrono-tz = "0.10" serde = { version = "1", features = ["derive"] } serde_html_form = "0.4" password-auth = "1" diff --git a/src/admin.rs b/src/admin.rs index 25ba67a..6318674 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -109,7 +109,7 @@ fn rand_client_color() -> &'static str { CLIENT_COLORS[(h.finish() as usize) % CLIENT_COLORS.len()] } -fn now() -> chrono::NaiveDateTime { +fn now_utc() -> chrono::NaiveDateTime { chrono::Utc::now().naive_utc() } @@ -260,6 +260,7 @@ struct ScheduleTemplate<'a> { t: &'a Translations, lang: Lang, admin_name: &'a str, + timezone: String, } #[derive(Debug, Template)] @@ -399,8 +400,8 @@ async fn setup_submit(request: Request, session: Session, db: Database) -> cot:: password_hash: password_auth::generate_hash(&form.password), display_name: display, status: "active".to_string(), - created_at: now(), - updated_at: now(), + created_at: now_utc(), + updated_at: now_utc(), }; user.save(&db).await?; @@ -468,7 +469,8 @@ async fn admin_index(request: Request, session: Session, db: Database) -> cot::R Err(resp) => return Ok(resp), }; let user_id = get_admin_id(&session).await.unwrap_or(0); - let today = chrono::Utc::now().date_naive(); + let tz = crate::tz::load_tz(&db).await; + let today = crate::tz::today_in_tz(tz); let all_visits = Visit::objects().all(&db).await?; let clients = Client::objects().all(&db).await?; @@ -664,7 +666,7 @@ async fn client_edit_submit( if let Some(color) = form.color.filter(|s| !s.trim().is_empty()) { client.color = Some(color); } - client.updated_at = now(); + client.updated_at = now_utc(); client.save(&db).await?; } Redirect::new(format!("/admin/clients?lang={}", lang.code())).into_response() @@ -740,8 +742,8 @@ async fn add_user(request: Request, session: Session, db: Database) -> cot::Resu password_hash: password_auth::generate_hash(&form.password), display_name: display, status: "active".to_string(), - created_at: now(), - updated_at: now(), + created_at: now_utc(), + updated_at: now_utc(), }; user.save(&db).await?; None @@ -770,6 +772,7 @@ struct SettingsForm { telegram_chat_id: String, contact_info: String, pricing_info: String, + timezone: String, } async fn save_settings(request: Request, session: Session, db: Database) -> cot::Result { @@ -784,13 +787,14 @@ async fn save_settings(request: Request, session: Session, db: Database) -> cot: ("telegram_chat_id", form.telegram_chat_id), ("contact_info", form.contact_info), ("pricing_info", form.pricing_info), + ("timezone", form.timezone), ] { let k = key.to_string(); let existing = query!(Setting, $key == k).get(&db).await?; match existing { Some(mut s) => { s.value = value; - s.updated_at = now(); + s.updated_at = now_utc(); s.save(&db).await?; } None => { @@ -798,7 +802,7 @@ async fn save_settings(request: Request, session: Session, db: Database) -> cot: id: Auto::auto(), key: key.to_string(), value, - updated_at: now(), + updated_at: now_utc(), }; s.save(&db).await?; } @@ -835,7 +839,7 @@ async fn lead_set_status( if let Some(mut lead) = query!(Lead, $id == lead_id).get(&db).await? { lead.status = form.status; - lead.updated_at = now(); + lead.updated_at = now_utc(); lead.save(&db).await?; } @@ -864,14 +868,14 @@ async fn lead_convert( media_token: rand_token(), color: Some(rand_client_color().to_string()), status: "active".to_string(), - created_at: now(), - updated_at: now(), + created_at: now_utc(), + updated_at: now_utc(), }; client.save(&db).await?; lead.status = "converted".to_string(); lead.client_id = Some(ForeignKey::from(&client)); - lead.updated_at = now(); + lead.updated_at = now_utc(); lead.save(&db).await?; } @@ -890,7 +894,7 @@ async fn client_archive( } if let Some(mut client) = query!(Client, $id == client_id).get(&db).await? { client.status = "archived".to_string(); - client.updated_at = now(); + client.updated_at = now_utc(); client.save(&db).await?; } Redirect::new(format!("/admin/clients?lang={}", lang.code())).into_response() @@ -908,7 +912,7 @@ async fn client_activate( } if let Some(mut client) = query!(Client, $id == client_id).get(&db).await? { client.status = "active".to_string(); - client.updated_at = now(); + client.updated_at = now_utc(); client.save(&db).await?; } Redirect::new(format!("/admin/clients?lang={}", lang.code())).into_response() @@ -926,7 +930,7 @@ async fn user_archive( } if let Some(mut user) = query!(User, $id == user_id).get(&db).await? { user.status = "archived".to_string(); - user.updated_at = now(); + user.updated_at = now_utc(); user.save(&db).await?; } Redirect::new(format!("/admin/users?lang={}", lang.code())).into_response() @@ -944,7 +948,7 @@ async fn user_activate( } if let Some(mut user) = query!(User, $id == user_id).get(&db).await? { user.status = "active".to_string(); - user.updated_at = now(); + user.updated_at = now_utc(); user.save(&db).await?; } Redirect::new(format!("/admin/users?lang={}", lang.code())).into_response() @@ -972,8 +976,8 @@ async fn add_lead(request: Request, session: Session, db: Database) -> cot::Resu comment: form.comment.filter(|s| !s.trim().is_empty()), status: "new".to_string(), client_id: None, - created_at: now(), - updated_at: now(), + created_at: now_utc(), + updated_at: now_utc(), }; lead.save(&db).await?; @@ -1014,8 +1018,8 @@ async fn add_client(request: Request, session: Session, db: Database) -> cot::Re media_token: rand_token(), color: Some(rand_client_color().to_string()), status: "active".to_string(), - created_at: now(), - updated_at: now(), + created_at: now_utc(), + updated_at: now_utc(), }; client.save(&db).await?; @@ -1026,16 +1030,18 @@ async fn add_client(request: Request, session: Session, db: Database) -> cot::Re // Schedule Handlers // --------------------------------------------------------------------------- -async fn schedule_page(request: Request, session: Session) -> cot::Result { +async fn schedule_page(request: Request, session: Session, db: Database) -> cot::Result { let lang = detect_lang(&request); let admin_name = match require_auth(&session, lang).await { Ok(name) => name, Err(resp) => return Ok(resp), }; + let tz = crate::tz::load_tz(&db).await; let body = ScheduleTemplate { t: lang.t(), lang, admin_name: &admin_name, + timezone: tz.to_string(), } .render()?; html_response(body, lang) @@ -1089,10 +1095,12 @@ async fn schedule_events( } } + let tz = crate::tz::load_tz(&db).await; + let tz_today = crate::tz::today_in_tz(tz); let start_date = chrono::NaiveDate::parse_from_str(start_str, "%Y-%m-%d") - .unwrap_or_else(|_| chrono::Utc::now().date_naive() - chrono::Duration::days(60)); + .unwrap_or_else(|_| tz_today - chrono::Duration::days(60)); let end_date = chrono::NaiveDate::parse_from_str(end_str, "%Y-%m-%d") - .unwrap_or_else(|_| chrono::Utc::now().date_naive() + chrono::Duration::days(60)); + .unwrap_or_else(|_| tz_today + chrono::Duration::days(60)); let visits = Visit::objects().all(&db).await?; let clients = Client::objects().all(&db).await?; @@ -1203,8 +1211,8 @@ async fn schedule_create( public_notes: None, client_feedback: None, status: "scheduled".to_string(), - created_at: now(), - updated_at: now(), + created_at: now_utc(), + updated_at: now_utc(), }; visit.save(&db).await?; } @@ -1286,7 +1294,7 @@ async fn schedule_edit_submit( visit.status = form.status; visit.notes = form.notes.filter(|s| !s.trim().is_empty()); visit.public_notes = form.public_notes.filter(|s| !s.trim().is_empty()); - visit.updated_at = now(); + visit.updated_at = now_utc(); visit.save(&db).await?; } Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response() @@ -1318,7 +1326,7 @@ async fn visit_set_done( } if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? { visit.status = "completed".to_string(); - visit.updated_at = now(); + visit.updated_at = now_utc(); visit.save(&db).await?; } Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response() @@ -1336,7 +1344,7 @@ async fn visit_set_cancel( } if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? { visit.status = "cancelled".to_string(); - visit.updated_at = now(); + visit.updated_at = now_utc(); visit.save(&db).await?; } Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response() @@ -1556,7 +1564,7 @@ async fn media_upload_submit( file_type: ftype, caption: caption_opt.clone(), status: "active".to_string(), - created_at: now(), + created_at: now_utc(), }; media.save(&db).await?; } @@ -1759,7 +1767,7 @@ async fn testimonial_add( image_path, status: "active".to_string(), sort_order: max_order + 1, - created_at: now(), + created_at: now_utc(), }; testimonial.save(&db).await?; } diff --git a/src/i18n.rs b/src/i18n.rs index cd8a452..e8a15fb 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -130,6 +130,7 @@ pub struct Translations { pub settings_telegram_chat_id: &'static str, pub settings_contact_info: &'static str, pub settings_pricing_info: &'static str, + pub settings_timezone: &'static str, pub landing_contact_label: &'static str, pub landing_pricing_title: &'static str, @@ -338,6 +339,7 @@ static RU: Translations = Translations { settings_telegram_chat_id: "Chat ID для уведомлений", settings_contact_info: "Контактная информация (отображается на лендинге)", settings_pricing_info: "Блок с ценами (отображается на лендинге)", + settings_timezone: "Часовой пояс (например Asia/Vladivostok)", landing_contact_label: "Или свяжитесь с нами напрямую", landing_pricing_title: "Стоимость", @@ -536,6 +538,7 @@ static EN: Translations = Translations { settings_telegram_chat_id: "Notification Chat ID", settings_contact_info: "Contact info (shown on landing page)", settings_pricing_info: "Pricing block (shown on landing page)", + settings_timezone: "Timezone (e.g. Asia/Vladivostok)", landing_contact_label: "Or contact us directly", landing_pricing_title: "Pricing", diff --git a/src/main.rs b/src/main.rs index 51826ca..663c6dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod migrations; pub mod models; mod public; mod telegram; +mod tz; use cot::cli::CliMetadata; use cot::config::{ diff --git a/src/public.rs b/src/public.rs index d35efcc..d0afeed 100644 --- a/src/public.rs +++ b/src/public.rs @@ -60,7 +60,7 @@ fn html_response(body: String, lang: Lang) -> cot::Result { .into_response() } -fn now() -> chrono::NaiveDateTime { +fn now_utc() -> chrono::NaiveDateTime { chrono::Utc::now().naive_utc() } @@ -131,8 +131,8 @@ async fn submit_lead(request: Request, db: Database) -> cot::Result { comment: form.comment.filter(|s| !s.trim().is_empty()), status: "new".to_string(), client_id: None, - created_at: now(), - updated_at: now(), + created_at: now_utc(), + updated_at: now_utc(), }; lead.save(&db).await?; @@ -188,7 +188,8 @@ async fn client_portal( }; let client_id = client.id.unwrap(); - let today = chrono::Utc::now().date_naive(); + let tz = crate::tz::load_tz(&db).await; + let today = crate::tz::today_in_tz(tz); let mut visits = Visit::objects().all(&db).await?; visits.retain(|v| v.client_id.primary_key().unwrap() == client_id && v.status != "cancelled"); @@ -277,7 +278,7 @@ async fn submit_feedback( if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? { if visit.client_id.primary_key().unwrap() == client_id { visit.client_feedback = Some(form.feedback); - visit.updated_at = now(); + visit.updated_at = now_utc(); visit.save(&db).await?; } } diff --git a/templates/admin/schedule.html b/templates/admin/schedule.html index 8481fea..64a6027 100644 --- a/templates/admin/schedule.html +++ b/templates/admin/schedule.html @@ -81,6 +81,7 @@ document.addEventListener('DOMContentLoaded', function() { const calendar = new FullCalendar.Calendar(calEl, { locale: lang, + timeZone: '{{ timezone }}', initialView: window.innerWidth < 768 ? 'listWeek' : 'dayGridMonth', headerToolbar: { left: 'prev,next today', diff --git a/templates/admin/settings.html b/templates/admin/settings.html index 412e7e3..e8e0ac1 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -38,6 +38,12 @@ +
+ +
+ +
+