add dashboard to phone app
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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>?,
|
||||
);
|
||||
}
|
||||
@@ -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() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -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?,
|
||||
);
|
||||
}
|
||||
@@ -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?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user