// js/SupervisorPanel.js import { CheckinList } from './components/CheckinList.js'; import { TeamSummary } from './components/TeamSummary.js'; import { PointsCalculator } from './utils/PointsCalculator.js'; import { DateFormatter } from './utils/DateFormatter.js'; import { NotificationService } from './services/NotificationService.js'; export class SupervisorPanel { constructor({ element, template, apiClient, eventBus }) { this.element = element; this.template = template; this.apiClient = apiClient; this.eventBus = eventBus; this.notification = new NotificationService(); this.pointsCalculator = new PointsCalculator(); this.state = { currentEvent: null, currentZekken: null, teamData: null, checkins: [] }; } async initialize() { this.render(); this.initializeComponents(); this.bindEvents(); await this.loadInitialData(); } render() { this.element.innerHTML = this.template.innerHTML; // コンポーネントの初期化 this.checkinList = new CheckinList({ element: document.getElementById('checkinList'), onUpdate: this.handleCheckinUpdate.bind(this) }); this.teamSummary = new TeamSummary({ element: document.getElementById('team-summary'), onGoalTimeUpdate: this.handleGoalTimeUpdate.bind(this) }); } initializeComponents() { // Sortable.jsの初期化 new Sortable(document.getElementById('checkinList'), { animation: 150, onEnd: this.handlePathOrderChange.bind(this) }); } bindEvents() { // イベント選択 document.getElementById('eventCode').addEventListener('change', this.handleEventChange.bind(this)); // ゼッケン番号選択 document.getElementById('zekkenNumber').addEventListener('change', this.handleZekkenChange.bind(this)); // ボタンのイベントハンドラ document.getElementById('addCpButton').addEventListener('click', this.handleAddCP.bind(this)); document.getElementById('saveButton').addEventListener('click', this.handleSave.bind(this)); document.getElementById('exportButton').addEventListener('click', this.handleExport.bind(this)); } async loadInitialData() { try { const events = await this.apiClient.getEvents(); this.populateEventSelect(events); } catch (error) { this.notification.showError('イベントの読み込みに失敗しました'); } } async handleEventChange(event) { const eventCode = event.target.value; if (!eventCode) return; try { const zekkenNumbers = await this.apiClient.getZekkenNumbers(eventCode); this.populateZekkenSelect(zekkenNumbers); this.state.currentEvent = eventCode; } catch (error) { this.notification.showError('ゼッケン番号の読み込みに失敗しました'); } } async handleZekkenChange(event) { const zekkenNumber = event.target.value; if (!zekkenNumber || !this.state.currentEvent) return; try { const [teamData, checkins] = await Promise.all([ this.apiClient.getTeamInfo(zekkenNumber), this.apiClient.getCheckins(zekkenNumber, this.state.currentEvent) ]); this.state.currentZekken = zekkenNumber; this.state.teamData = teamData; this.state.checkins = checkins; this.updateUI(); } catch (error) { this.notification.showError('チームデータの読み込みに失敗しました'); } } async handleGoalTimeUpdate(newTime) { if (!this.state.teamData) return; try { const response = await this.apiClient.updateTeamGoalTime( this.state.currentZekken, newTime ); this.state.teamData.end_datetime = newTime; this.validateGoalTime(); this.teamSummary.update(this.state.teamData); } catch (error) { this.notification.showError('ゴール時刻の更新に失敗しました'); } } async handleCheckinUpdate(checkinId, updates) { try { const response = await this.apiClient.updateCheckin(checkinId, updates); const index = this.state.checkins.findIndex(c => c.id === checkinId); if (index !== -1) { this.state.checkins[index] = { ...this.state.checkins[index], ...updates }; this.calculatePoints(); this.updateUI(); } } catch (error) { this.notification.showError('チェックインの更新に失敗しました'); } } async handlePathOrderChange(event) { const newOrder = Array.from(event.to.children).map((element, index) => ({ id: element.dataset.id, path_order: index + 1 })); try { await this.apiClient.updatePathOrders(newOrder); this.state.checkins = this.state.checkins.map(checkin => { const orderUpdate = newOrder.find(update => update.id === checkin.id); return orderUpdate ? { ...checkin, path_order: orderUpdate.path_order } : checkin; }); } catch (error) { this.notification.showError('走行順の更新に失敗しました'); } } async handleAddCP() { try { const newCP = await this.showAddCPModal(); if (!newCP) return; const response = await this.apiClient.addCheckin( this.state.currentZekken, newCP ); this.state.checkins.push(response); this.updateUI(); } catch (error) { this.notification.showError('CPの追加に失敗しました'); } } async handleSave() { try { await this.apiClient.saveAllChanges({ zekkenNumber: this.state.currentZekken, checkins: this.state.checkins, teamData: this.state.teamData }); this.notification.showSuccess('保存が完了しました'); } catch (error) { this.notification.showError('保存に失敗しました'); } } handleExport() { if (!this.state.currentZekken) { this.notification.showError('ゼッケン番号を選択してください'); return; } const exportUrl = `${this.apiClient.baseUrl}/export-excel/${this.state.currentZekken}`; window.open(exportUrl, '_blank'); } validateGoalTime() { if (!this.state.teamData || !this.state.teamData.end_datetime) return; const endTime = new Date(this.state.teamData.end_datetime); const eventEndTime = new Date(this.state.teamData.event_end_time); const timeDiff = (endTime - eventEndTime) / (1000 * 60); this.state.teamData.validation = { status: timeDiff <= 15 ? '合格' : '失格', latePoints: timeDiff > 15 ? Math.floor(timeDiff - 15) * -50 : 0 }; } calculatePoints() { const points = this.pointsCalculator.calculate({ checkins: this.state.checkins, latePoints: this.state.teamData?.validation?.latePoints || 0 }); this.state.points = points; } updateUI() { // チーム情報の更新 this.teamSummary.update(this.state.teamData); // チェックインリストの更新 this.checkinList.update(this.state.checkins); // ポイントの再計算と表示 this.calculatePoints(); this.updatePointsDisplay(); } updatePointsDisplay() { const { totalPoints, buyPoints, latePoints, finalPoints } = this.state.points; document.getElementById('totalPoints').textContent = totalPoints; document.getElementById('buyPoints').textContent = buyPoints; document.getElementById('latePoints').textContent = latePoints; document.getElementById('finalPoints').textContent = finalPoints; } populateEventSelect(events) { const select = document.getElementById('eventCode'); select.innerHTML = ''; events.forEach(event => { const option = document.createElement('option'); option.value = event.code; option.textContent = this.escapeHtml(event.name); select.appendChild(option); }); } populateZekkenSelect(numbers) { const select = document.getElementById('zekkenNumber'); select.innerHTML = ''; numbers.forEach(number => { const option = document.createElement('option'); option.value = number; option.textContent = this.escapeHtml(number.toString()); select.appendChild(option); }); } escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } }