From 0fc7503c088c166e7dc2a3a189fe28899d6898d8 Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sun, 4 Oct 2026 19:17:04 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Vermisst=20=E2=80=94=20Admin-=C3=9Cbers?= =?UTF-8?q?icht=20und=20Ansicht=20direkt=20in=20der=20App?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Admin „Vermisst“ (/p/[secret]/lost): alle Meldungen mit Status, Ort/Zeit, Reichweite (benachrichtigte Tiere), freigegebenen Kontaktwegen; Moderatoren schließen vergessene Meldungen (ohne Entwarnung) oder löschen Missbrauch, beides im Audit-Log (CLOSE_/DELETE_LOST_REPORT) - App: eigener Vermisst-Bildschirm /lost/[petId] mit Foto, Merkmalen, Kontakt (Anrufen, E-Mail, Nachricht in PawFeed), Fotos und Teilen — Profil-Banner und Push-Tipp auf PET_LOST führen dorthin statt in den Browser - lostReports.byPet + gemeinsamer Lader loadLostReportView für App und öffentliche Seite Co-Authored-By: Claude Opus 5.5 --- mobile/app/(app)/_layout.tsx | 1 + mobile/app/(app)/lost/[petId].tsx | 242 ++++++++++++++++++++++ mobile/app/(app)/pets/[petId]/index.tsx | 5 +- mobile/src/i18n/locales/de.json | 21 +- mobile/src/i18n/locales/en.json | 21 +- mobile/src/lib/notification-route.test.ts | 7 +- mobile/src/lib/notification-route.ts | 3 +- prisma/schema.prisma | 2 +- src/__tests__/helpers/prisma-mock.ts | 7 +- src/__tests__/lost-report-admin.test.ts | 107 ++++++++++ src/app/p/[secret]/layout.tsx | 2 + src/app/p/[secret]/lost/page.tsx | 190 +++++++++++++++++ src/app/vermisst/[code]/page.tsx | 38 +--- src/lib/lost-reports-view.ts | 69 ++++++ src/trpc/routers/_app.ts | 2 + src/trpc/routers/lost-report-admin.ts | 97 +++++++++ src/trpc/routers/lost-reports.ts | 9 + 17 files changed, 784 insertions(+), 39 deletions(-) create mode 100644 mobile/app/(app)/lost/[petId].tsx create mode 100644 src/__tests__/lost-report-admin.test.ts create mode 100644 src/app/p/[secret]/lost/page.tsx create mode 100644 src/lib/lost-reports-view.ts create mode 100644 src/trpc/routers/lost-report-admin.ts diff --git a/mobile/app/(app)/_layout.tsx b/mobile/app/(app)/_layout.tsx index 47d9cca..6bcb795 100644 --- a/mobile/app/(app)/_layout.tsx +++ b/mobile/app/(app)/_layout.tsx @@ -49,6 +49,7 @@ export default function AppLayout() { + diff --git a/mobile/app/(app)/lost/[petId].tsx b/mobile/app/(app)/lost/[petId].tsx new file mode 100644 index 0000000..e0e2e47 --- /dev/null +++ b/mobile/app/(app)/lost/[petId].tsx @@ -0,0 +1,242 @@ +import { useMemo } from "react"; +import { View, Text, ScrollView, Pressable, Image, ActivityIndicator, Linking, Share, StyleSheet } from "react-native"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { useTranslation } from "react-i18next"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { Clock, Info, Mail, MapPin, MessageCircle, Phone, Share2, Siren } from "lucide-react-native"; +import { useTRPC } from "../../../src/api/trpc"; +import { useActivePet } from "../../../src/context/ActivePetContext"; +import { useTheme } from "../../../src/context/ThemeContext"; +import { Button } from "../../../src/components/Button"; +import { env } from "../../../src/lib/env"; +import { getMediaUrl, getAvatarUrl } from "../../../src/lib/media-url"; +import { isoToDisplayDate } from "../../../src/lib/date-format"; +import { useBottomSafePadding } from "../../../src/hooks/useBottomSafePadding"; +import { radii, getTypography, type ColorTokens, type Typography } from "../../../src/theme/tokens"; + +const RED = "#dc2626"; + +function pad(n: number): string { + return String(n).padStart(2, "0"); +} + +/** „04.10.2026, 14:30“ in Ortszeit des Geräts. */ +function formatDateTime(value: Date | string): string { + const d = new Date(value); + const iso = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + return `${isoToDisplayDate(iso)}, ${pad(d.getHours())}:${pad(d.getMinutes())}`; +} + +/** + * Vermisst-Ansicht in der App — dieselben Angaben wie die öffentliche Seite + * /vermisst/, ohne die App zu verlassen. Ziel des Profil-Banners und des + * Antippens einer PET_LOST-Mitteilung (src/lib/notification-route.ts). + */ +export default function LostPetScreen() { + const { petId } = useLocalSearchParams<{ petId: string }>(); + const { t } = useTranslation(); + const trpc = useTRPC(); + const router = useRouter(); + const { activePetId, pets } = useActivePet(); + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); + const bottomPadding = useBottomSafePadding(); + + const { data: report, isLoading } = useQuery(trpc.lostReports.byPet.queryOptions({ petId })); + const openConversation = useMutation( + trpc.conversations.getOrCreate.mutationOptions({ + onSuccess: (conversation) => router.push(`/messages/${conversation.id}`), + }) + ); + + if (isLoading) { + return ( + + + + ); + } + + if (!report) { + return ( + + {t("lostView.notActiveTitle")} + {t("lostView.notActiveText")} + + ))} + + + + {isLoading ? ( + + ) : !data?.reports.length ? ( +
+ + Keine Meldungen in dieser Ansicht. +
+ ) : ( +
    + {data.reports.map((r) => { + const isActive = r.foundAt == null; + return ( +
  • +
    + + + {r.pet.name.slice(0, 2).toUpperCase()} + +
    + + {r.pet.name} + +

    gemeldet {DATE_FORMAT.format(new Date(r.createdAt))}

    +
    + {isActive ? ( + Aktiv + ) : ( + Wieder da · {DATE_FORMAT.format(new Date(r.foundAt!))} + )} +
    + +
    +

    + + {r.lastSeenPlace} · {DATE_FORMAT.format(new Date(r.lastSeenAt))} +

    + {r.description &&

    {r.description}

    } +
    + +
    + + + {r.notifiedCount} benachrichtigt + + {r.contactPhone && ( + + + Telefon + + )} + {r.contactEmail && ( + + + E-Mail + + )} + {r.allowIndexing ? "Suchmaschinen erlaubt" : "Nur per Link"} +
    + +
    + + Öffentliche Seite + +
    + {isActive && ( + + )} + +
    +
    +
  • + ); + })} +
+ )} + + !open && setTarget(null)}> + + + + {target?.mode === "DELETE" ? `Meldung zu ${target?.name} löschen?` : `Meldung zu ${target?.name} schließen?`} + + + {target?.mode === "DELETE" + ? "Für Missbrauch oder Fake-Meldungen. Die öffentliche Seite ist danach nicht mehr erreichbar. Es geht keine Mitteilung raus." + : "Für vergessene Meldungen. Das Abzeichen verschwindet, die öffentliche Seite zeigt „wieder zu Hause“. Es geht keine Entwarnung raus."} + + +