From d017da17d4a1163ee3acb440917a686661e60078 Mon Sep 17 00:00:00 2001 From: hayano Date: Tue, 29 Oct 2024 14:07:31 +0000 Subject: [PATCH] supervisor step3 --- .env.swp | Bin 12288 -> 0 bytes config/settings.py | 6 +- docker-compose.yaml | 5 +- rog/.serializers.py.swp | Bin 16384 -> 0 bytes rog/.urls.py.swp | Bin 16384 -> 0 bytes rog/.views.py.swp | Bin 16384 -> 0 bytes rog/models.py | 2 + rog/views.py | 51 +- supervisor/html/.index.html.swp | Bin 16384 -> 0 bytes supervisor/html/index.html | 638 ++++++++++++++++--- supervisor/html/index.html.new | 111 ++++ supervisor/html/js/ApiClient.js | 72 +++ supervisor/html/js/SupervisorPanel.js | 276 ++++++++ supervisor/html/js/main.js | 171 +++++ supervisor/html/js/utils/PointsCalculator.js | 32 + supervisor/html/js/vi | 27 + supervisor/html/login.html | 26 + 17 files changed, 1320 insertions(+), 97 deletions(-) delete mode 100644 .env.swp delete mode 100644 rog/.serializers.py.swp delete mode 100644 rog/.urls.py.swp delete mode 100644 rog/.views.py.swp delete mode 100644 supervisor/html/.index.html.swp create mode 100644 supervisor/html/index.html.new create mode 100644 supervisor/html/js/ApiClient.js create mode 100644 supervisor/html/js/SupervisorPanel.js create mode 100644 supervisor/html/js/main.js create mode 100644 supervisor/html/js/utils/PointsCalculator.js create mode 100644 supervisor/html/js/vi create mode 100644 supervisor/html/login.html diff --git a/.env.swp b/.env.swp deleted file mode 100644 index 0115a92dd2da3733224a755622cf11eb5dbe6a7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI&zi!h&90%|#2#KZq-C$`5VnB-jr7s@d7*n55Plkrb!w>)u}^~ewMyD-QDjlmrtfBHEYin1K5r1wW^!Dk8 z!{O)a=y-Sy<+F*OM`<~8?!&U!PtqifYe`x#KaGmB%ik|IHhrF^l=7te}f;M|Np=I{r~GGLO*!E z@_gcX&-0F_;2H8Xc&_sN@b-#~u)!wr$sQ@#@aDu)O3gEUt44<%MsZH2Vhj*epUWX*tBB&&UA|$9@`T{91S( z1v3`avRbJ6jR|`hs8xJ)GJmnW>p+Z$HLW z-MiUXCI~_f96;j2kw_rraU@6xPKXOaf)hdu{IGw36aT8HV~ zhaTOR7T3u!;23ZWI0hU8jseGjW56-s82JBWAe-Jvo`&f+8}qrj-_*FjZLY^m{iq@T zmyu6Q{ZAV5e;WB0O?}%8V8{7KL;h?-e$B|wn*M)k$lo&ZCycyn9J2b~G;+H~@vVmZ z4I^JS{oicJUpMj(8TrLqbiwL3)$ZmPa11yG90QI4$ADwNG2j?*3^)cH1CD|JNe0-E z5Ve$jSuJF7{%_a+Kfa5Q9|2zn1aKMPz(c@?fw$ieIq)p-HDCaI5_tU}A-@EE{XRmT z1HJ>ocLA@xhY$$};4-iTd=&T~@Xgx@`3CSA;IFq5@(l1akOHf~-$3-QfR}))z-Ivp z+y~qX{2hA^KLt221U?7c58MP4@zEJXYnNc5VSRONMF*WfAwP z;K%8ZM;LiImsuJ=iYzTm7(k1pD?DR55Jja;o-W$g(|LKq#0eGo_Lc}0`$@FjQ&juZ za&I0@UZz6!)^_BmNl81(yEcgSO}ToWkMXjJ;!V6z{=5l^R{hdxMe$R~>WQaA)LZky zp$Y;U#*4uvRu-oNbTv;$1D=${vdz(;-lki1?pDpawKjsYAc?*8GD(Wy_2|iPls^oi z+z~qMs+Cu6%yW?(q4t{b(&Jg6y+uKU<4LBxLOJ52q6iYYK~%bGON(XQVjxPXEp^Mf zh;Q-;4pzf!7f(-Poa16-g!K$PLiN+>b7zAon*8UEnsAm;dlw_StG5^O{vLdl zd0+XvcXxLU>VL`CW?I^qt>AZug)eIGdeo|z;e#@Hr-5s@(h33=MM2OaRxsviP$X~9 zLoxPBfljG2>7aP;gJ~LVkJF?O?=-L3tj6_8o}S<=%LSkRA+dk+qA=m}Y*TIaYFJ5w zd9n9x*&L(xYtNd8xbSf*wmrX@QZ^WKhF)e_ZFG6#PxB}pkGa5?jVj>xv7Ir`&J=a+ zAW-vyAXvckRHQLg-=)}mX;2~-U7 ztZapGgS3B%4{#H~vBhJ=3}Xz_y*owrmYr6)%!=%@CTit_G|5ER@31_ZSSwVSCt0ZI z-Bnj@3UH)uXrQYJXS3DXZhCg zO;A=Ax;|ayEcPiRNU|bJ(Z4Jm2kYBDrcH5Q4Y^-gvsOip0D z?H99+YOUr|WX52lT)Ppq{$Id){6#>m|LytqpRvaOKJY!@yTBKK&jZWABfzb|AF$qk z1$Yh^0d2qo?f`y)J%DF`Rp24uQ^0M&Z!ykGX1o{FnC@~6I0hU8jseGjW56-s7;p?Y z1{?!B7@)Y+`9m~VDz-z4Jpe4IREJ`PRUC3Ow)~H)0~Fg*Z_TNN z*%=I=*6&Ksg>rLeW=p%Ou5Z{2j?KD6ZPu`2U^V!TmJo;%j#b5y{8V1 zJp??@vG88j)>m7O$ezb2LZ?#zn>qsj!meuURokQK=xsQuwgS~& z+yG}Q8E+Szdc|L##>E~_Pv5XXXw|5D6=hqCb4->;ne1KhLK$S(7fJ=g4(Afpx`(re Pn5tWbji#RZMOr5 zx_>D8cKgbsTlJ+FPz)#r6a$I@#eiZ!F`yVw3@8Q^1BwB~z<+@Ov#MzyMcfJ6@Z
uAA#$D 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 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 56342d9b5ae84155f32f1b585d0bfaea8e892995..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeHOUu+!38K3^42~gTnR9=}}X>FtIuI-Q%%$*T+(~^idkz!H-56#~0_-^gl-D7vw z_651s-4V$krMN8-5dJhs7J@`Y3T=UiQXbl>FO}d4Rq8`4ZO?|6stxaX7>B$*2VrK`wq~X^g)8-ON5M4TG~HB$*JNt5)~s)L{hNv z5%?w3@f@$TaW(S&mO^8>{`bOQ$_S$h3#OdV4~(*Kx#4&=o6+UR3#e0K#eibqq8PY} z>>3>CmrcH!+d;4S@|_peT2)pIClh{|^I1DNpX)Qs<> z*H3j^-${=<((>CoDdXDa_nD!)E8IMbgNtBYblF`yVw3@8Q^1BwB~fMP%~pcqgLCw~R&b_ku^5aYO^IbG0 z9ZR(oOIq5NRZGyDwicu6My6h>b#J}!($WLZHRtEvdUmc$ zcec_x;R};*rF|oZHr=*%K1zXOQ+d)XpNHBYb?u>*uQ$??DV@MAYI`vJ*LUzP ze8!r3O3u}OTNj+9Jh;+m+J3v{TFfLS8?O) z;$!s>$K^jb`>H;iQcc5JkkyvgZlkpRN|Ch49)&J*i!A8!9K7Vo(s(*<{5)>V zqu1T~C_A!6k3PXnchuanl^`3T=KP5d&^RMxoLORRFpxP!-meJ9_b3+oh!t|V;kb_V zsu9=EVJwzTo@qYxQe6M>+qIV$AABmVJrdWRiE9tVwbOC!WL&#HZ?u}PuRq_qc@jh& zW0RB28;zv@EI&}Dweg5^TJ!^(1?i;~&+O{&Uk+wmn^tDDU9?iw2G`+${qVmKHy%qS!rZ1X z&!(6sMtpna5%&?Hz1>bsd$`>L7s31ni|4?GaZ%>^iQmY@Gj;C9$+-SpYZYB*%`6XO z-jxLIgEPG4SV;oVin!wX`K0-elJk6AUx@23W+f*O1munKz>52{8-S>0y6gzH?DM=t z!?J1n(+K4e+3(4Mx}?)>i=c%>1h0Ez_P8^e83|ve0hlEDqHkBT9uX}7wO z53Rtd2pR@fAy*cn5)K)LWqT8$ZgD@d$GI6W-SS<-oG@n$?i54gupbDs$eA&q5A4?a zm%r<-XrXbkFu`oPUbR+QWXb#E zwB*Z9iWlsKV{$FDOwI}eIuf@wRaV&yLWZVyj(uCZNpjwzJTKp)r`+^Ph1`TG%XfV1 z;KGmoxeCo7H`GZ%=!@o<2RmDjreUU~c8 zK6>BE=I?47X8Ts!W_?;+BP&IY8;IVXzrOZl^JkC7wX@BIbBiaj6^OrIC+DB`tfz}I z9x~iNQ`3I!P`R-tXPov1EUbVmru&o9rgmMkeYz+!Z@CIB40qI!wdLRr(fx-Hj_RQZ z@c;47c(pgG$RD`EzE%2U?b)4ciez!mNNZO>8>?1W&k*e~D;4gvoWPg}eXnP2JDK}q W&wOZ|K4Igb?AuUt=-}ZoG<_Gc$iL_S 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 @@ + - + - + + @@ -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 = ` - - + + - - - - + - - + + `; 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 = ` + - + + `; 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 @@ + + + + + + + スーパーバイザーパネル + + + + + +
+ + + + + + + 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 @@ + + +
+ + +
+
+ + +
+ + + +
走行順 規定写真 撮影写真CP名称CP名称 通過時刻 通過審査買物審査買物 獲得点数
${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)} + + + + ${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)} + +