// 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, filter: 'all', searchQuery: '', 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 response = await api.get('/api/notes'); // 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'); card.className = 'note-card'; card.onclick = () => openNoteModal(note.id); let html = `
${escapeHtml(note.title)}
`; if (note.content) { html += `
${escapeHtml(utils.truncate(note.content))}
`; } if (note.image_filename) { html += ``; } html += `
`; html += `${utils.formatDate(note.updated_at)}`; if (note.todos_count > 0) { html += `${note.todos_count} tasks`; } html += `
`; card.innerHTML = html; return card; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ==================== MODAL ==================== 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', () => openNoteModal()); } // 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', handleImageUpload); } 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(); }); } // Close modal on Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeNoteModal(); document.getElementById('adminModal').style.display = 'none'; } }); } // Démarrer l'application if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }