fix(ui): corriger les erreurs P1 de l audit
This commit is contained in:
parent
b116955299
commit
5238b18e39
12 changed files with 307 additions and 105 deletions
|
|
@ -12,7 +12,7 @@ from ..core.models import (
|
||||||
StockLocation, StockLot, StockLotBalance, StockMovement, Staff,
|
StockLocation, StockLot, StockLotBalance, StockMovement, Staff,
|
||||||
RequestProfile, StockInventory, StockInventoryLine,
|
RequestProfile, StockInventory, StockInventoryLine,
|
||||||
Equipment, MaterialAssignment,
|
Equipment, MaterialAssignment,
|
||||||
ProductDocument,
|
ProductDocument, ReusableContainer, CleaningForecastConfig,
|
||||||
)
|
)
|
||||||
from .services.stock import StockError, receive_stock, issue_stock, transfer_stock, transvasement, dilute
|
from .services.stock import StockError, receive_stock, issue_stock, transfer_stock, transvasement, dilute
|
||||||
from .services.forecast import forecast_product
|
from .services.forecast import forecast_product
|
||||||
|
|
@ -68,13 +68,21 @@ def product_new():
|
||||||
def product_edit(id):
|
def product_edit(id):
|
||||||
product = ProductGeneric.query.get_or_404(id)
|
product = ProductGeneric.query.get_or_404(id)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
try:
|
||||||
|
minimum = Decimal(request.form.get("stock_minimum") or 0)
|
||||||
|
security = Decimal(request.form.get("stock_security") or 0)
|
||||||
|
daily = Decimal(request.form.get("forecast_daily_quantity") or 0)
|
||||||
|
except (ArithmeticError, ValueError):
|
||||||
|
flash("Les quantités doivent être numériques (ex. 12,50).", "danger")
|
||||||
|
return render_template("cleaning/product_form.html", product=product,
|
||||||
|
categories=ProductCategory.query.order_by(ProductCategory.name).all()), 400
|
||||||
product.name = (request.form.get("name") or product.name).strip()
|
product.name = (request.form.get("name") or product.name).strip()
|
||||||
product.description = request.form.get("description")
|
product.description = request.form.get("description")
|
||||||
product.category_id = request.form.get("category_id") or None
|
product.category_id = request.form.get("category_id") or None
|
||||||
product.reference_unit = request.form.get("reference_unit") or product.reference_unit
|
product.reference_unit = request.form.get("reference_unit") or product.reference_unit
|
||||||
product.stock_minimum = Decimal(request.form.get("stock_minimum") or 0)
|
product.stock_minimum = minimum
|
||||||
product.stock_security = Decimal(request.form.get("stock_security") or 0)
|
product.stock_security = security
|
||||||
product.forecast_daily_quantity = Decimal(request.form.get("forecast_daily_quantity") or 0)
|
product.forecast_daily_quantity = daily
|
||||||
product.is_active = request.form.get("is_active") == "on"
|
product.is_active = request.form.get("is_active") == "on"
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Produit générique mis à jour.", "success")
|
flash("Produit générique mis à jour.", "success")
|
||||||
|
|
@ -140,7 +148,9 @@ def packaging_new():
|
||||||
@login_required
|
@login_required
|
||||||
def stock():
|
def stock():
|
||||||
balances = StockLotBalance.query.join(StockLot).join(ProductGeneric).join(StockLocation).order_by(ProductGeneric.name, StockLot.expiry_date).all()
|
balances = StockLotBalance.query.join(StockLot).join(ProductGeneric).join(StockLocation).order_by(ProductGeneric.name, StockLot.expiry_date).all()
|
||||||
return render_template("cleaning/stock.html", balances=balances, locations=StockLocation.query.filter_by(is_active=True).order_by(StockLocation.name).all())
|
return render_template("cleaning/stock.html", balances=balances,
|
||||||
|
locations=StockLocation.query.filter_by(is_active=True).order_by(StockLocation.name).all(),
|
||||||
|
current_date=date.today())
|
||||||
|
|
||||||
@cleaning_bp.route("/locations", methods=["GET", "POST"])
|
@cleaning_bp.route("/locations", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
|
@ -227,12 +237,18 @@ def forecast_config():
|
||||||
config = CleaningForecastConfig.query.filter_by(is_active=True).first()
|
config = CleaningForecastConfig.query.filter_by(is_active=True).first()
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if not config: config = CleaningForecastConfig(); db.session.add(config)
|
if not config: config = CleaningForecastConfig(); db.session.add(config)
|
||||||
|
try:
|
||||||
config.normal_coefficient = Decimal(request.form.get("normal_coefficient") or 1)
|
config.normal_coefficient = Decimal(request.form.get("normal_coefficient") or 1)
|
||||||
config.permanence_coefficient = Decimal(request.form.get("permanence_coefficient") or 0)
|
config.permanence_coefficient = Decimal(request.form.get("permanence_coefficient") or 0)
|
||||||
config.closed_coefficient = Decimal(request.form.get("closed_coefficient") or 0)
|
config.closed_coefficient = Decimal(request.form.get("closed_coefficient") or 0)
|
||||||
config.exceptional_coefficient = Decimal(request.form.get("exceptional_coefficient") or 1)
|
config.exceptional_coefficient = Decimal(request.form.get("exceptional_coefficient") or 1)
|
||||||
|
except (ArithmeticError, ValueError):
|
||||||
|
db.session.rollback()
|
||||||
|
flash("Les coefficients doivent être numériques (entre 0 et 1).", "danger")
|
||||||
|
return render_template("cleaning/forecast_config.html", forecast_config=config,
|
||||||
|
values=request.form), 400
|
||||||
db.session.commit(); flash("Coefficients de prévision enregistrés.", "success"); return redirect(url_for("cleaning.forecast_config"))
|
db.session.commit(); flash("Coefficients de prévision enregistrés.", "success"); return redirect(url_for("cleaning.forecast_config"))
|
||||||
return render_template("cleaning/forecast_config.html", config=config,
|
return render_template("cleaning/forecast_config.html", forecast_config=config,
|
||||||
values={"normal_coefficient": config.normal_coefficient if config else 1,
|
values={"normal_coefficient": config.normal_coefficient if config else 1,
|
||||||
"permanence_coefficient": config.permanence_coefficient if config else Decimal("0.4"),
|
"permanence_coefficient": config.permanence_coefficient if config else Decimal("0.4"),
|
||||||
"closed_coefficient": config.closed_coefficient if config else 0,
|
"closed_coefficient": config.closed_coefficient if config else 0,
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,12 @@ def detail(id):
|
||||||
def create():
|
def create():
|
||||||
"""Créer une entreprise."""
|
"""Créer une entreprise."""
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
name = (request.form.get('name') or '').strip()
|
||||||
|
if not name:
|
||||||
|
flash('Le nom de l’entreprise est obligatoire.', 'danger')
|
||||||
|
return render_template('companies/new.html', form_data=request.form), 400
|
||||||
company = Company(
|
company = Company(
|
||||||
name=request.form.get('name'),
|
name=name,
|
||||||
address=request.form.get('address'),
|
address=request.form.get('address'),
|
||||||
contact_phone=request.form.get('contact_phone') or request.form.get('phone'),
|
contact_phone=request.form.get('contact_phone') or request.form.get('phone'),
|
||||||
contact_email=request.form.get('contact_email') or request.form.get('email'),
|
contact_email=request.form.get('contact_email') or request.form.get('email'),
|
||||||
|
|
@ -40,7 +44,12 @@ def create():
|
||||||
specialty=request.form.get('specialty'),
|
specialty=request.form.get('specialty'),
|
||||||
)
|
)
|
||||||
db.session.add(company)
|
db.session.add(company)
|
||||||
|
try:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
flash('Impossible d’enregistrer cette entreprise. Vérifiez les données saisies.', 'danger')
|
||||||
|
return render_template('companies/new.html', form_data=request.form), 400
|
||||||
flash('Entreprise créée avec succès.', 'success')
|
flash('Entreprise créée avec succès.', 'success')
|
||||||
return redirect(url_for('companies.index'))
|
return redirect(url_for('companies.index'))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,21 @@ from app_new.core.models.equipment import Equipment
|
||||||
rooms_bp = Blueprint('rooms', __name__, template_folder='templates')
|
rooms_bp = Blueprint('rooms', __name__, template_folder='templates')
|
||||||
|
|
||||||
|
|
||||||
|
def _room_form_context(room=None):
|
||||||
|
"""Return all reference data used by the room form.
|
||||||
|
|
||||||
|
The building list is deliberately loaded independently from the zone
|
||||||
|
list. Previously the GET route only supplied ``zones`` which left the
|
||||||
|
required building select empty and made every room creation impossible.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'room': room,
|
||||||
|
'buildings': Building.query.order_by(Building.name).all(),
|
||||||
|
'zones': Zone.query.join(Building).order_by(Building.name, Zone.name).all(),
|
||||||
|
'room_types': RoomType.query.order_by(RoomType.name).all(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@rooms_bp.route('/')
|
@rooms_bp.route('/')
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
|
|
@ -88,22 +103,48 @@ def detail(id):
|
||||||
def create():
|
def create():
|
||||||
"""Créer une salle."""
|
"""Créer une salle."""
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
name = (request.form.get('name') or '').strip()
|
||||||
|
building_id = request.form.get('building_id', type=int)
|
||||||
|
zone_id = request.form.get('zone_id', type=int)
|
||||||
|
building = db.session.get(Building, building_id) if building_id else None
|
||||||
|
zone = db.session.get(Zone, zone_id) if zone_id else None
|
||||||
|
errors = []
|
||||||
|
if not name:
|
||||||
|
errors.append('Le nom de la salle est obligatoire.')
|
||||||
|
if not building:
|
||||||
|
errors.append('Sélectionnez un bâtiment valide.')
|
||||||
|
if zone and building and zone.building_id != building.id:
|
||||||
|
errors.append('La zone sélectionnée n’appartient pas au bâtiment choisi.')
|
||||||
|
if errors:
|
||||||
|
for error in errors:
|
||||||
|
flash(error, 'danger')
|
||||||
|
context = _room_form_context()
|
||||||
|
context['form_data'] = request.form
|
||||||
|
return render_template('equipments/room_form.html', **context), 400
|
||||||
room = Room(
|
room = Room(
|
||||||
name=request.form.get('name'),
|
name=name,
|
||||||
code=request.form.get('code') or None,
|
code=request.form.get('code') or None,
|
||||||
zone_id=request.form.get('zone_id', type=int),
|
zone_id=zone_id,
|
||||||
building_id=request.form.get('building_id', type=int),
|
building_id=building.id,
|
||||||
room_type_id=request.form.get('room_type_id', type=int),
|
room_type_id=request.form.get('room_type_id', type=int),
|
||||||
floor=request.form.get('floor', 0, type=int)
|
floor=request.form.get('floor', 0, type=int)
|
||||||
)
|
)
|
||||||
db.session.add(room)
|
db.session.add(room)
|
||||||
|
try:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
flash('Impossible d’enregistrer cette salle. Vérifiez les valeurs saisies.', 'danger')
|
||||||
|
context = _room_form_context()
|
||||||
|
context['form_data'] = request.form
|
||||||
|
return render_template('equipments/room_form.html', **context), 400
|
||||||
flash(f"Salle '{room.name}' créée.", 'success')
|
flash(f"Salle '{room.name}' créée.", 'success')
|
||||||
return redirect(url_for('rooms.index'))
|
return redirect(url_for('rooms.index'))
|
||||||
|
|
||||||
zones = Zone.query.join(Building).order_by(Building.name, Zone.name).all()
|
context = _room_form_context()
|
||||||
room_types = RoomType.query.order_by(RoomType.name).all()
|
context['form_data'] = {}
|
||||||
return render_template('equipments/room_form.html', zones=zones, room_types=room_types)
|
context['title'] = 'Nouvelle salle'
|
||||||
|
return render_template('equipments/room_form.html', **context)
|
||||||
|
|
||||||
|
|
||||||
@rooms_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
|
@rooms_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
|
||||||
|
|
@ -113,20 +154,39 @@ def edit(id):
|
||||||
room = Room.query.get_or_404(id)
|
room = Room.query.get_or_404(id)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
room.name = request.form.get('name')
|
name = (request.form.get('name') or '').strip()
|
||||||
|
building_id = request.form.get('building_id', type=int)
|
||||||
|
zone_id = request.form.get('zone_id', type=int)
|
||||||
|
building = db.session.get(Building, building_id) if building_id else None
|
||||||
|
zone = db.session.get(Zone, zone_id) if zone_id else None
|
||||||
|
if not name or not building or (zone and zone.building_id != building.id):
|
||||||
|
flash('Le nom, le bâtiment et une zone cohérente sont obligatoires.', 'danger')
|
||||||
|
context = _room_form_context(room)
|
||||||
|
context['form_data'] = request.form
|
||||||
|
context['title'] = 'Modifier la salle'
|
||||||
|
return render_template('equipments/room_form.html', **context), 400
|
||||||
|
room.name = name
|
||||||
room.code = request.form.get('code') or None
|
room.code = request.form.get('code') or None
|
||||||
room.zone_id = request.form.get('zone_id', type=int)
|
room.zone_id = zone_id
|
||||||
room.building_id = request.form.get('building_id', type=int)
|
room.building_id = building.id
|
||||||
room.room_type_id = request.form.get('room_type_id', type=int)
|
room.room_type_id = request.form.get('room_type_id', type=int)
|
||||||
room.floor = request.form.get('floor', 0, type=int)
|
room.floor = request.form.get('floor', 0, type=int)
|
||||||
|
try:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
flash('Impossible d’enregistrer cette salle. Vérifiez les valeurs saisies.', 'danger')
|
||||||
|
context = _room_form_context(room)
|
||||||
|
context['form_data'] = request.form
|
||||||
|
context['title'] = 'Modifier la salle'
|
||||||
|
return render_template('equipments/room_form.html', **context), 400
|
||||||
flash(f"Salle '{room.name}' mise à jour.", 'success')
|
flash(f"Salle '{room.name}' mise à jour.", 'success')
|
||||||
return redirect(url_for('rooms.index'))
|
return redirect(url_for('rooms.index'))
|
||||||
|
|
||||||
zones = Zone.query.join(Building).order_by(Building.name, Zone.name).all()
|
context = _room_form_context(room)
|
||||||
room_types = RoomType.query.order_by(RoomType.name).all()
|
context['form_data'] = {}
|
||||||
return render_template('equipments/room_form.html',
|
context['title'] = 'Modifier la salle'
|
||||||
room=room, zones=zones, room_types=room_types)
|
return render_template('equipments/room_form.html', **context)
|
||||||
|
|
||||||
|
|
||||||
@rooms_bp.route('/<int:id>/delete', methods=['POST'])
|
@rooms_bp.route('/<int:id>/delete', methods=['POST'])
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,24 @@ def index():
|
||||||
sort_dir=sort_dir)
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_intervention_form(prefilled=None, form_data=None, **extra):
|
||||||
|
"""Render the intervention form consistently, including validation errors."""
|
||||||
|
from ..core.models.user import User
|
||||||
|
return render_template(
|
||||||
|
'interventions/new.html',
|
||||||
|
lots=Lot.query.order_by(Lot.name).all(),
|
||||||
|
equipments=Equipment.query.filter_by(status='en_service').order_by(Equipment.name).all(),
|
||||||
|
rooms=Room.query.order_by(Room.name).all(),
|
||||||
|
users=User.query.filter_by(is_active=True).order_by(User.username).all(),
|
||||||
|
statuses=INTERVENTION_STATUSES,
|
||||||
|
priorities=PRIORITIES,
|
||||||
|
workflow_types=WORKFLOW_TYPES,
|
||||||
|
prefilled=prefilled or {},
|
||||||
|
form_data=form_data or {},
|
||||||
|
**extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@interventions_bp.route('/new', methods=['GET', 'POST'])
|
@interventions_bp.route('/new', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required('intervention.create')
|
@permission_required('intervention.create')
|
||||||
|
|
@ -128,6 +146,10 @@ def create():
|
||||||
from ..outlook.models import OutlookMailInterpretation
|
from ..outlook.models import OutlookMailInterpretation
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
title = (request.form.get('title') or '').strip()
|
||||||
|
if not title:
|
||||||
|
flash('Le titre de l’intervention est obligatoire.', 'danger')
|
||||||
|
return _render_intervention_form(form_data=request.form), 400
|
||||||
# Une intervention générale peut cibler une salle sans équipement.
|
# Une intervention générale peut cibler une salle sans équipement.
|
||||||
equipment_value = request.form.get('equipment_id')
|
equipment_value = request.form.get('equipment_id')
|
||||||
equipment_id = int(equipment_value) if equipment_value and equipment_value.isdigit() else None
|
equipment_id = int(equipment_value) if equipment_value and equipment_value.isdigit() else None
|
||||||
|
|
@ -143,8 +165,15 @@ def create():
|
||||||
'preventif': 'preventive', 'amelioratif': 'travaux',
|
'preventif': 'preventive', 'amelioratif': 'travaux',
|
||||||
'prevention': 'prevention', 'administratif': 'prevention',
|
'prevention': 'prevention', 'administratif': 'prevention',
|
||||||
}.get(selected_type, 'corrective')
|
}.get(selected_type, 'corrective')
|
||||||
|
try:
|
||||||
|
scheduled_date = datetime.strptime(request.form.get('intervention_date'), '%Y-%m-%d').date() if request.form.get('intervention_date') else None
|
||||||
|
scheduled_start = datetime.strptime(request.form.get('scheduled_start'), '%H:%M').time() if request.form.get('scheduled_start') else None
|
||||||
|
scheduled_end = datetime.strptime(request.form.get('scheduled_end'), '%H:%M').time() if request.form.get('scheduled_end') else None
|
||||||
|
except ValueError:
|
||||||
|
flash('La date ou l’heure saisie est invalide.', 'danger')
|
||||||
|
return _render_intervention_form(form_data=request.form), 400
|
||||||
intervention = Intervention(
|
intervention = Intervention(
|
||||||
title=request.form.get('title'),
|
title=title,
|
||||||
description=request.form.get('description') or '',
|
description=request.form.get('description') or '',
|
||||||
lot_id=request.form.get('lot_id') or None,
|
lot_id=request.form.get('lot_id') or None,
|
||||||
equipment_id=equipment_id,
|
equipment_id=equipment_id,
|
||||||
|
|
@ -157,9 +186,9 @@ def create():
|
||||||
workflow_type=workflow_type if workflow_type in WORKFLOW_TYPES else 'corrective',
|
workflow_type=workflow_type if workflow_type in WORKFLOW_TYPES else 'corrective',
|
||||||
notes=request.form.get('notes') or '',
|
notes=request.form.get('notes') or '',
|
||||||
requester_name=requester_name,
|
requester_name=requester_name,
|
||||||
scheduled_date=datetime.strptime(request.form.get('intervention_date'), '%Y-%m-%d').date() if request.form.get('intervention_date') else None,
|
scheduled_date=scheduled_date,
|
||||||
scheduled_start=datetime.strptime(request.form.get('scheduled_start'), '%H:%M').time() if request.form.get('scheduled_start') else None,
|
scheduled_start=scheduled_start,
|
||||||
scheduled_end=datetime.strptime(request.form.get('scheduled_end'), '%H:%M').time() if request.form.get('scheduled_end') else None
|
scheduled_end=scheduled_end
|
||||||
)
|
)
|
||||||
db.session.add(intervention)
|
db.session.add(intervention)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
@ -329,20 +358,7 @@ def create():
|
||||||
room_id = room.id
|
room_id = room.id
|
||||||
room_match = room
|
room_match = room
|
||||||
|
|
||||||
lots = Lot.query.order_by(Lot.name).all()
|
return _render_intervention_form(prefilled=prefilled,
|
||||||
equipments = Equipment.query.filter_by(status='en_service').order_by(Equipment.name).all()
|
|
||||||
rooms = Room.query.order_by(Room.name).all()
|
|
||||||
users = User.query.filter_by(is_active=True).order_by(User.username).all()
|
|
||||||
|
|
||||||
return render_template('interventions/new.html',
|
|
||||||
lots=lots,
|
|
||||||
equipments=equipments,
|
|
||||||
rooms=rooms,
|
|
||||||
users=users,
|
|
||||||
statuses=INTERVENTION_STATUSES,
|
|
||||||
priorities=PRIORITIES,
|
|
||||||
workflow_types=WORKFLOW_TYPES,
|
|
||||||
prefilled=prefilled,
|
|
||||||
equipment_id=equipment_id,
|
equipment_id=equipment_id,
|
||||||
equipment_match=equipment_match,
|
equipment_match=equipment_match,
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
|
|
@ -410,6 +426,7 @@ def detail(id):
|
||||||
# de ``interventions/detail.html`` est conservée pour compatibilité.
|
# de ``interventions/detail.html`` est conservée pour compatibilité.
|
||||||
return render_template('interventions_module/detail.html',
|
return render_template('interventions_module/detail.html',
|
||||||
intervention=intervention,
|
intervention=intervention,
|
||||||
|
documents=intervention.documents.all(),
|
||||||
statuses=INTERVENTION_STATUSES,
|
statuses=INTERVENTION_STATUSES,
|
||||||
transitions=transitions,
|
transitions=transitions,
|
||||||
history=history,
|
history=history,
|
||||||
|
|
|
||||||
|
|
@ -570,9 +570,10 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if intervention.documents.all() %}
|
{% set intervention_documents = documents|default([]) %}
|
||||||
|
{% if intervention_documents %}
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% for doc in intervention.documents %}
|
{% for doc in intervention_documents %}
|
||||||
<div class="list-group-item d-flex justify-content-between align-items-center px-0">
|
<div class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||||
<div>
|
<div>
|
||||||
{% set ext = doc.filename|lower %}
|
{% set ext = doc.filename|lower %}
|
||||||
|
|
@ -584,13 +585,13 @@
|
||||||
<br><small class="text-muted">{{ doc.uploaded_at.strftime('%d/%m/%Y') if doc.uploaded_at else '—' }}</small>
|
<br><small class="text-muted">{{ doc.uploaded_at.strftime('%d/%m/%Y') if doc.uploaded_at else '—' }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ url_for('documents.download_intervention', doc_id=doc.id) }}" class="btn btn-sm btn-outline-secondary" title="Télécharger">
|
<a href="{{ url_for('documents.download_intervention_document', doc_id=doc.id) }}" class="btn btn-sm btn-outline-secondary" title="Télécharger">
|
||||||
<i class="bi bi-download"></i>
|
<i class="bi bi-download"></i>
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" title="Modifier la description" data-bs-toggle="modal" data-bs-target="#editInterventionDocumentModal{{ doc.id }}">
|
<button type="button" class="btn btn-sm btn-outline-primary" title="Modifier la description" data-bs-toggle="modal" data-bs-target="#editInterventionDocumentModal{{ doc.id }}">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</button>
|
</button>
|
||||||
<form method="POST" action="{{ url_for('documents.delete_intervention', doc_id=doc.id) }}" style="display:inline;" onsubmit="return confirm('Supprimer ce document ?')">
|
<form method="POST" action="{{ url_for('documents.delete_intervention_document', doc_id=doc.id) }}" style="display:inline;" onsubmit="return confirm('Supprimer ce document ?')">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Supprimer">
|
<button type="submit" class="btn btn-sm btn-outline-danger" title="Supprimer">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -710,7 +711,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modals Edit Documents Intervention -->
|
<!-- Modals Edit Documents Intervention -->
|
||||||
{% for doc in intervention.documents %}
|
{% for doc in intervention_documents|default([]) %}
|
||||||
<div class="modal fade" id="editInterventionDocumentModal{{ doc.id }}" tabindex="-1">
|
<div class="modal fade" id="editInterventionDocumentModal{{ doc.id }}" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="fw-bold text-dark">#{{ interv.id }} — {{ interv.title[:80] }}{% if interv.title|length > 80 %}…{% endif %}</div>
|
<div class="fw-bold text-dark">#{{ interv.id }} — {{ interv.title[:80] }}{% if interv.title|length > 80 %}…{% endif %}</div>
|
||||||
<div class="text-muted" style="font-size:0.8rem;">
|
<div class="text-muted" style="font-size:0.8rem;">
|
||||||
<i class="bi bi-hdd"></i> {{ interv.equipment.name[:40] if interv.equipment else (interv.room.name if interv.room else 'Localisation générale') }}{% if interv.equipment and interv.equipment.name|length > 40 %}…{% endif %}
|
<i class="bi bi-hdd"></i> {% if interv.equipment %}{{ interv.equipment.name[:40] }}{% if interv.equipment.name|length > 40 %}…{% endif %}{% elif interv.room %}{{ interv.room.name }}{% else %}Localisation générale{% endif %}
|
||||||
{% if interv.assigned_to %}
|
{% if interv.assigned_to %}
|
||||||
· <i class="bi bi-person-badge"></i> {{ interv.assigned_to.full_name }}
|
· <i class="bi bi-person-badge"></i> {{ interv.assigned_to.full_name }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -129,10 +129,10 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Notifications système : interprétations et watchdogs -->
|
<!-- Notifications système : interprétations et watchdogs -->
|
||||||
<a href="/logs/" class="btn btn-sm btn-outline-light me-2 position-relative" title="Notifications système et logs watchdogs" aria-label="Notifications système et logs watchdogs">
|
{% if has_permission('audit.view') %}<a href="/logs/" class="btn btn-sm btn-outline-light me-2 position-relative" title="Notifications système et logs watchdogs" aria-label="Notifications système et logs watchdogs">
|
||||||
<i class="bi bi-journal-text"></i>
|
<i class="bi bi-journal-text"></i>
|
||||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" id="notif-count" style="font-size:0.6rem; display:none;">0</span>
|
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" id="notif-count" style="font-size:0.6rem; display:none;">0</span>
|
||||||
</a>
|
</a>{% endif %}
|
||||||
|
|
||||||
<!-- Utilisateur -->
|
<!-- Utilisateur -->
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
|
|
@ -163,67 +163,67 @@
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
|
||||||
<li class="nav-group-label d-lg-none mt-2">Principal</li>
|
<li class="nav-group-label d-lg-none mt-2">Principal</li>
|
||||||
<li class="nav-item">
|
{% if has_permission('dashboard.view') %}<li class="nav-item">
|
||||||
<a class="nav-link {% if request.path == '/' %}active{% endif %}" href="{{ url_for('dashboard.index') }}">
|
<a class="nav-link {% if request.path == '/' %}active{% endif %}" href="{{ url_for('dashboard.index') }}">
|
||||||
<i class="bi bi-speedometer2"></i> <span>Tableau de bord</span>
|
<i class="bi bi-speedometer2"></i> <span>Tableau de bord</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>{% endif %}
|
||||||
<li class="nav-item">
|
{% if has_permission('integration.outlook.view') or has_permission('integration.ent.view') %}<li class="nav-item">
|
||||||
<a class="nav-link {% if '/messagerie' in request.path %}active{% endif %}" href="/messagerie">
|
<a class="nav-link {% if '/messagerie' in request.path %}active{% endif %}" href="/messagerie">
|
||||||
<i class="bi bi-envelope-paper"></i> <span>Messagerie</span>
|
<i class="bi bi-envelope-paper"></i> <span>Messagerie</span>
|
||||||
{% if pending_interpretations_count is defined and pending_interpretations_count > 0 %}
|
{% if pending_interpretations_count is defined and pending_interpretations_count > 0 %}
|
||||||
<span class="badge bg-danger ms-1">{{ pending_interpretations_count }}</span>
|
<span class="badge bg-danger ms-1">{{ pending_interpretations_count }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>{% endif %}
|
||||||
<li class="nav-item dropdown">
|
{% if has_permission('intervention.view') or has_permission('planning.view') or has_permission('prevention.view') %}<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle {% if '/interventions' in request.path or '/planning' in request.path %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown">
|
<a class="nav-link dropdown-toggle {% if '/interventions' in request.path or '/planning' in request.path %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown">
|
||||||
<i class="bi bi-wrench-adjustable"></i> <span>Interventions</span>
|
<i class="bi bi-wrench-adjustable"></i> <span>Interventions</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-dark" style="background:#34495e;">
|
<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>
|
{% if has_permission('intervention.view') %}<li><a class="dropdown-item" href="{{ url_for('interventions.index') }}"><i class="bi bi-list-ul"></i> Toutes les interventions</a></li>{% endif %}
|
||||||
<li><a class="dropdown-item" href="{{ url_for('planning.index') }}"><i class="bi bi-calendar-check"></i> Planning unifié</a></li>
|
{% if has_permission('planning.view') %}<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.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><a class="dropdown-item" href="{{ url_for('planning.scheduled') }}"><i class="bi bi-calendar-week"></i> Tâches planifiées</a></li>{% endif %}
|
||||||
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></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.meters') }}"><i class="bi bi-speedometer2"></i> Compteurs</a></li>
|
{% if has_permission('planning.view') %}<li><a class="dropdown-item" href="{{ url_for('planning.meters') }}"><i class="bi bi-speedometer2"></i> Compteurs</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('planning.consumables') }}"><i class="bi bi-box"></i> Consommables</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('planning.consumables') }}"><i class="bi bi-box"></i> Consommables</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('planning.availability') }}"><i class="bi bi-clock"></i> Disponibilités</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('planning.availability') }}"><i class="bi bi-clock"></i> Disponibilités</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('planning.time_tracking') }}"><i class="bi bi-clock-history"></i> Horaires et suivi annuel</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('planning.time_tracking') }}"><i class="bi bi-clock-history"></i> Horaires et suivi annuel</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('planning.admin_tasks') }}"><i class="bi bi-clipboard"></i> Tâches administratives</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('planning.admin_tasks') }}"><i class="bi bi-clipboard"></i> Tâches administratives</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('planning.rules') }}"><i class="bi bi-shield-check"></i> Règles d'accès</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('planning.rules') }}"><i class="bi bi-shield-check"></i> Règles d'accès</a></li>{% endif %}
|
||||||
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('prevention.index') }}"><i class="bi bi-shield-check"></i> Prévention / DUERP</a></li>
|
{% if has_permission('prevention.view') %}<li><a class="dropdown-item" href="{{ url_for('prevention.index') }}"><i class="bi bi-shield-check"></i> Prévention / DUERP</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('trainings.index') }}"><i class="bi bi-mortarboard"></i> Formations</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('trainings.index') }}"><i class="bi bi-mortarboard"></i> Formations</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('trainings.my_trainings') }}"><i class="bi bi-calendar-check"></i> Mes formations</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('trainings.my_trainings') }}"><i class="bi bi-calendar-check"></i> Mes formations</a></li>{% endif %}
|
||||||
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
||||||
<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_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><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><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> Génération automatique</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>
|
</ul>
|
||||||
</li>
|
</li>{% endif %}
|
||||||
<li class="nav-item dropdown">
|
{% if has_permission('patrimoine.view') or has_permission('contract.view') or has_permission('stock.view') %}<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle {% if '/equipments' in request.path or '/companies' in request.path or '/parts' in request.path or '/lots' in request.path or '/services' in request.path %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown">
|
<a class="nav-link dropdown-toggle {% if '/equipments' in request.path or '/companies' in request.path or '/parts' in request.path or '/lots' in request.path or '/services' in request.path %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown">
|
||||||
<i class="bi bi-pc-display"></i> <span>Équipements</span>
|
<i class="bi bi-pc-display"></i> <span>Équipements</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-dark" style="background:#34495e;">
|
<ul class="dropdown-menu dropdown-menu-dark" style="background:#34495e;">
|
||||||
<li><a class="dropdown-item" href="{{ url_for('equipments.index') }}"><i class="bi bi-pc-display"></i> Tous les équipements</a></li>
|
{% if has_permission('patrimoine.view') %}<li><a class="dropdown-item" href="{{ url_for('equipments.index') }}"><i class="bi bi-pc-display"></i> Tous les équipements</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('equipments.categories') }}"><i class="bi bi-tags"></i> Catégories</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('equipments.categories') }}"><i class="bi bi-tags"></i> Catégories</a></li>
|
||||||
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('buildings.index') }}"><i class="bi bi-buildings"></i> Bâtiments</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('buildings.index') }}"><i class="bi bi-buildings"></i> Bâtiments</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('zones.index') }}"><i class="bi bi-diagram-3"></i> Zones</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('zones.index') }}"><i class="bi bi-diagram-3"></i> Zones</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('rooms.index') }}"><i class="bi bi-door-open"></i> Salles</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('rooms.index') }}"><i class="bi bi-door-open"></i> Salles</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('housing.index') }}"><i class="bi bi-house-door"></i> Logements de fonction</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('housing.index') }}"><i class="bi bi-house-door"></i> Logements de fonction</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('room_types.index') }}"><i class="bi bi-tags"></i> Types de salles</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('room_types.index') }}"><i class="bi bi-tags"></i> Types de salles</a></li>{% endif %}
|
||||||
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('companies.index') }}"><i class="bi bi-building"></i> Entreprises</a></li>
|
{% if has_permission('contract.view') %}<li><a class="dropdown-item" href="{{ url_for('companies.index') }}"><i class="bi bi-building"></i> Entreprises</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('contracts.index') }}"><i class="bi bi-file-earmark-text"></i> Contrats</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('contracts.index') }}"><i class="bi bi-file-earmark-text"></i> Contrats</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('lots.index') }}"><i class="bi bi-boxes"></i> Lots</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('lots.index') }}"><i class="bi bi-boxes"></i> Lots</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('services.index') }}"><i class="bi bi-building-gear"></i> Services</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('services.index') }}"><i class="bi bi-building-gear"></i> Services</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('parts.index') }}"><i class="bi bi-box-seam"></i> Pièces</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('parts.index') }}"><i class="bi bi-box-seam"></i> Pièces</a></li>{% endif %}
|
||||||
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('cleaning.dashboard') }}"><i class="bi bi-droplet-half"></i> Matériel & Entretien</a></li>
|
{% if has_permission('stock.view') %}<li><a class="dropdown-item" href="{{ url_for('cleaning.dashboard') }}"><i class="bi bi-droplet-half"></i> Matériel & Entretien</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('cleaning.products') }}"><i class="bi bi-flask"></i> Produits génériques</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('cleaning.products') }}"><i class="bi bi-flask"></i> Produits génériques</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('cleaning.references') }}"><i class="bi bi-upc-scan"></i> Références / conditionnements</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('cleaning.references') }}"><i class="bi bi-upc-scan"></i> Références / conditionnements</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('cleaning.documents') }}"><i class="bi bi-file-earmark-text"></i> FDS / fiches techniques</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('cleaning.documents') }}"><i class="bi bi-file-earmark-text"></i> FDS / fiches techniques</a></li>
|
||||||
|
|
@ -233,14 +233,14 @@
|
||||||
<li><a class="dropdown-item" href="{{ url_for('cleaning.transvasement_page') }}"><i class="bi bi-droplet"></i> Transvasements / dilutions</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('cleaning.transvasement_page') }}"><i class="bi bi-droplet"></i> Transvasements / dilutions</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('cleaning.request_profiles') }}"><i class="bi bi-printer"></i> Fiches papier</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('cleaning.request_profiles') }}"><i class="bi bi-printer"></i> Fiches papier</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('cleaning.material') }}"><i class="bi bi-tools"></i> Matériel durable</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('cleaning.material') }}"><i class="bi bi-tools"></i> Matériel durable</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('cleaning.forecast_config') }}"><i class="bi bi-sliders"></i> Prévision / permanences</a></li>
|
{% if has_permission('stock.configure') %}<li><a class="dropdown-item" href="{{ url_for('cleaning.forecast_config') }}"><i class="bi bi-sliders"></i> Prévision / permanences</a></li>{% endif %}
|
||||||
<li><a class="dropdown-item" href="{{ url_for('meters.index') }}"><i class="bi bi-graph-up-arrow"></i> Historique des compteurs</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('meters.index') }}"><i class="bi bi-graph-up-arrow"></i> Historique des compteurs</a></li>
|
||||||
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('equipments.to_trash') }}"><i class="bi bi-trash"></i> À jeter</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('equipments.to_trash') }}"><i class="bi bi-trash"></i> À jeter</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('equipments.lifecycle_dashboard') }}"><i class="bi bi-activity"></i> Cycle de vie</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('equipments.lifecycle_dashboard') }}"><i class="bi bi-activity"></i> Cycle de vie</a></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('equipments.trashed') }}"><i class="bi bi-archive"></i> Anciens</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('equipments.trashed') }}"><i class="bi bi-archive"></i> Anciens</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>{% endif %}{% endif %}
|
||||||
|
|
||||||
{% if current_user.is_admin() %}
|
{% if current_user.is_admin() %}
|
||||||
<li class="nav-group-label d-lg-none mt-2">Administration</li>
|
<li class="nav-group-label d-lg-none mt-2">Administration</li>
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@
|
||||||
<div class="card"><div class="card-body">
|
<div class="card"><div class="card-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="mb-3"><label class="form-label">Nom *</label><input type="text" name="name" class="form-control" required></div>
|
<div class="mb-3"><label class="form-label">Nom *</label><input type="text" name="name" class="form-control" value="{{ form_data.get('name', '') if form_data is defined else '' }}" required></div>
|
||||||
<div class="mb-3"><label class="form-label">Adresse</label><textarea name="address" class="form-control" rows="2"></textarea></div>
|
<div class="mb-3"><label class="form-label">Adresse</label><textarea name="address" class="form-control" rows="2">{{ form_data.get('address', '') if form_data is defined else '' }}</textarea></div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-6"><label class="form-label">Téléphone</label><input type="text" name="phone" class="form-control"></div>
|
<div class="col-md-6"><label class="form-label">Téléphone</label><input type="text" name="phone" class="form-control" value="{{ form_data.get('phone', '') if form_data is defined else '' }}"></div>
|
||||||
<div class="col-md-6"><label class="form-label">Email</label><input type="email" name="email" class="form-control"></div>
|
<div class="col-md-6"><label class="form-label">Email</label><input type="email" name="email" class="form-control" value="{{ form_data.get('email', '') if form_data is defined else '' }}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end"><button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Créer</button></div>
|
<div class="text-end"><button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Créer</button></div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -321,7 +321,7 @@
|
||||||
<div class="me-2" style="min-width:0;">
|
<div class="me-2" style="min-width:0;">
|
||||||
<a href="{{ url_for('interventions.detail', id=interv.id) }}" class="fw-bold text-decoration-none text-truncate d-block">{{ interv.title }}</a>
|
<a href="{{ url_for('interventions.detail', id=interv.id) }}" class="fw-bold text-decoration-none text-truncate d-block">{{ interv.title }}</a>
|
||||||
<small class="text-muted d-block text-truncate">
|
<small class="text-muted d-block text-truncate">
|
||||||
<i class="bi bi-pc-display"></i> {{ interv.equipment.name }}
|
<i class="bi bi-pc-display"></i> {% if interv.equipment %}{{ interv.equipment.name }}{% elif interv.room %}{{ interv.room.name }}{% else %}Aucune localisation{% endif %}
|
||||||
{% if interv.frequency_days %}
|
{% if interv.frequency_days %}
|
||||||
· <i class="bi bi-arrow-repeat"></i> {{ frequency_labels.get(interv.frequency_days, interv.frequency_days ~ "j") }}
|
· <i class="bi bi-arrow-repeat"></i> {{ frequency_labels.get(interv.frequency_days, interv.frequency_days ~ "j") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -492,7 +492,7 @@
|
||||||
{% else %}<span class="badge bg-info">Préventif</span>{% endif %}
|
{% else %}<span class="badge bg-info">Préventif</span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><span class="badge {{ interv.status|status_badge }}">{{ interv.status|format_status }}</span></td>
|
<td><span class="badge {{ interv.status|status_badge }}">{{ interv.status|format_status }}</span></td>
|
||||||
<td>{{ interv.equipment.name[:30] }}{% if interv.equipment.name|length > 30 %}…{% endif %}</td>
|
<td>{% if interv.equipment %}{{ interv.equipment.name[:30] }}{% if interv.equipment.name|length > 30 %}…{% endif %}{% elif interv.room %}{{ interv.room.name }}{% else %}Aucun équipement{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{ title }} — GMAO Collège{% endblock %}
|
{% block title %}{{ title|default('Salle') }} — GMAO Collège{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="mb-4">{{ title }}</h1>
|
<h1 class="mb-4">{{ title|default('Salle') }}</h1>
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
|
|
@ -10,17 +10,17 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-4 mb-3">
|
||||||
<label class="form-label">Nom *</label>
|
<label class="form-label">Nom *</label>
|
||||||
<input type="text" name="name" class="form-control" value="{{ room.name if room else '' }}" required>
|
<input type="text" name="name" class="form-control" value="{{ form_data.get('name', room.name if room else '') }}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 mb-3">
|
<div class="col-md-2 mb-3">
|
||||||
<label class="form-label">Code</label>
|
<label class="form-label">Code</label>
|
||||||
<input type="text" name="code" class="form-control" value="{{ room.code if room else '' }}">
|
<input type="text" name="code" class="form-control" value="{{ form_data.get('code', room.code if room else '') }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<label class="form-label">Bâtiment *</label>
|
<label class="form-label">Bâtiment *</label>
|
||||||
<select name="building_id" class="form-select" required id="building-select">
|
<select name="building_id" class="form-select" required id="building-select">
|
||||||
{% for b in buildings %}
|
{% for b in buildings %}
|
||||||
<option value="{{ b.id }}" {% if room and room.building_id == b.id %}selected{% endif %}>{{ b.name }}</option>
|
<option value="{{ b.id }}" {% if form_data.get('building_id', room.building_id if room else '')|string == b.id|string %}selected{% endif %}>{{ b.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
<select name="zone_id" class="form-select" id="zone-select">
|
<select name="zone_id" class="form-select" id="zone-select">
|
||||||
<option value="">— Aucune —</option>
|
<option value="">— Aucune —</option>
|
||||||
{% for z in zones %}
|
{% for z in zones %}
|
||||||
<option value="{{ z.id }}" data-building="{{ z.building_id }}" {% if room and room.zone_id == z.id %}selected{% endif %}>{{ z.name }}</option>
|
<option value="{{ z.id }}" data-building="{{ z.building_id }}" {% if form_data.get('zone_id', room.zone_id if room else '')|string == z.id|string %}selected{% endif %}>{{ z.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -38,11 +38,11 @@
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-4 mb-3">
|
||||||
<label class="form-label">Étage</label>
|
<label class="form-label">Étage</label>
|
||||||
<select name="floor" class="form-select">
|
<select name="floor" class="form-select">
|
||||||
<option value="-1" {% if room and room.floor == -1 %}selected{% endif %}>Sous-sol</option>
|
<option value="-1" {% if form_data.get('floor', room.floor if room else 0)|string == '-1' %}selected{% endif %}>Sous-sol</option>
|
||||||
<option value="0" {% if room and room.floor == 0 %}selected{% endif %}>RDC</option>
|
<option value="0" {% if form_data.get('floor', room.floor if room else 0)|string == '0' %}selected{% endif %}>RDC</option>
|
||||||
<option value="1" {% if room and room.floor == 1 %}selected{% endif %}>1er étage</option>
|
<option value="1" {% if form_data.get('floor', room.floor if room else 0)|string == '1' %}selected{% endif %}>1er étage</option>
|
||||||
<option value="2" {% if room and room.floor == 2 %}selected{% endif %}>2ème étage</option>
|
<option value="2" {% if form_data.get('floor', room.floor if room else 0)|string == '2' %}selected{% endif %}>2ème étage</option>
|
||||||
<option value="3" {% if room and room.floor == 3 %}selected{% endif %}>3ème étage</option>
|
<option value="3" {% if form_data.get('floor', room.floor if room else 0)|string == '3' %}selected{% endif %}>3ème étage</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-4 mb-3">
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
<select name="room_type_id" class="form-select">
|
<select name="room_type_id" class="form-select">
|
||||||
<option value="">— Aucun —</option>
|
<option value="">— Aucun —</option>
|
||||||
{% for rt in room_types %}
|
{% for rt in room_types %}
|
||||||
<option value="{{ rt.id }}" {% if room and room.room_type_id == rt.id %}selected{% endif %}>{{ rt.name }}{% if rt.is_teaching %} (enseignement){% endif %}</option>
|
<option value="{{ rt.id }}" {% if form_data.get('room_type_id', room.room_type_id if room else '')|string == rt.id|string %}selected{% endif %}>{{ rt.name }}{% if rt.is_teaching %} (enseignement){% endif %}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -494,9 +494,10 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if intervention.documents.all() %}
|
{% set intervention_documents = documents|default([]) %}
|
||||||
|
{% if intervention_documents %}
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% for doc in intervention.documents %}
|
{% for doc in intervention_documents %}
|
||||||
<div class="list-group-item d-flex justify-content-between align-items-center px-0">
|
<div class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||||
<div>
|
<div>
|
||||||
{% set ext = doc.filename|lower %}
|
{% set ext = doc.filename|lower %}
|
||||||
|
|
|
||||||
98
tests/integration/test_ui_p1_regressions.py
Normal file
98
tests/integration/test_ui_p1_regressions.py
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
"""Non-regression tests for the P1 issues found during the UI audit.
|
||||||
|
|
||||||
|
The audit VM is not available in this environment; these tests exercise the
|
||||||
|
same routes against the local Flask test database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app_new.extensions import db
|
||||||
|
from app_new.core.models.college import Building, Zone, Room
|
||||||
|
from app_new.core.models.maintenance import Intervention
|
||||||
|
from app_new.core.models.cleaning import ProductGeneric
|
||||||
|
|
||||||
|
|
||||||
|
def test_room_form_lists_buildings_and_rejects_cross_building_zone(authenticated_client, app):
|
||||||
|
authenticated_client.post('/equipments/buildings/new', data={'name': 'UI test bâtiment A'})
|
||||||
|
authenticated_client.post('/equipments/buildings/new', data={'name': 'UI test bâtiment B'})
|
||||||
|
with app.app_context():
|
||||||
|
building_a = Building.query.filter_by(name="UI test bâtiment A").one()
|
||||||
|
building_b = Building.query.filter_by(name="UI test bâtiment B").one()
|
||||||
|
zone_b = Zone(name="UI test zone B", building_id=building_b.id)
|
||||||
|
db.session.add(zone_b)
|
||||||
|
db.session.commit()
|
||||||
|
a_id, b_id, zone_id = building_a.id, building_b.id, zone_b.id
|
||||||
|
|
||||||
|
response = authenticated_client.get("/equipments/rooms/new")
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.get_data(as_text=True)
|
||||||
|
assert "UI test bâtiment A" in body
|
||||||
|
assert "UI test bâtiment B" in body
|
||||||
|
|
||||||
|
response = authenticated_client.post("/equipments/rooms/new", data={
|
||||||
|
"name": "Salle incohérente",
|
||||||
|
"building_id": str(a_id),
|
||||||
|
"zone_id": str(zone_id),
|
||||||
|
})
|
||||||
|
assert response.status_code == 400
|
||||||
|
with app.app_context():
|
||||||
|
assert Room.query.filter_by(name="Salle incohérente").first() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_optional_location_is_safe_in_interventions(authenticated_client, app):
|
||||||
|
with app.app_context():
|
||||||
|
intervention = Intervention(title="UI intervention sans localisation")
|
||||||
|
db.session.add(intervention)
|
||||||
|
db.session.commit()
|
||||||
|
assert authenticated_client.get("/interventions/").status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_optional_equipment_is_safe_on_dashboard(authenticated_client, app):
|
||||||
|
with app.app_context():
|
||||||
|
db.session.add(Intervention(title="UI dashboard sans équipement"))
|
||||||
|
db.session.commit()
|
||||||
|
assert authenticated_client.get("/").status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_intervention_detail_template_exposes_document_actions():
|
||||||
|
template = open('app_new/interventions/templates/detail.html', encoding='utf-8').read()
|
||||||
|
assert 'documents.download_intervention_document' in template
|
||||||
|
assert 'documents.delete_intervention_document' in template
|
||||||
|
assert 'documents.upload_intervention' in template
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_intervention_and_company_are_validation_errors(authenticated_client):
|
||||||
|
assert authenticated_client.post("/interventions/new", data={}).status_code == 400
|
||||||
|
assert authenticated_client.post("/companies/new", data={}).status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_product_quantity_is_validation_error(authenticated_client, app):
|
||||||
|
authenticated_client.post('/cleaning/products/new', data={
|
||||||
|
'name': 'UI produit quantité invalide',
|
||||||
|
'reference_unit': 'unité',
|
||||||
|
'stock_minimum': '0', 'stock_security': '0', 'forecast_daily_quantity': '0',
|
||||||
|
})
|
||||||
|
with app.app_context():
|
||||||
|
product = ProductGeneric.query.filter_by(name="UI produit quantité invalide").one()
|
||||||
|
product_id = product.id
|
||||||
|
response = authenticated_client.post(f"/cleaning/products/{product_id}/edit", data={
|
||||||
|
"name": "UI produit quantité invalide",
|
||||||
|
"stock_minimum": "pas-un-nombre",
|
||||||
|
"stock_security": "0",
|
||||||
|
"forecast_daily_quantity": "0",
|
||||||
|
})
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "numériques" in response.get_data(as_text=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleaning_pages_load_without_reference_data(authenticated_client, app):
|
||||||
|
# Some legacy RBAC tests intentionally alter the admin role. Restore only
|
||||||
|
# the permission required by this independent route smoke test.
|
||||||
|
with app.app_context():
|
||||||
|
from app_new.core.models.rbac import Role, Permission, RolePermission
|
||||||
|
role = Role.query.filter_by(slug='admin').first()
|
||||||
|
permission = Permission.query.filter_by(code='stock.view').first()
|
||||||
|
if role and permission and not RolePermission.query.filter_by(role_id=role.id, permission_id=permission.id).first():
|
||||||
|
db.session.add(RolePermission(role_id=role.id, permission_id=permission.id, effect='allow'))
|
||||||
|
db.session.commit()
|
||||||
|
assert authenticated_client.get("/cleaning/transvasement").status_code == 200
|
||||||
|
assert authenticated_client.get("/cleaning/forecast-config").status_code == 200
|
||||||
|
assert authenticated_client.get("/cleaning/stock").status_code == 200
|
||||||
Loading…
Reference in a new issue