Files
rogaining_srv/supervisor/html/index.html
2024-11-08 14:52:31 +09:00

1484 lines
63 KiB
HTML
Executable File
Raw 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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/exif-js/2.3.0/exif.min.js"></script>
<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.5.1/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-1 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-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">CP名称</th>
<th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">通過時刻</th>
<th class="px-1 py-3 text-left text-xs font-medium text-gray-500 uppercase">買物申請</th>
<th class="px-1 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="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="showAddCPDialog()" 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 = 'http://rogaining.sumasen.net/api';
let original_goal_time = '';
let selected_event_code = '';
// イベントリスナーの設定
document.addEventListener('DOMContentLoaded', function() {
// 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;
}
selected_event_code = eventCode;
loadTeamData(e.target.value, eventCode);
});
// イベントコード変更時の処理
eventCodeSelect.addEventListener('change', function(e) {
loadZekkenNumbers(e.target.value);
handleEventSelect(e.target.value);
});
// チェックボックス変更時の処理
//checkinList.addEventListener('change', function(e) {
// if (e.target.type === 'checkbox') {
// updateValidation(e.target);
// }
//});
// 保存ボタンの処理
document.getElementById('saveButton').addEventListener('click', saveChanges);
// Excel出力ボタンの処理
document.getElementById('exportButton').addEventListener('click', exportExcel);
//console.log('Page loaded, attempting to load events...');
// 初期データ読み込み
loadEventCodes();
});
// 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);
const jstDate = new Date(date.getTime() + (9 * 60 * 60 * 1000));
input.value = jstDate.toISOString().slice(0, 19);
} catch (e) {
console.error('Error parsing date:', e);
}
}
// input要素のstep属性を1秒に設定
input.setAttribute('step', '1'); // 1秒単位で入力可能に設定
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, 19); // YYYY-MM-DDThh:mm:ss 形式に変換
} catch (e) {
console.error('Error parsing date:', e);
}
}
// input要素のstep属性を1秒に設定
input.setAttribute('step', '1'); // 1秒単位で入力可能に設定
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);
// inputがnullの場合は棄権処理
if (!input) {
const validateElement = document.getElementById('validate');
display.textContent = '棄権';
if (validateElement) {
validateElement.textContent = '棄権';
}
return;
}
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}/team_info/${zekkenNumber}`,{
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(team => {
if (team.self_rogaining) {
// セルフロゲイニングの場合
fetch(`${API_BASE_URL}/checkins/${zekkenNumber}/${eventCode}/`,{
headers: {
'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; // 秒単位の差
const maxTime = team.duration + 15*60; // 制限時間+15分
updateValidation(timeDiff, maxTime );
const overtime = ((newTime - startTime) / 1000 - team.duration/1000 + 60 ) // 60;
if( overtime>0 ){
late_point = overtime*(-50);
lateElement = document.getElementById('latePoints');
lateElement.textContent = late_point;
lateElement.classList.add('text-red-600');
}else{
lateElement = document.getElementById('latePoints');
lateElement.textContent = 0;
lateElement.classList.remove('text-red-600');
}
}
})
.catch(error => handleApiError(error));
} else {
// 通常のロゲイニングの場合
const startTime = new Date(team.start_datetime);
const timeDiff = (newTime - startTime) / 1000; // 分単位の差
const maxTime = team.duration + 15*60; // 制限時間+15分
//console.info('startTime=',startTime,',goalTime=',newTime,',timeDiff=',timeDiff,'duration=',team.duration,',maxTime=',maxTime);
updateValidation(timeDiff, maxTime );
// 1秒でも遅刻すると、1分につき-50点
const overtime = ((newTime - startTime) / 1000 - team.duration );
if( overtime>0 ){
//console.info('overtime=',overtime);
late_point = Math.ceil(overtime/60)*(-50);
lateElement = document.getElementById('latePoints');
lateElement.textContent = late_point;
lateElement.classList.add('text-red-600');
}else{
lateElement = document.getElementById('latePoints');
lateElement.textContent = 0;
lateElement.classList.remove('text-red-600');
}
}
});
calculatePoints(); // 総合ポイントの計算
} 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/`);
if (!response.ok) {
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}`)
.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);
});
});
}
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}/`),
]);
// 各レスポンスのステータスを個別にチェック
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);
original_goal_time = 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;
updateGoalTime(goalTime) // ゴール時刻の表示を更新
console.info('teamData.goal_photo = ',teamData.goal_photo );
if (goalTime === '-') {
goalTimeDisplay.classList.add('cursor-pointer');
goalTimeDisplay.onclick = () => editGoalTime(goalTimeDisplay);
}
//console.info("step 0");
// ゴール時計の表示を更新
const goalTimeElement = document.getElementById('goalTime');
if (teamData.goal_photo) {
// 画像要素を作成
//console.info("step 1");
const img = document.createElement('img');
img.src = teamData.goal_photo;
img.classList.add('h-32', 'w-auto', 'object-contain', 'cursor-pointer');
img.onclick = () => showLargeImage(teamData.goal_photo);
//console.info("step 2");
// 既存の内容をクリアして画像を追加
goalTimeElement.innerHTML = '';
goalTimeElement.appendChild(img);
//console.info("Goal photo displayed: ",teamData.goal_photo);
} else {
goalTimeElement.textContent = '画像なし';
console.info("No goal photo available");
}
// チェックインリストの更新
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.local_id = index+1;
tr.dataset.cpNumber = checkin.cp_number;
tr.dataset.buyPoint = checkin.buy_point;
tr.dataset.checkinPoint = checkin.checkin_point;
tr.dataset.path_order = index+1;
const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : '';
tr.innerHTML = `
<td class="px-1 py-3 cursor-move">
<i class="fas fa-bars text-gray-400"></i>
</td>
<td class="px-2 py-3">${tr.dataset.path_order}</td>
<td class="px-2 py-3">
${checkin.photos ?
`<img src="/media/compressed/${checkin.photos}" class="h-20 w-20 object-cover rounded" onclick="showLargeImage(this.src)">` : ''}
</td>
<td class="px-2 py-3">
${checkin.cp_number===-1 || checkin.image_address===null ? "" :
`<img src="${checkin.image_address}"
class="h-20 w-20 object-cover rounded"
onclick="showLargeImage(this.src)"
onerror="this.parentElement.innerHTML=''">`}
</td>
<td class="px-2 py-3 ${bgColor}">
<div class="font-bold">${checkin.sub_loc_id}</div>
<div class="text-sm">${checkin.location_name}</div>
</td>
<td class="px-2 py-3">${checkin.create_at || '不明'}</td>
<td class="px-1 py-3">
${checkin.buy_point > 0 ? `
<input type="checkbox"
${checkin.buy_flag ? 'checked' : ''}
class="h-4 w-4 text-green-600 rounded buy-checkbox"
onchange="updatePoints(this)">
` : ''}
</td>
<td class="px-1 py-3">
<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-3 point-value">${calculatePointsForCheckin(checkin)}</td>
<td class="px-2 py-4">
</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);
calculatePoints(); // 総合ポイントの計算
} 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 deleteRow(rowIndex) {
try {
if (!confirm(`このチェックインを削除してもよろしいですか?`)) {
return;
}
const tbody = document.getElementById('checkinList');
const rows = tbody.getElementsByTagName('tr');
let target = null;
for (const row of rows) {
if( Number(row.dataset.local_id) === Number(rowIndex) ) {
target = row;
break;
}
}
// 対象の行が見つからなかった場合
if (!target) {
throw new Error('Target row not found');
}
target.remove();
updatePathOrders();
// ポイントを再計算
calculatePoints();
} catch (error) {
console.error('Error deleting row:', error);
alert('行の削除に失敗しました');
}
}
// 削除機能
async function old_deleteCheckin(id) {
if (!confirm('このチェックインを削除してもよろしいですか?')) {
return;
}
try {
const response = await fetch(`${API_BASE_URL}/checkins/${id}`, {
method: 'DELETE',
})
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}/`)
.then(response => response.json())
.then(location => {
const points = checkbox.checked ? location.checkin_point : 0;
pointCell.textContent = points;
calculateTotalPoints();
});
}
// 審査チェックボックス更新関数
function updatePoints(checkbox) {
const tr = checkbox.closest('tr');
const pointCell = tr.querySelector('.point-value');
const cpNumber = tr.dataset.cpNumber;
const buyPoint = tr.dataset.buyPoint;
const checkinPoint = tr.dataset.checkinPoint;
const validateCheckbox = tr.querySelector('.validate-checkbox');
const buyCheckbox = tr.querySelector('.buy-checkbox');
let checkin = {
validate_location: validateCheckbox ? validateCheckbox.checked : false,
buy_flag: buyCheckbox ? buyCheckbox.checked : false,
points: 0
};
if (checkin.validate_location) {
// チェックボックスがONの場合
if (checkin.buy_flag) {
checkin.points = buyPoint || 0;
} else {
checkin.points = checkinPoint || 0;
}
} else {
// チェックボックスがOFFの場合
checkin.points = 0;
}
// ポイントを表示
pointCell.textContent = checkin.points;
calculatePoints(); // 総合ポイントの計算
}
// 買い物ポイント更新関数
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}/`)
.then(response => response.json())
.then(location => {
const points = checkbox.checked ? location.buy_point : 0;
pointCell.textContent = points;
calculateTotalPoints();
});
}
// 買い物チェックボックス更新関数
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}/`)
.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(); // 総合ポイントの計算
})
}
// 画像拡大表示用のモーダル関数
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');
// 画像の向きを補正
applyImageOrientation(img);
modal.appendChild(img);
modal.onclick = () => modal.remove();
document.body.appendChild(modal);
}
// 画像の向きを取得して適用する関数
function applyImageOrientation(imgElement) {
return new Promise((resolve) => {
const img = new Image();
img.onload = function() {
// 仮想キャンバスを作成
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// EXIF情報を取得
EXIF.getData(img, function() {
const orientation = EXIF.getTag(this, 'Orientation') || 1;
let width = img.width;
let height = img.height;
// 向きに応じてキャンバスのサイズを調整
if (orientation > 4 && orientation < 9) {
canvas.width = height;
canvas.height = width;
} else {
canvas.width = width;
canvas.height = height;
}
// 向きに応じて回転を適用
switch (orientation) {
case 2: ctx.transform(-1, 0, 0, 1, width, 0); break;
case 3: ctx.transform(-1, 0, 0, -1, width, height); break;
case 4: ctx.transform(1, 0, 0, -1, 0, height); break;
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
case 6: ctx.transform(0, 1, -1, 0, height, 0); break;
case 7: ctx.transform(0, -1, -1, 0, height, width); break;
case 8: ctx.transform(0, -1, 1, 0, 0, width); break;
default: break;
}
// 画像を描画
ctx.drawImage(img, 0, 0);
// 回転済みの画像をソースとして設定
imgElement.src = canvas.toDataURL();
resolve();
});
};
img.src = imgElement.src;
});
}
// 新規CP追加のための関数
function showAddCPDialog() {
const cpInput = prompt('追加するCPをカンマ区切りで入力してくださいCP1,CP2,CP3');
if (!cpInput) return;
const cpList = cpInput.split(',').map(cp => cp.trim()).filter(cp => cp);
if (cpList.length === 0) {
alert('有効なCPを入力してください');
return;
}
// 既存のチェックインデータを取得
const existingCheckins = getCurrentCheckins(); // 現在の表示データを取得する関数
const newCheckins = [];
//console.info('existingCheckins.length =',existingCheckins.length);
cpList.forEach((cp,index) => {
cploc = findLocationByCP(cp);
//console.info('location=',cploc);
//console.info('index = ',index);
newCheckins.push({
id: cploc.id,
order: existingCheckins.length + index + 1,
photos: cploc.photos,
actual_photo: '-',
subLocId: cploc.subLocId,
cp: cploc.cp,
location_name: cploc.name,
checkinPoint: Number(cploc.checkinPoint),
buyPoint: Number(cploc.buyPoint),
points: Number(cploc.checkinPoint),
buy_flag: false,
validate_location: true
});
});
//console.info('newCheckins=',newCheckins);
// 新しいCPを表に追加
addCheckinsToTable(newCheckins);
updatePathOrders();
calculatePoints(); // 総合ポイントの計算
}
// 新規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',
},
body: JSON.stringify({
zekken_number: currentZekkenNumber,
checkins: newCheckins
})
})
.then(response => response.json())
.then(data => {
loadTeamData(currentZekkenNumber,eventCode);
closeModal();
});
}
function closeModal() {
document.querySelector('.fixed').remove();
}
function updatePathOrders() {
const rows = Array.from(document.getElementById('checkinList').children);
rows.forEach((row, index) => {
//console.info('row=',row);
row.children[1].textContent = index + 1;
row.dataset.path_order = index + 1;
});
}
// 総合ポイントの計算
function calculatePoints() {
//console.info('calculatePoints');
const rows = Array.from(document.getElementById('checkinList').children);
let totalPoints = 0; // チェックインポイントの合計をクリア
let cpPoints = 0; // チェックインポイントの合計をクリア
let buyPoints = 0; // 買い物ポイントの合計をクリア
let latePoints = 0; // 遅刻ポイントの合計をクリア
// 各行のチェックインポイント及び買い物ポイントを合算
rows.forEach(row => {
const buybox = row.children[6].querySelector('input[type="checkbox"]');
const checkbox = row.children[7].querySelector('input[type="checkbox"]');
const point = parseInt(row.children[8].textContent);
if (checkbox) {
totalPoints += point;
if (buybox && buybox.checked) {
buyPoints += point;
}else{
cpPoints += point;
}
}
});
// 遅刻ポイントの計算=ゴール時刻がEventのゴール時刻を超えていたら分につき-50点を加算する。
latePoints = parseInt(document.getElementById('latePoints').textContent) || 0;
// 総合得点を計算
const finalPoints = totalPoints + latePoints;
// 判定を更新。順位を表示、ゴール時刻を15分経過したら失格
//console.info('calculatePoints:totalPoints=',cpPoints,',buyPoints=',buyPoints,',finalPoints=',finalPoints);
document.getElementById('totalPoints').textContent = cpPoints;
document.getElementById('buyPoints').textContent = buyPoints;
document.getElementById('latePoints').textContent = latePoints;
document.getElementById('finalPoints').textContent = finalPoints;
}
function formatDateTime(dateString) {
return new Date(dateString).toLocaleString('ja-JP');
}
// 保存機能の実装
async function saveChanges() {
const zekkenNumber = document.querySelector('#zekkenNumber').value;
const eventCode = document.querySelector('#eventCode').value;
const display = document.getElementById('goalTimeDisplay');
if (!display) {
console.error('Goal time elements not found');
return;
}
let goalTime = display.textContent;
console.info('Goal time = ',goalTime);
try {
const checkins = getCurrentCheckins(); // 現在の表示データを取得
const response = await fetch(`${API_BASE_URL}/update_checkins/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
zekken_number: zekkenNumber,
event_code: eventCode,
checkins: checkins
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const success = await saveGoalTime(goalTime, zekkenNumber, eventCode);
if (success) {
alert('ゴール時間を保存しました');
}
alert('保存が完了しました');
} catch (error) {
console.error('保存中にエラーが発生しました:', error);
alert('保存中にエラーが発生しました');
}
}
// ゴール時間の更新と新規作成を処理する関数
async function saveGoalTime(goalTimeStr, zekkenNumber, eventCode) {
try {
const [teamResponse, checkinsResponse] = await Promise.all([
fetch(`${API_BASE_URL}/team_info/${zekkenNumber}/`),
]);
// 各レスポンスのステータスを個別にチェック
if (!teamResponse.ok)
throw new Error(`Team info fetch failed with status ${teamResponse.status}`);
const teamData = await teamResponse.json();
const teamName = teamData.team_name;
// 日付と時刻を結合して完全な日時文字列を作成
//const currentDate = new Date().toISOString().split('T')[0];
//const fullGoalTime = `${currentDate}T${goalTimeStr}:00`;
const formattedDateTime = goalTimeStr
.replace(/\//g, '-') // スラッシュをハイフンに変換
.replace(' ', 'T'); // スペースをTに変換
//console.log(formattedDateTime); // "2024-10-26T12:59:13"
console.info('goaltime=',formattedDateTime);
// 新規レコードを作成または既存レコードを更新
const createResponse = await fetch('/api/update-goal-time/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
//'Authorization': `Token ${localStorage.getItem('token')}`,
},
body: JSON.stringify({
goaltime: formattedDateTime, //fullGoalTime,
team_name: teamName,
event_code: eventCode,
zekken_number: zekkenNumber
})
});
if (!createResponse.ok) {
throw new Error('Failed to create new goal record');
}
return true;
} catch (error) {
console.error('Error processing goal time:', error);
alert(`ゴール時間の処理に失敗しました: ${error.message}`);
return false;
}
}
// 通過証明書出力・印刷機能の実装
async function exportExcel() {
const zekkenNumber = document.querySelector('#zekkenNumber').value;
const eventCode = document.querySelector('#eventCode').value;
try {
const response = await fetch(`${API_BASE_URL}/export_excel/${zekkenNumber}/${eventCode}/`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Blobとしてレスポンスを取得
const blob = await response.blob();
// BlobをURLに変換
const url = window.URL.createObjectURL(blob);
// 印刷方法の選択を提供する関数
const printPDF = () => {
// IEとその他のブラウザで異なる処理を行う
if (window.navigator.msSaveOrOpenBlob) {
// IEの場合
window.navigator.msSaveOrOpenBlob(blob, `通過証明書_${zekkenNumber}_${eventCode}.pdf`);
} else {
// その他のブラウザの場合
// iframeを作成して印刷用のコンテナとして使用
const printFrame = document.createElement('iframe');
printFrame.style.display = 'none';
printFrame.src = url;
printFrame.onload = () => {
try {
// iframe内のPDFを印刷
printFrame.contentWindow.print();
} catch (error) {
console.error('印刷プロセス中にエラーが発生しました:', error);
// 印刷に失敗した場合、新しいタブでPDFを開く
window.open(url, '_blank');
} finally {
// 少し遅延してからクリーンアップ
setTimeout(() => {
document.body.removeChild(printFrame);
window.URL.revokeObjectURL(url);
}, 1000);
}
};
document.body.appendChild(printFrame);
}
};
// 確認ダイアログを表示
const userChoice = window.confirm('PDFを印刷しますか\n「キャンセル」を選択すると保存できます。');
if (userChoice) {
// 印刷を実行
printPDF();
} else {
// PDFを保存
const a = document.createElement('a');
a.href = url;
a.download = `通過証明書_${zekkenNumber}_${eventCode}.pdf`;
document.body.appendChild(a);
a.click();
// クリーンアップ
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
} catch (error) {
console.error('エクスポート中にエラーが発生しました:', error);
alert('エクスポート中にエラーが発生しました');
}
}
// エラーハンドリングのためのユーティリティ関数
function handlePrintError(error) {
console.error('印刷中にエラーが発生しました:', error);
alert('印刷中にエラーが発生しました。PDFを新しいタブで開きます。');
}
// 通過証明書出力機能の実装
async function exportExcel_old() {
const zekkenNumber = document.querySelector('#zekkenNumber').value;
const eventCode = document.querySelector('#eventCode').value;
try {
const response = await fetch(`${API_BASE_URL}/export_excel/${zekkenNumber}/${eventCode}/`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Blobとしてレスポンスを取得
const blob = await response.blob();
// ダウンロードリンクを作成
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `通過証明書_${zekkenNumber}_${eventCode}.xlsx`;
// リンクをクリックしてダウンロードを開始
document.body.appendChild(a);
a.click();
// クリーンアップ
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('エクスポート中にエラーが発生しました:', error);
alert('エクスポート中にエラーが発生しました');
}
}
function old_exportExcel() {
const zekkenNumber = document.getElementById('zekkenNumber').value;
window.location.href = `/api/export-excel/${zekkenNumber}`;
}
// 現在の表示データを取得する補助関数
function getCurrentCheckins() {
const tbody = document.querySelector('table tbody');
const checkins = [];
// テーブルの行をループして現在のデータを収集
tbody.querySelectorAll('tr').forEach((row, index) => {
const cells = row.cells;
// チェックボックスの要素を正しく取得
const buyFlagCheckbox = cells[6].querySelector('input[type="checkbox"]');
const validationCheckbox = cells[7].querySelector('input[type="checkbox"]');
checkins.push({
id: row.dataset.id,
order: index + 1,
cp_number: row.dataset.cpNumber,
buy_flag: buyFlagCheckbox ? buyFlagCheckbox.checked : false,
validation: validationCheckbox ? validationCheckbox.checked : false,
points: parseInt(cells[8].textContent) || 0
});
});
return checkins;
}
// テーブルにチェックインを追加する補助関数
function addCheckinsToTable(checkins) {
const table = document.querySelector('table tbody');
checkins.forEach(checkin => {
const row = document.createElement('tr');
//console.info('checkin=',checkin);
row.dataset.id = 0;
row.dataset.local_id = checkin.order; // Unique
row.dataset.cpNumber = checkin.cp;
row.dataset.checkinPoint = checkin.checkinPoint;
row.dataset.buyPoint = checkin.buyPoint;
row.dataset.path_order = checkin.order; // reorderable
const bgColor = checkin.buyPoint > 0 ? 'bg-blue-100' : '';
row.innerHTML = `
<td class="px-1 py-3 cursor-move">
<i class="fas fa-bars text-gray-400"></i>
</td>
<td class="px-2 py-3">${checkin.order}</td>
<td class="px-2 py-3">
${checkin.photos ?
`<img src="/media/compressed/${checkin.photos}" class="h-20 w-20 object-cover rounded" onclick="showLargeImage(this.src)">` : ''}
</td>
<td class="px-2 py-3">
${checkin.image_address ?
`<img src="${checkin.image_address}" class="h-20 w-20 object-cover rounded" onclick="showLargeImage(this.src)">` : '無し'}
</td>
<td class="px-2 py-3 ${bgColor}">
<div class="font-bold">${checkin.subLocId}</div>
<div class="text-sm">${checkin.location_name}</div>
</td>
<td class="px-2 py-3">不明</td>
<td class="px-1 py-3">
${checkin.buyPoint > 0 ? `
<input type="checkbox"
${checkin.buy_flag ? 'checked' : ''}
class="h-4 w-4 text-green-600 rounded buy-checkbox"
onchange="updatePoints(this)">
` : ''}
</td>
<td class="px-1 py-3">
<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-3 point-value">${checkin.checkinPoint}</td>
<td class="px-2 py-4">
<button onclick="deleteRow(${row.dataset.local_id})"
class="text-red-600 hover:text-red-800">
<i class="fas fa-trash"></i>
</button>
</td>
`;
table.appendChild(row);
});
}
// グローバルな状態管理
let loadedLocations = [];
let currentEventCode = null;
// チェックポイントデータをロードする関数
async function loadLocations(eventCode) {
try {
//console.info('loadLocations-1:',eventCode);
if (!eventCode) {
console.error('Event code is required');
return;
}
// 既に同じイベントのデータがロードされている場合はスキップ
if (currentEventCode === eventCode && loadedLocations.length > 0) {
console.log('Locations already loaded for this event');
return loadedLocations;
}
//console.info('loadLocations-2:',eventCode);
// group__containsフィルターを使用してクエリパラメータを構築
const params = new URLSearchParams({
group__contains: eventCode
});
// Location APIを使用してデータを取得
const response = await fetch(`/api/location/?${params}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
// レスポンスをJSONとして解決
const data = await response.json();
//console.info('loadLocations-3:', data);
if (!response.ok) {
console.info('loadLocations-3: Bad Response :',response);
throw new Error(`HTTP error! status: ${response.status}`);
}
//console.info('loadLocations-4:',eventCode);
// 取得したデータを処理して保存
loadedLocations = data.features.map(feature => ({
cp: feature.properties.cp,
name: feature.properties.location_name,
subLocId: feature.properties.sub_loc_id,
checkinPoint: feature.properties.checkin_point,
buyPoint: feature.properties.buy_point,
photos: feature.properties.photos,
group: feature.properties.group,
coordinates: feature.geometry.coordinates[0],
latitude: feature.geometry.coordinates[0][1],
longitude: feature.geometry.coordinates[0][0]
})).filter(location => location.group && location.group.includes(eventCode));
currentEventCode = eventCode;
//console.info(`Loaded ${loadedLocations.length} locations for event ${eventCode}`);
//console.info('loadedLocation[0]=',loadedLocations[0]);
return loadedLocations;
} catch (error) {
console.error('Error loading locations:', error);
throw error;
}
}
// イベント選択時のハンドラー
async function handleEventSelect(eventCode) {
//console.info('handleEventSelect : ',eventCode);
try {
//document.getElementById('loading').style.display = 'block';
await loadLocations(eventCode);
//document.getElementById('loading').style.display = 'none';
} catch (error) {
console.error('Error handling event selection:', error);
//document.getElementById('loading').style.display = 'none';
alert('チェックポイントの読み込みに失敗しました。');
}
}
// ロードされたロケーションデータを取得する関数
function getLoadedLocations() {
return loadedLocations;
}
// 指定されたCP番号のロケーションを取得する関数
function findLocationByCP(cpNumber) {
if (!loadedLocations.length) {
console.warn('Locations not loaded yet');
return null;
}
// cpプロパティを数値として比較
const found = loadedLocations.find(loc => Number(loc.cp) === Number(cpNumber));
if (!found) {
console.warn(`Location with CP number ${cpNumber} not found`);
return null;
}
//console.info(`Found location with CP ${cpNumber}:`, found);
return found;
}
</script>
</body>
</html>