Fusionner les plannings dans une vue unifiée
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run

This commit is contained in:
root 2026-08-19 23:15:34 +00:00
parent 4f8237aba9
commit f0c3dafd25
6 changed files with 102 additions and 96 deletions

View file

@ -251,9 +251,10 @@ class PlanningService:
return schedule
# Récupérer les interventions planifiées pour ce jour
scheduled_tasks = ScheduledTask.query.filter_by(
scheduled_date=d,
status='planned'
scheduled_tasks = ScheduledTask.query.filter(
ScheduledTask.scheduled_date == d,
ScheduledTask.status.in_(('planned', 'in_progress', 'suspended', 'postponed')),
ScheduledTask.intervention_id.is_(None),
).all()
for task in scheduled_tasks:
@ -277,23 +278,26 @@ class PlanningService:
}
schedule.items.append(item)
# Récupérer les interventions curatives en cours
interventions = Intervention.query.filter_by(
scheduled_date=d,
type='curatif'
# Les interventions préventives et curatives appartiennent au même
# planning. Les échéances déjà converties sont représentées ici par
# leur intervention, sans doublon avec ScheduledTask.
interventions = Intervention.query.filter(
Intervention.scheduled_date == d,
Intervention.is_deleted.is_(False),
~Intervention.status.in_(('terminee', 'cloturee', 'annulee', 'ignoree')),
).all()
for interv in interventions:
item = {
'id': interv.id,
'type': 'curative',
'type': 'curative' if interv.type == 'curatif' else 'preventive',
'title': interv.title,
'start_time': None,
'end_time': None,
'duration': None,
'room_id': None,
'room_name': interv.room.name if interv.room else None,
'status': interv.status
'status': interv.status,
}
schedule.items.append(item)

View file

@ -12,18 +12,11 @@ planning_bp = Blueprint("interventions_planning", __name__, url_prefix="/interve
@planning_bp.route('/planning')
@login_required
def planning():
"""Planning des interventions préventives."""
from datetime import datetime, timezone, timedelta
# Recuperer les interventions preventives planifiees
preventive_interventions = Intervention.query.filter(
Intervention.type == 'preventif',
Intervention.scheduled_date != None
).order_by(Intervention.scheduled_date).all()
return render_template('interventions/planning.html',
interventions=preventive_interventions,
stats={'total': len(preventive_interventions), 'en_attente': 0, 'en_cours': 0, 'terminee': 0})
"""Compatibilité : redirige vers le planning unifié."""
params = {}
if request.args.get('year', type=int):
params['year'] = request.args.get('year', type=int)
return redirect(url_for('planning.index', **params))
@planning_bp.route('/<int:id>/delete', methods=['POST'])

View file

@ -28,7 +28,9 @@ def index():
# Récupérer l'année demandée ou année courante
year = request.args.get('year', date.today().year, type=int)
# Préparer les données du calendrier
# Préparer les données du calendrier. Le calendrier principal est la
# source unique : il rassemble les interventions et les échéances
# préventives qui n'ont pas encore été converties en intervention.
months_data = []
months_names = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
@ -52,6 +54,59 @@ def index():
TechnicianAvailability.date <= date(year, 12, 31)
).all()
leave_dates = {l.date: l for l in leaves}
year_start = date(year, 1, 1)
year_end = date(year, 12, 31)
terminal_intervention_statuses = ('terminee', 'cloturee', 'annulee', 'ignoree')
interventions = Intervention.query.filter(
Intervention.scheduled_date >= year_start,
Intervention.scheduled_date <= year_end,
Intervention.is_deleted.is_(False),
~Intervention.status.in_(terminal_intervention_statuses),
).order_by(Intervention.scheduled_date, Intervention.scheduled_start).all()
scheduled_tasks = ScheduledTask.query.filter(
ScheduledTask.scheduled_date >= year_start,
ScheduledTask.scheduled_date <= year_end,
ScheduledTask.status.in_(('planned', 'in_progress', 'suspended', 'postponed')),
# Une échéance convertie est déjà représentée par son intervention.
ScheduledTask.intervention_id.is_(None),
).order_by(ScheduledTask.scheduled_date, ScheduledTask.scheduled_start).all()
events_by_date = {}
for intervention in interventions:
events_by_date.setdefault(intervention.scheduled_date, []).append({
'title': intervention.title or 'Intervention',
'type': intervention.type or 'curatif',
'status': intervention.status,
'start_time': intervention.scheduled_start,
'end_time': intervention.scheduled_end,
'duration': intervention.estimated_duration,
'room_name': intervention.room.name if intervention.room else None,
'url': url_for('interventions.detail', id=intervention.id),
'source': 'Intervention',
'color': 'danger' if intervention.type == 'curatif' else 'primary',
})
for task in scheduled_tasks:
title = 'Tâche préventive'
if task.lot_task and task.lot_task.tache:
title = task.lot_task.tache
elif task.preventive_task and task.preventive_task.name:
title = task.preventive_task.name
elif task.equipment:
title = f'Intervention sur {task.equipment.name}'
room = task.room or (task.equipment.effective_room if task.equipment else None)
events_by_date.setdefault(task.scheduled_date, []).append({
'title': title,
'type': 'preventive',
'status': task.status,
'start_time': task.scheduled_start,
'end_time': task.scheduled_end,
'duration': task.estimated_duration,
'room_name': room.name if room else None,
'url': url_for('planning.scheduled_detail', id=task.id),
'source': 'Échéance préventive',
'color': 'info',
})
for month in range(1, 13):
# Nombre de jours dans le mois
@ -76,7 +131,8 @@ def index():
'is_vacation': vacation is not None,
'vacation_name': vacation,
'is_leave': leave is not None,
'leave_type': leave.availability_type if leave else None
'leave_type': leave.availability_type if leave else None,
'events': events_by_date.get(d, []),
})
# Si on atteint dimanche (fin de semaine), nouvelle semaine
@ -94,8 +150,8 @@ def index():
# Statistiques
stats = {
'preventive_tasks': ScheduledTask.query.filter_by(status='planned').count(),
'curative_interventions': Intervention.query.filter_by(type='curatif', status='en_cours').count(),
'preventive_tasks': len(scheduled_tasks),
'curative_interventions': sum(1 for i in interventions if i.type == 'curatif'),
'admin_tasks': AdminTask.query.filter_by(is_active=True).count(),
'pending_items': PlanningItem.query.filter_by(status='planned').count(),
}
@ -124,9 +180,10 @@ def day(date_str):
schedule = PlanningService.get_day_schedule(d)
# Récupérer les tâches planifiées pour ce jour
scheduled_tasks = ScheduledTask.query.filter_by(
scheduled_date=d,
status='planned'
scheduled_tasks = ScheduledTask.query.filter(
ScheduledTask.scheduled_date == d,
ScheduledTask.status.in_(('planned', 'in_progress', 'suspended', 'postponed')),
ScheduledTask.intervention_id.is_(None),
).all()
return render_template('planning/day.html',

View file

@ -145,67 +145,8 @@ def api_overdue_count():
@scheduler_bp.route('/calendar')
@login_required
def calendar():
"""Vue calendrier mensuel."""
from datetime import timedelta
import calendar as cal_module
month = request.args.get('month', date.today().month, type=int)
year = request.args.get('year', date.today().year, type=int)
cal_obj = cal_module.Calendar(firstweekday=0)
month_days = cal_obj.monthdays2calendar(year, month)
first_day = date(year, month, 1)
if month == 12:
last_day = date(year, month, 31)
else:
last_day = date(year, month + 1, 1) - timedelta(days=1)
# Toutes les interventions non terminees du mois
interventions = Intervention.query.filter(
Intervention.scheduled_date >= first_day,
Intervention.scheduled_date <= last_day,
Intervention.is_deleted == False,
~Intervention.status.in_(['terminee', 'cloturee', 'annulee'])
).all()
# Organiser par jour
items_by_day = {}
for i in interventions:
if i.scheduled_date:
items_by_day.setdefault(i.scheduled_date.day, []).append({
'title': i.title[:25] if i.title else 'Intervention',
'start_time': i.scheduled_start.strftime('%H:%M') if i.scheduled_start else '',
'color': 'danger' if i.type == 'curatif' else 'primary',
})
# Construire les semaines
weeks = []
today = date.today()
for week in month_days:
week_days = []
for day_num, day_week in week:
if day_num == 0:
week_days.append(None)
else:
week_days.append({
'day': day_num,
'is_today': date(year, month, day_num) == today,
'items': items_by_day.get(day_num, [])
})
weeks.append(week_days)
prev_month = date(year, month, 1) - timedelta(days=1)
next_month = date(year, month, 1) + timedelta(days=31)
month_names = ['', 'Janvier', 'Fevrier', 'Mars', 'Avril', 'Mai', 'Juin',
'Juillet', 'Aout', 'Septembre', 'Octobre', 'Novembre', 'Decembre']
return render_template('scheduler/calendar.html',
weeks=weeks,
month=month, year=year,
month_name=month_names[month],
prev_month=prev_month,
next_month=next_month)
"""Compatibilité : le calendrier est désormais celui de /planning/."""
return redirect(url_for('planning.index', year=request.args.get('year', date.today().year, type=int)))
@scheduler_bp.route('/api/daily-summary')
@ -258,4 +199,4 @@ def api_daily_summary():
'equipment': t.equipment.name if t.equipment else '',
'room': t.room.name if t.room else '',
'duration': t.estimated_duration or 60} for t in today_tasks]
})
})

View file

@ -181,9 +181,7 @@
</a>
<ul class="dropdown-menu dropdown-menu-dark" style="background:#34495e;">
<li><a class="dropdown-item" href="{{ url_for('interventions.index') }}"><i class="bi bi-list-ul"></i> Toutes les interventions</a></li>
<li><a class="dropdown-item" href="{{ url_for('interventions_planning.planning') }}"><i class="bi bi-calendar3"></i> Planning interventions</a></li>
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
<li><a class="dropdown-item" href="{{ url_for('planning.index') }}"><i class="bi bi-calendar-check"></i> Tableau planning</a></li>
<li><a class="dropdown-item" href="{{ url_for('planning.index') }}"><i class="bi bi-calendar-check"></i> Planning unifié</a></li>
<li><a class="dropdown-item" href="{{ url_for('planning.tasks') }}"><i class="bi bi-list-check"></i> Tâches préventives</a></li>
<li><a class="dropdown-item" href="{{ url_for('planning.scheduled') }}"><i class="bi bi-calendar-week"></i> Tâches planifiées</a></li>
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
@ -200,8 +198,7 @@
<li><a class="dropdown-item" href="{{ url_for('exports.export_interventions') }}"><i class="bi bi-file-earmark-pdf"></i> Export interventions</a></li>
<li><a class="dropdown-item" href="{{ url_for('exports.export_costs_csv') }}"><i class="bi bi-cash-stack"></i> Export coûts et stock</a></li>
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
<li><a class="dropdown-item" href="{{ url_for('scheduler.index') }}"><i class="bi bi-calendar-week"></i> Planificateur auto</a></li>
<li><a class="dropdown-item" href="{{ url_for('scheduler.calendar') }}"><i class="bi bi-calendar3"></i> Calendrier</a></li>
<li><a class="dropdown-item" href="{{ url_for('scheduler.index') }}"><i class="bi bi-calendar-week"></i> Génération automatique</a></li>
</ul>
</li>
<li class="nav-item dropdown">

View file

@ -62,6 +62,8 @@
<span><span class="badge bg-secondary">Week-end</span></span>
<span><span class="badge bg-dark">Vacances</span></span>
<span><span class="badge bg-warning text-dark">Congé</span></span>
<span><span class="badge bg-primary">Intervention</span></span>
<span><span class="badge bg-info text-dark">Échéance préventive</span></span>
</div>
</div>
</div>
@ -107,6 +109,18 @@
title="{{ day_info.vacation_name if day_info.vacation_name else '' }}">
{{ day_info.day }}
</a>
{% if day_info.events %}
<div class="mt-1 d-flex flex-column gap-1">
{% for event in day_info.events[:3] %}
<a href="{{ event.url }}" class="badge bg-{{ event.color }} text-truncate text-decoration-none" title="{{ event.title }}{% if event.room_name %} — {{ event.room_name }}{% endif %}">
{% if event.start_time %}{{ event.start_time.strftime('%H:%M') }} {% endif %}{{ event.title[:18] }}
</a>
{% endfor %}
{% if day_info.events|length > 3 %}
<a href="{{ url_for('planning.day', date_str=date_str) }}" class="small text-decoration-none">+{{ day_info.events|length - 3 }} autre(s)</a>
{% endif %}
</div>
{% endif %}
</td>
{% endif %}
{% endfor %}
@ -120,4 +134,4 @@
{% endfor %}
</div>
</div>
{% endblock %}
{% endblock %}