1247 lines
53 KiB
HTML
1247 lines
53 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}{{ title }} - Gestion Scolaire{% endblock %}
|
||
|
||
{% block head %}
|
||
<script>
|
||
// Désactiver les modules JavaScript automatiques pour cette page
|
||
window.notytexDisableAutoload = true;
|
||
</script>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="max-w-6xl mx-auto pb-20">
|
||
<div class="mb-4">
|
||
{% if is_edit %}
|
||
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
|
||
← Retour à l'évaluation
|
||
</a>
|
||
{% else %}
|
||
<a href="{{ url_for('assessments.list') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
|
||
← Retour aux évaluations
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="bg-white shadow rounded-lg">
|
||
<div class="px-4 py-3 border-b border-gray-200">
|
||
<h1 class="text-xl font-semibold text-gray-900">{{ title }}</h1>
|
||
{% if is_edit %}
|
||
<p class="text-sm text-gray-600 mt-1">Modifiez votre évaluation avec exercices et éléments de notation</p>
|
||
{% else %}
|
||
<p class="text-sm text-gray-600 mt-1">Créez votre évaluation avec exercices et éléments de notation</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<form id="unified-form" method="POST" class="px-4 py-4 space-y-4">
|
||
{{ form.hidden_tag() }}
|
||
|
||
<!-- Section Évaluation - Optimisée 3 colonnes -->
|
||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||
<h2 class="text-lg font-medium text-blue-900 mb-3">📝 Informations de l'évaluation</h2>
|
||
|
||
<!-- Première ligne : Titre, Classe, Date -->
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-3">
|
||
<div>
|
||
<label for="{{ form.title.id }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||
{{ form.title.label.text }}
|
||
</label>
|
||
{{ form.title(class="block w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-blue-500 focus:border-blue-500") }}
|
||
{% if form.title.errors %}
|
||
<div class="mt-1 text-sm text-red-600">
|
||
{% for error in form.title.errors %}
|
||
<p>{{ error }}</p>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div>
|
||
<label for="{{ form.class_group_id.id }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||
{{ form.class_group_id.label.text }}
|
||
</label>
|
||
{{ form.class_group_id(class="block w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-blue-500 focus:border-blue-500") }}
|
||
{% if form.class_group_id.errors %}
|
||
<div class="mt-1 text-sm text-red-600">
|
||
{% for error in form.class_group_id.errors %}
|
||
<p>{{ error }}</p>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div>
|
||
<label for="{{ form.date.id }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||
{{ form.date.label.text }}
|
||
</label>
|
||
{{ form.date(class="block w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-blue-500 focus:border-blue-500") }}
|
||
{% if form.date.errors %}
|
||
<div class="mt-1 text-sm text-red-600">
|
||
{% for error in form.date.errors %}
|
||
<p>{{ error }}</p>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Deuxième ligne : Trimestre, Coefficient, Description -->
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div>
|
||
<label for="{{ form.trimester.id }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||
{{ form.trimester.label.text }}
|
||
</label>
|
||
{{ form.trimester(class="block w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-blue-500 focus:border-blue-500") }}
|
||
{% if form.trimester.errors %}
|
||
<div class="mt-1 text-sm text-red-600">
|
||
{% for error in form.trimester.errors %}
|
||
<p>{{ error }}</p>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div>
|
||
<label for="{{ form.coefficient.id }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||
{{ form.coefficient.label.text }}
|
||
</label>
|
||
{{ form.coefficient(class="block w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-blue-500 focus:border-blue-500", step="0.5") }}
|
||
{% if form.coefficient.errors %}
|
||
<div class="mt-1 text-sm text-red-600">
|
||
{% for error in form.coefficient.errors %}
|
||
<p>{{ error }}</p>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div>
|
||
<label for="{{ form.description.id }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||
{{ form.description.label.text }}
|
||
</label>
|
||
{{ form.description(class="block w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-blue-500 focus:border-blue-500", rows="1") }}
|
||
{% if form.description.errors %}
|
||
<div class="mt-1 text-sm text-red-600">
|
||
{% for error in form.description.errors %}
|
||
<p>{{ error }}</p>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section Exercices - Compacte -->
|
||
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
||
<div class="mb-3">
|
||
<h2 class="text-lg font-medium text-green-900">🏃 Exercices</h2>
|
||
</div>
|
||
|
||
<div id="exercises-container" class="space-y-3">
|
||
<!-- Les exercices seront ajoutés ici dynamiquement -->
|
||
</div>
|
||
|
||
<div id="no-exercises" class="text-center py-4 text-green-700">
|
||
<p class="text-sm">Aucun exercice ajouté. Utilisez le bouton ci-dessous pour commencer.</p>
|
||
</div>
|
||
|
||
<!-- Bouton d'ajout compacte -->
|
||
<div class="mt-3 pt-3 border-t border-green-200">
|
||
<button type="button" id="add-exercise" class="bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2">
|
||
➕ Ajouter un exercice
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Guide d'aide compact -->
|
||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||
<h3 class="text-sm font-medium text-gray-900 mb-1">💡 Guide rapide</h3>
|
||
<div class="text-xs text-gray-700 space-y-1">
|
||
<p><strong>Points :</strong> Notation classique (ex: 2.5/4 points) | <strong>Score :</strong> Niveaux 0-3 (0=non acquis, 1=en cours, 2=acquis, 3=expert)</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Actions -->
|
||
<div class="flex justify-end space-x-3 pt-3 border-t border-gray-200">
|
||
{% if is_edit %}
|
||
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="px-4 py-1.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2">
|
||
Annuler
|
||
</a>
|
||
<button type="submit" class="px-4 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||
💾 Modifier l'évaluation
|
||
</button>
|
||
{% else %}
|
||
<a href="{{ url_for('assessments.list') }}" class="px-4 py-1.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2">
|
||
Annuler
|
||
</a>
|
||
<button type="submit" class="px-4 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||
💾 Créer l'évaluation
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bandeau sticky résumé temps réel (desktop uniquement) -->
|
||
<div id="summary-banner" class="fixed bottom-0 left-0 right-0 bg-white border-t shadow-lg px-4 py-2 hidden lg:block z-40">
|
||
<div class="max-w-6xl mx-auto flex items-center justify-between">
|
||
<div class="flex items-center space-x-6 text-sm">
|
||
<span class="font-medium">📊 Total: <span id="total-points" class="text-blue-600 font-bold">0</span> points</span>
|
||
<span>🏃 <span id="exercise-count" class="text-green-600 font-semibold">0</span> exercices</span>
|
||
<span>📝 <span id="element-count" class="text-purple-600 font-semibold">0</span> éléments</span>
|
||
</div>
|
||
<div class="flex items-center space-x-4 text-xs">
|
||
<div id="competence-summary" class="text-orange-600">
|
||
<!-- Compétences dynamiques -->
|
||
</div>
|
||
<div id="domain-summary" class="text-cyan-600">
|
||
<!-- Domaines dynamiques -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Template pour un exercice - Version compacte -->
|
||
<template id="exercise-template">
|
||
<div class="exercise-item border border-green-300 rounded-lg p-3 bg-white">
|
||
<div class="flex justify-between items-start mb-3">
|
||
<h3 class="text-md font-medium text-green-800">Exercice <span class="exercise-number"></span></h3>
|
||
<button type="button" class="remove-exercise text-red-600 hover:text-red-800 text-sm font-medium">
|
||
Supprimer
|
||
</button>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">Titre de l'exercice</label>
|
||
<input type="text" class="exercise-title block w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-green-500 focus:border-green-500" required>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">Ordre</label>
|
||
<input type="number" class="exercise-order block w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-green-500 focus:border-green-500" min="1" required>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">Description (optionnel)</label>
|
||
<input type="text" class="exercise-description block w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-green-500 focus:border-green-500">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Éléments de notation - Version compacte -->
|
||
<div class="bg-purple-50 border border-purple-200 rounded p-3">
|
||
<div class="mb-2">
|
||
<h4 class="text-sm font-medium text-purple-900">Éléments de notation</h4>
|
||
</div>
|
||
|
||
<div class="grading-elements-container space-y-2">
|
||
<!-- Les éléments de notation seront ajoutés ici -->
|
||
</div>
|
||
|
||
<div class="no-grading-elements text-center py-2 text-purple-700 text-xs">
|
||
Aucun élément de notation. Utilisez le bouton ci-dessous pour commencer.
|
||
</div>
|
||
|
||
<!-- Bouton d'ajout compacte -->
|
||
<div class="mt-2 pt-2 border-t border-purple-200">
|
||
<button type="button" class="add-grading-element bg-purple-600 hover:bg-purple-700 text-white px-2 py-1 rounded text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-1">
|
||
➕ Ajouter élément
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Template pour un élément de notation - Version compacte -->
|
||
<template id="grading-element-template">
|
||
<div class="grading-element-item border border-purple-300 rounded p-2 bg-white">
|
||
<div class="flex justify-between items-start mb-2">
|
||
<h5 class="text-sm font-medium text-purple-800">Élément de notation</h5>
|
||
<button type="button" class="remove-grading-element text-red-600 hover:text-red-800 text-xs font-medium">
|
||
Supprimer
|
||
</button>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-5 gap-2 mb-2">
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-700 mb-1">Label</label>
|
||
<input type="text" class="element-label block w-full border border-gray-300 rounded-md px-2 py-1 text-sm focus:ring-purple-500 focus:border-purple-500" required>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-700 mb-1">Compétence</label>
|
||
<select class="element-skill block w-full border border-gray-300 rounded-md px-2 py-1 text-sm focus:ring-purple-500 focus:border-purple-500">
|
||
<option value="">Non spécifiée</option>
|
||
{% for competence in competences %}
|
||
<option value="{{ competence.name }}" data-color="{{ competence.color }}" data-icon="{{ competence.icon }}">
|
||
{{ competence.name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-700 mb-1">Domaine</label>
|
||
<div class="relative">
|
||
<input type="text" class="element-domain-input block w-full border border-gray-300 rounded-md px-2 py-1 text-sm focus:ring-purple-500 focus:border-purple-500" placeholder="Domaine..." autocomplete="off">
|
||
<input type="hidden" class="element-domain-id">
|
||
<div class="element-domain-suggestions absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-32 overflow-y-auto hidden">
|
||
<!-- Suggestions dynamiques -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-700 mb-1">Points max</label>
|
||
<input type="number" step="0.1" min="0" class="element-max-points block w-full border border-gray-300 rounded-md px-2 py-1 text-sm focus:ring-purple-500 focus:border-purple-500" required>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-700 mb-1">Type</label>
|
||
<select class="element-grading-type block w-full border border-gray-300 rounded-md px-2 py-1 text-sm focus:ring-purple-500 focus:border-purple-500" required>
|
||
<option value="">Type...</option>
|
||
<option value="notes">Points</option>
|
||
<option value="score">Score</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-2">
|
||
<label class="block text-xs font-medium text-gray-700 mb-1">Description (optionnel)</label>
|
||
<input type="text" class="element-description block w-full border border-gray-300 rounded-md px-2 py-1 text-sm focus:ring-purple-500 focus:border-purple-500">
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Modal pour création de domaine - Version compacte -->
|
||
<div id="domain-creation-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 hidden">
|
||
<div class="relative top-20 mx-auto p-4 border w-96 shadow-lg rounded-md bg-white">
|
||
<div class="mt-2">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<h3 class="text-lg font-medium text-gray-900">Créer un nouveau domaine</h3>
|
||
<button type="button" class="close-modal text-gray-400 hover:text-gray-600">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="space-y-3">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">Nom du domaine</label>
|
||
<input type="text" id="new-domain-name" class="block w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-purple-500 focus:border-purple-500" placeholder="Ex: Calcul mental">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">Couleur associée</label>
|
||
<div class="flex flex-wrap gap-2 mb-2">
|
||
<button type="button" class="color-option w-6 h-6 rounded-full border-2 border-gray-300 hover:border-gray-400" style="background-color: #3b82f6" data-color="#3b82f6"></button>
|
||
<button type="button" class="color-option w-6 h-6 rounded-full border-2 border-gray-300 hover:border-gray-400" style="background-color: #10b981" data-color="#10b981"></button>
|
||
<button type="button" class="color-option w-6 h-6 rounded-full border-2 border-gray-300 hover:border-gray-400" style="background-color: #f59e0b" data-color="#f59e0b"></button>
|
||
<button type="button" class="color-option w-6 h-6 rounded-full border-2 border-gray-300 hover:border-gray-400" style="background-color: #8b5cf6" data-color="#8b5cf6"></button>
|
||
<button type="button" class="color-option w-6 h-6 rounded-full border-2 border-gray-300 hover:border-gray-400" style="background-color: #ef4444" data-color="#ef4444"></button>
|
||
<button type="button" class="color-option w-6 h-6 rounded-full border-2 border-gray-300 hover:border-gray-400" style="background-color: #06b6d4" data-color="#06b6d4"></button>
|
||
<button type="button" class="color-option w-6 h-6 rounded-full border-2 border-gray-300 hover:border-gray-400" style="background-color: #84cc16" data-color="#84cc16"></button>
|
||
<button type="button" class="color-option w-6 h-6 rounded-full border-2 border-gray-300 hover:border-gray-400" style="background-color: #f97316" data-color="#f97316"></button>
|
||
</div>
|
||
<input type="color" id="custom-color" class="w-full h-8 border border-gray-300 rounded cursor-pointer" value="#3b82f6">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">Description (optionnel)</label>
|
||
<input type="text" id="new-domain-description" class="block w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-purple-500 focus:border-purple-500" placeholder="Description du domaine...">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex justify-end space-x-2 mt-4">
|
||
<button type="button" class="close-modal px-3 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md">Annuler</button>
|
||
<button type="button" id="confirm-domain-creation" class="px-3 py-1.5 text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 rounded-md">Créer</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let exerciseCounter = 0;
|
||
|
||
// Variables globales pour les domaines
|
||
let availableDomains = [];
|
||
let currentDomainInput = null;
|
||
let selectedColor = '#3b82f6';
|
||
|
||
// Variables pour le résumé temps réel
|
||
let summaryState = {
|
||
totalPoints: 0,
|
||
exerciseCount: 0,
|
||
elementCount: 0,
|
||
byCompetence: {},
|
||
byDomain: {}
|
||
};
|
||
|
||
// Fonctions de calcul temps réel
|
||
function calculateLiveSummary() {
|
||
const exercises = collectFormData();
|
||
|
||
let totalPoints = 0;
|
||
let elementCount = 0;
|
||
let byCompetence = {};
|
||
let byDomain = {};
|
||
|
||
exercises.forEach(exercise => {
|
||
exercise.grading_elements.forEach(element => {
|
||
if (element.max_points && element.max_points > 0) {
|
||
const points = parseFloat(element.max_points);
|
||
totalPoints += points;
|
||
elementCount++;
|
||
|
||
// Grouper par compétence
|
||
if (element.skill && element.skill.trim()) {
|
||
if (!byCompetence[element.skill]) {
|
||
byCompetence[element.skill] = { count: 0, points: 0 };
|
||
}
|
||
byCompetence[element.skill].count++;
|
||
byCompetence[element.skill].points += points;
|
||
}
|
||
|
||
// Grouper par domaine (nom du domaine depuis l'input text)
|
||
const domainInput = element.domainInput;
|
||
if (domainInput && domainInput.trim()) {
|
||
if (!byDomain[domainInput]) {
|
||
byDomain[domainInput] = { count: 0, points: 0 };
|
||
}
|
||
byDomain[domainInput].count++;
|
||
byDomain[domainInput].points += points;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
return {
|
||
totalPoints: totalPoints,
|
||
exerciseCount: exercises.length,
|
||
elementCount: elementCount,
|
||
byCompetence: byCompetence,
|
||
byDomain: byDomain
|
||
};
|
||
}
|
||
|
||
function updateSummaryBanner() {
|
||
const summary = calculateLiveSummary();
|
||
|
||
// Mettre à jour les statistiques principales
|
||
document.getElementById('total-points').textContent = summary.totalPoints.toFixed(1);
|
||
document.getElementById('exercise-count').textContent = summary.exerciseCount;
|
||
document.getElementById('element-count').textContent = summary.elementCount;
|
||
|
||
// Mettre à jour le résumé compétences
|
||
const competenceDiv = document.getElementById('competence-summary');
|
||
if (Object.keys(summary.byCompetence).length > 0) {
|
||
const competenceList = Object.entries(summary.byCompetence)
|
||
.map(([comp, data]) => `${comp}: ${data.count}`)
|
||
.join(' | ');
|
||
competenceDiv.innerHTML = `🏷️ ${competenceList}`;
|
||
} else {
|
||
competenceDiv.innerHTML = '🏷️ Aucune compétence';
|
||
}
|
||
|
||
// Mettre à jour le résumé domaines
|
||
const domainDiv = document.getElementById('domain-summary');
|
||
if (Object.keys(summary.byDomain).length > 0) {
|
||
const domainList = Object.entries(summary.byDomain)
|
||
.map(([domain, data]) => `${domain}: ${data.points.toFixed(1)}pts`)
|
||
.join(' | ');
|
||
domainDiv.innerHTML = `🌐 ${domainList}`;
|
||
} else {
|
||
domainDiv.innerHTML = '🌐 Aucun domaine';
|
||
}
|
||
|
||
// Afficher/masquer le bandeau selon le contenu
|
||
const banner = document.getElementById('summary-banner');
|
||
if (summary.exerciseCount > 0 || summary.elementCount > 0) {
|
||
banner.classList.remove('hidden');
|
||
} else {
|
||
banner.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// Debounced version pour les performances
|
||
const debouncedUpdateSummary = debounce(updateSummaryBanner, 300);
|
||
|
||
function debounce(func, wait) {
|
||
let timeout;
|
||
return function executedFunction(...args) {
|
||
const later = () => {
|
||
clearTimeout(timeout);
|
||
func(...args);
|
||
};
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
};
|
||
}
|
||
|
||
// Fonction globale pour collecter les données du formulaire
|
||
function collectFormData() {
|
||
const exercisesContainer = document.getElementById('exercises-container');
|
||
if (!exercisesContainer) return [];
|
||
|
||
const exercises = [];
|
||
const exerciseItems = exercisesContainer.querySelectorAll('.exercise-item');
|
||
|
||
exerciseItems.forEach(exerciseItem => {
|
||
const titleInput = exerciseItem.querySelector('.exercise-title');
|
||
const descriptionInput = exerciseItem.querySelector('.exercise-description');
|
||
const orderInput = exerciseItem.querySelector('.exercise-order');
|
||
|
||
const title = titleInput ? titleInput.value : '';
|
||
const description = descriptionInput ? descriptionInput.value : '';
|
||
const order = orderInput ? parseInt(orderInput.value) || 0 : 0;
|
||
|
||
if (!title.trim()) return;
|
||
|
||
const gradingElements = [];
|
||
const elementItems = exerciseItem.querySelectorAll('.grading-element-item');
|
||
|
||
elementItems.forEach(elementItem => {
|
||
const label = elementItem.querySelector('.element-label')?.value || '';
|
||
const skill = elementItem.querySelector('.element-skill')?.value || '';
|
||
const maxPointsInput = elementItem.querySelector('.element-max-points');
|
||
const maxPoints = maxPointsInput ? parseFloat(maxPointsInput.value) || 0 : 0;
|
||
const gradingType = elementItem.querySelector('.element-grading-type')?.value || '';
|
||
const description = elementItem.querySelector('.element-description')?.value || '';
|
||
const domainHiddenInput = elementItem.querySelector('.element-domain-id');
|
||
const domainTextInput = elementItem.querySelector('.element-domain-input');
|
||
|
||
if (!label.trim() || maxPoints <= 0 || !gradingType) return;
|
||
|
||
const elementData = {
|
||
label: label.trim(),
|
||
skill: skill.trim(),
|
||
max_points: maxPoints,
|
||
grading_type: gradingType,
|
||
description: description.trim(),
|
||
domainInput: domainTextInput ? domainTextInput.value.trim() : '' // Pour le résumé
|
||
};
|
||
|
||
// Gestion du domaine
|
||
if (domainHiddenInput && domainHiddenInput.value) {
|
||
// Domaine existant sélectionné via autocomplétion
|
||
elementData.domain_id = parseInt(domainHiddenInput.value);
|
||
} else if (domainTextInput && domainTextInput.value.trim() && !domainHiddenInput.value) {
|
||
// Nouveau domaine à créer (texte saisi mais pas d'ID)
|
||
elementData.domain_name = domainTextInput.value.trim();
|
||
}
|
||
|
||
gradingElements.push(elementData);
|
||
});
|
||
|
||
exercises.push({
|
||
title: title.trim(),
|
||
description: description.trim(),
|
||
order: order,
|
||
grading_elements: gradingElements
|
||
});
|
||
});
|
||
|
||
return exercises;
|
||
}
|
||
|
||
// Charger les domaines disponibles
|
||
async function loadDomains() {
|
||
try {
|
||
const response = await fetch('/api/domains/');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
availableDomains = data.domains;
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors du chargement des domaines:', error);
|
||
}
|
||
}
|
||
|
||
// Configuration de l'autocomplétion pour les domaines
|
||
function setupDomainAutocomplete(container) {
|
||
const inputElement = container.querySelector('.element-domain-input');
|
||
const hiddenElement = container.querySelector('.element-domain-id');
|
||
const suggestionsElement = container.querySelector('.element-domain-suggestions');
|
||
|
||
if (!inputElement || !hiddenElement || !suggestionsElement) {
|
||
return;
|
||
}
|
||
|
||
let debounceTimer = null;
|
||
|
||
inputElement.addEventListener('input', function() {
|
||
const query = this.value.trim();
|
||
clearTimeout(debounceTimer);
|
||
|
||
// Mettre à jour le résumé à chaque changement
|
||
debouncedUpdateSummary();
|
||
|
||
if (query.length === 0) {
|
||
hideSuggestions(suggestionsElement);
|
||
hiddenElement.value = '';
|
||
return;
|
||
}
|
||
|
||
debounceTimer = setTimeout(() => {
|
||
searchAndShowSuggestions(query, inputElement, hiddenElement, suggestionsElement);
|
||
}, 300);
|
||
});
|
||
|
||
inputElement.addEventListener('blur', function(e) {
|
||
// Délai plus long pour permettre le clic sur les suggestions
|
||
setTimeout(() => {
|
||
if (!suggestionsElement.contains(document.activeElement) &&
|
||
!suggestionsElement.matches(':hover')) {
|
||
hideSuggestions(suggestionsElement);
|
||
}
|
||
}, 200);
|
||
});
|
||
|
||
inputElement.addEventListener('focus', function() {
|
||
if (this.value.trim()) {
|
||
searchAndShowSuggestions(this.value.trim(), inputElement, hiddenElement, suggestionsElement);
|
||
}
|
||
});
|
||
|
||
// Gestion des touches clavier avec navigation
|
||
let selectedIndex = -1;
|
||
|
||
inputElement.addEventListener('keydown', function(e) {
|
||
const suggestions = suggestionsElement.querySelectorAll('.suggestion-item');
|
||
|
||
if (e.key === 'Escape') {
|
||
hideSuggestions(suggestionsElement);
|
||
selectedIndex = -1;
|
||
this.blur();
|
||
} else if (e.key === 'ArrowDown') {
|
||
e.preventDefault();
|
||
if (suggestions.length > 0) {
|
||
selectedIndex = Math.min(selectedIndex + 1, suggestions.length - 1);
|
||
updateSelection(suggestions, selectedIndex);
|
||
}
|
||
} else if (e.key === 'ArrowUp') {
|
||
e.preventDefault();
|
||
if (suggestions.length > 0) {
|
||
selectedIndex = Math.max(selectedIndex - 1, -1);
|
||
updateSelection(suggestions, selectedIndex);
|
||
}
|
||
} else if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
if (selectedIndex >= 0 && suggestions[selectedIndex]) {
|
||
suggestions[selectedIndex].dispatchEvent(new Event('mousedown'));
|
||
} else if (suggestions.length > 0) {
|
||
suggestions[0].dispatchEvent(new Event('mousedown'));
|
||
}
|
||
} else if (e.key === 'Tab') {
|
||
if (suggestions.length > 0 && selectedIndex === -1) {
|
||
e.preventDefault();
|
||
suggestions[0].dispatchEvent(new Event('mousedown'));
|
||
}
|
||
}
|
||
});
|
||
|
||
// Reset de la sélection quand on tape
|
||
inputElement.addEventListener('input', function() {
|
||
selectedIndex = -1;
|
||
});
|
||
|
||
function updateSelection(suggestions, index) {
|
||
suggestions.forEach((suggestion, i) => {
|
||
suggestion.classList.remove('bg-purple-100', 'bg-blue-100', 'ring-2', 'ring-purple-500');
|
||
if (i === index) {
|
||
suggestion.classList.add('bg-blue-100', 'ring-2', 'ring-blue-500');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
async function searchAndShowSuggestions(query, inputElement, hiddenElement, suggestionsElement) {
|
||
try {
|
||
const response = await fetch(`/api/domains/search?q=${encodeURIComponent(query)}`);
|
||
const data = await response.json();
|
||
|
||
if (!data.success) {
|
||
console.error('Erreur lors de la recherche de domaines:', data.error);
|
||
return;
|
||
}
|
||
|
||
suggestionsElement.innerHTML = '';
|
||
const domains = data.domains || [];
|
||
|
||
if (domains.length > 0) {
|
||
domains.forEach(domain => {
|
||
const suggestionDiv = createSuggestionItem(domain, query, inputElement, hiddenElement, suggestionsElement);
|
||
suggestionsElement.appendChild(suggestionDiv);
|
||
});
|
||
}
|
||
|
||
// Vérifier s'il y a une correspondance exacte
|
||
const exactMatch = domains.find(domain =>
|
||
domain.name.toLowerCase() === query.toLowerCase()
|
||
);
|
||
|
||
// Option "Créer nouveau domaine" si pas de correspondance exacte
|
||
if (!exactMatch) {
|
||
const createDiv = document.createElement('div');
|
||
createDiv.className = 'suggestion-item px-2 py-1 text-sm cursor-pointer hover:bg-purple-100 border-t border-gray-200';
|
||
createDiv.innerHTML = `
|
||
<div class="flex items-center">
|
||
<svg class="w-3 h-3 text-purple-600 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
|
||
</svg>
|
||
<span class="text-purple-600 font-medium text-xs">Créer "${query}"</span>
|
||
</div>
|
||
`;
|
||
|
||
createDiv.addEventListener('mousedown', function(e) {
|
||
e.preventDefault();
|
||
currentDomainInput = inputElement;
|
||
showDomainCreationModal(query);
|
||
hideSuggestions(suggestionsElement);
|
||
});
|
||
|
||
suggestionsElement.appendChild(createDiv);
|
||
}
|
||
|
||
// Affichage conditionnel des suggestions
|
||
if (domains.length > 0 || !exactMatch) {
|
||
suggestionsElement.classList.remove('hidden');
|
||
} else {
|
||
suggestionsElement.classList.add('hidden');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Erreur lors de la recherche de domaines:', error);
|
||
suggestionsElement.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
function createSuggestionItem(domain, query, inputElement, hiddenElement, suggestionsElement) {
|
||
const div = document.createElement('div');
|
||
div.className = 'suggestion-item px-2 py-1 text-sm cursor-pointer hover:bg-gray-100 flex items-center';
|
||
|
||
const colorIndicator = `<div class="w-2 h-2 rounded-full mr-1" style="background-color: ${domain.color}"></div>`;
|
||
const highlightedName = highlightMatch(domain.name, query);
|
||
|
||
div.innerHTML = `${colorIndicator}<span class="text-xs">${highlightedName}</span>`;
|
||
|
||
div.addEventListener('mousedown', function(e) {
|
||
e.preventDefault();
|
||
inputElement.value = domain.name;
|
||
hiddenElement.value = domain.id;
|
||
hideSuggestions(suggestionsElement);
|
||
inputElement.focus();
|
||
// Mettre à jour le résumé après sélection
|
||
debouncedUpdateSummary();
|
||
});
|
||
|
||
return div;
|
||
}
|
||
|
||
function highlightMatch(text, query) {
|
||
const lowerText = text.toLowerCase();
|
||
const lowerQuery = query.toLowerCase();
|
||
|
||
if (lowerText.includes(lowerQuery)) {
|
||
const index = lowerText.indexOf(lowerQuery);
|
||
return text.substring(0, index) +
|
||
'<strong>' + text.substring(index, index + query.length) + '</strong>' +
|
||
text.substring(index + query.length);
|
||
}
|
||
|
||
return text;
|
||
}
|
||
|
||
function hideSuggestions(suggestionsElement) {
|
||
suggestionsElement.classList.add('hidden');
|
||
}
|
||
|
||
// Gestion du modal de création de domaine
|
||
function showDomainCreationModal(suggestedName = '') {
|
||
const modal = document.getElementById('domain-creation-modal');
|
||
const nameInput = document.getElementById('new-domain-name');
|
||
const customColorInput = document.getElementById('custom-color');
|
||
|
||
if (nameInput) {
|
||
nameInput.value = suggestedName;
|
||
}
|
||
if (customColorInput) {
|
||
customColorInput.value = selectedColor;
|
||
}
|
||
updateColorSelection(selectedColor);
|
||
|
||
setupModalEvents();
|
||
|
||
modal.classList.remove('hidden');
|
||
if (nameInput) {
|
||
nameInput.focus();
|
||
}
|
||
}
|
||
|
||
function setupModalEvents() {
|
||
// Bouton créer
|
||
const confirmBtn = document.getElementById('confirm-domain-creation');
|
||
if (confirmBtn) {
|
||
confirmBtn.replaceWith(confirmBtn.cloneNode(true));
|
||
const newConfirmBtn = document.getElementById('confirm-domain-creation');
|
||
newConfirmBtn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
createNewDomain();
|
||
});
|
||
}
|
||
|
||
// Boutons fermer
|
||
document.querySelectorAll('.close-modal').forEach(btn => {
|
||
btn.replaceWith(btn.cloneNode(true));
|
||
});
|
||
|
||
document.querySelectorAll('.close-modal').forEach(btn => {
|
||
btn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
hideDomainCreationModal();
|
||
});
|
||
});
|
||
|
||
// Couleurs prédéfinies
|
||
document.querySelectorAll('.color-option').forEach(btn => {
|
||
btn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
updateColorSelection(this.dataset.color);
|
||
});
|
||
});
|
||
|
||
// Sélecteur de couleur personnalisé
|
||
const customColorInput = document.getElementById('custom-color');
|
||
if (customColorInput) {
|
||
customColorInput.addEventListener('input', function() {
|
||
updateColorSelection(this.value);
|
||
});
|
||
}
|
||
}
|
||
|
||
function hideDomainCreationModal() {
|
||
const modal = document.getElementById('domain-creation-modal');
|
||
modal.classList.add('hidden');
|
||
currentDomainInput = null;
|
||
}
|
||
|
||
function updateColorSelection(color) {
|
||
selectedColor = color;
|
||
const customColorInput = document.getElementById('custom-color');
|
||
if (customColorInput) {
|
||
customColorInput.value = color;
|
||
}
|
||
|
||
// Mettre à jour la sélection visuelle
|
||
document.querySelectorAll('.color-option').forEach(btn => {
|
||
btn.classList.remove('ring-2', 'ring-purple-500');
|
||
if (btn.dataset.color === color) {
|
||
btn.classList.add('ring-2', 'ring-purple-500');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Créer un nouveau domaine via API
|
||
async function createNewDomain() {
|
||
const nameInput = document.getElementById('new-domain-name');
|
||
const descriptionInput = document.getElementById('new-domain-description');
|
||
|
||
if (!nameInput) {
|
||
alert('Erreur: champ nom non trouvé');
|
||
return;
|
||
}
|
||
|
||
const name = nameInput.value.trim();
|
||
const description = descriptionInput ? descriptionInput.value.trim() : '';
|
||
|
||
if (!name) {
|
||
alert('Le nom du domaine est obligatoire');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/domains/', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
name: name,
|
||
color: selectedColor,
|
||
description: description
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
// Ajouter le nouveau domaine à la liste
|
||
availableDomains.push(data.domain);
|
||
|
||
// Mettre à jour l'input actuel s'il existe
|
||
if (currentDomainInput) {
|
||
currentDomainInput.value = data.domain.name;
|
||
const hiddenInput = currentDomainInput.parentElement.querySelector('.element-domain-id');
|
||
if (hiddenInput) {
|
||
hiddenInput.value = data.domain.id;
|
||
}
|
||
// Mettre à jour le résumé
|
||
debouncedUpdateSummary();
|
||
}
|
||
|
||
hideDomainCreationModal();
|
||
showNotification('Domaine créé avec succès !', 'success');
|
||
} else {
|
||
showNotification(data.error || 'Erreur lors de la création du domaine', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur:', error);
|
||
showNotification('Erreur de connexion', 'error');
|
||
}
|
||
}
|
||
|
||
function showNotification(message, type) {
|
||
// Notification améliorée avec toast
|
||
const toast = document.createElement('div');
|
||
toast.className = `fixed top-4 right-4 px-3 py-2 rounded-md text-white font-medium z-50 text-sm ${
|
||
type === 'success' ? 'bg-green-500' : 'bg-red-500'
|
||
}`;
|
||
toast.textContent = message;
|
||
|
||
document.body.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.remove();
|
||
}, 3000);
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const addExerciseBtn = document.getElementById('add-exercise');
|
||
const exercisesContainer = document.getElementById('exercises-container');
|
||
const noExercisesMsg = document.getElementById('no-exercises');
|
||
const form = document.getElementById('unified-form');
|
||
|
||
// Pré-remplir les données en mode édition
|
||
{% if is_edit %}
|
||
loadExistingData();
|
||
{% endif %}
|
||
|
||
// Ajouter un exercice
|
||
addExerciseBtn.addEventListener('click', function() {
|
||
addExercise();
|
||
});
|
||
|
||
// Soumission du formulaire
|
||
form.addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
submitForm();
|
||
});
|
||
|
||
function addExercise() {
|
||
exerciseCounter++;
|
||
const template = document.getElementById('exercise-template');
|
||
const exerciseDiv = template.content.cloneNode(true);
|
||
|
||
// Mettre à jour le numéro d'exercice
|
||
exerciseDiv.querySelector('.exercise-number').textContent = exerciseCounter;
|
||
exerciseDiv.querySelector('.exercise-order').value = exerciseCounter;
|
||
|
||
// Ajouter les event listeners
|
||
const removeBtn = exerciseDiv.querySelector('.remove-exercise');
|
||
removeBtn.addEventListener('click', function() {
|
||
removeExercise(this);
|
||
});
|
||
|
||
const addElementBtn = exerciseDiv.querySelector('.add-grading-element');
|
||
addElementBtn.addEventListener('click', function() {
|
||
addGradingElement(this);
|
||
});
|
||
|
||
exercisesContainer.appendChild(exerciseDiv);
|
||
updateExercisesVisibility();
|
||
|
||
// Mettre à jour le résumé
|
||
debouncedUpdateSummary();
|
||
|
||
// Focus automatique sur le champ titre du nouvel exercice
|
||
setTimeout(() => {
|
||
const titleInput = exercisesContainer.lastElementChild.querySelector('.exercise-title');
|
||
if (titleInput) {
|
||
titleInput.focus();
|
||
// Ajouter listener pour mise à jour résumé
|
||
titleInput.addEventListener('input', debouncedUpdateSummary);
|
||
}
|
||
}, 100);
|
||
}
|
||
|
||
function removeExercise(btn) {
|
||
btn.closest('.exercise-item').remove();
|
||
updateExercisesVisibility();
|
||
renumberExercises();
|
||
// Mettre à jour le résumé
|
||
debouncedUpdateSummary();
|
||
}
|
||
|
||
function addGradingElement(btn) {
|
||
const exerciseDiv = btn.closest('.exercise-item');
|
||
const elementsContainer = exerciseDiv.querySelector('.grading-elements-container');
|
||
const noElementsMsg = exerciseDiv.querySelector('.no-grading-elements');
|
||
|
||
const template = document.getElementById('grading-element-template');
|
||
const elementDiv = template.content.cloneNode(true);
|
||
|
||
// Ajouter l'event listener pour supprimer
|
||
const removeBtn = elementDiv.querySelector('.remove-grading-element');
|
||
removeBtn.addEventListener('click', function() {
|
||
removeGradingElement(this);
|
||
});
|
||
|
||
elementsContainer.appendChild(elementDiv);
|
||
noElementsMsg.style.display = 'none';
|
||
|
||
// Configurer l'autocomplétion des domaines pour ce nouvel élément
|
||
setupDomainAutocomplete(elementsContainer.lastElementChild);
|
||
|
||
// Ajouter listeners pour mise à jour résumé sur tous les champs
|
||
setTimeout(() => {
|
||
const newElement = elementsContainer.lastElementChild;
|
||
if (newElement) {
|
||
newElement.querySelectorAll('input, select').forEach(input => {
|
||
input.addEventListener('input', debouncedUpdateSummary);
|
||
input.addEventListener('change', debouncedUpdateSummary);
|
||
});
|
||
// Mettre à jour le résumé après ajout des listeners
|
||
debouncedUpdateSummary();
|
||
}
|
||
}, 50);
|
||
|
||
// Focus automatique sur le champ label du nouvel élément
|
||
setTimeout(() => {
|
||
const labelInput = elementsContainer.lastElementChild.querySelector('.element-label');
|
||
if (labelInput) {
|
||
labelInput.focus();
|
||
}
|
||
}, 100);
|
||
}
|
||
|
||
function removeGradingElement(btn) {
|
||
const exerciseDiv = btn.closest('.exercise-item');
|
||
const elementsContainer = exerciseDiv.querySelector('.grading-elements-container');
|
||
const noElementsMsg = exerciseDiv.querySelector('.no-grading-elements');
|
||
|
||
btn.closest('.grading-element-item').remove();
|
||
|
||
if (elementsContainer.children.length === 0) {
|
||
noElementsMsg.style.display = 'block';
|
||
}
|
||
|
||
// Mettre à jour le résumé
|
||
debouncedUpdateSummary();
|
||
}
|
||
|
||
function updateExercisesVisibility() {
|
||
const hasExercises = exercisesContainer.children.length > 0;
|
||
noExercisesMsg.style.display = hasExercises ? 'none' : 'block';
|
||
}
|
||
|
||
function renumberExercises() {
|
||
const exercises = exercisesContainer.querySelectorAll('.exercise-item');
|
||
exercises.forEach((exercise, index) => {
|
||
const number = index + 1;
|
||
exercise.querySelector('.exercise-number').textContent = number;
|
||
exercise.querySelector('.exercise-order').value = number;
|
||
});
|
||
exerciseCounter = exercises.length;
|
||
}
|
||
|
||
// La fonction collectFormData est maintenant déclarée en portée globale
|
||
|
||
function submitForm() {
|
||
const formData = new FormData(form);
|
||
const exercises = collectFormData();
|
||
|
||
// Validation côté client
|
||
if (exercises.length === 0) {
|
||
alert('Vous devez ajouter au moins un exercice.');
|
||
return;
|
||
}
|
||
|
||
let hasElementsWithoutGrading = false;
|
||
exercises.forEach(ex => {
|
||
if (ex.grading_elements.length === 0) {
|
||
hasElementsWithoutGrading = true;
|
||
}
|
||
});
|
||
|
||
if (hasElementsWithoutGrading) {
|
||
if (!confirm('Certains exercices n\'ont pas d\'éléments de notation. Voulez-vous continuer ?')) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Envoyer via AJAX
|
||
const data = {
|
||
title: formData.get('title'),
|
||
description: formData.get('description'),
|
||
date: formData.get('date'),
|
||
trimester: formData.get('trimester'),
|
||
class_group_id: formData.get('class_group_id'),
|
||
coefficient: formData.get('coefficient'),
|
||
exercises: exercises
|
||
};
|
||
|
||
// Ajouter le CSRF token aux données
|
||
data.csrf_token = formData.get('csrf_token');
|
||
|
||
fetch(form.action, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(data)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
window.location.href = `/assessments/${data.assessment_id}`;
|
||
} else {
|
||
alert('Erreur lors de la création : ' + JSON.stringify(data.errors));
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Erreur:', error);
|
||
alert('Une erreur est survenue lors de la création de l\'évaluation.');
|
||
});
|
||
}
|
||
|
||
{% if is_edit %}
|
||
function loadExistingData() {
|
||
// Charger les exercices existants
|
||
const existingExercises = {{ exercises_json|tojson if exercises_json else "[]" }};
|
||
|
||
existingExercises.forEach(exercise => {
|
||
exerciseCounter++;
|
||
const template = document.getElementById('exercise-template');
|
||
const exerciseDiv = template.content.cloneNode(true);
|
||
|
||
// Remplir les données de l'exercice
|
||
exerciseDiv.querySelector('.exercise-number').textContent = exerciseCounter;
|
||
exerciseDiv.querySelector('.exercise-title').value = exercise.title;
|
||
exerciseDiv.querySelector('.exercise-description').value = exercise.description || '';
|
||
exerciseDiv.querySelector('.exercise-order').value = exercise.order;
|
||
|
||
// Ajouter les event listeners
|
||
const removeBtn = exerciseDiv.querySelector('.remove-exercise');
|
||
removeBtn.addEventListener('click', function() {
|
||
removeExercise(this);
|
||
});
|
||
|
||
const addElementBtn = exerciseDiv.querySelector('.add-grading-element');
|
||
addElementBtn.addEventListener('click', function() {
|
||
addGradingElement(this);
|
||
});
|
||
|
||
const elementsContainer = exerciseDiv.querySelector('.grading-elements-container');
|
||
const noElementsMsg = exerciseDiv.querySelector('.no-grading-elements');
|
||
|
||
// Charger les éléments de notation existants
|
||
exercise.grading_elements.forEach(element => {
|
||
const elementTemplate = document.getElementById('grading-element-template');
|
||
const elementDiv = elementTemplate.content.cloneNode(true);
|
||
|
||
// Remplir les données de l'élément
|
||
elementDiv.querySelector('.element-label').value = element.label;
|
||
elementDiv.querySelector('.element-skill').value = element.skill || '';
|
||
elementDiv.querySelector('.element-max-points').value = element.max_points;
|
||
elementDiv.querySelector('.element-grading-type').value = element.grading_type;
|
||
elementDiv.querySelector('.element-description').value = element.description || '';
|
||
|
||
// Remplir le domaine s'il existe
|
||
if (element.domain_id) {
|
||
const domainHiddenInput = elementDiv.querySelector('.element-domain-id');
|
||
const domainTextInput = elementDiv.querySelector('.element-domain-input');
|
||
if (domainHiddenInput && domainTextInput) {
|
||
domainHiddenInput.value = element.domain_id;
|
||
// Trouver le nom du domaine correspondant
|
||
const domain = availableDomains.find(d => d.id === element.domain_id);
|
||
if (domain) {
|
||
domainTextInput.value = domain.name;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Ajouter l'event listener pour supprimer
|
||
const removeElementBtn = elementDiv.querySelector('.remove-grading-element');
|
||
removeElementBtn.addEventListener('click', function() {
|
||
removeGradingElement(this);
|
||
});
|
||
|
||
elementsContainer.appendChild(elementDiv);
|
||
|
||
// Configurer l'autocomplétion pour cet élément
|
||
setupDomainAutocomplete(elementsContainer.lastElementChild);
|
||
|
||
// Ajouter listeners pour mise à jour résumé
|
||
const newElement = elementsContainer.lastElementChild;
|
||
newElement.querySelectorAll('input, select').forEach(input => {
|
||
input.addEventListener('input', debouncedUpdateSummary);
|
||
input.addEventListener('change', debouncedUpdateSummary);
|
||
});
|
||
});
|
||
|
||
if (exercise.grading_elements.length > 0) {
|
||
noElementsMsg.style.display = 'none';
|
||
}
|
||
|
||
exercisesContainer.appendChild(exerciseDiv);
|
||
|
||
// Ajouter listeners pour mise à jour résumé sur les champs d'exercice
|
||
const exerciseElement = exercisesContainer.lastElementChild;
|
||
exerciseElement.querySelector('.exercise-title').addEventListener('input', debouncedUpdateSummary);
|
||
});
|
||
|
||
updateExercisesVisibility();
|
||
|
||
// Mettre à jour le résumé initial
|
||
setTimeout(updateSummaryBanner, 500);
|
||
}
|
||
{% endif %}
|
||
|
||
// Initialisation des domaines
|
||
loadDomains();
|
||
|
||
// Fermer le modal avec Escape
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
hideDomainCreationModal();
|
||
}
|
||
});
|
||
|
||
// Configurer l'autocomplétion pour les éléments existants au chargement
|
||
setTimeout(() => {
|
||
document.querySelectorAll('.grading-element-item').forEach(setupDomainAutocomplete);
|
||
}, 500);
|
||
|
||
// Mettre à jour le résumé initial
|
||
setTimeout(updateSummaryBanner, 100);
|
||
|
||
// Ajouter listeners pour mise à jour résumé sur les champs de base
|
||
form.querySelectorAll('input[type="text"], input[type="number"], select, textarea').forEach(input => {
|
||
if (!input.id.includes('csrf')) {
|
||
input.addEventListener('input', debouncedUpdateSummary);
|
||
input.addEventListener('change', debouncedUpdateSummary);
|
||
}
|
||
});
|
||
|
||
// Event listener global avec délégation d'événements pour les champs dynamiques
|
||
form.addEventListener('input', function(e) {
|
||
if (e.target.matches('.element-label, .element-max-points, .element-grading-type, .element-skill, .element-domain-input, .exercise-title')) {
|
||
debouncedUpdateSummary();
|
||
}
|
||
});
|
||
|
||
form.addEventListener('change', function(e) {
|
||
if (e.target.matches('.element-label, .element-max-points, .element-grading-type, .element-skill, .element-domain-input, .exercise-title')) {
|
||
debouncedUpdateSummary();
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
{% endblock %} |