import { useEffect, useState, useRef, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Checkbox } from "@/components/ui/checkbox"; import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/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 } from "lucide-react"; import AuthService from "@/services/AuthService"; import AdminService from "@/services/AdminService"; import NotesService, { Note } from "@/services/NotesService"; import TodosService, { Todo } from "@/services/TodosService"; import RssService, { RssFeed, RssArticle } from "@/services/RssService"; import SettingsService, { Settings as AppSettings } from "@/services/SettingsService"; import CalendarService, { CalendarEvent } from "@/services/CalendarService"; import TagsService, { Tag } from "@/services/TagsService"; import OpenRouterService, { OpenRouterModel } from "@/services/OpenRouterService"; import { useNavigate } from "react-router-dom"; import { showError, showSuccess } from "@/utils/toast"; import { ConfirmModal } from "@/components/modals/ConfirmModal"; import { InputModal } from "@/components/modals/InputModal"; import { AddUserModal } from "@/components/modals/AddUserModal"; interface UserType { id: number; username: string; is_admin: boolean; } const Index = () => { const [user, setUser] = useState(null); const [showAdmin, setShowAdmin] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [notes, setNotes] = useState([]); const [openNote, setOpenNote] = useState(null); const [users, setUsers] = useState([]); const [todos, setTodos] = useState([]); const [rssFeeds, setRssFeeds] = useState([]); const [rssArticles, setRssArticles] = useState([]); const [calendarEvents, setCalendarEvents] = useState([]); const [settings, setSettings] = useState({}); const [adminTab, setAdminTab] = useState("users"); const [showArchived, setShowArchived] = useState(false); const [noteTags, setNoteTags] = useState([]); const [openRouterModels, setOpenRouterModels] = useState([]); const [loadingModels, setLoadingModels] = useState(false); const [currentDateTime, setCurrentDateTime] = useState(new Date()); const [upcomingAlert, setUpcomingAlert] = useState(null); const [calendarAuthStatus, setCalendarAuthStatus] = useState<{ isAuthenticated: boolean; isExpired: boolean; needsReauth: boolean; authType?: 'oauth2' | 'service_account' | 'api_externe'; }>({ isAuthenticated: false, isExpired: false, needsReauth: true }); const navigate = useNavigate(); // Modal states const [deleteNoteModal, setDeleteNoteModal] = useState(false); const [addTodoModal, setAddTodoModal] = useState(false); const [addRssFeedModal, setAddRssFeedModal] = useState(false); const [addUserModal, setAddUserModal] = useState(false); const [deleteUserModal, setDeleteUserModal] = useState<{open: boolean, userId?: number}>({open: false}); const [changePasswordModal, setChangePasswordModal] = useState<{open: boolean, userId?: number}>({open: false}); const [addNoteTodoModal, setAddNoteTodoModal] = useState(false); const [addTagModal, setAddTagModal] = useState(false); const [selectedImage, setSelectedImage] = useState(null); const [addEventModal, setAddEventModal] = useState(false); const [editEventModal, setEditEventModal] = useState<{open: boolean, event?: CalendarEvent}>({open: false}); // Pagination states const [notesPage, setNotesPage] = useState(0); const [todosActivePage, setTodosActivePage] = useState(0); const [todosCompletedPage, setTodosCompletedPage] = useState(0); const [rssPage, setRssPage] = useState(0); const [calendarPage, setCalendarPage] = useState(0); const NOTES_PER_PAGE = 5; const TODOS_PER_PAGE = 15; const RSS_MAX_ARTICLES = 14; const RSS_PER_PAGE = 8; const CALENDAR_PER_PAGE = 10; // Chatbox states const [chatOpen, setChatOpen] = useState(false); const [chatMessages, setChatMessages] = useState>([]); const [chatInput, setChatInput] = useState(""); const [chatLoading, setChatLoading] = useState(false); const [selectedModel, setSelectedModel] = useState(""); const [defaultModel, setDefaultModel] = useState(""); const [modelSelectorOpen, setModelSelectorOpen] = useState(false); // Debounce timer for auto-save const saveTimerRef = useRef(null); const chatEndRef = useRef(null); // Update current date/time every second useEffect(() => { const timer = setInterval(() => { setCurrentDateTime(new Date()); }, 1000); return () => clearInterval(timer); }, []); // Check for upcoming events every minute useEffect(() => { const checkUpcomingEvents = () => { const now = new Date(); const thirtyMinutesFromNow = new Date(now.getTime() + 30 * 60 * 1000); const upcomingEvent = calendarEvents.find(event => { const eventStart = new Date(event.start_time); return eventStart > now && eventStart <= thirtyMinutesFromNow; }); setUpcomingAlert(upcomingEvent || null); }; checkUpcomingEvents(); const timer = setInterval(checkUpcomingEvents, 60000); // Check every minute return () => clearInterval(timer); }, [calendarEvents]); useEffect(() => { const userFromAuth = AuthService.getUser(); if (userFromAuth) { setUser(userFromAuth); loadNotes(); loadTodos(); loadRssFeeds(); loadRssArticles(); loadCalendarEvents(); } else { navigate("/login"); } }, [navigate]); // Auto-refresh des articles RSS toutes les 2 minutes useEffect(() => { const interval = setInterval(() => { loadRssArticles(); }, 2 * 60 * 1000); // 2 minutes (synchronisé avec le scheduler backend) return () => clearInterval(interval); }, []); const loadNotes = async () => { try { const fetchedNotes = await NotesService.getNotes(); if (Array.isArray(fetchedNotes)) { setNotes(fetchedNotes); } } catch (error) { console.error("Erreur lors du chargement des notes:", error); } }; const loadTodos = async () => { try { const fetchedTodos = await TodosService.getTodos(); if (Array.isArray(fetchedTodos)) { setTodos(fetchedTodos); } } catch (error) { console.error("Erreur lors du chargement des todos:", error); } }; const loadRssFeeds = async () => { try { const feeds = await RssService.getFeeds(); if (Array.isArray(feeds)) { setRssFeeds(feeds); } } catch (error) { console.error("Erreur lors du chargement des flux RSS:", error); } }; const loadRssArticles = async () => { try { const articles = await RssService.getArticles(100); if (Array.isArray(articles)) { // Les articles arrivent triés par date DESC (récent en premier) depuis le backend setRssArticles(articles); } } catch (error) { console.error("Erreur lors du chargement des articles RSS:", error); } }; const loadCalendarEvents = async () => { try { const events = await CalendarService.getEvents(50); // Charger 50 événements pour pagination if (Array.isArray(events)) { setCalendarEvents(events); } } catch (error) { console.error("Erreur lors du chargement des événements:", error); } }; const loadUsers = async () => { if (user?.is_admin) { try { const fetchedUsers = await AdminService.getUsers(); if (Array.isArray(fetchedUsers)) { setUsers(fetchedUsers); } } catch (error) { console.error("Erreur lors du chargement des utilisateurs:", error); } } }; const loadSettings = async () => { if (user?.is_admin) { try { const fetchedSettings = await SettingsService.getSettings(); setSettings(fetchedSettings || {}); } catch (error) { console.error("Erreur lors du chargement des paramètres:", error); } } }; const loadOpenRouterModels = async () => { setLoadingModels(true); try { const models = await OpenRouterService.getModels(); setOpenRouterModels(models); } catch (error) { console.error("Erreur lors du chargement des modèles:", error); setOpenRouterModels([]); // Vider en cas d'erreur } finally { setLoadingModels(false); } }; useEffect(() => { if (showAdmin) { loadUsers(); loadSettings(); } }, [showAdmin]); useEffect(() => { if (showAdmin && adminTab === 'openrouter' && settings.openrouter_api_key && openRouterModels.length === 0) { loadOpenRouterModels(); } }, [showAdmin, adminTab, settings.openrouter_api_key]); useEffect(() => { if (showAdmin && adminTab === 'calendar') { checkCalendarAuthStatus(); } }, [showAdmin, adminTab]); const checkCalendarAuthStatus = async () => { try { const status = await CalendarService.getAuthStatus(); setCalendarAuthStatus(status); } catch (error) { console.error('Erreur lors de la vérification du statut OAuth:', error); } }; const handleGoogleConnect = async () => { try { const authUrl = await CalendarService.getAuthUrl(); // Ouvrir une popup pour l'authentification OAuth const width = 600; const height = 700; const left = window.screen.width / 2 - width / 2; const top = window.screen.height / 2 - height / 2; const popup = window.open( authUrl, 'GoogleAuth', `width=${width},height=${height},top=${top},left=${left}` ); // Écouter le message de la popup const handleMessage = (event: MessageEvent) => { if (event.data.type === 'google-auth-success') { showSuccess('Authentification Google réussie'); checkCalendarAuthStatus(); window.removeEventListener('message', handleMessage); } else if (event.data.type === 'google-auth-error') { showError(`Erreur d'authentification: ${event.data.error}`); window.removeEventListener('message', handleMessage); } }; window.addEventListener('message', handleMessage); // Fermer le listener si la popup est fermée manuellement const checkPopup = setInterval(() => { if (popup && popup.closed) { clearInterval(checkPopup); window.removeEventListener('message', handleMessage); } }, 1000); } catch (error) { showError('Erreur lors de la génération de l\'URL d\'authentification'); } }; const handleGoogleDisconnect = async () => { try { const success = await CalendarService.disconnect(); if (success) { showSuccess('Déconnecté de Google Calendar'); checkCalendarAuthStatus(); } } catch (error) { showError('Erreur lors de la déconnexion'); } }; const handleLogout = () => { AuthService.logout(); navigate("/login"); }; const handleCreateNote = async () => { try { const newNote = await NotesService.createNote("Nouvelle note"); if (newNote) { setNotes([newNote, ...notes]); setOpenNote(newNote); } } catch (error) { showError("Erreur lors de la création de la note"); } }; const loadNoteTags = async (noteId: number) => { try { const tags = await TagsService.getTags(noteId); setNoteTags(tags || []); } catch (error) { console.error("Erreur lors du chargement des tags:", error); setNoteTags([]); } }; const handleOpenNote = (note: Note) => { setOpenNote(note); if (note.id) { loadNoteTags(note.id); } }; const handleCloseNote = () => { setOpenNote(null); setNoteTags([]); }; // Update note with immediate local state update const handleUpdateNote = useCallback(async (updatedFields: Partial) => { if (!openNote) return; const updatedNote = { ...openNote, ...updatedFields, todos: updatedFields.todos || openNote.todos || [], images: updatedFields.images || openNote.images || [], files: updatedFields.files || openNote.files || [] }; // Update local state immediately setOpenNote(updatedNote); setNotes(prev => prev.map(note => note.id === updatedNote.id ? updatedNote : note)); try { const success = await NotesService.updateNote(updatedNote); if (!success) { showError("Erreur lors de la sauvegarde"); } } catch (error) { showError("Erreur lors de la mise à jour de la note"); } }, [openNote, setOpenNote, setNotes]); // Debounced content update for typing const handleContentChange = useCallback((content: string) => { if (!openNote) return; // Don't update local state to avoid re-render during typing // Just save to API in background const updatedNote = { ...openNote, content }; // Debounce the API call if (saveTimerRef.current) { clearTimeout(saveTimerRef.current); } saveTimerRef.current = setTimeout(async () => { try { await NotesService.updateNote(updatedNote); // Update notes list silently after save setNotes(prev => prev.map(note => note.id === updatedNote.id ? updatedNote : note)); } catch (error) { showError("Erreur lors de la sauvegarde automatique"); } }, 1000); // Save after 1 second of inactivity }, [openNote, setNotes]); // Clean up timer on unmount useEffect(() => { return () => { if (saveTimerRef.current) { clearTimeout(saveTimerRef.current); } }; }, []); const confirmDeleteNote = async () => { if (!openNote?.id) return; try { const success = await NotesService.deleteNote(openNote.id); if (success) { const updatedNotes = notes.filter(note => note.id !== openNote.id); setNotes(updatedNotes); setOpenNote(null); showSuccess("Note supprimée"); } } catch (error) { showError("Erreur lors de la suppression"); } }; const confirmAddTodo = async (text: string) => { try { const newTodo = await TodosService.createTodo(text); if (newTodo) { setTodos([newTodo, ...todos]); showSuccess("Tâche ajoutée"); } } catch (error) { showError("Erreur lors de l'ajout de la tâche"); } }; const handleToggleTodo = async (todoId: number) => { try { await TodosService.toggleComplete(todoId); setTodos(todos.map(t => t.id === todoId ? { ...t, completed: !t.completed } : t)); } catch (error) { showError("Erreur lors de la mise à jour"); } }; const handleDeleteTodo = async (todoId: number) => { try { await TodosService.deleteTodo(todoId); setTodos(todos.filter(t => t.id !== todoId)); showSuccess("Tâche supprimée"); } catch (error) { showError("Erreur lors de la suppression"); } }; const confirmAddRssFeed = async (url: string) => { try { const newFeed = await RssService.addFeed(url); if (newFeed) { setRssFeeds([...rssFeeds, newFeed]); showSuccess("Flux RSS ajouté"); loadRssArticles(); } } catch (error) { showError("Erreur lors de l'ajout du flux"); } }; const handleDeleteRssFeed = async (feedId: number) => { try { await RssService.deleteFeed(feedId); setRssFeeds(rssFeeds.filter(f => f.id !== feedId)); showSuccess("Flux supprimé"); loadRssArticles(); } catch (error) { showError("Erreur lors de la suppression"); } }; const handleRefreshRss = async () => { try { await RssService.refreshFeeds(); loadRssArticles(); showSuccess("Flux actualisés"); } catch (error) { showError("Erreur lors de l'actualisation"); } }; const handleSendMessage = async () => { if (!chatInput.trim() || !selectedModel || chatLoading) return; const userMessage = { role: 'user', content: chatInput }; const newMessages = [...chatMessages, userMessage]; setChatMessages(newMessages); setChatInput(""); setChatLoading(true); try { const response = await OpenRouterService.sendMessage(selectedModel, newMessages); setChatMessages([...newMessages, { role: 'assistant', content: response }]); } catch (error: any) { showError(error.message || 'Erreur lors de l\'envoi du message'); setChatMessages([...newMessages, { role: 'assistant', content: 'Erreur: ' + error.message }]); } finally { setChatLoading(false); } }; const handleClearChat = () => { setChatMessages([]); }; // Scroll to bottom when new message arrives useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [chatMessages]); // Load default model from localStorage on mount useEffect(() => { const savedDefaultModel = localStorage.getItem('defaultAiModel'); if (savedDefaultModel) { setDefaultModel(savedDefaultModel); } }, []); // Load models when chatbox opens useEffect(() => { if (chatOpen && openRouterModels.length === 0) { loadOpenRouterModels(); } }, [chatOpen]); // Set default model when models are loaded useEffect(() => { if (openRouterModels.length > 0 && !selectedModel) { // If there's a default model saved, use it if (defaultModel && openRouterModels.find(m => m.id === defaultModel)) { setSelectedModel(defaultModel); } else { // Otherwise use the first model setSelectedModel(openRouterModels[0].id); } } }, [openRouterModels, selectedModel, defaultModel]); // Save default model to localStorage const handleSetDefaultModel = (modelId: string) => { setDefaultModel(modelId); localStorage.setItem('defaultAiModel', modelId); showSuccess("Modèle par défaut défini"); }; const confirmAddUser = async (username: string, password: string, isAdmin: boolean) => { try { const newUser = await AdminService.createUser(username, password, isAdmin); if (newUser) { loadUsers(); showSuccess("Utilisateur créé"); } } catch (error) { showError("Erreur lors de la création"); } }; const confirmDeleteUser = async () => { if (!deleteUserModal.userId) return; try { const success = await AdminService.deleteUser(deleteUserModal.userId); if (success) { loadUsers(); showSuccess("Utilisateur supprimé"); } } catch (error) { showError("Erreur lors de la suppression"); } }; const confirmChangePassword = async (newPassword: string) => { if (!changePasswordModal.userId) return; try { const success = await AdminService.updateUserPassword(changePasswordModal.userId, newPassword); if (success) { showSuccess("Mot de passe modifié"); } } catch (error) { showError("Erreur lors de la modification"); } }; const handleSaveSettings = async () => { try { await SettingsService.updateSettings(settings); showSuccess("Paramètres enregistrés"); } catch (error) { showError("Erreur lors de l'enregistrement"); } }; const confirmAddTag = async (tagText: string) => { if (!openNote?.id) return; try { const newTag = await TagsService.addTag(openNote.id, tagText); if (newTag) { setNoteTags([...noteTags, newTag]); showSuccess("Tag ajouté"); } } catch (error) { showError("Erreur lors de l'ajout du tag"); } }; const handleDeleteTag = async (tagId: number) => { if (!openNote?.id) return; try { const success = await TagsService.deleteTag(openNote.id, tagId); if (success) { setNoteTags(noteTags.filter(t => t.id !== tagId)); showSuccess("Tag supprimé"); } } catch (error) { showError("Erreur lors de la suppression du tag"); } }; const handleFileUpload = async (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0] && openNote?.id) { const formData = new FormData(); formData.append('file', e.target.files[0]); try { const response = await fetch(`/api/notes/${openNote.id}/files`, { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: formData }); if (response.ok) { const result = await response.json(); const newFile = result.file || result; const updatedFiles = [...(openNote.files || []), newFile]; setOpenNote({ ...openNote, files: updatedFiles }); await handleUpdateNote({ files: updatedFiles }); showSuccess("Fichier ajouté"); } } catch (error) { showError("Erreur lors de l'upload"); } finally { e.target.value = ''; } } }; const filteredNotes = notes.filter(note => (showArchived ? note.archived : !note.archived) && (searchQuery === "" || (note.title && note.title.toLowerCase().includes(searchQuery.toLowerCase())) || (note.content && note.content.toLowerCase().includes(searchQuery.toLowerCase()))) ); // Pagination for notes const totalNotesPages = Math.ceil(filteredNotes.length / NOTES_PER_PAGE); const paginatedNotes = filteredNotes.slice( notesPage * NOTES_PER_PAGE, (notesPage + 1) * NOTES_PER_PAGE ); // Reset page when changing filters or search useEffect(() => { setNotesPage(0); }, [showArchived, searchQuery]); useEffect(() => { setTodosActivePage(0); setTodosCompletedPage(0); }, [todos.length]); useEffect(() => { setRssPage(0); }, [rssArticles.length]); useEffect(() => { setCalendarPage(0); }, [calendarEvents.length]); // Pagination for todos const activeTodos = todos.filter(t => !t.completed); const completedTodos = todos.filter(t => t.completed); const totalActiveTodosPages = Math.ceil(activeTodos.length / TODOS_PER_PAGE); const totalCompletedTodosPages = Math.ceil(completedTodos.length / TODOS_PER_PAGE); const paginatedActiveTodos = activeTodos.slice( todosActivePage * TODOS_PER_PAGE, (todosActivePage + 1) * TODOS_PER_PAGE ); const paginatedCompletedTodos = completedTodos.slice( todosCompletedPage * TODOS_PER_PAGE, (todosCompletedPage + 1) * TODOS_PER_PAGE ); // Pagination for RSS const totalRssPages = Math.ceil(rssArticles.length / RSS_PER_PAGE); const paginatedRssArticles = rssArticles.slice( rssPage * RSS_PER_PAGE, (rssPage + 1) * RSS_PER_PAGE ); // Pagination for Calendar const totalCalendarPages = Math.ceil(calendarEvents.length / CALENDAR_PER_PAGE); const paginatedCalendarEvents = calendarEvents.slice( calendarPage * CALENDAR_PER_PAGE, (calendarPage + 1) * CALENDAR_PER_PAGE ); return (
{/* Top Navigation */}
{/* Alert bar for upcoming events */} {upcomingAlert && (
Rendez-vous dans moins de 30 minutes : {upcomingAlert.title} à {new Date(upcomingAlert.start_time).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
)}
NoteFlow
{currentDateTime.toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
{currentDateTime.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
{user && ( Mon compte {user.is_admin && ( setShowAdmin(true)}> Administration )} Déconnexion )}
{/* Main Content */}
{/* Three Column Layout: Calendar Left | Notes Middle | Todo+RSS Right */}
{/* Left Column: Calendar */}
Rendez-vous
{calendarEvents.length > 0 ? ( paginatedCalendarEvents.map(event => { const startDate = new Date(event.start_time); const endDate = new Date(event.end_time); const isToday = startDate.toDateString() === new Date().toDateString(); const isSoon = startDate.getTime() - new Date().getTime() < 30 * 60 * 1000 && startDate > new Date(); return (
event.html_link && window.open(event.html_link, '_blank')} >
{isToday && (
)}

{event.title}

{isToday ? "Aujourd'hui" : startDate.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' })} {event.all_day ? ' - Toute la journée' : ` à ${startDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`}

{event.location && (

📍 {event.location}

)} {isSoon && ( Dans moins de 30 min )}
event.html_link && window.open(event.html_link, '_blank')} />
); }) ) : (

Aucun rendez-vous à venir

)}
{totalCalendarPages > 1 && (
{calendarPage + 1}/{totalCalendarPages}
)}
{/* Middle Column: Notes or Open Note */}
{!openNote ? ( <> {/* Notes Grid Header */}

Mes Notes

setSearchQuery(e.target.value)} />
{/* Notes Grid - 1 column for better visibility */}
{filteredNotes.length > 0 ? ( paginatedNotes.map(note => ( handleOpenNote(note)} > {/* Priority icon */}

{note.title || "Sans titre"}

{note.content ? note.content.replace(/<[^>]*>/g, '') : "Note vide"}

{note.todos && note.todos.length > 0 && ( {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.map((tag) => ( {tag.name} )) )}
{note.updated_at && (

Modifiée le {new Date(note.updated_at).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' })}

)}
)) ) : (

{searchQuery ? "Aucune note trouvée" : "Aucune note"}

{!searchQuery && ( )}
)}
{/* Pagination pour les notes */} {totalNotesPages > 1 && (
Page {notesPage + 1} sur {totalNotesPages}
)} ) : ( /* Open Note Editor */
handleUpdateNote({ title: e.target.value })} /> {/* Action buttons */}
{ if (e.target.files && e.target.files[0] && openNote?.id) { try { const image = await NotesService.uploadImage(openNote.id, e.target.files[0]); if (image) { const updatedImages = [...(openNote.images || []), image]; setOpenNote({ ...openNote, images: updatedImages }); await handleUpdateNote({ images: updatedImages }); showSuccess("Image ajoutée"); } } catch (error) { showError("Erreur lors de l'upload de l'image"); } e.target.value = ''; } }} />
{/* Tags */} {noteTags.length > 0 && (
{noteTags.map((tag) => ( {tag.tag} tag.id && handleDeleteTag(tag.id)} /> ))}
)} {/* Todos */} {openNote.todos && openNote.todos.length > 0 && ( <>
todo list:
{openNote.todos.map((todo, index) => (
{ const updatedTodos = [...(openNote.todos || [])]; updatedTodos[index] = { ...todo, completed: !todo.completed }; await handleUpdateNote({ todos: updatedTodos }); }} /> {todo.text}
))}
)} {/* Images in thumbnails */} {openNote.images && openNote.images.length > 0 && (
{openNote.images.map((image, index) => (
{image.original_name} setSelectedImage(`/uploads/images/${image.filename}`)} />
))}
)} {/* Files */} {openNote.files && openNote.files.length > 0 && ( <>
Fichiers divers : Fichier(s)
{openNote.files.map((file: any, index: number) => (

{file.original_name || file.filename}

{file.size && (

{(file.size / 1024).toFixed(2)} KB

)}
))}
)} {/* Rich text editor */} {/* Note metadata */}
Créée le: {openNote.created_at ? new Date(openNote.created_at).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'N/A'}
Modifiée le: {openNote.updated_at ? new Date(openNote.updated_at).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'N/A'}
{/* Save button */}
)}
{/* Right Column: Todos & RSS Boxes side by side */}
{/* Todos Box */}
Tâches
Actives ({todos.filter(t => !t.completed).length}) Complétées ({todos.filter(t => t.completed).length})
{activeTodos.length > 0 ? ( paginatedActiveTodos.map(todo => (
todo.id && handleToggleTodo(todo.id)} className="h-4 w-4" /> {todo.text}
)) ) : (

Aucune tâche active

)}
{totalActiveTodosPages > 1 && (
{todosActivePage + 1}/{totalActiveTodosPages}
)}
{completedTodos.length > 0 ? ( paginatedCompletedTodos.map(todo => (
todo.id && handleToggleTodo(todo.id)} className="h-4 w-4" /> {todo.text}
)) ) : (

Aucune tâche complétée

)}
{totalCompletedTodosPages > 1 && (
{todosCompletedPage + 1}/{totalCompletedTodosPages}
)}
{/* RSS Box */}
Flux RSS
{rssArticles.length > 0 ? ( paginatedRssArticles.map(article => (
article.link && window.open(article.link, '_blank')} >

{article.title}

{article.description && (

{article.description.replace(/<[^>]*>/g, '')}

)}
)) ) : (

Aucun article

)}
{totalRssPages > 1 && (
{rssPage + 1}/{totalRssPages}
)}
{/* Admin Modal */} Administration Gérez les utilisateurs et les paramètres de l'application Utilisateurs Flux RSS Google Calendar OpenRouter Paramètres
{users.map(u => (
{u.username.charAt(0).toUpperCase()}

{u.username}

{u.is_admin ? "Administrateur" : "Utilisateur"}

{u.id !== 1 && ( )}
))}
{rssFeeds.map(feed => (

{feed.title}

{feed.url}

))} {rssFeeds.length === 0 && (

Aucun flux RSS configuré

)}
Configuration Google Calendar OAuth 2.0
setSettings({ ...settings, app_url: e.target.value })} />

URL de votre site (utilisée pour les redirections OAuth)

setSettings({ ...settings, google_client_id: e.target.value })} />
setSettings({ ...settings, google_client_secret: e.target.value })} />

Créez un projet OAuth 2.0 sur{' '} Google Cloud Console {' '}et activez l'API Google Calendar.

⚠️ IMPORTANT : URI de redirection autorisée

Ajoutez cette URL exacte dans la console Google Cloud (OAuth 2.0 Client IDs → URIs de redirection autorisées) :

{settings.app_url || window.location.origin}/api/calendar/oauth-callback
{settings.google_client_id && settings.google_client_secret && ( <>

Statut de connexion

{calendarAuthStatus.isAuthenticated && !calendarAuthStatus.isExpired ? (
Connecté à Google Calendar (OAuth 2.0)
) : (
Non connecté
)}
{calendarAuthStatus.needsReauth ? ( ) : ( <> )}
)}
Configuration OpenRouter
setSettings({ ...settings, openrouter_api_key: e.target.value })} />

Obtenez votre clé API sur{' '} openrouter.ai

Paramètres généraux
setSettings({ ...settings, rss_refresh_interval: parseInt(e.target.value) })} />
{/* Modals */} setDeleteUserModal({open})} title="Supprimer l'utilisateur" description="Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible." onConfirm={confirmDeleteUser} confirmText="Supprimer" variant="destructive" /> setChangePasswordModal({open})} title="Changer le mot de passe" label="Nouveau mot de passe" placeholder="Entrez le nouveau mot de passe..." onConfirm={confirmChangePassword} confirmText="Modifier" type="password" /> { if (openNote) { const updatedTodos = [...(openNote.todos || []), { text, completed: false }]; await handleUpdateNote({ todos: updatedTodos }); } }} confirmText="Ajouter" /> {/* Add Event Modal */} Ajouter un rendez-vous Créer un événement dans votre Google Calendar
{ e.preventDefault(); const formData = new FormData(e.currentTarget); try { const reminderMinutes = formData.get('reminderMinutes') as string; const recurrenceValue = formData.get('recurrence') as string; const eventData = { title: formData.get('title') as string, description: formData.get('description') as string || undefined, startDateTime: new Date(formData.get('startDateTime') as string).toISOString(), endDateTime: new Date(formData.get('endDateTime') as string).toISOString(), location: formData.get('location') as string || undefined, attendees: (formData.get('attendees') as string || '').split(',').map(e => e.trim()).filter(e => e), reminders: reminderMinutes && reminderMinutes !== '0' ? [ { method: 'popup', minutes: parseInt(reminderMinutes) } ] : undefined, recurrence: recurrenceValue ? [recurrenceValue] : undefined, visibility: formData.get('visibility') as string || 'default', colorId: formData.get('colorId') as string || undefined }; await CalendarService.createEvent(eventData); showSuccess('Événement créé avec succès'); setAddEventModal(false); await loadCalendarEvents(); e.currentTarget.reset(); } catch (error: any) { showError(error.response?.data?.error || 'Erreur lors de la création de l\'événement'); } }} className="space-y-4">