Initial commit
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
// 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> j) {
|
||||
final user = Map<String, dynamic>.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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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(),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user