Files
notytex/templates/assessment_form.html

1247 lines
53 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}