Files
MulliganMates/components/scorecard/full-grid.tsx
T
2026-03-18 12:37:17 +01:00

175 lines
6.7 KiB
TypeScript

'use client'
import { strokesReceived, scoreColorClass, formatVsPar } from '@/lib/handicap'
import type { Hole, Player } from '@/app/(app)/rounds/[id]/scorecard/page'
type Props = {
holes: Hole[]
players: Player[]
scores: Record<string, number>
onTapScore: (player: Player, hole: Hole) => void
isActive: boolean
}
export function FullGrid({ holes, players, scores, onTapScore, isActive }: Props) {
const totalHoles = holes.length
const front = holes.filter((h) => h.hole_number <= 9)
const back = holes.filter((h) => h.hole_number > 9)
const hasBoth = front.length > 0 && back.length > 0
function getGross(playerId: string, hole: Hole) {
return scores[`${playerId}-${hole.id}`] ?? null
}
function getNet(playerId: string, hole: Hole) {
const gross = getGross(playerId, hole)
if (gross === null) return null
const ch = players.find((p) => p.user_id === playerId)?.course_handicap ?? 0
return gross - strokesReceived(ch, hole.stroke_index, totalHoles)
}
function subtotal(playerId: string, holeSet: Hole[], fn: (p: string, h: Hole) => number | null) {
const values = holeSet.map((h) => fn(playerId, h)).filter((v): v is number => v !== null)
return values.length === holeSet.length ? values.reduce((a, b) => a + b, 0) : null
}
const parFront = front.reduce((s, h) => s + h.par, 0)
const parBack = back.reduce((s, h) => s + h.par, 0)
const parTotal = parFront + parBack
return (
<div className="overflow-x-auto">
<table className="min-w-full text-xs border-collapse">
<thead>
<tr className="bg-muted/50 text-muted-foreground">
<th className="sticky left-0 z-10 bg-muted/50 px-3 py-2 text-left font-medium min-w-[100px]">
Player
</th>
{front.map((h) => (
<th key={h.id} className="px-2 py-2 text-center font-medium w-8">
{h.hole_number}
</th>
))}
{hasBoth && (
<th className="px-2 py-2 text-center font-semibold w-10 bg-muted">Out</th>
)}
{back.map((h) => (
<th key={h.id} className="px-2 py-2 text-center font-medium w-8">
{h.hole_number}
</th>
))}
{hasBoth && (
<th className="px-2 py-2 text-center font-semibold w-10 bg-muted">In</th>
)}
<th className="px-2 py-2 text-center font-semibold w-12 bg-muted">Tot</th>
<th className="px-2 py-2 text-center font-bold w-12 bg-muted">Net</th>
</tr>
{/* Par row */}
<tr className="bg-muted/20 text-muted-foreground border-b">
<td className="sticky left-0 z-10 bg-muted/20 px-3 py-1 text-xs italic">Par</td>
{front.map((h) => (
<td key={h.id} className="px-2 py-1 text-center">
{h.par}
</td>
))}
{hasBoth && (
<td className="px-2 py-1 text-center font-medium bg-muted">{parFront}</td>
)}
{back.map((h) => (
<td key={h.id} className="px-2 py-1 text-center">
{h.par}
</td>
))}
{hasBoth && (
<td className="px-2 py-1 text-center font-medium bg-muted">{parBack}</td>
)}
<td className="px-2 py-1 text-center font-medium bg-muted">{parTotal}</td>
<td className="px-2 py-1 text-center bg-muted" />
</tr>
</thead>
<tbody className="divide-y">
{players.map((player, pi) => {
const frontOut = subtotal(player.user_id, front, getGross)
const backIn = subtotal(player.user_id, back, getGross)
const total =
frontOut !== null && backIn !== null
? frontOut + backIn
: hasBoth
? null
: subtotal(player.user_id, holes, getGross)
const netTotal = subtotal(player.user_id, holes, getNet)
const netVsPar = netTotal !== null ? netTotal - parTotal : null
return (
<tr key={player.user_id} className={pi % 2 === 0 ? '' : 'bg-muted/20'}>
<td className="sticky left-0 z-10 bg-background px-3 py-2 font-medium truncate max-w-[100px]">
{player.profile?.display_name}
</td>
{front.map((h) => {
const gross = getGross(player.user_id, h)
const grossVsPar = gross !== null ? gross - h.par : null
return (
<td
key={h.id}
className="px-1 py-1.5 text-center"
onClick={() => isActive && onTapScore(player, h)}
>
<span
className={`flex h-6 w-6 mx-auto items-center justify-center text-xs cursor-pointer ${
gross !== null ? scoreColorClass(grossVsPar) : 'text-muted-foreground'
}`}
>
{gross ?? '·'}
</span>
</td>
)
})}
{hasBoth && (
<td className="px-2 py-1.5 text-center font-semibold bg-muted/30">
{frontOut ?? '—'}
</td>
)}
{back.map((h) => {
const gross = getGross(player.user_id, h)
const grossVsPar = gross !== null ? gross - h.par : null
return (
<td
key={h.id}
className="px-1 py-1.5 text-center"
onClick={() => isActive && onTapScore(player, h)}
>
<span
className={`flex h-6 w-6 mx-auto items-center justify-center text-xs cursor-pointer ${
gross !== null ? scoreColorClass(grossVsPar) : 'text-muted-foreground'
}`}
>
{gross ?? '·'}
</span>
</td>
)
})}
{hasBoth && (
<td className="px-2 py-1.5 text-center font-semibold bg-muted/30">
{backIn ?? '—'}
</td>
)}
<td className="px-2 py-1.5 text-center font-semibold bg-muted/30">
{total ?? '—'}
</td>
<td className="px-2 py-1.5 text-center font-bold bg-muted/30">
{netVsPar !== null ? formatVsPar(netVsPar) : '—'}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}