Files
tdb-rdb/public/index.html
T
Etienne Delvarre 8471e477db Recentrer les titres d'actes quand peu de cartes visibles après filtrage
Recalcule la largeur de chaque section d'acte dans applyFilters()
en comptant uniquement les cartes visibles (non filtrées).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-28 18:00:40 +02:00

670 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tableau de bord — RdB</title>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Lora:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
/* ── Light theme (default) ── */
:root {
--bg: #faf6f0;
--bg-alt: #f3ece2;
--bg-card: #fff;
--bg-input: #faf6f0;
--border: #e0d5c7;
--border-light: #f0ebe3;
--text: #2c1810;
--text-muted: #8a7968;
--text-hint: #a08a76;
--text-light: #c4b5a4;
--accent: #8a7060;
--accent-hover: #5c3d2e;
--brown: #5c3d2e;
--terra: #b85c3a;
--olive: #6a7a42;
--blue-grey: #6a7a8a;
--shadow: rgba(92,61,46,0.06);
--shadow-hover: rgba(92,61,46,0.12);
--overlay: rgba(44,24,16,0.4);
}
/* ── Dark theme ── */
.dark {
--bg: #1a1614;
--bg-alt: #231f1c;
--bg-card: #2a2420;
--bg-input: #1a1614;
--border: #3d3530;
--border-light: #332c27;
--text: #e8ddd0;
--text-muted: #a09080;
--text-hint: #807060;
--text-light: #5a4e44;
--accent: #c4a882;
--accent-hover: #e0c8a8;
--brown: #c4a882;
--terra: #d4845a;
--olive: #8aaa5a;
--blue-grey: #8a9aaa;
--shadow: rgba(0,0,0,0.2);
--shadow-hover: rgba(0,0,0,0.35);
--overlay: rgba(0,0,0,0.6);
}
body { font-family: 'Lora', Georgia, serif; background: var(--bg); color: var(--text); min-height: 100vh; transition: background 0.3s, color 0.3s; }
/* Layout */
.app { display: flex; min-height: 100vh; }
/* Sidebar */
.sidebar { width: 220px; background: var(--bg-alt); border-right: 1px solid var(--border); padding: 24px 0; flex-shrink: 0; display: flex; flex-direction: column; }
.sidebar-logo { padding: 0 22px 20px; font-family: 'Playfair Display', serif; font-size: 22px; font-weight: 700; color: var(--brown); }
.sidebar-project { padding: 12px 16px; margin: 0 14px 18px; background: var(--bg-card); border-radius: 10px; font-size: 16px; color: var(--brown); border: 1px solid var(--border); }
.nav-item { padding: 12px 18px; margin: 2px 14px; border-radius: 10px; font-size: 17px; color: var(--text-muted); cursor: pointer; display: flex; align-items: center; gap: 10px; }
.nav-item:hover { background: var(--border); color: var(--text); }
.nav-item.active { background: var(--border); color: var(--text); font-weight: 500; }
.nav-icon { width: 34px; height: 34px; font-size: 17px; background: var(--border); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
.sidebar-footer { margin-top: auto; padding: 16px 22px; font-size: 13px; color: var(--text-hint); }
/* Main */
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* Topbar */
.topbar { height: 58px; background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 28px; flex-shrink: 0; }
.topbar-title { font-family: 'Playfair Display', serif; font-size: 26px; font-weight: 600; }
.topbar-right { display: flex; align-items: center; gap: 14px; }
.btn-new { padding: 10px 22px; background: var(--accent); color: var(--bg); border: none; border-radius: 8px; font-size: 16px; font-weight: 600; font-family: 'Lora', serif; cursor: pointer; }
.btn-new:hover { background: var(--accent-hover); }
.dark-toggle { width: 36px; height: 20px; background: var(--border); border-radius: 10px; position: relative; cursor: pointer; border: none; flex-shrink: 0; }
.dark-toggle::after { content: '☀'; font-size: 12px; position: absolute; top: 2px; left: 3px; width: 16px; height: 16px; background: var(--bg-card); border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: left 0.2s; }
.dark .dark-toggle::after { content: '☾'; left: 17px; }
.avatar { width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; color: #fff; }
.avatar.e { background: #5c3d2e; }
.avatar.m { background: #b85c3a; }
/* ── Filter bar ── */
.filter-bar { background: var(--bg-alt); border-bottom: 1px solid var(--border); padding: 10px 28px; display: flex; align-items: center; gap: 12px; flex-shrink: 0; flex-wrap: wrap; }
.filter-bar select, .filter-bar input { padding: 7px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; color: var(--text); background: var(--bg-input); outline: none; }
.filter-bar select:focus, .filter-bar input:focus { border-color: var(--accent); }
.filter-bar label { font-size: 12px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
.filter-group { display: flex; align-items: center; gap: 6px; }
.btn-clear-filters { padding: 6px 14px; background: transparent; border: 1px solid var(--border); border-radius: 8px; font-size: 12px; font-family: 'Lora', serif; color: var(--text-muted); cursor: pointer; }
.btn-clear-filters:hover { background: var(--border); color: var(--text); }
.card.filtered-out { display: none; }
/* ── Frise container ── */
.frise-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* Timeline area */
.timeline-area { flex: 1; overflow-x: auto; overflow-y: auto; padding: 20px 24px 12px; position: relative; }
/* Acte markers row */
.acte-markers { display: flex; gap: 0; min-width: min-content; margin-bottom: 0; position: relative; }
.acte-section { min-width: 200px; flex-shrink: 0; position: relative; }
.acte-header { text-align: center; padding-bottom: 12px; }
.acte-label { font-family: 'Playfair Display', serif; font-size: 18px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; }
.acte-section[data-acte="Avant l'histoire"] .acte-label { color: var(--blue-grey); }
.acte-section[data-acte="Acte 1"] .acte-label { color: var(--brown); }
.acte-section[data-acte="Acte 2"] .acte-label { color: var(--terra); }
.acte-section[data-acte="Acte 3"] .acte-label { color: var(--olive); }
/* Timeline line */
.timeline-line { height: 3px; background: var(--border); display: flex; min-width: min-content; border-radius: 2px; }
.timeline-segment { flex-shrink: 0; height: 3px; }
.acte-section[data-acte="Avant l'histoire"] .timeline-segment { background: var(--blue-grey); }
.acte-section[data-acte="Acte 1"] .timeline-segment { background: var(--brown); }
.acte-section[data-acte="Acte 2"] .timeline-segment { background: var(--terra); }
.acte-section[data-acte="Acte 3"] .timeline-segment { background: var(--olive); }
/* Cards row under each acte */
.cards-row { display: flex; gap: 14px; padding: 16px 10px 10px; min-height: 140px; align-items: flex-start; }
/* Cards */
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 18px 20px; cursor: grab; border-top: 5px solid var(--border); width: 260px; min-width: 260px; flex-shrink: 0; box-shadow: 0 1px 4px var(--shadow); transition: box-shadow 0.15s; }
.card:hover { box-shadow: 0 4px 14px var(--shadow-hover); }
.card.sortable-ghost { opacity: 0.3; }
.card.sortable-chosen { box-shadow: 0 6px 20px var(--shadow-hover); }
.card-title { font-family: 'Playfair Display', serif; font-size: 18px; font-weight: 600; margin-bottom: 8px; color: var(--text); }
.card-summary { font-size: 15px; color: var(--text-muted); line-height: 1.5; margin-bottom: 12px; display: -webkit-box; -webkit-line-clamp: 8; -webkit-box-orient: vertical; overflow: hidden; }
.card-meta { display: flex; gap: 6px; flex-wrap: wrap; }
.card-badge { font-size: 13px; padding: 4px 10px; border-radius: 8px; font-weight: 500; }
/* Acte colors on card top border */
.card[data-acte="Avant l'histoire"] { border-top-color: var(--blue-grey); }
.card[data-acte="Acte 1"] { border-top-color: var(--brown); }
.card[data-acte="Acte 2"] { border-top-color: var(--terra); }
.card[data-acte="Acte 3"] { border-top-color: var(--olive); }
.card[data-acte="En attente"] { border-top-color: var(--border); }
/* Status badge colors */
.badge-germe { background: var(--border); color: var(--text-muted); }
.badge-etudier { background: #f5f0e4; color: #a07850; }
.badge-posee { background: #eef3e8; color: #5a7a3a; }
.badge-abandonnee { background: var(--border); color: var(--text-hint); text-decoration: line-through; }
.badge-conflit { background: #f5e4e4; color: #8b3030; }
.dark .badge-etudier { background: #3a3020; color: #c49860; }
.dark .badge-posee { background: #2a3520; color: #8aaa6a; }
.dark .badge-conflit { background: #3a2020; color: #c06060; }
.badge-perso { background: var(--bg); color: var(--text-muted); border: 1px solid var(--border); }
/* ── Waiting zone (bottom) ── */
.waiting-zone { border-top: 2px dashed var(--border); padding: 14px 24px; background: var(--bg-alt); flex-shrink: 0; }
.waiting-header { font-size: 13px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 10px; font-weight: 600; }
.waiting-cards { display: flex; gap: 12px; overflow-x: auto; min-height: 100px; padding: 4px 0; align-items: flex-start; }
.waiting-cards .card { border-style: dashed; border-top: 4px dashed var(--border); }
/* ── Modal ── */
.modal-overlay { display: none; position: fixed; inset: 0; background: var(--overlay); z-index: 100; align-items: center; justify-content: center; }
.modal-overlay.open { display: flex; }
.modal { background: var(--bg-card); border-radius: 16px; width: 580px; max-width: 92vw; max-height: 88vh; overflow-y: auto; box-shadow: 0 12px 40px var(--shadow-hover); }
.modal-header { padding: 22px 26px 0; display: flex; justify-content: space-between; align-items: start; }
.modal-close { background: none; border: none; font-size: 22px; color: var(--text-hint); cursor: pointer; padding: 4px 8px; }
.modal-close:hover { color: var(--text); }
.modal-body { padding: 14px 26px 26px; }
.modal-body .field { margin-bottom: 16px; }
.modal-body .field label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-hint); font-weight: 600; margin-bottom: 5px; }
.modal-body .field input,
.modal-body .field textarea,
.modal-body .field select { width: 100%; padding: 9px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; font-family: 'Lora', serif; color: var(--text); outline: none; background: var(--bg-input); }
.modal-body .field input:focus,
.modal-body .field textarea:focus,
.modal-body .field select:focus { border-color: var(--accent); }
.modal-body .field textarea { min-height: 70px; resize: vertical; line-height: 1.6; }
.modal-body .field-title input { font-family: 'Playfair Display', serif; font-size: 20px; font-weight: 600; border: none; background: transparent; padding: 0; }
.modal-body .field-title input:focus { border-bottom: 2px solid var(--accent); }
.modal-body .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.multi-select-container { display: flex; flex-wrap: wrap; gap: 5px; padding: 8px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-input); min-height: 38px; }
.multi-select-container .ms-tag { font-size: 12px; padding: 3px 9px; border-radius: 6px; background: var(--border); color: var(--text-muted); cursor: pointer; }
.multi-select-container .ms-tag.selected { background: var(--accent); color: var(--bg); }
.modal-actions { display: flex; gap: 10px; justify-content: space-between; margin-top: 8px; }
.btn-save { padding: 9px 22px; background: var(--accent); color: var(--bg); border: none; border-radius: 8px; font-size: 14px; font-weight: 600; font-family: 'Lora', serif; cursor: pointer; }
.btn-save:hover { background: var(--accent-hover); }
.btn-delete { padding: 9px 14px; background: transparent; color: #8b3030; border: 1px solid #e8c4c0; border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; cursor: pointer; }
.btn-delete:hover { background: #fdf0ef; }
.dark .btn-delete { border-color: #5a3030; }
.dark .btn-delete:hover { background: #3a2020; }
.loading { text-align: center; padding: 60px; color: var(--text-hint); font-style: italic; }
/* Responsive */
@media (max-width: 768px) {
.sidebar { display: none; }
.modal { width: 100%; max-width: 100%; border-radius: 12px 12px 0 0; }
.card { width: 150px; min-width: 150px; }
}
</style>
</head>
<body>
<div class="app">
<div class="sidebar">
<div class="sidebar-logo">Tableau de bord</div>
<div class="sidebar-project">Projet RdB</div>
<div class="nav-item active"><span class="nav-icon">&#9203;</span> Frise</div>
<div class="sidebar-footer">Etienne & Myriam</div>
</div>
<div class="main">
<div class="topbar">
<div class="topbar-title">Frise</div>
<div class="topbar-right">
<button class="btn-new" onclick="openNew()">+ Nouvelle idee</button>
<button class="dark-toggle" onclick="toggleDark()" title="Mode sombre"></button>
<div class="avatar e">E</div>
<div class="avatar m">M</div>
</div>
</div>
<div class="filter-bar" id="filter-bar">
<div class="filter-group">
<label>Personnage</label>
<select id="filter-perso" onchange="applyFilters()"><option value="">Tous</option></select>
</div>
<div class="filter-group">
<label>Statut</label>
<select id="filter-statut" onchange="applyFilters()"><option value="">Tous</option></select>
</div>
<div class="filter-group">
<label>Type</label>
<select id="filter-type" onchange="applyFilters()"><option value="">Tous</option></select>
</div>
<div class="filter-group">
<label>Recherche</label>
<input type="text" id="filter-search" placeholder="Mot-cle..." oninput="applyFilters()">
</div>
<button class="btn-clear-filters" onclick="clearFilters()">Effacer</button>
</div>
<div class="frise-wrapper">
<div class="timeline-area" id="timeline-area">
<div class="loading" id="loading">Chargement...</div>
</div>
<div class="waiting-zone">
<div class="waiting-header">En attente — glisser sur la frise</div>
<div class="waiting-cards" id="waiting-cards"></div>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal-overlay" id="modal">
<div class="modal">
<div class="modal-header">
<span style="font-family:'Playfair Display',serif;font-size:12px;text-transform:uppercase;letter-spacing:1px;color:var(--text-hint);" id="modal-label">Nouvelle idee</span>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field field-title">
<input type="text" id="f-title" placeholder="Titre de l'idee...">
</div>
<div class="field-row">
<div class="field">
<label>Type</label>
<select id="f-type"><option value=""></option></select>
</div>
<div class="field">
<label>Statut</label>
<select id="f-statut"><option value=""></option></select>
</div>
</div>
<div class="field-row">
<div class="field">
<label>Acte</label>
<select id="f-acte"><option value=""></option></select>
</div>
<div class="field">
<label>Personnages</label>
<div class="multi-select-container" id="f-perso"></div>
</div>
</div>
<div class="field">
<label>Resume</label>
<textarea id="f-resume" placeholder="Description, resume de la scene ou de l'idee..."></textarea>
</div>
<div class="field">
<label>Interet narratif</label>
<textarea id="f-interet" placeholder="Pourquoi cette idee est importante pour l'histoire..."></textarea>
</div>
<div class="field">
<label>Implications</label>
<textarea id="f-implications" placeholder="Consequences dans l'histoire..."></textarea>
</div>
<div class="field">
<label>Origine</label>
<textarea id="f-origine" placeholder="D'ou vient cette idee..."></textarea>
</div>
<div class="field">
<label>Lien redaction</label>
<textarea id="f-lien" placeholder="Notes pour la redaction..."></textarea>
</div>
<div class="modal-actions">
<button class="btn-delete" id="btn-delete" onclick="deleteCard()" style="display:none;">Supprimer</button>
<button class="btn-save" onclick="saveCard()">Enregistrer</button>
</div>
</div>
</div>
</div>
<script>
const ACTES_FRISE = ["Avant l'histoire", "Acte 1", "Acte 2", "Acte 3"];
let records = [];
let columns = {};
let editingId = null;
// ── Dark mode ──
function toggleDark() {
document.body.classList.toggle('dark');
localStorage.setItem('tdb-dark', document.body.classList.contains('dark') ? '1' : '0');
}
if (localStorage.getItem('tdb-dark') === '1') document.body.classList.add('dark');
// ── Filters ──
function populateFilterOptions() {
// Collect unique personnages from data
const persoSet = new Set();
records.forEach(r => {
if (r.Personnage) r.Personnage.split(',').forEach(p => persoSet.add(p.trim()));
});
const persoSel = document.getElementById('filter-perso');
const curPerso = persoSel.value;
persoSel.innerHTML = '<option value="">Tous</option>';
[...persoSet].sort().forEach(p => {
const opt = document.createElement('option');
opt.value = p; opt.textContent = p;
persoSel.appendChild(opt);
});
persoSel.value = curPerso;
// Statut and Type from column options
['Statut', 'Type'].forEach(name => {
const sel = document.getElementById(`filter-${name.toLowerCase()}`);
const cur = sel.value;
sel.innerHTML = '<option value="">Tous</option>';
if (columns[name]) {
columns[name].options.forEach(o => {
const opt = document.createElement('option');
opt.value = o; opt.textContent = o;
sel.appendChild(opt);
});
}
sel.value = cur;
});
}
function applyFilters() {
const perso = document.getElementById('filter-perso').value;
const statut = document.getElementById('filter-statut').value;
const type = document.getElementById('filter-type').value;
const search = document.getElementById('filter-search').value.toLowerCase().trim();
document.querySelectorAll('.card').forEach(card => {
const id = Number(card.dataset.id);
const r = records.find(rec => rec.Id === id);
if (!r) return;
let visible = true;
if (perso && !(r.Personnage || '').split(',').map(s => s.trim()).includes(perso)) visible = false;
if (statut && r.Statut !== statut) visible = false;
if (type && r.Type !== type) visible = false;
if (search) {
const haystack = [r.Title, r['Résumé'], r.Personnage, r['Intérêt narratif'], r.Implications, r.Origine].filter(Boolean).join(' ').toLowerCase();
if (!haystack.includes(search)) visible = false;
}
card.classList.toggle('filtered-out', !visible);
});
// Resize acte sections based on visible cards
document.querySelectorAll('.acte-section').forEach(section => {
const visibleCards = section.querySelectorAll('.card:not(.filtered-out)').length;
const minW = Math.max(280, visibleCards * 274 + 20);
section.style.width = minW + 'px';
});
}
function clearFilters() {
document.getElementById('filter-perso').value = '';
document.getElementById('filter-statut').value = '';
document.getElementById('filter-type').value = '';
document.getElementById('filter-search').value = '';
applyFilters();
}
const statusClass = s => {
if (!s) return '';
const map = { 'Germe': 'badge-germe', 'À étudier': 'badge-etudier', 'Posée': 'badge-posee', 'Abandonnée': 'badge-abandonnee', 'En conflit': 'badge-conflit' };
return map[s] || 'badge-germe';
};
function makeCard(r) {
const card = document.createElement('div');
card.className = 'card';
card.dataset.id = r.Id;
card.dataset.type = r.Type || '';
card.dataset.acte = r.Acte || 'En attente';
card.onclick = () => openEdit(r.Id);
const persos = r.Personnage ? r.Personnage.split(',').map(p =>
`<span class="card-badge badge-perso">${esc(p.trim())}</span>`
).join('') : '';
card.innerHTML = `
<div class="card-title">${esc(r.Title || '')}</div>
${r['Résumé'] ? `<div class="card-summary">${esc(r['Résumé'])}</div>` : ''}
<div class="card-meta">
${r.Statut ? `<span class="card-badge ${statusClass(r.Statut)}">${esc(r.Statut)}</span>` : ''}
${r.Type ? `<span class="card-badge badge-perso">${esc(r.Type)}</span>` : ''}
${persos}
</div>
`;
return card;
}
function renderFrise() {
const area = document.getElementById('timeline-area');
area.innerHTML = '';
// Build horizontal timeline
const container = document.createElement('div');
container.style.cssText = 'display:flex; min-width:min-content;';
ACTES_FRISE.forEach(acte => {
const acteRecords = records
.filter(r => (r.Acte || 'En attente') === acte)
.sort((a, b) => (a.Ordre || 0) - (b.Ordre || 0));
const section = document.createElement('div');
section.className = 'acte-section';
section.dataset.acte = acte;
// Width adapts to content
const minW = Math.max(280, acteRecords.length * 274 + 20);
section.style.width = minW + 'px';
section.innerHTML = `
<div class="acte-header"><span class="acte-label">${esc(acte)}</span></div>
<div class="timeline-segment" style="width:100%;height:3px;border-radius:2px;"></div>
`;
const row = document.createElement('div');
row.className = 'cards-row';
row.dataset.acte = acte;
acteRecords.forEach(r => row.appendChild(makeCard(r)));
section.appendChild(row);
container.appendChild(section);
// SortableJS on horizontal row
new Sortable(row, {
group: 'frise',
animation: 200,
direction: 'horizontal',
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
onEnd: handleDrop,
});
});
area.appendChild(container);
// Waiting zone
const waitingCards = document.getElementById('waiting-cards');
waitingCards.innerHTML = '';
const waitingRecords = records
.filter(r => !r.Acte || r.Acte === 'En attente')
.sort((a, b) => (a.Ordre || 0) - (b.Ordre || 0));
waitingRecords.forEach(r => waitingCards.appendChild(makeCard(r)));
new Sortable(waitingCards, {
group: 'frise',
animation: 200,
direction: 'horizontal',
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
onEnd: handleDrop,
});
}
async function handleDrop(evt) {
const toContainer = evt.to;
const newActe = toContainer.dataset.acte || 'En attente';
// Collect new order for target
const updates = [];
const cards = toContainer.querySelectorAll('.card');
cards.forEach((c, i) => {
updates.push({ id: Number(c.dataset.id), Acte: newActe, Ordre: i + 1 });
});
// Reorder source if different
if (evt.from !== evt.to) {
const srcActe = evt.from.dataset.acte || 'En attente';
const srcCards = evt.from.querySelectorAll('.card');
srcCards.forEach((c, i) => {
updates.push({ id: Number(c.dataset.id), Acte: srcActe, Ordre: i + 1 });
});
}
// Update local state
updates.forEach(u => {
const rec = records.find(r => r.Id === u.id);
if (rec) { rec.Acte = u.Acte; rec.Ordre = u.Ordre; }
});
await fetch('/api/reorder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
}
function populateSelects() {
['Type', 'Statut', 'Acte'].forEach(name => {
const sel = document.getElementById(`f-${name.toLowerCase()}`);
if (!sel || !columns[name]) return;
sel.innerHTML = '<option value="">—</option>';
columns[name].options.forEach(o => {
const opt = document.createElement('option');
opt.value = o; opt.textContent = o;
sel.appendChild(opt);
});
});
const container = document.getElementById('f-perso');
container.innerHTML = '';
if (columns.Personnage) {
columns.Personnage.options.forEach(p => {
const tag = document.createElement('span');
tag.className = 'ms-tag';
tag.textContent = p;
tag.onclick = () => tag.classList.toggle('selected');
container.appendChild(tag);
});
}
}
function openNew() {
editingId = null;
document.getElementById('modal-label').textContent = 'Nouvelle idee';
document.getElementById('f-title').value = '';
document.getElementById('f-type').value = '';
document.getElementById('f-statut').value = 'Germe';
document.getElementById('f-acte').value = 'En attente';
document.getElementById('f-resume').value = '';
document.getElementById('f-interet').value = '';
document.getElementById('f-implications').value = '';
document.getElementById('f-origine').value = '';
document.getElementById('f-lien').value = '';
document.querySelectorAll('#f-perso .ms-tag').forEach(t => t.classList.remove('selected'));
document.getElementById('btn-delete').style.display = 'none';
document.getElementById('modal').classList.add('open');
}
function openEdit(id) {
const r = records.find(rec => rec.Id === id);
if (!r) return;
editingId = id;
document.getElementById('modal-label').textContent = 'Modifier';
document.getElementById('f-title').value = r.Title || '';
document.getElementById('f-type').value = r.Type || '';
document.getElementById('f-statut').value = r.Statut || '';
document.getElementById('f-acte').value = r.Acte || '';
document.getElementById('f-resume').value = r['Résumé'] || '';
document.getElementById('f-interet').value = r['Intérêt narratif'] || '';
document.getElementById('f-implications').value = r.Implications || '';
document.getElementById('f-origine').value = r.Origine || '';
document.getElementById('f-lien').value = r['Lien rédaction'] || '';
const selected = r.Personnage ? r.Personnage.split(',').map(s => s.trim()) : [];
document.querySelectorAll('#f-perso .ms-tag').forEach(t => {
t.classList.toggle('selected', selected.includes(t.textContent));
});
document.getElementById('btn-delete').style.display = 'block';
document.getElementById('modal').classList.add('open');
}
function closeModal() {
document.getElementById('modal').classList.remove('open');
editingId = null;
}
async function saveCard() {
const persos = [];
document.querySelectorAll('#f-perso .ms-tag.selected').forEach(t => persos.push(t.textContent));
const data = {
Title: document.getElementById('f-title').value,
Type: document.getElementById('f-type').value || null,
Statut: document.getElementById('f-statut').value || null,
Acte: document.getElementById('f-acte').value || 'En attente',
Personnage: persos.join(',') || null,
'Résumé': document.getElementById('f-resume').value || null,
'Intérêt narratif': document.getElementById('f-interet').value || null,
Implications: document.getElementById('f-implications').value || null,
Origine: document.getElementById('f-origine').value || null,
'Lien rédaction': document.getElementById('f-lien').value || null,
};
if (editingId) {
await fetch(`/api/records/${editingId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
} else {
data.Ordre = records.filter(r => r.Acte === data.Acte).length + 1;
await fetch('/api/records', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
closeModal();
await loadData();
}
async function deleteCard() {
if (!editingId) return;
if (!confirm('Supprimer cette idee ?')) return;
await fetch(`/api/records/${editingId}`, { method: 'DELETE' });
closeModal();
await loadData();
}
async function loadData() {
const [recRes, colRes] = await Promise.all([
fetch('/api/records'),
fetch('/api/columns'),
]);
if (recRes.status === 401) { window.location.reload(); return; }
const recData = await recRes.json();
records = recData.list || [];
columns = await colRes.json();
populateSelects();
populateFilterOptions();
renderFrise();
applyFilters();
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
document.getElementById('modal').addEventListener('click', e => {
if (e.target === document.getElementById('modal')) closeModal();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeModal();
});
loadData();
</script>
</body>
</html>