feat(planning): connect Pronote synchronization to internal schedule
This commit is contained in:
parent
e006f4f496
commit
0e30470604
6 changed files with 115 additions and 35 deletions
|
|
@ -4,6 +4,7 @@ Pronote est traité ici comme une provenance optionnelle. Les autres services
|
||||||
ne doivent appeler que ``active_room_schedules`` ou ``room_is_available``.
|
ne doivent appeler que ``active_room_schedules`` ou ``room_is_available``.
|
||||||
"""
|
"""
|
||||||
from datetime import date, datetime, time, timedelta
|
from datetime import date, datetime, time, timedelta
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from ..models.college import RoomSchedule
|
from ..models.college import RoomSchedule
|
||||||
from ...extensions import db
|
from ...extensions import db
|
||||||
|
|
@ -110,6 +111,7 @@ def sync_pronote_schedules(room_id, entries, *, synced_at=None):
|
||||||
RoomSchedule.source == "manual",
|
RoomSchedule.source == "manual",
|
||||||
RoomSchedule.resolution_status == "active",
|
RoomSchedule.resolution_status == "active",
|
||||||
RoomSchedule.day_of_week == row.day_of_week,
|
RoomSchedule.day_of_week == row.day_of_week,
|
||||||
|
RoomSchedule.week_start == row.week_start,
|
||||||
RoomSchedule.protected_from_sync.is_(False),
|
RoomSchedule.protected_from_sync.is_(False),
|
||||||
).all()
|
).all()
|
||||||
protected_rows = RoomSchedule.query.filter(
|
protected_rows = RoomSchedule.query.filter(
|
||||||
|
|
@ -117,14 +119,22 @@ def sync_pronote_schedules(room_id, entries, *, synced_at=None):
|
||||||
RoomSchedule.source == "manual",
|
RoomSchedule.source == "manual",
|
||||||
RoomSchedule.resolution_status == "active",
|
RoomSchedule.resolution_status == "active",
|
||||||
RoomSchedule.day_of_week == row.day_of_week,
|
RoomSchedule.day_of_week == row.day_of_week,
|
||||||
|
RoomSchedule.week_start == row.week_start,
|
||||||
RoomSchedule.protected_from_sync.is_(True),
|
RoomSchedule.protected_from_sync.is_(True),
|
||||||
).all()
|
).all()
|
||||||
|
candidates = []
|
||||||
for manual in manual_rows:
|
for manual in manual_rows:
|
||||||
if (manual.subject and row.subject and manual.subject != row.subject) or (manual.class_name and row.class_name and manual.class_name != row.class_name):
|
if (manual.subject and row.subject and manual.subject != row.subject) or (manual.class_name and row.class_name and manual.class_name != row.class_name):
|
||||||
continue
|
continue
|
||||||
if _overlaps(manual.start_time, manual.end_time, row.start_time, row.end_time):
|
if _overlaps(manual.start_time, manual.end_time, row.start_time, row.end_time):
|
||||||
manual.resolution_status = "superseded"
|
candidates.append(manual)
|
||||||
break
|
if len(candidates) == 1:
|
||||||
|
candidates[0].resolution_status = "superseded"
|
||||||
|
elif len(candidates) > 1:
|
||||||
|
note = "Correspondance Pronote ambiguë : validation nécessaire"
|
||||||
|
row.conflict_note = note
|
||||||
|
for manual in candidates:
|
||||||
|
manual.conflict_note = note
|
||||||
for manual in protected_rows:
|
for manual in protected_rows:
|
||||||
if _overlaps(manual.start_time, manual.end_time, row.start_time, row.end_time):
|
if _overlaps(manual.start_time, manual.end_time, row.start_time, row.end_time):
|
||||||
note = "Conflit avec un créneau manuel protégé"
|
note = "Conflit avec un créneau manuel protégé"
|
||||||
|
|
@ -135,3 +145,41 @@ def sync_pronote_schedules(room_id, entries, *, synced_at=None):
|
||||||
if row.external_id not in seen:
|
if row.external_id not in seen:
|
||||||
row.resolution_status = "disabled"
|
row.resolution_status = "disabled"
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_pronote_lessons(lessons, week_start):
|
||||||
|
"""Transforme la sortie du client Pronote en entrées stables.
|
||||||
|
|
||||||
|
Le client historique ne fournit pas toujours d'identifiant de cours. Dans
|
||||||
|
ce cas, un identifiant déterministe est calculé à partir des attributs
|
||||||
|
visibles, ce qui permet de remettre à jour la même ligne sans doublon.
|
||||||
|
"""
|
||||||
|
entries = []
|
||||||
|
for lesson in lessons or []:
|
||||||
|
start_raw, end_raw = lesson.get("start_time"), lesson.get("end_time")
|
||||||
|
try:
|
||||||
|
start_time = time.fromisoformat(str(start_raw))
|
||||||
|
end_time = time.fromisoformat(str(end_raw))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
day_of_week = int(lesson.get("day_of_week", 0))
|
||||||
|
subject = (lesson.get("subject") or "").strip() or None
|
||||||
|
teacher = (lesson.get("teacher") or "").strip() or None
|
||||||
|
class_name = (lesson.get("class_name") or "").strip() or None
|
||||||
|
stable = lesson.get("external_id") or "|".join(str(value or "") for value in (
|
||||||
|
week_start, day_of_week, start_time, end_time, subject, teacher, class_name
|
||||||
|
))
|
||||||
|
external_id = hashlib.sha256(str(stable).encode("utf-8")).hexdigest()[:64]
|
||||||
|
entries.append({
|
||||||
|
"external_id": external_id,
|
||||||
|
"week_start": week_start,
|
||||||
|
"day_of_week": day_of_week,
|
||||||
|
"start_time": start_time,
|
||||||
|
"end_time": end_time,
|
||||||
|
"subject": subject,
|
||||||
|
"teacher": teacher,
|
||||||
|
"class_name": class_name,
|
||||||
|
"course_name": subject or class_name or "Cours",
|
||||||
|
"event_type": "cours",
|
||||||
|
})
|
||||||
|
return entries
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ def get_all_salles(client) -> list:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def get_lessons_for_room(client, room_name: str, date_start, date_end) -> list:
|
def get_lessons_for_room(client, room_name: str, date_start, date_end, *, strict=False) -> list:
|
||||||
"""Récupère les cours pour une salle donnée en utilisant l'API directe."""
|
"""Récupère les cours pour une salle donnée en utilisant l'API directe."""
|
||||||
try:
|
try:
|
||||||
# 1. Récupérer les salles
|
# 1. Récupérer les salles
|
||||||
|
|
@ -313,6 +313,8 @@ def get_lessons_for_room(client, room_name: str, date_start, date_end) -> list:
|
||||||
})
|
})
|
||||||
except Exception as api_err:
|
except Exception as api_err:
|
||||||
logger.warning(f"Erreur API pour {room_name} le {target_date}: {api_err}")
|
logger.warning(f"Erreur API pour {room_name} le {target_date}: {api_err}")
|
||||||
|
if strict:
|
||||||
|
raise
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 4. Parser les cours
|
# 4. Parser les cours
|
||||||
|
|
@ -367,6 +369,8 @@ def get_lessons_for_room(client, room_name: str, date_start, date_end) -> list:
|
||||||
logger.error(f"Erreur récupération cours pour {room_name}: {e}")
|
logger.error(f"Erreur récupération cours pour {room_name}: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
if strict:
|
||||||
|
raise
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,17 @@ def room_schedule_protect(id):
|
||||||
return redirect(url_for('planning.room_schedules', room_id=schedule.room_id))
|
return redirect(url_for('planning.room_schedules', room_id=schedule.room_id))
|
||||||
|
|
||||||
|
|
||||||
|
@planning_bp.route('/rooms/<int:id>/disable', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def room_schedule_disable(id):
|
||||||
|
"""Désactive explicitement un créneau sans supprimer son historique."""
|
||||||
|
schedule = RoomSchedule.query.get_or_404(id)
|
||||||
|
schedule.resolution_status = 'disabled'
|
||||||
|
db.session.commit()
|
||||||
|
flash('Créneau désactivé ; son historique est conservé.', 'success')
|
||||||
|
return redirect(url_for('planning.room_schedules', room_id=schedule.room_id))
|
||||||
|
|
||||||
|
|
||||||
@planning_bp.route('/')
|
@planning_bp.route('/')
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends "base.html" %}{% block title %}Planning des salles{% endblock %}{% block content %}
|
{% extends "base.html" %}{% block title %}Planning des salles{% endblock %}{% block content %}
|
||||||
<div class="container-fluid main-content"><div class="d-flex justify-content-between align-items-center mb-3"><div><h1 class="h3">Planning interne des salles</h1><p class="text-muted">Les créneaux restent utilisables sans Pronote. Leur provenance est indiquée pour vous aider à les comprendre.</p></div><a class="btn btn-primary" href="{{ url_for('planning.room_schedule_new') }}"><i class="bi bi-plus-lg"></i> Ajouter un créneau</a></div>
|
<div class="container-fluid main-content"><div class="d-flex justify-content-between align-items-center mb-3"><div><h1 class="h3">Planning interne des salles</h1><p class="text-muted">Les créneaux restent utilisables sans Pronote. Leur provenance est indiquée pour vous aider à les comprendre.</p></div><a class="btn btn-primary" href="{{ url_for('planning.room_schedule_new') }}"><i class="bi bi-plus-lg"></i> Ajouter un créneau</a></div>
|
||||||
<form class="row g-2 mb-3"><div class="col-md-6"><label class="form-label" for="room_id">Salle</label><select id="room_id" name="room_id" class="form-select"><option value="">Choisir une salle</option>{% for room in rooms %}<option value="{{ room.id }}" {% if selected_room_id == room.id %}selected{% endif %}>{{ room.full_name }}</option>{% endfor %}</select></div><div class="col-md-2 align-self-end"><button class="btn btn-outline-primary">Afficher</button></div></form>
|
<form class="row g-2 mb-3"><div class="col-md-6"><label class="form-label" for="room_id">Salle</label><select id="room_id" name="room_id" class="form-select"><option value="">Choisir une salle</option>{% for room in rooms %}<option value="{{ room.id }}" {% if selected_room_id == room.id %}selected{% endif %}>{{ room.full_name }}</option>{% endfor %}</select></div><div class="col-md-2 align-self-end"><button class="btn btn-outline-primary">Afficher</button></div></form>
|
||||||
{% if selected_room_id %}<div class="table-responsive table-responsive-stack"><table class="table"><thead><tr><th>Jour</th><th>Horaires</th><th>Libellé</th><th>Origine</th><th>Protection</th></tr></thead><tbody>{% for item in schedules %}<tr><td data-label="Jour">{{ item.day_of_week }}</td><td data-label="Horaires">{{ item.start_time }}–{{ item.end_time }}</td><td data-label="Libellé">{{ item.course_name or item.subject or item.event_type }}</td><td data-label="Origine">{% if item.source == 'pronote' %}Mis à jour par Pronote{% elif item.source == 'import' %}Importé{% else %}Saisi manuellement{% endif %}</td><td data-label="Protection">{% if item.protected_from_sync %}Protégé des mises à jour automatiques{% else %}Peut être repris par Pronote{% endif %}</td></tr>{% endfor %}{% if not schedules %}<tr><td colspan="5">Aucun créneau actif pour cette salle.</td></tr>{% endif %}</tbody></table></div>{% endif %}</div>{% endblock %}
|
{% if selected_room_id %}<div class="table-responsive table-responsive-stack"><table class="table"><thead><tr><th>Jour</th><th>Horaires</th><th>Libellé</th><th>Origine</th><th>Protection</th><th>Action</th></tr></thead><tbody>{% for item in schedules %}<tr><td data-label="Jour">{{ item.day_of_week }}</td><td data-label="Horaires">{{ item.start_time }}–{{ item.end_time }}</td><td data-label="Libellé">{{ item.course_name or item.subject or item.event_type }}{% if item.conflict_note %}<br><span class="badge bg-warning text-dark">Conflit à résoudre</span><br><small>{{ item.conflict_note }}</small>{% endif %}</td><td data-label="Origine">{% if item.source == 'pronote' %}Mis à jour par Pronote{% elif item.source == 'import' %}Importé{% else %}Saisi manuellement{% endif %}</td><td data-label="Protection">{% if item.protected_from_sync %}Conservé lors des synchronisations{% else %}Peut être repris par Pronote{% endif %}</td><td data-label="Action"><form method="post" action="{{ url_for('planning.room_schedule_protect', id=item.id) }}" class="d-inline"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><input type="hidden" name="protected_from_sync" value="{% if not item.protected_from_sync %}on{% endif %}"><button class="btn btn-sm btn-outline-secondary">{% if item.protected_from_sync %}Déprotéger{% else %}Protéger{% endif %}</button></form>{% if item.source == 'pronote' %}<form method="post" action="{{ url_for('planning.room_schedule_disable', id=item.id) }}" class="d-inline"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="btn btn-sm btn-outline-danger">Désactiver</button></form>{% endif %}</td></tr>{% endfor %}{% if not schedules %}<tr><td colspan="6">Aucun créneau actif pour cette salle.</td></tr>{% endif %}</tbody></table></div>{% endif %}</div>{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -149,22 +149,10 @@ def room_planning(room_id):
|
||||||
client = get_client()
|
client = get_client()
|
||||||
pronote_connected = client is not None
|
pronote_connected = client is not None
|
||||||
|
|
||||||
# Récupérer les cours depuis PRONOTE si connecté
|
# La page ne consulte pas Pronote pour calculer l'occupation : elle
|
||||||
|
# affiche uniquement la vue interne résolue. Pronote est une source
|
||||||
|
# facultative, déclenchée par l'import explicite ci-dessous.
|
||||||
pronote_by_day = {}
|
pronote_by_day = {}
|
||||||
if client and pronote_connected:
|
|
||||||
from app_new.lib_ext.pronote_client import get_lessons_for_room
|
|
||||||
lessons = get_lessons_for_room(client, room.name, monday, week_end)
|
|
||||||
# Grouper par jour (les données sont déjà groupées par 'day')
|
|
||||||
for lesson in lessons:
|
|
||||||
lesson_day = lesson.get('day_of_week', lesson.get('day', 0))
|
|
||||||
if lesson_day not in pronote_by_day:
|
|
||||||
pronote_by_day[lesson_day] = []
|
|
||||||
pronote_by_day[lesson_day].append({
|
|
||||||
'heure': lesson.get('start_time', lesson.get('heure', '')),
|
|
||||||
'matiere': lesson.get('subject', lesson.get('matiere', '')),
|
|
||||||
'prof': lesson.get('teacher', lesson.get('prof', '')),
|
|
||||||
'classe': lesson.get('class_name', lesson.get('classe', ''))
|
|
||||||
})
|
|
||||||
|
|
||||||
# Récupérer les cours importés depuis la base de données
|
# Récupérer les cours importés depuis la base de données
|
||||||
# Pronote reste une source facultative ; l'affichage des créneaux internes
|
# Pronote reste une source facultative ; l'affichage des créneaux internes
|
||||||
|
|
@ -183,22 +171,49 @@ def room_planning(room_id):
|
||||||
'end_time': schedule.end_time.strftime('%H:%M') if schedule.end_time else '',
|
'end_time': schedule.end_time.strftime('%H:%M') if schedule.end_time else '',
|
||||||
'subject': schedule.subject or '',
|
'subject': schedule.subject or '',
|
||||||
'teacher': schedule.teacher or '',
|
'teacher': schedule.teacher or '',
|
||||||
'class_name': schedule.class_name or ''
|
'class_name': schedule.class_name or '',
|
||||||
|
'source': schedule.source,
|
||||||
|
'protected': schedule.protected_from_sync,
|
||||||
|
'conflict_note': schedule.conflict_note,
|
||||||
|
'last_synced_at': schedule.last_synced_at,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
last_sync_at = max((item['last_synced_at'] for values in db_by_day.values() for item in values
|
||||||
|
if item['last_synced_at']), default=None)
|
||||||
|
|
||||||
return render_template('pronote/room_planning.html', room=room, week_start=monday, week_end=week_end,
|
return render_template('pronote/room_planning.html', room=room, week_start=monday, week_end=week_end,
|
||||||
prev_week=monday - timedelta(days=7), next_week=monday + timedelta(days=7),
|
prev_week=monday - timedelta(days=7), next_week=monday + timedelta(days=7),
|
||||||
today=date.today(), pronote_connected=pronote_connected, pronote_by_day=pronote_by_day,
|
today=date.today(), pronote_connected=pronote_connected, pronote_by_day=pronote_by_day,
|
||||||
db_by_day=db_by_day, timedelta=timedelta)
|
db_by_day=db_by_day, last_sync_at=last_sync_at, timedelta=timedelta)
|
||||||
|
|
||||||
|
|
||||||
@pronote_bp.route('/planning/<int:room_id>/import', methods=['POST'])
|
@pronote_bp.route('/planning/<int:room_id>/import', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def import_room_planning(room_id):
|
def import_room_planning(room_id):
|
||||||
"""Importer l'emploi du temps d'une salle depuis Pronote."""
|
"""Importer l'emploi du temps d'une salle depuis Pronote."""
|
||||||
|
from app_new.lib_ext.pronote_client import get_client, get_lessons_for_room
|
||||||
|
from app_new.core.services.room_planning import normalize_pronote_lessons, sync_pronote_schedules
|
||||||
room = Room.query.get_or_404(room_id)
|
room = Room.query.get_or_404(room_id)
|
||||||
flash(f'Import planning pour {room.name} non implémenté.', 'info')
|
client = get_client()
|
||||||
|
if not client:
|
||||||
|
flash('Pronote n’est pas connecté : le planning interne reste disponible.', 'warning')
|
||||||
return redirect(url_for('pronote.room_planning', room_id=room_id))
|
return redirect(url_for('pronote.room_planning', room_id=room_id))
|
||||||
|
week_value = request.form.get('week') or request.args.get('week') or date.today().isoformat()
|
||||||
|
try:
|
||||||
|
monday = date.fromisoformat(week_value)
|
||||||
|
monday -= timedelta(days=monday.weekday())
|
||||||
|
except ValueError:
|
||||||
|
monday = date.today() - timedelta(days=date.today().weekday())
|
||||||
|
try:
|
||||||
|
lessons = get_lessons_for_room(client, room.name, monday, monday + timedelta(days=6), strict=True)
|
||||||
|
entries = normalize_pronote_lessons(lessons, monday)
|
||||||
|
sync_pronote_schedules(room.id, entries)
|
||||||
|
flash(f'{len(entries)} créneaux Pronote synchronisés. Les protections locales ont été conservées.', 'success')
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
logger.exception('Synchronisation Pronote impossible pour la salle %s', room.id)
|
||||||
|
flash('Pronote est momentanément indisponible. Les derniers créneaux connus sont conservés.', 'warning')
|
||||||
|
return redirect(url_for('pronote.room_planning', room_id=room_id, week=monday.isoformat()))
|
||||||
|
|
||||||
|
|
||||||
@pronote_bp.route('/disconnect', methods=['POST'])
|
@pronote_bp.route('/disconnect', methods=['POST'])
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
<div>
|
<div>
|
||||||
{% if room.room_type and room.room_type.is_teaching %}
|
{% if room.room_type and room.room_type.is_teaching %}
|
||||||
<form method="POST" action="{{ url_for('pronote.import_room_planning', room_id=room.id) }}" style="display:inline;">
|
<form method="POST" action="{{ url_for('pronote.import_room_planning', room_id=room.id) }}" style="display:inline;">
|
||||||
|
<input type="hidden" name="week" value="{{ week_start.isoformat() }}">
|
||||||
<button type="submit" class="btn btn-outline-success">
|
<button type="submit" class="btn btn-outline-success">
|
||||||
<i class="bi bi-download"></i> Importer 3 semaines depuis Pronote
|
<i class="bi bi-download"></i> Importer 3 semaines depuis Pronote
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -57,12 +58,12 @@
|
||||||
|
|
||||||
<!-- Comparaison planning -->
|
<!-- Comparaison planning -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Planning GMAO (importé par watchdog) -->
|
<!-- Planning interne résolu -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header bg-light">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="bi bi-database"></i> Planning importé (Watchdog)
|
<i class="bi bi-database"></i> Planning interne résolu
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|
@ -75,7 +76,7 @@
|
||||||
{% set day_date = week_start + timedelta(days=day_num) %}
|
{% set day_date = week_start + timedelta(days=day_num) %}
|
||||||
<thead class="table-success">
|
<thead class="table-success">
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="4" style="background-color: #d1e7dd;">
|
<th colspan="5" style="background-color: #d1e7dd;">
|
||||||
<strong>{{ days[day_num] }} {{ day_date.strftime('%d/%m') }}</strong>
|
<strong>{{ days[day_num] }} {{ day_date.strftime('%d/%m') }}</strong>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -88,11 +89,12 @@
|
||||||
<td>{{ s.subject or '-' }}</td>
|
<td>{{ s.subject or '-' }}</td>
|
||||||
<td>{{ s.teacher or '-' }}</td>
|
<td>{{ s.teacher or '-' }}</td>
|
||||||
<td><span class="badge bg-secondary">{{ s.class_name or '-' }}</span></td>
|
<td><span class="badge bg-secondary">{{ s.class_name or '-' }}</span></td>
|
||||||
|
<td><small>{% if s.source == 'pronote' %}Mis à jour par Pronote{% elif s.source == 'import' %}Importé{% else %}Saisi manuellement{% endif %}</small>{% if s.protected %}<br><small class="text-success">Conservé lors des synchronisations</small>{% endif %}{% if s.conflict_note %}<br><span class="badge bg-warning text-dark">Conflit à résoudre</span><br><small>{{ s.conflict_note }}</small>{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr class="table-light">
|
<tr class="table-light">
|
||||||
<td colspan="4" class="text-center text-muted">
|
<td colspan="5" class="text-center text-muted">
|
||||||
<em>Pas de cours</em>
|
<em>Pas de cours</em>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -104,20 +106,20 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="p-3 text-center text-muted">
|
<div class="p-3 text-center text-muted">
|
||||||
<i class="bi bi-inbox"></i><br>
|
<i class="bi bi-inbox"></i><br>
|
||||||
Aucun planning importé.<br>
|
Aucun créneau interne pour cette semaine.<br>
|
||||||
<small>Le watchdog PRONOTE synchronise automatiquement les plannings.</small>
|
<small>Vous pouvez saisir des créneaux manuellement sans Pronote.</small>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Planning PRONOTE (direct) -->
|
<!-- État de la source Pronote -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="bi bi-wifi"></i> Planning PRONOTE (direct)
|
<i class="bi bi-wifi"></i> Source Pronote
|
||||||
</h5>
|
</h5>
|
||||||
{% if pronote_connected %}
|
{% if pronote_connected %}
|
||||||
<span class="badge bg-success">Connecté</span>
|
<span class="badge bg-success">Connecté</span>
|
||||||
|
|
@ -164,9 +166,9 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="p-3 text-center text-muted">
|
<div class="p-3 text-center text-muted">
|
||||||
<i class="bi bi-exclamation-triangle"></i><br>
|
<i class="bi bi-exclamation-triangle"></i><br>
|
||||||
Connexion à PRONOTE impossible.
|
Pronote n’est pas configuré ou momentanément indisponible.
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<small>Vérifiez la configuration du client PRONOTE.</small>
|
<small>Le planning interne reste utilisable et conserve les dernières données connues.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue