Detail-Karte zeigt jetzt Treffpunkt + User-Position via UserAnnotation,
darunter eine Navigation-Karte mit:
- Ortsname
- Luftlinien-Distanz vom aktuellen Standort
- 'Route'-Button öffnet Apple Maps (MKMapItem.openInMaps) — User wählt
dort Walking/Driving
Plus Teilnehmer-Sektion mit GET /api/walks/{id}/participants:
- Liste der Zugesagten ('yes'-RSVP) mit Namen + Hunden
- 'Noch niemand zugesagt'-Hinweis bei leerer Liste
- myRsvp-Status für korrekte Join-Button-Anzeige
- isOwn → 'Dein Treffen'-Badge statt Beitreten-Button
Footer-Hinweis: 'Fotos vom Treffen kannst du später in der banyaro.app
teilen' — Foto-Funktion bewusst PWA-only (User-Wunsch).
358 lines
7.5 KiB
Swift
358 lines
7.5 KiB
Swift
import Foundation
|
|
|
|
// MARK: - Auth
|
|
|
|
struct LoginRequest: Encodable {
|
|
let email: String
|
|
let password: String
|
|
}
|
|
|
|
struct LoginResponse: Decodable {
|
|
let token: String
|
|
let name: String
|
|
let isPremium: Bool
|
|
}
|
|
|
|
struct UserProfile: Decodable {
|
|
let id: Int
|
|
let name: String
|
|
let email: String
|
|
let realName: String?
|
|
let rolle: String?
|
|
let isPremium: Bool?
|
|
// Backend bool-konvertiert nur is_premium; is_founder/is_partner kommen
|
|
// als SQLite-Int 0/1 zurück — deshalb hier Int? statt Bool?.
|
|
let isFounder: Int?
|
|
let isPartner: Int?
|
|
let founderNumber: Int?
|
|
let subscriptionTier: String?
|
|
let avatarUrl: String?
|
|
let wohnort: String?
|
|
let bio: String?
|
|
|
|
var isFounderFlag: Bool { isFounder == 1 }
|
|
var isPartnerFlag: Bool { isPartner == 1 }
|
|
}
|
|
|
|
// MARK: - Dogs
|
|
|
|
struct Dog: Decodable, Identifiable {
|
|
let id: Int
|
|
let name: String
|
|
let rasse: String?
|
|
let fotoUrl: String?
|
|
let geburtstag: String?
|
|
}
|
|
|
|
// MARK: - Routes
|
|
|
|
struct GPSPoint: Codable, Hashable {
|
|
let lat: Double
|
|
let lon: Double
|
|
let alt: Double?
|
|
}
|
|
|
|
struct RouteListItem: Decodable, Identifiable {
|
|
let id: Int
|
|
let userId: Int
|
|
let name: String
|
|
let beschreibung: String?
|
|
let distanzKm: Double?
|
|
let dauerMin: Int?
|
|
let createdAt: String?
|
|
let previewTrack: [GPSPoint]
|
|
let fotoUrls: [String]?
|
|
let userName: String?
|
|
let isPublic: Bool?
|
|
}
|
|
|
|
struct RouteDetail: Decodable, Identifiable {
|
|
let id: Int
|
|
let userId: Int
|
|
let name: String
|
|
let beschreibung: String?
|
|
let distanzKm: Double?
|
|
let dauerMin: Int?
|
|
let gpsTrack: [GPSPoint]
|
|
let fotoUrls: [String]?
|
|
let createdAt: String?
|
|
let userName: String?
|
|
let dogIds: [Int]?
|
|
}
|
|
|
|
struct RouteCreateBody: Encodable {
|
|
let name: String
|
|
let gpsTrack: [GPSPoint]
|
|
let distanzKm: Double
|
|
let dauerMin: Int
|
|
let dogIds: [Int]
|
|
let isPublic: Bool
|
|
}
|
|
|
|
// MARK: - Expenses
|
|
|
|
struct Expense: Decodable, Identifiable {
|
|
let id: Int
|
|
let dogId: Int?
|
|
let kategorie: String
|
|
let betrag: Double
|
|
let datum: String
|
|
let notiz: String?
|
|
let dogName: String?
|
|
}
|
|
|
|
struct ExpenseCreateBody: Encodable {
|
|
let dogId: Int?
|
|
let kategorie: String
|
|
let betrag: Double
|
|
let datum: String
|
|
let notiz: String?
|
|
}
|
|
|
|
struct ExpenseCategory: Decodable, Identifiable {
|
|
let id: String
|
|
let label: String
|
|
let color: String?
|
|
}
|
|
|
|
// MARK: - Gassi-Zeiten
|
|
|
|
struct GassiZeit: Decodable, Identifiable {
|
|
let id: Int
|
|
let dogId: Int?
|
|
let wochentage: [String]
|
|
let uhrzeit: String
|
|
let ortName: String?
|
|
let lat: Double?
|
|
let lon: Double?
|
|
let radiusM: Int?
|
|
let notiz: String?
|
|
let aktiv: Int?
|
|
let distanceM: Int?
|
|
let isMine: Bool?
|
|
let userName: String?
|
|
let dogName: String?
|
|
let dogRasse: String?
|
|
}
|
|
|
|
// MARK: - Walks (Gassi-Treffen)
|
|
|
|
struct WalkMeeting: Decodable, Identifiable {
|
|
let id: Int
|
|
let userId: Int
|
|
let titel: String
|
|
let datum: String // YYYY-MM-DD
|
|
let uhrzeit: String // HH:MM
|
|
let lat: Double
|
|
let lon: Double
|
|
let ortName: String?
|
|
let maxTeilnehmer: Int
|
|
let beschreibung: String?
|
|
let status: String?
|
|
let veranstalterName: String?
|
|
let teilnehmerCount: Int?
|
|
}
|
|
|
|
struct WalkCreateBody: Encodable {
|
|
let titel: String
|
|
let datum: String
|
|
let uhrzeit: String
|
|
let lat: Double
|
|
let lon: Double
|
|
let ortName: String?
|
|
let maxTeilnehmer: Int
|
|
let beschreibung: String?
|
|
}
|
|
|
|
struct WalkJoinBody: Encodable {
|
|
let dogIds: [Int]
|
|
}
|
|
|
|
struct WalkParticipantsResponse: Decodable {
|
|
let invitations: [WalkInvitation]
|
|
let myRsvp: String?
|
|
let isOrganizer: Bool
|
|
}
|
|
|
|
struct WalkInvitation: Decodable, Identifiable {
|
|
let userId: Int
|
|
let status: String?
|
|
let userName: String?
|
|
let hunde: String?
|
|
var id: Int { userId }
|
|
}
|
|
|
|
struct GassiZeitCreateBody: Encodable {
|
|
let dogId: Int?
|
|
let wochentage: [String]
|
|
let uhrzeit: String
|
|
let ortName: String?
|
|
let lat: Double?
|
|
let lon: Double?
|
|
let radiusM: Int
|
|
let notiz: String?
|
|
}
|
|
|
|
// MARK: - Poison
|
|
|
|
struct PoisonAlert: Decodable, Identifiable {
|
|
let id: Int
|
|
let lat: Double
|
|
let lon: Double
|
|
let beschreibung: String?
|
|
let typ: String?
|
|
let distanzM: Int?
|
|
let fotoUrl: String?
|
|
let melderName: String?
|
|
let createdAt: String?
|
|
}
|
|
|
|
struct PoisonCreateBody: Encodable {
|
|
let lat: Double
|
|
let lon: Double
|
|
let beschreibung: String?
|
|
let typ: String
|
|
}
|
|
|
|
// MARK: - Lost Dogs
|
|
|
|
struct LostDog: Decodable, Identifiable {
|
|
let id: Int
|
|
let name: String
|
|
let rasse: String?
|
|
let beschreibung: String
|
|
let lat: Double
|
|
let lon: Double
|
|
let distanzM: Int?
|
|
let fotoUrl: String?
|
|
let melderName: String?
|
|
let createdAt: String?
|
|
}
|
|
|
|
struct LostDogCreateBody: Encodable {
|
|
let name: String
|
|
let rasse: String?
|
|
let beschreibung: String
|
|
let lat: Double
|
|
let lon: Double
|
|
let dogId: Int?
|
|
}
|
|
|
|
// MARK: - Diary (Tagebuch)
|
|
|
|
struct DiaryEntry: Decodable, Identifiable {
|
|
let id: Int
|
|
let dogId: Int?
|
|
let datum: String?
|
|
let typ: String?
|
|
let titel: String?
|
|
let text: String?
|
|
let tags: [String]?
|
|
let gpsLat: Double?
|
|
let gpsLon: Double?
|
|
let locationName: String?
|
|
// is_milestone kommt als SQLite-Int 0/1 — Backend bool-konvertiert es nicht.
|
|
let isMilestone: Int?
|
|
let mediaItems: [DiaryMedia]?
|
|
let createdAt: String?
|
|
|
|
var isMilestoneFlag: Bool { isMilestone == 1 }
|
|
}
|
|
|
|
struct DiaryMedia: Decodable, Identifiable {
|
|
let id: Int
|
|
let url: String
|
|
let mediaType: String?
|
|
let imgWidth: Int?
|
|
let imgHeight: Int?
|
|
}
|
|
|
|
struct DiaryCreateBody: Encodable {
|
|
let datum: String?
|
|
let typ: String
|
|
let titel: String?
|
|
let text: String?
|
|
let tags: [String]?
|
|
let gpsLat: Double?
|
|
let gpsLon: Double?
|
|
let locationName: String?
|
|
let isMilestone: Bool
|
|
}
|
|
|
|
// MARK: - Welcome Dashboard
|
|
|
|
struct DashboardSnapshot: Decodable {
|
|
let randomPhoto: DashboardPhoto?
|
|
let lastDiary: DashboardLastDiary?
|
|
let nextAppointment: DashboardNextAppointment?
|
|
let lastWeight: DashboardLastWeight?
|
|
let diaryCount: Int?
|
|
}
|
|
|
|
struct DashboardPhoto: Decodable {
|
|
let url: String
|
|
let previewUrl: String?
|
|
}
|
|
|
|
struct DashboardLastDiary: Decodable {
|
|
let titel: String?
|
|
let datum: String?
|
|
}
|
|
|
|
struct DashboardNextAppointment: Decodable {
|
|
let bezeichnung: String?
|
|
let naechstes: String?
|
|
let typ: String?
|
|
}
|
|
|
|
struct DashboardLastWeight: Decodable {
|
|
let wert: Double?
|
|
let einheit: String?
|
|
let datum: String?
|
|
}
|
|
|
|
// MARK: - Weather
|
|
|
|
struct WeatherForecast: Decodable {
|
|
let days: [WeatherDay]
|
|
}
|
|
|
|
struct WeatherDay: Decodable, Identifiable {
|
|
let date: String
|
|
let wday: String?
|
|
let weathercode: Int?
|
|
let desc: String?
|
|
let icon: String?
|
|
let tempMax: Double?
|
|
let tempMin: Double?
|
|
let precipProb: Int?
|
|
let precipSum: Double?
|
|
let windKmh: Double?
|
|
let uvIndex: Double?
|
|
let sunrise: String?
|
|
let sunset: String?
|
|
let asphaltTemp: Double?
|
|
let zecken: String?
|
|
|
|
var id: String { date }
|
|
}
|
|
|
|
/// Patch body for PATCH /api/routes/{id}. Only non-nil fields are encoded.
|
|
struct RouteUpdateBody: Encodable {
|
|
var name: String?
|
|
var beschreibung: String?
|
|
var isPublic: Bool?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case name
|
|
case beschreibung
|
|
case isPublic
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var c = encoder.container(keyedBy: CodingKeys.self)
|
|
try c.encodeIfPresent(name, forKey: .name)
|
|
try c.encodeIfPresent(beschreibung, forKey: .beschreibung)
|
|
try c.encodeIfPresent(isPublic, forKey: .isPublic)
|
|
}
|
|
}
|