"""Planning Schedules Routes - GMAO Collège""" from flask import Blueprint, render_template, request, redirect, url_for, flash from flask_login import login_required, current_user from ..extensions import db from ..core.models.planning import ( PreventiveTask, PreventiveTaskConsumable, ScheduledTask, Meter, MeterReading, Consumable, ConsumableUsage, TechnicianAvailability, AdminTask, ZoneAccessRule, WorkSchedule, CollegeClosure, PlanningDay, PlanningItem ) from ..core.models.maintenance import Lot, LotTask, Intervention from ..core.models.equipment import EquipmentCategory from ..core.models.company import Company from datetime import datetime, time as dt_time planning_bp = Blueprint('planning', __name__, url_prefix='/planning', template_folder='templates') @planning_bp.route('/') @login_required def index(): """Page principale de planification - calendrier sur 1 an.""" from datetime import date, timedelta from calendar import monthrange from ..core.services.planning_service import PlanningService from ..core.models.planning import PlanningDay, PlanningItem # 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 months_data = [] months_names = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'] # Récupérer les vacances closures = CollegeClosure.query.filter( CollegeClosure.start_date <= date(year, 12, 31), CollegeClosure.end_date >= date(year, 1, 1) ).all() # Fonction pour vérifier si une date est en vacances def is_vacation(d): for c in closures: if c.start_date <= d <= c.end_date: return c.name return None # Récupérer les congés personnels leaves = TechnicianAvailability.query.filter( TechnicianAvailability.date >= date(year, 1, 1), TechnicianAvailability.date <= date(year, 12, 31) ).all() leave_dates = {l.date: l for l in leaves} for month in range(1, 13): # Nombre de jours dans le mois days_in_month = monthrange(year, month)[1] # Jour de la semaine du premier jour (0=lundi, 6=dimanche) first_weekday = date(year, month, 1).weekday() weeks = [] week = [None] * first_weekday # Cases vides avant le 1er jour for day in range(1, days_in_month + 1): d = date(year, month, day) vacation = is_vacation(d) leave = leave_dates.get(d) weekday = d.weekday() week.append({ 'day': day, 'date': d, 'is_weekend': weekday >= 5, 'is_vacation': vacation is not None, 'vacation_name': vacation, 'is_leave': leave is not None, 'leave_type': leave.availability_type if leave else None }) # Si on atteint dimanche (fin de semaine), nouvelle semaine if weekday == 6 or day == days_in_month: # Compléter la semaine si nécessaire while len(week) < 7: week.append(None) weeks.append(week) week = [] months_data.append({ 'name': months_names[month - 1], 'weeks': weeks }) # Statistiques stats = { 'preventive_tasks': ScheduledTask.query.filter_by(status='planned').count(), 'curative_interventions': Intervention.query.filter_by(type='curatif', status='en_cours').count(), 'admin_tasks': AdminTask.query.filter_by(is_active=True).count(), 'pending_items': PlanningItem.query.filter_by(status='planned').count(), } return render_template('planning/calendar.html', year=year, months=months_data, stats=stats ) @planning_bp.route('/day/') @login_required def day(date_str): """Afficher le détail d'une journée.""" from datetime import datetime from ..core.services.planning_service import PlanningService try: d = datetime.strptime(date_str, '%Y-%m-%d').date() except ValueError: flash('Date invalide', 'error') return redirect(url_for('planning.index')) # Récupérer le planning du jour 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' ).all() return render_template('planning/day.html', date=d, schedule=schedule, tasks=scheduled_tasks ) @planning_bp.route('/tasks') @login_required def tasks(): """Liste des tâches préventives.""" from app_new.core.models.maintenance import LotTask tasks = LotTask.query.join(Lot).order_by(Lot.name, LotTask.num_tache).all() lots = Lot.query.order_by(Lot.name).all() categories = EquipmentCategory.query.order_by(EquipmentCategory.name).all() return render_template('planning/tasks.html', tasks=tasks, lots=lots, categories=categories) @planning_bp.route('/tasks//edit', methods=['GET', 'POST']) @login_required def edit_task(id): """Modifier une tâche préventive.""" from app_new.core.models.maintenance import LotTask task = LotTask.query.get_or_404(id) if request.method == 'POST': duree = request.form.get('duree_minutes') task.duree_minutes = int(duree) if duree and duree.isdigit() else None task.gestionnaire = request.form.get('gestionnaire') or None task.periodicite = request.form.get('periodicite') or None task.jours_entre_interventions = int(request.form.get('jours_entre_interventions')) if request.form.get('jours_entre_interventions') and request.form.get('jours_entre_interventions').isdigit() else None task.type_maintenance = request.form.get('type_maintenance') or None task.trigger_type = request.form.get('trigger_type', 'calendar') task.trigger_event = request.form.get('trigger_event') or None task.meter_threshold = request.form.get('meter_threshold', type=float) task.season_start_month = request.form.get('season_start_month', type=int) task.season_end_month = request.form.get('season_end_month', type=int) task.advance_days = request.form.get('advance_days', type=int, default=0) task.is_active = request.form.get('is_active') == 'on' task.contrat = request.form.get('contrat') == 'on' db.session.commit() flash('Tâche mise à jour', 'success') return redirect(url_for('planning.tasks')) return render_template('planning/edit_task.html', task=task) @planning_bp.route('/tasks//generate', methods=['POST']) @login_required def generate_task(id): from app_new.core.models.maintenance import LotTask from app_new.core.services.maintenance_engine import generate_due_tasks task = LotTask.query.get_or_404(id) created = generate_due_tasks(task, event=(request.form.get('event') or '').strip() or None) flash(f'{len(created)} échéance(s) créée(s), sans doublon.', 'success' if created else 'info') return redirect(url_for('planning.tasks')) @planning_bp.route('/tasks/new', methods=['GET', 'POST']) @login_required def new_task(): """Créer une nouvelle tâche préventive.""" if request.method == 'POST': task = LotTask( lot_id=request.form.get('lot_id', type=int), num_tache=request.form.get('task_number'), tache=request.form.get('name'), periodicite=request.form.get('periodicite'), jours_entre_interventions=request.form.get('period_days', type=int), duree_minutes=request.form.get('duration_minutes', type=int, default=30), contrat=request.form.get('has_contract') == 'on', gestionnaire=request.form.get('manager'), trigger_type=request.form.get('trigger_type', 'calendar'), trigger_event=request.form.get('trigger_event') or None, meter_threshold=request.form.get('meter_threshold', type=float), season_start_month=request.form.get('season_start_month', type=int), season_end_month=request.form.get('season_end_month', type=int), is_active=True ) db.session.add(task) db.session.commit() flash('Tâche préventive créée', 'success') return redirect(url_for('planning.tasks')) lots = Lot.query.order_by(Lot.name).all() categories = EquipmentCategory.query.order_by(EquipmentCategory.name).all() companies = Company.query.order_by(Company.name).all() return render_template('planning/task_form.html', title='Nouvelle tâche préventive', task=None, lots=lots, categories=categories, companies=companies) @planning_bp.route('/scheduled') @login_required def scheduled(): """Liste des tâches planifiées.""" status = request.args.get('status', 'all') date_filter = request.args.get('date') query = ScheduledTask.query.filter(ScheduledTask.equipment_id.isnot(None)) if status != 'all': query = query.filter_by(status=status) if date_filter: query = query.filter_by(scheduled_date=datetime.strptime(date_filter, '%Y-%m-%d').date()) tasks = query.order_by(ScheduledTask.scheduled_date, ScheduledTask.scheduled_start).all() return render_template('planning/scheduled.html', tasks=tasks, status=status) @planning_bp.route('/scheduled/') @login_required def scheduled_detail(id): """Détail d'une tâche planifiée.""" task = ScheduledTask.query.get_or_404(id) return render_template('planning/scheduled_detail.html', task=task) @planning_bp.route('/meters') @login_required def meters(): """Liste des compteurs.""" meters = Meter.query.order_by(Meter.equipment_id).all() return render_template('planning/meters.html', meters=meters) @planning_bp.route('/meters/new', methods=['GET', 'POST']) @login_required def new_meter(): """Créer un nouveau compteur.""" if request.method == 'POST': meter = Meter( equipment_id=request.form.get('equipment_id', type=int), name=request.form.get('name'), meter_type=request.form.get('meter_type', 'hours'), unit=request.form.get('unit', 'h'), current_value=request.form.get('current_value', type=float, default=0), initial_value=request.form.get('initial_value', type=float, default=0), warning_threshold=request.form.get('warning_threshold', type=float), critical_threshold=request.form.get('critical_threshold', type=float), maintenance_interval=request.form.get('maintenance_interval', type=float), is_active=True ) db.session.add(meter) db.session.commit() flash('Compteur créé', 'success') return redirect(url_for('planning.meters')) from ..core.models.equipment import Equipment equipments = Equipment.query.filter_by(is_group=False).order_by(Equipment.name).all() return render_template('planning/meter_form.html', title='Nouveau compteur', meter=None, equipments=equipments) @planning_bp.route('/consumables') @login_required def consumables(): """Liste des consommables.""" consumables = Consumable.query.order_by(Consumable.category, Consumable.name).all() return render_template('planning/consumables.html', consumables=consumables) @planning_bp.route('/consumables/new', methods=['GET', 'POST']) @login_required def new_consumable(): """Créer un nouveau consommable.""" if request.method == 'POST': consumable = Consumable( name=request.form.get('name'), reference=request.form.get('reference'), category=request.form.get('category'), quantity=request.form.get('quantity', type=int, default=0), min_quantity=request.form.get('min_quantity', type=int, default=0), unit=request.form.get('unit', 'pièce'), unit_price=request.form.get('unit_price', type=float), supplier_id=request.form.get('supplier_id', type=int) or None, storage_location=request.form.get('storage_location'), is_active=True ) db.session.add(consumable) db.session.commit() flash('Consommable créé', 'success') return redirect(url_for('planning.consumables')) companies = Company.query.order_by(Company.name).all() return render_template('planning/consumable_form.html', title='Nouveau consommable', consumable=None, companies=companies) @planning_bp.route('/consumables//edit', methods=['GET', 'POST']) @login_required def edit_consumable(id): consumable = Consumable.query.get_or_404(id) if request.method == 'POST': consumable.name = (request.form.get('name') or '').strip() consumable.reference = request.form.get('reference') or None consumable.category = request.form.get('category') or None consumable.quantity = max(0, request.form.get('quantity', type=int, default=0)) consumable.min_quantity = max(0, request.form.get('min_quantity', type=int, default=0)) consumable.unit = request.form.get('unit') or 'pièce' consumable.unit_price = request.form.get('unit_price', type=float) consumable.supplier_id = request.form.get('supplier_id', type=int) or None consumable.storage_location = request.form.get('storage_location') or request.form.get('location') or None db.session.commit() flash('Consommable mis à jour', 'success') return redirect(url_for('planning.consumables')) companies = Company.query.order_by(Company.name).all() return render_template('planning/consumable_form.html', title='Modifier le consommable', consumable=consumable, companies=companies)