diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index e084253..2467841 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,40 +1,55 @@ +import java.util.Properties + plugins { id("com.android.application") id("kotlin-android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") } +val keystoreProperties = Properties().apply { + val keystorePropertiesFile = rootProject.file("key.properties") + if (keystorePropertiesFile.exists()) { + keystorePropertiesFile.inputStream().use { load(it) } + } +} + android { namespace = "com.example.glowwheels" - compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + compileSdk = 34 + ndkVersion = "25.1.8937393" // Optional, or remove if not used compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 + isCoreLibraryDesugaringEnabled = true } kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = "11" } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "com.example.glowwheels" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion - versionCode = flutter.versionCode - versionName = flutter.versionName + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "1.0.0" + } + + signingConfigs { + create("release") { + storeFile = File(keystoreProperties["storeFile"] as String) + storePassword = keystoreProperties["storePassword"] as String + keyAlias = keystoreProperties["keyAlias"] as String + keyPassword = keystoreProperties["keyPassword"] as String + } } buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") + signingConfig = signingConfigs.getByName("release") + isMinifyEnabled = false + isShrinkResources = false } } } @@ -42,3 +57,7 @@ android { flutter { source = "../.." } + +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ae45c8c..769de7b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + '$baseUrl/shop/$shopId'; + + // Orders + static String ordersByShop(String shopId) => '$baseUrl/order/shop/$shopId'; + static String assignServiceBoy(String orderId) => '$baseUrl/order/assign-serviceboy/$orderId'; + static String updateOrderStatusByAdmin(String orderId) => '$baseUrl/order/update-status-admin/$orderId'; + + // Service Boy + static String serviceBoyDetails(String serviceBoyId) => '$baseUrl/service-boy/$serviceBoyId'; + static String serviceBoysByShop(String shopId) => '$baseUrl/service-boy/shop/$shopId'; + static String addServiceBoy = '$baseUrl/service-boy/add'; +} diff --git a/lib/helpers/shopid_helper.dart b/lib/helpers/shopid_helper.dart new file mode 100644 index 0000000..930027e --- /dev/null +++ b/lib/helpers/shopid_helper.dart @@ -0,0 +1,8 @@ +import 'package:flutter/widgets.dart'; +import 'package:glowwheels/provider/shop_provider.dart'; +import 'package:provider/provider.dart'; + + +String? getShopId(BuildContext context) { + return Provider.of(context, listen: false).shop?.user.id; +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 2a305e3..9028ff3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,10 +5,10 @@ import 'package:flutter/material.dart'; import 'package:glowwheels/models/shop_model.dart'; import 'package:glowwheels/provider/order_provider.dart'; import 'package:glowwheels/provider/serviceboy_provider.dart'; +import 'package:glowwheels/provider/shop_profile_provider.dart'; import 'package:glowwheels/provider/shop_provider.dart'; -import 'package:glowwheels/screens/login_screen.dart'; -import 'package:glowwheels/screens/main_screen.dart'; -import 'package:glowwheels/screens/order_screen.dart'; +import 'package:glowwheels/screens/splash_screen.dart'; + import 'package:google_fonts/google_fonts.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:provider/provider.dart'; @@ -17,13 +17,20 @@ void main() async { await Hive.initFlutter(); Hive.registerAdapter(ShopModelAdapter()); + Hive.registerAdapter(ShopDetailsAdapter()); + + if (!Hive.isBoxOpen('shopbox')) { + await Hive.openBox('shopbox'); + } + runApp( MultiProvider( providers: [ - ChangeNotifierProvider(create: (_) => OrderProvider()), + ChangeNotifierProvider(create: (_) => OrdersProvider()), ChangeNotifierProvider(create: (_) => ServiceBoyProvider()), ChangeNotifierProvider(create: (_) => ShopProvider()), + ChangeNotifierProvider(create: (_) => ShopProfileProvider()), ], child: GlowWheelsApp(), ), @@ -35,7 +42,7 @@ class GlowWheelsApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, - home: LoginScreen(), + home: SplashDecider(), ); } } diff --git a/lib/models/order_model.dart b/lib/models/order_model.dart index b9f0ec9..b5237fd 100644 --- a/lib/models/order_model.dart +++ b/lib/models/order_model.dart @@ -1,31 +1,229 @@ - - import 'package:glowwheels/models/serviceboy_model.dart'; class Order { - final String customerName; - final String mobileNumber; - final String serviceType; - final String service; - final String price; - final String time; + final String id; + final User user; + final Shop shop; + final Service service; + final String? address; final String date; - final String carName; + final String timeSlot; final String status; - final String imagePath; - ServiceBoy? assignedBoy; + final String? serviceBoyId; + final DateTime createdAt; + final DateTime updatedAt; + final ServiceBoy? assignedServiceBoy; Order({ - required this.customerName, - required this.mobileNumber, - required this.serviceType, + required this.id, + required this.user, + required this.shop, required this.service, - required this.price, - required this.time, + required this.address, required this.date, - required this.carName, + required this.timeSlot, required this.status, - required this.imagePath, - this.assignedBoy, + required this.serviceBoyId, + required this.createdAt, + required this.updatedAt, + this.assignedServiceBoy, }); + + factory Order.fromJson(Map json) { + return Order( + id: json['_id'], + user: User.fromJson(json['userId']), + shop: Shop.fromJson(json['shopId']), + service: Service.fromJson(json['serviceId']), + address : json['address'], + date: json['date'], + timeSlot: json['timeSlot'], + status: json['status'], + serviceBoyId: json['serviceBoyId'], + createdAt: DateTime.parse(json['createdAt']), + updatedAt: DateTime.parse(json['updatedAt']), + assignedServiceBoy: json['assignedServiceBoy'] != null + ? ServiceBoy.fromJson(json['assignedServiceBoy']) + : null, + ); + } + + Order copyWith({ + String? id, + User? user, + Shop? shop, + Service? service, + String? date, + String? timeSlot, + String? status, + String? serviceBoyId, + DateTime? createdAt, + DateTime? updatedAt, + ServiceBoy? assignedServiceBoy, + }) { + return Order( + id: id ?? this.id, + user: user ?? this.user, + shop: shop ?? this.shop, + service: service ?? this.service, + date: date ?? this.date, + timeSlot: timeSlot ?? this.timeSlot, + status: status ?? this.status, + serviceBoyId: serviceBoyId ?? this.serviceBoyId, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + assignedServiceBoy: assignedServiceBoy ?? this.assignedServiceBoy, address: '', + ); + } } + +class User { + final String id; + final String name; + final int phone; + + User({ + required this.id, + required this.name, + required this.phone, + }); + + factory User.fromJson(Map json) { + return User( + id: json['_id'], + name: json['name'], + phone: json['phone'], + ); + } +} + +class Shop { + final String id; + final String name; + final String address; + final double latitude; + final double longitude; + final String phone; + final String email; + final List images; + final int rating; + + Shop({ + required this.id, + required this.name, + required this.address, + required this.latitude, + required this.longitude, + required this.phone, + required this.email, + required this.images, + required this.rating, + }); + + factory Shop.fromJson(Map json) { + return Shop( + id: json['_id'], + name: json['name'], + address: json['address'], + latitude: json['latitude'].toDouble(), + longitude: json['longitude'].toDouble(), + phone: json['phone'], + email: json['email'], + images: List.from(json['images']), + rating: json['rating'], + ); + } +} + +class Service { + final String id; + final Vehicle vehicle; + final Manufacture manufacture; + final Model model; + final String serviceName; + final int price; + final String serviceType; + + Service({ + required this.id, + required this.vehicle, + required this.manufacture, + required this.model, + required this.serviceName, + required this.price, + required this.serviceType, + }); + + factory Service.fromJson(Map json) { + return Service( + id: json['_id'], + vehicle: Vehicle.fromJson(json['vehicleId']), + manufacture: Manufacture.fromJson(json['manufactureId']), + model: Model.fromJson(json['modelId']), + serviceName: json['serviceName'], + price: json['price'], + serviceType: json['serviceType'], + ); + } +} + +class Vehicle { + final String id; + final String vehicle; + final String image; + + Vehicle({ + required this.id, + required this.vehicle, + required this.image, + }); + + factory Vehicle.fromJson(Map json) { + return Vehicle( + id: json['_id'], + vehicle: json['vehicle'], + image: json['image'], + ); + } +} + +class Manufacture { + final String id; + final String manufacture; + final String image; + + Manufacture({ + required this.id, + required this.manufacture, + required this.image, + }); + + factory Manufacture.fromJson(Map json) { + return Manufacture( + id: json['_id'], + manufacture: json['manufacture'], + image: json['image'], + ); + } +} + +class Model { + final String id; + final String model; + final String image; + + Model({ + required this.id, + required this.model, + required this.image, + }); + + factory Model.fromJson(Map json) { + return Model( + id: json['_id'], + model: json['model'], + image: json['image'], + ); + } +} + diff --git a/lib/models/service_boy_response.dart b/lib/models/service_boy_response.dart new file mode 100644 index 0000000..b1a908e --- /dev/null +++ b/lib/models/service_boy_response.dart @@ -0,0 +1,42 @@ + + +import 'package:glowwheels/models/service_boy_response.dart'; +import 'package:glowwheels/models/serviceboy_model.dart'; + +class ServiceBoyResponse { + final String id; + final String shopId; + final List serviceBoy; + final String createdAt; + final String updatedAt; + + ServiceBoyResponse({ + required this.id, + required this.shopId, + required this.serviceBoy, + required this.createdAt, + required this.updatedAt, + }); + + factory ServiceBoyResponse.fromJson(Map json) { + return ServiceBoyResponse( + id: json['_id'], + shopId: json['shopId'], + serviceBoy: (json['serviceBoy'] as List) + .map((e) => ServiceBoy.fromJson(e)) + .toList(), + createdAt: json['createdAt'], + updatedAt: json['updatedAt'], + ); + } + + Map toJson() { + return { + '_id': id, + 'shopId': shopId, + 'serviceBoy': serviceBoy.map((e) => e.toJson()).toList(), + 'createdAt': createdAt, + 'updatedAt': updatedAt, + }; + } +} diff --git a/lib/models/serviceboy_model.dart b/lib/models/serviceboy_model.dart index 782370c..ccab4e7 100644 --- a/lib/models/serviceboy_model.dart +++ b/lib/models/serviceboy_model.dart @@ -1,6 +1,27 @@ class ServiceBoy { + final String id; final String name; final String phone; - ServiceBoy({required this.name, required this.phone}); + ServiceBoy({ + required this.id, + required this.name, + required this.phone, + }); + + factory ServiceBoy.fromJson(Map json) { + return ServiceBoy( + id: json['_id'], + name: json['name'], + phone: json['phone'], + ); + } + + Map toJson() { + return { + '_id': id, + 'name': name, + 'phone': phone, + }; + } } \ No newline at end of file diff --git a/lib/models/shop_model.dart b/lib/models/shop_model.dart index aedc0ab..32c354b 100644 --- a/lib/models/shop_model.dart +++ b/lib/models/shop_model.dart @@ -2,52 +2,74 @@ import 'package:hive/hive.dart'; part 'shop_model.g.dart'; -@HiveType(typeId: 1) +@HiveType(typeId: 0) class ShopModel { @HiveField(0) - final String id; + bool success; @HiveField(1) - final String image; + ShopDetails user; @HiveField(2) - final String mobile; + String token; @HiveField(3) - final String email; - - @HiveField(4) - final String shopName; - - @HiveField(5) - final String address; // ✅ NEW + String message; ShopModel({ - required this.id, - required this.image, - required this.mobile, - required this.email, - required this.shopName, - required this.address, + required this.success, + required this.user, + required this.token, + required this.message, }); - factory ShopModel.fromJson(Map json) { - return ShopModel( - id: json['id'] ?? '', - image: json['image'] ?? '', - mobile: json['mobile'] ?? '', - email: json['email'] ?? '', - shopName: json['shopName'] ?? '', - address: json['address'] ?? '', // ✅ NEW - ); - } + factory ShopModel.fromJson(Map json) => ShopModel( + success: json['success'], + user: ShopDetails.fromJson(json['user']), + token: json['token'], + message: json['message'], + ); + + Map toJson() => { + 'success': success, + 'user': user.toJson(), + 'token': token, + 'message': message, + }; +} + +@HiveType(typeId: 1) +class ShopDetails { + @HiveField(0) + String id; + + @HiveField(1) + String name; + + @HiveField(2) + String email; + + @HiveField(3) + String role; + + ShopDetails({ + required this.id, + required this.name, + required this.email, + required this.role, + }); + + factory ShopDetails.fromJson(Map json) => ShopDetails( + id: json['id'], + name: json['name'], + email: json['email'], + role: json['role'], + ); Map toJson() => { 'id': id, - 'image': image, - 'mobile': mobile, + 'name': name, 'email': email, - 'shopName': shopName, - 'address': address, // ✅ NEW + 'role': role, }; } diff --git a/lib/models/shop_model.g.dart b/lib/models/shop_model.g.dart index 2e05e0c..c524271 100644 --- a/lib/models/shop_model.g.dart +++ b/lib/models/shop_model.g.dart @@ -8,7 +8,7 @@ part of 'shop_model.dart'; class ShopModelAdapter extends TypeAdapter { @override - final int typeId = 1; + final int typeId = 0; @override ShopModel read(BinaryReader reader) { @@ -17,31 +17,25 @@ class ShopModelAdapter extends TypeAdapter { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return ShopModel( - id: fields[0] as String, - image: fields[1] as String, - mobile: fields[2] as String, - email: fields[3] as String, - shopName: fields[4] as String, - address: fields[5] as String, + success: fields[0] as bool, + user: fields[1] as ShopDetails, + token: fields[2] as String, + message: fields[3] as String, ); } @override void write(BinaryWriter writer, ShopModel obj) { writer - ..writeByte(6) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.image) - ..writeByte(2) - ..write(obj.mobile) - ..writeByte(3) - ..write(obj.email) ..writeByte(4) - ..write(obj.shopName) - ..writeByte(5) - ..write(obj.address); + ..writeByte(0) + ..write(obj.success) + ..writeByte(1) + ..write(obj.user) + ..writeByte(2) + ..write(obj.token) + ..writeByte(3) + ..write(obj.message); } @override @@ -54,3 +48,46 @@ class ShopModelAdapter extends TypeAdapter { runtimeType == other.runtimeType && typeId == other.typeId; } + +class ShopDetailsAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + ShopDetails read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ShopDetails( + id: fields[0] as String, + name: fields[1] as String, + email: fields[2] as String, + role: fields[3] as String, + ); + } + + @override + void write(BinaryWriter writer, ShopDetails obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.name) + ..writeByte(2) + ..write(obj.email) + ..writeByte(3) + ..write(obj.role); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ShopDetailsAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/models/shop_profile_model.dart b/lib/models/shop_profile_model.dart new file mode 100644 index 0000000..fd7131c --- /dev/null +++ b/lib/models/shop_profile_model.dart @@ -0,0 +1,67 @@ +class ShopProfileModel { + final String id; + final String name; + final String description; + final String address; + final double latitude; + final double longitude; + final String phone; + final String email; + final String password; + final String role; + final List images; + final int rating; + final DateTime timestamp; + + ShopProfileModel({ + required this.id, + required this.name, + required this.description, + required this.address, + required this.latitude, + required this.longitude, + required this.phone, + required this.email, + required this.password, + required this.role, + required this.images, + required this.rating, + required this.timestamp, + }); + + factory ShopProfileModel.fromJson(Map json) { + return ShopProfileModel( + id: json['_id'], + name: json['name'], + description: json['description'], + address: json['address'], + latitude: json['latitude'].toDouble(), + longitude: json['longitude'].toDouble(), + phone: json['phone'], + email: json['email'], + password: json['password'], + role: json['role'], + images: List.from(json['images']), + rating: json['rating'], + timestamp: DateTime.parse(json['timestamp']), + ); + } + + Map toJson() { + return { + '_id': id, + 'name': name, + 'description': description, + 'address': address, + 'latitude': latitude, + 'longitude': longitude, + 'phone': phone, + 'email': email, + 'password': password, + 'role': role, + 'images': images, + 'rating': rating, + 'timestamp': timestamp.toIso8601String(), + }; + } +} diff --git a/lib/provider/order_provider.dart b/lib/provider/order_provider.dart index 15112cb..aae57cf 100644 --- a/lib/provider/order_provider.dart +++ b/lib/provider/order_provider.dart @@ -1,41 +1,103 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:glowwheels/constants/api.dart'; +import 'package:glowwheels/models/order_model.dart'; +import 'package:http/http.dart' as http; -import '../models/order_model.dart'; -import '../models/serviceboy_model.dart'; - -class OrderProvider with ChangeNotifier { - final List _orders = [ - Order( - customerName: "Ankit Ghosh", - mobileNumber: "8617015476", - serviceType: "Doorstep Service", - service: "Foam Wash", - price: "₹ 104", - time: "10:00 - 11:00 AM", - date: "2025-05-28", - carName: "Mahindra XUV 700", - status: "Confirmed", - imagePath: "assets/images/car.jpg", - ), - Order( - customerName: "Ravi Kumar", - mobileNumber: "9876543210", - serviceType: "Workshop", - service: "Interior Cleaning", - price: "₹ 150", - time: "12:00 - 1:00 PM", - date: "2025-05-29", - carName: "Hyundai Creta", - status: "Pending", - imagePath: "assets/images/bike.png", - ), - // Add more sample orders if needed - ]; +class OrdersProvider with ChangeNotifier { + List _orders = []; + bool _isLoading = false; + bool _isRefreshing = false; + String? _errorMessage; List get orders => _orders; + bool get isLoading => _isLoading; + bool get isRefreshing => _isRefreshing; + String? get errorMessage => _errorMessage; - void assignServiceBoy(int index, ServiceBoy boy) { - _orders[index].assignedBoy = boy; + Future fetchOrders(String shopId, {bool refresh = false}) async { + final ordersUri = Uri.parse(ApiConstants.ordersByShop(shopId)); + + if (refresh) { + _isRefreshing = true; + } else { + _isLoading = true; + } + notifyListeners(); + + try { + final response = await http.get(ordersUri); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + + if (data['success'] == true) { + final List ordersJson = data['orders']; + _orders = ordersJson.map((orderJson) => Order.fromJson(orderJson)).toList(); + _errorMessage = null; + } else { + _errorMessage = 'Failed to fetch orders'; + } + } else { + _errorMessage = 'Server error: ${response.statusCode}'; + } + } catch (e) { + _errorMessage = 'An error occurred: $e'; + } + + _isLoading = false; + _isRefreshing = false; notifyListeners(); } + + Future assignServiceBoyToOrder({ + required String orderId, + required String serviceBoyId, + required String shopId, + }) async { + final assignUri = Uri.parse(ApiConstants.assignServiceBoy(orderId)); + + try { + final response = await http.put( + assignUri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'serviceBoyId': serviceBoyId}), + ); + + if (response.statusCode == 200) { + print('Successfully assigned service boy'); + } else { + print('Failed to assign service boy: ${response.statusCode}'); + } + } catch (e) { + print('Error assigning service boy: $e'); + } + } + + Future updateOrderStatus({ + required String orderId, + required String status, + }) async { + final updateStatusUri = Uri.parse(ApiConstants.updateOrderStatusByAdmin(orderId)); + + try { + final response = await http.put( + updateStatusUri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'status': status}), + ); + + if (response.statusCode == 200) { + final index = _orders.indexWhere((o) => o.id == orderId); + if (index != -1) { + _orders[index] = _orders[index].copyWith(status: status); + notifyListeners(); + } + } else { + print('Failed to update status: ${response.statusCode}'); + } + } catch (e) { + print('Error updating order status: $e'); + } + } } diff --git a/lib/provider/serviceboy_provider.dart b/lib/provider/serviceboy_provider.dart index 86269bb..27dd930 100644 --- a/lib/provider/serviceboy_provider.dart +++ b/lib/provider/serviceboy_provider.dart @@ -1,41 +1,152 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; -import '../models/serviceboy_model.dart'; - -class ServiceBoyProvider extends ChangeNotifier { - List _serviceBoys = [ - ServiceBoy(name: 'John Doe', phone: '9875643210'), - ServiceBoy(name: 'Amit Raj', phone: '9765432180'), - ServiceBoy(name: 'Manoj Sinha', phone: '9543219876'), - ]; +import 'package:glowwheels/constants/api.dart'; +import 'package:http/http.dart' as http; +import 'package:glowwheels/models/serviceboy_model.dart'; +class ServiceBoyProvider with ChangeNotifier { + List _serviceBoys = []; ServiceBoy? _selectedBoy; + bool _isLoading = false; List get serviceBoys => _serviceBoys; ServiceBoy? get selectedBoy => _selectedBoy; + bool get isLoading => _isLoading; - // Add a new service boy - void addServiceBoy(ServiceBoy boy) { - _serviceBoys.add(boy); + /// Fetch service boys by shop ID + Future fetchServiceBoys(String shopId) async { + _isLoading = true; + notifyListeners(); + final serviceBoyListUri = Uri.parse(ApiConstants.serviceBoysByShop(shopId)); + + try { + final response = await http.get(serviceBoyListUri); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + final List boysJson = data['data']['serviceBoy']; + _serviceBoys = boysJson.map((json) => ServiceBoy.fromJson(json)).toList(); + print('fetched service boys'); + print (_serviceBoys); + notifyListeners(); + } else { + print('Failed to fetch service boys: ${response.statusCode}'); + } + } catch (e) { + print('Error fetching service boys: $e'); + } + _isLoading = false; notifyListeners(); } - // Edit an existing service boy - void editServiceBoy(int index, ServiceBoy updatedBoy) { - if (index >= 0 && index < _serviceBoys.length) { - _serviceBoys[index] = updatedBoy; - notifyListeners(); + /// Fetch a single service boy by ID + Future fetchServiceBoyById(String serviceBoyId) async { + final serviceBoyUri = Uri.parse(ApiConstants.serviceBoyDetails(serviceBoyId)); + + try { + final response = await http.get(serviceBoyUri); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + final boyJson = data['data']; // Adjust if API wraps in `data` + final serviceBoy = ServiceBoy.fromJson(boyJson); + return serviceBoy; + } else { + print('Failed to fetch service boy: ${response.statusCode}'); + } + } catch (e) { + print('Error fetching service boy: $e'); + } + return null; + } + + /// Add new service boy to shop + Future addServiceBoy(String shopId, String name,String phone) async { + print('add api called'); + print(name+'Phone '+phone.toString()); + final addServiceBoyUri = Uri.parse(ApiConstants.addServiceBoy); + final body = jsonEncode({ + 'shopId': shopId, + 'serviceBoy': [ + { + 'name': name, + 'phone':phone, + } + ] + }); + + try { + final response = await http.post( + addServiceBoyUri, + headers: {'Content-Type': 'application/json'}, + body: body, + ); + print('add service boy response '+response.statusCode.toString()); + if (response.statusCode == 200) { + await fetchServiceBoys(shopId); // refresh list + } else { + print('Failed to add service boy: ${response.statusCode}'); + } + } catch (e) { + print('Error adding service boy: $e'); } } - // Delete a service boy - void deleteServiceBoy(int index) { - if (index >= 0 && index < _serviceBoys.length) { - _serviceBoys.removeAt(index); - notifyListeners(); + /// Edit service boy phone + Future editServiceBoy( + String serviceBoyId, + String updatedName, + String updatedPhone, + String shopId, + ) async { + final serviceBoyUri = Uri.parse(ApiConstants.serviceBoyDetails(serviceBoyId)); + + // Build request body only with non-empty fields + final Map data = {}; + if (updatedName.isNotEmpty) data['name'] = updatedName; + if (updatedPhone.isNotEmpty) data['phone'] = updatedPhone; + + if (data.isEmpty) { + print('No fields to update.'); + return; + } + + try { + final response = await http.put( + serviceBoyUri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(data), + ); + + if (response.statusCode == 200) { + print('Service boy updated successfully'); + await fetchServiceBoys(shopId); // Refresh list + } else { + print('Failed to update service boy: ${response.statusCode}'); + print('Response body: ${response.body}'); + } + } catch (e) { + print('Error updating service boy: $e'); } } - // Assign a selected service boy (for dialogs) + + /// Delete a service boy + Future deleteServiceBoy(String serviceBoyId, String shopId) async { + final serviceBoyUri = Uri.parse(ApiConstants.serviceBoyDetails(serviceBoyId)); + + try { + final response = await http.delete(serviceBoyUri); + + if (response.statusCode == 200) { + await fetchServiceBoys(shopId); // refresh list + } else { + print('Failed to delete service boy: ${response.statusCode}'); + } + } catch (e) { + print('Error deleting service boy: $e'); + } + } + + // Select a service boy void selectBoy(ServiceBoy boy) { _selectedBoy = boy; notifyListeners(); diff --git a/lib/provider/shop_profile_provider.dart b/lib/provider/shop_profile_provider.dart new file mode 100644 index 0000000..678cda0 --- /dev/null +++ b/lib/provider/shop_profile_provider.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:glowwheels/constants/api.dart'; +import 'package:glowwheels/models/shop_profile_model.dart'; +import 'package:http/http.dart' as http; + + +class ShopProfileProvider with ChangeNotifier { + ShopProfileModel? _shopProfile; + bool _isLoading = false; + String? _errorMessage; + + ShopProfileModel? get shopProfile => _shopProfile; + bool get isLoading => _isLoading; + String? get errorMessage => _errorMessage; + + Future fetchShopProfile(String shopId) async { + _isLoading = true; + _errorMessage = null; + notifyListeners(); + + final shopUri = Uri.parse(ApiConstants.shopDetails(shopId)); + + try { + final response = await http.get(shopUri); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + _shopProfile = ShopProfileModel.fromJson(data); + } else { + _errorMessage = 'Failed to load shop profile. Status code: ${response.statusCode}'; + } + } catch (e) { + _errorMessage = 'An error occurred: $e'; + } finally { + _isLoading = false; + notifyListeners(); + } + } +} diff --git a/lib/provider/shop_provider.dart b/lib/provider/shop_provider.dart index 202e8c6..7626ee0 100644 --- a/lib/provider/shop_provider.dart +++ b/lib/provider/shop_provider.dart @@ -1,52 +1,98 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:glowwheels/constants/api.dart'; import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'package:provider/provider.dart'; import '../models/shop_model.dart'; class ShopProvider with ChangeNotifier { ShopModel? _shop; - ShopModel? get shop => _shop; + String? _token; - final String _boxName = 'shopBox'; + ShopModel? get shop => _shop; + String? get token => _token; + + final String _shopBox = 'shopBox'; + final String _tokenBox = 'tokenBox'; ShopProvider() { - _loadOrCreateDummyShop(); + _loadShopAndTokenFromHive(); } - void _loadOrCreateDummyShop() async { - final box = await Hive.openBox(_boxName); - if (box.isNotEmpty) { - _shop = box.getAt(0); - } else { - // Dummy data - _shop = ShopModel( - id: '1', - shopName: "Omkara Car Wash Center", - email: "omkara@gmail.com", - mobile: "8617019854", - image: "assets/images/shop_image.jpg", - address: "Bidhannagar, Kolkata, pin-700017", + void _loadShopAndTokenFromHive() async { + final shopBox = await Hive.openBox(_shopBox); + _shop = shopBox.get('shop'); + + final tokenBox = await Hive.openBox(_tokenBox); + _token = tokenBox.get('token'); + + notifyListeners(); + } + String? getShopId(BuildContext context) { + return Provider.of(context, listen: false).shop?.user.id; + } + + Future login(String email, String password) async { + final loginUri = Uri.parse(ApiConstants.loginUrl); + + try { + final response = await http.post( + loginUri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'email': email, 'password': password}), ); - await box.add(_shop!); + + print('Response code: ${response.statusCode}'); + print('Response body: ${response.body}'); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + + final shopModel = ShopModel.fromJson(data); + + final shopBox = await Hive.openBox(_shopBox); + final tokenBox = await Hive.openBox(_tokenBox); + + await shopBox.put('shop', shopModel); + await tokenBox.put('token', shopModel.token); + + _shop = shopModel; + _token = shopModel.token; + + notifyListeners(); + return true; + } else { + print('HTTP Error: ${response.statusCode}'); + return false; + } + } catch (e) { + print('Login Exception: $e'); + return false; } - - notifyListeners(); } - Future setShop(ShopModel shop) async { - _shop = shop; - notifyListeners(); - - final box = await Hive.openBox(_boxName); - await box.clear(); // Keep only one shop - await box.add(shop); - } Future logout() async { _shop = null; + _token = null; notifyListeners(); - final box = await Hive.openBox(_boxName); - await box.clear(); + final shopBox = await Hive.openBox(_shopBox); + await shopBox.clear(); + + final tokenBox = await Hive.openBox(_tokenBox); + await tokenBox.clear(); + } + + void setShop(ShopModel shop) { + _shop = shop; + notifyListeners(); + } + + void setToken(String token) { + _token = token; + notifyListeners(); } } diff --git a/lib/screens/Serviceboy_screen.dart b/lib/screens/Serviceboy_screen.dart index ce284a7..214f6fa 100644 --- a/lib/screens/Serviceboy_screen.dart +++ b/lib/screens/Serviceboy_screen.dart @@ -1,10 +1,31 @@ import 'package:flutter/material.dart'; +import 'package:glowwheels/helpers/shopid_helper.dart'; import 'package:provider/provider.dart'; import '../provider/serviceboy_provider.dart'; import 'add_serviceboy_screen.dart'; import 'edit_serviceboy_screen.dart'; import 'package:google_fonts/google_fonts.dart'; -class ServiceBoyScreen extends StatelessWidget { + +class ServiceBoyScreen extends StatefulWidget { + @override + State createState() => _ServiceBoyScreenState(); +} +late String shopId=''; +class _ServiceBoyScreenState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + shopId = getShopId(context)!; + if (shopId != null) { + Provider.of(context, listen: false) + .fetchServiceBoys(shopId); + } else { + print("Shop ID is null"); + } + }); + } + @override Widget build(BuildContext context) { final provider = Provider.of(context); @@ -13,7 +34,6 @@ class ServiceBoyScreen extends StatelessWidget { decoration: BoxDecoration( gradient: LinearGradient( colors: [Color.fromRGBO(208, 235, 255, 1), Colors.white], - begin: Alignment.topCenter, end: Alignment.bottomCenter, ), @@ -26,54 +46,64 @@ class ServiceBoyScreen extends StatelessWidget { SizedBox(height: 40), Text( 'GLOWWHEELS', - style: GoogleFonts.inter( - fontSize: 32,color: Color.fromRGBO(25, 25, 112, 0.87) - ,fontWeight: FontWeight.w700 - ) + style: GoogleFonts.inter( + fontSize: 32, + color: Color.fromRGBO(25, 25, 112, 0.87), + fontWeight: FontWeight.w700, + ), ), Text( 'Service Center', - style: GoogleFonts.inter( - fontSize: 24,color: Color.fromRGBO(25, 25, 112, 0.87) - ,fontWeight: FontWeight.w400 - ) + style: GoogleFonts.inter( + fontSize: 24, + color: Color.fromRGBO(25, 25, 112, 0.87), + fontWeight: FontWeight.w400, + ), ), SizedBox(height: 20), Text( 'Service Boy List', - style: GoogleFonts.inter( - fontSize: 16,color: Color.fromRGBO(33, 33, 33, 1) - ,fontWeight: FontWeight.w500 - ) + style: GoogleFonts.inter( + fontSize: 16, + color: Color.fromRGBO(33, 33, 33, 1), + fontWeight: FontWeight.w500, + ), ), SizedBox(height: 10), Expanded( - child: provider.serviceBoys.isEmpty + child: provider.isLoading + ? Center(child: CircularProgressIndicator()) + : provider.serviceBoys.isEmpty ? Center(child: Text("No service boys found")) : ListView.builder( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: EdgeInsets.symmetric( + horizontal: 16, vertical: 8), itemCount: provider.serviceBoys.length, itemBuilder: (context, index) { final boy = provider.serviceBoys[index]; return Card( color: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), elevation: 2, child: ListTile( - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + contentPadding: EdgeInsets.symmetric( + horizontal: 16, vertical: 8), title: Text( boy.name, - style: GoogleFonts.inter( - fontSize: 18,color: Color.fromRGBO(33, 33, 33, 0.78) - ,fontWeight: FontWeight.w600 - ) + style: GoogleFonts.inter( + fontSize: 18, + color: Color.fromRGBO(33, 33, 33, 0.78), + fontWeight: FontWeight.w600, + ), ), subtitle: Text( boy.phone, - style: GoogleFonts.inter( - fontSize: 16,color: Color.fromRGBO(33, 33, 33, 0.78) - ,fontWeight: FontWeight.w400 - ) + style: GoogleFonts.inter( + fontSize: 16, + color: Color.fromRGBO(33, 33, 33, 0.78), + fontWeight: FontWeight.w400, + ), ), trailing: Row( mainAxisSize: MainAxisSize.min, @@ -83,7 +113,9 @@ class ServiceBoyScreen extends StatelessWidget { onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (_) => EditServiceBoyScreen(serviceBoy: boy), + builder: (_) => + EditServiceBoyScreen( + serviceBoy: boy), ), ); }, @@ -92,7 +124,7 @@ class ServiceBoyScreen extends StatelessWidget { _buildIconButton( icon: Icons.delete, onPressed: () { - _showDeleteDialog(context, ''); + _showDeleteDialog(context, boy.id); }, ), ], @@ -109,7 +141,11 @@ class ServiceBoyScreen extends StatelessWidget { shape: CircleBorder(), backgroundColor: Colors.white, elevation: 4, - child: Icon(Icons.add, color: Color(0xFF1F1762),size: 25,), + child: Icon( + Icons.add, + color: Color(0xFF1F1762), + size: 25, + ), onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (_) => AddServiceBoyScreen()), @@ -120,7 +156,8 @@ class ServiceBoyScreen extends StatelessWidget { ); } - Widget _buildIconButton({required IconData icon, required VoidCallback onPressed}) { + Widget _buildIconButton( + {required IconData icon, required VoidCallback onPressed}) { return Container( height: 40, width: 40, @@ -141,63 +178,72 @@ class ServiceBoyScreen extends StatelessWidget { } void _showDeleteDialog(BuildContext context, String id) { + final shopId = getShopId(context); + showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, - builder: (_) => Container( - padding: EdgeInsets.all(24), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset('assets/images/delete_serviceboy.png'), - SizedBox(height: 16), - Text( - 'Delete Service boy?', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black87, + builder: (BuildContext bottomSheetContext) { + return Container( + padding: EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset('assets/images/delete_serviceboy.png'), + SizedBox(height: 16), + Text( + 'Delete Service boy?', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), ), - ), - SizedBox(height: 8), - Text( - 'Are you sure you want to delete this service boy? You won’t be able to undo this.', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Colors.black54, + SizedBox(height: 8), + Text( + 'Are you sure you want to delete this service boy? You won’t be able to undo this.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.black54, + ), ), - ), - SizedBox(height: 24), - SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red[700], - padding: EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red[700], + padding: EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () async { + if (shopId != null) { + await Provider.of( + bottomSheetContext, + listen: false, + ).deleteServiceBoy(id, shopId); + } + Navigator.pop(bottomSheetContext); // Correct context + }, + child: Text( + 'Confirm', + style: TextStyle(fontSize: 16, color: Colors.white), ), ), - onPressed: () { - // Provider.of(context, listen: false).deleteServiceBoy(id); - Navigator.pop(context); - }, - child: Text( - 'Confirm', - style: TextStyle(fontSize: 16, color: Colors.white), - ), ), - ), - SizedBox(height: 12), - ], - ), - ), + SizedBox(height: 12), + ], + ), + ); + }, ); } diff --git a/lib/screens/account_screen.dart b/lib/screens/account_screen.dart index 0fcc75f..11408cc 100644 --- a/lib/screens/account_screen.dart +++ b/lib/screens/account_screen.dart @@ -1,15 +1,41 @@ import 'package:flutter/material.dart'; -import 'package:glowwheels/screens/privacy_policy_screen.dart'; -import 'package:glowwheels/screens/profile_details_screen.dart'; +import 'package:glowwheels/helpers/shopid_helper.dart'; +import 'package:glowwheels/provider/shop_profile_provider.dart'; +import 'package:glowwheels/provider/shop_provider.dart'; +import 'package:glowwheels/screens/login_screen.dart'; +import 'package:provider/provider.dart'; -import 'package:glowwheels/screens/terms_condition_screen.dart'; import 'package:glowwheels/widgets/profile_header.dart'; -import 'package:google_fonts/google_fonts.dart'; -class AccountScreen extends StatelessWidget { +import 'package:glowwheels/screens/profile_details_screen.dart'; +import 'package:glowwheels/screens/privacy_policy_screen.dart'; +import 'package:glowwheels/screens/terms_condition_screen.dart'; + +class AccountScreen extends StatefulWidget { const AccountScreen({super.key}); + @override + State createState() => _AccountScreenState(); +} + +class _AccountScreenState extends State { + bool _isInit = true; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_isInit) { + // Fetch the shop profile once when screen loads + final shopId=getShopId(context); + Provider.of(context, listen: false).fetchShopProfile(shopId!); + _isInit = false; + } + } + @override Widget build(BuildContext context) { + final shopProfileProvider = Provider.of(context); + final shopProfile = shopProfileProvider.shopProfile; + return Scaffold( backgroundColor: Colors.white, appBar: AppBar( @@ -20,7 +46,6 @@ class AccountScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: const [ ImageIcon(AssetImage("assets/icon/account_icon.png")), - SizedBox(width: 8), Text( "Account", @@ -33,15 +58,17 @@ class AccountScreen extends StatelessWidget { padding: const EdgeInsets.all(16), child: Column( children: [ - // Profile Info - ProfileHeader(), + // Profile Info (shows shimmer skeleton if loading) + ProfileHeader(shopProfile: shopProfile), const SizedBox(height: 24), // Section Title Align( alignment: Alignment.centerLeft, - child: Text("Options", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + child: Text( + "Options", + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), ), const SizedBox(height: 12), @@ -51,7 +78,7 @@ class AccountScreen extends StatelessWidget { title: "Profile Details", onTap: () { Navigator.of(context).push( - MaterialPageRoute(builder: (_) => ServiceCenterDetailsScreen()), + MaterialPageRoute(builder: (_) => ServiceCenterDetailsScreen()), ); }, ), @@ -60,7 +87,7 @@ class AccountScreen extends StatelessWidget { title: "Terms and Conditions", onTap: () { Navigator.of(context).push( - MaterialPageRoute(builder: (_) => TermsOfServiceScreen()), + MaterialPageRoute(builder: (_) => TermsOfServiceScreen()), ); }, ), @@ -69,14 +96,21 @@ class AccountScreen extends StatelessWidget { title: "Privacy Policy", onTap: () { Navigator.of(context).push( - MaterialPageRoute(builder: (_) => PrivacyPolicyScreen()), + MaterialPageRoute(builder: (_) => PrivacyPolicyScreen()), ); }, ), OptionTile( icon: "assets/icon/logout_icon.png", title: "Log Out", - onTap: () {}, + onTap: () async{ + await Provider.of(context, listen: false).logout(); + + Navigator.of(context, rootNavigator: true).pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => LoginScreen()), + (route) => false, + ); + }, isDestructive: true, ), ], @@ -103,19 +137,20 @@ class OptionTile extends StatelessWidget { @override Widget build(BuildContext context) { return Card( - color: isDestructive ? Colors.white : Colors.white, + color: Colors.white, elevation: 0.5, margin: const EdgeInsets.symmetric(vertical: 6), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: ListTile( - leading: ImageIcon(AssetImage(icon), - color: Color.fromRGBO(25, 25, 112, 1)), - title: Text(title, - - style: GoogleFonts.inter( - fontSize: 14,color: Color.fromRGBO(41, 45, 50, 1) - ,fontWeight: FontWeight.w500 - ),), + leading: ImageIcon(AssetImage(icon), color: const Color.fromRGBO(25, 25, 112, 1)), + title: Text( + title, + style: const TextStyle( + fontSize: 14, + color: Color.fromRGBO(41, 45, 50, 1), + fontWeight: FontWeight.w500, + ), + ), trailing: const Icon(Icons.arrow_forward_ios, size: 16), onTap: onTap, ), diff --git a/lib/screens/add_serviceboy_screen.dart b/lib/screens/add_serviceboy_screen.dart index ea9ca63..6dd7924 100644 --- a/lib/screens/add_serviceboy_screen.dart +++ b/lib/screens/add_serviceboy_screen.dart @@ -1,16 +1,33 @@ import 'package:flutter/material.dart'; +import 'package:glowwheels/helpers/shopid_helper.dart'; import 'package:provider/provider.dart'; -import '../provider/serviceboy_provider.dart'; import 'package:google_fonts/google_fonts.dart'; -class AddServiceBoyScreen extends StatelessWidget { +import '../provider/serviceboy_provider.dart'; +import '../provider/shop_provider.dart'; + +class AddServiceBoyScreen extends StatefulWidget { + @override + _AddServiceBoyScreenState createState() => _AddServiceBoyScreenState(); +} + +class _AddServiceBoyScreenState extends State { final _formKey = GlobalKey(); final TextEditingController nameController = TextEditingController(); final TextEditingController phoneController = TextEditingController(); - + bool isLoading = false; + late String shopId=''; + @override + void initState() { + // TODO: implement initState + shopId = getShopId(context)!; + } @override Widget build(BuildContext context) { + final serviceBoyProvider = Provider.of(context, listen: false); + + return Scaffold( - backgroundColor: Color(0xFFF9FAF4), // light background as per screenshot + backgroundColor: Color(0xFFF9FAF4), appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, @@ -20,12 +37,12 @@ class AddServiceBoyScreen extends StatelessWidget { ), title: Text( 'Add Service boy', - style: GoogleFonts.nunito( - fontSize: 18,color: Color.fromRGBO(26, 26, 26, 1) - ,fontWeight: FontWeight.w500 - ) + style: GoogleFonts.nunito( + fontSize: 18, + color: Color.fromRGBO(26, 26, 26, 1), + fontWeight: FontWeight.w500, + ), ), - //centerTitle: true, ), body: Padding( padding: const EdgeInsets.all(16), @@ -33,15 +50,15 @@ class AddServiceBoyScreen extends StatelessWidget { key: _formKey, child: Column( children: [ - SizedBox(height: 30,), + SizedBox(height: 30), TextFormField( controller: nameController, decoration: InputDecoration( - hintText: 'Name', - hintStyle: GoogleFonts.nunito( - fontSize: 14,color: Color.fromRGBO(26, 26, 26, 1) - ,fontWeight: FontWeight.w400 + hintStyle: GoogleFonts.nunito( + fontSize: 14, + color: Color.fromRGBO(26, 26, 26, 1), + fontWeight: FontWeight.w400, ), border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14), @@ -54,9 +71,10 @@ class AddServiceBoyScreen extends StatelessWidget { keyboardType: TextInputType.phone, decoration: InputDecoration( hintText: 'Mobile Number', - hintStyle: GoogleFonts.nunito( - fontSize: 14,color: Color.fromRGBO(26, 26, 26, 1) - ,fontWeight: FontWeight.w400 + hintStyle: GoogleFonts.nunito( + fontSize: 14, + color: Color.fromRGBO(26, 26, 26, 1), + fontWeight: FontWeight.w400, ), border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14), @@ -67,28 +85,69 @@ class AddServiceBoyScreen extends StatelessWidget { SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () { - /* if (_formKey.currentState!.validate()) { - Provider.of(context, listen: false).addServiceBoy( - nameController.text, - phoneController.text, - ); - Navigator.pop(context); - }*/ + onPressed: isLoading + ? null + : () async { + if (_formKey.currentState!.validate()) { + if (shopId == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Shop ID is missing')), + ); + return; + } + + setState(() { + isLoading = true; + }); + + try { + await serviceBoyProvider.addServiceBoy( + shopId, + nameController.text.trim(), + phoneController.text.trim(), + ); + + // After successful addition, navigate back + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Service boy added successfully!')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to add service boy')), + ); + } finally { + if (mounted) { + setState(() { + isLoading = false; + }); + } + } + } }, style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF000B8C), // dark blue + backgroundColor: Color(0xFF000B8C), padding: EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), ), - child: Text('Submit', style: - - GoogleFonts.inter( - fontSize: 18,color: Colors.white - ,fontWeight: FontWeight.w600 + child: isLoading + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2.5, + ), ) + : Text( + 'Submit', + style: GoogleFonts.inter( + fontSize: 18, + color: Colors.white, + fontWeight: FontWeight.w600, + ), ), ), ), diff --git a/lib/screens/edit_serviceboy_screen.dart b/lib/screens/edit_serviceboy_screen.dart index 133236b..11dedb2 100644 --- a/lib/screens/edit_serviceboy_screen.dart +++ b/lib/screens/edit_serviceboy_screen.dart @@ -1,38 +1,81 @@ import 'package:flutter/material.dart'; +import 'package:glowwheels/helpers/shopid_helper.dart'; import 'package:provider/provider.dart'; import '../models/serviceboy_model.dart'; import '../provider/serviceboy_provider.dart'; import 'package:google_fonts/google_fonts.dart'; -class EditServiceBoyScreen extends StatelessWidget { + +class EditServiceBoyScreen extends StatefulWidget { final ServiceBoy serviceBoy; EditServiceBoyScreen({required this.serviceBoy}); + @override + _EditServiceBoyScreenState createState() => _EditServiceBoyScreenState(); +} + +class _EditServiceBoyScreenState extends State { final _formKey = GlobalKey(); - late final TextEditingController nameController = - TextEditingController(text: serviceBoy.name); - late final TextEditingController phoneController = - TextEditingController(text: serviceBoy.phone); + late final TextEditingController nameController; + late final TextEditingController phoneController; + + bool isLoading = false; + + @override + void initState() { + super.initState(); + nameController = TextEditingController(text: widget.serviceBoy.name); + phoneController = TextEditingController(text: widget.serviceBoy.phone); + } + + @override + void dispose() { + nameController.dispose(); + phoneController.dispose(); + super.dispose(); + } + + void _saveDetails(BuildContext context) async { + final name = nameController.text.trim(); + final phone = phoneController.text.trim(); + + // Validation: at least one field must be non-empty and different + if (name.isEmpty && phone.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Please edit at least one field')), + ); + return; + } + + final shopId = getShopId(context)!; + + setState(() => isLoading = true); + + await Provider.of(context, listen: false) + .editServiceBoy(widget.serviceBoy.id, name, phone, shopId); + + setState(() => isLoading = false); + Navigator.pop(context); // back to previous screen + } @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFF9FAF4), // light background + backgroundColor: const Color(0xFFF9FAF4), appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, - leading: IconButton( + leading: IconButton( icon: Icon(Icons.arrow_back_ios), onPressed: () => Navigator.pop(context), ), - title: Text( + title: Text( 'Edit Service boy Details', - style: GoogleFonts.nunito( - fontSize: 18,color: Color.fromRGBO(26, 26, 26, 1) - ,fontWeight: FontWeight.w500 - ) + style: GoogleFonts.nunito( + fontSize: 18, + color: Color.fromRGBO(26, 26, 26, 1), + fontWeight: FontWeight.w500), ), - ), body: Padding( padding: const EdgeInsets.all(16), @@ -40,50 +83,40 @@ class EditServiceBoyScreen extends StatelessWidget { key: _formKey, child: Column( children: [ - SizedBox(height: 30,), + SizedBox(height: 30), TextFormField( controller: nameController, - decoration: InputDecoration( + decoration: InputDecoration( hintText: 'Name', - hintStyle: GoogleFonts.nunito( - fontSize: 14,color: Color.fromRGBO(26, 26, 26, 1) - ,fontWeight: FontWeight.w400 - ), + hintStyle: GoogleFonts.nunito( + fontSize: 14, + color: Color.fromRGBO(26, 26, 26, 1), + fontWeight: FontWeight.w400), border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14), + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 14), ), - validator: (value) => value!.isEmpty ? 'Enter name' : null, ), - const SizedBox(height: 25), + SizedBox(height: 25), TextFormField( controller: phoneController, keyboardType: TextInputType.phone, - decoration: InputDecoration( + decoration: InputDecoration( hintText: 'Mobile Number', - hintStyle: GoogleFonts.nunito( - fontSize: 14,color: Color.fromRGBO(26, 26, 26, 1) - ,fontWeight: FontWeight.w400 - ), + hintStyle: GoogleFonts.nunito( + fontSize: 14, + color: Color.fromRGBO(26, 26, 26, 1), + fontWeight: FontWeight.w400), border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14), + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 14), ), - validator: (value) => value!.isEmpty ? 'Enter phone number' : null, ), - const Spacer(), + Spacer(), SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () { - /* if (_formKey.currentState!.validate()) { - Provider.of(context, listen: false) - .editServiceBoy( - serviceBoy.id, - nameController.text, - phoneController.text, - ); - Navigator.pop(context); - }*/ - }, + onPressed: isLoading ? null : () => _saveDetails(context), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF000B8C), padding: const EdgeInsets.symmetric(vertical: 14), @@ -91,10 +124,20 @@ class EditServiceBoyScreen extends StatelessWidget { borderRadius: BorderRadius.circular(6), ), ), - child: Text('Save', style:GoogleFonts.inter( - fontSize: 18,color: Colors.white - ,fontWeight: FontWeight.w600 - )), + child: isLoading + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Text('Save', + style: GoogleFonts.inter( + fontSize: 18, + color: Colors.white, + fontWeight: FontWeight.w600)), ), ), ], diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 7359924..1301872 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -1,60 +1,97 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:glowwheels/provider/shop_provider.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; + +import '../screens/main_screen.dart'; + +class LoginScreen extends StatefulWidget { + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final _formKey = GlobalKey(); + bool _isLoading = false; + bool _obscurePassword = true; + + void _handleLogin(BuildContext context) async { + if (!_formKey.currentState!.validate()) return; + + final shopProvider = Provider.of(context, listen: false); + setState(() => _isLoading = true); + + final success = await shopProvider.login( + _emailController.text.trim(), + _passwordController.text.trim(), + ); + + setState(() => _isLoading = false); + + if (success) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => MainScreen()), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Login failed. Check credentials.')), + ); + } + } -import 'main_screen.dart'; -class LoginScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: SingleChildScrollView( - child: Column( children: [ - + // Top Banner Container( decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color.fromRGBO(80, 166, 242, 0.66), - Color.fromRGBO(168, 134, 255, 0.66), - Color.fromRGBO(86, 88, 255, 0.3828), - Color.fromRGBO(214, 246, 255, 0.66), - ], - ), - borderRadius: BorderRadius.only(bottomRight: Radius.elliptical(300, 110), ) + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color.fromRGBO(80, 166, 242, 0.66), + Color.fromRGBO(168, 134, 255, 0.66), + Color.fromRGBO(86, 88, 255, 0.38), + Color.fromRGBO(214, 246, 255, 0.66), + ], + ), + borderRadius: BorderRadius.only( + bottomRight: Radius.elliptical(300, 110), + ), ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( children: [ - const SizedBox(height: 30), + SizedBox(height: 30), Image.asset('assets/images/signinlogo.png', height: 250), - const SizedBox(height: 20), - - const SizedBox(height: 30), + SizedBox(height: 20), Align( alignment: Alignment.centerLeft, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Welcome back!", style: - GoogleFonts.istokWeb( - fontSize: 18,color: Color.fromRGBO(25, 25, 112, 0.87) - ,fontWeight: FontWeight.w700 - )), - + Text("Welcome back!", + style: GoogleFonts.istokWeb( + fontSize: 18, + color: Color.fromRGBO(25, 25, 112, 0.87), + fontWeight: FontWeight.w700, + )), Container( width: 200, child: Text("Please sign in to continue", - style:GoogleFonts.radioCanada( - fontSize: 24, - fontWeight: FontWeight.w700, - height: 0.9, - color: Color.fromRGBO(25, 25, 112, 0.87) + style: GoogleFonts.radioCanada( + fontSize: 24, + fontWeight: FontWeight.w700, + height: 0.9, + color: Color.fromRGBO(25, 25, 112, 0.87), )), ), ], @@ -64,48 +101,68 @@ class LoginScreen extends StatelessWidget { ), ), ), + + // Login Form Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Form( - + key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - - const SizedBox(height: 20), - Text('Enter Email Id',style:GoogleFonts.radioCanada( - fontSize: 16, - fontWeight: FontWeight.w400, - //color: Color.fromRGBO(25, 25, 112, 0.87) - )), - const SizedBox(height: 5), - TextField( + SizedBox(height: 20), + Text('Enter Email Id', style: GoogleFonts.radioCanada(fontSize: 16)), + SizedBox(height: 5), + TextFormField( + controller: _emailController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your email'; + } + if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { + return 'Enter a valid email'; + } + return null; + }, decoration: InputDecoration( - hintText: 'abcd@gmail.com', border: OutlineInputBorder( - borderRadius: BorderRadius.circular(15.0) + borderRadius: BorderRadius.circular(15.0), ), ), ), - const SizedBox(height: 20), - Text('Enter Password',style:GoogleFonts.radioCanada( - fontSize: 16, - fontWeight: FontWeight.w400, - //color: Color.fromRGBO(25, 25, 112, 0.87) - )), - const SizedBox(height: 5), - TextField( - obscureText: true, + SizedBox(height: 20), + Text('Enter Password', style: GoogleFonts.radioCanada(fontSize: 16)), + SizedBox(height: 5), + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your password'; + } + return null; + }, decoration: InputDecoration( - + hintText: 'Enter your password', border: OutlineInputBorder( - borderRadius: BorderRadius.circular(15.0) + borderRadius: BorderRadius.circular(15.0), + ), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, ), - suffixIcon: Icon(Icons.remove_red_eye_outlined), ), ), - const SizedBox(height: 30), + SizedBox(height: 30), + + // Login Button SizedBox( width: double.infinity, height: 50, @@ -116,16 +173,16 @@ class LoginScreen extends StatelessWidget { borderRadius: BorderRadius.circular(25), ), ), - onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (context)=>MainScreen())); - }, - child: Text('Login', style: - - GoogleFonts.inter( - fontSize: 18,color: Colors.white - ,fontWeight: FontWeight.w600 - ) - + onPressed: _isLoading ? null : () => _handleLogin(context), + child: _isLoading + ? CircularProgressIndicator(color: Colors.white) + : Text( + 'Login', + style: GoogleFonts.inter( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w700, + ), ), ), ) @@ -138,4 +195,4 @@ class LoginScreen extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/order_screen.dart b/lib/screens/order_screen.dart index bc4fe06..45be03a 100644 --- a/lib/screens/order_screen.dart +++ b/lib/screens/order_screen.dart @@ -1,43 +1,34 @@ import 'package:flutter/material.dart'; +import 'package:glowwheels/helpers/shopid_helper.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; - -import '../models/order_model.dart'; - +import 'package:glowwheels/models/order_model.dart'; import '../provider/order_provider.dart'; import '../widgets/order_card.dart'; -class OrdersScreen extends StatelessWidget { - final TextStyle labelStyle = TextStyle(fontWeight: FontWeight.w500); - final TextStyle valueStyle = TextStyle(fontWeight: FontWeight.normal); - List orders = [ - Order( - customerName: "Ankit Ghosh", - mobileNumber: "8617015476", - serviceType: "Doorstep Service", - service: "Foam Wash", - price: "₹ 104", - time: "10:00 - 11:00 AM", - date: "2025-05-28", - carName: "Mahindra XUV 700", - status: "Confirmed", - imagePath: "assets/images/car.jpg", - ), - Order( - customerName: "Ravi Kumar", - mobileNumber: "9876543210", - serviceType: "Workshop", - service: "Interior Cleaning", - price: "₹ 150", - time: "12:00 - 1:00 PM", - date: "2025-05-29", - carName: "Hyundai Creta", - status: "Pending", - imagePath: "assets/images/bike.png", - ), - // Add more orders... - ]; +class OrdersScreen extends StatefulWidget { + @override + _OrdersScreenState createState() => _OrdersScreenState(); +} +class _OrdersScreenState extends State { + late String shopId; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + shopId = getShopId(context)!; + Provider.of(context, listen: false) + .fetchOrders(shopId, refresh: false); + }); + } + + Future _refreshOrders() async { + shopId = getShopId(context)!; + await Provider.of(context, listen: false) + .fetchOrders(shopId, refresh: true); + } @override Widget build(BuildContext context) { @@ -67,30 +58,52 @@ class OrdersScreen extends StatelessWidget { ), body: Padding( padding: const EdgeInsets.only(top: 8.0), - child: Consumer( + child: Consumer( builder: (context, orderProvider, _) { final orders = orderProvider.orders; + if (orderProvider.isLoading && !orderProvider.isRefreshing) { + return Center(child: CircularProgressIndicator()); + } + if (orders.isEmpty) { - return Center( - child: Image.asset( - 'assets/images/noorder.png', - width: 200, - height: 200, + return RefreshIndicator( + onRefresh: _refreshOrders, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + SizedBox(height: 100), + Center( + child: Image.asset( + 'assets/images/noorder.png', + width: 200, + height: 200, + ), + ), + SizedBox(height: 16), + Center( + child: Text( + 'No orders found', + style: GoogleFonts.poppins(fontSize: 16), + ), + ), + ], ), ); } - return ListView.builder( - itemCount: orders.length, - itemBuilder: (context, index) { - return OrderCard(order: orders[index],); - }, + return RefreshIndicator( + onRefresh: _refreshOrders, + child: ListView.builder( + itemCount: orders.length, + itemBuilder: (context, index) { + return OrderCard(order: orders[index]); + }, + ), ); }, ), ), - ); } } diff --git a/lib/screens/profile_details_screen.dart b/lib/screens/profile_details_screen.dart index 5509131..2ab8ddf 100644 --- a/lib/screens/profile_details_screen.dart +++ b/lib/screens/profile_details_screen.dart @@ -1,24 +1,40 @@ import 'package:flutter/material.dart'; +import 'package:glowwheels/helpers/shopid_helper.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; -import '../provider/shop_provider.dart'; +import 'package:shimmer/shimmer.dart'; +import '../provider/shop_profile_provider.dart'; + +class ServiceCenterDetailsScreen extends StatefulWidget { + const ServiceCenterDetailsScreen({super.key}); + + @override + State createState() => _ServiceCenterDetailsScreenState(); +} + +class _ServiceCenterDetailsScreenState extends State { + bool _isInit = true; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_isInit) { + final shopId = getShopId(context); + Provider.of(context, listen: false).fetchShopProfile(shopId!); + _isInit = false; + } + } -class ServiceCenterDetailsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final shop = Provider.of(context).shop; - - if (shop == null) { - return Scaffold( - body: Center(child: CircularProgressIndicator()), - ); - } + final provider = Provider.of(context); + final shop = provider.shopProfile; return Scaffold( - backgroundColor: Color(0xFFEAF5FF), + backgroundColor: const Color(0xFFEAF5FF), body: SafeArea( child: Container( - decoration: BoxDecoration( + decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color.fromRGBO(208, 235, 255, 1), Colors.white], begin: Alignment.topCenter, @@ -35,9 +51,9 @@ class ServiceCenterDetailsScreen extends StatelessWidget { children: [ GestureDetector( onTap: () => Navigator.pop(context), - child: Icon(Icons.arrow_back_ios, color: Colors.black), + child: const Icon(Icons.arrow_back_ios, color: Colors.black), ), - SizedBox(width: 25), + const SizedBox(width: 25), Expanded( child: Padding( padding: const EdgeInsets.only(top: 15.0), @@ -52,22 +68,25 @@ class ServiceCenterDetailsScreen extends StatelessWidget { ), ), ), - SizedBox(width: 24), + const SizedBox(width: 24), ], ), ), - // Main content Expanded( - child: SingleChildScrollView( + child: provider.isLoading + ? _buildShimmer() + : shop == null + ? const Center(child: Text('Failed to load shop details')) + : SingleChildScrollView( child: Center( child: Container( - margin: EdgeInsets.symmetric(horizontal: 16, vertical: 10), - padding: EdgeInsets.all(8), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), - boxShadow: [ + boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 10, @@ -79,36 +98,54 @@ class ServiceCenterDetailsScreen extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(12), - child: Image.asset( - "assets/images/shop_image.jpg", + child: shop.images != null && shop.images!.isNotEmpty + ? Image.network( + shop.images[0]!, height: 230, width: double.infinity, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => - Icon(Icons.broken_image, size: 100), + const Icon(Icons.broken_image, size: 100), + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + height: 230, + width: double.infinity, + color: Colors.grey[300], + child: + const Center(child: CircularProgressIndicator()), + ); + }, + ) + : Image.asset( + "assets/images/shop_image.jpg", + height: 230, + width: double.infinity, + fit: BoxFit.cover, ), ), - SizedBox(height: 16), + + const SizedBox(height: 16), Text( - shop.shopName, + shop.name ?? 'No Shop Name', style: GoogleFonts.inter( fontSize: 24, color: Colors.black, fontWeight: FontWeight.w600, ), ), - SizedBox(height: 16), + const SizedBox(height: 16), InfoRow( icon: "assets/icon/location_icon.png", text: shop.address ?? 'Address not available', ), InfoRow( icon: "assets/icon/contact_icon.png", - text: shop.mobile, + text: shop.phone ?? 'Phone not available', ), InfoRow( icon: "assets/icon/Message_icon.png", - text: shop.email, + text: shop.email ?? 'Email not available', ), ], ), @@ -122,6 +159,82 @@ class ServiceCenterDetailsScreen extends StatelessWidget { ), ); } + + Widget _buildShimmer() { + final baseColor = Colors.grey.shade300; + final highlightColor = Colors.grey.shade100; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Image placeholder (same size & rounded corners) + Container( + height: 230, + decoration: BoxDecoration( + color: baseColor, + borderRadius: BorderRadius.circular(12), + ), + ), + const SizedBox(height: 16), + + // Shop name placeholder (matching width and height) + Container( + height: 30, + width: 220, + color: baseColor, + margin: const EdgeInsets.only(left: 8), + ), + const SizedBox(height: 16), + + // Location info row shimmer + _shimmerInfoRow(width: 200, baseColor: baseColor), + + // Phone info row shimmer + _shimmerInfoRow(width: 140, baseColor: baseColor), + + // Email info row shimmer + _shimmerInfoRow(width: 180, baseColor: baseColor), + ], + ), + ), + ), + ); + } + + Widget _shimmerInfoRow({required double width, required Color baseColor}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15), + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: baseColor, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(width: 10), + Container( + height: 16, + width: width, + color: baseColor, + ), + ], + ), + ); + } } class InfoRow extends StatelessWidget { @@ -129,6 +242,7 @@ class InfoRow extends StatelessWidget { final String text; const InfoRow({ + super.key, required this.icon, required this.text, }); @@ -141,7 +255,7 @@ class InfoRow extends StatelessWidget { child: Row( children: [ ImageIcon(AssetImage(icon)), - SizedBox(width: 10), + const SizedBox(width: 10), Expanded( child: Text( text, diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart new file mode 100644 index 0000000..eb43f79 --- /dev/null +++ b/lib/screens/splash_screen.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; +import 'package:glowwheels/models/shop_model.dart'; +import 'package:glowwheels/provider/shop_provider.dart'; +import 'package:provider/provider.dart'; + +import 'login_screen.dart'; +import 'main_screen.dart'; + +class SplashDecider extends StatefulWidget { + const SplashDecider({super.key}); + + @override + State createState() => _SplashDeciderState(); +} + +class _SplashDeciderState extends State { + @override + void initState() { + super.initState(); + _checkLoginStatus(); + } + + Future _checkLoginStatus() async { + final shopBox = await Hive.openBox('shopBox'); + final tokenBox = await Hive.openBox('tokenBox'); + + final shop = shopBox.get('shop'); + final token = tokenBox.get('token'); + + if (shop != null && token != null && token.isNotEmpty) { + // ✅ Set the provider values if needed + Provider.of(context, listen: false).setShop(shop); + Provider.of(context, listen: false).setToken(token); + + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => MainScreen()), + ); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => LoginScreen()), + ); + } + } + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } +} diff --git a/lib/widgets/assign_serviceboy_dialog.dart b/lib/widgets/assign_serviceboy_dialog.dart index eb623c9..9b28afe 100644 --- a/lib/widgets/assign_serviceboy_dialog.dart +++ b/lib/widgets/assign_serviceboy_dialog.dart @@ -1,14 +1,28 @@ import 'package:flutter/material.dart'; +import 'package:glowwheels/helpers/shopid_helper.dart'; import 'package:provider/provider.dart'; - -import '../models/serviceboy_model.dart'; -import '../provider/serviceboy_provider.dart'; import 'package:google_fonts/google_fonts.dart'; -class AssignServiceBoyDialog extends StatelessWidget { - final List serviceBoys; +import 'package:glowwheels/models/serviceboy_model.dart'; +import '../provider/serviceboy_provider.dart'; +import '../provider/order_provider.dart'; - const AssignServiceBoyDialog({super.key, required this.serviceBoys}); +class AssignServiceBoyDialog extends StatefulWidget { + final List serviceBoys; + final String orderId; + + const AssignServiceBoyDialog({ + Key? key, + required this.serviceBoys, + required this.orderId, + }) : super(key: key); + + @override + State createState() => _AssignServiceBoyDialogState(); +} + +class _AssignServiceBoyDialogState extends State { + bool _isLoading = false; @override Widget build(BuildContext context) { @@ -19,14 +33,17 @@ class AssignServiceBoyDialog extends StatelessWidget { return AlertDialog( backgroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: const Text("Select Service Boy", style: TextStyle(fontWeight: FontWeight.bold)), + title: const Text( + "Select Service Boy", + style: TextStyle(fontWeight: FontWeight.bold), + ), content: SizedBox( height: 200, width: double.maxFinite, child: ListView.builder( - itemCount: serviceBoys.length, + itemCount: widget.serviceBoys.length, itemBuilder: (context, index) { - final boy = serviceBoys[index]; + final boy = widget.serviceBoys[index]; final isSelected = assignProvider.selectedBoy == boy; return GestureDetector( @@ -35,8 +52,9 @@ class AssignServiceBoyDialog extends StatelessWidget { padding: const EdgeInsets.all(12), margin: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( - color: isSelected ? Color.fromRGBO(0, 80, 170, 1) : Colors.transparent, + color: isSelected ? const Color.fromRGBO(0, 80, 170, 1) : Colors.transparent, borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -64,28 +82,80 @@ class AssignServiceBoyDialog extends StatelessWidget { ), actions: [ TextButton( - child: Text("CANCEL",style: GoogleFonts.poppins( - fontSize: 14,color: Color.fromRGBO(25, 25, 112, 1) - ,fontWeight: FontWeight.w400 - ),), - onPressed: () { - Navigator.of(context).pop(); // return null - }, + onPressed: () => Navigator.of(context).pop(), + child: Text( + "CANCEL", + style: GoogleFonts.poppins( + fontSize: 14, + color: const Color.fromRGBO(25, 25, 112, 1), + fontWeight: FontWeight.w400, + ), + ), ), ElevatedButton( - onPressed: assignProvider.selectedBoy != null - ? () { - Navigator.of(context).pop(assignProvider.selectedBoy); + onPressed: assignProvider.selectedBoy != null && !_isLoading + ? () async { + setState(() { + _isLoading = true; + }); + + final selectedBoy = assignProvider.selectedBoy!; + final orderProvider = Provider.of(context, listen: false); + final shopId = getShopId(context); + + try { + await orderProvider.assignServiceBoyToOrder( + orderId: widget.orderId, + serviceBoyId: selectedBoy.id, + shopId: shopId!, + ); + + if (mounted) { + Navigator.of(context, rootNavigator: true).pop( + + selectedBoy + + ); // ✅ return selected boy + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Service boy assigned successfully!')), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to assign service boy: $e')), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } } : null, style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF1B1464), + backgroundColor: const Color(0xFF1B1464), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), ), - child: Text("Assign",style: GoogleFonts.inter( - fontSize: 12,color: Colors.white - ,fontWeight: FontWeight.w500 - ),), + child: _isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : Text( + "Assign", + style: GoogleFonts.inter( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), ), ], ); @@ -94,4 +164,3 @@ class AssignServiceBoyDialog extends StatelessWidget { ); } } - diff --git a/lib/widgets/order_card.dart b/lib/widgets/order_card.dart index cb1cb50..7d8b10a 100644 --- a/lib/widgets/order_card.dart +++ b/lib/widgets/order_card.dart @@ -1,8 +1,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:glowwheels/helpers/shopid_helper.dart'; +import 'package:glowwheels/provider/order_provider.dart'; +import 'package:glowwheels/provider/serviceboy_provider.dart'; import 'package:google_fonts/google_fonts.dart'; -import '../models/order_model.dart'; -import '../models/serviceboy_model.dart'; +import 'package:glowwheels/models/order_model.dart'; +import 'package:provider/provider.dart'; +import 'package:glowwheels/models/serviceboy_model.dart'; import 'assign_serviceboy_dialog.dart'; class OrderCard extends StatefulWidget { @@ -16,12 +20,18 @@ class OrderCard extends StatefulWidget { class _OrderCardState extends State { ServiceBoy? assignedBoy; + late String shopId = ''; + bool isLoading = false; + late String orderStatus=widget.order.status; + + @override + void initState() { + assignedBoy = widget.order.assignedServiceBoy; + super.initState(); + + } + - List serviceBoys = [ - ServiceBoy(name: 'John Doe', phone: '9875643210'), - ServiceBoy(name: 'Amit Raj', phone: '9765432180'), - ServiceBoy(name: 'Manoj Sinha', phone: '9543219876'), - ]; @override Widget build(BuildContext context) { @@ -40,18 +50,31 @@ class _OrderCardState extends State { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.asset(widget.order.imagePath, width: 100, height: 100, fit: BoxFit.contain), + widget.order.service.vehicle.image.isNotEmpty + ? Image.network( + widget.order.service.vehicle.image, + width: 100, + height: 100, + fit: BoxFit.contain, + ) + : SizedBox( + width: 100, + height: 100, + child: Center(child: Icon(Icons.image_not_supported)), + ), SizedBox(width: 16), Expanded( child: Column( children: [ - _buildRow("Customer Name", widget.order.customerName), - _buildRow("Mobile Number", widget.order.mobileNumber), - _buildRow("Service Type", widget.order.serviceType), - _buildRow("Service", widget.order.service), - _buildRow("Price", widget.order.price), - _buildRow("Service Time", widget.order.time), + _buildRow("Customer Name", widget.order.user.name), + _buildRow("Mobile Number", widget.order.user.phone.toString()), + _buildRow("Service Type", widget.order.service.serviceType), + _buildRow("Service", widget.order.service.serviceName), + _buildRow("Price", widget.order.service.price.toString()), + _buildRow("Service Time", widget.order.timeSlot), _buildRow("Service Date", widget.order.date), + (widget.order.address!=null)?_buildRow("Location", widget.order.address!):SizedBox(), + ], ), ), @@ -60,7 +83,9 @@ class _OrderCardState extends State { SizedBox(height: 12), Center( child: Text( - widget.order.carName, + widget.order.service.manufacture.manufacture + + " " + + widget.order.service.model.model, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), @@ -68,46 +93,210 @@ class _OrderCardState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - widget.order.status, - style: TextStyle( - color: widget.order.status == "Confirmed" ? Colors.green : Colors.orange, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - assignedBoy == null - ? ElevatedButton( - onPressed: () async { - final selected = await showDialog( - context: context, - builder: (_) => AssignServiceBoyDialog(serviceBoys: serviceBoys), - ); + GestureDetector( + onTap: () async { + String? newStatus; + String? dialogMessage; - if (selected != null) { - setState(() { - assignedBoy = selected; - }); + if (orderStatus == "pending") { + newStatus = "confirmed"; + dialogMessage = "Are you sure you want to change the status to Confirmed?"; + } else if (orderStatus == "confirmed" && assignedBoy!=null) { + newStatus = "completed"; + dialogMessage = "Are you sure you want to change the status to Completed?"; + } + + if (newStatus != null && dialogMessage != null) { + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Confirm Status Change"), + content: Text(dialogMessage!), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text("OK"), + ), + ], + ), + ); + + if (confirm == true) { + try { + setState(() => isLoading = true); + final orderProvider = Provider.of(context, listen: false); + await orderProvider.updateOrderStatus( + orderId: widget.order.id, + status: newStatus, + ); + + setState(() { + orderStatus = newStatus!; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Order status updated to $newStatus")), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Failed to update status: $e")), + ); + } finally { + setState(() => isLoading = false); + } + } } }, - style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF1B1464), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), - ), - child: Text("Assign",style: GoogleFonts.inter( - fontSize: 12,color: Colors.white - ,fontWeight: FontWeight.w500 - ),), + child: Text( + orderStatus, + style: TextStyle( + color:orderStatus == "confirmed" || orderStatus == "completed" + ? Colors.green + : orderStatus == "pending" + ? Colors.orange + : Colors.red, + fontWeight: FontWeight.bold, + fontSize: 16, - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Assigned to: ${assignedBoy!.name}", - style: const TextStyle(fontWeight: FontWeight.bold)), - Text("Phone: ${assignedBoy!.phone}"), - ], + // Visual cue for clickable + ), + ), ), + + + if (assignedBoy == null && + orderStatus.toLowerCase() == "confirmed") + ElevatedButton( + onPressed: isLoading + ? null + : () async { + setState(() { + isLoading = true; + }); + + final serviceBoyProvider = + Provider.of(context, listen: false); + + final shopId = getShopId(context); + await serviceBoyProvider.fetchServiceBoys(shopId!); + + final selected = await showDialog( + context: context, + builder: (_) => AssignServiceBoyDialog( + serviceBoys: serviceBoyProvider.serviceBoys, + orderId: widget.order.id, + ), + ); + + if (selected != null) { + setState(() { + assignedBoy = selected; + }); + } + + setState(() { + isLoading = false; + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF1B1464), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30)), + ), + child: isLoading + ? SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Text( + "Assign", + style: GoogleFonts.inter( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w500), + ), + ) + else if (orderStatus.toLowerCase() == "pending") + ElevatedButton( + onPressed: () async{ + + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Confirm Status Change"), + content: Text("Are you sure you want to cancel the order?"!), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text("OK"), + ), + ], + ), + ); + + if (confirm == true) { + try { + setState(() => isLoading = true); + final orderProvider = Provider.of(context, listen: false); + await orderProvider.updateOrderStatus( + orderId: widget.order.id, + status: "cancelled by admin", + ); + + setState(() { + orderStatus = "cancelled by admin"; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Order status updated to cancelled by admin")), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Failed to update status: $e")), + ); + } finally { + setState(() => isLoading = false); + } + } + + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), + ), + child: Text( + "Cancel", + style: GoogleFonts.inter( + fontSize: 12, color: Colors.white, fontWeight: FontWeight.w500), + ), + ) + else if (orderStatus.toLowerCase() == "completed") + SizedBox() + else if (orderStatus.toLowerCase() == "cancelled by admin") + SizedBox() + else if (assignedBoy != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Assigned to: ${assignedBoy!.name}", + style: const TextStyle(fontWeight: FontWeight.bold)), + Text("Phone: ${assignedBoy!.phone}"), + ], + ) + + ], ) ], @@ -121,18 +310,20 @@ class _OrderCardState extends State { padding: const EdgeInsets.symmetric(vertical: 3), child: Row( children: [ - Expanded(flex: 2, child: Text(label, style: GoogleFonts.inter( - fontSize: 10,color: Colors.black - ,fontWeight: FontWeight.w500 - ))), - Expanded(flex: 3, child: Text(value, style: GoogleFonts.inter( - fontSize: 10,color: Colors.black - ,fontWeight: FontWeight.w500 - ))), + Expanded( + flex: 2, + child: Text(label, + style: GoogleFonts.inter( + fontSize: 10, color: Colors.black, fontWeight: FontWeight.w500)), + ), + Expanded( + flex: 3, + child: Text(value, + style: GoogleFonts.inter( + fontSize: 10, color: Colors.black, fontWeight: FontWeight.w500)), + ), ], ), ); } } - - diff --git a/lib/widgets/profile_header.dart b/lib/widgets/profile_header.dart index 5891198..7a1c9cb 100644 --- a/lib/widgets/profile_header.dart +++ b/lib/widgets/profile_header.dart @@ -1,52 +1,66 @@ import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:glowwheels/models/shop_profile_model.dart'; class ProfileHeader extends StatelessWidget { - const ProfileHeader({super.key}); + final ShopProfileModel? shopProfile; + + const ProfileHeader({super.key, this.shopProfile}); @override Widget build(BuildContext context) { + if (shopProfile == null) { + // Show skeleton loader + return _buildSkeleton(context); + } + + final name = shopProfile!.name; + final phone = shopProfile!.phone; + final email = shopProfile!.email; + final imageUrl = (shopProfile!.images.isNotEmpty) ? shopProfile!.images.first : null; + return Container( color: Colors.white, padding: const EdgeInsets.all(16), child: Row( children: [ - // Circular Profile Image CircleAvatar( radius: 40, - backgroundImage: AssetImage('assets/images/shop_image.jpg'), // Replace with your asset + backgroundImage: imageUrl != null + ? NetworkImage(imageUrl) + : const AssetImage('assets/images/shop_image.jpg') as ImageProvider, ), const SizedBox(width: 16), - // Details Column Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Row( children: [ - ImageIcon(AssetImage("assets/icon/account_icon.png")), - SizedBox(width: 8), + const ImageIcon(AssetImage("assets/icon/account_icon.png")), + const SizedBox(width: 8), Expanded( child: Text( - 'Omkara Car Wash Center', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + name, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), ), ], ), - SizedBox(height: 8), + const SizedBox(height: 8), Row( children: [ - ImageIcon(AssetImage("assets/icon/contact_icon.png")), - SizedBox(width: 8), - Text('+91 9999988888'), + const ImageIcon(AssetImage("assets/icon/contact_icon.png")), + const SizedBox(width: 8), + Text(phone), ], ), - SizedBox(height: 8), + const SizedBox(height: 8), Row( children: [ - ImageIcon(AssetImage("assets/icon/Message_icon.png")), - SizedBox(width: 8), - Expanded(child: Text('loremipsum@gmail.com')), + const ImageIcon(AssetImage("assets/icon/Message_icon.png")), + const SizedBox(width: 8), + Expanded(child: Text(email)), ], ), ], @@ -56,4 +70,38 @@ class ProfileHeader extends StatelessWidget { ), ); } + + Widget _buildSkeleton(BuildContext context) { + final baseColor = Colors.grey[300]!; + final highlightColor = Colors.grey[100]!; + + return Container( + color: Colors.white, + padding: const EdgeInsets.all(16), + child: Shimmer.fromColors( + baseColor: baseColor, + highlightColor: highlightColor, + child: Row( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration(color: baseColor, shape: BoxShape.circle), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(height: 20, color: baseColor, margin: const EdgeInsets.only(bottom: 12)), + Container(height: 16, width: 150, color: baseColor, margin: const EdgeInsets.only(bottom: 12)), + Container(height: 16, width: 200, color: baseColor), + ], + ), + ), + ], + ), + ), + ); + } } diff --git a/pubspec.lock b/pubspec.lock index f607eb9..9de579e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -552,6 +552,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "1f1009b5845a1f88f1c5630212279540486f97409e9fc3f63883e71070d107bf" + url: "https://pub.dev" + source: hosted + version: "2.0.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 1574a6b..10c1050 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: hive: ^2.2.3 hive_flutter: ^1.1.0 path_provider: ^2.1.2 + shimmer: ^2.0.0 dev_dependencies: flutter_test: