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 @@
{{ t('actions.resetScores') }}
{{ t('messages.confirmReset') }}
-
{{ t('messages.resetProofPrompt') }}
+
-
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',