From d17d71e55a4166c09fc197178bdcfac48e1a5c2e Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Mon, 15 Jun 2026 17:39:46 +0200 Subject: [PATCH] feat(health): emergency vet with OSM search and map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EmergencyVet model: name, address, phone, website, lat, lng (1:1 to Pet) - health router: getEmergencyVet, setEmergencyVet, deleteEmergencyVet - EmergencyVetSection: Nominatim search (debounced 500ms, OSM attribution), result dropdown auto-fills address + coordinates, manual override for all fields - OSM iframe map (no API key) with "In Karte öffnen" link; shown in edit mode as coordinate preview and in view mode after save - Health card (public): red-accented emergency vet block with phone link, website link and OSM map; map hidden on print (print:hidden) Co-Authored-By: Claude Sonnet 4.6 --- prisma/schema.prisma | 23 +- src/app/health-card/[token]/page.tsx | 66 +++ src/components/health/EmergencyVetSection.tsx | 380 ++++++++++++++++++ src/components/health/HealthDashboard.tsx | 3 + src/trpc/routers/health.ts | 37 ++ 5 files changed, 505 insertions(+), 4 deletions(-) create mode 100644 src/components/health/EmergencyVetSection.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8844a6e..72d492a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -72,10 +72,11 @@ model Pet { sentMessages Message[] @relation("MessageSender") // Phase 7 back-relations - weightLogs WeightLog[] - vetVisits VetVisit[] - vaccines Vaccine[] - healthCard HealthCard? + weightLogs WeightLog[] + vetVisits VetVisit[] + vaccines Vaccine[] + healthCard HealthCard? + emergencyVet EmergencyVet? @@index([ownerId]) } @@ -389,6 +390,20 @@ model Vaccine { @@index([petId]) } +model EmergencyVet { + id String @id @default(cuid()) + pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade) + petId String @unique + name String @db.VarChar(150) + address String? @db.VarChar(300) + phone String? @db.VarChar(30) + website String? @db.VarChar(200) + lat Float? + lng Float? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + model HealthCard { id String @id @default(cuid()) pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade) diff --git a/src/app/health-card/[token]/page.tsx b/src/app/health-card/[token]/page.tsx index 2b77a13..8f1670d 100644 --- a/src/app/health-card/[token]/page.tsx +++ b/src/app/health-card/[token]/page.tsx @@ -8,6 +8,7 @@ import { getAvatarUrl } from "@/lib/avatar-url"; import { PrintButton } from "@/components/health/PrintButton"; import { AutoPrint } from "@/components/health/AutoPrint"; import { WeightChart } from "@/components/health/WeightChart"; +import { Phone, Globe, MapPin } from "lucide-react"; interface PageProps { params: Promise<{ token: string }>; @@ -46,6 +47,7 @@ export default async function HealthCardPage({ params, searchParams }: PageProps weightLogs: { orderBy: { date: "desc" }, take: 20 }, vetVisits: { orderBy: { date: "desc" }, take: 10 }, vaccines: { orderBy: { dateGiven: "desc" } }, + emergencyVet: true, }, }, }, @@ -115,6 +117,70 @@ export default async function HealthCardPage({ params, searchParams }: PageProps + {/* Emergency vet */} + {pet.emergencyVet && ( +
+

+ + Notfalltierarzt +

+
+

{pet.emergencyVet.name}

+ {pet.emergencyVet.address && ( +

+ + {pet.emergencyVet.address} +

+ )} +
+
+ {pet.emergencyVet.phone && ( + + + {pet.emergencyVet.phone} + + )} + {pet.emergencyVet.website && ( + + + Website + + )} +
+ {pet.emergencyVet.lat != null && pet.emergencyVet.lng != null && (() => { + const { lat, lng } = pet.emergencyVet as { lat: number; lng: number }; + const delta = 0.012; + const bbox = `${lng - delta},${lat - delta},${lng + delta},${lat + delta}`; + return ( +
+