// Application NoteFlow - JavaScript complet // Ce fichier regroupe toute la logique de l'application pour simplifier le déploiement // ==================== UTILS ==================== const utils = { formatDate(dateString) { const date = new Date(dateString); const now = new Date(); const diff = now - date; const days = Math.floor(diff / (1000 * 60 * 60 * 24)); if (days === 0) return 'Aujourd\'hui'; if (days === 1) return 'Hier'; if (days < 7) return `Il y a ${days} jours`; return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined }); }, truncate(text, length = 150) { if (!text || text.length <= length) return text; return text.substring(0, length) + '...'; }, debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } }; // ==================== MODAL DE CONFIRMATION ==================== const confirmDialog = { show(options = {}) { return new Promise((resolve) => { const modal = document.getElementById('confirmModal'); const icon = document.getElementById('confirmIcon'); const title = document.getElementById('confirmTitle'); const message = document.getElementById('confirmMessage'); const cancelBtn = document.getElementById('confirmCancel'); const okBtn = document.getElementById('confirmOk'); // Configurer le contenu icon.textContent = options.icon || '⚠️'; title.textContent = options.title || 'Confirmer l\'action'; message.textContent = options.message || 'Êtes-vous sûr de vouloir continuer ?'; cancelBtn.textContent = options.cancelText || 'Annuler'; okBtn.textContent = options.okText || 'Confirmer'; // Gérer les clics const handleCancel = () => { cleanup(); resolve(false); }; const handleConfirm = () => { cleanup(); resolve(true); }; const handleEscape = (e) => { if (e.key === 'Escape') { handleCancel(); } }; const cleanup = () => { modal.classList.remove('active'); setTimeout(() => { modal.style.display = 'none'; }, 300); cancelBtn.removeEventListener('click', handleCancel); okBtn.removeEventListener('click', handleConfirm); document.removeEventListener('keydown', handleEscape); }; // Ajouter les écouteurs cancelBtn.addEventListener('click', handleCancel); okBtn.addEventListener('click', handleConfirm); document.addEventListener('keydown', handleEscape); // Afficher le modal modal.style.display = 'flex'; setTimeout(() => modal.classList.add('active'), 10); }); } }; // ==================== API ==================== const api = { async request(url, options = {}) { const token = localStorage.getItem('token'); const headers = { 'Content-Type': 'application/json', ...(token && { 'Authorization': `Bearer ${token}` }), ...options.headers }; const config = { ...options, headers }; const response = await fetch(url, config); if (response.status === 401 || response.status === 403) { localStorage.removeItem('token'); localStorage.removeItem('user'); window.location.href = '/login.html'; throw new Error('Non authentifié'); } return response; }, async get(url) { const response = await this.request(url); return response.json(); }, async post(url, data) { const response = await this.request(url, { method: 'POST', body: JSON.stringify(data) }); return response.json(); }, async put(url, data) { const response = await this.request(url, { method: 'PUT', body: JSON.stringify(data) }); return response.json(); }, async delete(url) { const response = await this.request(url, { method: 'DELETE' }); return response.json(); }, async uploadFile(url, formData) { const token = localStorage.getItem('token'); const response = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData }); if (response.status === 401) { window.location.href = '/login.html'; throw new Error('Non authentifié'); } return response.json(); } }; // ==================== STATE ==================== const state = { notes: [], todos: [], currentNote: null, expandedNoteId: null, filter: 'all', searchQuery: '', view: 'active', user: null }; // ==================== AUTH ==================== function checkAuth() { const token = localStorage.getItem('token'); if (!token) { window.location.href = '/login.html'; return false; } const userStr = localStorage.getItem('user'); if (userStr) { state.user = JSON.parse(userStr); } return true; } async function initAuth() { if (!checkAuth()) return; // Afficher le nom d'utilisateur const userNameEl = document.getElementById('userName'); if (userNameEl && state.user) { userNameEl.textContent = state.user.username; } // Afficher le bouton admin si nécessaire const adminBtn = document.getElementById('adminBtn'); if (adminBtn && state.user && state.user.is_admin) { adminBtn.style.display = 'block'; } // Gérer la déconnexion const logoutBtn = document.getElementById('logoutBtn'); if (logoutBtn) { logoutBtn.addEventListener('click', async () => { try { await api.post('/api/auth/logout', {}); } catch (e) {} localStorage.removeItem('token'); localStorage.removeItem('user'); window.location.href = '/login.html'; }); } // Toggle user dropdown const userInfo = document.getElementById('userInfo'); const userDropdown = document.getElementById('userDropdown'); if (userInfo && userDropdown) { userInfo.addEventListener('click', (e) => { e.stopPropagation(); userDropdown.classList.toggle('show'); }); document.addEventListener('click', () => { userDropdown.classList.remove('show'); }); } } // ==================== NOTES ==================== async function loadNotes() { try { const archived = state.view === 'archived'; const response = await api.get(`/api/notes?archived=${archived}`); // S'assurer que response est un tableau state.notes = Array.isArray(response) ? response : []; renderNotes(); } catch (error) { console.error('Erreur chargement notes:', error); state.notes = []; renderNotes(); } } function renderNotes() { const notesGrid = document.getElementById('notesGrid'); if (!notesGrid) return; // S'assurer que state.notes est toujours un tableau if (!Array.isArray(state.notes)) { state.notes = []; } let filtered = state.notes; // Filtre de recherche if (state.searchQuery) { const query = state.searchQuery.toLowerCase(); filtered = filtered.filter(note => note.title.toLowerCase().includes(query) || (note.content && note.content.toLowerCase().includes(query)) ); } notesGrid.innerHTML = ''; if (filtered.length === 0) { notesGrid.innerHTML = '

Aucune note trouvée

'; return; } filtered.forEach(note => { const card = createNoteCard(note); notesGrid.appendChild(card); }); } function createNoteCard(note) { const card = document.createElement('div'); const isExpanded = state.expandedNoteId === note.id; card.className = `note-card ${isExpanded ? 'expanded' : ''}`; card.dataset.noteId = note.id; if (isExpanded) { // Mode édition inline card.innerHTML = `
${note.image_filename ? `
` : ''} ${note.files && note.files.length > 0 ? `
${renderInlineFiles(note.files || [])}
` : ''}

Todos

${renderInlineTodos(note.todos || [])}
`; // Auto-save sur changement setTimeout(() => { const titleInput = card.querySelector('.note-title-input-inline'); const contentTextarea = card.querySelector('.note-content-textarea-inline'); if (titleInput) titleInput.oninput = utils.debounce(() => saveNoteInline(note.id), 1000); if (contentTextarea) contentTextarea.oninput = utils.debounce(() => saveNoteInline(note.id), 1000); }, 0); } else { // Mode compact let html = `
${escapeHtml(note.title)}
`; if (note.content) { html += `
${escapeHtml(utils.truncate(note.content, 100))}
`; } if (note.image_filename) { html += ``; } // Afficher les todos if (note.todos && note.todos.length > 0) { const displayTodos = note.todos.slice(0, 3); html += `
`; displayTodos.forEach(todo => { html += `
${todo.completed ? '☑' : '☐'} ${escapeHtml(todo.text)}
`; }); if (note.todos.length > 3) { html += `
+${note.todos.length - 3} autre(s)
`; } html += `
`; } html += `
${utils.formatDate(note.updated_at)} ${note.todos_count > 0 ? `${note.todos_completed}/${note.todos_count}` : ''}
`; card.innerHTML = html; } return card; } function renderInlineTodos(todos) { if (!todos || todos.length === 0) return '

Aucune tâche

'; return todos.map(todo => `
${escapeHtml(todo.text)}
`).join(''); } function renderInlineFiles(files) { if (!files || files.length === 0) return ''; return files.map(file => { const sizeStr = formatFileSize(file.file_size); return `
${escapeHtml(file.original_name)}
${sizeStr}
`; }).join(''); } function formatFileSize(bytes) { if (!bytes) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ==================== INLINE NOTE EXPANSION ==================== function expandNote(noteId) { state.expandedNoteId = noteId; renderNotes(); } function collapseNote() { state.expandedNoteId = null; renderNotes(); } async function saveNoteInline(noteId) { const card = document.querySelector(`.note-card[data-note-id="${noteId}"]`); if (!card) return; const titleInput = card.querySelector('.note-title-input-inline'); const contentTextarea = card.querySelector('.note-content-textarea-inline'); if (!titleInput) return; try { await api.put(`/api/notes/${noteId}`, { title: titleInput.value, content: contentTextarea ? contentTextarea.value : '' }); // Update state without full reload const noteIndex = state.notes.findIndex(n => n.id === noteId); if (noteIndex !== -1) { state.notes[noteIndex].title = titleInput.value; state.notes[noteIndex].content = contentTextarea ? contentTextarea.value : ''; state.notes[noteIndex].updated_at = new Date().toISOString(); } } catch (error) { console.error('Erreur sauvegarde note:', error); } } async function archiveNote(noteId, archived) { try { await api.put(`/api/notes/${noteId}/archive`, { archived }); collapseNote(); await loadNotes(); } catch (error) { console.error('Erreur archivage note:', error); alert('Erreur lors de l\'archivage de la note'); } } async function deleteNoteInline(noteId) { const confirmed = await confirmDialog.show({ icon: '🗑️', title: 'Supprimer cette note', message: 'Êtes-vous sûr de vouloir supprimer cette note ?\n\nTous les todos associés seront également supprimés.', okText: 'Supprimer' }); if (!confirmed) return; try { await api.delete(`/api/notes/${noteId}`); collapseNote(); await loadNotes(); } catch (error) { console.error('Erreur suppression note:', error); alert('Erreur lors de la suppression de la note'); } } async function addNoteTodoInline(noteId, inputElement) { const text = inputElement.value.trim(); if (!text) return; try { await api.post(`/api/notes/${noteId}/todos`, { text }); // Recharger seulement cette note const note = await api.get(`/api/notes/${noteId}`); const noteIndex = state.notes.findIndex(n => n.id === noteId); if (noteIndex !== -1) { state.notes[noteIndex] = note; } // Re-render la liste des todos inline const todosList = document.getElementById(`todos-${noteId}`); if (todosList) { todosList.innerHTML = renderInlineTodos(note.todos || []); } // Clear input inputElement.value = ''; inputElement.focus(); } catch (error) { console.error('Erreur ajout todo:', error); alert('Erreur lors de l\'ajout du todo'); } } async function toggleNoteTodoInline(todoId, completed) { try { await api.put(`/api/notes/todos/${todoId}`, { completed }); // Update UI optimistically const todoItem = document.querySelector(`.todo-item-inline[data-todo-inline-id="${todoId}"]`); if (todoItem) { const textSpan = todoItem.querySelector('.todo-text-inline'); if (textSpan) { if (completed) { textSpan.classList.add('completed'); } else { textSpan.classList.remove('completed'); } } } } catch (error) { console.error('Erreur toggle todo:', error); } } async function deleteNoteTodoInline(todoId) { const confirmed = await confirmDialog.show({ icon: '🗑️', title: 'Supprimer ce todo', message: 'Êtes-vous sûr de vouloir supprimer ce todo ?', okText: 'Supprimer' }); if (!confirmed) return; try { await api.delete(`/api/notes/todos/${todoId}`); // Remove from DOM const todoItem = document.querySelector(`.todo-item-inline[data-todo-inline-id="${todoId}"]`); if (todoItem) { todoItem.remove(); } } catch (error) { console.error('Erreur suppression todo:', error); } } function triggerImageUpload(noteId) { const imageInput = document.getElementById('imageInput'); if (imageInput) { imageInput.dataset.noteId = noteId; imageInput.click(); } } function triggerFileUpload(noteId) { const fileInput = document.getElementById('fileInput'); if (fileInput) { fileInput.dataset.noteId = noteId; fileInput.click(); } } async function removeImageInline(noteId) { const confirmed = await confirmDialog.show({ icon: '🖼️', title: 'Supprimer l\'image', message: 'Êtes-vous sûr de vouloir supprimer cette image ?', okText: 'Supprimer' }); if (!confirmed) return; try { await api.delete(`/api/notes/${noteId}/image`); // Update state const noteIndex = state.notes.findIndex(n => n.id === noteId); if (noteIndex !== -1) { state.notes[noteIndex].image_filename = null; } // Re-render expanded card renderNotes(); } catch (error) { console.error('Erreur suppression image:', error); alert('Erreur lors de la suppression de l\'image'); } } async function handleImageUploadInline(event) { const file = event.target.files[0]; const noteId = event.target.dataset.noteId; if (!file || !noteId) return; const formData = new FormData(); formData.append('image', file); try { const result = await api.uploadFile(`/api/notes/${noteId}/image`, formData); // Update state const noteIndex = state.notes.findIndex(n => n.id === parseInt(noteId)); if (noteIndex !== -1) { state.notes[noteIndex].image_filename = result.filename; } // Re-render expanded card renderNotes(); } catch (error) { console.error('Erreur upload image:', error); alert('Erreur lors de l\'upload de l\'image'); } // Reset input event.target.value = ''; event.target.dataset.noteId = ''; } async function handleFileUploadInline(event) { const file = event.target.files[0]; const noteId = event.target.dataset.noteId; if (!file || !noteId) return; const formData = new FormData(); formData.append('file', file); try { const result = await api.uploadFile(`/api/notes/${noteId}/files`, formData); // Reload notes to get updated files list await loadNotes(); // Keep the note expanded state.expandedNoteId = parseInt(noteId); renderNotes(); } catch (error) { console.error('Erreur upload fichier:', error); alert('Erreur lors de l\'upload du fichier: ' + (error.message || 'Erreur inconnue')); } // Reset input event.target.value = ''; event.target.dataset.noteId = ''; } async function downloadFile(fileId) { try { // Ouvrir le lien de téléchargement dans un nouvel onglet window.open(`/api/notes/files/${fileId}/download`, '_blank'); } catch (error) { console.error('Erreur téléchargement fichier:', error); alert('Erreur lors du téléchargement du fichier'); } } async function deleteFile(fileId) { const confirmed = await confirmDialog.show({ icon: '📎', title: 'Supprimer ce fichier', message: 'Êtes-vous sûr de vouloir supprimer ce fichier ?', okText: 'Supprimer' }); if (!confirmed) return; try { await api.delete(`/api/notes/files/${fileId}`); // Reload notes to get updated files list await loadNotes(); // Keep the note expanded if (state.expandedNoteId) { renderNotes(); } } catch (error) { console.error('Erreur suppression fichier:', error); alert('Erreur lors de la suppression du fichier'); } } async function createNewNote() { try { const newNote = await api.post('/api/notes', { title: 'Nouvelle note', content: '' }); // Reload notes await loadNotes(); // Expand the new note state.expandedNoteId = newNote.id; renderNotes(); // Focus title input setTimeout(() => { const titleInput = document.querySelector('.note-title-input-inline'); if (titleInput) { titleInput.focus(); titleInput.select(); } }, 100); } catch (error) { console.error('Erreur création note:', error); alert('Erreur lors de la création de la note'); } } function setNoteView(view) { state.view = view; document.querySelectorAll('.view-filter').forEach(btn => { btn.classList.toggle('active', btn.dataset.view === view); }); loadNotes(); } // ==================== MODAL (OLD - DEPRECATED) ==================== let saveTimeout; let currentNoteId = null; async function openNoteModal(noteId = null) { currentNoteId = noteId; const modal = document.getElementById('noteEditorBackdrop'); const titleInput = document.getElementById('noteTitle'); const contentTextarea = document.getElementById('noteContent'); const imageContainer = document.getElementById('noteImageContainer'); const noteImage = document.getElementById('noteImage'); const noteTodosList = document.getElementById('noteTodosList'); const metadata = document.getElementById('noteMetadata'); if (noteId) { // Charger la note existante try { const note = await api.get(`/api/notes/${noteId}`); state.currentNote = note; titleInput.value = note.title; contentTextarea.value = note.content || ''; if (note.image_filename) { noteImage.src = `/uploads/${note.image_filename}`; imageContainer.style.display = 'block'; } else { imageContainer.style.display = 'none'; } renderNoteTodos(note.todos || []); metadata.textContent = `Créée ${utils.formatDate(note.created_at)} • Modifiée ${utils.formatDate(note.updated_at)}`; } catch (error) { console.error('Erreur chargement note:', error); return; } } else { // Nouvelle note try { const newNote = await api.post('/api/notes', { title: 'Nouvelle note', content: '' }); currentNoteId = newNote.id; state.currentNote = newNote; titleInput.value = newNote.title; contentTextarea.value = ''; imageContainer.style.display = 'none'; noteTodosList.innerHTML = ''; metadata.textContent = 'Nouvelle note'; // Rafraîchir la liste await loadNotes(); } catch (error) { console.error('Erreur création note:', error); return; } } modal.style.display = 'flex'; titleInput.focus(); titleInput.select(); // Auto-save sur les modifications setupAutoSave(); } function setupAutoSave() { const titleInput = document.getElementById('noteTitle'); const contentTextarea = document.getElementById('noteContent'); const saveNote = async () => { if (!currentNoteId) return; try { await api.put(`/api/notes/${currentNoteId}`, { title: titleInput.value, content: contentTextarea.value }); // Rafraîchir la liste await loadNotes(); } catch (error) { console.error('Erreur sauvegarde:', error); } }; const debouncedSave = utils.debounce(saveNote, 1000); titleInput.oninput = debouncedSave; contentTextarea.oninput = debouncedSave; } function renderNoteTodos(todos) { const list = document.getElementById('noteTodosList'); list.innerHTML = ''; todos.forEach(todo => { const item = document.createElement('div'); item.className = 'todo-item'; item.innerHTML = ` ${escapeHtml(todo.text)} `; if (todo.completed) item.classList.add('completed'); list.appendChild(item); }); } async function addNoteTodo() { if (!currentNoteId) return; const text = prompt('Texte du todo:'); if (!text) return; try { await api.post(`/api/notes/${currentNoteId}/todos`, { text }); const note = await api.get(`/api/notes/${currentNoteId}`); renderNoteTodos(note.todos || []); await loadNotes(); } catch (error) { console.error('Erreur ajout todo:', error); } } async function toggleNoteTodo(todoId, completed) { try { await api.put(`/api/notes/todos/${todoId}`, { completed }); const note = await api.get(`/api/notes/${currentNoteId}`); renderNoteTodos(note.todos || []); await loadNotes(); } catch (error) { console.error('Erreur toggle todo:', error); } } async function deleteNoteTodo(todoId) { const confirmed = await confirmDialog.show({ icon: '🗑️', title: 'Supprimer ce todo', message: 'Êtes-vous sûr de vouloir supprimer ce todo ?', okText: 'Supprimer' }); if (!confirmed) return; try { await api.delete(`/api/notes/todos/${todoId}`); const note = await api.get(`/api/notes/${currentNoteId}`); renderNoteTodos(note.todos || []); await loadNotes(); } catch (error) { console.error('Erreur suppression todo:', error); } } function closeNoteModal() { document.getElementById('noteEditorBackdrop').style.display = 'none'; currentNoteId = null; state.currentNote = null; } async function saveCurrentNote() { if (!currentNoteId) return; const titleInput = document.getElementById('noteTitle'); const contentTextarea = document.getElementById('noteContent'); try { await api.put(`/api/notes/${currentNoteId}`, { title: titleInput.value, content: contentTextarea.value }); await loadNotes(); } catch (error) { console.error('Erreur sauvegarde:', error); throw error; } } async function deleteCurrentNote() { if (!currentNoteId) return; const confirmed = await confirmDialog.show({ icon: '🗑️', title: 'Supprimer cette note', message: 'Êtes-vous sûr de vouloir supprimer cette note ?\n\nTous les todos associés seront également supprimés.', okText: 'Supprimer' }); if (!confirmed) return; try { await api.delete(`/api/notes/${currentNoteId}`); closeNoteModal(); await loadNotes(); } catch (error) { console.error('Erreur suppression note:', error); } } async function addNoteImage() { document.getElementById('imageInput').click(); } async function handleImageUpload(event) { const file = event.target.files[0]; if (!file || !currentNoteId) return; const formData = new FormData(); formData.append('image', file); try { const result = await api.uploadFile(`/api/notes/${currentNoteId}/image`, formData); document.getElementById('noteImage').src = result.url; document.getElementById('noteImageContainer').style.display = 'block'; await loadNotes(); } catch (error) { console.error('Erreur upload image:', error); alert('Erreur lors de l\'upload de l\'image'); } } async function removeNoteImage() { if (!currentNoteId) return; const confirmed = await confirmDialog.show({ icon: '🖼️', title: 'Supprimer l\'image', message: 'Êtes-vous sûr de vouloir supprimer cette image ?', okText: 'Supprimer' }); if (!confirmed) return; try { await api.delete(`/api/notes/${currentNoteId}/image`); document.getElementById('noteImageContainer').style.display = 'none'; await loadNotes(); } catch (error) { console.error('Erreur suppression image:', error); } } // ==================== TODOS GLOBAUX ==================== async function loadTodos() { try { state.todos = await api.get('/api/todos'); renderTodos(); } catch (error) { console.error('Erreur chargement todos:', error); } } function renderTodos() { const todoList = document.getElementById('todoList'); if (!todoList) return; todoList.innerHTML = ''; const filtered = state.todos.filter(todo => { if (state.filter === 'active') return !todo.completed; if (state.filter === 'completed') return todo.completed; return true; }); filtered.forEach(todo => { const item = document.createElement('div'); item.className = 'todo-item' + (todo.completed ? ' completed' : ''); item.innerHTML = ` ${escapeHtml(todo.text)} `; todoList.appendChild(item); }); updateTodoCounter(); } async function addTodo() { const input = document.getElementById('todoInput'); const text = input.value.trim(); if (!text) return; try { await api.post('/api/todos', { text }); input.value = ''; await loadTodos(); } catch (error) { console.error('Erreur ajout todo:', error); } } async function toggleTodo(id, completed) { try { await api.put(`/api/todos/${id}`, { completed }); await loadTodos(); } catch (error) { console.error('Erreur toggle todo:', error); } } async function deleteTodo(id) { const confirmed = await confirmDialog.show({ icon: '🗑️', title: 'Supprimer cette tâche', message: 'Êtes-vous sûr de vouloir supprimer cette tâche ?', okText: 'Supprimer' }); if (!confirmed) return; try { await api.delete(`/api/todos/${id}`); await loadTodos(); } catch (error) { console.error('Erreur suppression todo:', error); } } function setTodoFilter(filter) { state.filter = filter; document.querySelectorAll('.filter-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.filter === filter); }); renderTodos(); } function updateTodoCounter() { const counter = document.getElementById('todoCounter'); if (!counter) return; const remaining = state.todos.filter(t => !t.completed).length; counter.textContent = `${remaining} tâche${remaining > 1 ? 's' : ''} restante${remaining > 1 ? 's' : ''}`; } // ==================== SEARCH ==================== function setupSearch() { const searchInput = document.getElementById('searchInput'); const searchClear = document.getElementById('searchClear'); if (!searchInput) return; const performSearch = utils.debounce(() => { state.searchQuery = searchInput.value.trim(); renderNotes(); if (state.searchQuery) { searchClear.style.display = 'block'; } else { searchClear.style.display = 'none'; } }, 300); searchInput.addEventListener('input', performSearch); if (searchClear) { searchClear.addEventListener('click', () => { searchInput.value = ''; state.searchQuery = ''; searchClear.style.display = 'none'; renderNotes(); }); } } // ==================== ADMIN ==================== async function loadUsers() { try { const users = await api.get('/api/users'); renderUsersTable(users); } catch (error) { console.error('Erreur chargement utilisateurs:', error); alert('Erreur lors du chargement des utilisateurs'); } } function renderUsersTable(users) { const tbody = document.getElementById('usersTableBody'); if (!tbody) return; tbody.innerHTML = users.map(user => ` ${user.id} ${user.username} ${user.is_admin ? '✓ Oui' : '✗ Non'} ${new Date(user.created_at).toLocaleDateString('fr-FR')} ${user.id !== state.user.id ? ` ` : ''} `).join(''); } async function openAdminModal() { const modal = document.getElementById('adminModal'); if (!modal) return; modal.style.display = 'flex'; setTimeout(() => modal.classList.add('active'), 10); await loadUsers(); } function closeAdminModal() { const modal = document.getElementById('adminModal'); if (!modal) return; modal.classList.remove('active'); setTimeout(() => { modal.style.display = 'none'; }, 300); } async function createUser() { const username = prompt('Nom d\'utilisateur:'); if (!username || username.trim().length < 3) { alert('Le nom d\'utilisateur doit contenir au moins 3 caractères'); return; } const password = prompt('Mot de passe:'); if (!password || password.length < 4) { alert('Le mot de passe doit contenir au moins 4 caractères'); return; } const isAdmin = await confirmDialog.show({ icon: '👤', title: 'Droits administrateur', message: `Créer "${username}" en tant qu'administrateur ?`, okText: 'Oui, admin', cancelText: 'Non, utilisateur' }); try { await api.post('/api/users', { username: username.trim(), password, is_admin: isAdmin }); alert('Utilisateur créé avec succès'); await loadUsers(); } catch (error) { console.error('Erreur création utilisateur:', error); alert('Erreur lors de la création de l\'utilisateur. Il existe peut-être déjà.'); } } async function changeUserPassword(id, username) { const password = prompt(`Nouveau mot de passe pour "${username}" :`); if (!password) return; if (password.length < 6) { alert('Le mot de passe doit contenir au moins 6 caractères'); return; } const confirmPassword = prompt('Confirmer le nouveau mot de passe :'); if (password !== confirmPassword) { alert('Les mots de passe ne correspondent pas'); return; } try { await api.put(`/api/users/${id}`, { password }); alert('Mot de passe modifié avec succès'); } catch (error) { console.error('Erreur modification mot de passe:', error); alert('Erreur lors de la modification du mot de passe'); } } async function deleteUser(id, username) { const confirmed = await confirmDialog.show({ icon: '⚠️', title: 'Supprimer cet utilisateur', message: `Êtes-vous sûr de vouloir supprimer l'utilisateur "${username}" ?\n\nToutes ses notes et todos seront également supprimés définitivement.`, okText: 'Supprimer' }); if (!confirmed) return; try { await api.delete(`/api/users/${id}`); alert('Utilisateur supprimé avec succès'); await loadUsers(); } catch (error) { console.error('Erreur suppression utilisateur:', error); alert('Erreur lors de la suppression de l\'utilisateur'); } } // ==================== INIT ==================== async function init() { // Vérifier l'authentification await initAuth(); // Charger les données await Promise.all([loadNotes(), loadTodos()]); // Setup search setupSearch(); // Event listeners const newNoteBtn = document.getElementById('newNoteBtn'); if (newNoteBtn) { newNoteBtn.addEventListener('click', createNewNote); } // View filters (Active/Archived) document.querySelectorAll('.view-filter').forEach(btn => { btn.addEventListener('click', () => setNoteView(btn.dataset.view)); }); // Admin button const adminBtn = document.getElementById('adminBtn'); if (adminBtn) { adminBtn.addEventListener('click', openAdminModal); } const closeAdminModalBtn = document.getElementById('closeAdminModal'); if (closeAdminModalBtn) { closeAdminModalBtn.addEventListener('click', closeAdminModal); } const createUserBtn = document.getElementById('createUserBtn'); if (createUserBtn) { createUserBtn.addEventListener('click', createUser); } const adminModalBackdrop = document.getElementById('adminModal'); if (adminModalBackdrop) { adminModalBackdrop.addEventListener('click', (e) => { if (e.target === adminModalBackdrop) closeAdminModal(); }); } const closeEditor = document.getElementById('closeEditor'); if (closeEditor) { closeEditor.addEventListener('click', closeNoteModal); } const saveNoteBtn = document.getElementById('saveNote'); if (saveNoteBtn) { saveNoteBtn.addEventListener('click', async () => { if (currentNoteId) { await saveCurrentNote(); alert('Note enregistrée !'); } }); } const deleteNote = document.getElementById('deleteNote'); if (deleteNote) { deleteNote.addEventListener('click', deleteCurrentNote); } const addImageBtn = document.getElementById('addImageBtn'); if (addImageBtn) { addImageBtn.addEventListener('click', addNoteImage); } const imageInput = document.getElementById('imageInput'); if (imageInput) { imageInput.addEventListener('change', handleImageUploadInline); } const fileInput = document.getElementById('fileInput'); if (fileInput) { fileInput.addEventListener('change', handleFileUploadInline); } const removeImage = document.getElementById('removeImage'); if (removeImage) { removeImage.addEventListener('click', removeNoteImage); } const addNoteTodoBtn = document.getElementById('addNoteTodo'); if (addNoteTodoBtn) { addNoteTodoBtn.addEventListener('click', addNoteTodo); } const addTodoBtn = document.getElementById('addTodoBtn'); if (addTodoBtn) { addTodoBtn.addEventListener('click', addTodo); } const todoInput = document.getElementById('todoInput'); if (todoInput) { todoInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') addTodo(); }); } // Todo filters document.querySelectorAll('.filter-btn').forEach(btn => { btn.addEventListener('click', () => setTodoFilter(btn.dataset.filter)); }); // Event delegation pour les todos de la sidebar const todoList = document.getElementById('todoList'); if (todoList) { todoList.addEventListener('change', (e) => { if (e.target.classList.contains('todo-checkbox')) { const todoId = parseInt(e.target.dataset.todoId); toggleTodo(todoId, e.target.checked); } }); todoList.addEventListener('click', (e) => { if (e.target.classList.contains('todo-delete')) { const todoId = parseInt(e.target.dataset.todoId); deleteTodo(todoId); } }); } // Event delegation pour les todos des notes const noteTodosList = document.getElementById('noteTodosList'); if (noteTodosList) { noteTodosList.addEventListener('change', (e) => { if (e.target.classList.contains('todo-checkbox')) { const todoId = parseInt(e.target.dataset.noteTodoId); toggleNoteTodo(todoId, e.target.checked); } }); noteTodosList.addEventListener('click', (e) => { if (e.target.classList.contains('todo-delete')) { const todoId = parseInt(e.target.dataset.noteTodoId); deleteNoteTodo(todoId); } }); } // Event delegation pour les boutons d'administration des utilisateurs const usersTableBody = document.getElementById('usersTableBody'); if (usersTableBody) { usersTableBody.addEventListener('click', (e) => { // Bouton changer mot de passe if (e.target.classList.contains('btn-edit-user') || e.target.closest('.btn-edit-user')) { const btn = e.target.classList.contains('btn-edit-user') ? e.target : e.target.closest('.btn-edit-user'); const userId = parseInt(btn.dataset.userId); const username = btn.dataset.username; changeUserPassword(userId, username); } // Bouton supprimer utilisateur else if (e.target.classList.contains('btn-delete-user') || e.target.closest('.btn-delete-user')) { const btn = e.target.classList.contains('btn-delete-user') ? e.target : e.target.closest('.btn-delete-user'); const userId = parseInt(btn.dataset.userId); const username = btn.dataset.username; deleteUser(userId, username); } }); } // Close editor on backdrop click const noteEditorBackdrop = document.getElementById('noteEditorBackdrop'); if (noteEditorBackdrop) { noteEditorBackdrop.addEventListener('click', (e) => { if (e.target === noteEditorBackdrop) closeNoteModal(); }); } // Event delegation for notes grid const notesGrid = document.getElementById('notesGrid'); if (notesGrid) { // Click on compact note to expand notesGrid.addEventListener('click', (e) => { const compactNote = e.target.closest('.note-card-compact'); if (compactNote) { const noteId = parseInt(compactNote.dataset.noteId); expandNote(noteId); return; } // Collapse note button if (e.target.closest('.btn-collapse-note')) { collapseNote(); return; } // Archive note button const archiveBtn = e.target.closest('.btn-archive-note'); if (archiveBtn) { const noteId = parseInt(archiveBtn.dataset.noteId); const archived = archiveBtn.dataset.archived === 'true'; archiveNote(noteId, !archived); return; } // Delete note button const deleteBtn = e.target.closest('.btn-delete-note-inline'); if (deleteBtn) { const noteId = parseInt(deleteBtn.dataset.noteId); deleteNoteInline(noteId); return; } // Add image button const addImageBtn = e.target.closest('.btn-add-image-inline'); if (addImageBtn) { const noteId = parseInt(addImageBtn.dataset.noteId); triggerImageUpload(noteId); return; } // Remove image button const removeImageBtn = e.target.closest('.btn-remove-image-inline'); if (removeImageBtn) { const noteId = parseInt(removeImageBtn.dataset.noteId); removeImageInline(noteId); return; } // Add todo button const addTodoBtn = e.target.closest('.btn-add-todo-inline'); if (addTodoBtn) { const noteId = parseInt(addTodoBtn.dataset.noteId); const inputElement = document.querySelector(`.add-todo-inline-input[data-note-id="${noteId}"]`); if (inputElement) { addNoteTodoInline(noteId, inputElement); } return; } // Add file button const addFileBtn = e.target.closest('.btn-add-file-inline'); if (addFileBtn) { const noteId = parseInt(addFileBtn.dataset.noteId); triggerFileUpload(noteId); return; } // Delete todo inline button if (e.target.classList.contains('todo-delete-inline')) { const todoId = parseInt(e.target.dataset.todoInlineId); deleteNoteTodoInline(todoId); return; } // Download file button const downloadBtn = e.target.closest('.btn-download-file'); if (downloadBtn) { const fileId = parseInt(downloadBtn.dataset.fileId); downloadFile(fileId); return; } // Delete file button const deleteFileBtn = e.target.closest('.btn-delete-file'); if (deleteFileBtn) { const fileId = parseInt(deleteFileBtn.dataset.fileId); deleteFile(fileId); return; } }); // Checkbox change for inline todos notesGrid.addEventListener('change', (e) => { if (e.target.classList.contains('todo-checkbox-inline')) { const todoId = parseInt(e.target.dataset.todoInlineId); toggleNoteTodoInline(todoId, e.target.checked); } }); // Enter key on add todo input notesGrid.addEventListener('keypress', (e) => { if (e.target.classList.contains('add-todo-inline-input') && e.key === 'Enter') { const noteId = parseInt(e.target.dataset.noteId); addNoteTodoInline(noteId, e.target); } }); } // Close modal on Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (state.expandedNoteId) { collapseNote(); } else { closeNoteModal(); document.getElementById('adminModal').style.display = 'none'; } } }); } // Démarrer l'application if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }