Platform backbone (plan 2026-10-13-plattform-rueckgrat), reviewed tasks: - run roles api/worker/job/all with shared startup checks (Task 1) - schema and migration for DomainEvent, deliveries, JobRun, ThrottlerHit (Task 2) - audit, notifications and domain events written in the same transaction (Task 3) - dispatcher with SKIP LOCKED claims, retries and stale release (Task 4) - worker building blocks: tick, LISTEN/NOTIFY wake-up, health (Task 5) - worker process and entry points (Task 6) - throttler counters in Postgres (Task 19) - separate keys per purpose with key ring (Task 20) - public origin page without exact location (Task 22) - shop keys split into read and write (Task 23) - Kustomize base, components, overlay encry, CI validation (Task 26) Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), reviewed tasks: - brood frames, read endpoints, apiary overview and offline pack - network basics, offline queue v3, navigation sheets, form building blocks - new inspection with last-time hints and weather, varroa per day, treatment prefill, apiary page with round - inspection photo (Task 10) is implemented, review pending Not rolled out. Remaining tasks follow in later commits. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useConnectivity } from "@/lib/offline/connectivity";
|
|
import { fetchTreatmentPrefill } from "./api";
|
|
import type { TreatmentPrefill } from "./types";
|
|
|
|
const DEBOUNCE_MS = 500;
|
|
const MIN_PRODUCT_LENGTH = 2;
|
|
const MAX_PRODUCT_LENGTH = 200;
|
|
|
|
/**
|
|
* The beekeeper's own last values for the product typed so far. Waits until
|
|
* typing pauses, needs at least two characters and asks only while online -
|
|
* a prefill is a convenience, never a reason to block or queue anything.
|
|
* While a request for the same product is pending or the device is offline,
|
|
* the last answer for that product stays; for any other product it is dropped.
|
|
*/
|
|
export function useTreatmentPrefill(product: string): TreatmentPrefill | null {
|
|
const trimmed = product.trim();
|
|
const [debounced, setDebounced] = useState("");
|
|
const connectivity = useConnectivity();
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setDebounced(trimmed), DEBOUNCE_MS);
|
|
return () => clearTimeout(timer);
|
|
}, [trimmed]);
|
|
|
|
const isSettled = debounced === trimmed;
|
|
const isAskable =
|
|
trimmed.length >= MIN_PRODUCT_LENGTH && trimmed.length <= MAX_PRODUCT_LENGTH;
|
|
const enabled = connectivity === "online" && isSettled && isAskable;
|
|
const { data } = useQuery({
|
|
queryKey: ["treatments", "prefill", debounced],
|
|
queryFn: () => fetchTreatmentPrefill(debounced),
|
|
enabled,
|
|
});
|
|
|
|
// The last settled answer is kept only for the product it was fetched for
|
|
// (pending request, offline). Once the product differs it is dropped at
|
|
// once, so a failed request can never leave another product's values.
|
|
const [held, setHeld] = useState<{ product: string; prefill: TreatmentPrefill | null } | null>(
|
|
null,
|
|
);
|
|
let settled: TreatmentPrefill | null | undefined;
|
|
if (isSettled && !isAskable) settled = null;
|
|
else if (enabled) settled = data;
|
|
if (settled !== undefined && (held?.product !== trimmed || held.prefill !== settled)) {
|
|
setHeld({ product: trimmed, prefill: settled });
|
|
}
|
|
if (settled !== undefined) return settled;
|
|
return held?.product === trimmed ? held.prefill : null;
|
|
}
|