first app design
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
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 tokenKey = 'auth_token';
|
||||
static const String userTypeKey = 'user_type';
|
||||
|
||||
static const String userTypeEmployee = 'employee';
|
||||
static const String userTypeRenter = 'renter';
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
class DashboardMetrics {
|
||||
final int totalReservations;
|
||||
final int activeReservations;
|
||||
final double totalRevenue;
|
||||
final int totalVehicles;
|
||||
final int availableVehicles;
|
||||
final int totalCustomers;
|
||||
final double occupancyRate;
|
||||
|
||||
const DashboardMetrics({
|
||||
required this.totalReservations,
|
||||
required this.activeReservations,
|
||||
required this.totalRevenue,
|
||||
required this.totalVehicles,
|
||||
required this.availableVehicles,
|
||||
required this.totalCustomers,
|
||||
required this.occupancyRate,
|
||||
});
|
||||
|
||||
factory DashboardMetrics.fromJson(Map<String, dynamic> json) =>
|
||||
DashboardMetrics(
|
||||
totalReservations: json['totalReservations'] as int? ?? 0,
|
||||
activeReservations: json['activeReservations'] as int? ?? 0,
|
||||
totalRevenue: (json['totalRevenue'] as num?)?.toDouble() ?? 0,
|
||||
totalVehicles: json['totalVehicles'] as int? ?? 0,
|
||||
availableVehicles: json['availableVehicles'] as int? ?? 0,
|
||||
totalCustomers: json['totalCustomers'] as int? ?? 0,
|
||||
occupancyRate: (json['occupancyRate'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
|
||||
factory DashboardMetrics.empty() => const DashboardMetrics(
|
||||
totalReservations: 0,
|
||||
activeReservations: 0,
|
||||
totalRevenue: 0,
|
||||
totalVehicles: 0,
|
||||
availableVehicles: 0,
|
||||
totalCustomers: 0,
|
||||
occupancyRate: 0,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
class Employee {
|
||||
final String id;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String email;
|
||||
final String role;
|
||||
final bool isActive;
|
||||
final String? companyId;
|
||||
|
||||
const Employee({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.email,
|
||||
required this.role,
|
||||
required this.isActive,
|
||||
this.companyId,
|
||||
});
|
||||
|
||||
String get fullName => '$firstName $lastName';
|
||||
|
||||
factory Employee.fromJson(Map<String, dynamic> json) => Employee(
|
||||
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,
|
||||
isActive: json['isActive'] as bool? ?? true,
|
||||
companyId: json['companyId'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
class Renter {
|
||||
final String id;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String email;
|
||||
|
||||
const Renter({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.email,
|
||||
});
|
||||
|
||||
String get fullName => '$firstName $lastName';
|
||||
|
||||
factory Renter.fromJson(Map<String, dynamic> json) => Renter(
|
||||
id: json['id'] as String,
|
||||
firstName: json['firstName'] as String,
|
||||
lastName: json['lastName'] as String,
|
||||
email: json['email'] as String,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
class Customer {
|
||||
final String id;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String? email;
|
||||
final String? phone;
|
||||
final String licenseStatus;
|
||||
final bool isFlagged;
|
||||
final String? flagReason;
|
||||
final DateTime? dateOfBirth;
|
||||
final String? nationality;
|
||||
final DateTime createdAt;
|
||||
|
||||
const Customer({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
this.email,
|
||||
this.phone,
|
||||
required this.licenseStatus,
|
||||
required this.isFlagged,
|
||||
this.flagReason,
|
||||
this.dateOfBirth,
|
||||
this.nationality,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
String get fullName => '$firstName $lastName';
|
||||
|
||||
factory Customer.fromJson(Map<String, dynamic> json) => Customer(
|
||||
id: json['id'] as String,
|
||||
firstName: json['firstName'] as String,
|
||||
lastName: json['lastName'] as String,
|
||||
email: json['email'] as String?,
|
||||
phone: json['phone'] as String?,
|
||||
licenseStatus: json['licenseStatus'] as String? ?? 'PENDING',
|
||||
isFlagged: json['isFlagged'] as bool? ?? false,
|
||||
flagReason: json['flagReason'] as String?,
|
||||
dateOfBirth: json['dateOfBirth'] != null
|
||||
? DateTime.tryParse(json['dateOfBirth'] as String)
|
||||
: null,
|
||||
nationality: json['nationality'] as String?,
|
||||
createdAt: DateTime.parse(
|
||||
json['createdAt'] as String? ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
|
||||
class CustomerListResponse {
|
||||
final List<Customer> data;
|
||||
final int total;
|
||||
final int page;
|
||||
final int pageSize;
|
||||
|
||||
const CustomerListResponse({
|
||||
required this.data,
|
||||
required this.total,
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
});
|
||||
|
||||
factory CustomerListResponse.fromJson(Map<String, dynamic> json) =>
|
||||
CustomerListResponse(
|
||||
data: (json['data'] as List<dynamic>)
|
||||
.map((e) => Customer.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
total: json['total'] as int? ?? 0,
|
||||
page: json['page'] as int? ?? 1,
|
||||
pageSize: json['pageSize'] as int? ?? 20,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
class CompanyBrand {
|
||||
final String displayName;
|
||||
final String? logoUrl;
|
||||
final String subdomain;
|
||||
final double? marketplaceRating;
|
||||
final String primaryColor;
|
||||
|
||||
const CompanyBrand({
|
||||
required this.displayName,
|
||||
this.logoUrl,
|
||||
required this.subdomain,
|
||||
this.marketplaceRating,
|
||||
required this.primaryColor,
|
||||
});
|
||||
|
||||
factory CompanyBrand.fromJson(Map<String, dynamic> json) => CompanyBrand(
|
||||
displayName: json['displayName'] as String? ?? '',
|
||||
logoUrl: json['logoUrl'] as String?,
|
||||
subdomain: json['subdomain'] as String? ?? '',
|
||||
marketplaceRating:
|
||||
(json['marketplaceRating'] as num?)?.toDouble(),
|
||||
primaryColor: json['primaryColor'] as String? ?? '#1A56DB',
|
||||
);
|
||||
}
|
||||
|
||||
class MarketplaceCompany {
|
||||
final String id;
|
||||
final CompanyBrand brand;
|
||||
|
||||
const MarketplaceCompany({required this.id, required this.brand});
|
||||
|
||||
factory MarketplaceCompany.fromJson(Map<String, dynamic> json) =>
|
||||
MarketplaceCompany(
|
||||
id: json['id'] as String,
|
||||
brand: CompanyBrand.fromJson(
|
||||
json['brand'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
class MarketplaceVehicle {
|
||||
final String id;
|
||||
final String companyId;
|
||||
final String make;
|
||||
final String model;
|
||||
final int year;
|
||||
final String? color;
|
||||
final String licensePlate;
|
||||
final String category;
|
||||
final int seats;
|
||||
final String transmission;
|
||||
final String fuelType;
|
||||
final List<String> features;
|
||||
// dailyRate comes in cents from the API
|
||||
final int dailyRateCents;
|
||||
final String status;
|
||||
final List<String> photos;
|
||||
final int? mileage;
|
||||
final bool isPublished;
|
||||
final MarketplaceCompany company;
|
||||
final bool availability;
|
||||
final String availabilityStatus;
|
||||
final DateTime? nextAvailableAt;
|
||||
|
||||
const MarketplaceVehicle({
|
||||
required this.id,
|
||||
required this.companyId,
|
||||
required this.make,
|
||||
required this.model,
|
||||
required this.year,
|
||||
this.color,
|
||||
required this.licensePlate,
|
||||
required this.category,
|
||||
required this.seats,
|
||||
required this.transmission,
|
||||
required this.fuelType,
|
||||
required this.features,
|
||||
required this.dailyRateCents,
|
||||
required this.status,
|
||||
required this.photos,
|
||||
this.mileage,
|
||||
required this.isPublished,
|
||||
required this.company,
|
||||
required this.availability,
|
||||
required this.availabilityStatus,
|
||||
this.nextAvailableAt,
|
||||
});
|
||||
|
||||
String get displayName => '$year $make $model';
|
||||
String? get primaryPhoto => photos.isNotEmpty ? photos.first : null;
|
||||
// Convert cents to dollars for display
|
||||
double get dailyRate => dailyRateCents / 100;
|
||||
String get companySlug => company.brand.subdomain;
|
||||
|
||||
factory MarketplaceVehicle.fromJson(Map<String, dynamic> json) =>
|
||||
MarketplaceVehicle(
|
||||
id: json['id'] as String,
|
||||
companyId: json['companyId'] as String,
|
||||
make: json['make'] as String,
|
||||
model: json['model'] as String,
|
||||
year: json['year'] as int,
|
||||
color: json['color'] as String?,
|
||||
licensePlate: json['licensePlate'] as String,
|
||||
category: json['category'] as String? ?? 'ECONOMY',
|
||||
seats: json['seats'] as int? ?? 5,
|
||||
transmission: json['transmission'] as String? ?? 'MANUAL',
|
||||
fuelType: json['fuelType'] as String? ?? 'GASOLINE',
|
||||
features: (json['features'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
[],
|
||||
dailyRateCents: json['dailyRate'] as int? ?? 0,
|
||||
status: json['status'] as String? ?? 'AVAILABLE',
|
||||
photos: (json['photos'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
[],
|
||||
mileage: json['mileage'] as int?,
|
||||
isPublished: json['isPublished'] as bool? ?? false,
|
||||
company: MarketplaceCompany.fromJson(
|
||||
json['company'] as Map<String, dynamic>),
|
||||
availability: json['availability'] as bool? ?? true,
|
||||
availabilityStatus:
|
||||
json['availabilityStatus'] as String? ?? 'AVAILABLE',
|
||||
nextAvailableAt: json['nextAvailableAt'] != null
|
||||
? DateTime.tryParse(json['nextAvailableAt'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
class MarketplaceSearchResult {
|
||||
final List<MarketplaceVehicle> data;
|
||||
final int total;
|
||||
final int page;
|
||||
final int pageSize;
|
||||
|
||||
const MarketplaceSearchResult({
|
||||
required this.data,
|
||||
required this.total,
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
});
|
||||
|
||||
factory MarketplaceSearchResult.fromJson(Map<String, dynamic> json) {
|
||||
final raw = json['data'] ?? json;
|
||||
if (raw is List) {
|
||||
return MarketplaceSearchResult(
|
||||
data: raw
|
||||
.map((e) =>
|
||||
MarketplaceVehicle.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
total: json['total'] as int? ?? raw.length,
|
||||
page: json['page'] as int? ?? 1,
|
||||
pageSize: json['pageSize'] as int? ?? 20,
|
||||
);
|
||||
}
|
||||
return MarketplaceSearchResult(
|
||||
data: (json['data'] as List<dynamic>)
|
||||
.map((e) =>
|
||||
MarketplaceVehicle.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
total: json['total'] as int? ?? 0,
|
||||
page: json['page'] as int? ?? 1,
|
||||
pageSize: json['pageSize'] as int? ?? 20,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MarketplaceOffer {
|
||||
final String id;
|
||||
final String title;
|
||||
final String? description;
|
||||
final String? imageUrl;
|
||||
final String type;
|
||||
final int discountValue;
|
||||
final String? promoCode;
|
||||
final bool isFeatured;
|
||||
final DateTime validUntil;
|
||||
final MarketplaceCompany company;
|
||||
|
||||
const MarketplaceOffer({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.description,
|
||||
this.imageUrl,
|
||||
required this.type,
|
||||
required this.discountValue,
|
||||
this.promoCode,
|
||||
required this.isFeatured,
|
||||
required this.validUntil,
|
||||
required this.company,
|
||||
});
|
||||
|
||||
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(s)';
|
||||
case 'SPECIAL_RATE':
|
||||
return 'Special rate';
|
||||
default:
|
||||
return 'Offer';
|
||||
}
|
||||
}
|
||||
|
||||
factory MarketplaceOffer.fromJson(Map<String, dynamic> json) =>
|
||||
MarketplaceOffer(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String?,
|
||||
imageUrl: json['imageUrl'] as String?,
|
||||
type: json['type'] as String,
|
||||
discountValue: json['discountValue'] as int? ?? 0,
|
||||
promoCode: json['promoCode'] as String?,
|
||||
isFeatured: json['isFeatured'] as bool? ?? false,
|
||||
validUntil:
|
||||
DateTime.parse(json['validUntil'] as String),
|
||||
company: MarketplaceCompany.fromJson(
|
||||
json['company'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
class Reservation {
|
||||
final String id;
|
||||
final String status;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final double totalAmount;
|
||||
final String paymentStatus;
|
||||
final String? vehicleId;
|
||||
final String? customerId;
|
||||
final VehicleSummary? vehicle;
|
||||
final CustomerSummary? customer;
|
||||
final DateTime createdAt;
|
||||
final String? source;
|
||||
|
||||
const Reservation({
|
||||
required this.id,
|
||||
required this.status,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.totalAmount,
|
||||
required this.paymentStatus,
|
||||
this.vehicleId,
|
||||
this.customerId,
|
||||
this.vehicle,
|
||||
this.customer,
|
||||
required this.createdAt,
|
||||
this.source,
|
||||
});
|
||||
|
||||
int get days => endDate.difference(startDate).inDays;
|
||||
|
||||
factory Reservation.fromJson(Map<String, dynamic> json) => Reservation(
|
||||
id: json['id'] as String,
|
||||
status: json['status'] as String,
|
||||
startDate: DateTime.parse(json['startDate'] as String),
|
||||
endDate: DateTime.parse(json['endDate'] as String),
|
||||
totalAmount: (json['totalAmount'] as num?)?.toDouble() ?? 0,
|
||||
paymentStatus: json['paymentStatus'] as String? ?? 'PENDING',
|
||||
vehicleId: json['vehicleId'] as String?,
|
||||
customerId: json['customerId'] as String?,
|
||||
vehicle: json['vehicle'] != null
|
||||
? VehicleSummary.fromJson(
|
||||
json['vehicle'] as Map<String, dynamic>)
|
||||
: null,
|
||||
customer: json['customer'] != null
|
||||
? CustomerSummary.fromJson(
|
||||
json['customer'] as Map<String, dynamic>)
|
||||
: null,
|
||||
createdAt: DateTime.parse(
|
||||
json['createdAt'] as String? ?? DateTime.now().toIso8601String()),
|
||||
source: json['source'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
class VehicleSummary {
|
||||
final String id;
|
||||
final String make;
|
||||
final String model;
|
||||
final int year;
|
||||
final String licensePlate;
|
||||
final List<String> photos;
|
||||
|
||||
const VehicleSummary({
|
||||
required this.id,
|
||||
required this.make,
|
||||
required this.model,
|
||||
required this.year,
|
||||
required this.licensePlate,
|
||||
required this.photos,
|
||||
});
|
||||
|
||||
String get displayName => '$year $make $model';
|
||||
String? get primaryPhoto => photos.isNotEmpty ? photos.first : null;
|
||||
|
||||
factory VehicleSummary.fromJson(Map<String, dynamic> json) => VehicleSummary(
|
||||
id: json['id'] as String,
|
||||
make: json['make'] as String,
|
||||
model: json['model'] as String,
|
||||
year: json['year'] as int,
|
||||
licensePlate: json['licensePlate'] as String,
|
||||
photos: (json['photos'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
class CustomerSummary {
|
||||
final String id;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String? email;
|
||||
final String? phone;
|
||||
|
||||
const CustomerSummary({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
this.email,
|
||||
this.phone,
|
||||
});
|
||||
|
||||
String get fullName => '$firstName $lastName';
|
||||
|
||||
factory CustomerSummary.fromJson(Map<String, dynamic> json) =>
|
||||
CustomerSummary(
|
||||
id: json['id'] as String,
|
||||
firstName: json['firstName'] as String,
|
||||
lastName: json['lastName'] as String,
|
||||
email: json['email'] as String?,
|
||||
phone: json['phone'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
class ReservationListResponse {
|
||||
final List<Reservation> data;
|
||||
final int total;
|
||||
final int page;
|
||||
final int pageSize;
|
||||
|
||||
const ReservationListResponse({
|
||||
required this.data,
|
||||
required this.total,
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
});
|
||||
|
||||
factory ReservationListResponse.fromJson(Map<String, dynamic> json) =>
|
||||
ReservationListResponse(
|
||||
data: (json['data'] as List<dynamic>)
|
||||
.map((e) => Reservation.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
total: json['total'] as int? ?? 0,
|
||||
page: json['page'] as int? ?? 1,
|
||||
pageSize: json['pageSize'] as int? ?? 20,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
class Vehicle {
|
||||
final String id;
|
||||
final String make;
|
||||
final String model;
|
||||
final int year;
|
||||
final String licensePlate;
|
||||
final double dailyRate;
|
||||
final String status;
|
||||
final String category;
|
||||
final List<String> photos;
|
||||
final int? mileage;
|
||||
final int? seats;
|
||||
final String? transmission;
|
||||
final String? fuelType;
|
||||
final bool isPublished;
|
||||
|
||||
const Vehicle({
|
||||
required this.id,
|
||||
required this.make,
|
||||
required this.model,
|
||||
required this.year,
|
||||
required this.licensePlate,
|
||||
required this.dailyRate,
|
||||
required this.status,
|
||||
required this.category,
|
||||
required this.photos,
|
||||
this.mileage,
|
||||
this.seats,
|
||||
this.transmission,
|
||||
this.fuelType,
|
||||
required this.isPublished,
|
||||
});
|
||||
|
||||
String get displayName => '$year $make $model';
|
||||
|
||||
String? get primaryPhoto => photos.isNotEmpty ? photos.first : null;
|
||||
|
||||
factory Vehicle.fromJson(Map<String, dynamic> json) => Vehicle(
|
||||
id: json['id'] as String,
|
||||
make: json['make'] as String,
|
||||
model: json['model'] as String,
|
||||
year: json['year'] as int,
|
||||
licensePlate: json['licensePlate'] as String,
|
||||
dailyRate: (json['dailyRate'] as num).toDouble(),
|
||||
status: json['status'] as String? ?? 'AVAILABLE',
|
||||
category: json['category'] as String? ?? 'ECONOMY',
|
||||
photos: (json['photos'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
[],
|
||||
mileage: json['mileage'] as int?,
|
||||
seats: json['seats'] as int?,
|
||||
transmission: json['transmission'] as String?,
|
||||
fuelType: json['fuelType'] as String?,
|
||||
isPublished: json['isPublished'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
class VehicleListResponse {
|
||||
final List<Vehicle> data;
|
||||
final int total;
|
||||
final int page;
|
||||
final int pageSize;
|
||||
|
||||
const VehicleListResponse({
|
||||
required this.data,
|
||||
required this.total,
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
});
|
||||
|
||||
factory VehicleListResponse.fromJson(Map<String, dynamic> json) =>
|
||||
VehicleListResponse(
|
||||
data: (json['data'] as List<dynamic>)
|
||||
.map((e) => Vehicle.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
total: json['total'] as int? ?? 0,
|
||||
page: json['page'] as int? ?? 1,
|
||||
pageSize: json['pageSize'] as int? ?? 20,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
import '../models/auth_models.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../storage/token_storage.dart';
|
||||
|
||||
enum AuthStatus { unknown, authenticated, unauthenticated }
|
||||
|
||||
class AuthState {
|
||||
final AuthStatus status;
|
||||
final String? userType;
|
||||
final Employee? employee;
|
||||
final Renter? renter;
|
||||
|
||||
const AuthState({
|
||||
required this.status,
|
||||
this.userType,
|
||||
this.employee,
|
||||
this.renter,
|
||||
});
|
||||
|
||||
AuthState copyWith({
|
||||
AuthStatus? status,
|
||||
String? userType,
|
||||
Employee? employee,
|
||||
Renter? renter,
|
||||
}) =>
|
||||
AuthState(
|
||||
status: status ?? this.status,
|
||||
userType: userType ?? this.userType,
|
||||
employee: employee ?? this.employee,
|
||||
renter: renter ?? this.renter,
|
||||
);
|
||||
}
|
||||
|
||||
class AuthNotifier extends StateNotifier<AuthState> {
|
||||
final AuthService _service;
|
||||
|
||||
AuthNotifier(this._service)
|
||||
: super(const AuthState(status: AuthStatus.unknown));
|
||||
|
||||
Future<void> init() async {
|
||||
final token = await TokenStorage.getToken();
|
||||
final userType = await TokenStorage.getUserType();
|
||||
if (token == null || userType == null) {
|
||||
state = const AuthState(status: AuthStatus.unauthenticated);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (userType == AppConstants.userTypeEmployee) {
|
||||
final employee = await _service.getMe();
|
||||
state = AuthState(
|
||||
status: AuthStatus.authenticated,
|
||||
userType: userType,
|
||||
employee: employee,
|
||||
);
|
||||
} else {
|
||||
final renter = await _service.getRenterMe();
|
||||
state = AuthState(
|
||||
status: AuthStatus.authenticated,
|
||||
userType: userType,
|
||||
renter: renter,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
await TokenStorage.clear();
|
||||
state = const AuthState(status: AuthStatus.unauthenticated);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loginEmployee(String email, String password) async {
|
||||
final result = await _service.loginEmployee(email: email, password: password);
|
||||
await TokenStorage.saveToken(result.token);
|
||||
await TokenStorage.saveUserType(AppConstants.userTypeEmployee);
|
||||
state = AuthState(
|
||||
status: AuthStatus.authenticated,
|
||||
userType: AppConstants.userTypeEmployee,
|
||||
employee: result.employee,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await _service.logout();
|
||||
state = const AuthState(status: AuthStatus.unauthenticated);
|
||||
}
|
||||
}
|
||||
|
||||
final authServiceProvider = Provider((_) => AuthService());
|
||||
|
||||
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
|
||||
return AuthNotifier(ref.read(authServiceProvider));
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
const _localeKey = 'app_locale';
|
||||
|
||||
const _storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
|
||||
class LocaleNotifier extends StateNotifier<Locale> {
|
||||
LocaleNotifier() : super(const Locale('en')) {
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final code = await _storage.read(key: _localeKey);
|
||||
if (code != null) state = Locale(code);
|
||||
}
|
||||
|
||||
Future<void> setLocale(Locale locale) async {
|
||||
await _storage.write(key: _localeKey, value: locale.languageCode);
|
||||
state = locale;
|
||||
}
|
||||
}
|
||||
|
||||
final localeProvider = StateNotifierProvider<LocaleNotifier, Locale>(
|
||||
(_) => LocaleNotifier(),
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
const _themeKey = 'app_theme';
|
||||
|
||||
const _storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
|
||||
class ThemeNotifier extends StateNotifier<ThemeMode> {
|
||||
ThemeNotifier() : super(ThemeMode.dark) {
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final val = await _storage.read(key: _themeKey);
|
||||
if (val == 'light') state = ThemeMode.light;
|
||||
if (val == 'dark') state = ThemeMode.dark;
|
||||
}
|
||||
|
||||
Future<void> setMode(ThemeMode mode) async {
|
||||
await _storage.write(
|
||||
key: _themeKey, value: mode == ThemeMode.light ? 'light' : 'dark');
|
||||
state = mode;
|
||||
}
|
||||
|
||||
void toggle() =>
|
||||
setMode(state == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark);
|
||||
}
|
||||
|
||||
final themeProvider = StateNotifierProvider<ThemeNotifier, ThemeMode>(
|
||||
(_) => ThemeNotifier(),
|
||||
);
|
||||
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
import '../models/marketplace.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
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';
|
||||
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/client/screens/client_home_screen.dart';
|
||||
import '../../features/client/screens/client_vehicle_detail_screen.dart';
|
||||
import '../../features/client/screens/booking_screen.dart';
|
||||
import '../../features/client/screens/my_bookings_screen.dart';
|
||||
|
||||
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;
|
||||
final isSplash = loc == '/splash';
|
||||
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
|
||||
? '/dashboard'
|
||||
: '/client';
|
||||
}
|
||||
return '/landing';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
if (status == AuthStatus.authenticated) {
|
||||
if (isLogin) {
|
||||
return auth.userType == AppConstants.userTypeEmployee
|
||||
? '/dashboard'
|
||||
: '/client';
|
||||
}
|
||||
}
|
||||
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: '/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']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Trigger redirect re-evaluation whenever auth state changes
|
||||
ref.listen<AuthState>(authProvider, (_, _) => router.refresh());
|
||||
|
||||
return router;
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import '../models/analytics.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
class AnalyticsService {
|
||||
final _client = ApiClient.instance;
|
||||
|
||||
Future<DashboardMetrics> getDashboard() async {
|
||||
final res = await _client.get('/analytics/dashboard');
|
||||
return DashboardMetrics.fromJson(res.data as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
import '../storage/token_storage.dart';
|
||||
|
||||
class ApiClient {
|
||||
static final ApiClient _instance = ApiClient._();
|
||||
late final Dio _dio;
|
||||
|
||||
ApiClient._() {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: AppConstants.baseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
));
|
||||
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
final token = await TokenStorage.getToken();
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
onResponse: (response, handler) {
|
||||
// Every successful response from this API is wrapped: { data: <payload> }
|
||||
// Unwrap it so services always receive the raw payload.
|
||||
final body = response.data;
|
||||
if (body is Map<String, dynamic> &&
|
||||
body.length == 1 &&
|
||||
body.containsKey('data')) {
|
||||
response.data = body['data'];
|
||||
}
|
||||
handler.next(response);
|
||||
},
|
||||
onError: (e, handler) {
|
||||
handler.next(e);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
static ApiClient get instance => _instance;
|
||||
|
||||
// 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;
|
||||
|
||||
Future<Response> get(String path, {Map<String, dynamic>? params}) =>
|
||||
_dio.get(path, queryParameters: params);
|
||||
|
||||
Future<Response> post(String path, {dynamic data}) =>
|
||||
_dio.post(path, data: data);
|
||||
|
||||
Future<Response> patch(String path, {dynamic data}) =>
|
||||
_dio.patch(path, data: data);
|
||||
|
||||
Future<Response> delete(String path) => _dio.delete(path);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import '../models/auth_models.dart';
|
||||
import '../storage/token_storage.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
class AuthService {
|
||||
final _client = ApiClient.instance;
|
||||
|
||||
// Response after envelope unwrap: { token, employee: { id, firstName, ... } }
|
||||
Future<({String token, Employee employee})> loginEmployee({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
final res = await _client.post('/auth/employee/login', data: {
|
||||
'email': email,
|
||||
'password': password,
|
||||
});
|
||||
final data = res.data as Map<String, dynamic>;
|
||||
final token = data['token'] as String;
|
||||
final employee =
|
||||
Employee.fromJson(data['employee'] as Map<String, dynamic>);
|
||||
return (token: token, employee: employee);
|
||||
}
|
||||
|
||||
// Response after envelope unwrap: { employee: { id, firstName, ... } }
|
||||
Future<Employee> getMe() async {
|
||||
final res = await _client.get('/auth/employee/me');
|
||||
final data = res.data as Map<String, dynamic>;
|
||||
return Employee.fromJson(data['employee'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<Renter> getRenterMe() async {
|
||||
final res = await _client.get('/auth/renter/me');
|
||||
final data = res.data as Map<String, dynamic>;
|
||||
return Renter.fromJson(data);
|
||||
}
|
||||
|
||||
Future<void> logout() => TokenStorage.clear();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import '../models/customer.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
class CustomerService {
|
||||
final _client = ApiClient.instance;
|
||||
|
||||
Future<CustomerListResponse> getCustomers({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
String? search,
|
||||
}) async {
|
||||
final res = await _client.get('/customers', params: {
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
if (search != null && search.isNotEmpty) 'search': search,
|
||||
});
|
||||
return CustomerListResponse.fromJson(res.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<Customer> getCustomer(String id) async {
|
||||
final res = await _client.get('/customers/$id');
|
||||
return Customer.fromJson(res.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<Customer> createCustomer(Map<String, dynamic> data) async {
|
||||
final res = await _client.post('/customers', data: data);
|
||||
return Customer.fromJson(res.data as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import '../models/marketplace.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
class MarketplaceService {
|
||||
final _client = ApiClient.instance;
|
||||
|
||||
// After envelope unwrap: [ "City1", "City2", ... ]
|
||||
Future<List<String>> getCities() async {
|
||||
final res = await _client.get('/marketplace/cities');
|
||||
final data = res.data;
|
||||
if (data is List) {
|
||||
return data.whereType<String>().toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// After envelope unwrap: [ { id, title, ... }, ... ]
|
||||
Future<List<MarketplaceOffer>> getOffers() async {
|
||||
final res = await _client.get('/marketplace/offers');
|
||||
final data = res.data;
|
||||
if (data is List) {
|
||||
return data
|
||||
.map((e) => MarketplaceOffer.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// After envelope unwrap: [ { id, make, model, ... }, ... ]
|
||||
Future<MarketplaceSearchResult> search({
|
||||
String? city,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
String? category,
|
||||
String? transmission,
|
||||
String? make,
|
||||
int? maxPriceCents,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
}) async {
|
||||
final res = await _client.get('/marketplace/search', params: {
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
if (city != null && city.isNotEmpty) 'city': city,
|
||||
if (startDate != null)
|
||||
'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,
|
||||
if (make != null && make.isNotEmpty) 'make': make,
|
||||
if (maxPriceCents != null) 'maxPrice': maxPriceCents,
|
||||
});
|
||||
|
||||
final data = res.data;
|
||||
if (data is List) {
|
||||
final vehicles = data
|
||||
.map((e) => MarketplaceVehicle.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
return MarketplaceSearchResult(
|
||||
data: vehicles,
|
||||
total: vehicles.length,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
}
|
||||
// Fallback: paginated object shape { data: [...], total: N }
|
||||
return MarketplaceSearchResult.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<String> createReservation({
|
||||
required String vehicleId,
|
||||
required String companySlug,
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String email,
|
||||
String? phone,
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
String? notes,
|
||||
String language = 'en',
|
||||
}) async {
|
||||
final res = await _client.post('/marketplace/reservations', data: {
|
||||
'vehicleId': vehicleId,
|
||||
'companySlug': companySlug,
|
||||
'firstName': firstName,
|
||||
'lastName': lastName,
|
||||
'email': email,
|
||||
if (phone != null && phone.isNotEmpty) 'phone': phone,
|
||||
'startDate': '${startDate.toIso8601String().substring(0, 10)}T00:00:00.000Z',
|
||||
'endDate': '${endDate.toIso8601String().substring(0, 10)}T00:00:00.000Z',
|
||||
if (notes != null && notes.isNotEmpty) 'notes': notes,
|
||||
'language': language,
|
||||
});
|
||||
final data = res.data as Map<String, dynamic>;
|
||||
return data['reservationId'] as String;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import '../models/reservation.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
class ReservationService {
|
||||
final _client = ApiClient.instance;
|
||||
|
||||
Future<ReservationListResponse> getReservations({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
String? status,
|
||||
String? search,
|
||||
}) async {
|
||||
final res = await _client.get('/reservations', params: {
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
if (status != null) 'status': status,
|
||||
if (search != null && search.isNotEmpty) 'search': search,
|
||||
});
|
||||
return ReservationListResponse.fromJson(res.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<Reservation> getReservation(String id) async {
|
||||
final res = await _client.get('/reservations/$id');
|
||||
return Reservation.fromJson(res.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<Reservation> createReservation(Map<String, dynamic> data) async {
|
||||
final res = await _client.post('/reservations', data: data);
|
||||
return Reservation.fromJson(res.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<void> confirmReservation(String id) async {
|
||||
await _client.post('/reservations/$id/confirm');
|
||||
}
|
||||
|
||||
Future<void> cancelReservation(String id, String reason) async {
|
||||
await _client.post('/reservations/$id/cancel', data: {'reason': reason});
|
||||
}
|
||||
|
||||
Future<void> checkin(String id, int mileage) async {
|
||||
await _client.post('/reservations/$id/checkin', data: {'mileage': mileage});
|
||||
}
|
||||
|
||||
Future<void> checkout(String id, int mileage) async {
|
||||
await _client.post('/reservations/$id/checkout', data: {'mileage': mileage});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import '../models/vehicle.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
class VehicleService {
|
||||
final _client = ApiClient.instance;
|
||||
|
||||
Future<VehicleListResponse> getVehicles({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
String? status,
|
||||
String? search,
|
||||
}) async {
|
||||
final res = await _client.get('/vehicles', params: {
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
if (status != null) 'status': status,
|
||||
if (search != null && search.isNotEmpty) 'search': search,
|
||||
});
|
||||
return VehicleListResponse.fromJson(res.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<Vehicle> getVehicle(String id) async {
|
||||
final res = await _client.get('/vehicles/$id');
|
||||
return Vehicle.fromJson(res.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<Vehicle> createVehicle(Map<String, dynamic> data) async {
|
||||
final res = await _client.post('/vehicles', data: data);
|
||||
return Vehicle.fromJson(res.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<Vehicle> updateVehicle(String id, Map<String, dynamic> data) async {
|
||||
final res = await _client.patch('/vehicles/$id', data: data);
|
||||
return Vehicle.fromJson(res.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<void> togglePublish(String id, bool publish) async {
|
||||
await _client.patch('/vehicles/$id/publish', data: {'isPublished': publish});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
|
||||
class TokenStorage {
|
||||
static const _storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
|
||||
static Future<void> saveToken(String token) =>
|
||||
_storage.write(key: AppConstants.tokenKey, value: token);
|
||||
|
||||
static Future<String?> getToken() =>
|
||||
_storage.read(key: AppConstants.tokenKey);
|
||||
|
||||
static Future<void> saveUserType(String type) =>
|
||||
_storage.write(key: AppConstants.userTypeKey, value: type);
|
||||
|
||||
static Future<String?> getUserType() =>
|
||||
_storage.read(key: AppConstants.userTypeKey);
|
||||
|
||||
static Future<void> clear() => _storage.deleteAll();
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const _blue = Color(0xFF1A56DB);
|
||||
const _orange = Color(0xFFFF6B00);
|
||||
|
||||
class AppTheme {
|
||||
static ThemeData get light => ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: _blue,
|
||||
onPrimary: Colors.white,
|
||||
secondary: _orange,
|
||||
onSecondary: Colors.white,
|
||||
surface: Color(0xFFF4F5F7),
|
||||
surfaceContainerHigh: Color(0xFFFFFFFF),
|
||||
surfaceContainerHighest: Color(0xFFEFF0F2),
|
||||
onSurface: Color(0xFF111928),
|
||||
onSurfaceVariant: Color(0xFF6B7280),
|
||||
outline: Color(0xFFD1D5DB),
|
||||
outlineVariant: Color(0xFFE5E7EB),
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xFFF4F5F7),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Color(0xFF111928),
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: const BorderSide(color: Color(0xFFE5E7EB)),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D5DB)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D5DB)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(color: _blue, width: 2),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _blue,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
textButtonTheme:
|
||||
TextButtonThemeData(style: TextButton.styleFrom(foregroundColor: _blue)),
|
||||
chipTheme: ChipThemeData(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: Colors.white,
|
||||
indicatorColor: _orange.withValues(alpha: 0.12),
|
||||
iconTheme: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return const IconThemeData(color: _orange);
|
||||
}
|
||||
return const IconThemeData(color: Color(0xFF9CA3AF));
|
||||
}),
|
||||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return const TextStyle(
|
||||
color: _orange, fontWeight: FontWeight.w600, fontSize: 12);
|
||||
}
|
||||
return const TextStyle(color: Color(0xFF9CA3AF), fontSize: 12);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
static ThemeData get dark => ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: _blue,
|
||||
onPrimary: Colors.white,
|
||||
secondary: _orange,
|
||||
onSecondary: Colors.white,
|
||||
surface: Color(0xFF1C1C28),
|
||||
surfaceContainerHigh: Color(0xFF28293A),
|
||||
surfaceContainerHighest: Color(0xFF32334A),
|
||||
onSurface: Colors.white,
|
||||
onSurfaceVariant: Color(0xFF9CA3AF),
|
||||
outline: Color(0xFF4B4C63),
|
||||
outlineVariant: Color(0xFF3A3B52),
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xFF1C1C28),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color(0xFF1C1C28),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: const Color(0xFF28293A),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: const BorderSide(color: Color(0xFF3A3B52)),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF28293A),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(color: Color(0xFF3A3B52)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(color: Color(0xFF3A3B52)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(color: _blue, width: 2),
|
||||
),
|
||||
hintStyle: const TextStyle(color: Color(0xFF9CA3AF)),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _blue,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
textButtonTheme:
|
||||
TextButtonThemeData(style: TextButton.styleFrom(foregroundColor: _blue)),
|
||||
chipTheme: ChipThemeData(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: const Color(0xFF1E1F2E),
|
||||
indicatorColor: _orange.withValues(alpha: 0.18),
|
||||
iconTheme: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return const IconThemeData(color: _orange);
|
||||
}
|
||||
return const IconThemeData(color: Color(0xFF9CA3AF));
|
||||
}),
|
||||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return const TextStyle(
|
||||
color: _orange, fontWeight: FontWeight.w600, fontSize: 12);
|
||||
}
|
||||
return const TextStyle(color: Color(0xFF9CA3AF), fontSize: 12);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user