In der PWA ist die Seite 'Gassi-Treffen' mit drei Tabs:
- Treffen (walks.py — sich verabreden)
- Challenge (Monatsfoto)
- Stamm-Gassis (gassi_zeiten.py — regelmäßige Runden)
Mein bisheriger Mehr-Eintrag hieß 'Stamm-Gassi-Zeiten' und zeigte nur die
Stamm-Gassi-Funktion isoliert — das stimmte nicht mit der PWA überein.
Neu:
- GassiView mit Segmented Picker (Treffen / Stamm-Gassis)
- GassiTreffenList: GET /api/walks?lat&lon&radius=20000, Liste mit Datum,
Uhrzeit, Ort, Teilnehmer-Zahl
- GassiTreffenDetail: Karte mit Pin, Stats, Beitreten/Verlassen
(POST/DELETE /api/walks/{id}/join), Owner-Check
- AddWalkSheet: Titel, Datum, Uhrzeit, Treffpunkt-Name, Max-Teilnehmer,
Beschreibung — POST /api/walks
- StammGassisList = bisherige GassiZeitenView umbenannt + Nav-Title raus
(wird vom GassiView vergeben)
Im Mehr-Tab heißt der Link jetzt 'Gassi-Treffen' (pawprint-Icon) statt
'Stamm-Gassi-Zeiten' (alarm-Icon).
DTOs: WalkMeeting, WalkCreateBody, WalkJoinBody.
344 lines
7.2 KiB
Swift
344 lines
7.2 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 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)
|
|
}
|
|
}
|