| <!DOCTYPE html> |
| <html lang="fr" class="h-full"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>TaskMaster - Gestionnaire de tâches</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <script> |
| tailwind.config = { |
| darkMode: 'class', |
| theme: { |
| extend: { |
| colors: { |
| urgent: '#dc2626', |
| high: '#ea580c', |
| normal: '#2563eb', |
| low: '#6b7280', |
| work: '#2563eb', |
| personal: '#16a34a', |
| property: '#7c3aed', |
| admin: '#db2777', |
| } |
| } |
| } |
| } |
| </script> |
| <style> |
| .task-card:hover .task-actions { |
| opacity: 1; |
| } |
| .priority-column { |
| min-height: calc(100vh - 200px); |
| } |
| .drawer { |
| transition: transform 0.3s ease-in-out; |
| } |
| .drawer-closed { |
| transform: translateX(100%); |
| } |
| .drawer-open { |
| transform: translateX(0); |
| } |
| .fade-in { |
| animation: fadeIn 0.3s ease-in-out; |
| } |
| @keyframes fadeIn { |
| from { opacity: 0; } |
| to { opacity: 1; } |
| } |
| .slide-up { |
| animation: slideUp 0.2s ease-out; |
| } |
| @keyframes slideUp { |
| from { transform: translateY(20px); opacity: 0; } |
| to { transform: translateY(0); opacity: 1; } |
| } |
| .checkbox-container { |
| position: relative; |
| padding-left: 35px; |
| cursor: pointer; |
| } |
| .checkbox-container input { |
| position: absolute; |
| opacity: 0; |
| cursor: pointer; |
| height: 0; |
| width: 0; |
| } |
| .checkmark { |
| position: absolute; |
| top: 0; |
| left: 0; |
| height: 24px; |
| width: 24px; |
| border: 2px solid #d1d5db; |
| border-radius: 6px; |
| transition: all 0.2s; |
| } |
| .checkbox-container:hover .checkmark { |
| border-color: #9ca3af; |
| } |
| .checkbox-container input:checked ~ .checkmark { |
| background-color: #2563eb; |
| border-color: #2563eb; |
| } |
| .checkmark:after { |
| content: ""; |
| position: absolute; |
| display: none; |
| } |
| .checkbox-container input:checked ~ .checkmark:after { |
| display: block; |
| } |
| .checkbox-container .checkmark:after { |
| left: 7px; |
| top: 3px; |
| width: 6px; |
| height: 12px; |
| border: solid white; |
| border-width: 0 2px 2px 0; |
| transform: rotate(45deg); |
| } |
| .dark .checkbox-container input:checked ~ .checkmark { |
| background-color: #3b82f6; |
| border-color: #3b82f6; |
| } |
| .dark .checkmark { |
| border-color: #4b5563; |
| } |
| .dark .checkbox-container:hover .checkmark { |
| border-color: #6b7280; |
| } |
| </style> |
| </head> |
| <body class="h-full bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-200"> |
| |
| <div id="auth-modal" class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 hidden"> |
| <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6 mx-4 fade-in"> |
| <div class="flex justify-between items-center mb-6"> |
| <h2 class="text-2xl font-bold" id="auth-modal-title">Connexion</h2> |
| <button onclick="toggleAuthModal()" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| |
| <div id="auth-tabs" class="flex border-b border-gray-200 dark:border-gray-700 mb-6"> |
| <button onclick="switchAuthTab('login')" class="px-4 py-2 font-medium border-b-2 border-transparent hover:text-blue-600 dark:hover:text-blue-400" id="login-tab">Connexion</button> |
| <button onclick="switchAuthTab('register')" class="px-4 py-2 font-medium border-b-2 border-transparent hover:text-blue-600 dark:hover:text-blue-400" id="register-tab">Inscription</button> |
| </div> |
| |
| <form id="login-form" class="space-y-4"> |
| <div> |
| <label for="login-email" class="block text-sm font-medium mb-1">Email</label> |
| <input type="email" id="login-email" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| </div> |
| <div> |
| <label for="login-password" class="block text-sm font-medium mb-1">Mot de passe</label> |
| <input type="password" id="login-password" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| </div> |
| <button type="button" onclick="login()" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition duration-200"> |
| Se connecter |
| </button> |
| </form> |
| |
| <form id="register-form" class="space-y-4 hidden"> |
| <div> |
| <label for="register-email" class="block text-sm font-medium mb-1">Email</label> |
| <input type="email" id="register-email" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| </div> |
| <div> |
| <label for="register-password" class="block text-sm font-medium mb-1">Mot de passe</label> |
| <input type="password" id="register-password" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| </div> |
| <div> |
| <label for="register-confirm-password" class="block text-sm font-medium mb-1">Confirmer le mot de passe</label> |
| <input type="password" id="register-confirm-password" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| </div> |
| <button type="button" onclick="register()" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition duration-200"> |
| S'inscrire |
| </button> |
| </form> |
| </div> |
| </div> |
|
|
| |
| <div id="notification" class="fixed top-4 right-4 z-50 px-4 py-3 rounded-md shadow-lg hidden slide-up" style="max-width: 350px;"> |
| <div class="flex items-center"> |
| <div id="notification-icon" class="mr-3"></div> |
| <div> |
| <h4 id="notification-title" class="font-medium"></h4> |
| <p id="notification-message" class="text-sm"></p> |
| </div> |
| <button onclick="hideNotification()" class="ml-auto text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="overdue-banner" class="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-100 px-4 py-3 mb-4 rounded-md hidden"> |
| <div class="flex items-center"> |
| <i class="fas fa-exclamation-circle mr-2"></i> |
| <span>Vous avez <span id="overdue-count">0</span> tâche(s) en retard</span> |
| <button onclick="hideOverdueBanner()" class="ml-auto text-red-500 hover:text-red-700 dark:hover:text-red-300"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="task-drawer" class="drawer drawer-closed fixed inset-y-0 right-0 z-40 w-full max-w-md bg-white dark:bg-gray-800 shadow-xl overflow-y-auto"> |
| <div class="p-6"> |
| <div class="flex justify-between items-center mb-6"> |
| <h3 class="text-xl font-bold" id="task-drawer-title">Détails de la tâche</h3> |
| <button onclick="closeTaskDrawer()" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| |
| <div class="space-y-6"> |
| <div> |
| <label class="block text-sm font-medium mb-1">Titre</label> |
| <input type="text" id="task-title" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium mb-1">Description</label> |
| <textarea id="task-description" rows="3" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"></textarea> |
| </div> |
| |
| <div class="grid grid-cols-2 gap-4"> |
| <div> |
| <label class="block text-sm font-medium mb-1">Date d'échéance</label> |
| <input type="datetime-local" id="task-due" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium mb-1">Priorité</label> |
| <select id="task-priority" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| <option value="low">Basse</option> |
| <option value="normal">Normale</option> |
| <option value="high">Haute</option> |
| <option value="urgent">Urgente</option> |
| </select> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium mb-1">Catégorie</label> |
| <select id="task-category" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| <option value="">Aucune catégorie</option> |
| </select> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium mb-1">Statut</label> |
| <select id="task-status" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| <option value="todo">À faire</option> |
| <option value="in_progress">En cours</option> |
| <option value="done">Terminée</option> |
| <option value="archived">Archivée</option> |
| </select> |
| </div> |
| |
| <div> |
| <div class="flex justify-between items-center mb-2"> |
| <label class="block text-sm font-medium">Rappels</label> |
| <button onclick="addReminder()" class="text-sm text-blue-600 dark:text-blue-400 hover:underline">Ajouter un rappel</button> |
| </div> |
| <div id="task-reminders" class="space-y-2"> |
| |
| </div> |
| </div> |
| </div> |
| |
| <div class="mt-8 flex justify-between"> |
| <button onclick="deleteTask()" class="px-4 py-2 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900 rounded-md transition duration-200"> |
| Supprimer |
| </button> |
| <div class="space-x-2"> |
| <button onclick="saveTask()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition duration-200"> |
| Enregistrer |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="flex flex-col h-full"> |
| |
| <header class="bg-white dark:bg-gray-800 shadow-sm"> |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> |
| <div class="flex justify-between items-center h-16"> |
| <div class="flex items-center"> |
| <i class="fas fa-tasks text-blue-600 dark:text-blue-400 mr-2 text-xl"></i> |
| <h1 class="text-xl font-bold">TaskMaster</h1> |
| </div> |
| <div class="flex items-center space-x-4"> |
| <button onclick="toggleDarkMode()" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"> |
| <i id="dark-mode-icon" class="fas fa-moon"></i> |
| </button> |
| <div class="relative"> |
| <button onclick="toggleUserMenu()" class="flex items-center space-x-2 focus:outline-none"> |
| <div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white"> |
| <span id="user-initial">U</span> |
| </div> |
| </button> |
| <div id="user-menu" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 z-50"> |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700">Profil</a> |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700">Paramètres</a> |
| <a href="#" onclick="logout()" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700">Déconnexion</a> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </header> |
|
|
| |
| <main class="flex-1 overflow-hidden"> |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full"> |
| <div class="flex h-full"> |
| |
| <aside class="hidden md:block w-64 border-r border-gray-200 dark:border-gray-700 pr-4 py-6 overflow-y-auto"> |
| <div class="space-y-1"> |
| <button onclick="switchView('dashboard')" class="w-full flex items-center px-3 py-2 text-sm font-medium rounded-md bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-200"> |
| <i class="fas fa-tachometer-alt mr-3"></i> |
| Tableau de bord |
| </button> |
| <button onclick="switchView('all-tasks')" class="w-full flex items-center px-3 py-2 text-sm font-medium rounded-md hover:bg-gray-200 dark:hover:bg-gray-700"> |
| <i class="fas fa-list-ul mr-3"></i> |
| Toutes les tâches |
| </button> |
| <button onclick="switchView('categories')" class="w-full flex items-center px-3 py-2 text-sm font-medium rounded-md hover:bg-gray-200 dark:hover:bg-gray-700"> |
| <i class="fas fa-tags mr-3"></i> |
| Catégories |
| </button> |
| <button onclick="switchView('priorities')" class="w-full flex items-center px-3 py-2 text-sm font-medium rounded-md hover:bg-gray-200 dark:hover:bg-gray-700"> |
| <i class="fas fa-flag mr-3"></i> |
| Priorités |
| </button> |
| <button onclick="switchView('calendar')" class="w-full flex items-center px-3 py-2 text-sm font-medium rounded-md hover:bg-gray-200 dark:hover:bg-gray-700"> |
| <i class="fas fa-calendar-alt mr-3"></i> |
| Calendrier |
| </button> |
| </div> |
|
|
| <div class="mt-8"> |
| <h3 class="px-3 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider"> |
| Catégories |
| </h3> |
| <div class="mt-1 space-y-1" id="category-list"> |
| |
| </div> |
| </div> |
| </aside> |
|
|
| |
| <div class="flex-1 flex flex-col overflow-hidden"> |
| |
| <div class="md:hidden flex justify-around items-center border-b border-gray-200 dark:border-gray-700 py-2"> |
| <button onclick="switchView('dashboard')" class="flex flex-col items-center px-2 py-1 text-xs rounded-md"> |
| <i class="fas fa-tachometer-alt mb-1"></i> |
| <span>Dashboard</span> |
| </button> |
| <button onclick="switchView('all-tasks')" class="flex flex-col items-center px-2 py-1 text-xs rounded-md"> |
| <i class="fas fa-list-ul mb-1"></i> |
| <span>Toutes</span> |
| </button> |
| <button onclick="switchView('categories')" class="flex flex-col items-center px-2 py-1 text-xs rounded-md"> |
| <i class="fas fa-tags mb-1"></i> |
| <span>Catégories</span> |
| </button> |
| <button onclick="switchView('priorities')" class="flex flex-col items-center px-2 py-1 text-xs rounded-md"> |
| <i class="fas fa-flag mb-1"></i> |
| <span>Priorités</span> |
| </button> |
| </div> |
|
|
| |
| <div class="flex-1 overflow-y-auto p-4"> |
| |
| <div id="dashboard-view"> |
| <div class="mb-6"> |
| <div class="relative"> |
| <input type="text" id="quick-add-input" placeholder="Que devez-vous faire ? #catégorie !priorité" class="w-full px-4 py-3 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| <button onclick="quickAddTask()" class="absolute right-3 top-1/2 transform -translate-y-1/2 p-2 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"> |
| <i class="fas fa-plus"></i> |
| </button> |
| </div> |
| </div> |
|
|
| <div id="overdue-banner-container"></div> |
|
|
| <div class="space-y-6"> |
| <div> |
| <h2 class="text-lg font-semibold mb-3 flex items-center"> |
| <span class="w-2 h-2 bg-red-500 rounded-full mr-2"></span> |
| En retard |
| </h2> |
| <div id="overdue-tasks" class="space-y-2"> |
| |
| </div> |
| </div> |
|
|
| <div> |
| <h2 class="text-lg font-semibold mb-3 flex items-center"> |
| <span class="w-2 h-2 bg-orange-500 rounded-full mr-2"></span> |
| Aujourd'hui |
| </h2> |
| <div id="today-tasks" class="space-y-2"> |
| |
| </div> |
| </div> |
|
|
| <div> |
| <h2 class="text-lg font-semibold mb-3 flex items-center"> |
| <span class="w-2 h-2 bg-yellow-500 rounded-full mr-2"></span> |
| Demain |
| </h2> |
| <div id="tomorrow-tasks" class="space-y-2"> |
| |
| </div> |
| </div> |
|
|
| <div> |
| <h2 class="text-lg font-semibold mb-3 flex items-center"> |
| <span class="w-2 h-2 bg-blue-500 rounded-full mr-2"></span> |
| À venir |
| </h2> |
| <div id="upcoming-tasks" class="space-y-2"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="all-tasks-view" class="hidden"> |
| <div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between space-y-3 md:space-y-0"> |
| <div class="relative w-full md:w-64"> |
| <input type="text" id="task-search" placeholder="Rechercher des tâches..." class="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> |
| </div> |
| <div class="flex space-x-2"> |
| <select id="task-filter-status" class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| <option value="">Tous les statuts</option> |
| <option value="todo">À faire</option> |
| <option value="in_progress">En cours</option> |
| <option value="done">Terminée</option> |
| <option value="archived">Archivée</option> |
| </select> |
| <select id="task-filter-priority" class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| <option value="">Toutes priorités</option> |
| <option value="urgent">Urgente</option> |
| <option value="high">Haute</option> |
| <option value="normal">Normale</option> |
| <option value="low">Basse</option> |
| </select> |
| </div> |
| </div> |
|
|
| <div class="mb-4 flex justify-between items-center"> |
| <h2 class="text-lg font-semibold">Toutes les tâches</h2> |
| <select id="task-sort" class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700"> |
| <option value="due_asc">Échéance (proche)</option> |
| <option value="due_desc">Échéance (lointaine)</option> |
| <option value="priority">Priorité</option> |
| <option value="created">Date de création</option> |
| <option value="updated">Dernière modification</option> |
| </select> |
| </div> |
|
|
| <div id="all-tasks-list" class="space-y-2"> |
| |
| </div> |
| </div> |
|
|
| |
| <div id="categories-view" class="hidden"> |
| <div class="mb-6 flex justify-between items-center"> |
| <h2 class="text-lg font-semibold">Catégories</h2> |
| <button onclick="showAddCategoryModal()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition duration-200"> |
| <i class="fas fa-plus mr-1"></i> Nouvelle catégorie |
| </button> |
| </div> |
|
|
| <div id="categories-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> |
| |
| </div> |
| </div> |
|
|
| |
| <div id="priorities-view" class="hidden"> |
| <div class="mb-6"> |
| <h2 class="text-lg font-semibold">Vue par priorités</h2> |
| </div> |
|
|
| <div class="grid grid-cols-1 md:grid-cols-4 gap-4"> |
| <div class="priority-column bg-red-50 dark:bg-red-900/20 p-4 rounded-lg"> |
| <div class="flex items-center mb-4"> |
| <h3 class="font-medium text-red-600 dark:text-red-400">Urgent</h3> |
| <span class="ml-auto px-2 py-1 text-xs bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200 rounded-full">3</span> |
| </div> |
| <div id="urgent-tasks" class="space-y-2"> |
| |
| </div> |
| </div> |
|
|
| <div class="priority-column bg-orange-50 dark:bg-orange-900/20 p-4 rounded-lg"> |
| <div class="flex items-center mb-4"> |
| <h3 class="font-medium text-orange-600 dark:text-orange-400">Haute</h3> |
| <span class="ml-auto px-2 py-1 text-xs bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-200 rounded-full">2</span> |
| </div> |
| <div id="high-tasks" class="space-y-2"> |
| |
| </div> |
| </div> |
|
|
| <div class="priority-column bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg"> |
| <div class="flex items-center mb-4"> |
| <h3 class="font-medium text-blue-600 dark:text-blue-400">Normale</h3> |
| <span class="ml-auto px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 rounded-full">4</span> |
| </div> |
| <div id="normal-tasks" class="space-y-2"> |
| |
| </div> |
| </div> |
|
|
| <div class="priority-column bg-gray-100 dark:bg-gray-800 p-4 rounded-lg"> |
| <div class="flex items-center mb-4"> |
| <h3 class="font-medium text-gray-600 dark:text-gray-300">Basse</h3> |
| <span class="ml-auto px-2 py-1 text-xs bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-full">1</span> |
| </div> |
| <div id="low-tasks" class="space-y-2"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="calendar-view" class="hidden"> |
| <div class="mb-6 flex justify-between items-center"> |
| <h2 class="text-lg font-semibold">Calendrier</h2> |
| <div class="flex items-center space-x-2"> |
| <button class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"> |
| <i class="fas fa-chevron-left"></i> |
| </button> |
| <span class="font-medium">Mai 2023</span> |
| <button class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"> |
| <i class="fas fa-chevron-right"></i> |
| </button> |
| </div> |
| </div> |
|
|
| <div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden"> |
| <div class="grid grid-cols-7 gap-px bg-gray-200 dark:bg-gray-700"> |
| <div class="bg-gray-100 dark:bg-gray-800 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400">Lun</div> |
| <div class="bg-gray-100 dark:bg-gray-800 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400">Mar</div> |
| <div class="bg-gray-100 dark:bg-gray-800 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400">Mer</div> |
| <div class="bg-gray-100 dark:bg-gray-800 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400">Jeu</div> |
| <div class="bg-gray-100 dark:bg-gray-800 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400">Ven</div> |
| <div class="bg-gray-100 dark:bg-gray-800 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400">Sam</div> |
| <div class="bg-gray-100 dark:bg-gray-800 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400">Dim</div> |
| </div> |
| <div class="grid grid-cols-7 gap-px bg-gray-200 dark:bg-gray-700"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| <script> |
| // State management |
| let currentUser = null; |
| let tasks = []; |
| let categories = []; |
| let reminders = []; |
| let currentView = 'dashboard'; |
| let currentTaskId = null; |
| |
| // Demo data |
| const demoCategories = [ |
| { id: 1, name: 'Travail', color: 'work', icon: 'briefcase' }, |
| { id: 2, name: 'Personnel', color: 'personal', icon: 'home' }, |
| { id: 3, name: 'Immobilier', color: 'property', icon: 'building' }, |
| { id: 4, name: 'Administration', color: 'admin', icon: 'file-alt' } |
| ]; |
| |
| const demoTasks = [ |
| { |
| id: 1, |
| title: 'Payer l\'assurance', |
| description: 'Payer l\'assurance voiture avant la date limite', |
| status: 'todo', |
| priority: 'high', |
| category_id: 4, |
| due_at: new Date(Date.now() + 86400000).toISOString(), // Tomorrow |
| created_at: new Date().toISOString(), |
| updated_at: new Date().toISOString() |
| }, |
| { |
| id: 2, |
| title: 'Préparer présentation client', |
| description: 'Préparer les slides pour la réunion de mercredi', |
| status: 'in_progress', |
| priority: 'urgent', |
| category_id: 1, |
| due_at: new Date(Date.now() + 172800000).toISOString(), // 2 days |
| created_at: new Date().toISOString(), |
| updated_at: new Date().toISOString() |
| }, |
| { |
| id: 3, |
| title: 'Faire les courses', |
| description: 'Acheter fruits, légumes et produits ménagers', |
| status: 'todo', |
| priority: 'normal', |
| category_id: 2, |
| due_at: new Date(Date.now() + 259200000).toISOString(), // 3 days |
| created_at: new Date().toISOString(), |
| updated_at: new Date().toISOString() |
| }, |
| { |
| id: 4, |
| title: 'Réparer robinet cuisine', |
| description: 'Le robinet fuit depuis plusieurs jours', |
| status: 'todo', |
| priority: 'low', |
| category_id: 2, |
| due_at: null, |
| created_at: new Date().toISOString(), |
| updated_at: new Date().toISOString() |
| }, |
| { |
| id: 5, |
| title: 'Rappel taxe foncière', |
| description: 'Payer la taxe foncière avant le 15 octobre', |
| status: 'todo', |
| priority: 'high', |
| category_id: 3, |
| due_at: new Date('2023-10-15').toISOString(), |
| created_at: new Date().toISOString(), |
| updated_at: new Date().toISOString() |
| }, |
| { |
| id: 6, |
| title: 'Réunion équipe', |
| description: 'Préparer le point sur les projets en cours', |
| status: 'done', |
| priority: 'normal', |
| category_id: 1, |
| due_at: new Date(Date.now() - 86400000).toISOString(), // Yesterday |
| completed_at: new Date().toISOString(), |
| created_at: new Date().toISOString(), |
| updated_at: new Date().toISOString() |
| }, |
| { |
| id: 7, |
| title: 'Rendez-vous médecin', |
| description: 'Contrôle annuel à 14h30', |
| status: 'todo', |
| priority: 'urgent', |
| category_id: 2, |
| due_at: new Date(Date.now() + 3600000).toISOString(), // In 1 hour |
| created_at: new Date().toISOString(), |
| updated_at: new Date().toISOString() |
| } |
| ]; |
| |
| const demoReminders = [ |
| { id: 1, task_id: 1, remind_at: new Date(Date.now() + 82800000).toISOString(), channel: 'in_app' }, // 1h before |
| { id: 2, task_id: 1, remind_at: new Date(Date.now() + 86040000).toISOString(), channel: 'in_app' }, // 10min before |
| { id: 3, task_id: 7, remind_at: new Date(Date.now() + 1800000).toISOString(), channel: 'in_app' } // 30min before |
| ]; |
| |
| // Initialize app |
| document.addEventListener('DOMContentLoaded', function() { |
| // Check if user is logged in |
| const storedUser = localStorage.getItem('currentUser'); |
| if (storedUser) { |
| currentUser = JSON.parse(storedUser); |
| initApp(); |
| } else { |
| toggleAuthModal(); |
| } |
| |
| // Load demo data if no user data exists |
| if (!localStorage.getItem('tasks')) { |
| categories = demoCategories; |
| tasks = demoTasks; |
| reminders = demoReminders; |
| saveData(); |
| } else { |
| loadData(); |
| } |
| |
| // Check for overdue tasks |
| checkOverdueTasks(); |
| }); |
| |
| // Data management |
| function saveData() { |
| if (currentUser) { |
| localStorage.setItem(`tasks_${currentUser.id}`, JSON.stringify(tasks)); |
| localStorage.setItem(`categories_${currentUser.id}`, JSON.stringify(categories)); |
| localStorage.setItem(`reminders_${currentUser.id}`, JSON.stringify(reminders)); |
| } |
| } |
| |
| function loadData() { |
| if (currentUser) { |
| const storedTasks = localStorage.getItem(`tasks_${currentUser.id}`); |
| const storedCategories = localStorage.getItem(`categories_${currentUser.id}`); |
| const storedReminders = localStorage.getItem(`reminders_${currentUser.id}`); |
| |
| if (storedTasks) tasks = JSON.parse(storedTasks); |
| if (storedCategories) categories = JSON.parse(storedCategories); |
| if (storedReminders) reminders = JSON.parse(storedReminders); |
| } |
| } |
| |
| // Auth functions |
| function toggleAuthModal() { |
| const modal = document.getElementById('auth-modal'); |
| modal.classList.toggle('hidden'); |
| } |
| |
| function switchAuthTab(tab) { |
| const loginForm = document.getElementById('login-form'); |
| const registerForm = document.getElementById('register-form'); |
| const loginTab = document.getElementById('login-tab'); |
| const registerTab = document.getElementById('register-tab'); |
| const modalTitle = document.getElementById('auth-modal-title'); |
| |
| if (tab === 'login') { |
| loginForm.classList.remove('hidden'); |
| registerForm.classList.add('hidden'); |
| loginTab.classList.add('border-blue-500', 'text-blue-600', 'dark:text-blue-400'); |
| loginTab.classList.remove('border-transparent'); |
| registerTab.classList.add('border-transparent'); |
| registerTab.classList.remove('border-blue-500', 'text-blue-600', 'dark:text-blue-400'); |
| modalTitle.textContent = 'Connexion'; |
| } else { |
| loginForm.classList.add('hidden'); |
| registerForm.classList.remove('hidden'); |
| registerTab.classList.add('border-blue-500', 'text-blue-600', 'dark:text-blue-400'); |
| registerTab.classList.remove('border-transparent'); |
| loginTab.classList.add('border-transparent'); |
| loginTab.classList.remove('border-blue-500', 'text-blue-600', 'dark:text-blue-400'); |
| modalTitle.textContent = 'Inscription'; |
| } |
| } |
| |
| function login() { |
| const email = document.getElementById('login-email').value; |
| const password = document.getElementById('login-password').value; |
| |
| // Simple validation |
| if (!email || !password) { |
| showNotification('error', 'Erreur', 'Veuillez remplir tous les champs'); |
| return; |
| } |
| |
| // In a real app, this would be an API call |
| currentUser = { |
| id: 1, |
| email: email, |
| name: email.split('@')[0] |
| }; |
| localStorage.setItem('currentUser', JSON.stringify(currentUser)); |
| |
| toggleAuthModal(); |
| initApp(); |
| showNotification('success', 'Connecté', 'Vous êtes maintenant connecté'); |
| } |
| |
| function register() { |
| const email = document.getElementById('register-email').value; |
| const password = document.getElementById('register-password').value; |
| const confirmPassword = document.getElementById('register-confirm-password').value; |
| |
| // Simple validation |
| if (!email || !password || !confirmPassword) { |
| showNotification('error', 'Erreur', 'Veuillez remplir tous les champs'); |
| return; |
| } |
| |
| if (password !== confirmPassword) { |
| showNotification('error', 'Erreur', 'Les mots de passe ne correspondent pas'); |
| return; |
| } |
| |
| // In a real app, this would be an API call |
| currentUser = { |
| id: 1, |
| email: email, |
| name: email.split('@')[0] |
| }; |
| localStorage.setItem('currentUser', JSON.stringify(currentUser)); |
| |
| toggleAuthModal(); |
| initApp(); |
| showNotification('success', 'Inscription réussie', 'Votre compte a été créé'); |
| } |
| |
| function logout() { |
| currentUser = null; |
| localStorage.removeItem('currentUser'); |
| toggleAuthModal(); |
| document.getElementById('user-menu').classList.add('hidden'); |
| } |
| |
| function initApp() { |
| loadData(); |
| renderUserInfo(); |
| renderCategories(); |
| renderTasks(); |
| setupEventListeners(); |
| } |
| |
| function renderUserInfo() { |
| if (currentUser) { |
| document.getElementById('user-initial').textContent = currentUser.name.charAt(0).toUpperCase(); |
| } |
| } |
| |
| // UI functions |
| function toggleUserMenu() { |
| const menu = document.getElementById('user-menu'); |
| menu.classList.toggle('hidden'); |
| } |
| |
| function toggleDarkMode() { |
| document.documentElement.classList.toggle('dark'); |
| const icon = document.getElementById('dark-mode-icon'); |
| if (document.documentElement.classList.contains('dark')) { |
| icon.classList.remove('fa-moon'); |
| icon.classList.add('fa-sun'); |
| } else { |
| icon.classList.remove('fa-sun'); |
| icon.classList.add('fa-moon'); |
| } |
| } |
| |
| function showNotification(type, title, message) { |
| const notification = document.getElementById('notification'); |
| const icon = document.getElementById('notification-icon'); |
| const notificationTitle = document.getElementById('notification-title'); |
| const notificationMessage = document.getElementById('notification-message'); |
| |
| // Set notification content |
| notificationTitle.textContent = title; |
| notificationMessage.textContent = message; |
| |
| // Set notification style based on type |
| notification.className = `fixed top-4 right-4 z-50 px-4 py-3 rounded-md shadow-lg slide-up ${type === 'error' ? 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-100' : 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-100'}`; |
| |
| // Set icon |
| icon.className = `fas ${type === 'error' ? 'fa-exclamation-circle text-red-500 dark:text-red-300' : 'fa-check-circle text-green-500 dark:text-green-300'}`; |
| |
| // Show notification |
| notification.classList.remove('hidden'); |
| |
| // Hide after 5 seconds |
| setTimeout(hideNotification, 5000); |
| } |
| |
| function hideNotification() { |
| const notification = document.getElementById('notification'); |
| notification.classList.add('hidden'); |
| } |
| |
| function showOverdueBanner(count) { |
| const banner = document.getElementById('overdue-banner'); |
| const countElement = document.getElementById('overdue-count'); |
| |
| countElement.textContent = count; |
| banner.classList.remove('hidden'); |
| |
| // Also add to dashboard view |
| const dashboardBanner = document.getElementById('overdue-banner-container'); |
| dashboardBanner.innerHTML = ` |
| <div class="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-100 px-4 py-3 mb-4 rounded-md"> |
| <div class="flex items-center"> |
| <i class="fas fa-exclamation-circle mr-2"></i> |
| <span>Vous avez ${count} tâche(s) en retard</span> |
| <button onclick="hideOverdueBanner()" class="ml-auto text-red-500 hover:text-red-700 dark:hover:text-red-300"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| </div> |
| `; |
| } |
| |
| function hideOverdueBanner() { |
| const banner = document.getElementById('overdue-banner'); |
| banner.classList.add('hidden'); |
| document.getElementById('overdue-banner-container').innerHTML = ''; |
| } |
| |
| function checkOverdueTasks() { |
| const now = new Date(); |
| const overdueTasks = tasks.filter(task => |
| task.due_at && |
| new Date(task.due_at) < now && |
| task.status !== 'done' && |
| task.status !== 'archived' |
| ); |
| |
| if (overdueTasks.length > 0) { |
| showOverdueBanner(overdueTasks.length); |
| } |
| } |
| |
| // View switching |
| function switchView(view) { |
| currentView = view; |
| |
| // Hide all views |
| document.getElementById('dashboard-view').classList.add('hidden'); |
| document.getElementById('all-tasks-view').classList.add('hidden'); |
| document.getElementById('categories-view').classList.add('hidden'); |
| document.getElementById('priorities-view').classList.add('hidden'); |
| document.getElementById('calendar-view').classList.add('hidden'); |
| |
| // Show selected view |
| document.getElementById(`${view}-view`).classList.remove('hidden'); |
| |
| // Update sidebar active state |
| updateSidebarActiveState(); |
| |
| // Render specific content if needed |
| if (view === 'priorities') { |
| renderPriorityColumns(); |
| } else if (view === 'calendar') { |
| renderCalendar(); |
| } |
| } |
| |
| function updateSidebarActiveState() { |
| // Reset all buttons |
| const sidebarButtons = document.querySelectorAll('aside button'); |
| sidebarButtons.forEach(button => { |
| button.classList.remove('bg-blue-100', 'dark:bg-blue-900', 'text-blue-700', 'dark:text-blue-200'); |
| button.classList.add('hover:bg-gray-200', 'dark:hover:bg-gray-700'); |
| }); |
| |
| // Set active button |
| const activeButton = document.querySelector(`aside button[onclick="switchView('${currentView}')"]`); |
| if (activeButton) { |
| activeButton.classList.add('bg-blue-100', 'dark:bg-blue-900', 'text-blue-700', 'dark:text-blue-200'); |
| activeButton.classList.remove('hover:bg-gray-200', 'dark:hover:bg-gray-700'); |
| } |
| } |
| |
| // Task functions |
| function renderTasks() { |
| renderDashboardTasks(); |
| renderAllTasks(); |
| renderPriorityColumns(); |
| } |
| |
| function renderDashboardTasks() { |
| const now = new Date(); |
| const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); |
| const tomorrow = new Date(today); |
| tomorrow.setDate(tomorrow.getDate() + 1); |
| |
| // Overdue tasks (due before today and not done) |
| const overdueTasks = tasks.filter(task => |
| task.due_at && |
| new Date(task.due_at) < today && |
| task.status !== 'done' && |
| task.status !== 'archived' |
| ); |
| |
| // Today's tasks (due today) |
| const todayTasks = tasks.filter(task => |
| task.due_at && |
| new Date(task.due_at) >= today && |
| new Date(task.due_at) < tomorrow && |
| task.status !== 'done' && |
| task.status !== 'archived' |
| ); |
| |
| // Tomorrow's tasks (due tomorrow) |
| const tomorrowTasks = tasks.filter(task => |
| task.due_at && |
| new Date(task.due_at) >= tomorrow && |
| new Date(task.due_at) < new Date(tomorrow.getTime() + 86400000) && |
| task.status !== 'done' && |
| task.status !== 'archived' |
| ); |
| |
| // Upcoming tasks (due after tomorrow) |
| const upcomingTasks = tasks.filter(task => |
| task.due_at && |
| new Date(task.due_at) >= new Date(tomorrow.getTime() + 86400000) && |
| task.status !== 'done' && |
| task.status !== 'archived' |
| ); |
| |
| // Render each section |
| renderTaskList('overdue-tasks', overdueTasks); |
| renderTaskList('today-tasks', todayTasks); |
| renderTaskList('tomorrow-tasks', tomorrowTasks); |
| renderTaskList('upcoming-tasks', upcomingTasks); |
| } |
| |
| function renderAllTasks() { |
| const filteredTasks = filterAndSortTasks(); |
| renderTaskList('all-tasks-list', filteredTasks); |
| } |
| |
| function filterAndSortTasks() { |
| const statusFilter = document.getElementById('task-filter-status').value; |
| const priorityFilter = document.getElementById('task-filter-priority').value; |
| const searchQuery = document.getElementById('task-search').value.toLowerCase(); |
| const sortOption = document.getElementById('task-sort').value; |
| |
| let filteredTasks = [...tasks]; |
| |
| // Apply status filter |
| if (statusFilter) { |
| filteredTasks = filteredTasks.filter(task => task.status === statusFilter); |
| } |
| |
| // Apply priority filter |
| if (priorityFilter) { |
| filteredTasks = filteredTasks.filter(task => task.priority === priorityFilter); |
| } |
| |
| // Apply search filter |
| if (searchQuery) { |
| filteredTasks = filteredTasks.filter(task => |
| task.title.toLowerCase().includes(searchQuery) || |
| (task.description && task.description.toLowerCase().includes(searchQuery)) |
| ); |
| } |
| |
| // Apply sorting |
| switch (sortOption) { |
| case 'due_asc': |
| filteredTasks.sort((a, b) => { |
| if (!a.due_at && !b.due_at) return 0; |
| if (!a.due_at) return 1; |
| if (!b.due_at) return -1; |
| return new Date(a.due_at) - new Date(b.due_at); |
| }); |
| break; |
| case 'due_desc': |
| filteredTasks.sort((a, b) => { |
| if (!a.due_at && !b.due_at) return 0; |
| if (!a.due_at) return 1; |
| if (!b.due_at) return -1; |
| return new Date(b.due_at) - new Date(a.due_at); |
| }); |
| break; |
| case 'priority': |
| const priorityOrder = { 'urgent': 0, 'high': 1, 'normal': 2, 'low': 3 }; |
| filteredTasks.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]); |
| break; |
| case 'created': |
| filteredTasks.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); |
| break; |
| case 'updated': |
| filteredTasks.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); |
| break; |
| } |
| |
| return filteredTasks; |
| } |
| |
| function renderPriorityColumns() { |
| const urgentTasks = tasks.filter(task => |
| task.priority === 'urgent' && |
| task.status !== 'done' && |
| task.status !== 'archived' |
| ); |
| |
| const highTasks = tasks.filter(task => |
| task.priority === 'high' && |
| task.status !== 'done' && |
| task.status !== 'archived' |
| ); |
| |
| const normalTasks = tasks.filter(task => |
| task.priority === 'normal' && |
| task.status !== 'done' && |
| task.status !== 'archived' |
| ); |
| |
| const lowTasks = tasks.filter(task => |
| task.priority === 'low' && |
| task.status !== 'done' && |
| task.status !== 'archived' |
| ); |
| |
| // Update counts |
| document.querySelector('#priorities-view .priority-column:nth-child(1) span').textContent = urgentTasks.length; |
| document.querySelector('#priorities-view .priority-column:nth-child(2) span').textContent = highTasks.length; |
| document.querySelector('#priorities-view .priority-column:nth-child(3) span').textContent = normalTasks.length; |
| document.querySelector('#priorities-view .priority-column:nth-child(4) span').textContent = lowTasks.length; |
| |
| // Render tasks |
| renderTaskList('urgent-tasks', urgentTasks); |
| renderTaskList('high-tasks', highTasks); |
| renderTaskList('normal-tasks', normalTasks); |
| renderTaskList('low-tasks', lowTasks); |
| } |
| |
| function renderTaskList(containerId, taskList) { |
| const container = document.getElementById(containerId); |
| container.innerHTML = ''; |
| |
| if (taskList.length === 0) { |
| container.innerHTML = '<p class="text-gray-500 dark:text-gray-400 text-center py-4">Aucune tâche à afficher</p>'; |
| return; |
| } |
| |
| taskList.forEach(task => { |
| const taskElement = createTaskElement(task); |
| container.appendChild(taskElement); |
| }); |
| } |
| |
| function createTaskElement(task) { |
| const category = categories.find(c => c.id === task.category_id); |
| const taskReminders = reminders.filter(r => r.task_id === task.id); |
| |
| const taskElement = document.createElement('div'); |
| taskElement.className = 'task-card bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 border border-gray-200 dark:border-gray-700 hover:shadow-md transition duration-200'; |
| taskElement.dataset.taskId = task.id; |
| |
| // Priority indicator |
| let priorityClass = ''; |
| let priorityText = ''; |
| switch (task.priority) { |
| case 'urgent': |
| priorityClass = 'bg-urgent text-white'; |
| priorityText = 'Urgent'; |
| break; |
| case 'high': |
| priorityClass = 'bg-high text-white'; |
| priorityText = 'Haute'; |
| break; |
| case 'normal': |
| priorityClass = 'bg-normal text-white'; |
| priorityText = 'Normale'; |
| break; |
| case 'low': |
| priorityClass = 'bg-low text-white'; |
| priorityText = 'Basse'; |
| break; |
| } |
| |
| // Status checkbox |
| const isChecked = task.status === 'done'; |
| |
| // Due date display |
| let dueDateDisplay = ''; |
| if (task.due_at) { |
| const dueDate = new Date(task.due_at); |
| const now = new Date(); |
| const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); |
| const tomorrow = new Date(today); |
| tomorrow.setDate(tomorrow.getDate() + 1); |
| |
| if (dueDate < today && task.status !== 'done') { |
| dueDateDisplay = `<span class="text-red-500 dark:text-red-400 text-sm"><i class="fas fa-exclamation-circle mr-1"></i> En retard</span>`; |
| } else if (dueDate >= today && dueDate < tomorrow) { |
| dueDateDisplay = `<span class="text-orange-500 dark:text-orange-400 text-sm"><i class="far fa-clock mr-1"></i> Aujourd'hui à ${dueDate.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>`; |
| } else if (dueDate >= tomorrow && dueDate < new Date(tomorrow.getTime() + 86400000)) { |
| dueDateDisplay = `<span class="text-blue-500 dark:text-blue-400 text-sm"><i class="far fa-clock mr-1"></i> Demain à ${dueDate.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>`; |
| } else { |
| dueDateDisplay = `<span class="text-gray-500 dark:text-gray-400 text-sm"><i class="far fa-calendar-alt mr-1"></i> ${dueDate.toLocaleDateString()}</span>`; |
| } |
| } |
| |
| taskElement.innerHTML = ` |
| <div class="flex items-start"> |
| <label class="checkbox-container"> |
| <input type="checkbox" ${isChecked ? 'checked' : ''} onchange="toggleTaskStatus(${task.id})"> |
| <span class="checkmark"></span> |
| </label> |
| <div class="flex-1 ml-3"> |
| <div class="flex items-center justify-between"> |
| <h3 class="font-medium ${isChecked ? 'line-through text-gray-400 dark:text-gray-500' : ''}">${task.title}</h3> |
| <div class="task-actions opacity-0 md:opacity-100 transition-opacity duration-200"> |
| <button onclick="event.stopPropagation();openTaskDrawer(${task.id})" class="p-1 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"> |
| <i class="fas fa-pencil-alt"></i> |
| </button> |
| <button onclick="event.stopPropagation();duplicateTask(${task.id})" class="p-1 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"> |
| <i class="fas fa-copy"></i> |
| </button> |
| <button onclick="event.stopPropagation();deleteTask(${task.id})" class="p-1 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"> |
| <i class="fas fa-trash-alt"></i> |
| </button> |
| </div> |
| </div> |
| ${task.description ? `<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">${task.description}</p>` : ''} |
| <div class="mt-2 flex flex-wrap items-center gap-2"> |
| ${task.priority !== 'normal' ? `<span class="px-2 py-1 text-xs rounded-full ${priorityClass}">${priorityText}</span>` : ''} |
| ${category ? `<span class="px-2 py-1 text-xs rounded-full bg-${category.color}-100 dark:bg-${category.color}-900/30 text-${category.color}-800 dark:text-${category.color}-200"><i class="fas fa-${category.icon} mr-1"></i> ${category.name}</span>` : ''} |
| ${task.due_at ? dueDateDisplay : ''} |
| ${taskReminders.length > 0 ? `<span class="text-yellow-500 dark:text-yellow-400 text-sm"><i class="fas fa-bell mr-1"></i> ${taskReminders.length} rappel(s)</span>` : ''} |
| </div> |
| </div> |
| </div> |
| `; |
| |
| // Add click handler to open task drawer |
| taskElement.addEventListener('click', () => openTaskDrawer(task.id)); |
| |
| return taskElement; |
| } |
| |
| function toggleTaskStatus(taskId) { |
| const taskIndex = tasks.findIndex(t => t.id === taskId); |
| if (taskIndex === -1) return; |
| |
| const task = tasks[taskIndex]; |
| const isChecked = document.querySelector(`.task-card[data-task-id="${taskId}"] input[type="checkbox"]`).checked; |
| |
| if (isChecked) { |
| task.status = 'done'; |
| task.completed_at = new Date().toISOString(); |
| |
| // Remove future reminders |
| reminders = reminders.filter(r => r.task_id !== taskId || new Date(r.remind_at) < new Date()); |
| } else { |
| task.status = 'todo'; |
| task.completed_at = null; |
| } |
| |
| task.updated_at = new Date().toISOString(); |
| tasks[taskIndex] = task; |
| saveData(); |
| renderTasks(); |
| |
| showNotification('success', 'Tâche mise à jour', `La tâche a été marquée comme ${isChecked ? 'terminée' : 'à faire'}`); |
| } |
| |
| function quickAddTask() { |
| const input = document.getElementById('quick-add-input'); |
| const text = input.value.trim(); |
| if (!text) return; |
| |
| // Parse quick add syntax (#category !priority) |
| let categoryName = null; |
| let priority = 'normal'; |
| |
| const categoryMatch = text.match(/#(\w+)/); |
| if (categoryMatch) { |
| categoryName = categoryMatch[1]; |
| } |
| |
| const priorityMatch = text.match(/!(\w+)/); |
| if (priorityMatch) { |
| const priorityText = priorityMatch[1].toLowerCase(); |
| if (['urgent', 'high', 'normal', 'low'].includes(priorityText)) { |
| priority = priorityText; |
| } |
| } |
| |
| // Clean title (remove tags) |
| const title = text.replace(/#\w+/g, '').replace(/!\w+/g, '').trim(); |
| |
| // Find category |
| let categoryId = null; |
| if (categoryName) { |
| const category = categories.find(c => c.name.toLowerCase() === categoryName.toLowerCase()); |
| if (category) { |
| categoryId = category.id; |
| } |
| } |
| |
| // Create task |
| const newTask = { |
| id: Date.now(), |
| title: title, |
| description: '', |
| status: 'todo', |
| priority: priority, |
| category_id: categoryId, |
| due_at: null, |
| created_at: new Date().toISOString(), |
| updated_at: new Date().toISOString() |
| }; |
| |
| tasks.push(newTask); |
| saveData(); |
| renderTasks(); |
| |
| input.value = ''; |
| showNotification('success', 'Tâche ajoutée', 'La nouvelle tâche a été créée'); |
| } |
| |
| function openTaskDrawer(taskId) { |
| currentTaskId = taskId; |
| const task = tasks.find(t => t.id === taskId); |
| if (!task) return; |
| |
| // Fill form |
| document.getElementById('task-title').value = task.title; |
| document.getElementById('task-description').value = task.description || ''; |
| document.getElementById('task-due').value = task.due_at ? formatDateTimeForInput(task.due_at) : ''; |
| document.getElementById('task-priority').value = task.priority; |
| document.getElementById('task-status').value = task.status; |
| |
| // Fill category select |
| const categorySelect = document.getElementById('task-category'); |
| categorySelect.innerHTML = '<option value="">Aucune catégorie</option>'; |
| categories.forEach(category => { |
| const option = document.createElement('option'); |
| option.value = category.id; |
| option.textContent = category.name; |
| option.selected = task.category_id === category.id; |
| categorySelect.appendChild(option); |
| }); |
| |
| // Fill reminders |
| const remindersContainer = document.getElementById('task-reminders'); |
| remindersContainer.innerHTML = ''; |
| |
| const taskReminders = reminders.filter(r => r.task_id === taskId); |
| taskReminders.forEach(reminder => { |
| const reminderElement = document.createElement('div'); |
| reminderElement.className = 'flex items-center justify-between p-2 bg-gray-100 dark:bg-gray-700 rounded'; |
| reminderElement.innerHTML = ` |
| <span>${formatDateTime(new Date(reminder.remind_at))}</span> |
| <button onclick="removeReminder(${reminder.id})" class="text-red-500 hover:text-red-700 dark:hover:text-red-300"> |
| <i class="fas fa-trash-alt"></i> |
| </button> |
| `; |
| remindersContainer.appendChild(reminderElement); |
| }); |
| |
| // Open drawer |
| document.getElementById('task-drawer').classList.remove('drawer-closed'); |
| document.getElementById('task-drawer').classList.add('drawer-open'); |
| } |
| |
| function closeTaskDrawer() { |
| document.getElementById('task-drawer').classList.remove('drawer-open'); |
| document.getElementById('task-drawer').classList.add('drawer-closed'); |
| currentTaskId = null; |
| } |
| |
| function saveTask() { |
| if (!currentTaskId) return; |
| |
| const taskIndex = tasks.findIndex(t => t.id === currentTaskId); |
| if (taskIndex === -1) return; |
| |
| const task = tasks[taskIndex]; |
| |
| // Update task |
| task.title = document.getElementById('task-title').value; |
| task.description = document.getElementById('task-description').value; |
| task.due_at = document.getElementById('task-due').value ? new Date(document.getElementById('task-due').value).toISOString() : null; |
| task.priority = document.getElementById('task-priority').value; |
| task.status = document.getElementById('task-status').value; |
| task.category_id = document.getElementById('task-category').value ? parseInt(document.getElementById('task-category').value) : null; |
| task.updated_at = new Date().toISOString(); |
| |
| tasks[taskIndex] = task; |
| saveData(); |
| renderTasks(); |
| closeTaskDrawer(); |
| |
| showNotification('success', 'Tâche sauvegardée', 'Les modifications ont été enregistrées'); |
| } |
| |
| function deleteTask(taskId) { |
| if (!taskId) taskId = currentTaskId; |
| if (!taskId) return; |
| |
| if (confirm('Êtes-vous sûr de vouloir supprimer cette tâche ?')) { |
| tasks = tasks.filter(t => t.id !== taskId); |
| reminders = reminders.filter(r => r.task_id !== taskId); |
| saveData(); |
| renderTasks(); |
| |
| if (taskId === currentTaskId) { |
| closeTaskDrawer(); |
| } |
| |
| showNotification('success', 'Tâche supprimée', 'La tâche a été supprimée'); |
| } |
| } |
| |
| function duplicateTask(taskId) { |
| const task = tasks.find(t => t.id === taskId); |
| if (!task) return; |
| |
| const newTask = { |
| ...task, |
| id: Date.now(), |
| status: 'todo', |
| completed_at: null, |
| created_at: new Date().toISOString(), |
| updated_at: new Date().toISOString() |
| }; |
| |
| tasks.push(newTask); |
| saveData(); |
| renderTasks(); |
| |
| showNotification('success', 'Tâche dupliquée', 'La tâche a été dupliquée'); |
| } |
| |
| function addReminder() { |
| if (!currentTaskId) return; |
| |
| // Check if we already have 5 reminders for this task |
| const taskReminders = reminders.filter(r => r.task_id === currentTaskId); |
| if (taskReminders.length >= 5) { |
| showNotification('error', 'Erreur', 'Maximum 5 rappels par tâche'); |
| return; |
| } |
| |
| // In a real app, we would show a modal to select date/time |
| // For demo, we'll just add a reminder for 1 hour before due date |
| const task = tasks.find(t => t.id === currentTaskId); |
| if (!task.due_at) { |
| showNotification('error', 'Erreur', 'Ajoutez d\'abord une date d\'échéance'); |
| return; |
| } |
| |
| const dueDate = new Date(task.due_at); |
| const reminderDate = new Date(dueDate.getTime() - 3600000); // 1 hour before |
| |
| const newReminder = { |
| id: Date.now(), |
| task_id: currentTaskId, |
| remind_at: reminderDate.toISOString(), |
| channel: 'in_app', |
| created_at: new Date().toISOString() |
| }; |
| |
| reminders.push(newReminder); |
| saveData(); |
| |
| // Refresh task drawer |
| openTaskDrawer(currentTaskId); |
| |
| showNotification('success', 'Rappel ajouté', 'Un nouveau rappel a été programmé'); |
| } |
| |
| function removeReminder(reminderId) { |
| reminders = reminders.filter(r => r.id !== reminderId); |
| saveData(); |
| |
| // Refresh task drawer |
| openTaskDrawer(currentTaskId); |
| } |
| |
| // Category functions |
| function renderCategories() { |
| // Sidebar categories |
| const sidebarCategoryList = document.getElementById('category-list'); |
| sidebarCategoryList.innerHTML = ''; |
| |
| // Categories view |
| const categoriesViewList = document.getElementById('categories-list'); |
| categoriesViewList.innerHTML = ''; |
| |
| categories.forEach(category => { |
| // Count tasks in this category |
| const taskCount = tasks.filter(t => t.category_id === category.id && t.status !== 'done' && t.status !== 'archived').length; |
| |
| // Sidebar item |
| const sidebarItem = document.createElement('button'); |
| sidebarItem.className = 'w-full flex items-center px-3 py-2 text-sm font-medium rounded-md hover:bg-gray-200 dark:hover:bg-gray-700'; |
| sidebarItem.innerHTML = ` |
| <span class="w-3 h-3 rounded-full bg-${category.color}-500 mr-3"></span> |
| <span class="truncate">${category.name}</span> |
| <span class="ml-auto px-2 py-1 text-xs bg-gray-200 dark:bg-gray-700 rounded-full">${taskCount}</span> |
| `; |
| sidebarItem.onclick = () => filterByCategory(category.id); |
| sidebarCategoryList.appendChild(sidebarItem); |
| |
| // Categories view card |
| const categoryCard = document.createElement('div'); |
| categoryCard.className = 'bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 border border-gray-200 dark:border-gray-700'; |
| categoryCard.innerHTML = ` |
| <div class="flex items-center mb-3"> |
| <span class="w-4 h-4 rounded-full bg-${category.color}-500 mr-3"></span> |
| <h3 class="font-medium flex-1">${category.name}</h3> |
| <div class="flex space-x-2"> |
| <button onclick="editCategory(${category.id})" class="p-1 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"> |
| <i class="fas fa-pencil-alt"></i> |
| </button> |
| <button onclick="deleteCategory(${category.id})" class="p-1 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"> |
| <i class="fas fa-trash-alt"></i> |
| </button> |
| </div> |
| </div> |
| <div class="flex justify-between items-center"> |
| <span class="text-sm text-gray-500 dark:text-gray-400"><i class="fas fa-${category.icon} mr-1"></i> ${taskCount} tâche(s)</span> |
| <button onclick="filterByCategory(${category.id})" class="text-sm text-blue-600 dark:text-blue-400 hover:underline"> |
| Voir les tâches |
| </button> |
| </div> |
| `; |
| categoriesViewList.appendChild(categoryCard); |
| }); |
| } |
| |
| function filterByCategory(categoryId) { |
| // Switch to all tasks view |
| switchView('all-tasks'); |
| |
| // Set category filter |
| document.getElementById('task-filter-status').value = ''; |
| document.getElementById('task-filter-priority').value = ''; |
| document.getElementById('task-search').value = ''; |
| document.getElementById('task-sort').value = 'due_asc'; |
| |
| // Trigger filter |
| renderAllTasks(); |
| } |
| |
| function showAddCategoryModal() { |
| // In a real app, we would show a modal to add a new category |
| // For demo, we'll just add a random category |
| const colors = ['work', 'personal', 'property', 'admin']; |
| const icons = ['briefcase', 'home', 'building', 'file-alt', 'shopping-cart', 'utensils', 'car', 'heart']; |
| |
| const newCategory = { |
| id: Date.now(), |
| name: `Nouvelle catégorie ${categories.length + 1}`, |
| color: colors[Math.floor(Math.random() * colors.length)], |
| icon: icons[Math.floor(Math.random() * icons.length)], |
| created_at: new Date().toISOString() |
| }; |
| |
| categories.push(newCategory); |
| saveData(); |
| renderCategories(); |
| |
| showNotification('success', 'Catégorie ajoutée', 'Une nouvelle catégorie a été créée'); |
| } |
| |
| function editCategory(categoryId) { |
| // In a real app, we would show a modal to edit the category |
| const category = categories.find(c => c.id === categoryId); |
| if (!category) return; |
| |
| // For demo, we'll just add a " (modifié)" to the name |
| category.name = `${category.name} (modifié)`; |
| saveData(); |
| renderCategories(); |
| |
| showNotification('success', 'Catégorie modifiée', 'La cat |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=stephaneibens/task-manager" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |