Files
ApiCore/ApiCore-Frontend/src/lib/suggestions/grouping.ts
T
Nick SzaunigandClaude Opus 5.5 c2f71bcb03
Backend Image / build-and-push (push) Failing after 2m45s
Frontend Image / build-and-push (push) Successful in 5m52s
fix: T4a browser check findings
- 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]>
2026-10-14 18:26:09 +02:00

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 };
}