Spaces:
Running
Running
On the Participant List Tab, Make the list minimizable for example on the Participant List Titled "SFAT" then there will be a SFAT tab that when I Maximize or click it it will show all participant name. - Follow Up Deployment
7d7a39d verified | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Safety Officers Specialized Training Calculator and Tracker</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 = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| darkgreen: { | |
| 50: '#f0fdf4', | |
| 100: '#dcfce7', | |
| 200: '#bbf7d0', | |
| 300: '#86efac', | |
| 400: '#4ade80', | |
| 500: '#22c55e', | |
| 600: '#16a34a', | |
| 700: '#15803d', | |
| 800: '#166534', | |
| 900: '#14532d', | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| .photo-preview { | |
| max-height: 300px; | |
| object-fit: contain; | |
| background-color: #f0f0f0; | |
| } | |
| .training-item { | |
| transition: all 0.3s ease; | |
| } | |
| .training-item.collapsed { | |
| max-height: 60px; | |
| overflow: hidden; | |
| } | |
| .scrollable-list { | |
| max-height: 400px; | |
| overflow-y: auto; | |
| } | |
| /* Custom scrollbar */ | |
| .scrollable-list::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .scrollable-list::-webkit-scrollbar-track { | |
| background: #e5e7eb; | |
| } | |
| .scrollable-list::-webkit-scrollbar-thumb { | |
| background-color: #16a34a; | |
| border-radius: 4px; | |
| } | |
| .tab-content { | |
| transition: all 0.3s ease; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <!-- Header with Logo Upload --> | |
| <div class="flex flex-col md:flex-row justify-between items-center mb-10 bg-darkgreen-800 p-8 rounded-xl shadow-xl"> | |
| <div class="flex items-center mb-4 md:mb-0"> | |
| <div class="mr-6 relative"> | |
| <img id="companyLogo" src="https://via.placeholder.com/120x80?text=Company+Logo" alt="Company Logo" class="h-20 w-auto"> | |
| <label for="logoUpload" class="absolute -bottom-2 -right-2 bg-darkgreen-600 text-white p-1 rounded-full cursor-pointer hover:bg-darkgreen-700"> | |
| <i class="fas fa-camera text-xs"></i> | |
| </label> | |
| <input type="file" id="logoUpload" accept="image/*" class="hidden" onchange="handleLogoUpload(event)"> | |
| </div> | |
| <h1 class="text-4xl font-bold text-white">Safety Officers Specialized Training Calculator and Tracker</h1> | |
| </div> | |
| <div class="flex space-x-2"> | |
| <button onclick="exportData()" class="bg-darkgreen-600 hover:bg-darkgreen-700 text-white px-6 py-3 rounded-xl flex items-center text-lg"> | |
| <i class="fas fa-file-export mr-3 text-xl"></i> Export | |
| </button> | |
| <button onclick="document.getElementById('importModal').classList.remove('hidden')" class="bg-darkgreen-600 hover:bg-darkgreen-700 text-white px-6 py-3 rounded-xl flex items-center text-lg"> | |
| <i class="fas fa-file-import mr-3 text-xl"></i> Import | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Input Form --> | |
| <div class="bg-white rounded-xl shadow-lg p-8 mb-8"> | |
| <h2 class="text-3xl font-semibold text-darkgreen-800 mb-8 border-b-2 pb-3">Add New Training Program</h2> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Training Title*</label> | |
| <input type="text" id="trainingTitle" class="w-full px-4 py-3 text-lg border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-darkgreen-500" placeholder="Enter training title"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Status*</label> | |
| <select id="trainingStatus" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500"> | |
| <option value="ongoing">Ongoing</option> | |
| <option value="pending">Pending</option> | |
| <option value="closed">Closed</option> | |
| <option value="open">Open</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Number of Days*</label> | |
| <input type="number" id="daysCount" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" placeholder="Enter number of days" min="1"> | |
| </div> | |
| <div> | |
| <div class="flex justify-between items-center mb-1"> | |
| <label class="block text-sm font-medium text-gray-700">Participants*</label> | |
| <button type="button" onclick="addParticipant()" class="text-xs bg-darkgreen-600 text-white px-2 py-1 rounded hover:bg-darkgreen-700"> | |
| <i class="fas fa-plus mr-1"></i> Add Participant | |
| </button> | |
| </div> | |
| <div id="participantsContainer"> | |
| <input type="text" id="participant1" class="w-full px-3 py-2 border border-gray-300 rounded-md mb-2 focus:outline-none focus:ring-2 focus:ring-darkgreen-500" placeholder="Participant name"> | |
| </div> | |
| <div class="mt-2"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Additional Unnamed Participants</label> | |
| <input type="number" id="unnamedParticipants" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" placeholder="Enter count" min="0" value="0"> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Venue</label> | |
| <input type="text" id="trainingVenue" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" placeholder="Enter venue"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Trainer/Facilitator</label> | |
| <input type="text" id="trainingTrainer" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" placeholder="Enter trainer name"> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Description/Details*</label> | |
| <textarea id="trainingDescription" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" placeholder="Enter training description"></textarea> | |
| </div> | |
| <!-- Cost Section --> | |
| <div class="mt-6 border-t pt-4"> | |
| <h3 class="text-lg font-medium text-darkgreen-800 mb-3">Cost Information</h3> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Cost per Participant (₱)*</label> | |
| <input type="number" id="costPerParticipant" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" placeholder="Enter cost per participant" min="0" step="0.01" onchange="calculateTotalCost()"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Total Cost (₱)</label> | |
| <input type="text" id="totalCost" class="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-100" readonly> | |
| </div> | |
| </div> | |
| <!-- Additional Costs --> | |
| <div class="mt-4"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <label class="block text-sm font-medium text-gray-700">Additional Costs</label> | |
| <button type="button" onclick="addAdditionalCost()" class="text-xs bg-darkgreen-600 text-white px-2 py-1 rounded hover:bg-darkgreen-700"> | |
| <i class="fas fa-plus mr-1"></i> Add Cost | |
| </button> | |
| </div> | |
| <div id="additionalCostsContainer"> | |
| <!-- Additional costs will be added here --> | |
| </div> | |
| <div class="mt-2"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Total Additional Costs (₱)</label> | |
| <input type="text" id="totalAdditionalCosts" class="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-100" readonly value="0.00"> | |
| </div> | |
| <div class="mt-2"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Grand Total (₱)</label> | |
| <input type="text" id="grandTotal" class="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-100 font-bold" readonly value="0.00"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Training Photo --> | |
| <div class="mt-6 border-t pt-4"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Upload Training Photo (Landscape)</label> | |
| <div class="flex items-center"> | |
| <label for="trainingPhoto" class="cursor-pointer bg-darkgreen-600 text-white px-4 py-2 rounded-md hover:bg-darkgreen-700"> | |
| <i class="fas fa-upload mr-2"></i> Choose Photo | |
| </label> | |
| <input type="file" id="trainingPhoto" accept="image/*" class="hidden" onchange="handleTrainingPhotoUpload(event)"> | |
| <span id="photoFileName" class="ml-3 text-sm text-gray-500">No photo selected</span> | |
| </div> | |
| <div id="photoPreviewContainer" class="mt-4 hidden"> | |
| <img id="photoPreview" class="photo-preview w-full rounded-lg shadow-md" src="#" alt="Training photo preview"> | |
| </div> | |
| </div> | |
| <!-- Submit Button --> | |
| <div class="mt-10 flex justify-end"> | |
| <button type="button" onclick="addTrainingProgram()" class="bg-darkgreen-600 hover:bg-darkgreen-700 text-white px-8 py-4 rounded-xl font-bold text-xl"> | |
| <i class="fas fa-save mr-3 text-xl"></i> Save Training Program | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Tabs for Training Programs and Participants --> | |
| <div class="bg-white rounded-xl shadow-lg p-4 w-full"> | |
| <div class="flex border-b mb-4"> | |
| <button id="programsTabBtn" class="px-4 py-2 font-medium text-darkgreen-800 border-b-2 border-darkgreen-600">Training Programs</button> | |
| <button id="participantsTabBtn" class="px-4 py-2 font-medium text-gray-500">Participants List</button> | |
| </div> | |
| <!-- Training Programs Tab --> | |
| <div id="programsTab" class="tab-content"> | |
| <div class="flex justify-between items-center mb-8 border-b-2 pb-3"> | |
| <h2 class="text-3xl font-semibold text-darkgreen-800">Training Programs</h2> | |
| <div class="flex items-center"> | |
| <select id="filterStatus" class="px-3 py-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-darkgreen-500" onchange="filterTrainings()"> | |
| <option value="all">All Status</option> | |
| <option value="ongoing">Ongoing</option> | |
| <option value="pending">Pending</option> | |
| <option value="closed">Closed</option> | |
| <option value="open">Open</option> | |
| </select> | |
| <input type="text" id="searchInput" placeholder="Search..." class="ml-2 px-3 py-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-darkgreen-500" onkeyup="searchTrainings()"> | |
| </div> | |
| </div> | |
| <div class="overflow-x-auto"> | |
| <div id="trainingList" class="min-w-full"> | |
| <!-- Training programs will be listed here --> | |
| <div class="text-center py-10 text-gray-500"> | |
| <i class="fas fa-clipboard-list text-4xl mb-2"></i> | |
| <p>No training programs added yet</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Participants Tab --> | |
| <div id="participantsTab" class="tab-content hidden"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-semibold text-darkgreen-800">All Participants</h3> | |
| <button onclick="toggleParticipantsList()" class="text-sm bg-darkgreen-600 text-white px-3 py-1 rounded hover:bg-darkgreen-700"> | |
| <i class="fas fa-minus mr-1"></i> Minimize | |
| </button> | |
| </div> | |
| <div id="participantsListContent" class="overflow-y-auto max-h-96"> | |
| <div id="participantsList"> | |
| <!-- Participants will be grouped by program here --> | |
| <div class="text-center py-10 text-gray-500"> | |
| <i class="fas fa-users text-4xl mb-2"></i> | |
| <p>No participants added yet</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Import Modal --> | |
| <div id="importModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> | |
| <div class="bg-white rounded-lg p-6 w-full max-w-md"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-semibold text-darkgreen-800">Import Data</h3> | |
| <button onclick="document.getElementById('importModal').classList.add('hidden')" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Select JSON file to import</label> | |
| <input type="file" id="importFile" accept=".json" class="w-full px-3 py-2 border border-gray-300 rounded-md"> | |
| </div> | |
| <div class="flex justify-end space-x-3"> | |
| <button onclick="document.getElementById('importModal').classList.add('hidden')" class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-100">Cancel</button> | |
| <button onclick="importData()" class="bg-darkgreen-600 hover:bg-darkgreen-700 text-white px-4 py-2 rounded-md">Import</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Edit Modal --> | |
| <div id="editModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> | |
| <div class="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-semibold text-darkgreen-800">Edit Training Program</h3> | |
| <button onclick="closeEditModal()" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div id="editFormContent"> | |
| <!-- Edit form content will be loaded here --> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Tab functionality | |
| document.getElementById('programsTabBtn').addEventListener('click', function() { | |
| document.getElementById('programsTab').classList.remove('hidden'); | |
| document.getElementById('participantsTab').classList.add('hidden'); | |
| this.classList.add('text-darkgreen-800', 'border-b-2', 'border-darkgreen-600'); | |
| this.classList.remove('text-gray-500'); | |
| document.getElementById('participantsTabBtn').classList.add('text-gray-500'); | |
| document.getElementById('participantsTabBtn').classList.remove('text-darkgreen-800', 'border-b-2', 'border-darkgreen-600'); | |
| }); | |
| document.getElementById('participantsTabBtn').addEventListener('click', function() { | |
| document.getElementById('programsTab').classList.add('hidden'); | |
| document.getElementById('participantsTab').classList.remove('hidden'); | |
| this.classList.add('text-darkgreen-800', 'border-b-2', 'border-darkgreen-600'); | |
| this.classList.remove('text-gray-500'); | |
| document.getElementById('programsTabBtn').classList.add('text-gray-500'); | |
| document.getElementById('programsTabBtn').classList.remove('text-darkgreen-800', 'border-b-2', 'border-darkgreen-600'); | |
| renderParticipantsList(); | |
| }); | |
| function toggleParticipantsList() { | |
| const content = document.getElementById('participantsListContent'); | |
| const button = document.querySelector('#participantsTab button'); | |
| if (content.classList.contains('max-h-96')) { | |
| content.classList.remove('max-h-96'); | |
| button.innerHTML = '<i class="fas fa-plus mr-1"></i> Expand'; | |
| } else { | |
| content.classList.add('max-h-96'); | |
| button.innerHTML = '<i class="fas fa-minus mr-1"></i> Minimize'; | |
| } | |
| } | |
| // Initialize data storage | |
| let trainingPrograms = []; | |
| let companyLogo = null; | |
| // Load data from localStorage when page loads | |
| document.addEventListener('DOMContentLoaded', function() { | |
| loadData(); | |
| renderTrainingList(); | |
| }); | |
| // Save data to localStorage before page unload | |
| window.addEventListener('beforeunload', function() { | |
| saveData(); | |
| }); | |
| // Logo upload handler | |
| function handleLogoUpload(event) { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| document.getElementById('companyLogo').src = e.target.result; | |
| companyLogo = e.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| } | |
| // Training photo upload handler | |
| function handleTrainingPhotoUpload(event) { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| document.getElementById('photoFileName').textContent = file.name; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const preview = document.getElementById('photoPreview'); | |
| preview.src = e.target.result; | |
| document.getElementById('photoPreviewContainer').classList.remove('hidden'); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| } | |
| // Calculate total cost | |
| function calculateTotalCost() { | |
| const participants = parseInt(document.getElementById('participantsCount').value) || 0; | |
| const costPerParticipant = parseFloat(document.getElementById('costPerParticipant').value) || 0; | |
| const totalCost = participants * costPerParticipant; | |
| document.getElementById('totalCost').value = formatCurrency(totalCost); | |
| updateGrandTotal(); | |
| } | |
| // Add additional cost field | |
| function addAdditionalCost() { | |
| const container = document.getElementById('additionalCostsContainer'); | |
| const costId = Date.now(); | |
| const costDiv = document.createElement('div'); | |
| costDiv.className = 'flex items-center mb-2'; | |
| costDiv.innerHTML = ` | |
| <input type="text" placeholder="Description" class="flex-1 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500"> | |
| <input type="number" placeholder="Amount (₱)" min="0" step="0.01" class="w-32 px-3 py-2 border-t border-b border-gray-300 focus:outline-none focus:ring-2 focus:ring-darkgreen-500" onchange="updateAdditionalCostsTotal()"> | |
| <button type="button" onclick="removeAdditionalCost(this)" class="px-3 py-2 bg-red-500 text-white rounded-r-md hover:bg-red-600"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| `; | |
| container.appendChild(costDiv); | |
| } | |
| // Remove additional cost field | |
| function removeAdditionalCost(button) { | |
| const costDiv = button.parentElement; | |
| costDiv.remove(); | |
| updateAdditionalCostsTotal(); | |
| } | |
| // Update additional costs total | |
| function updateAdditionalCostsTotal() { | |
| const inputs = document.querySelectorAll('#additionalCostsContainer input[type="number"]'); | |
| let total = 0; | |
| inputs.forEach(input => { | |
| total += parseFloat(input.value) || 0; | |
| }); | |
| document.getElementById('totalAdditionalCosts').value = formatCurrency(total); | |
| updateGrandTotal(); | |
| } | |
| // Update grand total | |
| function updateGrandTotal() { | |
| const totalCost = parseFloat(document.getElementById('totalCost').value.replace(/[^0-9.-]+/g,"")) || 0; | |
| const additionalCosts = parseFloat(document.getElementById('totalAdditionalCosts').value.replace(/[^0-9.-]+/g,"")) || 0; | |
| const grandTotal = totalCost + additionalCosts; | |
| document.getElementById('grandTotal').value = formatCurrency(grandTotal); | |
| } | |
| // Format currency (Philippine Peso) | |
| function formatCurrency(amount) { | |
| return '₱' + amount.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'); | |
| } | |
| // Add participant field | |
| function addParticipant() { | |
| const container = document.getElementById('participantsContainer'); | |
| const count = container.querySelectorAll('input').length + 1; | |
| const participantDiv = document.createElement('div'); | |
| participantDiv.className = 'flex items-center mb-2'; | |
| participantDiv.innerHTML = ` | |
| <input type="text" id="participant${count}" class="flex-1 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" placeholder="Participant name"> | |
| <button type="button" onclick="removeParticipant(this)" class="px-3 py-2 bg-red-500 text-white rounded-r-md hover:bg-red-600"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| `; | |
| container.appendChild(participantDiv); | |
| } | |
| // Remove participant field | |
| function removeParticipant(button) { | |
| const participantDiv = button.parentElement; | |
| participantDiv.remove(); | |
| } | |
| // Add new training program | |
| function addTrainingProgram() { | |
| // Validate required fields | |
| const title = document.getElementById('trainingTitle').value.trim(); | |
| const description = document.getElementById('trainingDescription').value.trim(); | |
| const days = document.getElementById('daysCount').value; | |
| // Get participants | |
| const participantInputs = document.querySelectorAll('#participantsContainer input[type="text"]'); | |
| const participants = []; | |
| participantInputs.forEach(input => { | |
| if (input.value.trim()) { | |
| participants.push(input.value.trim()); | |
| } | |
| }); | |
| const unnamedCount = parseInt(document.getElementById('unnamedParticipants').value) || 0; | |
| if (participants.length === 0 && unnamedCount === 0) { | |
| alert('Please add at least one participant or specify participant count'); | |
| return; | |
| } | |
| const costPerParticipant = document.getElementById('costPerParticipant').value; | |
| if (!title || !description || !days || !participants || !costPerParticipant) { | |
| alert('Please fill in all required fields (marked with *)'); | |
| return; | |
| } | |
| // Get additional costs | |
| const additionalCosts = []; | |
| const costInputs = document.querySelectorAll('#additionalCostsContainer > div'); | |
| costInputs.forEach(costDiv => { | |
| const inputs = costDiv.querySelectorAll('input'); | |
| const description = inputs[0].value.trim(); | |
| const amount = parseFloat(inputs[1].value) || 0; | |
| if (description) { | |
| additionalCosts.push({ | |
| description: description, | |
| amount: amount | |
| }); | |
| } | |
| }); | |
| // Get photo if uploaded | |
| let photo = null; | |
| const photoInput = document.getElementById('trainingPhoto'); | |
| if (photoInput.files.length > 0) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| photo = e.target.result; | |
| saveTrainingProgram(photo); | |
| }; | |
| reader.readAsDataURL(photoInput.files[0]); | |
| } else { | |
| saveTrainingProgram(photo); | |
| } | |
| function saveTrainingProgram(photo) { | |
| const newTraining = { | |
| id: Date.now(), | |
| title: title, | |
| status: document.getElementById('trainingStatus').value, | |
| days: parseInt(days), | |
| participants: participants.length + unnamedCount, | |
| namedParticipants: participants, | |
| unnamedParticipants: unnamedCount, | |
| venue: document.getElementById('trainingVenue').value.trim(), | |
| trainer: document.getElementById('trainingTrainer').value.trim(), | |
| description: description, | |
| costPerParticipant: parseFloat(costPerParticipant), | |
| additionalCosts: additionalCosts, | |
| photo: photo, | |
| createdAt: new Date().toISOString() | |
| }; | |
| trainingPrograms.push(newTraining); | |
| saveData(); | |
| renderTrainingList(); | |
| resetForm(); | |
| } | |
| } | |
| // Reset form after submission | |
| function resetForm() { | |
| document.getElementById('trainingTitle').value = ''; | |
| document.getElementById('trainingStatus').value = 'ongoing'; | |
| document.getElementById('startDate').value = new Date().toISOString().split('T')[0]; | |
| document.getElementById('endDate').value = new Date().toISOString().split('T')[0]; | |
| document.getElementById('daysCount').value = ''; | |
| document.getElementById('participantsContainer').innerHTML = ` | |
| <input type="text" id="participant1" class="w-full px-3 py-2 border border-gray-300 rounded-md mb-2 focus:outline-none focus:ring-2 focus:ring-darkgreen-500" placeholder="Participant name"> | |
| `; | |
| document.getElementById('unnamedParticipants').value = '0'; | |
| document.getElementById('trainingVenue').value = ''; | |
| document.getElementById('trainingTrainer').value = ''; | |
| document.getElementById('trainingDescription').value = ''; | |
| document.getElementById('costPerParticipant').value = ''; | |
| document.getElementById('totalCost').value = ''; | |
| document.getElementById('additionalCostsContainer').innerHTML = ''; | |
| document.getElementById('totalAdditionalCosts').value = '₱0.00'; | |
| document.getElementById('grandTotal').value = '₱0.00'; | |
| document.getElementById('trainingPhoto').value = ''; | |
| document.getElementById('photoFileName').textContent = 'No photo selected'; | |
| document.getElementById('photoPreviewContainer').classList.add('hidden'); | |
| } | |
| // Render participants list grouped by program | |
| function renderParticipantsList() { | |
| const container = document.getElementById('participantsList'); | |
| // Group participants by program | |
| const programsWithParticipants = trainingPrograms.filter(program => | |
| program.namedParticipants && program.namedParticipants.length > 0 | |
| ); | |
| if (programsWithParticipants.length === 0) { | |
| container.innerHTML = ` | |
| <div class="text-center py-10 text-gray-500"> | |
| <i class="fas fa-users text-4xl mb-2"></i> | |
| <p>No participants added yet</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = programsWithParticipants.map(program => { | |
| const programId = `participants-${program.id}`; | |
| return ` | |
| <div class="mb-4 border border-gray-200 rounded-lg overflow-hidden"> | |
| <div class="flex justify-between items-center bg-gray-100 p-3 cursor-pointer" | |
| onclick="toggleParticipantsGroup('${programId}')"> | |
| <h3 class="font-medium text-darkgreen-800">${program.title}</h3> | |
| <div class="flex items-center"> | |
| <span class="text-sm text-gray-600 mr-2">${program.namedParticipants.length} participants</span> | |
| <i class="fas fa-chevron-down text-gray-500 transition-transform duration-200" id="icon-${programId}"></i> | |
| </div> | |
| </div> | |
| <div id="${programId}" class="hidden bg-white p-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| ${program.namedParticipants.map(name => ` | |
| <div class="bg-gray-50 p-3 rounded border border-gray-200"> | |
| <p class="font-medium text-darkgreen-800">${name}</p> | |
| </div> | |
| `).join('')} | |
| ${program.unnamedParticipants > 0 ? ` | |
| <div class="bg-gray-100 p-3 rounded border border-gray-200"> | |
| <p class="text-gray-600">+ ${program.unnamedParticipants} unnamed participants</p> | |
| </div> | |
| ` : ''} | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| // Toggle participants group visibility | |
| function toggleParticipantsGroup(id) { | |
| const group = document.getElementById(id); | |
| const icon = document.getElementById(`icon-${id}`); | |
| group.classList.toggle('hidden'); | |
| icon.classList.toggle('fa-chevron-down'); | |
| icon.classList.toggle('fa-chevron-up'); | |
| } | |
| // Render training list | |
| function renderTrainingList(filteredPrograms = null) { | |
| const container = document.getElementById('trainingList'); | |
| if (trainingPrograms.length === 0) { | |
| container.innerHTML = ` | |
| <div class="text-center py-10 text-gray-500"> | |
| <i class="fas fa-clipboard-list text-4xl mb-2"></i> | |
| <p>No training programs added yet</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| const programsToRender = filteredPrograms || trainingPrograms; | |
| container.innerHTML = programsToRender.map(program => { | |
| const totalCost = program.costPerParticipant * program.participants; | |
| const additionalCostsTotal = program.additionalCosts.reduce((sum, cost) => sum + cost.amount, 0); | |
| const grandTotal = totalCost + additionalCostsTotal; | |
| // Format dates | |
| const startDate = new Date(program.startDate).toLocaleDateString(); | |
| const endDate = new Date(program.endDate).toLocaleDateString(); | |
| // Status badge color | |
| let statusClass = ''; | |
| switch(program.status) { | |
| case 'ongoing': statusClass = 'bg-blue-100 text-blue-800'; break; | |
| case 'pending': statusClass = 'bg-yellow-100 text-yellow-800'; break; | |
| case 'closed': statusClass = 'bg-green-100 text-green-800'; break; | |
| case 'open': statusClass = 'bg-purple-100 text-purple-800'; break; | |
| default: statusClass = 'bg-gray-100 text-gray-800'; | |
| } | |
| return ` | |
| <div class="training-item border-2 rounded-xl mb-4 overflow-hidden ${program.collapsed ? 'collapsed' : ''}"> | |
| <div class="flex justify-between items-center bg-gray-50 p-4 cursor-pointer" onclick="toggleTrainingItem(this.parentElement)"> | |
| <div class="flex items-center"> | |
| <span class="text-xs font-medium px-2 py-1 rounded-full ${statusClass} mr-3">${program.status.charAt(0).toUpperCase() + program.status.slice(1)}</span> | |
| <h3 class="font-medium text-darkgreen-800">${program.title}</h3> | |
| </div> | |
| <div class="flex items-center"> | |
| <span class="text-sm font-medium">${formatCurrency(grandTotal)}</span> | |
| <i class="fas fa-chevron-${program.collapsed ? 'down' : 'up'} ml-3 text-gray-500"></i> | |
| </div> | |
| </div> | |
| <div class="p-4 border-t"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <p class="text-sm text-gray-600 mb-1">Description</p> | |
| <p class="text-gray-800">${program.description}</p> | |
| </div> | |
| <div> | |
| <p class="text-sm text-gray-600 mb-1">Details</p> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div class="space-y-2"> | |
| <div> | |
| <span class="text-xs text-gray-500">Days</span> | |
| <p class="text-gray-800">${program.days}</p> | |
| </div> | |
| <div> | |
| <span class="text-xs text-gray-500">Venue</span> | |
| <p class="text-gray-800">${program.venue || 'N/A'}</p> | |
| </div> | |
| </div> | |
| <div class="space-y-2"> | |
| <div> | |
| <span class="text-xs text-gray-500">Trainer</span> | |
| <p class="text-gray-800">${program.trainer || 'N/A'}</p> | |
| </div> | |
| <div> | |
| <span class="text-xs text-gray-500">Participants (${program.participants})</span> | |
| <div class="flex flex-wrap gap-1 mt-1"> | |
| ${program.namedParticipants?.length > 0 ? | |
| program.namedParticipants.map(name => ` | |
| <span class="bg-darkgreen-100 text-darkgreen-800 text-xs px-2 py-1 rounded">${name}</span> | |
| `).join('') : ''} | |
| ${program.unnamedParticipants > 0 ? | |
| `<span class="bg-gray-200 text-gray-800 text-xs px-2 py-1 rounded">+${program.unnamedParticipants} unnamed</span>` : | |
| ''} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <p class="text-sm text-gray-600 mb-1">Cost Breakdown</p> | |
| <div class="bg-gray-50 p-3 rounded-lg"> | |
| <div class="flex justify-between mb-1"> | |
| <span>Cost per participant:</span> | |
| <span>${formatCurrency(program.costPerParticipant)} × ${program.participants} participants</span> | |
| <span class="font-medium">${formatCurrency(totalCost)}</span> | |
| </div> | |
| ${program.additionalCosts.length > 0 ? ` | |
| <div class="border-t pt-2 mt-2"> | |
| <p class="text-sm font-medium mb-1">Additional Costs:</p> | |
| ${program.additionalCosts.map(cost => ` | |
| <div class="grid grid-cols-2 gap-2 text-sm mb-1"> | |
| <span>${cost.description}:</span> | |
| <span class="text-right">${formatCurrency(cost.amount)}</span> | |
| </div> | |
| `).join('')} | |
| <div class="grid grid-cols-2 gap-2 font-medium border-t pt-1 mt-1"> | |
| <span>Total Additional Costs:</span> | |
| <span class="text-right">${formatCurrency(additionalCostsTotal)}</span> | |
| </div> | |
| </div> | |
| ` : ''} | |
| <div class="grid grid-cols-2 gap-2 font-bold border-t pt-2 mt-2"> | |
| <span>Grand Total:</span> | |
| <span class="text-right">${formatCurrency(grandTotal)}</span> | |
| </div> | |
| </div> | |
| </div> | |
| ${program.photo ? ` | |
| <div class="mb-4"> | |
| <p class="text-sm text-gray-600 mb-1">Training Photo</p> | |
| <img src="${program.photo}" class="photo-preview w-full rounded-lg shadow-md"> | |
| </div> | |
| ` : ''} | |
| <div class="flex justify-end space-x-2"> | |
| <button onclick="editTrainingProgram(${program.id})" class="text-sm bg-darkgreen-600 text-white px-3 py-1 rounded hover:bg-darkgreen-700"> | |
| <i class="fas fa-edit mr-1"></i> Edit | |
| </button> | |
| <button onclick="deleteTrainingProgram(${program.id})" class="text-sm bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600"> | |
| <i class="fas fa-trash mr-1"></i> Delete | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| // Toggle training item collapse/expand | |
| function toggleTrainingItem(item) { | |
| const isCollapsed = item.classList.contains('collapsed'); | |
| item.classList.toggle('collapsed'); | |
| // Update the icon | |
| const icon = item.querySelector('.fa-chevron-up, .fa-chevron-down'); | |
| if (icon) { | |
| icon.classList.toggle('fa-chevron-up'); | |
| icon.classList.toggle('fa-chevron-down'); | |
| } | |
| // Update the collapsed state in the data | |
| const id = parseInt(item.querySelector('button[onclick^="editTrainingProgram"]').getAttribute('onclick').match(/\d+/)[0]); | |
| const program = trainingPrograms.find(p => p.id === id); | |
| if (program) { | |
| program.collapsed = !isCollapsed; | |
| saveData(); | |
| } | |
| } | |
| // Filter trainings by status | |
| function filterTrainings() { | |
| const status = document.getElementById('filterStatus').value; | |
| if (status === 'all') { | |
| renderTrainingList(); | |
| } else { | |
| const filtered = trainingPrograms.filter(program => program.status === status); | |
| renderTrainingList(filtered); | |
| } | |
| } | |
| // Search trainings | |
| function searchTrainings() { | |
| const searchTerm = document.getElementById('searchInput').value.toLowerCase(); | |
| if (!searchTerm) { | |
| renderTrainingList(); | |
| return; | |
| } | |
| const filtered = trainingPrograms.filter(program => | |
| program.title.toLowerCase().includes(searchTerm) || | |
| program.description.toLowerCase().includes(searchTerm) || | |
| (program.venue && program.venue.toLowerCase().includes(searchTerm)) || | |
| (program.trainer && program.trainer.toLowerCase().includes(searchTerm)) | |
| ); | |
| renderTrainingList(filtered); | |
| } | |
| // Edit training program | |
| function editTrainingProgram(id) { | |
| const program = trainingPrograms.find(p => p.id === id); | |
| if (!program) return; | |
| // Create edit form | |
| const totalCost = program.costPerParticipant * program.participants; | |
| const additionalCostsTotal = program.additionalCosts.reduce((sum, cost) => sum + cost.amount, 0); | |
| const grandTotal = totalCost + additionalCostsTotal; | |
| let editForm = ` | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Training Title*</label> | |
| <input type="text" id="editTitle" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" value="${program.title}"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Status*</label> | |
| <select id="editStatus" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500"> | |
| <option value="ongoing" ${program.status === 'ongoing' ? 'selected' : ''}>Ongoing</option> | |
| <option value="pending" ${program.status === 'pending' ? 'selected' : ''}>Pending</option> | |
| <option value="closed" ${program.status === 'closed' ? 'selected' : ''}>Closed</option> | |
| <option value="open" ${program.status === 'open' ? 'selected' : ''}>Open</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Number of Days*</label> | |
| <input type="number" id="editDaysCount" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" value="${program.days}" min="1"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Number of Participants*</label> | |
| <input type="number" id="editParticipantsCount" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" value="${program.namedParticipants?.length || 0}" min="0" onchange="updateEditTotalCost()" readonly> | |
| <div class="mt-2"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Additional Unnamed Participants</label> | |
| <input type="number" id="editUnnamedParticipants" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" value="${program.unnamedParticipants || 0}" min="0" onchange="updateEditTotalCost()"> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Venue</label> | |
| <input type="text" id="editVenue" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" value="${program.venue || ''}"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Trainer/Facilitator</label> | |
| <input type="text" id="editTrainer" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" value="${program.trainer || ''}"> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Description/Details*</label> | |
| <textarea id="editDescription" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500">${program.description}</textarea> | |
| </div> | |
| <div class="mt-6 border-t pt-4"> | |
| <h3 class="text-lg font-medium text-darkgreen-800 mb-3">Cost Information</h3> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Cost per Participant (₱)*</label> | |
| <input type="number" id="editCostPerParticipant" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500" value="${program.costPerParticipant}" min="0" step="0.01" onchange="updateEditTotalCost()"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Total Cost (₱)</label> | |
| <input type="text" id="editTotalCost" class="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-100" readonly value="${formatCurrency(totalCost)}"> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <label class="block text-sm font-medium text-gray-700">Additional Costs</label> | |
| <button type="button" onclick="addEditAdditionalCost()" class="text-xs bg-darkgreen-600 text-white px-2 py-1 rounded hover:bg-darkgreen-700"> | |
| <i class="fas fa-plus mr-1"></i> Add Cost | |
| </button> | |
| </div> | |
| <div id="editAdditionalCostsContainer"> | |
| ${program.additionalCosts.map(cost => ` | |
| <div class="flex items-center mb-2"> | |
| <input type="text" placeholder="Description" value="${cost.description}" class="flex-1 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500"> | |
| <input type="number" placeholder="Amount (₱)" value="${cost.amount}" min="0" step="0.01" class="w-32 px-3 py-2 border-t border-b border-gray-300 focus:outline-none focus:ring-2 focus:ring-darkgreen-500" onchange="updateEditAdditionalCostsTotal()"> | |
| <button type="button" onclick="removeEditAdditionalCost(this)" class="px-3 py-2 bg-red-500 text-white rounded-r-md hover:bg-red-600"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| `).join('')} | |
| </div> | |
| <div class="mt-2"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Total Additional Costs (₱)</label> | |
| <input type="text" id="editTotalAdditionalCosts" class="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-100" readonly value="${formatCurrency(additionalCostsTotal)}"> | |
| </div> | |
| <div class="mt-2"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Grand Total (₱)</label> | |
| <input type="text" id="editGrandTotal" class="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-100 font-bold" readonly value="${formatCurrency(grandTotal)}"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-6 border-t pt-4"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Training Photo</label> | |
| ${program.photo ? ` | |
| <div class="mb-3"> | |
| <img src="${program.photo}" class="photo-preview w-full rounded-lg shadow-md mb-2"> | |
| <button type="button" onclick="removeEditPhoto()" class="text-sm bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600"> | |
| <i class="fas fa-trash mr-1"></i> Remove Photo | |
| </button> | |
| </div> | |
| ` : ''} | |
| <div class="flex items-center"> | |
| <label for="editTrainingPhoto" class="cursor-pointer bg-darkgreen-600 text-white px-4 py-2 rounded-md hover:bg-darkgreen-700"> | |
| <i class="fas fa-upload mr-2"></i> ${program.photo ? 'Change Photo' : 'Upload Photo'} | |
| </label> | |
| <input type="file" id="editTrainingPhoto" accept="image/*" class="hidden"> | |
| <span id="editPhotoFileName" class="ml-3 text-sm text-gray-500">${program.photo ? 'Photo uploaded' : 'No photo selected'}</span> | |
| </div> | |
| </div> | |
| <div class="mt-8 flex justify-end space-x-3"> | |
| <button type="button" onclick="closeEditModal()" class="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-100">Cancel</button> | |
| <button type="button" onclick="saveEditedTraining(${program.id})" class="bg-darkgreen-600 hover:bg-darkgreen-700 text-white px-6 py-2 rounded-lg font-medium"> | |
| <i class="fas fa-save mr-2"></i> Save Changes | |
| </button> | |
| </div> | |
| `; | |
| document.getElementById('editFormContent').innerHTML = editForm; | |
| document.getElementById('editModal').classList.remove('hidden'); | |
| document.getElementById('editTrainingPhoto').addEventListener('change', function(event) { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| document.getElementById('editPhotoFileName').textContent = file.name; | |
| } | |
| }); | |
| } | |
| // Close edit modal | |
| function closeEditModal() { | |
| document.getElementById('editModal').classList.add('hidden'); | |
| } | |
| // Add additional cost in edit form | |
| function addEditAdditionalCost() { | |
| const container = document.getElementById('editAdditionalCostsContainer'); | |
| const costDiv = document.createElement('div'); | |
| costDiv.className = 'flex items-center mb-2'; | |
| costDiv.innerHTML = ` | |
| <input type="text" placeholder="Description" class="flex-1 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-darkgreen-500"> | |
| <input type="number" placeholder="Amount (₱)" min="0" step="0.01" class="w-32 px-3 py-2 border-t border-b border-gray-300 focus:outline-none focus:ring-2 focus:ring-darkgreen-500" onchange="updateEditAdditionalCostsTotal()"> | |
| <button type="button" onclick="removeEditAdditionalCost(this)" class="px-3 py-2 bg-red-500 text-white rounded-r-md hover:bg-red-600"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| `; | |
| container.appendChild(costDiv); | |
| } | |
| // Remove additional cost in edit form | |
| function removeEditAdditionalCost(button) { | |
| const costDiv = button.parentElement; | |
| costDiv.remove(); | |
| updateEditAdditionalCostsTotal(); | |
| } | |
| // Update total cost in edit form | |
| function updateEditTotalCost() { | |
| const participants = parseInt(document.getElementById('editParticipantsCount').value) || 0; | |
| const costPerParticipant = parseFloat(document.getElementById('editCostPerParticipant').value) || 0; | |
| const totalCost = participants * costPerParticipant; | |
| document.getElementById('editTotalCost').value = formatCurrency(totalCost); | |
| updateEditGrandTotal(); | |
| } | |
| // Update additional costs total in edit form | |
| function updateEditAdditionalCostsTotal() { | |
| const inputs = document.querySelectorAll('#editAdditionalCostsContainer input[type="number"]'); | |
| let total = 0; | |
| inputs.forEach(input => { | |
| total += parseFloat(input.value) || 0; | |
| }); | |
| document.getElementById('editTotalAdditionalCosts').value = formatCurrency(total); | |
| updateEditGrandTotal(); | |
| } | |
| // Update grand total in edit form | |
| function updateEditGrandTotal() { | |
| const totalCost = parseFloat(document.getElementById('editTotalCost').value.replace(/[^0-9.-]+/g,"")) || 0; | |
| const additionalCosts = parseFloat(document.getElementById('editTotalAdditionalCosts').value.replace(/[^0-9.-]+/g,"")) || 0; | |
| const grandTotal = totalCost + additionalCosts; | |
| document.getElementById('editGrandTotal').value = formatCurrency(grandTotal); | |
| } | |
| // Remove photo in edit form | |
| function removeEditPhoto() { | |
| const programId = parseInt(document.querySelector('#editFormContent button[onclick^="saveEditedTraining"]').getAttribute('onclick').match(/\d+/)[0]); | |
| const program = trainingPrograms.find(p => p.id === programId); | |
| if (program) { | |
| program.photo = null; | |
| document.getElementById('editPhotoFileName').textContent = 'No photo selected'; | |
| document.querySelector('#editFormContent img.photo-preview')?.remove(); | |
| } | |
| } | |
| // Save edited training | |
| function saveEditedTraining(id) { | |
| const program = trainingPrograms.find(p => p.id === id); | |
| if (!program) return; | |
| // Validate required fields | |
| const title = document.getElementById('editTitle').value.trim(); | |
| const description = document.getElementById('editDescription').value.trim(); | |
| const days = document.getElementById('editDaysCount').value; | |
| const namedCount = parseInt(document.getElementById('editParticipantsCount').value) || 0; | |
| const unnamedCount = parseInt(document.getElementById('editUnnamedParticipants').value) || 0; | |
| const participants = namedCount + unnamedCount; | |
| const costPerParticipant = document.getElementById('editCostPerParticipant').value; | |
| if (!title || !description || !days || !participants || !costPerParticipant) { | |
| alert('Please fill in all required fields (marked with *)'); | |
| return; | |
| } | |
| // Get additional costs | |
| const additionalCosts = []; | |
| const costInputs = document.querySelectorAll('#editAdditionalCostsContainer > div'); | |
| costInputs.forEach(costDiv => { | |
| const inputs = costDiv.querySelectorAll('input'); | |
| const description = inputs[0].value.trim(); | |
| const amount = parseFloat(inputs[1].value) || 0; | |
| if (description) { | |
| additionalCosts.push({ | |
| description: description, | |
| amount: amount | |
| }); | |
| } | |
| }); | |
| // Update program data | |
| program.title = title; | |
| program.status = document.getElementById('editStatus').value; | |
| program.days = parseInt(days); | |
| program.participants = parseInt(participants); | |
| program.venue = document.getElementById('editVenue').value.trim(); | |
| program.trainer = document.getElementById('editTrainer').value.trim(); | |
| program.description = description; | |
| program.costPerParticipant = parseFloat(costPerParticipant); | |
| program.additionalCosts = additionalCosts; | |
| // Handle photo update | |
| const photoInput = document.getElementById('editTrainingPhoto'); | |
| if (photoInput.files.length > 0) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| program.photo = e.target.result; | |
| saveData(); | |
| renderTrainingList(); | |
| closeEditModal(); | |
| }; | |
| reader.readAsDataURL(photoInput.files[0]); | |
| } else { | |
| saveData(); | |
| renderTrainingList(); | |
| closeEditModal(); | |
| } | |
| } | |
| // Delete training program | |
| function deleteTrainingProgram(id) { | |
| if (confirm('Are you sure you want to delete this training program?')) { | |
| trainingPrograms = trainingPrograms.filter(program => program.id !== id); | |
| saveData(); | |
| renderTrainingList(); | |
| renderParticipantsList(); | |
| } | |
| } | |
| // Export data to JSON file | |
| function exportData() { | |
| const data = { | |
| trainingPrograms: trainingPrograms, | |
| companyLogo: companyLogo | |
| }; | |
| const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'}); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'safety-training-data.json'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| // Import data from JSON file | |
| function importData() { | |
| const fileInput = document.getElementById('importFile'); | |
| const file = fileInput.files[0]; | |
| if (!file) { | |
| alert('Please select a file to import'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| const data = JSON.parse(e.target.result); | |
| if (data.trainingPrograms) { | |
| trainingPrograms = data.trainingPrograms; | |
| if (data.companyLogo) { | |
| companyLogo = data.companyLogo; | |
| document.getElementById('companyLogo').src = companyLogo; | |
| } | |
| saveData(); | |
| renderTrainingList(); | |
| renderParticipantsList(); | |
| document.getElementById('importModal').classList.add('hidden'); | |
| fileInput.value = ''; | |
| alert('Data imported successfully!'); | |
| } else { | |
| alert('Invalid data format'); | |
| } | |
| } catch (error) { | |
| alert('Error parsing JSON file: ' + error.message); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| } | |
| // Save data to localStorage | |
| function saveData() { | |
| const data = { | |
| trainingPrograms: trainingPrograms, | |
| companyLogo: companyLogo | |
| }; | |
| localStorage.setItem('safetyTrainingData', JSON.stringify(data)); | |
| } | |
| // Load data from localStorage | |
| function loadData() { | |
| const savedData = localStorage.getItem('safetyTrainingData'); | |
| if (savedData) { | |
| const data = JSON.parse(savedData); | |
| trainingPrograms = data.trainingPrograms || []; | |
| companyLogo = data.companyLogo || null; | |
| if (companyLogo) { | |
| document.getElementById('companyLogo').src = companyLogo; | |
| } | |
| } | |
| } | |
| </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=froiln1565/training" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |