From 2c170f9ef252c032dc0161fc453325413d3ea0fa Mon Sep 17 00:00:00 2001 From: Etienne Delvarre Date: Wed, 3 Jun 2026 15:34:27 +0200 Subject: [PATCH] =?UTF-8?q?Archives=20=E2=80=94=20panneau=20des=20fiches?= =?UTF-8?q?=20supprim=C3=A9es=20avec=20restauration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bouton Archives dans la barre de filtres de la frise. Panneau latéral listant les fiches supprimées avec aperçu complet. Restauration recrée la fiche dans la frise. Co-Authored-By: Claude Opus 4.6 --- public/index.html | 114 ++++++++++++++++++++++++++++++++++++++++++---- server.js | 13 ++++++ 2 files changed, 117 insertions(+), 10 deletions(-) diff --git a/public/index.html b/public/index.html index 311fcf0..426485b 100644 --- a/public/index.html +++ b/public/index.html @@ -604,6 +604,7 @@ window.dispatchEvent(new Event('tiptap-loaded')); +
@@ -3168,26 +3169,119 @@ function selectVersion(id) { async function restoreVersion() { if (!selectedVersionId) return; - if (!confirm('Restaurer cette version ? L\u2019état actuel sera sauvegardé avant la restauration.')) return; + const isArchive = versionType === 'archive'; + const msg = isArchive + ? 'Restaurer cette fiche supprimée ? Elle sera recréée dans la frise.' + : 'Restaurer cette version ? L\u2019état actuel sera sauvegardé avant la restauration.'; + if (!confirm(msg)) return; try { - const r = await fetch(`/api/versions/${selectedVersionId}/restore`, { method: 'POST' }); - if (r.ok) { - closeVersionPanel(); - if (versionType === 'frise') { + if (isArchive) { + // Re-create the card from snapshot + const v = versionList.find(x => x.Id === selectedVersionId); + if (!v) return; + const snap = JSON.parse(v.Snapshot); + const { Title, Acte, Ordre, Personnage, Tags, Notes } = snap; + const data = { Title, Acte: Acte || 'En attente', Ordre: Ordre || 999, Personnage, Tags, Notes, 'Résumé': snap['Résumé'], 'Intérêt narratif': snap['Intérêt narratif'], Implications: snap.Implications, Origine: snap.Origine, 'Lien rédaction': snap['Lien rédaction'] }; + const r = await fetch('/api/records', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); + if (r.ok) { + closeVersionPanel(); await loadData(); - if (editingId) openEdit(editingId); - } else if (versionType === 'redaction' && currentSceneId) { - const scene = records.find(rec => rec.Id === currentSceneId); - if (scene) openScene(scene); + } else { + alert('Erreur lors de la restauration'); } } else { - alert('Erreur lors de la restauration'); + const r = await fetch(`/api/versions/${selectedVersionId}/restore`, { method: 'POST' }); + if (r.ok) { + closeVersionPanel(); + if (versionType === 'frise') { + await loadData(); + if (editingId) openEdit(editingId); + } else if (versionType === 'redaction' && currentSceneId) { + const scene = records.find(rec => rec.Id === currentSceneId); + if (scene) openScene(scene); + } + } else { + alert('Erreur lors de la restauration'); + } } } catch (e) { alert('Erreur : ' + e.message); } } +// ── Archives (deleted cards) ── +async function openArchives() { + versionType = 'archive'; + versionRecordId = null; + selectedVersionId = null; + document.getElementById('version-panel-title').textContent = 'Archives — Fiches supprimées'; + document.getElementById('version-list').innerHTML = '
Chargement...
'; + document.getElementById('version-preview').style.display = 'none'; + document.getElementById('version-actions').style.display = 'none'; + document.getElementById('version-panel').classList.add('open'); + + try { + const r = await fetch('/api/archives'); + const data = await r.json(); + versionList = data.list || []; + renderArchiveList(); + } catch (e) { + document.getElementById('version-list').innerHTML = '
Erreur de chargement
'; + } +} + +function renderArchiveList() { + const container = document.getElementById('version-list'); + if (versionList.length === 0) { + container.innerHTML = '
Aucune fiche supprimée.
'; + return; + } + container.innerHTML = ''; + for (const v of versionList) { + const div = document.createElement('div'); + div.className = 'version-item'; + div.dataset.id = v.Id; + const date = new Date(v.CreatedAt); + const dateStr = date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }); + let title = '', preview = ''; + try { + const snap = JSON.parse(v.Snapshot); + title = snap.Title || snap.Titre || 'Sans titre'; + preview = [snap.Acte, snap['Résumé']].filter(Boolean).join(' — '); + } catch (e) { title = 'Fiche'; } + div.innerHTML = `
${dateStr}
` + + `
${title.replace(/` + + (preview ? `
${preview.replace(/` : ''); + div.onclick = () => selectArchive(v.Id); + container.appendChild(div); + } +} + +function selectArchive(id) { + selectedVersionId = id; + versionType = 'archive'; + document.querySelectorAll('.version-item').forEach(el => el.classList.toggle('selected', Number(el.dataset.id) === id)); + const v = versionList.find(x => x.Id === id); + if (!v) return; + try { + const snap = JSON.parse(v.Snapshot); + const content = document.getElementById('version-preview-content'); + const parts = []; + if (snap.Title) parts.push(`${snap.Title.replace(/`); + if (snap.Acte) parts.push(`${snap.Acte}`); + if (snap.Personnage) parts.push('Personnages : ' + snap.Personnage.replace(/Intérêt : ' + snap['Intérêt narratif'].replace(/Tags : ' + snap.Tags.replace(/'); + if (snap.Notes) parts.push('Notes : ' + snap.Notes.replace(/'); + document.getElementById('version-preview').style.display = ''; + document.getElementById('version-actions').style.display = ''; + } catch (e) { + document.getElementById('version-preview').style.display = 'none'; + } +} + // Close popup on version panel click outside document.addEventListener('click', (e) => { const panel = document.getElementById('version-panel'); diff --git a/server.js b/server.js index 14a79ea..a11d5d4 100644 --- a/server.js +++ b/server.js @@ -385,6 +385,19 @@ app.post('/api/versions/:id/restore', requireAuth, async (req, res) => { } }); +// List archived (deleted) frise cards +app.get('/api/archives', requireAuth, async (req, res) => { + if (!NOCODB_VERSIONS_TABLE_ID) return res.json({ list: [] }); + try { + const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_VERSIONS_TABLE_ID}?where=(Type,eq,frise)~and(Label,like,%25supprimé%25)&sort=-CreatedAt&limit=100`; + const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } }); + const data = await r.json(); + res.json(data); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + // ── Personnages API ── function personnagesProxy(tableIdVar) {