All checks were successful
Build and Push Image / build-and-push (push) Successful in 1m26s
573 lines
19 KiB
JavaScript
573 lines
19 KiB
JavaScript
// Snauw Counter - Frontend JavaScript
|
|
|
|
// Global variables
|
|
let selectedSeverity = null;
|
|
|
|
// Fast datetime initialization - runs as soon as script loads
|
|
function fastDateTimeInit() {
|
|
const dateTimeInput = document.getElementById('incident_time');
|
|
if (dateTimeInput) {
|
|
const now = new Date();
|
|
const formattedDateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}T${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
dateTimeInput.value = formattedDateTime;
|
|
}
|
|
}
|
|
|
|
// Try to initialize datetime immediately if elements exist
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', fastDateTimeInit);
|
|
} else {
|
|
fastDateTimeInit();
|
|
}
|
|
|
|
// Initialize app when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize date/time FIRST - before any user interaction
|
|
initializeDateTime();
|
|
trackDateTimeModification();
|
|
|
|
// Then initialize other components
|
|
initializeSeveritySelector();
|
|
initializeAnonymousUserSupport();
|
|
loadChart();
|
|
updateHeaderCounter();
|
|
|
|
// Hide privacy notice als gebruiker het al eerder heeft weggedaan
|
|
if (localStorage.getItem('privacy-notice-hidden')) {
|
|
const notice = document.getElementById('privacy-notice');
|
|
if (notice) notice.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Initialize date/time fields with current date and time
|
|
function initializeDateTime() {
|
|
const dateTimeInput = document.getElementById('incident_time');
|
|
if (dateTimeInput) {
|
|
// Get current date and time
|
|
const now = new Date();
|
|
|
|
// Format to datetime-local format (YYYY-MM-DDTHH:mm)
|
|
const year = now.getFullYear();
|
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
const day = String(now.getDate()).padStart(2, '0');
|
|
const hours = String(now.getHours()).padStart(2, '0');
|
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
|
|
const formattedDateTime = `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
dateTimeInput.value = formattedDateTime;
|
|
|
|
// Add helper text
|
|
const helpText = dateTimeInput.nextElementSibling;
|
|
if (helpText && helpText.tagName === 'SMALL') {
|
|
helpText.innerHTML = `<span style="color: #28a745;">✓ Automatisch ingevuld: ${formatDisplayTime(now)}</span>`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Format time for display
|
|
function formatDisplayTime(date) {
|
|
return date.toLocaleString('nl-NL', {
|
|
weekday: 'short',
|
|
day: 'numeric',
|
|
month: 'short',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
// Update date/time when page becomes visible (for accuracy)
|
|
document.addEventListener('visibilitychange', function() {
|
|
if (!document.hidden) {
|
|
// Only update if the field hasn't been manually changed
|
|
const dateTimeInput = document.getElementById('incident_time');
|
|
if (dateTimeInput && !dateTimeInput.dataset.userModified) {
|
|
initializeDateTime();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Track if user manually modifies the datetime field
|
|
function trackDateTimeModification() {
|
|
const dateTimeInput = document.getElementById('incident_time');
|
|
if (dateTimeInput) {
|
|
dateTimeInput.addEventListener('input', function() {
|
|
this.dataset.userModified = 'true';
|
|
const helpText = this.nextElementSibling;
|
|
if (helpText && helpText.tagName === 'SMALL') {
|
|
helpText.innerHTML = '<span style="color: #6c757d;">Handmatig aangepast</span>';
|
|
}
|
|
});
|
|
|
|
// Add reset button
|
|
const resetBtn = document.createElement('button');
|
|
resetBtn.type = 'button';
|
|
resetBtn.className = 'btn btn-secondary';
|
|
resetBtn.style.marginLeft = '0.5rem';
|
|
resetBtn.style.padding = '0.25rem 0.5rem';
|
|
resetBtn.style.fontSize = '0.8rem';
|
|
resetBtn.innerHTML = '🔄 Nu';
|
|
resetBtn.title = 'Reset naar huidige tijd';
|
|
resetBtn.onclick = function() {
|
|
delete dateTimeInput.dataset.userModified;
|
|
initializeDateTime();
|
|
};
|
|
|
|
// Only add reset button if it doesn't exist yet
|
|
if (!dateTimeInput.parentNode.querySelector('.btn.btn-secondary')) {
|
|
dateTimeInput.parentNode.insertBefore(resetBtn, dateTimeInput.nextSibling);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update date/time when page becomes visible (for accuracy)
|
|
document.addEventListener('visibilitychange', function() {
|
|
if (!document.hidden) {
|
|
// Only update if the field hasn't been manually changed
|
|
const dateTimeInput = document.getElementById('incident_time');
|
|
if (dateTimeInput && !dateTimeInput.dataset.userModified) {
|
|
initializeDateTime();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Quick add incident functionality
|
|
function quickAddIncident() {
|
|
// Check if we're already on add incident page
|
|
if (window.location.pathname.includes('/add')) {
|
|
return;
|
|
}
|
|
|
|
// For anonymous users, show quick add modal
|
|
if (!document.body.dataset.isAuthenticated) {
|
|
showQuickAddModal();
|
|
} else {
|
|
// Redirect to add incident page
|
|
window.location.href = '/add-incident';
|
|
}
|
|
}
|
|
|
|
// Show quick add modal for anonymous users
|
|
function showQuickAddModal() {
|
|
const modal = document.createElement('div');
|
|
modal.className = 'modal';
|
|
modal.innerHTML = `
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3>🔥 Snel incident toevoegen</h3>
|
|
<button onclick="closeModal()" class="close-btn">✕</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form onsubmit="submitQuickIncident(event)">
|
|
<div class="form-group">
|
|
<label>Snauw-index (1-5):</label>
|
|
<div class="severity-selector">
|
|
${getSnauwIndexButtons()}
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="quick-notes">Opmerkingen (optioneel):</label>
|
|
<textarea id="quick-notes" placeholder="Wat gebeurde er?"></textarea>
|
|
</div>
|
|
<button type="submit" class="btn">📝 Opslaan</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
// Initialize severity selector for modal
|
|
initializeSeveritySelector();
|
|
|
|
// Add modal styles
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.modal {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 2000;
|
|
padding: 1rem;
|
|
}
|
|
.modal-content {
|
|
background: white;
|
|
border-radius: 12px;
|
|
max-width: 500px;
|
|
width: 100%;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
}
|
|
.modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem 1.5rem;
|
|
border-bottom: 1px solid #e9ecef;
|
|
}
|
|
.modal-body {
|
|
padding: 1.5rem;
|
|
}
|
|
.close-btn {
|
|
background: none;
|
|
border: none;
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
color: #6c757d;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
// Initialize severity selector buttons
|
|
function initializeSeveritySelector() {
|
|
const severityButtons = document.querySelectorAll('.severity-btn');
|
|
const severityInput = document.getElementById('severity-input');
|
|
|
|
severityButtons.forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
// Remove active class from all buttons
|
|
severityButtons.forEach(btn => btn.classList.remove('active'));
|
|
|
|
// Add active class to clicked button
|
|
this.classList.add('active');
|
|
|
|
// Update selected severity
|
|
selectedSeverity = parseInt(this.dataset.severity);
|
|
|
|
// Update hidden input value if it exists
|
|
if (severityInput) {
|
|
severityInput.value = selectedSeverity;
|
|
}
|
|
|
|
console.log(`Selected severity: ${selectedSeverity}`);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Update header counter with total incidents
|
|
function updateHeaderCounter() {
|
|
const counterDigit1 = document.getElementById('counter-digit-1');
|
|
const counterDigit2 = document.getElementById('counter-digit-2');
|
|
const counterDigit3 = document.getElementById('counter-digit-3');
|
|
|
|
// Also update the old header-counter for backwards compatibility
|
|
const counterElement = document.getElementById('header-counter');
|
|
|
|
if (!counterDigit1 && !counterElement) return;
|
|
|
|
let totalIncidents = 0;
|
|
|
|
if (document.body.dataset.isAuthenticated === 'true') {
|
|
// For authenticated users - would normally fetch from API
|
|
// For now, we'll use a placeholder
|
|
totalIncidents = 0;
|
|
} else {
|
|
// For anonymous users - use localStorage
|
|
const incidents = JSON.parse(localStorage.getItem('snauw_incidents') || '[]');
|
|
totalIncidents = incidents.length;
|
|
}
|
|
|
|
// Format number with leading zeros
|
|
const formattedCount = totalIncidents.toString().padStart(3, '0');
|
|
|
|
// Update individual digits if they exist (new banner)
|
|
if (counterDigit1 && counterDigit2 && counterDigit3) {
|
|
counterDigit1.textContent = formattedCount[0];
|
|
counterDigit2.textContent = formattedCount[1];
|
|
counterDigit3.textContent = formattedCount[2];
|
|
|
|
// Add pulsing animation for new incidents
|
|
if (totalIncidents > 0) {
|
|
[counterDigit1, counterDigit2, counterDigit3].forEach(digit => {
|
|
digit.style.animation = 'pulse 2s ease-in-out infinite';
|
|
});
|
|
}
|
|
}
|
|
|
|
// Update old counter element if it exists (backwards compatibility)
|
|
if (counterElement) {
|
|
counterElement.textContent = formattedCount;
|
|
|
|
// Add pulsing animation for new incidents
|
|
if (totalIncidents > 0) {
|
|
counterElement.style.animation = 'pulse 2s ease-in-out infinite';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate Snauw-index buttons with descriptions
|
|
function getSnauwIndexButtons() {
|
|
const descriptions = [
|
|
'Lichte correctie',
|
|
'Geprikkelde waarschuwing',
|
|
'Openlijke snauw',
|
|
'Bijtende aanval',
|
|
'Explosieve uitval'
|
|
];
|
|
|
|
return [...Array(5)].map((_, i) => `
|
|
<button type="button" class="severity-btn" data-severity="${i+1}" title="Fase ${i+1}: ${descriptions[i]}">
|
|
${i+1}
|
|
</button>
|
|
`).join('');
|
|
}
|
|
|
|
// Close modal
|
|
function closeModal() {
|
|
const modal = document.querySelector('.modal');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
}
|
|
|
|
// Submit quick incident for anonymous users
|
|
async function submitQuickIncident(event) {
|
|
event.preventDefault();
|
|
|
|
if (!selectedSeverity) {
|
|
alert('Selecteer een Snauw-index');
|
|
return;
|
|
}
|
|
|
|
const notes = document.getElementById('quick-notes').value;
|
|
const now = new Date();
|
|
const incident = {
|
|
severity: selectedSeverity,
|
|
notes: notes,
|
|
timestamp: now.toISOString()
|
|
};
|
|
|
|
// Store in localStorage for anonymous users
|
|
let incidents = JSON.parse(localStorage.getItem('snauw_incidents') || '[]');
|
|
incident.id = Date.now(); // Simple ID for local storage
|
|
incidents.push(incident);
|
|
localStorage.setItem('snauw_incidents', JSON.stringify(incidents));
|
|
|
|
// Update header counter
|
|
updateHeaderCounter();
|
|
|
|
// Add animation to counter
|
|
const counterElement = document.getElementById('header-counter');
|
|
if (counterElement) {
|
|
counterElement.classList.add('updated');
|
|
setTimeout(() => counterElement.classList.remove('updated'), 3000);
|
|
}
|
|
|
|
// Show success message
|
|
showNotification('✅ Incident opgeslagen!', 'success');
|
|
|
|
// Close modal
|
|
closeModal();
|
|
|
|
// Refresh statistics if on stats page
|
|
if (window.location.pathname.includes('statistics')) {
|
|
location.reload();
|
|
}
|
|
}
|
|
|
|
// Show notification
|
|
function showNotification(message, type = 'info') {
|
|
const notification = document.createElement('div');
|
|
notification.className = `alert alert-${type}`;
|
|
notification.textContent = message;
|
|
notification.style.position = 'fixed';
|
|
notification.style.top = '2rem';
|
|
notification.style.right = '2rem';
|
|
notification.style.zIndex = '3000';
|
|
notification.style.minWidth = '250px';
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 3000);
|
|
}
|
|
|
|
// Anonymous user support
|
|
function initializeAnonymousUserSupport() {
|
|
// Check if user has incidents in localStorage
|
|
const localIncidents = localStorage.getItem('snauw_incidents');
|
|
if (localIncidents && !document.body.dataset.isAuthenticated) {
|
|
const incidents = JSON.parse(localIncidents);
|
|
if (incidents.length > 0) {
|
|
showMigrationOffer();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show migration offer to anonymous users with data
|
|
function showMigrationOffer() {
|
|
const incidents = JSON.parse(localStorage.getItem('snauw_incidents') || '[]');
|
|
if (incidents.length < 3) return; // Only show after a few incidents
|
|
|
|
const lastOffer = localStorage.getItem('migration-offer-shown');
|
|
const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
|
|
|
|
if (lastOffer && parseInt(lastOffer) > oneWeekAgo) return; // Don't show too often
|
|
|
|
const banner = document.createElement('div');
|
|
banner.className = 'migration-banner';
|
|
banner.innerHTML = `
|
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
|
|
<strong>💾 Je hebt ${incidents.length} incidenten opgeslagen!</strong><br>
|
|
Maak een account om je gegevens veilig te bewaren en op meerdere apparaten te gebruiken.
|
|
<div style="margin-top: 0.5rem;">
|
|
<a href="/register" class="btn btn-success" style="display: inline-block; margin-right: 0.5rem;">Account maken</a>
|
|
<button onclick="hideMigrationBanner()" class="btn btn-secondary" style="display: inline-block;">Later</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const main = document.querySelector('main.container');
|
|
main.insertBefore(banner, main.firstChild);
|
|
|
|
localStorage.setItem('migration-offer-shown', Date.now().toString());
|
|
}
|
|
|
|
// Hide migration banner
|
|
function hideMigrationBanner() {
|
|
const banner = document.querySelector('.migration-banner');
|
|
if (banner) banner.remove();
|
|
}
|
|
|
|
// Hide privacy notice
|
|
function hidePrivacyNotice() {
|
|
const notice = document.getElementById('privacy-notice');
|
|
if (notice) {
|
|
notice.style.display = 'none';
|
|
localStorage.setItem('privacy-notice-hidden', 'true');
|
|
}
|
|
}
|
|
|
|
// Chart functionality
|
|
function loadChart() {
|
|
const chartCanvas = document.getElementById('incidentsChart');
|
|
if (!chartCanvas) return;
|
|
|
|
// For anonymous users, use localStorage data
|
|
if (!document.body.dataset.isAuthenticated) {
|
|
loadAnonymousChart();
|
|
} else {
|
|
loadUserChart();
|
|
}
|
|
}
|
|
|
|
// Load chart for anonymous users
|
|
function loadAnonymousChart() {
|
|
const incidents = JSON.parse(localStorage.getItem('snauw_incidents') || '[]');
|
|
|
|
// Group by date
|
|
const dateGroups = {};
|
|
incidents.forEach(incident => {
|
|
const date = new Date(incident.timestamp).toDateString();
|
|
if (!dateGroups[date]) {
|
|
dateGroups[date] = [];
|
|
}
|
|
dateGroups[date].push(incident);
|
|
});
|
|
|
|
// Prepare chart data
|
|
const dates = Object.keys(dateGroups).slice(-7); // Last 7 days
|
|
const counts = dates.map(date => dateGroups[date].length);
|
|
const averages = dates.map(date => {
|
|
const dayIncidents = dateGroups[date];
|
|
const sum = dayIncidents.reduce((acc, inc) => acc + inc.severity, 0);
|
|
return sum / dayIncidents.length || 0;
|
|
});
|
|
|
|
renderChart(dates, counts, averages);
|
|
}
|
|
|
|
// Load chart for authenticated users (would fetch from server)
|
|
function loadUserChart() {
|
|
// This would make an API call to get user's incident data
|
|
// For now, placeholder
|
|
renderChart([], [], []);
|
|
}
|
|
|
|
// Render chart using Chart.js (simplified version without external dependency)
|
|
function renderChart(dates, counts, averages) {
|
|
const canvas = document.getElementById('incidentsChart');
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Simple bar chart implementation
|
|
// In a real implementation, you'd use Chart.js library
|
|
ctx.fillStyle = '#667eea';
|
|
ctx.fillRect(10, 10, 50, 100);
|
|
ctx.fillText('Chart placeholder - Use Chart.js in production', 10, 130);
|
|
}
|
|
|
|
// Form validation
|
|
function validateIncidentForm() {
|
|
const severity = document.getElementById('severity-input');
|
|
if (!severity || !severity.value) {
|
|
alert('Selecteer een Snauw-index');
|
|
return false;
|
|
}
|
|
|
|
const severityValue = parseInt(severity.value);
|
|
if (severityValue < 1 || severityValue > 5) {
|
|
alert('Snauw-index moet tussen 1 en 5 zijn');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Export data (for GDPR compliance)
|
|
function exportData() {
|
|
let data;
|
|
|
|
if (document.body.dataset.isAuthenticated) {
|
|
// Would fetch from server for authenticated users
|
|
alert('Export functionaliteit wordt nog geïmplementeerd voor geregistreerde gebruikers');
|
|
return;
|
|
} else {
|
|
// Export localStorage data for anonymous users
|
|
data = {
|
|
incidents: JSON.parse(localStorage.getItem('snauw_incidents') || '[]'),
|
|
exportDate: new Date().toISOString(),
|
|
type: 'anonymous_user_data'
|
|
};
|
|
}
|
|
|
|
const dataStr = JSON.stringify(data, null, 2);
|
|
const dataBlob = new Blob([dataStr], {type: 'application/json'});
|
|
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(dataBlob);
|
|
link.download = `snauw_counter_export_${new Date().toISOString().split('T')[0]}.json`;
|
|
link.click();
|
|
}
|
|
|
|
// Delete all data (for GDPR compliance)
|
|
function deleteAllData() {
|
|
if (!confirm('Weet je zeker dat je alle gegevens wilt verwijderen? Dit kan niet ongedaan worden gemaakt.')) {
|
|
return;
|
|
}
|
|
|
|
if (document.body.dataset.isAuthenticated) {
|
|
// Would make API call for authenticated users
|
|
alert('Data verwijdering voor geregistreerde gebruikers wordt nog geïmplementeerd');
|
|
return;
|
|
} else {
|
|
// Clear localStorage for anonymous users
|
|
localStorage.removeItem('snauw_incidents');
|
|
localStorage.removeItem('privacy-notice-hidden');
|
|
localStorage.removeItem('migration-offer-shown');
|
|
|
|
showNotification('✅ Alle lokale gegevens zijn verwijderd', 'success');
|
|
|
|
setTimeout(() => {
|
|
location.reload();
|
|
}, 2000);
|
|
}
|
|
}
|