feat(health): emergency vet with OSM search and map

- 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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Sonnet 4.6 committed 2026-06-15 17:39:46 +02:00
1 parent c9f385a9fa
commit d17d71e55a
5 files changed
+505 -4

No files matched your search

+37
View File
@@ -132,6 +132,43 @@ export const healthRouter = router({
return { success: true };
}),
// ── Emergency Vet ────────────────────────────────────────────────────────────
getEmergencyVet: protectedProcedure
.input(z.object({ petId: z.string().min(1) }))
.query(async ({ ctx, input }) => {
await assertOwner(ctx.prisma, input.petId, ctx.userId);
return ctx.prisma.emergencyVet.findUnique({ where: { petId: input.petId } });
}),
setEmergencyVet: protectedProcedure
.input(z.object({
petId: z.string().min(1),
name: z.string().min(1).max(150),
address: z.string().max(300).optional().nullable(),
phone: z.string().max(30).optional().nullable(),
website: z.string().max(200).optional().nullable(),
lat: z.number().optional().nullable(),
lng: z.number().optional().nullable(),
}))
.mutation(async ({ ctx, input }) => {
await assertOwner(ctx.prisma, input.petId, ctx.userId);
const { petId, ...data } = input;
return ctx.prisma.emergencyVet.upsert({
where: { petId },
update: data,
create: { petId, ...data },
});
}),
deleteEmergencyVet: protectedProcedure
.input(z.object({ petId: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
await assertOwner(ctx.prisma, input.petId, ctx.userId);
await ctx.prisma.emergencyVet.deleteMany({ where: { petId: input.petId } });
return { success: true };
}),
// ── Health Card ──────────────────────────────────────────────────────────────
getOrCreateCard: protectedProcedure