```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 = `
${icon}
${this.getTypeTitle(type)}
${message}
${options.actions.length > 0 ? this.createActions(options.actions, id) : ''}
${options.closable ? `
` : ''}
`;
notification.innerHTML = content;
// Animate in
setTimeout(() => {
notification.style.transform = 'translateX(0)';
}, 100);
return notification;
}
createActions(actions, notificationId) {
const buttons = actions.map(action => `
`).join('');
return `${buttons}
`;
}
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: '',
error: '',
warning: '',
info: ''
};
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 ? `
` : '';
const content = `
${options.content || ''}
`;
const footer = options.actions ? `
` : '';
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___