2026-04-11
This commit is contained in:
commit
27e27fb70b
270 files changed
+30296
No files matched your search
@@ -0,0 +1,179 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:taxglide/consts/app_asstes.dart';
|
||||
import 'package:taxglide/consts/comman_webscoket.dart';
|
||||
import 'package:taxglide/consts/local_store.dart';
|
||||
import 'package:taxglide/consts/notification_webscoket.dart';
|
||||
import 'package:taxglide/controller/api_contoller.dart';
|
||||
import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class CommanChatBox extends ConsumerStatefulWidget {
|
||||
final String? chatId;
|
||||
|
||||
const CommanChatBox({super.key, this.chatId});
|
||||
|
||||
@override
|
||||
ConsumerState<CommanChatBox> createState() => _CommanChatBoxState();
|
||||
}
|
||||
|
||||
class _CommanChatBoxState extends ConsumerState<CommanChatBox> {
|
||||
bool _isWebSocketInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeWebSocket();
|
||||
}
|
||||
|
||||
/// ✅ Initialize WebSocket on widget load
|
||||
Future<void> _initializeWebSocket() async {
|
||||
if (_isWebSocketInitialized) return;
|
||||
|
||||
try {
|
||||
final String? userId = await LocalStore().getUserId();
|
||||
if (userId != null && userId.isNotEmpty) {
|
||||
final wsService = CommonWebSocketService();
|
||||
|
||||
// Setup message handler to refresh count
|
||||
wsService.onMessageReceived = (message) {
|
||||
debugPrint("📨 WebSocket message received: $message");
|
||||
_refreshCount();
|
||||
};
|
||||
|
||||
// Connect WebSocket
|
||||
await wsService.connect(userId: userId);
|
||||
_isWebSocketInitialized = true;
|
||||
debugPrint("✅ WebSocket initialized for chat box");
|
||||
|
||||
// ✅ Subscribe NotificationWebSocket to the chat channel early
|
||||
// so notifications arrive from ANY screen, not just the chat screen.
|
||||
final String chatId =
|
||||
widget.chatId ?? await LocalStore().getGeneralChatId() ?? '0';
|
||||
if (chatId != '0' && chatId.isNotEmpty) {
|
||||
NotificationWebSocket().watchChatChannel(chatId);
|
||||
debugPrint(
|
||||
"👁 CommanChatBox: watching chat channel $chatId for notifications",
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("❌ Failed to initialize WebSocket: $e");
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ Refresh count provider
|
||||
void _refreshCount() {
|
||||
try {
|
||||
final chatId = widget.chatId ?? "0";
|
||||
// Invalidate the count provider to trigger API call
|
||||
ref.invalidate(countProvider(chatId));
|
||||
debugPrint("🔄 Count refreshed for chatId: $chatId");
|
||||
} catch (e) {
|
||||
debugPrint("❌ Error refreshing count: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<String?>(
|
||||
future: widget.chatId != null
|
||||
? Future.value(widget.chatId)
|
||||
: LocalStore().getGeneralChatId(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final id = snapshot.data ?? "0";
|
||||
final countAsync = ref.watch(countProvider(id));
|
||||
|
||||
return Positioned(
|
||||
right: 20,
|
||||
bottom: 120,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
// Main Chat Button
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
int cid = int.tryParse(id) ?? 0;
|
||||
|
||||
ref.invalidate(chatMessagesProvider);
|
||||
|
||||
// Navigate to chat screen
|
||||
Get.to(() => LiveChatScreen(chatid: cid, fileid: '0'))?.then((
|
||||
_,
|
||||
) {
|
||||
ref.read(notificationTriggerProvider.notifier).state++;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
width: 53,
|
||||
height: 53,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFBD49F7), Color(0xFF630A73)],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.25),
|
||||
blurRadius: 7.2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(3),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Image.asset(AppAssets.generalchatimage),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 🔴 Count Badge
|
||||
Positioned(
|
||||
right: -2,
|
||||
top: -2,
|
||||
child: countAsync.when(
|
||||
loading: () => const SizedBox(),
|
||||
error: (_, __) => const SizedBox(),
|
||||
data: (data) {
|
||||
if (data.count == 0) return const SizedBox();
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Text(
|
||||
data.count.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Don't disconnect WebSocket here as it's shared across app
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user