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 { RichTextEditor } from "@/components/RichTextEditor"; import { MadeWithDyad } from "@/components/made-with-dyad"; 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 } 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); // Pagination states const [notesPage, setNotesPage] = useState(0); const [todosActivePage, setTodosActivePage] = useState(0); const [todosCompletedPage, setTodosCompletedPage] = useState(0); const [rssPage, setRssPage] = useState(0); const NOTES_PER_PAGE = 5; const TODOS_PER_PAGE = 10; const RSS_MAX_ARTICLES = 14; const RSS_PER_PAGE = 7; // Debounce timer for auto-save const saveTimerRef = 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]); 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(14); if (Array.isArray(articles)) { setRssArticles(articles); } } catch (error) { console.error("Erreur lors du chargement des articles RSS:", error); } }; const loadCalendarEvents = async () => { try { const events = await CalendarService.getEvents(10); // Limité à 10 événements 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 () => { if (user?.is_admin && settings.openrouter_api_key) { setLoadingModels(true); try { const models = await OpenRouterService.getModels(); setOpenRouterModels(models); } catch (error) { console.error("Erreur lors du chargement des modèles:", error); } 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 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]); // 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 ); 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 ? ( calendarEvents.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' })} à {startDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}

{event.location && (

📍 {event.location}

)} {isSoon && ( Dans moins de 30 min )}
); }) ) : (

Aucun rendez-vous à venir

)}
{/* 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)} >

{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.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 && (
{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 && (
{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'}
)}
{/* 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 {/* Choix du type d'authentification */}

OAuth 2.0 : Pour accéder au calendrier personnel avec autorisation de l'utilisateur.
Service Account : Pour accéder à des calendriers partagés sans interaction utilisateur.
API externe : Utilisez votre propre API backend pour gérer la connexion à Google Calendar.

{/* Configuration OAuth2 */} {(!settings.google_auth_type || settings.google_auth_type === 'oauth2') && ( <>
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) :

{window.location.origin}/api/calendar/oauth-callback

💡 Si vous utilisez un domaine différent, créez un fichier .env avec APP_URL=https://votre-domaine.com

)} {/* Configuration Service Account */} {settings.google_auth_type === 'service_account' && ( <>