feat: Wetter-Widget auf der Bienenstand-Detailseite
Neue WeatherCard auf /apiaries/[id]: Trachtprognose-Badge mit Begruendung, 7-Tage-Vorhersagestreifen (Temperatur + Niederschlag). Zeigt einen Hinweis statt eines Fehlers, wenn fuer den Bienenstand noch keine Koordinaten hinterlegt sind. TASKS.md: Wetterintegration + Trachtprognosen jetzt vollstaendig (Backend aus vorherigem Commit + Frontend hier) abgehakt. Verifiziert: tsc, lint, build, alle Unit-Tests (14/14) gruen. Live- Browser-Check war blockiert durch dieselbe wiederkehrende WSL/ Postgres-Portweiterleitungs-Unzuverlaessigkeit wie zuvor in der Session - die Funktionalitaet ist stattdessen durch die Backend-e2e- Suite abgedeckt, die echten Postgres UND die echte Open-Meteo-API aufruft (163/163 gruen). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
ce9cea1b94
commit
0518a5740f
5 files changed
+165
-2
No files matched your search
@@ -9,6 +9,7 @@ import { QrCodeDialog } from "@/components/qr-code-dialog";
|
|||||||
import { DocumentUploadCard } from "@/components/document-upload-card";
|
import { DocumentUploadCard } from "@/components/document-upload-card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ApiaryEditCard } from "./apiary-edit-card";
|
import { ApiaryEditCard } from "./apiary-edit-card";
|
||||||
|
import { WeatherCard } from "./weather-card";
|
||||||
|
|
||||||
const CURRENT_YEAR = new Date().getFullYear();
|
const CURRENT_YEAR = new Date().getFullYear();
|
||||||
|
|
||||||
@@ -40,6 +41,10 @@ export default function ApiaryDetailPage() {
|
|||||||
<QrCodeDialog entityType="apiaries" id={apiary.id} />
|
<QrCodeDialog entityType="apiaries" id={apiary.id} />
|
||||||
</div>
|
</div>
|
||||||
<ApiaryEditCard key={apiary.id} apiary={apiary} />
|
<ApiaryEditCard key={apiary.id} apiary={apiary} />
|
||||||
|
<WeatherCard
|
||||||
|
apiaryId={apiary.id}
|
||||||
|
hasCoordinates={apiary.latitude !== null && apiary.longitude !== null}
|
||||||
|
/>
|
||||||
<DocumentUploadCard entityType="APIARY" entityId={apiary.id} />
|
<DocumentUploadCard entityType="APIARY" entityId={apiary.id} />
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { CloudSun } from "lucide-react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { fetchApiaryWeather, fetchTrachtprognose } from "@/lib/weather/api";
|
||||||
|
import { TrachtprognoseRating } from "@/lib/weather/types";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
|
const RATING_LABELS: Record<TrachtprognoseRating, string> = {
|
||||||
|
GUT: "Gute Tracht",
|
||||||
|
MAESSIG: "Mäßige Tracht",
|
||||||
|
SCHWACH: "Schwache Tracht",
|
||||||
|
KEINE_TRACHT: "Keine Tracht",
|
||||||
|
};
|
||||||
|
|
||||||
|
const RATING_VARIANTS: Record<
|
||||||
|
TrachtprognoseRating,
|
||||||
|
"default" | "secondary" | "warning" | "outline"
|
||||||
|
> = {
|
||||||
|
GUT: "default",
|
||||||
|
MAESSIG: "secondary",
|
||||||
|
SCHWACH: "warning",
|
||||||
|
KEINE_TRACHT: "outline",
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatDayLabel(dateIso: string): string {
|
||||||
|
return new Date(dateIso).toLocaleDateString("de-DE", {
|
||||||
|
weekday: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WeatherCard({
|
||||||
|
apiaryId,
|
||||||
|
hasCoordinates,
|
||||||
|
}: {
|
||||||
|
apiaryId: string;
|
||||||
|
hasCoordinates: boolean;
|
||||||
|
}) {
|
||||||
|
const { data: trachtprognose, isLoading: trachtLoading } = useQuery({
|
||||||
|
queryKey: ["apiaries", apiaryId, "trachtprognose"],
|
||||||
|
queryFn: () => fetchTrachtprognose(apiaryId),
|
||||||
|
enabled: hasCoordinates,
|
||||||
|
});
|
||||||
|
const { data: weather, isLoading: weatherLoading } = useQuery({
|
||||||
|
queryKey: ["apiaries", apiaryId, "weather"],
|
||||||
|
queryFn: () => fetchApiaryWeather(apiaryId),
|
||||||
|
enabled: hasCoordinates,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasCoordinates) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<CloudSun className="size-4.5" />
|
||||||
|
Wetter & Trachtprognose
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Für diesen Bienenstand sind noch keine Koordinaten hinterlegt.
|
||||||
|
Trage Breiten- und Längengrad ein, um Wetterdaten zu sehen.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
const forecastDays = (weather?.daily ?? []).filter((day) => day.date >= today).slice(0, 7);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<CloudSun className="size-4.5" />
|
||||||
|
Wetter & Trachtprognose
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{trachtLoading ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Lädt…</p>
|
||||||
|
) : trachtprognose ? (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={RATING_VARIANTS[trachtprognose.rating]}>
|
||||||
|
{RATING_LABELS[trachtprognose.rating]}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-sm text-muted-foreground">{trachtprognose.season}</span>
|
||||||
|
</div>
|
||||||
|
<ul className="flex list-disc flex-col gap-1 pl-5 text-sm text-muted-foreground">
|
||||||
|
{trachtprognose.reasoning.map((line) => (
|
||||||
|
<li key={line}>{line}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{weatherLoading ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Wetterdaten laden…</p>
|
||||||
|
) : forecastDays.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-3 gap-2 border-t border-border/50 pt-3 sm:grid-cols-7">
|
||||||
|
{forecastDays.map((day) => (
|
||||||
|
<div key={day.date} className="flex flex-col items-center gap-0.5 text-center">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{formatDayLabel(day.date)}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{day.tempMaxC !== null ? `${Math.round(day.tempMaxC)}°` : "–"}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{day.tempMinC !== null ? `${Math.round(day.tempMinC)}°` : "–"}
|
||||||
|
</span>
|
||||||
|
{day.precipitationMm !== null && day.precipitationMm > 0 ? (
|
||||||
|
<span className="text-xs text-primary">
|
||||||
|
{day.precipitationMm.toFixed(0)} mm
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { apiFetch } from "@/lib/auth/api-client";
|
||||||
|
import { ApiaryWeather, Trachtprognose } from "./types";
|
||||||
|
|
||||||
|
export function fetchApiaryWeather(apiaryId: string): Promise<ApiaryWeather> {
|
||||||
|
return apiFetch<ApiaryWeather>(`/apiaries/${apiaryId}/weather`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchTrachtprognose(apiaryId: string): Promise<Trachtprognose> {
|
||||||
|
return apiFetch<Trachtprognose>(`/apiaries/${apiaryId}/trachtprognose`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export interface DailyWeather {
|
||||||
|
date: string;
|
||||||
|
tempMaxC: number | null;
|
||||||
|
tempMinC: number | null;
|
||||||
|
precipitationMm: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiaryWeather {
|
||||||
|
daily: DailyWeather[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TrachtprognoseRating = "GUT" | "MAESSIG" | "SCHWACH" | "KEINE_TRACHT";
|
||||||
|
|
||||||
|
export interface Trachtprognose {
|
||||||
|
rating: TrachtprognoseRating;
|
||||||
|
season: string;
|
||||||
|
reasoning: string[];
|
||||||
|
historicalAverageYieldKg: number | null;
|
||||||
|
}
|
||||||
@@ -281,8 +281,8 @@ Reihenfolge = Bau-Reihenfolge. Gleiche Haken-Regel wie MVP: erst setzen, wenn Li
|
|||||||
Software-seitig umsetzbare Punkte werden gebaut. Punkte, die dedizierte native Build-Pipelines, echte ML-Modelle oder physische Hardware voraussetzen, sind als "nicht umsetzbar in diesem Kontext" markiert statt mit einer Alibi-Umsetzung – Begründung jeweils dahinter.
|
Software-seitig umsetzbare Punkte werden gebaut. Punkte, die dedizierte native Build-Pipelines, echte ML-Modelle oder physische Hardware voraussetzen, sind als "nicht umsetzbar in diesem Kontext" markiert statt mit einer Alibi-Umsetzung – Begründung jeweils dahinter.
|
||||||
|
|
||||||
- [x] Kartenansicht (Bienenstände mit Koordinaten auf einer Karte)
|
- [x] Kartenansicht (Bienenstände mit Koordinaten auf einer Karte)
|
||||||
- [ ] Wetterintegration / Wetterhistorie je Bienenstand (externe Wetter-API anhand Bienenstand-Koordinaten)
|
- [x] Wetterintegration / Wetterhistorie je Bienenstand (externe Wetter-API anhand Bienenstand-Koordinaten)
|
||||||
- [ ] Trachtprognosen (heuristische Einschätzung aus Wetterdaten + Saison + historischen Ernten, kein ML-Modell)
|
- [x] Trachtprognosen (heuristische Einschätzung aus Wetterdaten + Saison + historischen Ernten, kein ML-Modell)
|
||||||
- [x] Öffentliche Herkunftsseiten für Honigchargen + Kunden-QR-Code (Abschnitt 27: `wabenhain.com/herkunft/{lot}`, öffentlicher Read-Endpoint)
|
- [x] Öffentliche Herkunftsseiten für Honigchargen + Kunden-QR-Code (Abschnitt 27: `wabenhain.com/herkunft/{lot}`, öffentlicher Read-Endpoint)
|
||||||
- [x] Vollständig offlinefähige PWA mit Sync (Service Worker + IndexedDB-Warteschlange für Durchsichten, Sync bei Wiederverbindung)
|
- [x] Vollständig offlinefähige PWA mit Sync (Service Worker + IndexedDB-Warteschlange für Durchsichten, Sync bei Wiederverbindung)
|
||||||
- [x] NFC-Tags (Web-NFC-API, nur Chrome/Android; kodiert denselben Deep-Link wie der QR-Code; kein Fallback für andere Browser möglich)
|
- [x] NFC-Tags (Web-NFC-API, nur Chrome/Android; kodiert denselben Deep-Link wie der QR-Code; kein Fallback für andere Browser möglich)
|
||||||
|
|||||||
Reference in new issue
Block a user