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 = `
${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 = `
${this.escapeHtml(title)}
${this.escapeHtml(message)}
`;
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();