Files
ohmj2/partitions/src/routes/admin/[id]/+page.svelte
T

768 lines
29 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { apiService } from '$lib/api';
import { auth } from '$lib/stores/auth';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { INSTRUMENTS, getInstrument, getDefaultKey, getInstrumentFile } from '$lib/instruments';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import BackLink from '$lib/components/BackLink.svelte';
import Alert from '$lib/components/Alert.svelte';
let scoreId = $derived(page.params.id || '');
let scoreName = $state('');
let scoreCompositor = $state('');
let scoreRessource = $state('');
let loading = $state(true);
let error = $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('');
let saving = $state(false);
// Upload form
let uploadPiece = $state('1');
let uploadInstrument = $state('');
let uploadVersion = $state('1');
let uploadFile: File | null = $state(null);
let uploading = $state(false);
let uploadError = $state('');
let uploadSuccess = $state('');
let selectedScorePieceCount = $state(1);
let selectedScorePieces = $state<{id: string; name: string}[]>([]);
let showAdvanced = $state(false);
let uploadKey = $state('');
let uploadClef = $state('');
let uploadVariant = $state('');
let uploadPart = $state('1');
let uploadWatermark = $state(false);
// Combined parts (`sup`/`par`): list of instruments with their key/clef/part
type CombinedPart = { instrument: string; key: string; clef: string; part: string };
const KEYS = ['sib', 'mib', 'fa', 'do', 'ut', 'sol', 're', 'mi', 'si'];
const CLEFS = ['clesol', 'clefa', 'cleut'];
let combinedParts = $state<CombinedPart[]>([
{ instrument: '', key: '', clef: '', part: '1' }
]);
let uploadCombined = $derived(
combinedParts
.filter((p) => p.instrument)
.map((p) =>
[getInstrumentFile(p.instrument), p.key, p.clef, p.part]
.filter((v) => v !== '')
.join('-')
)
.join('+')
);
let uploadWatermarkPosition = $state<'left' | 'right'>('left');
// Edit pieces
let editingPieces = $state(false);
let piecesEdit = $state<{id: string; name: string}[]>([]);
let savingPieces = $state(false);
async function savePieces() {
savingPieces = true;
try {
const result = await apiService.updatePieces(scoreId, piecesEdit);
if (result.success) {
selectedScorePieces = [...piecesEdit];
editingPieces = false;
} else {
alert('Erreur: ' + result.error);
}
} catch (err) {
console.error(err);
alert('Erreur lors de la sauvegarde');
} finally {
savingPieces = false;
}
}
// Auto-set default key when instrument changes
$effect(() => {
if (uploadInstrument) {
const inst = getInstrument(uploadInstrument);
if (inst?.defaultKey) {
uploadKey = inst.defaultKey;
}
}
});
// File tree
let files: any[] = $state([]);
// Delete modal
let deleteFilePath = $state<string | null>(null);
let deletingFile = $state(false);
async function loadScoreInfo() {
try {
const score = await apiService.getScore(scoreId);
scoreName = score.name;
scoreCompositor = score.compositor || '';
scoreRessource = score.ressource || '';
} catch (err) {
console.error(err);
error = 'Partition non trouvée';
}
}
async function loadFiles() {
try {
const result = await apiService.getScoreFiles(scoreId);
if (result.success) {
files = result.files;
}
} catch (err) {
console.error(err);
}
}
async function handleScoreSelect() {
if (scoreId) {
try {
const pieces = await apiService.getPieces(scoreId);
selectedScorePieceCount = pieces.length;
selectedScorePieces = pieces.map((p: any) => ({
id: String(p.id),
name: p.name || `Partie ${p.id}`
}));
if (selectedScorePieceCount === 1) {
uploadPiece = '1';
}
} catch (err) {
console.error(err);
selectedScorePieceCount = 1;
selectedScorePieces = [];
}
}
}
function getExpectedFilename(): string {
if (!uploadInstrument) return '';
// sup/par: the file is named after the combined instruments (see NAMING.md)
if (uploadInstrument === 'sup' || uploadInstrument === 'par') {
const combined = uploadCombined.trim().toLowerCase();
return combined ? `${combined}.pdf` : '';
}
const inst = getInstrument(uploadInstrument);
const base = inst?.file || uploadInstrument;
const key = uploadKey || inst?.defaultKey || '';
const parts = [base, key, uploadVariant, uploadClef].filter((v) => v !== '');
parts.push(uploadPart);
return `${parts.join('-')}.pdf`;
}
let expectedFilename = $derived(getExpectedFilename());
onMount(async () => {
if (userRole !== 'admin') {
goto('/scores');
return;
}
await loadScoreInfo();
await handleScoreSelect();
await loadFiles();
loading = false;
});
function fileExists(tree: any[], targetPath: string): boolean {
const parts = targetPath.split('/');
let current = tree;
for (const part of parts) {
const found = current.find((item: any) => item.name === part);
if (!found) return false;
if (found.type === 'file') {
return true; // Found the file
}
current = found.children || [];
}
return false;
}
async function uploadPdf() {
if (!uploadFile || !uploadInstrument) {
uploadError = 'Veuillez remplir tous les champs';
return;
}
uploadError = '';
uploadSuccess = '';
// Check if file already exists
const targetPath = `${uploadPiece}/${uploadInstrument}/${uploadVersion}/${expectedFilename}`;
if (fileExists(files, targetPath)) {
uploadError = `Le fichier existe déjà. Supprimez-le d'abord pour le remplacer.`;
return;
}
uploading = true;
try {
const result = await apiService.uploadPdf(
scoreId,
uploadFile,
uploadPiece,
uploadInstrument,
uploadVersion,
uploadKey,
uploadClef,
uploadVariant,
uploadPart,
uploadWatermark,
uploadWatermarkPosition,
uploadCombined
);
if (result.success) {
uploadSuccess = 'Fichier uploadé avec succès';
uploadFile = null;
(document.getElementById('file-input') as HTMLInputElement).value = '';
await loadFiles();
} else {
uploadError = result.error || 'Erreur';
}
} catch (err) {
uploadError = 'Erreur lors de l\'upload';
console.error(err);
} finally {
uploading = false;
}
}
async function deleteFile() {
if (!deleteFilePath) return;
deletingFile = true;
try {
const result = await apiService.deleteScoreFile(scoreId, deleteFilePath);
if (result.success) {
await loadFiles();
} else {
error = result.error || 'Erreur lors de la suppression';
}
} catch (err) {
error = 'Erreur lors de la suppression';
console.error(err);
} finally {
deleteFilePath = null;
deletingFile = false;
}
}
function startEdit(field: string, value: string) {
editingField = field;
editValue = value;
}
function cancelEdit() {
editingField = null;
editValue = '';
}
async function saveEdit(field: string) {
saving = true;
try {
if (field === 'name') {
scoreName = editValue;
} else if (field === 'compositor') {
scoreCompositor = editValue;
} else if (field === 'ressource') {
scoreRessource = editValue;
}
const result = await apiService.updateScore(scoreId, scoreName, scoreCompositor, scoreRessource);
if (!result.success) {
error = result.error || 'Erreur lors de la mise à jour';
await loadScoreInfo();
}
} catch (err) {
error = 'Erreur lors de la mise à jour';
console.error(err);
await loadScoreInfo();
} finally {
editingField = null;
editValue = '';
saving = false;
}
}
</script>
<div class="container mx-auto px-4 py-8">
<div class="mb-6">
<BackLink href="/admin" label="Retour à l'admin" class="mb-4" />
<!-- Info Card -->
<div class="card mb-6 overflow-hidden">
<div class="flex items-center gap-3 border-b border-gray-200 bg-gray-50 px-6 py-4">
<span class="badge bg-ohmj-secondary/20 font-semibold text-ohmj-primary">№ {scoreId}</span>
<h2 class="text-lg font-semibold text-ohmj-primary">Informations de la partition</h2>
</div>
<div class="p-4">
<!-- Name -->
<div class="flex items-center gap-4 py-3 border-b border-gray-100">
<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}
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"
/>
<button onclick={() => saveEdit('name')} disabled={saving} class="text-green-600 hover:text-green-800 p-2 rounded-full hover:bg-green-50" title="Enregistrer">
<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="M5 13l4 4L19 7"></path></svg>
</button>
<button onclick={() => cancelEdit()} disabled={saving} class="text-red-600 hover:text-red-800 p-2 rounded-full hover:bg-red-50" title="Annuler">
<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}
<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'}
</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>
{/if}
</div>
<!-- Compositor -->
<div class="flex items-center gap-4 py-3 border-b border-gray-100">
<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}
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"
/>
<button onclick={() => saveEdit('compositor')} disabled={saving} class="text-green-600 hover:text-green-800 p-2 rounded-full hover:bg-green-50" title="Enregistrer">
<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="M5 13l4 4L19 7"></path></svg>
</button>
<button onclick={() => cancelEdit()} disabled={saving} class="text-red-600 hover:text-red-800 p-2 rounded-full hover:bg-red-50" title="Annuler">
<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}
<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'}
</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>
{/if}
</div>
<!-- Ressource -->
<div class="flex items-center gap-4 py-3">
<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}
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"
/>
<button onclick={() => saveEdit('ressource')} disabled={saving} class="text-green-600 hover:text-green-800 p-2 rounded-full hover:bg-green-50" title="Enregistrer">
<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="M5 13l4 4L19 7"></path></svg>
</button>
<button onclick={() => cancelEdit()} disabled={saving} class="text-red-600 hover:text-red-800 p-2 rounded-full hover:bg-red-50" title="Annuler">
<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}
{#if scoreRessource}
<button type="button" class="flex flex-1 items-center gap-1.5 px-3 py-2 text-left text-sm text-ohmj-primary hover:underline" onclick={() => startEdit('ressource', scoreRessource)} title="Cliquer pour modifier">
<svg class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7.5M10 14 20 4m0 0h-5m5 0v5" />
</svg>
{scoreRessource}
</button>
{:else}
<button type="button" class="flex-1 rounded-lg px-3 py-2 text-left text-sm text-gray-400 hover:bg-gray-50 hover:text-gray-600" onclick={() => startEdit('ressource', '')} title="Cliquer pour ajouter">
Aucune ressource
</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>
</button>
{/if}
</div>
</div>
</div>
<!-- Parties -->
<div class="card mb-6 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50 flex items-center justify-between">
<h2 class="text-lg font-semibold text-ohmj-primary">Parties ({selectedScorePieceCount})</h2>
{#if !editingPieces}
<button onclick={() => { piecesEdit = [...selectedScorePieces]; editingPieces = true; }} class="btn btn-ghost">
Modifier
</button>
{/if}
</div>
<div class="p-4">
{#if editingPieces}
<div class="space-y-3">
{#each piecesEdit as piece, i}
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500 w-8">#{i + 1}</span>
<input
type="text"
bind:value={piece.name}
class="input flex-1"
placeholder="Nom de la partie"
/>
<button onclick={() => piecesEdit = piecesEdit.filter((_, idx) => idx !== i)} class="text-red-500 hover:text-red-700 p-2" title="Supprimer">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
</button>
</div>
{/each}
<button onclick={() => piecesEdit = [...piecesEdit, {id: String(piecesEdit.length + 1), name: ''}]} class="btn btn-ghost">
+ Ajouter une partie
</button>
<div class="flex gap-2 pt-3 border-t">
<button onclick={() => { piecesEdit = [...selectedScorePieces]; editingPieces = false; }} class="btn btn-ghost">Annuler</button>
<button onclick={savePieces} disabled={savingPieces} class="btn btn-primary">
{savingPieces ? 'Enregistrement...' : 'Enregistrer'}
</button>
</div>
</div>
{:else}
<div class="flex flex-wrap gap-2">
{#each selectedScorePieces as piece}
<span class="px-3 py-1 bg-gray-100 rounded-full text-sm">{piece.name || `Partie ${piece.id}`}</span>
{/each}
</div>
{/if}
</div>
</div>
</div>
{#if error}
<Alert message={error} class="mb-4" />
{/if}
{#if !isAdmin}
<Alert message="Accès refusé. Vous devez être administrateur." />
{:else}
<!-- Upload PDF -->
<div class="card mb-8 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h2 class="text-lg font-semibold text-ohmj-primary">Ajouter une partition</h2>
</div>
<div class="p-6">
<form onsubmit={(e) => { e.preventDefault(); uploadPdf(); }} class="space-y-4">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label for="up-instrument" class="label">Instrument</label>
<select
id="up-instrument"
bind:value={uploadInstrument}
required
class="input mt-1"
>
<option value="">Sélectionner...</option>
{#each INSTRUMENTS as inst}
<option value={inst.code}>{inst.name}</option>
{/each}
</select>
</div>
</div>
{#if uploadInstrument === 'sup' || uploadInstrument === 'par'}
<div>
<span class="label">Parties combinées</span>
<div class="mt-1 space-y-2">
{#each combinedParts as p, i}
<div class="flex flex-wrap items-center gap-2">
<select
bind:value={p.instrument}
onchange={() => (p.key = getDefaultKey(p.instrument))}
class="input w-44"
aria-label="Instrument {i + 1}"
>
<option value="">Instrument…</option>
{#each INSTRUMENTS.filter((x) => x.file) as inst}
<option value={inst.code}>{inst.name}</option>
{/each}
</select>
<select bind:value={p.key} class="input w-28" aria-label="Tonalité {i + 1}">
<option value="">-</option>
{#each KEYS as k}<option value={k}>{k}</option>{/each}
</select>
<select bind:value={p.clef} class="input w-28" aria-label="Clé {i + 1}">
<option value="">-</option>
{#each CLEFS as c}<option value={c}>{c}</option>{/each}
</select>
<input
bind:value={p.part}
class="input w-20"
placeholder="Partie"
aria-label="Partie {i + 1}"
/>
{#if combinedParts.length > 1}
<button
type="button"
onclick={() => (combinedParts = combinedParts.filter((_, k) => k !== i))}
class="rounded-full p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700"
title="Retirer"
aria-label="Retirer l'instrument {i + 1}"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
{/if}
</div>
{/each}
</div>
<button
type="button"
onclick={() => (combinedParts = [...combinedParts, { instrument: '', key: '', clef: '', part: '1' }])}
class="btn btn-ghost mt-2 px-2 py-1"
>
+ Ajouter un instrument
</button>
<p class="mt-1 text-xs text-gray-400">
Chaque instrument avec sa tonalité/clé/partie, reliés par « + » dans le nom
(ex. <code>contrebasse-sib-clefa-1+baryton-sib-clefa-1</code>).
</p>
</div>
{/if}
<div>
<button
type="button"
onclick={() => { showAdvanced = !showAdvanced; if (showAdvanced && !uploadKey) { uploadKey = INSTRUMENTS.find(i => i.code === uploadInstrument)?.defaultKey || ''; } }}
class="btn btn-ghost"
>
<svg class="h-4 w-4 transition-transform {showAdvanced ? 'rotate-90' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
{showAdvanced ? 'Masquer les options' : 'Options avancées'}
</button>
</div>
{#if showAdvanced}
<div class="grid grid-cols-2 gap-4 rounded-md border border-gray-200 bg-gray-50 p-3 md:grid-cols-7">
{#if selectedScorePieceCount > 1}
<div>
<label for="up-piece" class="label">Pièce</label>
<select
id="up-piece"
bind:value={uploadPiece}
required
class="input mt-1"
>
{#each selectedScorePieces as piece}
<option value={piece.id}>{piece.name}</option>
{/each}
</select>
</div>
{/if}
<div>
<label for="up-version" class="label">Version</label>
<input
id="up-version"
type="number"
min="1"
bind:value={uploadVersion}
required
class="input mt-1"
placeholder="1"
/>
</div>
<div>
<label for="up-key" class="label">Tonalité</label>
<select id="up-key" bind:value={uploadKey} class="input mt-1">
<option value="">-</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 for="up-clef" class="label">Clé</label>
<select id="up-clef" bind:value={uploadClef} class="input mt-1">
<option value="">-</option>
<option value="clesol">Sol</option>
<option value="clefa">Fa</option>
<option value="cleut">Ut</option>
</select>
</div>
<div>
<label for="up-part" class="label">Partie</label>
<select id="up-part" bind:value={uploadPart} class="input mt-1">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</div>
<div>
<label for="up-variant" class="label">Variante</label>
<select id="up-variant" bind:value={uploadVariant} class="input mt-1">
<option value="">-</option>
<option value="solo">Solo</option>
<option value="default">Default</option>
</select>
</div>
<div class="flex items-end">
<button type="button" onclick={() => showAdvanced = false} class="btn btn-ghost">
Masquer
</button>
</div>
</div>
{/if}
<div>
<label for="file-input" class="label">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" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-ohmj-primary rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-ohmj-primary"></div>
<span class="ml-2 text-sm text-gray-700">Ajouter le numéro</span>
</label>
{#if uploadWatermark}
<select
bind:value={uploadWatermarkPosition}
class="input w-auto"
>
<option value="left">Gauche</option>
<option value="right">Droite</option>
</select>
{/if}
</div>
<input
id="file-input"
type="file"
accept="application/pdf"
onchange={(e) => uploadFile = (e.target as HTMLInputElement).files?.[0] || null}
required
class="mt-1 block w-full text-sm"
/>
</div>
{#if expectedFilename}
<p class="text-sm text-gray-500">Nom attendu: <code class="bg-gray-100 px-1 rounded">{expectedFilename}</code></p>
{/if}
{#if uploadError}
<Alert message={uploadError} />
{/if}
{#if uploadSuccess}
<p class="rounded-md border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
{uploadSuccess}
</p>
{/if}
<button
type="submit"
disabled={uploading}
class="btn btn-primary"
>
{uploading ? 'Upload...' : 'Uploader'}
</button>
</form>
</div>
</div>
<!-- File Tree -->
<div class="card overflow-hidden">
<div class="border-b bg-gray-50 p-4">
<h2 class="text-lg font-semibold text-ohmj-primary">Fichiers</h2>
</div>
<div class="p-4">
{#if files.length === 0}
<p class="py-8 text-center text-gray-500">Aucun fichier uploadé</p>
{:else}
<div class="space-y-3 text-sm">
{#each files as piece}
<div>
<div class="flex items-center gap-1.5 font-medium text-gray-700">
<svg class="h-4 w-4 text-ohmj-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</svg>
{piece.name}/
</div>
{#if piece.children}
<div class="ml-2 mt-1.5 space-y-2 border-l border-gray-200 pl-3">
{#each piece.children as instrument}
<div>
<div class="flex items-center gap-1.5 text-gray-600">
<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</svg>
{instrument.name}/
</div>
{#if instrument.children}
<div class="ml-2 mt-1.5 space-y-2 border-l border-gray-200 pl-3">
{#each instrument.children as version}
<div>
<div class="flex items-center gap-1.5 text-gray-500">
<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</svg>
{version.name}/
</div>
{#if version.children}
<ul class="ml-2 mt-1 space-y-0.5 border-l border-gray-200 pl-3">
{#each version.children as file}
<li class="flex items-center gap-1.5">
<svg class="h-4 w-4 shrink-0 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
<path stroke-linecap="round" stroke-linejoin="round" d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M14 2v6h6" />
</svg>
<span class="truncate font-mono text-gray-600">{file.name}</span>
<button
type="button"
onclick={() => (deleteFilePath = file.path)}
class="ml-1 rounded-full p-1 text-red-500 hover:bg-red-50 hover:text-red-700"
title="Supprimer"
aria-label="Supprimer"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.997-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</li>
{/each}
</ul>
{/if}
</div>
{/each}
</div>
{/if}
</div>
{/each}
</div>
{/if}
</div>
{/each}
</div>
{/if}
</div>
</div>
{/if}
<!-- Delete Confirmation Modal -->
<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>