diff --git a/.env.swp b/.env.swp
deleted file mode 100644
index 0115a92..0000000
Binary files a/.env.swp and /dev/null differ
diff --git a/config/settings.py b/config/settings.py
index 6c6985a..8edce1c 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -48,6 +48,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'django.contrib.gis',
'rest_framework',
+ 'rest_framework.authtoken',
'rest_framework_gis',
'knox',
'leaflet',
@@ -215,7 +216,10 @@ LEAFLET_CONFIG = {
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
- 'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication', ),
+ 'DEFAULT_AUTHENTICATION_CLASSES': ['knox.auth.TokenAuthentication','rest_framework.authentication.TokenAuthentication', ],
+ 'DEFAULT_PERMISSION_CLASSES': [
+ 'rest_framework.permissions.IsAuthenticated',
+ ],
}
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 44e07a1..7bda0b7 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -53,7 +53,9 @@ services:
- type: volume
source: nginx_logs
target: /var/log/nginx
- - media_data:/app/media:ro
+ - type: bind
+ source: ./media
+ target: /usr/share/nginx/html/media
ports:
- "80:80"
depends_on:
@@ -73,4 +75,3 @@ volumes:
geoserver-data:
static_volume:
nginx_logs:
- media_data:
diff --git a/rog/.serializers.py.swp b/rog/.serializers.py.swp
deleted file mode 100644
index cb59a7d..0000000
Binary files a/rog/.serializers.py.swp and /dev/null differ
diff --git a/rog/.urls.py.swp b/rog/.urls.py.swp
deleted file mode 100644
index 6b690aa..0000000
Binary files a/rog/.urls.py.swp and /dev/null differ
diff --git a/rog/.views.py.swp b/rog/.views.py.swp
deleted file mode 100644
index d010457..0000000
Binary files a/rog/.views.py.swp and /dev/null differ
diff --git a/rog/models.py b/rog/models.py
index 4339589..d418fea 100644
--- a/rog/models.py
+++ b/rog/models.py
@@ -323,6 +323,8 @@ class NewEvent2(models.Model):
class_solo_male = models.BooleanField(default=True)
class_solo_female = models.BooleanField(default=True)
+ self_rogaining = models.BooleanField(default=False)
+
def __str__(self):
return f"{self.event_name} - From:{self.start_datetime} To:{self.end_datetime}"
diff --git a/rog/views.py b/rog/views.py
index 16a4e23..9db0b8d 100644
--- a/rog/views.py
+++ b/rog/views.py
@@ -40,6 +40,7 @@ from rest_framework.response import Response
from rest_framework.parsers import JSONParser, MultiPartParser
from .serializers import LocationSerializer
from django.http import JsonResponse
+from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from django.contrib.gis.db.models import Extent, Union
@@ -52,7 +53,7 @@ from django.db.models import Q
from rest_framework import permissions
from rest_framework.views import APIView
-from rest_framework.decorators import api_view, permission_classes
+from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.parsers import JSONParser, MultiPartParser
from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import render
@@ -2318,6 +2319,8 @@ class UserLastGoalTimeView(APIView):
# ----- for Supervisor -----
@api_view(['GET'])
+@authentication_classes([TokenAuthentication])
+@permission_classes([IsAuthenticated])
def debug_urls(request):
"""デバッグ用:利用可能なURLパターンを表示"""
resolver = get_resolver()
@@ -2330,6 +2333,8 @@ def debug_urls(request):
return JsonResponse({'urls': urls})
@api_view(['GET'])
+@authentication_classes([TokenAuthentication])
+@permission_classes([IsAuthenticated])
def get_events(request):
logger.debug(f"get_events was called. Path: {request.path}")
try:
@@ -2340,6 +2345,7 @@ def get_events(request):
'name': event.event_name,
'start_datetime': event.start_datetime,
'end_datetime': event.end_datetime,
+ 'self_rogaining': event.self_rogaining,
} for event in events])
logger.debug(f"Returning data: {data}") # デバッグ用ログ
return JsonResponse(data, safe=False)
@@ -2349,7 +2355,10 @@ def get_events(request):
{"error": "Failed to retrieve events"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
+
@api_view(['GET'])
+@authentication_classes([TokenAuthentication])
+@permission_classes([IsAuthenticated])
def get_zekken_numbers(request, event_code):
entries = Entry.objects.filter(
event__event_name=event_code,
@@ -2358,6 +2367,8 @@ def get_zekken_numbers(request, event_code):
return Response([entry.zekken_number for entry in entries])
@api_view(['GET'])
+@authentication_classes([TokenAuthentication])
+@permission_classes([IsAuthenticated])
def get_team_info(request, zekken_number):
entry = Entry.objects.select_related('team','event').get(zekken_number=zekken_number)
members = Member.objects.filter(team=entry.team)
@@ -2373,7 +2384,8 @@ def get_team_info(request, zekken_number):
'members': ', '.join([f"{m.lastname} {m.firstname}" for m in members]),
'event_code': entry.event.event_name,
'start_datetime': entry.event.start_datetime,
- 'end_datetime': goal_record.goaltime if goal_record else None
+ 'end_datetime': goal_record.goaltime if goal_record else None,
+ 'self_rogaining': entry.event.self_rogaining,
})
def create(self, request, *args, **kwargs):
@@ -2396,6 +2408,8 @@ def get_image_url(image_path):
@api_view(['GET'])
+@authentication_classes([TokenAuthentication])
+@permission_classes([IsAuthenticated])
def get_checkins(request, *args, **kwargs):
#def get_checkins(request, zekken_number, event_code):
try:
@@ -2442,20 +2456,19 @@ def get_checkins(request, *args, **kwargs):
data.append({
'id': c.id,
- 'path_order': c.path_order,
- 'cp_number': c.cp_number,
- 'sub_loc_id': location.sub_loc_id if location else f"#{c.cp_number}",
- 'location_name': location.location_name if location else None,
- 'create_at': formatted_time, #(c.create_at + timedelta(hours=9)).strftime('%H:%M:%S') if c.create_at else None,
- 'validate_location': c.validate_location,
- 'points': c.points or 0,
- 'buy_flag': c.buy_flag,
- 'photos': location.photos if location else None,
- 'image_address': get_image_url(c.image_address),
- 'receipt_address': get_image_url(c.image_receipt),
- 'location_name': location.location_name if location else None,
- 'checkin_point': location.checkin_point if location else None,
- 'buy_point': location.buy_point
+ 'path_order': c.path_order, # 通過順序
+ 'cp_number': c.cp_number, # 通過ポイント
+ 'sub_loc_id': location.sub_loc_id if location else f"#{c.cp_number}", # アプリ上のチェックポイント番号+点数
+ 'location_name': location.location_name if location else None, # アプリ上のチェックポイント名
+ 'create_at': formatted_time, # 通過時刻
+ 'validate_location': c.validate_location, # 通過審査結果
+ 'points': c.points or 0, # 審査後の公式得点
+ 'buy_flag': c.buy_flag, # お買い物撮影で TRUE
+ 'photos': location.photos if location else None, # アプリ上の規定写真
+ 'image_address': get_image_url(c.image_address), # 撮影写真
+ 'receipt_address': get_image_url(c.image_receipt), # まだ使われていない
+ 'checkin_point': location.checkin_point if location else None, # アプリ上の規定ポイント
+ 'buy_point': location.buy_point if location else None, # アプリ上の規定買い物ポイント
})
#logger.debug(f"data={data}")
@@ -2469,6 +2482,8 @@ def get_checkins(request, *args, **kwargs):
)
@api_view(['POST'])
+@authentication_classes([TokenAuthentication])
+@permission_classes([IsAuthenticated])
def update_checkins(request):
with transaction.atomic():
for update in request.data:
@@ -2479,6 +2494,8 @@ def update_checkins(request):
return Response({'status': 'success'})
@api_view(['GET'])
+@authentication_classes([TokenAuthentication])
+@permission_classes([IsAuthenticated])
def export_excel(request, zekken_number):
# エントリー情報の取得
entry = Entry.objects.select_related('team').get(zekken_number=zekken_number)
@@ -2528,6 +2545,8 @@ def export_excel(request, zekken_number):
# ----- for Supervisor -----
@api_view(['GET'])
+@authentication_classes([TokenAuthentication])
+@permission_classes([IsAuthenticated])
def test_api(request):
logger.debug("Test API endpoint called")
return JsonResponse({"status": "API is working"})
diff --git a/supervisor/html/.index.html.swp b/supervisor/html/.index.html.swp
deleted file mode 100644
index 56342d9..0000000
Binary files a/supervisor/html/.index.html.swp and /dev/null differ
diff --git a/supervisor/html/index.html b/supervisor/html/index.html
index 7fed476..d7db96f 100755
--- a/supervisor/html/index.html
+++ b/supervisor/html/index.html
@@ -6,6 +6,7 @@
スーパーバイザーパネル
+
@@ -17,13 +18,13 @@
@@ -42,9 +43,14 @@
スタート時刻
+
ゴール時計
@@ -80,14 +86,16 @@
+ |
走行順 |
規定写真 |
撮影写真 |
- CP名称 |
+ CP名称 |
通過時刻 |
通過審査 |
- 買物審査 |
+ 買物 |
獲得点数 |
+ |
@@ -115,9 +123,38 @@
// APIのベースURLを環境に応じて設定
const API_BASE_URL = '/api';
+ async function login(email, password) {
+ try {
+ const response = await fetch(`${API_BASE_URL}/login/`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ email: email,
+ password: password
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Login failed');
+ }
+
+ const data = await response.json();
+ localStorage.setItem('authToken', data.token);
+ window.location.href = '/supervisor/'; // スーパーバイザーパネルへリダイレクト
+
+ } catch (error) {
+ console.error('Login error:', error);
+ alert('ログインに失敗しました。');
+ }
+ }
+ // イベントリスナーの設定
document.addEventListener('DOMContentLoaded', function() {
- // Sortable初期化
+ if (!checkAuthStatus()) return;
+
+ // Sortable初期化これで、通過順序を変更できる
const checkinList = document.getElementById('checkinList');
new Sortable(checkinList, {
animation: 150,
@@ -126,7 +163,7 @@
}
});
- // イベントコードのドロップダウン要素を取得
+ // 選択されたイベントコード・ゼッケン番号を取得
const eventCodeSelect = document.getElementById('eventCode');
const zekkenNumberSelect = document.getElementById('zekkenNumber');
@@ -137,20 +174,23 @@
alert('イベントコードを選択してください');
return;
}
- loadTeamData(e.target.value, eventCode);
+ loadTeamData(e.target.value, eventCode); // チームデータのロード
});
// イベントコード変更時の処理
document.getElementById('eventCode').addEventListener('change', function(e) {
- loadZekkenNumbers(e.target.value);
+ loadZekkenNumbers(e.target.value); // ゼッケン番号をロードする
});
// チェックボックス変更時の処理
- checkinList.addEventListener('change', function(e) {
- if (e.target.type === 'checkbox') {
- updateValidation(e.target);
- }
- });
+ //checkinList.addEventListener('change', function(e) {
+ // if (e.target.type === 'checkbox') {
+ // //updateValidation(e.target);
+ // updatePoints(e.target)
+ // }else if(e.target.type === 'buy_checkbox' ) {
+ // updateBuyPoints(e.target)
+ // }
+ //});
// 保存ボタンの処理
document.getElementById('saveButton').addEventListener('click', saveChanges);
@@ -163,10 +203,190 @@
loadEventCodes();
});
+ // Get auth token from localStorage or wherever it's stored
+ function getAuthToken() {
+ return localStorage.getItem('authToken'); // または sessionStorage から
+ }
+
+ // editGoalTime関数の修正
+ function editGoalTime(element) {
+ const container = element.closest('.goal-time-container');
+ if (!container) {
+ console.error('Goal time container not found');
+ return;
+ }
+
+ const display = container.querySelector('.goal-time-display');
+ const input = container.querySelector('.goal-time-input');
+
+ if (!display || !input) {
+ console.error('Goal time elements not found');
+ return;
+ }
+
+ if (display.textContent && display.textContent !== '-') {
+ try {
+ const date = new Date(display.textContent);
+ input.value = date.toISOString().slice(0, 16);
+ } catch (e) {
+ console.error('Error parsing date:', e);
+ }
+ }
+
+ display.classList.add('hidden');
+ input.classList.remove('hidden');
+ input.focus();
+ }
+
+ // ゴール時刻編集機能
+ function old_editGoalTime(element) {
+ const display = element.getElementById('goalTimeDisplay');
+ const input = element.getElementById('goalTimeInput');
+
+
+ if (!display || !input) {
+ console.error('Goal time elements not found');
+ return;
+ }
+
+ // 現在の表示時刻をinputの初期値として設定
+ const currentTime = display.textContent;
+ if (currentTime && currentTime !== '-') {
+ try {
+ const date = new Date(currentTime);
+ input.value = date.toISOString().slice(0, 16); // YYYY-MM-DDThh:mm 形式に変換
+ } catch (e) {
+ console.error('Error parsing date:', e);
+ }
+ }
+
+ display.classList.add('hidden');
+ input.classList.remove('hidden');
+ input.focus();
+
+ }
+
+ function updateGoalTime(input) {
+
+ const display = document.getElementById('goalTimeDisplay');
+ const validateElement = document.getElementById('validate');
+
+ if (!display) {
+ console.error('Goal time display element not found');
+ return;
+ }
+
+ try {
+ const newTime = new Date(input.value);
+ display.textContent = newTime.toLocaleString();
+
+ const eventCodeSelect = document.getElementById('eventCode');
+ const event_code = eventCodeSelect.value;
+ const zekkenNumberSelect = document.getElementById('zekkenNumber');
+ const zekkenNumber = zekkenNumberSelect.value
+
+ // イベントとチェックインデータを取得
+ fetch(`${API_BASE_URL}/get_team_info/${eventCode}`,{
+ headers: {
+ 'Authorization': `Token ${getAuthToken()}`,
+ 'Content-Type': 'application/json'
+ }
+ .then(response => response.json())
+ .then(team => {
+ if (team.self_rogaining) {
+ // セルフロゲイニングの場合
+ fetch(`${API_BASE_URL}/checkins/${zekkenNumber}/${eventCode}/`,
+ headers: {
+ 'Authorization': `Token ${getAuthToken()}`,
+ 'Content-Type': 'application/json'
+ }
+ .then(response => response.json())
+ .then(checkins => {
+ const startCheckin = checkins.find(c => c.cp_number === -1);
+ if (startCheckin) {
+ const startTime = new Date(startCheckin.create_at);
+ const timeDiff = (newTime - startTime) / (1000 * 60); // 分単位の差
+ const maxTime = event.duration + 15; // 制限時間+15分
+
+ updateValidation(timeDiff, maxTime);
+ }
+ })
+ .catch(error => handleApiError(error));
+ ;
+ } else {
+ // 通常のロゲイニングの場合
+ const startTime = new Date(event.start_datetime);
+ const timeDiff = (newTime - startTime) / (1000 * 60); // 分単位の差
+ const maxTime = (event.hour_3 ? 180 : event.hour_5 ? 300 : 0) + 15; // 3時間or5時間+15分
+
+ updateValidation(timeDiff, maxTime);
+ }
+ });
+
+ } catch (e) {
+ console.error('Error updating goal time:', e);
+ alert('無効な日時形式です。');
+ return;
+ }
+
+ display.classList.remove('hidden');
+ input.classList.add('hidden');
+
+
+ // 判定を入れる
+
+ }
+
+ // 判定の更新を行う補助関数
+ function updateValidation(timeDiff, maxTime) {
+ console.log('updateValidation',timeDiff,' > ',maxTime)
+ const validateElement = document.getElementById('validate');
+ if (validateElement) {
+ if (timeDiff > maxTime) {
+ validateElement.textContent = '失格';
+ validateElement.classList.add('text-red-600');
+ validateElement.classList.remove('text-green-600');
+ } else {
+ validateElement.textContent = '合格';
+ validateElement.classList.add('text-green-600');
+ validateElement.classList.remove('text-red-600');
+ }
+ }
+ }
+
+
+ function validateGoalTime(goalTime) {
+ const eventEndTime = new Date(document.getElementById('eventEndTime').value);
+ const goalDateTime = new Date(goalTime);
+ const timeDiff = (goalDateTime - eventEndTime) / (1000 * 60); // 分単位の差
+
+ const validateElement = document.getElementById('validate');
+ if (timeDiff > 15) {
+ validateElement.textContent = '失格';
+ validateElement.classList.add('text-red-600');
+ } else {
+ validateElement.textContent = '正常';
+ validateElement.classList.remove('text-red-600');
+ }
+ }
+
async function loadEventCodes() {
try {
- const response = await fetch(`${API_BASE_URL}/events/`);
+ const response = await fetch(`${API_BASE_URL}/new-events/`,{
+ headers: {
+ 'Authorization': `Token ${getAuthToken()}`,
+ 'Content-Type': 'application/json'
+ }
+ })
+ .catch(error => handleApiError(error));
+ ;
+
if (!response.ok) {
+ if (response.status === 401) {
+ // 認証エラーの場合はログインページにリダイレクト
+ window.location.href = '/login/';
+ return;
+ }
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get("content-type");
@@ -177,7 +397,7 @@
const data = await response.json();
const select = document.getElementById('eventCode');
// 既存のオプションをクリア
- select.innerHTML = '';
+ select.innerHTML = '';
data.forEach(event => {
const option = document.createElement('option');
@@ -195,28 +415,47 @@
}
}
+ // ゼッケン番号をロードする
function loadZekkenNumbers(eventCode) {
// APIからゼッケン番号を取得して選択肢を設定
- fetch(`${API_BASE_URL}/zekken_numbers/${eventCode}`)
+ fetch(`${API_BASE_URL}/zekken_numbers/${eventCode}`,{
+ headers: {
+ 'Authorization': `Token ${getAuthToken()}`
+ })
.then(response => response.json())
.then(data => {
const select = document.getElementById('zekkenNumber');
- select.innerHTML = '';
+ select.innerHTML = '';
data.forEach(number => {
const option = document.createElement('option');
option.value = number;
option.textContent = number;
select.appendChild(option);
});
- });
- }
+ })
+ .catch(error => handleApiError(error));
+
+ });
+ // チームデータのロード
async function loadTeamData(zekkenNumber,event_code) {
try {
const [teamResponse, checkinsResponse] = await Promise.all([
- fetch(`${API_BASE_URL}/team_info/${zekkenNumber}/`),
- fetch(`${API_BASE_URL}/checkins/${zekkenNumber}/${event_code}/`),
+ fetch(`${API_BASE_URL}/team_info/${zekkenNumber}/`, {
+ headers: {
+ 'Authorization': `Token ${getAuthToken()}`
+ }
+ })
+ .catch(error => handleApiError(error));
+ ;
+ fetch(`${API_BASE_URL}/checkins/${zekkenNumber}/${event_code}/`, {
+ headers: {
+ 'Authorization': `Token ${getAuthToken()}`
+ }
+ })
+ .catch(error => handleApiError(error));
+ ;
]);
// 各レスポンスのステータスを個別にチェック
@@ -228,18 +467,33 @@
const teamData = await teamResponse.json();
const checkinsData = await checkinsResponse.json();
-
+
+ // ゴール時刻の表示を更新
+ updateGoalTimeDisplay(teamData.end_datetime);
+
// イベントコードに対応するイベントを検索
+ // イベントがセルフなら、開始時刻はロゲ開始をタップした時刻。イベントなら、規定開始時刻。
//const event = eventData.find(e => e.code === document.getElementById('eventCode').value);
document.getElementById('teamName').textContent = teamData.team_name || '-';
document.getElementById('members').textContent = teamData.members || '-';
document.getElementById('startTime').textContent =
teamData.start_datetime ? new Date(teamData.start_datetime).toLocaleString() : '-';
- document.getElementById('goalTime').textContent =
- teamData.end_datetime ? new Date(teamData.end_datetime).toLocaleString() : '-';
+ //document.getElementById('goalTime').textContent =
+ // teamData.end_datetime ? new Date(teamData.end_datetime).toLocaleString() : '未ゴール';
//'(未ゴール)';
+ const goalTimeDisplay = document.getElementById('goalTimeDisplay');
+ const goalTime = teamData.end_datetime ?
+ new Date(teamData.end_datetime).toLocaleString() :
+ '未ゴール';
+ goalTimeDisplay.textContent = goalTime;
+
+ if (goalTime === '-') {
+ goalTimeDisplay.classList.add('cursor-pointer');
+ goalTimeDisplay.onclick = () => editGoalTime(goalTimeDisplay);
+ }
+
// チェックインリストの更新
const tbody = document.getElementById('checkinList');
tbody.innerHTML = ''; // 既存のデータをクリア
@@ -252,46 +506,50 @@
tr.dataset.id = checkin.id;
tr.dataset.cpNumber = checkin.cp_number;
- const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : 'bg-red-100';
+ const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : '';
tr.innerHTML = `
- ${checkin.path_order} |
-
+ |
+
+ |
+ ${checkin.path_order} |
+
${checkin.photos ?
` ` : ''}
- ${checkin.photos}
|
-
+ |
${checkin.image_address ?
` ` : ''}
- ${checkin.receipt_address && checkin.buy_flag ?
- ` ` : ''}
- ${checkin.image_address}
- ${checkin.receipt_address}
|
-
+ |
${checkin.sub_loc_id}
${checkin.location_name}
|
- ${checkin.create_at || '不明'} |
-
+ | ${checkin.create_at || '不明'} |
+
|
-
+ |
${checkin.buy_point > 0 ? `
+ class="h-4 w-4 text-green-600 rounded buy-checkbox" >
` : ''}
|
- ${checkin.points} |
+ ${calculatePointsForCheckin(checkin)} |
+
+
+ |
`;
tbody.appendChild(tr);
+ // 合計計算
if (checkin.points) {
if (checkin.buy_flag) {
buyPoints += checkin.points;
@@ -300,13 +558,14 @@
}
}
});
+ updatePathOrders();
// 合計ポイントの更新
document.getElementById('totalPoints').textContent = totalPoints;
document.getElementById('buyPoints').textContent = buyPoints;
- document.getElementById('latePoints').textContent = teamData.late_points || 0;
+ //document.getElementById('latePoints').textContent = teamData.late_points || 0;
document.getElementById('finalPoints').textContent =
- totalPoints + buyPoints - (teamData.late_points || 0);
+ totalPoints + buyPoints + (teamData.late_points || 0);
} catch (error) {
@@ -329,20 +588,64 @@
}
}
- function loadTeamData_old(zekkenNumber) {
- // チーム情報とチェックインデータを取得
- Promise.all([
- fetch(`${API_BASE_URL}/team_info/${zekkenNumber}`),
- fetch(`${API_BASE_URL}/checkins/${zekkenNumber}`)
- ]).then(responses => Promise.all(responses.map(r => r.json())))
- .then(([teamInfo, checkins]) => {
- updateTeamInfo(teamInfo);
- updateCheckinList(checkins);
- calculatePoints();
- });
+ // ゴール時刻表示を更新する関数
+ function updateGoalTimeDisplay(endDateTime) {
+ const goalTimeDisplay = document.getElementById('goalTimeDisplay');
+ const goalTimeInput = document.getElementById('goalTimeInput');
+
+ if (!goalTimeDisplay || !goalTimeInput) {
+ console.error('Goal time elements not found');
+ return;
+ }
+
+ let displayText = '-';
+ if (endDateTime) {
+ try {
+ const date = new Date(endDateTime);
+ displayText = date.toLocaleString();
+ // input要素の値も更新
+ goalTimeInput.value = date.toISOString().slice(0, 16);
+ } catch (e) {
+ console.error('Error formatting date:', e);
+ }
+ }
+
+ goalTimeDisplay.textContent = displayText;
+ goalTimeDisplay.onclick = () => editGoalTime(goalTimeDisplay);
+ }
+
+ // UI要素をリセットする関数
+ function resetUIElements() {
+ const elements = {
+ 'goalTimeDisplay': '-',
+ 'teamName': '-',
+ 'members': '-',
+ 'startTime': '-',
+ 'totalPoints': '0',
+ 'buyPoints': '0',
+ 'latePoints': '0',
+ 'finalPoints': '0'
+ };
+
+ for (const [id, defaultValue] of Object.entries(elements)) {
+ const element = document.getElementById(id);
+ if (element) {
+ element.textContent = defaultValue;
+ }
+ }
+
+ // チェックインリストをクリア
+ const checkinList = document.getElementById('checkinList');
+ if (checkinList) {
+ checkinList.innerHTML = '';
+ }
}
- function updateTeamInfo(teamInfo) {
+
+
+
+ // チーム情報の表示...使われていない
+ function nouse_updateTeamInfo(teamInfo) {
document.getElementById('teamName').textContent = teamInfo.team_name;
document.getElementById('members').textContent = teamInfo.members;
document.getElementById('startTime').textContent = teamInfo.start_time;
@@ -350,7 +653,9 @@
document.getElementById('latePoints').textContent = teamInfo.late_points;
}
- function updateCheckinList(checkins) {
+
+ // 通過リストの表示...呼ばれていない
+ function nouse_updateCheckinList(checkins) {
const tbody = document.getElementById('checkinList');
tbody.innerHTML = '';
@@ -359,9 +664,12 @@
tr.dataset.id = checkin.id;
tr.dataset.cpNumber = checkin.cp_number;
- const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : 'bg-red-100';
+ const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : '';
tr.innerHTML = `
+
+
+ |
${checkin.path_order} |
${checkin.sub_loc_id}
@@ -370,8 +678,6 @@
|
${checkin.image_address ?
` ` : ''}
- ${checkin.receipt_address && checkin.buy_flag ?
- ` ` : ''}
|
${checkin.create_at || '不明'} |
@@ -388,41 +694,181 @@
onchange="updateBuyPoints(this)">
` : ''}
|
- ${checkin.points} |
+ ${calculatePointsForCheckin(checkin)} |
+
+
+ |
`;
tbody.appendChild(tr);
});
- calculateTotalPoints();
+ updatePathOrders();
+ calculatePoints(); // 総合ポイントの計算
+ }
+
+
+ // 削除機能
+ async function deleteCheckin(id) {
+ if (!confirm('このチェックインを削除してもよろしいですか?')) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/checkins/${id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Token ${getAuthToken()}`
+ }
+ })
+ .catch(error => handleApiError(error));
+ ;
+
+ if (response.ok) {
+ const row = document.querySelector(`tr[data-id="${id}"]`);
+ row.remove();
+ calculatePoints(); // 総合ポイントを再計算
+ } else {
+ throw new Error('Delete failed');
+ }
+ } catch (error) {
+ console.error('Error deleting checkin:', error);
+ alert('削除に失敗しました');
+ }
}
- // ポイント更新関数
- function updatePoints(checkbox) {
+ // ポイント計算関数
+ function calculatePointsForCheckin(checkin) {
+ let points = 0;
+ if (checkin.validate_location) {
+ if(checkin.buy_flag){
+ points += Number(checkin.buy_point) || 0;
+ }else{
+ points += Number(checkin.checkin_point) || 0;
+ }
+ }
+ return points;
+ }
+
+ // 審査チェックボックス更新関数
+ function old_updatePoints(checkbox) {
+
const tr = checkbox.closest('tr');
const pointCell = tr.querySelector('.point-value');
const cpNumber = tr.dataset.cpNumber;
- fetch(`${API_BASE_URL}/location/${cpNumber}/`)
+ fetch(`${API_BASE_URL}/location/${cpNumber}/`,
+ headers: {
+ 'Authorization': `Token ${getAuthToken()}`
+ }
+ )
.then(response => response.json())
.then(location => {
const points = checkbox.checked ? location.checkin_point : 0;
pointCell.textContent = points;
- calculateTotalPoints();
- });
+ calculatePoints(); // 総合ポイントの計算
+ })
+ .catch(error => handleApiError(error));
+ ;
}
- // 買い物ポイント更新関数
- function updateBuyPoints(checkbox) {
+ // 審査チェックボックス更新関数
+ function updatePoints(checkbox) {
+ const tr = checkbox.closest('tr');
+ const pointCell = tr.querySelector('.point-value');
+ const cpNumber = tr.dataset.cpNumber;
+ const buyCheckbox = tr.querySelector('.buy-checkbox');
+ let checkin = {
+ validate_location: checkbox.checked,
+ buy_flag: buyCheckbox ? buyCheckbox.checked : false,
+ points: 0
+ };
+
+ fetch(`${API_BASE_URL}/location/${cpNumber}/`,
+ headers: {
+ 'Authorization': `Token ${getAuthToken()}`
+ }
+ )
+ .then(response => response.json())
+ .then(location => {
+ if (checkin.validate_location) {
+ // チェックボックスがONの場合
+ if (checkin.buy_flag) {
+ checkin.points = location.buy_point || 0;
+ } else {
+ checkin.points = location.checkin_point || 0;
+ }
+ } else {
+ // チェックボックスがOFFの場合
+ checkin.points = 0;
+ }
+
+ // ポイントを表示
+ pointCell.textContent = checkin.points;
+ calculatePoints(); // 総合ポイントの計算
+
+ // APIに更新を送信
+ // updateCheckinOnServer(cpNumber, checkin);
+ })
+ .catch(error => handleApiError(error));
+ ;
+ }
+
+ // 買い物チェックボックス更新関数
+ function old_updateBuyPoints(checkbox) {
const tr = checkbox.closest('tr');
const pointCell = tr.querySelector('.point-value');
const cpNumber = tr.dataset.cpNumber;
- fetch(`${API_BASE_URL}/location/${cpNumber}/`)
+ fetch(`${API_BASE_URL}/location/${cpNumber}/`,
+ headers: {
+ 'Authorization': `Token ${getAuthToken()}`
+ }
+ )
.then(response => response.json())
.then(location => {
const points = checkbox.checked ? location.buy_point : 0;
pointCell.textContent = points;
- calculateTotalPoints();
- });
+ calculatePoints(); // 総合ポイントの計算
+ })
+ .catch(error => handleApiError(error));
+ ;
+ }
+
+ // 買い物チェックボックス更新関数
+ function updateBuyPoints(checkbox) {
+ const tr = checkbox.closest('tr');
+ const pointCell = tr.querySelector('.point-value');
+ const cpNumber = tr.dataset.cpNumber;
+ const validateCheckbox = tr.querySelector('.validate-checkbox');
+ let checkin = {
+ validate_location: validateCheckbox.checked,
+ buy_flag: checkbox.checked,
+ points: 0
+ };
+
+ fetch(`${API_BASE_URL}/location/${cpNumber}/`,
+ headers: {
+ 'Authorization': `Token ${getAuthToken()}`
+ }
+ )
+ .then(response => response.json())
+ .then(location => {
+ if (checkin.validate_location) {
+ // 通過審査がONの場合
+ checkin.points = checkbox.checked ? location.buy_point : location.checkin_point;
+ } else {
+ // 通過審査がOFFの場合
+ checkin.points = 0;
+ }
+
+ // ポイントを表示
+ pointCell.textContent = checkin.points;
+ calculatePoints(); // 総合ポイントの計算
+ })
+ .catch(error => handleApiError(error));
+ ;
}
// 画像拡大表示用のモーダル関数
@@ -484,6 +930,7 @@
method: 'POST',
headers: {
'Content-Type': 'application/json',
+ 'Authorization': `Token ${getAuthToken()}`
},
body: JSON.stringify({
zekken_number: currentZekkenNumber,
@@ -492,9 +939,11 @@
})
.then(response => response.json())
.then(data => {
- loadTeamData(currentZekkenNumber,eventCode);
+ loadTeamData(currentZekkenNumber,eventCode); // チームデータのロード
closeModal();
- });
+ })
+ .catch(error => handleApiError(error));
+ ;
}
function closeModal() {
@@ -505,15 +954,17 @@
function updatePathOrders() {
const rows = Array.from(document.getElementById('checkinList').children);
rows.forEach((row, index) => {
- row.children[0].textContent = index + 1;
+ row.children[1].textContent = index + 1;
});
}
+ // 総合ポイントの計算
function calculatePoints() {
const rows = Array.from(document.getElementById('checkinList').children);
- let totalPoints = 0;
- let buyPoints = 0;
+ let totalPoints = 0; // チェックインポイントの合計をクリア
+ let buyPoints = 0; // 買い物ポイントの合計をクリア
+ // 各行のチェックインポイント及び買い物ポイントを合算
rows.forEach(row => {
const points = parseInt(row.children[4].textContent);
if (!isNaN(points)) {
@@ -524,8 +975,13 @@
}
});
+ // 遅刻ポイントの計算=ゴール時刻がEventのゴール時刻を超えていたら1分につき-50点を加算する。
const latePoints = parseInt(document.getElementById('latePoints').textContent) || 0;
- const finalPoints = totalPoints + buyPoints - latePoints;
+
+ // 総合得点を計算
+ const finalPoints = totalPoints + buyPoints - latePoints;
+
+ // 判定を更新。順位を表示、ゴール時刻を15分経過したら失格
document.getElementById('totalPoints').textContent = totalPoints;
document.getElementById('buyPoints').textContent = buyPoints;
@@ -548,6 +1004,7 @@
method: 'POST',
headers: {
'Content-Type': 'application/json',
+ 'Authorization': `Token ${getAuthToken()}`
},
body: JSON.stringify(updates)
}).then(response => {
@@ -556,9 +1013,34 @@
} else {
alert('保存に失敗しました');
}
- });
+ })
+ .catch(error => handleApiError(error));
+ ;
}
+ // エラーハンドリング関数
+ function handleApiError(error) {
+ if (error.response && error.response.status === 401) {
+ // 認証エラーの場合
+ window.location.href = '/login/';
+ } else {
+ // その他のエラー
+ alert('データの取得に失敗しました。');
+ console.error('API Error:', error);
+ }
+ }
+
+ // ログイン状態の確認
+ function checkAuthStatus() {
+ const token = getAuthToken();
+ if (!token) {
+ window.location.href = '/login/';
+ return false;
+ }
+ return true;
+ }
+
+
function exportExcel() {
const zekkenNumber = document.getElementById('zekkenNumber').value;
window.location.href = `/api/export-excel/${zekkenNumber}`;
diff --git a/supervisor/html/index.html.new b/supervisor/html/index.html.new
new file mode 100644
index 0000000..ca9386b
--- /dev/null
+++ b/supervisor/html/index.html.new
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+ スーパーバイザーパネル
+
+
+
+
+
+
+
+
+
+
+
スーパーバイザーパネル
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ 走行順 |
+ 規定写真 |
+ 撮影写真 |
+ CP名称 |
+ 通過時刻 |
+ 通過審査 |
+ 買物 |
+ 獲得点数 |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/supervisor/html/js/ApiClient.js b/supervisor/html/js/ApiClient.js
new file mode 100644
index 0000000..0937aae
--- /dev/null
+++ b/supervisor/html/js/ApiClient.js
@@ -0,0 +1,72 @@
+// js/ApiClient.js
+export class ApiClient {
+ constructor({ baseUrl, authToken, csrfToken }) {
+ this.baseUrl = baseUrl;
+ this.authToken = authToken;
+ this.csrfToken = csrfToken;
+ }
+
+ async request(endpoint, options = {}) {
+ const url = `${this.baseUrl}${endpoint}`;
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Token ${this.authToken}`,
+ 'X-CSRF-Token': this.csrfToken
+ };
+
+ try {
+ const response = await fetch(url, {
+ ...options,
+ headers: {
+ ...headers,
+ ...options.headers
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const contentType = response.headers.get("content-type");
+ if (contentType && contentType.includes("application/json")) {
+ return await response.json();
+ }
+
+ return await response.text();
+ } catch (error) {
+ console.error('API request failed:', error);
+ throw error;
+ }
+ }
+
+ // イベント関連のAPI
+ async getEvents() {
+ return this.request('/new-events/');
+ }
+
+ async getZekkenNumbers(eventCode) {
+ return this.request(`/zekken_numbers/${eventCode}`);
+ }
+
+ // チーム関連のAPI
+ async getTeamInfo(zekkenNumber) {
+ return this.request(`/team_info/${zekkenNumber}/`);
+ }
+
+ async getCheckins(zekkenNumber, eventCode) {
+ return this.request(`/checkins/${zekkenNumber}/${eventCode}/`);
+ }
+
+ async updateCheckin(checkinId, data) {
+ return this.request(`/checkins/${checkinId}`, {
+ method: 'PUT',
+ body: JSON.stringify(data)
+ });
+ }
+
+ async deleteCheckin(checkinId) {
+ return this.request(`/checkins/${checkinId}`, {
+ method: 'DELETE'
+ });
+ }
+}
diff --git a/supervisor/html/js/SupervisorPanel.js b/supervisor/html/js/SupervisorPanel.js
new file mode 100644
index 0000000..068c2f7
--- /dev/null
+++ b/supervisor/html/js/SupervisorPanel.js
@@ -0,0 +1,276 @@
+// 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;
+ }
+}
diff --git a/supervisor/html/js/main.js b/supervisor/html/js/main.js
new file mode 100644
index 0000000..bf90c94
--- /dev/null
+++ b/supervisor/html/js/main.js
@@ -0,0 +1,171 @@
+// js/main.js
+
+// EventBus
+const EventBus = {
+ listeners: {},
+
+ on(event, callback) {
+ if (!this.listeners[event]) {
+ this.listeners[event] = [];
+ }
+ this.listeners[event].push(callback);
+ },
+
+ emit(event, data) {
+ if (this.listeners[event]) {
+ this.listeners[event].forEach(callback => callback(data));
+ }
+ }
+};
+
+// NotificationService
+class NotificationService {
+ constructor() {
+ this.toastElement = document.getElementById('toast');
+ }
+
+ showMessage(message, type = 'info') {
+ this.toastElement.textContent = message;
+ this.toastElement.className = `fixed bottom-4 right-4 px-6 py-3 rounded shadow-lg ${
+ type === 'error' ? 'bg-red-500' : 'bg-green-500'
+ } text-white`;
+
+ this.toastElement.classList.remove('hidden');
+
+ setTimeout(() => {
+ this.toastElement.classList.add('hidden');
+ }, 3000);
+ }
+
+ showError(message) {
+ this.showMessage(message, 'error');
+ }
+
+ showSuccess(message) {
+ this.showMessage(message, 'success');
+ }
+}
+
+// ApiClient
+class ApiClient {
+ constructor({ baseUrl, authToken, csrfToken }) {
+ this.baseUrl = baseUrl;
+ this.authToken = authToken;
+ this.csrfToken = csrfToken;
+ }
+
+ async request(endpoint, options = {}) {
+ const url = `${this.baseUrl}${endpoint}`;
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Token ${this.authToken}`,
+ 'X-CSRF-Token': this.csrfToken
+ };
+
+ try {
+ const response = await fetch(url, {
+ ...options,
+ headers: {
+ ...headers,
+ ...options.headers
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const contentType = response.headers.get("content-type");
+ if (contentType && contentType.includes("application/json")) {
+ return await response.json();
+ }
+
+ return await response.text();
+ } catch (error) {
+ console.error('API request failed:', error);
+ throw error;
+ }
+ }
+
+ // API methods
+ async getEvents() {
+ return this.request('/new-events/');
+ }
+
+ async getZekkenNumbers(eventCode) {
+ return this.request(`/zekken_numbers/${eventCode}`);
+ }
+
+ async getTeamInfo(zekkenNumber) {
+ return this.request(`/team_info/${zekkenNumber}/`);
+ }
+
+ // ... その他のAPI methods
+}
+
+// PointsCalculator
+class PointsCalculator {
+ calculate({ checkins, latePoints = 0 }) {
+ const totalPoints = this.calculateTotalPoints(checkins);
+ const buyPoints = this.calculateBuyPoints(checkins);
+
+ return {
+ totalPoints,
+ buyPoints,
+ latePoints,
+ finalPoints: totalPoints + buyPoints + latePoints
+ };
+ }
+
+ calculateTotalPoints(checkins) {
+ return checkins.reduce((total, checkin) => {
+ if (checkin.validate_location && !checkin.buy_flag) {
+ return total + (checkin.checkin_point || 0);
+ }
+ return total;
+ }, 0);
+ }
+
+ calculateBuyPoints(checkins) {
+ return checkins.reduce((total, checkin) => {
+ if (checkin.validate_location && checkin.buy_flag) {
+ return total + (checkin.buy_point || 0);
+ }
+ return total;
+ }, 0);
+ }
+}
+
+// SupervisorPanel - メインアプリケーションクラス
+class SupervisorPanel {
+ constructor(options) {
+ this.element = options.element;
+ this.apiClient = new ApiClient(options.apiConfig);
+ this.notification = new NotificationService();
+ this.pointsCalculator = new PointsCalculator();
+ this.eventBus = EventBus;
+
+ this.state = {
+ currentEvent: null,
+ currentZekken: null,
+ teamData: null,
+ checkins: []
+ };
+ }
+
+ // ... SupervisorPanelの実装 ...
+}
+
+// アプリケーションの初期化
+document.addEventListener('DOMContentLoaded', () => {
+ const app = new SupervisorPanel({
+ element: document.getElementById('app'),
+ apiConfig: {
+ baseUrl: '/api',
+ authToken: localStorage.getItem('authToken'),
+ csrfToken: document.querySelector('meta[name="csrf-token"]').content
+ }
+ });
+
+ app.initialize();
+});
diff --git a/supervisor/html/js/utils/PointsCalculator.js b/supervisor/html/js/utils/PointsCalculator.js
new file mode 100644
index 0000000..58e7a0e
--- /dev/null
+++ b/supervisor/html/js/utils/PointsCalculator.js
@@ -0,0 +1,32 @@
+// js/utils/PointsCalculator.js
+export class PointsCalculator {
+ calculate({ checkins, latePoints = 0 }) {
+ const totalPoints = this.calculateTotalPoints(checkins);
+ const buyPoints = this.calculateBuyPoints(checkins);
+
+ return {
+ totalPoints,
+ buyPoints,
+ latePoints,
+ finalPoints: totalPoints + buyPoints + latePoints
+ };
+ }
+
+ calculateTotalPoints(checkins) {
+ return checkins.reduce((total, checkin) => {
+ if (checkin.validate_location && !checkin.buy_flag) {
+ return total + (checkin.checkin_point || 0);
+ }
+ return total;
+ }, 0);
+ }
+
+ calculateBuyPoints(checkins) {
+ return checkins.reduce((total, checkin) => {
+ if (checkin.validate_location && checkin.buy_flag) {
+ return total + (checkin.buy_point || 0);
+ }
+ return total;
+ }, 0);
+ }
+}
diff --git a/supervisor/html/js/vi b/supervisor/html/js/vi
new file mode 100644
index 0000000..39c717f
--- /dev/null
+++ b/supervisor/html/js/vi
@@ -0,0 +1,27 @@
+// js/services/NotificationService.js
+export class NotificationService {
+ constructor() {
+ this.toastElement = document.getElementById('toast');
+ }
+
+ showMessage(message, type = 'info') {
+ this.toastElement.textContent = message;
+ this.toastElement.className = `fixed bottom-4 right-4 px-6 py-3 rounded shadow-lg ${
+ type === 'error' ? 'bg-red-500' : 'bg-green-500'
+ } text-white`;
+
+ this.toastElement.classList.remove('hidden');
+
+ setTimeout(() => {
+ this.toastElement.classList.add('hidden');
+ }, 3000);
+ }
+
+ showError(message) {
+ this.showMessage(message, 'error');
+ }
+
+ showSuccess(message) {
+ this.showMessage(message, 'success');
+ }
+}
diff --git a/supervisor/html/login.html b/supervisor/html/login.html
new file mode 100644
index 0000000..5bb8f9a
--- /dev/null
+++ b/supervisor/html/login.html
@@ -0,0 +1,26 @@
+
+
+
+