From 712d3fd21a598ad611eaaad4b87ec3335ffa8f67 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Nov 2025 14:01:14 +0000 Subject: [PATCH] feat: add Kanban board view for notes organization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create KanbanBoard component with drag-and-drop functionality - Support 5 columns: Backlog, À faire, En cours, En révision, Terminé - Use tag-based system to track note status in columns - Add view toggle between List and Kanban modes in desktop interface - Display note metadata (todos, images, files, tags) in Kanban cards - Support moving notes between columns by dragging - Auto-update tags when moving notes to different columns - Show task completion progress in Kanban cards --- src/components/KanbanBoard.tsx | 196 +++++++++++++++++++++++++++++++++ src/pages/Index.tsx | 67 ++++++++++- 2 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 src/components/KanbanBoard.tsx diff --git a/src/components/KanbanBoard.tsx b/src/components/KanbanBoard.tsx new file mode 100644 index 0000000..4e5ac7b --- /dev/null +++ b/src/components/KanbanBoard.tsx @@ -0,0 +1,196 @@ +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { CheckSquare, Image as ImageIcon, Paperclip, Tag as TagIcon, Plus, GripVertical } from "lucide-react"; +import { Note } from "@/services/NotesService"; + +interface KanbanColumn { + id: string; + title: string; + color: string; +} + +const DEFAULT_COLUMNS: KanbanColumn[] = [ + { id: 'backlog', title: 'Backlog', color: 'bg-gray-100 dark:bg-gray-800' }, + { id: 'todo', title: 'À faire', color: 'bg-blue-100 dark:bg-blue-900' }, + { id: 'in_progress', title: 'En cours', color: 'bg-yellow-100 dark:bg-yellow-900' }, + { id: 'review', title: 'En révision', color: 'bg-purple-100 dark:bg-purple-900' }, + { id: 'done', title: 'Terminé', color: 'bg-green-100 dark:bg-green-900' }, +]; + +interface KanbanBoardProps { + notes: Note[]; + onNoteClick: (note: Note) => void; + onCreateNote: (columnId: string) => void; + onMoveNote?: (noteId: number, newStatus: string) => void; +} + +export function KanbanBoard({ notes, onNoteClick, onCreateNote, onMoveNote }: KanbanBoardProps) { + const [draggedNote, setDraggedNote] = useState(null); + const [dragOverColumn, setDragOverColumn] = useState(null); + + // Group notes by their kanban_status tag or default to 'backlog' + const getNoteStatus = (note: Note): string => { + const statusTag = note.tags?.find(tag => + DEFAULT_COLUMNS.some(col => col.id === tag.name.toLowerCase()) + ); + return statusTag?.name.toLowerCase() || 'backlog'; + }; + + const notesByColumn = DEFAULT_COLUMNS.reduce((acc, column) => { + acc[column.id] = notes.filter(note => + !note.archived && getNoteStatus(note) === column.id + ); + return acc; + }, {} as Record); + + const handleDragStart = (note: Note) => { + setDraggedNote(note); + }; + + const handleDragOver = (e: React.DragEvent, columnId: string) => { + e.preventDefault(); + setDragOverColumn(columnId); + }; + + const handleDragLeave = () => { + setDragOverColumn(null); + }; + + const handleDrop = (e: React.DragEvent, columnId: string) => { + e.preventDefault(); + setDragOverColumn(null); + + if (draggedNote && onMoveNote && getNoteStatus(draggedNote) !== columnId) { + onMoveNote(draggedNote.id!, columnId); + } + setDraggedNote(null); + }; + + const handleDragEnd = () => { + setDraggedNote(null); + setDragOverColumn(null); + }; + + return ( +
+ {DEFAULT_COLUMNS.map((column) => ( +
handleDragOver(e, column.id)} + onDragLeave={handleDragLeave} + onDrop={(e) => handleDrop(e, column.id)} + > + + +
+ +
+ {column.title} + + {notesByColumn[column.id]?.length || 0} + + + +
+ + + +
+ {notesByColumn[column.id]?.map((note) => ( +
handleDragStart(note)} + onDragEnd={handleDragEnd} + onClick={() => onNoteClick(note)} + className={`p-3 border rounded-lg cursor-move hover:shadow-md transition-all group ${ + draggedNote?.id === note.id ? 'opacity-50' : '' + } ${note.priority ? 'border-l-4 border-l-red-500' : ''}`} + > +
+ +
+

+ {note.title || "Sans titre"} +

+ {note.content && ( +

+ {note.content.replace(/<[^>]*>/g, '')} +

+ )} +
+
+ + {/* Metadata badges */} +
+ {note.todos && note.todos.length > 0 && ( + + + {note.todos.filter(t => t.completed).length}/{note.todos.length} + + )} + {note.images && note.images.length > 0 && ( + + + {note.images.length} + + )} + {note.files && note.files.length > 0 && ( + + + {note.files.length} + + )} + {note.tags && note.tags.length > 0 && ( +
+ {note.tags.slice(0, 2).map((tag) => ( + + + {tag.name} + + ))} + {note.tags.length > 2 && ( + + +{note.tags.length - 2} + + )} +
+ )} +
+ + {/* Date */} + {note.updated_at && ( +

+ {new Date(note.updated_at).toLocaleDateString('fr-FR', { + day: '2-digit', + month: 'short' + })} +

+ )} +
+ ))} + {notesByColumn[column.id]?.length === 0 && ( +
+ Aucune note +
+ )} +
+
+
+ +
+ ))} +
+ ); +} diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index ba5de9f..44bc16a 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -15,7 +15,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command import { RichTextEditor } from "@/components/RichTextEditor"; import { PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2, - Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit, Calendar as CalendarIcon, Tag as TagIcon, MessageSquare, Send, Check, ChevronsUpDown, Star, Activity, FileDown + Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit, Calendar as CalendarIcon, Tag as TagIcon, MessageSquare, Send, Check, ChevronsUpDown, Star, Activity, FileDown, LayoutGrid, List } from "lucide-react"; import AuthService from "@/services/AuthService"; import AdminService from "@/services/AdminService"; @@ -38,6 +38,7 @@ import { TemplateSelector } from "@/components/TemplateSelector"; import type { NoteTemplate } from "@/utils/noteTemplates"; import { AdvancedSearch, type SearchFilters, type TagOption } from "@/components/AdvancedSearch"; import { PomodoroTimer } from "@/components/PomodoroTimer"; +import { KanbanBoard } from "@/components/KanbanBoard"; // ===== FONCTIONS UTILITAIRES TIMEZONE EUROPE/PARIS ===== @@ -136,6 +137,7 @@ const Index = () => { const [settings, setSettings] = useState({}); const [adminTab, setAdminTab] = useState("users"); const [showArchived, setShowArchived] = useState(false); + const [notesViewMode, setNotesViewMode] = useState<'list' | 'kanban'>('list'); const [noteTags, setNoteTags] = useState([]); const [openRouterModels, setOpenRouterModels] = useState([]); const [loadingModels, setLoadingModels] = useState(false); @@ -1229,11 +1231,35 @@ const Index = () => { onFiltersChange={setSearchFilters} availableTags={availableTags} /> + + {/* View Mode Toggle */} +
+ + +
- {/* Notes Grid - 1 column for better visibility */} -
+ {/* Notes View - List or Kanban */} + {notesViewMode === 'list' ? ( + <> +
{filteredNotes.length > 0 ? ( paginatedNotes.map(note => ( {
)} + + ) : ( + /* Kanban View */ + { + setTemplateSelectorOpen(true); + // TODO: Set initial tag based on columnId + }} + onMoveNote={async (noteId, newStatus) => { + // Move note to new column by updating its status tag + const note = notes.find(n => n.id === noteId); + if (!note) return; + + // Remove old status tags + const oldStatusTags = note.tags?.filter(tag => + ['backlog', 'todo', 'in_progress', 'review', 'done'].includes(tag.name.toLowerCase()) + ) || []; + + for (const tag of oldStatusTags) { + if (tag.id && note.id) { + await TagsService.deleteTag(note.id, tag.id); + } + } + + // Add new status tag + if (note.id) { + await TagsService.addTag(note.id, newStatus); + await loadNotes(); // Refresh notes + showSuccess(`Note déplacée vers ${newStatus}`); + } + }} + /> + )} ) : ( /* Open Note Editor */