// Domain models mirroring the API Server JSON (which mirrors Car Service.xlsx). int _asInt(dynamic v) => v is int ? v : (v is num ? v.toInt() : 0); String _asStr(dynamic v) => v == null ? "" : v.toString(); bool _asBool(dynamic v) => v == true; class Car { final String id; final String name; final String make; final String model; final int year; final String registration; final String registrationCountry; final String vin; final String oilSpec; final String transmissionOilSpec; final String differentialOilSpec; final String brakeFluidSpec; final String coolantSpec; final String fuelType; // petrol | diesel | hybrid | electric final String buildDate; // ISO YYYY-MM-DD (date-only) final String firstRegistrationDate; // ISO YYYY-MM-DD (date-only) final int serviceIntervalDays; final int serviceIntervalKm; final int currentKm; /// The requesting user's permission on this car: "owner", "write", or "read". /// The API sets it on every car read. Defaults to "owner" so older responses /// (or any code that constructs a Car without it) stay fully editable. final String access; Car({ required this.id, required this.name, required this.make, required this.model, required this.year, required this.registration, this.registrationCountry = "", required this.vin, required this.oilSpec, required this.transmissionOilSpec, required this.differentialOilSpec, required this.brakeFluidSpec, required this.coolantSpec, this.fuelType = "", this.buildDate = "", this.firstRegistrationDate = "", required this.serviceIntervalDays, required this.serviceIntervalKm, required this.currentKm, this.access = "owner", }); factory Car.fromJson(Map j) => Car( id: _asStr(j["id"]), name: _asStr(j["name"]), make: _asStr(j["make"]), model: _asStr(j["model"]), year: _asInt(j["year"]), registration: _asStr(j["registration"]), registrationCountry: _asStr(j["registrationCountry"]), vin: _asStr(j["vin"]), oilSpec: _asStr(j["oilSpec"]), transmissionOilSpec: _asStr(j["transmissionOilSpec"]), differentialOilSpec: _asStr(j["differentialOilSpec"]), brakeFluidSpec: _asStr(j["brakeFluidSpec"]), coolantSpec: _asStr(j["coolantSpec"]), fuelType: _asStr(j["fuelType"]), buildDate: _asStr(j["buildDate"]), firstRegistrationDate: _asStr(j["firstRegistrationDate"]), serviceIntervalDays: _asInt(j["serviceIntervalDays"]), serviceIntervalKm: _asInt(j["serviceIntervalKm"]), currentKm: _asInt(j["currentKm"]), access: j["access"] == null ? "owner" : _asStr(j["access"]), ); bool get isOwner => access == "owner"; bool get canWrite => access == "owner" || access == "write"; bool get isReadOnly => access == "read"; String get subtitle => [make, model, year > 0 ? "$year" : ""].where((s) => s.isNotEmpty).join(" "); } class ServiceRecord { final String id; final String car; final DateTime? date; final int km; final bool changedOil; final bool changedEngineAirFilter; final bool changedCabinAirFilter; final String notes; final DateTime? nextServiceDate; final int? nextServiceKm; ServiceRecord({ required this.id, required this.car, required this.date, required this.km, required this.changedOil, required this.changedEngineAirFilter, required this.changedCabinAirFilter, required this.notes, required this.nextServiceDate, required this.nextServiceKm, }); factory ServiceRecord.fromJson(Map j) => ServiceRecord( id: _asStr(j["id"]), car: _asStr(j["car"]), date: DateTime.tryParse(_asStr(j["date"]))?.toLocal(), km: _asInt(j["km"]), changedOil: _asBool(j["changedOil"]), changedEngineAirFilter: _asBool(j["changedEngineAirFilter"]), changedCabinAirFilter: _asBool(j["changedCabinAirFilter"]), notes: _asStr(j["notes"]), nextServiceDate: j["nextServiceDate"] != null ? DateTime.tryParse(_asStr(j["nextServiceDate"]))?.toLocal() : null, nextServiceKm: j["nextServiceKm"] == null ? null : _asInt(j["nextServiceKm"]), ); } class Part { final String id; final String car; final String name; final String partNumber; final String category; Part({ required this.id, required this.car, required this.name, required this.partNumber, this.category = "", }); factory Part.fromJson(Map j) => Part( id: _asStr(j["id"]), car: _asStr(j["car"]), name: _asStr(j["name"]), partNumber: _asStr(j["partNumber"]), category: _asStr(j["category"]), ); } /// A sharing grant: another user's access to one of your cars. class CarShare { final String userId; final String email; final String name; final String permission; // "read" | "write" CarShare({ required this.userId, required this.email, required this.name, required this.permission, }); factory CarShare.fromJson(Map j) { final user = Map.from(j["user"] ?? {}); return CarShare( userId: _asStr(user["id"]), email: _asStr(user["email"]), name: _asStr(user["name"]), permission: _asStr(j["permission"]), ); } String get label => name.isNotEmpty ? name : email; } class AuthUser { final String id; final String email; final String name; final String role; // "user" | "admin" AuthUser({required this.id, required this.email, required this.name, this.role = "user"}); factory AuthUser.fromJson(Map j) => AuthUser( id: _asStr(j["id"]), email: _asStr(j["email"]), name: _asStr(j["name"]), role: j["role"] == null ? "user" : _asStr(j["role"]), ); bool get isAdmin => role == "admin"; Map toJson() => {"id": id, "email": email, "name": name, "role": role}; } /// A user record as returned by the admin user-management endpoints. class AdminUser { final String id; final String email; final String name; final String role; final String created; AdminUser({ required this.id, required this.email, required this.name, required this.role, required this.created, }); factory AdminUser.fromJson(Map j) => AdminUser( id: _asStr(j["id"]), email: _asStr(j["email"]), name: _asStr(j["name"]), role: _asStr(j["role"]), created: _asStr(j["created"]), ); } /// The full authenticated profile (Settings panel), mirroring /api/me. class UserProfile { final String id; final String email; final bool verified; final String name; final String bio; final bool hasAvatar; final String theme; // light | dark | system final String locale; final String dateFormat; // YMD | DMY_NUM | DMY | MDY final String fontSize; // small | medium | large final String role; // user | admin final DateTime? deletionRequestedAt; UserProfile({ required this.id, required this.email, required this.verified, required this.name, required this.bio, required this.hasAvatar, required this.theme, required this.locale, required this.dateFormat, required this.fontSize, required this.role, required this.deletionRequestedAt, }); factory UserProfile.fromJson(Map j) => UserProfile( id: _asStr(j["id"]), email: _asStr(j["email"]), verified: _asBool(j["verified"]), name: _asStr(j["name"]), bio: _asStr(j["bio"]), hasAvatar: _asBool(j["hasAvatar"]), theme: j["theme"] == null ? "system" : _asStr(j["theme"]), locale: j["locale"] == null ? "en-US" : _asStr(j["locale"]), dateFormat: j["dateFormat"] == null ? "YMD" : _asStr(j["dateFormat"]), fontSize: j["fontSize"] == null ? "medium" : _asStr(j["fontSize"]), role: j["role"] == null ? "user" : _asStr(j["role"]), deletionRequestedAt: j["deletionRequestedAt"] == null ? null : DateTime.tryParse(_asStr(j["deletionRequestedAt"]))?.toLocal(), ); bool get isAdmin => role == "admin"; bool get deletionPending => deletionRequestedAt != null; } /// One active login session (device), mirroring /api/sessions. class Session { final String id; final String deviceLabel; final String ip; final bool current; final DateTime? created; final DateTime? expiresAt; Session({ required this.id, required this.deviceLabel, required this.ip, required this.current, required this.created, required this.expiresAt, }); factory Session.fromJson(Map j) => Session( id: _asStr(j["id"]), deviceLabel: _asStr(j["deviceLabel"]), ip: _asStr(j["ip"]), current: _asBool(j["current"]), created: DateTime.tryParse(_asStr(j["created"]))?.toLocal(), expiresAt: DateTime.tryParse(_asStr(j["expiresAt"]))?.toLocal(), ); }