class MusicBandApp { constructor() { this.API_BASE = '/is1/api'; this.currentPage = 0; this.pageSize = 10; this.currentSort = 'id'; this.currentAscending = true; this.currentFilter = ''; this.editingBandId = null; this.websocket = null; this.reconnectAttempts = 0; this.token = localStorage.getItem('token'); this.user = JSON.parse(localStorage.getItem('user') || 'null'); this.toastEl = document.getElementById('toast'); this.initializeApp(); } initializeApp() { if (this.token && this.user) { this.showAppScreen(); } else { this.showAuthScreen(); } } showAuthScreen() { document.getElementById('authScreen').style.display = 'grid'; document.getElementById('appScreen').style.display = 'none'; this.initializeAuthListeners(); } showAppScreen() { document.getElementById('authScreen').style.display = 'none'; document.getElementById('appScreen').style.display = 'block'; document.getElementById('userInfo').textContent = `${this.user.username} (${this.user.role})`; this.initializeEventListeners(); this.loadBands(); setTimeout(() => { this.initializeWebSocket(); }, 500); } initializeAuthListeners() { document.getElementById('showRegisterBtn')?.addEventListener('click', () => { document.getElementById('loginForm').style.display = 'none'; document.getElementById('registerForm').style.display = 'block'; }); document.getElementById('showLoginBtn')?.addEventListener('click', () => { document.getElementById('registerForm').style.display = 'none'; document.getElementById('loginForm').style.display = 'block'; }); document.getElementById('loginFormElement')?.addEventListener('submit', (e) => { e.preventDefault(); this.login(); }); document.getElementById('registerFormElement')?.addEventListener('submit', (e) => { e.preventDefault(); this.register(); }); } async login() { const username = document.getElementById('loginUsername').value.trim(); const password = document.getElementById('loginPassword').value; if (!username || !password) { this.showError('Please fill in all fields'); return; } try { const response = await fetch(`${this.API_BASE}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Login failed'); } this.token = data.token; this.user = { userId: data.userId, username: data.username, role: data.role }; localStorage.setItem('token', this.token); localStorage.setItem('user', JSON.stringify(this.user)); this.showSuccess('Login successful!'); this.showAppScreen(); } catch (error) { this.showError(error.message); } } async register() { const username = document.getElementById('registerUsername').value.trim(); const password = document.getElementById('registerPassword').value; const passwordConfirm = document.getElementById('registerPasswordConfirm').value; if (!username || !password || !passwordConfirm) { this.showError('Please fill in all fields'); return; } if (password !== passwordConfirm) { this.showError('Passwords do not match'); return; } if (password.length < 6) { this.showError('Password must be at least 6 characters'); return; } try { const response = await fetch(`${this.API_BASE}/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Registration failed'); } this.token = data.token; this.user = { userId: data.userId, username: data.username, role: data.role }; localStorage.setItem('token', this.token); localStorage.setItem('user', JSON.stringify(this.user)); this.showSuccess('Registration successful!'); this.showAppScreen(); } catch (error) { this.showError(error.message); } } logout() { this.token = null; this.user = null; localStorage.removeItem('token'); localStorage.removeItem('user'); if (this.websocket) { this.websocket.close(); this.websocket = null; } this.showAuthScreen(); this.showInfo('Logged out successfully'); } initializeEventListeners() { document.getElementById('logoutBtn').addEventListener('click', () => this.logout()); document.getElementById('showBandsBtn').addEventListener('click', (e) => this.showSection('bandsSection', e.currentTarget)); document.getElementById('showStatisticsBtn').addEventListener('click', (e) => this.showSection('statisticsSection', e.currentTarget)); document.getElementById('showSpecialOpsBtn').addEventListener('click', (e) => this.showSection('specialOpsSection', e.currentTarget)); document.getElementById('showImportBtn').addEventListener('click', (e) => this.showSection('importSection', e.currentTarget)); document.getElementById('showHistoryBtn').addEventListener('click', (e) => { this.showSection('historySection', e.currentTarget); this.loadImportHistory(); }); document.getElementById('addBandBtn').addEventListener('click', () => this.showAddBandModal()); document.getElementById('nameFilter').addEventListener('input', (e) => this.filterBands(e.target.value)); document.getElementById('sortBy').addEventListener('change', (e) => this.sortBands(e.target.value)); document.getElementById('sortAscBtn').addEventListener('click', () => this.sortBands(this.currentSort, true)); document.getElementById('sortDescBtn').addEventListener('click', () => this.sortBands(this.currentSort, false)); document.getElementById('prevPageBtn').addEventListener('click', () => this.previousPage()); document.getElementById('nextPageBtn').addEventListener('click', () => this.nextPage()); document.getElementById('calcAvgBtn').addEventListener('click', () => this.calculateAverageAlbums()); document.getElementById('findMaxNameBtn').addEventListener('click', () => this.findBandWithMaxName()); document.getElementById('groupByParticipantsBtn').addEventListener('click', () => this.groupByParticipants()); document.getElementById('addSingleBtn').addEventListener('click', () => this.addSingle()); document.getElementById('removeParticipantBtn').addEventListener('click', () => this.removeParticipant()); document.getElementById('importBtn').addEventListener('click', () => this.importFile()); document.getElementById('refreshHistoryBtn').addEventListener('click', () => this.loadImportHistory()); document.querySelector('.close').addEventListener('click', () => this.closeModal()); document.getElementById('cancelBtn').addEventListener('click', () => this.closeModal()); document.getElementById('bandForm').addEventListener('submit', (e) => this.saveBand(e)); window.addEventListener('click', (e) => { if (e.target === document.getElementById('bandModal')) this.closeModal(); if (e.target === document.getElementById('viewModal')) this.closeViewModal(); }); } async makeRequest(url, options = {}) { const headers = { 'Content-Type': 'application/json', ...options.headers }; if (this.token && !url.includes('/auth/')) { headers['Authorization'] = `Bearer ${this.token}`; } try { const response = await fetch(url, { headers, ...options }); let data; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { try { data = await response.json(); } catch (e) { data = {}; } } else { data = {}; } if (!response.ok) { if (response.status === 401) { this.showError('Session expired. Please login again.'); this.logout(); throw new Error('Unauthorized'); } const errorMessage = this.extractErrorMessage(data, response.status); throw new Error(errorMessage); } return data; } catch (error) { if (error.name === 'TypeError' && error.message.includes('fetch')) { throw new Error('Failed to connect to server. Please check your connection.'); } throw error; } } extractErrorMessage(data, status) { if (data.error) return data.error; if (data.message) return data.message; switch (status) { case 400: return 'Invalid request data. Please check your input.'; case 401: return 'Authentication required.'; case 403: return 'Access denied.'; case 404: return 'Resource not found.'; case 409: return 'Conflict with existing data.'; case 422: return 'Validation failed. Please check your input.'; case 500: return 'Internal server error. Please try again later.'; case 503: return 'Service temporarily unavailable.'; default: return `Request failed with status ${status}`; } } showSection(sectionId, btn) { document.querySelectorAll('.section').forEach((s) => s.classList.remove('active')); document.querySelectorAll('.nav-btn').forEach((b) => b.classList.remove('active')); document.getElementById(sectionId).classList.add('active'); btn.classList.add('active'); } async loadBands() { try { const params = new URLSearchParams({ page: this.currentPage, size: this.pageSize, sortBy: this.currentSort, ascending: this.currentAscending }); if (this.currentFilter) { params.append('nameFilter', this.currentFilter); } const data = await this.makeRequest(`${this.API_BASE}/music-bands?${params}`); const bands = data.bands || []; const totalCount = data.totalCount || 0; this.renderBandsTable(bands); this.updatePagination(totalCount); if (totalCount === 0 && this.currentFilter) { this.showInfo('No bands found matching your filter'); } } catch (error) { this.showError(error.message); this.renderBandsTable([]); this.updatePagination(0); } } renderBandsTable(bands) { const tbody = document.getElementById('bandsTableBody'); tbody.innerHTML = ''; if (bands.length === 0) { const row = document.createElement('tr'); row.innerHTML = 'No bands found'; tbody.appendChild(row); return; } bands.forEach((band) => { const row = document.createElement('tr'); const canEdit = this.user.role === 'ADMIN' || this.user.userId === band.createdBy; row.innerHTML = ` ${band.id} ${this.escapeHtml(band.name || '')} ${band.genre || '—'} ${band.numberOfParticipants || '—'} ${band.singlesCount || '—'} ${band.albumsCount ?? '—'} ${band.establishmentDate ? new Date(band.establishmentDate).toLocaleDateString() : '—'}
${canEdit ? '' : ''} ${canEdit ? '' : ''}
`; row.querySelector('[data-action="view"]').addEventListener('click', () => this.viewBand(band.id)); if (canEdit) { row.querySelector('[data-action="edit"]').addEventListener('click', () => this.editBand(band.id)); row.querySelector('[data-action="del"]').addEventListener('click', () => this.deleteBand(band.id)); } tbody.appendChild(row); }); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } updatePagination(totalCount) { const totalPages = Math.max(1, Math.ceil(totalCount / this.pageSize)); this.currentPage = Math.min(this.currentPage, totalPages - 1); document.getElementById('pageInfo').textContent = `Page ${this.currentPage + 1} of ${totalPages}`; document.getElementById('prevPageBtn').disabled = this.currentPage === 0; document.getElementById('nextPageBtn').disabled = this.currentPage >= totalPages - 1; } filterBands(filter) { this.currentFilter = filter.trim(); this.currentPage = 0; this.loadBands(); } sortBands(sortBy, ascending = this.currentAscending) { this.currentSort = sortBy; this.currentAscending = ascending; this.currentPage = 0; document.getElementById('sortAscBtn').classList.toggle('active', ascending); document.getElementById('sortDescBtn').classList.toggle('active', !ascending); this.loadBands(); } previousPage() { if (this.currentPage > 0) { this.currentPage--; this.loadBands(); } } nextPage() { this.currentPage++; this.loadBands(); } showAddBandModal() { this.editingBandId = null; document.getElementById('modalTitle').textContent = 'Add New Band'; document.getElementById('bandForm').reset(); this.setDefaultEstablishmentDate(); this.clearFormErrors(); document.getElementById('bandModal').style.display = 'block'; } setDefaultEstablishmentDate() { if (!this.editingBandId) { const now = new Date(); now.setFullYear(now.getFullYear() - 1); const formatted = now.toISOString().slice(0, 16); document.getElementById('establishmentDate').value = formatted; } } async editBand(id) { try { const band = await this.makeRequest(`${this.API_BASE}/music-bands/${id}`); this.editingBandId = id; document.getElementById('modalTitle').textContent = 'Edit Band'; this.populateForm(band); this.clearFormErrors(); document.getElementById('bandModal').style.display = 'block'; } catch (error) { this.showError(error.message); } } async viewBand(id) { try { const band = await this.makeRequest(`${this.API_BASE}/music-bands/${id}`); this.showViewModal(band); } catch (error) { this.showError(error.message); } } showViewModal(band) { const modal = document.getElementById('viewModal') || this.createViewModal(); const content = modal.querySelector('.view-content'); content.innerHTML = `

${this.escapeHtml(band.name)}

${band.genre}
${band.id}
${band.numberOfParticipants}
${band.singlesCount}
${band.albumsCount ?? '—'}
(${band.coordinates?.x ?? '—'}, ${band.coordinates?.y ?? '—'})
${band.establishmentDate ? new Date(band.establishmentDate).toLocaleString() : '—'}
${band.creationDate ? new Date(band.creationDate).toLocaleString() : '—'}
${band.description ? `

Description

${this.escapeHtml(band.description)}

` : ''} ${band.bestAlbum ? `

Best Album

${this.escapeHtml(band.bestAlbum.name)}
${band.bestAlbum.tracks}
` : ''} ${band.frontMan ? `

Front Man

${this.escapeHtml(band.frontMan.name)}
${band.frontMan.nationality.replace(/_/g, ' ')}
${band.frontMan.eyeColor}
${band.frontMan.hairColor ? `
${band.frontMan.hairColor}
` : ''}
${band.frontMan.height} cm
${band.frontMan.location ? `
(${band.frontMan.location.x}, ${band.frontMan.location.y}, ${band.frontMan.location.z})
` : ''}
` : ''} `; modal.style.display = 'block'; } createViewModal() { const modal = document.createElement('div'); modal.id = 'viewModal'; modal.className = 'modal'; modal.innerHTML = ` `; modal.querySelector('.close').addEventListener('click', () => this.closeViewModal()); document.body.appendChild(modal); return modal; } closeViewModal() { const modal = document.getElementById('viewModal'); if (modal) { modal.style.display = 'none'; } } populateForm(band) { document.getElementById('bandName').value = band.name || ''; document.getElementById('bandGenre').value = band.genre || ''; document.getElementById('coordinatesX').value = band.coordinates?.x || ''; document.getElementById('coordinatesY').value = band.coordinates?.y || ''; document.getElementById('numberOfParticipants').value = band.numberOfParticipants || ''; document.getElementById('singlesCount').value = band.singlesCount || ''; document.getElementById('albumsCount').value = band.albumsCount || ''; document.getElementById('establishmentDate').value = this.convertFromZonedDateTime(band.establishmentDate); document.getElementById('description').value = band.description || ''; if (band.bestAlbum) { document.getElementById('albumName').value = band.bestAlbum.name || ''; document.getElementById('albumTracks').value = band.bestAlbum.tracks || ''; } if (band.frontMan) { document.getElementById('frontManName').value = band.frontMan.name || ''; document.getElementById('eyeColor').value = band.frontMan.eyeColor || ''; document.getElementById('hairColor').value = band.frontMan.hairColor || ''; document.getElementById('height').value = band.frontMan.height || ''; document.getElementById('nationality').value = band.frontMan.nationality || ''; if (band.frontMan.location) { document.getElementById('locationX').value = band.frontMan.location.x || ''; document.getElementById('locationY').value = band.frontMan.location.y || ''; document.getElementById('locationZ').value = band.frontMan.location.z || ''; } } } convertFromZonedDateTime(zonedDateTime) { if (!zonedDateTime) return ''; try { const date = new Date(zonedDateTime); if (isNaN(date.getTime())) return ''; const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day}T${hours}:${minutes}`; } catch (e) { return ''; } } convertToZonedDateTime(datetimeLocalValue) { if (!datetimeLocalValue) return null; try { const date = new Date(datetimeLocalValue); if (isNaN(date.getTime())) throw new Error('Invalid date format'); const offsetMinutes = date.getTimezoneOffset(); const offsetHours = Math.abs(Math.floor(offsetMinutes / 60)); const offsetMins = Math.abs(offsetMinutes % 60); const offsetSign = offsetMinutes <= 0 ? '+' : '-'; const offset = `${offsetSign}${String(offsetHours).padStart(2, '0')}:${String(offsetMins).padStart(2, '0')}`; const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offset}`; } catch (e) { throw new Error('Invalid date format'); } } async saveBand(event) { event.preventDefault(); this.clearFormErrors(); const bandData = this.collectFormData(); if (!this.validateFormData(bandData)) { return; } const submitButton = event.target.querySelector('button[type="submit"]'); const originalText = submitButton.textContent; submitButton.textContent = 'Saving...'; submitButton.disabled = true; try { const url = this.editingBandId ? `${this.API_BASE}/music-bands/${this.editingBandId}` : `${this.API_BASE}/music-bands`; const method = this.editingBandId ? 'PUT' : 'POST'; const savedBand = await this.makeRequest(url, { method, body: JSON.stringify(bandData) }); this.closeModal(); await this.loadBands(); this.showSuccess(this.editingBandId ? 'Band updated successfully!' : 'Band created successfully!'); } catch (error) { this.showError(error.message); this.highlightErrorFields(error.message); } finally { submitButton.textContent = originalText; submitButton.disabled = false; } } validateFormData(data) { const errors = []; if (!data.name?.trim()) { errors.push('Band name is required'); } if (!data.genre) { errors.push('Genre is required'); } if (data.coordinates?.x === null || data.coordinates?.x === undefined) { errors.push('Coordinates X is required'); } if (data.coordinates?.y === null || data.coordinates?.y === undefined) { errors.push('Coordinates Y is required'); } if (!data.numberOfParticipants || data.numberOfParticipants <= 0) { errors.push('Number of participants must be greater than 0'); } if (!data.singlesCount || data.singlesCount <= 0) { errors.push('Singles count must be greater than 0'); } if (data.albumsCount !== null && data.albumsCount !== undefined && data.albumsCount <= 0) { errors.push('Albums count must be greater than 0'); } if (!data.establishmentDate) { errors.push('Establishment date is required'); } else { try { const estDate = new Date(data.establishmentDate); const currentDate = new Date(); const minDate = new Date('1900-01-01'); if (isNaN(estDate.getTime())) { errors.push('Invalid establishment date format'); } else if (estDate > currentDate) { errors.push('Establishment date cannot be in the future'); } else if (estDate < minDate) { errors.push('Establishment date must be after 1900'); } } catch (e) { errors.push('Invalid establishment date'); } } if (data.bestAlbum) { if (!data.bestAlbum.name?.trim()) { errors.push('Album name cannot be empty if album is specified'); } if (!data.bestAlbum.tracks || data.bestAlbum.tracks <= 0) { errors.push('Album tracks must be greater than 0 if album is specified'); } } if (data.frontMan) { if (!data.frontMan.name?.trim()) { errors.push('Front man name cannot be empty if front man is specified'); } if (!data.frontMan.eyeColor) { errors.push('Front man eye color is required if front man is specified'); } if (!data.frontMan.height || data.frontMan.height <= 0) { errors.push('Front man height must be greater than 0 if front man is specified'); } if (!data.frontMan.nationality) { errors.push('Front man nationality is required if front man is specified'); } if (data.frontMan.location) { const loc = data.frontMan.location; if (loc.x === null || loc.x === undefined) { errors.push('Front man location X is required if location is specified'); } if (loc.y === null || loc.y === undefined) { errors.push('Front man location Y is required if location is specified'); } if (loc.z === null || loc.z === undefined) { errors.push('Front man location Z is required if location is specified'); } } } if (errors.length > 0) { this.showError('Validation failed:\n• ' + errors.join('\n• ')); return false; } return true; } highlightErrorFields(errorMessage) { const fieldMap = { 'name': 'bandName', 'genre': 'bandGenre', 'coordinates': ['coordinatesX', 'coordinatesY'], 'numberOfParticipants': 'numberOfParticipants', 'singlesCount': 'singlesCount', 'establishmentDate': 'establishmentDate' }; Object.entries(fieldMap).forEach(([keyword, fieldIds]) => { if (errorMessage.toLowerCase().includes(keyword.toLowerCase())) { const ids = Array.isArray(fieldIds) ? fieldIds : [fieldIds]; ids.forEach(id => { const field = document.getElementById(id); if (field) { field.style.borderColor = 'var(--error)'; field.style.boxShadow = '0 0 0 2px rgba(239,68,68,0.2)'; } }); } }); } clearFormErrors() { document.querySelectorAll('#bandForm input, #bandForm select, #bandForm textarea').forEach(field => { field.style.borderColor = ''; field.style.boxShadow = ''; }); } collectFormData() { const data = { name: document.getElementById('bandName').value.trim(), genre: document.getElementById('bandGenre').value, coordinates: { x: this.parseInteger(document.getElementById('coordinatesX').value), y: this.parseInteger(document.getElementById('coordinatesY').value) }, numberOfParticipants: this.parseInteger(document.getElementById('numberOfParticipants').value), singlesCount: this.parseInteger(document.getElementById('singlesCount').value), establishmentDate: this.convertToZonedDateTime(document.getElementById('establishmentDate').value) }; const albumsCount = this.parseInteger(document.getElementById('albumsCount').value); if (albumsCount !== null && albumsCount > 0) { data.albumsCount = albumsCount; } const description = document.getElementById('description').value.trim(); if (description) { data.description = description; } const albumName = document.getElementById('albumName').value.trim(); const albumTracks = this.parseInteger(document.getElementById('albumTracks').value); if (albumName && albumTracks && albumTracks > 0) { data.bestAlbum = { name: albumName, tracks: albumTracks }; } else if (albumName || (albumTracks && albumTracks > 0)) { data.bestAlbum = { name: albumName || '', tracks: albumTracks || 0 }; } const frontManName = document.getElementById('frontManName').value.trim(); const eyeColor = document.getElementById('eyeColor').value; const height = this.parseFloat(document.getElementById('height').value); const nationality = document.getElementById('nationality').value; if (frontManName || eyeColor || height || nationality) { data.frontMan = { name: frontManName, eyeColor, height, nationality }; const hairColor = document.getElementById('hairColor').value; if (hairColor) { data.frontMan.hairColor = hairColor; } const lx = this.parseInteger(document.getElementById('locationX').value); const ly = this.parseFloat(document.getElementById('locationY').value); const lz = this.parseInteger(document.getElementById('locationZ').value); if (lx !== null || ly !== null || lz !== null) { if (lx !== null && ly !== null && lz !== null) { data.frontMan.location = { x: lx, y: ly, z: lz }; } else { data.frontMan.location = { x: lx ?? 0, y: ly ?? 0.0, z: lz ?? 0 }; } } } return data; } parseInteger(value) { if (!value || value.trim() === '') return null; const parsed = parseInt(value, 10); return isNaN(parsed) ? null : parsed; } parseFloat(value) { if (!value || value.trim() === '') return null; const parsed = parseFloat(value); return isNaN(parsed) ? null : parsed; } async deleteBand(id) { const confirmed = await this.showConfirm( 'Delete Band', 'Are you sure you want to delete this band? This action cannot be undone and will also delete all related data.' ); if (!confirmed) return; try { await this.makeRequest(`${this.API_BASE}/music-bands/${id}`, { method: 'DELETE' }); if (this.currentPage > 0) { const bands = document.querySelectorAll('#bandsTableBody tr'); if (bands.length <= 1) { this.currentPage = Math.max(0, this.currentPage - 1); } } await this.loadBands(); this.showSuccess('Band deleted successfully!'); } catch (error) { this.showError(error.message); } } closeModal() { document.getElementById('bandModal').style.display = 'none'; this.clearFormErrors(); } async calculateAverageAlbums() { try { const data = await this.makeRequest(`${this.API_BASE}/music-bands/statistics/average-albums`); const avg = (data.averageAlbumsCount ?? 0).toFixed(2); document.getElementById('avgAlbumsCount').textContent = avg; this.showSuccess(`Average albums count: ${avg}`); } catch (error) { this.showError(error.message); } } async findBandWithMaxName() { try { const data = await this.makeRequest(`${this.API_BASE}/music-bands/statistics/max-name`); const name = data.name ?? 'No bands found'; document.getElementById('maxNameBand').textContent = name; this.showSuccess(`Band with max name: ${name}`); } catch (error) { this.showError(error.message); } } async groupByParticipants() { try { const data = await this.makeRequest(`${this.API_BASE}/music-bands/statistics/group-by-participants`); const lines = Object.entries(data).map(([participants, count]) => `${participants} participants: ${count} band${count !== 1 ? 's' : ''}` ); document.getElementById('participantsGroups').innerHTML = lines.length > 0 ? lines.join('
') : 'No data available'; this.showSuccess('Bands grouped by participants'); } catch (error) { this.showError(error.message); } } async addSingle() { const bandId = this.parseInteger(document.getElementById('addSingleBandId').value); if (!bandId || bandId <= 0) { this.showError('Please enter a valid band ID'); return; } try { await this.makeRequest(`${this.API_BASE}/music-bands/${bandId}/add-single`, { method: 'POST' }); document.getElementById('addSingleBandId').value = ''; await this.loadBands(); this.showSuccess(`Single added to band #${bandId}!`); } catch (error) { this.showError(error.message); } } async removeParticipant() { const bandId = this.parseInteger(document.getElementById('removeParticipantBandId').value); if (!bandId || bandId <= 0) { this.showError('Please enter a valid band ID'); return; } try { await this.makeRequest(`${this.API_BASE}/music-bands/${bandId}/remove-participant`, { method: 'POST' }); document.getElementById('removeParticipantBandId').value = ''; await this.loadBands(); this.showSuccess(`Participant removed from band #${bandId}!`); } catch (error) { this.showError(error.message); } } async importFile() { const fileInput = document.getElementById('importFile'); const formatSelect = document.getElementById('importFormat'); if (!fileInput.files || fileInput.files.length === 0) { this.showError('Please select a file to import'); return; } const file = fileInput.files[0]; let format = formatSelect.value; if (!format) { const ext = file.name.split('.').pop().toLowerCase(); if (['json', 'xml', 'yaml', 'yml'].includes(ext)) { format = ext === 'yml' ? 'yaml' : ext; } else { this.showError('Could not detect file format. Please select format manually.'); return; } } try { const fileContent = await this.readFileAsText(file); const response = await fetch(`${this.API_BASE}/import?format=${format}`, { method: 'POST', headers: { 'Content-Type': 'text/plain', 'Authorization': `Bearer ${this.token}` }, body: fileContent }); const data = await response.json(); if (response.ok) { fileInput.value = ''; this.showSuccess(`Import completed successfully! ${data.objectsCount} bands imported.`); await this.loadBands(); } else { const errorMsg = data.error || 'Import failed'; const opId = data.operationId ? ` (Operation ID: ${data.operationId})` : ''; this.showError(`Import failed: ${errorMsg}${opId}`); } } catch (error) { this.showError(`Import error: ${error.message}`); } } readFileAsText(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = () => reject(new Error('Failed to read file')); reader.readAsText(file); }); } async loadImportHistory() { try { const history = await this.makeRequest(`${this.API_BASE}/import/history`); this.renderImportHistory(history); } catch (error) { this.showError(error.message); } } renderImportHistory(history) { const tbody = document.getElementById('historyTableBody'); tbody.innerHTML = ''; if (history.length === 0) { const row = document.createElement('tr'); row.innerHTML = 'No import history'; tbody.appendChild(row); return; } history.forEach((item) => { const row = document.createElement('tr'); const statusClass = item.status === 'SUCCESS' ? 'text-success' : 'text-error'; row.innerHTML = ` ${item.id} ${this.escapeHtml(item.username)} ${item.status} ${item.objectsCount ?? '—'} ${new Date(item.createdAt).toLocaleString()} ${item.errorMessage ? this.escapeHtml(item.errorMessage) : '—'} `; tbody.appendChild(row); }); } initializeWebSocket() { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { return; } const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${location.host}/is1/websocket/bands`; try { this.websocket = new WebSocket(wsUrl); this.websocket.onopen = () => { this.reconnectAttempts = 0; }; this.websocket.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'band_update') { this.handleBandUpdate(data.action); } } catch (e) {} }; this.websocket.onclose = (event) => { this.websocket = null; this.reconnectAttempts = (this.reconnectAttempts || 0) + 1; const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); setTimeout(() => this.initializeWebSocket(), delay); }; this.websocket.onerror = (error) => { if (this.websocket) { this.websocket.close(); } }; } catch (e) { setTimeout(() => this.initializeWebSocket(), 5000); } } handleBandUpdate(action) { if (['create', 'update', 'delete'].includes(action)) { this.loadBands(); if (action === 'create') { this.showInfo('New band added by another user'); } else if (action === 'update') { this.showInfo('Band updated by another user'); } else if (action === 'delete') { this.showInfo('Band deleted by another user'); } } } showError(message) { this.showToast(message, 'error'); } showSuccess(message) { this.showToast(message, 'success'); } showInfo(message) { this.showToast(message, 'info'); } showToast(message, type = 'info') { const el = this.toastEl; el.className = `toast ${type} show`; const icon = type === 'success' ? '✓' : type === 'error' ? '✗' : 'ℹ'; el.innerHTML = `${icon}${this.escapeHtml(message)}`; clearTimeout(this._toastTimer); this._toastTimer = setTimeout(() => { el.classList.remove('show'); }, type === 'error' ? 6000 : 4000); } async showConfirm(title, message) { return new Promise((resolve) => { const modal = document.createElement('div'); modal.className = 'modal'; modal.style.display = 'block'; modal.innerHTML = ` `; const handleResponse = (result) => { document.body.removeChild(modal); resolve(result); }; modal.querySelector('.cancel-btn').addEventListener('click', () => handleResponse(false)); modal.querySelector('.confirm-btn').addEventListener('click', () => handleResponse(true)); modal.addEventListener('click', (e) => { if (e.target === modal) handleResponse(false); }); document.body.appendChild(modal); }); } } const app = new MusicBandApp();