Files
notytex/templates/config/scale.html

777 lines
40 KiB
HTML

{% extends "base.html" %}
{% block title %}Configuration de l'échelle - Notytex{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Breadcrumb -->
<nav class="flex" aria-label="Breadcrumb">
<ol class="flex items-center space-x-4">
<li>
<a href="{{ url_for('config.index') }}" class="text-gray-400 hover:text-gray-500">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/>
</svg>
<span class="sr-only">Configuration</span>
</a>
</li>
<li>
<div class="flex items-center">
<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
</svg>
<span class="ml-4 text-sm font-medium text-gray-500">Échelle de réussite</span>
</div>
</li>
</ol>
</nav>
<!-- En-tête -->
<div class="bg-gradient-to-r from-green-600 to-teal-600 text-white rounded-xl p-8 shadow-lg">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold mb-2">📏 Échelle de réussite</h1>
<p class="text-xl opacity-90">Configurez l'échelle de réussite pour les compétences et domaines</p>
</div>
<div class="hidden md:block">
<div class="w-20 h-20 bg-white/20 rounded-full flex items-center justify-center">
<svg class="w-10 h-10" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2a2 2 0 002-2V5a1 1 0 100-2H3zm6 2a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1zm1 5a1 1 0 100 2h6a1 1 0 100-2h-6zm0 4a1 1 0 100 2h6a1 1 0 100-2h-6z" clip-rule="evenodd"/>
</svg>
</div>
</div>
</div>
</div>
<!-- Information sur l'échelle actuelle -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6">
<div class="flex items-start">
<svg class="w-6 h-6 text-blue-400 mt-0.5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>
<div>
<h3 class="text-sm font-medium text-blue-800">Organisation de l'échelle</h3>
<ul class="text-sm text-blue-700 mt-2 space-y-1">
<li><strong>Échelle numérique :</strong> Valeurs de 0 à N avec progression continue</li>
<li><strong>Valeurs spéciales :</strong> "." pour non évalué et autres valeurs personnalisées</li>
<li><strong>Réglage de l'étendue :</strong> Vous pouvez étendre l'échelle jusqu'à 10</li>
</ul>
</div>
</div>
</div>
<!-- Formulaire de configuration -->
<div class="bg-white rounded-lg shadow-md">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">🎚️ Configuration de l'échelle</h3>
<p class="text-sm text-gray-500 mt-1">{{ competence_scale|length }} valeur(s) configurée(s)</p>
</div>
<form method="POST" action="{{ url_for('config.update_scale') }}" class="p-6">
<!-- Configuration du dégradé de couleurs pour les notes -->
<div class="mb-8">
<div class="mb-4">
<h4 class="text-lg font-medium text-gray-900">🎨 Dégradé de couleurs pour les notes</h4>
<p class="text-sm text-gray-500 mt-1">Définissez les couleurs min et max, le dégradé sera calculé automatiquement pour toutes les valeurs intermédiaires</p>
</div>
<!-- Interface sur une ligne : Min - Dégradé - Max -->
<div class="bg-white border border-gray-200 rounded-lg p-6">
<div class="flex items-center justify-between space-x-6">
<!-- Couleur minimum (0) -->
<div class="flex flex-col items-center space-y-3 min-w-0">
<div class="text-center">
<div class="text-sm font-medium text-gray-700 mb-1">Note 0</div>
<div class="text-xs text-gray-500">Échec</div>
</div>
<div class="flex flex-col items-center space-y-2">
<input
type="color"
id="notes_gradient_color_min"
name="notes_gradient_color_min"
value="{{ notes_gradient.min_color }}"
class="w-16 h-16 border-2 border-gray-300 rounded-lg cursor-pointer shadow-sm hover:border-gray-400"
onchange="updateNotesGradientPreview()"
title="Couleur pour note 0"
>
<input
type="text"
id="notes_gradient_color_min_text"
value="{{ notes_gradient.min_color }}"
class="w-20 px-2 py-1 text-xs text-center border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-purple-500"
pattern="#[0-9a-fA-F]{6}"
onchange="syncNotesGradientColor('min')"
title="Code couleur hexadécimal"
>
</div>
</div>
<!-- Dégradé visuel -->
<div class="flex-1 flex flex-col space-y-3">
<div class="text-center">
<div class="text-sm font-medium text-gray-700 mb-1">Dégradé automatique</div>
<div class="text-xs text-gray-500">Exemples de notes</div>
</div>
<!-- Barre de dégradé -->
<div class="w-full">
<div id="gradient-bar" class="h-8 rounded-lg shadow-inner bg-gray-200"></div>
</div>
<!-- Exemples de notes positionnés sur le dégradé -->
<div class="relative w-full">
<!-- Note 5/20 à 25% -->
<div class="absolute flex flex-col items-center" style="left: 25%; transform: translateX(-50%);">
<div id="notes-preview-25" class="w-8 h-8 rounded border-2 border-white shadow-sm flex items-center justify-center text-white text-xs font-bold">5</div>
<span class="text-xs text-gray-600 mt-1">5/20</span>
</div>
<!-- Note 10/20 à 50% -->
<div class="absolute flex flex-col items-center" style="left: 50%; transform: translateX(-50%);">
<div id="notes-preview-50" class="w-8 h-8 rounded border-2 border-white shadow-sm flex items-center justify-center text-white text-xs font-bold">10</div>
<span class="text-xs text-gray-600 mt-1">10/20</span>
</div>
<!-- Note 15/20 à 75% -->
<div class="absolute flex flex-col items-center" style="left: 75%; transform: translateX(-50%);">
<div id="notes-preview-75" class="w-8 h-8 rounded border-2 border-white shadow-sm flex items-center justify-center text-white text-xs font-bold">15</div>
<span class="text-xs text-gray-600 mt-1">15/20</span>
</div>
<!-- Espacement pour les labels -->
<div class="h-12"></div>
</div>
</div>
<!-- Couleur maximum (max) -->
<div class="flex flex-col items-center space-y-3 min-w-0">
<div class="text-center">
<div class="text-sm font-medium text-gray-700 mb-1">Note max</div>
<div class="text-xs text-gray-500">Réussite</div>
</div>
<div class="flex flex-col items-center space-y-2">
<input
type="color"
id="notes_gradient_color_max"
name="notes_gradient_color_max"
value="{{ notes_gradient.max_color }}"
class="w-16 h-16 border-2 border-gray-300 rounded-lg cursor-pointer shadow-sm hover:border-gray-400"
onchange="updateNotesGradientPreview()"
title="Couleur pour note maximale"
>
<input
type="text"
id="notes_gradient_color_max_text"
value="{{ notes_gradient.max_color }}"
class="w-20 px-2 py-1 text-xs text-center border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-purple-500"
pattern="#[0-9a-fA-F]{6}"
onchange="syncNotesGradientColor('max')"
title="Code couleur hexadécimal"
>
</div>
</div>
</div>
</div>
</div>
<!-- Échelle numérique -->
<div class="mb-8">
<div class="flex items-center justify-between mb-4">
<h4 class="text-lg font-medium text-gray-900">🔢 Échelle numérique (0 à 3)</h4>
</div>
<div class="bg-gray-50 rounded-lg p-4 mb-4">
<p class="text-sm text-gray-600">
Configurez chaque niveau de l'échelle numérique fixe de 0 à 3.
</p>
</div>
<div id="numeric-scale" class="space-y-4">
{% for value, config in competence_scale.items() %}
{% if value in ['0', '1', '2', '3'] %}
<div class="border border-gray-200 rounded-lg p-4" data-numeric-value="{{ value }}">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center space-x-3">
<div
class="w-8 h-8 rounded-full border-2 border-white shadow-sm flex items-center justify-center text-white font-bold text-sm"
style="background-color: {{ config.color }}"
>
{{ value }}
</div>
<h5 class="font-medium text-gray-900">
{% if value == '0' %}
Niveau {{ value }} (minimum)
{% else %}
Niveau {{ value }}
{% endif %}
</h5>
</div>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {% if config.included_in_total %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}">
{% if config.included_in_total %}✅ Inclus{% else %}❌ Exclu{% endif %}
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="scale_{{ value }}_label" class="block text-sm font-medium text-gray-700 mb-1">
Libellé
</label>
<input
type="text"
id="scale_{{ value }}_label"
name="scale_{{ value }}_label"
value="{{ config.label }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
required
>
</div>
<div>
<label for="scale_{{ value }}_color" class="block text-sm font-medium text-gray-700 mb-1">
Couleur
</label>
<div class="flex items-center space-x-2">
<input
type="color"
id="scale_{{ value }}_color"
name="scale_{{ value }}_color"
value="{{ config.color }}"
class="w-10 h-10 border border-gray-300 rounded-md cursor-pointer"
>
<input
type="text"
id="scale_{{ value }}_color_text"
value="{{ config.color }}"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
pattern="#[0-9a-fA-F]{6}"
onchange="syncColor('{{ value }}')"
>
</div>
</div>
<div>
<label for="scale_{{ value }}_included_in_total" class="block text-sm font-medium text-gray-700 mb-1">
Inclusion dans le total
</label>
<select
id="scale_{{ value }}_included_in_total"
name="scale_{{ value }}_included_in_total"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
>
<option value="true" {% if config.included_in_total %}selected{% endif %}>✅ Inclus dans le calcul</option>
<option value="false" {% if not config.included_in_total %}selected{% endif %}>❌ Exclu du calcul</option>
</select>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<!-- Valeurs spéciales -->
<div class="mb-8">
<div class="flex items-center justify-between mb-4">
<h4 class="text-lg font-medium text-gray-900">🔤 Valeurs spéciales</h4>
<button
type="button"
onclick="showAddSpecialValue()"
class="inline-flex items-center px-3 py-1 text-sm border border-gray-300 rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50"
>
<svg class="w-4 h-4 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>
Ajouter
</button>
</div>
<div class="space-y-4">
{% for value, config in competence_scale.items() %}
{% if value not in ['0', '1', '2', '3'] %}
<div class="border border-gray-200 rounded-lg p-4">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center space-x-3">
<div
class="w-8 h-8 rounded-full border-2 border-white shadow-sm flex items-center justify-center text-white font-bold text-sm"
style="background-color: {{ config.color }}"
>
{% if value == '.' %}
{% else %}
{{ value }}
{% endif %}
</div>
<h5 class="font-medium text-gray-900">
{% if value == '.' %}
Valeur "." (Non évalué)
{% else %}
Valeur "{{ value }}"
{% endif %}
</h5>
</div>
<div class="flex items-center space-x-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {% if config.included_in_total %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}">
{% if config.included_in_total %}✅ Inclus{% else %}❌ Exclu{% endif %}
</span>
{% if value != '.' %}
<button
type="button"
onclick="deleteSpecialValue('{{ value }}', '{{ config.label }}')"
class="p-1 text-gray-400 hover:text-red-600 transition-colors"
title="Supprimer cette valeur"
>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
</button>
{% endif %}
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="scale_{{ value }}_label" class="block text-sm font-medium text-gray-700 mb-1">
Libellé
</label>
<input
type="text"
id="scale_{{ value }}_label"
name="scale_{{ value }}_label"
value="{{ config.label }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
required
>
</div>
<div>
<label for="scale_{{ value }}_color" class="block text-sm font-medium text-gray-700 mb-1">
Couleur
</label>
<div class="flex items-center space-x-2">
<input
type="color"
id="scale_{{ value }}_color"
name="scale_{{ value }}_color"
value="{{ config.color }}"
class="w-10 h-10 border border-gray-300 rounded-md cursor-pointer"
>
<input
type="text"
id="scale_{{ value }}_color_text"
value="{{ config.color }}"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
pattern="#[0-9a-fA-F]{6}"
onchange="syncColor('{{ value }}')"
>
</div>
</div>
<div>
<label for="scale_{{ value }}_included_in_total" class="block text-sm font-medium text-gray-700 mb-1">
Inclusion dans le total
</label>
<select
id="scale_{{ value }}_included_in_total"
name="scale_{{ value }}_included_in_total"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
>
<option value="true" {% if config.included_in_total %}selected{% endif %}>✅ Inclus dans le calcul</option>
<option value="false" {% if not config.included_in_total %}selected{% endif %}>❌ Exclu du calcul</option>
</select>
{% if value == '.' %}
<p class="text-xs text-gray-500 mt-1">
Généralement inclus car compte dans le total possible
</p>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<!-- Actions -->
<div class="flex justify-between items-center pt-8 border-t border-gray-200 mt-8">
<a
href="{{ url_for('config.index') }}"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.707 14.707a1 1 0 01-1.414 0L2.586 11H13a1 1 0 110 2H2.586l3.707 3.707a1 1 0 01-1.414 1.414l-5-5a1 1 0 010-1.414l5-5a1 1 0 011.414 1.414L2.586 9H13a1 1 0 110 2H7.707z" clip-rule="evenodd"/>
</svg>
Retour
</a>
<div class="flex space-x-3">
<button
type="button"
onclick="resetToDefaults()"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd"/>
</svg>
Valeurs par défaut
</button>
<button
type="submit"
class="inline-flex items-center px-6 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
Enregistrer les paramètres
</button>
</div>
</div>
</form>
</div>
</div>
<script>
// Synchroniser les champs de couleur
function syncColor(value) {
const colorPicker = document.getElementById(`scale_${value}_color`);
const colorText = document.getElementById(`scale_${value}_color_text`);
if (colorText.value.match(/^#[0-9a-fA-F]{6}$/)) {
colorPicker.value = colorText.value;
}
}
// Synchroniser tous les color pickers avec leurs champs texte
{% for value, config in competence_scale.items() %}
document.getElementById('scale_{{ value }}_color').addEventListener('input', function() {
document.getElementById('scale_{{ value }}_color_text').value = this.value;
});
{% endfor %}
// === GESTION DU DÉGRADÉ DE COULEURS POUR LES NOTES ===
// Synchroniser les champs de couleur du dégradé des notes
function syncNotesGradientColor(type) {
const colorPicker = document.getElementById(`notes_gradient_color_${type}`);
const colorText = document.getElementById(`notes_gradient_color_${type}_text`);
if (colorText.value.match(/^#[0-9a-fA-F]{6}$/)) {
colorPicker.value = colorText.value;
updateNotesGradientPreview();
}
}
// Synchroniser les color pickers avec leurs champs texte
document.addEventListener('DOMContentLoaded', function() {
const minPicker = document.getElementById('notes_gradient_color_min');
const maxPicker = document.getElementById('notes_gradient_color_max');
if (minPicker) {
minPicker.addEventListener('input', function() {
document.getElementById('notes_gradient_color_min_text').value = this.value;
updateNotesGradientPreview();
});
}
if (maxPicker) {
maxPicker.addEventListener('input', function() {
document.getElementById('notes_gradient_color_max_text').value = this.value;
updateNotesGradientPreview();
});
}
// Initialiser la prévisualisation au chargement
updateNotesGradientPreview();
});
// Convertir une couleur hex en RGB
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
// Convertir RGB en hex
function rgbToHex(r, g, b) {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
// Convertir RGB vers HSL
function rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return { h: h * 360, s: s * 100, l: l * 100 };
}
// Convertir HSL vers RGB
function hslToRgb(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
// Interpoler entre deux couleurs en HSL pour un rendu plus naturel
function interpolateColorHSL(color1, color2, factor) {
const rgb1 = hexToRgb(color1);
const rgb2 = hexToRgb(color2);
if (!rgb1 || !rgb2) return color1;
const hsl1 = rgbToHsl(rgb1.r, rgb1.g, rgb1.b);
const hsl2 = rgbToHsl(rgb2.r, rgb2.g, rgb2.b);
// Interpolation en HSL
let h1 = hsl1.h;
let h2 = hsl2.h;
// Gérer la transition des teintes (éviter le long chemin autour du cercle)
let deltaH = h2 - h1;
if (deltaH > 180) {
h2 -= 360;
} else if (deltaH < -180) {
h2 += 360;
}
const h = h1 + (h2 - h1) * factor;
const s = hsl1.s + (hsl2.s - hsl1.s) * factor;
const l = hsl1.l + (hsl2.l - hsl1.l) * factor;
// Normaliser la teinte
const normalizedH = ((h % 360) + 360) % 360;
const rgb = hslToRgb(normalizedH, s, l);
return rgbToHex(rgb.r, rgb.g, rgb.b);
}
// Fonction de compatibilité (utilise maintenant HSL)
function interpolateColor(color1, color2, factor) {
return interpolateColorHSL(color1, color2, factor);
}
// Calculer la couleur d'une note selon sa valeur relative
function calculateNoteColor(note, maxPoints, minColor, maxColor) {
if (!minColor || !maxColor || maxPoints <= 0) return '#6b7280'; // Gris par défaut
const factor = Math.max(0, Math.min(1, note / maxPoints)); // Borner entre 0 et 1
return interpolateColor(minColor, maxColor, factor);
}
// Mettre à jour la prévisualisation du dégradé des notes
function updateNotesGradientPreview() {
const minColor = document.getElementById('notes_gradient_color_min')?.value;
const maxColor = document.getElementById('notes_gradient_color_max')?.value;
if (!minColor || !maxColor) return;
// Créer une barre de dégradé avec plusieurs stops HSL pour éviter le gris CSS
const gradientBar = document.getElementById('gradient-bar');
if (gradientBar) {
// Créer 10 stops pour un dégradé fluide en HSL
const stops = [];
for (let i = 0; i <= 10; i++) {
const factor = i / 10;
const color = interpolateColorHSL(minColor, maxColor, factor);
stops.push(`${color} ${factor * 100}%`);
}
gradientBar.style.background = `linear-gradient(to right, ${stops.join(', ')})`;
}
// Exemples sur 20 points (25%, 50%, 75% seulement car 0% et 100% sont déjà visibles)
const examples = [
{ percent: 0.25, note: 5, id: 'notes-preview-25' },
{ percent: 0.5, note: 10, id: 'notes-preview-50' },
{ percent: 0.75, note: 15, id: 'notes-preview-75' }
];
examples.forEach(example => {
const preview = document.getElementById(example.id);
if (preview) {
const color = interpolateColorHSL(minColor, maxColor, example.percent);
preview.style.backgroundColor = color;
// Ajuster la couleur du texte pour la lisibilité
const rgb = hexToRgb(color);
if (rgb) {
const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
preview.style.color = brightness > 128 ? '#000000' : '#ffffff';
}
}
});
}
// Sauvegarder la configuration du dégradé des notes
function saveNotesGradientConfig() {
const minColor = document.getElementById('notes_gradient_color_min').value;
const maxColor = document.getElementById('notes_gradient_color_max').value;
if (!minColor || !maxColor) {
alert('Veuillez sélectionner les couleurs minimum et maximum');
return;
}
if (!confirm('Sauvegarder cette configuration de dégradé pour les notes ?')) {
return;
}
// Créer un formulaire pour envoyer les données
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ url_for("config.save_notes_gradient") }}';
const minInput = document.createElement('input');
minInput.type = 'hidden';
minInput.name = 'notes_gradient_min_color';
minInput.value = minColor;
const maxInput = document.createElement('input');
maxInput.type = 'hidden';
maxInput.name = 'notes_gradient_max_color';
maxInput.value = maxColor;
form.appendChild(minInput);
form.appendChild(maxInput);
document.body.appendChild(form);
form.submit();
}
// Fonction globale pour calculer la couleur d'une note (utilisable ailleurs)
window.getNotesGradientColor = function(note, maxPoints) {
// Récupérer les couleurs configurées
const minColor = '{{ notes_gradient.min_color }}';
const maxColor = '{{ notes_gradient.max_color }}';
const enabled = {{ notes_gradient.enabled|tojson }};
// Si le dégradé n'est pas activé, retourner une couleur neutre
if (!enabled) return '#6b7280';
return calculateNoteColor(note, maxPoints, minColor, maxColor);
};
// Réinitialiser aux valeurs par défaut
function resetToDefaults() {
if (confirm('Êtes-vous sûr de vouloir restaurer l\'échelle par défaut ? Toutes vos modifications seront perdues.')) {
fetch('{{ url_for("config.reset_scale") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}).then(response => {
if (response.ok) {
location.reload();
}
});
}
}
// Ajouter une valeur spéciale
function showAddSpecialValue() {
const value = prompt('Entrez la valeur spéciale (ex: NA, X, etc.) :');
if (value && value.trim()) {
const label = prompt('Entrez le libellé pour cette valeur :');
if (label && label.trim()) {
const color = prompt('Entrez la couleur (hex, ex: #ff0000) :', '#6b7280');
const included = confirm('Cette valeur doit-elle être incluse dans le calcul du total ?');
// Créer un formulaire temporaire pour envoyer les données
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ url_for("config.add_scale_value") }}';
const valueInput = document.createElement('input');
valueInput.type = 'hidden';
valueInput.name = 'value';
valueInput.value = value.trim();
const labelInput = document.createElement('input');
labelInput.type = 'hidden';
labelInput.name = 'label';
labelInput.value = label.trim();
const colorInput = document.createElement('input');
colorInput.type = 'hidden';
colorInput.name = 'color';
colorInput.value = color || '#6b7280';
const includedInput = document.createElement('input');
includedInput.type = 'hidden';
includedInput.name = 'included_in_total';
includedInput.value = included ? 'true' : 'false';
form.appendChild(valueInput);
form.appendChild(labelInput);
form.appendChild(colorInput);
form.appendChild(includedInput);
document.body.appendChild(form);
form.submit();
}
}
}
// Supprimer une valeur spéciale
function deleteSpecialValue(value, label) {
if (confirm(`Êtes-vous sûr de vouloir supprimer la valeur "${value}" (${label}) ?`)) {
const form = document.createElement('form');
form.method = 'POST';
form.action = `{{ url_for("config.delete_scale_value", value="PLACEHOLDER") }}`.replace('PLACEHOLDER', encodeURIComponent(value));
document.body.appendChild(form);
form.submit();
}
}
</script>
{% endblock %}