261 lines
9.2 KiB
Python
261 lines
9.2 KiB
Python
|
|
"""
|
||
|
|
Routes du planificateur - GMAO College
|
||
|
|
Permet de lancer le planificateur manuellement et de voir les resultats.
|
||
|
|
FUSION: ScheduledTask est maintenant integre dans Intervention (statut 'planifiee').
|
||
|
|
"""
|
||
|
|
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||
|
|
from flask_login import login_required
|
||
|
|
from ..extensions import db
|
||
|
|
from ..scheduler.engine import run_scheduler, get_due_tasks, group_by_zone
|
||
|
|
from ..core.models.maintenance import Intervention
|
||
|
|
from datetime import date, datetime, timezone, timedelta
|
||
|
|
|
||
|
|
scheduler_bp = Blueprint('scheduler', __name__, url_prefix='/scheduler', template_folder='templates')
|
||
|
|
|
||
|
|
|
||
|
|
@scheduler_bp.route('/')
|
||
|
|
@login_required
|
||
|
|
def index():
|
||
|
|
"""Page du planificateur."""
|
||
|
|
due_tasks = get_due_tasks(weeks_ahead=4)
|
||
|
|
groups = group_by_zone(due_tasks)
|
||
|
|
|
||
|
|
# Interventions planifiees (anciennes ScheduledTask 'planned')
|
||
|
|
planned = Intervention.query.filter(
|
||
|
|
Intervention.status == 'planifiee',
|
||
|
|
Intervention.scheduled_date >= date.today(),
|
||
|
|
Intervention.is_deleted == False
|
||
|
|
).order_by(Intervention.scheduled_date).all()
|
||
|
|
|
||
|
|
# Interventions en retard
|
||
|
|
overdue = Intervention.query.filter(
|
||
|
|
Intervention.status == 'planifiee',
|
||
|
|
Intervention.scheduled_date < date.today(),
|
||
|
|
Intervention.is_deleted == False
|
||
|
|
).order_by(Intervention.scheduled_date).all()
|
||
|
|
|
||
|
|
return render_template('scheduler/index.html',
|
||
|
|
due_tasks=due_tasks,
|
||
|
|
groups=groups,
|
||
|
|
planned_tasks=planned,
|
||
|
|
overdue_tasks=overdue,
|
||
|
|
today=date.today())
|
||
|
|
|
||
|
|
|
||
|
|
@scheduler_bp.route('/run', methods=['POST'])
|
||
|
|
@login_required
|
||
|
|
def run():
|
||
|
|
"""Lance le planificateur manuellement."""
|
||
|
|
weeks = int(request.form.get('weeks', 4))
|
||
|
|
result = run_scheduler(weeks_ahead=weeks)
|
||
|
|
if result.get('success'):
|
||
|
|
flash(result.get('message', 'Planificateur execute'), 'success')
|
||
|
|
else:
|
||
|
|
flash(f"Erreur: {result.get('error', 'inconnue')}", 'danger')
|
||
|
|
return redirect(url_for('scheduler.index'))
|
||
|
|
|
||
|
|
|
||
|
|
@scheduler_bp.route('/api/run', methods=['POST'])
|
||
|
|
@login_required
|
||
|
|
def api_run():
|
||
|
|
"""Lance le planificateur via API."""
|
||
|
|
weeks = int(request.json.get('weeks', 4)) if request.is_json else 4
|
||
|
|
result = run_scheduler(weeks_ahead=weeks)
|
||
|
|
return jsonify(result)
|
||
|
|
|
||
|
|
|
||
|
|
@scheduler_bp.route('/api/preview')
|
||
|
|
@login_required
|
||
|
|
def api_preview():
|
||
|
|
"""Previsualise les taches dues sans les planifier."""
|
||
|
|
due = get_due_tasks(weeks_ahead=4)
|
||
|
|
groups = group_by_zone(due)
|
||
|
|
result = {
|
||
|
|
'total': len(due),
|
||
|
|
'groups': {}
|
||
|
|
}
|
||
|
|
for zone, tasks in groups.items():
|
||
|
|
result['groups'][zone] = len(tasks)
|
||
|
|
return jsonify(result)
|
||
|
|
|
||
|
|
|
||
|
|
@scheduler_bp.route('/task/<int:task_id>/ignore', methods=['POST'])
|
||
|
|
@login_required
|
||
|
|
def ignore_task(task_id):
|
||
|
|
"""Ignore une intervention planifiee."""
|
||
|
|
t = Intervention.query.get_or_404(task_id)
|
||
|
|
reason = request.form.get('reason', '')
|
||
|
|
t.ignore(reason)
|
||
|
|
flash('Tache ignoree.', 'info')
|
||
|
|
return redirect(url_for('scheduler.index'))
|
||
|
|
|
||
|
|
|
||
|
|
@scheduler_bp.route('/task/<int:task_id>/postpone', methods=['POST'])
|
||
|
|
@login_required
|
||
|
|
def postpone_task(task_id):
|
||
|
|
"""Reporte une intervention planifiee."""
|
||
|
|
t = Intervention.query.get_or_404(task_id)
|
||
|
|
new_date_str = request.form.get('new_date')
|
||
|
|
if new_date_str:
|
||
|
|
t.scheduled_date = datetime.strptime(new_date_str, '%Y-%m-%d').date()
|
||
|
|
t.postpone_reason = request.form.get('reason', '')
|
||
|
|
db.session.commit()
|
||
|
|
flash('Tache reportee.', 'success')
|
||
|
|
return redirect(url_for('scheduler.index'))
|
||
|
|
|
||
|
|
|
||
|
|
@scheduler_bp.route('/task/<int:task_id>/start', methods=['POST'])
|
||
|
|
@login_required
|
||
|
|
def start_task(task_id):
|
||
|
|
"""Demarre une intervention planifiee (planifiee -> en_cours)."""
|
||
|
|
t = Intervention.query.get_or_404(task_id)
|
||
|
|
t.status = 'en_cours'
|
||
|
|
t.started_at = datetime.now(timezone.utc)
|
||
|
|
db.session.commit()
|
||
|
|
flash('Intervention demarree.', 'success')
|
||
|
|
return redirect(url_for('scheduler.index'))
|
||
|
|
|
||
|
|
|
||
|
|
@scheduler_bp.route('/task/<int:task_id>/complete', methods=['POST'])
|
||
|
|
@login_required
|
||
|
|
def complete_task(task_id):
|
||
|
|
"""Marque une intervention comme terminee."""
|
||
|
|
t = Intervention.query.get_or_404(task_id)
|
||
|
|
t.status = 'terminee'
|
||
|
|
t.completed_at = datetime.now(timezone.utc)
|
||
|
|
t.completed_date = date.today()
|
||
|
|
t.actual_duration = float(request.form.get('actual_duration', t.estimated_duration or 60))
|
||
|
|
db.session.commit()
|
||
|
|
flash('Intervention terminee.', 'success')
|
||
|
|
return redirect(url_for('scheduler.index'))
|
||
|
|
|
||
|
|
|
||
|
|
@scheduler_bp.route('/api/overdue-count')
|
||
|
|
@login_required
|
||
|
|
def api_overdue_count():
|
||
|
|
"""Compte les interventions en retard (pour alerte dashboard)."""
|
||
|
|
overdue = Intervention.query.filter(
|
||
|
|
Intervention.is_deleted == False,
|
||
|
|
Intervention.status == 'planifiee',
|
||
|
|
Intervention.scheduled_date < date.today()
|
||
|
|
).count()
|
||
|
|
return jsonify({'overdue_tasks': overdue, 'overdue_interventions': overdue})
|
||
|
|
|
||
|
|
|
||
|
|
@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)
|
||
|
|
|
||
|
|
|
||
|
|
@scheduler_bp.route('/api/daily-summary')
|
||
|
|
@login_required
|
||
|
|
def api_daily_summary():
|
||
|
|
"""Synthese quotidienne (P13) : resume de la journee."""
|
||
|
|
from ..scheduler.engine import get_working_hours, is_user_available
|
||
|
|
from ..outlook.models import OutlookMailInterpretation
|
||
|
|
from app_new.ent.interpretation_models import EntMessageInterpretation
|
||
|
|
|
||
|
|
today = date.today()
|
||
|
|
|
||
|
|
# Interventions planifiees aujourd'hui
|
||
|
|
today_tasks = Intervention.query.filter(
|
||
|
|
Intervention.scheduled_date == today,
|
||
|
|
Intervention.status == 'planifiee',
|
||
|
|
Intervention.is_deleted == False
|
||
|
|
).all()
|
||
|
|
|
||
|
|
# Interventions en cours
|
||
|
|
active_interventions = Intervention.query.filter(
|
||
|
|
Intervention.is_deleted == False,
|
||
|
|
~Intervention.status.in_(['terminee', 'cloturee', 'annulee', 'brouillon', 'ignoree'])
|
||
|
|
).count()
|
||
|
|
|
||
|
|
# Interpretations en attente
|
||
|
|
outlook_pending = OutlookMailInterpretation.query.filter_by(status='pending').count()
|
||
|
|
ent_pending = EntMessageInterpretation.query.filter_by(status='pending').count()
|
||
|
|
|
||
|
|
# Retards
|
||
|
|
overdue = Intervention.query.filter(
|
||
|
|
Intervention.is_deleted == False,
|
||
|
|
Intervention.status == 'planifiee',
|
||
|
|
Intervention.scheduled_date < today
|
||
|
|
).count()
|
||
|
|
|
||
|
|
# Horaires
|
||
|
|
wh = get_working_hours(today)
|
||
|
|
avail, _ = is_user_available(today)
|
||
|
|
|
||
|
|
return jsonify({
|
||
|
|
'date': today.strftime('%d/%m/%Y'),
|
||
|
|
'tasks_today': len(today_tasks),
|
||
|
|
'active_interventions': active_interventions,
|
||
|
|
'outlook_pending': outlook_pending,
|
||
|
|
'ent_pending': ent_pending,
|
||
|
|
'overdue': overdue,
|
||
|
|
'working': wh is not None and avail,
|
||
|
|
'tasks': [{'time': t.scheduled_start.strftime('%H:%M') if t.scheduled_start else '',
|
||
|
|
'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]
|
||
|
|
})
|