feat: platform backbone and field capture complete (checkpoint)
Platform backbone (plan 2026-10-13-plattform-rueckgrat), all tasks reviewed: - webhooks with delivery table, retries and stable delivery id - calendar handlers; a finished task removes its Outlook event - tasks, automations, inspections, varroa, treatments, feedings, harvests, colonies, wintering, stock and bottling record domain events in the same transaction; legacy post-commit calls removed - row locks through one helper (lockRowForUpdate, lockRowsForShare, lockRowsForUpdate) - job framework with JobRun, cli exit codes, cleanup, task templates, daily evaluation, hourly app-backup and the rekey job - system API and page "Verwaltung > System" with retry - @nestjs/schedule removed, guard tests against in-process schedulers Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), all tasks reviewed: - inspection photo offline without metadata - bulk capture for treatments, feedings and varroa (backend + frontend) - offline stand mode with service worker, offline pages and packs - offline session, cold start and logout on shared devices Docs: ARCHITECTURE.md assumptions and sections updated, TASKS.md. Gates: backend lint, typecheck, 1770 unit, 420 e2e, build, no schema drift; frontend lint, typecheck, 913 tests, build. Not rolled out. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
c01a38835a
commit
cdd30cf3ee
288 files changed
+39165
-2853
No files matched your search
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchApiaries } from "@/lib/apiaries/api";
|
||||
import { livingColoniesFromPacks } from "@/lib/apiaries/offline-pack";
|
||||
import { useCurrentUserId } from "@/lib/auth/auth-context";
|
||||
import { fetchColonies } from "@/lib/colonies/api";
|
||||
import { isLivingColony } from "@/lib/colonies/status";
|
||||
import type { Colony } from "@/lib/colonies/types";
|
||||
import { withPackFallback } from "@/lib/offline/pack-fallback";
|
||||
|
||||
/** What a bulk choice needs of a colony - a server colony or one from an offline pack. */
|
||||
export type BulkColony = Pick<Colony, "id" | "code" | "status" | "apiaryId">;
|
||||
export interface BulkApiary {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** Where the choices came from: the server, or - only without network - the loaded packs. */
|
||||
export type ChoiceSource = "server" | "packs";
|
||||
|
||||
interface Choices<T> {
|
||||
items: T[];
|
||||
source: ChoiceSource;
|
||||
}
|
||||
|
||||
/** Prefix of the queries here; they read the server or this device's storage, never both as one. */
|
||||
const BULK_CHOICES_KEY = ["bulk-choices"] as const;
|
||||
|
||||
function byCode(a: BulkColony, b: BulkColony): number {
|
||||
return a.code.localeCompare(b.code, "de", { numeric: true });
|
||||
}
|
||||
|
||||
function livingOf(colonies: readonly BulkColony[]): BulkColony[] {
|
||||
return colonies
|
||||
.filter(isLivingColony)
|
||||
.map(({ id, code, status, apiaryId }) => ({ id, code, status, apiaryId }))
|
||||
.sort(byCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* The apiaries to choose from: the server's active ones, or - only when the
|
||||
* network failed - those of the user's offline packs. Names only: nothing
|
||||
* else of an apiary (location!) is needed or kept.
|
||||
*/
|
||||
export function useApiaryChoices() {
|
||||
const userId = useCurrentUserId();
|
||||
return useQuery({
|
||||
queryKey: [...BULK_CHOICES_KEY, userId, "apiaries"],
|
||||
queryFn: (): Promise<Choices<BulkApiary>> =>
|
||||
withPackFallback<Choices<BulkApiary>>(
|
||||
async () => ({
|
||||
source: "server",
|
||||
items: (await fetchApiaries({ status: "ACTIVE" })).map(({ id, name }) => ({ id, name })),
|
||||
}),
|
||||
(packs) =>
|
||||
packs.length === 0
|
||||
? null
|
||||
: {
|
||||
source: "packs",
|
||||
items: packs
|
||||
.map(({ pack }) => ({ id: pack.apiary.id, name: pack.apiary.name }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, "de")),
|
||||
},
|
||||
userId ?? "",
|
||||
),
|
||||
// Reads this device's storage when offline, never waits for the network.
|
||||
networkMode: "always",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The living colonies of one apiary - ended ones never appear. Without
|
||||
* network the apiary's offline pack answers; with no pack for it the
|
||||
* request fails and the caller says so.
|
||||
*/
|
||||
export function useApiaryColonies(apiaryId: string | null) {
|
||||
const userId = useCurrentUserId();
|
||||
return useQuery({
|
||||
queryKey: [...BULK_CHOICES_KEY, userId, "colonies", apiaryId],
|
||||
queryFn: (): Promise<Choices<BulkColony>> =>
|
||||
withPackFallback<Choices<BulkColony>>(
|
||||
async () => ({
|
||||
source: "server",
|
||||
items: livingOf(await fetchColonies({ apiaryId: apiaryId ?? undefined })),
|
||||
}),
|
||||
(packs) =>
|
||||
packs.some((stored) => stored.apiaryId === apiaryId)
|
||||
? {
|
||||
source: "packs",
|
||||
items: livingOf(
|
||||
livingColoniesFromPacks(packs).filter((colony) => colony.apiaryId === apiaryId),
|
||||
),
|
||||
}
|
||||
: null,
|
||||
userId ?? "",
|
||||
),
|
||||
enabled: apiaryId !== null && apiaryId !== "",
|
||||
networkMode: "always",
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user