mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
UX: Correction angles carte + Input inline todos + Système de fichiers complet
## Corrections UX: - Fix coins arrondis en bas des notes étendues (overflow: hidden) - Remplacement prompt() par input inline pour ajout de tâches - Bouton "Ajouter" avec validation au clic ou touche Entrée - Expérience utilisateur fluide sans popups intrusifs ## Système de fichiers complet: ### Backend: - Création table note_files (id, note_id, filename, original_name, file_size, mime_type, created_at) - POST /api/notes/:id/files - Upload fichier (multer, 10MB max) - GET /api/notes/:id/files - Liste fichiers d'une note - GET /api/notes/files/:fileId/download - Téléchargement sécurisé - DELETE /api/notes/files/:fileId - Suppression fichier - Stockage dans public/uploads/files/ - Auto-création dossier si nécessaire ### Frontend: - Bouton "Ajouter un fichier" dans footer des notes - Input hidden fileInput pour sélection - Affichage liste fichiers avec icône, nom, taille - Boutons télécharger/supprimer par fichier - Fonction formatFileSize() pour affichage lisible - Event delegation pour tous les boutons fichiers - Confirmation avant suppression ### Intégration: - GET /api/notes charge maintenant files[] et files_count - renderInlineFiles() génère HTML des fichiers - downloadFile() ouvre download dans nouvel onglet - deleteFile() avec confirmation dialog - Reload automatique après upload/delete - Conservation état étendu après opérations ### CSS moderne: - .note-file-item avec hover effect - .note-file-icon, .note-file-info, .note-file-actions - Buttons download/delete avec transitions - Design cohérent avec reste de l'app - Truncate nom fichier avec ellipsis Tous types de fichiers acceptés (PDF, DOC, ZIP, etc.)
This commit is contained in:
5 files changed
+526
-16
No files matched your search
@@ -74,10 +74,25 @@ function initDatabase() {
|
||||
)
|
||||
`);
|
||||
|
||||
// Table note_files (fichiers attachés aux notes)
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS note_files (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
note_id INTEGER NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
file_size INTEGER,
|
||||
mime_type TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Créer les index pour la performance
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_note_todos ON note_todos(note_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_global_todos_user ON global_todos(user_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_note_files ON note_files(note_id)');
|
||||
|
||||
logger.info('✓ Tables de base de données créées avec succès');
|
||||
|
||||
|
||||
+152
-8
@@ -28,6 +28,7 @@
|
||||
cursor: default;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-color: var(--color-accent);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.note-card.expanded:hover {
|
||||
@@ -312,26 +313,54 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.btn-add-todo-inline {
|
||||
.add-todo-inline-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-todo-inline-input {
|
||||
flex: 1;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 1px dashed var(--color-border);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
background: var(--color-background);
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.add-todo-inline-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
background: var(--color-card);
|
||||
}
|
||||
|
||||
.add-todo-inline-input::placeholder {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.btn-add-todo-inline {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 1px solid var(--color-accent);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-add-todo-inline:hover {
|
||||
background: var(--color-hover);
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-accent-hover);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.btn-add-todo-inline:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.note-footer-inline {
|
||||
@@ -375,6 +404,121 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.btn-add-file-inline {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-add-file-inline:hover {
|
||||
background: var(--color-hover);
|
||||
color: var(--color-text-primary);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.btn-add-file-inline svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-files-container-inline {
|
||||
padding: 0 var(--spacing-lg) var(--spacing-md);
|
||||
}
|
||||
|
||||
.note-files-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.note-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--color-background);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.note-file-item:hover {
|
||||
background: var(--color-hover);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.note-file-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.note-file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.note-file-name {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.note-file-size {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.note-file-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.btn-download-file,
|
||||
.btn-delete-file {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 6px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-download-file:hover {
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-delete-file:hover {
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-download-file svg,
|
||||
.btn-delete-file svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TODO BOX
|
||||
============================================ */
|
||||
|
||||
@@ -195,6 +195,7 @@
|
||||
|
||||
<!-- Hidden file input -->
|
||||
<input type="file" id="imageInput" accept="image/*" style="display: none;">
|
||||
<input type="file" id="fileInput" style="display: none;">
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="/js/complete-app.js"></script>
|
||||
|
||||
+182
-7
@@ -327,21 +327,37 @@ function createNoteCard(note) {
|
||||
<button class="btn-remove-image-inline" data-note-id="${note.id}">✕</button>
|
||||
</div>
|
||||
` : ''}
|
||||
${note.files && note.files.length > 0 ? `
|
||||
<div class="note-files-container-inline">
|
||||
<div class="note-files-list" id="files-${note.id}">
|
||||
${renderInlineFiles(note.files || [])}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="note-todos-inline">
|
||||
<h4>Todos</h4>
|
||||
<div class="todos-list-inline" id="todos-${note.id}">
|
||||
${renderInlineTodos(note.todos || [])}
|
||||
</div>
|
||||
<button class="btn-add-todo-inline" data-note-id="${note.id}">+ Ajouter une tâche</button>
|
||||
<div class="add-todo-inline-container">
|
||||
<input type="text" class="add-todo-inline-input" placeholder="Ajouter une tâche..." data-note-id="${note.id}">
|
||||
<button class="btn-add-todo-inline" data-note-id="${note.id}">Ajouter</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note-footer-inline">
|
||||
<button class="btn-add-image-inline" data-note-id="${note.id}">
|
||||
<button class="btn-add-image-inline" data-note-id="${note.id}" title="Ajouter une image">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-add-file-inline" data-note-id="${note.id}" title="Ajouter un fichier">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
|
||||
<polyline points="13 2 13 9 20 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="note-meta-inline">${utils.formatDate(note.updated_at)}</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -409,6 +425,49 @@ function renderInlineTodos(todos) {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderInlineFiles(files) {
|
||||
if (!files || files.length === 0) return '';
|
||||
|
||||
return files.map(file => {
|
||||
const sizeStr = formatFileSize(file.file_size);
|
||||
return `
|
||||
<div class="note-file-item" data-file-id="${file.id}">
|
||||
<svg class="note-file-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
|
||||
<polyline points="13 2 13 9 20 9"></polyline>
|
||||
</svg>
|
||||
<div class="note-file-info">
|
||||
<div class="note-file-name">${escapeHtml(file.original_name)}</div>
|
||||
<div class="note-file-size">${sizeStr}</div>
|
||||
</div>
|
||||
<div class="note-file-actions">
|
||||
<button class="btn-download-file" data-file-id="${file.id}" title="Télécharger">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-delete-file" data-file-id="${file.id}" title="Supprimer">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).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;
|
||||
@@ -484,12 +543,12 @@ async function deleteNoteInline(noteId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function addNoteTodoInline(noteId) {
|
||||
const text = prompt('Texte du todo:');
|
||||
if (!text || !text.trim()) return;
|
||||
async function addNoteTodoInline(noteId, inputElement) {
|
||||
const text = inputElement.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
try {
|
||||
await api.post(`/api/notes/${noteId}/todos`, { text: text.trim() });
|
||||
await api.post(`/api/notes/${noteId}/todos`, { text });
|
||||
|
||||
// Recharger seulement cette note
|
||||
const note = await api.get(`/api/notes/${noteId}`);
|
||||
@@ -503,6 +562,10 @@ async function addNoteTodoInline(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');
|
||||
@@ -559,6 +622,14 @@ function triggerImageUpload(noteId) {
|
||||
}
|
||||
}
|
||||
|
||||
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: '🖼️',
|
||||
@@ -616,6 +687,70 @@ async function handleImageUploadInline(event) {
|
||||
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', {
|
||||
@@ -1234,6 +1369,11 @@ async function init() {
|
||||
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);
|
||||
@@ -1381,7 +1521,18 @@ async function init() {
|
||||
const addTodoBtn = e.target.closest('.btn-add-todo-inline');
|
||||
if (addTodoBtn) {
|
||||
const noteId = parseInt(addTodoBtn.dataset.noteId);
|
||||
addNoteTodoInline(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;
|
||||
}
|
||||
|
||||
@@ -1391,6 +1542,22 @@ async function init() {
|
||||
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
|
||||
@@ -1400,6 +1567,14 @@ async function init() {
|
||||
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
|
||||
|
||||
+176
-1
@@ -58,7 +58,7 @@ router.get('/', async (req, res) => {
|
||||
ORDER BY n.updated_at DESC
|
||||
`, [req.user.id, archivedFilter]);
|
||||
|
||||
// Charger les todos pour chaque note
|
||||
// Charger les todos et fichiers pour chaque note
|
||||
for (const note of notes) {
|
||||
const todos = await getAll(`
|
||||
SELECT id, text, completed, position
|
||||
@@ -67,6 +67,15 @@ router.get('/', async (req, res) => {
|
||||
ORDER BY position ASC, id ASC
|
||||
`, [note.id]);
|
||||
note.todos = todos || [];
|
||||
|
||||
const files = await getAll(`
|
||||
SELECT id, filename, original_name, file_size, mime_type, created_at
|
||||
FROM note_files
|
||||
WHERE note_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [note.id]);
|
||||
note.files = files || [];
|
||||
note.files_count = files?.length || 0;
|
||||
}
|
||||
|
||||
res.json(notes || []);
|
||||
@@ -478,5 +487,171 @@ const searchNotes = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== FILE ATTACHMENTS ====================
|
||||
|
||||
// Configuration multer pour tous types de fichiers
|
||||
const fileStorage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const filesDir = path.join(__dirname, '../public/uploads/files');
|
||||
// Créer le dossier s'il n'existe pas
|
||||
if (!fs.existsSync(filesDir)) {
|
||||
fs.mkdirSync(filesDir, { recursive: true });
|
||||
}
|
||||
cb(null, filesDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueName = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${file.originalname}`;
|
||||
cb(null, uniqueName);
|
||||
}
|
||||
});
|
||||
|
||||
const fileUpload = multer({
|
||||
storage: fileStorage,
|
||||
limits: { fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024 } // 10MB
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/notes/:id/files
|
||||
* Ajouter un fichier à une note
|
||||
*/
|
||||
router.post('/:id/files', fileUpload.single('file'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'Aucun fichier fourni' });
|
||||
}
|
||||
|
||||
// Vérifier que la note appartient à l'utilisateur
|
||||
const note = await getOne('SELECT id FROM notes WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
|
||||
if (!note) {
|
||||
// Supprimer le fichier uploadé
|
||||
fs.unlinkSync(req.file.path);
|
||||
return res.status(404).json({ error: 'Note non trouvée' });
|
||||
}
|
||||
|
||||
// Enregistrer le fichier dans la base de données
|
||||
const result = await runQuery(`
|
||||
INSERT INTO note_files (note_id, filename, original_name, file_size, mime_type)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [req.params.id, req.file.filename, req.file.originalname, req.file.size, req.file.mimetype]);
|
||||
|
||||
// Mettre à jour la date de modification de la note
|
||||
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.params.id]);
|
||||
|
||||
logger.info(`Fichier ajouté à la note ${req.params.id}: ${req.file.originalname}`);
|
||||
|
||||
res.json({
|
||||
message: 'Fichier ajouté avec succès',
|
||||
file: {
|
||||
id: result.id,
|
||||
filename: req.file.filename,
|
||||
original_name: req.file.originalname,
|
||||
file_size: req.file.size,
|
||||
mime_type: req.file.mimetype
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de l\'ajout du fichier:', error);
|
||||
if (req.file && fs.existsSync(req.file.path)) {
|
||||
fs.unlinkSync(req.file.path);
|
||||
}
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/notes/:id/files
|
||||
* Liste les fichiers d'une note
|
||||
*/
|
||||
router.get('/:id/files', async (req, res) => {
|
||||
try {
|
||||
// Vérifier que la note appartient à l'utilisateur
|
||||
const note = await getOne('SELECT id FROM notes WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
|
||||
if (!note) {
|
||||
return res.status(404).json({ error: 'Note non trouvée' });
|
||||
}
|
||||
|
||||
const files = await getAll(`
|
||||
SELECT id, filename, original_name, file_size, mime_type, created_at
|
||||
FROM note_files
|
||||
WHERE note_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [req.params.id]);
|
||||
|
||||
res.json(files);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la récupération des fichiers:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/notes/files/:fileId/download
|
||||
* Télécharger un fichier
|
||||
*/
|
||||
router.get('/files/:fileId/download', async (req, res) => {
|
||||
try {
|
||||
// Vérifier que le fichier appartient à une note de l'utilisateur
|
||||
const file = await getOne(`
|
||||
SELECT nf.id, nf.filename, nf.original_name, nf.mime_type
|
||||
FROM note_files nf
|
||||
JOIN notes n ON nf.note_id = n.id
|
||||
WHERE nf.id = ? AND n.user_id = ?
|
||||
`, [req.params.fileId, req.user.id]);
|
||||
|
||||
if (!file) {
|
||||
return res.status(404).json({ error: 'Fichier non trouvé' });
|
||||
}
|
||||
|
||||
const filePath = path.join(__dirname, '../public/uploads/files', file.filename);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return res.status(404).json({ error: 'Fichier physique non trouvé' });
|
||||
}
|
||||
|
||||
res.download(filePath, file.original_name);
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors du téléchargement du fichier:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/notes/files/:fileId
|
||||
* Supprimer un fichier
|
||||
*/
|
||||
router.delete('/files/:fileId', async (req, res) => {
|
||||
try {
|
||||
// Vérifier que le fichier appartient à une note de l'utilisateur
|
||||
const file = await getOne(`
|
||||
SELECT nf.id, nf.note_id, nf.filename
|
||||
FROM note_files nf
|
||||
JOIN notes n ON nf.note_id = n.id
|
||||
WHERE nf.id = ? AND n.user_id = ?
|
||||
`, [req.params.fileId, req.user.id]);
|
||||
|
||||
if (!file) {
|
||||
return res.status(404).json({ error: 'Fichier non trouvé' });
|
||||
}
|
||||
|
||||
// Supprimer le fichier physique
|
||||
const filePath = path.join(__dirname, '../public/uploads/files', file.filename);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
// Supprimer de la base de données
|
||||
await runQuery('DELETE FROM note_files WHERE id = ?', [req.params.fileId]);
|
||||
|
||||
// Mettre à jour la date de modification de la note
|
||||
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', [file.note_id]);
|
||||
|
||||
logger.info(`Fichier supprimé: ${file.filename}`);
|
||||
|
||||
res.json({ message: 'Fichier supprimé avec succès' });
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la suppression du fichier:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports.searchNotes = searchNotes;
|
||||
Reference in new issue
Block a user