diff --git a/api/manage-event.php b/api/manage-event.php deleted file mode 100644 index 47db538..0000000 --- a/api/manage-event.php +++ /dev/null @@ -1,36 +0,0 @@ - 'error', 'message' => 'Action ou ID manquant.']); - exit; -} - -$action = $input['action']; -$eventId = $input['event_id']; - -try { - if ($action === 'delete') { - $stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?"); - $stmt->execute([$eventId]); - echo json_encode(['status' => 'success', 'message' => 'Événement supprimé.']); - - } elseif ($action === 'update' && isset($input['new_type'])) { - $stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?"); - $stmt->execute([$input['new_type'], $eventId]); - echo json_encode(['status' => 'success', 'message' => 'Événement mis à jour.']); - - } else { - http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'Action non valide ou données manquantes.']); - } - -} catch (PDOException $e) { - http_response_code(500); - echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); -} -?> diff --git a/family-calendar.php b/family-calendar.php index 940586f..292607b 100644 --- a/family-calendar.php +++ b/family-calendar.php @@ -3,17 +3,22 @@ ini_set('display_errors', 1); error_reporting(E_ALL); -// On se connecte à la base de données +require __DIR__ . '/includes/auth.php'; +require_login('/family-calendar.php'); + require __DIR__ . '/includes/db.php'; // --- On récupère TOUS les événements sauvegardés en base --- $stmt_events = $pdo->query("SELECT * FROM pf_events"); $dbEvents = $stmt_events->fetchAll(); -// --- Configuration de la page --- -$pageTitle = "PachaFamily - Family Calendar"; +$pageTitle = "PachaFamily - Family Calendar"; $activePage = "family-calendar"; +$bodyClass = "pf-family-calendar"; +$pageCss = "/modules/family-calendar/family-calendar.css"; + require __DIR__ . '/header.php'; + ?> @@ -26,7 +31,6 @@ require __DIR__ . '/header.php';

Family Calendar

- @@ -38,24 +42,33 @@ require __DIR__ . '/header.php';

Légende

-
+
Vacances scolaires
-
+
Jour férié
-
+
Off Carole
-
+
Extra Off Carole
+
+
+ Centre +
+
+
+ Avis +
+

Récapitulatif annuel

@@ -65,31 +78,30 @@ require __DIR__ . '/header.php';
-
-

Vacances scolaires - Zone C (2025-2026)

-
-
- - - - - - - - - - - - -
PériodeDuAuZones
+
+

Vacances scolaires - Zone C (2025-2026)

+
+
+ + + + + + + + + + + + +
PériodeDuAuZones
+
+
+
-
- - - @@ -110,15 +122,11 @@ require __DIR__ . '/header.php';
- -
-
- -
+ @@ -153,12 +161,10 @@ require __DIR__ . '/header.php'; - - - + © PachaFamily + + diff --git a/global.css b/global.css new file mode 100644 index 0000000..92c2b90 --- /dev/null +++ b/global.css @@ -0,0 +1,209 @@ +/* === RESET SIMPLE === */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +/* === BASE HTML / BODY === */ +html, +body { + height: 100%; +} + +body { + margin: 0; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; + background: #f5f5f5; + color: #222; +} + +/* === LAYOUT GLOBAL (structure page + footer collé en bas) === */ + +.pf-page { + min-height: 100vh; /* au minimum la hauteur de la fenêtre */ + display: flex; + flex-direction: column; +} + +.pf-main { + flex: 1 0 auto; +} + +.pf-footer { + flex-shrink: 0; +} + +/* === LAYOUT CONTAINER / HEADER / NAV / FOOTER === */ + +.pf-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 16px; +} + +.pf-header { + background: #243b53; + color: #fff; + padding: 8px 0; +} + +.pf-header-content { + display: flex; + align-items: center; + justify-content: space-between; +} + +.pf-logo { + font-weight: 600; + font-size: 18px; +} + +.pf-nav { + display: flex; + gap: 16px; +} + +.pf-nav-link { + color: #d9e2ec; + text-decoration: none; + font-size: 14px; + padding: 4px 8px; + border-radius: 4px; +} + +.pf-nav-link:hover { + background: rgba(255, 255, 255, 0.1); +} + +.pf-nav-link--active { + background: #102a43; + color: #f0f4f8; +} + +.pf-main { + padding: 24px 10px 48px; +} + +.pf-section { + margin-top: 32px; +} + +.pf-section--panel { + background: #fff; + border-radius: 8px; + padding: 16px 16px 20px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); +} + +.pf-footer { + border-top: 1px solid #d9e2ec; + padding: 8px 0; + font-size: 12px; + color: #627d98; + background: #f0f4f8; +} + +/* Sur certaines pages (comme la home), on pourra override le header via une classe de body */ + +/* === FLEX & CARDS GÉNÉRIQUES === */ + +.pf-flex { + display: flex; +} + +.pf-flex--wrap { + flex-wrap: wrap; +} + +.pf-gap-lg { + gap: 24px; +} + +.pf-card { + background: #f8fafc; + border-radius: 8px; + border: 1px solid #d9e2ec; + padding: 12px 14px; +} + +.pf-card--small { + min-width: 240px; + max-width: 260px; +} + +/* Large card (utilisée pour le tableau des vacances) */ +.pf-card.pf-card--wide { + min-width: 400px; + flex: 1 1 400px; +} + +.pf-card-title { + margin: 0 0 8px; + font-size: 15px; +} + +.pf-card-body { + font-size: 13px; +} + +/* === TABLES GÉNÉRIQUES === */ + +.pf-table-wrapper { + max-height: 600px; + overflow: auto; + border-radius: 12px; + background: #ffffff; + border: 1px solid #d9e2ec; + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08); +} + +.pf-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} + +.pf-table th, +.pf-table td { + border: 1px solid #d9e2ec; + padding: 4px 6px; + text-align: center; + position: relative; +} + +.pf-table thead th { + position: sticky; + top: 0; + background: #f0f4f8; + z-index: 1; +} + +.pf-table--compact th, +.pf-table--compact td { + padding: 3px 4px; +} + +/* === FORMULAIRES / TEXTE GÉNÉRIQUE === */ + +label { + font-size: 12px; +} + +input[type="number"] { + width: 3.1rem; + font-size: 11px; + padding: 2px 3px; +} + +/* Récapitulatif global (utilisé dans le calendrier mais style simple) */ +#globalSummary p { + margin: 4px 0; + font-size: 12px; +} + +#globalSummary strong { + font-weight: 600; + color: #243b53; +} diff --git a/header.php b/header.php index 787ff35..37f1757 100644 --- a/header.php +++ b/header.php @@ -1,4 +1,9 @@ - + - <?php echo htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8'); ?> - - - -
-
- - -
-
+ <?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8'); ?> -
+ + + + + + + + + +
+ +
+
+ + + +
+
+ +
diff --git a/includes/auth.php b/includes/auth.php new file mode 100644 index 0000000..3085073 --- /dev/null +++ b/includes/auth.php @@ -0,0 +1,19 @@ + - -

Bienvenue sur PachaFamily

-

Centre de controle de l'organisation familiale.

- -
-

Modules

- -
- - + + + +
+

Bienvenue sur PachaFamily

+

Centre de contrôle de l'organisation familiale.

+ + + + + +
+

Modules

+ +
+ + +
📅
+

Family calendar

+
+ Gérez les congés, vacances et la garde sur un calendrier partagé + pour toute la famille. +
+ Accéder au module +
+ + +
+
💰
+

Budget familial

+
+ Bientôt disponible : suivi des dépenses, catégories et budget mensuel. +
+ À venir +
+ + +
+
📦
+

Liste de courses

+
+ Centralisez les listes de courses et les partages entre membres. +
+ À venir +
+
+
+
+ +prepare(" + SELECT id, username, password_hash, display_name + FROM pf_users + WHERE username = :username + "); + $stmt->execute([':username' => $username]); + $user = $stmt->fetch(PDO::FETCH_ASSOC); + + // Vérification du mot de passe + if ($user && password_verify($password, $user['password_hash'])) { + // Connexion OK : on stocke les infos utiles en session + $_SESSION['user'] = [ + 'id' => (int)$user['id'], + 'username' => $user['username'], + 'display_name' => $user['display_name'], + ]; + + // Redirection : soit la page demandée, soit l'accueil + $redirectTo = $_GET['redirect'] ?? '/index.php'; + header('Location: ' . $redirectTo); + exit; + } else { + $error = "Identifiants incorrects."; + } + } +} + +require __DIR__ . '/header.php'; +?> + +

Connexion

+ + +
+ + +
+
+ + +
+ +
+ + +
+ + +
+ +query(" + SELECT + l.id, + l.person_id, + p.name AS person_name, + l.leave_type, + l.leave_date, + l.duration + FROM pf_leaves l + JOIN pf_people p ON p.id = l.person_id + ORDER BY l.leave_date, l.person_id + "); + + $leaves = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode([ + 'status' => 'success', + 'leaves' => $leaves, + ]); + +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); +} diff --git a/modules/family-calendar/assets/includes/api/manage-event.php b/modules/family-calendar/assets/includes/api/manage-event.php new file mode 100644 index 0000000..007bfea --- /dev/null +++ b/modules/family-calendar/assets/includes/api/manage-event.php @@ -0,0 +1,103 @@ + 'error', 'message' => 'Action manquante.']); + exit; +} + +$action = $input['action']; + +try { + if ($action === 'delete') { + // Suppression d'un seul événement + if (!isset($input['event_id'])) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'ID manquant pour la suppression.']); + exit; + } + + $eventId = (int)$input['event_id']; + $stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?"); + $stmt->execute([$eventId]); + + echo json_encode(['status' => 'success', 'message' => 'Événement supprimé.']); + + } elseif ($action === 'update') { + // Mise à jour d'un seul événement + if (!isset($input['event_id'], $input['new_type'])) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'ID ou nouveau type manquant pour la mise à jour.']); + exit; + } + + $eventId = (int)$input['event_id']; + $newType = $input['new_type']; + + $stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?"); + $stmt->execute([$newType, $eventId]); + + echo json_encode(['status' => 'success', 'message' => 'Événement mis à jour.']); + + } elseif ($action === 'bulk_delete') { + // Suppression en masse + if (empty($input['event_ids']) || !is_array($input['event_ids'])) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Liste event_ids manquante pour bulk_delete.']); + exit; + } + + $eventIds = array_map('intval', $input['event_ids']); + $eventIds = array_filter($eventIds, fn($id) => $id > 0); + + if (empty($eventIds)) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Aucun ID valide pour bulk_delete.']); + exit; + } + + $placeholders = implode(',', array_fill(0, count($eventIds), '?')); + $stmt = $pdo->prepare("DELETE FROM pf_events WHERE id IN ($placeholders)"); + $stmt->execute($eventIds); + + echo json_encode(['status' => 'success', 'message' => 'Événements supprimés en masse.']); + + } elseif ($action === 'bulk_update') { + // Mise à jour en masse + if (empty($input['event_ids']) || !is_array($input['event_ids']) || !isset($input['new_type'])) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'event_ids ou new_type manquant pour bulk_update.']); + exit; + } + + $eventIds = array_map('intval', $input['event_ids']); + $eventIds = array_filter($eventIds, fn($id) => $id > 0); + $newType = $input['new_type']; + + if (empty($eventIds)) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Aucun ID valide pour bulk_update.']); + exit; + } + + $placeholders = implode(',', array_fill(0, count($eventIds), '?')); + $params = array_merge([$newType], $eventIds); + + $stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id IN ($placeholders)"); + $stmt->execute($params); + + echo json_encode(['status' => 'success', 'message' => 'Événements mis à jour en masse.']); + + } else { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Action non valide.']); + } + +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); +} diff --git a/modules/family-calendar/assets/includes/api/manage-leaf.php b/modules/family-calendar/assets/includes/api/manage-leaf.php new file mode 100644 index 0000000..5e6f8b6 --- /dev/null +++ b/modules/family-calendar/assets/includes/api/manage-leaf.php @@ -0,0 +1,45 @@ + 'error', 'message' => 'Action manquante.']); + exit; +} + +$action = $input['action']; + +try { + if ($action === 'delete_day') { + if (empty($input['date'])) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Date manquante pour delete_day.']); + exit; + } + + $dateObj = date_create($input['date']); + if (!$dateObj) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Date invalide.']); + exit; + } + $leaveDate = $dateObj->format('Y-m-d'); + + $stmt = $pdo->prepare("DELETE FROM pf_leaves WHERE leave_date = ?"); + $stmt->execute([$leaveDate]); + + echo json_encode(['status' => 'success', 'message' => 'Congés supprimés pour ce jour.']); + + } else { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Action non valide.']); + } + +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); +} diff --git a/api/save-events.php b/modules/family-calendar/assets/includes/api/save-events.php similarity index 100% rename from api/save-events.php rename to modules/family-calendar/assets/includes/api/save-events.php diff --git a/modules/family-calendar/assets/includes/api/save-leaves.php b/modules/family-calendar/assets/includes/api/save-leaves.php new file mode 100644 index 0000000..f48159e --- /dev/null +++ b/modules/family-calendar/assets/includes/api/save-leaves.php @@ -0,0 +1,68 @@ + 'error', 'message' => 'Données invalides (attendu tableau JSON).']); + exit; +} + +// Exemple d'élément attendu : +// { date: '2025-09-01', person_id: 2, leave_type: 'CP', duration: 1.0 } + +$validLeaveTypes = ['CP', 'JRA', 'JA']; + +try { + $pdo->beginTransaction(); + + $stmtInsert = $pdo->prepare(" + INSERT INTO pf_leaves (person_id, leave_type, leave_date, duration) + VALUES (:person_id, :leave_type, :leave_date, :duration) + "); + + foreach ($input as $item) { + $date = $item['date'] ?? null; + $personId = $item['person_id'] ?? null; + $leaveType = $item['leave_type'] ?? null; + $duration = isset($item['duration']) ? (float)$item['duration'] : 1.0; + + if (!$date || !$personId || !$leaveType) { + continue; // on ignore les lignes incomplètes + } + + if (!in_array($leaveType, $validLeaveTypes, true)) { + continue; // leave_type invalide + } + + // Normalisation de la date + $dateObj = date_create($date); + if (!$dateObj) { + continue; + } + $leaveDate = $dateObj->format('Y-m-d'); + + $stmtInsert->execute([ + ':person_id' => (int)$personId, + ':leave_type' => $leaveType, + ':leave_date' => $leaveDate, + ':duration' => $duration, + ]); + } + + $pdo->commit(); + + echo json_encode([ + 'status' => 'success', + 'message' => 'Congés enregistrés.', + ]); + +} catch (PDOException $e) { + $pdo->rollBack(); + http_response_code(500); + echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); +} diff --git a/assets/css/style.css b/modules/family-calendar/family-calendar.css similarity index 67% rename from assets/css/style.css rename to modules/family-calendar/family-calendar.css index ce20120..32aa315 100644 --- a/assets/css/style.css +++ b/modules/family-calendar/family-calendar.css @@ -1,94 +1,6 @@ -/* === RESET SIMPLE === */ -*, -*::before, -*::after { - box-sizing: border-box; -} +/* === TABLES SPÉCIFIQUES AU FAMILY CALENDAR === */ -body { - margin: 0; - font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", - sans-serif; - background: #f5f5f5; - color: #222; -} - -/* === LAYOUT GLOBAL === */ -.pf-container { - max-width: 1200px; - margin: 0 auto; - padding: 0 16px; -} - -.pf-header { - background: #243b53; - color: #fff; - padding: 8px 0; -} - -.pf-header-content { - display: flex; - align-items: center; - justify-content: space-between; -} - -.pf-logo { - font-weight: 600; - font-size: 18px; -} - -.pf-nav { - display: flex; - gap: 16px; -} - -.pf-nav-link { - color: #d9e2ec; - text-decoration: none; - font-size: 14px; - padding: 4px 8px; - border-radius: 4px; -} - -.pf-nav-link:hover { - background: rgba(255, 255, 255, 0.1); -} - -.pf-nav-link--active { - background: #102a43; - color: #f0f4f8; -} - -.pf-main { - padding: 24px 10px 48px; -} - -.pf-section { - margin-top: 24px; -} - -.pf-section--panel { - background: #fff; - border-radius: 8px; - padding: 16px 16px 20px; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); -} - -.pf-footer { - border-top: 1px solid #d9e2ec; - padding: 8px 0; - font-size: 12px; - color: #627d98; - background: #f0f4f8; -} - -/* === CARDS & FLEX === */ - -.pf-card.pf-card--wide { - min-width: 400px; /* tu peux monter à 500 ou 600 selon ton goût */ - flex: 1 1 400px; /* laisse la carte prendre plus de place dans la ligne */ -} -/* Le tableau des vacances occupe toute la largeur disponible */ +/* Tableau des vacances scolaires */ #schoolHolidaysTable { width: auto; table-layout: auto; @@ -99,81 +11,39 @@ body { width: 100%; overflow-x: auto; } + /* En-têtes et cellules sans retour à la ligne */ #schoolHolidaysTable th, #schoolHolidaysTable td { - white-space: nowrap; /* pas de retour à la ligne dans les cellules */ - overflow: visible; /* pas de tronquage */ + white-space: nowrap; + overflow: visible; padding: 4px 8px; text-align: center; } -.pf-flex { - display: flex; -} - -.pf-flex--wrap { - flex-wrap: wrap; -} - -.pf-gap-lg { - gap: 24px; -} - -.pf-card { - background: #f8fafc; - border-radius: 8px; - border: 1px solid #d9e2ec; - padding: 12px 14px; -} - -.pf-card--small { - min-width: 240px; - max-width: 260px; -} - -.pf-card-title { - margin: 0 0 8px; - font-size: 15px; -} - -.pf-card-body { - font-size: 13px; -} - -label { - font-size: 12px; -} - -/* === TABLES === */ - -/* Appliquer un layout fixe pour harmoniser la largeur des colonnes */ +/* Planning hebdomadaire principal */ #planningTable { table-layout: fixed; - width: 100%; /* le tableau prend toute la largeur disponible */ + width: 100%; } -/* Colonnes : texte centré et césures possibles */ #planningTable th, #planningTable td { text-align: center; - white-space: nowrap; /* évite les retours à la ligne sauvages */ + white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; /* coupe avec "..." si le texte déborde */ + text-overflow: ellipsis; } -/* Colonnes "Mois" et "Semaine" un peu plus étroites */ +/* Colonnes "Mois" et "Semaine" plus étroites */ #planningTable th:nth-child(1), -#planningTable td:nth-child(1) { - width: 2%; /* Mois */ -} - +#planningTable td:nth-child(1), #planningTable th:nth-child(2), #planningTable td:nth-child(2) { - width: 2%; /* Semaine */ + width: 2%; } -/* Colonnes jours (Lundi à Vendredi) réparties sur une largeur raisonnable */ +/* Colonnes jours (Lundi à Vendredi) */ #planningTable th:nth-child(3), #planningTable td:nth-child(3), #planningTable th:nth-child(4), @@ -184,10 +54,10 @@ label { #planningTable td:nth-child(6), #planningTable th:nth-child(7), #planningTable td:nth-child(7) { - width: 2%; /* ajustable selon ton rendu */ + width: 2%; } -/* Colonnes de totaux (Off / Extra / Centre / Avis) un peu plus compactes */ +/* Colonnes de totaux */ #planningTable th:nth-child(8), #planningTable td:nth-child(8), #planningTable th:nth-child(9), @@ -199,48 +69,8 @@ label { width: 4%; } -.pf-table-wrapper { - max-height: 600px; - overflow: auto; - border-radius: 8px; - background: #fff; - border: 1px solid #d9e2ec; -} +/* === COLOR CODING / LÉGENDE === */ -.pf-table { - width: 100%; - border-collapse: collapse; - font-size: 12px; -} - -.pf-table th, -.pf-table td { - border: 1px solid #d9e2ec; - padding: 4px 6px; - text-align: center; - position: relative; -} - -.pf-table thead th { - position: sticky; - top: 0; - background: #f0f4f8; - z-index: 1; -} - -.pf-table--compact th, -.pf-table--compact td { - padding: 3px 4px; -} - -/* === INPUTS === */ -input[type="number"] { - width: 3.1rem; - font-size: 11px; - padding: 2px 3px; -} - -/* === COLOR CODING POUR LE FAMILY CALENDAR === */ .fc-row--carole-off { background: #ffe0e0; } @@ -254,30 +84,41 @@ input[type="number"] { width: 100%; border-collapse: collapse; font-size: 12px; - margin-top: 12px; + margin-top: 8px; } .fc-holidays-table th, .fc-holidays-table td { - border: 1px solid #d9e2ec; - padding: 4px 6px; - text-align: left; + border: none; + padding: 4px 4px; } -.fc-holidays-table th { - background-color: #f0f4f8; +.fc-holidays-table thead th { + background: none; + font-weight: 600; + color: #243b53; + padding-bottom: 4px; + border-bottom: 1px solid #d9e2ec; +} + +.fc-holidays-table tbody tr:nth-child(odd) td { + background: #f8fafc; +} + +.fc-holidays-table tbody tr:nth-child(even) td { + background: transparent; } /* Cellule de jour sélectionnée dans le calendrier */ .fc-day--selected { - background-color: #bde4ff !important; /* Une couleur de sélection claire, !important pour forcer le style */ + background-color: #bde4ff !important; cursor: pointer; } /* Le menu contextuel flottant */ .fc-selection-menu { - display: none; /* Caché par défaut */ - position: absolute; /* Permet de le positionner près de la souris */ + display: none; + position: absolute; z-index: 100; background-color: white; border: 1px solid #ccc; @@ -290,6 +131,7 @@ input[type="number"] { .fc-selection-menu .fc-menu-section { margin-bottom: 8px; } + .fc-selection-menu .fc-menu-section:last-child { margin-bottom: 0; } @@ -334,25 +176,12 @@ input[type="number"] { box-sizing: border-box; } -/* Picto pour Centre (toboggan/balançoire) */ +/* Picto pour Centre */ .fc-day--has-guard.fc-day--centre { position: relative; } .fc-day--has-guard.fc-day--centre::after { - content: "🎢"; - position: absolute; - top: 2px; - right: 2px; - font-size: 14px; - line-height: 1; - z-index: 2; -} -/* Picto pour Avis (FC Barcelone) */ -.fc-day--has-guard.fc-day--avis { - position: relative; -} -.fc-day--has-guard.fc-day--avis::after { - content: "⚽"; + content: "🏫"; position: absolute; top: 2px; right: 2px; @@ -361,31 +190,131 @@ input[type="number"] { z-index: 2; } -/* --- Combinaisons avec dégradés --- */ -/* Combinaison Vacances + Off Carole */ +/* Combinaisons avec dégradés */ .fc-day--school-holiday.fc-day--off-carole { background-image: linear-gradient(45deg, #e5d9f2 49%, #ffe9a7 51%); } -/* Combinaison Vacances + Extra Off Carole */ .fc-day--school-holiday.fc-day--extra-off-carole { background-image: linear-gradient(45deg, #e5d9f2 49%, #ffd59b 51%); } -/* Eléments pour la légende */ +/* Légende */ .pf-legend-item { display: flex; align-items: center; margin-bottom: 5px; } + .pf-legend-color { - width: 20px; - height: 20px; - border: 1px solid #ccc; - margin-right: 10px; - flex-shrink: 0; + width: 18px; + height: 18px; + border-radius: 4px; + border: 1px solid #d9e2ec; + margin-right: 8px; +} + +.pf-legend-item span { + font-size: 12px; + color: #334e68; +} + +.fc-legend-school-holiday { + background-color: #e5d9f2; +} + +.fc-legend-public-holiday { + background-color: #e0e0e0; +} + +.fc-legend-off-carole { + background-color: #ffe9a7; +} + +.fc-legend-extra-off-carole { + background-color: #ffd59b; +} + +.fc-legend-centre { + display: flex; + justify-content: center; + align-items: center; + font-size: 16px; + background: none; + border: none; +} + +.fc-legend-centre::before { + content: "🏫"; +} + +/* Avis indicateur */ +.fc-legend-avis { + border-radius: 2px; + width: 24px; + height: 16px; + background: linear-gradient( + to bottom, + #ffcc00 0%, + #ffcc00 11%, + #c8102e 11%, + #c8102e 22%, + #ffcc00 22%, + #ffcc00 33%, + #c8102e 33%, + #c8102e 44%, + #ffcc00 44%, + #ffcc00 55%, + #c8102e 55%, + #c8102e 66%, + #ffcc00 66%, + #ffcc00 77%, + #c8102e 77%, + #c8102e 88%, + #ffcc00 88%, + #ffcc00 100% + ); + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2); } /* === CALENDRIER MENSUEL === */ + +.fc-day--has-guard.fc-day--avis { + position: relative; +} + +.fc-day--has-guard.fc-day--avis::after { + content: ""; + position: absolute; + top: 2px; + right: 2px; + width: 14px; + height: 8px; + border-radius: 2px; + background: linear-gradient( + to bottom, + #ffcc00 0%, + #ffcc00 11%, + #c8102e 11%, + #c8102e 22%, + #ffcc00 22%, + #ffcc00 33%, + #c8102e 33%, + #c8102e 44%, + #ffcc00 44%, + #ffcc00 55%, + #c8102e 55%, + #c8102e 66%, + #ffcc00 66%, + #ffcc00 77%, + #c8102e 77%, + #c8102e 88%, + #ffcc00 88%, + #ffcc00 100% + ); + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2); + z-index: 2; +} + .fc-month-calendar-wrapper { background: #fff; border-radius: 8px; @@ -395,10 +324,7 @@ input[type="number"] { } .fc-calendar-and-summary { - display: grid; - grid-template-columns: 1fr auto; - gap: 24px; - align-items: start; + display: block; } .fc-month-header { @@ -506,6 +432,36 @@ input[type="number"] { transition: background-color 0.2s; } +.fc-month-summary-inline { + margin-top: 8px; + background: #f8fafc; + border-radius: 6px; + border: 1px solid #d9e2ec; + font-size: 12px; + padding: 6px 8px; +} + +.fc-month-summary-inline table { + width: 100%; + border-collapse: collapse; +} + +.fc-month-summary-inline th, +.fc-month-summary-inline td { + padding: 4px 6px; + text-align: center; +} + +.fc-month-summary-inline th { + font-weight: 600; + color: #243b53; + border-bottom: 1px solid #d9e2ec; +} + +.fc-month-summary-inline td:first-child { + text-align: left; +} + .fc-month-day { background: #fff; } @@ -515,9 +471,9 @@ input[type="number"] { } .fc-day--other-month { - background: #f8fafc; + background: #f0f4f8; color: #9fb3c8; - cursor: default; + box-shadow: none; } .fc-day--weekend { @@ -555,8 +511,9 @@ input[type="number"] { .fc-month-table .fc-day--has-guard.fc-day--centre { position: relative; } + .fc-month-table .fc-day--has-guard.fc-day--centre::after { - content: "🎢"; + content: "🏫"; position: absolute; top: 2px; right: 2px; @@ -564,72 +521,96 @@ input[type="number"] { line-height: 1; z-index: 2; } + .fc-month-table .fc-day--has-guard.fc-day--avis { position: relative; } + .fc-month-table .fc-day--has-guard.fc-day--avis::after { - content: "⚽"; + content: ""; position: absolute; top: 2px; right: 2px; - font-size: 12px; - line-height: 1; + width: 12px; + height: 7px; + border-radius: 2px; + background: linear-gradient( + to bottom, + #ffcc00 0%, + #ffcc00 11%, + #c8102e 11%, + #c8102e 22%, + #ffcc00 22%, + #ffcc00 33%, + #c8102e 33%, + #c8102e 44%, + #ffcc00 44%, + #ffcc00 55%, + #c8102e 55%, + #c8102e 66%, + #ffcc00 66%, + #ffcc00 77%, + #c8102e 77%, + #c8102e 88%, + #ffcc00 88%, + #ffcc00 100% + ); + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2); z-index: 2; } -/* Combinaisons avec dégradés pour le calendrier mensuel */ -.fc-month-table .fc-day--school-holiday.fc-day--off-carole { - background-image: linear-gradient(45deg, #e5d9f2 49%, #ffe9a7 51%); -} - -.fc-month-table .fc-day--school-holiday.fc-day--extra-off-carole { - background-image: linear-gradient(45deg, #e5d9f2 49%, #ffd59b 51%); -} - .fc-month-table .fc-day--selected { background-color: #bde4ff !important; cursor: pointer; } +.fc-leaves-label-container { + position: absolute; + top: 1px; + left: 2px; + display: flex; + flex-direction: column; + gap: 0; + z-index: 2; +} + +.fc-leaves-label { + font-size: 8px; + font-weight: 600; + color: #1f2933; + line-height: 1; +} + /* === TABLEAU RÉCAPITULATIF === */ + .fc-month-summary { min-width: 300px; } .fc-summary-table-wrapper { background: #f8fafc; - border-radius: 6px; - padding: 12px; + border-radius: 10px; + padding: 10px 10px 12px; border: 1px solid #d9e2ec; + box-shadow: 0 4px 10px rgba(15, 23, 42, 0.06); } .fc-summary-table { - width: 100%; - border-collapse: collapse; - font-size: 13px; + font-size: 12px; } .fc-summary-table thead th { - background: #f0f4f8; - padding: 8px 6px; - text-align: center; - font-weight: 600; - color: #243b53; - border: 1px solid #d9e2ec; - white-space: nowrap; + background: transparent; + border-bottom: 1px solid #d9e2ec; } .fc-summary-table tbody td { - border: 1px solid #d9e2ec; - padding: 6px 8px; - text-align: center; - background: #fff; + border: none; + border-bottom: 1px solid #edf2f7; } -.fc-summary-table tbody td:first-child { - text-align: left; - font-weight: 500; - color: #243b53; +.fc-summary-table tbody tr:last-child td { + border-bottom: none; } .fc-summary-total-row { diff --git a/assets/js/family-calendar.js b/modules/family-calendar/family-calendar.js similarity index 68% rename from assets/js/family-calendar.js rename to modules/family-calendar/family-calendar.js index 899bd87..9b948ff 100644 --- a/assets/js/family-calendar.js +++ b/modules/family-calendar/family-calendar.js @@ -42,6 +42,7 @@ document.addEventListener("DOMContentLoaded", () => { this.fixedEvents = []; this.events = []; this.menuJustOpened = false; + this.leaves = []; this.init(); window.cal = this; @@ -55,6 +56,7 @@ document.addEventListener("DOMContentLoaded", () => { this.dbEvents = this.loadDbEvents(); this.fixedEvents = await this.fetchPublicAndSchoolHolidays(); this.events = [...this.dbEvents, ...this.fixedEvents]; + this.leaves = await this.fetchLeaves(); this.reprocessAndRender(); this.renderMonthCalendar(); @@ -73,6 +75,20 @@ document.addEventListener("DOMContentLoaded", () => { return []; } + async fetchLeaves() { + try { + const res = await fetch("/api/get-leaves.php"); + if (!res.ok) { + throw new Error("Erreur HTTP " + res.status); + } + const data = await res.json(); + return data.leaves || []; + } catch (err) { + console.error("Erreur lors du chargement des congés Alex/Laia :", err); + return []; + } + } + async fetchPublicAndSchoolHolidays() { // 1. Jours fériés (fixes) const publicHolidays = [ @@ -329,6 +345,36 @@ document.addEventListener("DOMContentLoaded", () => { if (gardeType === "AVIS") td.classList.add("fc-day--avis"); } + // Indicateur congés Alex/Laia + const isoDate = td.dataset.date; + const leavesOnDay = this.leaves.filter( + (lv) => lv.leave_date === isoDate + ); + + if (leavesOnDay.length > 0) { + const container = document.createElement("div"); + container.className = "fc-leaves-label-container"; + + const hasAlex = leavesOnDay.some((lv) => lv.person_id === 2); // Alex + const hasLaia = leavesOnDay.some((lv) => lv.person_id === 3); // Laia + + if (hasAlex) { + const alexSpan = document.createElement("span"); + alexSpan.className = "fc-leaves-label"; + alexSpan.textContent = "AF"; + container.appendChild(alexSpan); + } + + if (hasLaia) { + const laiaSpan = document.createElement("span"); + laiaSpan.className = "fc-leaves-label"; + laiaSpan.textContent = "LM"; + container.appendChild(laiaSpan); + } + + td.appendChild(container); + } + tr.appendChild(td); }); @@ -378,9 +424,15 @@ document.addEventListener("DOMContentLoaded", () => { ); document.addEventListener("mouseup", (e) => this.handleMonthMouseUp(e)); if (this.monthSelectionMenu) { + // Gestion add / add-single / delete / update sur le calendrier mensuel this.monthSelectionMenu.addEventListener("click", (e) => this.handleMonthMenuClick(e) ); + + // Gestion des actions bulk (bulk-update*, bulk-delete*) via handleMenuClick + this.monthSelectionMenu.addEventListener("click", (e) => + this.handleMenuClick(e) + ); } } @@ -405,49 +457,174 @@ document.addEventListener("DOMContentLoaded", () => { } handleMouseDown(e) { - const cell = e.target.closest("td[data-date]"); - if (!cell) return; - e.preventDefault(); - this.clearSelection(); + console.log("handleMouseDown raw target:", e.target); + const cell = e.target.closest("#planningTable td[data-date]"); + console.log("handleMouseDown cell:", cell); + + if (!cell) return; + + e.preventDefault(); + + this.clearSelection(); this.isSelecting = true; + cell.classList.add("fc-day--selected"); - this.selectedCells.push(cell); + this.selectedCells = [cell]; + + console.log( + "handleMouseDown after select, selectedCells =", + this.selectedCells + ); } handleMouseMove(e) { if (!this.isSelecting) return; - const cell = e.target.closest("td[data-date]"); - if (cell && !this.selectedCells.includes(cell)) { + + const cell = e.target.closest("#planningTable td[data-date]"); + if (!cell) return; + + if (!this.selectedCells.includes(cell)) { cell.classList.add("fc-day--selected"); this.selectedCells.push(cell); } } handleMouseUp(e) { - if (!this.isSelecting) return; + console.log( + "handleMouseUp called, selectedCells.length =", + this.selectedCells.length + ); + + if (!this.isSelecting) { + return; + } + this.isSelecting = false; + if (this.selectedCells.length === 0) return; if (this.selectedCells.length === 1) { + // Cas 1 jour : logique habituelle const date = this.selectedCells[0].dataset.date; const eventsOnDay = this.events.filter( (evt) => evt.date === date && MODIFIABLE_TYPES.includes(evt.type) ); - const conge = eventsOnDay.find((e) => CONGE_TYPES.includes(e.type)); - const garde = eventsOnDay.find((e) => GUARDE_TYPES.includes(e.type)); + const conge = eventsOnDay.find((ev) => CONGE_TYPES.includes(ev.type)); + const garde = eventsOnDay.find((ev) => GUARDE_TYPES.includes(ev.type)); if (!conge && !garde) { this.showAddMenu(e); } else { this.showEditMenuForDay(e, { conge, garde, date }); } + return; + } + + // ===== Multi-jours ===== + const selectedDates = this.selectedCells.map((c) => c.dataset.date); + const eventsOnDates = this.events.filter( + (evt) => + selectedDates.includes(evt.date) && + MODIFIABLE_TYPES.includes(evt.type) + ); + + const conges = eventsOnDates.filter((ev) => + CONGE_TYPES.includes(ev.type) + ); + const gardes = eventsOnDates.filter((ev) => + GUARDE_TYPES.includes(ev.type) + ); + + const uniqueCongeTypes = new Set(conges.map((c) => c.type)); + const uniqueGardeTypes = new Set(gardes.map((g) => g.type)); + + const allDatesHaveConge = + conges.length === selectedDates.length && uniqueCongeTypes.size === 1; + const allDatesHaveGarde = + gardes.length === selectedDates.length && uniqueGardeTypes.size === 1; + + console.log("Multi-jours:"); + console.log("selectedDates:", selectedDates); + console.log("eventsOnDates:", eventsOnDates); + console.log("conges:", conges); + console.log("gardes:", gardes); + console.log("uniqueCongeTypes:", Array.from(uniqueCongeTypes)); + console.log("uniqueGardeTypes:", Array.from(uniqueGardeTypes)); + console.log("allDatesHaveConge:", allDatesHaveConge); + console.log("allDatesHaveGarde:", allDatesHaveGarde); + + if (allDatesHaveConge || allDatesHaveGarde) { + const bulkInfo = { + selectedDates, + conges, + gardes, + congeType: allDatesHaveConge ? conges[0].type : null, + gardeType: allDatesHaveGarde ? gardes[0].type : null, + }; + this.showBulkMenu(e, bulkInfo); } else { - // Multi-jours : on reste en mode ajout, avec contrôles plus tard this.showAddMenu(e); } } + showBulkMenu(e, bulkInfo) { + const { selectedDates, conges, gardes, congeType, gardeType } = bulkInfo; + const nbDays = selectedDates.length; + + let html = + '
Actions multi-jours
'; + + if (congeType) { + const oppositeConge = + congeType === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE"; + html += ` +
Congés Carole (${nbDays} jours)
+ + + `; + } + + if (gardeType) { + const oppositeGarde = gardeType === "CENTRE" ? "AVIS" : "CENTRE"; + html += ` +
Mode de garde (${nbDays} jours)
+ + + `; + } + + this.selectionMenu.innerHTML = html; + // On stocke bulkInfo pour réutilisation dans handleMenuClick + this._currentBulkInfo = bulkInfo; + this.positionAndShowMenu(e); + } + handleClickOutsideMenu(e) { if (this.menuJustOpened) { this.menuJustOpened = false; @@ -498,58 +675,105 @@ document.addEventListener("DOMContentLoaded", () => { "garde =", garde ); + + // --- Section congé Carole --- let congeSection = ""; if (conge) { const oppositeConge = conge.type === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE"; congeSection = ` -
Congé Carole
- - - `; +
Congé Carole
+ + + `; } else { congeSection = ` -
Ajouter un congé
- - - `; +
Ajouter un congé
+ + + `; } + // --- Section mode de garde --- let gardeSection = ""; if (garde) { const oppositeGarde = garde.type === "CENTRE" ? "AVIS" : "CENTRE"; gardeSection = ` -
Mode de garde
- - - `; +
Mode de garde
+ + + `; } else { gardeSection = ` -
Ajouter mode de garde
- - - `; +
Ajouter mode de garde
+ + + `; } - this.selectionMenu.innerHTML = congeSection + gardeSection; + // --- Section congés Alex / Laia --- + console.log( + "[EDIT MENU] this.leaves length =", + (this.leaves || []).length + ); + + const leavesOnDay = (this.leaves || []).filter( + (lv) => lv.leave_date === date + ); + console.log("[EDIT MENU] leavesOnDay =", leavesOnDay); + + let leavesSection = ` +
Congés Alex / Laia
+ + + + + + + `; + + if (leavesOnDay.length > 0) { + leavesSection += ` + + `; + } + + const fullHtml = congeSection + gardeSection + leavesSection; + console.log("[EDIT MENU] full menu HTML length =", fullHtml.length); + + this.selectionMenu.innerHTML = fullHtml; this.positionAndShowMenu(e); } @@ -566,7 +790,160 @@ document.addEventListener("DOMContentLoaded", () => { const { action, eventId, newType, type, person, date } = button.dataset; - // === AJOUT MULTI-JOURS (sélection de plusieurs cellules) === + // === BULK ACTIONS (multi-jours) ============================================ + if ( + action === "bulk-update-conge" || + action === "bulk-delete-conge" || + action === "bulk-update-garde" || + action === "bulk-delete-garde" + ) { + if (!this._currentBulkInfo) { + this.clearSelection(); + return; + } + + const bulkInfo = this._currentBulkInfo; + const target = button.dataset.target; // "conge" ou "garde" + + let events = []; + if (target === "conge") { + events = bulkInfo.conges; + } else if (target === "garde") { + events = bulkInfo.gardes; + } + + const eventIds = events.map((ev) => ev.id); + this.clearSelection(); + this._currentBulkInfo = null; + + try { + let payload; + if ( + action === "bulk-delete-conge" || + action === "bulk-delete-garde" + ) { + payload = { + action: "bulk_delete", + event_ids: eventIds, + }; + } else { + const bulkNewType = button.dataset.newType; + payload = { + action: "bulk_update", + event_ids: eventIds, + new_type: bulkNewType, + }; + } + + const res = await fetch("/api/manage-event.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!res.ok) { + const errData = await res.json().catch(() => ({})); + throw new Error(errData.message || "Erreur HTTP " + res.status); + } + + const resEvents = await fetch("/api/get-events.php"); + if (!resEvents.ok) { + throw new Error("Erreur lors du rechargement des événements."); + } + const dataEvents = await resEvents.json(); + + this.dbEvents = dataEvents.events || []; + this.events = [...this.dbEvents, ...this.fixedEvents]; + + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error("Erreur bulk:", err); + alert("Erreur lors de l'action multi-jours: " + err.message); + } + + return; + } + + // === AJOUT D'UN CONGÉ ALEX/LAIA SUR UNE SEULE DATE ========================== + if (action === "add-leave") { + const leaveDate = date; + const personId = parseInt(button.dataset.personId, 10); + const leaveType = button.dataset.leaveType; + + if (!leaveDate || !personId || !leaveType) { + this.clearSelection(); + return; + } + + const newLeave = { + date: leaveDate, + person_id: personId, + leave_type: leaveType, + duration: 1.0, + }; + + this.clearSelection(); + + try { + const response = await fetch("/api/save-leaves.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([newLeave]), + }); + if (!response.ok) { + throw new Error("Erreur HTTP " + response.status); + } + + // Recharger les congés + this.leaves = await this.fetchLeaves(); + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error(err); + alert("Erreur lors de l'ajout de congé Alex/Laia."); + } + + return; + } + + // === SUPPRIMER TOUS LES CONGÉS ALEX/LAIA D'UN JOUR ========================== + if (action === "delete-leaves-day") { + const leaveDate = date; + if (!leaveDate) { + this.clearSelection(); + return; + } + + this.clearSelection(); + + try { + const response = await fetch("/api/manage-leaf.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "delete_day", date: leaveDate }), + }); + if (!response.ok) { + const errData = await response.json().catch(() => ({})); + throw new Error( + errData.message || "Erreur HTTP " + response.status + ); + } + + this.leaves = await this.fetchLeaves(); + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error(err); + alert( + "Erreur lors de la suppression des congés Alex/Laia pour ce jour: " + + err.message + ); + } + + return; + } + + // === AJOUT MULTI-JOURS (sélection de plusieurs cellules) =================== if (action === "add") { const selectedDates = this.selectedCells.map((c) => c.dataset.date); const existingOnDates = this.dbEvents.filter((evt) => @@ -636,7 +1013,7 @@ document.addEventListener("DOMContentLoaded", () => { return; } - // === AJOUT SUR UNE SEULE DATE (menu combiné d'un jour) === + // === AJOUT SUR UNE SEULE DATE (menu combiné d'un jour) ===================== if (action === "add-single") { const targetDate = date; const existingOnDate = this.dbEvents.filter( @@ -682,7 +1059,6 @@ document.addEventListener("DOMContentLoaded", () => { throw new Error("Erreur HTTP " + response.status); } - // Resync DB events const resEvents = await fetch("/api/get-events.php"); if (!resEvents.ok) { throw new Error("Erreur lors du rechargement des événements."); @@ -701,7 +1077,7 @@ document.addEventListener("DOMContentLoaded", () => { return; } - // === SUPPRESSION === + // === SUPPRESSION SIMPLE ==================================================== if (action === "delete") { if (!eventId) { console.warn("Bouton delete sans eventId, action ignorée."); @@ -724,7 +1100,6 @@ document.addEventListener("DOMContentLoaded", () => { ); } - // Resync DB events const resEvents = await fetch("/api/get-events.php"); if (!resEvents.ok) { throw new Error("Erreur lors du rechargement des événements."); @@ -743,7 +1118,7 @@ document.addEventListener("DOMContentLoaded", () => { return; } - // === MODIFICATION (change OFF <-> EXTRA ou CENTRE <-> AVIS) === + // === MODIFICATION SIMPLE =================================================== if (action === "update") { console.log("[UPDATE] click sur bouton update", { eventId, newType }); @@ -772,7 +1147,6 @@ document.addEventListener("DOMContentLoaded", () => { ); } - // Resync DB events const resEvents = await fetch("/api/get-events.php"); if (!resEvents.ok) { throw new Error("Erreur lors du rechargement des événements."); @@ -836,36 +1210,20 @@ document.addEventListener("DOMContentLoaded", () => { navigateMonth(direction) { if (this.viewMode === "year") { - // Navigation par année scolaire (septembre à août) - this.currentMonth.setFullYear( - this.currentMonth.getFullYear() + direction - ); - } else if (this.viewMode === "2months") { - this.currentMonth.setMonth(this.currentMonth.getMonth() + direction); - // S'assurer qu'on reste dans une année scolaire valide - this.adjustToSchoolYear(); + // Navigation par année scolaire : on avance/recul d'un an, + // en gardant currentMonth fixé sur septembre + const year = this.currentMonth.getFullYear() + direction; + this.currentMonth = new Date(year, 8, 1); // 8 = septembre } else { - this.currentMonth.setMonth(this.currentMonth.getMonth() + direction); - // S'assurer qu'on reste dans une année scolaire valide - this.adjustToSchoolYear(); - } - this.renderMonthCalendar(); - } + // Navigation simple par mois (1 ou 2 mois) + const year = this.currentMonth.getFullYear(); + const month = this.currentMonth.getMonth() + direction; - adjustToSchoolYear() { - const month = this.currentMonth.getMonth(); - const year = this.currentMonth.getFullYear(); - // Si on dépasse août (mois 7), on passe à septembre de l'année suivante - if (month > 7) { - this.currentMonth.setFullYear(year + 1); - this.currentMonth.setMonth(8); // Septembre - } - // Si on est avant septembre (mois 8), on reste dans l'année scolaire - // mais on pourrait vouloir revenir à septembre de l'année en cours - if (month < 8) { - // On garde l'année mais on pourrait vouloir ajuster - // Pour l'instant, on laisse comme ça car ça peut être août de l'année scolaire + // new Date gère automatiquement le dépassement (ex: 2025, 12 => jan 2026) + this.currentMonth = new Date(year, month, 1); } + + this.renderMonthCalendar(); } renderMonthCalendar() { @@ -875,30 +1233,27 @@ document.addEventListener("DOMContentLoaded", () => { const monthTitle = document.getElementById("fc-current-month-year"); if (monthTitle) { const year = this.currentMonth.getFullYear(); + const month = this.currentMonth.getMonth(); + + // Calcul de l'année scolaire à partir du mois courant + const schoolYearStart = month >= 8 ? year : year - 1; + const schoolYearEnd = schoolYearStart + 1; + if (this.viewMode === "year") { - // Afficher l'année scolaire (ex: 2025-2026) - monthTitle.textContent = `${year}-${year + 1}`; + // Année scolaire complète + monthTitle.textContent = `Année scolaire ${schoolYearStart}–${schoolYearEnd}`; } else if (this.viewMode === "2months") { - const month = this.currentMonth.getMonth(); const monthName = getMonthNameFr(month); - const nextMonth = new Date(year, month + 1, 1); - const nextMonthName = getMonthNameFr(nextMonth.getMonth()); - // Ajuster l'année si on passe de décembre à janvier - const displayYear = month >= 8 ? year : year + 1; - monthTitle.textContent = `${monthName} - ${nextMonthName} ${displayYear}`; + const next = new Date(year, month + 1, 1); + const nextMonthIndex = next.getMonth(); + const nextYear = next.getFullYear(); + const nextMonthName = getMonthNameFr(nextMonthIndex); + + monthTitle.textContent = `${monthName} ${year} – ${nextMonthName} ${nextYear} (${schoolYearStart}–${schoolYearEnd})`; } else { - const month = this.currentMonth.getMonth(); + // Vue 1 mois const monthName = getMonthNameFr(month); - // Afficher l'année scolaire si on est après août - if (month >= 8) { - monthTitle.textContent = `${monthName} ${year} (${year}-${ - year + 1 - })`; - } else { - monthTitle.textContent = `${monthName} ${year} (${ - year - 1 - }-${year})`; - } + monthTitle.textContent = `${monthName} ${year} (${schoolYearStart}–${schoolYearEnd})`; } } @@ -910,15 +1265,14 @@ document.addEventListener("DOMContentLoaded", () => { } else { this.renderSingleMonthView(); } - - // Afficher le tableau récapitulatif - this.renderMonthSummary(); } renderSingleMonthView() { const year = this.currentMonth.getFullYear(); const month = this.currentMonth.getMonth(); - this.monthCalendar.innerHTML = this.generateMonthHTML(year, month); + const calendarHTML = this.generateMonthHTML(year, month); + const summaryHTML = this.generateMonthSummaryHTML(year, month); + this.monthCalendar.innerHTML = calendarHTML + summaryHTML; } calculateMonthTotals(year, month) { @@ -971,6 +1325,39 @@ document.addEventListener("DOMContentLoaded", () => { return totals; } + generateMonthSummaryHTML(year, month) { + const totals = this.calculateMonthTotals(year, month); + const formatTotal = (total) => + total > 0 ? (Number.isInteger(total) ? total : total.toFixed(1)) : ""; + + const monthLabel = `${getMonthNameFr(month)} ${year}`; + + return ` +
+ + + + + + + + + + + + + + + + + + + +
Mois# Off Carole# Extra off Carole# Centre# Avis
${monthLabel}${formatTotal(totals.offCarole)}${formatTotal(totals.extraOffCarole)}${formatTotal(totals.centre)}${formatTotal(totals.avis)}
+
+ `; + } + renderMonthSummary() { const summaryDiv = document.getElementById("fc-month-summary"); if (!summaryDiv) return; @@ -1114,18 +1501,25 @@ document.addEventListener("DOMContentLoaded", () => { const nextMonthIndex = nextMonth > 11 ? 0 : nextMonth; let html = '
'; + + // Premier mois html += `
`; html += `
${getMonthNameFr(month)} ${ month >= 8 ? year : year + 1 }
`; html += this.generateMonthHTML(year, month); + html += this.generateMonthSummaryHTML(year, month); html += `
`; + + // Deuxième mois html += `
`; html += `
${getMonthNameFr(nextMonthIndex)} ${ nextMonthIndex >= 8 ? nextYear : nextYear + 1 }
`; html += this.generateMonthHTML(nextYear, nextMonthIndex); + html += this.generateMonthSummaryHTML(nextYear, nextMonthIndex); html += `
`; + html += "
"; this.monthCalendar.innerHTML = html; } @@ -1133,13 +1527,10 @@ document.addEventListener("DOMContentLoaded", () => { renderYearView() { const year = this.currentMonth.getFullYear(); let html = '
'; - // Année scolaire : septembre (8) à août (7) de l'année suivante const schoolYearMonths = []; - // Septembre à décembre de l'année en cours for (let month = 8; month < 12; month++) { schoolYearMonths.push({ year, month }); } - // Janvier à août de l'année suivante for (let month = 0; month < 8; month++) { schoolYearMonths.push({ year: year + 1, month }); } @@ -1150,6 +1541,7 @@ document.addEventListener("DOMContentLoaded", () => { month )} ${monthYear}
`; html += this.generateMonthHTML(monthYear, month); + html += this.generateMonthSummaryHTML(monthYear, month); html += `
`; }); html += ""; @@ -1235,7 +1627,27 @@ document.addEventListener("DOMContentLoaded", () => { if (gardeType === "AVIS") classes += " fc-day--avis"; } - html += `${day}`; + // Construire la cellule avec indicateur congés Alex/Laia + // (on génère un TD "manuel" pour pouvoir inclure le conteneur) + const leavesOnDay = this.leaves.filter( + (lv) => lv.leave_date === isoDate + ); + + let cellInnerHTML = `${day}`; + + if (leavesOnDay.length > 0) { + const hasAlex = leavesOnDay.some((lv) => lv.person_id === 2); // Alex + const hasLaia = leavesOnDay.some((lv) => lv.person_id === 3); // Laia + + let leavesHtml = `
`; + if (hasAlex) leavesHtml += `AF`; + if (hasLaia) leavesHtml += `LM`; + leavesHtml += `
`; + + cellInnerHTML += leavesHtml; + } + + html += `${cellInnerHTML}`; dayCount++; } @@ -1277,6 +1689,7 @@ document.addEventListener("DOMContentLoaded", () => { if (this.monthSelectedCells.length === 0) return; if (this.monthSelectedCells.length === 1) { + // Cas 1 jour : logique habituelle const date = this.monthSelectedCells[0].dataset.date; const eventsOnDay = this.events.filter( (evt) => evt.date === date && MODIFIABLE_TYPES.includes(evt.type) @@ -1289,6 +1702,41 @@ document.addEventListener("DOMContentLoaded", () => { } else { this.showMonthEditMenuForDay(e, { conge, garde, date }); } + return; + } + + // ===== Multi-jours : tentative de bulk edit ===== + const selectedDates = this.monthSelectedCells.map((c) => c.dataset.date); + const eventsOnDates = this.events.filter( + (evt) => + selectedDates.includes(evt.date) && + MODIFIABLE_TYPES.includes(evt.type) + ); + + const conges = eventsOnDates.filter((ev) => + CONGE_TYPES.includes(ev.type) + ); + const gardes = eventsOnDates.filter((ev) => + GUARDE_TYPES.includes(ev.type) + ); + + const uniqueCongeTypes = new Set(conges.map((c) => c.type)); + const uniqueGardeTypes = new Set(gardes.map((g) => g.type)); + + const allDatesHaveConge = + conges.length === selectedDates.length && uniqueCongeTypes.size === 1; + const allDatesHaveGarde = + gardes.length === selectedDates.length && uniqueGardeTypes.size === 1; + + if (allDatesHaveConge || allDatesHaveGarde) { + const bulkInfo = { + selectedDates, + conges, + gardes, + congeType: allDatesHaveConge ? conges[0].type : null, + gardeType: allDatesHaveGarde ? gardes[0].type : null, + }; + this.showMonthBulkMenu(e, bulkInfo); } else { this.showMonthAddMenu(e); } @@ -1319,58 +1767,157 @@ document.addEventListener("DOMContentLoaded", () => { showMonthEditMenuForDay(e, { conge, garde, date }) { if (!this.monthSelectionMenu) return; + + // --- Section congé Carole --- let congeSection = ""; if (conge) { const oppositeConge = conge.type === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE"; congeSection = ` -
Congé Carole
- - - `; +
Congé Carole
+ + + `; } else { congeSection = ` -
Ajouter un congé
- - - `; +
Ajouter un congé
+ + + `; } + // --- Section mode de garde --- let gardeSection = ""; if (garde) { const oppositeGarde = garde.type === "CENTRE" ? "AVIS" : "CENTRE"; gardeSection = ` -
Mode de garde
- - - `; +
Mode de garde
+ + + `; } else { gardeSection = ` -
Ajouter mode de garde
- - - `; +
Ajouter mode de garde
+ + + `; } - this.monthSelectionMenu.innerHTML = congeSection + gardeSection; + // --- Section congés Alex / Laia --- + const leavesOnDay = (this.leaves || []).filter( + (lv) => lv.leave_date === date + ); + + let leavesSection = ` +
Congés Alex / Laia
+ + + + + + + `; + + if (leavesOnDay.length > 0) { + leavesSection += ` + + `; + } + + // --- Assemblage du menu --- + this.monthSelectionMenu.innerHTML = + congeSection + gardeSection + leavesSection; + this.positionAndShowMonthMenu(e); + } + + showMonthBulkMenu(e, bulkInfo) { + if (!this.monthSelectionMenu) return; + + const { selectedDates, conges, gardes, congeType, gardeType } = bulkInfo; + const nbDays = selectedDates.length; + + let html = + '
Actions multi-jours
'; + + if (congeType) { + const oppositeConge = + congeType === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE"; + html += ` +
Congés Carole (${nbDays} jours)
+ + + `; + } + + if (gardeType) { + const oppositeGarde = gardeType === "CENTRE" ? "AVIS" : "CENTRE"; + html += ` +
Mode de garde (${nbDays} jours)
+ + + `; + } + + this.monthSelectionMenu.innerHTML = html; + // On réutilise la même propriété que pour le planning hebdo : + this._currentBulkInfo = bulkInfo; this.positionAndShowMonthMenu(e); } diff --git a/modules/home/assets/img/background.jpg b/modules/home/assets/img/background.jpg new file mode 100644 index 0000000..b326f32 Binary files /dev/null and b/modules/home/assets/img/background.jpg differ diff --git a/modules/home/home.css b/modules/home/home.css new file mode 100644 index 0000000..01e96df --- /dev/null +++ b/modules/home/home.css @@ -0,0 +1,117 @@ +/* === HOME : fond et overlay === */ + +/* Fond image + overlay uniquement pour la home */ +body.pf-home { + position: relative; + background-image: url("/modules/home/assets/img/background.jpg"); + background-size: cover; + background-position: top center; + background-repeat: no-repeat; +} + +/* Header translucide par-dessus l'image sur la home */ +.pf-home .pf-header { + background: rgba(36, 59, 83, 0.85); + backdrop-filter: blur(4px); /* optionnel, si supporté */ +} + +/* Voile sombre sur toute la page home */ +body.pf-home::before { + content: ""; + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.35); + z-index: -1; +} + +/* S'assurer que le contenu passe au-dessus du voile */ +body.pf-home > * { + position: relative; + z-index: 0; +} + +/* === HOME / MODULES CARDS === */ + +/* Container des cartes de modules */ +.pf-modules-cards { + margin-top: 16px; +} + +/* Card module : réutilise .pf-card mais plus "clicable" */ +.pf-card--module { + min-width: 240px; + max-width: 320px; + display: flex; + flex-direction: column; + text-decoration: none; + color: inherit; + background: #ffffff; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease, + border-color 0.15s ease, background-color 0.15s ease; + box-shadow: 0 1px 4px rgba(15, 23, 42, 0.06); +} + +.pf-card--module:hover { + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(15, 23, 42, 0.12); + border-color: #bcccdc; + background-color: #f8fafc; +} + +/* Désactivation / "à venir" */ +.pf-card--disabled { + cursor: default; + opacity: 0.7; +} + +.pf-card--disabled:hover { + transform: none; + box-shadow: 0 1px 4px rgba(15, 23, 42, 0.06); + border-color: #d9e2ec; + background-color: #f8fafc; +} + +/* Icône dans la card */ +.pf-card-icon { + font-size: 24px; + margin-bottom: 8px; +} + +/* CTA (texte en bas de card) */ +.pf-card-cta { + margin-top: 12px; + font-size: 12px; + font-weight: 600; + color: #243b53; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.pf-card-cta::after { + content: "→"; + font-size: 12px; +} + +/* Assure que le CTA reste en bas quand le texte est plus long */ +.pf-card--module .pf-card-body { + flex: 1; +} + +/* Bloc user info */ +.pf-user-info { + margin-top: 12px; + font-size: 13px; + color: #4b5563; +} + +.pf-user-info a { + color: #c81e1e; + text-decoration: none; + font-weight: 500; +} + +.pf-user-info a:hover { + text-decoration: underline; +}