admin login migration to new structure, new design

This commit is contained in:
2025-10-19 19:20:21 +02:00
parent f65a40b533
commit a7d9d44a13
25 changed files with 1081 additions and 970 deletions

View File

@@ -1 +0,0 @@
// Admin analytics

View File

@@ -1,203 +0,0 @@
/**
* Admin Dashboard Component
* Extends adminLayout with dashboard-specific functionality
*/
function adminDashboard() {
return {
// Inherit all adminLayout functionality
...window.adminLayout(),
// Dashboard-specific state
currentSection: 'dashboard',
stats: {
vendors: {},
users: {},
imports: {}
},
vendors: [],
users: [],
imports: [],
recentVendors: [],
recentImports: [],
loading: false,
/**
* Initialize dashboard
*/
async init() {
// Call parent init from adminLayout
this.currentPage = this.getCurrentPage();
await this.loadUserData();
// Load dashboard data
await this.loadDashboardData();
},
/**
* Load all dashboard data
*/
async loadDashboardData() {
this.loading = true;
try {
await Promise.all([
this.loadStats(),
this.loadRecentVendors(),
this.loadRecentImports()
]);
} catch (error) {
console.error('Error loading dashboard data:', error);
this.showErrorModal({
message: 'Failed to load dashboard data',
details: error.message
});
} finally {
this.loading = false;
}
},
/**
* Load statistics
*/
async loadStats() {
try {
const response = await apiClient.get('/admin/stats');
this.stats = response;
} catch (error) {
console.error('Failed to load stats:', error);
// Don't show error modal for stats, just log it
}
},
/**
* Load recent vendors
*/
async loadRecentVendors() {
try {
const response = await apiClient.get('/admin/vendors', {
skip: 0,
limit: 5
});
this.recentVendors = response.vendors || response;
} catch (error) {
console.error('Failed to load recent vendors:', error);
}
},
/**
* Load recent import jobs
*/
async loadRecentImports() {
try {
const response = await apiClient.get('/admin/imports', {
skip: 0,
limit: 5
});
this.recentImports = response.imports || response;
} catch (error) {
console.error('Failed to load recent imports:', error);
}
},
/**
* Show different sections
*/
async showSection(section) {
this.currentSection = section;
// Load data based on section
if (section === 'vendors' && this.vendors.length === 0) {
await this.loadAllVendors();
} else if (section === 'users' && this.users.length === 0) {
await this.loadAllUsers();
} else if (section === 'imports' && this.imports.length === 0) {
await this.loadAllImports();
}
},
/**
* Load all vendors
*/
async loadAllVendors() {
this.loading = true;
try {
const response = await apiClient.get('/admin/vendors', {
skip: 0,
limit: 100
});
this.vendors = response.vendors || response;
} catch (error) {
console.error('Failed to load vendors:', error);
this.showErrorModal({
message: 'Failed to load vendors',
details: error.message
});
} finally {
this.loading = false;
}
},
/**
* Load all users
*/
async loadAllUsers() {
this.loading = true;
try {
const response = await apiClient.get('/admin/users', {
skip: 0,
limit: 100
});
this.users = response.users || response;
} catch (error) {
console.error('Failed to load users:', error);
this.showErrorModal({
message: 'Failed to load users',
details: error.message
});
} finally {
this.loading = false;
}
},
/**
* Load all import jobs
*/
async loadAllImports() {
this.loading = true;
try {
const response = await apiClient.get('/admin/imports', {
skip: 0,
limit: 100
});
this.imports = response.imports || response;
} catch (error) {
console.error('Failed to load import jobs:', error);
this.showErrorModal({
message: 'Failed to load import jobs',
details: error.message
});
} finally {
this.loading = false;
}
},
/**
* Format date for display
*/
formatDate(dateString) {
if (!dateString) return '-';
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
} catch (error) {
return dateString;
}
}
};
}

View File

@@ -1,87 +0,0 @@
// Admin Login Component
function adminLogin() {
return {
credentials: {
username: '',
password: ''
},
loading: false,
error: null,
success: null,
errors: {},
init() {
// Check if already logged in
this.checkExistingAuth();
},
checkExistingAuth() {
if (Auth.isAuthenticated() && Auth.isAdmin()) {
window.location.href = '/static/admin/dashboard.html';
}
},
clearErrors() {
this.error = null;
this.errors = {};
},
validateForm() {
this.clearErrors();
let isValid = true;
if (!this.credentials.username.trim()) {
this.errors.username = 'Username is required';
isValid = false;
}
if (!this.credentials.password) {
this.errors.password = 'Password is required';
isValid = false;
}
return isValid;
},
async handleLogin() {
if (!this.validateForm()) {
return;
}
this.loading = true;
this.clearErrors();
try {
const response = await apiClient.post('/admin/auth/login', {
username: this.credentials.username.trim(),
password: this.credentials.password
});
// Check if user is admin
if (response.user.role !== 'admin') {
throw new Error('Access denied. Admin privileges required.');
}
// Store authentication data
localStorage.setItem('admin_token', response.access_token);
localStorage.setItem('admin_user', JSON.stringify(response.user));
// Show success message
this.success = 'Login successful! Redirecting...';
Utils.showToast('Login successful!', 'success', 2000);
// Redirect after short delay
setTimeout(() => {
window.location.href = '/static/admin/dashboard.html';
}, 1000);
} catch (error) {
console.error('Login error:', error);
this.error = error.message || 'Login failed. Please check your credentials.';
Utils.showToast(this.error, 'error');
} finally {
this.loading = false;
}
}
}
}

View File

@@ -1 +0,0 @@
// System monitoring

View File

@@ -1,338 +0,0 @@
// static/js/admin/vendor-edit.js
function vendorEdit() {
return {
currentUser: {},
vendor: {},
formData: {},
errors: {},
loadingVendor: true,
saving: false,
vendorId: null,
// Confirmation modal
confirmModal: {
show: false,
title: '',
message: '',
warning: '',
buttonText: '',
buttonClass: 'btn-primary',
onConfirm: () => {},
onCancel: null
},
// Success modal
successModal: {
show: false,
title: '',
message: '',
details: null,
note: ''
},
// Transfer ownership
showTransferOwnership: false,
transferring: false,
transferData: {
new_owner_user_id: null,
transfer_reason: '',
confirm_transfer: false
},
init() {
console.log('=== Vendor Edit Initialization ===');
// Check authentication
if (!Auth.isAuthenticated() || !Auth.isAdmin()) {
console.log('Not authenticated as admin, redirecting to login');
window.location.href = '/static/admin/login.html';
return;
}
this.currentUser = Auth.getCurrentUser();
console.log('Current user:', this.currentUser.username);
// Get vendor ID from URL
const urlParams = new URLSearchParams(window.location.search);
this.vendorId = urlParams.get('id');
if (!this.vendorId) {
console.error('No vendor ID in URL');
alert('No vendor ID provided');
window.location.href = '/static/admin/dashboard.html#vendors';
return;
}
console.log('Vendor ID:', this.vendorId);
// Load vendor details
this.loadVendor();
},
async loadVendor() {
this.loadingVendor = true;
try {
console.log('Loading vendor with ID:', this.vendorId);
this.vendor = await apiClient.get(`/admin/vendors/${this.vendorId}`);
console.log('✅ Vendor loaded:', this.vendor.vendor_code);
console.log('Owner email:', this.vendor.owner_email);
console.log('Contact email:', this.vendor.contact_email);
// Populate form data
this.formData = {
name: this.vendor.name,
subdomain: this.vendor.subdomain,
description: this.vendor.description || '',
contact_email: this.vendor.contact_email || '',
contact_phone: this.vendor.contact_phone || '',
website: this.vendor.website || '',
business_address: this.vendor.business_address || '',
tax_number: this.vendor.tax_number || ''
};
console.log('Form data populated');
} catch (error) {
console.error('❌ Failed to load vendor:', error);
Utils.showToast('Failed to load vendor details: ' + (error.message || 'Unknown error'), 'error');
window.location.href = '/static/admin/dashboard.html#vendors';
} finally {
this.loadingVendor = false;
}
},
formatSubdomain() {
this.formData.subdomain = this.formData.subdomain
.toLowerCase()
.replace(/[^a-z0-9-]/g, '');
},
async handleSubmit() {
console.log('Submitting vendor update...');
this.errors = {};
this.saving = true;
try {
const updatedVendor = await apiClient.put(
`/admin/vendors/${this.vendorId}`,
this.formData
);
console.log('✅ Vendor updated successfully');
Utils.showToast('Vendor updated successfully!', 'success');
this.vendor = updatedVendor;
// Refresh form data with latest values
this.formData.name = updatedVendor.name;
this.formData.subdomain = updatedVendor.subdomain;
this.formData.contact_email = updatedVendor.contact_email;
} catch (error) {
console.error('❌ Failed to update vendor:', error);
Utils.showToast(error.message || 'Failed to update vendor', 'error');
} finally {
this.saving = false;
}
},
showVerificationModal() {
const action = this.vendor.is_verified ? 'unverify' : 'verify';
const actionCap = this.vendor.is_verified ? 'Unverify' : 'Verify';
this.confirmModal = {
show: true,
title: `${actionCap} Vendor`,
message: `Are you sure you want to ${action} this vendor?`,
warning: this.vendor.is_verified
? 'Unverifying this vendor will prevent them from being publicly visible and may affect their operations.'
: 'Verifying this vendor will make them publicly visible and allow them to operate fully.',
buttonText: actionCap,
buttonClass: this.vendor.is_verified ? 'btn-warning' : 'btn-success',
onConfirm: () => this.toggleVerification(),
onCancel: null
};
},
async toggleVerification() {
const action = this.vendor.is_verified ? 'unverify' : 'verify';
console.log(`Toggling verification: ${action}`);
this.saving = true;
try {
const result = await apiClient.put(`/admin/vendors/${this.vendorId}/verify`);
this.vendor.is_verified = result.vendor.is_verified;
console.log('✅ Verification toggled');
Utils.showToast(result.message, 'success');
} catch (error) {
console.error('❌ Failed to toggle verification:', error);
Utils.showToast('Failed to update verification status', 'error');
} finally {
this.saving = false;
}
},
showStatusModal() {
const action = this.vendor.is_active ? 'deactivate' : 'activate';
const actionCap = this.vendor.is_active ? 'Deactivate' : 'Activate';
this.confirmModal = {
show: true,
title: `${actionCap} Vendor`,
message: `Are you sure you want to ${action} this vendor?`,
warning: this.vendor.is_active
? 'Deactivating this vendor will immediately suspend all their operations and make them invisible to customers.'
: 'Activating this vendor will restore their operations and make them visible again.',
buttonText: actionCap,
buttonClass: this.vendor.is_active ? 'btn-danger' : 'btn-success',
onConfirm: () => this.toggleStatus(),
onCancel: null
};
},
async toggleStatus() {
const action = this.vendor.is_active ? 'deactivate' : 'activate';
console.log(`Toggling status: ${action}`);
this.saving = true;
try {
const result = await apiClient.put(`/admin/vendors/${this.vendorId}/status`);
this.vendor.is_active = result.vendor.is_active;
console.log('✅ Status toggled');
Utils.showToast(result.message, 'success');
} catch (error) {
console.error('❌ Failed to toggle status:', error);
Utils.showToast('Failed to update vendor status', 'error');
} finally {
this.saving = false;
}
},
async handleTransferOwnership() {
// Validate inputs
if (!this.transferData.confirm_transfer) {
Utils.showToast('Please confirm the ownership transfer', 'error');
return;
}
if (!this.transferData.new_owner_user_id) {
Utils.showToast('Please enter the new owner user ID', 'error');
return;
}
// Close the transfer modal first
this.showTransferOwnership = false;
// Wait a moment for modal to close
await new Promise(resolve => setTimeout(resolve, 300));
// Show final confirmation modal
this.confirmModal = {
show: true,
title: '⚠️ FINAL CONFIRMATION: Transfer Ownership',
message: `You are about to transfer ownership of "${this.vendor.name}" to user ID ${this.transferData.new_owner_user_id}.`,
warning: `Current Owner: ${this.vendor.owner_username} (${this.vendor.owner_email})\n\n` +
`This action will:\n` +
`• Assign full ownership rights to the new user\n` +
`• Demote the current owner to Manager role\n` +
`• Be permanently logged for audit purposes\n\n` +
`This action cannot be easily undone. Are you absolutely sure?`,
buttonText: '🔄 Yes, Transfer Ownership',
buttonClass: 'btn-danger',
onConfirm: () => this.executeTransferOwnership(),
onCancel: () => {
// If cancelled, reopen the transfer modal with preserved data
this.showTransferOwnership = true;
}
};
},
async executeTransferOwnership() {
console.log('Transferring ownership to user:', this.transferData.new_owner_user_id);
this.transferring = true;
this.saving = true;
try {
const result = await apiClient.post(
`/admin/vendors/${this.vendorId}/transfer-ownership`,
this.transferData
);
console.log('✅ Ownership transferred successfully');
// Show beautiful success modal
this.successModal = {
show: true,
title: 'Ownership Transfer Complete',
message: `The ownership of "${this.vendor.name}" has been successfully transferred.`,
details: {
oldOwner: {
username: result.old_owner.username,
email: result.old_owner.email
},
newOwner: {
username: result.new_owner.username,
email: result.new_owner.email
}
},
note: 'The transfer has been logged for audit purposes. The previous owner has been assigned the Manager role.'
};
Utils.showToast('Ownership transferred successfully', 'success');
// Reload vendor data to reflect new owner
await this.loadVendor();
// Reset transfer form data
this.transferData = {
new_owner_user_id: null,
transfer_reason: '',
confirm_transfer: false
};
} catch (error) {
console.error('❌ Failed to transfer ownership:', error);
const errorMsg = error.message || error.detail || 'Unknown error';
Utils.showToast(`Transfer failed: ${errorMsg}`, 'error');
// Show error in modal format (reuse success modal structure)
alert(`❌ Transfer Failed\n\n${errorMsg}\n\nPlease check the user ID and try again.`);
// Reopen transfer modal so user can try again
this.showTransferOwnership = true;
} finally {
this.transferring = false;
this.saving = false;
}
},
async handleLogout() {
// Show confirmation modal for logout
this.confirmModal = {
show: true,
title: '🚪 Confirm Logout',
message: 'Are you sure you want to logout from the Admin Portal?',
warning: 'You will need to login again to access the admin dashboard.',
buttonText: 'Yes, Logout',
buttonClass: 'btn-danger',
onConfirm: () => this.executeLogout(),
onCancel: null
};
},
async executeLogout() {
console.log('Logging out...');
// Show loading state briefly
this.saving = true;
// Clear authentication
Auth.logout();
// Show success message
Utils.showToast('Logged out successfully', 'success', 1000);
// Redirect to login after brief delay
setTimeout(() => {
window.location.href = '/static/admin/login.html';
}, 500);
},
};
}

View File

@@ -1,241 +0,0 @@
// Admin Vendor Creation Component
function vendorCreation() {
return {
currentUser: {},
formData: {
vendor_code: '',
name: '',
subdomain: '',
description: '',
owner_email: '',
business_phone: '',
website: '',
business_address: '',
tax_number: ''
},
loading: false,
errors: {},
showCredentials: false,
credentials: null,
init() {
if (!this.checkAuth()) {
return;
}
},
checkAuth() {
if (!Auth.isAuthenticated()) {
window.location.href = '/static/admin/login.html';
return false;
}
const user = Auth.getCurrentUser();
if (!user || user.role !== 'admin') {
Utils.showToast('Access denied. Admin privileges required.', 'error');
Auth.logout();
window.location.href = '/static/admin/login.html';
return false;
}
this.currentUser = user;
return true;
},
async handleLogout() {
const confirmed = await Utils.confirm(
'Are you sure you want to logout?',
'Confirm Logout'
);
if (confirmed) {
Auth.logout();
Utils.showToast('Logged out successfully', 'success', 2000);
setTimeout(() => {
window.location.href = '/static/admin/login.html';
}, 500);
}
},
// Auto-format vendor code (uppercase)
formatVendorCode() {
this.formData.vendor_code = this.formData.vendor_code
.toUpperCase()
.replace(/[^A-Z0-9_-]/g, '');
},
// Auto-format subdomain (lowercase)
formatSubdomain() {
this.formData.subdomain = this.formData.subdomain
.toLowerCase()
.replace(/[^a-z0-9-]/g, '');
},
clearErrors() {
this.errors = {};
},
validateForm() {
this.clearErrors();
let isValid = true;
// Required fields validation
if (!this.formData.vendor_code.trim()) {
this.errors.vendor_code = 'Vendor code is required';
isValid = false;
}
if (!this.formData.name.trim()) {
this.errors.name = 'Vendor name is required';
isValid = false;
}
if (!this.formData.subdomain.trim()) {
this.errors.subdomain = 'Subdomain is required';
isValid = false;
}
if (!this.formData.owner_email.trim()) {
this.errors.owner_email = 'Owner email is required';
isValid = false;
}
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (this.formData.owner_email && !emailRegex.test(this.formData.owner_email)) {
this.errors.owner_email = 'Invalid email format';
isValid = false;
}
// Subdomain validation (must start and end with alphanumeric)
const subdomainRegex = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
if (this.formData.subdomain && this.formData.subdomain.length > 1 &&
!subdomainRegex.test(this.formData.subdomain)) {
this.errors.subdomain = 'Subdomain must start and end with a letter or number';
isValid = false;
}
return isValid;
},
async handleSubmit() {
if (!this.validateForm()) {
Utils.showToast('Please fix validation errors', 'error');
window.scrollTo({ top: 0, behavior: 'smooth' });
return;
}
this.loading = true;
try {
// Prepare data (remove empty fields)
const submitData = {};
for (const [key, value] of Object.entries(this.formData)) {
if (value !== '' && value !== null && value !== undefined) {
submitData[key] = value;
}
}
console.log('Submitting vendor data:', submitData);
const response = await apiClient.post('/admin/vendors', submitData);
console.log('Vendor creation response:', response);
// Store credentials - be flexible with response structure
this.credentials = {
vendor_code: response.vendor_code || this.formData.vendor_code,
subdomain: response.subdomain || this.formData.subdomain,
name: response.name || this.formData.name,
owner_username: response.owner_username || `${this.formData.subdomain}_owner`,
owner_email: response.owner_email || this.formData.owner_email,
temporary_password: response.temporary_password || 'PASSWORD_NOT_RETURNED',
login_url: response.login_url ||
`http://localhost:8000/vendor/${this.formData.subdomain}/login` ||
`${this.formData.subdomain}.platform.com/vendor/login`
};
console.log('Stored credentials:', this.credentials);
// Check if password was returned
if (!response.temporary_password) {
console.warn('⚠️ Warning: temporary_password not returned from API');
console.warn('Full API response:', response);
Utils.showToast('Vendor created but password not returned. Check server logs.', 'warning', 5000);
}
// Show credentials display
this.showCredentials = true;
// Success notification
Utils.showToast('Vendor created successfully!', 'success');
// Scroll to top to see credentials
window.scrollTo({ top: 0, behavior: 'smooth' });
} catch (error) {
console.error('Error creating vendor:', error);
// Check for specific validation errors
if (error.message.includes('vendor_code') || error.message.includes('Vendor code')) {
this.errors.vendor_code = 'Vendor code already exists';
} else if (error.message.includes('subdomain')) {
this.errors.subdomain = 'Subdomain already exists';
} else if (error.message.includes('email')) {
this.errors.owner_email = 'Email already in use';
}
Utils.showToast(
error.message || 'Failed to create vendor',
'error'
);
} finally {
this.loading = false;
}
},
resetForm() {
this.formData = {
vendor_code: '',
name: '',
subdomain: '',
description: '',
owner_email: '',
business_phone: '',
website: '',
business_address: '',
tax_number: ''
};
this.clearErrors();
this.showCredentials = false;
this.credentials = null;
},
copyToClipboard(text, label) {
if (!text) {
Utils.showToast('Nothing to copy', 'error');
return;
}
navigator.clipboard.writeText(text).then(() => {
Utils.showToast(`${label} copied to clipboard`, 'success', 2000);
}).catch((err) => {
console.error('Failed to copy:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
Utils.showToast(`${label} copied to clipboard`, 'success', 2000);
} catch (err) {
Utils.showToast('Failed to copy to clipboard', 'error');
}
document.body.removeChild(textArea);
});
}
}
}

View File

@@ -1,377 +0,0 @@
// static/js/shared/api-client.js
/**
* API Client for Multi-Tenant Ecommerce Platform
*
* Provides utilities for:
* - Making authenticated API calls
* - Token management
* - Error handling
* - Request/response interceptors
*/
const API_BASE_URL = '/api/v1';
/**
* API Client Class
*/
class APIClient {
constructor(baseURL = API_BASE_URL) {
this.baseURL = baseURL;
}
/**
* Get stored authentication token
*/
getToken() {
return localStorage.getItem('admin_token') || localStorage.getItem('vendor_token');
}
/**
* Get default headers with authentication
*/
getHeaders(additionalHeaders = {}) {
const headers = {
'Content-Type': 'application/json',
...additionalHeaders
};
const token = this.getToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}
/**
* Make API request
*/
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...options,
headers: this.getHeaders(options.headers)
};
try {
const response = await fetch(url, config);
// Handle 401 Unauthorized
if (response.status === 401) {
this.handleUnauthorized();
throw new Error('Unauthorized - please login again');
}
// Parse response
const data = await response.json();
// Handle non-OK responses
if (!response.ok) {
throw new Error(data.detail || data.message || 'Request failed');
}
return data;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
/**
* GET request
*/
async get(endpoint, params = {}) {
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url, {
method: 'GET'
});
}
/**
* POST request
*/
async post(endpoint, data = {}) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
/**
* PUT request
*/
async put(endpoint, data = {}) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
/**
* DELETE request
*/
async delete(endpoint) {
return this.request(endpoint, {
method: 'DELETE'
});
}
/**
* Handle unauthorized access
*/
handleUnauthorized() {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_user');
localStorage.removeItem('vendor_token');
localStorage.removeItem('vendor_user');
// 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';
}
}
}
// Create global API client instance
const apiClient = new APIClient();
/**
* Authentication helpers
*/
const Auth = {
/**
* Check if user is authenticated
*/
isAuthenticated() {
const token = localStorage.getItem('admin_token') || localStorage.getItem('vendor_token');
return !!token;
},
/**
* Get current user
*/
getCurrentUser() {
const userStr = localStorage.getItem('admin_user') || localStorage.getItem('vendor_user');
if (!userStr) return null;
try {
return JSON.parse(userStr);
} catch (e) {
return null;
}
},
/**
* Check if user is admin
*/
isAdmin() {
const user = this.getCurrentUser();
return user && user.role === 'admin';
},
/**
* Login
*/
async login(username, password) {
const response = await apiClient.post('/auth/login', {
username,
password
});
// Store token and user
if (response.user.role === 'admin') {
localStorage.setItem('admin_token', response.access_token);
localStorage.setItem('admin_user', JSON.stringify(response.user));
} else {
localStorage.setItem('vendor_token', response.access_token);
localStorage.setItem('vendor_user', JSON.stringify(response.user));
}
return response;
},
/**
* Logout
*/
logout() {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_user');
localStorage.removeItem('vendor_token');
localStorage.removeItem('vendor_user');
}
};
/**
* Utility functions
*/
const Utils = {
/**
* Format date
*/
formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('en-GB', {
day: '2-digit',
month: 'short',
year: 'numeric'
});
},
/**
* Format datetime
*/
formatDateTime(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleString('en-GB', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
},
/**
* Format currency
*/
formatCurrency(amount, currency = 'EUR') {
if (amount === null || amount === undefined) return '-';
return new Intl.NumberFormat('en-GB', {
style: 'currency',
currency: currency
}).format(amount);
},
/**
* Debounce function
*/
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
/**
* Show toast notification
*/
showToast(message, type = 'info', duration = 3000) {
// Create toast element
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
// Style
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 16px 24px;
background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3'};
color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
animation: slideIn 0.3s ease;
max-width: 400px;
`;
// Add to page
document.body.appendChild(toast);
// Remove after duration
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, duration);
},
/**
* Confirm dialog
*/
async confirm(message, title = 'Confirm') {
return window.confirm(`${title}\n\n${message}`);
}
};
// Add animation styles
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
`;
document.head.appendChild(style);
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = { APIClient, apiClient, Auth, Utils };
}
// Table scroll detection helper
function initTableScrollDetection() {
const observer = new MutationObserver(() => {
const tables = document.querySelectorAll('.table-responsive');
tables.forEach(table => {
if (!table.hasAttribute('data-scroll-initialized')) {
table.setAttribute('data-scroll-initialized', 'true');
table.addEventListener('scroll', function() {
if (this.scrollLeft > 0) {
this.classList.add('is-scrolled');
} else {
this.classList.remove('is-scrolled');
}
});
// Check initial state
if (table.scrollLeft > 0) {
table.classList.add('is-scrolled');
}
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initTableScrollDetection);
} else {
initTableScrollDetection();
}