banyaro-ios/BanYaroGo/API/DTOs.swift
rene a2646a18ef 1.1: Offline-Cache + Outbox für Touren/Tagebuch, WeatherKit-Fix, Aufräumen
App-Review-Fix (Guideline 2.1 WeatherKit):
- OneShotLocation: deterministisches async resolve() mit 10s-Timeout statt
  onChange-Lauschen; WetterView lädt bei fehlendem Standort einen Berlin-Fallback
  → kein ewiges Hängen bei "Hole Standort…", WeatherKit ist immer sichtbar.

Offline-Lesen (SwiftData):
- CachedRoute/CachedDiaryEntry/CachedImage + CachedAsyncImage: Touren, Tagebuch
  und Fotos werden cache-first geladen und sind offline verfügbar.
- Cache wird bei Logout/401 geleert (RootView), kein Durchschimmern fremder User.

Offline-Speichern (Outbox):
- PendingRoute/PendingRoutePhoto: Tour inkl. unterwegs hinzugefügter Fotos wird
  offline lokal gesichert und automatisch hochgeladen (Touren-Tab + App-Start).
- Touren-Liste zeigt offline gesicherte Touren mit "wird hochgeladen"-Badge.

FinishWalkSheet:
- Dismiss-Schutz: Speichern-Dialog lässt sich nicht mehr wegwischen — eine
  aufgezeichnete Tour geht nicht mehr durch Runterwischen verloren.

Wetter:
- Ortslabel (Reverse-Geocoding; Fallback "Berlin · Näherung").
- Saubere Offline-Meldung statt rohem networkError.

Aufräumen:
- Doppeltes "Gassi-Treffen" im Mehr-Tab entfernt.
- Veraltete Phase-1/2-Texte neu getextet.
- Tote DogsListView gelöscht (Hund-Wechsel läuft über den Heim-Picker).
2026-06-02 19:37:30 +02:00

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: Codable, 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)
}
}