feat(workouts): save an edited resubmit of a hand-entered workout

The manual form sends one manual: id per opening and resends it after
an edit. The batch route now treats a MANUAL entry with a manual: id
as an overwrite, the way stats: measurements are: the stored row takes
the new values and the entry reports "updated", or "duplicate" when
nothing changed. Records keyed on the workout are removed under the
personal-records lock and re-derived. Every other id stays
first-write-wins. The status enum gains "updated".
This commit is contained in:
Marc-André Bombeck committed 2026-10-03 17:59:20 +02:00
1 parent 6bb65824bf
commit 916f918674
6 files changed
+271 -19

No files matched your search

@@ -138,6 +138,18 @@ describe("saveManualWorkout — the wire", () => {
).resolves.toBe("duplicate");
});
it("treats an edited resubmit (`updated`) as saved and refreshes", async () => {
fetchSpy.mockResolvedValue(
envelope({ entries: [{ index: 0, status: "updated" }] }),
);
const client = fakeClient();
await expect(
saveManualWorkout(ENTRY, client as unknown as QueryClient),
).resolves.toBe("updated");
expect(client.invalidateQueries).toHaveBeenCalled();
});
it("throws when the route skipped the entry, and refreshes nothing", async () => {
fetchSpy.mockResolvedValue(
envelope({ entries: [{ index: 0, status: "skipped" }] }),
@@ -59,7 +59,7 @@ interface BatchResult {
entries?: Array<{ status: string }>;
}
export type ManualWorkoutSaveOutcome = "inserted" | "duplicate";
export type ManualWorkoutSaveOutcome = "inserted" | "duplicate" | "updated";
/**
* Post the entry and refresh what shows workouts. A `duplicate` is a success:
@@ -74,7 +74,7 @@ export async function saveManualWorkout(
workouts: [entry],
});
const status = result?.entries?.[0]?.status;
if (status !== "inserted" && status !== "duplicate") {
if (status !== "inserted" && status !== "duplicate" && status !== "updated") {
throw new Error(`workout entry not stored: ${status ?? "unknown"}`);
}
// The list, the dashboard tile and every detail read sit under this
@@ -112,7 +112,8 @@ export function ManualWorkoutForm({
const timezone = user?.timezone || DEFAULT_TIMEZONE;
// Minted once per opened form and sent with every submit, so a second tap
// or a retried request lands as a duplicate rather than a second row.
// or a retried request lands as a duplicate rather than a second row, and
// a submit after an edit updates the row the first submit stored.
const [externalId] = useState(newManualWorkoutExternalId);
const [draft, setDraft] = useState<ManualWorkoutDraft>(() =>
emptyManualWorkoutDraft(new Date(), timezone),