snauwcounter/app/static/js/app.js
Michael Trip b56e866071
All checks were successful
Build and Push Image / build-and-push (push) Successful in 1m26s
initial commit
2026-01-09 21:58:53 +01:00

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);
}
}