202 lines
7.1 KiB
Python
202 lines
7.1 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():
|
|
"""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')
|
|
@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]
|
|
})
|