add dashboard to phone app

This commit is contained in:
root
2026-05-25 05:10:43 -04:00
parent d04c8ce437
commit 277db06d26
67 changed files with 7872 additions and 570 deletions
+15
View File
@@ -1,4 +1,5 @@
import '../models/analytics.dart';
import '../models/report.dart';
import 'api_client.dart';
class AnalyticsService {
@@ -8,4 +9,18 @@ class AnalyticsService {
final res = await _client.get('/analytics/dashboard');
return DashboardMetrics.fromJson(res.data as Map<String, dynamic>);
}
Future<ReportSummary> getReport({
required String startDate,
required String endDate,
String? status,
}) async {
final res = await _client.get('/analytics/report', params: {
'startDate': startDate,
'endDate': endDate,
'status': ?status,
'format': 'JSON',
});
return ReportSummary.fromJson(res.data as Map<String, dynamic>);
}
}
+9 -1
View File
@@ -17,7 +17,7 @@ class ApiClient {
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await TokenStorage.getToken();
if (token != null) {
if (token != null && !_shouldSkipAuthHeader(options.path)) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
@@ -41,6 +41,11 @@ class ApiClient {
static ApiClient get instance => _instance;
bool _shouldSkipAuthHeader(String path) {
return path.endsWith('/auth/employee/login') ||
path.endsWith('/auth/renter/login');
}
// baseUrl is dynamic (platform-aware), so recreate when it may have changed.
// In practice this is called once and cached via the singleton.
Dio get dio => _dio;
@@ -54,5 +59,8 @@ class ApiClient {
Future<Response> patch(String path, {dynamic data}) =>
_dio.patch(path, data: data);
Future<Response> put(String path, {dynamic data}) =>
_dio.put(path, data: data);
Future<Response> delete(String path) => _dio.delete(path);
}
+1 -1
View File
@@ -11,7 +11,7 @@ class AuthService {
required String password,
}) async {
final res = await _client.post('/auth/employee/login', data: {
'email': email,
'email': email.trim().toLowerCase(),
'password': password,
});
final data = res.data as Map<String, dynamic>;
+293
View File
@@ -0,0 +1,293 @@
import 'api_client.dart';
class ContractCompany {
final String name;
final String? email;
final String? phone;
final String? address;
const ContractCompany(
{required this.name, this.email, this.phone, this.address});
factory ContractCompany.fromJson(Map<String, dynamic> j) {
final parts = <String>[];
final addr = j['address'];
if (addr is String && addr.trim().isNotEmpty) parts.add(addr.trim());
if (addr is Map) {
for (final v in addr.values) {
if (v is String && v.trim().isNotEmpty) parts.add(v.trim());
}
}
if (j['city'] is String) parts.add(j['city'] as String);
if (j['country'] is String) parts.add(j['country'] as String);
return ContractCompany(
name: (j['legalName'] as String?)?.isNotEmpty == true
? j['legalName'] as String
: j['name'] as String,
email: j['email'] as String?,
phone: j['phone'] as String?,
address: parts.isEmpty ? null : parts.join(', '),
);
}
}
class ContractDriver {
final String firstName;
final String lastName;
final String email;
final String? phone;
final String? driverLicense;
final String? licenseCountry;
final String? licenseExpiry;
final String? nationality;
const ContractDriver({
required this.firstName,
required this.lastName,
required this.email,
this.phone,
this.driverLicense,
this.licenseCountry,
this.licenseExpiry,
this.nationality,
});
String get fullName => '$firstName $lastName';
factory ContractDriver.fromJson(Map<String, dynamic> j) => ContractDriver(
firstName: j['firstName'] as String,
lastName: j['lastName'] as String,
email: j['email'] as String,
phone: j['phone'] as String?,
driverLicense: j['driverLicense'] as String?,
licenseCountry: j['licenseCountry'] as String?,
licenseExpiry: j['licenseExpiry'] as String?,
nationality: j['nationality'] as String?,
);
}
class ContractVehicle {
final String make;
final String model;
final int year;
final String? color;
final String licensePlate;
final String? vin;
final String category;
const ContractVehicle({
required this.make,
required this.model,
required this.year,
this.color,
required this.licensePlate,
this.vin,
required this.category,
});
String get displayName => '$year $make $model';
factory ContractVehicle.fromJson(Map<String, dynamic> j) => ContractVehicle(
make: j['make'] as String,
model: j['model'] as String,
year: j['year'] as int,
color: j['color'] as String?,
licensePlate: j['licensePlate'] as String,
vin: j['vin'] as String?,
category: j['category'] as String,
);
}
class ContractLineItem {
final String description;
final int qty;
final int unitPrice;
final int total;
const ContractLineItem({
required this.description,
required this.qty,
required this.unitPrice,
required this.total,
});
factory ContractLineItem.fromJson(Map<String, dynamic> j) => ContractLineItem(
description: j['description'] as String,
qty: (j['qty'] as num).toInt(),
unitPrice: (j['unitPrice'] as num).toInt(),
total: (j['total'] as num).toInt(),
);
}
class ContractPayment {
final String id;
final int amount;
final String currency;
final String type;
final String status;
final String? paymentMethod;
final DateTime? paidAt;
const ContractPayment({
required this.id,
required this.amount,
required this.currency,
required this.type,
required this.status,
this.paymentMethod,
this.paidAt,
});
factory ContractPayment.fromJson(Map<String, dynamic> j) => ContractPayment(
id: j['id'] as String,
amount: (j['amount'] as num).toInt(),
currency: j['currency'] as String? ?? 'USD',
type: j['type'] as String,
status: j['status'] as String,
paymentMethod: j['paymentMethod'] as String?,
paidAt: j['paidAt'] != null
? DateTime.tryParse(j['paidAt'] as String)
: null,
);
}
class ContractInspection {
final int? mileage;
final String? fuelLevel;
final String? generalCondition;
final String? employeeNotes;
const ContractInspection(
{this.mileage, this.fuelLevel, this.generalCondition, this.employeeNotes});
factory ContractInspection.fromJson(Map<String, dynamic> j) =>
ContractInspection(
mileage: j['mileage'] as int?,
fuelLevel: j['fuelLevel'] as String?,
generalCondition: j['generalCondition'] as String?,
employeeNotes: j['employeeNotes'] as String?,
);
}
class ContractPayload {
final String reservationId;
final String? contractNumber;
final String? invoiceNumber;
final String status;
final String paymentStatus;
final String? notes;
final DateTime generatedAt;
final ContractCompany company;
final ContractDriver driver;
final ContractVehicle vehicle;
final DateTime startDate;
final DateTime endDate;
final int totalDays;
final String? pickupLocation;
final String? returnLocation;
final List<ContractLineItem> lineItems;
final int subtotal;
final int taxTotal;
final int total;
final int amountPaid;
final int balanceDue;
final String currency;
final List<ContractPayment> payments;
final ContractInspection? checkIn;
final ContractInspection? checkOut;
final String? terms;
final String? fuelPolicy;
final String? depositPolicy;
const ContractPayload({
required this.reservationId,
this.contractNumber,
this.invoiceNumber,
required this.status,
required this.paymentStatus,
this.notes,
required this.generatedAt,
required this.company,
required this.driver,
required this.vehicle,
required this.startDate,
required this.endDate,
required this.totalDays,
this.pickupLocation,
this.returnLocation,
required this.lineItems,
required this.subtotal,
required this.taxTotal,
required this.total,
required this.amountPaid,
required this.balanceDue,
required this.currency,
required this.payments,
this.checkIn,
this.checkOut,
this.terms,
this.fuelPolicy,
this.depositPolicy,
});
factory ContractPayload.fromJson(Map<String, dynamic> j) {
final period = j['rentalPeriod'] as Map<String, dynamic>;
final invoice = j['invoice'] as Map<String, dynamic>;
final inspections =
j['inspections'] as Map<String, dynamic>? ?? {};
final termsData = j['terms'] as Map<String, dynamic>? ?? {};
return ContractPayload(
reservationId: j['reservationId'] as String,
contractNumber: j['contractNumber'] as String?,
invoiceNumber: j['invoiceNumber'] as String?,
status: j['status'] as String,
paymentStatus: j['paymentStatus'] as String,
notes: j['notes'] as String?,
generatedAt: DateTime.parse(j['generatedAt'] as String),
company:
ContractCompany.fromJson(j['company'] as Map<String, dynamic>),
driver:
ContractDriver.fromJson(j['driver'] as Map<String, dynamic>),
vehicle:
ContractVehicle.fromJson(j['vehicle'] as Map<String, dynamic>),
startDate: DateTime.parse(period['startDate'] as String),
endDate: DateTime.parse(period['endDate'] as String),
totalDays: (period['totalDays'] as num).toInt(),
pickupLocation: period['pickupLocation'] as String?,
returnLocation: period['returnLocation'] as String?,
lineItems: (invoice['lineItems'] as List<dynamic>? ?? [])
.map((e) => ContractLineItem.fromJson(e as Map<String, dynamic>))
.toList(),
subtotal: (invoice['subtotal'] as num).toInt(),
taxTotal: (invoice['taxTotal'] as num).toInt(),
total: (invoice['total'] as num).toInt(),
amountPaid: (invoice['amountPaid'] as num).toInt(),
balanceDue: (invoice['balanceDue'] as num).toInt(),
currency: invoice['currency'] as String? ?? 'USD',
payments: (invoice['payments'] as List<dynamic>? ?? [])
.map((e) => ContractPayment.fromJson(e as Map<String, dynamic>))
.toList(),
checkIn: inspections['checkIn'] != null
? ContractInspection.fromJson(
inspections['checkIn'] as Map<String, dynamic>)
: null,
checkOut: inspections['checkOut'] != null
? ContractInspection.fromJson(
inspections['checkOut'] as Map<String, dynamic>)
: null,
terms: termsData['terms'] as String?,
fuelPolicy: termsData['fuelPolicy'] as String?,
depositPolicy: termsData['depositPolicy'] as String?,
);
}
}
class ContractService {
final _client = ApiClient.instance;
Future<ContractPayload> getContract(String reservationId) async {
final res = await _client.get('/reservations/$reservationId/contract');
return ContractPayload.fromJson(res.data as Map<String, dynamic>);
}
}
+37
View File
@@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import '../models/customer.dart';
import 'api_client.dart';
@@ -26,4 +27,40 @@ class CustomerService {
final res = await _client.post('/customers', data: data);
return Customer.fromJson(res.data as Map<String, dynamic>);
}
Future<Customer> updateCustomer(String id, Map<String, dynamic> data) async {
final res = await _client.patch('/customers/$id', data: data);
return Customer.fromJson(res.data as Map<String, dynamic>);
}
Future<void> flagCustomer(String id, String reason) async {
await _client.post('/customers/$id/flag', data: {'reason': reason});
}
Future<void> unflagCustomer(String id) async {
await _client.delete('/customers/$id/flag');
}
Future<Customer> approveLicense(
String id, String decision, {String? note}) async {
final res = await _client.post('/customers/$id/approve-license',
data: {'decision': decision, 'note': ?note});
return Customer.fromJson(res.data as Map<String, dynamic>);
}
Future<Customer> uploadLicenseImage(String id, String filePath) async {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(filePath),
});
final res = await _client.dio.post(
'/customers/$id/license-image',
data: formData,
options: Options(headers: {'Content-Type': 'multipart/form-data'}),
);
final body = res.data;
final payload = (body is Map<String, dynamic> && body.containsKey('data'))
? body['data']
: body;
return Customer.fromJson(payload as Map<String, dynamic>);
}
}
+3 -3
View File
@@ -46,10 +46,10 @@ class MarketplaceService {
'startDate': '${startDate.toIso8601String().substring(0, 10)}T00:00:00.000Z',
if (endDate != null)
'endDate': '${endDate.toIso8601String().substring(0, 10)}T00:00:00.000Z',
if (category != null) 'category': category,
if (transmission != null) 'transmission': transmission,
'category': ?category,
'transmission': ?transmission,
if (make != null && make.isNotEmpty) 'make': make,
if (maxPriceCents != null) 'maxPrice': maxPriceCents,
'maxPrice': ?maxPriceCents,
});
final data = res.data;
@@ -0,0 +1,29 @@
import '../models/notification.dart';
import 'api_client.dart';
class NotificationService {
final _client = ApiClient.instance;
Future<List<AppNotification>> getNotifications({bool? unreadOnly}) async {
final res = await _client.get('/notifications/company', params: {
if (unreadOnly == true) 'unread': true,
});
final list = res.data as List<dynamic>;
return list
.map((e) => AppNotification.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<int> getUnreadCount() async {
final res = await _client.get('/notifications/unread-count');
return (res.data as Map<String, dynamic>)['unread'] as int? ?? 0;
}
Future<void> markRead(String id) async {
await _client.post('/notifications/company/$id/read');
}
Future<void> markAllRead() async {
await _client.post('/notifications/company/read-all');
}
}
+41
View File
@@ -0,0 +1,41 @@
import '../models/offer.dart';
import 'api_client.dart';
class OfferService {
final _client = ApiClient.instance;
Future<List<CompanyOffer>> getOffers() async {
final res = await _client.get('/offers');
final list = res.data as List<dynamic>;
return list
.map((e) => CompanyOffer.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<CompanyOffer> getOffer(String id) async {
final res = await _client.get('/offers/$id');
return CompanyOffer.fromJson(res.data as Map<String, dynamic>);
}
Future<CompanyOffer> createOffer(Map<String, dynamic> data) async {
final res = await _client.post('/offers', data: data);
return CompanyOffer.fromJson(res.data as Map<String, dynamic>);
}
Future<CompanyOffer> updateOffer(String id, Map<String, dynamic> data) async {
final res = await _client.patch('/offers/$id', data: data);
return CompanyOffer.fromJson(res.data as Map<String, dynamic>);
}
Future<void> deleteOffer(String id) async {
await _client.delete('/offers/$id');
}
Future<void> activateOffer(String id) async {
await _client.post('/offers/$id/activate');
}
Future<void> deactivateOffer(String id) async {
await _client.post('/offers/$id/deactivate');
}
}
+52
View File
@@ -0,0 +1,52 @@
import 'api_client.dart';
class Payment {
final String id;
final int amount;
final String currency;
final String type;
final String paymentMethod;
final String status;
final DateTime createdAt;
const Payment({
required this.id,
required this.amount,
required this.currency,
required this.type,
required this.paymentMethod,
required this.status,
required this.createdAt,
});
factory Payment.fromJson(Map<String, dynamic> json) => Payment(
id: json['id'] as String,
amount: json['amount'] as int? ?? 0,
currency: json['currency'] as String? ?? 'USD',
type: json['type'] as String? ?? 'CHARGE',
paymentMethod: json['paymentMethod'] as String? ?? 'CASH',
status: json['status'] as String? ?? 'COMPLETED',
createdAt: DateTime.parse(
json['createdAt'] as String? ?? DateTime.now().toIso8601String()),
);
}
class PaymentService {
final _client = ApiClient.instance;
Future<List<Payment>> getPayments(String reservationId) async {
final res = await _client.get('/payments/reservations/$reservationId');
final list = res.data as List<dynamic>;
return list
.map((e) => Payment.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<Payment> recordManualPayment(
String reservationId, Map<String, dynamic> data) async {
final res = await _client.post(
'/payments/reservations/$reservationId/manual',
data: data);
return Payment.fromJson(res.data as Map<String, dynamic>);
}
}
+59 -1
View File
@@ -1,6 +1,38 @@
import '../models/reservation.dart';
import 'api_client.dart';
class ReservationInspection {
final String id;
final String type; // CHECKIN | CHECKOUT
final int? mileage;
final String? fuelLevel;
final String? generalCondition;
final String? employeeNotes;
final DateTime createdAt;
const ReservationInspection({
required this.id,
required this.type,
this.mileage,
this.fuelLevel,
this.generalCondition,
this.employeeNotes,
required this.createdAt,
});
factory ReservationInspection.fromJson(Map<String, dynamic> json) =>
ReservationInspection(
id: json['id'] as String,
type: json['type'] as String,
mileage: json['mileage'] as int?,
fuelLevel: json['fuelLevel'] as String?,
generalCondition: json['generalCondition'] as String?,
employeeNotes: json['employeeNotes'] as String?,
createdAt: DateTime.parse(
json['createdAt'] as String? ?? DateTime.now().toIso8601String()),
);
}
class ReservationService {
final _client = ApiClient.instance;
@@ -8,13 +40,17 @@ class ReservationService {
int page = 1,
int pageSize = 20,
String? status,
String? source,
String? search,
String? customerId,
}) async {
final res = await _client.get('/reservations', params: {
'page': page,
'pageSize': pageSize,
if (status != null) 'status': status,
'status': ?status,
'source': ?source,
if (search != null && search.isNotEmpty) 'search': search,
'customerId': ?customerId,
});
return ReservationListResponse.fromJson(res.data as Map<String, dynamic>);
}
@@ -44,4 +80,26 @@ class ReservationService {
Future<void> checkout(String id, int mileage) async {
await _client.post('/reservations/$id/checkout', data: {'mileage': mileage});
}
Future<Reservation> closeReservation(String id) async {
final res = await _client.post('/reservations/$id/close');
return Reservation.fromJson(res.data as Map<String, dynamic>);
}
Future<List<ReservationInspection>> getInspections(String id) async {
final res = await _client.get('/reservations/$id/inspections');
final list = res.data as List<dynamic>;
return list
.map((e) =>
ReservationInspection.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<ReservationInspection> upsertInspection(
String id, String type, Map<String, dynamic> data) async {
final res = await _client.put(
'/reservations/$id/inspections/$type',
data: data);
return ReservationInspection.fromJson(res.data as Map<String, dynamic>);
}
}
+16
View File
@@ -0,0 +1,16 @@
import '../models/settings.dart';
import 'api_client.dart';
class SettingsService {
final _client = ApiClient.instance;
Future<BrandSettings> getBrand() async {
final res = await _client.get('/companies/me/brand');
return BrandSettings.fromJson(res.data as Map<String, dynamic>);
}
Future<BrandSettings> updateBrand(Map<String, dynamic> data) async {
final res = await _client.patch('/companies/me/brand', data: data);
return BrandSettings.fromJson(res.data as Map<String, dynamic>);
}
}
+62
View File
@@ -0,0 +1,62 @@
import 'api_client.dart';
class TeamMember {
final String id;
final String firstName;
final String lastName;
final String email;
final String role;
final bool isActive;
final DateTime createdAt;
const TeamMember({
required this.id,
required this.firstName,
required this.lastName,
required this.email,
required this.role,
required this.isActive,
required this.createdAt,
});
String get fullName => '$firstName $lastName';
factory TeamMember.fromJson(Map<String, dynamic> json) => TeamMember(
id: json['id'] as String,
firstName: json['firstName'] as String,
lastName: json['lastName'] as String,
email: json['email'] as String,
role: json['role'] as String? ?? 'AGENT',
isActive: json['isActive'] as bool? ?? true,
createdAt: DateTime.parse(
json['createdAt'] as String? ?? DateTime.now().toIso8601String()),
);
}
class TeamService {
final _client = ApiClient.instance;
Future<List<TeamMember>> getMembers() async {
final res = await _client.get('/team');
final list = res.data as List<dynamic>;
return list
.map((e) => TeamMember.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<void> inviteMember(Map<String, dynamic> data) async {
await _client.post('/team/invite', data: data);
}
Future<void> updateRole(String id, String role) async {
await _client.patch('/team/$id/role', data: {'role': role});
}
Future<void> deactivateMember(String id) async {
await _client.post('/team/$id/deactivate');
}
Future<void> reactivateMember(String id) async {
await _client.post('/team/$id/reactivate');
}
}
+127 -1
View File
@@ -1,6 +1,69 @@
import 'package:dio/dio.dart';
import '../models/vehicle.dart';
import 'api_client.dart';
class MaintenanceLog {
final String id;
final String type;
final String? description;
final int? cost;
final int? mileage;
final DateTime performedAt;
final DateTime? nextDueAt;
final int? nextDueMileage;
const MaintenanceLog({
required this.id,
required this.type,
this.description,
this.cost,
this.mileage,
required this.performedAt,
this.nextDueAt,
this.nextDueMileage,
});
factory MaintenanceLog.fromJson(Map<String, dynamic> json) => MaintenanceLog(
id: json['id'] as String,
type: json['type'] as String,
description: json['description'] as String?,
cost: json['cost'] as int?,
mileage: json['mileage'] as int?,
performedAt: DateTime.parse(json['performedAt'] as String),
nextDueAt: json['nextDueAt'] != null
? DateTime.parse(json['nextDueAt'] as String)
: null,
nextDueMileage: json['nextDueMileage'] as int?,
);
}
class CalendarEvent {
final String id;
final String type; // RESERVATION | MAINTENANCE | BLOCK
final DateTime startDate;
final DateTime endDate;
final String? status;
final String label;
const CalendarEvent({
required this.id,
required this.type,
required this.startDate,
required this.endDate,
this.status,
required this.label,
});
factory CalendarEvent.fromJson(Map<String, dynamic> json) => CalendarEvent(
id: json['id'] as String,
type: json['type'] as String,
startDate: DateTime.parse(json['startDate'] as String),
endDate: DateTime.parse(json['endDate'] as String),
status: json['status'] as String?,
label: json['label'] as String,
);
}
class VehicleService {
final _client = ApiClient.instance;
@@ -13,7 +76,7 @@ class VehicleService {
final res = await _client.get('/vehicles', params: {
'page': page,
'pageSize': pageSize,
if (status != null) 'status': status,
'status': ?status,
if (search != null && search.isNotEmpty) 'search': search,
});
return VehicleListResponse.fromJson(res.data as Map<String, dynamic>);
@@ -34,7 +97,70 @@ class VehicleService {
return Vehicle.fromJson(res.data as Map<String, dynamic>);
}
Future<void> deleteVehicle(String id) async {
await _client.delete('/vehicles/$id');
}
Future<void> togglePublish(String id, bool publish) async {
await _client.patch('/vehicles/$id/publish', data: {'isPublished': publish});
}
Future<Vehicle> uploadPhotos(String id, List<String> filePaths) async {
final formData = FormData.fromMap({
'photos': [
for (final path in filePaths)
await MultipartFile.fromFile(path),
],
});
final res = await _client.dio.post(
'/vehicles/$id/photos',
data: formData,
options: Options(headers: {'Content-Type': 'multipart/form-data'}),
);
final body = res.data;
final payload = (body is Map<String, dynamic> && body.containsKey('data'))
? body['data']
: body;
return Vehicle.fromJson(payload as Map<String, dynamic>);
}
Future<Vehicle> deletePhoto(String id, int idx) async {
final res = await _client.delete('/vehicles/$id/photos/$idx');
return Vehicle.fromJson(res.data as Map<String, dynamic>);
}
Future<List<MaintenanceLog>> getMaintenance(String id) async {
final res = await _client.get('/vehicles/$id/maintenance');
final list = res.data as List<dynamic>;
return list
.map((e) => MaintenanceLog.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<MaintenanceLog> addMaintenance(
String id, Map<String, dynamic> data) async {
final res = await _client.post('/vehicles/$id/maintenance', data: data);
return MaintenanceLog.fromJson(res.data as Map<String, dynamic>);
}
Future<List<CalendarEvent>> getCalendar(
String id, int year, int month) async {
final res = await _client.get('/vehicles/$id/calendar',
params: {'year': year, 'month': month});
final list = res.data as List<dynamic>;
return list
.map((e) => CalendarEvent.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<CalendarEvent> createCalendarBlock(
String id, Map<String, dynamic> data) async {
final res =
await _client.post('/vehicles/$id/calendar/blocks', data: data);
return CalendarEvent.fromJson(res.data as Map<String, dynamic>);
}
Future<void> deleteCalendarBlock(String id, String blockId) async {
await _client.delete('/vehicles/$id/calendar/blocks/$blockId');
}
}