Sechs Offline-Features: Erste Hilfe, Ausgaben, Wetter, Gassi-Zeiten, Giftköder, Verlorene

Pitch-Karte erweitert um die neuen Features (sowie Hundesitting, Züchter).

Neue DTOs in DTOs.swift:
- Expense + ExpenseCreateBody
- GassiZeit + GassiZeitCreateBody (mit wochentage [String], radius_m)
- PoisonAlert + PoisonCreateBody
- LostDog + LostDogCreateBody
- WeatherForecast + WeatherDay (mit asphalt_temp, zecken, pollen-Felder)

Neue Views:
- ErsteHilfeView + Detail: sechs Notfall-Topics (Vergiftung, Hitzschlag,
  Wunden, Atemnot, Krampfanfall, Magendrehung) — komplett offline, kein API
- AusgabenView: Liste mit Total, AddExpenseSheet mit Kategorie/Betrag/
  Datum/Hund-Picker
- WetterView: One-Shot Location + /api/weather/forecast, 7-Tage-Vorhersage
  mit Hunde-Tipps (Hitze ab 25°/30°, Frost, Asphalt ≥50°, Zecken, Regen)
- GassiZeitenView: eigene Zeiten + Add-Sheet (Wochentag-Picker, Hund-
  Auswahl), automatische lokale UNCalendarNotifications via Scheduler
- GiftkoederView: Map mit Pins + Liste in 5km Umkreis, Report-Sheet mit
  Typ-Auswahl
- VerloreneHundeView: Liste mit Foto/Distanz, Detail mit Karte

Support:
- OneShotLocation: kleiner CLLocationManager-Wrapper für einmalige
  Positionsabfrage (Wetter, Giftköder)
- GassiZeitenScheduler: UNCalendarNotificationTrigger pro Wochentag,
  Identifier-Schema "gz-{id}-{weekday}"

Navigation: Section "Hund & Alltag" im Mehr-Tab mit NavigationLinks zu
allen sechs neuen Ansichten.
This commit is contained in:
rene 2026-05-30 12:03:24 +02:00
parent f1b3ff4035
commit 68b084be97
11 changed files with 1547 additions and 0 deletions

View file

@ -0,0 +1,270 @@
import SwiftUI
struct ErsteHilfeView: View {
var body: some View {
List {
Section {
HStack(spacing: 12) {
Image(systemName: "phone.fill")
.foregroundStyle(.red)
VStack(alignment: .leading) {
Text("Im Ernstfall sofort den Tierarzt anrufen!").font(.subheadline.bold())
Text("Diese Hinweise ersetzen keine tierärztliche Behandlung.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 4)
}
ForEach(ErsteHilfeContent.topics) { topic in
NavigationLink {
ErsteHilfeDetailView(topic: topic)
} label: {
HStack(spacing: 14) {
Image(systemName: topic.icon)
.font(.title3)
.foregroundStyle(topic.tint)
.frame(width: 32)
VStack(alignment: .leading, spacing: 2) {
Text(topic.title).font(.headline)
Text(topic.summary)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
}
}
.padding(.vertical, 4)
}
}
}
.navigationTitle("Erste Hilfe")
.navigationBarTitleDisplayMode(.inline)
}
}
private struct ErsteHilfeDetailView: View {
let topic: ErsteHilfeTopic
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 18) {
HStack(spacing: 14) {
Image(systemName: topic.icon)
.font(.system(size: 44))
.foregroundStyle(topic.tint)
.frame(width: 70, height: 70)
.background(topic.tint.opacity(0.15), in: Circle())
VStack(alignment: .leading, spacing: 4) {
Text(topic.title).font(.title2.bold())
Text(topic.summary)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
ForEach(Array(topic.sections.enumerated()), id: \.offset) { _, section in
VStack(alignment: .leading, spacing: 8) {
Text(section.heading).font(.headline)
VStack(alignment: .leading, spacing: 6) {
ForEach(Array(section.steps.enumerated()), id: \.offset) { i, step in
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text("\(i + 1).")
.font(.subheadline.bold())
.foregroundStyle(topic.tint)
.frame(width: 20, alignment: .trailing)
Text(step)
.font(.subheadline)
}
}
}
}
.padding()
.background(.background.secondary, in: RoundedRectangle(cornerRadius: 12))
}
if !topic.warnings.isEmpty {
VStack(alignment: .leading, spacing: 6) {
Label("Sofort zum Tierarzt", systemImage: "exclamationmark.triangle.fill")
.font(.headline)
.foregroundStyle(.red)
ForEach(topic.warnings, id: \.self) { w in
Text("\(w)").font(.subheadline)
}
}
.padding()
.background(Color.red.opacity(0.08), in: RoundedRectangle(cornerRadius: 12))
}
}
.padding()
}
.navigationTitle(topic.title)
.navigationBarTitleDisplayMode(.inline)
}
}
// MARK: - Content
struct ErsteHilfeTopic: Identifiable {
let id: String
let title: String
let summary: String
let icon: String
let tint: Color
let sections: [Section]
let warnings: [String]
struct Section {
let heading: String
let steps: [String]
}
}
enum ErsteHilfeContent {
static let topics: [ErsteHilfeTopic] = [
ErsteHilfeTopic(
id: "vergiftung",
title: "Vergiftung",
summary: "Schokolade, Trauben, Giftköder, Frostschutz, Medikamente.",
icon: "exclamationmark.octagon.fill",
tint: .red,
sections: [
.init(heading: "Sofortmaßnahmen", steps: [
"Hund von der Giftquelle entfernen. Reste sichern (Foto/Verpackung).",
"NICHT zum Erbrechen bringen, außer der Tierarzt sagt es ausdrücklich.",
"Tierarzt oder Tierklinik anrufen — Art und Menge des Gifts angeben.",
"Bei Bewusstlosigkeit: stabile Seitenlage, Atemwege freihalten."
]),
.init(heading: "Was du nicht tun solltest", steps: [
"Kein Salzwasser, keine Milch, kein Öl geben — kann gefährlich sein.",
"Nicht abwarten, ob Symptome verschwinden."
])
],
warnings: [
"Krämpfe, Erbrechen, Durchfall, Speicheln, Apathie",
"Schaum vor dem Maul, Blutungen",
"Verdacht auf Giftköder (Wurst mit Glas, Nägel, Tabletten)"
]
),
ErsteHilfeTopic(
id: "hitzschlag",
title: "Hitzschlag",
summary: "Hecheln, Taumeln, Bewusstseinsverlust durch Überhitzung.",
icon: "thermometer.sun.fill",
tint: .orange,
sections: [
.init(heading: "Sofort kühlen", steps: [
"Hund in den Schatten oder kühlen Raum bringen.",
"Pfoten, Innenschenkel und Bauch mit lauwarmem (nicht eiskaltem!) Wasser kühlen.",
"Kleine Schlucke Wasser anbieten, nicht zwingen.",
"Auf dem Weg zum Tierarzt weiterkühlen — feuchte Tücher auf den Körper."
])
],
warnings: [
"Körpertemperatur über 40 °C",
"Verwirrung, Krämpfe, Erbrechen",
"Zahnfleisch dunkelrot, blau oder bleich"
]
),
ErsteHilfeTopic(
id: "wunde",
title: "Wunden & Blutungen",
summary: "Schnitte, Bisse, Verletzungen an Pfoten und Körper.",
icon: "bandage.fill",
tint: .pink,
sections: [
.init(heading: "Bei starker Blutung", steps: [
"Druckverband mit sauberem Tuch anlegen — fest, aber nicht abschnüren.",
"Bei spritzendem Blut: oberhalb der Wunde mit den Fingern abdrücken.",
"Pfote/Bein hochlagern, ruhig halten.",
"Sofort zum Tierarzt."
]),
.init(heading: "Kleine Wunden", steps: [
"Fremdkörper (Glas, Splitter) NICHT selbst rausziehen, wenn tief.",
"Wunde mit klarem Wasser ausspülen.",
"Trocken tupfen, locker abdecken — Hund vom Lecken abhalten."
])
],
warnings: [
"Stark blutende oder pumpende Wunde",
"Fremdkörper steckt fest",
"Tiefe Biss­wunden (auch wenn klein) → Infektionsgefahr"
]
),
ErsteHilfeTopic(
id: "atemnot",
title: "Atemnot & Bewusstlosigkeit",
summary: "Erstickung, Würgen, Kollaps, Reanimation.",
icon: "lungs.fill",
tint: .blue,
sections: [
.init(heading: "Fremdkörper im Hals", steps: [
"Maul vorsichtig öffnen, mit Taschenlampe schauen.",
"Sichtbaren Fremdkörper mit den Fingern (nicht Pinzette!) lösen.",
"Bei kleinem Hund: Kopf nach unten halten und zwischen die Schulterblätter klopfen.",
"Bei großem Hund: Heimlich-Manöver — von hinten umfassen, ruckartig nach oben drücken."
]),
.init(heading: "Reanimation (CPR)", steps: [
"Hund auf rechte Seite legen, Atemwege kontrollieren.",
"Maul schließen, in die Nase atmen — Brustkorb soll sich heben.",
"30 Herzdruckmassagen (auf Brustkorb-Höhe der Schulterblätter), dann 2 Beatmungen.",
"Weiter bis Atmung einsetzt oder Tierarzt übernimmt."
])
],
warnings: [
"Bewusstlosigkeit, keine Atmung, kein Puls",
"Blaue Schleimhäute",
"Würgen ohne Erfolg über mehrere Minuten"
]
),
ErsteHilfeTopic(
id: "krampfanfall",
title: "Krampfanfall",
summary: "Epileptischer Anfall, Zittern, Bewusstseinsstörung.",
icon: "waveform.path.ecg",
tint: .purple,
sections: [
.init(heading: "Während des Anfalls", steps: [
"Ruhe bewahren, Zeit messen (Dauer ist wichtig für den Tierarzt).",
"Umgebung sichern — Möbel/Kanten wegräumen.",
"NICHT festhalten, NICHTS ins Maul stecken.",
"Licht, Geräusche und Reize reduzieren."
]),
.init(heading: "Nach dem Anfall", steps: [
"Hund kann verwirrt, blind oder unruhig sein — Zeit geben.",
"Wasser bereitstellen, in eine ruhige Ecke legen.",
"Auch nach erstem Anfall zum Tierarzt — Ursache abklären."
])
],
warnings: [
"Anfall dauert länger als 5 Minuten (Notfall!)",
"Mehrere Anfälle in Folge",
"Erster Anfall überhaupt"
]
),
ErsteHilfeTopic(
id: "magendrehung",
title: "Magendrehung",
summary: "Akuter Notfall, vor allem bei großen Rassen.",
icon: "stomach.fill",
tint: .red,
sections: [
.init(heading: "Erkennen", steps: [
"Aufgeblähter, harter Bauch.",
"Erfolgloses Würgen ohne Erbrechen.",
"Unruhe, Speicheln, Atemnot.",
"Häufig nach dem Fressen / Trinken großer Mengen."
]),
.init(heading: "Sofort handeln", steps: [
"JEDE Minute zählt — direkt zum Notfall-Tierarzt fahren.",
"Beim Transport ruhig halten, nicht füttern, nicht tränken.",
"Vorher anrufen, damit OP-Team bereitsteht."
])
],
warnings: [
"Symptome wie oben → IMMER Notfall",
"Ohne OP innerhalb weniger Stunden tödlich"
]
)
]
}