Working state before icon/utils fixes - Oct 22

This commit is contained in:
2025-10-21 21:56:54 +02:00
parent a7d9d44a13
commit 5be47b91a2
39 changed files with 6017 additions and 508 deletions

View File

@@ -1,4 +1,4 @@
// static/js/shared/api-client.js
// static/shared/js/api-client.js
/**
* API Client for Multi-Tenant Ecommerce Platform
*
@@ -9,6 +9,16 @@
* - Request/response interceptors
*/
// Log levels: 0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug
const API_LOG_LEVEL = 3; // Set to 3 for production, 4 for full debugging
const apiLog = {
error: (...args) => API_LOG_LEVEL >= 1 && console.error('❌ [API ERROR]', ...args),
warn: (...args) => API_LOG_LEVEL >= 2 && console.warn('⚠️ [API WARN]', ...args),
info: (...args) => API_LOG_LEVEL >= 3 && console.info(' [API INFO]', ...args),
debug: (...args) => API_LOG_LEVEL >= 4 && console.log('🔍 [API DEBUG]', ...args)
};
const API_BASE_URL = '/api/v1';
/**
@@ -17,13 +27,24 @@ const API_BASE_URL = '/api/v1';
class APIClient {
constructor(baseURL = API_BASE_URL) {
this.baseURL = baseURL;
apiLog.info('API Client initialized with base URL:', baseURL);
}
/**
* Get stored authentication token
*/
getToken() {
return localStorage.getItem('admin_token') || localStorage.getItem('vendor_token');
const adminToken = localStorage.getItem('admin_token');
const vendorToken = localStorage.getItem('vendor_token');
const token = adminToken || vendorToken;
apiLog.debug('Getting token:', {
hasAdminToken: !!adminToken,
hasVendorToken: !!vendorToken,
usingToken: token ? 'admin or vendor' : 'none'
});
return token;
}
/**
@@ -38,6 +59,9 @@ class APIClient {
const token = this.getToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
apiLog.debug('Authorization header added');
} else {
apiLog.debug('No token available, request will be unauthenticated');
}
return headers;
@@ -48,6 +72,14 @@ class APIClient {
*/
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const method = options.method || 'GET';
apiLog.info(`${method} ${url}`);
apiLog.debug('Request options:', {
method,
hasBody: !!options.body,
customHeaders: Object.keys(options.headers || {})
});
const config = {
...options,
@@ -55,26 +87,61 @@ class APIClient {
};
try {
const startTime = Date.now();
const response = await fetch(url, config);
const duration = Date.now() - startTime;
// Handle 401 Unauthorized
if (response.status === 401) {
this.handleUnauthorized();
throw new Error('Unauthorized - please login again');
}
apiLog.info(`Response: ${response.status} ${response.statusText} (${duration}ms)`);
// Parse response
const data = await response.json();
let data;
try {
data = await response.json();
apiLog.debug('Response data received:', {
hasData: !!data,
dataType: typeof data,
keys: data ? Object.keys(data) : []
});
} catch (parseError) {
apiLog.error('Failed to parse JSON response:', parseError);
throw new Error('Invalid JSON response from server');
}
// Handle 401 Unauthorized - Just clear tokens, DON'T redirect
if (response.status === 401) {
apiLog.warn('401 Unauthorized - Authentication failed');
apiLog.debug('Error details:', data);
apiLog.info('Clearing authentication tokens');
this.clearTokens();
const errorMessage = data.message || data.detail || 'Unauthorized - please login again';
apiLog.error('Throwing authentication error:', errorMessage);
throw new Error(errorMessage);
}
// Handle non-OK responses
if (!response.ok) {
throw new Error(data.detail || data.message || 'Request failed');
const errorMessage = data.detail || data.message || `Request failed with status ${response.status}`;
apiLog.error('Request failed:', {
status: response.status,
message: errorMessage,
errorCode: data.error_code
});
throw new Error(errorMessage);
}
apiLog.info('Request completed successfully');
return data;
} catch (error) {
console.error('API request failed:', error);
// Log error details
if (error.name === 'TypeError' && error.message.includes('fetch')) {
apiLog.error('Network error - Failed to connect to server');
} else {
apiLog.error('Request error:', error.message);
}
apiLog.debug('Full error:', error);
throw error;
}
}
@@ -86,6 +153,8 @@ class APIClient {
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
apiLog.debug('GET request params:', params);
return this.request(url, {
method: 'GET'
});
@@ -95,6 +164,11 @@ class APIClient {
* POST request
*/
async post(endpoint, data = {}) {
apiLog.debug('POST request data:', {
hasData: !!data,
dataKeys: Object.keys(data)
});
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
@@ -105,6 +179,11 @@ class APIClient {
* PUT request
*/
async put(endpoint, data = {}) {
apiLog.debug('PUT request data:', {
hasData: !!data,
dataKeys: Object.keys(data)
});
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
@@ -115,31 +194,59 @@ class APIClient {
* DELETE request
*/
async delete(endpoint) {
apiLog.debug('DELETE request');
return this.request(endpoint, {
method: 'DELETE'
});
}
/**
* Handle unauthorized access
* Clear authentication tokens
*/
handleUnauthorized() {
clearTokens() {
apiLog.info('Clearing all authentication tokens...');
const tokensBefore = {
admin_token: !!localStorage.getItem('admin_token'),
admin_user: !!localStorage.getItem('admin_user'),
vendor_token: !!localStorage.getItem('vendor_token'),
vendor_user: !!localStorage.getItem('vendor_user'),
token: !!localStorage.getItem('token')
};
apiLog.debug('Tokens before clear:', tokensBefore);
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_user');
localStorage.removeItem('vendor_token');
localStorage.removeItem('vendor_user');
localStorage.removeItem('token');
// Redirect to appropriate login page
if (window.location.pathname.includes('/admin/')) {
window.location.href = '/static/admin/login.html';
} else if (window.location.pathname.includes('/vendor/')) {
window.location.href = '/static/vendor/login.html';
}
const tokensAfter = {
admin_token: !!localStorage.getItem('admin_token'),
admin_user: !!localStorage.getItem('admin_user'),
vendor_token: !!localStorage.getItem('vendor_token'),
vendor_user: !!localStorage.getItem('vendor_user'),
token: !!localStorage.getItem('token')
};
apiLog.debug('Tokens after clear:', tokensAfter);
apiLog.info('All tokens cleared');
}
/**
* Handle unauthorized access
* DEPRECATED - Now just clears tokens, doesn't redirect
* Server handles redirects via exception handler
*/
handleUnauthorized() {
apiLog.warn('handleUnauthorized called (DEPRECATED) - use clearTokens instead');
this.clearTokens();
}
}
// Create global API client instance
const apiClient = new APIClient();
apiLog.info('Global API client created');
/**
* Authentication helpers
@@ -150,7 +257,9 @@ const Auth = {
*/
isAuthenticated() {
const token = localStorage.getItem('admin_token') || localStorage.getItem('vendor_token');
return !!token;
const isAuth = !!token;
apiLog.debug('Auth check:', isAuth ? 'authenticated' : 'not authenticated');
return isAuth;
},
/**
@@ -158,11 +267,21 @@ const Auth = {
*/
getCurrentUser() {
const userStr = localStorage.getItem('admin_user') || localStorage.getItem('vendor_user');
if (!userStr) return null;
if (!userStr) {
apiLog.debug('No user found in storage');
return null;
}
try {
return JSON.parse(userStr);
const user = JSON.parse(userStr);
apiLog.debug('Current user:', {
username: user.username,
role: user.role,
id: user.id
});
return user;
} catch (e) {
apiLog.error('Failed to parse user data:', e);
return null;
}
},
@@ -172,13 +291,16 @@ const Auth = {
*/
isAdmin() {
const user = this.getCurrentUser();
return user && user.role === 'admin';
const isAdmin = user && user.role === 'admin';
apiLog.debug('Admin check:', isAdmin ? 'is admin' : 'not admin');
return isAdmin;
},
/**
* Login
*/
async login(username, password) {
apiLog.info('Auth.login called');
const response = await apiClient.post('/auth/login', {
username,
password
@@ -186,9 +308,11 @@ const Auth = {
// Store token and user
if (response.user.role === 'admin') {
apiLog.info('Storing admin credentials');
localStorage.setItem('admin_token', response.access_token);
localStorage.setItem('admin_user', JSON.stringify(response.user));
} else {
apiLog.info('Storing vendor credentials');
localStorage.setItem('vendor_token', response.access_token);
localStorage.setItem('vendor_user', JSON.stringify(response.user));
}
@@ -200,10 +324,9 @@ const Auth = {
* Logout
*/
logout() {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_user');
localStorage.removeItem('vendor_token');
localStorage.removeItem('vendor_user');
apiLog.info('Auth.logout called');
apiClient.clearTokens();
apiLog.info('User logged out');
}
};
@@ -269,6 +392,8 @@ const Utils = {
* Show toast notification
*/
showToast(message, type = 'info', duration = 3000) {
apiLog.debug('Showing toast:', { message, type, duration });
// Create toast element
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
@@ -374,4 +499,6 @@ if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initTableScrollDetection);
} else {
initTableScrollDetection();
}
}
apiLog.info('API Client module loaded');

406
static/shared/js/icons.js Normal file
View File

@@ -0,0 +1,406 @@
/**
* Heroicons Helper - Inline SVG Icons
* Usage: icon('home') or icon('home', 'w-6 h-6')
*/
const Icons = {
// Navigation
home: `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>`,
menu: `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/>
</svg>`,
search: `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/>
</svg>`,
// User & Profile
user: `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>`,
users: `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>`,
// Actions
edit: `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/>
</svg>`,
delete: `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>`,
plus: `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>`,
check: `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>`,
close: `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>`,
// Theme & Settings
sun: `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"/>
</svg>`,
moon: `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
</svg>`,
cog: `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>`,
// Notifications & Communication
bell: `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z"/>
</svg>`,
mail: `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"/>
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/>
</svg>`,
// Logout
logout: `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg>`,
// Business/Commerce
'shopping-bag': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"/>
</svg>`,
cube: `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>`,
chart: `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"/>
</svg>`,
// Arrows & Directions
'chevron-down': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>`,
'chevron-right': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
</svg>`,
'arrow-left': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>`,
// Status & Indicators
'exclamation': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>`,
'information-circle': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>`,
// Loading
spinner: `<svg class="{{classes}} animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>`,
// E-commerce Specific
'shopping-cart': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M3 1a1 1 0 000 2h1.22l.305 1.222a.997.997 0 00.01.042l1.358 5.43-.893.892C3.74 11.846 4.632 14 6.414 14H15a1 1 0 000-2H6.414l1-1H14a1 1 0 00.894-.553l3-6A1 1 0 0017 3H6.28l-.31-1.243A1 1 0 005 1H3zM16 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM6.5 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"/>
</svg>`,
'credit-card': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
</svg>`,
'currency-dollar': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>`,
'gift': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"/>
</svg>`,
'tag': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/>
</svg>`,
'truck': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17a2 2 0 11-4 0 2 2 0 014 0zM19 17a2 2 0 11-4 0 2 2 0 014 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1 1H9m4-1V8a1 1 0 011-1h2.586a1 1 0 01.707.293l3.414 3.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6-1a1 1 0 001 1h1M5 17a2 2 0 104 0m-4 0a2 2 0 114 0m6 0a2 2 0 104 0m-4 0a2 2 0 114 0"/>
</svg>`,
'receipt': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
</svg>`,
'clipboard-list': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
</svg>`,
// Inventory & Products
'collection': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>`,
'photograph': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>`,
'color-swatch': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/>
</svg>`,
'template': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/>
</svg>`,
// Analytics & Reports
'trending-up': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>`,
'trending-down': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"/>
</svg>`,
'presentation-chart-line': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/>
</svg>`,
'calculator': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
</svg>`,
// Customer Management
'user-circle': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z" clip-rule="evenodd"/>
</svg>`,
'user-group': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"/>
</svg>`,
'identification': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2"/>
</svg>`,
'badge-check': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>`,
// Documents & Files
'document': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>`,
'folder': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
</svg>`,
'folder-open': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z" clip-rule="evenodd"/>
<path d="M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z"/>
</svg>`,
'download': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>`,
'upload': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
</svg>`,
// Time & Calendar
'calendar': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>`,
'clock': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>`,
// System & Settings
'database': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/>
</svg>`,
'server': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>
</svg>`,
'shield-check': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>`,
'key': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd"/>
</svg>`,
'lock-closed': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
</svg>`,
'lock-open': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a5 5 0 00-5 5v2a2 2 0 00-2 2v5a2 2 0 002 2h10a2 2 0 002-2v-5a2 2 0 00-2-2H7V7a3 3 0 015.905-.75 1 1 0 001.937-.5A5.002 5.002 0 0010 2z"/>
</svg>`,
// Actions & Interactions
'refresh': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>`,
'duplicate': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>`,
'eye': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>`,
'eye-off': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
</svg>`,
'filter': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"/>
</svg>`,
'dots-vertical': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"/>
</svg>`,
'dots-horizontal': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M6 10a2 2 0 11-4 0 2 2 0 014 0zM12 10a2 2 0 11-4 0 2 2 0 014 0zM16 12a2 2 0 100-4 2 2 0 000 4z"/>
</svg>`,
// Communication
'chat': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
</svg>`,
'annotation': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/>
</svg>`,
'phone': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z"/>
</svg>`,
// Location
'location-marker': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"/>
</svg>`,
'globe': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>`,
// Links & External
'external-link': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>`,
'link': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
</svg>`,
// Status Badges
'star': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>`,
'heart': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clip-rule="evenodd"/>
</svg>`,
'flag': `<svg class="{{classes}}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 6a3 3 0 013-3h10a1 1 0 01.8 1.6L14.25 8l2.55 3.4A1 1 0 0116 13H6a1 1 0 00-1 1v3a1 1 0 11-2 0V6z" clip-rule="evenodd"/>
</svg>`
};
/**
* Get icon SVG with custom classes
* @param {string} name - Icon name from Icons object
* @param {string} classes - Tailwind classes (default: 'w-5 h-5')
* @returns {string} SVG markup
*/
function icon(name, classes = 'w-5 h-5') {
const iconTemplate = Icons[name];
if (!iconTemplate) {
console.warn(`Icon "${name}" not found`);
return '';
}
return iconTemplate.replace('{{classes}}', classes);
}
/**
* Alpine.js magic helper
* Usage in Alpine: x-html="$icon('home')" or x-html="$icon('home', 'w-6 h-6')"
*/
if (typeof Alpine !== 'undefined') {
document.addEventListener('alpine:init', () => {
Alpine.magic('icon', () => {
return (name, classes) => icon(name, classes);
});
});
}
// Export for use in modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { icon, Icons };
}
// Make available globally
window.icon = icon;
window.Icons = Icons;
/**
* Get icon SVG with custom classes
* @param {string} name - Icon name from Icons object
* @param {string} classes - Tailwind classes (default: 'w-5 h-5')
* @returns {string} SVG markup
*/
function icon(name, classes = 'w-5 h-5') {
const iconTemplate = Icons[name];
if (!iconTemplate) {
console.warn(`Icon "${name}" not found`);
return '';
}
return iconTemplate.replace('{{classes}}', classes);
}
/**
* Alpine.js magic helper
* Usage in Alpine: x-html="$icon('home')" or x-html="$icon('home', 'w-6 h-6')"
*/
if (typeof Alpine !== 'undefined') {
document.addEventListener('alpine:init', () => {
Alpine.magic('icon', () => {
return (name, classes) => icon(name, classes);
});
});
}
// Export for use in modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { icon, Icons };
}
// Make available globally
window.icon = icon;
window.Icons = Icons;

193
static/shared/js/utils.js Normal file
View File

@@ -0,0 +1,193 @@
// static/shared/js/utils.js
/**
* Utility functions for the application
*/
const Utils = {
/**
* Format date for display
* @param {string} dateString - ISO date string
* @returns {string} Formatted date
*/
formatDate(dateString) {
if (!dateString) return '-';
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
} catch (error) {
console.error('Error formatting date:', error);
return dateString;
}
},
/**
* Format date with time
* @param {string} dateString - ISO date string
* @returns {string} Formatted date with time
*/
formatDateTime(dateString) {
if (!dateString) return '-';
try {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch (error) {
console.error('Error formatting datetime:', error);
return dateString;
}
},
/**
* Format currency
* @param {number} amount - Amount to format
* @param {string} currency - Currency code (default: USD)
* @returns {string} Formatted currency
*/
formatCurrency(amount, currency = 'USD') {
if (amount === null || amount === undefined) return '-';
try {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency
}).format(amount);
} catch (error) {
console.error('Error formatting currency:', error);
return amount.toString();
}
},
/**
* Format number with commas
* @param {number} num - Number to format
* @returns {string} Formatted number
*/
formatNumber(num) {
if (num === null || num === undefined) return '0';
return num.toLocaleString('en-US');
},
/**
* Show toast notification
* @param {string} message - Toast message
* @param {string} type - Toast type: 'success', 'error', 'warning', 'info'
* @param {number} duration - Duration in ms (default: 3000)
*/
showToast(message, type = 'info', duration = 3000) {
// Create toast element
const toast = document.createElement('div');
toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg text-white z-50 transition-opacity duration-300 ${
type === 'success' ? 'bg-green-500' :
type === 'error' ? 'bg-red-500' :
type === 'warning' ? 'bg-yellow-500' :
'bg-blue-500'
}`;
toast.textContent = message;
document.body.appendChild(toast);
// Fade out and remove
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, duration);
},
/**
* Debounce function
* @param {Function} func - Function to debounce
* @param {number} wait - Wait time in ms
* @returns {Function} Debounced function
*/
debounce(func, wait = 300) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
/**
* Get query parameter from URL
* @param {string} param - Parameter name
* @returns {string|null} Parameter value
*/
getQueryParam(param) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(param);
},
/**
* Copy text to clipboard
* @param {string} text - Text to copy
*/
async copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
this.showToast('Copied to clipboard', 'success');
} catch (error) {
console.error('Failed to copy:', error);
this.showToast('Failed to copy', 'error');
}
},
/**
* Truncate string
* @param {string} str - String to truncate
* @param {number} maxLength - Maximum length
* @returns {string} Truncated string
*/
truncate(str, maxLength = 50) {
if (!str || str.length <= maxLength) return str;
return str.substring(0, maxLength - 3) + '...';
},
/**
* Validate email format
* @param {string} email - Email to validate
* @returns {boolean} Is valid email
*/
isValidEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
},
/**
* Get status badge class
* @param {string} status - Status value
* @returns {string} Tailwind classes for badge
*/
getStatusBadgeClass(status) {
const statusClasses = {
'active': 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100',
'inactive': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-100',
'pending': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100',
'verified': 'bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100',
'rejected': 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'
};
return statusClasses[status.toLowerCase()] || 'bg-gray-100 text-gray-800';
}
};
// Make available globally
window.Utils = Utils;
// Export for modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = Utils;
}