| <!DOCTYPE html> |
| <html lang="fr"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Workflow de Validation Multi-Niveaux</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"> |
| <style> |
| .workflow-step { |
| position: relative; |
| } |
| .workflow-step:not(:last-child):after { |
| content: ''; |
| position: absolute; |
| top: 50%; |
| right: -20px; |
| width: 40px; |
| height: 2px; |
| background-color: #d1d5db; |
| } |
| .workflow-connector { |
| height: 30px; |
| width: 2px; |
| background-color: #d1d5db; |
| margin: 0 auto; |
| } |
| .animate-pulse { |
| animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; |
| } |
| @keyframes pulse { |
| 0%, 100% { |
| opacity: 1; |
| } |
| 50% { |
| opacity: 0.5; |
| } |
| } |
| .custom-scrollbar::-webkit-scrollbar { |
| width: 8px; |
| height: 8px; |
| } |
| .custom-scrollbar::-webkit-scrollbar-track { |
| background: #f1f1f1; |
| } |
| .custom-scrollbar::-webkit-scrollbar-thumb { |
| background: #888; |
| border-radius: 4px; |
| } |
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { |
| background: #555; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 font-sans"> |
| <div class="flex h-screen overflow-hidden"> |
| |
| <div class="hidden md:flex md:flex-shrink-0"> |
| <div class="flex flex-col w-64 bg-indigo-700 text-white"> |
| <div class="flex items-center justify-center h-16 px-4 bg-indigo-800"> |
| <div class="flex items-center"> |
| <i class="fas fa-tasks text-xl mr-2"></i> |
| <span class="text-xl font-semibold">WorkflowPro</span> |
| </div> |
| </div> |
| <div class="flex flex-col flex-grow px-4 py-4 overflow-y-auto"> |
| <div class="space-y-1"> |
| <a href="#" class="flex items-center px-2 py-3 text-sm font-medium rounded-md bg-indigo-800 text-white"> |
| <i class="fas fa-tachometer-alt mr-3"></i> |
| Tableau de bord |
| </a> |
| <a href="#" class="flex items-center px-2 py-3 text-sm font-medium rounded-md text-indigo-200 hover:text-white hover:bg-indigo-600"> |
| <i class="fas fa-inbox mr-3"></i> |
| Mes demandes |
| </a> |
| <a href="#" class="flex items-center px-2 py-3 text-sm font-medium rounded-md text-indigo-200 hover:text-white hover:bg-indigo-600"> |
| <i class="fas fa-check-circle mr-3"></i> |
| Validations |
| </a> |
| <a href="#" class="flex items-center px-2 py-3 text-sm font-medium rounded-md text-indigo-200 hover:text-white hover:bg-indigo-600"> |
| <i class="fas fa-chart-bar mr-3"></i> |
| Statistiques |
| </a> |
| <a href="#" class="flex items-center px-2 py-3 text-sm font-medium rounded-md text-indigo-200 hover:text-white hover:bg-indigo-600"> |
| <i class="fas fa-cog mr-3"></i> |
| Paramètres |
| </a> |
| </div> |
| <div class="mt-auto mb-4"> |
| <div class="p-4 bg-indigo-800 rounded-lg"> |
| <p class="text-sm text-indigo-200">Besoin d'aide?</p> |
| <a href="#" class="text-white font-medium hover:underline">Contactez le support</a> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="flex flex-col flex-1 overflow-hidden"> |
| |
| <div class="flex items-center justify-between h-16 px-4 bg-white border-b border-gray-200"> |
| <div class="flex items-center"> |
| <button class="md:hidden text-gray-500 focus:outline-none"> |
| <i class="fas fa-bars"></i> |
| </button> |
| <h1 class="ml-4 text-xl font-semibold text-gray-800">Tableau de bord</h1> |
| </div> |
| <div class="flex items-center space-x-4"> |
| <button class="p-1 text-gray-500 rounded-full hover:bg-gray-100 focus:outline-none"> |
| <i class="fas fa-bell"></i> |
| </button> |
| <div class="relative"> |
| <button id="user-menu" class="flex items-center text-sm text-gray-700 rounded-full focus:outline-none"> |
| <img class="w-8 h-8 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="User photo"> |
| <span class="ml-2 hidden md:inline">Jean Dupont</span> |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="flex-1 overflow-auto custom-scrollbar p-4 md:p-6"> |
| |
| <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6"> |
| <div class="bg-white rounded-lg shadow p-4"> |
| <div class="flex items-center"> |
| <div class="p-3 rounded-full bg-blue-100 text-blue-600"> |
| <i class="fas fa-file-alt"></i> |
| </div> |
| <div class="ml-4"> |
| <p class="text-sm font-medium text-gray-500">Demandes en cours</p> |
| <p class="text-xl font-semibold text-gray-800">24</p> |
| </div> |
| </div> |
| </div> |
| <div class="bg-white rounded-lg shadow p-4"> |
| <div class="flex items-center"> |
| <div class="p-3 rounded-full bg-green-100 text-green-600"> |
| <i class="fas fa-check-circle"></i> |
| </div> |
| <div class="ml-4"> |
| <p class="text-sm font-medium text-gray-500">Demandes approuvées</p> |
| <p class="text-xl font-semibold text-gray-800">42</p> |
| </div> |
| </div> |
| </div> |
| <div class="bg-white rounded-lg shadow p-4"> |
| <div class="flex items-center"> |
| <div class="p-3 rounded-full bg-red-100 text-red-600"> |
| <i class="fas fa-times-circle"></i> |
| </div> |
| <div class="ml-4"> |
| <p class="text-sm font-medium text-gray-500">Demandes rejetées</p> |
| <p class="text-xl font-semibold text-gray-800">8</p> |
| </div> |
| </div> |
| </div> |
| <div class="bg-white rounded-lg shadow p-4"> |
| <div class="flex items-center"> |
| <div class="p-3 rounded-full bg-yellow-100 text-yellow-600"> |
| <i class="fas fa-clock"></i> |
| </div> |
| <div class="ml-4"> |
| <p class="text-sm font-medium text-gray-500">En attente de moi</p> |
| <p class="text-xl font-semibold text-gray-800">5</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
| |
| <div class="bg-white rounded-lg shadow overflow-hidden"> |
| <div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200"> |
| <h3 class="text-lg font-medium text-gray-900">Créer une nouvelle demande</h3> |
| </div> |
| <div class="p-4"> |
| <form id="request-form"> |
| <div class="mb-4"> |
| <label for="request-type" class="block text-sm font-medium text-gray-700 mb-1">Type de demande</label> |
| <select id="request-type" class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> |
| <option value="">Sélectionnez un type</option> |
| <option value="conges">Demande de congés</option> |
| <option value="achat">Demande d'achat</option> |
| <option value="formation">Demande de formation</option> |
| <option value="remboursement">Demande de remboursement</option> |
| </select> |
| </div> |
| <div class="mb-4"> |
| <label for="request-title" class="block text-sm font-medium text-gray-700 mb-1">Titre</label> |
| <input type="text" id="request-title" class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> |
| </div> |
| <div class="mb-4"> |
| <label for="request-description" class="block text-sm font-medium text-gray-700 mb-1">Description</label> |
| <textarea id="request-description" rows="3" class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"></textarea> |
| </div> |
| <div class="mb-4"> |
| <label for="request-amount" class="block text-sm font-medium text-gray-700 mb-1">Montant (si applicable)</label> |
| <input type="number" id="request-amount" class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> |
| </div> |
| <div class="mb-4"> |
| <label for="request-date" class="block text-sm font-medium text-gray-700 mb-1">Date (si applicable)</label> |
| <input type="date" id="request-date" class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> |
| </div> |
| <div class="flex justify-end"> |
| <button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> |
| Soumettre la demande |
| </button> |
| </div> |
| </form> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-lg shadow overflow-hidden"> |
| <div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200"> |
| <h3 class="text-lg font-medium text-gray-900">Mes demandes récentes</h3> |
| </div> |
| <div class="overflow-y-auto" style="max-height: 500px;"> |
| <ul class="divide-y divide-gray-200" id="recent-requests"> |
| |
| </ul> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-lg shadow overflow-hidden"> |
| <div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200"> |
| <h3 class="text-lg font-medium text-gray-900">Workflow de validation</h3> |
| </div> |
| <div class="p-4"> |
| <div class="flex justify-between mb-6" id="workflow-steps"> |
| |
| </div> |
| <div class="workflow-connector"></div> |
| <div class="flex justify-between mt-6" id="approvers-list"> |
| |
| </div> |
| </div> |
| <div class="px-4 py-4 border-t border-gray-200"> |
| <h4 class="text-md font-medium text-gray-900 mb-2">Détails de la demande</h4> |
| <div class="bg-gray-50 p-3 rounded-lg"> |
| <p class="text-sm text-gray-600" id="workflow-details">Sélectionnez une demande pour voir son workflow de validation.</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const requests = [ |
| { |
| id: 1, |
| type: 'conges', |
| title: 'Demande de congés annuels', |
| description: 'Je souhaite prendre 2 semaines de congés en août pour des vacances familiales.', |
| date: '2023-08-15', |
| status: 'pending', |
| created: '2023-06-10', |
| workflow: [ |
| { level: 1, approver: 'Responsable d\'équipe', status: 'approved', date: '2023-06-12' }, |
| { level: 2, approver: 'DRH', status: 'pending', date: null }, |
| { level: 3, approver: 'Directeur Général', status: 'pending', date: null } |
| ] |
| }, |
| { |
| id: 2, |
| type: 'achat', |
| title: 'Achat nouveau matériel', |
| description: 'Besoin d\'un nouvel ordinateur portable pour le nouveau collaborateur.', |
| amount: 1200, |
| status: 'approved', |
| created: '2023-05-28', |
| workflow: [ |
| { level: 1, approver: 'Responsable d\'équipe', status: 'approved', date: '2023-05-29' }, |
| { level: 2, approver: 'Responsable financier', status: 'approved', date: '2023-05-30' } |
| ] |
| }, |
| { |
| id: 3, |
| type: 'formation', |
| title: 'Formation React Avancé', |
| description: 'Participation à une formation React avancé de 3 jours pour améliorer mes compétences.', |
| amount: 800, |
| status: 'rejected', |
| created: '2023-06-05', |
| workflow: [ |
| { level: 1, approver: 'Responsable d\'équipe', status: 'approved', date: '2023-06-06' }, |
| { level: 2, approver: 'Responsable formation', status: 'rejected', date: '2023-06-07', comment: 'Budget formation épuisé pour ce trimestre.' } |
| ] |
| }, |
| { |
| id: 4, |
| type: 'remboursement', |
| title: 'Remboursement frais de transport', |
| description: 'Remboursement des frais de transport pour le déplacement client du 20 mai.', |
| amount: 145.50, |
| status: 'pending', |
| created: '2023-06-08', |
| workflow: [ |
| { level: 1, approver: 'Responsable d\'équipe', status: 'pending', date: null }, |
| { level: 2, approver: 'Comptabilité', status: 'pending', date: null } |
| ] |
| } |
| ]; |
| |
| |
| const recentRequestsList = document.getElementById('recent-requests'); |
| const workflowSteps = document.getElementById('workflow-steps'); |
| const approversList = document.getElementById('approvers-list'); |
| const workflowDetails = document.getElementById('workflow-details'); |
| const requestForm = document.getElementById('request-form'); |
| |
| |
| function renderRecentRequests() { |
| recentRequestsList.innerHTML = ''; |
| |
| |
| const sortedRequests = [...requests].sort((a, b) => new Date(b.created) - new Date(a.created)); |
| |
| sortedRequests.forEach(request => { |
| const listItem = document.createElement('li'); |
| |
| |
| let statusColor, statusIcon; |
| if (request.status === 'approved') { |
| statusColor = 'bg-green-100 text-green-800'; |
| statusIcon = 'fa-check-circle'; |
| } else if (request.status === 'rejected') { |
| statusColor = 'bg-red-100 text-red-800'; |
| statusIcon = 'fa-times-circle'; |
| } else { |
| statusColor = 'bg-yellow-100 text-yellow-800'; |
| statusIcon = 'fa-clock'; |
| } |
| |
| |
| const createdDate = new Date(request.created); |
| const formattedDate = createdDate.toLocaleDateString('fr-FR'); |
| |
| listItem.className = 'px-4 py-4 hover:bg-gray-50 cursor-pointer'; |
| listItem.innerHTML = ` |
| <div class="flex items-center justify-between"> |
| <div class="flex items-center"> |
| <div class="min-w-0"> |
| <p class="text-sm font-medium text-indigo-600 truncate">${request.title}</p> |
| <p class="text-sm text-gray-500 truncate">${request.description.substring(0, 50)}...</p> |
| </div> |
| </div> |
| <div class="ml-2 flex-shrink-0 flex"> |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor}"> |
| <i class="fas ${statusIcon} mr-1"></i> |
| ${request.status === 'approved' ? 'Approuvé' : request.status === 'rejected' ? 'Rejeté' : 'En attente'} |
| </span> |
| </div> |
| </div> |
| <div class="mt-2 flex justify-between"> |
| <div class="flex"> |
| <p class="flex items-center text-sm text-gray-500"> |
| <i class="fas fa-calendar-alt mr-1.5"></i> |
| ${formattedDate} |
| </p> |
| </div> |
| <div class="flex"> |
| <button class="text-sm text-indigo-600 hover:text-indigo-900 view-workflow" data-id="${request.id}"> |
| Voir workflow |
| </button> |
| </div> |
| </div> |
| `; |
| |
| recentRequestsList.appendChild(listItem); |
| }); |
| |
| |
| document.querySelectorAll('.view-workflow').forEach(button => { |
| button.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| const requestId = parseInt(button.getAttribute('data-id')); |
| const request = requests.find(r => r.id === requestId); |
| renderWorkflow(request); |
| }); |
| }); |
| |
| |
| document.querySelectorAll('#recent-requests li').forEach((item, index) => { |
| item.addEventListener('click', () => { |
| renderWorkflow(sortedRequests[index]); |
| }); |
| }); |
| } |
| |
| |
| function renderWorkflow(request) { |
| |
| workflowSteps.innerHTML = ''; |
| approversList.innerHTML = ''; |
| |
| |
| request.workflow.forEach(step => { |
| const stepElement = document.createElement('div'); |
| stepElement.className = 'workflow-step'; |
| |
| |
| let stepStatusColor, stepStatusIcon; |
| if (step.status === 'approved') { |
| stepStatusColor = 'bg-green-500'; |
| stepStatusIcon = 'fa-check'; |
| } else if (step.status === 'rejected') { |
| stepStatusColor = 'bg-red-500'; |
| stepStatusIcon = 'fa-times'; |
| } else { |
| stepStatusColor = 'bg-gray-300 animate-pulse'; |
| stepStatusIcon = 'fa-clock'; |
| } |
| |
| stepElement.innerHTML = ` |
| <div class="flex flex-col items-center"> |
| <div class="w-10 h-10 rounded-full ${stepStatusColor} flex items-center justify-center text-white"> |
| <i class="fas ${stepStatusIcon}"></i> |
| </div> |
| <p class="mt-2 text-xs font-medium text-gray-700">Étape ${step.level}</p> |
| </div> |
| `; |
| |
| workflowSteps.appendChild(stepElement); |
| }); |
| |
| |
| request.workflow.forEach(step => { |
| const approverElement = document.createElement('div'); |
| approverElement.className = 'text-center'; |
| |
| approverElement.innerHTML = ` |
| <p class="text-sm font-medium text-gray-900">${step.approver}</p> |
| <p class="text-xs text-gray-500"> |
| ${step.date ? new Date(step.date).toLocaleDateString('fr-FR') : 'En attente'} |
| </p> |
| ${step.comment ? `<p class="text-xs text-red-500 mt-1">${step.comment}</p>` : ''} |
| `; |
| |
| approversList.appendChild(approverElement); |
| }); |
| |
| |
| let detailsHtml = ` |
| <p><strong>Type:</strong> ${getRequestTypeName(request.type)}</p> |
| <p><strong>Titre:</strong> ${request.title}</p> |
| <p><strong>Description:</strong> ${request.description}</p> |
| `; |
| |
| if (request.amount) { |
| detailsHtml += `<p><strong>Montant:</strong> ${request.amount} €</p>`; |
| } |
| |
| if (request.date) { |
| detailsHtml += `<p><strong>Date:</strong> ${new Date(request.date).toLocaleDateString('fr-FR')}</p>`; |
| } |
| |
| detailsHtml += `<p><strong>Statut:</strong> ${request.status === 'approved' ? 'Approuvé' : request.status === 'rejected' ? 'Rejeté' : 'En attente'}</p>`; |
| |
| workflowDetails.innerHTML = detailsHtml; |
| } |
| |
| |
| function getRequestTypeName(type) { |
| const types = { |
| 'conges': 'Demande de congés', |
| 'achat': 'Demande d\'achat', |
| 'formation': 'Demande de formation', |
| 'remboursement': 'Demande de remboursement' |
| }; |
| return types[type] || type; |
| } |
| |
| |
| requestForm.addEventListener('submit', (e) => { |
| e.preventDefault(); |
| |
| const type = document.getElementById('request-type').value; |
| const title = document.getElementById('request-title').value; |
| const description = document.getElementById('request-description').value; |
| const amount = document.getElementById('request-amount').value; |
| const date = document.getElementById('request-date').value; |
| |
| if (!type || !title || !description) { |
| alert('Veuillez remplir tous les champs obligatoires.'); |
| return; |
| } |
| |
| |
| const newRequest = { |
| id: requests.length + 1, |
| type, |
| title, |
| description, |
| status: 'pending', |
| created: new Date().toISOString().split('T')[0], |
| workflow: [ |
| { level: 1, approver: 'Responsable d\'équipe', status: 'pending', date: null }, |
| { level: 2, approver: type === 'conges' ? 'DRH' : 'Responsable financier', status: 'pending', date: null } |
| ] |
| }; |
| |
| if (amount) newRequest.amount = parseFloat(amount); |
| if (date) newRequest.date = date; |
| |
| requests.unshift(newRequest); |
| |
| |
| requestForm.reset(); |
| |
| |
| alert('Votre demande a été soumise avec succès!'); |
| |
| |
| renderRecentRequests(); |
| |
| |
| renderWorkflow(newRequest); |
| }); |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| renderRecentRequests(); |
| |
| |
| if (requests.length > 0) { |
| renderWorkflow(requests[0]); |
| } |
| }); |
| </script> |
| <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=NaderY/idph" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |