mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
7fffd4300f
PAD-06 and PAD-09 on the client's data side. A tour's cached row keeps its checklist beside its provenance, written through one cache helper by the repository and the queue replay, which also keeps provenance an answer lacks. The checklist is read for the bench the list shows and served from the cache while its revision matches. Decisions made offline, or behind a queued write of the same tour, change the cached checklist and bench at once under a local revision and replay with their idempotency key; a route written offline marks the checklist stale until the server judged it. The obstacle store learns a camera focus: a click in the rail moves the camera onto an obstacle and the other parts of its lock, the newest of such a focus and a picked search result wins, and a selected lock lights up whole on both maps. The checklist hook and its model hold the logic the cards will show.
64 lines
2.5 KiB
TypeScript
64 lines
2.5 KiB
TypeScript
import { obstacleStatusOf, type ObstacleReviewOp, type TourObstacleCheck, type TourObstacleEntry } from '@trek/shared';
|
|
|
|
interface LocalContext {
|
|
/** When the change was made, as an ISO instant. */
|
|
now: string;
|
|
userId: number | null;
|
|
/** The revision the changed checklist carries until the server answers. */
|
|
revision: string;
|
|
}
|
|
|
|
/** A decision on a current entry, read against the entry's own basis as the server will. */
|
|
function decided(entry: TourObstacleEntry, op: ObstacleReviewOp, ctx: LocalContext): TourObstacleEntry {
|
|
if (!entry.basis || !op.decision || !op.basis) return entry;
|
|
const note = op.note || null;
|
|
const record = entry.decision;
|
|
const same =
|
|
record?.decision === op.decision &&
|
|
record.note === note &&
|
|
entry.basis.route === op.basis.route &&
|
|
entry.basis.source === op.basis.source;
|
|
const { status, changed } = obstacleStatusOf({
|
|
current: { route: entry.basis.route, source: entry.basis.source, portage: entry.portage },
|
|
review: { decision: op.decision, route: op.basis.route, source: op.basis.source },
|
|
});
|
|
return {
|
|
...entry,
|
|
status,
|
|
changed,
|
|
decision: same && record ? record : { decision: op.decision, note, decided_at: ctx.now, decided_by: ctx.userId },
|
|
};
|
|
}
|
|
|
|
function applied(entry: TourObstacleEntry, op: ObstacleReviewOp, ctx: LocalContext): TourObstacleEntry | null {
|
|
if (op.op === 'decide') return entry.relation === null ? entry : decided(entry, op, ctx);
|
|
if (op.op === 'note')
|
|
return entry.decision ? { ...entry, decision: { ...entry.decision, note: op.note || null } } : entry;
|
|
if (op.op === 'clear') {
|
|
// An earlier decision goes with its entry; a current entry is open again.
|
|
if (entry.relation === null) return null;
|
|
return { ...entry, status: 'open', changed: [], decision: null };
|
|
}
|
|
return entry.status === 'gone' ? { ...entry, status: 'kept' } : entry;
|
|
}
|
|
|
|
/**
|
|
* The checklist as it reads once the changes land, worked out here for an
|
|
* offline save or the moment before the server answers: the server stores
|
|
* the same decisions against the same basis and answers the same statuses.
|
|
*/
|
|
export function applyObstacleOps(
|
|
check: TourObstacleCheck,
|
|
ops: readonly ObstacleReviewOp[],
|
|
ctx: LocalContext
|
|
): TourObstacleCheck {
|
|
const byKey = new Map(ops.map((op) => [op.key, op]));
|
|
const entries = check.entries.flatMap((entry) => {
|
|
const op = byKey.get(entry.key);
|
|
if (!op) return [entry];
|
|
const next = applied(entry, op, ctx);
|
|
return next ? [next] : [];
|
|
});
|
|
return { ...check, revision: ctx.revision, entries };
|
|
}
|