- Add player_positions JSONB column to roster_links (migration 006) - Add player_data JSONB column to cache name/image/headshot (migration 007) - Add is_pitcher/is_batter computed properties for two-way player support - Update lineup submission to populate RosterLink with all players + positions - Update get_bench handler to use cached data (no runtime API calls) - Add BenchPlayer type to frontend with proper filtering - Add new Lineup components: InlineSubstitutionPanel, LineupSlotRow, PositionSelector, UnifiedLineupTab - Add integration tests for get_bench_players Bench players now load instantly without API dependency, and properly filter batters vs pitchers (including CP closer position). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
94 lines
2.3 KiB
Vue
94 lines
2.3 KiB
Vue
<template>
|
|
<div class="position-selector">
|
|
<div class="text-xs text-gray-400 mb-2">
|
|
{{ label }}
|
|
<span v-if="required" class="text-red-400">(required)</span>
|
|
</div>
|
|
<div class="flex flex-wrap gap-1.5">
|
|
<button
|
|
v-for="pos in displayPositions"
|
|
:key="pos"
|
|
:class="[
|
|
'px-3 py-2 text-xs font-bold rounded-lg transition-colors select-none touch-manipulation',
|
|
getPositionClass(pos)
|
|
]"
|
|
:disabled="isDisabled(pos)"
|
|
@click="selectPosition(pos)"
|
|
>
|
|
{{ pos }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
|
|
type SubstitutionMode = 'pinch_hitter' | 'pinch_runner' | 'defensive_replacement' | 'relief_pitcher' | 'position_change'
|
|
|
|
interface Props {
|
|
mode: SubstitutionMode
|
|
modelValue: string | null
|
|
currentPosition?: string | null
|
|
label?: string
|
|
required?: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
currentPosition: null,
|
|
label: 'Position for new player:',
|
|
required: false,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: string]
|
|
}>()
|
|
|
|
// Standard field positions
|
|
const FIELD_POSITIONS = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'] as const
|
|
|
|
// Compute display positions based on mode
|
|
const displayPositions = computed(() => {
|
|
switch (props.mode) {
|
|
case 'pinch_hitter':
|
|
return ['PH', ...FIELD_POSITIONS]
|
|
case 'pinch_runner':
|
|
return ['PR', ...FIELD_POSITIONS]
|
|
case 'defensive_replacement':
|
|
case 'position_change':
|
|
return FIELD_POSITIONS
|
|
case 'relief_pitcher':
|
|
return ['P']
|
|
default:
|
|
return FIELD_POSITIONS
|
|
}
|
|
})
|
|
|
|
// Check if a position should be disabled
|
|
const isDisabled = (pos: string): boolean => {
|
|
// For position change, disable the current position
|
|
if (props.mode === 'position_change' && pos === props.currentPosition) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Get CSS class for position button
|
|
const getPositionClass = (pos: string): string => {
|
|
if (isDisabled(pos)) {
|
|
return 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
|
}
|
|
if (props.modelValue === pos) {
|
|
return 'bg-blue-600 text-white'
|
|
}
|
|
return 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
}
|
|
|
|
// Handle position selection
|
|
const selectPosition = (pos: string) => {
|
|
if (!isDisabled(pos)) {
|
|
emit('update:modelValue', pos)
|
|
}
|
|
}
|
|
</script>
|