Files
notytex/templates/assessment_form_unified.html
2025-08-06 20:34:55 +02:00

1031 lines
46 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 content %}
<div class="max-w-6xl mx-auto">
<div class="mb-6">
{% 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-6 py-4 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 complète avec exercices et éléments de notation</p>
{% else %}
<p class="text-sm text-gray-600 mt-1">Créez votre évaluation complète avec exercices et éléments de notation</p>
{% endif %}
</div>
<form id="unified-form" method="POST" class="px-6 py-6 space-y-8">
{{ form.hidden_tag() }}
<!-- Section Évaluation -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6">
<h2 class="text-lg font-medium text-blue-900 mb-4">📝 Informations de l'évaluation</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<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-2 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-2 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-2 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>
<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-2 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-2 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>
<div class="mt-4">
<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-2 focus:ring-blue-500 focus:border-blue-500", rows="3") }}
{% 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>
<!-- Section Exercices -->
<div class="bg-green-50 border border-green-200 rounded-lg p-6">
<div class="mb-4">
<h2 class="text-lg font-medium text-green-900">🏃 Exercices</h2>
</div>
<div id="exercises-container" class="space-y-4">
<!-- Les exercices seront ajoutés ici dynamiquement -->
</div>
<div id="no-exercises" class="text-center py-8 text-green-700">
<p class="text-sm">Aucun exercice ajouté. Utilisez le bouton ci-dessous pour commencer.</p>
</div>
<!-- Bouton d'ajout placé après la liste pour une meilleure navigation clavier -->
<div class="mt-4 pt-4 border-t border-green-200">
<button type="button" id="add-exercise" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 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 -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-2">💡 Guide rapide</h3>
<div class="text-xs text-gray-700 space-y-1">
<p><strong>Points :</strong> Notation classique (ex: 2.5/4 points)</p>
<p><strong>Score :</strong> Évaluation par niveaux (0=non acquis, 1=en cours, 2=acquis, 3=expert, .=non évalué)</p>
<p><strong>Ordre :</strong> Les exercices et éléments seront affichés dans l'ordre numérique</p>
</div>
</div>
<!-- Actions -->
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
{% if is_edit %}
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="px-6 py-2 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-6 py-2 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 complète
</button>
{% else %}
<a href="{{ url_for('assessments.list') }}" class="px-6 py-2 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-6 py-2 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 complète
</button>
{% endif %}
</div>
</form>
</div>
</div>
<!-- Template pour un exercice -->
<template id="exercise-template">
<div class="exercise-item border border-green-300 rounded-lg p-4 bg-white">
<div class="flex justify-between items-start mb-4">
<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-2 gap-4 mb-4">
<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-2 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-2 focus:ring-green-500 focus:border-green-500" min="1" required>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Description (optionnel)</label>
<textarea class="exercise-description block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-green-500 focus:border-green-500" rows="2"></textarea>
</div>
<!-- Éléments de notation -->
<div class="bg-purple-50 border border-purple-200 rounded p-4">
<div class="mb-3">
<h4 class="text-sm font-medium text-purple-900">Éléments de notation</h4>
</div>
<div class="grading-elements-container space-y-3">
<!-- Les éléments de notation seront ajoutés ici -->
</div>
<div class="no-grading-elements text-center py-4 text-purple-700 text-xs">
Aucun élément de notation. Utilisez le bouton ci-dessous pour commencer.
</div>
<!-- Bouton d'ajout placé après la liste pour une meilleure navigation clavier -->
<div class="mt-3 pt-3 border-t border-purple-200">
<button type="button" class="add-grading-element bg-purple-600 hover:bg-purple-700 text-white px-3 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 -->
<template id="grading-element-template">
<div class="grading-element-item border border-purple-300 rounded p-3 bg-white">
<div class="flex justify-between items-start mb-3">
<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-2 gap-3">
<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="Saisissez un 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-48 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 de notation</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="">Choisir...</option>
<option value="notes">Points</option>
<option value="score">Score</option>
</select>
</div>
</div>
<div class="mt-3">
<label class="block text-xs font-medium text-gray-700 mb-1">Description (optionnel)</label>
<textarea 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" rows="2"></textarea>
</div>
</div>
</template>
<!-- Modal pour création de domaine -->
<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-5 border w-96 shadow-lg rounded-md bg-white">
<div class="mt-3">
<div class="flex items-center justify-between mb-4">
<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-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Nom du domaine</label>
<input type="text" id="new-domain-name" class="block w-full border border-gray-300 rounded-md px-3 py-2 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-2">Couleur associée</label>
<div class="flex flex-wrap gap-2 mb-3">
<button type="button" class="color-option w-8 h-8 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-8 h-8 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-8 h-8 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-8 h-8 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-8 h-8 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-8 h-8 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-8 h-8 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-8 h-8 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-10 border border-gray-300 rounded cursor-pointer" value="#3b82f6">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Description (optionnel)</label>
<textarea id="new-domain-description" class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-purple-500 focus:border-purple-500" rows="2" placeholder="Description du domaine..."></textarea>
</div>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button type="button" class="close-modal px-4 py-2 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-4 py-2 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';
// 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);
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-3 py-2 text-sm cursor-pointer hover:bg-purple-100 border-t border-gray-200';
createDiv.innerHTML = `
<div class="flex items-center">
<svg class="w-4 h-4 text-purple-600 mr-2" 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">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-3 py-2 text-sm cursor-pointer hover:bg-gray-100 flex items-center';
const colorIndicator = `<div class="w-3 h-3 rounded-full mr-2" style="background-color: ${domain.color}"></div>`;
const highlightedName = highlightMatch(domain.name, query);
div.innerHTML = `${colorIndicator}<span>${highlightedName}</span>`;
div.addEventListener('mousedown', function(e) {
e.preventDefault();
inputElement.value = domain.name;
hiddenElement.value = domain.id;
hideSuggestions(suggestionsElement);
inputElement.focus();
});
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;
}
}
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-4 py-2 rounded-md text-white font-medium z-50 ${
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();
// Focus automatique sur le champ titre du nouvel exercice pour faciliter la navigation clavier
setTimeout(() => {
const titleInput = exercisesContainer.lastElementChild.querySelector('.exercise-title');
if (titleInput) {
titleInput.focus();
}
}, 100);
}
function removeExercise(btn) {
btn.closest('.exercise-item').remove();
updateExercisesVisibility();
renumberExercises();
}
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);
// Focus automatique sur le champ label du nouvel élément pour faciliter la navigation clavier
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';
}
}
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;
}
function collectFormData() {
const exercises = [];
const exerciseItems = exercisesContainer.querySelectorAll('.exercise-item');
exerciseItems.forEach(exerciseItem => {
const title = exerciseItem.querySelector('.exercise-title').value;
const description = exerciseItem.querySelector('.exercise-description').value;
const order = parseInt(exerciseItem.querySelector('.exercise-order').value);
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 maxPoints = parseFloat(elementItem.querySelector('.element-max-points').value);
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 || !gradingType) return;
const elementData = {
label: label.trim(),
skill: skill.trim(),
max_points: maxPoints,
grading_type: gradingType,
description: description.trim()
};
// 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;
}
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);
});
if (exercise.grading_elements.length > 0) {
noElementsMsg.style.display = 'none';
}
exercisesContainer.appendChild(exerciseDiv);
});
updateExercisesVisibility();
}
// 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);
{% endif %}
});
</script>
{% endblock %}