diff --git a/Cargo.toml b/Cargo.toml index a89446b..0eb8cc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "web-petting" -version = "0.1.7" +version = "0.1.8" edition = "2024" [dependencies] diff --git a/src/admin.rs b/src/admin.rs index 680a153..c36cb13 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -401,6 +401,8 @@ async fn setup_submit(request: Request, session: Session, db: Database) -> cot:: login: form.login, password_hash: password_auth::generate_hash(&form.password), display_name: display, + telegram_chat_id: None, + telegram_notifications: Some(false), status: "active".to_string(), created_at: now_utc(), updated_at: now_utc(), @@ -760,6 +762,8 @@ async fn add_user(request: Request, session: Session, db: Database) -> cot::Resu login: form.login, password_hash: password_auth::generate_hash(&form.password), display_name: display, + telegram_chat_id: None, + telegram_notifications: Some(false), status: "active".to_string(), created_at: now_utc(), updated_at: now_utc(), @@ -788,7 +792,6 @@ async fn add_user(request: Request, session: Session, db: Database) -> cot::Resu #[derive(Deserialize)] struct SettingsForm { telegram_bot_token: String, - telegram_chat_id: String, contact_info: String, pricing_info: String, timezone: String, @@ -804,7 +807,6 @@ async fn save_settings(request: Request, session: Session, db: Database) -> cot: for (key, value) in [ ("telegram_bot_token", form.telegram_bot_token), - ("telegram_chat_id", form.telegram_chat_id), ("contact_info", form.contact_info), ("pricing_info", form.pricing_info), ("timezone", form.timezone), @@ -975,6 +977,31 @@ async fn user_activate( Redirect::new(format!("/admin/users?lang={}", lang.code())).into_response() } +#[derive(Deserialize)] +struct UserTelegramForm { + telegram_chat_id: Option, + telegram_notifications: Option, +} + +async fn user_update_telegram( + request: Request, + session: Session, + db: Database, + Path(user_id): Path, +) -> cot::Result { + let (lang, form): (_, UserTelegramForm) = parse_form_from_request(request).await?; + if let Err(resp) = require_auth(&session, lang).await { + return Ok(resp); + } + if let Some(mut user) = query!(User, $id == user_id).get(&db).await? { + user.telegram_chat_id = form.telegram_chat_id.filter(|s| !s.trim().is_empty()); + user.telegram_notifications = Some(form.telegram_notifications.as_deref() == Some("true")); + user.updated_at = now_utc(); + user.save(&db).await?; + } + Redirect::new(format!("/admin/users?lang={}", lang.code())).into_response() +} + #[derive(Deserialize)] struct AddLeadForm { name: String, @@ -2077,6 +2104,11 @@ pub fn admin_router() -> Router { user_activate, "admin-user-activate", ), + Route::with_handler_and_name( + "/users/{user_id}/telegram", + user_update_telegram, + "admin-user-telegram", + ), Route::with_handler_and_name("/media", media_page, "admin-media"), Route::with_handler_and_name( "/media/{visit_id}/upload", diff --git a/src/i18n.rs b/src/i18n.rs index 319afe2..ed8b7b5 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -118,6 +118,8 @@ pub struct Translations { pub users_add_button: &'static str, pub users_error_passwords_mismatch: &'static str, pub users_error_login_taken: &'static str, + pub users_telegram_chat_id: &'static str, + pub users_telegram_enabled: &'static str, // Settings pub settings_title: &'static str, @@ -329,6 +331,8 @@ static RU: Translations = Translations { users_add_button: "Добавить", users_error_passwords_mismatch: "Пароли не совпадают.", users_error_login_taken: "Этот логин уже занят.", + users_telegram_chat_id: "Telegram Chat ID", + users_telegram_enabled: "Уведомления", settings_title: "Настройки", settings_key: "Параметр", @@ -529,6 +533,8 @@ static EN: Translations = Translations { users_add_button: "Add", users_error_passwords_mismatch: "Passwords do not match.", users_error_login_taken: "This login is already taken.", + users_telegram_chat_id: "Telegram Chat ID", + users_telegram_enabled: "Notifications", settings_title: "Settings", settings_key: "Parameter", diff --git a/src/migrations.rs b/src/migrations.rs index 2cc87bc..e62bf6a 100644 --- a/src/migrations.rs +++ b/src/migrations.rs @@ -7,6 +7,7 @@ pub mod m_0002_visit_schedule; pub mod m_0003_visit_feedback; pub mod m_0004_visit_public_notes; pub mod m_0005_testimonials; +pub mod m_0006_user_telegram; /// The list of migrations for current app. pub const MIGRATIONS: &[&::cot::db::migrations::SyncDynMigration] = &[ &m_0001_initial::Migration, @@ -14,4 +15,5 @@ pub const MIGRATIONS: &[&::cot::db::migrations::SyncDynMigration] = &[ &m_0003_visit_feedback::Migration, &m_0004_visit_public_notes::Migration, &m_0005_testimonials::Migration, + &m_0006_user_telegram::Migration, ]; diff --git a/src/migrations/m_0006_user_telegram.rs b/src/migrations/m_0006_user_telegram.rs new file mode 100644 index 0000000..53951c0 --- /dev/null +++ b/src/migrations/m_0006_user_telegram.rs @@ -0,0 +1,35 @@ +//! Migration: add telegram_chat_id and telegram_notifications to User + +#[derive(Debug, Copy, Clone)] +pub(super) struct Migration; +impl ::cot::db::migrations::Migration for Migration { + const APP_NAME: &'static str = "web-petting"; + const MIGRATION_NAME: &'static str = "m_0006_user_telegram"; + const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] = + &[::cot::db::migrations::MigrationDependency::migration( + "web-petting", + "m_0005_testimonials", + )]; + const OPERATIONS: &'static [::cot::db::migrations::Operation] = &[ + ::cot::db::migrations::Operation::add_field() + .table_name(::cot::db::Identifier::new("web_petting__user")) + .field( + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("telegram_chat_id"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ) + .build(), + ::cot::db::migrations::Operation::add_field() + .table_name(::cot::db::Identifier::new("web_petting__user")) + .field( + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("telegram_notifications"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( as ::cot::db::DatabaseField>::NULLABLE), + ) + .build(), + ]; +} diff --git a/src/models.rs b/src/models.rs index a077fef..a510919 100644 --- a/src/models.rs +++ b/src/models.rs @@ -179,6 +179,8 @@ pub struct User { pub login: String, pub password_hash: String, pub display_name: Option, + pub telegram_chat_id: Option, + pub telegram_notifications: Option, /// active | archived pub status: String, pub created_at: chrono::NaiveDateTime, diff --git a/src/telegram.rs b/src/telegram.rs index c90fc33..14a0285 100644 --- a/src/telegram.rs +++ b/src/telegram.rs @@ -1,8 +1,8 @@ use cot::db::{Database, query}; -use crate::models::Setting; +use crate::models::{Setting, User}; -/// Send a Telegram message using bot settings from DB. +/// Send a Telegram notification to all admins with notifications enabled. /// Silently ignores errors (missing config, network issues) — notifications are best-effort. pub async fn notify_new_lead( db: &Database, @@ -14,10 +14,6 @@ pub async fn notify_new_lead( Some(t) if !t.is_empty() => t, _ => return, }; - let chat_id = match get_setting(db, "telegram_chat_id").await { - Some(c) if !c.is_empty() => c, - _ => return, - }; let mut text = format!("📋 Новая заявка!\n\nИмя: {name}"); if let Some(phone) = phone.filter(|s| !s.is_empty()) { @@ -27,15 +23,33 @@ pub async fn notify_new_lead( text.push_str(&format!("\nКомментарий: {comment}")); } + let active = "active".to_string(); + let users = match query!(User, $status == active).all(db).await { + Ok(u) => u, + Err(_) => return, + }; + + let client = reqwest::Client::new(); let url = format!("https://api.telegram.org/bot{token}/sendMessage"); - let _ = reqwest::Client::new() - .post(&url) - .json(&serde_json::json!({ - "chat_id": chat_id, - "text": text, - })) - .send() - .await; + + for user in &users { + if user.telegram_notifications != Some(true) { + continue; + } + let chat_id = match &user.telegram_chat_id { + Some(id) if !id.is_empty() => id, + _ => continue, + }; + + let _ = client + .post(&url) + .json(&serde_json::json!({ + "chat_id": chat_id, + "text": text, + })) + .send() + .await; + } } async fn get_setting(db: &Database, key_name: &str) -> Option { diff --git a/templates/admin/settings.html b/templates/admin/settings.html index 11ce647..f2bf9c5 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -20,12 +20,6 @@ -
- -
- -
-
diff --git a/templates/admin/users.html b/templates/admin/users.html index c815f6f..1ec861c 100644 --- a/templates/admin/users.html +++ b/templates/admin/users.html @@ -17,6 +17,29 @@
🕐 {{ user.created_at.format("%d.%m.%Y %H:%M") }}
+ {% if user.status == "active" %} +
+
+
+
+ +
+ +
+
+
+
+ +
+
+ +
+
+
+ {% endif %}
{% if user.status == "active" %}
diff --git a/templates/landing.html b/templates/landing.html index f9e5493..392b46a 100644 --- a/templates/landing.html +++ b/templates/landing.html @@ -304,7 +304,7 @@ /* ── Mobile ── */ @media (max-width: 600px) { - .hero { padding: 6.5rem 1rem 3rem; } + .hero { padding: 6.5rem 1rem 10rem; } .section { padding: 3rem 1rem; } .form-section { padding: 3rem 1rem; } .form-wrapper { padding: 1.75rem 1.25rem; }