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)) { $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', 'groceries', '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."; } } 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 = "Language updated."; } } if ($action === 'update_profile') { $display_name = trim($_POST['display_name'] ?? ''); 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]); $_SESSION['user']['display_name'] = $display_name; $success = "Profil mis à jour."; } } 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; // Supprimer anciens fichiers de ce family 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'); } catch (\Throwable $e) { $error = tr('groceries_settings_error') . ' ' . $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 (tu avais mis la racine iCloud)."; } $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érifie identifiant Apple, mot de passe d’app (16 caractères) et que le compte iCloud a le Calendrier activé."; } } 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."; } } } // ─── Chargement données ─────────────────────────────────────────────────────── $user = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?"); $user->execute([$user_id]); $user = $user->fetch(); $family = null; $members = []; 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(); } $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) { require_once __DIR__ . '/includes/db.php'; if (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) { // ignore } } } $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')], 'groceries' => ['icon' => '🛒', 'label' => tr('menu_groceries')], 'calendar_ios' => ['icon' => '📱', 'label' => tr('menu_calendar_ios')], 'printvault' => ['icon' => '🖨️', 'label' => tr('menu_printvault')], 'planka' => ['icon' => '📋', 'label' => tr('menu_planka')], ]; ?>= htmlspecialchars(tr('groceries_settings_intro')) ?>
Configurez ici votre calendrier iCloud pour synchroniser les événements créés dans HouseHub.
Pour voir et gérer les événements : ouvrez le module Calendrier iOS (menu du haut ou burger « Calendrier iOS », ou carte sur l’accueil). Le module doit être coché dans « Modules actifs » ci-dessus.
Après enregistrement, utilisez « Tester la connexion » : un message vert confirme que l’URL CalDAV répond avec tes identifiants. Sur la page Calendrier iOS, le bouton « Synchroniser » envoie les événements vers iCloud et récupère ceux créés sur l’iPhone.
Dernière synchro: = htmlspecialchars($calendarIntegration['last_sync_at']) ?>
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