[FIX] Uge session of debugging and code coverage.

This commit is contained in:
NADAL Jean-Baptiste
2026-09-14 17:22:18 +02:00
parent e79c865f8c
commit a38fc0bfa9
35 changed files with 2315 additions and 1092 deletions
+55 -97
View File
@@ -3,42 +3,23 @@
import { apiService } from '$lib/api';
import { auth } from '$lib/stores/auth';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import { INSTRUMENTS, getInstrument } from '$lib/instruments';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
const INSTRUMENTS = [
{ code: 'dir', name: 'Direction', defaultKey: '' },
{ code: 'pic', name: 'Piccolo', defaultKey: 'do' },
{ code: 'flu', name: 'Flûte', defaultKey: 'do' },
{ code: 'cla', name: 'Clarinette', defaultKey: 'sib' },
{ code: 'clb', name: 'Clarinette Basse', defaultKey: 'sib' },
{ code: 'sax', name: 'Saxophone Alto', defaultKey: 'mib' },
{ code: 'sat', name: 'Saxophone Ténor', defaultKey: 'sib' },
{ code: 'sab', name: 'Saxophone Baryton', defaultKey: 'mib' },
{ code: 'coa', name: 'Cor Anglais', defaultKey: 'fa' },
{ code: 'htb', name: 'Hautbois', defaultKey: 'do' },
{ code: 'bas', name: 'Basson', defaultKey: 'do' },
{ code: 'cor', name: 'Cor', defaultKey: 'fa' },
{ code: 'trp', name: 'Trompette', defaultKey: 'sib' },
{ code: 'crn', name: 'Cornet', defaultKey: 'sib' },
{ code: 'trb', name: 'Trombone', defaultKey: 'do' },
{ code: 'eup', name: 'Euphonium', defaultKey: 'sib' },
{ code: 'tub', name: 'Tuba', defaultKey: 'do' },
{ code: 'cba', name: 'Contrebasse', defaultKey: 'sib' },
{ code: 'per', name: 'Percussions', defaultKey: '' },
{ code: 'pia', name: 'Piano', defaultKey: '' },
{ code: 'har', name: 'Harpe', defaultKey: '' },
{ code: 'sup', name: 'Parties supplémentaires', defaultKey: '' }
];
let scoreId = $derived($page.params.id || '');
let scoreId = $derived(page.params.id || '');
let scoreName = $state('');
let scoreCompositor = $state('');
let scoreRessource = $state('');
let loading = $state(true);
let error = $state('');
let userRole = $state('');
let userRole = $derived($auth.user?.role || '');
let isAdmin = $derived(userRole === 'admin');
function focusOnMount(node: HTMLElement) {
node.focus();
}
// Edit mode
let editingField = $state<string | null>(null);
let editValue = $state('');
@@ -88,7 +69,7 @@
// Auto-set default key when instrument changes
$effect(() => {
if (uploadInstrument) {
const inst = INSTRUMENTS.find(i => i.code === uploadInstrument);
const inst = getInstrument(uploadInstrument);
if (inst?.defaultKey) {
uploadKey = inst.defaultKey;
}
@@ -147,7 +128,7 @@
function getExpectedFilename(): string {
if (!uploadInstrument) return '';
const inst = INSTRUMENTS.find(i => i.code === uploadInstrument);
const inst = getInstrument(uploadInstrument);
const instName = inst ? inst.name.toLowerCase().replace(/ /g, '_') : uploadInstrument;
const key = uploadKey || inst?.defaultKey || '';
const keyStr = key ? `_${key}` : '';
@@ -159,18 +140,11 @@
let expectedFilename = $derived(getExpectedFilename());
onMount(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
const unsubscribe = auth.subscribe((state) => {
userRole = state.user?.role || '';
});
if (userRole !== 'admin') {
unsubscribe();
goto('/scores');
return;
}
await loadScoreInfo();
await handleScoreSelect();
await loadFiles();
@@ -311,12 +285,12 @@
<!-- Name -->
<div class="flex items-center gap-4 py-3 border-b border-gray-100">
<label class="w-32 text-sm font-semibold text-gray-600 flex-shrink-0">Nom</label>
<span class="w-32 text-sm font-semibold text-gray-600 flex-shrink-0">Nom</span>
{#if editingField === 'name'}
<input
type="text"
bind:value={editValue}
autofocus
use:focusOnMount
onkeydown={(e) => { if (e.key === 'Enter') saveEdit('name'); if (e.key === 'Escape') cancelEdit(); }}
class="flex-1 text-xl font-bold text-ohmj-primary border-b-2 border-ohmj-primary bg-transparent px-3 py-2 focus:outline-none"
/>
@@ -327,9 +301,9 @@
<svg class="w-5 h-5" 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>
{:else}
<span class="flex-1 text-xl font-bold text-ohmj-primary cursor-pointer hover:text-ohmj-secondary px-3 py-2 rounded-lg hover:bg-gray-50" onclick={() => startEdit('name', scoreName)} title="Cliquer pour modifier">
<button type="button" class="flex-1 text-left text-xl font-bold text-ohmj-primary cursor-pointer hover:text-ohmj-secondary px-3 py-2 rounded-lg hover:bg-gray-50" onclick={() => startEdit('name', scoreName)} title="Cliquer pour modifier">
{scoreName || 'Sans titre'}
</span>
</button>
<button onclick={() => startEdit('name', scoreName)} class="text-gray-400 hover:text-ohmj-primary p-2 rounded-full hover:bg-gray-100" title="Modifier">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
</button>
@@ -338,12 +312,12 @@
<!-- Compositor -->
<div class="flex items-center gap-4 py-3 border-b border-gray-100">
<label class="w-32 text-sm font-semibold text-gray-600 flex-shrink-0">Compositeur</label>
<span class="w-32 text-sm font-semibold text-gray-600 flex-shrink-0">Compositeur</span>
{#if editingField === 'compositor'}
<input
type="text"
bind:value={editValue}
autofocus
use:focusOnMount
placeholder="Compositeur"
onkeydown={(e) => { if (e.key === 'Enter') saveEdit('compositor'); if (e.key === 'Escape') cancelEdit(); }}
class="flex-1 text-lg text-gray-700 border-b-2 border-ohmj-primary bg-transparent px-3 py-2 focus:outline-none"
@@ -355,9 +329,9 @@
<svg class="w-5 h-5" 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>
{:else}
<span class="flex-1 text-lg text-gray-700 cursor-pointer hover:text-ohmj-primary px-3 py-2 rounded-lg hover:bg-gray-50" onclick={() => startEdit('compositor', scoreCompositor)} title="Cliquer pour modifier">
<button type="button" class="flex-1 text-left text-lg text-gray-700 cursor-pointer hover:text-ohmj-primary px-3 py-2 rounded-lg hover:bg-gray-50" onclick={() => startEdit('compositor', scoreCompositor)} title="Cliquer pour modifier">
{scoreCompositor || 'Aucun compositeur'}
</span>
</button>
<button onclick={() => startEdit('compositor', scoreCompositor)} class="text-gray-400 hover:text-ohmj-primary p-2 rounded-full hover:bg-gray-100" title="Modifier">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
</button>
@@ -366,12 +340,12 @@
<!-- Ressource -->
<div class="flex items-center gap-4 py-3">
<label class="w-32 text-sm font-semibold text-gray-600 flex-shrink-0">Ressource</label>
<span class="w-32 text-sm font-semibold text-gray-600 flex-shrink-0">Ressource</span>
{#if editingField === 'ressource'}
<input
type="url"
bind:value={editValue}
autofocus
use:focusOnMount
placeholder="URL (YouTube, Google Drive, etc.)"
onkeydown={(e) => { if (e.key === 'Enter') saveEdit('ressource'); if (e.key === 'Escape') cancelEdit(); }}
class="flex-1 text-sm text-blue-600 border-b-2 border-ohmj-primary bg-transparent px-3 py-2 focus:outline-none"
@@ -384,13 +358,13 @@
</button>
{:else}
{#if scoreRessource}
<span class="flex-1 text-sm text-blue-600 cursor-pointer hover:text-blue-800 px-3 py-2 rounded-lg hover:bg-gray-50" onclick={() => startEdit('ressource', scoreRessource)} title="Cliquer pour modifier">
<button type="button" class="flex-1 text-left text-sm text-blue-600 cursor-pointer hover:text-blue-800 px-3 py-2 rounded-lg hover:bg-gray-50" onclick={() => startEdit('ressource', scoreRessource)} title="Cliquer pour modifier">
🔗 {scoreRessource}
</span>
</button>
{:else}
<span class="flex-1 text-sm text-gray-400 cursor-pointer hover:text-gray-600 px-3 py-2 rounded-lg hover:bg-gray-50" onclick={() => startEdit('ressource', '')} title="Cliquer pour ajouter">
<button type="button" class="flex-1 text-left text-sm text-gray-400 cursor-pointer hover:text-gray-600 px-3 py-2 rounded-lg hover:bg-gray-50" onclick={() => startEdit('ressource', '')} title="Cliquer pour ajouter">
Aucune ressource
</span>
</button>
{/if}
<button onclick={() => startEdit('ressource', scoreRessource)} class="text-gray-400 hover:text-ohmj-primary p-2 rounded-full hover:bg-gray-100" title="Modifier">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
@@ -468,8 +442,9 @@
<form onsubmit={(e) => { e.preventDefault(); uploadPdf(); }} class="space-y-4">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Instrument</label>
<label for="up-instrument" class="block text-sm font-medium text-gray-700">Instrument</label>
<select
id="up-instrument"
bind:value={uploadInstrument}
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white"
@@ -496,8 +471,9 @@
<div class="grid grid-cols-2 md:grid-cols-7 gap-4 p-3 bg-gray-100 rounded">
{#if selectedScorePieceCount > 1}
<div>
<label class="block text-sm font-medium text-gray-700">Pièce</label>
<label for="up-piece" class="block text-sm font-medium text-gray-700">Pièce</label>
<select
id="up-piece"
bind:value={uploadPiece}
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white"
@@ -509,8 +485,9 @@
</div>
{/if}
<div>
<label class="block text-sm font-medium text-gray-700">Version</label>
<label for="up-version" class="block text-sm font-medium text-gray-700">Version</label>
<input
id="up-version"
type="number"
min="1"
bind:value={uploadVersion}
@@ -520,19 +497,19 @@
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Tonalité</label>
<select bind:value={uploadKey} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<label for="up-key" class="block text-sm font-medium text-gray-700">Tonalité</label>
<select id="up-key" bind:value={uploadKey} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<option value="">-</option>
<option value="sib">Si♭ (sib)</option>
<option value="mib">Mi♭ (mif)</option>
<option value="fa">Fa (fa)</option>
<option value="do">Do (ut)</option>
<option value="sol">Sol</option>
<option value="sib">Si♭ (B♭)</option>
<option value="mib">Mi♭ (E♭)</option>
<option value="fa">Fa (F)</option>
<option value="do">Do (C/Ut)</option>
<option value="sol">Sol (G)</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Clé</label>
<select bind:value={uploadClef} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<label for="up-clef" class="block text-sm font-medium text-gray-700">Clé</label>
<select id="up-clef" bind:value={uploadClef} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<option value="">-</option>
<option value="clesol">Sol</option>
<option value="clefa">Fa</option>
@@ -540,8 +517,8 @@
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Partie</label>
<select bind:value={uploadPart} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<label for="up-part" class="block text-sm font-medium text-gray-700">Partie</label>
<select id="up-part" bind:value={uploadPart} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
@@ -549,8 +526,8 @@
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Variante</label>
<select bind:value={uploadVariant} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<label for="up-variant" class="block text-sm font-medium text-gray-700">Variante</label>
<select id="up-variant" bind:value={uploadVariant} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<option value="">-</option>
<option value="solo">Solo</option>
<option value="default">Default</option>
@@ -569,7 +546,7 @@
{/if}
<div>
<label class="block text-sm font-medium text-gray-700">Fichier PDF</label>
<label for="file-input" class="block text-sm font-medium text-gray-700">Fichier PDF</label>
<div class="flex items-center gap-3 mt-1">
<label class="relative inline-flex items-center cursor-pointer whitespace-nowrap">
<input type="checkbox" bind:checked={uploadWatermark} class="sr-only peer" />
@@ -682,32 +659,13 @@
{/if}
<!-- Delete Confirmation Modal -->
{#if deleteFilePath}
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" onclick={() => deleteFilePath = null}>
<div class="bg-white rounded-lg shadow-xl p-6 max-w-md w-full mx-4" onclick={(e) => e.stopPropagation()}>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Confirmer la suppression</h3>
<p class="text-gray-600 mb-6">
Êtes-vous sûr de vouloir supprimer ce fichier ? Cette action est irréversible.
</p>
<p class="text-sm text-gray-500 mb-6 font-mono bg-gray-100 p-2 rounded">
{deleteFilePath}
</p>
<div class="flex gap-3 justify-end">
<button
onclick={() => deleteFilePath = null}
class="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded transition-colors"
>
Annuler
</button>
<button
onclick={deleteFile}
disabled={deletingFile}
class="px-4 py-2 bg-red-600 text-white hover:bg-red-700 rounded transition-colors disabled:opacity-50"
>
{deletingFile ? 'Suppression...' : 'Supprimer'}
</button>
</div>
</div>
</div>
{/if}
<ConfirmDialog
open={!!deleteFilePath}
title="Confirmer la suppression"
message="Êtes-vous sûr de vouloir supprimer ce fichier ? Cette action est irréversible."
detail={deleteFilePath ?? ''}
busy={deletingFile}
onconfirm={deleteFile}
oncancel={() => (deleteFilePath = null)}
/>
</div>