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
+18 -4
View File
@@ -1,10 +1,24 @@
import 'dart:io';
class AppConstants {
// Android emulator routes localhost as 10.0.2.2; iOS simulator uses localhost fine
static String get baseUrl => Platform.isAndroid
? 'http://10.0.2.2:4000/api/v1'
: 'http://localhost:4000/api/v1';
static const String apiBaseUrlOverride =
String.fromEnvironment('API_BASE_URL');
static const String developmentAndroidApiUrl = 'http://10.0.2.2:4000/api/v1';
static const String developmentLocalApiUrl = 'http://localhost:4000/api/v1';
// Dev builds stay local by default. Real phones still need a LAN-reachable
// override because their localhost is the phone itself, not the dev machine.
static String get baseUrl {
if (apiBaseUrlOverride.isNotEmpty) {
return apiBaseUrlOverride;
}
if (Platform.isAndroid) {
return developmentAndroidApiUrl;
}
return developmentLocalApiUrl;
}
static const String tokenKey = 'auth_token';
static const String userTypeKey = 'user_type';
+1 -1
View File
@@ -193,7 +193,7 @@ class MarketplaceOffer {
String get discountLabel {
switch (type) {
case 'PERCENTAGE':
return '${discountValue}% off';
return '$discountValue% off';
case 'FIXED_AMOUNT':
return '\$${(discountValue / 100).toStringAsFixed(0)} off';
case 'FREE_DAY':
+31
View File
@@ -0,0 +1,31 @@
class AppNotification {
final String id;
final String type;
final String title;
final String body;
final bool isRead;
final DateTime createdAt;
final Map<String, dynamic>? data;
const AppNotification({
required this.id,
required this.type,
required this.title,
required this.body,
required this.isRead,
required this.createdAt,
this.data,
});
factory AppNotification.fromJson(Map<String, dynamic> json) =>
AppNotification(
id: json['id'] as String,
type: json['type'] as String? ?? '',
title: json['title'] as String? ?? '',
body: json['body'] as String? ?? '',
isRead: json['isRead'] as bool? ?? false,
createdAt: DateTime.parse(
json['createdAt'] as String? ?? DateTime.now().toIso8601String()),
data: json['data'] as Map<String, dynamic>?,
);
}
+68
View File
@@ -0,0 +1,68 @@
class CompanyOffer {
final String id;
final String title;
final String? description;
final String type; // PERCENTAGE | FIXED_AMOUNT | FREE_DAY | SPECIAL_RATE
final int discountValue;
final String? promoCode;
final bool isActive;
final bool isPublic;
final bool isFeatured;
final DateTime validFrom;
final DateTime validUntil;
final int? maxRedemptions;
final int redemptionCount;
final List<String> categories;
const CompanyOffer({
required this.id,
required this.title,
this.description,
required this.type,
required this.discountValue,
this.promoCode,
required this.isActive,
required this.isPublic,
required this.isFeatured,
required this.validFrom,
required this.validUntil,
this.maxRedemptions,
required this.redemptionCount,
required this.categories,
});
String get discountLabel {
switch (type) {
case 'PERCENTAGE':
return '$discountValue% off';
case 'FIXED_AMOUNT':
return '\$${(discountValue / 100).toStringAsFixed(0)} off';
case 'FREE_DAY':
return '$discountValue free day${discountValue == 1 ? '' : 's'}';
case 'SPECIAL_RATE':
return 'Special rate';
default:
return 'Offer';
}
}
factory CompanyOffer.fromJson(Map<String, dynamic> json) => CompanyOffer(
id: json['id'] as String,
title: json['title'] as String,
description: json['description'] as String?,
type: json['type'] as String? ?? 'PERCENTAGE',
discountValue: json['discountValue'] as int? ?? 0,
promoCode: json['promoCode'] as String?,
isActive: json['isActive'] as bool? ?? true,
isPublic: json['isPublic'] as bool? ?? false,
isFeatured: json['isFeatured'] as bool? ?? false,
validFrom: DateTime.parse(json['validFrom'] as String),
validUntil: DateTime.parse(json['validUntil'] as String),
maxRedemptions: json['maxRedemptions'] as int?,
redemptionCount: json['redemptionCount'] as int? ?? 0,
categories: (json['categories'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
);
}
+62
View File
@@ -0,0 +1,62 @@
class ReportSummary {
final int totalReservations;
final double rentalRevenue;
final double totalPaid;
final double totalOutstanding;
final List<ReportItem> items;
const ReportSummary({
required this.totalReservations,
required this.rentalRevenue,
required this.totalPaid,
required this.totalOutstanding,
required this.items,
});
factory ReportSummary.fromJson(Map<String, dynamic> json) => ReportSummary(
totalReservations: json['totalReservations'] as int? ?? 0,
rentalRevenue: (json['rentalRevenue'] as num?)?.toDouble() ?? 0,
totalPaid: (json['totalPaid'] as num?)?.toDouble() ?? 0,
totalOutstanding:
(json['totalOutstanding'] as num?)?.toDouble() ?? 0,
items: (json['reservations'] as List<dynamic>? ?? [])
.map((e) => ReportItem.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
class ReportItem {
final String id;
final String? customerName;
final String? vehicleName;
final DateTime startDate;
final DateTime endDate;
final double totalAmount;
final String status;
final String paymentStatus;
final String? source;
const ReportItem({
required this.id,
this.customerName,
this.vehicleName,
required this.startDate,
required this.endDate,
required this.totalAmount,
required this.status,
required this.paymentStatus,
this.source,
});
factory ReportItem.fromJson(Map<String, dynamic> json) => ReportItem(
id: json['id'] as String,
customerName: json['customerName'] as String?,
vehicleName: json['vehicleName'] as String?,
startDate: DateTime.parse(json['startDate'] as String),
endDate: DateTime.parse(json['endDate'] as String),
totalAmount: (json['totalAmount'] as num?)?.toDouble() ?? 0,
status: json['status'] as String? ?? '',
paymentStatus: json['paymentStatus'] as String? ?? '',
source: json['source'] as String?,
);
}
+6
View File
@@ -11,6 +11,8 @@ class Reservation {
final CustomerSummary? customer;
final DateTime createdAt;
final String? source;
final String? contractNumber;
final String? invoiceNumber;
const Reservation({
required this.id,
@@ -25,6 +27,8 @@ class Reservation {
this.customer,
required this.createdAt,
this.source,
this.contractNumber,
this.invoiceNumber,
});
int get days => endDate.difference(startDate).inDays;
@@ -49,6 +53,8 @@ class Reservation {
createdAt: DateTime.parse(
json['createdAt'] as String? ?? DateTime.now().toIso8601String()),
source: json['source'] as String?,
contractNumber: json['contractNumber'] as String?,
invoiceNumber: json['invoiceNumber'] as String?,
);
}
+63
View File
@@ -0,0 +1,63 @@
class BrandSettings {
final String displayName;
final String? tagline;
final String? publicEmail;
final String? publicPhone;
final String? publicCity;
final String? publicCountry;
final String? websiteUrl;
final String? whatsappNumber;
final String? logoUrl;
final String primaryColor;
final bool isListedOnMarketplace;
final String? defaultLocale;
final String? defaultCurrency;
const BrandSettings({
required this.displayName,
this.tagline,
this.publicEmail,
this.publicPhone,
this.publicCity,
this.publicCountry,
this.websiteUrl,
this.whatsappNumber,
this.logoUrl,
required this.primaryColor,
required this.isListedOnMarketplace,
this.defaultLocale,
this.defaultCurrency,
});
factory BrandSettings.fromJson(Map<String, dynamic> json) => BrandSettings(
displayName: json['displayName'] as String? ?? '',
tagline: json['tagline'] as String?,
publicEmail: json['publicEmail'] as String?,
publicPhone: json['publicPhone'] as String?,
publicCity: json['publicCity'] as String?,
publicCountry: json['publicCountry'] as String?,
websiteUrl: json['websiteUrl'] as String?,
whatsappNumber: json['whatsappNumber'] as String?,
logoUrl: json['logoUrl'] as String?,
primaryColor: json['primaryColor'] as String? ?? '#1A56DB',
isListedOnMarketplace:
json['isListedOnMarketplace'] as bool? ?? false,
defaultLocale: json['defaultLocale'] as String?,
defaultCurrency: json['defaultCurrency'] as String?,
);
Map<String, dynamic> toJson() => {
'displayName': displayName,
if (tagline != null) 'tagline': tagline,
if (publicEmail != null) 'publicEmail': publicEmail,
if (publicPhone != null) 'publicPhone': publicPhone,
if (publicCity != null) 'publicCity': publicCity,
if (publicCountry != null) 'publicCountry': publicCountry,
if (websiteUrl != null) 'websiteUrl': websiteUrl,
if (whatsappNumber != null) 'whatsappNumber': whatsappNumber,
'primaryColor': primaryColor,
'isListedOnMarketplace': isListedOnMarketplace,
if (defaultLocale != null) 'defaultLocale': defaultLocale,
if (defaultCurrency != null) 'defaultCurrency': defaultCurrency,
};
}
+3
View File
@@ -12,6 +12,7 @@ class Vehicle {
final int? seats;
final String? transmission;
final String? fuelType;
final String? color;
final bool isPublished;
const Vehicle({
@@ -28,6 +29,7 @@ class Vehicle {
this.seats,
this.transmission,
this.fuelType,
this.color,
required this.isPublished,
});
@@ -52,6 +54,7 @@ class Vehicle {
seats: json['seats'] as int?,
transmission: json['transmission'] as String?,
fuelType: json['fuelType'] as String?,
color: json['color'] as String?,
isPublished: json['isPublished'] as bool? ?? false,
);
}
+92 -86
View File
@@ -7,7 +7,6 @@ import '../../features/auth/screens/splash_screen.dart';
import '../../features/auth/screens/landing_screen.dart';
import '../../features/auth/screens/role_screen.dart';
import '../../features/auth/screens/employee_login_screen.dart';
import '../../features/dashboard/screens/dashboard_shell.dart';
import '../../features/dashboard/screens/home_screen.dart';
import '../../features/dashboard/screens/vehicles_screen.dart';
import '../../features/dashboard/screens/vehicle_detail_screen.dart';
@@ -15,7 +14,14 @@ import '../../features/dashboard/screens/reservations_screen.dart';
import '../../features/dashboard/screens/reservation_detail_screen.dart';
import '../../features/dashboard/screens/customers_screen.dart';
import '../../features/dashboard/screens/customer_detail_screen.dart';
import '../../features/client/screens/client_shell.dart';
import '../../features/dashboard/screens/notifications_screen.dart';
import '../../features/dashboard/screens/online_reservations_screen.dart';
import '../../features/dashboard/screens/contract_screen.dart';
import '../../features/dashboard/screens/contracts_screen.dart';
import '../../features/dashboard/screens/offers_screen.dart';
import '../../features/dashboard/screens/reports_screen.dart';
import '../../features/dashboard/screens/settings_screen.dart';
import '../../features/dashboard/screens/team_screen.dart';
import '../../features/client/screens/client_home_screen.dart';
import '../../features/client/screens/client_vehicle_detail_screen.dart';
import '../../features/client/screens/booking_screen.dart';
@@ -25,7 +31,6 @@ final routerProvider = Provider<GoRouter>((ref) {
final router = GoRouter(
initialLocation: '/splash',
redirect: (context, state) {
// Read (not watch) so redirect is evaluated on router.refresh()
final auth = ref.read(authProvider);
final status = auth.status;
final loc = state.matchedLocation;
@@ -33,12 +38,10 @@ final routerProvider = Provider<GoRouter>((ref) {
final isLanding = loc == '/landing';
final isLogin = loc.startsWith('/login');
// Still initialising — stay on splash
if (status == AuthStatus.unknown) {
return isSplash ? null : '/splash';
}
// Auth resolved — always leave splash
if (isSplash) {
if (status == AuthStatus.authenticated) {
return auth.userType == AppConstants.userTypeEmployee
@@ -49,7 +52,6 @@ final routerProvider = Provider<GoRouter>((ref) {
}
if (status == AuthStatus.unauthenticated) {
// Allow landing, login routes, and the entire client section without auth
if (isLanding || isLogin || loc.startsWith('/client')) return null;
return '/landing';
}
@@ -64,95 +66,99 @@ final routerProvider = Provider<GoRouter>((ref) {
return null;
},
routes: [
GoRoute(
path: '/splash',
builder: (context, _) => const SplashScreen(),
),
GoRoute(
path: '/landing',
builder: (context, _) => const LandingScreen(),
),
GoRoute(
path: '/role',
builder: (context, _) => const RoleScreen(),
),
GoRoute(path: '/splash', builder: (context, _) => const SplashScreen()),
GoRoute(path: '/landing', builder: (context, _) => const LandingScreen()),
GoRoute(path: '/role', builder: (context, _) => const RoleScreen()),
GoRoute(
path: '/login/employee',
builder: (context, _) => const EmployeeLoginScreen(),
),
ShellRoute(
builder: (context, state, child) => DashboardShell(child: child),
routes: [
GoRoute(
path: '/dashboard',
builder: (context, _) => const HomeScreen(),
),
GoRoute(
path: '/dashboard/vehicles',
builder: (context, _) => const VehiclesScreen(),
routes: [
GoRoute(
path: ':id',
builder: (_, state) =>
VehicleDetailScreen(id: state.pathParameters['id']!),
),
],
),
GoRoute(
path: '/dashboard/reservations',
builder: (context, _) => const ReservationsScreen(),
routes: [
GoRoute(
path: ':id',
builder: (_, state) =>
ReservationDetailScreen(id: state.pathParameters['id']!),
),
],
),
GoRoute(
path: '/dashboard/customers',
builder: (context, _) => const CustomersScreen(),
routes: [
GoRoute(
path: ':id',
builder: (_, state) =>
CustomerDetailScreen(id: state.pathParameters['id']!),
),
],
),
],
GoRoute(path: '/dashboard', builder: (context, _) => const HomeScreen()),
GoRoute(
path: '/dashboard/vehicles',
builder: (context, _) => const VehiclesScreen(),
),
ShellRoute(
builder: (context, state, child) => ClientShell(child: child),
routes: [
GoRoute(
path: '/client',
builder: (context, _) => const ClientHomeScreen(),
),
GoRoute(
path: '/client/vehicles/:id',
builder: (_, state) => ClientVehicleDetailScreen(
id: state.pathParameters['id']!,
vehicle: state.extra as MarketplaceVehicle?,
),
),
GoRoute(
path: '/client/book/:vehicleId',
builder: (_, state) => BookingScreen(
vehicleId: state.pathParameters['vehicleId']!,
vehicle: state.extra as MarketplaceVehicle?,
),
),
GoRoute(
path: '/client/bookings',
builder: (context, _) => const MyBookingsScreen(),
),
],
GoRoute(
path: '/dashboard/reservations',
builder: (context, _) => const ReservationsScreen(),
),
GoRoute(
path: '/dashboard/customers',
builder: (context, _) => const CustomersScreen(),
),
GoRoute(
path: '/dashboard/vehicles/:id',
builder: (_, state) =>
VehicleDetailScreen(id: state.pathParameters['id']!),
),
GoRoute(
path: '/dashboard/reservations/:id',
builder: (_, state) =>
ReservationDetailScreen(id: state.pathParameters['id']!),
),
GoRoute(
path: '/dashboard/reservations/:id/contract',
builder: (_, state) =>
ContractScreen(reservationId: state.pathParameters['id']!),
),
GoRoute(
path: '/dashboard/customers/:id',
builder: (_, state) =>
CustomerDetailScreen(id: state.pathParameters['id']!),
),
GoRoute(
path: '/dashboard/notifications',
builder: (context, _) => const NotificationsScreen(),
),
GoRoute(
path: '/dashboard/online-reservations',
builder: (context, _) => const OnlineReservationsScreen(),
),
GoRoute(
path: '/dashboard/contracts',
builder: (context, _) => const ContractsScreen(),
),
GoRoute(
path: '/dashboard/offers',
builder: (context, _) => const OffersScreen(),
),
GoRoute(
path: '/dashboard/reports',
builder: (context, _) => const ReportsScreen(),
),
GoRoute(
path: '/dashboard/settings',
builder: (context, _) => const SettingsScreen(),
),
GoRoute(
path: '/dashboard/team',
builder: (context, _) => const TeamScreen(),
),
GoRoute(
path: '/client',
builder: (context, _) => const ClientHomeScreen(),
),
GoRoute(
path: '/client/bookings',
builder: (context, _) => const MyBookingsScreen(),
),
GoRoute(
path: '/client/vehicles/:id',
builder: (_, state) => ClientVehicleDetailScreen(
id: state.pathParameters['id']!,
vehicle: state.extra as MarketplaceVehicle?,
),
),
GoRoute(
path: '/client/book/:vehicleId',
builder: (_, state) => BookingScreen(
vehicleId: state.pathParameters['vehicleId']!,
vehicle: state.extra as MarketplaceVehicle?,
),
),
],
);
// Trigger redirect re-evaluation whenever auth state changes
ref.listen<AuthState>(authProvider, (_, _) => router.refresh());
return router;
+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');
}
}