diff --git a/ApiCore-Frontend/package-lock.json b/ApiCore-Frontend/package-lock.json index 43f032d..1c4b4b4 100644 --- a/ApiCore-Frontend/package-lock.json +++ b/ApiCore-Frontend/package-lock.json @@ -12,6 +12,7 @@ "@tanstack/react-query": "^5.101.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "idb": "^8.0.3", "leaflet": "^1.9.4", "lucide-react": "^1.33.0", "next": "16.3.1", @@ -7433,6 +7434,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/idb": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", diff --git a/ApiCore-Frontend/package.json b/ApiCore-Frontend/package.json index 27ecfb0..a6f4451 100644 --- a/ApiCore-Frontend/package.json +++ b/ApiCore-Frontend/package.json @@ -16,6 +16,7 @@ "@tanstack/react-query": "^5.101.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "idb": "^8.0.3", "leaflet": "^1.9.4", "lucide-react": "^1.33.0", "next": "16.3.1", diff --git a/ApiCore-Frontend/public/sw.js b/ApiCore-Frontend/public/sw.js new file mode 100644 index 0000000..dc253e0 --- /dev/null +++ b/ApiCore-Frontend/public/sw.js @@ -0,0 +1,39 @@ +const CACHE_NAME = "apicore-shell-v1"; + +self.addEventListener("install", () => { + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))), + ) + .then(() => self.clients.claim()), + ); +}); + +self.addEventListener("fetch", (event) => { + const { request } = event; + + if (request.method !== "GET") { + return; + } + + const url = new URL(request.url); + if (url.origin !== self.location.origin || url.pathname.startsWith("/api")) { + return; + } + + event.respondWith( + fetch(request) + .then((response) => { + const responseClone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, responseClone)); + return response; + }) + .catch(() => caches.match(request).then((cached) => cached || Response.error())), + ); +}); diff --git a/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx b/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx index cc0e54c..da3c8f2 100644 --- a/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx @@ -7,6 +7,8 @@ import { toast } from "sonner"; import { ClipboardCheck } from "lucide-react"; import { fetchColonies } from "@/lib/colonies/api"; import { createInspection } from "@/lib/inspections/api"; +import { ApiError } from "@/lib/auth/api-client"; +import { queueInspection } from "@/lib/offline/db"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -60,8 +62,15 @@ export default function NewInspectionPage() { toast.success("Durchsicht gespeichert"); router.push(`/colonies/${inspection.colonyId}`); }, - onError: () => { - setError("Durchsicht konnte nicht gespeichert werden."); + onError: (mutationError, variables) => { + if (mutationError instanceof ApiError) { + setError("Durchsicht konnte nicht gespeichert werden."); + return; + } + void queueInspection(variables).then(() => { + toast.info("Kein Netzwerk – Durchsicht wurde offline gespeichert und wird synchronisiert."); + router.push(`/colonies/${variables.colonyId}`); + }); }, }); diff --git a/ApiCore-Frontend/src/app/(app)/layout.tsx b/ApiCore-Frontend/src/app/(app)/layout.tsx index 8ff827e..5a018dc 100644 --- a/ApiCore-Frontend/src/app/(app)/layout.tsx +++ b/ApiCore-Frontend/src/app/(app)/layout.tsx @@ -6,6 +6,7 @@ import { useRouter } from "next/navigation"; import { Hexagon } from "lucide-react"; import { useAuth } from "@/lib/auth/auth-context"; import { NotificationBell } from "@/components/notification-bell"; +import { OfflineSyncBadge } from "@/components/offline-sync-badge"; import { Button } from "@/components/ui/button"; import { Sidebar } from "./sidebar"; import { BottomNav } from "./bottom-nav"; @@ -44,6 +45,7 @@ export default function AppLayout({ children }: { children: ReactNode }) { ApiCore