This commit is contained in:
2026-05-20 10:34:32 +04:00
parent 99d28c2114
commit 2ccfac2d04
7 changed files with 85 additions and 28 deletions

View File

@@ -74,6 +74,7 @@
:position-boards="positionBoards" :position-boards="positionBoards"
:live-group-code="liveMonitorGroupCode" :live-group-code="liveMonitorGroupCode"
:live-group-members="liveMonitorMembers" :live-group-members="liveMonitorMembers"
:prelim-tie="prelimTie"
:prelim-tie-rows="prelimTieRows" :prelim-tie-rows="prelimTieRows"
:preliminary-rows="preliminaryRows" :preliminary-rows="preliminaryRows"
:finalists="finalists" :finalists="finalists"
@@ -178,6 +179,7 @@
@remove-player-image="removePlayerImage" @remove-player-image="removePlayerImage"
@delete-player="deletePlayer" @delete-player="deletePlayer"
@request-reset-stage="openResetStageModal" @request-reset-stage="openResetStageModal"
@reset-final-group-by-seed="resetFinalGroupBySeed"
@save-position-board="savePositionBoard" @save-position-board="savePositionBoard"
@update-score-filter="updateScoreFilter" @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() { async function autoGroupEvenly() {
if (primaryGroups.value.length === 0) { if (primaryGroups.value.length === 0) {
showToast(t('messages.noGroupsConfigured'), 'error') showToast(t('messages.noGroupsConfigured'), 'error')

View File

@@ -135,6 +135,7 @@
:score-proof-for="scoreProofFor" :score-proof-for="scoreProofFor"
:open-score-proof-uploader="openScoreProofUploader" :open-score-proof-uploader="openScoreProofUploader"
:open-proof-preview="openProofPreview" :open-proof-preview="openProofPreview"
:show-proof-controls="false"
/> />
</section> </section>
@@ -195,6 +196,7 @@
:request-score-advice="requestScoreAdvice" :request-score-advice="requestScoreAdvice"
:highlight-stage="activeScoringStage" :highlight-stage="activeScoringStage"
:highlight-group="activeScoringGroup" :highlight-group="activeScoringGroup"
:show-proof-controls="false"
@update:filter="$emit('update-score-filter', { stage: 'prelim_tiebreak', value: $event })" @update:filter="$emit('update-score-filter', { stage: 'prelim_tiebreak', value: $event })"
/> />
</template> </template>
@@ -208,6 +210,7 @@
<div class="panel-actions"> <div class="panel-actions">
<button class="btn btn-outline" @click="$emit('request-reset-stage', 'final')">{{ t('actions.resetScores') }}</button> <button class="btn btn-outline" @click="$emit('request-reset-stage', 'final')">{{ t('actions.resetScores') }}</button>
<button class="btn btn-outline" @click="$emit('reset-final-group-by-seed')">{{ t('actions.resetFinalGroupBySeed') }}</button>
</div> </div>
<div class="stage-filter-bar"> <div class="stage-filter-bar">
@@ -259,6 +262,7 @@
:score-proof-for="scoreProofFor" :score-proof-for="scoreProofFor"
:open-score-proof-uploader="openScoreProofUploader" :open-score-proof-uploader="openScoreProofUploader"
:open-proof-preview="openProofPreview" :open-proof-preview="openProofPreview"
:show-proof-controls="false"
/> />
</section> </section>
@@ -314,6 +318,7 @@
:request-score-advice="requestScoreAdvice" :request-score-advice="requestScoreAdvice"
:highlight-stage="activeScoringStage" :highlight-stage="activeScoringStage"
:highlight-group="activeScoringGroup" :highlight-group="activeScoringGroup"
:show-proof-controls="false"
@update:filter="$emit('update-score-filter', { stage: 'final_tiebreak', value: $event })" @update:filter="$emit('update-score-filter', { stage: 'final_tiebreak', value: $event })"
/> />
</template> </template>
@@ -439,6 +444,7 @@ defineEmits([
'remove-player-image', 'remove-player-image',
'delete-player', 'delete-player',
'request-reset-stage', 'request-reset-stage',
'reset-final-group-by-seed',
'save-position-board', 'save-position-board',
'update-score-filter', 'update-score-filter',
]) ])

View File

@@ -151,7 +151,7 @@
<td class="mono strong">{{ row.score }}</td> <td class="mono strong">{{ row.score }}</td>
<td class="mono">{{ row.tieBreak }}</td> <td class="mono">{{ row.tieBreak }}</td>
</tr> </tr>
<tr v-if="preliminaryRows.length === 0"><td colspan="4" class="muted center">{{ t('labels.noPlayers') }}</td></tr> <tr v-if="preliminaryRowsForLive.length === 0"><td colspan="4" class="muted center">{{ t('labels.noPlayers') }}</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -374,6 +374,7 @@ const props = defineProps({
positionBoards: { type: Object, default: () => ({}) }, positionBoards: { type: Object, default: () => ({}) },
liveGroupCode: { type: String, default: '' }, liveGroupCode: { type: String, default: '' },
liveGroupMembers: { type: Array, required: true }, liveGroupMembers: { type: Array, required: true },
prelimTie: { type: Object, default: () => ({ required: false, resolved: true, slots: 0, playerIds: [] }) },
prelimTieRows: { type: Array, required: true }, prelimTieRows: { type: Array, required: true },
preliminaryRows: { type: Array, required: true }, preliminaryRows: { type: Array, required: true },
finalists: { 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 finalistIds = computed(() => new Set((props.finalists || []).map((row) => row.playerId)))
const prelimTieIds = computed(() => new Set((props.prelimTieRows || []).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 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 activeView = computed(() => props.liveSettings.activeView || 'group_live')
const listPageIndex = ref(0) const listPageIndex = ref(0)
const tieGroupIndex = ref(0) const tieGroupIndex = ref(0)
@@ -418,8 +420,10 @@ const rotationPlayerCount = computed(() => {
return Math.floor(raw) return Math.floor(raw)
}) })
const preliminaryRowsForLive = computed(() => rankPreliminaryRowsWithTieBreak(props.preliminaryRows || []))
const listRowsForActiveView = computed(() => { 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 || [] if (activeView.value === 'final_overall') return props.finalOverallRows || []
return [] return []
}) })
@@ -432,7 +436,7 @@ const listPageCount = computed(() => {
const currentListPageDisplay = computed(() => `${listPageIndex.value + 1} / ${listPageCount.value}`) 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 pagedFinalOverallRows = computed(() => paginateRows(props.finalOverallRows || []))
const groupedPrelimTieRows = computed(() => groupTieRowsByPositionBoard('prelim_tiebreak', props.prelimTieRows || [])) const groupedPrelimTieRows = computed(() => groupTieRowsByPositionBoard('prelim_tiebreak', props.prelimTieRows || []))
const groupedFinalTieRows = computed(() => groupTieRowsByPositionBoard('final_tiebreak', props.finalTieRows || [])) const groupedFinalTieRows = computed(() => groupTieRowsByPositionBoard('final_tiebreak', props.finalTieRows || []))
@@ -508,7 +512,7 @@ function isFinalTiePlayer(playerId) {
} }
function prelimOverallHintLabel(row) { 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') if (isFinalist(row.playerId)) return props.t('labels.finalist')
return '' return ''
} }
@@ -538,6 +542,31 @@ function paginateRows(rows) {
return rows.slice(start, start + rotationPlayerCount.value) 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() { function restartListRotation() {
stopListRotation() stopListRotation()
listPageIndex.value = 0 listPageIndex.value = 0

View File

@@ -3,10 +3,9 @@
<div class="modal-card"> <div class="modal-card">
<h3 class="modal-title">{{ t('actions.resetScores') }}</h3> <h3 class="modal-title">{{ t('actions.resetScores') }}</h3>
<p class="modal-text">{{ t('messages.confirmReset') }}</p> <p class="modal-text">{{ t('messages.confirmReset') }}</p>
<p class="modal-text subtle">{{ t('messages.resetProofPrompt') }}</p> <!-- <p class="modal-text subtle">{{ t('messages.resetProofPrompt') }}</p> -->
<div class="modal-actions"> <div class="modal-actions">
<button class="btn btn-outline" @click="$emit('confirm', false)">{{ t('actions.resetOnlyScores') }}</button> <button class="btn btn-outline" @click="$emit('confirm', false)">{{ t('actions.resetOnlyScores') }}</button>
<button class="btn btn-danger" @click="$emit('confirm', true)">{{ t('actions.resetScoresAndProofs') }}</button>
<button class="btn btn-secondary" @click="$emit('cancel')">{{ t('actions.cancel') }}</button> <button class="btn btn-secondary" @click="$emit('cancel')">{{ t('actions.cancel') }}</button>
</div> </div>
</div> </div>

View File

@@ -40,7 +40,7 @@
{{ round.label }} {{ round.label }}
</th> </th>
<th>{{ t('table.total') }}</th> <th>{{ t('table.total') }}</th>
<th>{{ t('table.verification') }}</th> <th v-if="showProofControls">{{ t('table.verification') }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -80,7 +80,7 @@
/> />
</td> </td>
<td class="mono strong">{{ totalScore(row.playerId) }}</td> <td class="mono strong">{{ totalScore(row.playerId) }}</td>
<td class="verify-round-cell"> <td v-if="showProofControls" class="verify-round-cell">
<div class="verify-round-list"> <div class="verify-round-list">
<div v-for="round in roundStages" :key="'verify-' + round.stage + '-' + row.playerId" class="verify-round-item"> <div v-for="round in roundStages" :key="'verify-' + round.stage + '-' + row.playerId" class="verify-round-item">
<span class="verify-round-label">{{ round.label }}</span> <span class="verify-round-label">{{ round.label }}</span>
@@ -151,7 +151,7 @@
<strong class="mono">{{ totalScore(row.playerId) }}</strong> <strong class="mono">{{ totalScore(row.playerId) }}</strong>
</div> </div>
<div class="mobile-verify-block"> <div v-if="showProofControls" class="mobile-verify-block">
<p class="mobile-verify-title">{{ t('table.verification') }}</p> <p class="mobile-verify-title">{{ t('table.verification') }}</p>
<div class="verify-round-list mobile"> <div class="verify-round-list mobile">
<div v-for="round in roundStages" :key="'mobile-verify-' + round.stage + '-' + row.playerId" class="verify-round-item"> <div v-for="round in roundStages" :key="'mobile-verify-' + round.stage + '-' + row.playerId" class="verify-round-item">
@@ -195,6 +195,7 @@ const props = defineProps({
showSeed: { type: Boolean, default: false }, showSeed: { type: Boolean, default: false },
sectionByGroup: { type: Boolean, default: false }, sectionByGroup: { type: Boolean, default: false },
overflowLimit: { type: Number, default: 0 }, overflowLimit: { type: Number, default: 0 },
showProofControls: { type: Boolean, default: true },
playerImage: { type: Function, required: true }, playerImage: { type: Function, required: true },
displayName: { type: Function, required: true }, displayName: { type: Function, required: true },
secondaryName: { type: Function, required: true }, secondaryName: { type: Function, required: true },

View File

@@ -25,7 +25,7 @@
<th v-if="showGroup">{{ t('table.group') }}</th> <th v-if="showGroup">{{ t('table.group') }}</th>
<th v-if="showScoreBeforeInput">{{ t('table.score') }}</th> <th v-if="showScoreBeforeInput">{{ t('table.score') }}</th>
<th :class="{ 'stage-column-highlight': isHighlightedStage(stage, section) }">{{ inputLabel }}</th> <th :class="{ 'stage-column-highlight': isHighlightedStage(stage, section) }">{{ inputLabel }}</th>
<th>{{ t('table.verification') }}</th> <th v-if="showProofControls">{{ t('table.verification') }}</th>
<th v-if="showRank">{{ t('table.rank') }}</th> <th v-if="showRank">{{ t('table.rank') }}</th>
<th v-if="showSeed">{{ t('table.seed') }}</th> <th v-if="showSeed">{{ t('table.seed') }}</th>
</tr> </tr>
@@ -65,23 +65,7 @@
@keydown.enter.prevent="onScoreCommit(stage, row.playerId)" @keydown.enter.prevent="onScoreCommit(stage, row.playerId)"
/> />
</td> </td>
<td>
<div class="proof-actions">
<button class="btn btn-outline btn-xs" @click="openScoreProofUploader(stage, row.playerId)">
{{ hasScoreProof(stage, row.playerId) ? t('actions.replaceProof') : t('actions.uploadProof') }}
</button>
<button v-if="hasScoreProof(stage, row.playerId)" class="btn btn-danger btn-xs" @click="removeScoreProof(stage, row.playerId)">
{{ t('actions.removeProof') }}
</button>
<img
v-if="hasScoreProof(stage, row.playerId)"
class="proof-thumb"
:src="scoreProofFor(stage, row.playerId)"
:alt="t('table.verification')"
@click="openProofPreview(stage, row.playerId)"
/>
</div>
</td>
<td v-if="showRank" class="mono rank">{{ row.rank }}</td> <td v-if="showRank" class="mono rank">{{ row.rank }}</td>
<td v-if="showSeed" class="mono">{{ row.seed }}</td> <td v-if="showSeed" class="mono">{{ row.seed }}</td>
</tr> </tr>
@@ -130,7 +114,7 @@
@keydown.enter.prevent="onScoreCommit(stage, row.playerId)" @keydown.enter.prevent="onScoreCommit(stage, row.playerId)"
/> />
<div class="proof-actions mobile-proof-actions"> <div v-if="showProofControls" class="proof-actions mobile-proof-actions">
<button class="btn btn-outline btn-xs" @click="openScoreProofUploader(stage, row.playerId)"> <button class="btn btn-outline btn-xs" @click="openScoreProofUploader(stage, row.playerId)">
{{ hasScoreProof(stage, row.playerId) ? t('actions.replaceProof') : t('actions.uploadProof') }} {{ hasScoreProof(stage, row.playerId) ? t('actions.replaceProof') : t('actions.uploadProof') }}
</button> </button>
@@ -173,6 +157,7 @@ const props = defineProps({
inputLabel: { type: String, required: true }, inputLabel: { type: String, required: true },
sectionByGroup: { type: Boolean, default: false }, sectionByGroup: { type: Boolean, default: false },
tieBreakSectionMode: { type: Boolean, default: false }, tieBreakSectionMode: { type: Boolean, default: false },
showProofControls: { type: Boolean, default: true },
playerImage: { type: Function, required: true }, playerImage: { type: Function, required: true },
displayName: { type: Function, required: true }, displayName: { type: Function, required: true },
secondaryName: { type: Function, required: true }, secondaryName: { type: Function, required: true },

View File

@@ -139,6 +139,7 @@ export const I18N = {
removeImage: 'حذف الصورة', removeImage: 'حذف الصورة',
delete: 'حذف', delete: 'حذف',
resetScores: 'تصفير نتائج المرحلة', resetScores: 'تصفير نتائج المرحلة',
resetFinalGroupBySeed: 'إعادة مجموعات النهائي حسب التصنيف (1-6 / 7-12)',
resetOnlyScores: 'تصفير النتائج فقط', resetOnlyScores: 'تصفير النتائج فقط',
resetScoresAndProofs: 'تصفير النتائج وحذف الإثبات', resetScoresAndProofs: 'تصفير النتائج وحذف الإثبات',
randomEvenGroups: 'توزيع عشوائي متوازن', randomEvenGroups: 'توزيع عشوائي متوازن',
@@ -340,6 +341,7 @@ export const I18N = {
removeImage: 'Remove Image', removeImage: 'Remove Image',
delete: 'Delete', delete: 'Delete',
resetScores: 'Reset Stage Scores', resetScores: 'Reset Stage Scores',
resetFinalGroupBySeed: 'Reset Final Groups By Seed (1-6 / 7-12)',
resetOnlyScores: 'Reset scores only', resetOnlyScores: 'Reset scores only',
resetScoresAndProofs: 'Reset scores and proofs', resetScoresAndProofs: 'Reset scores and proofs',
randomEvenGroups: 'Random even grouping', randomEvenGroups: 'Random even grouping',