mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
- Adaptation pour note editor au lieu de modal - Changement des IDs (noteModal → noteEditorBackdrop) - Ajout bouton Enregistrer avec feedback - Event listeners mis à jour - Fonction saveCurrentNote pour sauvegarde manuelle - Fermeture par backdrop ou Escape maintenue - Auto-save conservé avec debounce - Toutes les fonctionnalités préservées
1004 lines
28 KiB
JavaScript
1004 lines
28 KiB
JavaScript
// 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 = '<p style="grid-column: 1/-1; text-align: center; color: var(--color-text-secondary); padding: 40px;">Aucune note trouvée</p>';
|
|
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 = `<div class="note-card-title">${escapeHtml(note.title)}</div>`;
|
|
|
|
if (note.content) {
|
|
html += `<div class="note-card-content">${escapeHtml(utils.truncate(note.content))}</div>`;
|
|
}
|
|
|
|
if (note.image_filename) {
|
|
html += `<img src="/uploads/${note.image_filename}" class="note-card-image" alt="">`;
|
|
}
|
|
|
|
html += `<div class="note-card-meta">`;
|
|
html += `<span>${utils.formatDate(note.updated_at)}</span>`;
|
|
if (note.todos_count > 0) {
|
|
html += `<span class="note-card-badge">${note.todos_count} tasks</span>`;
|
|
}
|
|
html += `</div>`;
|
|
|
|
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 = `
|
|
<input type="checkbox" class="todo-checkbox" data-note-todo-id="${todo.id}" ${todo.completed ? 'checked' : ''}>
|
|
<span class="todo-text">${escapeHtml(todo.text)}</span>
|
|
<button class="todo-delete" data-note-todo-id="${todo.id}">✕</button>
|
|
`;
|
|
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 = `
|
|
<input type="checkbox" class="todo-checkbox" data-todo-id="${todo.id}" ${todo.completed ? 'checked' : ''}>
|
|
<span class="todo-text">${escapeHtml(todo.text)}</span>
|
|
<button class="todo-delete" data-todo-id="${todo.id}">✕</button>
|
|
`;
|
|
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 => `
|
|
<tr>
|
|
<td>${user.id}</td>
|
|
<td>${user.username}</td>
|
|
<td>${user.is_admin ? '✓ Oui' : '✗ Non'}</td>
|
|
<td>${new Date(user.created_at).toLocaleDateString('fr-FR')}</td>
|
|
<td>
|
|
<button class="btn-edit-user" data-user-id="${user.id}" data-username="${escapeHtml(user.username)}">
|
|
🔑 Mot de passe
|
|
</button>
|
|
${user.id !== state.user.id ? `
|
|
<button class="btn-delete-user" data-user-id="${user.id}" data-username="${escapeHtml(user.username)}">
|
|
🗑️ Supprimer
|
|
</button>
|
|
` : ''}
|
|
</td>
|
|
</tr>
|
|
`).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();
|
|
}
|