452 lines
14 KiB
Dart
452 lines
14 KiB
Dart
import 'dart:math' as math;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:taxglide/consts/app_asstes.dart';
|
|
import 'package:taxglide/consts/local_store.dart';
|
|
import 'package:taxglide/controller/api_contoller.dart'; // ✅ Needed for providers
|
|
import 'package:taxglide/view/Main_controller/comman_chat_box.dart';
|
|
import 'package:taxglide/view/screens/history/serivces_status_screen.dart'
|
|
show ServicesStatusScreen;
|
|
import 'package:taxglide/view/screens/home_screen.dart';
|
|
import 'package:taxglide/view/screens/list_service_screen.dart';
|
|
import 'package:taxglide/view/screens/profile/employee_profile/employee_profile_screen.dart';
|
|
import 'package:taxglide/view/screens/profile/profile_screen.dart';
|
|
|
|
class MainController extends ConsumerStatefulWidget {
|
|
final Widget? child; // Optional child (like ServiceRequestScreen)
|
|
final int? initialIndex; // Which tab to show initially
|
|
final int? sourceTabIndex; // Source tab (Home=0, Services=1)
|
|
|
|
const MainController({
|
|
Key? key,
|
|
this.child,
|
|
this.initialIndex,
|
|
this.sourceTabIndex,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
ConsumerState<MainController> createState() => _MainControllerState();
|
|
}
|
|
|
|
class _MainControllerState extends ConsumerState<MainController> {
|
|
int currentIndex = 0;
|
|
DateTime? lastBackPressed;
|
|
String? userRole;
|
|
|
|
/// 📌 Bottom bar labels
|
|
final List<String> _labels = [
|
|
'Home',
|
|
'Services',
|
|
'Service Details',
|
|
'Profile',
|
|
];
|
|
|
|
/// 📌 Screens list
|
|
List<Widget> _screens = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
currentIndex = widget.initialIndex ?? 0;
|
|
_loadRole();
|
|
}
|
|
|
|
/// 🔹 Load user role from local storage
|
|
Future<void> _loadRole() async {
|
|
userRole = await LocalStore().getRole(); // "employee" or "user"
|
|
_initScreens();
|
|
setState(() {});
|
|
}
|
|
|
|
/// 🔹 Initialize screens based on role
|
|
void _initScreens() {
|
|
_screens = [
|
|
const HomeScreen(),
|
|
ListServiceScreen(key: UniqueKey()),
|
|
ServicesStatusScreen(),
|
|
_getProfileScreen(), // ✅ role based
|
|
];
|
|
}
|
|
|
|
/// 🔹 Role based profile screen
|
|
Widget _getProfileScreen() {
|
|
if (userRole == "employee") {
|
|
return const EmployeeProfileScreen(); // 👈 employee profile
|
|
}
|
|
return const ProfileScreen(); // 👈 normal user profile
|
|
}
|
|
|
|
/// 🔄 Change tab index
|
|
void setBottomBarIndex(int index) {
|
|
setState(() {
|
|
currentIndex = index;
|
|
});
|
|
_refreshTab(index);
|
|
}
|
|
|
|
/// ✅ Refresh API data based on the current tab
|
|
void _refreshTab(int index) {
|
|
final container = ProviderScope.containerOf(context, listen: false);
|
|
|
|
switch (index) {
|
|
case 0:
|
|
// 🔁 Refresh Home screen APIs
|
|
container.refresh(profileProvider);
|
|
container.refresh(serviceListProvider);
|
|
ref.invalidate(chatMessagesProvider);
|
|
ref.invalidate(notificationCountProvider);
|
|
ref.invalidate(dashboardProvider);
|
|
break;
|
|
case 1:
|
|
// 🔁 Refresh ListServiceScreen APIs
|
|
container.refresh(serviceListProvider);
|
|
ref.invalidate(chatMessagesProvider);
|
|
ref.invalidate(dashboardProvider);
|
|
break;
|
|
case 2:
|
|
// 🔁 Refresh Service Status APIs
|
|
try {
|
|
ref.invalidate(serviceHistoryNotifierProvider);
|
|
ref.invalidate(serviceDetailProvider);
|
|
ref.invalidate(chatMessagesProvider);
|
|
ref.invalidate(dashboardProvider);
|
|
} catch (_) {}
|
|
break;
|
|
case 3:
|
|
// 🔁 Refresh Profile screen APIs
|
|
ref.invalidate(chatMessagesProvider);
|
|
container.refresh(profileProvider);
|
|
ref.invalidate(staffListProvider);
|
|
ref.invalidate(dashboardProvider);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// ✅ Handle Back button presses
|
|
Future<bool> _onWillPop() async {
|
|
// If inside ServiceRequestScreen → go back to originating tab
|
|
if (widget.child != null) {
|
|
Get.offAll(
|
|
() => MainController(initialIndex: widget.sourceTabIndex ?? 0),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// If not in Home tab, go back to Home
|
|
if (currentIndex != 0) {
|
|
setState(() => currentIndex = 0);
|
|
return false;
|
|
}
|
|
|
|
// Double back press to exit
|
|
DateTime now = DateTime.now();
|
|
if (lastBackPressed == null ||
|
|
now.difference(lastBackPressed!) > const Duration(seconds: 2)) {
|
|
lastBackPressed = now;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text("Press back again to exit"),
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
await SystemNavigator.pop();
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final Size size = MediaQuery.of(context).size;
|
|
|
|
// ✅ Show loading while screens are being initialized
|
|
if (_screens.isEmpty) {
|
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
|
}
|
|
|
|
final Widget mainContent = widget.child ?? _screens[currentIndex];
|
|
final dashboardAsync = ref.watch(dashboardProvider);
|
|
|
|
return WillPopScope(
|
|
onWillPop: _onWillPop,
|
|
child: Scaffold(
|
|
backgroundColor: Colors.white.withAlpha(55),
|
|
body: Stack(
|
|
children: [
|
|
mainContent,
|
|
|
|
/// ✅ Bottom Navigation Bar
|
|
Positioned(
|
|
bottom: 0,
|
|
left: 0,
|
|
child: SafeArea(
|
|
top: false,
|
|
child: Container(
|
|
width: size.width,
|
|
height: 80,
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
CustomPaint(
|
|
size: Size(size.width, 80),
|
|
painter: BNBCustomPainter(),
|
|
),
|
|
Center(
|
|
heightFactor: 0.6,
|
|
child: GestureDetector(
|
|
onTap: () {},
|
|
child: CustomPaint(
|
|
painter: PentagonPainter(
|
|
color: const Color(0xFF61277A),
|
|
),
|
|
child: Container(
|
|
width: 70,
|
|
height: 70,
|
|
alignment: Alignment.center,
|
|
child: Image.asset(
|
|
AppAssets.maincontroller,
|
|
color: Colors.white,
|
|
fit: BoxFit.contain,
|
|
height: 40,
|
|
width: 40,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: size.width,
|
|
height: 80,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
_buildNavItem(Icons.home, 0, _labels[0]),
|
|
_buildNavItem(Icons.list_alt_sharp, 1, _labels[1]),
|
|
SizedBox(width: size.width * 0.20),
|
|
_buildNavItem(Icons.history_edu, 2, _labels[2]),
|
|
_buildNavItem(Icons.person_2, 3, _labels[3]),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
dashboardAsync.when(
|
|
data: (dashboard) {
|
|
final int? chatIdInt = dashboard.generalChatId;
|
|
|
|
if (chatIdInt == null || chatIdInt == 0) {
|
|
return const SizedBox(); // ❌ No chat box
|
|
}
|
|
|
|
return CommanChatBox(
|
|
chatId: chatIdInt.toString(), // ✅ INT → STRING
|
|
);
|
|
},
|
|
loading: () => const SizedBox(),
|
|
error: (e, _) => const SizedBox(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// ✅ Navigation Logic with Refresh
|
|
Widget _buildNavItem(IconData icon, int index, String label) {
|
|
bool isSelected = currentIndex == index;
|
|
|
|
return GestureDetector(
|
|
onTap: () {
|
|
// 🔹 If inside ServiceRequestScreen
|
|
if (widget.child != null) {
|
|
if (widget.initialIndex == index) return;
|
|
|
|
if (widget.sourceTabIndex == 0 && index == 1) {
|
|
Get.offAll(() => const MainController(initialIndex: 1));
|
|
return;
|
|
}
|
|
|
|
if (widget.sourceTabIndex == 1 && index == 1) return;
|
|
|
|
Get.offAll(() => MainController(initialIndex: index));
|
|
return;
|
|
}
|
|
|
|
// 🔹 If same tab tapped again → refresh
|
|
if (currentIndex == index) {
|
|
_refreshTab(index);
|
|
return;
|
|
}
|
|
|
|
// 🔹 Switch to a different tab
|
|
setBottomBarIndex(index);
|
|
},
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: isSelected
|
|
? Border.all(color: const Color(0xFF61277A), width: 2)
|
|
: null,
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: isSelected
|
|
? const Color(0xFF61277A)
|
|
: const Color(0xFF6C7278),
|
|
size: isSelected ? 19.5 : 20.8,
|
|
),
|
|
),
|
|
if (isSelected)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4),
|
|
child: Text(
|
|
label,
|
|
style: const TextStyle(
|
|
color: Color(0xFF61277A),
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// ✅ Custom Curved Bottom Navigation Painter
|
|
class BNBCustomPainter extends CustomPainter {
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
Paint paint = Paint()
|
|
..color = Colors.white
|
|
..style = PaintingStyle.fill;
|
|
|
|
Path path = Path();
|
|
path.moveTo(0, 20);
|
|
path.quadraticBezierTo(size.width * 0.20, 0, size.width * 0.35, 0);
|
|
path.quadraticBezierTo(size.width * 0.40, 0, size.width * 0.40, 20);
|
|
path.arcToPoint(
|
|
Offset(size.width * 0.60, 20),
|
|
radius: const Radius.circular(20.0),
|
|
clockwise: false,
|
|
);
|
|
path.quadraticBezierTo(size.width * 0.60, 0, size.width * 0.65, 0);
|
|
path.quadraticBezierTo(size.width * 0.80, 0, size.width, 20);
|
|
path.lineTo(size.width, size.height);
|
|
path.lineTo(0, size.height);
|
|
path.close();
|
|
|
|
canvas.drawShadow(path, Colors.black.withOpacity(0.2), 5, true);
|
|
canvas.drawPath(path, paint);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
|
}
|
|
|
|
/// ✅ Pentagon Button Painter
|
|
/// ✅ Pentagon Button Painter
|
|
class PentagonPainter extends CustomPainter {
|
|
final Color color;
|
|
final double cornerRadius;
|
|
|
|
PentagonPainter({
|
|
required this.color,
|
|
this.cornerRadius = 8.0, // Adjust this value for more/less rounding
|
|
});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint = Paint()
|
|
..color = color
|
|
..style = PaintingStyle.fill;
|
|
|
|
final double w = size.width;
|
|
final double cx = w / 2;
|
|
final double cy = size.height / 2;
|
|
final double r = w / 2;
|
|
|
|
// Calculate pentagon vertices
|
|
List<Offset> vertices = [];
|
|
for (int i = 0; i < 5; i++) {
|
|
double angle = (72 * i - 90) * math.pi / 180;
|
|
double x = cx + r * 0.95 * math.cos(angle);
|
|
double y = cy + r * 0.95 * math.sin(angle);
|
|
vertices.add(Offset(x, y));
|
|
}
|
|
|
|
// Create path with rounded corners
|
|
final path = Path();
|
|
|
|
for (int i = 0; i < vertices.length; i++) {
|
|
final current = vertices[i];
|
|
final next = vertices[(i + 1) % vertices.length];
|
|
final prev = vertices[(i - 1 + vertices.length) % vertices.length];
|
|
|
|
// Calculate direction vectors
|
|
final toCurrent = Offset(current.dx - prev.dx, current.dy - prev.dy);
|
|
final toNext = Offset(next.dx - current.dx, next.dy - current.dy);
|
|
|
|
// Normalize and scale by corner radius
|
|
final lengthToCurrent = math.sqrt(
|
|
toCurrent.dx * toCurrent.dx + toCurrent.dy * toCurrent.dy,
|
|
);
|
|
final lengthToNext = math.sqrt(
|
|
toNext.dx * toNext.dx + toNext.dy * toNext.dy,
|
|
);
|
|
|
|
final normalizedToCurrent = Offset(
|
|
toCurrent.dx / lengthToCurrent,
|
|
toCurrent.dy / lengthToCurrent,
|
|
);
|
|
final normalizedToNext = Offset(
|
|
toNext.dx / lengthToNext,
|
|
toNext.dy / lengthToNext,
|
|
);
|
|
|
|
// Points before and after the corner
|
|
final beforeCorner = Offset(
|
|
current.dx - normalizedToCurrent.dx * cornerRadius,
|
|
current.dy - normalizedToCurrent.dy * cornerRadius,
|
|
);
|
|
final afterCorner = Offset(
|
|
current.dx + normalizedToNext.dx * cornerRadius,
|
|
current.dy + normalizedToNext.dy * cornerRadius,
|
|
);
|
|
|
|
if (i == 0) {
|
|
path.moveTo(beforeCorner.dx, beforeCorner.dy);
|
|
} else {
|
|
path.lineTo(beforeCorner.dx, beforeCorner.dy);
|
|
}
|
|
|
|
// Draw rounded corner using quadratic bezier
|
|
path.quadraticBezierTo(
|
|
current.dx,
|
|
current.dy,
|
|
afterCorner.dx,
|
|
afterCorner.dy,
|
|
);
|
|
}
|
|
path.close();
|
|
|
|
canvas.drawShadow(path, Colors.black.withOpacity(0.3), 4, true);
|
|
canvas.drawPath(path, paint);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
|
}
|