- Willkommen zurück, Vorname Nachname!
+ Willkommen zurück, {`${user.firstName} ${user.lastName}`}!
Labore id duis minim nisi duis incididunt. Aliqua qui dolor laborum anim aliquip sit nulla eiusmod laboris excepteur sit non laboris do.
diff --git a/frontend/src/routes/_protected/info.tsx b/frontend/src/routes/_protected/info.tsx
new file mode 100644
index 00000000..729b2dbc
--- /dev/null
+++ b/frontend/src/routes/_protected/info.tsx
@@ -0,0 +1,28 @@
+import { infoApi } from "@/api/backendApi";
+import { useAuthHeader } from "@/hooks/useAuthHeader";
+import { useQuery } from "@tanstack/react-query";
+import { createFileRoute } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/_protected/info")({
+ component: Info,
+});
+
+function Info() {
+ const authorization = useAuthHeader();
+
+ const { data, isFetching } = useQuery({
+ queryKey: ["info"],
+ queryFn: () => infoApi.getAppInfo({ authorization }),
+ });
+
+ return (
+
+
App Info
+ {isFetching ? (
+
Loading...
+ ) : (
+
{JSON.stringify(data, null, 2)}
+ )}
+
+ );
+}
diff --git a/frontend/src/routes/_protected/map/index.tsx b/frontend/src/routes/_protected/map/index.tsx
new file mode 100644
index 00000000..40f6bb69
--- /dev/null
+++ b/frontend/src/routes/_protected/map/index.tsx
@@ -0,0 +1,60 @@
+import Map from "@/components/Map";
+import useMapStore from "@/store/store";
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useMapEvents } from "react-leaflet/hooks";
+import { z } from "zod";
+
+const mapSearchParamsSchema = z.object({
+ selected: z.string().optional(),
+ lat: z.number().catch(useMapStore.getState().map.center[0]),
+ lng: z.number().catch(useMapStore.getState().map.center[1]),
+ zoom: z
+ .number()
+ .int()
+ .max(useMapStore.getState().map.maxZoom)
+ .min(useMapStore.getState().map.minZoom)
+ .catch(useMapStore.getState().map.minZoom),
+});
+
+export const Route = createFileRoute("/_protected/map/")({
+ component: MapView,
+ validateSearch: mapSearchParamsSchema,
+ loaderDeps: ({ search: { lat, lng, zoom } }) => ({ lat, lng, zoom }),
+ loader: ({ deps: { lat, lng, zoom } }) => {
+ useMapStore.setState((state) => ({
+ map: { ...state.map, center: [lat, lng], zoom },
+ }));
+ },
+});
+
+function MapView() {
+ return (
+
+
+
+ );
+}
+
+const MapConroller = () => {
+ const navigate = useNavigate({ from: Route.fullPath });
+ const { setCenter, setZoom } = useMapStore((state) => ({
+ setCenter: state.map.setCenter,
+ setZoom: state.map.setZoom,
+ }));
+ const map = useMapEvents({
+ moveend: () => {
+ const center = map.getCenter();
+ const zoom = map.getZoom();
+ setCenter([center.lat, center.lng]);
+ setZoom(zoom);
+ navigate({
+ search: (prev) => ({ ...prev, lat: center.lat, lng: center.lng, zoom }),
+ });
+ },
+ });
+
+ return null;
+};
+
diff --git a/frontend/src/routes/sensors.tsx b/frontend/src/routes/_protected/sensors.tsx
similarity index 93%
rename from frontend/src/routes/sensors.tsx
rename to frontend/src/routes/_protected/sensors.tsx
index 525e5483..86530033 100644
--- a/frontend/src/routes/sensors.tsx
+++ b/frontend/src/routes/_protected/sensors.tsx
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
-export const Route = createFileRoute('/sensors')({
+export const Route = createFileRoute('/_protected/sensors')({
component: Sensors,
})
diff --git a/frontend/src/routes/settings.tsx b/frontend/src/routes/_protected/settings.tsx
similarity index 92%
rename from frontend/src/routes/settings.tsx
rename to frontend/src/routes/_protected/settings.tsx
index 3158a49f..3c56e5bc 100644
--- a/frontend/src/routes/settings.tsx
+++ b/frontend/src/routes/_protected/settings.tsx
@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
-export const Route = createFileRoute("/settings")({
+export const Route = createFileRoute("/_protected/settings")({
component: Settings,
});
diff --git a/frontend/src/routes/team.tsx b/frontend/src/routes/_protected/team.tsx
similarity index 96%
rename from frontend/src/routes/team.tsx
rename to frontend/src/routes/_protected/team.tsx
index 6e7b8cd5..b500c74b 100644
--- a/frontend/src/routes/team.tsx
+++ b/frontend/src/routes/_protected/team.tsx
@@ -1,5 +1,5 @@
import { createFileRoute } from "@tanstack/react-router";
-import { Separator } from "../components/ui/separator";
+import { Separator } from "../../components/ui/separator";
import {
Table,
TableBody,
@@ -8,16 +8,16 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
-import { Button } from "../components/ui/button";
+import { Button } from "@/components/ui/button";
import { Edit, PlusCircleIcon, Trash, Filter } from "lucide-react";
import {
Popover,
PopoverContent,
PopoverTrigger,
-} from "../components/ui/popover";
+} from "@/components/ui/popover";
import { Checkbox } from "@/components/ui/checkbox";
-export const Route = createFileRoute("/team")({
+export const Route = createFileRoute("/_protected/team")({
component: Team,
});
diff --git a/frontend/src/routes/treecluster.tsx b/frontend/src/routes/_protected/treecluster.tsx
similarity index 91%
rename from frontend/src/routes/treecluster.tsx
rename to frontend/src/routes/_protected/treecluster.tsx
index a3c8e298..6f5ebaaf 100644
--- a/frontend/src/routes/treecluster.tsx
+++ b/frontend/src/routes/_protected/treecluster.tsx
@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
-export const Route = createFileRoute("/treecluster")({
+export const Route = createFileRoute("/_protected/treecluster")({
component: Treecluster,
});
diff --git a/frontend/src/routes/vehicles.tsx b/frontend/src/routes/_protected/vehicles.tsx
similarity index 94%
rename from frontend/src/routes/vehicles.tsx
rename to frontend/src/routes/_protected/vehicles.tsx
index 02b5587d..c10eb9cf 100644
--- a/frontend/src/routes/vehicles.tsx
+++ b/frontend/src/routes/_protected/vehicles.tsx
@@ -1,5 +1,5 @@
import { createFileRoute } from "@tanstack/react-router";
-import { Separator } from "../components/ui/separator";
+import { Separator } from "../../components/ui/separator";
import {
Table,
TableBody,
@@ -7,17 +7,17 @@ import {
TableHead,
TableHeader,
TableRow,
-} from "../components/ui/table";
-import { Button } from "../components/ui/button";
+} from "../../components/ui/table";
+import { Button } from "../../components/ui/button";
import { Edit, PlusCircleIcon, Trash, Filter } from "lucide-react";
import {
Popover,
PopoverContent,
PopoverTrigger,
-} from "../components/ui/popover";
-import { Checkbox } from "../components/ui/checkbox";
+} from "../../components/ui/popover";
+import { Checkbox } from "../../components/ui/checkbox";
-export const Route = createFileRoute("/vehicles")({
+export const Route = createFileRoute("/_protected/vehicles")({
component: Vehicles,
});
diff --git a/frontend/src/routes/_protected/waypoints/index.tsx b/frontend/src/routes/_protected/waypoints/index.tsx
new file mode 100644
index 00000000..143c749f
--- /dev/null
+++ b/frontend/src/routes/_protected/waypoints/index.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_protected/waypoints/')({
+ component: () => Hello /_protected/waypoints/!
+})
\ No newline at end of file
diff --git a/frontend/src/routes/_protected/waypoints/new.tsx b/frontend/src/routes/_protected/waypoints/new.tsx
new file mode 100644
index 00000000..e673dd9a
--- /dev/null
+++ b/frontend/src/routes/_protected/waypoints/new.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_protected/waypoints/new')({
+ component: () => Hello /_protected/waypoints/new!
+})
diff --git a/frontend/src/routes/auth/callback.tsx b/frontend/src/routes/auth/callback.tsx
new file mode 100644
index 00000000..624b585a
--- /dev/null
+++ b/frontend/src/routes/auth/callback.tsx
@@ -0,0 +1,60 @@
+import { userApi } from "@/api/backendApi";
+import { KeycloakJWT } from "@/lib/types/keycloak";
+import { decodeJWT } from "@/lib/utils";
+import useStore from "@/store/store";
+import {
+ createFileRoute,
+ redirect as routerRedirect,
+} from "@tanstack/react-router";
+import { z } from "zod";
+
+const authSearchParamsSchema = z.object({
+ session_state: z.string(),
+ iss: z.string(),
+ code: z.string(),
+ redirect: z.string(),
+});
+
+export const Route = createFileRoute("/auth/callback")({
+ validateSearch: authSearchParamsSchema,
+ loaderDeps: ({ search: { code } }) => ({ code }),
+ beforeLoad: async ({ search: { code, redirect } }) => {
+ const token = await userApi
+ .v1UserLoginTokenPost({
+ redirectUrl: `${window.location.origin}/auth/callback?redirect=${encodeURIComponent(redirect)}`,
+ body: {
+ code,
+ },
+ })
+ .catch((err) => {
+ console.error(err);
+ throw new Error(err.message);
+ });
+
+ if (!token) {
+ console.error("Error while fetching token");
+ throw new Error("Error while fetching token");
+ }
+
+
+ useStore.setState((state) => {
+ state.auth.isAuthenticated = true;
+ state.auth.token = token;
+ });
+
+ const jwtInfo = decodeJWT(token.accessToken);
+ if (jwtInfo) {
+ useStore.setState((state) => {
+ state.user.email = jwtInfo.email;
+ state.user.username = jwtInfo.preferred_username;
+ state.user.firstName = jwtInfo.given_name;
+ state.user.lastName = jwtInfo.family_name;
+ });
+ }
+
+ throw routerRedirect({
+ to: redirect,
+ replace: true,
+ });
+ },
+});
diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx
new file mode 100644
index 00000000..cb7963f2
--- /dev/null
+++ b/frontend/src/routes/login.tsx
@@ -0,0 +1,13 @@
+import { userApi } from '@/api/backendApi'
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/login')({
+ beforeLoad: async () => {
+ const loginUrl = await userApi.v1UserLoginGet({
+ redirectUrl: `${window.location.origin}/auth/callback?redirect=${encodeURIComponent("/")}`
+ }).then((res) => res.loginUrl)
+
+ window.location.href = loginUrl
+ }
+
+})
diff --git a/frontend/src/routes/logout.tsx b/frontend/src/routes/logout.tsx
new file mode 100644
index 00000000..0989155d
--- /dev/null
+++ b/frontend/src/routes/logout.tsx
@@ -0,0 +1,33 @@
+import { userApi } from "@/api/backendApi";
+import useStore from "@/store/store";
+import { createFileRoute, redirect } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/logout")({
+ beforeLoad: async () => {
+ const store = useStore.getState();
+
+ if (!store.auth.isAuthenticated) {
+ throw redirect({ to: "/login", replace: true });
+ }
+
+ await userApi
+ .v1UserLogoutPost({
+ body: {
+ refreshToken: store.auth.token?.refreshToken || "",
+ },
+ })
+ .then(() => {
+ console.log("Logged out");
+ useStore.setState((state) => {
+ state.auth.isAuthenticated = false;
+ state.auth.token = null;
+ });
+ })
+ .catch((err) => {
+ console.error(err);
+ throw new Error(err.message);
+ });
+
+ throw redirect({ to: "/login", replace: true });
+ },
+});
diff --git a/frontend/src/routes/map/index.tsx b/frontend/src/routes/map/index.tsx
deleted file mode 100644
index d9a80979..00000000
--- a/frontend/src/routes/map/index.tsx
+++ /dev/null
@@ -1,240 +0,0 @@
-import { treeApi } from "@/api/backendApi";
-import Map from "@/components/Map";
-import MapMarker, { TreeIcon } from "@/components/MapMarker";
-import MapTooltip from "@/components/MapTooltip";
-import { useTrees } from "@/context/TreeDataContext";
-import { cn } from "@/lib/utils";
-import useMapStore from "@/store/map/store";
-import { Tree } from "@green-ecolution/backend-client";
-import { useQuery } from "@tanstack/react-query";
-import { createFileRoute, useNavigate } from "@tanstack/react-router";
-import { Activity, Battery, Droplet, TreePine } from "lucide-react";
-import { useEffect, useMemo } from "react";
-import { useMapEvents } from "react-leaflet/hooks";
-import { toast } from "sonner";
-import { z } from "zod";
-
-const mapSearchParamsSchema = z.object({
- selected: z.string().optional(),
- lat: z.number().catch(useMapStore.getState().map.center[0]),
- lng: z.number().catch(useMapStore.getState().map.center[1]),
- zoom: z
- .number()
- .int()
- .max(useMapStore.getState().map.maxZoom)
- .min(useMapStore.getState().map.minZoom)
- .catch(useMapStore.getState().map.minZoom),
-});
-
-export const Route = createFileRoute("/map/")({
- component: MapView,
- validateSearch: mapSearchParamsSchema,
- loaderDeps: ({ search: { lat, lng, zoom } }) => ({ lat, lng, zoom }),
- loader: ({ deps: { lat, lng, zoom } }) => {
- useMapStore.setState((state) => ({
- map: { ...state.map, center: [lat, lng], zoom },
- }));
- },
-});
-
-function MapView() {
- const { tooltipOpen, tooltipContent, closeTooltip } =
- useMapStore((state) => ({
- tooltipContent: state.tooltip.content,
- tooltipOpen: state.tooltip.isOpen,
- closeTooltip: state.tooltip.close,
- }));
-
- const trees = useTrees();
- const tooltipTitle = "Baumgruppe 110";
- const tooltipDescription = "Zuletzt gegossen: vor 2 Tagen";
-
- return (
-
- closeTooltip()}
- title={tooltipTitle}
- description={tooltipDescription}
- >
-
-
-
-
- );
-}
-
-const MapConroller = () => {
- const navigate = useNavigate({ from: Route.fullPath });
- const { setCenter, setZoom } = useMapStore((state) => ({
- setCenter: state.map.setCenter,
- setZoom: state.map.setZoom,
- }));
- const map = useMapEvents({
- moveend: () => {
- const center = map.getCenter();
- const zoom = map.getZoom();
- setCenter([center.lat, center.lng]);
- setZoom(zoom);
- navigate({
- search: (prev) => ({ ...prev, lat: center.lat, lng: center.lng, zoom }),
- });
- },
- });
-
- return null;
-};
-
-const TreeMarker = ({ trees }: { trees: Tree[] }) => {
- const { openTooltip } = useMapStore((state) => ({
- openTooltip: state.tooltip.open,
- }));
- const treeMarkers = useMemo(
- () =>
- trees.map((tree) => (
- {
- openTooltip(tree);
- }}
- />
- )),
- [trees],
- );
-
- return <>{treeMarkers}>;
-};
-
-const MapTooltipContent = ({ tree }: { tree: Tree }) => {
- const { data, isError, error, isLoading } = useQuery({
- queryKey: ["tree_prediction", tree.id],
- refetchInterval: 10000,
- queryFn: () =>
- treeApi.getTreePredictionById({ treeID: tree.id, sensorData: true }),
- });
-
- useEffect(() => {
- if (data === undefined && isError) {
- console.error("Error while fetching trees", error);
- toast.error(
- "Es ist ein Fehler beim Abrufen der Sensor Daten zum Baum: " + tree.id,
- {
- description: error.message,
- },
- );
- }
- }, [isError, error]);
-
- const sensorData = useMemo(() => {
- return (
- data?.sensorData?.map((d) => ({
- humidity: d.uplinkMessage.decodedPayload.humidity,
- battery: d.uplinkMessage.decodedPayload.battery,
- trunkHumidity: 113,
- timestamp: new Date(d.uplinkMessage.receivedAt),
- })) ?? []
- );
- }, [data]);
-
- const treeActionRecommendation = useMemo(() => {
- if (data?.sensorPrediction.health === "good") {
- return {
- health: "In Ordnung",
- content:
- "Die Baumgruppe benötigt keine Pflege und ist in einem guten Zustand",
- icon: (
-
- ),
- };
- } else if (data?.sensorPrediction.health === "moderate") {
- return {
- health: "Mäßig",
- content: "Die Baumgruppe benötigt bald Wasser und Pflege",
- icon: (
-
- ),
- };
- } else {
- return {
- health: "Schlecht",
- content: "Die Baumgruppe benötigt dringend Wasser und Pflege",
- icon: (
-
- ),
- };
- }
- }, [data?.sensorPrediction]);
-
- const lastSensorData = useMemo(
- () => ({
- data: sensorData[sensorData.length - 1],
- humidityDiff:
- sensorData[sensorData.length - 1]?.humidity -
- sensorData[sensorData.length - 2]?.humidity || 0,
- batteryDiff:
- sensorData[sensorData.length - 1]?.battery -
- sensorData[sensorData.length - 2]?.battery || 0,
- }),
- [sensorData],
- );
-
- if (!data || isLoading) {
- return Loading...
;
- }
-
- return (
-
-
-
-
-
Handlungsempfehlung
-
-
- {treeActionRecommendation.icon}
- {treeActionRecommendation.health}
-
-
-
-
-
-
- Bodenfeuchtigkeit
-
-
- {lastSensorData.data.humidity}%
-
-
-
-
-
-
- Stammfeuchte
-
-
- {lastSensorData.data.trunkHumidity} kΩ
-
-
-
-
-
-
- Batterie
-
-
- {lastSensorData.data.battery} Volt
-
-
-
- );
-};
diff --git a/frontend/src/routes/waypoints/index.tsx b/frontend/src/routes/waypoints/index.tsx
deleted file mode 100644
index a61a283b..00000000
--- a/frontend/src/routes/waypoints/index.tsx
+++ /dev/null
@@ -1,327 +0,0 @@
-import { createFileRoute, Link } from "@tanstack/react-router";
-import { Button } from "../../components/ui/button";
-import { Check, Droplet, Maximize2, PlusCircleIcon, Trees } from "lucide-react";
-import { Separator } from "../../components/ui/separator";
-import { cn } from "../../lib/utils";
-import { useEffect, useMemo, useRef, useState } from "react";
-import {
- ResizableHandle,
- ResizablePanel,
- ResizablePanelGroup,
-} from "../../components/ui/resizable";
-import {
- MapContainer,
- Marker,
- Polyline,
- Popup,
- TileLayer,
-} from "react-leaflet";
-import { LatLngExpression, Map } from "leaflet";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "../../components/ui/card";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "../../components/ui/tooltip";
-import { FakeTree, useFakeTrees } from "@/context/FakeTreeDataContext";
-import { TreeIcon } from "@/components/MapMarker";
-
-export const Route = createFileRoute("/waypoints/")({
- component: Waypoints,
-});
-
-interface Waypoint {
- id: string;
- title: string;
- description: string;
- teamMembers: string;
- trees?: FakeTree[];
-}
-
-function Waypoints() {
- const trees = useFakeTrees();
- const usedTrees = useMemo(
- () => trees.filter((tree) => tree.status !== "healthy"),
- [trees],
- );
- const waypoints: Waypoint[] = useMemo(
- () => [
- {
- id: "1",
- title: "Einsatzplanung 1",
- description: "Mürwik -> Sonwick -> ...",
- teamMembers: "Timo Müller, Hans Dieter",
- trees: usedTrees.slice(0, 3),
- },
- {
- id: "2",
- title: "Einsatzplanung 2",
- description: "Christiansen Park -> Wassersleben -> ...",
- teamMembers: "Robert Petersen",
- trees: usedTrees.slice(3, 6),
- },
- {
- id: "3",
- title: "Einsatzplanung 3",
- description: "Zob -> Exe -> ...",
- teamMembers: "Hans Dieter, Jonas Heinrich",
- trees: usedTrees.slice(6, trees.length - 1),
- },
- ],
- [],
- );
-
- const [selectedWaypoint, setSelectedWaypoint] = useState(
- waypoints[0],
- );
- return (
-
-
-
-
-
-
Einsatzplanung
-
-
-
-
-
-
-
-
-
-
-
- {waypoints.map((waypoint) => (
-
- ))}
-
-
-
-
-
- {selectedWaypoint ? (
-
- ) : (
- Wähle eine Einsatzplanung aus
- )}
-
-
-
- );
-}
-
-interface WaypointCardProps {
- waypoint: Waypoint;
- selected?: boolean;
- onSelect?: (waypoint: Waypoint) => void;
-}
-
-const WaypointCard = ({ waypoint, selected, onSelect }: WaypointCardProps) => {
- return (
- onSelect?.(waypoint)}
- >
-
-
-
- {waypoint.description.substring(0, 300)}
-
-
-
- );
-};
-
-export const WaypointDetails = ({ waypoint }: { waypoint: Waypoint }) => {
- const mapRef = useRef