diff --git a/public/css/components.css b/public/css/components.css index ff6a055..53f7229 100644 --- a/public/css/components.css +++ b/public/css/components.css @@ -508,3 +508,84 @@ .users-table tbody tr:hover { background: var(--color-background); } + +.btn-delete-user { + padding: var(--spacing-sm) var(--spacing-md); + background: var(--color-danger); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.875rem; + transition: all var(--transition-speed); +} + +.btn-delete-user:hover { + background: #c0392b; + transform: translateY(-1px); +} + +/* Modal de confirmation */ +.confirm-modal { + max-width: 450px; + text-align: center; + padding: var(--spacing-xl) var(--spacing-lg); +} + +.confirm-icon { + font-size: 3rem; + margin-bottom: var(--spacing-md); +} + +.confirm-title { + font-size: var(--font-size-xl); + font-weight: 600; + margin-bottom: var(--spacing-md); + color: var(--color-text); +} + +.confirm-message { + font-size: var(--font-size-base); + color: var(--color-text-secondary); + margin-bottom: var(--spacing-xl); + line-height: 1.6; +} + +.confirm-actions { + display: flex; + gap: var(--spacing-md); + justify-content: center; +} + +.btn-cancel, +.btn-confirm { + padding: var(--spacing-md) var(--spacing-xl); + border: none; + border-radius: var(--radius-md); + font-size: var(--font-size-base); + font-weight: 500; + cursor: pointer; + transition: all var(--transition-speed); + min-width: 120px; +} + +.btn-cancel { + background: var(--color-background); + color: var(--color-text); + border: 2px solid var(--color-border); +} + +.btn-cancel:hover { + background: var(--color-border); +} + +.btn-confirm { + background: var(--color-danger); + color: white; +} + +.btn-confirm:hover { + background: #c0392b; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(231, 76, 60, 0.3); +} diff --git a/public/index.html b/public/index.html index a81447b..838f31d 100644 --- a/public/index.html +++ b/public/index.html @@ -146,6 +146,19 @@ + + + diff --git a/public/js/complete-app.js b/public/js/complete-app.js index e33a060..37868a0 100644 --- a/public/js/complete-app.js +++ b/public/js/complete-app.js @@ -38,6 +38,63 @@ const utils = { } }; +// ==================== 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 = {}) { @@ -401,7 +458,14 @@ async function toggleNoteTodo(todoId, completed) { } async function deleteNoteTodo(todoId) { - if (!confirm('Supprimer ce todo ?')) return; + 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}`); @@ -421,7 +485,15 @@ function closeNoteModal() { async function deleteCurrentNote() { if (!currentNoteId) return; - if (!confirm('Supprimer cette note ?')) 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}`); @@ -456,7 +528,15 @@ async function handleImageUpload(event) { async function removeNoteImage() { if (!currentNoteId) return; - if (!confirm('Supprimer l\'image ?')) 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`); @@ -519,23 +599,33 @@ async function addTodo() { } } -async function toggleTodo(id, completed) { +// Exposer globalement pour les event handlers inline +window.toggleTodo = async function(id, completed) { try { await api.put(`/api/todos/${id}`, { completed }); await loadTodos(); } catch (error) { console.error('Erreur toggle todo:', error); } -} +}; + +window.deleteTodo = async function(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; -async function deleteTodo(id) { try { await api.delete(`/api/todos/${id}`); await loadTodos(); } catch (error) { console.error('Erreur suppression todo:', error); } -} +}; function setTodoFilter(filter) { state.filter = filter; @@ -583,6 +673,113 @@ function setupSearch() { } } +// ==================== 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 ? ` + + ` : 'Vous-même'} + + + `).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à.'); + } +} + +// Fonction globale pour la suppression d'utilisateur (appelée depuis onclick) +window.deleteUser = async function(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 @@ -600,6 +797,29 @@ async function init() { 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 closeModal = document.getElementById('closeModal'); if (closeModal) { closeModal.addEventListener('click', closeNoteModal);