Spaces:
Configuration error
Configuration error
| ```javascript | |
| /** | |
| * UTILITY SYSTEM | |
| * Professional Helper Functions and Classes | |
| */ | |
| // Storage Manager | |
| class StorageManager { | |
| constructor(prefix = 'patrol_') { | |
| this.prefix = prefix; | |
| } | |
| set(key, value, expiry = null) { | |
| const item = { | |
| value: value, | |
| expiry: expiry ? Date.now() + expiry : null | |
| }; | |
| try { | |
| localStorage.setItem(this.prefix + key, JSON.stringify(item)); | |
| return true; | |
| } catch (error) { | |
| console.error('Storage set failed:', error); | |
| return false; | |
| } | |
| } | |
| get(key) { | |
| try { | |
| const itemStr = localStorage.getItem(this.prefix + key); | |
| if (!itemStr) return null; | |
| const item = JSON.parse(itemStr); | |
| // Check expiry | |
| if (item.expiry && Date.now() > item.expiry) { | |
| this.remove(key); | |
| return null; | |
| } | |
| return item.value; | |
| } catch (error) { | |
| console.error('Storage get failed:', error); | |
| return null; | |
| } | |
| } | |
| remove(key) { | |
| try { | |
| localStorage.removeItem(this.prefix + key); | |
| return true; | |
| } catch (error) { | |
| console.error('Storage remove failed:', error); | |
| return false; | |
| } | |
| } | |
| clear() { | |
| try { | |
| const keys = Object.keys(localStorage); | |
| keys.forEach(key => { | |
| if (key.startsWith(this.prefix)) { | |
| localStorage.removeItem(key); | |
| } | |
| }); | |
| return true; | |
| } catch (error) { | |
| console.error('Storage clear failed:', error); | |
| return false; | |
| } | |
| } | |
| getAll() { | |
| const items = {}; | |
| try { | |
| const keys = Object.keys(localStorage); | |
| keys.forEach(key => { | |
| if (key.startsWith(this.prefix)) { | |
| const cleanKey = key.replace(this.prefix, ''); | |
| items[cleanKey] = this.get(cleanKey); | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('Storage getAll failed:', error); | |
| } | |
| return items; | |
| } | |
| } | |
| // Event Bus | |
| class EventBus { | |
| constructor() { | |
| this.events = new Map(); | |
| } | |
| on(event, callback) { | |
| if (!this.events.has(event)) { | |
| this.events.set(event, new Set()); | |
| } | |
| this.events.get(event).add(callback); | |
| // Return unsubscribe function | |
| return () => this.off(event, callback); | |
| } | |
| off(event, callback) { | |
| const callbacks = this.events.get(event); | |
| if (callbacks) { | |
| callbacks.delete(callback); | |
| if (callbacks.size === 0) { | |
| this.events.delete(event); | |
| } | |
| } | |
| } | |
| emit(event, data = {}) { | |
| const callbacks = this.events.get(event); | |
| if (callbacks) { | |
| callbacks.forEach(callback => { | |
| try { | |
| callback(data); | |
| } catch (error) { | |
| console.error(`Event callback error for "${event}":`, error); | |
| } | |
| }); | |
| } | |
| } | |
| once(event, callback) { | |
| const onceCallback = (data) => { | |
| callback(data); | |
| this.off(event, onceCallback); | |
| }; | |
| return this.on(event, onceCallback); | |
| } | |
| clear() { | |
| this.events.clear(); | |
| } | |
| } | |
| // Notification Manager | |
| class NotificationManager { | |
| constructor() { | |
| this.container = null; | |
| this.notifications = new Map(); | |
| this.init(); | |
| } | |
| init() { | |
| this.container = document.createElement('div'); | |
| this.container.className = 'notification-container'; | |
| this.container.style.cssText = ` | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| z-index: 9999; | |
| pointer-events: none; | |
| `; | |
| document.body.appendChild(this.container); | |
| } | |
| show(message, type = 'info', options = {}) { | |
| const { | |
| duration = 5000, | |
| closable = true, | |
| actions = [] | |
| } = options; | |
| const id = Date.now() + Math.random(); | |
| const notification = this.createNotification(id, message, type, { | |
| duration, | |
| closable, | |
| actions | |
| }); | |
| this.notifications.set(id, notification); | |
| this.container.appendChild(notification); | |
| // Auto dismiss | |
| if (duration > 0) { | |
| setTimeout(() => this.dismiss(id), duration); | |
| } | |
| return id; | |
| } | |
| createNotification(id, message, type, options) { | |
| const notification = document.createElement('div'); | |
| notification.className = `notification notification-${type}`; | |
| notification.style.cssText = ` | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | |
| margin-bottom: 12px; | |
| padding: 16px; | |
| min-width: 300px; | |
| max-width: 500px; | |
| transform: translateX(100%); | |
| transition: transform 0.3s ease; | |
| pointer-events: auto; | |
| border-left: 4px solid ${this.getTypeColor(type)}; | |
| `; | |
| const icon = this.getTypeIcon(type); | |
| const content = ` | |
| <div style="display: flex; align-items: flex-start; gap: 12px;"> | |
| <div style="flex-shrink: 0; margin-top: 2px;"> | |
| ${icon} | |
| </div> | |
| <div style="flex: 1;"> | |
| <div style="font-weight: 500; margin-bottom: 4px;">${this.getTypeTitle(type)}</div> | |
| <div style="color: #666; line-height: 1.4;">${message}</div> | |
| ${options.actions.length > 0 ? this.createActions(options.actions, id) : ''} | |
| </div> | |
| ${options.closable ? ` | |
| <button onclick="window.patrolSystem.notifications.dismiss('${id}')" | |
| style="background: none; border: none; cursor: pointer; padding: 0; color: #999;"> | |
| × | |
| </button> | |
| ` : ''} | |
| </div> | |
| `; | |
| notification.innerHTML = content; | |
| // Animate in | |
| setTimeout(() => { | |
| notification.style.transform = 'translateX(0)'; | |
| }, 100); | |
| return notification; | |
| } | |
| createActions(actions, notificationId) { | |
| const buttons = actions.map(action => ` | |
| <button onclick="${action.handler}(${notificationId})" | |
| style="background: ${action.primary ? '#2563eb' : 'transparent'}; | |
| color: ${action.primary ? 'white' : '#2563eb'}; | |
| border: 1px solid #2563eb; | |
| padding: 4px 12px; | |
| border-radius: 4px; | |
| margin-right: 8px; | |
| cursor: pointer; | |
| font-size: 12px;"> | |
| ${action.label} | |
| </button> | |
| `).join(''); | |
| return `<div style="margin-top: 8px;">${buttons}</div>`; | |
| } | |
| dismiss(id) { | |
| const notification = this.notifications.get(id); | |
| if (notification) { | |
| notification.style.transform = 'translateX(100%)'; | |
| setTimeout(() => { | |
| if (notification.parentNode) { | |
| notification.parentNode.removeChild(notification); | |
| } | |
| this.notifications.delete(id); | |
| }, 300); | |
| } | |
| } | |
| success(message, options = {}) { | |
| return this.show(message, 'success', options); | |
| } | |
| error(message, options = {}) { | |
| return this.show(message, 'error', { ...options, duration: 8000 }); | |
| } | |
| warning(message, options = {}) { | |
| return this.show(message, 'warning', { ...options, duration: 6000 }); | |
| } | |
| info(message, options = {}) { | |
| return this.show(message, 'info', options); | |
| } | |
| getTypeColor(type) { | |
| const colors = { | |
| success: '#10b981', | |
| error: '#ef4444', | |
| warning: '#f59e0b', | |
| info: '#3b82f6' | |
| }; | |
| return colors[type] || colors.info; | |
| } | |
| getTypeIcon(type) { | |
| const icons = { | |
| success: '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.061L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/></svg>', | |
| error: '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="m7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/></svg>', | |
| warning: '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566ZM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg>', | |
| info: '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/></svg>' | |
| }; | |
| return icons[type] || icons.info; | |
| } | |
| getTypeTitle(type) { | |
| const titles = { | |
| success: 'Succès', | |
| error: 'Erreur', | |
| warning: 'Attention', | |
| info: 'Information' | |
| }; | |
| return titles[type] || titles.info; | |
| } | |
| } | |
| // Theme Manager | |
| class ThemeManager { | |
| constructor() { | |
| this.currentTheme = 'light'; | |
| this.storage = new StorageManager(); | |
| this.init(); | |
| } | |
| init() { | |
| const saved = this.storage.get('theme'); | |
| if (saved) { | |
| this.currentTheme = saved; | |
| } | |
| this.apply(); | |
| } | |
| toggle() { | |
| this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light'; | |
| this.apply(); | |
| this.storage.set('theme', this.currentTheme); | |
| } | |
| apply() { | |
| const body = document.body; | |
| body.className = body.className.replace(/theme-\w+/g, ''); | |
| body.classList.add(`theme-${this.currentTheme}`); | |
| } | |
| } | |
| // Form Validator | |
| class FormValidator { | |
| constructor() { | |
| this.rules = { | |
| required: (value) => value !== null && value !== undefined && value.toString().trim() !== '', | |
| email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), | |
| min: (value, min) => value.length >= min, | |
| max: (value, max) => value.length <= max, | |
| pattern: (value, pattern) => new RegExp(pattern).test(value), | |
| phone: (value) => /^\+?[\d\s\-\(\)]+$/.test(value) | |
| }; | |
| } | |
| validate(formData, rules) { | |
| const errors = {}; | |
| for (const field in rules) { | |
| const fieldRules = rules[field]; | |
| const value = formData[field]; | |
| for (const rule of fieldRules) { | |
| const { type, value: ruleValue, message } = rule; | |
| const validator = this.rules[type]; | |
| if (validator && !validator(value, ruleValue)) { | |
| if (!errors[field]) { | |
| errors[field] = []; | |
| } | |
| errors[field].push(message || `Champ invalide: ${type}`); | |
| } | |
| } | |
| } | |
| return { | |
| isValid: Object.keys(errors).length === 0, | |
| errors | |
| }; | |
| } | |
| validateField(value, rules) { | |
| for (const rule of rules) { | |
| const { type, value: ruleValue, message } = rule; | |
| const validator = this.rules[type]; | |
| if (validator && !validator(value, ruleValue)) { | |
| return { | |
| isValid: false, | |
| message: message || `Champ invalide: ${type}` | |
| }; | |
| } | |
| } | |
| return { isValid: true }; | |
| } | |
| } | |
| // Modal Manager | |
| class ModalManager { | |
| constructor() { | |
| this.modals = new Map(); | |
| this.init(); | |
| } | |
| init() { | |
| this.container = document.createElement('div'); | |
| this.container.className = 'modal-container'; | |
| this.container.style.cssText = ` | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.5); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 10000; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: all 0.3s ease; | |
| `; | |
| document.body.appendChild(this.container); | |
| } | |
| open(id, options = {}) { | |
| const modal = this.createModal(id, options); | |
| this.modals.set(id, modal); | |
| this.container.appendChild(modal); | |
| setTimeout(() => { | |
| modal.classList.add('active'); | |
| this.container.classList.add('active'); | |
| }, 10); | |
| // Focus first input if exists | |
| const firstInput = modal.querySelector('input, textarea, select'); | |
| if (firstInput) { | |
| firstInput.focus(); | |
| } | |
| } | |
| close(id) { | |
| const modal = this.modals.get(id); | |
| if (modal) { | |
| modal.classList.remove('active'); | |
| this.container.classList.remove('active'); | |
| setTimeout(() => { | |
| if (modal.parentNode) { | |
| modal.parentNode.removeChild(modal); | |
| } | |
| this.modals.delete(id); | |
| }, 300); | |
| } | |
| } | |
| createModal(id, options) { | |
| const modal = document.createElement('div'); | |
| modal.className = 'modal'; | |
| modal.style.cssText = ` | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); | |
| max-width: ${options.maxWidth || '500px'}; | |
| width: 90vw; | |
| max-height: 90vh; | |
| overflow: auto; | |
| transform: translateY(-20px); | |
| transition: transform 0.3s ease; | |
| `; | |
| const header = options.title ? ` | |
| <div class="modal-header" style="padding: 20px; border-bottom: 1px solid #e5e7eb;"> | |
| <h3 style="margin: 0; font-size: 18px; font-weight: 600;">${options.title}</h3> | |
| <button onclick="window.patrolSystem.modalManager.close('${id}')" | |
| style="position: absolute; top: 15px; right: 15px; background: none; border: none; cursor: pointer; font-size: 20px; color: #9ca3af;"> | |
| × | |
| </button> | |
| </div> | |
| ` : ''; | |
| const content = ` | |
| <div class="modal-content" style="padding: 20px;"> | |
| ${options.content || ''} | |
| </div> | |
| `; | |
| const footer = options.actions ? ` | |
| <div class="modal-footer" style="padding: 20px; border-top: 1px solid #e5e7eb; display: flex; justify-content: flex-end; gap: 12px;"> | |
| ${options.actions.map(action => ` | |
| <button onclick="${action.handler}('${id}')" | |
| style="padding: 8px 16px; border-radius: 4px; border: 1px solid ${action.primary ? '#2563eb' : '#d1d5db'}; | |
| background: ${action.primary ? '#2563eb' : 'white'}; | |
| color: ${action.primary ? 'white' : '#374151'}; | |
| cursor: pointer;"> | |
| ${action.label} | |
| </button> | |
| `).join('')} | |
| </div> | |
| ` : ''; | |
| modal.innerHTML = header + content + footer; | |
| return modal; | |
| } | |
| } | |
| // Utility Functions | |
| const Utils = { | |
| // Date formatting | |
| formatDate(date, format = 'fr-FR') { | |
| return new Intl.DateTimeFormat(format, { | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric' | |
| }).format(new Date(date)); | |
| }, | |
| formatDateTime(date, format = 'fr-FR') { | |
| return new Intl.DateTimeFormat(format, { | |
| year: 'numeric', | |
| month: 'short', | |
| day: 'numeric', | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }).format(new Date(date)); | |
| }, | |
| // Number formatting | |
| formatNumber(number, decimals = 0) { | |
| return new Intl.NumberFormat('fr-FR', { | |
| minimumFractionDigits: decimals, | |
| maximumFractionDigits: decimals | |
| }).format(number); | |
| }, | |
| // File utilities | |
| formatFileSize(bytes) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| }, | |
| // String utilities | |
| truncate(str, length = 50) { | |
| return str.length > length ? str.substring(0, length) + '...' : str; | |
| }, | |
| slugify(str) { | |
| return str.toLowerCase() | |
| .replace(/[^\w\s-]/g, '') | |
| .replace(/[\s_-]+/g, '-') | |
| .replace(/^-+|-+$/g, ''); | |
| }, | |
| // Array utilities | |
| chunk(array, size) { | |
| const chunks = []; | |
| for (let i = 0; i < array.length; i += size) { | |
| chunks.push(array.slice(i, i + size)); | |
| } | |
| return chunks; | |
| }, | |
| // Object utilities | |
| deepClone(obj) { | |
| return JSON.parse(JSON.stringify(obj)); | |
| }, | |
| isEmpty(obj) { | |
| return Object.keys(obj).length === | |
| ___METADATA_START___ | |
| {"repoId":"secutorpro/patrol-control-system","isNew":false,"userName":"secutorpro"} | |
| ___METADATA_END___ |