Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions app/assets/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,78 @@ main.mega-report {
background-color: var(--alliance-red-bg) !important;
}

/* ── PMVA Track Form ────────────────────────────────────────────────── */

main.track.pmva button {
font-size: 1rem;
padding: 0.5rem 1.5rem;
}

.pmva-form {
display: flex;
flex-direction: column;
gap: var(--space-lg);
max-width: 40rem;
}

.pmva-form-row {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}

.pmva-form-row label {
font-weight: 600;
}

.pmva-form-row textarea {
resize: vertical;
font-family: inherit;
font-size: 1rem;
padding: var(--space-sm);
min-height: 3rem;
}

.pmva-form-row input[type="number"] {
width: 6rem;
font-size: 1rem;
padding: var(--space-sm);
}

.pmva-hint {
font-size: 0.85rem;
color: var(--color-text-secondary);
margin: 0;
}

.pmva-btn-group {
display: flex;
gap: var(--space-xs);
}

.pmva-counter-row {
display: flex;
align-items: center;
gap: var(--space-sm);
}

.pmva-counter-value {
font-size: 1.2rem;
font-weight: 700;
min-width: 1.5rem;
text-align: center;
}

.pmva-counter-pct {
font-size: 1rem;
font-weight: 600;
color: var(--color-accent);
}

.pmva-form-actions {
padding-top: var(--space-base);
}

/* ── PMVA Report ─────────────────────────────────────────────────────── */

.pmva-stats-table {
Expand Down
152 changes: 101 additions & 51 deletions app/common/track/seq/PMVALoadShootSeqPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,62 +32,112 @@ const PMVALoadShootSeqPage = ({}: TrackScreenProps) => {
// loadNotes --> stores pmva-load-comments
// pmva-unload-comments
}

return <main className="track pmva">
<div>
<TrackNav/>
<h2>Post-Match Video Analysis - Pickup, Score & Shoot Sequence</h2>
<p>Was the hopper filled?
<button onClick={() => setHopperFilled(true)}
disabled={hopperFilled}>Yes</button>
<button onClick={() => setHopperFilled(false)}
disabled={!(hopperFilled === undefined) && !hopperFilled}>No
</button>
</p>
<p>
Anything noteworthy about this load?
<textarea onChange={(e) => setLoadNotes(e.target.value)}></textarea>
</p>
<p>Click on the buttons below to add to the count for this sequence. Play the video back slowly and get an
accurate count for the sequence. The count is shown so that you can verify the amout if you're not sure
what you saw.</p>
<button onClick={() => setScoreCount(scoreCount + 1)}>Score One</button>
({scoreCount})
<button onClick={() => setMissCount(missCount + 1)}>Miss One</button> ({missCount})
({Math.round(100 * scoreCount / Math.max(1, scoreCount + missCount))}%)
<p>How long did it take to unload (rewind the video and play the sequence back. Use
either the video time or a stopwatch)
<input type="number" min={0} max={155}
onClick={(e) => setUnloadSeconds(Number((e.target as HTMLInputElement).value))}/> seconds.
</p>
<p>How much fuel was stuck after the end of the sequence?
<input type="number" min={0} max={50}
onClick={(e) => setStuckFuelCount(Number((e.target as HTMLInputElement).value))}/> pieces.
</p>
<p>Anything noteworthy about this shooting session?
<textarea onChange={(e) => setUnloadComments(e.target.value)}></textarea>
</p>
<p>Where was the robot while shooting? (select one)
<button disabled={shootPosition === ShootPosition.close}
onClick={() => setShootPosition(ShootPosition.close)}>Close</button>
<button disabled={shootPosition === ShootPosition.mid}
onClick={() => setShootPosition(ShootPosition.mid)}>Mid
</button>
<button disabled={shootPosition === ShootPosition.far}
onClick={() => setShootPosition(ShootPosition.far)}>Far
</button>
</p>
<p>Was the robot moving while shooting?
<button disabled={movingWhileShooting} onClick={() => setMovingWhileShooting(true)}>Moving</button>
<button disabled={!(movingWhileShooting === undefined) && !movingWhileShooting} onClick={() => setMovingWhileShooting(false)}>Still</button>
</p>
<p>Was the robot shooting while intaking during this sequence?
<button disabled={shootingWhileIntaking} onClick={() => setShootingWhileIntaking(true)}>Yes</button>
<button disabled={!(shootingWhileIntaking === undefined) && !shootingWhileIntaking} onClick={() => setShootingWhileIntaking(false)}>No</button>
</p>
<button disabled={isIncomplete()} onClick={e => handleSubmit(e)}>Save Sequence</button>
<h2>Post-Match Video Analysis — Pickup, Score & Shoot Sequence</h2>

<div className="pmva-form">
<div className="pmva-form-row">
<label>Was the hopper filled?</label>
<div className="pmva-btn-group">
<button onClick={() => setHopperFilled(true)}
disabled={hopperFilled}>Yes
</button>
<button onClick={() => setHopperFilled(false)}
disabled={!(hopperFilled === undefined) && !hopperFilled}>No
</button>
</div>
</div>

<div className="pmva-form-row">
<label>Anything noteworthy about this load?</label>
<textarea onChange={(e) => setLoadNotes(e.target.value)}/>
</div>

<div className="pmva-form-row">
<label>Score / Miss count</label>
<p className="pmva-hint">
Play the video back slowly and get an accurate count for the sequence.
</p>
<div className="pmva-counter-row">
<button onClick={() => setScoreCount(scoreCount + 1)}>Score One</button>
<span className="pmva-counter-value">{scoreCount}</span>
<button onClick={() => setMissCount(missCount + 1)}>Miss One</button>
<span className="pmva-counter-value">{missCount}</span>
<span className="pmva-counter-pct">
{Math.round(100 * scoreCount / Math.max(1, scoreCount + missCount))}%
</span>
</div>
</div>

<div className="pmva-form-row">
<label>How long did it take to unload? (seconds)</label>
<p className="pmva-hint">
Rewind the video and play the sequence back. Use either the video time or a stopwatch.
</p>
<input type="number" min={0} max={155}
onChange={(e) => setUnloadSeconds(Number(e.target.value))}/>
</div>

<div className="pmva-form-row">
<label>How much fuel was stuck after the sequence?</label>
<input type="number" min={0} max={50}
onChange={(e) => setStuckFuelCount(Number(e.target.value))}/>
</div>

<div className="pmva-form-row">
<label>Anything noteworthy about this shooting session?</label>
<textarea onChange={(e) => setUnloadComments(e.target.value)}/>
</div>

<div className="pmva-form-row">
<label>Where was the robot while shooting?</label>
<div className="pmva-btn-group">
<button disabled={shootPosition === ShootPosition.close}
onClick={() => setShootPosition(ShootPosition.close)}>Close
</button>
<button disabled={shootPosition === ShootPosition.mid}
onClick={() => setShootPosition(ShootPosition.mid)}>Mid
</button>
<button disabled={shootPosition === ShootPosition.far}
onClick={() => setShootPosition(ShootPosition.far)}>Far
</button>
</div>
</div>

<div className="pmva-form-row">
<label>Was the robot moving while shooting?</label>
<div className="pmva-btn-group">
<button disabled={movingWhileShooting}
onClick={() => setMovingWhileShooting(true)}>Moving
</button>
<button disabled={!(movingWhileShooting === undefined) && !movingWhileShooting}
onClick={() => setMovingWhileShooting(false)}>Still
</button>
</div>
</div>

<div className="pmva-form-row">
<label>Was the robot shooting while intaking during this sequence?</label>
<div className="pmva-btn-group">
<button disabled={shootingWhileIntaking}
onClick={() => setShootingWhileIntaking(true)}>Yes
</button>
<button disabled={!(shootingWhileIntaking === undefined) && !shootingWhileIntaking}
onClick={() => setShootingWhileIntaking(false)}>No
</button>
</div>
</div>

<div className="pmva-form-actions">
<button disabled={isIncomplete()} onClick={e => handleSubmit(e)}>Save Sequence</button>
</div>
</div>
</div>
</main>

}

export default PMVALoadShootSeqPage;
export default PMVALoadShootSeqPage;
2 changes: 1 addition & 1 deletion app/common/track/strat-area/AreaPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const AreaPage = ({ areaCode }: TrackScreenProps) => {
))}
{standaloneEvents.length > 0 && (
<>
<h3>Stand-Alone Events</h3>
<h3>Non-Sequence Events</h3>
{standaloneEvents.map((et) => (
<span key={et.eventtype}>
<EventTypeControl eventType={et} sequenceStart={false} sequenceEnd={false} />
Expand Down
78 changes: 60 additions & 18 deletions app/routes/report/summary-report-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
TeamReportComment,
TeamReportRobotAlert,
SequenceReportLink,
DefenceNote,
CustomTournamentStats,
} from "~/types/TeamSummaryReport.ts";

Expand All @@ -25,6 +26,7 @@ const SummaryReportPage = () => {
const [sequenceLinks, setSequenceLinks] = useState<
SequenceReportLink[] | null
>(null);
const [defenceNotes, setDefenceNotes] = useState<DefenceNote[] | null>(null);
const [customStats, setCustomStats] = useState<CustomTournamentStats[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -52,6 +54,7 @@ const SummaryReportPage = () => {
setComments(resp.report.comments);
setRobotAlerts(resp.report.robotAlerts);
setSequenceLinks(resp.report.sequenceReportLinks);
setDefenceNotes(resp.report.defenceNotes ?? []);
} else {
setError(resp.reason || "Failed to load team summary report");
}
Expand Down Expand Up @@ -154,24 +157,6 @@ const SummaryReportPage = () => {
</section>
)}

{compLinks.length > 0 && (
<section className="card">
<h2>Tournament Sequence Reports</h2>
<ul className="nav-list">
{compLinks.map((link) => (
<li key={`${link.sequenceTypeCode}-${link.tournamentId}`}>
<NavLink
to={`/report/tournament/${link.sequenceTypeCode}/${teamId}/${link.tournamentId}`}
className="btn-secondary"
>
{link.sequenceTypeName} @ {tournamentName(link.tournamentId)}
</NavLink>
</li>
))}
</ul>
</section>
)}

{customStats.length > 0 && (
<section className="card">
<h2>
Expand Down Expand Up @@ -229,6 +214,63 @@ const SummaryReportPage = () => {
</section>
)}

{defenceNotes && defenceNotes.length > 0 && (
<section className="card">
<h2>Defence Strategy Notes</h2>
{[...new Set(defenceNotes.map((n) => n.tournamentId))].map(
(tid) => (
<div key={tid}>
<h3>{tournamentName(tid)}</h3>
<div className="mega-report-table-wrapper">
<table className="mega-report-table chrono-table">
<thead>
<tr>
<th>Time</th>
<th>Scout</th>
<th>Match</th>
<th>Note</th>
</tr>
</thead>
<tbody>
{defenceNotes
.filter((n) => n.tournamentId === tid)
.map((n, i) => (
<tr key={i}>
<td>
{new Date(n.timestamp).toLocaleString()}
</td>
<td>{n.displayName}</td>
<td>{n.matchId}</td>
<td>{n.note}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
),
)}
</section>
)}

{compLinks.length > 0 && (
<section className="card">
<h2>Tournament Sequence Reports</h2>
<ul className="nav-list">
{compLinks.map((link) => (
<li key={`${link.sequenceTypeCode}-${link.tournamentId}`}>
<NavLink
to={`/report/tournament/${link.sequenceTypeCode}/${teamId}/${link.tournamentId}`}
className="btn-secondary"
>
{link.sequenceTypeName} @ {tournamentName(link.tournamentId)}
</NavLink>
</li>
))}
</ul>
</section>
)}

{drillLinks.length > 0 && (
<section className="card">
<h2>Drill Sequence Reports</h2>
Expand Down
9 changes: 9 additions & 0 deletions app/types/TeamSummaryReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,19 @@ export interface TeamReportRobotAlert {
alert: string;
}

export interface DefenceNote {
timestamp: string;
displayName: string;
tournamentId: string;
matchId: number;
note: string;
}

export interface TeamSummaryReport {
comments: TeamReportComment[];
robotAlerts: TeamReportRobotAlert[];
sequenceReportLinks: SequenceReportLink[];
defenceNotes: DefenceNote[];
}

export interface TeamSummaryReportResponse {
Expand Down
Loading