training / index.html
froiln1565's picture
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
Raw
History Blame Contribute Delete
66.1 kB
<!DOCTYPE html>
<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>