From 2ccfac2d045cf4e33592c12d10adcb05c676be52 Mon Sep 17 00:00:00 2001 From: Dian Afif Date: Wed, 20 May 2026 10:34:32 +0400 Subject: [PATCH] final --- frontend/src/App.vue | 35 ++++++++++++++++++ frontend/src/components/AdminPanel.vue | 6 +++ frontend/src/components/LiveModePanel.vue | 37 +++++++++++++++++-- frontend/src/components/ResetStageModal.vue | 3 +- .../admin/MultiRoundScoreEditor.vue | 7 ++-- .../src/components/admin/ScoreStageEditor.vue | 23 ++---------- frontend/src/constants/i18n.js | 2 + 7 files changed, 85 insertions(+), 28 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 601c16a..fe885b4 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -74,6 +74,7 @@ :position-boards="positionBoards" :live-group-code="liveMonitorGroupCode" :live-group-members="liveMonitorMembers" + :prelim-tie="prelimTie" :prelim-tie-rows="prelimTieRows" :preliminary-rows="preliminaryRows" :finalists="finalists" @@ -178,6 +179,7 @@ @remove-player-image="removePlayerImage" @delete-player="deletePlayer" @request-reset-stage="openResetStageModal" + @reset-final-group-by-seed="resetFinalGroupBySeed" @save-position-board="savePositionBoard" @update-score-filter="updateScoreFilter" /> @@ -1021,6 +1023,39 @@ async function savePositionBoard(payload) { } } +async function resetFinalGroupBySeed() { + const ranked = [...adminFinalRows.value] + .filter((row) => Number(row.seed || 0) > 0) + .sort((a, b) => { + const seedA = Number(a.seed || 0) + const seedB = Number(b.seed || 0) + if (seedA !== seedB) return seedA - seedB + return Number(a.playerId || 0) - Number(b.playerId || 0) + }) + + if (ranked.length === 0) { + showToast(t('labels.noFinalists'), 'error') + return + } + + const slots = ranked.map((row, index) => ({ + playerId: row.playerId, + groupKey: index < 6 ? '1' : '2', + position: (index % 6) + 1, + })) + + try { + state.value = await api('/admin/positions/final', { + method: 'PUT', + admin: true, + body: { slots }, + }) + showToast(t('messages.saved')) + } catch (error) { + showToast(`${t('messages.errorPrefix')}: ${error.message}`, 'error') + } +} + async function autoGroupEvenly() { if (primaryGroups.value.length === 0) { showToast(t('messages.noGroupsConfigured'), 'error') diff --git a/frontend/src/components/AdminPanel.vue b/frontend/src/components/AdminPanel.vue index d2e9fd1..e1169c8 100644 --- a/frontend/src/components/AdminPanel.vue +++ b/frontend/src/components/AdminPanel.vue @@ -135,6 +135,7 @@ :score-proof-for="scoreProofFor" :open-score-proof-uploader="openScoreProofUploader" :open-proof-preview="openProofPreview" + :show-proof-controls="false" /> @@ -195,6 +196,7 @@ :request-score-advice="requestScoreAdvice" :highlight-stage="activeScoringStage" :highlight-group="activeScoringGroup" + :show-proof-controls="false" @update:filter="$emit('update-score-filter', { stage: 'prelim_tiebreak', value: $event })" /> @@ -208,6 +210,7 @@
+
@@ -259,6 +262,7 @@ :score-proof-for="scoreProofFor" :open-score-proof-uploader="openScoreProofUploader" :open-proof-preview="openProofPreview" + :show-proof-controls="false" /> @@ -314,6 +318,7 @@ :request-score-advice="requestScoreAdvice" :highlight-stage="activeScoringStage" :highlight-group="activeScoringGroup" + :show-proof-controls="false" @update:filter="$emit('update-score-filter', { stage: 'final_tiebreak', value: $event })" /> @@ -439,6 +444,7 @@ defineEmits([ 'remove-player-image', 'delete-player', 'request-reset-stage', + 'reset-final-group-by-seed', 'save-position-board', 'update-score-filter', ]) diff --git a/frontend/src/components/LiveModePanel.vue b/frontend/src/components/LiveModePanel.vue index 0380102..f24e2d7 100644 --- a/frontend/src/components/LiveModePanel.vue +++ b/frontend/src/components/LiveModePanel.vue @@ -151,7 +151,7 @@ {{ row.score }} {{ row.tieBreak }} - {{ t('labels.noPlayers') }} + {{ t('labels.noPlayers') }}
@@ -374,6 +374,7 @@ const props = defineProps({ positionBoards: { type: Object, default: () => ({}) }, liveGroupCode: { type: String, default: '' }, liveGroupMembers: { type: Array, required: true }, + prelimTie: { type: Object, default: () => ({ required: false, resolved: true, slots: 0, playerIds: [] }) }, prelimTieRows: { type: Array, required: true }, preliminaryRows: { type: Array, required: true }, finalists: { type: Array, required: true }, @@ -398,6 +399,7 @@ const props = defineProps({ const finalistIds = computed(() => new Set((props.finalists || []).map((row) => row.playerId))) const prelimTieIds = computed(() => new Set((props.prelimTieRows || []).map((row) => row.playerId))) const finalTieIds = computed(() => new Set((props.finalTieRows || []).map((row) => row.playerId))) +const prelimTieResolved = computed(() => Boolean(props.prelimTie?.resolved)) const activeView = computed(() => props.liveSettings.activeView || 'group_live') const listPageIndex = ref(0) const tieGroupIndex = ref(0) @@ -418,8 +420,10 @@ const rotationPlayerCount = computed(() => { return Math.floor(raw) }) +const preliminaryRowsForLive = computed(() => rankPreliminaryRowsWithTieBreak(props.preliminaryRows || [])) + const listRowsForActiveView = computed(() => { - if (activeView.value === 'prelim_overall') return props.preliminaryRows || [] + if (activeView.value === 'prelim_overall') return preliminaryRowsForLive.value if (activeView.value === 'final_overall') return props.finalOverallRows || [] return [] }) @@ -432,7 +436,7 @@ const listPageCount = computed(() => { const currentListPageDisplay = computed(() => `${listPageIndex.value + 1} / ${listPageCount.value}`) -const pagedPreliminaryRows = computed(() => paginateRows(props.preliminaryRows || [])) +const pagedPreliminaryRows = computed(() => paginateRows(preliminaryRowsForLive.value)) const pagedFinalOverallRows = computed(() => paginateRows(props.finalOverallRows || [])) const groupedPrelimTieRows = computed(() => groupTieRowsByPositionBoard('prelim_tiebreak', props.prelimTieRows || [])) const groupedFinalTieRows = computed(() => groupTieRowsByPositionBoard('final_tiebreak', props.finalTieRows || [])) @@ -508,7 +512,7 @@ function isFinalTiePlayer(playerId) { } function prelimOverallHintLabel(row) { - if (isPrelimTiePlayer(row.playerId)) return props.t('labels.tieBreak') + if (!prelimTieResolved.value && isPrelimTiePlayer(row.playerId)) return props.t('labels.tieBreak') if (isFinalist(row.playerId)) return props.t('labels.finalist') return '' } @@ -538,6 +542,31 @@ function paginateRows(rows) { return rows.slice(start, start + rotationPlayerCount.value) } +function rankPreliminaryRowsWithTieBreak(rows) { + const sorted = [...(rows || [])].sort((a, b) => { + const scoreA = Number(a.score || 0) + const scoreB = Number(b.score || 0) + if (scoreA !== scoreB) return scoreB - scoreA + const tieA = Number(a.tieBreak || 0) + const tieB = Number(b.tieBreak || 0) + if (tieA !== tieB) return tieB - tieA + return Number(a.playerId || 0) - Number(b.playerId || 0) + }) + + let currentRank = 1 + return sorted.map((row, index) => { + if (index > 0) { + const prev = sorted[index - 1] + const isSameScore = Number(prev.score || 0) === Number(row.score || 0) + const isSameTieBreak = Number(prev.tieBreak || 0) === Number(row.tieBreak || 0) + if (!(isSameScore && isSameTieBreak)) { + currentRank = index + 1 + } + } + return { ...row, rank: currentRank } + }) +} + function restartListRotation() { stopListRotation() listPageIndex.value = 0 diff --git a/frontend/src/components/ResetStageModal.vue b/frontend/src/components/ResetStageModal.vue index 1dfdc90..79b5eaa 100644 --- a/frontend/src/components/ResetStageModal.vue +++ b/frontend/src/components/ResetStageModal.vue @@ -3,10 +3,9 @@ diff --git a/frontend/src/components/admin/MultiRoundScoreEditor.vue b/frontend/src/components/admin/MultiRoundScoreEditor.vue index f32bd47..a30fe7c 100644 --- a/frontend/src/components/admin/MultiRoundScoreEditor.vue +++ b/frontend/src/components/admin/MultiRoundScoreEditor.vue @@ -40,7 +40,7 @@ {{ round.label }} {{ t('table.total') }} - {{ t('table.verification') }} + {{ t('table.verification') }} @@ -80,7 +80,7 @@ /> {{ totalScore(row.playerId) }} - +
{{ round.label }} @@ -151,7 +151,7 @@ {{ totalScore(row.playerId) }}
-
+

{{ t('table.verification') }}

@@ -195,6 +195,7 @@ const props = defineProps({ showSeed: { type: Boolean, default: false }, sectionByGroup: { type: Boolean, default: false }, overflowLimit: { type: Number, default: 0 }, + showProofControls: { type: Boolean, default: true }, playerImage: { type: Function, required: true }, displayName: { type: Function, required: true }, secondaryName: { type: Function, required: true }, diff --git a/frontend/src/components/admin/ScoreStageEditor.vue b/frontend/src/components/admin/ScoreStageEditor.vue index 6f4590b..3203fe2 100644 --- a/frontend/src/components/admin/ScoreStageEditor.vue +++ b/frontend/src/components/admin/ScoreStageEditor.vue @@ -25,7 +25,7 @@ {{ t('table.group') }} {{ t('table.score') }} {{ inputLabel }} - {{ t('table.verification') }} + {{ t('table.verification') }} {{ t('table.rank') }} {{ t('table.seed') }} @@ -65,23 +65,7 @@ @keydown.enter.prevent="onScoreCommit(stage, row.playerId)" /> - -
- - - -
- + {{ row.rank }} {{ row.seed }} @@ -130,7 +114,7 @@ @keydown.enter.prevent="onScoreCommit(stage, row.playerId)" /> -
+
@@ -173,6 +157,7 @@ const props = defineProps({ inputLabel: { type: String, required: true }, sectionByGroup: { type: Boolean, default: false }, tieBreakSectionMode: { type: Boolean, default: false }, + showProofControls: { type: Boolean, default: true }, playerImage: { type: Function, required: true }, displayName: { type: Function, required: true }, secondaryName: { type: Function, required: true }, diff --git a/frontend/src/constants/i18n.js b/frontend/src/constants/i18n.js index 0fbccd3..664327a 100644 --- a/frontend/src/constants/i18n.js +++ b/frontend/src/constants/i18n.js @@ -139,6 +139,7 @@ export const I18N = { removeImage: 'حذف الصورة', delete: 'حذف', resetScores: 'تصفير نتائج المرحلة', + resetFinalGroupBySeed: 'إعادة مجموعات النهائي حسب التصنيف (1-6 / 7-12)', resetOnlyScores: 'تصفير النتائج فقط', resetScoresAndProofs: 'تصفير النتائج وحذف الإثبات', randomEvenGroups: 'توزيع عشوائي متوازن', @@ -340,6 +341,7 @@ export const I18N = { removeImage: 'Remove Image', delete: 'Delete', resetScores: 'Reset Stage Scores', + resetFinalGroupBySeed: 'Reset Final Groups By Seed (1-6 / 7-12)', resetOnlyScores: 'Reset scores only', resetScoresAndProofs: 'Reset scores and proofs', randomEvenGroups: 'Random even grouping',