- Withdrawal wording from one backend helper everywhere ("bis
einschließlich TT.MM. – ab TT.MM. wieder erntefähig", year when it
crosses New Year); withdrawal-status returns lastBlockedDay and the
ready text, the frontend no longer computes dates
- Calendar events name colony or apiary, withdrawal events carry
product and detail; compact withdrawal rows on mobile
- One bottling warning per colony and treatment; pre-save hint
- Offline: online-only actions disabled with a reason, cached data
shows its timestamp, open sheets block submit offline
- Mobile: no horizontal scroll at 360 px, touch targets >= 44 px,
toasts and the add button no longer cover content
- Cleanup sheet without destructive preselection; natural sort of
colony codes; collapsible automation rule cards; varroa list newest
first with chart axes; text fixes
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
import { compareColonyCodes } from "@/lib/colonies/sort";
|
|
import type { Suggestion, SuggestionIntent } from "./types";
|
|
|
|
export type SuggestionRow =
|
|
| { kind: "single"; suggestion: Suggestion }
|
|
| { kind: "combined"; intent: SuggestionIntent; suggestions: Suggestion[] };
|
|
|
|
export interface SuggestionGroupModel {
|
|
key: string;
|
|
label: string;
|
|
apiaryId: string | null;
|
|
rows: SuggestionRow[];
|
|
}
|
|
|
|
const OPERATION_KEY = "operation";
|
|
const OPERATION_LABEL = "Betrieb";
|
|
|
|
function compareText(a: string, b: string): number {
|
|
return a.localeCompare(b, "de", { numeric: true });
|
|
}
|
|
|
|
function rowsOf(list: readonly Suggestion[]): SuggestionRow[] {
|
|
const byIntent = new Map<SuggestionIntent, Suggestion[]>();
|
|
for (const suggestion of list) {
|
|
byIntent.set(suggestion.intent, [...(byIntent.get(suggestion.intent) ?? []), suggestion]);
|
|
}
|
|
const rows: SuggestionRow[] = [];
|
|
const done = new Set<SuggestionIntent>();
|
|
for (const suggestion of list) {
|
|
if (done.has(suggestion.intent)) continue;
|
|
done.add(suggestion.intent);
|
|
const same = byIntent.get(suggestion.intent) ?? [];
|
|
rows.push(
|
|
same.length > 1
|
|
? { kind: "combined", intent: suggestion.intent, suggestions: same }
|
|
: { kind: "single", suggestion },
|
|
);
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
/**
|
|
* One group per stand (by name), then „Betrieb“ for everything without a
|
|
* stand. Inside a group the API's order (rank) is kept; the same intent is
|
|
* one row, placed where its first suggestion is.
|
|
*/
|
|
export function groupSuggestions(list: readonly Suggestion[]): SuggestionGroupModel[] {
|
|
const stands = new Map<string, Suggestion[]>();
|
|
const operation: Suggestion[] = [];
|
|
for (const suggestion of list) {
|
|
if (suggestion.apiaryId === null) {
|
|
operation.push(suggestion);
|
|
} else {
|
|
stands.set(suggestion.apiaryId, [...(stands.get(suggestion.apiaryId) ?? []), suggestion]);
|
|
}
|
|
}
|
|
const groups: SuggestionGroupModel[] = [...stands.entries()].map(([apiaryId, members]) => ({
|
|
key: apiaryId,
|
|
label: members[0].apiaryName ?? "Stand",
|
|
apiaryId,
|
|
rows: rowsOf(members),
|
|
}));
|
|
groups.sort((a, b) => compareText(a.label, b.label));
|
|
if (operation.length > 0) {
|
|
groups.push({ key: OPERATION_KEY, label: OPERATION_LABEL, apiaryId: null, rows: rowsOf(operation) });
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
export interface RoundTarget {
|
|
apiary: { id: string; name: string };
|
|
colonies: { id: string; code: string }[];
|
|
/** The first inspection of the round. */
|
|
href: string;
|
|
/** The first inspection without round parameters (no signed-in user to keep the round). */
|
|
plainHref: string;
|
|
}
|
|
|
|
/**
|
|
* The stand round for due inspections of one stand: the colonies once each,
|
|
* in code order. Null without a colony at a stand.
|
|
*/
|
|
export function roundTarget(suggestions: readonly Suggestion[]): RoundTarget | null {
|
|
const colonies = new Map<string, string>();
|
|
let apiary: RoundTarget["apiary"] | null = null;
|
|
for (const suggestion of suggestions) {
|
|
if (suggestion.apiaryId === null || suggestion.colonyId === null) continue;
|
|
apiary ??= { id: suggestion.apiaryId, name: suggestion.apiaryName ?? "Stand" };
|
|
if (suggestion.apiaryId === apiary.id) {
|
|
colonies.set(suggestion.colonyId, suggestion.colonyCode ?? suggestion.colonyId);
|
|
}
|
|
}
|
|
if (apiary === null) return null;
|
|
const sorted = [...colonies.entries()]
|
|
.map(([id, code]) => ({ id, code }))
|
|
.sort((a, b) => compareColonyCodes(a.code, b.code));
|
|
const href = `/inspections/new?colonyId=${encodeURIComponent(sorted[0].id)}&round=${encodeURIComponent(apiary.id)}`;
|
|
const plainHref = `/inspections/new?colonyId=${encodeURIComponent(sorted[0].id)}`;
|
|
return { apiary, colonies: sorted, href, plainHref };
|
|
}
|