1403 lines
50 KiB
HTML
1403 lines
50 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Forgium Calendar | Emploi du Temps 7J / 24H (Q1 & Q2)</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&family=JetBrains+Mono:wght@500;700;800&display=swap" rel="stylesheet">
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
|
|
|
<style>
|
|
:root {
|
|
--bg-dark: #0b0f17;
|
|
--bg-card: rgba(18, 24, 38, 0.85);
|
|
--border-color: rgba(255, 255, 255, 0.08);
|
|
--text-main: #f3f4f6;
|
|
--text-muted: #94a3b8;
|
|
--primary: #6366f1;
|
|
--accent-cyan: #06b6d4;
|
|
--accent-amber: #f59e0b;
|
|
--hour-height: 72px; /* Hauteur confortable évitant tout écrasement */
|
|
--sidebar-width: 65px;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
background-color: var(--bg-dark);
|
|
color: var(--text-main);
|
|
min-height: 100vh;
|
|
background-image:
|
|
radial-gradient(circle at 10% 15%, rgba(99, 102, 241, 0.1) 0%, transparent 40%),
|
|
radial-gradient(circle at 90% 85%, rgba(6, 182, 212, 0.08) 0%, transparent 40%);
|
|
background-attachment: fixed;
|
|
}
|
|
|
|
/* Barre supérieure */
|
|
header {
|
|
backdrop-filter: blur(16px);
|
|
background: rgba(11, 15, 23, 0.9);
|
|
border-bottom: 1px solid var(--border-color);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 50;
|
|
padding: 0.75rem 1.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.brand-section {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.logo-badge {
|
|
width: 36px;
|
|
height: 36px;
|
|
background: linear-gradient(135deg, #6366f1, #06b6d4);
|
|
border-radius: 10px;
|
|
display: grid;
|
|
place-items: center;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-weight: 800;
|
|
font-size: 1.1rem;
|
|
color: white;
|
|
}
|
|
|
|
.brand-title {
|
|
font-size: 1.15rem;
|
|
font-weight: 800;
|
|
letter-spacing: -0.02em;
|
|
color: #fff;
|
|
}
|
|
|
|
.toolbar-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
padding: 0.5rem 0.85rem;
|
|
border-radius: 8px;
|
|
font-size: 0.82rem;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
border: 1px solid var(--border-color);
|
|
background: rgba(255, 255, 255, 0.04);
|
|
color: var(--text-main);
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.btn:hover {
|
|
background: rgba(255, 255, 255, 0.09);
|
|
border-color: rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, #4f46e5, #6366f1);
|
|
border-color: rgba(99, 102, 241, 0.4);
|
|
color: #ffffff;
|
|
}
|
|
|
|
.btn-emerald {
|
|
background: rgba(16, 185, 129, 0.15);
|
|
border-color: rgba(16, 185, 129, 0.3);
|
|
color: #34d399;
|
|
}
|
|
|
|
.btn-cyan {
|
|
background: rgba(6, 182, 212, 0.15);
|
|
border-color: rgba(6, 182, 212, 0.3);
|
|
color: #38bdf8;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: rgba(244, 63, 94, 0.12);
|
|
border-color: rgba(244, 63, 94, 0.3);
|
|
color: #fb7185;
|
|
}
|
|
|
|
.calendar-wrapper {
|
|
max-width: 1600px;
|
|
margin: 1.25rem auto;
|
|
padding: 0 1rem 3rem 1rem;
|
|
}
|
|
|
|
/* Barre d'information & Titre personnalisable */
|
|
.calendar-info-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 0.85rem;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 12px;
|
|
padding: 0.75rem 1.25rem;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.schedule-title-input {
|
|
background: transparent;
|
|
border: none;
|
|
font-size: 1.15rem;
|
|
font-weight: 700;
|
|
color: var(--text-main);
|
|
outline: none;
|
|
border-bottom: 1px dashed transparent;
|
|
padding: 0.2rem 0.4rem;
|
|
border-radius: 4px;
|
|
min-width: 280px;
|
|
}
|
|
.schedule-title-input:hover, .schedule-title-input:focus {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-bottom-color: var(--accent-cyan);
|
|
}
|
|
|
|
.badge-indicator {
|
|
font-size: 0.8rem;
|
|
padding: 0.3rem 0.75rem;
|
|
border-radius: 20px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid var(--border-color);
|
|
color: var(--text-muted);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
}
|
|
.badge-indicator strong {
|
|
color: var(--text-main);
|
|
}
|
|
|
|
/* Grille principale */
|
|
.calendar-container {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 14px;
|
|
overflow: hidden;
|
|
box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.6);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* En-tête des 7 jours */
|
|
.days-header {
|
|
display: grid;
|
|
grid-template-columns: var(--sidebar-width) repeat(7, 1fr);
|
|
background: rgba(15, 23, 42, 0.95);
|
|
border-bottom: 1px solid var(--border-color);
|
|
position: sticky;
|
|
top: 57px;
|
|
z-index: 20;
|
|
}
|
|
|
|
.day-col-header {
|
|
padding: 0.85rem 0.5rem;
|
|
text-align: center;
|
|
border-right: 1px solid var(--border-color);
|
|
}
|
|
.day-col-header:last-child {
|
|
border-right: none;
|
|
}
|
|
|
|
.day-col-header .day-name {
|
|
font-size: 0.85rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: #e2e8f0;
|
|
}
|
|
|
|
.day-col-header.weekend .day-name {
|
|
color: var(--accent-amber);
|
|
}
|
|
|
|
.timezone-label {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.7rem;
|
|
color: var(--text-muted);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
border-right: 1px solid var(--border-color);
|
|
background: rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
/* Grille 24h scrollable */
|
|
.calendar-grid-scroll {
|
|
position: relative;
|
|
overflow-y: auto;
|
|
max-height: calc(100vh - 185px);
|
|
display: grid;
|
|
grid-template-columns: var(--sidebar-width) repeat(7, 1fr);
|
|
}
|
|
|
|
.time-sidebar {
|
|
background: rgba(11, 15, 23, 0.85);
|
|
border-right: 1px solid var(--border-color);
|
|
user-select: none;
|
|
}
|
|
|
|
.hour-label {
|
|
height: var(--hour-height);
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
padding-top: 4px;
|
|
font-size: 0.72rem;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
color: var(--text-muted);
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
}
|
|
|
|
.day-column {
|
|
position: relative;
|
|
border-right: 1px solid var(--border-color);
|
|
background: rgba(255, 255, 255, 0.01);
|
|
}
|
|
.day-column:last-child {
|
|
border-right: none;
|
|
}
|
|
.day-column.weekend {
|
|
background: rgba(255, 255, 255, 0.025);
|
|
}
|
|
|
|
.hour-cell {
|
|
height: var(--hour-height);
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: background 0.15s ease;
|
|
}
|
|
.hour-cell:hover {
|
|
background: rgba(99, 102, 241, 0.08);
|
|
}
|
|
|
|
/* Compression des heures de nuit (00:00 à 05:00) : les 5 heures tiennent ensemble dans l'espace d'1 heure (~72px au total) */
|
|
.hour-label.compact-night {
|
|
height: 14.4px;
|
|
font-size: 0.62rem;
|
|
padding-top: 0;
|
|
align-items: center;
|
|
opacity: 0.7;
|
|
background: rgba(0, 0, 0, 0.35);
|
|
}
|
|
.hour-cell.compact-night {
|
|
height: 14.4px;
|
|
background: rgba(0, 0, 0, 0.15);
|
|
border-bottom: 1px dashed rgba(255, 255, 255, 0.03);
|
|
}
|
|
|
|
/* =========================================================================
|
|
BLOCS DE COURS / ÉVÉNEMENTS (LISIBILITÉ MAXIMALE SANS ÉCRASEMENT)
|
|
========================================================================= */
|
|
.event-block {
|
|
position: absolute;
|
|
border-radius: 6px;
|
|
padding: 5px 6px;
|
|
color: #0f172a;
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.35);
|
|
z-index: 10;
|
|
cursor: pointer;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: flex-start;
|
|
overflow: hidden;
|
|
border: 1.5px solid rgba(0, 0, 0, 0.3);
|
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.event-block:hover {
|
|
transform: translateY(-2px) scale(1.02);
|
|
z-index: 20;
|
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.55);
|
|
}
|
|
|
|
.event-top-meta {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 3px;
|
|
line-height: 1;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.event-time-tag {
|
|
font-size: 0.62rem;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-weight: 700;
|
|
color: #1e293b;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.event-q-badge {
|
|
font-size: 0.58rem;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
font-weight: 800;
|
|
line-height: 1;
|
|
border: 1px solid rgba(0,0,0,0.35);
|
|
}
|
|
.badge-q1 { background: #bfdbfe; color: #1e40af; }
|
|
.badge-q2 { background: #fde68a; color: #92400e; }
|
|
|
|
.event-title {
|
|
font-weight: 800;
|
|
font-size: 0.72rem;
|
|
line-height: 1.15;
|
|
color: #000000;
|
|
text-transform: uppercase;
|
|
word-break: break-word;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.event-meta {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1px;
|
|
margin-top: auto;
|
|
font-size: 0.63rem;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
line-height: 1.1;
|
|
color: #1e293b;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.event-prof {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.event-salle {
|
|
align-self: flex-start;
|
|
background: rgba(0, 0, 0, 0.12);
|
|
padding: 0 4px;
|
|
border-radius: 3px;
|
|
color: #000000;
|
|
font-weight: 800;
|
|
font-size: 0.62rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Thèmes couleurs */
|
|
.bg-bleu-clair { background: #89cff0; border-color: #0284c7; }
|
|
.bg-peche-beige { background: #f6c2a1; border-color: #ea580c; }
|
|
.bg-bleu-gris { background: #b0c4de; border-color: #475569; }
|
|
.bg-vert-eau { background: #a7f3d0; border-color: #059669; }
|
|
.bg-gris-tres-clair { background: #e2e8f0; border-color: #64748b; }
|
|
.bg-blanc { background: #ffffff; border-color: #94a3b8; }
|
|
|
|
/* Modal Formulaire */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(5, 8, 15, 0.75);
|
|
backdrop-filter: blur(8px);
|
|
z-index: 100;
|
|
display: none;
|
|
place-items: center;
|
|
padding: 1rem;
|
|
}
|
|
.modal-overlay.active { display: grid; }
|
|
|
|
.modal-card {
|
|
background: #111827;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 16px;
|
|
width: 100%;
|
|
max-width: 480px;
|
|
padding: 1.35rem;
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8);
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.modal-title { font-size: 1.1rem; font-weight: 700; color: #fff; }
|
|
.close-btn { background: transparent; border: none; color: var(--text-muted); cursor: pointer; font-size: 1.25rem; }
|
|
|
|
.form-group { margin-bottom: 0.85rem; }
|
|
.form-label { display: block; font-size: 0.78rem; font-weight: 600; color: var(--text-muted); margin-bottom: 0.3rem; }
|
|
|
|
.form-input, .form-select {
|
|
width: 100%;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
padding: 0.6rem 0.8rem;
|
|
color: var(--text-main);
|
|
font-family: inherit;
|
|
font-size: 0.88rem;
|
|
outline: none;
|
|
}
|
|
.form-input:focus, .form-select:focus { border-color: var(--primary); }
|
|
|
|
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.7rem; }
|
|
|
|
.color-picker-group { display: flex; gap: 0.45rem; margin-top: 0.3rem; }
|
|
.color-dot {
|
|
width: 26px;
|
|
height: 26px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
border: 2px solid transparent;
|
|
}
|
|
.color-dot.selected { border-color: #fff; box-shadow: 0 0 8px rgba(255,255,255,0.4); }
|
|
|
|
.modal-actions {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-top: 1.25rem;
|
|
padding-top: 0.85rem;
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
|
|
/* Toast */
|
|
.toast {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
background: #1e293b;
|
|
color: white;
|
|
border: 1px solid var(--border-color);
|
|
padding: 0.7rem 1.15rem;
|
|
border-radius: 9px;
|
|
z-index: 200;
|
|
font-size: 0.85rem;
|
|
transform: translateY(100px);
|
|
opacity: 0;
|
|
transition: all 0.3s ease;
|
|
}
|
|
.toast.show { transform: translateY(0); opacity: 1; }
|
|
|
|
/* =========================================================================
|
|
STYLE D'IMPRESSION ET D'EXPORT PDF (1 SEULE PAGE A4 PAYSAGE PARFAITE)
|
|
========================================================================= */
|
|
@page {
|
|
size: A4 landscape;
|
|
margin: 4mm 5mm;
|
|
}
|
|
|
|
@media print {
|
|
html, body {
|
|
background: #ffffff !important;
|
|
color: #000000 !important;
|
|
background-image: none !important;
|
|
padding: 0 !important;
|
|
margin: 0 !important;
|
|
width: 100% !important;
|
|
height: auto !important;
|
|
overflow: visible !important;
|
|
-webkit-print-color-adjust: exact !important;
|
|
print-color-adjust: exact !important;
|
|
}
|
|
|
|
header, .toolbar-actions, .modal-overlay, .toast {
|
|
display: none !important;
|
|
}
|
|
|
|
.calendar-wrapper {
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
max-width: 100% !important;
|
|
page-break-inside: avoid !important;
|
|
break-inside: avoid !important;
|
|
}
|
|
|
|
.calendar-info-bar {
|
|
border: none !important;
|
|
background: transparent !important;
|
|
padding: 0 0 2px 0 !important;
|
|
margin-bottom: 2px !important;
|
|
}
|
|
|
|
.schedule-title-input {
|
|
color: #000000 !important;
|
|
font-size: 1.1rem !important;
|
|
font-weight: 800 !important;
|
|
border: none !important;
|
|
}
|
|
|
|
.badge-indicator {
|
|
border-color: #cbd5e1 !important;
|
|
background: #f8fafc !important;
|
|
color: #475569 !important;
|
|
padding: 1px 6px !important;
|
|
font-size: 0.72rem !important;
|
|
}
|
|
.badge-indicator strong {
|
|
color: #0f172a !important;
|
|
}
|
|
|
|
.calendar-container {
|
|
border: 1.5px solid #0f172a !important;
|
|
box-shadow: none !important;
|
|
border-radius: 4px !important;
|
|
page-break-inside: avoid !important;
|
|
break-inside: avoid !important;
|
|
}
|
|
|
|
.days-header {
|
|
position: static !important;
|
|
background: #f1f5f9 !important;
|
|
border-bottom: 1.5px solid #0f172a !important;
|
|
}
|
|
|
|
.day-col-header {
|
|
padding: 2px 2px !important;
|
|
border-right: 1px solid #cbd5e1 !important;
|
|
}
|
|
|
|
.day-col-header .day-name {
|
|
color: #0f172a !important;
|
|
font-size: 0.72rem !important;
|
|
font-weight: 800 !important;
|
|
}
|
|
|
|
.calendar-grid-scroll {
|
|
max-height: none !important;
|
|
overflow: visible !important;
|
|
height: auto !important;
|
|
}
|
|
|
|
.time-sidebar {
|
|
background: #f8fafc !important;
|
|
border-right: 1.5px solid #0f172a !important;
|
|
}
|
|
|
|
.hour-label {
|
|
color: #334155 !important;
|
|
font-weight: 700 !important;
|
|
font-size: 0.6rem !important;
|
|
border-bottom: 1px solid #e2e8f0 !important;
|
|
}
|
|
|
|
.day-column {
|
|
border-right: 1px solid #cbd5e1 !important;
|
|
background: #ffffff !important;
|
|
}
|
|
|
|
.hour-cell {
|
|
border-bottom: 1px solid #f1f5f9 !important;
|
|
}
|
|
|
|
.hour-label.compact-night {
|
|
height: 5px !important;
|
|
font-size: 0.48rem !important;
|
|
background: #f1f5f9 !important;
|
|
}
|
|
.hour-cell.compact-night {
|
|
height: 5px !important;
|
|
background: #f8fafc !important;
|
|
}
|
|
|
|
.event-block {
|
|
box-shadow: none !important;
|
|
border-radius: 3px !important;
|
|
page-break-inside: avoid !important;
|
|
break-inside: avoid !important;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Barre Supérieure -->
|
|
<header>
|
|
<div class="brand-section">
|
|
<div class="logo-badge">FG</div>
|
|
<div>
|
|
<div class="brand-title">FORGIUM CALENDAR</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toolbar-actions">
|
|
<!-- Nouvel événement -->
|
|
<button class="btn btn-primary" onclick="openCreateModal()">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
|
Nouveau créneau
|
|
</button>
|
|
|
|
<!-- Charger modèle de base -->
|
|
<button class="btn" onclick="resetDefaultTemplate()" title="Recharger le planning type avec Q1/Q2">
|
|
Recharger Modèle
|
|
</button>
|
|
|
|
<!-- Format FG Calendar -->
|
|
<button class="btn btn-cyan" onclick="exportFgCalendar()" title="Sauvegarder en fichier .fgcalendar">
|
|
Exporter .fgcalendar
|
|
</button>
|
|
|
|
<label class="btn btn-cyan" style="cursor:pointer;" title="Charger un fichier .fgcalendar">
|
|
Importer .fgcalendar
|
|
<input type="file" id="file-import" accept=".fgcalendar,.json" style="display:none;" onchange="importFgCalendar(event)">
|
|
</label>
|
|
|
|
<!-- Export PDF Téléchargement direct -->
|
|
<button class="btn btn-emerald" onclick="exportToPDF()" title="Télécharger directement l'emploi du temps en fichier PDF">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
|
Export PDF
|
|
</button>
|
|
|
|
<!-- Imprimer / Enregistrer en PDF natif vectoriel -->
|
|
<button class="btn" onclick="window.print()" title="Imprimer ou enregistrer en PDF HD vectoriel (1 page)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
|
|
Imprimer
|
|
</button>
|
|
|
|
<!-- Vider la grille -->
|
|
<button class="btn btn-danger" onclick="clearAllEvents()" title="Effacer tous les cours">
|
|
Vider
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Contenu Principal -->
|
|
<main class="calendar-wrapper" id="calendar-export-area">
|
|
|
|
<div class="calendar-info-bar">
|
|
<div style="display:flex; align-items:center; gap:0.6rem;">
|
|
<span style="font-size:1.1rem;">📅</span>
|
|
<input type="text" id="schedule-title" class="schedule-title-input" value="Emploi du Temps Hebdomadaire" oninput="saveScheduleTitle()">
|
|
</div>
|
|
<div style="display:flex; align-items:center; gap:0.6rem;">
|
|
<div class="badge-indicator">
|
|
<span>Format :</span>
|
|
<strong style="color:var(--accent-cyan);">7 Jours / 24 Heures</strong>
|
|
</div>
|
|
<div class="badge-indicator">
|
|
<span>Créneaux programmés :</span>
|
|
<strong id="events-count">0</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="calendar-container">
|
|
|
|
<!-- En-tête des 7 jours (Lundi à Dimanche) -->
|
|
<div class="days-header">
|
|
<div class="timezone-label">HEURE</div>
|
|
<div class="day-col-header" data-day="0"><div class="day-name">Lundi</div></div>
|
|
<div class="day-col-header" data-day="1"><div class="day-name">Mardi</div></div>
|
|
<div class="day-col-header" data-day="2"><div class="day-name">Mercredi</div></div>
|
|
<div class="day-col-header" data-day="3"><div class="day-name">Jeudi</div></div>
|
|
<div class="day-col-header" data-day="4"><div class="day-name">Vendredi</div></div>
|
|
<div class="day-col-header weekend" data-day="5"><div class="day-name">Samedi</div></div>
|
|
<div class="day-col-header weekend" data-day="6"><div class="day-name">Dimanche</div></div>
|
|
</div>
|
|
|
|
<!-- Grille 24 heures complète (00:00 à 23:00) -->
|
|
<div class="calendar-grid-scroll" id="grid-scroll">
|
|
<div class="time-sidebar" id="time-sidebar"></div>
|
|
|
|
<div class="day-column" id="day-col-0" data-day="0"></div>
|
|
<div class="day-column" id="day-col-1" data-day="1"></div>
|
|
<div class="day-column" id="day-col-2" data-day="2"></div>
|
|
<div class="day-column" id="day-col-3" data-day="3"></div>
|
|
<div class="day-column" id="day-col-4" data-day="4"></div>
|
|
<div class="day-column weekend" id="day-col-5" data-day="5"></div>
|
|
<div class="day-column weekend" id="day-col-6" data-day="6"></div>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Modal Création / Modification d'événement -->
|
|
<div class="modal-overlay" id="event-modal">
|
|
<div class="modal-card">
|
|
<div class="modal-header">
|
|
<div class="modal-title" id="modal-title">Nouveau créneau</div>
|
|
<button class="close-btn" onclick="closeModal()">×</button>
|
|
</div>
|
|
|
|
<form id="event-form" onsubmit="handleFormSubmit(event)">
|
|
<input type="hidden" id="event-id">
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="event-title-input">Matière / Titre *</label>
|
|
<input type="text" id="event-title-input" class="form-input" placeholder="Ex: INGENIERIE & DEVLPT DURABLE, MATHEMATIQUES..." required autofocus>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label" for="event-prof-input">Professeur(s) / Intervenant</label>
|
|
<input type="text" id="event-prof-input" class="form-input" placeholder="Ex: BOUTBHIRT Y., LARRAZET P.">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="event-salle-input">Salle / Lieu</label>
|
|
<input type="text" id="event-salle-input" class="form-input" placeholder="Ex: T209, B12, J20...">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label" for="event-day-select">Jour *</label>
|
|
<select id="event-day-select" class="form-select">
|
|
<option value="0">Lundi</option>
|
|
<option value="1">Mardi</option>
|
|
<option value="2">Mercredi</option>
|
|
<option value="3">Jeudi</option>
|
|
<option value="4">Vendredi</option>
|
|
<option value="5">Samedi</option>
|
|
<option value="6">Dimanche</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="event-qmode-select">Alternance Quinzaine *</label>
|
|
<select id="event-qmode-select" class="form-select">
|
|
<option value="">Commun (Toutes les semaines Q1 & Q2)</option>
|
|
<option value="Q1">Semaine Q1 uniquement (Impaire)</option>
|
|
<option value="Q2">Semaine Q2 uniquement (Paire)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label" for="event-start-time">Heure de début *</label>
|
|
<input type="time" id="event-start-time" class="form-input" required onchange="handleStartTimeChange()">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="event-end-time">Heure de fin *</label>
|
|
<input type="time" id="event-end-time" class="form-input" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Couleur</label>
|
|
<div class="color-picker-group">
|
|
<div class="color-dot bg-bleu-clair selected" data-color="bg-bleu-clair" onclick="selectColor('bg-bleu-clair')"></div>
|
|
<div class="color-dot bg-peche-beige" data-color="bg-peche-beige" onclick="selectColor('bg-peche-beige')"></div>
|
|
<div class="color-dot bg-bleu-gris" data-color="bg-bleu-gris" onclick="selectColor('bg-bleu-gris')"></div>
|
|
<div class="color-dot bg-vert-eau" data-color="bg-vert-eau" onclick="selectColor('bg-vert-eau')"></div>
|
|
<div class="color-dot bg-gris-tres-clair" data-color="bg-gris-tres-clair" onclick="selectColor('bg-gris-tres-clair')"></div>
|
|
<div class="color-dot bg-blanc" data-color="bg-blanc" onclick="selectColor('bg-blanc')"></div>
|
|
</div>
|
|
<input type="hidden" id="event-color" value="bg-bleu-clair">
|
|
</div>
|
|
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn" id="btn-delete-event" onclick="deleteCurrentEvent()" style="display:none; color:#fb7185; border-color:rgba(244,63,94,0.3);">
|
|
Supprimer
|
|
</button>
|
|
<div style="display:flex; gap:0.5rem; margin-left:auto;">
|
|
<button type="button" class="btn" onclick="closeModal()">Annuler</button>
|
|
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast-message">Action effectuée</div>
|
|
|
|
<!-- Logique JavaScript -->
|
|
<script>
|
|
const STORAGE_KEY = 'forgium_calendar_7j_24h_v2';
|
|
const HOUR_HEIGHT = 72; // 72px par heure normale
|
|
const NIGHT_HOURS_COUNT = 5; // 00h00 à 05h00
|
|
const NIGHT_TOTAL_HEIGHT = HOUR_HEIGHT; // Les 5 heures de nuit tiennent dans 72px (14.4px par heure)
|
|
const NIGHT_HOUR_HEIGHT = NIGHT_TOTAL_HEIGHT / NIGHT_HOURS_COUNT; // 14.4px par heure de nuit
|
|
|
|
let state = {
|
|
title: "Emploi du Temps Hebdomadaire",
|
|
events: []
|
|
};
|
|
|
|
// Modèle de base avec les cours réels, samedi et dimanche prêts, et cohabitation Q1/Q2
|
|
function getDefaultSchedule() {
|
|
return [
|
|
// LUNDI
|
|
{ id: "c1", day: 0, startTime: "07:55", endTime: "08:55", title: "HISTOIRE-GEOGRAPHIE", prof: "ELICHONDOBORDE V.", salle: "J13", qMode: "", color: "bg-blanc" },
|
|
{ id: "c2", day: 0, startTime: "08:55", endTime: "09:55", title: "HISTOIRE-GEOGRAPHIE", prof: "ELICHONDOBORDE V.", salle: "J13", qMode: "", color: "bg-blanc" },
|
|
// Lundi 10h05 - 11h05 : FRANCAIS (Q2)
|
|
{ id: "c3", day: 0, startTime: "10:05", endTime: "11:05", title: "FRANCAIS", prof: "ROUCHON C.", salle: "J20", qMode: "Q2", color: "bg-peche-beige" },
|
|
// Lundi 11h05 - 11h55 : Scission Q1 / Q2 (Français vs Anglais)
|
|
{ id: "c4", day: 0, startTime: "11:05", endTime: "11:55", title: "FRANCAIS", prof: "ROUCHON C.", salle: "J20", qMode: "Q1", color: "bg-peche-beige" },
|
|
{ id: "c5", day: 0, startTime: "11:05", endTime: "11:55", title: "ANGLAIS LVA", prof: "LASSERRE V.", salle: "B11", qMode: "Q2", color: "bg-blanc" },
|
|
// Lundi après-midi
|
|
{ id: "c6", day: 0, startTime: "15:00", endTime: "15:55", title: "ENS. TECHNO. EN LV1", prof: "BOUTBHIRT Y., LASSERRE V.", salle: "T201", qMode: "", color: "bg-peche-beige" },
|
|
{ id: "c7", day: 0, startTime: "16:00", endTime: "18:00", title: "PHYSIQ.CHIMIE&MATHS", prof: "DESTRIBOS N.", salle: "<1H-STI2D> B23 tp", qMode: "", color: "bg-blanc" },
|
|
|
|
// MARDI
|
|
{ id: "c8", day: 1, startTime: "07:55", endTime: "08:55", title: "INGENIERIE & DEVLPT DURABLE", prof: "BOUTBHIRT Y.", salle: "T209", qMode: "", color: "bg-bleu-clair" },
|
|
{ id: "c9", day: 1, startTime: "10:05", endTime: "11:55", title: "INGENIERIE & DEVLPT DURABLE", prof: "BOUTBHIRT Y., LARRAZET P.", salle: "T209", qMode: "", color: "bg-bleu-clair" },
|
|
{ id: "c10", day: 1, startTime: "11:55", endTime: "12:55", title: "INGENIERIE & DEVLPT DURABLE", prof: "LARRAZET P.", salle: "T209", qMode: "", color: "bg-bleu-clair" },
|
|
{ id: "c11", day: 1, startTime: "14:00", endTime: "15:00", title: "ANGLAIS LVA", prof: "LASSERRE V.", salle: "B11", qMode: "", color: "bg-blanc" },
|
|
{ id: "c12", day: 1, startTime: "15:00", endTime: "16:00", title: "MATHEMATIQUES", prof: "CHARLAT C.", salle: "B13", qMode: "", color: "bg-bleu-gris" },
|
|
|
|
// MERCREDI
|
|
{ id: "c13", day: 2, startTime: "08:55", endTime: "09:55", title: "PHYSIQ.CHIMIE&MATHS", prof: "DESTRIBOS N.", salle: "B22", qMode: "", color: "bg-gris-tres-clair" },
|
|
{ id: "c14", day: 2, startTime: "10:05", endTime: "11:55", title: "ED.PHYSIQUE & SPORT.", prof: "COUDRAY M., DUPIN L., PAULIEN G.", salle: "Gymnase", qMode: "", color: "bg-blanc" },
|
|
|
|
// JEUDI
|
|
{ id: "c15", day: 3, startTime: "07:55", endTime: "08:55", title: "INGENIERIE & DEVLPT DURABLE", prof: "LARRAZET P.", salle: "T209", qMode: "", color: "bg-bleu-clair" },
|
|
{ id: "c16", day: 3, startTime: "10:05", endTime: "11:55", title: "ING. & DEVLPT DURA.", prof: "BOUTBHIRT Y., LARRAZET P.", salle: "T209", qMode: "", color: "bg-bleu-clair" },
|
|
{ id: "c17", day: 3, startTime: "11:55", endTime: "12:55", title: "PHYSIQ.CHIMIE&MATHS", prof: "CHARLAT C.", salle: "B12", qMode: "", color: "bg-gris-tres-clair" },
|
|
{ id: "c18", day: 3, startTime: "14:00", endTime: "15:00", title: "MATHEMATIQUES", prof: "CHARLAT C.", salle: "B12", qMode: "", color: "bg-bleu-gris" },
|
|
{ id: "c19", day: 3, startTime: "16:00", endTime: "18:00", title: "ING. & DEVLPT DURA.", prof: "BOUTBHIRT Y., LAFARGUE E.", salle: "T105 FABLAB / T111", qMode: "", color: "bg-bleu-clair" },
|
|
|
|
// VENDREDI
|
|
{ id: "c20", day: 4, startTime: "08:55", endTime: "09:55", title: "FRANCAIS", prof: "ROUCHON C.", salle: "J20", qMode: "", color: "bg-peche-beige" },
|
|
{ id: "c21", day: 4, startTime: "10:05", endTime: "11:05", title: "ESP2 - ESPAGNOL LV2", prof: "DAUDIGNON I.", salle: "B15", qMode: "", color: "bg-blanc" },
|
|
{ id: "c22", day: 4, startTime: "14:00", endTime: "15:00", title: "MATHEMATIQUES", prof: "CHARLAT C.", salle: "B12", qMode: "", color: "bg-vert-eau" },
|
|
{ id: "c23", day: 4, startTime: "15:00", endTime: "16:00", title: "PHYSIQ.CHIMIE&MATHS", prof: "CHARLAT C.", salle: "B12", qMode: "", color: "bg-gris-tres-clair" }
|
|
];
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
loadState();
|
|
buildGrid();
|
|
renderEvents();
|
|
scrollToMorning();
|
|
});
|
|
|
|
function showToast(msg) {
|
|
const toast = document.getElementById('toast-message');
|
|
toast.textContent = msg;
|
|
toast.classList.add('show');
|
|
setTimeout(() => toast.classList.remove('show'), 2500);
|
|
}
|
|
|
|
function loadState() {
|
|
// Nettoyage / migration transparente pour écraser les anciennes données trop courtes
|
|
const saved = localStorage.getItem(STORAGE_KEY);
|
|
if (saved) {
|
|
try {
|
|
const parsed = JSON.parse(saved);
|
|
state.title = parsed.title || state.title;
|
|
state.events = parsed.events || [];
|
|
document.getElementById('schedule-title').value = state.title;
|
|
} catch(e) {
|
|
state.events = getDefaultSchedule();
|
|
saveState();
|
|
}
|
|
} else {
|
|
state.events = getDefaultSchedule();
|
|
saveState();
|
|
}
|
|
}
|
|
|
|
function saveState() {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
document.getElementById('events-count').textContent = state.events.length;
|
|
}
|
|
|
|
function saveScheduleTitle() {
|
|
state.title = document.getElementById('schedule-title').value;
|
|
saveState();
|
|
}
|
|
|
|
function resetDefaultTemplate() {
|
|
if (confirm("Recharger le modèle d'emploi du temps par défaut ?")) {
|
|
state.title = "Emploi du Temps Hebdomadaire";
|
|
document.getElementById('schedule-title').value = state.title;
|
|
state.events = getDefaultSchedule();
|
|
saveState();
|
|
renderEvents();
|
|
showToast("Modèle rechargé avec succès !");
|
|
}
|
|
}
|
|
|
|
function clearAllEvents() {
|
|
if (confirm("Voulez-vous effacer TOUS les créneaux ?")) {
|
|
state.events = [];
|
|
saveState();
|
|
renderEvents();
|
|
showToast("Planning réinitialisé à vide.");
|
|
}
|
|
}
|
|
|
|
// Construction de la grille 24 heures (00:00 à 23:00) sur 7 jours
|
|
// Les heures de nuit 00h à 05h sont compactées pour n'occuper que l'espace d'1 heure
|
|
function buildGrid() {
|
|
const sidebar = document.getElementById('time-sidebar');
|
|
sidebar.innerHTML = '';
|
|
|
|
for (let h = 0; h < 24; h++) {
|
|
const hourStr = String(h).padStart(2, '0') + ':00';
|
|
const label = document.createElement('div');
|
|
label.className = 'hour-label' + (h < 5 ? ' compact-night' : '');
|
|
label.textContent = hourStr;
|
|
sidebar.appendChild(label);
|
|
}
|
|
|
|
for (let day = 0; day < 7; day++) {
|
|
const col = document.getElementById(`day-col-${day}`);
|
|
col.innerHTML = '';
|
|
for (let h = 0; h < 24; h++) {
|
|
const cell = document.createElement('div');
|
|
cell.className = 'hour-cell' + (h < 5 ? ' compact-night' : '');
|
|
cell.dataset.hour = h;
|
|
cell.dataset.day = day;
|
|
cell.title = `Cliquer pour ajouter un créneau à ${String(h).padStart(2, '0')}:00`;
|
|
cell.addEventListener('click', (e) => {
|
|
if (e.target.closest('.event-block')) return;
|
|
openCreateModal(day, h);
|
|
});
|
|
col.appendChild(cell);
|
|
}
|
|
}
|
|
}
|
|
|
|
function timeToMinutes(timeStr) {
|
|
const [h, m] = timeStr.split(':').map(Number);
|
|
return h * 60 + m;
|
|
}
|
|
|
|
// Convertit les minutes (0 à 1440) en position verticale exacte en pixels
|
|
// De 00:00 à 05:00 (0 à 300 min), l'ensemble fait 72px (NIGHT_TOTAL_HEIGHT)
|
|
// De 05:00 à 24:00 (300 à 1440 min), chaque heure fait 72px (HOUR_HEIGHT)
|
|
function minuteToY(min) {
|
|
const NIGHT_MINUTES = 5 * 60; // 300 min
|
|
if (min <= NIGHT_MINUTES) {
|
|
return (min / NIGHT_MINUTES) * NIGHT_TOTAL_HEIGHT;
|
|
} else {
|
|
return NIGHT_TOTAL_HEIGHT + ((min - NIGHT_MINUTES) / 60) * HOUR_HEIGHT;
|
|
}
|
|
}
|
|
|
|
// Affichage des créneaux aéré et net
|
|
function renderEvents() {
|
|
document.querySelectorAll('.event-block').forEach(el => el.remove());
|
|
|
|
const dayEvents = {};
|
|
for (let d = 0; d < 7; d++) dayEvents[d] = [];
|
|
|
|
state.events.forEach(ev => {
|
|
if (dayEvents[ev.day]) dayEvents[ev.day].push(ev);
|
|
});
|
|
|
|
for (let day = 0; day < 7; day++) {
|
|
const col = document.getElementById(`day-col-${day}`);
|
|
if (!col) continue;
|
|
|
|
const events = dayEvents[day];
|
|
|
|
events.forEach(event => {
|
|
const startMin = timeToMinutes(event.startTime);
|
|
let endMin = timeToMinutes(event.endTime);
|
|
if (endMin <= startMin) endMin = startMin + 55;
|
|
|
|
const topPx = minuteToY(startMin);
|
|
const bottomPx = minuteToY(endMin);
|
|
// Hauteur proportionnelle à la zone, minimum 22px si très court
|
|
const heightPx = Math.max(bottomPx - topPx - 3, 22);
|
|
|
|
// Détecter chevauchement Q1 / Q2
|
|
const overlapping = events.filter(other => {
|
|
if (other.id === event.id) return false;
|
|
const oStart = timeToMinutes(other.startTime);
|
|
const oEnd = timeToMinutes(other.endTime);
|
|
return (startMin < oEnd && endMin > oStart);
|
|
});
|
|
|
|
const card = document.createElement('div');
|
|
card.className = `event-block ${event.color || 'bg-bleu-clair'}`;
|
|
card.style.top = `${topPx}px`;
|
|
card.style.height = `${heightPx}px`;
|
|
|
|
if (overlapping.length > 0 || event.qMode === 'Q2' || event.qMode === 'Q1') {
|
|
if (event.qMode === 'Q1') {
|
|
card.style.left = '2px';
|
|
card.style.width = 'calc(50% - 3px)';
|
|
} else if (event.qMode === 'Q2') {
|
|
card.style.left = 'calc(50% + 1px)';
|
|
card.style.width = 'calc(50% - 3px)';
|
|
} else {
|
|
card.style.left = '2px';
|
|
card.style.width = 'calc(100% - 4px)';
|
|
}
|
|
} else {
|
|
card.style.left = '2px';
|
|
card.style.width = 'calc(100% - 4px)';
|
|
}
|
|
|
|
let qBadge = '';
|
|
if (event.qMode === 'Q1') {
|
|
qBadge = `<span class="event-q-badge badge-q1">Q1</span>`;
|
|
} else if (event.qMode === 'Q2') {
|
|
qBadge = `<span class="event-q-badge badge-q2">Q2</span>`;
|
|
}
|
|
|
|
// Métadonnées (prof & salle)
|
|
let metaHtml = '';
|
|
if (event.prof || event.salle) {
|
|
metaHtml = `
|
|
<div class="event-meta">
|
|
${event.prof ? `<div class="event-prof" title="${escapeHtml(event.prof)}">${escapeHtml(event.prof)}</div>` : ''}
|
|
${event.salle ? `<div class="event-salle">${escapeHtml(event.salle)}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
card.innerHTML = `
|
|
<div class="event-top-meta">
|
|
<span class="event-time-tag">${event.startTime} - ${event.endTime}</span>
|
|
${qBadge}
|
|
</div>
|
|
<div class="event-title" title="${escapeHtml(event.title)}">${escapeHtml(event.title)}</div>
|
|
${heightPx >= 46 ? metaHtml : ''}
|
|
`;
|
|
|
|
card.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
openEditModal(event.id);
|
|
});
|
|
|
|
col.appendChild(card);
|
|
});
|
|
}
|
|
|
|
document.getElementById('events-count').textContent = state.events.length;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
}
|
|
|
|
function scrollToMorning() {
|
|
const scrollEl = document.getElementById('grid-scroll');
|
|
// Positionnement fluide pour voir immédiatement l'ensemble de la matinée dès 07h00
|
|
scrollEl.scrollTop = 0;
|
|
}
|
|
|
|
// Modal Création / Edition
|
|
function openCreateModal(defaultDay = 0, defaultHour = 8) {
|
|
document.getElementById('modal-title').textContent = "Nouveau créneau";
|
|
document.getElementById('event-id').value = '';
|
|
document.getElementById('event-title-input').value = '';
|
|
document.getElementById('event-prof-input').value = '';
|
|
document.getElementById('event-salle-input').value = '';
|
|
document.getElementById('event-day-select').value = String(defaultDay);
|
|
document.getElementById('event-qmode-select').value = '';
|
|
|
|
const startH = String(defaultHour).padStart(2, '0');
|
|
let endH = String(defaultHour + 1).padStart(2, '0');
|
|
let endM = "00";
|
|
if (defaultHour >= 23) {
|
|
endH = "23";
|
|
endM = "59";
|
|
}
|
|
|
|
document.getElementById('event-start-time').value = `${startH}:00`;
|
|
document.getElementById('event-end-time').value = `${endH}:${endM}`;
|
|
|
|
document.getElementById('btn-delete-event').style.display = 'none';
|
|
selectColor('bg-bleu-clair');
|
|
|
|
document.getElementById('event-modal').classList.add('active');
|
|
document.getElementById('event-title-input').focus();
|
|
}
|
|
|
|
// Calcul automatique de l'heure de fin conseillée (+1h) quand l'heure de début est modifiée
|
|
function handleStartTimeChange() {
|
|
const startVal = document.getElementById('event-start-time').value;
|
|
if (!startVal) return;
|
|
const [h, m] = startVal.split(':').map(Number);
|
|
let endMin = h * 60 + m + 60; // +1 heure par défaut
|
|
if (endMin >= 1440) endMin = 1439; // Max 23:59
|
|
const endH = String(Math.floor(endMin / 60)).padStart(2, '0');
|
|
const endM = String(endMin % 60).padStart(2, '0');
|
|
document.getElementById('event-end-time').value = `${endH}:${endM}`;
|
|
}
|
|
|
|
function openEditModal(id) {
|
|
const event = state.events.find(e => e.id === id);
|
|
if (!event) return;
|
|
|
|
document.getElementById('modal-title').textContent = "Modifier le cours";
|
|
document.getElementById('event-id').value = event.id;
|
|
document.getElementById('event-title-input').value = event.title;
|
|
document.getElementById('event-prof-input').value = event.prof || '';
|
|
document.getElementById('event-salle-input').value = event.salle || '';
|
|
document.getElementById('event-day-select').value = String(event.day);
|
|
document.getElementById('event-qmode-select').value = event.qMode || '';
|
|
document.getElementById('event-start-time').value = event.startTime;
|
|
document.getElementById('event-end-time').value = event.endTime;
|
|
|
|
selectColor(event.color || 'bg-bleu-clair');
|
|
document.getElementById('btn-delete-event').style.display = 'block';
|
|
|
|
document.getElementById('event-modal').classList.add('active');
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('event-modal').classList.remove('active');
|
|
}
|
|
|
|
function selectColor(colorClass) {
|
|
document.getElementById('event-color').value = colorClass;
|
|
document.querySelectorAll('.color-dot').forEach(dot => {
|
|
dot.classList.toggle('selected', dot.dataset.color === colorClass);
|
|
});
|
|
}
|
|
|
|
function handleFormSubmit(e) {
|
|
e.preventDefault();
|
|
const id = document.getElementById('event-id').value;
|
|
const title = document.getElementById('event-title-input').value.trim();
|
|
const prof = document.getElementById('event-prof-input').value.trim();
|
|
const salle = document.getElementById('event-salle-input').value.trim();
|
|
const day = parseInt(document.getElementById('event-day-select').value, 10);
|
|
const qMode = document.getElementById('event-qmode-select').value;
|
|
const startTime = document.getElementById('event-start-time').value;
|
|
let endTime = document.getElementById('event-end-time').value;
|
|
const color = document.getElementById('event-color').value;
|
|
|
|
if (!title || !startTime || !endTime) return;
|
|
|
|
let startMin = timeToMinutes(startTime);
|
|
let endMin = timeToMinutes(endTime);
|
|
|
|
// Si l'utilisateur saisit 00:00 comme fin pour un créneau de nuit qui termine la journée, le traiter comme fin de journée (24h00 = 1440 min)
|
|
if (endTime === "00:00" && startMin > 0) {
|
|
endMin = 1440;
|
|
endTime = "23:59";
|
|
}
|
|
|
|
if (endMin <= startMin) {
|
|
alert(`Attention : l'heure de fin (${endTime}) doit être après l'heure de début (${startTime}) !\nPar exemple : ${startTime} à ${String(Math.min(parseInt(startTime.split(':')[0])+1, 23)).padStart(2, '0')}:${startTime.split(':')[1] || '00'}`);
|
|
document.getElementById('event-end-time').focus();
|
|
return;
|
|
}
|
|
|
|
if (id) {
|
|
const index = state.events.findIndex(e => e.id === id);
|
|
if (index !== -1) {
|
|
state.events[index] = { id, title, prof, salle, day, qMode, startTime, endTime, color };
|
|
showToast("Créneau mis à jour !");
|
|
}
|
|
} else {
|
|
const newEvent = {
|
|
id: 'c_' + Math.random().toString(36).substr(2, 9),
|
|
title, prof, salle, day, qMode, startTime, endTime, color
|
|
};
|
|
state.events.push(newEvent);
|
|
showToast("Créneau ajouté !");
|
|
}
|
|
|
|
saveState();
|
|
renderEvents();
|
|
closeModal();
|
|
}
|
|
|
|
function deleteCurrentEvent() {
|
|
const id = document.getElementById('event-id').value;
|
|
if (!id) return;
|
|
if (confirm("Supprimer ce créneau ?")) {
|
|
state.events = state.events.filter(e => e.id !== id);
|
|
saveState();
|
|
renderEvents();
|
|
closeModal();
|
|
showToast("Créneau supprimé.");
|
|
}
|
|
}
|
|
|
|
// Export .fgcalendar
|
|
function exportFgCalendar() {
|
|
const data = {
|
|
format: "FORGIUM_CALENDAR_7J_24H",
|
|
version: "5.0",
|
|
title: state.title,
|
|
exportDate: new Date().toISOString(),
|
|
events: state.events
|
|
};
|
|
|
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = `${state.title.replace(/[^a-zA-Z0-9]/g, '_')}.fgcalendar`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
|
|
showToast("Fichier .fgcalendar exporté avec succès !");
|
|
}
|
|
|
|
// Rétrocompatibilité universelle de l'import
|
|
function importFgCalendar(e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function(evt) {
|
|
try {
|
|
const content = JSON.parse(evt.target.result);
|
|
|
|
state.title = content.title || content.student || file.name.replace(/\.(fgcalendar|json)$/i, '') || "Emploi du Temps Hebdomadaire";
|
|
|
|
const SLOT_TIMES = [
|
|
{ start: "07:55", end: "08:55" },
|
|
{ start: "08:55", end: "09:55" },
|
|
{ start: "09:55", end: "10:05" },
|
|
{ start: "10:05", end: "11:05" },
|
|
{ start: "11:05", end: "11:55" },
|
|
{ start: "11:55", end: "12:55" },
|
|
{ start: "12:55", end: "13:55" },
|
|
{ start: "13:55", end: "14:00" },
|
|
{ start: "14:00", end: "15:00" },
|
|
{ start: "15:00", end: "15:55" },
|
|
{ start: "15:55", end: "16:00" },
|
|
{ start: "16:00", end: "17:00" },
|
|
{ start: "17:00", end: "18:00" }
|
|
];
|
|
|
|
let rawItems = [];
|
|
if (Array.isArray(content.events)) {
|
|
rawItems = content.events;
|
|
} else if (Array.isArray(content.courses)) {
|
|
rawItems = content.courses;
|
|
} else if (Array.isArray(content)) {
|
|
rawItems = content;
|
|
} else {
|
|
throw new Error("Aucune liste de créneaux reconnue dans ce fichier.");
|
|
}
|
|
|
|
state.events = rawItems.map((item, index) => {
|
|
let startTime = item.startTime;
|
|
let endTime = item.endTime;
|
|
|
|
if (!startTime && typeof item.slotStart === 'number') {
|
|
const sSlot = Math.min(Math.max(0, item.slotStart), SLOT_TIMES.length - 1);
|
|
const eSlot = Math.min(Math.max(0, item.slotStart + (item.slotCount || 1) - 1), SLOT_TIMES.length - 1);
|
|
startTime = SLOT_TIMES[sSlot].start;
|
|
endTime = SLOT_TIMES[eSlot].end;
|
|
}
|
|
|
|
let qMode = item.qMode || item.quinzaine || '';
|
|
if (!qMode) {
|
|
if (item.recurrence === 'Q1') qMode = 'Q1';
|
|
else if (item.recurrence === 'Q2') qMode = 'Q2';
|
|
else if (item.week === 1) qMode = 'Q1';
|
|
else if (item.week === 2) qMode = 'Q2';
|
|
}
|
|
|
|
let color = item.color || 'bg-bleu-clair';
|
|
if (color.startsWith('color-')) {
|
|
const colorMap = {
|
|
'color-cyan': 'bg-bleu-clair',
|
|
'color-indigo': 'bg-bleu-gris',
|
|
'color-emerald': 'bg-vert-eau',
|
|
'color-amber': 'bg-peche-beige',
|
|
'color-rose': 'bg-peche-beige',
|
|
'color-purple': 'bg-gris-tres-clair',
|
|
'color-slate': 'bg-blanc'
|
|
};
|
|
color = colorMap[color] || 'bg-bleu-clair';
|
|
}
|
|
|
|
return {
|
|
id: item.id || 'c_' + Date.now() + '_' + index,
|
|
day: typeof item.day === 'number' ? item.day : 0,
|
|
startTime: startTime || "08:00",
|
|
endTime: endTime || "09:00",
|
|
title: item.title || (item.isCantine ? "PAUSE REPAS" : "Cours"),
|
|
prof: item.prof || '',
|
|
salle: item.salle || '',
|
|
qMode: qMode,
|
|
color: color
|
|
};
|
|
});
|
|
|
|
document.getElementById('schedule-title').value = state.title;
|
|
saveState();
|
|
renderEvents();
|
|
showToast(`Succès : ${state.events.length} cours chargés (compatible) !`);
|
|
} catch(err) {
|
|
alert("Erreur importation : " + err.message);
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
e.target.value = '';
|
|
}
|
|
|
|
// Export PDF fiable sur 1 page A4 Paysage
|
|
function exportToPDF() {
|
|
showToast("Préparation de l'export PDF...");
|
|
|
|
const filename = `${state.title.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`;
|
|
const exportArea = document.getElementById('calendar-export-area');
|
|
const scrollEl = document.getElementById('grid-scroll');
|
|
|
|
// 1. Sauvegarder les styles d'ascenseur
|
|
const prevMaxHeight = scrollEl.style.maxHeight;
|
|
const prevOverflow = scrollEl.style.overflow;
|
|
const prevScrollTop = scrollEl.scrollTop;
|
|
|
|
// Déplier temporairement la grille pour capturer l'ensemble des créneaux
|
|
scrollEl.style.maxHeight = 'none';
|
|
scrollEl.style.overflow = 'visible';
|
|
|
|
const opt = {
|
|
margin: [4, 4, 4, 4],
|
|
filename: filename,
|
|
image: { type: 'jpeg', quality: 0.98 },
|
|
html2canvas: {
|
|
scale: 2,
|
|
useCORS: true,
|
|
logging: false,
|
|
scrollY: 0,
|
|
scrollX: 0
|
|
},
|
|
jsPDF: { unit: 'mm', format: 'a4', orientation: 'landscape' },
|
|
pagebreak: { mode: ['avoid-all', 'css', 'legacy'] }
|
|
};
|
|
|
|
if (window.html2pdf) {
|
|
window.html2pdf().set(opt).from(exportArea).toPdf().get('pdf').then(function(pdf) {
|
|
// Si le document a créé une page blanche initiale ou superflue
|
|
const totalPages = pdf.internal.getNumberOfPages();
|
|
console.log("Pages générées :", totalPages);
|
|
}).save().then(() => {
|
|
scrollEl.style.maxHeight = prevMaxHeight;
|
|
scrollEl.style.overflow = prevOverflow;
|
|
scrollEl.scrollTop = prevScrollTop;
|
|
showToast("PDF téléchargé avec succès !");
|
|
}).catch((err) => {
|
|
scrollEl.style.maxHeight = prevMaxHeight;
|
|
scrollEl.style.overflow = prevOverflow;
|
|
scrollEl.scrollTop = prevScrollTop;
|
|
console.warn("Bascule vers impression navigateur :", err);
|
|
window.print();
|
|
});
|
|
} else {
|
|
scrollEl.style.maxHeight = prevMaxHeight;
|
|
scrollEl.style.overflow = prevOverflow;
|
|
window.print();
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|