Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
564a18cfcb | ||
|
|
ce363d3b51 |
@@ -23,6 +23,7 @@ import { EventsService, EventStatusSchema } from "./services/events";
|
|||||||
import { AccountsService, type AccountRole } from "./services/accounts";
|
import { AccountsService, type AccountRole } from "./services/accounts";
|
||||||
import { Value } from "@sinclair/typebox/value";
|
import { Value } from "@sinclair/typebox/value";
|
||||||
import { AssessmentsService } from "./services/assessments";
|
import { AssessmentsService } from "./services/assessments";
|
||||||
|
import { Not } from "@sinclair/typebox";
|
||||||
|
|
||||||
// CONSTANTS
|
// CONSTANTS
|
||||||
// -----------------------
|
// -----------------------
|
||||||
@@ -509,6 +510,16 @@ export const app = new Elysia()
|
|||||||
id: t.Optional(t.String()),
|
id: t.Optional(t.String()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.put("/:id", async ({ body: { player, activityPerformances }, params: { id } }) => {
|
||||||
|
const updated = await s.Assessments.update(id, player, activityPerformances);
|
||||||
|
if (!updated) throw new NotFoundError("Assessment not found");
|
||||||
|
}, {
|
||||||
|
params: t.Object({ id: t.String() }),
|
||||||
|
body: t.Object({
|
||||||
|
player: PlayerSchema,
|
||||||
|
activityPerformances: t.Array(ActivityPerformanceSchema),
|
||||||
|
})
|
||||||
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { Activity, type ActivityPerformance, type Player } from "@blade-and-braw
|
|||||||
import { db } from "../database/db";
|
import { db } from "../database/db";
|
||||||
|
|
||||||
export class AssessmentsService {
|
export class AssessmentsService {
|
||||||
async create(player: Player, activityPerformances: ActivityPerformance[], accountId?: string) {
|
private static performanceFor = (activityPerformances: ActivityPerformance[], activity: Activity) =>
|
||||||
const performanceFor = (activity: Activity) =>
|
activityPerformances.find((p) => p.activity === activity)?.performance ?? null;
|
||||||
activityPerformances.find((p) => p.activity === activity)?.performance ?? null;
|
|
||||||
|
|
||||||
|
async create(player: Player, activityPerformances: ActivityPerformance[], accountId?: string) {
|
||||||
return await db.insertInto("assessments")
|
return await db.insertInto("assessments")
|
||||||
.values({
|
.values({
|
||||||
account_id: accountId ?? null,
|
account_id: accountId ?? null,
|
||||||
@@ -13,12 +13,12 @@ export class AssessmentsService {
|
|||||||
age: player.metrics.age,
|
age: player.metrics.age,
|
||||||
weight: player.metrics.weight,
|
weight: player.metrics.weight,
|
||||||
gender: player.metrics.gender,
|
gender: player.metrics.gender,
|
||||||
perf_back_squat: performanceFor(Activity.BackSquat),
|
perf_back_squat: AssessmentsService.performanceFor(activityPerformances, Activity.BackSquat),
|
||||||
perf_deadlift: performanceFor(Activity.Deadlift),
|
perf_deadlift: AssessmentsService.performanceFor(activityPerformances, Activity.Deadlift),
|
||||||
perf_bench_press: performanceFor(Activity.BenchPress),
|
perf_bench_press: AssessmentsService.performanceFor(activityPerformances, Activity.BenchPress),
|
||||||
perf_broad_jump: performanceFor(Activity.BroadJump),
|
perf_broad_jump: AssessmentsService.performanceFor(activityPerformances, Activity.BroadJump),
|
||||||
perf_run: performanceFor(Activity.Run),
|
perf_run: AssessmentsService.performanceFor(activityPerformances, Activity.Run),
|
||||||
perf_cone_drill: performanceFor(Activity.ConeDrill),
|
perf_cone_drill: AssessmentsService.performanceFor(activityPerformances, Activity.ConeDrill),
|
||||||
})
|
})
|
||||||
.returning("id")
|
.returning("id")
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
@@ -40,6 +40,25 @@ export class AssessmentsService {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update(id: string, player: Player, activityPerformances: ActivityPerformance[]) {
|
||||||
|
const result = await db.updateTable("assessments")
|
||||||
|
.where("id", "=", id)
|
||||||
|
.set({
|
||||||
|
name: player.name ?? "Anonymous",
|
||||||
|
age: player.metrics.age,
|
||||||
|
weight: player.metrics.weight,
|
||||||
|
gender: player.metrics.gender,
|
||||||
|
perf_back_squat: AssessmentsService.performanceFor(activityPerformances, Activity.BackSquat),
|
||||||
|
perf_deadlift: AssessmentsService.performanceFor(activityPerformances, Activity.Deadlift),
|
||||||
|
perf_bench_press: AssessmentsService.performanceFor(activityPerformances, Activity.BenchPress),
|
||||||
|
perf_broad_jump: AssessmentsService.performanceFor(activityPerformances, Activity.BroadJump),
|
||||||
|
perf_run: AssessmentsService.performanceFor(activityPerformances, Activity.Run),
|
||||||
|
perf_cone_drill: AssessmentsService.performanceFor(activityPerformances, Activity.ConeDrill),
|
||||||
|
})
|
||||||
|
.executeTakeFirst();
|
||||||
|
return result.numUpdatedRows > 0n;
|
||||||
|
}
|
||||||
|
|
||||||
async delete(id: string, accountId: string): Promise<boolean> {
|
async delete(id: string, accountId: string): Promise<boolean> {
|
||||||
const result = await db.deleteFrom("assessments")
|
const result = await db.deleteFrom("assessments")
|
||||||
.where("id", "=", id)
|
.where("id", "=", id)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
import { beforeNavigate } from "$app/navigation";
|
||||||
import {
|
import {
|
||||||
LevelCalculator,
|
LevelCalculator,
|
||||||
type LevelCalculatorOutput,
|
type LevelCalculatorOutput,
|
||||||
@@ -26,12 +27,30 @@
|
|||||||
levels?: LevelCalculatorOutput;
|
levels?: LevelCalculatorOutput;
|
||||||
player: Player;
|
player: Player;
|
||||||
activityPerformances: ActivityPerformance[];
|
activityPerformances: ActivityPerformance[];
|
||||||
|
/** JSON snapshot of {player, activityPerformances} as of the last successful save/load. "" means never saved. */
|
||||||
|
savedSnapshot: string;
|
||||||
saving?: boolean;
|
saving?: boolean;
|
||||||
saveError?: string;
|
saveError?: string;
|
||||||
deleting?: boolean;
|
deleting?: boolean;
|
||||||
deleteError?: string;
|
deleteError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function snapshotOf(
|
||||||
|
player: Player,
|
||||||
|
activityPerformances: ActivityPerformance[],
|
||||||
|
): string {
|
||||||
|
return JSON.stringify({ player, activityPerformances });
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDirty(calculation: CalcData): boolean {
|
||||||
|
return (
|
||||||
|
snapshotOf(
|
||||||
|
calculation.player,
|
||||||
|
calculation.activityPerformances,
|
||||||
|
) !== calculation.savedSnapshot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
allStandards: Standards;
|
allStandards: Standards;
|
||||||
}
|
}
|
||||||
@@ -54,21 +73,25 @@
|
|||||||
} as const satisfies Record<Activity, keyof AssessmentRow>;
|
} as const satisfies Record<Activity, keyof AssessmentRow>;
|
||||||
|
|
||||||
function assessmentToCalc(assessment: AssessmentRow): CalcData {
|
function assessmentToCalc(assessment: AssessmentRow): CalcData {
|
||||||
|
const player: Player = {
|
||||||
|
name: assessment.name,
|
||||||
|
metrics: {
|
||||||
|
age: assessment.age,
|
||||||
|
weight: assessment.weight,
|
||||||
|
gender: assessment.gender as Gender,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const activityPerformances = Object.values(Activity).map((a) => ({
|
||||||
|
activity: a,
|
||||||
|
performance: (assessment[PERF_COLUMN[a]] as number | null) ?? 0,
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: crypto.randomUUID(),
|
key: crypto.randomUUID(),
|
||||||
id: assessment.id,
|
id: assessment.id,
|
||||||
player: {
|
player,
|
||||||
name: assessment.name,
|
activityPerformances,
|
||||||
metrics: {
|
savedSnapshot: snapshotOf(player, activityPerformances),
|
||||||
age: assessment.age,
|
|
||||||
weight: assessment.weight,
|
|
||||||
gender: assessment.gender as Gender,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
activityPerformances: Object.values(Activity).map((a) => ({
|
|
||||||
activity: a,
|
|
||||||
performance: (assessment[PERF_COLUMN[a]] as number | null) ?? 0,
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +127,10 @@
|
|||||||
});
|
});
|
||||||
if (res.error) throw res.error;
|
if (res.error) throw res.error;
|
||||||
calculation.id = res.data?.id;
|
calculation.id = res.data?.id;
|
||||||
|
calculation.savedSnapshot = snapshotOf(
|
||||||
|
calculation.player,
|
||||||
|
calculation.activityPerformances,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
calculation.saveError = errorMessage(err);
|
calculation.saveError = errorMessage(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -132,6 +159,24 @@
|
|||||||
|
|
||||||
onMount(loadAssessments);
|
onMount(loadAssessments);
|
||||||
|
|
||||||
|
beforeNavigate((navigation) => {
|
||||||
|
if (!calculations.some(isDirty)) return;
|
||||||
|
|
||||||
|
if (navigation.type === "leave") {
|
||||||
|
// triggers the browser's native "leave site?" confirmation
|
||||||
|
navigation.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
"You have unsaved player changes that will be lost. Leave anyway?",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
navigation.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
for (const calculation of calculations) {
|
for (const calculation of calculations) {
|
||||||
calculation.levels = levelCalculator.calculate(
|
calculation.levels = levelCalculator.calculate(
|
||||||
@@ -144,6 +189,7 @@
|
|||||||
const createCalculation = function (name: string): CalcData {
|
const createCalculation = function (name: string): CalcData {
|
||||||
return {
|
return {
|
||||||
key: crypto.randomUUID(),
|
key: crypto.randomUUID(),
|
||||||
|
savedSnapshot: "", // never saved — always dirty until the first save
|
||||||
player: {
|
player: {
|
||||||
name: name,
|
name: name,
|
||||||
metrics: {
|
metrics: {
|
||||||
@@ -260,12 +306,19 @@
|
|||||||
class="card card-compact bg-base-200 p-4 shadow-lg max-w-sm w-full"
|
class="card card-compact bg-base-200 p-4 shadow-lg max-w-sm w-full"
|
||||||
>
|
>
|
||||||
<div class="card-body gap-4 p-4">
|
<div class="card-body gap-4 p-4">
|
||||||
<input
|
<div class="flex items-center gap-2">
|
||||||
class="input input-bordered input-sm w-full"
|
<input
|
||||||
type="text"
|
class="input input-bordered input-sm w-full"
|
||||||
bind:value={calculation.player.name}
|
type="text"
|
||||||
placeholder="Player name"
|
bind:value={calculation.player.name}
|
||||||
/>
|
placeholder="Player name"
|
||||||
|
/>
|
||||||
|
{#if isDirty(calculation)}
|
||||||
|
<div class="badge badge-warning badge-sm shrink-0">
|
||||||
|
Unsaved
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul
|
<ul
|
||||||
class="bg-base-100 rounded-box shadow-xs divide-y divide-base-300"
|
class="bg-base-100 rounded-box shadow-xs divide-y divide-base-300"
|
||||||
@@ -387,7 +440,7 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onclick={() => saveCalculation(calculation)}
|
onclick={() => saveCalculation(calculation)}
|
||||||
disabled={calculation.saving}
|
disabled={calculation.saving || !isDirty(calculation)}
|
||||||
class="btn btn-primary btn-sm flex-1"
|
class="btn btn-primary btn-sm flex-1"
|
||||||
>
|
>
|
||||||
{calculation.saving ? "Saving..." : "Save"}
|
{calculation.saving ? "Saving..." : "Save"}
|
||||||
|
|||||||
Reference in New Issue
Block a user