last 16-10-2025

This commit is contained in:
mageshwaran committed 2025-10-16 11:21:52 +05:30
commit 7546b7348c
269 files changed
+36404

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
import 'dart:core';
class AppAssets {
static const String flashscreen = 'assets/images/logo.jpeg';
static const String welcome = 'assets/images/welcomepage.png';
static const String login = 'assets/images/login.png';
static const String history = 'assets/images/history.png';
static const String categories = 'assets/images/categories.png';
static const String package = 'assets/images/package.png';
static const String home = 'assets/images/home.png';
static const String menu = 'assets/images/menu.png';
static const String banner = 'assets/images/banner.png';
static const String free = 'assets/images/free.png';
static const String paid = 'assets/images/paid.png';
static const String clean = 'assets/images/clean.png';
static const String subscription = 'assets/images/subscription.png';
static const String cleaning = 'assets/images/cleaning.png';
static const String arrowbutton = 'assets/images/arrowbutton.png';
static const String drawer = 'assets/images/drawer.png';
static const String background = 'assets/images/background.png';
static const String subscriptionimage = 'assets/images/subscriptionimage.png';
static const String allicon = 'assets/images/Allicon.png';
static const String filtericon = 'assets/images/filtericon.png';
static const String sucess = 'assets/images/sucess.png';
static const String changepass = 'assets/images/changepass.png';
static const String vendorwelcome = 'assets/images/vendorwelcome.png';
static const String profile = 'assets/images/profileimage.png';
static const String freeicon = 'assets/images/freeicon.png';
static const String paidicon = 'assets/images/paidicon.png';
static const String share = 'assets/images/share.png';
static const String map = 'assets/images/map.png';
}
+40
View File
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/base_controller.dart';
class AppColors {
bool darkTheme = false;
BaseController baseCtrl = Get.put(BaseController());
AppColors() {
baseCtrl.isDarkModeEnabled.listen((value) {
changeColors();
});
}
void changeColors() {
if (darkTheme == true) {
dark = Colors.white;
} else {
dark = Colors.black;
}
}
static Color dark = Colors.black;
static const Color primary = Color(0xFF0066FF);
static const Color secondprimary = Color(0xFFffffff);
static const Color thridprimary = Color(0xFF000000);
static const Color flitericoncolor = Color(0xFF625353);
static const Color hittext = Color(0xFF5E5858);
static const Color ratingstar = Color(0xFFF55B03);
static const Color flashscreen = Color(0xFFFEFAF7);
static const Color lightgray = Color(0xff555555);
static const Color lightGrey = Color(0xFFEBE9E9);
static const Color lightWhite = Color(0xFFFAFAFA);
static const Color lightBlue = Color(0xFFE8F5FF);
static const Color purple = Color(0xFF9C34C2);
static const Color red = Color(0xFFFF0000);
}
+55
View File
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
class CommanButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final bool isPrimary;
final Color backgroundColor;
final Color borderColor;
final Color textColor;
final double width;
final double height;
final double borderRadius;
final TextStyle? textStyle;
// ignore: use_super_parameters
const CommanButton({
Key? key,
required this.text,
required this.onPressed,
this.isPrimary = true,
this.backgroundColor = const Color(0xFF0066FF),
this.borderColor = const Color(0xFFAAAAAA),
this.textColor = Colors.white,
this.width = 311.1557922363281,
this.height = 64.22110748291016,
this.borderRadius = 36.18,
this.textStyle,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final Color bgColor = isPrimary ? backgroundColor : Colors.white;
final Color txtColor = isPrimary ? textColor : borderColor;
return SizedBox(
width: width,
height: height,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: bgColor,
side: BorderSide(color: borderColor, width: 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
),
child: Text(
text,
style: textStyle ?? TextStyle(color: txtColor, fontSize: 16),
),
),
);
}
}
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
class CommonTextFormField extends StatelessWidget {
final String? hintText;
final TextEditingController? controller;
final String? Function(String?)? validator;
final bool obscureText;
final Widget? prefixIcon;
final Widget? suffixIcon;
final TextInputType? keyboardType;
final String? errorText;
final void Function(String)? onChanged;
final int? maxLines;
final int? minLines;
final bool expands;
const CommonTextFormField({
super.key,
this.hintText,
this.controller,
this.validator,
this.obscureText = false,
this.prefixIcon,
this.suffixIcon,
this.keyboardType,
this.errorText,
this.onChanged,
this.maxLines,
this.minLines,
this.expands = false,
});
@override
Widget build(BuildContext context) {
final bool isMultiline = (maxLines != null && maxLines! > 1) || expands;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 360,
height: isMultiline ? null : 57.89,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.28),
border: Border.all(
color: errorText != null ? Colors.red : const Color(0xFFBDBCBC),
width: 0.9,
),
),
child: TextFormField(
controller: controller,
validator: validator,
obscureText: obscureText,
keyboardType: keyboardType ??
(isMultiline ? TextInputType.multiline : TextInputType.text),
onChanged: onChanged,
maxLines: expands ? null : maxLines ?? 1,
minLines: expands ? null : minLines,
expands: expands,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 18,
),
hintText: hintText,
border: InputBorder.none,
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
errorStyle: const TextStyle(height: 0),
),
),
),
if (errorText != null)
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 4.0),
child: Text(
errorText!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
],
);
}
}
+186
View File
@@ -0,0 +1,186 @@
import 'package:bookmywages/routers/consts_router.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class DrawerMenu extends StatelessWidget {
final String userName;
final String userImage;
const DrawerMenu({
super.key,
required this.userName,
required this.userImage,
});
@override
Widget build(BuildContext context) {
// final indexController = InheritedIndexController.of(context);
return SizedBox(
width: MediaQuery.of(context).size.width * 0.8,
height: 580,
child: GestureDetector(
// This captures horizontal drag gestures for the entire drawer
onHorizontalDragEnd: (details) {
if (details.primaryVelocity! < 0) {
// Close drawer only when swiped left (negative velocity)
Navigator.of(context).pop();
}
},
child: Drawer(
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(50),
bottomRight: Radius.circular(50),
),
),
child: SafeArea(
child: Column(
children: [
const SizedBox(height: 16),
// User Profile Section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
CircleAvatar(
radius: 24,
backgroundImage: NetworkImage(userImage),
),
const SizedBox(width: 16),
Text(
userName,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(height: 30),
// Menu Items - Completely removed Navigator.pop
_buildMenuItem(
icon: Icons.home,
iconColor: Colors.orange,
title: 'Dashboard',
onTap: () {
Navigator.of(context).pop();
},
),
_buildMenuItem(
icon: Icons.card_membership,
iconColor: Colors.blue,
title: 'Package/Subscription',
onTap: () {
Get.offAllNamed(RouterConts.packageList, arguments: 1);
},
),
_buildMenuItem(
icon: Icons.book,
iconColor: Colors.deepPurple,
title: 'Book Services',
onTap: () {
Get.offAllNamed(
RouterConts.history,
arguments: {
'historyTab': 0, // Enquiry list tab
},
);
},
),
_buildMenuItem(
icon: Icons.history,
iconColor: Colors.pink,
title: 'History',
onTap: () {
Get.offAllNamed(
RouterConts.history,
arguments: {
'historyTab': 0, // Enquiry list tab
},
);
},
),
_buildMenuItem(
icon: Icons.notifications,
iconColor: Colors.amber[800]!,
title: 'Notification',
onTap: () {
// Only handle navigation logic here
// DO NOT close drawer
},
),
_buildMenuItem(
icon: Icons.person,
iconColor: Colors.green,
title: 'My account',
onTap: () {
Get.offAllNamed(RouterConts.profilemainscreen);
},
),
const Spacer(),
// Sign Out
_buildMenuItem(
icon: Icons.exit_to_app,
iconColor: Colors.indigo,
title: 'Sign out',
onTap: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('userId');
await prefs.remove('vendor_id');
await prefs.remove('data');
// Using GetX navigation
Get.offAllNamed(RouterConts.loginpage);
},
),
const SizedBox(height: 24),
],
),
),
),
),
);
}
Widget _buildMenuItem({
required IconData icon,
required Color iconColor,
required String title,
required VoidCallback onTap,
}) {
return ListTile(
leading: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: iconColor, size: 20),
),
title: Text(
title,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
onTap: onTap,
);
}
}
+205
View File
@@ -0,0 +1,205 @@
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class DrawerMenuVendor extends StatelessWidget {
final String userName;
final String userImage;
const DrawerMenuVendor({
super.key,
required this.userName,
required this.userImage,
});
@override
Widget build(BuildContext context) {
// final indexController = InheritedIndex1Controller.of(context);
return SizedBox(
width: MediaQuery.of(context).size.width * 0.8,
height: 600,
child: GestureDetector(
// This captures horizontal drag gestures for the entire drawer
onHorizontalDragEnd: (details) {
if (details.primaryVelocity! < 0) {
// Close drawer only when swiped left (negative velocity)
Navigator.of(context).pop();
}
},
child: Drawer(
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(50),
bottomRight: Radius.circular(50),
),
),
child: SafeArea(
child: Column(
children: [
// User Profile Section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
CircleAvatar(
radius: 24,
backgroundImage: NetworkImage(userImage),
),
const SizedBox(width: 16),
Text(
userName,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(height: 20),
// Menu Items - Fixed navigation logic
_buildMenuItem(
icon: Icons.home,
iconColor: Colors.orange,
title: 'Dashboard',
onTap: () {
Navigator.of(context).pop();
},
),
_buildMenuItem(
icon: Icons.card_membership,
iconColor: Colors.blue,
title: 'Package/Subscription',
onTap: () {
Get.offAllNamed(RouterConts.vendorpackage, arguments: 1);
},
),
_buildMenuItem(
icon: Icons.book,
iconColor: Colors.deepPurple,
title: 'Book Services',
onTap: () {
Get.offAllNamed(
RouterConts.vendorhistory,
arguments: {
'historyTab': 0, // Enquiry list tab
},
);
},
),
_buildMenuItem(
icon: Icons.payment,
iconColor: Colors.pink,
title: 'Payment',
onTap: () {
Get.offAllNamed(
RouterConts.vendorhistory,
arguments: {
'historyTab': 2, // Enquiry list tab
},
);
},
),
_buildMenuItem(
icon: Icons.person_add_alt_rounded,
iconColor: Colors.amber[800]!,
title: 'Service Management',
onTap: () {
Get.offAllNamed(
RouterConts.vendorhistory,
arguments: {
'historyTab': 1, // Enquiry list tab
},
);
},
),
_buildMenuItem(
icon: Icons.person,
iconColor: AppColors.primary,
title: 'Enquiry Management',
onTap: () {
Get.offAllNamed(
RouterConts.vendorhistory,
arguments: {
'historyTab': 4, // Enquiry list tab
},
);
},
),
_buildMenuItem(
icon: Icons.person,
iconColor: Colors.green,
title: 'My account',
onTap: () {
Navigator.of(context).pop();
Get.toNamed(RouterConts.profilemainvendor);
},
),
const Spacer(),
// Sign Out
_buildMenuItem(
icon: Icons.exit_to_app,
iconColor: Colors.indigo,
title: 'Sign out',
onTap: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('userId');
await prefs.remove('vendor_id');
await prefs.remove('data');
// Using GetX navigation
Get.offAllNamed(RouterConts.loginpage);
},
),
const SizedBox(height: 24),
],
),
),
),
),
);
}
Widget _buildMenuItem({
required IconData icon,
required Color iconColor,
required String title,
required VoidCallback onTap,
}) {
return ListTile(
leading: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: iconColor, size: 20),
),
title: Text(
title,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
onTap: onTap,
);
}
}
+17
View File
@@ -0,0 +1,17 @@
import 'package:get/get.dart';
class BaseController extends GetxController {
RxBool isDarkModeEnabled = false.obs;
RxInt currentIndex = 0.obs;
String? fbUserId;
RxMap<String, dynamic> filterData = <String, dynamic>{}.obs;
void updateFilterData(Map<String, dynamic> newData) {
filterData.assignAll(newData);
}
Map<String, dynamic> getFilterData() {
return filterData;
}
}
+22
View File
@@ -0,0 +1,22 @@
import 'package:bookmywages/routers/consts_router.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:bookmywages/routers/router.dart'; // Your actual routes file
void main() {
runApp(const ProviderScope(child: BookMyWage()));
}
class BookMyWage extends StatelessWidget {
const BookMyWage({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: RouterConts.flashScreen,
getPages: AppRoutes.routes, // This should match your routes file
);
}
}
+32
View File
@@ -0,0 +1,32 @@
class BannerModel {
final int id;
final String documentUrl; // Added to store the full URL
final String createdDate;
BannerModel({
required this.id,
required this.documentUrl,
required this.createdDate,
});
// Fixed factory method
factory BannerModel.fromJson(Map<String, dynamic> json) {
return BannerModel(
id: json['id'] as int,
documentUrl:
json['document1']
as String, // Use the document1 field which contains the full URL
createdDate: json['created_date'] as String,
);
}
Map<String, dynamic> toJson() {
return {'id': id, 'document1': documentUrl, 'created_date': createdDate};
}
// Convenience method to get the image URL if needed
String get imageUrl => documentUrl;
}
+48
View File
@@ -0,0 +1,48 @@
// ignore: file_names
class BookingModel {
final String name;
final String mobileNumber;
final String email;
final String message;
final String serviceDate;
final String serviceTime;
final String serviceId;
final String address;
BookingModel({
required this.name,
required this.mobileNumber,
required this.email,
required this.message,
required this.serviceDate,
required this.serviceTime,
required this.serviceId,
required this.address,
});
factory BookingModel.fromJson(Map<String, dynamic> json) {
return BookingModel(
name: json['name'] ?? '',
mobileNumber: json['mobile_number'] ?? '',
email: json['email'] ?? '',
message: json['message'] ?? '',
serviceDate: json['service_date'] ?? '',
serviceTime: json['service_time'] ?? '',
serviceId: json['service_id'] ?? '',
address: json['address'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'mobile_number': mobileNumber,
'email': email,
'message': message,
'service_date': serviceDate,
'service_time': serviceTime,
'service_id': serviceId,
'address': address,
};
}
}
+47
View File
@@ -0,0 +1,47 @@
class CategoriesModel {
final int id;
final String name;
// Icon filename
final String iconUrl; // Full icon URL
// Image filename
final String imageUrl; // Full image URL
final String createdDate;
CategoriesModel({
required this.id,
required this.name,
required this.iconUrl,
required this.imageUrl,
required this.createdDate,
});
factory CategoriesModel.fromJson(Map<String, dynamic> json) {
return CategoriesModel(
id: json['id'],
name: json['name'],
iconUrl: json['icon1'] ?? '',
imageUrl: json['image1'] ?? '',
createdDate: json['created_date'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'icon1': iconUrl,
'image1': imageUrl,
'created_date': createdDate,
};
}
// Convenience getters to access the image URLs
String getIconUrl() => iconUrl;
String getImageUrl() => imageUrl;
}
+13
View File
@@ -0,0 +1,13 @@
class BookingModifyModel {
final String id;
final String serviceId;
final String servicedate;
final String servicetime;
BookingModifyModel({
required this.id,
required this.serviceId,
required this.servicedate,
required this.servicetime,
});
}
+11
View File
@@ -0,0 +1,11 @@
class CancelBookingRequest {
final String id;
final String serviceId;
final String type;
CancelBookingRequest({
required this.id,
required this.serviceId,
required this.type,
});
}
+150
View File
@@ -0,0 +1,150 @@
class DetailPageModel {
int id;
int vendorId;
int serviceType;
String vendorname;
String servicename;
int category;
int subcategory;
String workinghours;
String workingduration;
String amount;
String location;
String description;
String details;
String averageReview;
List<String>? videos;
String createdDate;
String vendorName;
String? profilePic;
String categoryName;
String subcategoryName;
List<String> images1;
DetailPageModel({
required this.id,
required this.vendorId,
required this.serviceType,
required this.vendorname,
required this.servicename,
required this.category,
required this.subcategory,
required this.workinghours,
required this.workingduration,
required this.amount,
required this.location,
required this.description,
required this.details,
required this.averageReview,
this.videos,
required this.createdDate,
required this.vendorName,
this.profilePic,
required this.categoryName,
required this.subcategoryName,
required this.images1,
});
factory DetailPageModel.fromJson(Map<String, dynamic> json) {
// Helper function to safely convert to string
String safeString(dynamic value) {
if (value == null) return '';
return value.toString();
}
// Helper function to safely convert to int
int safeInt(dynamic value) {
if (value == null) return 0;
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
// Helper function to safely handle videos
List<String>? safeVideos(dynamic value) {
if (value == null) return null;
if (value is String && value.isNotEmpty) return [value];
if (value is List) {
return value
.map((e) => e?.toString() ?? '')
.where((e) => e.isNotEmpty)
.toList();
}
return null;
}
// Helper function to safely handle images
List<String> safeImages(dynamic value) {
if (value == null) return [];
if (value is List) {
return value
.map((e) => e?.toString() ?? '')
.where((e) => e.isNotEmpty)
.toList();
}
if (value is String && value.isNotEmpty) return [value];
return [];
}
return DetailPageModel(
id: safeInt(json['id']),
vendorId: safeInt(json['vendor_id']),
serviceType: safeInt(json['service_type']),
vendorname: safeString(json['vendorname']),
servicename: safeString(json['servicename']),
category: safeInt(json['category']),
subcategory: safeInt(json['subcategory']),
workinghours: safeString(json['workinghours']),
workingduration: safeString(json['workingduration']),
amount: safeString(json['amount']),
location: safeString(json['location']),
description: safeString(json['description']),
details: safeString(json['details']),
averageReview: safeString(json['average_review']),
videos: safeVideos(json['videos']),
createdDate: safeString(json['created_date']),
vendorName: safeString(json['vendor_name']),
profilePic: json['profile_pic']?.toString(),
categoryName: safeString(json['category_name']),
subcategoryName: safeString(json['subcategory_name']),
images1: safeImages(json['images1']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'vendor_id': vendorId,
'service_type': serviceType,
'vendorname': vendorname,
'servicename': servicename,
'category': category,
'subcategory': subcategory,
'workinghours': workinghours,
'workingduration': workingduration,
'amount': amount,
'location': location,
'description': description,
'details': details,
'average_review': averageReview,
'videos': videos,
'created_date': createdDate,
'vendor_name': vendorName,
'profile_pic': profilePic,
'category_name': categoryName,
'subcategory_name': subcategoryName,
'images1': images1,
};
}
// Helper method to get a safe display value
String getDisplayValue(String value) {
return value.isEmpty ? 'N/A' : value;
}
// Helper method to check if images are available
bool get hasImages => images1.isNotEmpty;
// Helper method to check if videos are available
bool get hasVideos => videos != null && videos!.isNotEmpty;
}
+62
View File
@@ -0,0 +1,62 @@
class EnquiryListModel {
final int id;
final String name;
final String mobile;
final String email;
final int serviceId;
final int userId;
final String message;
final String createdDate;
final String serviceName;
final String vendorName;
final List<String> images1;
EnquiryListModel({
required this.id,
required this.name,
required this.mobile,
required this.email,
required this.serviceId,
required this.userId,
required this.message,
required this.createdDate,
required this.serviceName,
required this.vendorName,
required this.images1,
});
factory EnquiryListModel.fromJson(Map<String, dynamic> json) {
return EnquiryListModel(
id: json['id'] ?? 0,
name: json['name'] ?? '',
mobile: json['mobile'] ?? '',
email: json['email'] ?? '',
serviceId: json['service_id'] ?? 0,
userId: json['user_id'] ?? 0,
message: json['message'] ?? '',
createdDate: json['created_date'] ?? '',
serviceName: json['service_name'] ?? '',
vendorName: json['vendorname'] ?? '',
images1: List<String>.from(json['images1'] ?? []),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'mobile': mobile,
'email': email,
'service_id': serviceId,
'user_id': userId,
'message': message,
'created_date': createdDate,
'service_name': serviceName,
'vendorname': vendorName,
'images1': images1,
};
}
}
+37
View File
@@ -0,0 +1,37 @@
class EnquiryModel {
String? mobile;
String? name;
String? serviceId;
String? message;
String? email;
EnquiryModel({
this.mobile,
this.name,
this.serviceId,
this.message,
this.email,
});
// Factory method to create an EnquiryModel from a JSON map
factory EnquiryModel.fromJson(Map<String, dynamic> json) {
return EnquiryModel(
mobile: json['mobile'],
name: json['name'],
serviceId: json['service_id'],
message: json['message'],
email: json['email'],
);
}
// Method to convert EnquiryModel to a JSON map
Map<String, dynamic> toJson() {
return {
'mobile': mobile,
'name': name,
'service_id': serviceId,
'message': message,
'email': email,
};
}
}
+70
View File
@@ -0,0 +1,70 @@
class ExpiredPlanModel {
final int? id;
final int? userId;
final int? type;
final int? planId;
final String? endDate;
final String? createdDate;
final String? planName;
final String? price;
final int? duration;
final String? description;
ExpiredPlanModel({
this.id,
this.userId,
this.type,
this.planId,
this.endDate,
this.createdDate,
this.planName,
this.price,
this.duration,
this.description,
});
factory ExpiredPlanModel.fromJson(Map<String, dynamic> json) {
return ExpiredPlanModel(
id:
json['id'] is int
? json['id']
: int.tryParse(json['id']?.toString() ?? ''),
userId:
json['user_id'] is int
? json['user_id']
: int.tryParse(json['user_id']?.toString() ?? ''),
type:
json['type'] is int
? json['type']
: int.tryParse(json['type']?.toString() ?? ''),
planId:
json['plan_id'] is int
? json['plan_id']
: int.tryParse(json['plan_id']?.toString() ?? ''),
endDate: json['end_date']?.toString(),
createdDate: json['created_date']?.toString(),
planName: json['plan_name']?.toString(),
price: json['price']?.toString(),
duration:
json['duration'] is int
? json['duration']
: int.tryParse(json['duration']?.toString() ?? ''),
description: json['description']?.toString(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'user_id': userId,
'type': type,
'plan_id': planId,
'end_date': endDate,
'created_date': createdDate,
'plan_name': planName,
'price': price,
'duration': duration,
'description': description,
};
}
}
+47
View File
@@ -0,0 +1,47 @@
class GetReviewModel {
final int id;
final int userId;
final int serviceId;
final String review;
final String createdDate;
final String userName;
final String? profilePic1;
GetReviewModel({
required this.id,
required this.userId,
required this.serviceId,
required this.review,
required this.createdDate,
required this.userName,
this.profilePic1,
});
factory GetReviewModel.fromJson(Map<String, dynamic> json) {
return GetReviewModel(
id: json['id'] ?? 0,
userId: json['user_id'] ?? 0,
serviceId: json['service_id'] ?? 0,
review: json['review'] ?? '',
createdDate: json['created_date'] ?? '',
userName: json['user_name'] ?? '',
profilePic1: json['profile_pic1'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'user_id': userId,
'service_id': serviceId,
'review': review,
'created_date': createdDate,
'user_name': userName,
'profile_pic1': profilePic1,
};
}
}
+22
View File
@@ -0,0 +1,22 @@
class LoginModel {
String email;
String password;
LoginModel({required this.email, required this.password});
// For converting to JSON
Map<String, dynamic> toJson() {
return {
'email': email,
'password': password,
};
}
// For creating an instance from JSON
factory LoginModel.fromJson(Map<String, dynamic> json) {
return LoginModel(
email: json['email'] ?? '',
password: json['password'] ?? '',
);
}
}
+105
View File
@@ -0,0 +1,105 @@
class MostPopularModel {
final int id;
final int vendorId;
final int serviceType;
final String vendorName;
final String? vendorNameAlt;
final String serviceName;
final int category;
final int subcategory;
final String workingHours;
final String workingDuration;
final String amount;
final String location;
final String description;
final String details;
final List<String>? images1; // ✅ updated from String? to List<String>?
final String? videos;
final String createdDate;
final String? profilePic1; // ✅ new field
final double? averageReview;
final String phoneNumber;
MostPopularModel({
required this.id,
required this.vendorId,
required this.serviceType,
required this.vendorName,
this.vendorNameAlt,
required this.serviceName,
required this.category,
required this.subcategory,
required this.workingHours,
required this.workingDuration,
required this.amount,
required this.location,
required this.description,
required this.details,
this.images1,
this.videos,
required this.createdDate,
this.profilePic1, // ✅ new field
this.averageReview,
required this.phoneNumber,
});
factory MostPopularModel.fromJson(Map<String, dynamic> json) {
return MostPopularModel(
id: json['id'] ?? 0,
vendorId: json['vendor_id'] ?? 0,
serviceType: json['service_type'] ?? 0,
vendorName: json['vendorname'] ?? '',
vendorNameAlt: json['vendor_name'],
serviceName: json['servicename'] ?? '',
category: json['category'] ?? 0,
subcategory: json['subcategory'] ?? 0,
workingHours: json['workinghours'] ?? '',
workingDuration: json['workingduration'] ?? '',
amount: json['amount']?.toString() ?? '',
location: json['location'] ?? '',
description: json['description'] ?? '',
details: json['details'] ?? '',
images1:
(json['images1'] != null) ? List<String>.from(json['images1']) : null,
videos: json['videos'],
createdDate: json['created_date'] ?? '',
profilePic1: json['profile_pic1'], // ✅ new field
averageReview:
json['average_review'] != null
? (json['average_review'] is double)
? json['average_review']
: double.tryParse(json['average_review'].toString())
: null,
phoneNumber: json['phone_number'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'vendor_id': vendorId,
'service_type': serviceType,
'vendorname': vendorName,
'vendor_name': vendorNameAlt,
'servicename': serviceName,
'category': category,
'subcategory': subcategory,
'workinghours': workingHours,
'workingduration': workingDuration,
'amount': amount,
'location': location,
'description': description,
'details': details,
'images1': images1,
'videos': videos,
'created_date': createdDate,
'profile_pic1': profilePic1, // ✅ new field
'average_review': averageReview,
'phone_number': phoneNumber,
};
}
}
+43
View File
@@ -0,0 +1,43 @@
class NotificationModel {
final int id;
final int userId;
final int type;
final int serviceId;
final String message;
final bool isRead; // Add this field
final String createdDate;
final List<String> images;
NotificationModel({
required this.id,
required this.userId,
required this.type,
required this.serviceId,
required this.isRead,
required this.message,
required this.createdDate,
required this.images,
});
factory NotificationModel.fromJson(Map<String, dynamic> json) {
// Handle both 'images' and 'images1' fields from API
List<String> imageList = [];
if (json['images1'] != null && json['images1'] is List) {
imageList = List<String>.from(json['images1']);
}
return NotificationModel(
id: json['id'] ?? 0,
userId: json['user_id'] ?? 0,
type: json['type'] ?? 0,
isRead: json['is_read'] ?? false,
serviceId: json['service_id'] ?? 0,
message: json['message'] ?? '',
createdDate: json['created_date'] ?? '',
images: imageList,
);
}
// ... rest of the class
}
+13
View File
@@ -0,0 +1,13 @@
class OtpModel {
final String userId; // Keep as String
final String otp;
OtpModel({required this.userId, required this.otp});
Map<String, dynamic> toJson() {
return {
'user_id': userId, // Send as String
'otp': otp,
};
}
}
+47
View File
@@ -0,0 +1,47 @@
class PackageModel {
final int id;
final String name;
final String price;
final int duration;
final String description;
final int type;
final int noOfService;
final String createdDate;
PackageModel({
required this.id,
required this.name,
required this.price,
required this.duration,
required this.description,
required this.type,
required this.noOfService,
required this.createdDate,
});
factory PackageModel.fromJson(Map<String, dynamic> json) {
return PackageModel(
id: json['id'],
name: json['name'],
price: json['price'],
duration: json['duration'],
description: json['description'],
type: json['type'],
noOfService: json['no_of_service'],
createdDate: json['created_date'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'price': price,
'duration': duration,
'description': description,
'type': type,
'no_of_service': noOfService,
'created_date': createdDate,
};
}
}
+55
View File
@@ -0,0 +1,55 @@
class PaymentDetailsModel {
final int id;
final int userId;
final int type;
final int planId;
final String endDate;
final String createdDate;
final String planName;
final String price;
final int duration;
final String description;
PaymentDetailsModel({
required this.id,
required this.userId,
required this.type,
required this.planId,
required this.endDate,
required this.createdDate,
required this.planName,
required this.price,
required this.duration,
required this.description,
});
factory PaymentDetailsModel.fromJson(Map<String, dynamic> json) {
return PaymentDetailsModel(
id: json['id'] ?? 0,
userId: json['user_id'] ?? 0,
type: json['type'] ?? 0,
planId: json['plan_id'] ?? 0,
endDate: json['end_date'] ?? '',
createdDate: json['created_date'] ?? '',
planName: json['plan_name'] ?? '',
price: json['price'] ?? '',
duration: json['duration'] ?? 0,
description: json['description'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'user_id': userId,
'type': type,
'plan_id': planId,
'end_date': endDate,
'created_date': createdDate,
'plan_name': planName,
'price': price,
'duration': duration,
'description': description,
};
}
}
+22
View File
@@ -0,0 +1,22 @@
class PlanSuccessModel {
final String userId;
final String planId;
final int duration;
final int type;
PlanSuccessModel({
required this.userId,
required this.planId,
required this.duration,
required this.type,
});
Map<String, dynamic> toJson() {
return {
'user_id': userId,
'plan_id': planId,
'duration': duration.toString(),
'type': type.toString(),
};
}
}
+74
View File
@@ -0,0 +1,74 @@
class ProfileGetModel {
final int id;
final String name;
final String number;
final String email;
final String password;
final int otp;
final int isVerified;
final String? address;
final String? profilePic1;
final String? updatedAt;
final String createdAt;
final String? vendorStatus;
final String? vendorId;
final String? endDate;
ProfileGetModel({
required this.id,
required this.name,
required this.number,
required this.email,
required this.password,
required this.otp,
required this.isVerified,
this.address,
this.profilePic1,
this.updatedAt,
required this.createdAt,
this.vendorStatus,
this.vendorId,
this.endDate,
});
/// Factory method to create an instance from a JSON map
factory ProfileGetModel.fromJson(Map<String, dynamic> json) {
return ProfileGetModel(
id: int.tryParse(json['id'].toString()) ?? 0,
name: json['name']?.toString() ?? '',
number: json['number']?.toString() ?? '',
email: json['email']?.toString() ?? '',
password: json['password']?.toString() ?? '',
otp: int.tryParse(json['otp'].toString()) ?? 0,
isVerified: int.tryParse(json['is_verified'].toString()) ?? 0,
address: json['address']?.toString(),
profilePic1: json['profile_pic1']?.toString(),
updatedAt: json['updated_at']?.toString(),
createdAt: json['created_at']?.toString() ?? '',
vendorStatus: json['vendor_status']?.toString(),
vendorId: json['vendor_id']?.toString(),
endDate: json['end_date']?.toString(),
);
}
/// Method to convert the object to a JSON map
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'number': number,
'email': email,
'password': password,
'otp': otp,
'is_verified': isVerified,
'address': address,
'profile_pic1': profilePic1,
'updated_at': updatedAt,
'created_at': createdAt,
'vendor_status': vendorStatus,
'vendor_id': vendorId,
'end_date': endDate,
};
}
}
+118
View File
@@ -0,0 +1,118 @@
class ServiceModel {
final int id;
final int vendorId;
final int serviceType;
final String vendorName;
final String serviceName;
final int category;
final int subcategory;
final String workingHours;
final String workingDuration;
final String amount;
final String location;
final String description;
final String details;
final String? videos;
final String createdDate;
final String vendorDisplayName;
final String? profilePic1; // Added for JSON field "profile_pic1"
final String? averageReview;
final String phoneNumber;
final String categoryName;
final String subcategoryName;
final List<String>? images1; // Changed to List<String>?
ServiceModel({
required this.id,
required this.vendorId,
required this.serviceType,
required this.vendorName,
required this.serviceName,
required this.category,
required this.subcategory,
required this.workingHours,
required this.workingDuration,
required this.amount,
required this.location,
required this.description,
required this.details,
this.videos,
required this.createdDate,
required this.vendorDisplayName,
this.profilePic1,
this.averageReview,
required this.phoneNumber,
required this.categoryName,
required this.subcategoryName,
this.images1,
});
factory ServiceModel.fromJson(Map<String, dynamic> json) {
return ServiceModel(
id: json['id'] ?? 0,
vendorId: json['vendor_id'] ?? 0,
serviceType: json['service_type'] ?? 0,
vendorName: json['vendorname']?.toString() ?? '',
serviceName: json['servicename']?.toString() ?? '',
category: json['category'] ?? 0,
subcategory: json['subcategory'] ?? 0,
workingHours: json['workinghours']?.toString() ?? '',
workingDuration: json['workingduration']?.toString() ?? '',
amount: json['amount']?.toString() ?? '',
location: json['location']?.toString() ?? '',
description: json['description']?.toString() ?? '',
details: json['details']?.toString() ?? '',
videos:
(json['videos']?.toString().isNotEmpty ?? false)
? json['videos'].toString()
: null,
createdDate: json['created_date']?.toString() ?? '',
vendorDisplayName: json['vendor_name']?.toString() ?? '',
profilePic1: json['profile_pic1']?.toString(), // New field
averageReview:
(json['average_review']?.toString().isNotEmpty ?? false)
? json['average_review'].toString()
: null,
phoneNumber: json['phone_number']?.toString() ?? '',
categoryName: json['category_name']?.toString() ?? '',
subcategoryName: json['subcategory_name']?.toString() ?? '',
images1:
json['images1'] != null ? List<String>.from(json['images1']) : null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'vendor_id': vendorId,
'service_type': serviceType,
'vendorname': vendorName,
'servicename': serviceName,
'category': category,
'subcategory': subcategory,
'workinghours': workingHours,
'workingduration': workingDuration,
'amount': amount,
'location': location,
'description': description,
'details': details,
'videos': videos,
'created_date': createdDate,
'vendor_name': vendorDisplayName,
'profile_pic1': profilePic1,
'average_review': averageReview,
'phone_number': phoneNumber,
'category_name': categoryName,
'subcategory_name': subcategoryName,
'images1': images1,
};
}
}
+33
View File
@@ -0,0 +1,33 @@
class SignUpModel {
final String name;
final String number;
final String password;
final String email;
SignUpModel({
required this.name,
required this.number,
required this.password,
required this.email,
});
// Optional: toJson for API calls
Map<String, dynamic> toJson() {
return {
'name': name,
'number': number,
'password': password,
'email': email,
};
}
// Optional: fromJson for parsing API responses
factory SignUpModel.fromJson(Map<String, dynamic> json) {
return SignUpModel(
name: json['name'],
number: json['number'],
password: json['password'],
email: json['email'],
);
}
}
+39
View File
@@ -0,0 +1,39 @@
class SubcategoryModel {
final int id;
final int category;
final String name;
final String createdDate;
final String icon1; // Added the missing icon1 field
SubcategoryModel({
required this.id,
required this.category,
required this.name,
required this.createdDate,
required this.icon1, // Added to constructor
});
factory SubcategoryModel.fromJson(Map<String, dynamic> json) {
return SubcategoryModel(
id: json['id'],
category: json['category'],
name: json['name'],
createdDate: json['created_date'],
icon1: json['icon1'], // Parse icon1 from JSON
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'category': category,
'name': name,
'created_date': createdDate,
'icon1': icon1, // Include icon1 in JSON output
};
}
}
+155
View File
@@ -0,0 +1,155 @@
import 'dart:convert';
class UserBookingDetails {
final int id;
final String name;
final String mobileNumber;
final String email;
final String message;
final String serviceDate;
final String serviceTime;
final int serviceId;
final int userId;
final String address;
final int status;
final int type;
final String cancelDate;
final String createdDate;
final String serviceName;
final String vendorName;
final String workingHours;
final String amount;
final List<String> videos;
final String workingDuration;
final String description;
final String? profilePic;
final String? averageReview;
final int isRated;
final String? vendorname;
final String categoryName;
final String subcategoryName;
final List<String> images1;
UserBookingDetails({
required this.id,
required this.name,
required this.mobileNumber,
required this.email,
required this.message,
required this.serviceDate,
required this.serviceTime,
required this.serviceId,
required this.userId,
required this.address,
required this.status,
required this.type,
required this.cancelDate,
required this.createdDate,
required this.serviceName,
required this.vendorName,
required this.workingHours,
required this.amount,
required this.videos,
required this.workingDuration,
required this.description,
this.profilePic,
this.averageReview,
required this.isRated,
this.vendorname,
required this.categoryName,
required this.subcategoryName,
required this.images1,
});
factory UserBookingDetails.fromJson(Map<String, dynamic> json) {
// Helper function to safely convert dynamic data to List<String>
List<String> convertToList(dynamic value) {
if (value == null) return <String>[];
if (value is List) {
return List<String>.from(value.map((x) => x.toString()));
}
if (value is String) {
try {
// Try to parse if it's a JSON string
final parsed = jsonDecode(value);
if (parsed is List) {
return List<String>.from(parsed.map((x) => x.toString()));
}
return [value];
} catch (e) {
return [value];
}
}
return <String>[];
}
return UserBookingDetails(
id: json['id'] ?? 0,
name: json['name'] ?? '',
mobileNumber: json['mobile_number'] ?? '',
email: json['email'] ?? '',
message: json['message'] ?? '',
serviceDate: json['service_date'] ?? '',
serviceTime: json['service_time'] ?? '',
serviceId: json['service_id'] ?? 0,
userId: json['user_id'] ?? 0,
address: json['address'] ?? '',
status: json['status'] ?? 0,
type: json['type'] ?? 0,
cancelDate: json['cancel_date'] ?? '',
createdDate: json['created_date'] ?? '',
serviceName: json['service_name'] ?? '',
vendorName: json['vendorname'] ?? '',
workingHours: json['workinghours'] ?? '',
amount: json['amount'] ?? '',
videos: convertToList(json['videos']),
workingDuration: json['workingduration'] ?? '',
description: json['description'] ?? '',
profilePic: json['profile_pic'],
averageReview: json['average_review']?.toString(),
isRated: json['is_rated'] ?? 0,
vendorname: json['vendor_name'],
categoryName: json['category_name'] ?? '',
subcategoryName: json['subcategory_name'] ?? '',
images1: convertToList(json['images1']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'mobile_number': mobileNumber,
'email': email,
'message': message,
'service_date': serviceDate,
'service_time': serviceTime,
'service_id': serviceId,
'user_id': userId,
'address': address,
'status': status,
'type': type,
'cancel_date': cancelDate,
'created_date': createdDate,
'service_name': serviceName,
'vendorname': vendorName,
'workinghours': workingHours,
'amount': amount,
'videos': videos,
'workingduration': workingDuration,
'description': description,
'profile_pic': profilePic,
'average_review': averageReview,
'is_rated': isRated,
'vendor_name': vendorname,
'category_name': categoryName,
'subcategory_name': subcategoryName,
'images1': images1,
};
}
@override
String toString() {
return 'UserBookingDetails{id: $id, name: $name, mobileNumber: $mobileNumber, email: $email, message: $message, serviceDate: $serviceDate, serviceTime: $serviceTime, serviceId: $serviceId, userId: $userId, address: $address, status: $status, type: $type, cancelDate: $cancelDate, createdDate: $createdDate, serviceName: $serviceName, vendorName: $vendorName, workingHours: $workingHours, amount: $amount, videos: $videos, workingDuration: $workingDuration, description: $description, profilePic: $profilePic, averageReview: $averageReview, isRated: $isRated, vendor_name: $vendorname, categoryName: $categoryName, subcategoryName: $subcategoryName, images1: $images1}';
}
}
@@ -0,0 +1,19 @@
class TermsAndConditionsModel {
final String data;
final String message;
final bool success;
TermsAndConditionsModel({
required this.data,
required this.message,
required this.success,
});
factory TermsAndConditionsModel.fromJson(Map<String, dynamic> json) {
return TermsAndConditionsModel(
data: json['data'] ?? '',
message: json['message'] ?? '',
success: json['success'] ?? false,
);
}
}
@@ -0,0 +1,120 @@
class VendorBookingModel {
int? id;
String? name;
String? mobileNumber;
String? email;
String? message;
String? serviceDate;
String? serviceTime;
int? serviceId;
int? userId;
String? address;
int? status;
String? type;
String? cancelDate;
String? createdDate;
String? serviceName;
String? username;
String? workingHours;
String? categoryName;
String? images1; // Added missing field
String? profilePic1; // Added missing field
VendorBookingModel({
this.id,
this.name,
this.mobileNumber,
this.email,
this.message,
this.serviceDate,
this.serviceTime,
this.serviceId,
this.userId,
this.address,
this.status,
this.type,
this.cancelDate,
this.createdDate,
this.serviceName,
this.username,
this.workingHours,
this.categoryName,
this.images1, // Added to constructor
this.profilePic1, // Added to constructor
});
VendorBookingModel.fromJson(Map<String, dynamic> json) {
// Helper function to safely convert values to int
int? toInt(dynamic value) {
if (value == null) return null;
if (value is int) return value;
if (value is String) {
try {
return int.parse(value);
} catch (e) {
print('Error parsing to int: $value');
return null;
}
}
return null;
}
// Helper function to safely convert values to string
String? toString(dynamic value) {
if (value == null) return null;
return value.toString();
}
id = toInt(json['id']);
name = toString(json['name']);
mobileNumber = toString(json['mobile_number']);
email = toString(json['email']);
message = toString(json['message']);
serviceDate = toString(json['service_date']);
serviceTime = toString(json['service_time']);
serviceId = toInt(json['service_id']);
userId = toInt(json['user_id']);
address = toString(json['address']);
status = toInt(json['status']);
type = toString(json['type']);
cancelDate = toString(json['cancel_date']);
createdDate = toString(json['created_date']);
serviceName = toString(json['service_name']);
username = toString(json['username']);
workingHours = toString(json['workinghours']);
categoryName = toString(json['category_name']);
images1 = toString(json['images1']); // Parse images1 from JSON
profilePic1 = toString(
json['profile_pic1'],
); // Parse profile_pic1 from JSON
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'mobile_number': mobileNumber,
'email': email,
'message': message,
'service_date': serviceDate,
'service_time': serviceTime,
'service_id': serviceId,
'user_id': userId,
'address': address,
'status': status,
'type': type,
'cancel_date': cancelDate,
'created_date': createdDate,
'service_name': serviceName,
'username': username,
'workinghours': workingHours,
'category_name': categoryName,
'images1': images1, // Include images1 in JSON output
'profile_pic1': profilePic1, // Include profile_pic1 in JSON output
};
}
}
@@ -0,0 +1,28 @@
class VendorBookingStatus {
final String id;
final String status;
VendorBookingStatus({
required this.id,
required this.status,
});
factory VendorBookingStatus.fromJson(Map<String, dynamic> json) {
return VendorBookingStatus(
id: json['id']?.toString() ?? "0",
status: json['status']?.toString() ?? "0",
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'status': status
};
}
@override
String toString() {
return 'VendorBookingStatus{id: $id, status: $status}';
}
}
@@ -0,0 +1,43 @@
class VendorCategoriesModel {
int? id;
String? name;
String? createdDate;
int? noOfService;
String? icon1;
String? image1;
VendorCategoriesModel({
this.id,
this.name,
this.createdDate,
this.noOfService,
this.icon1,
this.image1,
});
factory VendorCategoriesModel.fromJson(Map<String, dynamic> json) {
return VendorCategoriesModel(
id: json['id'],
name: json['name'] ?? '',
createdDate: json['created_date'] ?? '',
noOfService: json['no_of_service'] ?? 0,
icon1: json['icon1'] ?? '',
image1: json['image1'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'created_date': createdDate,
'no_of_service': noOfService,
'icon1': icon1,
'image1': image1,
};
}
}
@@ -0,0 +1,75 @@
class VendorGetProfileModel {
final int id;
final String name;
final String email;
final String number;
final String location;
final String pincode;
final String aadharcard;
final String pancard;
final String drivingLicence;
final String passport;
final String? profilePic1;
final int status;
final int userId;
final String createdDate;
final String? endDate;
VendorGetProfileModel({
required this.id,
required this.name,
required this.email,
required this.number,
required this.location,
required this.pincode,
required this.aadharcard,
required this.pancard,
required this.drivingLicence,
required this.passport,
this.profilePic1,
required this.status,
required this.userId,
required this.createdDate,
this.endDate,
});
factory VendorGetProfileModel.fromJson(Map<String, dynamic> json) {
return VendorGetProfileModel(
id: json['id'],
name: json['name'],
email: json['email'],
number: json['number'],
location: json['location'],
pincode: json['pincode'],
aadharcard: json['aadharcard'],
pancard: json['pancard'],
drivingLicence: json['driving_licence'],
passport: json['passport'],
profilePic1: json['profile_pic1'],
status: json['status'],
userId: json['user_id'],
createdDate: json['created_date'],
endDate: json['end_date'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'number': number,
'location': location,
'pincode': pincode,
'aadharcard': aadharcard,
'pancard': pancard,
'driving_licence': drivingLicence,
'passport': passport,
'profile_pic1': profilePic1,
'status': status,
'user_id': userId,
'created_date': createdDate,
'end_date': endDate,
};
}
}
@@ -0,0 +1,74 @@
class VendorProfileModel {
final int id;
final String name;
final String number;
final String email;
final String password;
final int otp;
final int isVerified;
final String? address;
final String? profilePic1;
final String? updatedAt;
final String createdAt;
final String? vendorStatus;
final String? vendorId;
final String? endDate;
VendorProfileModel({
required this.id,
required this.name,
required this.number,
required this.email,
required this.password,
required this.otp,
required this.isVerified,
this.address,
this.profilePic1,
this.updatedAt,
required this.createdAt,
this.vendorStatus,
this.vendorId,
this.endDate,
});
/// Factory method to create an instance from a JSON map
factory VendorProfileModel.fromJson(Map<String, dynamic> json) {
return VendorProfileModel(
id: int.tryParse(json['id'].toString()) ?? 0,
name: json['name']?.toString() ?? '',
number: json['number']?.toString() ?? '',
email: json['email']?.toString() ?? '',
password: json['password']?.toString() ?? '',
otp: int.tryParse(json['otp'].toString()) ?? 0,
isVerified: int.tryParse(json['is_verified'].toString()) ?? 0,
address: json['location']?.toString(),
profilePic1: json['profile_pic1']?.toString(),
updatedAt: json['updated_at']?.toString(),
createdAt: json['created_at']?.toString() ?? '',
vendorStatus: json['vendor_status']?.toString(),
vendorId: json['vendor_id']?.toString(),
endDate: json['end_date']?.toString(),
);
}
/// Method to convert the object to a JSON map
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'number': number,
'email': email,
'password': password,
'otp': otp,
'is_verified': isVerified,
'location': address,
'profile_pic1': profilePic1,
'updated_at': updatedAt,
'created_at': createdAt,
'vendor_status': vendorStatus,
'vendor_id': vendorId,
'end_date': endDate,
};
}
}
@@ -0,0 +1,122 @@
import 'dart:convert';
class VendorServiceModel {
final int id;
final int vendorId;
final int serviceType;
final String vendorName;
final String serviceName;
final int category;
final int subcategory;
final String workingHours;
final String workingDuration;
final String amount;
final String location;
final String description;
final String details;
final String createdDate;
final String categoryName;
final String subcategoryName;
final List<String> video;
final List<String> images1;
VendorServiceModel({
required this.id,
required this.vendorId,
required this.serviceType,
required this.vendorName,
required this.serviceName,
required this.category,
required this.subcategory,
required this.workingHours,
required this.workingDuration,
required this.amount,
required this.location,
required this.description,
required this.details,
required this.createdDate,
required this.categoryName,
required this.subcategoryName,
required this.video,
required this.images1,
});
factory VendorServiceModel.fromJson(Map<String, dynamic> json) {
return VendorServiceModel(
id: json['id'] ?? 0,
vendorId: json['vendor_id'] ?? 0,
serviceType: json['service_type'] ?? 0,
vendorName: json['vendorname'] ?? '',
serviceName: json['servicename'] ?? '',
category: json['category'] ?? 0,
subcategory: json['subcategory'] ?? 0,
workingHours: json['workinghours'] ?? '',
workingDuration: json['workingduration'] ?? '',
amount: json['amount']?.toString() ?? '',
location: json['location'] ?? '',
description: json['description'] ?? '',
details: json['details'] ?? '',
createdDate: json['created_date'] ?? '',
categoryName: json['category_name'] ?? '',
subcategoryName: json['subcategory_name'] ?? '',
video: _parseStringList(json['videos']),
images1: _parseStringList(json['images1']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'vendor_id': vendorId,
'service_type': serviceType,
'vendorname': vendorName,
'servicename': serviceName,
'category': category,
'subcategory': subcategory,
'workinghours': workingHours,
'workingduration': workingDuration,
'amount': amount,
'location': location,
'description': description,
'details': details,
'created_date': createdDate,
'category_name': categoryName,
'subcategory_name': subcategoryName,
'videos': video,
'images1': images1,
};
}
static List<String> _parseStringList(dynamic input) {
if (input == null) return [];
if (input is List) {
return input.map((e) => e.toString()).toList();
}
if (input is String) {
if (input.trim().isEmpty) return [];
if (input.trim().startsWith('[')) {
try {
final parsed = jsonDecode(input);
if (parsed is List) {
return parsed.map((e) => e.toString()).toList();
}
} catch (_) {}
}
if (input.contains(',')) {
return input
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
}
return [input];
}
return [];
}
}
@@ -0,0 +1,74 @@
class VendorServiceUploadModel {
final String id;
final String vendorName;
final String serviceName;
final String serviceType;
final String category;
final String subcategory;
final String workingHours;
final String workingDuration;
final double amount;
final String location;
final String? description;
final String details;
final List<String> images; // This will store image URLs after upload
final List<String> videos; // YouTube video links
VendorServiceUploadModel({
required this.id,
required this.vendorName,
required this.serviceName,
required this.serviceType,
required this.category,
required this.subcategory,
required this.workingHours,
required this.workingDuration,
required this.amount,
required this.location,
this.description,
required this.details,
required this.images,
required this.videos,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'vendorName': vendorName,
'serviceName': serviceName,
'serviceType': serviceType,
'category': category,
'subcategory': subcategory,
'workingHours': workingHours,
'workingDuration': workingDuration,
'amount': amount,
'location': location,
'description': description,
'details': details,
'images': images,
'videos': videos,
};
}
factory VendorServiceUploadModel.fromJson(Map<String, dynamic> json) {
return VendorServiceUploadModel(
id: json['id'] as String,
vendorName: json['vendorName'] as String,
serviceName: json['serviceName'] as String,
serviceType: json['serviceType'] as String,
category: json['category'] as String,
subcategory: json['subcategory'] as String,
workingHours: json['workingHours'] as String,
workingDuration: json['workingDuration'] as String,
amount:
json['amount'] is int
? (json['amount'] as int).toDouble()
: json['amount'] as double,
location: json['location'] as String,
description: json['description'] as String?,
details: json['details'] as String,
images: (json['images'] as List?)?.map((e) => e as String).toList() ?? [],
videos: (json['videos'] as List?)?.map((e) => e as String).toList() ?? [],
);
}
}
@@ -0,0 +1,38 @@
class VendorRegisterModel {
String name;
String number;
String email;
String? location;
String? pincode;
String? aadharCard;
String? panCard;
String? drivingLicense;
String? passport;
String userId;
VendorRegisterModel({
required this.name,
required this.number,
required this.email,
this.location,
this.pincode,
this.aadharCard,
this.panCard,
this.drivingLicense,
this.passport,
this.userId = '',
});
Map<String, dynamic> toJson() => {
'name': name,
'number': number,
'email': email,
'location': location,
'pincode': pincode,
'aadhar_card': aadharCard,
'pan_card': panCard,
'driving_license': drivingLicense,
'passport': passport,
'user_id': userId,
};
}
+33
View File
@@ -0,0 +1,33 @@
class RouterConts {
static const String flashScreen = '/flash';
static const String welcomepage = '/welcomepage';
static const String loginpage = '/loginpage';
static const String siguppage = '/siguppage';
static const String otp = '/otp';
static const String main = '/main';
static const String homescreen = '/homescreen';
static const String packageList = '/packages';
static const String history = '/history';
static const String listservice = '/listservice';
static const String profilemainscreen = '/profilemainscreen';
static const String editprofile = '/editprofile';
static const String categorypage = '/categorypage';
static const String detailserivce = '/detailserivce';
static const String bookingserivce = '/bookingserivce';
static const String changepassword = '/changepassword';
//comman
static const String notification = '/notification';
//--------------------vendor flow-------------------------
static const String vendorwelcome = '/vendorwelcome';
static const String vendorresgister = '/vendorresgister';
static const String vendorhomepage = '/vendorhomepage';
static const String vendorpackage = '/vendorpackage';
static const String vendorcategory = '/vendorcategory';
static const String venodor = '/main';
static const String vendorhistory = '/vendorhistory';
static const String vendorservice = '/vendorservice';
static const String vendorserviceupload = '/vendorserviceupload';
static const String profilemainvendor = '/profilemainvendor';
static const String vendoreditprofile = '/vendoreditprofile';
static const String mostpopluarserviceviewall = '/mostpopluarserviceviewall';
}
+247
View File
@@ -0,0 +1,247 @@
import 'package:bookmywages/model/detail_page_model.dart';
import 'package:bookmywages/model/vendor_model/vendor_service_model.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/view/auth/login_page.dart';
import 'package:bookmywages/view/auth/otp_screen.dart';
import 'package:bookmywages/view/auth/sign_up_screen.dart';
import 'package:bookmywages/view/intro_screens/flash_screen.dart';
import 'package:bookmywages/view/intro_screens/vendor_welcome_page.dart';
import 'package:bookmywages/view/intro_screens/welcome_page.dart';
import 'package:bookmywages/view/user_main_screens/Category_page.dart';
import 'package:bookmywages/view/user_main_screens/booking_screen.dart';
import 'package:bookmywages/view/user_main_screens/detail_service_page.dart';
import 'package:bookmywages/view/user_main_screens/history_screen/history_main_contoller.dart';
import 'package:bookmywages/view/user_main_screens/home_screen.dart';
import 'package:bookmywages/view/user_main_screens/list_service_screen.dart';
import 'package:bookmywages/view/user_main_screens/main_contoller.dart';
import 'package:bookmywages/view/user_main_screens/most_popluar_viewall.dart';
import 'package:bookmywages/view/user_main_screens/notification_page.dart';
import 'package:bookmywages/view/user_main_screens/package_screen.dart';
import 'package:bookmywages/view/user_main_screens/profile_screens/edit_profile.dart';
import 'package:bookmywages/view/user_main_screens/profile_screens/profile_changepass.dart';
import 'package:bookmywages/view/user_main_screens/profile_screens/profile_main_page.dart';
import 'package:bookmywages/view/vendor_main_screens/Vendor_profile/vendor_edit_profile.dart';
import 'package:bookmywages/view/vendor_main_screens/Vendor_profile/vendor_profile_main.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_catgories.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_history_maincontroller.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_service.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_serviceupload.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_homepage.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_maincontoller.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_package_page.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_register_page.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AppRoutes {
static final routes = [
GetPage(name: RouterConts.flashScreen, page: () => const FlashScreen()),
GetPage(name: RouterConts.welcomepage, page: () => const WelcomePage()),
GetPage(name: RouterConts.loginpage, page: () => const LoginScreen()),
GetPage(name: RouterConts.siguppage, page: () => const SignUpScreen()),
GetPage(
name: RouterConts.otp,
page: () => OtpScreen(userId: Get.arguments),
),
// 🎯 Wrap main screens inside MainController
GetPage(
name: RouterConts.main,
page: () => MainController(child: HomeScreen()),
),
GetPage(
name: RouterConts.homescreen,
page: () => MainController(child: HomeScreen()),
),
GetPage(
name: RouterConts.listservice,
page: () {
final arguments = Get.arguments ?? {};
final id = arguments['id']?.toString() ?? '0';
final subcategoryId = arguments['subcategoryId'];
final service = arguments['service'];
final sourceTab = arguments['sourceTab'] ?? 0;
return MainController(
initialBottomIndex: sourceTab,
child: ListServiceScreen(
id: id,
subcategoryId: subcategoryId,
service: service,
),
);
},
),
GetPage(
name: RouterConts.detailserivce,
page: () {
final arguments = Get.arguments ?? {};
// Extract and convert ID to int
int id = 0; // Default value
int sourceTab = 0;
if (arguments is String) {
id = int.tryParse(arguments) ?? 0;
} else if (arguments is Map) {
id = arguments['id'] is int
? arguments['id']
: int.tryParse(arguments['id']?.toString() ?? '') ?? 0;
sourceTab = arguments['sourceTab'] ?? 0;
} else if (arguments is int) {
id = arguments;
}
return MainController(
initialBottomIndex: sourceTab,
child: DetailServicePage(id: id), // Now passing int
);
},
),
GetPage(
name: RouterConts.bookingserivce,
page: () {
// Get the service model from arguments
final arguments = Get.arguments ?? {};
final DetailPageModel service = arguments is DetailPageModel
? arguments
: arguments['service'] as DetailPageModel;
// 🎯 NEW: Get source tab for booking screen
final sourceTab = arguments is Map ? arguments['sourceTab'] ?? 0 : 0;
return MainController(
initialBottomIndex: sourceTab, // 🎯 Maintain tab state
child: BookingScreen(service: service),
);
},
),
GetPage(
name: RouterConts.mostpopluarserviceviewall,
page: () => MainController(
initialBottomIndex: 0, // 🎯 Always from home
child: MostPopluarViewall(),
),
),
GetPage(
name: RouterConts.profilemainscreen,
page: () => MainController(child: ProfileMainPage()),
),
GetPage(
name: RouterConts.editprofile,
page: () => MainController(child: EditProfile()),
),
GetPage(
name: RouterConts.changepassword,
page: () => MainController(child: ProfileChangepass()),
),
GetPage(
name: RouterConts.packageList,
page: () => MainController(child: PackageScreen()),
),
GetPage(
name: RouterConts.categorypage,
page: () => MainController(child: CategoryPage()),
),
GetPage(
name: RouterConts.history,
page: () {
final arguments = Get.arguments;
int? historyTabIndex;
// Handle different argument types
if (arguments is Map) {
// Get history tab index from map
historyTabIndex = arguments['historyTab'];
} else if (arguments is int) {
// For backward compatibility - treat as history tab index
historyTabIndex = arguments;
}
return MainController(
child: HistoryScreen(initialTabIndex: historyTabIndex),
initialBottomIndex: 3, // Always set bottom nav to history (index 3)
);
},
),
GetPage(name: RouterConts.vendorwelcome, page: () => VendorWelcomePage()),
GetPage(
name: RouterConts.vendorresgister,
page: () => VendorRegisterPage(),
),
GetPage(
name: RouterConts.venodor,
page: () => VendorController(child: VendorHomepage()),
),
GetPage(
name: RouterConts.vendorhomepage,
page: () => VendorController(child: VendorHomepage()),
),
GetPage(
name: RouterConts.profilemainvendor,
page: () => VendorController(child: VendorProfileMain()),
),
GetPage(
name: RouterConts.vendoreditprofile,
page: () => VendorController(child: VendorEditProfile()),
),
GetPage(
name: RouterConts.vendorpackage,
page: () => VendorController(child: VendorPackagePage()),
),
GetPage(
name: RouterConts.vendorcategory,
page: () => VendorController(child: VendorCatgories()),
),
GetPage(
name: RouterConts.vendorhistory,
page: () {
final arguments = Get.arguments;
int? historyTabIndex;
// Handle different argument types
if (arguments is Map) {
// Get history tab index from map
historyTabIndex = arguments['historyTab'];
} else if (arguments is int) {
// For backward compatibility - treat as history tab index
historyTabIndex = arguments;
}
return VendorController(
child: VendorHistoryMainController(
initialTabIndex: historyTabIndex, // Pass the tab index
),
);
},
),
GetPage(
name: RouterConts.vendorservice,
page: () {
final args = Get.arguments as Map<String, dynamic>;
return VendorController(
child: VendorService(id: args['id'], tittle: args['tittle']),
);
},
),
GetPage(
name: RouterConts.vendorserviceupload,
page: () {
final VendorServiceModel service = Get.arguments as VendorServiceModel;
return VendorController(child: VendorServiceupload(service: service));
},
),
];
}
File diff suppressed because it is too large. Load diff
+301
View File
@@ -0,0 +1,301 @@
// ignore_for_file: use_build_context_synchronously
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/consts_widgets/comman_button.dart';
import 'package:bookmywages/consts_widgets/comman_textformfiled.dart';
import 'package:bookmywages/model/login_model.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LoginScreen extends ConsumerStatefulWidget {
const LoginScreen({super.key});
@override
ConsumerState<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends ConsumerState<LoginScreen> {
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
bool _obscurePassword = true;
// Add form key and error messages
final _formKey = GlobalKey<FormState>();
String? _emailError;
String? _passwordError;
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
// Validation methods
void _validateEmail(String value) {
if (value.isEmpty) {
setState(() => _emailError = 'Email is required');
} else if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
setState(() => _emailError = 'Enter a valid email');
} else {
setState(() => _emailError = null);
}
}
void _validatePassword(String value) {
if (value.isEmpty) {
setState(() => _passwordError = 'Password is required');
} else if (value.length < 4) {
setState(() => _passwordError = 'Password must be at least 4 characters');
} else {
setState(() => _passwordError = null);
}
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final double horizontalPadding = screenSize.width * 0.05;
final double imageHeight = screenSize.height * 0.45;
return Scaffold(
backgroundColor: AppColors.secondprimary,
resizeToAvoidBottomInset: true,
body: SingleChildScrollView(
child: Column(
children: [
// Image
Image.asset(
AppAssets.login,
width: screenSize.width,
height: imageHeight,
fit: BoxFit.cover,
),
// Overlapping Login Container using Transform
Transform.translate(
offset: const Offset(0, -100),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32.73),
border: Border.all(
width: 1.09,
color: const Color(0xFFA39898),
),
boxShadow: const [
BoxShadow(
offset: Offset(0, -4),
blurRadius: 12.3,
color: Color(0x66E0E0E0),
),
BoxShadow(
offset: Offset(0, 4),
blurRadius: 4,
color: Color(0x66000000),
),
],
color: Colors.white,
),
child: Padding(
padding: EdgeInsets.all(screenSize.width * 0.05),
child: Form(
key: _formKey,
child: Column(
children: [
const SizedBox(height: 20),
const Text(
"Sign in",
style: TextStyle(
fontSize: 32,
fontFamily: "Gilroy-ExtraBold",
fontWeight: FontWeight.w800,
height: 1.0,
letterSpacing: 0.64,
),
),
SizedBox(height: screenSize.height * 0.03),
CommonTextFormField(
hintText: 'Enter your email',
controller: _usernameController,
prefixIcon: const Icon(
Icons.person,
color: AppColors.hittext,
),
errorText: _emailError,
onChanged: (value) {
_validateEmail(value);
},
),
const SizedBox(height: 42),
CommonTextFormField(
hintText: 'Enter your password',
controller: _passwordController,
obscureText: _obscurePassword,
prefixIcon: const Icon(
Icons.lock,
color: AppColors.hittext,
),
errorText: _passwordError,
onChanged: (value) {
_validatePassword(value);
},
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off
: Icons.visibility,
color: AppColors.hittext,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {},
child: const Text(
"Forgot Password?",
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14,
height: 1.0,
letterSpacing: 0.1357,
color: Color(0xFFFF0000),
),
),
),
),
const SizedBox(height: 20),
CommanButton(
text: 'Sign in',
textStyle: const TextStyle(
fontFamily: 'Gilroy-Black',
fontWeight: FontWeight.w800,
fontSize: 20,
height: 1.0,
color: AppColors.secondprimary,
),
onPressed: () {
// Validate all fields
_validateEmail(_usernameController.text);
_validatePassword(_passwordController.text);
if (_emailError == null &&
_passwordError == null) {
final loginModel = LoginModel(
email: _usernameController.text.trim(),
password: _passwordController.text.trim(),
);
ref
.read(
loginFutureProvider(loginModel).future,
)
.then((response) async {
Fluttertoast.showToast(
msg:
'Login Successful: ${response['message']}',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 10.0,
);
final userId = response['data'][0]['id'];
final prefs =
await SharedPreferences.getInstance();
final isVerified = prefs.getString(
'is_verified',
);
if (isVerified == '1') {
Get.toNamed(RouterConts.homescreen);
} else {
Get.toNamed(
RouterConts.otp,
arguments: userId,
); // Replace with your OTP route
}
})
.catchError((error) {
Fluttertoast.showToast(
msg: 'Login Failed: $error',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
fontSize: 10.0,
);
});
} else {
Fluttertoast.showToast(
msg: 'Please fix all errors',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.orange,
textColor: Colors.white,
fontSize: 10.0,
);
}
},
isPrimary: true,
backgroundColor: AppColors.primary,
textColor: AppColors.secondprimary,
width: 230,
),
SizedBox(height: 30),
RichText(
text: TextSpan(
text: "Didn't have account? ",
style: TextStyle(
fontFamily: 'Gilroy-ExtraBold',
fontWeight: FontWeight.w700,
fontSize: 17,
height: 1.0,
letterSpacing: 0.01,
color: Color(0xFF9F9F9F),
),
children: [
TextSpan(
text: "Sign up",
style: TextStyle(color: Color(0xFF0066FF)),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed(RouterConts.siguppage);
},
),
],
),
),
SizedBox(height: 30),
],
),
),
),
),
),
),
],
),
),
);
}
}
+300
View File
@@ -0,0 +1,300 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/consts_widgets/comman_button.dart';
import 'package:bookmywages/model/otp_model.dart' show OtpModel;
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:pinput/pinput.dart';
class OtpScreen extends ConsumerStatefulWidget {
final String? userId;
const OtpScreen({super.key, required this.userId});
@override
ConsumerState<OtpScreen> createState() => _OtpScreenState();
}
class _OtpScreenState extends ConsumerState<OtpScreen> {
String _enteredOtp = '';
bool _isLoading = false;
@override
void initState() {
super.initState();
// No need to parse userId, keeping it as String
}
void _verifyOtp() async {
if (widget.userId!.isEmpty || _enteredOtp.length != 4) {
Fluttertoast.showToast(
msg: "Invalid OTP or missing user information",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
return;
}
setState(() {
_isLoading = true;
});
try {
// Use userId directly as String
final otpModel = OtpModel(
userId: widget.userId.toString(),
otp: _enteredOtp,
);
final result = await ref.read(otpVerificationProvider(otpModel).future);
if (mounted) {
Fluttertoast.showToast(
msg: "Verification Successful: ${result['message'] ?? 'Success'}",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
);
// context.push(RouterConts.homescreen);
}
} catch (e) {
if (mounted) {
Fluttertoast.showToast(
msg: "Verification Failed: ${e.toString()}",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
void _resendOtp() async {
if (widget.userId!.isEmpty) {
Fluttertoast.showToast(
msg: "Missing user information",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
return;
}
setState(() {
_isLoading = true;
});
try {
// Implement resend OTP API call using String userId
// For example:
// final result = await ref.read(resendOtpProvider(widget.userId).future);
if (mounted) {
Fluttertoast.showToast(
msg: "OTP resent successfully",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
);
}
} catch (e) {
if (mounted) {
Fluttertoast.showToast(
msg: "Failed to resend OTP: ${e.toString()}",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final double horizontalPadding = screenSize.width * 0.05;
return WillPopScope(
onWillPop: () async {
// context.push(RouterConts.siguppage);
return false;
},
child: Scaffold(
backgroundColor: AppColors.secondprimary,
resizeToAvoidBottomInset: true,
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
Image.asset(
AppAssets.login,
width: screenSize.width,
fit: BoxFit.cover,
),
Transform.translate(
offset: const Offset(0, -150),
child: Container(
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: horizontalPadding),
padding: EdgeInsets.all(screenSize.width * 0.05),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32.73),
border: Border.all(
width: 1.09,
color: const Color(0xFFA39898),
),
boxShadow: const [
BoxShadow(
offset: Offset(0, -4),
blurRadius: 12.3,
color: Color(0x66E0E0E0),
),
BoxShadow(
offset: Offset(0, 4),
blurRadius: 4,
color: Color(0x66000000),
),
],
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 20),
const Text(
"OTP Verification",
style: TextStyle(
fontSize: 28,
fontFamily: "Gilroy-ExtraBold",
fontWeight: FontWeight.w800,
height: 1.0,
letterSpacing: 0.64,
),
),
SizedBox(height: screenSize.height * 0.03),
const Text(
"Enter the OTP sent to your number",
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 16,
height: 1.0,
letterSpacing: 0.16,
),
),
const SizedBox(height: 27),
Pinput(
length: 4,
defaultPinTheme: PinTheme(
width: 56,
height: 56,
textStyle: const TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.w600,
),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey,
width: 3,
),
),
),
),
focusedPinTheme: PinTheme(
width: 56,
height: 56,
textStyle: const TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.w600,
),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: AppColors.primary,
width: 3,
),
),
),
),
onChanged: (pin) => setState(() => _enteredOtp = pin),
onCompleted:
(pin) => setState(() => _enteredOtp = pin),
),
const SizedBox(height: 40),
RichText(
text: TextSpan(
text: "Didn't receive code? ",
style: const TextStyle(
fontFamily: 'Gilroy-ExtraBold',
fontWeight: FontWeight.w700,
fontSize: 17,
height: 1.0,
letterSpacing: 0.01,
color: Color(0xFF9F9F9F),
),
children: [
TextSpan(
text: "Resend",
style: const TextStyle(
color: Color(0xFF0066FF),
),
recognizer:
TapGestureRecognizer()..onTap = _resendOtp,
),
],
),
),
const SizedBox(height: 40),
CommanButton(
text: 'Verify',
textStyle: const TextStyle(
fontFamily: 'Gilroy-Black',
fontWeight: FontWeight.w800,
fontSize: 20,
height: 1.0,
color: AppColors.secondprimary,
),
onPressed: _verifyOtp,
isPrimary: true,
backgroundColor: AppColors.primary,
textColor: AppColors.secondprimary,
width: 230,
),
const SizedBox(height: 10),
],
),
),
),
],
),
),
),
),
);
}
}
+223
View File
@@ -0,0 +1,223 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/consts_widgets/comman_button.dart';
import 'package:bookmywages/consts_widgets/comman_textformfiled.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
class SignUpScreen extends ConsumerStatefulWidget {
const SignUpScreen({super.key});
@override
ConsumerState<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends ConsumerState<SignUpScreen> {
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _mobilenumberController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
bool _obscurePassword = true;
@override
void dispose() {
_usernameController.dispose();
_mobilenumberController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _onSignUp() async {
final userData = {
'name': _usernameController.text.trim(),
'number': _mobilenumberController.text.trim(),
'email': _emailController.text.trim(),
'password': _passwordController.text.trim(),
};
try {
final result = await ref.read(signupFutureProvider(userData).future);
final userId = result['data'][0]['id'];
if (mounted) {
Fluttertoast.showToast(
msg: "Signup Successful: ${result['message'] ?? 'Success'}",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
);
Get.toNamed(RouterConts.otp, arguments: userId);
// Pass userId to OTP screen
}
} catch (e) {
if (mounted) {
Fluttertoast.showToast(
msg: "Signup Failed: ${e.toString()}",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final double horizontalPadding = screenSize.width * 0.05;
// ignore: deprecated_member_use
return WillPopScope(
onWillPop: () async {
// context.push(RouterConts.welcomepage); // Actually go back
return false; // Prevent default behavior (optional)
},
child: Scaffold(
backgroundColor: AppColors.secondprimary,
resizeToAvoidBottomInset: true,
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
Image.asset(
AppAssets.login,
width: screenSize.width,
fit: BoxFit.cover,
),
Transform.translate(
offset: const Offset(0, -150),
child: Container(
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: horizontalPadding),
padding: EdgeInsets.all(screenSize.width * 0.05),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32.73),
border: Border.all(
width: 1.09,
color: const Color(0xFFA39898),
),
boxShadow: const [
BoxShadow(
offset: Offset(0, -4),
blurRadius: 12.3,
color: Color(0x66E0E0E0),
),
BoxShadow(
offset: Offset(0, 4),
blurRadius: 4,
color: Color(0x66000000),
),
],
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Sign up",
style: TextStyle(
fontSize: 32,
fontFamily: "Gilroy-ExtraBold",
fontWeight: FontWeight.w800,
height: 1.0,
letterSpacing: 0.64,
),
),
SizedBox(height: screenSize.height * 0.03),
CommonTextFormField(
hintText: 'Enter your username',
controller: _usernameController,
prefixIcon: const Icon(
Icons.person,
color: AppColors.hittext,
),
),
const SizedBox(height: 16),
CommonTextFormField(
hintText: 'Enter your mobile number',
controller: _mobilenumberController,
prefixIcon: const Icon(
Icons.phone,
color: AppColors.hittext,
),
),
const SizedBox(height: 16),
CommonTextFormField(
hintText: 'Enter your email',
controller: _emailController,
prefixIcon: const Icon(
Icons.email,
color: AppColors.hittext,
),
),
const SizedBox(height: 16),
CommonTextFormField(
hintText: 'Enter your password',
controller: _passwordController,
obscureText: _obscurePassword,
prefixIcon: const Icon(
Icons.lock,
color: AppColors.hittext,
),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off
: Icons.visibility,
color: AppColors.hittext,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
const SizedBox(height: 15),
CommanButton(
text: 'Sign up',
textStyle: const TextStyle(
fontFamily: 'Gilroy-Black',
fontWeight: FontWeight.w800,
fontSize: 20,
height: 1.0,
color: AppColors.secondprimary,
),
onPressed: _onSignUp,
isPrimary: true,
backgroundColor: AppColors.primary,
textColor: AppColors.secondprimary,
width: 230,
),
const SizedBox(height: 10),
],
),
),
),
],
),
),
),
),
);
}
}
+49
View File
@@ -0,0 +1,49 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:shared_preferences/shared_preferences.dart';
class FlashScreen extends StatefulWidget {
const FlashScreen({super.key});
@override
State<FlashScreen> createState() => _FlashScreenState();
}
class _FlashScreenState extends State<FlashScreen> {
@override
void initState() {
super.initState();
_navigateUser();
}
Future<void> _navigateUser() async {
await Future.delayed(const Duration(seconds: 2));
final prefs = await SharedPreferences.getInstance();
final userId = prefs.getString('userId');
final isVerified = prefs.getString('is_verified');
if (!mounted) return;
if (userId != null && userId.isNotEmpty && isVerified == '1') {
Get.toNamed(RouterConts.homescreen);
} else {
Get.toNamed(RouterConts.welcomepage);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.flashscreen,
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(child: Image.asset(AppAssets.flashscreen)),
),
);
}
}
@@ -0,0 +1,204 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class VendorWelcomePage extends ConsumerStatefulWidget {
const VendorWelcomePage({super.key});
@override
ConsumerState<VendorWelcomePage> createState() => _VendorWelcomePageState();
}
class _VendorWelcomePageState extends ConsumerState<VendorWelcomePage> {
bool _isLoading = true;
@override
void initState() {
super.initState();
_checkProfileStatus();
}
Future<void> _checkProfileStatus() async {
try {
setState(() => _isLoading = true);
final prefs = await SharedPreferences.getInstance();
final vendorId = prefs.getString('vendor_id');
final vendorStatus = prefs.getString('vendor_status');
print('Stored vendor_id: $vendorId');
print('Stored vendor_status: $vendorStatus');
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
if (vendorId == null || vendorId.isEmpty) {
// Case 1: vendor_id is null or empty
Get.offNamed(RouterConts.vendorresgister);
} else if (vendorStatus == '0') {
// Case 2: vendor_id present but vendor_status is null
_showVerificationDialog(context);
} else {
// Case 3: both are present
Get.offNamed(RouterConts.vendorhomepage);
}
} catch (e) {
// ignore: avoid_print
print('Error checking profile status: $e');
if (mounted) {
_showVerificationDialog(context);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: AppColors.secondprimary,
body: LayoutBuilder(
builder: (context, constraints) {
return Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 33.0,
top: 48,
right: 59,
),
child: Align(
alignment: Alignment.topLeft,
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: "Welcome back to ",
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 32,
height: 1.5,
letterSpacing: 0.26,
),
),
TextSpan(
text: "VENDOR LOGIN",
style: TextStyle(
fontFamily: 'Gilroy-ExtraBold',
fontWeight: FontWeight.w800,
fontSize: 32,
height: 1.5,
letterSpacing: 0.26,
color: Color(0xFF3A47C9),
),
),
],
),
),
),
),
Expanded(
child: Center(
child: Image.asset(
AppAssets.vendorwelcome,
fit: BoxFit.contain,
width: constraints.maxWidth * 0.8,
),
),
),
],
);
},
),
),
);
}
void _showVerificationDialog(BuildContext context) {
showDialog(
context: context,
barrierDismissible: false, // Prevent closing by tapping outside
builder: (BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
side: const BorderSide(color: Colors.white, width: 1),
),
elevation: 10,
backgroundColor: Colors.white,
child: Container(
width: 365,
height: 305,
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Verification',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 32,
height: 1.0,
letterSpacing: 0.32,
),
),
const SizedBox(height: 20),
const Text(
'Still your account will not verify by Admin, please wait with us.',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20.23,
height: 1.74,
letterSpacing: 0.2023,
),
),
const SizedBox(height: 30),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0066FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(29.36),
),
padding: const EdgeInsets.symmetric(
horizontal: 50,
vertical: 15,
),
),
onPressed: () {
Navigator.of(context).pop(); // Close dialog
Get.offNamed(RouterConts.homescreen);
},
child: const Text(
'OK',
style: TextStyle(
fontFamily: 'Gilroy-Black',
fontWeight: FontWeight.w400,
fontSize: 20.29,
height: 1.0,
letterSpacing: 0.4058,
color: Colors.white,
),
),
),
],
),
),
);
},
);
}
}
+154
View File
@@ -0,0 +1,154 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/consts_widgets/comman_button.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
class WelcomePage extends StatefulWidget {
const WelcomePage({super.key});
@override
State<WelcomePage> createState() => _WelcomePageState();
}
class _WelcomePageState extends State<WelcomePage> {
@override
Widget build(BuildContext context) {
final Size screenSize = MediaQuery.of(context).size;
final double textScale = MediaQuery.of(context).textScaleFactor;
final bool isMobile = screenSize.width < 600;
final bool isTablet = screenSize.width >= 600 && screenSize.width < 1200;
final bool isDesktop = screenSize.width >= 1200;
final double headingFontSize =
(isMobile ? 25 : (isTablet ? 28 : 32)) * textScale;
final double buttonFontSize =
(isMobile ? 18 : (isTablet ? 22 : 25)) * textScale;
final double horizontalPadding = screenSize.width * 0.05;
final double topPadding = screenSize.height * 0.05;
final double verticalSpacing = screenSize.height * 0.03;
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: SafeArea(
child: SingleChildScrollView(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding.clamp(16.0, 32.0),
vertical: topPadding,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 250),
child: RichText(
textAlign: TextAlign.start,
text: TextSpan(
style: TextStyle(
fontFamily: 'Gilroy',
fontWeight: FontWeight.w900,
fontSize: headingFontSize,
height: 1.65,
letterSpacing: headingFontSize * 0.01,
),
children: const [
TextSpan(
text: "Explore The All Types Of ",
style: TextStyle(color: AppColors.thridprimary),
),
TextSpan(
text: "Service",
style: TextStyle(color: Color(0xFF3A47C9)),
),
],
),
),
),
SizedBox(height: verticalSpacing),
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: isDesktop
? screenSize.width * 0.5
: (isTablet
? screenSize.width * 0.7
: screenSize.width * 0.85),
),
child: Image.asset(
AppAssets.welcome,
fit: BoxFit.contain,
),
),
),
SizedBox(height: verticalSpacing * 1.6),
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: isDesktop
? screenSize.width * 0.3
: (isTablet
? screenSize.width * 0.5
: screenSize.width * 0.85),
),
child: CommanButton(
text: 'Sign in',
textStyle: TextStyle(
fontFamily: 'Gilroy-Black',
fontWeight: FontWeight.w800,
fontSize: buttonFontSize,
height: 1.0,
letterSpacing: buttonFontSize * 0.02,
color: AppColors.secondprimary,
),
onPressed: () => Get.toNamed(RouterConts.loginpage),
isPrimary: true,
backgroundColor: AppColors.primary,
textColor: AppColors.secondprimary,
),
),
),
SizedBox(height: verticalSpacing),
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: isDesktop
? screenSize.width * 0.3
: (isTablet
? screenSize.width * 0.5
: screenSize.width * 0.85),
),
child: CommanButton(
text: 'Sign up',
textStyle: TextStyle(
fontFamily: 'Gilroy-Black',
fontWeight: FontWeight.w800,
fontSize: buttonFontSize,
height: 1.0,
letterSpacing: buttonFontSize * 0.02,
color: AppColors.hittext,
),
onPressed: () => Get.toNamed(RouterConts.siguppage),
isPrimary: false,
),
),
),
SizedBox(height: verticalSpacing),
],
),
),
),
),
),
),
);
}
}
@@ -0,0 +1,350 @@
// ignore: file_names
import 'dart:ui';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/view/user_main_screens/main_contoller.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
class CategoryPage extends ConsumerStatefulWidget {
const CategoryPage({super.key});
@override
ConsumerState<CategoryPage> createState() => _CategoryPageState();
}
class _CategoryPageState extends ConsumerState<CategoryPage> {
final ScrollController _scrollController = ScrollController();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _handleBackNavigation() {
// Use post-frame callback to avoid accessing scaffold geometry during build
WidgetsBinding.instance.addPostFrameCallback((_) {
try {
final indexController = InheritedIndexController.of(context);
indexController?.changeIndex(0);
} catch (e) {
// Handle any potential errors gracefully
debugPrint('Error resetting index: $e');
}
});
}
@override
Widget build(BuildContext context) {
final categoryAsyncValue = ref.watch(categoryListProvider);
return PopScope(
canPop: true,
onPopInvoked: (didPop) {
if (didPop) {
_handleBackNavigation();
}
},
child: Scaffold(
backgroundColor: AppColors.secondprimary,
body: categoryAsyncValue.when(
data: (categories) => _buildSuccessUI(categories),
loading: () => _buildLoadingUI(),
error: (error, _) => _buildErrorUI(error),
),
),
);
}
Widget _buildSuccessUI(List categories) {
return CustomScrollView(
controller: _scrollController,
physics: const BouncingScrollPhysics(),
slivers: [
// Custom App Bar
SliverToBoxAdapter(child: _buildAppBar()),
// Optimized Grid
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 196 / 209,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index >= categories.length) return null;
return _buildCategoryCard(categories[index]);
},
childCount: categories.length,
addAutomaticKeepAlives: false,
addRepaintBoundaries: true,
),
),
),
// Bottom padding
const SliverToBoxAdapter(child: SizedBox(height: 20)),
],
);
}
Widget _buildAppBar() {
return Container(
height: 90,
padding: const EdgeInsets.only(top: 50),
child: Stack(
alignment: Alignment.center,
children: [
const Center(
child: Text(
"Category",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
),
],
),
);
}
Widget _buildCategoryCard(dynamic category) {
return RepaintBoundary(
child: GestureDetector(
onTap: () {
// 🎯 UPDATED: Pass source tab as 2 (Categories tab index)
Get.toNamed(
RouterConts.listservice,
arguments: {
'id': category.id,
'subcategoryId': null,
'service': '0',
'sourceTab': 2, // 🎯 Categories tab is index 2
},
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Stack(
fit: StackFit.expand,
children: [
_buildCategoryImage(category.imageUrl),
_buildImageOverlay(category.name),
],
),
),
),
),
);
}
Widget _buildCategoryImage(String imageUrl) {
return Image.network(
imageUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
color: Colors.grey[300],
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
),
);
},
errorBuilder: (context, error, stackTrace) => Container(
color: Colors.grey[300],
child: const Center(
child: Icon(Icons.image_not_supported, color: Colors.grey, size: 32),
),
),
cacheWidth: 400,
cacheHeight: 400,
);
}
Widget _buildImageOverlay(String categoryName) {
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(12)),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
height: 56,
width: 196,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(255, 255, 255, 0.54),
Color.fromRGBO(153, 153, 153, 0.54),
],
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Center(
child: Text(
categoryName,
style: const TextStyle(
color: Colors.black87,
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14,
height: 1.2,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
),
),
),
);
}
Widget _buildLoadingUI() {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(child: _buildAppBar()),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 196 / 209,
),
delegate: SliverChildBuilderDelegate(
(context, index) => _buildShimmerCard(),
childCount: 6,
),
),
),
],
);
}
Widget _buildShimmerCard() {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.grey[300],
),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
colors: [
Colors.grey[300]!,
Colors.grey[100]!,
Colors.grey[300]!,
],
),
),
),
Positioned(
bottom: 12,
left: 12,
right: 12,
child: Container(
height: 20,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.grey[400],
),
),
),
],
),
);
}
Widget _buildErrorUI(Object error) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(child: _buildAppBar()),
SliverFillRemaining(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'Oops! Something went wrong',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'Please check your connection and try again',
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
// Use post-frame callback for state changes
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.invalidate(categoryListProvider);
});
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
),
),
),
],
);
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,932 @@
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/model/booking_modify_model.dart';
import 'package:bookmywages/model/cancel_booking.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
class ServiceBooking extends ConsumerStatefulWidget {
const ServiceBooking({super.key});
@override
ConsumerState<ServiceBooking> createState() => _ServiceBookingState();
}
class _ServiceBookingState extends ConsumerState<ServiceBooking> {
@override
Widget build(BuildContext context) {
final bookingAsyncValue = ref.watch(userbookinghistorydetailsProvider);
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: bookingAsyncValue.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Error: $err')),
data: (bookings) {
return ListView.builder(
itemCount: bookings.length,
shrinkWrap: false,
itemBuilder: (context, index) {
final booking = bookings[index];
return Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.lightGrey),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Top Row: ID and "View order"
Padding(
padding: const EdgeInsets.only(
right: 12,
left: 12,
top: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID : ${booking.id}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
// Update the onTap method in the ServiceBooking class
GestureDetector(
onTap: () {
Get.toNamed(
RouterConts.detailserivce,
arguments: booking.serviceId,
);
},
child: Text(
'View order',
style: TextStyle(
color: Colors.blue.shade600,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
const Divider(),
/// Image, Company, and Status
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: (booking.images1.isNotEmpty)
? Image.network(
booking.images1[0],
width: 100,
height: 110,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
return child;
}
return Center(
child: CircularProgressIndicator(
value:
loadingProgress
.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
(loadingProgress
.expectedTotalBytes ??
1)
: null,
),
);
},
errorBuilder:
(context, error, stackTrace) {
return const Icon(
Icons.error,
size: 100,
);
},
)
: const Icon(
Icons.image_not_supported,
size: 100,
),
),
Positioned(
top: 6,
left: 6,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
const Text(
'Live',
style: TextStyle(
fontSize: 12,
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
const SizedBox(width: 12),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
booking.vendorName ??
'Arun Cleaning Company',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16.11,
height: 14.5 / 16.11,
letterSpacing: 0.01 * 16.11,
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
booking.serviceName ??
'Cleaning service',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 13.91,
height: 1.3,
letterSpacing: 0.01 * 13.91,
color: Color(0xFF5A5A5A),
),
),
),
const SizedBox(width: 10),
// Updated status display code in the ServiceBooking class
Expanded(
child: Container(
width: booking.status == 3
? 70
: booking.status == 1
? 85 // Width for Scheduled
: 77.84, // Width for Pending and other statuses
height: booking.status == 3
? 25
: booking.status == 1
? 25 // Height for Scheduled
: 27.96, // Height for Pending and other statuses
decoration: BoxDecoration(
color: booking.status == 3
? const Color(
0xFFFFEEEE,
) // #FFEEEE for Cancel
: booking.status == 1
? const Color(
0xFFE6F7E6,
) // Light green for Scheduled
: const Color(
0xFFDAE9FF,
), // #DAE9FF for Pending
borderRadius: BorderRadius.circular(
6.05,
), // 6.05px border radius
),
child: Center(
child: Text(
booking.status == 3
? 'Cancel'
: booking.status == 1
? 'Scheduled' // Text for status 1
: 'Pending', // Text for other statuses
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 10.92,
fontWeight: FontWeight.w400,
color: booking.status == 3
? const Color(
0xFFFF0000,
) // Red color for Cancel
: booking.status == 1
? const Color(
0xFF2E8B57,
) // Green color for Scheduled
: const Color(
0xFF0066FF,
), // Blue color for Pending
letterSpacing: 1.0,
height: 0.98,
),
),
),
),
),
],
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
color: AppColors.lightGrey,
),
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 15,
backgroundImage:
booking.profilePic != null &&
booking.profilePic!.isNotEmpty
? NetworkImage(
booking.profilePic.toString(),
)
: null,
child:
booking.profilePic == null ||
booking.profilePic!.isEmpty
? const Icon(
Icons.person,
size: 20,
)
: null,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
booking.vendorName ??
'no data',
style: const TextStyle(
fontFamily:
'Gilroy-Medium',
fontWeight:
FontWeight.w600,
fontSize: 11,
height: 10.65 / 11.84,
letterSpacing: 0,
color: Color(
0xFF353434,
),
),
overflow:
TextOverflow.visible,
),
),
const Icon(
Icons.star,
color: Colors.orange,
size: 15,
),
const SizedBox(width: 4),
// Fixed: Changed to properly handle isRated as an integer
Text(
'${booking.isRated}',
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily:
'SF UI Display',
fontWeight:
FontWeight.w800,
fontSize: 10,
height: 12.5 / 8.82,
letterSpacing: -0.31,
),
),
],
),
SizedBox(height: 7),
Text(
booking.categoryName,
style: const TextStyle(
fontFamily: 'Gilroy-Regular',
fontWeight: FontWeight.w400,
fontSize: 11.84,
height: 10.65 / 11.84,
letterSpacing: 0,
color: Color(0xFF717171),
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
/// Date / Time / Hours
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Date :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.serviceDate ?? 'April 23, 2024',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Time :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.serviceTime ?? '12:00 PM',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Working hours :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
SizedBox(height: 10),
Text(
booking.workingHours,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
],
),
),
const SizedBox(height: 20),
/// Canceled Booking Button
/// Buttons Section - Modified for Scheduled Status
booking.status != 3
? Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
child: Row(
mainAxisAlignment: booking.status == 1
? MainAxisAlignment
.end // Align to the right for Scheduled status
: MainAxisAlignment
.spaceBetween, // Normal alignment for other statuses
children: [
// Only show Modification button if status is NOT Scheduled (status 1)
booking.status != 1
? Expanded(
child: SizedBox(
height: 42,
child: ElevatedButton(
onPressed: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(
20,
),
),
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(
color: AppColors
.secondprimary,
borderRadius:
BorderRadius.circular(
16,
),
),
child:
BookingModificationDialog(
id: booking.id
.toString(),
serviceId: booking
.serviceId
.toString(),
),
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(
0xFF0066FF,
),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(20),
),
),
child: const FittedBox(
child: Text(
"Modification",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w800,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0.289,
color: Colors.white,
),
),
),
),
),
)
: const SizedBox.shrink(), // Empty space when Scheduled
booking.status != 1
? const SizedBox(width: 8)
: const SizedBox.shrink(),
// Cancel Button - Always visible
GestureDetector(
onTap: () async {
final data = CancelBookingRequest(
id: booking.id.toString(),
serviceId: booking.serviceId.toString(),
type: booking.type == 0
? "0"
: booking.type.toString(),
);
try {
// Call the cancel API
await ref.read(
cancelbookingProvider(data).future,
);
// Show success message
Fluttertoast.showToast(
msg: 'Booking cancelled successfully',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
);
// Refresh the booking history data
ref.invalidate(
userbookinghistorydetailsProvider,
);
} catch (e) {
// Show error message
Fluttertoast.showToast(
msg: 'Failed to cancel booking: $e',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'Cancel Booking',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
)
: const SizedBox.shrink(),
const SizedBox(height: 20),
],
),
);
},
);
},
),
),
);
}
// Updated _loadBookingImage and related methods to handle images properly
// Check if URL is likely to cause a 500 error
}
class BookingModificationDialog extends ConsumerStatefulWidget {
final String id;
final String serviceId;
const BookingModificationDialog({
super.key,
required this.id,
required this.serviceId,
});
@override
_BookingModificationDialogState createState() =>
_BookingModificationDialogState();
}
class _BookingModificationDialogState
extends ConsumerState<BookingModificationDialog> {
int selectedDateIndex = 0;
int selectedTimeIndex = 0;
DateTime currentMonth = DateTime.now();
late List<DateTime> dateList;
List<String> times = [];
@override
void initState() {
super.initState();
_generateDateList();
_generateTimes();
}
void _generateDateList() {
final firstDay = DateTime(currentMonth.year, currentMonth.month, 1);
final lastDay = DateTime(currentMonth.year, currentMonth.month + 1, 0);
dateList = List.generate(
lastDay.day,
(index) => firstDay.add(Duration(days: index)),
);
selectedDateIndex = 0;
}
void _generateTimes() {
times = [];
for (int hour = 0; hour < 24; hour++) {
for (int minute = 0; minute < 60; minute += 30) {
final time = DateTime(0, 0, 0, hour, minute);
times.add(DateFormat('h:mm a').format(time));
}
}
}
void _goToPreviousMonth() {
final newMonth = DateTime(currentMonth.year, currentMonth.month - 1);
if (newMonth.isBefore(
DateTime(DateTime.now().year, DateTime.now().month, 1),
)) {
return;
}
setState(() {
currentMonth = newMonth;
_generateDateList();
});
}
void _goToNextMonth() {
setState(() {
currentMonth = DateTime(currentMonth.year, currentMonth.month + 1);
_generateDateList();
});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
top: 20,
left: 20,
right: 20,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Booking Modification',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Change Date',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
Icon(Icons.calendar_today),
],
),
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: _goToPreviousMonth,
icon: Icon(Icons.arrow_back_ios),
),
Text(
DateFormat('MMMM yyyy').format(currentMonth),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
IconButton(
onPressed: _goToNextMonth,
icon: Icon(Icons.arrow_forward_ios),
),
],
),
SizedBox(height: 10),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(dateList.length, (index) {
final date = dateList[index];
final dayLabel =
'${DateFormat('E').format(date)}\n${DateFormat('d').format(date)}';
final isSelected = selectedDateIndex == index;
return GestureDetector(
onTap: () {
setState(() {
selectedDateIndex = index;
});
},
child: Container(
margin: EdgeInsets.symmetric(horizontal: 5),
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.grey.shade200,
borderRadius: BorderRadius.circular(20),
),
child: Text(
dayLabel,
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected ? Colors.white : Colors.black,
fontWeight: FontWeight.bold,
),
),
),
);
}),
),
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Change Time',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
Icon(Icons.access_time),
],
),
SizedBox(height: 10),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(times.length, (index) {
final isSelected = selectedTimeIndex == index;
return GestureDetector(
onTap: () {
setState(() {
selectedTimeIndex = index;
});
},
child: Container(
margin: EdgeInsets.symmetric(horizontal: 5),
padding: EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.grey.shade200,
borderRadius: BorderRadius.circular(30),
),
child: Text(
times[index],
style: TextStyle(
color: isSelected ? Colors.white : Colors.black,
fontWeight: FontWeight.bold,
),
),
),
);
}),
),
),
SizedBox(height: 20),
// GestureDetector(
// onTap: () {
// Navigator.pop(context);
// },
// child: Container(
// width: double.infinity,
// padding: EdgeInsets.all(15),
// decoration: BoxDecoration(
// color: Colors.red.shade50,
// borderRadius: BorderRadius.circular(15),
// ),
// child: Center(
// child: Text(
// 'Cancel the Booking',
// style: TextStyle(
// color: Colors.red,
// fontWeight: FontWeight.bold,
// fontSize: 16,
// ),
// ),
// ),
// ),
// ),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
final selectedDate = DateFormat(
'yyyy-MM-dd',
).format(dateList[selectedDateIndex]);
final selectedTime = times[selectedTimeIndex];
final model = BookingModifyModel(
id: widget.id,
serviceId: widget.serviceId,
servicedate: selectedDate,
servicetime: selectedTime,
);
final result = await ref
.read(modifybookingProvider(model).future)
.catchError((e) {
Fluttertoast.showToast(
msg: 'Error: ${e.toString()}',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
return false;
});
if (result) {
Fluttertoast.showToast(
msg: 'Booking modified successfully',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
);
Navigator.pop(context);
ref.invalidate(userbookinghistorydetailsProvider);
} else {
Fluttertoast.showToast(
msg: 'Failed to modify booking',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
padding: EdgeInsets.symmetric(horizontal: 100, vertical: 15),
),
child: Text('Save', style: TextStyle(fontSize: 18)),
),
SizedBox(height: 20),
],
),
),
);
}
}
@@ -0,0 +1,460 @@
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
// Make sure to import your custom colors and dialog widget
// Assuming this exists
class CanceledPage extends ConsumerWidget {
// Changed to ConsumerWidget
const CanceledPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Added WidgetRef parameter
final bookingAsyncValue = ref.watch(userbookinghistorydetailsProvider);
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: bookingAsyncValue.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Error: $err')),
data: (bookings) {
// Filter only canceled bookings (status == 3)
final canceledBookings =
bookings.where((booking) => booking.status == 3).toList();
if (canceledBookings.isEmpty) {
return const Center(
child: Text(
'No canceled bookings found',
style: TextStyle(fontSize: 16),
),
);
}
return ListView.builder(
itemCount: canceledBookings.length,
shrinkWrap: false,
itemBuilder: (context, index) {
final booking = canceledBookings[index];
return Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.lightGrey),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Top Row: ID and "View order"
Padding(
padding: const EdgeInsets.only(
right: 12,
left: 12,
top: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID : ${booking.id}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
const Divider(),
/// Image, Company, and Status
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child:
(booking.images1.isNotEmpty)
? Image.network(
booking.images1[0],
width: 100,
height: 110,
fit: BoxFit.cover,
loadingBuilder: (
context,
child,
loadingProgress,
) {
if (loadingProgress == null) {
return child;
}
return Center(
child: CircularProgressIndicator(
value:
loadingProgress
.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
(loadingProgress
.expectedTotalBytes ??
1)
: null,
),
);
},
errorBuilder: (
context,
error,
stackTrace,
) {
return const Icon(
Icons.error,
size: 100,
);
},
)
: const Icon(
Icons.image_not_supported,
size: 100,
),
),
Positioned(
top: 6,
left: 6,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
const Text(
'Live',
style: TextStyle(
fontSize: 12,
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
const SizedBox(width: 12),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
booking.vendorName ?? 'No vendor name',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16.11,
height: 14.5 / 16.11,
letterSpacing: 0.01 * 16.11,
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
booking.serviceName ??
'No service name',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 13.91,
height: 12.52 / 13.91,
letterSpacing: 0.01 * 13.91,
color: Color(0xFF5A5A5A),
),
),
const SizedBox(width: 10),
Container(
width: 70,
height: 25,
decoration: BoxDecoration(
color: const Color(0xFFFFEEEE),
borderRadius: BorderRadius.circular(
6.05,
),
),
child: Center(
child: Text(
'Cancel',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 10.92,
fontWeight: FontWeight.w400,
color: const Color(0xFFFF0000),
letterSpacing: 1.0,
height: 0.98,
),
),
),
),
],
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
color: AppColors.lightGrey,
),
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 15,
backgroundImage:
booking.profilePic != null &&
booking
.profilePic!
.isNotEmpty
? NetworkImage(
booking.profilePic!,
)
: null,
child:
booking.profilePic == null ||
booking
.profilePic!
.isEmpty
? const Icon(
Icons.person,
size: 20,
)
: null,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
booking.vendorName ??
'No data',
style: const TextStyle(
fontFamily:
'Gilroy-Medium',
fontWeight:
FontWeight.w600,
fontSize: 11,
height: 10.65 / 11.84,
letterSpacing: 0,
color: Color(
0xFF353434,
),
),
overflow:
TextOverflow.ellipsis,
),
),
const Icon(
Icons.star,
color: Colors.orange,
size: 15,
),
const SizedBox(width: 4),
Text(
'${booking.isRated ?? '0.0'}',
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily:
'SF UI Display',
fontWeight:
FontWeight.w800,
fontSize: 10,
height: 12.5 / 8.82,
letterSpacing: -0.31,
),
),
],
),
const SizedBox(height: 7),
Text(
booking.categoryName ??
'No category',
style: const TextStyle(
fontFamily: 'Gilroy-Regular',
fontWeight: FontWeight.w400,
fontSize: 11.84,
height: 10.65 / 11.84,
letterSpacing: 0,
color: Color(0xFF717171),
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
/// Date / Time / Hours
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Date :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.cancelDate != null
? _formatDate(booking.cancelDate)
: 'No date',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Time :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.cancelDate != null
? DateFormat('h:mm a').format(
DateTime.parse(booking.cancelDate),
)
: 'No time',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Working hours :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.workingHours ?? 'No hours',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
],
),
),
const SizedBox(height: 20),
],
),
);
},
);
},
),
),
);
}
String _formatDate(String dateString) {
try {
return DateFormat('dd/MM/yyyy').format(DateTime.parse(dateString));
} catch (e) {
return dateString; // Fallback: return the original string if parsing fails
}
}
}
@@ -0,0 +1,446 @@
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
// Make sure to import your custom colors and dialog widget
// Assuming this exists
class CompletedPage extends ConsumerWidget {
// Changed to ConsumerWidget
const CompletedPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Added WidgetRef parameter
final bookingAsyncValue = ref.watch(userbookinghistorydetailsProvider);
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: bookingAsyncValue.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Error: $err')),
data: (bookings) {
// Filter only canceled bookings (status == 3)
final canceledBookings =
bookings.where((booking) => booking.status == 1).toList();
if (canceledBookings.isEmpty) {
return const Center(
child: Text(
'No canceled bookings found',
style: TextStyle(fontSize: 16),
),
);
}
return ListView.builder(
itemCount: canceledBookings.length,
shrinkWrap: false,
itemBuilder: (context, index) {
final booking = canceledBookings[index];
return Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.lightGrey),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Top Row: ID and "View order"
Padding(
padding: const EdgeInsets.only(
right: 12,
left: 12,
top: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID : ${booking.id}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
const Divider(),
/// Image, Company, and Status
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child:
booking.images1.isNotEmpty
? Image.network(
booking.images1[0],
width: 100,
height: 110,
fit: BoxFit.cover,
loadingBuilder: (
context,
child,
loadingProgress,
) {
if (loadingProgress == null) {
return child;
} else {
return Center(
child: CircularProgressIndicator(
value:
loadingProgress
.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
(loadingProgress
.expectedTotalBytes ??
1)
: null,
),
);
}
},
errorBuilder: (
context,
error,
stackTrace,
) {
return const Icon(
Icons.error,
size: 100,
);
},
)
: const Icon(
Icons.image_not_supported,
size: 100,
),
),
),
Positioned(
top: 6,
left: 6,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
const Text(
'Live',
style: TextStyle(
fontSize: 12,
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
const SizedBox(width: 12),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
booking.vendorName ?? 'No vendor name',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16.11,
height: 14.5 / 16.11,
letterSpacing: 0.01 * 16.11,
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
booking.serviceName ??
'No service name',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 13.91,
height: 12.52 / 13.91,
letterSpacing: 0.01 * 13.91,
color: Color(0xFF5A5A5A),
),
),
const SizedBox(width: 10),
Container(
width: 70,
height: 25,
decoration: BoxDecoration(
color: const Color(0xffFFEEDB),
borderRadius: BorderRadius.circular(
6.05,
),
),
child: Center(
child: Text(
'Completed',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 10.92,
fontWeight: FontWeight.w400,
color: const Color(0xffFF8515),
letterSpacing: 1.0,
height: 0.98,
),
),
),
),
],
),
const SizedBox(height: 10),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'Date :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.serviceDate != null
? _formatDate(
booking.serviceDate,
)
: 'No date',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'Time :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.serviceDate != null
? DateFormat('h:mm a').format(
DateTime.parse(
booking.serviceDate,
),
)
: 'No time',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
],
),
],
),
),
],
),
),
const SizedBox(height: 16),
/// Date / Time / Hours
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
"Mobile number : ",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
SizedBox(width: 7),
Text(
booking.mobileNumber,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
SizedBox(height: 10),
Row(
children: [
Text(
"E-mail ID :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
SizedBox(width: 7),
Text(
booking.email,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
SizedBox(height: 10),
Row(
children: [
Text(
"Address : ",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
SizedBox(width: 7),
Text(
booking.address,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
SizedBox(height: 10),
Text(
"Message : ",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
SizedBox(height: 7),
Text(
booking.message,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
),
const SizedBox(height: 20),
],
),
);
},
);
},
),
),
);
}
String _formatDate(String dateString) {
try {
return DateFormat('dd/MM/yyyy').format(DateTime.parse(dateString));
} catch (e) {
return dateString; // Fallback: return the original string if parsing fails
}
}
}
@@ -0,0 +1,376 @@
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
class EnquriyPage extends ConsumerStatefulWidget {
const EnquriyPage({super.key});
@override
ConsumerState<EnquriyPage> createState() => _EnquriyPageState();
}
class _EnquriyPageState extends ConsumerState<EnquriyPage> {
int? expandedIndex;
@override
Widget build(BuildContext context) {
final enquiryListAsync = ref.watch(enquriylistProvider);
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: enquiryListAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
data: (enquiries) => SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: List.generate(enquiries.length, (index) {
final enquiry = enquiries[index];
final isExpanded = expandedIndex == index;
return SizedBox(
width: double.infinity,
child: Column(
children: [
Container(
margin: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
decoration: BoxDecoration(
color: AppColors.secondprimary,
borderRadius: BorderRadius.circular(11),
border: Border.all(
color: const Color(0xFFE8E8E8),
width: 1,
),
boxShadow: const [
BoxShadow(
color: Color(0x66AEAEAE),
offset: Offset(0, 2),
blurRadius: 4,
),
],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: (enquiry.images1.isEmpty)
? Container(
width: 89,
height: 64,
color: Colors.grey[300],
child: Icon(
Icons.image,
color: Colors.grey[600],
),
)
: Image.network(
enquiry.images1.first,
width: 89,
height: 64,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) =>
Container(
width: 89,
height: 64,
color: Colors.grey[300],
child: Icon(
Icons.broken_image,
color: Colors.grey[600],
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
enquiry.vendorName ?? 'vendorName',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16,
height: 18.14 / 16,
letterSpacing: 0.16,
color: Colors.black,
),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
setState(() {
expandedIndex = isExpanded
? null
: index;
});
},
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
enquiry.serviceName,
textAlign: TextAlign.left,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 16.11,
height: 17.3 / 16.11,
letterSpacing: 0.1611,
color: Colors.black,
),
),
),
const SizedBox(width: 4),
Flex(
direction: Axis.horizontal,
mainAxisSize: MainAxisSize.min,
children: [
Text(
isExpanded
? "View Less"
: "View More",
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 13.15,
height: 5.71 / 13.15,
color: Color(0xFFFF3D00),
),
),
const SizedBox(width: 5),
Icon(
isExpanded
? Icons.keyboard_arrow_up
: Icons
.keyboard_arrow_down,
color: const Color(
0xffFF3D00,
),
),
],
),
],
),
),
],
),
),
],
),
],
),
),
),
if (isExpanded)
Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.43),
border: Border.all(
color: const Color(0xFFE8E8E8),
width: 0.82,
),
boxShadow: const [
BoxShadow(
color: Color(0xA9A9A940),
blurRadius: 3.29,
offset: Offset(0, 0.82),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text.rich(
TextSpan(
children: [
const TextSpan(
text: "Name : ",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 13,
height: 1.239,
),
),
TextSpan(
text: enquiry.name ?? 'No Name',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 13,
height: 1.239,
color: Color(0xFF373636),
),
),
],
),
),
IconButton(
icon: const Icon(
Icons.delete,
color: AppColors.red,
size: 20,
),
onPressed: () async {
final shouldDelete = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Enquiry'),
content: const Text(
'Are you sure you want to delete this enquiry?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(
context,
).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () =>
Navigator.of(context).pop(true),
child: const Text('Delete'),
),
],
),
);
if (shouldDelete == true) {
Fluttertoast.showToast(
msg: 'Deleting enquiry...',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.grey,
textColor: Colors.white,
);
try {
final success = await ref.read(
enquriydeleteProvider(
enquiry.id.toString(),
).future,
);
if (success) {
// Reset expanded index and refresh the list
setState(() {
expandedIndex = null;
});
await Future.delayed(
const Duration(milliseconds: 300),
);
ref.invalidate(enquriylistProvider);
Fluttertoast.showToast(
msg: 'Enquiry deleted successfully',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
);
} else {
Fluttertoast.showToast(
msg: 'Failed to delete enquiry',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
} catch (e) {
Fluttertoast.showToast(
msg: 'Error: ${e.toString()}',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
},
),
],
),
const SizedBox(height: 8),
_infoText(
"Mobile number : ",
enquiry.mobile ?? 'No Mobile',
),
const SizedBox(height: 13),
_infoText(
"E-mail Id : ",
enquiry.email ?? 'No Email',
),
const SizedBox(height: 13),
_infoText(
"Message : ",
enquiry.message ?? 'No Message',
),
],
),
),
],
),
);
}),
),
),
),
);
}
Widget _infoText(String label, String value) {
return Text.rich(
TextSpan(
children: [
TextSpan(
text: label,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 13,
height: 1.239,
),
),
TextSpan(
text: value,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 13,
height: 1.239,
color: Color(0xFF373636),
),
),
],
),
);
}
}
@@ -0,0 +1,175 @@
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/view/user_main_screens/history_screen/Service_Booking.dart';
import 'package:bookmywages/view/user_main_screens/history_screen/canceled_page.dart';
import 'package:bookmywages/view/user_main_screens/history_screen/completed_screen.dart';
import 'package:bookmywages/view/user_main_screens/history_screen/enquriy_page.dart';
import 'package:bookmywages/view/user_main_screens/history_screen/payment_deatils.dart';
import 'package:bookmywages/view/user_main_screens/main_contoller.dart';
import 'package:flutter/material.dart';
class HistoryScreen extends StatefulWidget {
final int? initialTabIndex;
const HistoryScreen({super.key, this.initialTabIndex});
@override
State<HistoryScreen> createState() => _HistoryScreenState();
}
class _HistoryScreenState extends State<HistoryScreen>
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
int _currentIndex = 0;
TabController? _tabController;
@override
bool get wantKeepAlive => true;
final List<String> tabTitles = [
"Service Booking", // Index 0
"Enquiry list", // Index 1
"Payment Details", // Index 2
"Completed Booking", // Index 3
"Cancelation Booking", // Index 4
];
@override
void initState() {
super.initState();
// Set initial tab index from widget parameter
if (widget.initialTabIndex != null &&
widget.initialTabIndex! >= 0 &&
widget.initialTabIndex! < tabTitles.length) {
_currentIndex = widget.initialTabIndex!;
}
// Initialize TabController with the correct initial index
_tabController = TabController(
length: tabTitles.length,
vsync: this,
initialIndex: _currentIndex,
);
_tabController!.addListener(() {
if (_tabController!.indexIsChanging) {
setState(() {
_currentIndex = _tabController!.index;
});
}
});
}
@override
void dispose() {
_tabController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
if (_tabController == null) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
return PopScope(
onPopInvoked: (didPop) {
if (didPop) {
final indexController = InheritedIndexController.of(context);
indexController?.changeIndex(0);
}
},
child: Scaffold(
backgroundColor: Colors.white,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(150.0),
child: Column(
children: [
AppBar(
backgroundColor: Colors.white,
elevation: 0,
title: const Text(
"History",
style: TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
centerTitle: true,
),
Container(
margin: const EdgeInsets.only(top: 16.0),
alignment: Alignment.centerLeft,
height: 65,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: TabBar(
controller: _tabController,
isScrollable: true,
tabAlignment: TabAlignment.start,
dividerColor: Colors.transparent,
indicatorColor: Colors.transparent,
labelPadding: const EdgeInsets.symmetric(horizontal: 8.0),
tabs: List.generate(
tabTitles.length,
(index) => _buildCustomTab(
tabTitles[index],
index == _currentIndex,
),
),
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
),
),
),
],
),
),
body: TabBarView(
controller: _tabController,
physics: const NeverScrollableScrollPhysics(),
children: const [
ServiceBooking(), // Index 0
EnquriyPage(), // Index 1
PaymentDetails(), // Index 2
CompletedPage(), // Index 3
CanceledPage(), // Index 4
],
),
),
);
}
Widget _buildCustomTab(String text, bool isSelected) {
return Tab(
height: 53,
child: Container(
width: 180,
height: 45,
decoration: BoxDecoration(
color: isSelected ? AppColors.primary : AppColors.secondprimary,
border: Border.all(color: const Color(0xFFDBDBDB)),
borderRadius: BorderRadius.circular(15),
),
alignment: Alignment.center,
child: Text(
text,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w400,
fontSize: 15,
height: 1.0,
letterSpacing: 0.5,
color: isSelected ? AppColors.secondprimary : AppColors.lightgray,
),
),
),
);
}
}
@@ -0,0 +1,448 @@
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:intl/intl.dart';
class PaymentDetails extends ConsumerStatefulWidget {
const PaymentDetails({super.key});
@override
ConsumerState<PaymentDetails> createState() => _PaymentDetailsState();
}
class _PaymentDetailsState extends ConsumerState<PaymentDetails> {
List<dynamic> _localPayments = [];
@override
Widget build(BuildContext context) {
final paymentAsync = ref.watch(paymentdetailsProvider);
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: paymentAsync.when(
data: (payments) {
final filteredPayments = payments.where((p) => p.type == 1).toList();
// Initialize local copy only once
if (_localPayments.isEmpty) {
_localPayments = List.from(filteredPayments);
}
if (_localPayments.isEmpty) {
return const Center(child: Text("No payment history found."));
}
return RefreshIndicator(
onRefresh: () async {
final refreshed = await ref.refresh(
paymentdetailsProvider.future,
);
setState(() {
_localPayments = refreshed.where((p) => p.type == 2).toList();
});
},
child: ListView.builder(
itemCount: _localPayments.length,
itemBuilder: (context, index) {
final payment = _localPayments[index];
return Dismissible(
key: Key(payment.id.toString()),
direction: DismissDirection.endToStart,
background: Container(
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(20),
),
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (direction) async {
return await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Confirm Delete"),
content: const Text(
"Are you sure you want to delete this payment record?",
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text("CANCEL"),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text(
"DELETE",
style: TextStyle(color: Colors.red),
),
),
],
);
},
);
},
onDismissed: (direction) async {
Fluttertoast.showToast(
msg: 'Deleting payment...',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.grey,
textColor: Colors.white,
);
final success = await ref.read(
paymentdeleteProvider(payment.id.toString()).future,
);
if (success) {
setState(() {
_localPayments.removeAt(index);
});
Fluttertoast.showToast(
msg: '${payment.planName} deleted successfully',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
);
} else {
Fluttertoast.showToast(
msg: 'Failed to delete payment',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
},
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(0xFFCFCFCF),
width: 1,
),
),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 130,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF334E95),
borderRadius: BorderRadius.circular(12.86),
border: Border.all(
color: Colors.white,
width: 0.43,
),
boxShadow: [
BoxShadow(
color: const Color(0x40828282),
blurRadius: 1.71,
offset: const Offset(0, 2.14),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
payment.planName,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14,
height: 1.0,
letterSpacing: 0.1371,
color: Colors.white,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 15),
Row(
children: [
Container(
width: 6.86,
height: 6.86,
decoration: const BoxDecoration(
color: Color(0xFFF9E369),
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
_formatDuration(payment.duration),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 12,
height: 1.0,
letterSpacing: 0.0771,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 10),
Row(
children: [
Container(
width: 6.86,
height: 6.86,
decoration: const BoxDecoration(
color: Color(0xFFF9E369),
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
payment.description,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 12,
height: 1.0,
letterSpacing: 0.0771,
color: Colors.white,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
const Divider(color: Colors.white),
Center(
child: Text(
_formatPrice(payment.price),
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
payment.planName,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 19.9,
height: 1.0,
letterSpacing: 0.199,
color: Colors.black,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 10),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"Start :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 15.61,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Colors.black,
),
),
const SizedBox(height: 10),
Text(
DateFormat('MMM d, y').format(
DateTime.parse(
payment.createdDate,
),
),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w700,
fontSize: 12,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Color(0xFF636363),
),
),
],
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"End :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 15.61,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Colors.black,
),
),
const SizedBox(height: 10),
Text(
DateFormat('MMM d, y').format(
DateTime.parse(payment.endDate),
),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w700,
fontSize: 12,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Color(0xFF636363),
),
),
],
),
],
),
const SizedBox(height: 10),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"Time :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 15.61,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Colors.black,
),
),
const SizedBox(height: 10),
Text(
DateFormat('hh:mm a').format(
DateTime.parse(
payment.createdDate,
),
),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w700,
fontSize: 12,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Color(0xFF636363),
),
),
],
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"Payment :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 15.61,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Colors.black,
),
),
const SizedBox(height: 10),
Text(
_formatPrice(payment.price),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w700,
fontSize: 12,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Color(0xFF636363),
),
),
],
),
],
),
],
),
),
),
],
),
),
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text("Error: ${e.toString()}")),
),
);
}
static String _formatDuration(int days) {
if (days == 30) return '1 Month';
if (days == 60) return '2 Months';
if (days == 90) return '3 Months';
if (days == 180) return '6 Months';
if (days == 365) return '1 Year';
return '$days Days';
}
static String _formatPrice(String price) {
if (price.startsWith('Rs.')) {
return price;
}
return 'Rs.$price';
}
}
File diff suppressed because it is too large. Load diff
+116
View File
@@ -0,0 +1,116 @@
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:get/get.dart';
class ImagePage extends StatefulWidget {
final List<String> mediaUrls;
const ImagePage({super.key, required this.mediaUrls});
@override
State<ImagePage> createState() => _ImagePageState();
}
class _ImagePageState extends State<ImagePage> {
int selectedImageIndex = 0; // Track the currently selected image
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.secondprimary,
appBar: AppBar(
backgroundColor: AppColors.secondprimary,
leading: IconButton(
onPressed: () => Get.back(),
icon: Icon(Icons.arrow_back_ios_outlined),
),
title: Text("Service Image"),
centerTitle: true,
),
body: Column(
children: [
SizedBox(height: 50),
Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(
22.94,
), // Set border-radius to 22.94
child: CachedNetworkImage(
imageUrl: widget.mediaUrls.isNotEmpty
? widget.mediaUrls[selectedImageIndex] // Use selected index
: '',
width: 250, // Set width to 250
height: 250, // Set height to 250
fit: BoxFit.cover,
placeholder: (context, url) => const Center(
child: SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
errorWidget: (context, url, error) => const Icon(Icons.error),
),
),
),
SizedBox(height: 20),
CarouselSlider(
options: CarouselOptions(
height: 150, // Set height of the carousel
enlargeCenterPage: true,
enableInfiniteScroll: false,
viewportFraction: 0.3,
aspectRatio: 1.0,
),
items: widget.mediaUrls.asMap().entries.map((entry) {
int index = entry.key;
String url = entry.value;
bool isSelected = index == selectedImageIndex;
return GestureDetector(
onTap: () {
setState(() {
selectedImageIndex = index; // Update selected image
});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22.94),
border: isSelected
? Border.all(
color: Colors.blue,
width: 3,
) // Highlight selected image
: null,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
22.94,
), // Set border-radius to 22.94
child: CachedNetworkImage(
imageUrl: url,
width: 176, // Set width to 176
height: 158, // Set height to 158
fit: BoxFit.cover,
placeholder: (context, url) => const Center(
child: SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
errorWidget: (context, url, error) =>
const Icon(Icons.error),
),
),
),
);
}).toList(),
),
],
),
);
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,377 @@
import 'dart:convert';
import 'dart:ui';
import 'dart:math' as math;
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // Add this import
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
class MainController extends ConsumerStatefulWidget {
final Widget child;
final int? initialBottomIndex;
const MainController({
super.key,
required this.child,
this.initialBottomIndex,
});
@override
ConsumerState<MainController> createState() => _MainControllerState();
}
class _MainControllerState extends ConsumerState<MainController>
with SingleTickerProviderStateMixin {
int _selectedIndex = 0;
bool _showMenu = false;
late AnimationController _animationController;
@override
void initState() {
super.initState();
// Set bottom navigation index
if (widget.initialBottomIndex != null) {
_selectedIndex = widget.initialBottomIndex!;
} else {
// Auto-detect based on current route
final currentRoute = Get.currentRoute;
if (currentRoute == RouterConts.homescreen) {
_selectedIndex = 0;
} else if (currentRoute == RouterConts.packageList) {
_selectedIndex = 1;
} else if (currentRoute == RouterConts.categorypage) {
_selectedIndex = 2;
} else if (currentRoute == RouterConts.history) {
_selectedIndex = 3;
} else if (currentRoute == RouterConts.profilemainscreen ||
currentRoute == RouterConts.editprofile ||
currentRoute == RouterConts.changepassword) {
_selectedIndex = 0; // No tab selected
}
}
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
}
void _toggleMenu() {
setState(() {
_showMenu = !_showMenu;
_showMenu
? _animationController.forward()
: _animationController.reverse();
});
}
// 🎯 NEW: Handle back button press
Future<bool> _onWillPop() async {
final currentRoute = Get.currentRoute;
// If menu is open, close it first
if (_showMenu) {
_toggleMenu();
return false; // Don't exit
}
// Handle back navigation based on current route
switch (currentRoute) {
case RouterConts.profilemainscreen:
case RouterConts.editprofile:
case RouterConts.changepassword:
// Navigate back to home when coming from profile screens
Get.offAllNamed(RouterConts.homescreen);
return false; // Don't use default back behavior
case RouterConts.packageList:
case RouterConts.categorypage:
case RouterConts.history:
// For other main tabs, go to home first
Get.offAllNamed(RouterConts.homescreen);
return false;
case RouterConts.homescreen:
// If already on home, allow default back behavior (exit app)
return true;
default:
// For any other screens, try to go back to home
try {
Get.back();
} catch (e) {
// If can't go back, navigate to home
Get.offAllNamed(RouterConts.homescreen);
}
return false;
}
}
void _onItemTapped(int index, BuildContext context) {
if (_selectedIndex == index) return;
switch (index) {
case 0:
Get.offAllNamed(RouterConts.homescreen);
break;
case 1:
Get.offAllNamed(RouterConts.packageList);
break;
case 2:
Get.offAllNamed(RouterConts.categorypage);
break;
case 3:
// Navigate to history with default tab (Service Booking)
Get.offAllNamed(
RouterConts.history,
arguments: {
'historyTab': 0, // Default to first tab
},
);
break;
}
}
void changeSelectedIndex(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
final isKeyboardOpen = MediaQuery.of(context).viewInsets.bottom > 0;
// 🎯 Wrap with WillPopScope to handle back button
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
InheritedIndexController(
changeIndex: changeSelectedIndex,
child: widget.child,
),
if (_showMenu && !isKeyboardOpen)
AnimatedOpacity(
opacity: 1.0,
duration: const Duration(milliseconds: 300),
child: GestureDetector(
onTap: _toggleMenu,
child: Container(
alignment: Alignment.center,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0),
child: Container(
color: Colors.black.withOpacity(0.2),
width: double.infinity,
height: double.infinity,
),
),
),
),
),
if (_showMenu && !isKeyboardOpen)
Positioned(
bottom: 60,
left: 0,
right: 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 110),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildMenuOption(
icon: Icons.store,
label: 'Vendor',
color: const Color(0xFF0066FF),
onTap: () async {
_toggleMenu();
try {
await ref.read(getvendorIdProvider.future);
Get.toNamed(RouterConts.vendorwelcome);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to fetch Vendor ID'),
),
);
}
},
),
_buildMenuOption(
icon: Icons.person,
label: 'Profile',
color: const Color(0xFF0066FF),
onTap: () {
_toggleMenu();
Get.toNamed(RouterConts.profilemainscreen);
},
),
],
),
),
),
],
),
floatingActionButton: isKeyboardOpen
? null
: FloatingActionButton(
heroTag: "main_controller_fab",
onPressed: _toggleMenu,
shape: const CircleBorder(),
backgroundColor: const Color(0xFF0066FF),
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.rotate(
angle: _animationController.value * 0.75 * math.pi,
child: Icon(
_animationController.value > 0.5
? Icons.close
: Icons.add,
color: Colors.white,
),
);
},
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
notchMargin: 8,
color: Colors.white,
elevation: 8,
child: SizedBox(
height: 60,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_bottomNavItem(0, AppAssets.home),
_bottomNavItem(1, AppAssets.package),
const Expanded(child: SizedBox()),
_bottomNavItem(2, AppAssets.categories),
_bottomNavItem(3, AppAssets.history),
],
),
),
),
),
);
}
Widget _bottomNavItem(int index, String assetPath) {
return Expanded(
child: InkWell(
onTap: () => _onItemTapped(index, context),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
assetPath,
color: _selectedIndex == index
? const Color(0xFF0066FF)
: Colors.grey,
width: 26,
height: 29,
),
const SizedBox(height: 4),
Container(
height: 3,
width: 30,
decoration: BoxDecoration(
color: _selectedIndex == index
? const Color(0xFF0066FF)
: Colors.transparent,
borderRadius: BorderRadius.circular(2),
),
),
],
),
),
);
}
Widget _buildMenuOption({
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
return ScaleTransition(
scale: CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
),
],
),
child: IconButton(
icon: Icon(icon, color: Colors.white),
onPressed: onTap,
padding: EdgeInsets.zero,
),
),
const SizedBox(height: 5),
Text(
label,
style: const TextStyle(
color: Color(0xFF0066FF),
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
class InheritedIndexController extends InheritedWidget {
final void Function(int) changeIndex;
const InheritedIndexController({
super.key,
required this.changeIndex,
required super.child,
});
static InheritedIndexController? of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<InheritedIndexController>();
}
@override
bool updateShouldNotify(InheritedIndexController oldWidget) {
return oldWidget.changeIndex != changeIndex;
}
}
@@ -0,0 +1,401 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:get/get.dart';
class MostPopluarViewall extends ConsumerStatefulWidget {
const MostPopluarViewall({super.key});
@override
ConsumerState<MostPopluarViewall> createState() => _MostPopluarViewallState();
}
class _MostPopluarViewallState extends ConsumerState<MostPopluarViewall> {
bool _isValidCompleteUrl(String? url) {
if (url == null || url.isEmpty) return false;
try {
final uri = Uri.parse(url);
return uri.isAbsolute && uri.path.isNotEmpty;
} catch (e) {
return false;
}
}
@override
Widget build(BuildContext context) {
final popularServicesAsync = ref.watch(mostPopularProvider('0'));
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: SafeArea(
child: popularServicesAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
data: (services) {
if (services.isEmpty) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: Center(
child: Text("No services available for this category"),
),
);
}
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () => Navigator.pop(context),
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Text(
"Most popular service",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: MediaQuery.of(context).size.width / 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 0.74,
),
delegate: SliverChildBuilderDelegate((context, index) {
final service = services[index];
return LayoutBuilder(
builder: (context, constraints) {
return Container(
decoration: BoxDecoration(
color: const Color(0xFFFCFAFA),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(0xFFE3E3E3),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Service image with CachedNetworkImage
ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(19),
),
child: SizedBox(
height: constraints.maxWidth * 0.6,
width: double.infinity,
child:
_isValidCompleteUrl(
service.images1?.first,
)
? CachedNetworkImage(
imageUrl: service.images1!.first,
width: double.infinity,
height: constraints.maxWidth * 0.6,
fit: BoxFit.cover,
placeholder: (context, url) =>
Container(
color: Colors.grey[200],
child: const Center(
child:
CircularProgressIndicator(
strokeWidth: 2,
),
),
),
errorWidget:
(context, url, error) =>
Image.asset(
AppAssets.cleaning,
width: double.infinity,
height:
constraints.maxWidth *
0.6,
fit: BoxFit.cover,
),
)
: Image.asset(
AppAssets.cleaning,
width: double.infinity,
height: constraints.maxWidth * 0.6,
fit: BoxFit.cover,
),
),
),
// Rest of your widget code remains the same...
Align(
alignment: Alignment.centerRight,
child: Container(
height: 30,
width: 80,
transform: Matrix4.translationValues(
-10,
-15,
0,
),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white,
width: 1.5,
),
),
child: Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
),
child: Text(
'Rs.${service.amount ?? "N/A"}',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w400,
fontSize: 16.86,
height: 1.0,
letterSpacing: 0.02,
color: Color(0xFFFCFAFA),
),
),
),
),
),
),
),
// Service details
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(
8,
0,
8,
8,
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
service.serviceName ??
'Service',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w700,
fontSize: 15.02,
height: 1.0,
letterSpacing: 0.0,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.star,
color: Colors.orange,
size: 18,
),
const SizedBox(width: 2),
Text(
service.averageReview
?.toString() ??
'0.0',
style: const TextStyle(
fontFamily: 'SF UI Display',
fontWeight: FontWeight.w600,
fontSize: 14,
height: 1.0,
letterSpacing: -0.5,
),
),
],
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(
4,
),
decoration: BoxDecoration(
border: Border.all(
color: AppColors.lightGrey,
),
color: Colors.white,
borderRadius:
BorderRadius.circular(12),
),
child: Row(
mainAxisSize:
MainAxisSize.min,
children: [
_isValidCompleteUrl(
service.profilePic1,
)
? CircleAvatar(
radius: 15,
backgroundImage:
CachedNetworkImageProvider(
service
.profilePic1!,
),
)
: const CircleAvatar(
radius: 15,
backgroundImage:
AssetImage(
AppAssets
.login,
),
),
const SizedBox(width: 4),
Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
mainAxisSize:
MainAxisSize.min,
children: [
Text(
service.vendorName ??
'Vendor',
style: const TextStyle(
fontFamily:
'Gilroy-Medium',
fontWeight:
FontWeight
.w400,
fontSize: 11.75,
height: 1.0,
letterSpacing:
0.0,
color: Color(
0xFF353434,
),
),
maxLines: 1,
overflow:
TextOverflow
.ellipsis,
),
const SizedBox(
height: 2,
),
Text(
service.serviceName ??
'Service',
style: const TextStyle(
fontFamily:
'Gilroy-Regular',
fontWeight:
FontWeight
.w400,
fontSize: 11.75,
height: 1.0,
letterSpacing:
0.0,
color: Color(
0xFF717171,
),
),
maxLines: 1,
overflow:
TextOverflow
.ellipsis,
),
],
),
),
],
),
),
),
const SizedBox(width: 4),
GestureDetector(
onTap: () {
Get.toNamed(
RouterConts.detailserivce,
arguments: service.id,
);
},
child: Container(
padding: const EdgeInsets.all(
8,
),
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius:
BorderRadius.circular(12),
),
child: const Icon(
Icons
.arrow_forward_ios_rounded,
size: 16,
color: Colors.white,
),
),
),
],
),
],
),
),
),
],
),
);
},
);
}, childCount: services.length),
),
),
],
);
},
),
),
);
}
}
@@ -0,0 +1,165 @@
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/model/notification_model.dart';
import 'package:bookmywages/view/auth/auth_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Badge count state provider
final notificationCountProvider = StateProvider<int>((ref) => 0);
// Repository provider
final notificationRepositoryProvider = Provider(
(ref) => NotificationRepository(),
);
// Future provider with parameters
final notificationProvider = FutureProvider.family
.autoDispose<List<NotificationModel>, ({int type, String userId})>((
ref,
params,
) async {
final repo = ref.read(notificationRepositoryProvider);
return repo.fetchNotification(type: params.type, userId: params.userId);
});
class NotificationPage extends ConsumerWidget {
final int type;
final String id;
const NotificationPage({super.key, required this.type, required this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncNotif = ref.watch(
notificationProvider((type: type, userId: id)),
);
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(Icons.arrow_back_ios),
),
),
const Center(
child: Text(
"Notification",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 20),
// Notification list with badge update
asyncNotif.when(
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(child: Text('Error: $err')),
data: (notifications) {
Future.microtask(() {
ref.read(notificationCountProvider.notifier).state =
notifications.length;
});
if (notifications.isEmpty) {
return const Center(child: Text("No Notification"));
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: notifications.length,
itemBuilder: (context, index) {
final item = notifications[index];
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 67.08,
height: 67.08,
margin: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 10,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(9.75),
border: Border.all(
color: const Color(0xFFB7B7B7),
width: 0.98,
),
),
child: _buildNotificationImage(item.images),
),
Expanded(
child: Text(
item.message,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 15,
height: 28 / 15,
letterSpacing: 0.15,
color: AppColors.thridprimary,
),
),
),
],
);
},
);
},
),
],
),
),
),
),
);
}
Widget _buildNotificationImage(List<String> images) {
if (images.isEmpty || images[0].isEmpty) {
return const Icon(Icons.notifications, size: 30, color: Colors.grey);
}
return ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.network(
images[0],
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 30, color: Colors.grey);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
);
},
),
);
}
}
@@ -0,0 +1,370 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/model/package_model.dart';
import 'package:bookmywages/model/plan_sucess_model.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/view/user_main_screens/main_contoller.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class PackageScreen extends ConsumerStatefulWidget {
const PackageScreen({super.key});
@override
ConsumerState<PackageScreen> createState() => _PackageScreenState();
}
class _PackageScreenState extends ConsumerState<PackageScreen> {
bool _isMounted = false;
@override
void initState() {
super.initState();
_isMounted = true;
}
@override
void dispose() {
_isMounted = false;
super.dispose();
}
@override
Widget build(BuildContext context) {
final packageAsyncValue = ref.watch(pacakgeProvider);
final indexController = InheritedIndexController.of(context);
return PopScope(
onPopInvoked: (didPop) {
if (didPop) {
// Reset main controller index to 0 when going back
final indexController = InheritedIndexController.of(context);
indexController?.changeIndex(0);
}
},
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(
Icons.arrow_back_ios_new_sharp,
color: Colors.black,
),
onPressed: () => Navigator.pop(context),
),
centerTitle: true,
title: const Text(
"Subscription Plan",
style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
),
),
extendBodyBehindAppBar: true,
body: LayoutBuilder(
builder: (context, constraints) {
final isSmallScreen = constraints.maxWidth < 360;
final cardWidth = constraints.maxWidth * 0.8;
final cardHeight = constraints.maxHeight * 0.4;
return Container(
height: double.infinity,
width: double.infinity,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(AppAssets.background),
fit: BoxFit.cover,
),
),
child: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: constraints.maxWidth * 0.04,
vertical: constraints.maxHeight * 0.02,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: constraints.maxHeight * 0.2,
),
child: Image.asset(
AppAssets.subscriptionimage,
fit: BoxFit.contain,
),
),
SizedBox(height: constraints.maxHeight * 0.02),
Text(
"Choose your right plan",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w400,
fontSize: isSmallScreen ? 24 : 32,
height: 1.0,
letterSpacing: 0.32,
color: Colors.black,
),
),
SizedBox(height: constraints.maxHeight * 0.01),
Padding(
padding: EdgeInsets.symmetric(
horizontal: constraints.maxWidth * 0.05,
),
child: Text(
"Make upgrade your plan and get more advantages of services",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: isSmallScreen ? 13 : 15,
height: 1.71,
letterSpacing: 0.15,
color: Colors.black,
),
),
),
SizedBox(height: constraints.maxHeight * 0.02),
Expanded(
child: packageAsyncValue.when(
data: (packages) {
final filteredPackages = packages
.where((pkg) => pkg.type == 1)
.toList();
return Center(
child: SizedBox(
height: cardHeight,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: filteredPackages.length,
itemBuilder: (context, index) {
final PackageModel package =
filteredPackages[index];
return Container(
width: cardWidth,
margin: EdgeInsets.symmetric(
horizontal: constraints.maxWidth * 0.02,
),
constraints: BoxConstraints(
maxHeight: cardHeight,
),
decoration: BoxDecoration(
color: const Color(0xFF334E95),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: const Color(0xFF334E95),
width: 1,
),
boxShadow: const [
BoxShadow(
color: Color(0x40828282),
offset: Offset(0, 5),
blurRadius: 4,
),
],
),
child: _PackageCard(
package: package,
onSubscribe: () => _subscribeToPlan(
package,
indexController,
),
isSmallScreen: isSmallScreen,
),
);
},
),
),
);
},
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Text(
'Error loading packages: $error',
style: const TextStyle(color: Colors.red),
),
),
),
),
],
),
),
),
);
},
),
),
);
}
Future<void> _subscribeToPlan(
PackageModel package,
InheritedIndexController? indexController,
) async {
try {
final prefs = await SharedPreferences.getInstance();
final userId = prefs.getString('userId') ?? '';
final model = PlanSuccessModel(
userId: userId,
planId: package.id.toString(),
duration: package.duration,
type: package.type,
);
final result = await ref.read(planSuccessProvider(model).future);
if (_isMounted) {
if (result) {
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Success'),
content: const Text('Plan subscribed successfully!'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
indexController?.changeIndex(0);
ref.invalidate(expiredPlanProvider);
Get.toNamed(RouterConts.homescreen);
},
child: const Text('OK'),
),
],
),
);
} else {
Fluttertoast.showToast(
msg: 'Failed to subscribe to plan',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
} catch (error) {
if (_isMounted) {
Fluttertoast.showToast(
msg: 'Error: ${error.toString()}',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
}
}
class _PackageCard extends StatelessWidget {
final PackageModel package;
final VoidCallback onSubscribe;
final bool isSmallScreen;
const _PackageCard({
required this.package,
required this.onSubscribe,
required this.isSmallScreen,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(MediaQuery.of(context).size.width * 0.04),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
package.name,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: isSmallScreen ? 16 : 18,
height: 1.0,
letterSpacing: 0.32,
color: Colors.white,
),
),
),
const SizedBox(height: 8),
_buildFeatureRow(package.description),
const SizedBox(height: 16),
_buildFeatureRow(_getFormattedDuration(package.duration)),
const SizedBox(height: 16),
_buildFeatureRow('${package.noOfService} book service'),
],
),
const Divider(color: Colors.white),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
package.price,
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
GestureDetector(
onTap: onSubscribe,
child: Container(
width: 30,
height: 30,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: const Icon(Icons.arrow_forward, color: Colors.black),
),
),
],
),
],
),
);
}
Widget _buildFeatureRow(String text) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const CircleAvatar(radius: 8, backgroundColor: Color(0xFFF9E369)),
const SizedBox(width: 6),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: isSmallScreen ? 12 : 14,
color: Colors.white,
),
),
),
],
);
}
String _getFormattedDuration(int duration) {
if (duration < 30) {
return '$duration days';
} else {
int months = duration ~/ 30;
return '$months month${months > 1 ? 's' : ''}';
}
}
}
@@ -0,0 +1,456 @@
import 'dart:io';
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/consts_widgets/comman_button.dart';
import 'package:bookmywages/consts_widgets/comman_textformfiled.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:bookmywages/viewmodel/consts_api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_picker/image_picker.dart';
class EditProfile extends StatefulWidget {
const EditProfile({super.key});
@override
State<EditProfile> createState() => _EditProfileState();
}
class _EditProfileState extends State<EditProfile> {
File? _image;
final ImagePicker _picker = ImagePicker();
final TextEditingController nameController = TextEditingController();
final TextEditingController numberController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController addressController = TextEditingController();
bool _isProfileDataInitialized = false;
Future<void> _pickImage() async {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
_image = File(pickedFile.path);
});
}
}
// Helper method to validate and construct image URL
String? _getValidImageUrl(String? imageUrl) {
if (imageUrl == null || imageUrl.isEmpty) {
return null;
}
// Check if URL is complete (has a filename)
if (imageUrl.endsWith('/') || imageUrl.endsWith('/images/')) {
return null;
}
// Ensure URL is properly formatted
if (!imageUrl.startsWith('http')) {
return null;
}
return imageUrl;
}
// Helper widget for safe network image loading
Widget _buildNetworkImage(String? imageUrl, {BoxFit fit = BoxFit.cover}) {
final validUrl = _getValidImageUrl(imageUrl);
if (validUrl == null) {
return Image.asset(AppAssets.profile, fit: fit);
}
return Image.network(
validUrl,
fit: fit,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
print('Error loading image: $error');
return Image.asset(AppAssets.profile, fit: fit);
},
);
}
// Helper method for CircleAvatar image provider
ImageProvider _getAvatarImageProvider(String? imageUrl) {
if (_image != null) {
return FileImage(_image!);
}
final validUrl = _getValidImageUrl(imageUrl);
if (validUrl != null) {
return NetworkImage(validUrl);
}
return const AssetImage(AppAssets.profile);
}
final TextStyle gilroyTextStyle = const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20,
height: 1.45,
letterSpacing: 0.01,
);
@override
void dispose() {
nameController.dispose();
numberController.dispose();
emailController.dispose();
addressController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double height = MediaQuery.of(context).size.height;
return Scaffold(
backgroundColor: AppColors.secondprimary,
resizeToAvoidBottomInset: false,
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Column(
children: [
Stack(
children: [
SizedBox(
width: double.infinity,
height: height * 0.4,
child: Consumer(
builder: (context, ref, _) {
final profileData = ref.watch(
profilegetuserProvider,
);
return profileData.when(
data: (profiles) {
final profile = profiles.isNotEmpty
? profiles[0]
: null;
if (profile != null &&
!_isProfileDataInitialized) {
WidgetsBinding.instance
.addPostFrameCallback((_) {
setState(() {
nameController.text =
profile.name ?? '';
numberController.text =
profile.number ?? '';
emailController.text =
profile.email ?? '';
addressController.text =
profile.address ?? '';
_isProfileDataInitialized = true;
});
});
}
return _image == null
? _buildNetworkImage(
profile?.profilePic1,
)
: Image.file(
_image!,
fit: BoxFit.cover,
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, _) => Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 50),
Text('Error loading profile: $error'),
],
),
),
);
},
),
),
Positioned(
bottom: 0,
child: CustomPaint(
size: Size(width, height * 0.4),
painter: RightToLeftFullWidthTrianglePainter(),
),
),
Positioned(
bottom: height * 0.03,
left: width / 2 - 170,
child: Consumer(
builder: (context, ref, _) {
final profileData = ref.watch(
profilegetuserProvider,
);
return profileData.when(
data: (profiles) {
final profile = profiles.isNotEmpty
? profiles[0]
: null;
return CircleAvatar(
radius: 68,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 65,
backgroundImage:
_getAvatarImageProvider(
profile?.profilePic1,
),
onBackgroundImageError:
(error, stackTrace) {
print(
'Avatar image error: $error',
);
},
),
);
},
loading: () => const CircleAvatar(
radius: 68,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 65,
child: CircularProgressIndicator(),
),
),
error: (error, _) => const CircleAvatar(
radius: 68,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 65,
backgroundImage: AssetImage(
AppAssets.menu,
),
),
),
);
},
),
),
Positioned(
bottom: height * 0.08,
left: width / 2 - 20,
child: Consumer(
builder: (context, ref, _) {
final profileData = ref.watch(
profilegetuserProvider,
);
return profileData.when(
data: (profiles) {
final profile = profiles.isNotEmpty
? profiles[0]
: null;
if (profile == null) {
return Text(
'No Profile Data',
style: gilroyTextStyle,
);
}
return Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
profile.name ?? 'No Name',
style: gilroyTextStyle,
),
IconButton(
onPressed: _pickImage,
icon: const Icon(Icons.edit),
),
],
),
Row(
children: [
const Icon(
Icons.location_city,
size: 20,
),
const SizedBox(width: 4),
Text(
profile.address ??
'No address available',
style: const TextStyle(
fontSize: 12,
),
),
],
),
],
);
},
loading: () =>
const CircularProgressIndicator(),
error: (error, _) => Text(
'Error: $error',
style: gilroyTextStyle,
),
);
},
),
),
],
),
const SizedBox(height: 30),
Padding(
padding: EdgeInsets.symmetric(horizontal: width * 0.05),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Name", style: gilroyTextStyle),
const SizedBox(height: 15),
CommonTextFormField(
hintText: "Enter your name",
controller: nameController,
),
const SizedBox(height: 30),
Text("Mobile Number", style: gilroyTextStyle),
const SizedBox(height: 15),
CommonTextFormField(
hintText: "Enter your mobile number",
keyboardType: TextInputType.phone,
controller: numberController,
),
const SizedBox(height: 30),
Text("Address", style: gilroyTextStyle),
const SizedBox(height: 15),
CommonTextFormField(
hintText: "Enter your address",
controller: addressController,
),
const SizedBox(height: 30),
Text("Email", style: gilroyTextStyle),
const SizedBox(height: 15),
CommonTextFormField(
hintText: "Enter your email",
keyboardType: TextInputType.emailAddress,
controller: emailController,
),
const SizedBox(height: 30),
Center(
child: Consumer(
builder: (context, ref, _) {
return CommanButton(
width: width * 0.6,
text: "Save",
textStyle: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 23,
height: 1.0,
letterSpacing: 0.01,
color: AppColors.secondprimary,
),
onPressed: () async {
try {
final repo = ref.read(
profileupdateRepositoryProvider,
);
await repo.updateProfile(
url: ConstsApi.upadateprofile,
name: nameController.text,
number: numberController.text,
email: emailController.text,
address: addressController.text,
imageFile: _image,
);
if (mounted) {
Fluttertoast.showToast(
msg: "Profile updated successfully",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
);
ref.refresh(profilegetuserProvider);
Future.delayed(
const Duration(milliseconds: 1500),
() {
if (mounted) {
Navigator.of(context).pop();
}
},
);
}
} catch (e) {
if (mounted) {
Fluttertoast.showToast(
msg: "Error updating profile: $e",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
},
);
},
),
),
const SizedBox(height: 30),
],
),
),
],
),
),
),
);
},
),
),
);
}
}
class RightToLeftFullWidthTrianglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.white;
final path = Path();
path.moveTo(size.width, 0);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
@@ -0,0 +1,421 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
class ProfileChangepass extends ConsumerStatefulWidget {
const ProfileChangepass({super.key});
@override
ConsumerState<ProfileChangepass> createState() => _ProfileChangepassState();
}
class _ProfileChangepassState extends ConsumerState<ProfileChangepass> {
final TextEditingController _newPasswordController = TextEditingController();
final TextEditingController _confirmPasswordController =
TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _obscureNewPassword = true;
bool _obscureConfirmPassword = true;
String? _newPasswordError;
String? _confirmPasswordError;
@override
void dispose() {
_newPasswordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _submitForm() async {
FocusScope.of(context).unfocus();
// Reset errors
setState(() {
_newPasswordError = null;
_confirmPasswordError = null;
});
if (_formKey.currentState!.validate()) {
try {
final success = await ref.read(
changepasswordProvider(_newPasswordController.text).future,
);
if (success && mounted) {
Fluttertoast.showToast(
msg: "Password changed successfully",
backgroundColor: Colors.green,
textColor: Colors.white,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
Navigator.pop(context);
} else {
Fluttertoast.showToast(
msg: "Failed to change password",
backgroundColor: Colors.red,
textColor: Colors.white,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
}
} catch (e) {
if (mounted) {
Fluttertoast.showToast(
msg: "Error: ${e.toString()}",
backgroundColor: Colors.red,
textColor: Colors.white,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
);
}
}
}
}
String? _validateNewPassword(String? value) {
if (value == null || value.isEmpty) {
setState(() {
_newPasswordError = "Please enter a password";
});
return "";
}
if (value.length < 6) {
// Fixed: was 4, should be 6 as per error message
setState(() {
_newPasswordError = "Password must be at least 6 characters";
});
return "";
}
setState(() {
_newPasswordError = null;
});
return null;
}
String? _validateConfirmPassword(String? value) {
if (value == null || value.isEmpty) {
setState(() {
_confirmPasswordError = "Please confirm your password";
});
return "";
}
if (value != _newPasswordController.text) {
setState(() {
_confirmPasswordError = "Passwords don't match";
});
return "";
}
setState(() {
_confirmPasswordError = null;
});
return null;
}
@override
Widget build(BuildContext context) {
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
final isKeyboardVisible = keyboardHeight > 0;
final screenHeight = MediaQuery.of(context).size.height;
return Scaffold(
backgroundColor: AppColors.secondprimary,
resizeToAvoidBottomInset: false, // Keep false to prevent white space
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Container(
height: screenHeight, // Fixed height
color: AppColors.secondprimary,
child: SafeArea(
child: Column(
children: [
// Header with back button
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(
Icons.arrow_back_ios_new,
size: 20,
color: Colors.black,
),
),
const Expanded(
child: Center(
child: Text(
'Change Password',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(width: 20),
],
),
),
// Scrollable content with proper height calculation
Expanded(
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
padding: EdgeInsets.only(
left: 16.0,
right: 16.0,
bottom: isKeyboardVisible ? keyboardHeight + 16 : 16.0,
),
child: Column(
children: [
// Show smaller image when keyboard is visible, normal size when not
if (!isKeyboardVisible) ...[
Image.asset(
AppAssets.changepass,
width: MediaQuery.of(context).size.width * 0.7,
height: MediaQuery.of(context).size.height * 0.3,
fit: BoxFit.contain,
),
const SizedBox(height: 16),
] else ...[
// Smaller image when keyboard is open
Image.asset(
AppAssets.changepass,
width: MediaQuery.of(context).size.width * 0.4,
height: screenHeight * 0.12, // Even smaller height
fit: BoxFit.contain,
),
const SizedBox(height: 8),
],
// Form to change password
Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Create Password",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 20,
fontWeight: FontWeight.w700,
color: Colors.black87,
height: 18.68 / 20,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 53,
decoration: BoxDecoration(
color: const Color(0xFFFDFFFF),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: const Color(0xFF8C8A8A),
),
),
child: TextFormField(
controller: _newPasswordController,
obscureText: _obscureNewPassword,
decoration: InputDecoration(
hintText: "Enter new password",
contentPadding:
const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
border: InputBorder.none,
hintStyle: const TextStyle(
fontFamily: 'Martel',
fontWeight: FontWeight.w700,
fontSize: 14,
height: 29 / 14,
letterSpacing: -0.5,
color: Color(0xFF757576),
),
suffixIcon: IconButton(
icon: Icon(
_obscureNewPassword
? Icons.visibility_off
: Icons.visibility,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscureNewPassword =
!_obscureNewPassword;
});
},
),
),
validator: _validateNewPassword,
),
),
SizedBox(
height: _newPasswordError != null ? 28 : 0,
child:
_newPasswordError != null
? Padding(
padding: const EdgeInsets.only(
top: 8.0,
left: 16.0,
),
child: Text(
_newPasswordError!,
style: const TextStyle(
color: Colors.red,
fontSize: 12,
),
),
)
: null,
),
],
),
const SizedBox(height: 20),
const Text(
"Confirm Password",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 20,
fontWeight: FontWeight.w700,
color: Colors.black87,
height: 18.68 / 20,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 53,
decoration: BoxDecoration(
color: const Color(0xFFFDFFFF),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: const Color(0xFF8C8A8A),
),
),
child: TextFormField(
controller: _confirmPasswordController,
obscureText: _obscureConfirmPassword,
decoration: InputDecoration(
hintText: "Confirm your password",
contentPadding:
const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
border: InputBorder.none,
hintStyle: const TextStyle(
fontFamily: 'Martel',
fontWeight: FontWeight.w700,
fontSize: 14,
height: 29 / 14,
letterSpacing: -0.5,
color: Color(0xFF757576),
),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword
? Icons.visibility_off
: Icons.visibility,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscureConfirmPassword =
!_obscureConfirmPassword;
});
},
),
),
validator: _validateConfirmPassword,
),
),
SizedBox(
height:
_confirmPasswordError != null ? 28 : 0,
child:
_confirmPasswordError != null
? Padding(
padding: const EdgeInsets.only(
top: 8.0,
left: 16.0,
),
child: Text(
_confirmPasswordError!,
style: const TextStyle(
color: Colors.red,
fontSize: 12,
),
),
)
: null,
),
],
),
const SizedBox(height: 20),
Center(
child: SizedBox(
width: 299,
height: 61.71,
child: ElevatedButton(
onPressed: _submitForm,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0066FF),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
34.77,
),
),
elevation: 0,
),
child: const Text(
"Save",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 28.76,
height: 1.0,
letterSpacing: 0.01,
),
),
),
),
),
// Add some bottom spacing
SizedBox(height: isKeyboardVisible ? 16 : 30),
],
),
),
],
),
),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,668 @@
import 'dart:ui';
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:bookmywages/viewmodel/consts_api.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ProfileMainPage extends StatefulWidget {
const ProfileMainPage({super.key});
@override
State<ProfileMainPage> createState() => _ProfileMainPageState();
}
class _ProfileMainPageState extends State<ProfileMainPage> {
File? _image;
final ImagePicker _picker = ImagePicker();
bool _isProfileDataInitialized = false;
bool _isUploading = false;
// Modified _pickImage method with automatic API upload using existing repository
Future<void> _pickImage() async {
try {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
_image = File(pickedFile.path);
_isUploading = true;
});
// Automatically upload to API using existing repository
await _uploadImageToAPI(File(pickedFile.path));
}
} catch (e) {
print('Error picking image: $e');
_showErrorToast('Failed to pick image: $e');
}
}
// Upload image using existing ProfileupdateRepository
Future<void> _uploadImageToAPI(File imageFile) async {
try {
// Get current profile data
final profileData = ProviderScope.containerOf(
context,
).read(profilegetuserProvider);
await profileData.when(
data: (profiles) async {
if (profiles.isNotEmpty) {
final profile = profiles[0];
// Get the repository
final repository = ProviderScope.containerOf(
context,
).read(profileupdateRepositoryProvider);
// Update profile with new image and existing data
await repository.updateProfile(
url: ConstsApi.upadateprofile, // Use your existing API constant
name: profile.name,
number: profile.number,
email: profile.email,
address: profile.address ?? '',
imageFile: imageFile,
);
// Refresh profile data to get updated image URL
ProviderScope.containerOf(context).refresh(profilegetuserProvider);
ProviderScope.containerOf(
context,
).refresh(profileupdateuserProvider);
_showSuccessToast('Profile picture updated successfully!');
// Clear local image since it's now uploaded
setState(() {
_image = null;
});
} else {
_showErrorToast('No profile data available');
setState(() {
_image = null;
});
}
},
loading: () async {
_showErrorToast('Profile data is still loading');
setState(() {
_image = null;
});
},
error: (error, _) async {
_showErrorToast('Error getting profile data: $error');
setState(() {
_image = null;
});
},
);
} catch (e) {
print('Error uploading image: $e');
_showErrorToast('Upload failed: $e');
setState(() {
_image = null;
});
} finally {
setState(() {
_isUploading = false;
});
}
}
// Helper methods for showing toasts
void _showSuccessToast(String message) {
Fluttertoast.showToast(
msg: message,
backgroundColor: Colors.green,
textColor: Colors.white,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
}
void _showErrorToast(String message) {
Fluttertoast.showToast(
msg: message,
backgroundColor: Colors.red,
textColor: Colors.white,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
);
}
// Helper method to validate and construct image URL
String? _getValidImageUrl(String? imageUrl) {
if (imageUrl == null || imageUrl.isEmpty) {
return null;
}
// Check if URL is complete (has a filename)
if (imageUrl.endsWith('/') || imageUrl.endsWith('/images/')) {
return null;
}
// Ensure URL is properly formatted
if (!imageUrl.startsWith('http')) {
return null;
}
return imageUrl;
}
// Helper widget for safe network image loading
Widget _buildNetworkImage(String? imageUrl, {BoxFit fit = BoxFit.cover}) {
final validUrl = _getValidImageUrl(imageUrl);
if (validUrl == null) {
return Image.asset(AppAssets.profile, fit: fit);
}
return Image.network(
validUrl,
fit: fit,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
print('Error loading image: $error');
return Image.asset(AppAssets.profile, fit: fit);
},
);
}
// Helper method for CircleAvatar image provider
ImageProvider _getAvatarImageProvider(String? imageUrl) {
if (_image != null) {
return FileImage(_image!);
}
final validUrl = _getValidImageUrl(imageUrl);
if (validUrl != null) {
return NetworkImage(validUrl);
}
return const AssetImage(AppAssets.profile);
}
final TextStyle gilroyTextStyle = const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20,
height: 1.45,
letterSpacing: 0.01,
);
@override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double height = MediaQuery.of(context).size.height;
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: SafeArea(
child: Column(
children: [
Flexible(
flex: 1,
child: Stack(
children: [
SizedBox(
width: double.infinity,
height: height * 0.4,
child: Consumer(
builder: (context, ref, _) {
final profileData = ref.watch(profilegetuserProvider);
return profileData.when(
data: (profiles) {
final profile = profiles.isNotEmpty
? profiles[0]
: null;
if (profile != null && !_isProfileDataInitialized) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
profile.address ?? '';
_isProfileDataInitialized = true;
});
});
}
return _image == null
? _buildNetworkImage(profile?.profilePic1)
: Image.file(_image!, fit: BoxFit.cover);
},
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 50),
Text('Error loading profile: $error'),
],
),
),
);
},
),
),
Positioned(
bottom: 0,
child: CustomPaint(
size: Size(width, height * 0.4),
painter: RightToLeftFullWidthTrianglePainter(),
),
),
Positioned(
bottom: height * 0.03,
left: width / 2 - 170,
child: Consumer(
builder: (context, ref, _) {
final profileData = ref.watch(profilegetuserProvider);
return profileData.when(
data: (profiles) {
final profile = profiles.isNotEmpty
? profiles[0]
: null;
return Stack(
alignment: Alignment.center,
children: [
CircleAvatar(
radius: 68,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 65,
backgroundImage: _getAvatarImageProvider(
profile?.profilePic1,
),
onBackgroundImageError:
(error, stackTrace) {
print('Avatar image error: $error');
},
),
),
// Show upload progress indicator
if (_isUploading)
Container(
width: 136,
height: 136,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: const Center(
child: CircularProgressIndicator(
color: Colors.white,
),
),
),
],
);
},
loading: () => const CircleAvatar(
radius: 68,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 65,
child: CircularProgressIndicator(),
),
),
error: (error, _) => const CircleAvatar(
radius: 68,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 65,
backgroundImage: AssetImage(AppAssets.menu),
),
),
);
},
),
),
Positioned(
bottom: height * 0.08,
left: width / 2 - 20,
child: Consumer(
builder: (context, ref, _) {
final profileData = ref.watch(profilegetuserProvider);
return profileData.when(
data: (profiles) {
final profile = profiles.isNotEmpty
? profiles[0]
: null;
if (profile == null) {
return Text(
'No Profile Data',
style: gilroyTextStyle,
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
profile.name ?? 'No Name',
style: gilroyTextStyle,
),
IconButton(
onPressed: _isUploading
? null
: _pickImage,
icon: _isUploading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.edit),
),
],
),
Row(
children: [
const Icon(Icons.location_city, size: 20),
const SizedBox(width: 4),
Text(
profile.address ?? 'No address available',
style: const TextStyle(fontSize: 12),
),
],
),
],
);
},
loading: () => const CircularProgressIndicator(),
error: (error, _) =>
Text('Error: $error', style: gilroyTextStyle),
);
},
),
),
],
),
),
const SizedBox(height: 30),
Expanded(
flex: 1,
child: ListView(
shrinkWrap: false,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildMenuItem(Icons.person, 'My Profile'),
_buildMenuItem(Icons.lock, 'Change Password'),
_buildMenuItem(Icons.notifications, 'Notification'),
_buildMenuItem(Icons.description, 'Terms and Conditions'),
_buildMenuItem(Icons.logout, 'Sign out'),
],
),
),
],
),
),
);
}
Widget _buildMenuItem(IconData icon, String title) {
return ListTile(
leading: Icon(
icon,
size: 23.000513076782227,
color: const Color(0xFF3E3E3F),
),
title: Text(
title,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 18,
height: 1.0,
letterSpacing: 0.2,
color: Color(0xFF3E3E3F),
),
),
trailing: const Icon(Icons.arrow_forward_ios_rounded),
onTap: () async {
try {
if (title == 'My Profile') {
await Get.toNamed(RouterConts.editprofile);
} else if (title == 'Change Password') {
await Get.toNamed(RouterConts.changepassword);
} else if (title == 'Sign out') {
_showSignOutDialog();
} else if (title == 'Terms and Conditions') {
_showTermsAndConditionsDialog(); // Removed ref parameter
}
} catch (e) {
print('Navigation error: $e');
if (mounted) {
Fluttertoast.showToast(
msg: 'Navigation error: $e',
backgroundColor: Colors.red,
textColor: Colors.white,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
);
}
}
},
);
}
void _showSignOutDialog() {
if (!mounted) return;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Sign Out'),
content: const Text('Are you sure you want to sign out?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
final prefs = await SharedPreferences.getInstance();
await prefs.remove('userId');
await prefs.remove('vendor_id');
await prefs.remove('data');
if (context.mounted) {
// context.go(
// RouterConts.loginpage,
// ); // or use pushReplacement if needed
}
},
child: const Text('Sign Out'),
),
],
);
},
);
}
void _showTermsAndConditionsDialog() {
// Removed WidgetRef ref parameter
if (!mounted) return;
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Center(
child: Consumer(
builder: (context, ref, _) {
final termsAsync = ref.watch(termsAndConditionsProvider);
return termsAsync.when(
data: (terms) => Container(
width: 389,
height: 547,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(0xFF959595),
width: 1,
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Center(
child: Text(
"Terms & Condition",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
const SizedBox(height: 12),
Expanded(
child: ScrollbarTheme(
data: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all(
AppColors.primary,
),
trackColor: WidgetStateProperty.all(
Colors.transparent,
),
trackBorderColor: WidgetStateProperty.all(
Colors.transparent,
),
thickness: WidgetStateProperty.all(13),
radius: const Radius.circular(19),
thumbVisibility: WidgetStateProperty.all(true),
),
child: Scrollbar(
thumbVisibility: true,
interactive: true,
thickness: 13,
radius: const Radius.circular(19),
child: ListView(
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
terms.data,
style: const TextStyle(
color: Colors.black,
),
),
const SizedBox(height: 20),
Center(
child: SizedBox(
width: 228,
height: 51.82,
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(
0xFF0066FF,
),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(
22.95,
),
),
),
child: const Text(
"OK",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Poppins',
fontWeight: FontWeight.w500,
fontSize: 18.87,
height: 23.59 / 18.87,
letterSpacing: -0.59,
color: Colors.white,
),
),
),
),
),
],
),
],
),
),
),
),
],
),
),
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (e, _) => Container(
padding: const EdgeInsets.all(16),
child: Text(
"Error: ${e.toString()}",
style: const TextStyle(color: Colors.red),
),
),
);
},
),
),
),
);
},
);
}
}
class RightToLeftFullWidthTrianglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.white;
final path = Path();
path.moveTo(size.width, 0); // top right corner
path.lineTo(size.width, size.height); // bottom right corner
path.lineTo(0, size.height); // bottom left (tip)
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
@@ -0,0 +1,51 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:flutter/material.dart';
class SucessfullScreen extends StatefulWidget {
const SucessfullScreen({super.key});
@override
State<SucessfullScreen> createState() => _SucessfullScreenState();
}
class _SucessfullScreenState extends State<SucessfullScreen> {
@override
void initState() {
super.initState();
// Wait 3 seconds then go back
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
Navigator.pop(context); // This goes back to the previous screen
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(AppAssets.sucess, width: 300, height: 300),
Text(
"Successful your Booking",
style: TextStyle(
fontFamily: 'Mogra',
fontWeight: FontWeight.w600,
fontSize: 22,
height: 57.82 / 25.46, // line-height ratio
letterSpacing: 1.0,
color: Color(0xFF0C2C5C),
),
),
],
),
),
);
}
}
@@ -0,0 +1,456 @@
import 'dart:io';
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/consts_widgets/comman_button.dart';
import 'package:bookmywages/consts_widgets/comman_textformfiled.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:bookmywages/viewmodel/consts_api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_picker/image_picker.dart';
class VendorEditProfile extends StatefulWidget {
const VendorEditProfile({super.key});
@override
State<VendorEditProfile> createState() => _VendorEditProfileState();
}
class _VendorEditProfileState extends State<VendorEditProfile> {
File? _image;
final ImagePicker _picker = ImagePicker();
final TextEditingController nameController = TextEditingController();
final TextEditingController numberController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController addressController = TextEditingController();
bool _isProfileDataInitialized = false;
Future<void> _pickImage() async {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
_image = File(pickedFile.path);
});
}
}
// Helper method to validate and construct image URL
String? _getValidImageUrl(String? imageUrl) {
if (imageUrl == null || imageUrl.isEmpty) {
return null;
}
// Check if URL is complete (has a filename)
if (imageUrl.endsWith('/') || imageUrl.endsWith('/images/')) {
return null;
}
// Ensure URL is properly formatted
if (!imageUrl.startsWith('http')) {
return null;
}
return imageUrl;
}
// Helper widget for safe network image loading
Widget _buildNetworkImage(String? imageUrl, {BoxFit fit = BoxFit.cover}) {
final validUrl = _getValidImageUrl(imageUrl);
if (validUrl == null) {
return Image.asset(AppAssets.profile, fit: fit);
}
return Image.network(
validUrl,
fit: fit,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
print('Error loading image: $error');
return Image.asset(AppAssets.profile, fit: fit);
},
);
}
// Helper method for CircleAvatar image provider
ImageProvider _getAvatarImageProvider(String? imageUrl) {
if (_image != null) {
return FileImage(_image!);
}
final validUrl = _getValidImageUrl(imageUrl);
if (validUrl != null) {
return NetworkImage(validUrl);
}
return const AssetImage(AppAssets.profile);
}
final TextStyle gilroyTextStyle = const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20,
height: 1.45,
letterSpacing: 0.01,
);
@override
void dispose() {
nameController.dispose();
numberController.dispose();
emailController.dispose();
addressController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double height = MediaQuery.of(context).size.height;
return Scaffold(
backgroundColor: AppColors.secondprimary,
resizeToAvoidBottomInset: false,
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Column(
children: [
Stack(
children: [
SizedBox(
width: double.infinity,
height: height * 0.4,
child: Consumer(
builder: (context, ref, _) {
final profileData = ref.watch(
profilegetvendorProvider,
);
return profileData.when(
data: (profiles) {
final profile = profiles.isNotEmpty
? profiles[0]
: null;
if (profile != null &&
!_isProfileDataInitialized) {
WidgetsBinding.instance
.addPostFrameCallback((_) {
setState(() {
nameController.text =
profile.name ?? '';
numberController.text =
profile.number ?? '';
emailController.text =
profile.email ?? '';
addressController.text =
profile.address ?? '';
_isProfileDataInitialized = true;
});
});
}
return _image == null
? _buildNetworkImage(
profile?.profilePic1,
)
: Image.file(
_image!,
fit: BoxFit.cover,
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, _) => Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 50),
Text('Error loading profile: $error'),
],
),
),
);
},
),
),
Positioned(
bottom: 0,
child: CustomPaint(
size: Size(width, height * 0.4),
painter: RightToLeftFullWidthTrianglePainter(),
),
),
Positioned(
bottom: height * 0.03,
left: width / 2 - 170,
child: Consumer(
builder: (context, ref, _) {
final profileData = ref.watch(
profilegetvendorProvider,
);
return profileData.when(
data: (profiles) {
final profile = profiles.isNotEmpty
? profiles[0]
: null;
return CircleAvatar(
radius: 68,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 65,
backgroundImage:
_getAvatarImageProvider(
profile?.profilePic1,
),
onBackgroundImageError:
(error, stackTrace) {
print(
'Avatar image error: $error',
);
},
),
);
},
loading: () => const CircleAvatar(
radius: 68,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 65,
child: CircularProgressIndicator(),
),
),
error: (error, _) => const CircleAvatar(
radius: 68,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 65,
backgroundImage: AssetImage(
AppAssets.profile,
),
),
),
);
},
),
),
Positioned(
bottom: height * 0.08,
left: width / 2 - 20,
child: Consumer(
builder: (context, ref, _) {
final profileData = ref.watch(
profilegetvendorProvider,
);
return profileData.when(
data: (profiles) {
final profile = profiles.isNotEmpty
? profiles[0]
: null;
if (profile == null) {
return Text(
'No Profile Data',
style: gilroyTextStyle,
);
}
return Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
profile.name ?? 'No Name',
style: gilroyTextStyle,
),
IconButton(
onPressed: _pickImage,
icon: const Icon(Icons.edit),
),
],
),
Row(
children: [
const Icon(
Icons.location_city,
size: 20,
),
const SizedBox(width: 4),
Text(
profile.address ??
'No address available',
style: const TextStyle(
fontSize: 12,
),
),
],
),
],
);
},
loading: () =>
const CircularProgressIndicator(),
error: (error, _) => Text(
'Error: $error',
style: gilroyTextStyle,
),
);
},
),
),
],
),
const SizedBox(height: 30),
Padding(
padding: EdgeInsets.symmetric(horizontal: width * 0.05),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Name", style: gilroyTextStyle),
const SizedBox(height: 15),
CommonTextFormField(
hintText: "Enter your name",
controller: nameController,
),
const SizedBox(height: 30),
Text("Mobile Number", style: gilroyTextStyle),
const SizedBox(height: 15),
CommonTextFormField(
hintText: "Enter your mobile number",
keyboardType: TextInputType.phone,
controller: numberController,
),
const SizedBox(height: 30),
Text("Address", style: gilroyTextStyle),
const SizedBox(height: 15),
CommonTextFormField(
hintText: "Enter your address",
controller: addressController,
),
const SizedBox(height: 30),
Text("Email", style: gilroyTextStyle),
const SizedBox(height: 15),
CommonTextFormField(
hintText: "Enter your email",
keyboardType: TextInputType.emailAddress,
controller: emailController,
),
const SizedBox(height: 30),
Center(
child: Consumer(
builder: (context, ref, _) {
return CommanButton(
width: width * 0.6,
text: "Save",
textStyle: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 23,
height: 1.0,
letterSpacing: 0.01,
color: AppColors.secondprimary,
),
onPressed: () async {
try {
final repo = ref.read(
profileupdatevendorRepositoryProvider,
);
await repo.updateProfilevendor(
url: ConstsApi.upadatevendorprofile,
name: nameController.text,
number: numberController.text,
email: emailController.text,
address: addressController.text,
imageFile: _image,
);
if (mounted) {
Fluttertoast.showToast(
msg: "Profile updated successfully",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
);
ref.refresh(profilegetvendorProvider);
Future.delayed(
const Duration(milliseconds: 1500),
() {
if (mounted) {
Navigator.of(context).pop();
}
},
);
}
} catch (e) {
if (mounted) {
Fluttertoast.showToast(
msg: "Error updating profile: $e",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
},
);
},
),
),
const SizedBox(height: 30),
],
),
),
],
),
),
),
);
},
),
),
);
}
}
class RightToLeftFullWidthTrianglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.white;
final path = Path();
path.moveTo(size.width, 0);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
@@ -0,0 +1,833 @@
import 'dart:io';
import 'dart:ui';
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:bookmywages/viewmodel/consts_api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:image_picker/image_picker.dart';
import 'package:shared_preferences/shared_preferences.dart';
class VendorProfileMain extends ConsumerStatefulWidget {
const VendorProfileMain({super.key});
@override
ConsumerState<VendorProfileMain> createState() => _VendorProfileMainState();
}
class _VendorProfileMainState extends ConsumerState<VendorProfileMain> {
File? _image;
final ImagePicker _picker = ImagePicker();
bool _isProfileDataInitialized = false;
bool _isUploading = false;
final TextStyle gilroyTextStyle = const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20,
height: 1.45,
letterSpacing: 0.01,
);
// Modified _pickImage method with automatic API upload using existing repository
Future<void> _pickImage() async {
try {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
_image = File(pickedFile.path);
_isUploading = true;
});
// Automatically upload to API using existing repository
await _uploadImageToAPI(File(pickedFile.path));
}
} catch (e) {
debugPrint('Error picking image: $e');
_showErrorToast('Failed to pick image: $e');
}
}
// Upload image using existing ProfileupdateRepository
Future<void> _uploadImageToAPI(File imageFile) async {
try {
// Get current profile data
final profileData = ref.read(profilegetvendorProvider);
await profileData.when(
data: (profiles) async {
if (profiles.isNotEmpty) {
final profile = profiles[0];
// Get the repository
final repository = ref.read(profileupdatevendorRepositoryProvider);
// Update profile with new image and existing data
await repository.updateProfilevendor(
url: ConstsApi.upadatevendorprofile,
name: profile.name ?? '',
number: profile.number ?? '',
email: profile.email ?? '',
address: profile.address ?? '',
imageFile: imageFile,
);
// Refresh profile data to get updated image URL
ref.invalidate(profilegetvendorProvider);
_showSuccessToast('Profile picture updated successfully!');
// Clear local image since it's now uploaded
if (mounted) {
setState(() {
_image = null;
});
}
} else {
_showErrorToast('No profile data available');
if (mounted) {
setState(() {
_image = null;
});
}
}
},
loading: () async {
_showErrorToast('Profile data is still loading');
if (mounted) {
setState(() {
_image = null;
});
}
},
error: (error, _) async {
_showErrorToast('Error getting profile data: $error');
if (mounted) {
setState(() {
_image = null;
});
}
},
);
} catch (e) {
debugPrint('Error uploading image: $e');
_showErrorToast('Upload failed: $e');
if (mounted) {
setState(() {
_image = null;
});
}
} finally {
if (mounted) {
setState(() {
_isUploading = false;
});
}
}
}
// Helper methods for showing toasts
void _showSuccessToast(String message) {
if (mounted) {
Fluttertoast.showToast(
msg: message,
backgroundColor: Colors.green,
textColor: Colors.white,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
}
}
void _showErrorToast(String message) {
if (mounted) {
Fluttertoast.showToast(
msg: message,
backgroundColor: Colors.red,
textColor: Colors.white,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
);
}
}
// Helper method to validate and construct image URL
String? _getValidImageUrl(String? imageUrl) {
if (imageUrl == null || imageUrl.isEmpty) {
return null;
}
// Check if URL is complete (has a filename)
if (imageUrl.endsWith('/') || imageUrl.endsWith('/images/')) {
return null;
}
// Ensure URL is properly formatted
if (!imageUrl.startsWith('http')) {
return null;
}
return imageUrl;
}
// Helper widget for safe network image loading
Widget _buildNetworkImage(String? imageUrl, {BoxFit fit = BoxFit.cover}) {
final validUrl = _getValidImageUrl(imageUrl);
if (validUrl == null) {
return Image.asset(AppAssets.profile, fit: fit);
}
return Image.network(
validUrl,
fit: fit,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value:
loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
debugPrint('Error loading image: $error');
return Image.asset(AppAssets.profile, fit: fit);
},
);
}
// Helper method for CircleAvatar image provider
ImageProvider _getAvatarImageProvider(String? imageUrl) {
if (_image != null) {
return FileImage(_image!);
}
final validUrl = _getValidImageUrl(imageUrl);
if (validUrl != null) {
return NetworkImage(validUrl);
}
return const AssetImage(AppAssets.profile);
}
void _showSignOutDialog() {
if (!mounted) return;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Sign Out'),
content: const Text('Are you sure you want to sign out?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
final prefs = await SharedPreferences.getInstance();
await prefs.remove('userId');
await prefs.remove('vendor_id');
await prefs.remove('data');
if (context.mounted) {
// context.go(
// RouterConts.loginpage,
// ); // or use pushReplacement if needed
}
},
child: const Text('Sign Out'),
),
],
);
},
);
}
Widget _buildMenuItem(IconData icon, String title) {
return ListTile(
leading: Icon(
icon,
size: 23.000513076782227,
color: const Color(0xFF3E3E3F),
),
title: Text(
title,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 18,
height: 1.0,
letterSpacing: 0.2,
color: Color(0xFF3E3E3F),
),
),
trailing: const Icon(Icons.arrow_forward_ios_rounded),
onTap: () async {
try {
if (title == 'My Profile') {
await Get.toNamed(RouterConts.vendoreditprofile);
} else if (title == 'Change Password') {
// await context.push(RouterConts.changepassword);
} else if (title == 'Sign out') {
_showSignOutDialog();
} else if (title == 'Terms and Conditions') {
_showTermsAndConditionsDialog(ref); // Call the new dialog
} else if (title == 'Privacy policy') {
_showprivacyDialog(ref); // Call the new dialog
}
// Add other navigation cases as needed
} catch (e) {
debugPrint('Navigation error: $e');
if (mounted) {
_showErrorToast('Navigation error: $e');
}
}
},
);
}
@override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double height = MediaQuery.of(context).size.height;
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: SafeArea(
child: Column(
children: [
Flexible(
flex: 1,
child: Stack(
children: [
SizedBox(
width: double.infinity,
height: height * 0.4,
child: Consumer(
builder: (context, ref, _) {
final profileData = ref.watch(profilegetvendorProvider);
return profileData.when(
data: (profiles) {
final profile =
profiles.isNotEmpty ? profiles[0] : null;
if (profile != null && !_isProfileDataInitialized) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_isProfileDataInitialized = true;
});
}
});
}
return _image == null
? _buildNetworkImage(profile?.profilePic1)
: Image.file(_image!, fit: BoxFit.cover);
},
loading:
() => const Center(
child: CircularProgressIndicator(),
),
error:
(error, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 50),
Text('Error loading profile: $error'),
],
),
),
);
},
),
),
Positioned(
bottom: 0,
child: CustomPaint(
size: Size(width, height * 0.4),
painter: RightToLeftFullWidthTrianglePainter(),
),
),
Positioned(
bottom: height * 0.03,
left: width / 2 - 170,
child: Consumer(
builder: (context, ref, _) {
final profileData = ref.watch(profilegetvendorProvider);
return profileData.when(
data: (profiles) {
final profile =
profiles.isNotEmpty ? profiles[0] : null;
return Stack(
alignment: Alignment.center,
children: [
CircleAvatar(
radius: 68,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 65,
backgroundImage: _getAvatarImageProvider(
profile?.profilePic1,
),
onBackgroundImageError: (
error,
stackTrace,
) {
debugPrint('Avatar image error: $error');
},
),
),
// Show upload progress indicator
if (_isUploading)
Container(
width: 136,
height: 136,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: const Center(
child: CircularProgressIndicator(
color: Colors.white,
),
),
),
],
);
},
loading:
() => const CircleAvatar(
radius: 68,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 65,
child: CircularProgressIndicator(),
),
),
error:
(error, _) => const CircleAvatar(
radius: 68,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 65,
backgroundImage: AssetImage(
AppAssets.profile,
),
),
),
);
},
),
),
Positioned(
bottom: height * 0.08,
left: width / 2 - 20,
child: Consumer(
builder: (context, ref, _) {
final profileData = ref.watch(profilegetvendorProvider);
return profileData.when(
data: (profiles) {
final profile =
profiles.isNotEmpty ? profiles[0] : null;
if (profile == null) {
return Text(
'No Profile Data',
style: gilroyTextStyle,
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
profile.name ?? 'No Name',
style: gilroyTextStyle,
),
IconButton(
onPressed:
_isUploading ? null : _pickImage,
icon:
_isUploading
? const SizedBox(
width: 16,
height: 16,
child:
CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.edit),
),
],
),
Row(
children: [
const Icon(Icons.location_city, size: 20),
const SizedBox(width: 4),
Text(
profile.address ?? 'No address available',
style: const TextStyle(fontSize: 12),
),
],
),
],
);
},
loading: () => const CircularProgressIndicator(),
error:
(error, _) =>
Text('Error: $error', style: gilroyTextStyle),
);
},
),
),
],
),
),
const SizedBox(height: 30),
Expanded(
flex: 1,
child: ListView(
shrinkWrap: true,
children: [
_buildMenuItem(Icons.person, 'My Profile'),
_buildMenuItem(Icons.notifications, 'Notification'),
_buildMenuItem(Icons.safety_check, 'Privacy policy'),
_buildMenuItem(Icons.description, 'Terms and Conditions'),
_buildMenuItem(Icons.logout, 'Sign out'),
],
),
),
],
),
),
);
}
void _showTermsAndConditionsDialog(WidgetRef ref) {
if (!mounted) return;
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Center(
child: Consumer(
builder: (context, ref, _) {
final termsAsync = ref.watch(termsAndConditionsProvider);
return termsAsync.when(
data:
(terms) => Container(
width: 389,
height: 547,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(0xFF959595),
width: 1,
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Center(
child: Text(
"Terms & Condition",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
const SizedBox(height: 12),
Expanded(
child: ScrollbarTheme(
data: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all(
AppColors.primary,
),
trackColor: WidgetStateProperty.all(
Colors.transparent,
),
trackBorderColor: WidgetStateProperty.all(
Colors.transparent,
),
thickness: WidgetStateProperty.all(13),
radius: const Radius.circular(19),
thumbVisibility: WidgetStateProperty.all(
true,
),
),
child: Scrollbar(
thumbVisibility: true,
interactive: true,
thickness: 13,
radius: const Radius.circular(19),
child: ListView(
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
terms.data,
style: const TextStyle(
color: Colors.black,
),
),
const SizedBox(height: 20),
Center(
child: SizedBox(
width: 228,
height: 51.82,
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor:
const Color(0xFF0066FF),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(
22.95,
),
),
),
child: const Text(
"OK",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Poppins',
fontWeight:
FontWeight.w500,
fontSize: 18.87,
height: 23.59 / 18.87,
letterSpacing: -0.59,
color: Colors.white,
),
),
),
),
),
],
),
],
),
),
),
),
],
),
),
loading:
() => const Center(child: CircularProgressIndicator()),
error:
(e, _) => Container(
padding: const EdgeInsets.all(16),
child: Text(
"Error: ${e.toString()}",
style: const TextStyle(color: Colors.red),
),
),
);
},
),
),
),
);
},
);
}
void _showprivacyDialog(WidgetRef ref) {
if (!mounted) return;
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Center(
child: Consumer(
builder: (context, ref, _) {
final privacyAsync = ref.watch(privacypolicyProvider);
return privacyAsync.when(
data:
(terms) => Container(
width: 389,
height: 547,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(0xFF959595),
width: 1,
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Center(
child: Text(
"Privacy policy",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
const SizedBox(height: 12),
Expanded(
child: ScrollbarTheme(
data: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all(
AppColors.primary,
),
trackColor: WidgetStateProperty.all(
Colors.transparent,
),
trackBorderColor: WidgetStateProperty.all(
Colors.transparent,
),
thickness: WidgetStateProperty.all(13),
radius: const Radius.circular(19),
thumbVisibility: WidgetStateProperty.all(
true,
),
),
child: Scrollbar(
thumbVisibility: true,
interactive: true,
thickness: 13,
radius: const Radius.circular(19),
child: ListView(
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
terms.data,
style: const TextStyle(
color: Colors.black,
),
),
const SizedBox(height: 20),
Center(
child: SizedBox(
width: 228,
height: 51.82,
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor:
const Color(0xFF0066FF),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(
22.95,
),
),
),
child: const Text(
"OK",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Poppins',
fontWeight:
FontWeight.w500,
fontSize: 18.87,
height: 23.59 / 18.87,
letterSpacing: -0.59,
color: Colors.white,
),
),
),
),
),
],
),
],
),
),
),
),
],
),
),
loading:
() => const Center(child: CircularProgressIndicator()),
error:
(e, _) => Container(
padding: const EdgeInsets.all(16),
child: Text(
"Error: ${e.toString()}",
style: const TextStyle(color: Colors.red),
),
),
);
},
),
),
),
);
},
);
}
}
class RightToLeftFullWidthTrianglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.white;
final path = Path();
path.moveTo(size.width, 0); // top right corner
path.lineTo(size.width, size.height); // bottom right corner
path.lineTo(0, size.height); // bottom left (tip)
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,516 @@
import 'dart:ui';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:bookmywages/viewmodel/consts_api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
final selectedCategoryIndexProvider = StateProvider<int?>((ref) => null);
final deleteLoadingProvider = StateProvider<bool>((ref) => false);
class CategoryManagement extends ConsumerStatefulWidget {
const CategoryManagement({super.key});
@override
ConsumerState<CategoryManagement> createState() => _CategoryManagementState();
}
class _CategoryManagementState extends ConsumerState<CategoryManagement> {
final ScrollController _scrollController = ScrollController();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Future<void> _showDeleteConfirmation(
BuildContext context,
WidgetRef ref,
String categoryId,
String categoryName,
) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Delete Category'),
content: Text('Are you sure you want to delete "$categoryName"?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Delete'),
),
],
);
},
);
if (confirmed == true && context.mounted) {
await _deleteCategory(context, ref, categoryId);
}
}
Future<void> _deleteCategory(
BuildContext context,
WidgetRef ref,
String categoryId,
) async {
try {
ref.read(deleteLoadingProvider.notifier).state = true;
final repo = ref.read(catDeleteRepositoryProvider);
final success = await repo.deleteCategoryManagement(
ConstsApi.catmangementdelete,
categoryId,
);
ref.read(deleteLoadingProvider.notifier).state = false;
if (success) {
ref.read(selectedCategoryIndexProvider.notifier).state = null;
ref.invalidate(vendorcatgoriesProvider);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Category deleted successfully'),
backgroundColor: Colors.green,
),
);
}
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to delete category'),
backgroundColor: Colors.red,
),
);
}
}
} catch (e) {
ref.read(deleteLoadingProvider.notifier).state = false;
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
final categoryAsyncValue = ref.watch(vendorcatgoriesProvider);
final selectedIndex = ref.watch(selectedCategoryIndexProvider);
final isDeleting = ref.watch(deleteLoadingProvider);
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: Stack(
children: [
categoryAsyncValue.when(
data: (categories) =>
_buildSuccessUI(categories, selectedIndex, isDeleting),
loading: () => _buildLoadingUI(),
error: (error, _) => _buildErrorUI(error),
),
if (isDeleting) _buildDeleteOverlay(),
],
),
);
}
Widget _buildSuccessUI(List categories, int? selectedIndex, bool isDeleting) {
final filteredCategories = categories
.where((c) => c.noOfService! > 0)
.toList();
return CustomScrollView(
controller: _scrollController,
physics: const BouncingScrollPhysics(),
slivers: [
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 196 / 209,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index >= filteredCategories.length) return null;
return _buildCategoryCard(
filteredCategories[index],
index,
selectedIndex,
isDeleting,
);
},
childCount: filteredCategories.length,
addAutomaticKeepAlives: false,
addRepaintBoundaries: true,
),
),
),
const SliverToBoxAdapter(child: SizedBox(height: 20)),
],
);
}
Widget _buildCategoryCard(
dynamic category,
int index,
int? selectedIndex,
bool isDeleting,
) {
final isSelected = selectedIndex == index;
return RepaintBoundary(
child: GestureDetector(
onTap: () {
final current = ref.read(selectedCategoryIndexProvider);
ref.read(selectedCategoryIndexProvider.notifier).state =
current == index ? null : index;
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: AspectRatio(
aspectRatio: 196 / 209,
child: Stack(
fit: StackFit.expand,
children: [
// Category Image
_buildCategoryImage(category.image1.toString()),
// Action Buttons (Edit/Delete)
if (isSelected) _buildActionButtons(category, isDeleting),
// Name Overlay with Blur
_buildImageOverlay(category.name.toString()),
],
),
),
),
),
),
);
}
Widget _buildCategoryImage(String imageUrl) {
return Image.network(
imageUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
color: Colors.grey[300],
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
),
);
},
errorBuilder: (context, error, stackTrace) => Container(
color: Colors.grey[300],
child: const Center(
child: Icon(Icons.image_not_supported, color: Colors.grey, size: 32),
),
),
cacheWidth: 400,
cacheHeight: 400,
);
}
Widget _buildActionButtons(dynamic category, bool isDeleting) {
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Edit Button
GestureDetector(
onTap: () {
Get.toNamed(
RouterConts.vendorservice,
arguments: {
'id': category.id,
'tittle': category
.name, // Note: using 'tittle' to match VendorService parameter
},
);
},
child: Container(
width: 57,
height: 57,
alignment: Alignment.center,
decoration: const BoxDecoration(
color: AppColors.secondprimary,
shape: BoxShape.circle,
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.edit_document, color: AppColors.primary, size: 20),
Text(
"edit",
style: TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
),
),
),
const SizedBox(width: 10),
// Delete Button
GestureDetector(
onTap: () {
_showDeleteConfirmation(
context,
ref,
category.id.toString(),
category.name.toString(),
);
},
child: Container(
width: 57,
height: 57,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDeleting ? Colors.grey : Colors.white,
shape: BoxShape.circle,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isDeleting ? Icons.hourglass_empty : Icons.delete,
color: Colors.red,
size: 20,
),
Text(
isDeleting ? "wait" : "delete",
style: const TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
),
),
),
],
),
);
}
Widget _buildImageOverlay(String categoryName) {
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(12)),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
height: 56,
width: 196,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(255, 255, 255, 0.54),
Color.fromRGBO(153, 153, 153, 0.54),
],
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Center(
child: Text(
categoryName,
style: const TextStyle(
color: Colors.black87,
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14,
height: 1.2,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
),
),
),
);
}
Widget _buildLoadingUI() {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 196 / 209,
),
delegate: SliverChildBuilderDelegate(
(context, index) => _buildShimmerCard(),
childCount: 6,
),
),
),
],
);
}
Widget _buildShimmerCard() {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.grey[300],
),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
colors: [
Colors.grey[300]!,
Colors.grey[100]!,
Colors.grey[300]!,
],
),
),
),
Positioned(
bottom: 12,
left: 12,
right: 12,
child: Container(
height: 20,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.grey[400],
),
),
),
],
),
);
}
Widget _buildErrorUI(Object error) {
return CustomScrollView(
slivers: [
SliverFillRemaining(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'Oops! Something went wrong',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'Please check your connection and try again',
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
ref.invalidate(vendorcatgoriesProvider);
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
),
),
),
],
);
}
Widget _buildDeleteOverlay() {
return Container(
color: Colors.black.withOpacity(0.5),
child: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: Colors.white),
SizedBox(height: 16),
Text(
'Deleting category...',
style: TextStyle(color: Colors.white, fontSize: 16),
),
],
),
),
);
}
}
@@ -0,0 +1,733 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/model/cancel_booking.dart';
import 'package:bookmywages/model/vendor_model/vendor_booking_status.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:intl/intl.dart';
class VendorBookinglist extends ConsumerStatefulWidget {
const VendorBookinglist({super.key});
@override
ConsumerState<VendorBookinglist> createState() => _VendorBookinglistState();
}
class _VendorBookinglistState extends ConsumerState<VendorBookinglist> {
Map<String, dynamic> getStatusInfo(int status) {
switch (status) {
case 0:
return {
'text': 'Pending',
'color': Colors.orange,
'bgColor': Colors.orange.shade100,
};
case 1:
return {
'text': 'Scheduled',
'color': Colors.green,
'bgColor': Colors.green.shade100,
};
case 2:
return {
'text': 'Completed',
'color': Colors.blue,
'bgColor': Colors.blue.shade100,
};
case 3:
return {
'text': 'Canceled',
'color': Colors.red,
'bgColor': Colors.red.shade100,
};
default:
return {
'text': 'pending',
'color': Colors.blue,
'bgColor': Colors.blue.shade100,
};
}
}
// Format date from API response
String formatDate(String dateStr) {
try {
final date = DateTime.parse(dateStr);
return DateFormat('MMMM dd, yyyy').format(date);
} catch (e) {
return dateStr;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ref
.watch(vendorbookingdetailsProvider)
.when(
data: (bookings) {
if (bookings.isEmpty) {
return const Center(child: Text('No bookings found'));
}
return ListView.builder(
itemCount: bookings.length,
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) {
final booking = bookings[index];
final statusInfo = getStatusInfo(booking.status ?? 0);
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.lightGrey),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ID and View order
Padding(
padding: const EdgeInsets.only(
right: 12,
left: 12,
top: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID : ${booking.id}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
const Divider(),
// Image, Company, and Status
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child:
(booking.images1 != null &&
booking.images1!.isNotEmpty)
? Image.network(
booking.images1!,
width: 100,
height: 110,
fit: BoxFit.cover,
errorBuilder: (
context,
error,
stackTrace,
) {
return Image.asset(
AppAssets.cleaning,
width: 100,
height: 110,
fit: BoxFit.cover,
);
},
)
: Image.asset(
AppAssets.cleaning,
width: 100,
height: 110,
fit: BoxFit.cover,
),
),
Positioned(
top: 6,
left: 6,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(
8,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
const Text(
'Live',
style: TextStyle(
fontSize: 12,
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
const SizedBox(width: 12),
Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
booking.name ?? 'No data',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16.11,
height: 14.5 / 16.11,
letterSpacing: 0.01 * 16.11,
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
booking.serviceName ?? '',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 13.91,
height: 1.3,
letterSpacing: 0.01 * 13.91,
color: Color(0xFF5A5A5A),
),
),
),
const SizedBox(width: 10),
Expanded(
child: Container(
width:
booking.status == 3
? 70
: booking.status == 1
? 85
: 77.84,
height:
booking.status == 3
? 25
: booking.status == 1
? 25
: 27.96,
decoration: BoxDecoration(
color:
booking.status == 3
? const Color(
0xFFFFEEEE,
)
: booking.status == 1
? const Color(
0xFFDAE9FF,
)
: const Color(
0xFFE6F7E6,
),
borderRadius:
BorderRadius.circular(6.05),
),
child: Center(
child: Text(
booking.status == 3
? 'Cancel'
: booking.status == 1
? 'completed'
: booking.status == 4
? 'Scheduled'
: 'pending',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 10.92,
fontWeight: FontWeight.w400,
color:
booking.status == 3
? const Color(
0xFFFF0000,
)
: booking.status ==
1
? const Color(
0xFF0066FF,
)
: const Color(
0xFF2E8B57,
),
letterSpacing: 1.0,
height: 0.98,
),
),
),
),
),
],
),
const SizedBox(height: 6),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'Date :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.serviceDate != null
? _formatDate(
booking.serviceDate!,
)
: 'No date',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'Time :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.serviceDate != null
? DateFormat(
'h:mm a',
).format(
DateTime.parse(
booking.serviceDate!,
),
)
: 'No time',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
],
),
],
),
),
],
),
),
const SizedBox(height: 16),
/// Date / Time / Hours
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
"Mobile number : ",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(width: 7),
Text(
booking.mobileNumber.toString(),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
const SizedBox(height: 13),
Row(
children: [
const Text(
"E-mail ID :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(width: 7),
Text(
booking.email.toString(),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
const SizedBox(height: 13),
Row(
children: [
const Text(
"Address : ",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(width: 7),
Text(
booking.address.toString(),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
const SizedBox(height: 13),
const Text(
"Message : ",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 13),
Text(
booking.message.toString(),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
),
const SizedBox(height: 20),
if (booking.status == 0)
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
SizedBox(
height: 42,
child: ElevatedButton(
onPressed: () async {
final scaffoldMessenger =
ScaffoldMessenger.of(context);
try {
// Prepare data for status update
final data = VendorBookingStatus(
id: booking.id.toString(),
status:
"4", // Change to status 1 (confirmed)
);
// Call the provider to update status
await ref.read(
changebookingProvider(data).future,
);
Fluttertoast.showToast(
msg:
'Booking confirmed successfully',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
// Refresh the booking list
ref.invalidate(
vendorbookingdetailsProvider,
);
} catch (e) {
Fluttertoast.showToast(
msg:
'Failed to confirm booking: $e',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(
0xFF0066FF,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
20,
),
),
),
child: const FittedBox(
child: Text(
"Confirmed",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w800,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0.289,
color: Colors.white,
),
),
),
),
),
GestureDetector(
onTap: () async {
final data = CancelBookingRequest(
id: booking.id.toString(),
serviceId: booking.serviceId.toString(),
type:
booking.type == null
? "0"
: booking.type.toString(),
);
final scaffoldMessenger =
ScaffoldMessenger.of(context);
final navigator = Navigator.of(context);
try {
await ref.read(
cancelbookingProvider(data).future,
);
Fluttertoast.showToast(
msg: 'Booking cancelled successfully',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
ref.invalidate(
vendorbookingdetailsProvider,
);
} catch (e) {
Fluttertoast.showToast(
msg: 'Failed to cancel booking: $e',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
}
},
child: const Text(
'Cancel Booking',
style: TextStyle(
fontFamily: 'Gilroy-Bold', // Add this
fontWeight:
FontWeight
.w400, // 400 is normal weight
fontSize: 14.45,
height:
13 /
14.45, // Convert line-height to Flutter's height property
letterSpacing: 0.02, // 2% as decimal
color: Color(0xff534E4E),
),
),
),
],
),
)
else if (booking.status == 4)
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
SizedBox(
height: 42,
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(
0xFF0066FF,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
20,
),
),
),
child: const FittedBox(
child: Text(
"pending",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w800,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0.289,
color: Colors.white,
),
),
),
),
),
GestureDetector(
onTap: () async {
final scaffoldMessenger =
ScaffoldMessenger.of(context);
try {
// Prepare data for status update
final data = VendorBookingStatus(
id: booking.id.toString(),
status:
"1", // Change to status 1 (confirmed)
);
// Call the provider to update status
await ref.read(
changebookingProvider(data).future,
);
Fluttertoast.showToast(
msg: 'Booking confirmed successfully',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
// Refresh the booking list
ref.invalidate(
vendorbookingdetailsProvider,
);
} catch (e) {
Fluttertoast.showToast(
msg: 'Failed to confirm booking: $e',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
}
},
child: const Text(
'complete',
style: TextStyle(
fontFamily: 'Gilroy-Bold', // Add this
fontWeight:
FontWeight
.w400, // 400 is normal weight
fontSize: 14.45,
height:
13 /
14.45, // Convert line-height to Flutter's height property
letterSpacing: 0.02, // 2% as decimal
color: Color(0xff534E4E),
),
),
),
],
),
),
const SizedBox(height: 10),
],
),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) =>
Center(child: Text('Error loading bookings: $err')),
),
),
);
}
String _formatDate(String dateString) {
try {
return DateFormat('dd/MM/yyyy').format(DateTime.parse(dateString));
} catch (e) {
return dateString; // Fallback: return the original string if parsing fails
}
}
}
@@ -0,0 +1,420 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
class VendorCancelBooking extends ConsumerWidget {
// Changed to ConsumerWidget
const VendorCancelBooking({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Added WidgetRef parameter
final bookingAsyncValue = ref.watch(vendorbookingdetailsProvider);
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: bookingAsyncValue.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Error: $err')),
data: (bookings) {
// Filter only canceled bookings (status == 3)
final canceledBookings =
bookings.where((booking) => booking.status == 3).toList();
if (canceledBookings.isEmpty) {
return const Center(
child: Text(
'No canceled bookings found',
style: TextStyle(fontSize: 16),
),
);
}
return ListView.builder(
itemCount: canceledBookings.length,
shrinkWrap: false,
itemBuilder: (context, index) {
final booking = canceledBookings[index];
return Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.lightGrey),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Top Row: ID and "View order"
Padding(
padding: const EdgeInsets.only(
right: 12,
left: 12,
top: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID : ${booking.id}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
const Divider(),
/// Image, Company, and Status
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child:
(booking.images1 != null &&
booking.images1!.isNotEmpty)
? Image.network(
booking.images1!,
width: 100,
height: 110,
fit: BoxFit.cover,
errorBuilder: (
context,
error,
stackTrace,
) {
return Image.asset(
AppAssets.cleaning,
width: 100,
height: 110,
fit: BoxFit.cover,
);
},
)
: Image.asset(
AppAssets.cleaning,
width: 100,
height: 110,
fit: BoxFit.cover,
),
),
Positioned(
top: 6,
left: 6,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
const Text(
'Live',
style: TextStyle(
fontSize: 12,
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
const SizedBox(width: 12),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
booking.serviceName ?? 'No vendor name',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16.11,
height: 14.5 / 16.11,
letterSpacing: 0.01 * 16.11,
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
booking.categoryName ??
'No service name',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 13.91,
height: 12.52 / 13.91,
letterSpacing: 0.01 * 13.91,
color: Color(0xFF5A5A5A),
),
),
const SizedBox(width: 10),
Container(
width: 70,
height: 25,
decoration: BoxDecoration(
color: const Color(0xFFFFEEEE),
borderRadius: BorderRadius.circular(
6.05,
),
),
child: Center(
child: Text(
'Cancel',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 10.92,
fontWeight: FontWeight.w400,
color: const Color(0xFFFF0000),
letterSpacing: 1.0,
height: 0.98,
),
),
),
),
],
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
color: AppColors.lightGrey,
),
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 15,
backgroundImage:
booking.profilePic1 != null &&
booking
.profilePic1!
.isNotEmpty
? NetworkImage(
booking.profilePic1!,
)
: null,
child:
booking.profilePic1 == null ||
booking
.profilePic1!
.isEmpty
? const Icon(
Icons.person,
size: 20,
)
: null,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
booking.username ??
'No data',
style: const TextStyle(
fontFamily:
'Gilroy-Medium',
fontWeight:
FontWeight.w600,
fontSize: 11,
height: 10.65 / 11.84,
letterSpacing: 0,
color: Color(
0xFF353434,
),
),
overflow:
TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 7),
Text(
'client',
style: const TextStyle(
fontFamily: 'Gilroy-Regular',
fontWeight: FontWeight.w400,
fontSize: 11.84,
height: 10.65 / 11.84,
letterSpacing: 0,
color: Color(0xFF717171),
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
/// Date / Time / Hours
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Date :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.cancelDate != null
? _formatDate(booking.cancelDate!)
: 'No date',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Time :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.cancelDate != null
? DateFormat('h:mm a').format(
DateTime.parse(booking.cancelDate!),
)
: 'No time',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Working hours :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.workingHours ?? 'No hours',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
],
),
),
const SizedBox(height: 20),
],
),
);
},
);
},
),
),
);
}
String _formatDate(String dateString) {
try {
return DateFormat('dd-MM-yyyy').format(DateTime.parse(dateString));
} catch (e) {
return dateString; // Fallback: return the original string if parsing fails
}
}
}
@@ -0,0 +1,417 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
class VendorCompletedBooking extends ConsumerWidget {
// Changed to ConsumerWidget
const VendorCompletedBooking({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Added WidgetRef parameter
final bookingAsyncValue = ref.watch(vendorbookingdetailsProvider);
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: bookingAsyncValue.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Error: $err')),
data: (bookings) {
// Filter only canceled bookings (status == 3)
final canceledBookings =
bookings.where((booking) => booking.status == 1).toList();
if (canceledBookings.isEmpty) {
return const Center(
child: Text(
'No canceled bookings found',
style: TextStyle(fontSize: 16),
),
);
}
return ListView.builder(
itemCount: canceledBookings.length,
shrinkWrap: false,
itemBuilder: (context, index) {
final booking = canceledBookings[index];
return Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.lightGrey),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Top Row: ID and "View order"
Padding(
padding: const EdgeInsets.only(
right: 12,
left: 12,
top: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID : ${booking.id}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
const Divider(),
/// Image, Company, and Status
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child:
(booking.images1 != null &&
booking.images1!.isNotEmpty)
? Image.network(
booking.images1!,
width: 100,
height: 110,
fit: BoxFit.cover,
errorBuilder: (
context,
error,
stackTrace,
) {
return Image.asset(
AppAssets.cleaning,
width: 100,
height: 110,
fit: BoxFit.cover,
);
},
)
: Image.asset(
AppAssets.cleaning,
width: 100,
height: 110,
fit: BoxFit.cover,
),
),
Positioned(
top: 6,
left: 6,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
const Text(
'Live',
style: TextStyle(
fontSize: 12,
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
const SizedBox(width: 12),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
booking.serviceName ?? 'No vendor name',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16.11,
height: 14.5 / 16.11,
letterSpacing: 0.01 * 16.11,
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
booking.categoryName ??
'No service name',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 13.91,
height: 12.52 / 13.91,
letterSpacing: 0.01 * 13.91,
color: Color(0xFF5A5A5A),
),
),
const SizedBox(width: 10),
Container(
width: 70,
height: 25,
decoration: BoxDecoration(
color: const Color(0xffFFEEDB),
borderRadius: BorderRadius.circular(
6.05,
),
),
child: Center(
child: Text(
'Completed',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontSize: 10.92,
fontWeight: FontWeight.w400,
color: const Color(0xffFF8515),
letterSpacing: 1.0,
height: 0.98,
),
),
),
),
],
),
const SizedBox(height: 10),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'Date :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.serviceDate != null
? _formatDate(
booking.serviceDate!,
)
: 'No date',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'Time :',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
const SizedBox(height: 10),
Text(
booking.serviceTime.toString(),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
],
),
],
),
),
],
),
),
const SizedBox(height: 16),
/// Date / Time / Hours
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
"Mobile number : ",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
SizedBox(width: 7),
Text(
booking.mobileNumber.toString(),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
SizedBox(height: 10),
Row(
children: [
Text(
"E-mail ID :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
SizedBox(width: 7),
Text(
booking.email.toString(),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
SizedBox(height: 10),
Row(
children: [
Text(
"Address : ",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
SizedBox(width: 7),
Text(
booking.address.toString(),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
SizedBox(height: 10),
Text(
"Message : ",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
SizedBox(height: 7),
Text(
booking.message.toString(),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 14.45,
height: 13 / 14.45,
letterSpacing: 0,
),
),
],
),
),
const SizedBox(height: 20),
],
),
);
},
);
},
),
),
);
}
String _formatDate(String dateString) {
try {
return DateFormat('dd-MM-yyyy').format(DateTime.parse(dateString));
} catch (e) {
return dateString; // Fallback: return the original string if parsing fails
}
}
}
@@ -0,0 +1,395 @@
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
class VendorEnquriyList extends ConsumerStatefulWidget {
const VendorEnquriyList({super.key});
@override
ConsumerState<VendorEnquriyList> createState() => _VendorEnquriyListState();
}
class _VendorEnquriyListState extends ConsumerState<VendorEnquriyList> {
int? expandedIndex;
@override
Widget build(BuildContext context) {
final enquiryListAsync = ref.watch(enquriylistProvider);
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: enquiryListAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
data:
(enquiries) => SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: List.generate(enquiries.length, (index) {
final enquiry = enquiries[index];
final isExpanded = expandedIndex == index;
return SizedBox(
width: double.infinity,
child: Column(
children: [
Container(
margin: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
decoration: BoxDecoration(
color: AppColors.secondprimary,
borderRadius: BorderRadius.circular(11),
border: Border.all(
color: const Color(0xFFE8E8E8),
width: 1,
),
boxShadow: const [
BoxShadow(
color: Color(0x66AEAEAE),
offset: Offset(0, 2),
blurRadius: 4,
),
],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child:
(enquiry.images1.isEmpty)
? Container(
width: 89,
height: 64,
color: Colors.grey[300],
child: Icon(
Icons.image,
color: Colors.grey[600],
),
)
: Image.network(
enquiry.images1.first,
width: 89,
height: 64,
fit: BoxFit.cover,
errorBuilder:
(
context,
error,
stackTrace,
) => Container(
width: 89,
height: 64,
color: Colors.grey[300],
child: Icon(
Icons.broken_image,
color: Colors.grey[600],
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
enquiry.vendorName ?? 'vendorName',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16,
height: 18.14 / 16,
letterSpacing: 0.16,
color: Colors.black,
),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
setState(() {
expandedIndex =
isExpanded ? null : index;
});
},
child: Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Expanded(
child: Text(
enquiry.serviceName,
textAlign: TextAlign.left,
maxLines: 2,
overflow:
TextOverflow.ellipsis,
style: const TextStyle(
fontFamily:
'Gilroy-Medium',
fontWeight:
FontWeight.w400,
fontSize: 16.11,
height: 17.3 / 16.11,
letterSpacing: 0.1611,
color: Colors.black,
),
),
),
const SizedBox(width: 4),
Flex(
direction: Axis.horizontal,
mainAxisSize:
MainAxisSize.min,
children: [
Text(
isExpanded
? "View Less"
: "View More",
style: const TextStyle(
fontWeight:
FontWeight.w600,
fontSize: 13.15,
height: 5.71 / 13.15,
color: Color(
0xFFFF3D00,
),
),
),
const SizedBox(width: 5),
Icon(
isExpanded
? Icons
.keyboard_arrow_up
: Icons
.keyboard_arrow_down,
color: const Color(
0xffFF3D00,
),
),
],
),
],
),
),
],
),
),
],
),
],
),
),
),
if (isExpanded)
Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.43),
border: Border.all(
color: const Color(0xFFE8E8E8),
width: 0.82,
),
boxShadow: const [
BoxShadow(
color: Color(0xA9A9A940),
blurRadius: 3.29,
offset: Offset(0, 0.82),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text.rich(
TextSpan(
children: [
const TextSpan(
text: "Name : ",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 13,
height: 1.239,
),
),
TextSpan(
text: enquiry.name ?? 'No Name',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 13,
height: 1.239,
color: Color(0xFF373636),
),
),
],
),
),
IconButton(
icon: const Icon(
Icons.delete,
color: AppColors.red,
size: 20,
),
onPressed: () async {
final shouldDelete = await showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text(
'Delete Enquiry',
),
content: const Text(
'Are you sure you want to delete this enquiry?',
),
actions: [
TextButton(
onPressed:
() => Navigator.of(
context,
).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed:
() => Navigator.of(
context,
).pop(true),
child: const Text('Delete'),
),
],
),
);
if (shouldDelete == true) {
Fluttertoast.showToast(
msg: 'Deleting enquiry...',
toastLength:
Toast
.LENGTH_SHORT, // ~2 seconds
gravity: ToastGravity.BOTTOM,
);
try {
final success = await ref.read(
enquriydeleteProvider(
enquiry.id.toString(),
).future,
);
if (success) {
// Reset expanded index and refresh the list
setState(() {
expandedIndex = null;
});
await Future.delayed(
const Duration(
milliseconds: 300,
),
);
ref.invalidate(
enquriylistProvider,
);
Fluttertoast.showToast(
msg:
'Enquiry deleted successfully',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
} else {
Fluttertoast.showToast(
msg: 'Failed to delete enquiry',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
}
} catch (e) {
Fluttertoast.showToast(
msg: 'Error: ${e.toString()}',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
}
}
},
),
],
),
const SizedBox(height: 8),
_infoText(
"Mobile number : ",
enquiry.mobile ?? 'No Mobile',
),
const SizedBox(height: 13),
_infoText(
"E-mail Id : ",
enquiry.email ?? 'No Email',
),
const SizedBox(height: 13),
_infoText(
"Message : ",
enquiry.message ?? 'No Message',
),
],
),
),
],
),
);
}),
),
),
),
);
}
Widget _infoText(String label, String value) {
return Text.rich(
TextSpan(
children: [
TextSpan(
text: label,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 13,
height: 1.239,
),
),
TextSpan(
text: value,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 13,
height: 1.239,
color: Color(0xFF373636),
),
),
],
),
);
}
}
@@ -0,0 +1,181 @@
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_history/category_management.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_bookinglist.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_cancel_booking.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_completed_booking.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_enquriy_list.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_history/vendor_payment_details.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_maincontoller.dart';
import 'package:flutter/material.dart';
class VendorHistoryMainController extends StatefulWidget {
final int? initialTabIndex;
const VendorHistoryMainController({super.key, this.initialTabIndex});
@override
State<VendorHistoryMainController> createState() =>
_VendorHistoryMainControllerState();
}
class _VendorHistoryMainControllerState
extends State<VendorHistoryMainController>
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
int _currentIndex = 0;
TabController? _tabController;
@override
bool get wantKeepAlive => true;
final List<String> tabTitles = [
"Booking List", // Index 0
"Category Management", // Index 1
"Payment Details", // Index 2
"Completed Booking", // Index 3
"Enquiry List", // Index 4
"Cancelation Booking", // Index 5
];
@override
void initState() {
super.initState();
// Set initial tab index from widget parameter
if (widget.initialTabIndex != null &&
widget.initialTabIndex! >= 0 &&
widget.initialTabIndex! < tabTitles.length) {
_currentIndex = widget.initialTabIndex!;
}
// Initialize TabController with the correct initial index
_tabController = TabController(
length: tabTitles.length,
vsync: this,
initialIndex: _currentIndex,
);
_tabController!.addListener(() {
if (_tabController!.indexIsChanging) {
setState(() {
_currentIndex = _tabController!.index;
});
}
});
}
@override
void dispose() {
_tabController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
if (_tabController == null) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
return PopScope(
onPopInvoked: (didPop) {
if (didPop) {
// Reset main controller index to 0 when going back
final indexController = InheritedVendorIndexController.of(context);
indexController?.changeIndex(0);
}
},
child: Scaffold(
backgroundColor: Colors.white,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(150.0),
child: Column(
children: [
AppBar(
backgroundColor: Colors.white,
elevation: 0,
title: const Text(
"History",
style: TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
centerTitle: true,
),
Container(
margin: const EdgeInsets.only(top: 16.0),
alignment: Alignment.centerLeft,
height: 65,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: TabBar(
controller: _tabController,
isScrollable: true,
tabAlignment: TabAlignment.start,
dividerColor: Colors.transparent,
indicatorColor: Colors.transparent,
labelPadding: const EdgeInsets.symmetric(horizontal: 8.0),
tabs: List.generate(
tabTitles.length,
(index) => _buildCustomTab(
tabTitles[index],
index == _currentIndex,
),
),
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
),
),
),
],
),
),
body: TabBarView(
controller: _tabController,
physics: const NeverScrollableScrollPhysics(),
children: const [
VendorBookinglist(), // Index 0
CategoryManagement(), // Index 1
VendorPaymentDetails(), // Index 2
VendorCompletedBooking(), // Index 3
VendorEnquriyList(), // Index 4
VendorCancelBooking(), // Index 5
],
),
),
);
}
Widget _buildCustomTab(String text, bool isSelected) {
return Tab(
height: 53,
child: Container(
width: 180,
height: 45,
decoration: BoxDecoration(
color: isSelected ? AppColors.primary : AppColors.secondprimary,
border: Border.all(color: const Color(0xFFDBDBDB)),
borderRadius: BorderRadius.circular(15),
),
alignment: Alignment.center,
child: Text(
text,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w400,
fontSize: 15,
height: 1.0,
letterSpacing: 0.5,
color: isSelected ? AppColors.secondprimary : AppColors.lightgray,
),
),
),
);
}
}
@@ -0,0 +1,445 @@
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:intl/intl.dart';
class VendorPaymentDetails extends ConsumerStatefulWidget {
const VendorPaymentDetails({super.key});
@override
ConsumerState<VendorPaymentDetails> createState() =>
_VendorPaymentDetailsState();
}
class _VendorPaymentDetailsState extends ConsumerState<VendorPaymentDetails> {
List<dynamic> _localPayments = [];
@override
Widget build(BuildContext context) {
final paymentAsync = ref.watch(paymentdetailsProvider);
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: paymentAsync.when(
data: (payments) {
final filteredPayments = payments.where((p) => p.type == 2).toList();
// Initialize local copy only once
if (_localPayments.isEmpty) {
_localPayments = List.from(filteredPayments);
}
if (_localPayments.isEmpty) {
return const Center(child: Text("No payment history found."));
}
return RefreshIndicator(
onRefresh: () async {
final refreshed = await ref.refresh(
paymentdetailsProvider.future,
);
setState(() {
_localPayments = refreshed.where((p) => p.type == 2).toList();
});
},
child: ListView.builder(
itemCount: _localPayments.length,
itemBuilder: (context, index) {
final payment = _localPayments[index];
return Dismissible(
key: Key(payment.id.toString()),
direction: DismissDirection.endToStart,
background: Container(
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(20),
),
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (direction) async {
return await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Confirm Delete"),
content: const Text(
"Are you sure you want to delete this payment record?",
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text("CANCEL"),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text(
"DELETE",
style: TextStyle(color: Colors.red),
),
),
],
);
},
);
},
onDismissed: (direction) async {
final scaffold = ScaffoldMessenger.of(context);
Fluttertoast.showToast(
msg: 'Deleting payment...',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
final success = await ref.read(
paymentdeleteProvider(payment.id.toString()).future,
);
if (success) {
setState(() {
_localPayments.removeAt(index);
});
scaffold.showSnackBar(
SnackBar(
content: Text(
"${payment.planName} deleted successfully",
),
),
);
} else {
scaffold.showSnackBar(
const SnackBar(
content: Text("Failed to delete payment"),
),
);
}
},
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(0xFFCFCFCF),
width: 1,
),
),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 130,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF334E95),
borderRadius: BorderRadius.circular(12.86),
border: Border.all(
color: Colors.white,
width: 0.43,
),
boxShadow: [
BoxShadow(
color: const Color(0x40828282),
blurRadius: 1.71,
offset: const Offset(0, 2.14),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
payment.planName,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14,
height: 1.0,
letterSpacing: 0.1371,
color: Colors.white,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 15),
Row(
children: [
Container(
width: 6.86,
height: 6.86,
decoration: const BoxDecoration(
color: Color(0xFFF9E369),
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
_formatDuration(payment.duration),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 12,
height: 1.0,
letterSpacing: 0.0771,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 10),
Row(
children: [
Container(
width: 6.86,
height: 6.86,
decoration: const BoxDecoration(
color: Color(0xFFF9E369),
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
payment.description,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 12,
height: 1.0,
letterSpacing: 0.0771,
color: Colors.white,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
const Divider(color: Colors.white),
Center(
child: Text(
_formatPrice(payment.price),
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
payment.planName,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 19.9,
height: 1.0,
letterSpacing: 0.199,
color: Colors.black,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 10),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"Start :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 15.61,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Colors.black,
),
),
const SizedBox(height: 10),
Text(
DateFormat('MMM d, y').format(
DateTime.parse(
payment.createdDate,
),
),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w700,
fontSize: 12,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Color(0xFF636363),
),
),
],
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"End :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 15.61,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Colors.black,
),
),
const SizedBox(height: 10),
Text(
DateFormat('MMM d, y').format(
DateTime.parse(payment.endDate),
),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w700,
fontSize: 12,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Color(0xFF636363),
),
),
],
),
],
),
const SizedBox(height: 10),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"Time :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 15.61,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Colors.black,
),
),
const SizedBox(height: 10),
Text(
DateFormat('hh:mm a').format(
DateTime.parse(
payment.createdDate,
),
),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w700,
fontSize: 12,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Color(0xFF636363),
),
),
],
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"Payment :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 15.61,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Colors.black,
),
),
const SizedBox(height: 10),
Text(
_formatPrice(payment.price),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w700,
fontSize: 12,
height: 14.04 / 15.61,
letterSpacing: 0.0,
color: Color(0xFF636363),
),
),
],
),
],
),
],
),
),
),
],
),
),
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text("Error: ${e.toString()}")),
),
);
}
static String _formatDuration(int days) {
if (days == 30) return '1 Month';
if (days == 60) return '2 Months';
if (days == 90) return '3 Months';
if (days == 180) return '6 Months';
if (days == 365) return '1 Year';
return '$days Days';
}
static String _formatPrice(String price) {
if (price.startsWith('Rs.')) {
return price;
}
return 'Rs.$price';
}
}
@@ -0,0 +1,325 @@
import 'dart:io';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
class VendorService extends ConsumerStatefulWidget {
final int id;
final String tittle;
const VendorService({super.key, required this.id, required this.tittle});
@override
ConsumerState<VendorService> createState() => _VendorServiceState();
}
class _VendorServiceState extends ConsumerState<VendorService> {
@override
Widget build(BuildContext context) {
final vendorServiceAsyncValue = ref.watch(vendorserviceProvider);
return Scaffold(
backgroundColor: AppColors.secondprimary,
appBar: AppBar(
backgroundColor: AppColors.secondprimary,
leading: const Icon(Icons.arrow_back_ios_new),
title: Text(
'${widget.tittle} Service List',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20,
height: 1.0,
letterSpacing: 0.2,
),
),
centerTitle: true,
),
body: vendorServiceAsyncValue.when(
data: (services) {
final filteredServices = services
.where((service) => service.category == widget.id)
.toList();
if (filteredServices.isEmpty) {
return const Center(child: Text("No services found"));
}
return ListView.builder(
itemCount: filteredServices.length,
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) {
final service = filteredServices[index];
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFCFCFCF), width: 1),
boxShadow: [
BoxShadow(
color: const Color(0x66A9A9A9),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 100,
height: 100,
child: _buildServiceImage(
(service.images1.isNotEmpty
? service.images1.first
: null),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
service.serviceName,
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 16,
height: 1.2,
letterSpacing: 0.2,
color: Colors.black,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 10),
Text(
'Name: ${service.serviceName}',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 14,
color: Color(0xFF5A5A5A),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 6),
Text(
'Cat: ${service.categoryName}',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 14,
color: Color(0xFF5A5A5A),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 6),
Text(
'Sub-Cat: ${service.categoryName}',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 14,
color: Color(0xFF5A5A5A),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 10),
Align(
alignment: Alignment.centerLeft,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: SizedBox(
height: 32,
child: ElevatedButton(
onPressed: () {
Get.toNamed(
RouterConts.vendorserviceupload,
arguments: service,
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(
0xFF0066FF,
),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
20,
),
side: const BorderSide(
color: Color(0xFFAAAAAA),
width: 0.5,
),
),
padding: EdgeInsets.zero,
),
child: const FittedBox(
child: Text(
'Edit',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14,
height: 1.0,
letterSpacing: 0.28,
),
),
),
),
),
),
const SizedBox(width: 8),
Flexible(
child: SizedBox(
height: 32,
child: OutlinedButton(
onPressed: () async {
final result = await ref.read(
deleteserviceProvider(
service.id.toString(),
).future,
);
if (result) {
Fluttertoast.showToast(
msg: "Service deleted successfully",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
ref.invalidate(
vendorserviceProvider,
); // Refresh the service list
} else {
Fluttertoast.showToast(
msg: "Failed to delete service",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
}
},
style: OutlinedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
20,
),
),
side: const BorderSide(
color: Color(0xFFAAAAAA),
width: 0.5,
),
padding: EdgeInsets.zero,
),
child: const FittedBox(
child: Text(
'Delete',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 14,
height: 1.0,
letterSpacing: 0.28,
),
),
),
),
),
),
],
),
),
],
),
),
],
),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
"Error loading services: $err",
textAlign: TextAlign.center,
),
),
),
),
);
}
// Helper method to handle image loading with error handling
Widget _buildServiceImage(String? imagePath) {
if (imagePath == null || imagePath.isEmpty) {
return Container(
color: Colors.grey[200],
child: const Icon(Icons.work, size: 40, color: Colors.grey),
);
}
final bool isNetworkImage = imagePath.startsWith('http');
final bool isValidFilePath =
imagePath.startsWith('/') ||
(imagePath.startsWith('file://') && imagePath.length > 7);
if (isNetworkImage) {
return CachedNetworkImage(
imageUrl: imagePath,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: Colors.grey[200],
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => Container(
color: Colors.grey[200],
child: const Icon(Icons.error, size: 40, color: Colors.red),
),
);
} else if (isValidFilePath) {
return Image.file(
File(imagePath.replaceFirst('file://', '')),
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
print('Error loading image: $error');
return Container(
color: Colors.grey[200],
child: const Icon(Icons.broken_image, size: 40, color: Colors.red),
);
},
);
} else {
print('Invalid image path format: $imagePath');
return Container(
color: Colors.grey[200],
child: const Icon(
Icons.image_not_supported,
size: 40,
color: Colors.grey,
),
);
}
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,356 @@
import 'dart:convert';
import 'dart:ui';
import 'dart:math' as math;
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
class VendorController extends ConsumerStatefulWidget {
final Widget child;
final int? initialBottomIndex;
const VendorController({
super.key,
required this.child,
this.initialBottomIndex,
});
@override
ConsumerState<VendorController> createState() => _VendorControllerState();
}
class _VendorControllerState extends ConsumerState<VendorController>
with SingleTickerProviderStateMixin {
int _selectedIndex = 0;
bool _showMenu = false;
late AnimationController _animationController;
@override
void initState() {
super.initState();
// Set bottom navigation index
if (widget.initialBottomIndex != null) {
_selectedIndex = widget.initialBottomIndex!;
} else {
// Auto-detect based on current route
final currentRoute = Get.currentRoute;
if (currentRoute == RouterConts.vendorhomepage) {
_selectedIndex = 0;
} else if (currentRoute == RouterConts.vendorpackage) {
_selectedIndex = 1;
} else if (currentRoute == RouterConts.vendorcategory) {
_selectedIndex = 2;
} else if (currentRoute == RouterConts.vendorhistory) {
_selectedIndex = 3;
}
}
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
}
void _toggleMenu() {
setState(() {
_showMenu = !_showMenu;
_showMenu
? _animationController.forward()
: _animationController.reverse();
});
}
void _onItemTapped(int index, BuildContext context) {
if (_selectedIndex == index) return;
switch (index) {
case 0:
Get.offAllNamed(RouterConts.vendorhomepage);
break;
case 1:
Get.offAllNamed(RouterConts.vendorpackage);
break;
case 2:
Get.offAllNamed(RouterConts.vendorcategory);
break;
case 3:
// Navigate to vendor history with default tab
Get.offAllNamed(
RouterConts.vendorhistory,
arguments: {
'historyTab': 0, // Default to first tab
},
);
break;
}
}
void changeSelectedIndex(int index) {
setState(() {
_selectedIndex = index;
});
}
// Handle back button press
bool _onWillPop() {
if (_showMenu) {
// If menu is open, close it first
_toggleMenu();
return false;
} else if (_selectedIndex != 0) {
// If not on home tab, go to home tab
_onItemTapped(0, context);
return false;
} else {
// If on home tab, allow app to exit
return true;
}
}
// Check if current route should show FAB
bool _shouldShowFAB() {
final currentRoute = Get.currentRoute;
// Hide FAB on edit profile and other specific routes
final hideFABRoutes = [
'/vendor-edit-profile', // Add your edit profile route here
'/profile-edit',
// Add other routes where you don't want FAB
];
return !hideFABRoutes.contains(currentRoute);
}
@override
Widget build(BuildContext context) {
final isKeyboardOpen = MediaQuery.of(context).viewInsets.bottom > 0;
final currentRoute = Get.currentRoute;
return PopScope(
canPop: _selectedIndex == 0 && !_showMenu,
onPopInvoked: (bool didPop) {
if (!didPop) {
_onWillPop();
}
},
child: Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
InheritedVendorIndexController(
changeIndex: changeSelectedIndex,
child: widget.child,
),
if (_showMenu && !isKeyboardOpen)
AnimatedOpacity(
opacity: 1.0,
duration: const Duration(milliseconds: 300),
child: GestureDetector(
onTap: _toggleMenu,
child: Container(
alignment: Alignment.center,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0),
child: Container(
color: Colors.black.withOpacity(0.2),
width: double.infinity,
height: double.infinity,
),
),
),
),
),
if (_showMenu && !isKeyboardOpen)
Positioned(
bottom: 60,
left: 0,
right: 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 110),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildMenuOption(
icon: Icons.person,
label: 'User',
color: AppColors.primary,
onTap: () {
_toggleMenu();
Get.offNamed(RouterConts.homescreen);
},
),
_buildMenuOption(
icon: Icons.person,
label: 'Profile',
color: AppColors.primary,
onTap: () {
_toggleMenu();
Get.toNamed(RouterConts.profilemainvendor);
},
),
],
),
),
),
],
),
floatingActionButton: (isKeyboardOpen || !_shouldShowFAB())
? null
: FloatingActionButton(
// 🔧 FIXED: Dynamic hero tag based on route and timestamp
heroTag:
"vendor_controller_fab_${currentRoute}_${DateTime.now().millisecondsSinceEpoch}",
onPressed: _toggleMenu,
shape: const CircleBorder(),
backgroundColor: AppColors.primary,
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.rotate(
angle: _animationController.value * 0.75 * math.pi,
child: Icon(
_animationController.value > 0.5
? Icons.close
: Icons.add,
color: Colors.white,
),
);
},
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
notchMargin: 8,
color: Colors.white,
elevation: 8,
child: SizedBox(
height: 60,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_bottomNavItem(0, AppAssets.home),
_bottomNavItem(1, AppAssets.package),
const Expanded(child: SizedBox()),
_bottomNavItem(2, AppAssets.categories),
_bottomNavItem(3, AppAssets.history),
],
),
),
),
),
);
}
Widget _bottomNavItem(int index, String assetPath) {
return Expanded(
child: InkWell(
onTap: () => _onItemTapped(index, context),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
assetPath,
color: _selectedIndex == index ? AppColors.primary : Colors.grey,
width: 26,
height: 29,
),
const SizedBox(height: 4),
Container(
height: 3,
width: 30,
decoration: BoxDecoration(
color: _selectedIndex == index
? AppColors.primary
: Colors.transparent,
borderRadius: BorderRadius.circular(2),
),
),
],
),
),
);
}
Widget _buildMenuOption({
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
return ScaleTransition(
scale: CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
),
],
),
child: IconButton(
icon: Icon(icon, color: Colors.white),
onPressed: onTap,
padding: EdgeInsets.zero,
),
),
const SizedBox(height: 5),
Text(
label,
style: const TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
class InheritedVendorIndexController extends InheritedWidget {
final void Function(int) changeIndex;
const InheritedVendorIndexController({
super.key,
required this.changeIndex,
required super.child,
});
static InheritedVendorIndexController? of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<InheritedVendorIndexController>();
}
@override
bool updateShouldNotify(InheritedVendorIndexController oldWidget) {
return oldWidget.changeIndex != changeIndex;
}
}
@@ -0,0 +1,382 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/model/package_model.dart';
import 'package:bookmywages/model/plan_sucess_model.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/view/vendor_main_screens/vendor_maincontoller.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class VendorPackagePage extends ConsumerStatefulWidget {
const VendorPackagePage({super.key});
@override
ConsumerState<VendorPackagePage> createState() => _VendorPackagePageState();
}
class _VendorPackagePageState extends ConsumerState<VendorPackagePage> {
bool isLoading = false;
@override
Widget build(BuildContext context) {
final packageAsyncValue = ref.watch(pacakgeProvider);
final indexController = InheritedVendorIndexController.of(context);
// Get screen size for responsive calculations
final screenSize = MediaQuery.of(context).size;
final isSmallScreen = screenSize.width < 360;
return PopScope(
onPopInvoked: (didPop) {
if (didPop) {
// Reset main controller index to 0 when going back
final indexController = InheritedVendorIndexController.of(context);
indexController?.changeIndex(0);
}
},
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: Text(
"Subscription Plan",
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: isSmallScreen ? 16 : 18,
),
),
),
extendBodyBehindAppBar: true,
body: Container(
height: double.infinity,
width: double.infinity,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(AppAssets.background),
fit: BoxFit.cover,
),
),
child: SafeArea(
// Added SafeArea to ensure content doesn't overlap with system UI
child: Padding(
padding: EdgeInsets.symmetric(
horizontal:
screenSize.width * 0.04, // Responsive horizontal padding
vertical:
screenSize.height * 0.02, // Responsive vertical padding
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Responsive spacing based on screen height
SizedBox(height: screenSize.height * 0.05),
Text(
"Choose your right plan",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w400,
fontSize: isSmallScreen
? 24
: 32, // Smaller font for small screens
height: 1.0,
letterSpacing: 0.32,
color: Colors.black,
),
),
SizedBox(height: screenSize.height * 0.01),
Text(
"Make upgrade your plan and get more advantages of services",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: isSmallScreen
? 13
: 15, // Smaller font for small screens
height: 1.71,
letterSpacing: 0.15,
color: Colors.black,
),
),
SizedBox(height: screenSize.height * 0.02),
packageAsyncValue.when(
data: (packages) {
final filteredPackages = packages
.where((pkg) => pkg.type == 2)
.toList();
// Responsive container height based on screen size
return SizedBox(
height:
screenSize.height *
0.3, // 35% of screen height instead of fixed 250
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: filteredPackages.length,
itemBuilder: (context, index) {
final PackageModel package =
filteredPackages[index];
// Calculate responsive card width (based on screen width)
final cardWidth = screenSize.width * 0.75 > 250
? 250.0
: screenSize.width * 0.75;
return Container(
width: cardWidth,
margin: EdgeInsets.symmetric(
horizontal: screenSize.width * 0.02,
vertical: screenSize.height * 0.01,
),
decoration: BoxDecoration(
color: const Color(0xFF334E95),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: const Color(0xFF334E95),
width: 1,
),
boxShadow: const [
BoxShadow(
color: Color(0x40828282),
offset: Offset(0, 5),
blurRadius: 4,
),
],
),
child: Padding(
padding: EdgeInsets.all(
screenSize.width * 0.04,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
package.name,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: isSmallScreen
? 16
: 18, // Smaller font for small screens
height: 1.0,
letterSpacing: 0.32,
color: Colors.white,
),
),
),
SizedBox(height: screenSize.height * 0.01),
_buildFeatureRow(
package.description,
isSmallScreen,
),
SizedBox(height: screenSize.height * 0.02),
_buildFeatureRow(
getFormattedDuration(package.duration),
isSmallScreen,
),
SizedBox(height: screenSize.height * 0.02),
_buildFeatureRow(
'${package.noOfService} book service',
isSmallScreen,
),
const Divider(color: Colors.white),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
package.price,
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
GestureDetector(
onTap: () => _subscribeToPlan(
package,
indexController,
),
child: Container(
width: 30,
height: 30,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: const Icon(
Icons.arrow_forward,
color: Colors.black,
),
),
),
],
),
],
),
),
);
},
),
);
},
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Text(
'Error loading packages: $error',
style: const TextStyle(color: Colors.red),
),
),
),
SizedBox(height: screenSize.height * 0.01),
],
),
),
),
),
),
);
}
// Extracted feature row widget for better code organization
Widget _buildFeatureRow(String text, bool isSmallScreen) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const CircleAvatar(radius: 8, backgroundColor: Color(0xFFF9E369)),
const SizedBox(width: 6),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: isSmallScreen ? 12 : 14,
color: Colors.white,
),
),
),
],
);
}
Future<void> _subscribeToPlan(
PackageModel package,
InheritedVendorIndexController? indexController,
) async {
setState(() {
isLoading = true;
});
try {
final prefs = await SharedPreferences.getInstance();
final userId = prefs.getString('vendor_id') ?? '';
final model = PlanSuccessModel(
userId: userId,
planId: package.id.toString(),
duration: package.duration,
type: package.type,
);
final result = await ref
.read(planSuccessProvider(model).future)
.catchError((e) {
Fluttertoast.showToast(
msg: 'Error: ${e.toString()}',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
return false;
});
if (result) {
if (mounted) {
// Show success popup
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Success'),
content: const Text('Plan subscribed successfully!'),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
Get.offNamed(RouterConts.vendorcategory, arguments: 2);
},
),
],
);
},
);
}
} else {
if (mounted) {
// Show error popup
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Error'),
content: const Text('Failed to subscribe to plan'),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
}
} catch (error) {
if (mounted) {
// Show error popup
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Error'),
content: Text('Failed to subscribe: ${error.toString()}'),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
} finally {
if (mounted) {
setState(() {
isLoading = false;
});
}
}
}
String getFormattedDuration(int duration) {
if (duration < 30) {
return '$duration month';
} else {
int months = duration ~/ 30;
return '$months month';
}
}
}
@@ -0,0 +1,374 @@
import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/consts_widgets/comman_button.dart';
import 'package:bookmywages/consts_widgets/comman_textformfiled.dart';
import 'package:bookmywages/model/vendor_model/vendorregister_model.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
class VendorRegisterPage extends ConsumerStatefulWidget {
const VendorRegisterPage({super.key});
@override
ConsumerState<VendorRegisterPage> createState() => _VendorRegisterPageState();
}
class _VendorRegisterPageState extends ConsumerState<VendorRegisterPage> {
final _nameController = TextEditingController();
final _numberController = TextEditingController();
final _emailController = TextEditingController();
final _locationController = TextEditingController();
final _pincodeController = TextEditingController();
final _aadharController = TextEditingController();
final _panController = TextEditingController();
final _licenseController = TextEditingController();
final _passportController = TextEditingController();
String? _nameError;
String? _numberError;
String? _emailError;
void _validateName(String value) {
setState(() => _nameError = value.isEmpty ? 'Name is required' : null);
}
void _validateNumber(String value) {
setState(() {
if (value.isEmpty) {
_numberError = 'Phone number is required';
} else if (!RegExp(r'^[0-9]{10}$').hasMatch(value)) {
_numberError = 'Enter a valid 10-digit phone number';
} else {
_numberError = null;
}
});
}
void _validateEmail(String value) {
setState(() {
if (value.isEmpty) {
_emailError = 'Email is required';
} else if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
_emailError = 'Enter a valid email';
} else {
_emailError = null;
}
});
}
@override
void dispose() {
_nameController.dispose();
_numberController.dispose();
_emailController.dispose();
_locationController.dispose();
_pincodeController.dispose();
_aadharController.dispose();
_panController.dispose();
_licenseController.dispose();
_passportController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final double horizontalPadding = screenSize.width * 0.05;
final double imageHeight = screenSize.height * 0.45;
return Scaffold(
backgroundColor: AppColors.secondprimary,
resizeToAvoidBottomInset: true,
body: SingleChildScrollView(
child: Column(
children: [
Image.asset(
AppAssets.login,
width: screenSize.width,
height: imageHeight,
fit: BoxFit.cover,
),
Transform.translate(
offset: const Offset(0, -100),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32.73),
border: Border.all(
width: 1.09,
color: const Color(0xFFA39898),
),
boxShadow: const [
BoxShadow(
offset: Offset(0, -4),
blurRadius: 12.3,
color: Color(0x66E0E0E0),
),
BoxShadow(
offset: Offset(0, 4),
blurRadius: 4,
color: Color(0x66000000),
),
],
color: Colors.white,
),
child: Padding(
padding: EdgeInsets.all(screenSize.width * 0.05),
child: Column(
children: [
const SizedBox(height: 20),
const Text(
"Vendor Registration",
style: TextStyle(
fontSize: 25,
fontFamily: "Gilroy-ExtraBold",
fontWeight: FontWeight.w800,
height: 1.0,
letterSpacing: 0.64,
),
),
SizedBox(height: screenSize.height * 0.03),
CommonTextFormField(
controller: _nameController,
hintText: 'Enter your name',
prefixIcon: const Icon(
Icons.person,
color: AppColors.hittext,
),
errorText: _nameError,
onChanged: _validateName,
),
SizedBox(height: screenSize.height * 0.03),
CommonTextFormField(
controller: _numberController,
hintText: 'Enter your number',
prefixIcon: const Icon(
Icons.phone_android,
color: AppColors.hittext,
),
errorText: _numberError,
onChanged: _validateNumber,
),
SizedBox(height: screenSize.height * 0.03),
CommonTextFormField(
controller: _emailController,
hintText: 'Enter your E-mail Id',
prefixIcon: const Icon(
Icons.email,
color: AppColors.hittext,
),
errorText: _emailError,
onChanged: _validateEmail,
),
SizedBox(height: screenSize.height * 0.03),
CommonTextFormField(
controller: _locationController,
hintText: 'Location',
prefixIcon: const Icon(
Icons.location_on,
color: AppColors.hittext,
),
),
SizedBox(height: screenSize.height * 0.03),
CommonTextFormField(
controller: _pincodeController,
hintText: 'Pin code',
prefixIcon: const Icon(
Icons.pin_drop,
color: AppColors.hittext,
),
),
SizedBox(height: screenSize.height * 0.03),
CommonTextFormField(
controller: _aadharController,
hintText: 'Aadhar card number',
prefixIcon: const Icon(
Icons.badge,
color: AppColors.hittext,
),
),
SizedBox(height: screenSize.height * 0.03),
CommonTextFormField(
controller: _panController,
hintText: 'Pan card number',
prefixIcon: const Icon(
Icons.fact_check,
color: AppColors.hittext,
),
),
SizedBox(height: screenSize.height * 0.03),
CommonTextFormField(
controller: _licenseController,
hintText: 'Driving License number',
prefixIcon: const Icon(
Icons.directions_car,
color: AppColors.hittext,
),
),
SizedBox(height: screenSize.height * 0.03),
CommonTextFormField(
controller: _passportController,
hintText: 'Passport number',
prefixIcon: const Icon(
Icons.flight,
color: AppColors.hittext,
),
),
SizedBox(height: screenSize.height * 0.04),
CommanButton(
text: 'Submit',
textStyle: const TextStyle(
fontFamily: 'Gilroy-Black',
fontWeight: FontWeight.w800,
fontSize: 20,
height: 1.0,
color: AppColors.secondprimary,
),
onPressed: () async {
_validateName(_nameController.text);
_validateNumber(_numberController.text);
_validateEmail(_emailController.text);
if (_nameError == null &&
_numberError == null &&
_emailError == null) {
final model = VendorRegisterModel(
name: _nameController.text.trim(),
number: _numberController.text.trim(),
email: _emailController.text.trim(),
location: _locationController.text.trim(),
pincode: _pincodeController.text.trim(),
aadharCard: _aadharController.text.trim(),
panCard: _panController.text.trim(),
drivingLicense: _licenseController.text.trim(),
passport: _passportController.text.trim(),
);
try {
final response = await ref.read(
vendorRegisterFutureProvider(model).future,
);
print('API Response: $response');
_showVerificationDialog(context);
} catch (e) {
print("Registration Error: $e");
Fluttertoast.showToast(
msg: "Error: $e",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
}
}
},
isPrimary: true,
backgroundColor: AppColors.primary,
textColor: AppColors.secondprimary,
width: 230,
),
const SizedBox(height: 30),
],
),
),
),
),
),
],
),
),
);
}
void _showVerificationDialog(BuildContext context) {
showDialog(
context: context,
barrierDismissible: false, // Prevent closing by tapping outside
builder: (BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
side: const BorderSide(color: Colors.white, width: 1),
),
elevation: 10,
backgroundColor: Colors.white,
child: Container(
width: 365,
height: 305,
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Verification',
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 32,
height: 1.0,
letterSpacing: 0.32,
),
),
const SizedBox(height: 20),
const Text(
'Still your account will not verify by Admin, please wait with us.',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20.23,
height: 1.74,
letterSpacing: 0.2023,
),
),
const SizedBox(height: 30),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0066FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(29.36),
),
padding: const EdgeInsets.symmetric(
horizontal: 50,
vertical: 15,
),
),
onPressed: () {
Navigator.of(context).pop(); // Close dialog
Get.toNamed(RouterConts.homescreen);
},
child: const Text(
'OK',
style: TextStyle(
fontFamily: 'Gilroy-Black',
fontWeight: FontWeight.w400,
fontSize: 20.29,
height: 1.0,
letterSpacing: 0.4058,
color: Colors.white,
),
),
),
],
),
),
);
},
);
}
}
+490
View File
@@ -0,0 +1,490 @@
import 'dart:io';
import 'package:bookmywages/model/Banner_model.dart';
import 'package:bookmywages/model/Categories_model.dart';
import 'package:bookmywages/model/booking_modify_model.dart';
import 'package:bookmywages/model/cancel_booking.dart';
import 'package:bookmywages/model/detail_page_model.dart';
import 'package:bookmywages/model/enquriy_list_model.dart';
import 'package:bookmywages/model/enquriy_model.dart';
import 'package:bookmywages/model/expired_plan_model.dart';
import 'package:bookmywages/model/get_review_model.dart';
import 'package:bookmywages/model/most_popular_model.dart';
import 'package:bookmywages/model/notification_model.dart';
import 'package:bookmywages/model/otp_model.dart';
import 'package:bookmywages/model/package_model.dart';
import 'package:bookmywages/model/payment_details_model.dart';
import 'package:bookmywages/model/plan_sucess_model.dart';
import 'package:bookmywages/model/profile_get_model.dart';
import 'package:bookmywages/model/service_model.dart';
import 'package:bookmywages/model/subcategory_model.dart';
import 'package:bookmywages/model/user_booking_details.dart';
import 'package:bookmywages/model/vendor_model/terms_and_conditions_model.dart';
import 'package:bookmywages/model/vendor_model/vendor_booking_model.dart'
show VendorBookingModel;
import 'package:bookmywages/model/vendor_model/vendor_booking_status.dart';
import 'package:bookmywages/model/vendor_model/vendor_catgories_model.dart';
import 'package:bookmywages/model/vendor_model/vendor_profile_model.dart';
import 'package:bookmywages/model/vendor_model/vendor_service_model.dart';
import 'package:bookmywages/model/vendor_model/vendor_serviceupload_model.dart';
import 'package:bookmywages/model/vendor_model/vendorregister_model.dart';
import 'package:bookmywages/view/auth/auth_repository.dart';
import 'package:bookmywages/view/user_main_screens/notification_page.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../model/login_model.dart';
import '../viewmodel/consts_api.dart';
final loginRepositoryProvider = Provider((ref) => AuthRepository());
final loginFutureProvider = FutureProvider.family
.autoDispose<Map<String, dynamic>, LoginModel>((ref, loginModel) async {
final repo = ref.read(loginRepositoryProvider);
return repo.loginUser(ConstsApi.login, loginModel);
});
final signupFutureProvider = FutureProvider.family
.autoDispose<Map<String, dynamic>, Map<String, dynamic>>((
ref,
signupData,
) async {
final repo = ref.read(loginRepositoryProvider);
return repo.registerUser(ConstsApi.register, signupData);
});
// Add this to your providers file
final otpVerificationProvider = FutureProvider.family
.autoDispose<Map<String, dynamic>, OtpModel>((ref, otpModel) async {
final repo = ref.read(loginRepositoryProvider);
return repo.verifyOtp(ConstsApi.otp, otpModel);
});
final bannerRepositoryProvider = Provider((ref) => BannerRepository());
final bannerListProvider = FutureProvider.autoDispose<List<BannerModel>>((
ref,
) async {
final repo = ref.read(bannerRepositoryProvider);
return repo.fetchBanners(ConstsApi.banner);
});
final categoryRepositoryProvider = Provider((ref) => CategoryRepository());
final categoryListProvider = FutureProvider.autoDispose<List<CategoriesModel>>((
ref,
) async {
final repo = ref.read(categoryRepositoryProvider);
return repo.fetchCategories(ConstsApi.catgories);
});
final packageRepositoryProvider = Provider((ref) => packageRepository());
final pacakgeProvider = FutureProvider.autoDispose<List<PackageModel>>((
ref,
) async {
final repo = ref.read(packageRepositoryProvider);
return repo.fetchpackage(ConstsApi.package);
});
final profilegetRepositoryProvider = Provider((ref) => ProfilegetRepository());
final profilegetuserProvider =
FutureProvider.autoDispose<List<ProfileGetModel>>((ref) async {
final repo = ref.read(profilegetRepositoryProvider);
return repo.fetchprofileget(ConstsApi.profileget);
});
final subcategoryRepositoryProvider = Provider(
(ref) => SubcategoryRepository(),
);
final subcategoryProvider = FutureProvider.family
.autoDispose<List<SubcategoryModel>, String>((ref, categoryId) async {
final repo = ref.read(subcategoryRepositoryProvider);
return repo.fetchSubcategories(ConstsApi.subcat, categoryId);
});
final serviceRepositoryProvider = Provider((ref) => ServiceRepository());
final serviceProvider = FutureProvider.family
.autoDispose<
List<ServiceModel>,
({String categoryId, String? subcategoryId, String? selecttype})
>((ref, params) async {
final repo = ref.read(serviceRepositoryProvider);
return repo.fetchService(
ConstsApi.service,
params.categoryId,
params.subcategoryId,
params.selecttype,
);
});
final profileupdateRepositoryProvider = Provider(
(ref) => ProfileupdateRepository(),
);
final profileupdateuserProvider =
FutureProvider.autoDispose<List<ProfileGetModel>>((ref) async {
final repo = ref.read(profileupdateRepositoryProvider);
return repo.fetchprofileupdate(ConstsApi.upadateprofile);
});
final getreviewRepositoryProvider = Provider((ref) => GetReviewRepository());
final getreviewuserProvider =
FutureProvider.family<List<GetReviewModel>, String>((ref, serviceId) async {
final repo = ref.read(getreviewRepositoryProvider);
return repo.fetchgetreview(ConstsApi.getreview, serviceId);
});
final mostPopularRepositoryProvider = Provider(
(ref) => MostPopularRepository(),
);
// Create a provider family that accepts a categoryId parameter
final mostPopularProvider =
FutureProvider.family<List<MostPopularModel>, String>((
ref,
categoryId,
) async {
final repo = ref.read(mostPopularRepositoryProvider);
return repo.fetchMostPopular(ConstsApi.mostpopular, categoryId);
});
final enquriyupdateRepositoryProvider = Provider((ref) => EnquriyRepository());
final enquriyProvider = FutureProvider.autoDispose<List<EnquiryModel>>((
ref,
) async {
final repo = ref.read(enquriyupdateRepositoryProvider);
return repo.fetchenquriy(ConstsApi.enquery);
});
final enquriyRepositoryProvider = Provider((ref) => enquriylistRepository());
final enquriylistProvider = FutureProvider.autoDispose<List<EnquiryListModel>>((
ref,
) async {
final repo = ref.read(enquriyRepositoryProvider);
return repo.fetchenquirylist(ConstsApi.enquirylist);
});
final enquriydeleteRepositoryProvider = Provider(
(ref) => EnquriydeleteRepository(),
);
final enquriydeleteProvider = FutureProvider.autoDispose.family<bool, String>((
ref,
id,
) async {
final repo = ref.read(enquriydeleteRepositoryProvider);
return repo.enquriydelete(ConstsApi.enquirydelete, id);
});
final paymentlistRepositoryProvider = Provider(
(ref) => PaymentdetailsRepository(),
);
final paymentdetailsProvider =
FutureProvider.autoDispose<List<PaymentDetailsModel>>((ref) async {
final repo = ref.read(paymentlistRepositoryProvider);
return repo.fetchpayment(ConstsApi.paymentdetails);
});
final paymentdeleteRepositoryProvider = Provider(
(ref) => PaymentdeleteRepository(),
);
final paymentdeleteProvider = FutureProvider.autoDispose.family<bool, String>((
ref,
id,
) async {
final repo = ref.read(paymentdeleteRepositoryProvider);
return repo.paymentdelete(ConstsApi.paymentdelete, id);
});
final changepassRepositoryProvider = Provider(
(ref) => ChangepasswordRepository(),
);
final changepasswordProvider = FutureProvider.autoDispose.family<bool, String>((
ref,
password,
) async {
final repo = ref.read(changepassRepositoryProvider);
return repo.changepassword(ConstsApi.changepassword, password);
});
final servicebookinguserhistorydetailsRepositoryProvider = Provider(
(ref) => UserhistoryBookingdetailsRepository(),
);
final userbookinghistorydetailsProvider =
FutureProvider.autoDispose<List<UserBookingDetails>>((ref) async {
final repo = ref.read(servicebookinguserhistorydetailsRepositoryProvider);
return repo.fetchhistorybooking(ConstsApi.historybooking);
});
final cancelbookingRepositoryProvider = Provider(
(ref) => CancelbookingRepository(),
);
final cancelbookingProvider = FutureProvider.autoDispose
.family<bool, CancelBookingRequest>((ref, data) async {
final repo = ref.read(cancelbookingRepositoryProvider);
return repo.cancelbooking(
url: ConstsApi.cancelbooking,
id: data.id,
serviceId: data.serviceId,
type: data.type,
);
});
final bookingmodifyRepositoryProvider = Provider(
(ref) => BookingmodifyRepository(),
);
final modifybookingProvider = FutureProvider.autoDispose
.family<bool, BookingModifyModel>((ref, data) async {
final repo = ref.read(bookingmodifyRepositoryProvider);
return repo.bookingmodify(
url: ConstsApi.bookingmodify,
id: data.id,
serviceId: data.serviceId,
servicedate: data.servicedate,
servicetime: data.servicetime,
);
});
final expiredPlanRepositoryProvider = Provider(
(ref) => ExpiredPlanRepository(),
);
final expiredPlanProvider = FutureProvider.autoDispose<ExpiredPlanModel>((
ref,
) async {
final repo = ref.read(expiredPlanRepositoryProvider);
return repo.fetchExpiredPlan(ConstsApi.expiredplan);
});
final planSuccessRepositoryProvider = Provider(
(ref) => PlanSuccessRepository(),
);
// Provider for plan success functionality
final planSuccessProvider = FutureProvider.autoDispose
.family<bool, PlanSuccessModel>((ref, data) async {
final repo = ref.read(planSuccessRepositoryProvider);
return repo.planSuccess(
url: ConstsApi.plansucess,
userId: data.userId,
planId: data.planId,
duration: data.duration,
type: data.type,
);
});
final detailpageRepositoryProvider = Provider((ref) => DetailpageRepository());
final detailpageProvider = FutureProvider.family
.autoDispose<List<DetailPageModel>, String>((ref, serviceid) async {
final repo = ref.read(detailpageRepositoryProvider);
return repo.fetchDetailpage(ConstsApi.detail, int.parse(serviceid));
});
//--------------------vendor flow-----------------------------------------
final getvendorIdRepositoryProvider = Provider((ref) => GetvendorId());
final getvendorIdProvider = FutureProvider.autoDispose((ref) async {
final repo = ref.read(getvendorIdRepositoryProvider);
return repo.fetchGetid(ConstsApi.getvendorid);
});
final vendorRegisterRepositoryProvider = Provider((ref) => VendorRegister());
// Future provider to call the register API
final vendorRegisterFutureProvider = FutureProvider.family
.autoDispose<Map<String, dynamic>, VendorRegisterModel>((
ref,
vendorModel,
) async {
final repo = ref.read(vendorRegisterRepositoryProvider);
return repo.vendorRegister(ConstsApi.vendorregister, vendorModel);
});
final vendorserviceuploadRepositoryProvider = Provider(
(ref) => Vendorserviceupload(),
);
final vendorserviceFutureProvider = FutureProvider.family
.autoDispose<Map<String, dynamic>, (VendorServiceUploadModel, List<File>)>((
ref,
data,
) async {
final repo = ref.read(vendorserviceuploadRepositoryProvider);
return repo.vendorserviceupload(
ConstsApi.vendorserivceupload,
data.$1, // VendorServiceUploadModel
data.$2, // List<File> images
);
});
final vendorbookingdetailsRepositoryProvider = Provider(
(ref) => VendorbookingdetailsRepository(),
);
final vendorbookingdetailsProvider =
FutureProvider.autoDispose<List<VendorBookingModel>>((ref) async {
final repo = ref.read(vendorbookingdetailsRepositoryProvider);
return repo.fetchvendorbooking(ConstsApi.vendormybooking);
});
final changebookingstatusRepositoryProvider = Provider(
(ref) => ChangebookingstatusRepository(),
);
final changebookingProvider = FutureProvider.autoDispose
.family<bool, VendorBookingStatus>((ref, data) async {
final repo = ref.read(changebookingstatusRepositoryProvider);
return repo.changebooking(
url: ConstsApi.vendorchangebookingstatus,
id: data.id.toString(),
status: data.status.toString(),
);
});
final vendorcatgoriesRepositoryProvider = Provider(
(ref) => VendorcatgoriesRepository(),
);
final vendorcatgoriesProvider =
FutureProvider.autoDispose<List<VendorCategoriesModel>>((ref) async {
final repo = ref.read(vendorcatgoriesRepositoryProvider);
return repo.fetchvendorcat(ConstsApi.vendorcat);
});
final vendorserviceRepositoryProvider = Provider(
(ref) => VendorserviceRepository(),
);
final vendorserviceProvider =
FutureProvider.autoDispose<List<VendorServiceModel>>((ref) async {
final repo = ref.read(vendorserviceRepositoryProvider);
return repo.fetchvendorservice(ConstsApi.vendormyservice);
});
final vendorserviceupdateRepositoryProvider = Provider(
(ref) => Vendorserviceupadate(),
);
final vendorServiceUploadUpdateProvider = FutureProvider.family
.autoDispose<Map<String, dynamic>, (VendorServiceUploadModel, List<File>)>((
ref,
data,
) async {
final repo = ref.read(vendorserviceupdateRepositoryProvider);
return repo.vendorserviceupdate(
ConstsApi.vendorserviceupload,
data.$1,
data.$2,
);
});
final vendorexpiredPlanRepositoryProvider = Provider(
(ref) => VendorExpiredPlanRepository(),
);
final vendorexpiredPlanProvider = FutureProvider.autoDispose<ExpiredPlanModel>((
ref,
) async {
final repo = ref.read(vendorexpiredPlanRepositoryProvider);
return repo.fetchvendorExpiredPlan(ConstsApi.expiredplan);
});
final catDeleteRepositoryProvider = Provider(
(ref) => CatManagementDeleteRepository(),
);
// Provider for delete operation
final deleteCategoryProvider = FutureProvider.family.autoDispose<bool, String>((
ref,
categoryId,
) async {
final repo = ref.read(catDeleteRepositoryProvider);
return repo.deleteCategoryManagement(
ConstsApi.catmangementdelete,
categoryId,
);
});
final ServiceDeleteRepositoryProvider = Provider(
(ref) => SerciceDeleteRepository(),
);
// Provider for delete operation
final deleteserviceProvider = FutureProvider.family.autoDispose<bool, String>((
ref,
id,
) async {
final repo = ref.read(ServiceDeleteRepositoryProvider);
return repo.deleteService(ConstsApi.servicedelete, id);
});
final profilegetvendorRepositoryProvider = Provider(
(ref) => ProfilegetvendorRepository(),
);
final profilegetvendorProvider =
FutureProvider.autoDispose<List<ProfileGetModel>>((ref) async {
final repo = ref.read(profilegetvendorRepositoryProvider);
return repo.fetchprofilegetvendor(ConstsApi.vendorprofileget);
});
final profileupdatevendorRepositoryProvider = Provider(
(ref) => ProfileupdatevendorRepository(),
);
final profileupdatevendorProvider =
FutureProvider.autoDispose<List<VendorProfileModel>>((ref) async {
final repo = ref.read(profileupdatevendorRepositoryProvider);
return repo.fetchProfileupdatevendor(ConstsApi.upadatevendorprofile);
});
final termsAndConditionsRepositoryProvider = Provider(
(ref) => TermsAndConditionsRepository(),
);
final termsAndConditionsProvider =
FutureProvider.autoDispose<TermsAndConditionsModel>((ref) async {
final repo = ref.read(termsAndConditionsRepositoryProvider);
return repo.fetchTermsAndCondition(ConstsApi.termsandcondition);
});
final privacypolicyRepositoryProvider = Provider(
(ref) => PrivacypolicyRepository(),
);
final privacypolicyProvider =
FutureProvider.autoDispose<TermsAndConditionsModel>((ref) async {
final repo = ref.read(privacypolicyRepositoryProvider);
return repo.fetchPrivacypolicy(ConstsApi.privacypolicy);
});
final notificationRepositoryProvider = Provider(
(ref) => NotificationRepository(),
);
final notificationProvider = FutureProvider.family
.autoDispose<List<NotificationModel>, ({int type, String userId})>((
ref,
params,
) async {
final repo = ref.read(notificationRepositoryProvider);
return repo.fetchNotification(type: params.type, userId: params.userId);
});
Future<void> updateNotificationCount(
WidgetRef ref, {
required int type,
required String userId,
}) async {
final repo = ref.read(notificationRepositoryProvider);
final notifications = await repo.fetchNotification(
type: type,
userId: userId,
);
ref.read(notificationCountProvider.notifier).state = notifications.length;
}
+55
View File
@@ -0,0 +1,55 @@
class ConstsApi {
static const String baseUrl =
"https://www.demo603.amrithaa.com/bookmywages/admin/public/api";
// Example endpoints
static const String login = "$baseUrl/login";
static const String register = "$baseUrl/register";
static const String otp = "$baseUrl/verifyotp";
static const String banner = "$baseUrl/getslider";
static const String catgories = "$baseUrl/getcategory";
static const String trendingserivce = "$baseUrl/trendingservice";
static const String package = "$baseUrl/getplan";
static const String subcat = "$baseUrl/getsubcategory";
static const String service = "$baseUrl/filterservice";
static const String detail = "$baseUrl/getservice";
static const String profileget = "$baseUrl/getuserprofile";
static const String upadateprofile = "$baseUrl/updateuserprofile";
static const String getreview = "$baseUrl/getreview";
static const String mostpopular = "$baseUrl/popularservice";
static const String enquery = "$baseUrl/enquiry";
static const String bookservice = "$baseUrl/bookservice";
static const String updatereview = "$baseUrl/addreview";
static const String enquirylist = "$baseUrl/userenquiry";
static const String enquirydelete = "$baseUrl/deleteenquiry";
static const String paymentdetails = "$baseUrl/paymenthistory";
static const String paymentdelete = "$baseUrl/deletepaymentdetail";
static const String changepassword = "$baseUrl/changepassword";
static const String historybooking = "$baseUrl/mybooking";
static const String cancelbooking = "$baseUrl/cancelbooking";
static const String bookingmodify = "$baseUrl/bookingmodify";
static const String expiredplan = "$baseUrl/getlastexpiredplan";
static const String plansucess = "$baseUrl/subscription";
//vendor flow
static const String getvendorid = "$baseUrl/getvendordetail";
static const String vendorregister = "$baseUrl/vendorregister";
static const String vendorserivceupload = "$baseUrl/addservice";
static const String vendormybooking = "$baseUrl/vendorbooking";
static const String vendorchangebookingstatus =
"$baseUrl/changebookingstatus";
static const String vendorcat = "$baseUrl/getcategoryvendor";
static const String imageupload = "$baseUrl/uploadimage";
static const String vendormyservice = "$baseUrl/myservice";
static const String vendorserviceupload = "$baseUrl/updateservice";
static const String catmangementdelete = "$baseUrl/deletecategory";
static const String servicedelete = "$baseUrl/deleteservice";
static const String vendorprofileget = "$baseUrl/getvendorprofile";
static const String upadatevendorprofile = "$baseUrl/updatevendorprofile";
static const String termsandcondition = "$baseUrl/getTermsandCondition";
static const String privacypolicy = "$baseUrl/getPrivacyPolicy";
static const String notification = "$baseUrl/getnotification";
}