trial / index.html
yanglops147's picture
Remove the start and end Date, add another functionality for named Participants. also on the Training Programs Tab once the added participant has Name included be sure to reflect it on the Training Programs Tab. and the name of Participants should be stacked in parallel. - Initial Deployment
b9ee90e verified
Raw
History Blame Contribute Delete
65.3 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 Training Calculator</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.photo-preview {
width: 100%;
height: 300px;
object-fit: cover;
border-radius: 0.5rem;
margin-top: 1rem;
display: none;
}
.training-item {
transition: all 0.3s ease;
}
.training-item.collapsed {
max-height: 60px;
overflow: hidden;
}
.scrollable-list {
max-height: 400px;
overflow-y: auto;
}
.scrollable-list::-webkit-scrollbar {
width: 8px;
}
.scrollable-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.scrollable-list::-webkit-scrollbar-thumb {
background: #2d7d46;
border-radius: 10px;
}
.scrollable-list::-webkit-scrollbar-thumb:hover {
background: #1e5631;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
</style>
</head>
<body class="bg-gray-100">
<div class="container mx-auto p-4 max-w-6xl">
<!-- Header with Logo Upload -->
<div class="bg-green-800 text-white p-6 rounded-lg shadow-lg mb-6 flex justify-between items-center">
<div class="flex items-center">
<div class="mr-4">
<img id="company-logo" src="" alt="Company Logo" class="h-16 w-16 object-contain hidden">
<label for="logo-upload" class="cursor-pointer bg-green-700 hover:bg-green-600 p-2 rounded-lg">
<i class="fas fa-upload mr-2"></i>Upload Logo
</label>
<input type="file" id="logo-upload" accept="image/*" class="hidden">
</div>
<div>
<h1 class="text-3xl font-bold">Safety Officers Specialized Training Calculator and Tracker</h1>
<p class="text-green-200">Track and manage all your safety training programs efficiently</p>
</div>
</div>
</div>
<!-- Main Content -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Input Form -->
<div class="lg:col-span-2 bg-white p-6 rounded-lg shadow-lg">
<h2 class="text-2xl font-semibold text-green-800 mb-4">Add New Training Program</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Basic Information -->
<div class="md:col-span-2 bg-green-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-green-800 mb-3">Basic Information</h3>
<div class="space-y-3">
<div>
<label for="training-title" class="block text-sm font-medium text-green-700">Training Title*</label>
<input type="text" id="training-title" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
<div>
<label for="training-description" class="block text-sm font-medium text-green-700">Description*</label>
<textarea id="training-description" rows="3" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border"></textarea>
</div>
</div>
</div>
<!-- Training Details -->
<div class="bg-green-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-green-800 mb-3">Training Details</h3>
<div class="space-y-3">
<div>
<label for="start-date" class="block text-sm font-medium text-green-700">Start Date*</label>
<input type="date" id="start-date" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
<div>
<label for="end-date" class="block text-sm font-medium text-green-700">End Date*</label>
<input type="date" id="end-date" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
<div>
<label for="participants" class="block text-sm font-medium text-green-700">Number of Participants*</label>
<input type="number" id="participants" min="1" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
<div>
<label for="days" class="block text-sm font-medium text-green-700">Training Days*</label>
<input type="number" id="days" min="1" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
</div>
</div>
<!-- Cost Information -->
<div class="bg-green-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-green-800 mb-3">Cost Information</h3>
<div class="space-y-3">
<div>
<label for="base-cost" class="block text-sm font-medium text-green-700">Base Cost (₱)*</label>
<input type="number" id="base-cost" min="0" step="0.01" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
<div>
<label for="cost-per-participant" class="block text-sm font-medium text-green-700">Cost per Participant (₱)*</label>
<input type="number" id="cost-per-participant" min="0" step="0.01" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
<div>
<label for="cost-per-day" class="block text-sm font-medium text-green-700">Cost per Day (₱)*</label>
<input type="number" id="cost-per-day" min="0" step="0.01" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
</div>
</div>
<!-- Additional Costs -->
<div class="md:col-span-2 bg-green-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-green-800 mb-3">Additional Costs</h3>
<div id="additional-costs-container">
<div class="additional-cost-item grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
<div>
<label class="block text-sm font-medium text-green-700">Cost Type</label>
<input type="text" class="cost-type mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" placeholder="e.g., Materials, Venue">
</div>
<div>
<label class="block text-sm font-medium text-green-700">Amount (₱)</label>
<input type="number" min="0" step="0.01" class="cost-amount mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
<div class="flex items-end">
<button type="button" class="remove-cost bg-red-500 hover:bg-red-600 text-white py-2 px-3 rounded-md text-sm">
<i class="fas fa-trash mr-1"></i> Remove
</button>
</div>
</div>
</div>
<button type="button" id="add-cost" class="mt-2 bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-md text-sm">
<i class="fas fa-plus mr-1"></i> Add Another Cost
</button>
</div>
<!-- Status and Photo -->
<div class="md:col-span-2 bg-green-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-green-800 mb-3">Status & Documentation</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="status" class="block text-sm font-medium text-green-700">Status*</label>
<select id="status" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
<option value="pending">Pending</option>
<option value="ongoing">Ongoing</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div>
<label for="training-photo" class="block text-sm font-medium text-green-700">Training Photo</label>
<input type="file" id="training-photo" accept="image/*" class="mt-1 block w-full text-sm text-green-700 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-green-500 file:text-white hover:file:bg-green-600">
<img id="photo-preview" class="photo-preview mt-2" alt="Training photo preview">
</div>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="mt-6 flex justify-end">
<button type="button" id="save-training" class="bg-green-700 hover:bg-green-800 text-white font-bold py-2 px-6 rounded-lg shadow">
<i class="fas fa-save mr-2"></i> Save Training
</button>
</div>
</div>
<!-- Summary and Calculations -->
<div class="bg-white p-6 rounded-lg shadow-lg">
<h2 class="text-2xl font-semibold text-green-800 mb-4">Training Summary</h2>
<div class="bg-green-50 p-4 rounded-lg mb-4">
<h3 class="text-lg font-medium text-green-800 mb-3">Calculated Costs</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-green-700">Base Cost:</span>
<span id="summary-base-cost" class="font-medium">₱0.00</span>
</div>
<div class="flex justify-between">
<span class="text-green-700">Participants Cost:</span>
<span id="summary-participants-cost" class="font-medium">₱0.00</span>
</div>
<div class="flex justify-between">
<span class="text-green-700">Days Cost:</span>
<span id="summary-days-cost" class="font-medium">₱0.00</span>
</div>
<div class="flex justify-between">
<span class="text-green-700">Additional Costs:</span>
<span id="summary-additional-costs" class="font-medium">₱0.00</span>
</div>
<div class="border-t border-green-300 my-2 pt-2 flex justify-between font-bold text-green-800">
<span>Total Cost:</span>
<span id="summary-total-cost">₱0.00</span>
</div>
<div class="border-t border-green-300 my-2 pt-2 flex justify-between">
<span class="text-green-700">Cost per Participant:</span>
<span id="summary-cost-per-participant" class="font-medium">₱0.00</span>
</div>
</div>
</div>
<!-- Filters -->
<div class="bg-green-50 p-4 rounded-lg mb-4">
<h3 class="text-lg font-medium text-green-800 mb-3">Filter Trainings</h3>
<div class="space-y-3">
<div>
<label for="filter-status" class="block text-sm font-medium text-green-700">Status</label>
<select id="filter-status" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
<option value="all">All Statuses</option>
<option value="pending">Pending</option>
<option value="ongoing">Ongoing</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div>
<label for="filter-date" class="block text-sm font-medium text-green-700">Date Range</label>
<select id="filter-date" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
<option value="all">All Dates</option>
<option value="this-month">This Month</option>
<option value="last-month">Last Month</option>
<option value="this-year">This Year</option>
<option value="last-year">Last Year</option>
</select>
</div>
<div>
<label for="search-title" class="block text-sm font-medium text-green-700">Search Title</label>
<input type="text" id="search-title" placeholder="Search training title..." class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
</div>
</div>
<!-- Data Management -->
<div class="bg-green-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-green-800 mb-3">Data Management</h3>
<div class="grid grid-cols-1 gap-2">
<button id="export-data" class="bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-md text-sm w-full text-left">
<i class="fas fa-file-export mr-2"></i> Export Data
</button>
<label for="import-data" class="bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-md text-sm w-full text-left cursor-pointer">
<i class="fas fa-file-import mr-2"></i> Import Data
<input type="file" id="import-data" accept=".json" class="hidden">
</label>
<button id="clear-data" class="bg-red-500 hover:bg-red-600 text-white py-2 px-4 rounded-md text-sm w-full text-left">
<i class="fas fa-trash-alt mr-2"></i> Clear All Data
</button>
</div>
</div>
</div>
</div>
<!-- Training Programs List -->
<div class="mt-8 bg-white p-6 rounded-lg shadow-lg">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-semibold text-green-800">Training Programs</h2>
<div class="flex items-center">
<span class="text-sm text-green-700 mr-2">Collapse All</span>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="toggle-collapse" class="sr-only peer">
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-green-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-600"></div>
</label>
</div>
</div>
<div id="training-list" class="scrollable-list space-y-3">
<!-- Training items will be added here dynamically -->
<div class="text-center py-8 text-gray-500" id="empty-message">
<i class="fas fa-clipboard-list text-4xl mb-2"></i>
<p>No training programs added yet.</p>
<p class="text-sm">Add your first training program using the form above.</p>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div id="edit-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-xl 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-green-800">Edit Training Program</h3>
<button id="close-edit-modal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div id="edit-form-content" class="space-y-4">
<!-- Edit form content will be inserted here -->
</div>
<div class="mt-6 flex justify-end space-x-3">
<button id="cancel-edit" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded">
Cancel
</button>
<button id="save-edit" class="bg-green-700 hover:bg-green-800 text-white font-bold py-2 px-4 rounded">
Save Changes
</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize variables
let trainings = [];
let currentEditId = null;
// Load data from localStorage
loadData();
// Calculate costs when inputs change
const calculateInputs = ['participants', 'days', 'base-cost', 'cost-per-participant', 'cost-per-day'];
calculateInputs.forEach(inputId => {
document.getElementById(inputId).addEventListener('input', calculateCosts);
});
// Add additional cost row
document.getElementById('add-cost').addEventListener('click', function() {
addAdditionalCostRow();
});
// Remove additional cost row (delegated event)
document.getElementById('additional-costs-container').addEventListener('click', function(e) {
if (e.target.classList.contains('remove-cost') || e.target.closest('.remove-cost')) {
const costItem = e.target.closest('.additional-cost-item');
if (costItem && document.querySelectorAll('.additional-cost-item').length > 1) {
costItem.remove();
calculateCosts();
}
}
});
// Save training program
document.getElementById('save-training').addEventListener('click', saveTraining);
// Handle photo preview
document.getElementById('training-photo').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
const preview = document.getElementById('photo-preview');
preview.src = event.target.result;
preview.style.display = 'block';
};
reader.readAsDataURL(file);
}
});
// Handle company logo upload
document.getElementById('logo-upload').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
const logo = document.getElementById('company-logo');
logo.src = event.target.result;
logo.classList.remove('hidden');
};
reader.readAsDataURL(file);
}
});
// Filter trainings
document.getElementById('filter-status').addEventListener('change', filterTrainings);
document.getElementById('filter-date').addEventListener('change', filterTrainings);
document.getElementById('search-title').addEventListener('input', filterTrainings);
// Toggle collapse all
document.getElementById('toggle-collapse').addEventListener('change', function(e) {
const items = document.querySelectorAll('.training-item');
items.forEach(item => {
if (item.id !== 'empty-message') {
item.classList.toggle('collapsed', e.target.checked);
}
});
});
// Data management
document.getElementById('export-data').addEventListener('click', exportData);
document.getElementById('import-data').addEventListener('change', importData);
document.getElementById('clear-data').addEventListener('click', clearData);
// Edit modal
document.getElementById('close-edit-modal').addEventListener('click', closeEditModal);
document.getElementById('cancel-edit').addEventListener('click', closeEditModal);
document.getElementById('save-edit').addEventListener('click', saveEditedTraining);
// Functions
function calculateCosts() {
const participants = parseInt(document.getElementById('participants').value) || 0;
const days = parseInt(document.getElementById('days').value) || 0;
const baseCost = parseFloat(document.getElementById('base-cost').value) || 0;
const costPerParticipant = parseFloat(document.getElementById('cost-per-participant').value) || 0;
const costPerDay = parseFloat(document.getElementById('cost-per-day').value) || 0;
// Calculate additional costs
let additionalCosts = 0;
document.querySelectorAll('.additional-cost-item').forEach(item => {
const amount = parseFloat(item.querySelector('.cost-amount').value) || 0;
additionalCosts += amount;
});
// Calculate totals
const participantsCost = participants * costPerParticipant;
const daysCost = days * costPerDay;
const totalCost = baseCost + participantsCost + daysCost + additionalCosts;
const costPerParticipantTotal = participants > 0 ? totalCost / participants : 0;
// Update summary
document.getElementById('summary-base-cost').textContent = formatCurrency(baseCost);
document.getElementById('summary-participants-cost').textContent = formatCurrency(participantsCost);
document.getElementById('summary-days-cost').textContent = formatCurrency(daysCost);
document.getElementById('summary-additional-costs').textContent = formatCurrency(additionalCosts);
document.getElementById('summary-total-cost').textContent = formatCurrency(totalCost);
document.getElementById('summary-cost-per-participant').textContent = formatCurrency(costPerParticipantTotal);
}
function formatCurrency(amount) {
return '₱' + amount.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
}
function addAdditionalCostRow(type = '', amount = '') {
const container = document.getElementById('additional-costs-container');
const div = document.createElement('div');
div.className = 'additional-cost-item grid grid-cols-1 md:grid-cols-3 gap-3 mb-3';
div.innerHTML = `
<div>
<label class="block text-sm font-medium text-green-700">Cost Type</label>
<input type="text" class="cost-type mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" placeholder="e.g., Materials, Venue" value="${type}">
</div>
<div>
<label class="block text-sm font-medium text-green-700">Amount (₱)</label>
<input type="number" min="0" step="0.01" class="cost-amount mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" value="${amount}">
</div>
<div class="flex items-end">
<button type="button" class="remove-cost bg-red-500 hover:bg-red-600 text-white py-2 px-3 rounded-md text-sm">
<i class="fas fa-trash mr-1"></i> Remove
</button>
</div>
`;
container.appendChild(div);
}
function saveTraining() {
// Validate required fields
const title = document.getElementById('training-title').value.trim();
const description = document.getElementById('training-description').value.trim();
const startDate = document.getElementById('start-date').value;
const endDate = document.getElementById('end-date').value;
const participants = parseInt(document.getElementById('participants').value) || 0;
const days = parseInt(document.getElementById('days').value) || 0;
const baseCost = parseFloat(document.getElementById('base-cost').value) || 0;
const costPerParticipant = parseFloat(document.getElementById('cost-per-participant').value) || 0;
const costPerDay = parseFloat(document.getElementById('cost-per-day').value) || 0;
const status = document.getElementById('status').value;
if (!title || !description || !startDate || !endDate || participants <= 0 || days <= 0) {
alert('Please fill in all required fields with valid values.');
return;
}
// Get additional costs
const additionalCosts = [];
document.querySelectorAll('.additional-cost-item').forEach(item => {
const type = item.querySelector('.cost-type').value.trim();
const amount = parseFloat(item.querySelector('.cost-amount').value) || 0;
if (type || amount > 0) {
additionalCosts.push({ type, amount });
}
});
// Get photo if available
const photoInput = document.getElementById('training-photo');
let photoBase64 = '';
if (photoInput.files.length > 0) {
const file = photoInput.files[0];
const reader = new FileReader();
reader.onload = function(event) {
photoBase64 = event.target.result;
completeSave(title, description, startDate, endDate, participants, days, baseCost,
costPerParticipant, costPerDay, additionalCosts, status, photoBase64);
};
reader.readAsDataURL(file);
} else {
completeSave(title, description, startDate, endDate, participants, days, baseCost,
costPerParticipant, costPerDay, additionalCosts, status, photoBase64);
}
}
function completeSave(title, description, startDate, endDate, participants, days, baseCost,
costPerParticipant, costPerDay, additionalCosts, status, photoBase64) {
// Calculate totals
const participantsCost = participants * costPerParticipant;
const daysCost = days * costPerDay;
const additionalCostsTotal = additionalCosts.reduce((sum, cost) => sum + cost.amount, 0);
const totalCost = baseCost + participantsCost + daysCost + additionalCostsTotal;
const costPerParticipantTotal = participants > 0 ? totalCost / participants : 0;
// Create training object
const training = {
id: Date.now().toString(),
title,
description,
startDate,
endDate,
participants,
days,
baseCost,
costPerParticipant,
costPerDay,
additionalCosts,
status,
photo: photoBase64,
participantsCost,
daysCost,
additionalCostsTotal,
totalCost,
costPerParticipantTotal,
createdAt: new Date().toISOString()
};
// Add to array and save
trainings.push(training);
saveData();
renderTrainings();
resetForm();
}
function resetForm() {
document.getElementById('training-title').value = '';
document.getElementById('training-description').value = '';
document.getElementById('start-date').value = '';
document.getElementById('end-date').value = '';
document.getElementById('participants').value = '';
document.getElementById('days').value = '';
document.getElementById('base-cost').value = '';
document.getElementById('cost-per-participant').value = '';
document.getElementById('cost-per-day').value = '';
document.getElementById('status').value = 'pending';
document.getElementById('training-photo').value = '';
document.getElementById('photo-preview').style.display = 'none';
// Reset additional costs (keep one empty row)
const container = document.getElementById('additional-costs-container');
container.innerHTML = '';
addAdditionalCostRow();
// Reset calculations
document.getElementById('summary-base-cost').textContent = formatCurrency(0);
document.getElementById('summary-participants-cost').textContent = formatCurrency(0);
document.getElementById('summary-days-cost').textContent = formatCurrency(0);
document.getElementById('summary-additional-costs').textContent = formatCurrency(0);
document.getElementById('summary-total-cost').textContent = formatCurrency(0);
document.getElementById('summary-cost-per-participant').textContent = formatCurrency(0);
}
function renderTrainings(filteredTrainings = null) {
const container = document.getElementById('training-list');
const trainingsToRender = filteredTrainings || trainings;
if (trainingsToRender.length === 0) {
container.innerHTML = `
<div class="text-center py-8 text-gray-500" id="empty-message">
<i class="fas fa-clipboard-list text-4xl mb-2"></i>
<p>No training programs found.</p>
<p class="text-sm">Add your first training program using the form above.</p>
</div>
`;
return;
}
container.innerHTML = '';
trainingsToRender.forEach(training => {
const startDate = new Date(training.startDate);
const endDate = new Date(training.endDate);
const dateOptions = { year: 'numeric', month: 'short', day: 'numeric' };
const item = document.createElement('div');
item.className = 'training-item collapsed bg-green-50 rounded-lg overflow-hidden shadow';
item.id = training.id;
item.innerHTML = `
<div class="p-4 cursor-pointer" onclick="toggleTrainingDetails('${training.id}')">
<div class="flex justify-between items-center">
<div>
<h3 class="font-semibold text-green-800">${training.title}</h3>
<p class="text-sm text-green-600">${startDate.toLocaleDateString(undefined, dateOptions)} - ${endDate.toLocaleDateString(undefined, dateOptions)}</p>
</div>
<div class="flex items-center space-x-2">
<span class="px-2 py-1 text-xs rounded-full ${getStatusClass(training.status)}">
${training.status.charAt(0).toUpperCase() + training.status.slice(1)}
</span>
<span class="font-bold text-green-800">${formatCurrency(training.totalCost)}</span>
<i class="fas fa-chevron-down text-green-600 transition-transform"></i>
</div>
</div>
</div>
<div class="px-4 pb-4 hidden">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 class="font-medium text-green-700 mb-2">Details</h4>
<p class="text-sm text-gray-700 mb-3">${training.description}</p>
<div class="grid grid-cols-2 gap-2 text-sm">
<div>
<span class="text-green-700">Participants:</span> ${training.participants}
</div>
<div>
<span class="text-green-700">Days:</span> ${training.days}
</div>
<div>
<span class="text-green-700">Base Cost:</span> ${formatCurrency(training.baseCost)}
</div>
<div>
<span class="text-green-700">Per Participant:</span> ${formatCurrency(training.costPerParticipant)}
</div>
<div>
<span class="text-green-700">Per Day:</span> ${formatCurrency(training.costPerDay)}
</div>
<div>
<span class="text-green-700">Cost/Participant:</span> ${formatCurrency(training.costPerParticipantTotal)}
</div>
</div>
</div>
<div>
<h4 class="font-medium text-green-700 mb-2">Additional Costs</h4>
${training.additionalCosts.length > 0 ?
training.additionalCosts.map(cost => `
<div class="flex justify-between text-sm mb-1">
<span>${cost.type || 'Unspecified'}:</span>
<span>${formatCurrency(cost.amount)}</span>
</div>
`).join('') :
'<p class="text-sm text-gray-500">No additional costs</p>'}
<div class="mt-4 border-t pt-2">
<div class="flex justify-between font-semibold">
<span>Total Cost:</span>
<span>${formatCurrency(training.totalCost)}</span>
</div>
</div>
</div>
</div>
${training.photo ? `
<div class="mt-4">
<h4 class="font-medium text-green-700 mb-2">Training Photo</h4>
<img src="${training.photo}" alt="Training photo" class="w-full h-auto rounded-lg">
</div>
` : ''}
<div class="mt-4 flex justify-end space-x-2">
<button onclick="editTraining('${training.id}')" class="bg-blue-500 hover:bg-blue-600 text-white py-1 px-3 rounded text-sm">
<i class="fas fa-edit mr-1"></i> Edit
</button>
<button onclick="deleteTraining('${training.id}')" class="bg-red-500 hover:bg-red-600 text-white py-1 px-3 rounded text-sm">
<i class="fas fa-trash mr-1"></i> Delete
</button>
</div>
</div>
`;
container.appendChild(item);
});
}
function getStatusClass(status) {
switch(status) {
case 'pending': return 'bg-yellow-100 text-yellow-800';
case 'ongoing': return 'bg-blue-100 text-blue-800';
case 'completed': return 'bg-green-100 text-green-800';
case 'cancelled': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
}
function toggleTrainingDetails(id) {
const item = document.getElementById(id);
if (item) {
const details = item.querySelector('.hidden');
const icon = item.querySelector('.fa-chevron-down');
details.classList.toggle('hidden');
icon.classList.toggle('rotate-180');
}
}
function editTraining(id) {
const training = trainings.find(t => t.id === id);
if (!training) return;
currentEditId = id;
// Create edit form
let additionalCostsHTML = '';
if (training.additionalCosts.length > 0) {
additionalCostsHTML = training.additionalCosts.map(cost => `
<div class="additional-cost-item grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
<div>
<label class="block text-sm font-medium text-green-700">Cost Type</label>
<input type="text" class="cost-type mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" value="${cost.type || ''}">
</div>
<div>
<label class="block text-sm font-medium text-green-700">Amount (₱)</label>
<input type="number" min="0, step="0.01" class="cost-amount mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" value="${cost.amount || ''}">
</div>
<div class="flex items-end">
<button type="button" class="remove-cost bg-red-500 hover:bg-red-600 text-white py-2 px-3 rounded-md text-sm">
<i class="fas fa-trash mr-1"></i> Remove
</button>
</div>
</div>
`).join('');
} else {
additionalCostsHTML = `
<div class="additional-cost-item grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
<div>
<label class="block text-sm font-medium text-green-700">Cost Type</label>
<input type="text" class="cost-type mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
<div>
<label class="block text-sm font-medium text-green-700">Amount (₱)</label>
<input type="number" min="0" step="0.01" class="cost-amount mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
<div class="flex items-end">
<button type="button" class="remove-cost bg-red-500 hover:bg-red-600 text-white py-2 px-3 rounded-md text-sm">
<i class="fas fa-trash mr-1"></i> Remove
</button>
</div>
</div>
`;
}
const editForm = `
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-green-700">Training Title*</label>
<input type="text" id="edit-title" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" value="${training.title}">
</div>
<div>
<label class="block text-sm font-medium text-green-700">Status*</label>
<select id="edit-status" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
<option value="pending" ${training.status === 'pending' ? 'selected' : ''}>Pending</option>
<option value="ongoing" ${training.status === 'ongoing' ? 'selected' : ''}>Ongoing</option>
<option value="completed" ${training.status === 'completed' ? 'selected' : ''}>Completed</option>
<option value="cancelled" ${training.status === 'cancelled' ? 'selected' : ''}>Cancelled</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-green-700">Start Date*</label>
<input type="date" id="edit-start-date" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" value="${training.startDate}">
</div>
<div>
<label class="block text-sm font-medium text-green-700">End Date*</label>
<input type="date" id="edit-end-date" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" value="${training.endDate}">
</div>
<div>
<label class="block text-sm font-medium text-green-700">Participants*</label>
<input type="number" id="edit-participants" min="1" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" value="${training.participants}">
</div>
<div>
<label class="block text-sm font-medium text-green-700">Days*</label>
<input type="number" id="edit-days" min="1" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" value="${training.days}">
</div>
<div>
<label class="block text-sm font-medium text-green-700">Base Cost (₱)*</label>
<input type="number" id="edit-base-cost" min="0" step="0.01" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" value="${training.baseCost}">
</div>
<div>
<label class="block text-sm font-medium text-green-700">Cost per Participant (₱)*</label>
<input type="number" id="edit-cost-per-participant" min="0" step="0.01" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" value="${training.costPerParticipant}">
</div>
<div>
<label class="block text-sm font-medium text-green-700">Cost per Day (₱)*</label>
<input type="number" id="edit-cost-per-day" min="0" step="0.01" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" value="${training.costPerDay}">
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-green-700">Description*</label>
<textarea id="edit-description" rows="3" class="mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">${training.description}</textarea>
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-green-700">Training Photo</label>
<input type="file" id="edit-photo" accept="image/*" class="mt-1 block w-full text-sm text-green-700 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-green-500 file:text-white hover:file:bg-green-600">
${training.photo ? `<img src="${training.photo}" alt="Current photo" class="mt-2 h-32 object-contain rounded-lg border">` : ''}
</div>
</div>
<div class="mt-4">
<h4 class="text-lg font-medium text-green-800 mb-3">Additional Costs</h4>
<div id="edit-additional-costs">
${additionalCostsHTML}
</div>
<button type="button" id="edit-add-cost" class="mt-2 bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-md text-sm">
<i class="fas fa-plus mr-1"></i> Add Another Cost
</button>
</div>
`;
document.getElementById('edit-form-content').innerHTML = editForm;
// Add event for new cost button in edit form
document.getElementById('edit-add-cost').addEventListener('click', function() {
const container = document.getElementById('edit-additional-costs');
const div = document.createElement('div');
div.className = 'additional-cost-item grid grid-cols-1 md:grid-cols-3 gap-3 mb-3';
div.innerHTML = `
<div>
<label class="block text-sm font-medium text-green-700">Cost Type</label>
<input type="text" class="cost-type mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border" placeholder="e.g., Materials, Venue">
</div>
<div>
<label class="block text-sm font-medium text-green-700">Amount (₱)</label>
<input type="number" min="0" step="0.01" class="cost-amount mt-1 block w-full rounded-md border-green-300 shadow-sm focus:border-green-500 focus:ring-green-500 p-2 border">
</div>
<div class="flex items-end">
<button type="button" class="remove-cost bg-red-500 hover:bg-red-600 text-white py-2 px-3 rounded-md text-sm">
<i class="fas fa-trash mr-1"></i> Remove
</button>
</div>
`;
container.appendChild(div);
});
// Remove additional cost row in edit form
document.getElementById('edit-additional-costs').addEventListener('click', function(e) {
if (e.target.classList.contains('remove-cost') || e.target.closest('.remove-cost')) {
const costItem = e.target.closest('.additional-cost-item');
if (costItem) {
costItem.remove();
}
}
});
// Show modal
document.getElementById('edit-modal').classList.remove('hidden');
}
function saveEditedTraining() {
if (!currentEditId) return;
const trainingIndex = trainings.findIndex(t => t.id === currentEditId);
if (trainingIndex === -1) return;
// Validate required fields
const title = document.getElementById('edit-title').value.trim();
const description = document.getElementById('edit-description').value.trim();
const startDate = document.getElementById('edit-start-date').value;
const endDate = document.getElementById('edit-end-date').value;
const participants = parseInt(document.getElementById('edit-participants').value) || 0;
const days = parseInt(document.getElementById('edit-days').value) || 0;
const baseCost = parseFloat(document.getElementById('edit-base-cost').value) || 0;
const costPerParticipant = parseFloat(document.getElementById('edit-cost-per-participant').value) || 0;
const costPerDay = parseFloat(document.getElementById('edit-cost-per-day').value) || 0;
const status = document.getElementById('edit-status').value;
if (!title || !description || !startDate || !endDate || participants <= 0 || days <= 0) {
alert('Please fill in all required fields with valid values.');
return;
}
// Get additional costs
const additionalCosts = [];
document.querySelectorAll('#edit-additional-costs .additional-cost-item').forEach(item => {
const type = item.querySelector('.cost-type').value.trim();
const amount = parseFloat(item.querySelector('.cost-amount').value) || 0;
if (type || amount > 0) {
additionalCosts.push({ type, amount });
}
});
// Get photo if available
const photoInput = document.getElementById('edit-photo');
let photoBase64 = trainings[trainingIndex].photo;
if (photoInput.files.length > 0) {
const file = photoInput.files[0];
const reader = new FileReader();
reader.onload = function(event) {
photoBase64 = event.target.result;
completeEdit(title, description, startDate, endDate, participants, days, baseCost,
costPerParticipant, costPerDay, additionalCosts, status, photoBase64, trainingIndex);
};
reader.readAsDataURL(file);
} else {
completeEdit(title, description, startDate, endDate, participants, days, baseCost,
costPerParticipant, costPerDay, additionalCosts, status, photoBase64, trainingIndex);
}
}
function completeEdit(title, description, startDate, endDate, participants, days, baseCost,
costPerParticipant, costPerDay, additionalCosts, status, photoBase64, trainingIndex) {
// Calculate totals
const participantsCost = participants * costPerParticipant;
const daysCost = days * costPerDay;
const additionalCostsTotal = additionalCosts.reduce((sum, cost) => sum + cost.amount, 0);
const totalCost = baseCost + participantsCost + daysCost + additionalCostsTotal;
const costPerParticipantTotal = participants > 0 ? totalCost / participants : 0;
// Update training object
trainings[trainingIndex] = {
...trainings[trainingIndex],
title,
description,
startDate,
endDate,
participants,
days,
baseCost,
costPerParticipant,
costPerDay,
additionalCosts,
status,
photo: photoBase64,
participantsCost,
daysCost,
additionalCostsTotal,
totalCost,
costPerParticipantTotal
};
saveData();
renderTrainings();
closeEditModal();
}
function closeEditModal() {
document.getElementById('edit-modal').classList.add('hidden');
currentEditId = null;
}
function deleteTraining(id) {
if (confirm('Are you sure you want to delete this training program?')) {
trainings = trainings.filter(t => t.id !== id);
saveData();
renderTrainings();
}
}
function filterTrainings() {
const statusFilter = document.getElementById('filter-status').value;
const dateFilter = document.getElementById('filter-date').value;
const searchTerm = document.getElementById('search-title').value.toLowerCase();
let filtered = [...trainings];
// Apply status filter
if (statusFilter !== 'all') {
filtered = filtered.filter(t => t.status === statusFilter);
}
// Apply date filter
if (dateFilter !== 'all') {
const now = new Date();
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
filtered = filtered.filter(t => {
const startDate = new Date(t.startDate);
const startMonth = startDate.getMonth();
const startYear = startDate.getFullYear();
switch(dateFilter) {
case 'this-month':
return startMonth === currentMonth && startYear === currentYear;
case 'last-month':
const lastMonth = currentMonth === 0 ? 11 : currentMonth - 1;
const lastMonthYear = currentMonth === 0 ? currentYear - 1 : currentYear;
return startMonth === lastMonth && startYear === lastMonthYear;
case 'this-year':
return startYear === currentYear;
case 'last-year':
return startYear === currentYear - 1;
default:
return true;
}
});
}
// Apply search filter
if (searchTerm) {
filtered = filtered.filter(t => t.title.toLowerCase().includes(searchTerm));
}
renderTrainings(filtered);
}
function saveData() {
localStorage.setItem('safetyTrainingData', JSON.stringify({
trainings,
logo: document.getElementById('company-logo').src || ''
}));
}
function loadData() {
const savedData = localStorage.getItem('safetyTrainingData');
if (savedData) {
try {
const data = JSON.parse(savedData);
trainings = data.trainings || [];
if (data.logo) {
const logo = document.getElementById('company-logo');
logo.src = data.logo;
logo.classList.remove('hidden');
}
renderTrainings();
} catch (e) {
console.error('Error loading saved data:', e);
}
}
}
function exportData() {
const data = {
trainings,
logo: document.getElementById('company-logo').src || '',
exportedAt: new Date().toISOString()
};
const dataStr = JSON.stringify(data, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportName = 'safety-training-data_' + new Date().toISOString().slice(0, 10) + '.json';
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportName);
linkElement.click();
}
function importData(e) {
const file = e.target.files[0];
if (!file) return;
if (confirm('Importing data will replace all current data. Continue?')) {
const reader = new FileReader();
reader.onload = function(event) {
try {
const data = JSON.parse(event.target.result);
if (data.trainings) {
trainings = data.trainings;
if (data.logo) {
const logo = document.getElementById('company-logo');
logo.src = data.logo;
logo.classList.remove('hidden');
}
saveData();
renderTrainings();
alert('Data imported successfully!');
} else {
alert('Invalid data format: missing trainings array');
}
} catch (e) {
alert('Error parsing file: ' + e.message);
}
};
reader.readAsText(file);
}
// Reset input to allow importing the same file again
e.target.value = '';
}
function clearData() {
if (confirm('Are you sure you want to clear all data? This cannot be undone.')) {
trainings = [];
localStorage.removeItem('safetyTrainingData');
document.getElementById('company-logo').src = '';
document.getElementById('company-logo').classList.add('hidden');
renderTrainings();
resetForm();
}
}
});
// Global functions needed for event handlers in dynamically created elements
window.toggleTrainingDetails = function(id) {
const item = document.getElementById(id);
if (item) {
const details = item.querySelector('.hidden');
const icon = item.querySelector('.fa-chevron-down');
details.classList.toggle('hidden');
icon.classList.toggle('rotate-180');
}
};
window.editTraining = function(id) {
const event = new Event('DOMContentLoaded');
document.dispatchEvent(event);
};
window.deleteTraining = function(id) {
const event = new Event('DOMContentLoaded');
document.dispatchEvent(event);
};
</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=yanglops147/trial" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>