exec(" CREATE TABLE IF NOT EXISTS user_calendar_integrations ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, provider VARCHAR(50) NOT NULL DEFAULT 'icloud_caldav', username VARCHAR(255) NOT NULL, secret_encrypted TEXT NOT NULL, dav_principal_url VARCHAR(1024) DEFAULT NULL, calendar_url VARCHAR(1024) DEFAULT NULL, status VARCHAR(30) DEFAULT 'connected', last_sync_at DATETIME DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_user_provider (user_id, provider) )"); // ─── Actions ────────────────────────────────────────────────────────────────── if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!verify_csrf($_POST['csrf_token'] ?? null)) { if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') { header('Content-Type: application/json'); echo json_encode(['success' => false, 'error' => 'Session invalide (CSRF). Rechargez la page.']); exit; } $error = "Session invalide (CSRF). Rechargez la page."; } else { $action = $_POST['action'] ?? ''; if ($action === 'set_modules' && $family_id) { $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka']; $enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m]))); if (empty($enabled)) { $error = "Vous devez garder au moins un module actif."; } else { $meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?") ->execute([json_encode($enabled), $family_id]); $_SESSION['enabled_modules'] = $enabled; $success = "Modules mis à jour avec succès."; } } if ($action === 'set_lang') { $lang = $_POST['lang'] ?? 'fr'; if (in_array($lang, ['fr', 'ca', 'en'])) { $_SESSION['app_lang'] = $lang; $meta_pdo->prepare("UPDATE users SET lang = ? WHERE id = ?") ->execute([$lang, $user_id]); $success = "Langue mise à jour."; } } if ($action === 'update_profile') { $display_name = trim($_POST['display_name'] ?? ''); $color = trim($_POST['color'] ?? '#0891b2'); if (!$display_name) { $error = "Le prénom ne peut pas être vide."; } else { $meta_pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?") ->execute([$display_name, $user_id]); require_once __DIR__ . '/includes/db.php'; if (isset($pdo)) { $stmtPeopleColor = $pdo->prepare("UPDATE pf_people SET color = ? WHERE user_id = ?"); $stmtPeopleColor->execute([$color, $user_id]); } $_SESSION['user']['display_name'] = $display_name; $success = "Profil mis à jour avec succès."; } } if ($action === 'change_password') { $current = $_POST['current_password'] ?? ''; $new = $_POST['new_password'] ?? ''; $confirm = $_POST['confirm_password'] ?? ''; $row = $meta_pdo->prepare("SELECT password_hash FROM users WHERE id = ?"); $row->execute([$user_id]); $row = $row->fetch(); if (!password_verify($current, $row['password_hash'])) { $error = "Mot de passe actuel incorrect."; } elseif (strlen($new) < 6) { $error = "Le nouveau mot de passe doit faire au moins 6 caractères."; } elseif ($new !== $confirm) { $error = "Les mots de passe ne correspondent pas."; } else { $meta_pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?") ->execute([password_hash($new, PASSWORD_BCRYPT), $user_id]); $success = "Mot de passe modifié avec succès."; } } if ($action === 'update_family_name' && $family_id) { $name = trim($_POST['family_name'] ?? ''); if ($name) { $meta_pdo->prepare("UPDATE families SET name = ? WHERE id = ?") ->execute([$name, $family_id]); $success = "Nom de l'espace mis à jour."; } } if ($action === 'regen_invite' && $family_id) { $new_code = bin2hex(random_bytes(8)); $meta_pdo->prepare("UPDATE families SET invite_code = ? WHERE id = ?") ->execute([$new_code, $family_id]); $success = "Nouveau code d'invitation généré."; } if ($action === 'upload_home_bg' && $family_id) { $upload_dir = '/uploads/'; if (!is_dir($upload_dir)) @mkdir($upload_dir, 0755, true); $file = $_FILES['home_bg'] ?? null; if ($file && $file['error'] === 0) { $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($ext, ['jpg','jpeg','png','webp'])) { $error = "Format non supporté (jpg, png, webp)."; } else { $dest = $upload_dir . 'home_bg_' . $family_id . '.' . $ext; foreach (glob($upload_dir . 'home_bg_' . $family_id . '.*') as $old) @unlink($old); if (move_uploaded_file($file['tmp_name'], $dest)) { $success = "Image d'accueil mise à jour."; } else { $error = "Erreur lors de l'upload."; } } } } if ($action === 'reset_home_bg' && $family_id) { foreach (glob('/uploads/home_bg_' . $family_id . '.*') as $old) @unlink($old); $success = "Image d'accueil réinitialisée."; } if ($action === 'grocery_history_max' && $family_id) { require_once __DIR__ . '/includes/db.php'; if (!isset($pdo)) { $error = "Base famille indisponible."; } else { $n = (int) ($_POST['history_max'] ?? 20); $n = max(1, min(50, $n)); try { $pdo->prepare( "INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('grocery_settings','history_max',?) ON DUPLICATE KEY UPDATE content=VALUES(content)" )->execute([(string) $n]); $success = tr('groceries_settings_saved') ?: 'Paramètres des courses enregistrés.'; } catch (\Throwable $e) { $error = (tr('groceries_settings_error') ?: 'Erreur') . ' : ' . $e->getMessage(); } } } if ($action === 'calendar_ios_save') { $username = trim($_POST['icloud_username'] ?? ''); $appPassword = hh_normalize_apple_app_password((string) ($_POST['icloud_app_password'] ?? '')); $calendarUrl = trim($_POST['icloud_calendar_url'] ?? ''); if (!$username || !$appPassword || !$calendarUrl) { $error = "Merci de renseigner identifiant iCloud, mot de passe d'app et URL CalDAV."; } else { try { $resolvedUrl = hh_icloud_resolve_calendar_url_if_needed($username, $appPassword, $calendarUrl); $encrypted = hh_encrypt_secret($appPassword); $meta_pdo->prepare(" INSERT INTO user_calendar_integrations (user_id, provider, username, secret_encrypted, calendar_url, status, updated_at) VALUES (?, 'icloud_caldav', ?, ?, ?, 'connected', NOW()) ON DUPLICATE KEY UPDATE username=VALUES(username), secret_encrypted=VALUES(secret_encrypted), calendar_url=VALUES(calendar_url), status='connected', updated_at=NOW() ")->execute([$user_id, $username, $encrypted, $resolvedUrl]); $msg = "Connexion calendrier iOS enregistrée."; if (rtrim($resolvedUrl, '/') !== rtrim($calendarUrl, '/')) { $msg .= " URL du calendrier détectée automatiquement."; } $success = $msg; } catch (\Throwable $e) { $error = "Impossible d'enregistrer la connexion iOS: " . $e->getMessage(); } } } if ($action === 'calendar_ios_test') { $row = $meta_pdo->prepare("SELECT id, username, secret_encrypted, calendar_url FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'"); $row->execute([$user_id]); $integration = $row->fetch(); if (!$integration) { $error = "Aucune connexion iOS configurée."; } else { try { $pwd = hh_normalize_apple_app_password(hh_decrypt_secret($integration['secret_encrypted'])); $calendarUrl = trim($integration['calendar_url']); $resolvedUrl = hh_icloud_resolve_calendar_url_if_needed($integration['username'], $pwd, $calendarUrl); $code = hh_caldav_test_calendar_collection($integration['username'], $pwd, $resolvedUrl); if (in_array($code, [200, 207], true)) { if (rtrim($resolvedUrl, '/') !== rtrim($calendarUrl, '/')) { $meta_pdo->prepare("UPDATE user_calendar_integrations SET calendar_url = ? WHERE id = ?") ->execute([$resolvedUrl, $integration['id']]); } $success = "Connexion iCloud CalDAV valide (HTTP $code)."; } else { $error = "Test connexion échoué (HTTP $code). Vérifiez vos identifiants."; } } catch (\Throwable $e) { $error = "Test connexion impossible: " . $e->getMessage(); } } } if ($action === 'calendar_ios_disconnect') { $meta_pdo->prepare("DELETE FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'")->execute([$user_id]); $success = "Connexion iOS supprimée."; } // --- GESTION DES PROFILS RATTACHÉS (Enfants, Nounous, etc.) --- if ($action === 'add_attached_profile') { $name = trim($_POST['profile_name'] ?? ''); $role = trim($_POST['profile_role'] ?? 'child'); if ($name) { require_once __DIR__ . '/includes/db.php'; if (isset($pdo)) { $stmt = $pdo->prepare("INSERT INTO pf_people (name, role) VALUES (?, ?)"); $stmt->execute([$name, $role]); $success = "Profil de {$name} ajouté avec succès."; } } else { $error = "Le prénom est obligatoire."; } } if ($action === 'delete_attached_profile') { $profile_id = (int)($_POST['profile_id'] ?? 0); if ($profile_id > 0) { require_once __DIR__ . '/includes/db.php'; if (isset($pdo)) { // On protège les vrais utilisateurs en s'assurant que user_id est NULL $stmt = $pdo->prepare("DELETE FROM pf_people WHERE id = ? AND user_id IS NULL"); $stmt->execute([$profile_id]); $success = "Profil supprimé."; } } } } } // ─── Chargement données ─────────────────────────────────────────────────────── if ($family_id) { require_once __DIR__ . '/includes/db.php'; } $stmtUser = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?"); $stmtUser->execute([$user_id]); $user = $stmtUser->fetch(); $user_color = '#0891b2'; if ($family_id && isset($pdo)) { $stmtColorFetch = $pdo->prepare("SELECT color FROM pf_people WHERE user_id = ?"); $stmtColorFetch->execute([$user_id]); $fetched_color = $stmtColorFetch->fetchColumn(); if ($fetched_color) { $user_color = $fetched_color; } } $family = null; $members = []; $attached_profiles = []; if ($family_id) { $fam = $meta_pdo->prepare("SELECT * FROM families WHERE id = ?"); $fam->execute([$family_id]); $family = $fam->fetch(); $mem = $meta_pdo->prepare("SELECT display_name, username, created_at, is_admin FROM users WHERE family_id = ? ORDER BY id"); $mem->execute([$family_id]); $members = $mem->fetchAll(); // Récupération des profils rattachés (ceux qui n'ont pas de compte de connexion, donc user_id IS NULL) if (isset($pdo)) { $stmtAttached = $pdo->query("SELECT id, name, role FROM pf_people WHERE user_id IS NULL ORDER BY role ASC, name ASC"); $attached_profiles = $stmtAttached->fetchAll(PDO::FETCH_ASSOC); } } $calendarIntegration = $meta_pdo->prepare("SELECT username, calendar_url, status, last_sync_at FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'"); $calendarIntegration->execute([$user_id]); $calendarIntegration = $calendarIntegration->fetch(); $groceryHistoryMaxSetting = 20; if ($family_id && isset($pdo)) { try { $gv = $pdo->query("SELECT content FROM pf_notes WHERE note_type='grocery_settings' AND reference_id='history_max'")->fetchColumn(); if ($gv !== false && $gv !== null && $gv !== '') { $groceryHistoryMaxSetting = max(1, min(50, (int) $gv)); } } catch (\Throwable $e) {} } $pageTitle = "Paramètres — HouseHub"; $activePage = "settings"; require __DIR__ . '/header.php'; ?>
Choisissez les modules visibles dans la navigation (partagé avec tous les membres de l'espace).
['icon' => '📅', 'label' => tr('menu_calendar')], 'budget' => ['icon' => '💰', 'label' => tr('menu_budget')], 'holidays' => ['icon' => '🏖️', 'label' => tr('menu_holidays')], 'gifts' => ['icon' => '🎁', 'label' => tr('menu_gifts')], 'garage' => ['icon' => '🚗', 'label' => tr('menu_garage')], 'memo' => ['icon' => '📝', 'label' => tr('menu_memo')], 'todo' => ['icon' => '✅', 'label' => tr('menu_todo')], 'liste' => ['icon' => '📝', 'label' => tr('menu_liste')], 'calendar_ios' => ['icon' => '📱', 'label' => tr('menu_calendar_ios')], 'printvault' => ['icon' => '🖨️', 'label' => tr('menu_printvault')], 'planka' => ['icon' => '📋', 'label' => tr('menu_planka')], ]; ?>= htmlspecialchars(tr('liste_settings_intro')) ?>
Configurez ici votre calendrier iCloud pour synchroniser les événements créés dans HouseHub.
Photo de fond affichée sur la page d'accueil (partagée avec tous les membres).
Code d'invitation — partagez-le pour inviter quelqu'un
= htmlspecialchars($family['invite_code']) ?>
✓ Copié !
Lien d'inscription : = $_SERVER['HTTP_HOST'] ?>/register.php
Membres gérés par le foyer (Enfants, Nounous, Proches) n'ayant pas d'accès direct.