strat-gameplay-webapp/frontend-sba/components/Lineup/PositionSelector.vue
Cal Corum e058bc4a6c CLAUDE: RosterLink refactor for bench players with cached player data
- 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>
2026-01-17 22:15:12 -06:00

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>