Files
rogaining_srv/supervisor/html/index.html
2024-10-30 06:57:51 +09:00

1042 lines
45 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>スーパーバイザーパネル</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-gray-50">
<div class="container mx-auto p-4">
<div class="bg-white rounded-lg shadow-lg p-6 mb-6">
<h1 class="text-2xl font-bold mb-6">スーパーバイザーパネル</h1>
<!-- 選択フォーム -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">イベントコード</label>
<select id="eventCode" class="w-full border border-gray-300 rounded-md px-3 py-2">
<option value="">イベントを選択</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">ゼッケン番号</label>
<select id="zekkenNumber" class="w-full border border-gray-300 rounded-md px-3 py-2">
<option value="">ゼッケン番号を選択</option>
</select>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">チーム名</div>
<div id="teamName" class="font-semibold"></div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">メンバー</div>
<div id="members" class="font-semibold"></div>
</div>
</div>
<!-- チーム情報サマリー -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">スタート時刻</div>
<div id="startTime" class="font-semibold"></div>
</div>
<!-- ゴール時刻フィールドを修正 -->
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">ゴール時刻</div>
<div class="goal-time-container">
<span id="goalTimeDisplay" class="goal-time-display cursor-pointer"></span>
<input type="datetime-local" id="goalTimeInput" class="goal-time-input hidden border rounded px-2 py-1"
onchange="updateGoalTime(this)">
</div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">ゴール時計</div>
<div id="goalTime" class="font-semibold"></div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">判定</div>
<div id="validate" class="font-semibold text-blue-600"></div>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">CP合計</div>
<div id="totalPoints" class="font-semibold"></div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">買物合計</div>
<div id="buyPoints" class="font-semibold"></div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">遅刻減点</div>
<div id="latePoints" class="font-semibold text-red-600"></div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">総合計</div>
<div id="finalPoints" class="font-semibold text-blue-600"></div>
</div>
</div>
<!-- チェックインデータテーブル -->
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="w-8"></th> <!-- ハンバーガーアイコン用 -->
<th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">走行順</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">規定写真</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">撮影写真</th>
<th class="px-5 py-3 text-left text-xs font-medium text-gray-500 uppercase">CP名称</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">通過時刻</th>
<th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">通過審査</th>
<th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">買物</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">獲得点数</th>
<th class="w-8"></th> <!-- 削除ボタン用 -->
</tr>
</thead>
<tbody id="checkinList" class="bg-white divide-y divide-gray-200">
<!-- JavaScript で動的に生成 -->
</tbody>
</table>
</div>
<!-- アクションボタン -->
<div class="mt-6 flex justify-end space-x-4">
<button onclick="showAddCPModal()" class="px-4 py-2 bg-blue-500 text-white rounded">
新規CP追加
</button>
<button id="saveButton" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
保存
</button>
<button id="exportButton" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">
通過証明書出力
</button>
</div>
</div>
</div>
<script>
// 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() {
if (!checkAuthStatus()) return;
// Sortable初期化これで、通過順序を変更できる
const checkinList = document.getElementById('checkinList');
new Sortable(checkinList, {
animation: 150,
onEnd: function(evt) {
updatePathOrders();
}
});
// 選択されたイベントコード・ゼッケン番号を取得
const eventCodeSelect = document.getElementById('eventCode');
const zekkenNumberSelect = document.getElementById('zekkenNumber');
// ゼッケン番号変更時の処理
zekkenNumberSelect.addEventListener('change', function(e) {
const eventCode = eventCodeSelect.value;
if (!eventCode) {
alert('イベントコードを選択してください');
return;
}
loadTeamData(e.target.value, eventCode); // チームデータのロード
});
// イベントコード変更時の処理
document.getElementById('eventCode').addEventListener('change', function(e) {
loadZekkenNumbers(e.target.value); // ゼッケン番号をロードする
});
// チェックボックス変更時の処理
//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);
// Excel出力ボタンの処理
document.getElementById('exportButton').addEventListener('click', exportExcel);
console.log('Page loaded, attempting to load events...');
// 初期データ読み込み
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}/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");
if (!contentType || !contentType.includes("application/json")) {
throw new TypeError("Response is not JSON");
}
const data = await response.json();
const select = document.getElementById('eventCode');
// 既存のオプションをクリア
select.innerHTML = '<option value="">イベントを選択</option>';
data.forEach(event => {
const option = document.createElement('option');
option.value = event.code;
option.textContent = event.name;
select.appendChild(option);
});
} catch (error) {
console.error('Error loading events:', error);
// ユーザーにエラーを表示
const select = document.getElementById('eventCode');
select.innerHTML = '<option value="">エラー: イベントの読み込みに失敗しました</option>';
}
}
// ゼッケン番号をロードする
function loadZekkenNumbers(eventCode) {
// APIからゼッケン番号を取得して選択肢を設定
fetch(`${API_BASE_URL}/zekken_numbers/${eventCode}`,{
headers: {
'Authorization': `Token ${getAuthToken()}`
}
})
.then(response => response.json())
.then(data => {
const select = document.getElementById('zekkenNumber');
select.innerHTML = '<option value="">ゼッケン番号を選択</option>';
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}/`, {
headers: {
'Authorization': `Token ${getAuthToken()}`
}
})
.catch(error => handleApiError(error)),
fetch(`${API_BASE_URL}/checkins/${zekkenNumber}/${event_code}/`, {
headers: {
'Authorization': `Token ${getAuthToken()}`
}
})
.catch(error => handleApiError(error))
]);
// 各レスポンスのステータスを個別にチェック
if (!teamResponse.ok)
throw new Error(`Team info fetch failed with status ${teamResponse.status}`);
if (!checkinsResponse.ok)
throw new Error(`Checkins fetch failed with status ${checkinsResponse.status}`);
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() : '未ゴール';
//'(未ゴール)';
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 = ''; // 既存のデータをクリア
let totalPoints = 0;
let buyPoints = 0;
checkinsData.forEach((checkin, index) => {
const tr = document.createElement('tr');
tr.dataset.id = checkin.id;
tr.dataset.cpNumber = checkin.cp_number;
const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : '';
tr.innerHTML = `
<td class="px-2 py-4 cursor-move">
<i class="fas fa-bars text-gray-400"></i>
</td>
<td class="px-2 py-4">${checkin.path_order}</td>
<td class="px-4 py-4">
${checkin.photos ?
`<img src="/media/compressed/${checkin.photos}" class="h-20 w-20 object-cover rounded" onclick="showLargeImage(this.src)">` : ''}
</td>
<td class="px-3 py-4">
${checkin.image_address ?
`<img src="${checkin.image_address}" class="h-20 w-20 object-cover rounded">` : ''}
</td>
<td class="px-5 py-4 ${bgColor}">
<div class="font-bold">${checkin.sub_loc_id}</div>
<div class="text-sm">${checkin.location_name}</div>
</td>
<td class="px-4 py-4">${checkin.create_at || '不明'}</td>
<td class="px-2 py-4">
<input type="checkbox"
${checkin.validate_location ? 'checked' : ''}
class="h-4 w-4 text-blue-600 rounded validate-checkbox"
onchange="updatePoints(this)">
</td>
<td class="px-2 py-4">
${checkin.buy_point > 0 ? `
<input type="checkbox"
${checkin.buy_flag ? 'checked' : ''}
class="h-4 w-4 text-green-600 rounded buy-checkbox" >
` : ''}
</td>
<td class="px-4 py-4 point-value">${calculatePointsForCheckin(checkin)}</td>
<td class="px-2 py-4">
<button onclick="deleteCheckin(${checkin.id})"
class="text-red-600 hover:text-red-800">
<i class="fas fa-trash"></i>
</button>
</td>
`;
tbody.appendChild(tr);
// 合計計算
if (checkin.points) {
if (checkin.buy_flag) {
buyPoints += checkin.points;
} else {
totalPoints += checkin.points;
}
}
});
updatePathOrders();
// 合計ポイントの更新
document.getElementById('totalPoints').textContent = totalPoints;
document.getElementById('buyPoints').textContent = buyPoints;
//document.getElementById('latePoints').textContent = teamData.late_points || 0;
document.getElementById('finalPoints').textContent =
totalPoints + buyPoints + (teamData.late_points || 0);
} catch (error) {
console.error('Error loading team info:', error);
// エラーメッセージをユーザーに表示
alert(`データの読み込みに失敗しました: ${error.message}`);
// UIをクリア
document.getElementById('teamName').textContent = '-';
document.getElementById('members').textContent = '-';
document.getElementById('startTime').textContent = '-';
document.getElementById('goalTime').textContent = '-';
document.getElementById('checkinList').innerHTML = '';
document.getElementById('totalPoints').textContent = '0';
document.getElementById('buyPoints').textContent = '0';
document.getElementById('latePoints').textContent = '0';
document.getElementById('finalPoints').textContent = '0';
}
}
// ゴール時刻表示を更新する関数
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 nouse_updateTeamInfo(teamInfo) {
document.getElementById('teamName').textContent = teamInfo.team_name;
document.getElementById('members').textContent = teamInfo.members;
document.getElementById('startTime').textContent = teamInfo.start_time;
document.getElementById('goalTime').textContent = teamInfo.goal_time;
document.getElementById('latePoints').textContent = teamInfo.late_points;
}
// 通過リストの表示...呼ばれていない
function nouse_updateCheckinList(checkins) {
const tbody = document.getElementById('checkinList');
tbody.innerHTML = '';
checkins.forEach((checkin, index) => {
const tr = document.createElement('tr');
tr.dataset.id = checkin.id;
tr.dataset.cpNumber = checkin.cp_number;
const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : '';
tr.innerHTML = `
<td class="px-2 py-4 cursor-move">
<i class="fas fa-bars text-gray-400"></i>
</td>
<td class="px-6 py-4">${checkin.path_order}</td>
<td class="px-6 py-4 ${bgColor}">
<div class="font-bold">${checkin.sub_loc_id}</div>
<div class="text-sm">${checkin.location_name}</div>
</td>
<td class="px-6 py-4">
${checkin.image_address ?
`<img src="${checkin.image_address}" class="h-20 w-20 object-cover rounded">` : ''}
</td>
<td class="px-6 py-4">${checkin.create_at || '不明'}</td>
<td class="px-6 py-4">
<input type="checkbox"
${checkin.validate_location ? 'checked' : ''}
class="h-4 w-4 text-blue-600 rounded validate-checkbox"
onchange="updatePoints(this)">
</td>
<td class="px-6 py-4">
${checkin.buy_point > 0 ? `
<input type="checkbox"
${checkin.buy_flag ? 'checked' : ''}
class="h-4 w-4 text-green-600 rounded buy-checkbox"
onchange="updateBuyPoints(this)">
` : ''}
</td>
<td class="px-6 py-4 point-value">${calculatePointsForCheckin(checkin)}</td>
<td class="px-2 py-4">
<button onclick="deleteCheckin(${checkin.id})"
class="text-red-600 hover:text-red-800">
<i class="fas fa-trash"></i>
</button>
</td>
`;
tbody.appendChild(tr);
});
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 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}/`,{
headers: {
'Authorization': `Token ${getAuthToken()}`
}
})
.then(response => response.json())
.then(location => {
const points = checkbox.checked ? location.checkin_point : 0;
pointCell.textContent = points;
calculatePoints(); // 総合ポイントの計算
})
.catch(error => handleApiError(error));
}
// 審査チェックボックス更新関数
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}/`,{
headers: {
'Authorization': `Token ${getAuthToken()}`
}
})
.then(response => response.json())
.then(location => {
const points = checkbox.checked ? location.buy_point : 0;
pointCell.textContent = points;
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));
}
// 画像拡大表示用のモーダル関数
function showLargeImage(src) {
const modal = document.createElement('div');
modal.classList.add('fixed', 'inset-0', 'bg-black', 'bg-opacity-75', 'flex', 'items-center', 'justify-center', 'z-50');
const img = document.createElement('img');
img.src = src;
img.classList.add('max-w-3xl', 'max-h-[90vh]', 'object-contain');
modal.appendChild(img);
modal.onclick = () => modal.remove();
document.body.appendChild(modal);
}
// 新規CP追加用のモーダル
function showAddCPModal() {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center';
modal.innerHTML = `
<div class="bg-white p-6 rounded-lg w-96">
<h3 class="text-lg font-bold mb-4">新規CPを追加</h3>
<div class="space-y-4" id="cpInputs">
<div class="flex gap-2">
<input type="number" class="cp-input border rounded px-2 py-1 w-full" placeholder="CP番号">
</div>
</div>
<div class="flex justify-end mt-4 space-x-2">
<button onclick="addCPInput()" class="px-4 py-2 bg-blue-500 text-white rounded">
追加
</button>
<button onclick="saveCPs()" class="px-4 py-2 bg-green-500 text-white rounded">
保存
</button>
<button onclick="closeModal()" class="px-4 py-2 bg-gray-500 text-white rounded">
キャンセル
</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
function saveCPs() {
const inputs = document.querySelectorAll('.cp-input');
const eventCode = eventCodeSelect.value;
const newCheckins = Array.from(inputs)
.map(input => ({
cp_number: input.value,
create_at: null,
validate_location: false,
buy_flag: false,
points: 0
}));
// APIを呼び出して保存
fetch(`${API_BASE_URL}/checkins/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${getAuthToken()}`
},
body: JSON.stringify({
zekken_number: currentZekkenNumber,
checkins: newCheckins
})
})
.then(response => response.json())
.then(data => {
loadTeamData(currentZekkenNumber,eventCode); // チームデータのロード
closeModal();
})
.catch(error => handleApiError(error));
}
function closeModal() {
document.querySelector('.fixed').remove();
}
function updatePathOrders() {
const rows = Array.from(document.getElementById('checkinList').children);
rows.forEach((row, index) => {
row.children[1].textContent = index + 1;
});
}
// 総合ポイントの計算
function calculatePoints() {
const rows = Array.from(document.getElementById('checkinList').children);
let totalPoints = 0; // チェックインポイントの合計をクリア
let buyPoints = 0; // 買い物ポイントの合計をクリア
// 各行のチェックインポイント及び買い物ポイントを合算
rows.forEach(row => {
const points = parseInt(row.children[4].textContent);
if (!isNaN(points)) {
totalPoints += points;
if (row.dataset.buyFlag === 'true') {
buyPoints += points;
}
}
});
// 遅刻ポイントの計算=ゴール時刻がEventのゴール時刻を超えていたら分につき-50点を加算する。
const latePoints = parseInt(document.getElementById('latePoints').textContent) || 0;
// 総合得点を計算
const finalPoints = totalPoints + buyPoints - latePoints;
// 判定を更新。順位を表示、ゴール時刻を15分経過したら失格
document.getElementById('totalPoints').textContent = totalPoints;
document.getElementById('buyPoints').textContent = buyPoints;
document.getElementById('finalPoints').textContent = finalPoints;
}
function formatDateTime(dateString) {
return new Date(dateString).toLocaleString('ja-JP');
}
function saveChanges() {
const rows = Array.from(document.getElementById('checkinList').children);
const updates = rows.map((row, index) => ({
id: row.dataset.id,
path_order: index + 1,
validate_location: row.querySelector('.validate-checkbox').checked
}));
fetch('${API_BASE_URL}/update_checkins', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${getAuthToken()}`
},
body: JSON.stringify(updates)
})
.then(response => {
if (response.ok) {
alert('保存しました');
} 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}`;
}
</script>
</body>
</html>