From 5c6c6289cd07c97f908683d8313274b833bd097a Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 12 Dec 2025 15:24:37 +0530 Subject: [PATCH 1/6] added gemini api --- lib/helpers/services/app_initializer.dart | 47 +- lib/helpers/services/gemini_service.dart | 36 ++ lib/view/dashboard/dashboard_screen.dart | 93 ++-- lib/view/dashboard/gemini_chat_card.dart | 548 ++++++++++++++++++++++ pubspec.yaml | 3 +- 5 files changed, 678 insertions(+), 49 deletions(-) create mode 100644 lib/helpers/services/gemini_service.dart create mode 100644 lib/view/dashboard/gemini_chat_card.dart diff --git a/lib/helpers/services/app_initializer.dart b/lib/helpers/services/app_initializer.dart index 1196355..f22542f 100644 --- a/lib/helpers/services/app_initializer.dart +++ b/lib/helpers/services/app_initializer.dart @@ -8,6 +8,7 @@ import 'package:on_field_work/helpers/services/firebase/firebase_messaging_servi import 'package:on_field_work/helpers/services/device_info_service.dart'; import 'package:on_field_work/helpers/theme/theme_customizer.dart'; import 'package:on_field_work/helpers/theme/app_theme.dart'; +import 'package:on_field_work/helpers/services/gemini_service.dart'; Future initializeApp() async { try { @@ -38,11 +39,16 @@ Future initializeApp() async { } } +/// --------------------------------------------------------------------------- +/// πŸ”Ή AUTH TOKEN HANDLER +/// --------------------------------------------------------------------------- Future _handleAuthTokens() async { final refreshToken = await LocalStorage.getRefreshToken(); + if (refreshToken?.isNotEmpty ?? false) { logSafe("πŸ” Refresh token found. Attempting to refresh JWT..."); final success = await AuthService.refreshToken(); + if (!success) { logSafe("⚠️ Refresh token invalid or expired. User must login again."); } @@ -51,43 +57,68 @@ Future _handleAuthTokens() async { } } +/// --------------------------------------------------------------------------- +/// πŸ”Ή UI SETUP +/// --------------------------------------------------------------------------- Future _setupUI() async { setPathUrlStrategy(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - logSafe("πŸ’‘ UI setup completed with default system behavior."); + logSafe("πŸ’‘ UI setup completed."); } +/// --------------------------------------------------------------------------- +/// πŸ”Ή FIREBASE + GEMINI SETUP +/// --------------------------------------------------------------------------- Future _setupFirebase() async { + // Firebase Core await Firebase.initializeApp(); - logSafe("πŸ’‘ Firebase initialized."); + logSafe("πŸ”₯ Firebase initialized."); + await GeminiService.initialize(); + logSafe("✨ Gemini service initialized (Gemini 2.5 Flash)."); } +/// --------------------------------------------------------------------------- +/// πŸ”Ή LOCAL STORAGE SETUP +/// --------------------------------------------------------------------------- Future _setupLocalStorage() async { if (!LocalStorage.isInitialized) { await LocalStorage.init(); - logSafe("πŸ’‘ Local storage initialized."); + logSafe("πŸ’Ύ Local storage initialized."); } else { - logSafe("ℹ️ Local storage already initialized, skipping."); + logSafe("ℹ️ Local storage already initialized. Skipping."); } } +/// --------------------------------------------------------------------------- +/// πŸ”Ή DEVICE INFO +/// --------------------------------------------------------------------------- Future _setupDeviceInfo() async { final deviceInfoService = DeviceInfoService(); await deviceInfoService.init(); - logSafe("πŸ“± Device Info: ${deviceInfoService.deviceData}"); + + logSafe("πŸ“± Device Info Loaded: ${deviceInfoService.deviceData}"); } +/// --------------------------------------------------------------------------- +/// πŸ”Ή THEME SETUP +/// --------------------------------------------------------------------------- Future _setupTheme() async { await ThemeCustomizer.init(); - logSafe("πŸ’‘ Theme customizer initialized."); + logSafe("🎨 Theme customizer initialized."); } +/// --------------------------------------------------------------------------- +/// πŸ”Ή FIREBASE CLOUD MESSAGING (PUSH) +/// --------------------------------------------------------------------------- Future _setupFirebaseMessaging() async { await FirebaseNotificationService().initialize(); - logSafe("πŸ’‘ Firebase Messaging initialized."); + logSafe("πŸ“¨ Firebase Messaging initialized."); } +/// --------------------------------------------------------------------------- +/// πŸ”Ή FINAL APP STYLE +/// --------------------------------------------------------------------------- void _finalizeAppStyle() { AppStyle.init(); - logSafe("πŸ’‘ AppStyle initialized."); + logSafe("🎯 AppStyle initialized."); } diff --git a/lib/helpers/services/gemini_service.dart b/lib/helpers/services/gemini_service.dart new file mode 100644 index 0000000..dcaf907 --- /dev/null +++ b/lib/helpers/services/gemini_service.dart @@ -0,0 +1,36 @@ +import 'package:firebase_ai/firebase_ai.dart'; + +class GeminiService { + static late GenerativeModel _model; + + /// Initializes Gemini Developer API + static Future initialize() async { + _model = FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.5-flash', + ); + } + + /// Generate text from a text prompt + static Future generateText(String prompt) async { + try { + final content = [Content.text(prompt)]; + + final response = await _model.generateContent(content); + + return response.text ?? "No response text found."; + } catch (e) { + return "Error: $e"; + } + } + + /// Stream text response (optional) + static Stream streamText(String prompt) async* { + final content = [Content.text(prompt)]; + + final stream = _model.generateContentStream(content); + + await for (final chunk in stream) { + if (chunk.text != null) yield chunk.text!; + } + } +} diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index b14af24..ec94488 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -20,6 +20,7 @@ import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/model/attendance/attendence_action_button.dart'; import 'package:on_field_work/model/attendance/log_details_view.dart'; import 'package:on_field_work/view/layouts/layout.dart'; +import 'package:on_field_work/view/dashboard/gemini_chat_card.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @@ -77,6 +78,7 @@ class _DashboardScreenState extends State with UIMixin { ); } + Widget _sectionTitle(String title) { return Padding( padding: const EdgeInsets.only(left: 4, bottom: 8), @@ -558,51 +560,62 @@ class _DashboardScreenState extends State with UIMixin { // --------------------------------------------------------------------------- @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xfff5f6fa), - body: Layout( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _projectSelector(), - MySpacing.height(20), - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _quickActions(), - MySpacing.height(20), - _dashboardModules(), - MySpacing.height(20), - _sectionTitle('Reports & Analytics'), - _cardWrapper( - child: ExpenseTypeReportChart(), - ), - _cardWrapper( - child: ExpenseByStatusWidget( - controller: dashboardController, +Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xfff5f6fa), + body: Layout( + child: Stack( + children: [ + // Main content + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _projectSelector(), + MySpacing.height(20), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _quickActions(), + MySpacing.height(20), + _sectionTitle('Modules'), + _dashboardModules(), + MySpacing.height(20), + _sectionTitle('Reports & Analytics'), + _cardWrapper( + child: ExpenseTypeReportChart(), ), - ), - _cardWrapper( - child: MonthlyExpenseDashboardChart(), - ), - MySpacing.height(20), - ], + _cardWrapper( + child: ExpenseByStatusWidget( + controller: dashboardController, + ), + ), + _cardWrapper( + child: MonthlyExpenseDashboardChart(), + ), + MySpacing.height(80), // give space under content + ], + ), ), ), - ), - ], + ], + ), ), - ), - ), - ); - } -} + // Floating Gemini assistant overlay + GeminiFloatingAssistant( + dashboardController: dashboardController, + ), + ], + ), + ), + ); +} +} class _DashboardCardMeta { final IconData icon; final Color color; diff --git a/lib/view/dashboard/gemini_chat_card.dart b/lib/view/dashboard/gemini_chat_card.dart new file mode 100644 index 0000000..0565740 --- /dev/null +++ b/lib/view/dashboard/gemini_chat_card.dart @@ -0,0 +1,548 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import 'package:get/get.dart'; +import 'package:on_field_work/controller/dashboard/dashboard_controller.dart'; +import 'package:on_field_work/helpers/services/gemini_service.dart'; +import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; +import 'package:on_field_work/helpers/widgets/my_spacing.dart'; +import 'package:on_field_work/helpers/widgets/my_text.dart'; + +/// Floating Gemini assistant for mobile dashboard. +/// Shows a circular FAB, which expands into a small chat panel from bottom-right. +class GeminiFloatingAssistant extends StatefulWidget { + final DashboardController dashboardController; + + const GeminiFloatingAssistant({ + super.key, + required this.dashboardController, + }); + + @override + State createState() => + _GeminiFloatingAssistantState(); +} + +class _GeminiFloatingAssistantState extends State + with SingleTickerProviderStateMixin, UIMixin { + // State + final TextEditingController _promptController = TextEditingController(); + final RxList> _chatHistory = + >[].obs; + final RxBool _isLoading = false.obs; + final RxBool _isAnalyzingData = false.obs; + final RxString _error = ''.obs; + final RxBool _isOpen = false.obs; + + final ScrollController _scrollController = ScrollController(); + + late final AnimationController _animController; + late final Animation _scaleAnimation; + late final Animation _opacityAnimation; + + @override + void initState() { + super.initState(); + _animController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 280), + ); + _scaleAnimation = CurvedAnimation( + parent: _animController, + curve: Curves.fastOutSlowIn, + ); + _opacityAnimation = CurvedAnimation( + parent: _animController, + curve: Curves.easeInOut, + ); + } + + // Scroll helper + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + ); + } + }); + } + + // Toggle FAB / panel + void _toggleOpen() { + _error.value = ''; + final newValue = !_isOpen.value; + _isOpen.value = newValue; + if (newValue) { + _animController.forward(); + } else { + _animController.reverse(); + } + } + + // Chat bubble + Widget _buildResponseBubble(String text, bool isAI) { + if (text.isEmpty) return const SizedBox.shrink(); + + final Color backgroundColor = + isAI ? Colors.blue.shade50 : contentTheme.primary.withOpacity(0.9); + final Color textColor = isAI ? Colors.black87 : Colors.white; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + isAI ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + if (isAI) ...[ + const Icon(LucideIcons.bot, color: Colors.blue, size: 20), + MySpacing.width(8), + ], + Flexible( + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(14), + topRight: const Radius.circular(14), + bottomLeft: + isAI ? Radius.zero : const Radius.circular(14), + bottomRight: + isAI ? const Radius.circular(14) : Radius.zero, + ), + ), + child: MyText.bodySmall( + text, + color: textColor, + height: 1.4, + ), + ), + ), + if (!isAI) ...[ + MySpacing.width(8), + const Icon(LucideIcons.user, color: Colors.black54, size: 18), + ], + ], + ), + ); + } + + // Prompt submit + Future _handlePromptSubmission() async { + final promptText = _promptController.text.trim(); + if (promptText.isEmpty || _isLoading.value || _isAnalyzingData.value) { + return; + } + + _chatHistory.add({'sender': 'user', 'message': promptText}); + _promptController.clear(); + _scrollToBottom(); + + _isLoading.value = true; + _error.value = ''; + + // Placeholder + _chatHistory.add({'sender': 'ai', 'message': 'Thinking...'}); + _scrollToBottom(); + final placeholderIndex = _chatHistory.length - 1; + + try { + final result = await GeminiService.generateText(promptText); + if (placeholderIndex >= 0 && + _chatHistory.length > placeholderIndex) { + _chatHistory[placeholderIndex] = { + 'sender': 'ai', + 'message': result, + }; + } + _scrollToBottom(); + } catch (e, stack) { + // ignore: avoid_print + print("Gemini Error: $e\n$stack"); + _error.value = + "An error occurred. Failed to get response: ${e.toString().split(':').first}"; + if (placeholderIndex >= 0 && + _chatHistory.length > placeholderIndex) { + _chatHistory.removeAt(placeholderIndex); + } + } finally { + _isLoading.value = false; + } + } + + // Expense analysis + Future _handleDataAnalysis() async { + if (_isLoading.value || _isAnalyzingData.value) return; + + _isAnalyzingData.value = true; + _error.value = ''; + + const String analysisPrompt = 'Analyze my monthly expense overview.'; + _chatHistory.add({'sender': 'user', 'message': analysisPrompt}); + _chatHistory + .add({'sender': 'ai', 'message': 'Analyzing monthly expenses...'}); + _scrollToBottom(); + final placeholderIndex = _chatHistory.length - 1; + + try { + await widget.dashboardController.fetchMonthlyExpenses(); + final rawDataList = widget.dashboardController.monthlyExpenseList; + + if (rawDataList.isEmpty) { + _error.value = "No monthly expense data found for analysis."; + _chatHistory.removeAt(placeholderIndex); + return; + } + + final rawDataJson = + jsonEncode(rawDataList.map((e) => e.toJson()).toList()); + + final prompt = + "Analyze the following monthly expense data and provide a concise " + "summary, key trends (like increasing/decreasing expenses), and " + "highlight the highest expense months. The data is a list of " + "monthly expense summaries over time in JSON format:\n\n$rawDataJson"; + + final result = await GeminiService.generateText(prompt); + + if (placeholderIndex >= 0 && + _chatHistory.length > placeholderIndex) { + _chatHistory[placeholderIndex] = { + 'sender': 'ai', + 'message': result, + }; + } + _scrollToBottom(); + } catch (e, stack) { + // ignore: avoid_print + print("Gemini Data Analysis Error: $e\n$stack"); + _error.value = + "Failed to analyze data. An error occurred: ${e.toString().split(':').first}"; + if (placeholderIndex >= 0 && + _chatHistory.length > placeholderIndex) { + _chatHistory.removeAt(placeholderIndex); + } + } finally { + _isAnalyzingData.value = false; + } + } + + // Small floating chat panel (mobile-friendly) + Widget _buildFloatingPanel(BuildContext context) { + final media = MediaQuery.of(context).size; + final double panelWidth = media.width * 0.9; + final double panelHeight = media.height * 0.45; + + return FadeTransition( + opacity: _opacityAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + alignment: Alignment.bottomRight, + child: Align( + alignment: Alignment.bottomRight, + child: Container( + width: panelWidth, + height: panelHeight, + margin: const EdgeInsets.only(right: 16, bottom: 80), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.18), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: contentTheme.primary.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon( + LucideIcons.bot, + size: 18, + color: Colors.blue, + ), + ), + MySpacing.width(8), + Expanded( + child: MyText.titleSmall( + "Gemini AI Assistant", + fontWeight: 700, + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + splashRadius: 18, + iconSize: 18, + onPressed: _toggleOpen, + icon: const Icon(Icons.close, color: Colors.black54), + ), + ], + ), + MySpacing.height(4), + MyText.labelSmall( + "Ask questions or analyze expenses.", + color: Colors.grey.shade600, + ), + MySpacing.height(8), + + // Error + Obx( + () => _error.value.isNotEmpty + ? Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(8), + border: + Border.all(color: Colors.red.shade200), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon( + LucideIcons.badge_alert, + color: Colors.red, + size: 16, + ), + MySpacing.width(6), + Expanded( + child: MyText.bodySmall( + _error.value, + color: Colors.red.shade800, + ), + ), + ], + ), + ) + : const SizedBox.shrink(), + ), + + // Chat list + Expanded( + child: Obx( + () { + if (_chatHistory.isEmpty) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + LucideIcons.bot, + size: 40, + color: Colors.blue.shade300, + ), + MySpacing.height(6), + Text( + "Ask me about your data!", + style: TextStyle( + color: Colors.grey.shade500, + ), + ), + ], + ), + ); + } + return ListView.builder( + controller: _scrollController, + itemCount: _chatHistory.length, + padding: + const EdgeInsets.symmetric(vertical: 4), + itemBuilder: (context, index) { + final message = _chatHistory[index]; + return _buildResponseBubble( + message['message'] ?? '', + message['sender'] == 'ai', + ); + }, + ); + }, + ), + ), + MySpacing.height(6), + + // Bottom row: Analyze + input + Row( + children: [ + Obx( + () => IconButton( + splashRadius: 18, + onPressed: (_isAnalyzingData.value || + _isLoading.value) + ? null + : _handleDataAnalysis, + iconSize: 22, + icon: _isAnalyzingData.value + ? SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: contentTheme.primary, + ), + ) + : const Icon( + LucideIcons.chart_line, + color: Colors.blue, + ), + tooltip: 'Analyze expenses', + ), + ), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: Colors.grey.shade300, + ), + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _promptController, + minLines: 1, + maxLines: 3, + keyboardType: TextInputType.multiline, + decoration: InputDecoration( + hintText: "Ask Gemini…", + border: InputBorder.none, + isDense: true, + hintStyle: TextStyle( + color: Colors.grey.shade500, + fontSize: 13, + ), + ), + onSubmitted: (_) => + _handlePromptSubmission(), + enabled: !_isAnalyzingData.value, + ), + ), + Obx( + () => IconButton( + splashRadius: 18, + iconSize: 18, + onPressed: (_isLoading.value || + _isAnalyzingData.value || + _promptController.text + .trim() + .isEmpty) + ? null + : _handlePromptSubmission, + icon: _isLoading.value + ? SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: contentTheme.primary, + ), + ) + : Icon( + LucideIcons.send, + color: _promptController.text + .trim() + .isEmpty + ? Colors.grey.shade400 + : contentTheme.primary, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + // FAB + Widget _buildFab() { + return Obx( + () => AnimatedScale( + scale: _isOpen.value ? 0.0 : 1.0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + child: FloatingActionButton.extended( + onPressed: _toggleOpen, + heroTag: 'gemini_fab', + backgroundColor: contentTheme.primary, + icon: const Icon(LucideIcons.bot), + label: const Text( + 'Ask Me Anything', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + ), + ), + ), + ); + } + + // Dim background when open + Widget _buildScrim() { + return Obx( + () => _isOpen.value + ? FadeTransition( + opacity: _opacityAnimation, + child: GestureDetector( + onTap: _toggleOpen, + child: Container( + color: Colors.black.withOpacity(0.25), + ), + ), + ) + : const SizedBox.shrink(), + ); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // Scrim behind panel + _buildScrim(), + // Panel + _buildFloatingPanel(context), + // FAB + Positioned( + bottom: 16, + right: 16, + child: _buildFab(), + ), + ], + ); + } + + @override + void dispose() { + _promptController.dispose(); + _scrollController.dispose(); + _animController.dispose(); + super.dispose(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 9288b1f..9881b9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,7 +73,7 @@ dependencies: tab_indicator_styler: ^2.0.0 connectivity_plus: ^7.0.0 geocoding: ^4.0.0 - firebase_core: ^4.0.0 + firebase_core: ^4.2.1 firebase_messaging: ^16.0.0 googleapis_auth: ^2.0.0 device_info_plus: ^12.3.0 @@ -88,6 +88,7 @@ dependencies: timeline_tile: ^2.0.0 encrypt: ^5.0.3 flutter_in_store_app_version_checker: ^1.10.0 + firebase_ai: ^3.6.0 dev_dependencies: flutter_test: From 0e79aa47932af14c784a2b206c52eebfb3221934 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Sat, 13 Dec 2025 14:34:02 +0530 Subject: [PATCH 2/6] imprroved --- lib/view/dashboard/dashboard_screen.dart | 1 - lib/view/dashboard/gemini_chat_card.dart | 70 ++++++++++++++++-------- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index ec94488..832cf42 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -582,7 +582,6 @@ Widget build(BuildContext context) { children: [ _quickActions(), MySpacing.height(20), - _sectionTitle('Modules'), _dashboardModules(), MySpacing.height(20), _sectionTitle('Reports & Analytics'), diff --git a/lib/view/dashboard/gemini_chat_card.dart b/lib/view/dashboard/gemini_chat_card.dart index 0565740..be05c70 100644 --- a/lib/view/dashboard/gemini_chat_card.dart +++ b/lib/view/dashboard/gemini_chat_card.dart @@ -56,6 +56,13 @@ class _GeminiFloatingAssistantState extends State parent: _animController, curve: Curves.easeInOut, ); + + // Add initial welcome message + _chatHistory.add({ + 'sender': 'ai', + 'message': + 'Hello! I am your AI assistant. How can I help you today? You can ask general questions or use the chart button below to analyze your monthly expenses.', + }); } // Scroll helper @@ -152,6 +159,7 @@ class _GeminiFloatingAssistantState extends State final placeholderIndex = _chatHistory.length - 1; try { + // NOTE: Assuming GeminiService.generateText handles the API call final result = await GeminiService.generateText(promptText); if (placeholderIndex >= 0 && _chatHistory.length > placeholderIndex) { @@ -191,11 +199,16 @@ class _GeminiFloatingAssistantState extends State try { await widget.dashboardController.fetchMonthlyExpenses(); + // NOTE: Assuming monthlyExpenseList contains a list of objects that have a .toJson() method final rawDataList = widget.dashboardController.monthlyExpenseList; if (rawDataList.isEmpty) { _error.value = "No monthly expense data found for analysis."; - _chatHistory.removeAt(placeholderIndex); + if (placeholderIndex >= 0 && + _chatHistory.length > placeholderIndex) { + _chatHistory.removeAt(placeholderIndex); + } + _isAnalyzingData.value = false; return; } @@ -435,7 +448,7 @@ class _GeminiFloatingAssistantState extends State ), onSubmitted: (_) => _handlePromptSubmission(), - enabled: !_isAnalyzingData.value, + enabled: !_isAnalyzingData.value && !_isLoading.value, ), ), Obx( @@ -449,7 +462,7 @@ class _GeminiFloatingAssistantState extends State .isEmpty) ? null : _handlePromptSubmission, - icon: _isLoading.value + icon: _isLoading.value && _promptController.text.trim().isNotEmpty // Show loading only for text submission ? SizedBox( width: 18, height: 18, @@ -493,17 +506,21 @@ class _GeminiFloatingAssistantState extends State onPressed: _toggleOpen, heroTag: 'gemini_fab', backgroundColor: contentTheme.primary, - icon: const Icon(LucideIcons.bot), + icon: const Icon(LucideIcons.bot, color: Colors.white), label: const Text( 'Ask Me Anything', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), ), ), ), ); } - // Dim background when open + // Dim background when open - Full screen scrim Widget _buildScrim() { return Obx( () => _isOpen.value @@ -511,8 +528,11 @@ class _GeminiFloatingAssistantState extends State opacity: _opacityAnimation, child: GestureDetector( onTap: _toggleOpen, - child: Container( - color: Colors.black.withOpacity(0.25), + // Use Positioned.fill to guarantee it covers the entire Stack + child: const Positioned.fill( + child: ColoredBox( + color: Colors.black38, // Use Colors.black38 for 38% opacity, slightly stronger than 0.25 + ), ), ), ) @@ -522,19 +542,25 @@ class _GeminiFloatingAssistantState extends State @override Widget build(BuildContext context) { - return Stack( - children: [ - // Scrim behind panel - _buildScrim(), - // Panel - _buildFloatingPanel(context), - // FAB - Positioned( - bottom: 16, - right: 16, - child: _buildFab(), - ), - ], + // Wrap the Stack in SizedBox.expand to force the widget to take the full space + // of its parent, which is typically the whole screen area in a dashboard. + return SizedBox.expand( + child: Stack( + children: [ + // 1. Scrim (Background layer when open) - Now using Positioned.fill for reliability + _buildScrim(), + + // 2. Panel (Middle layer when open) + _buildFloatingPanel(context), + + // 3. FAB (Top layer when closed) + Positioned( + bottom: 16, + right: 16, + child: _buildFab(), + ), + ], + ), ); } @@ -545,4 +571,4 @@ class _GeminiFloatingAssistantState extends State _animController.dispose(); super.dispose(); } -} +} \ No newline at end of file From 9389e081c9b61935a27e61bf21ec7a4529677fec Mon Sep 17 00:00:00 2001 From: Manish Date: Sat, 13 Dec 2025 17:21:11 +0530 Subject: [PATCH 3/6] bypass changes to debugg without key.proporties --- android/app/build.gradle | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 5242a55..2643465 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -48,32 +48,30 @@ android { // Define signing configurations for different build types signingConfigs { - release { - // Reference the key alias from key.properties + release { + if (keystorePropertiesFile.exists()) { keyAlias keystoreProperties['keyAlias'] - // Reference the key password from key.properties keyPassword keystoreProperties['keyPassword'] - // Reference the keystore file path from key.properties storeFile file(keystoreProperties['storeFile']) - // Reference the keystore password from key.properties storePassword keystoreProperties['storePassword'] } } +} + // Define different build types (e.g., debug, release) - buildTypes { - release { - // Apply the 'release' signing configuration defined above to the release build + buildTypes { + release { + if (keystorePropertiesFile.exists()) { signingConfig signingConfigs.release - // Enable code minification to reduce app size - minifyEnabled true - // Enable resource shrinking to remove unused resources - shrinkResources true - // Other release specific configurations can be added here, e.g., ProGuard rules } + minifyEnabled true + shrinkResources true } } +} + // Configure Flutter specific settings, pointing to the root of your Flutter project flutter { source = "../.." From d4c7eae981e83eee030dadf974fa8c28013a8932 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Sat, 13 Dec 2025 14:36:54 +0530 Subject: [PATCH 4/6] improved logic --- .../daily_task_planning_controller.dart | 27 +++++++++---------- lib/helpers/services/api_service.dart | 1 - .../assign_task_bottom_sheet .dart | 8 +++--- .../taskPlanning/daily_task_planning.dart | 13 +-------- 4 files changed, 16 insertions(+), 33 deletions(-) diff --git a/lib/controller/task_planning/daily_task_planning_controller.dart b/lib/controller/task_planning/daily_task_planning_controller.dart index 8ebd877..33cc9b2 100644 --- a/lib/controller/task_planning/daily_task_planning_controller.dart +++ b/lib/controller/task_planning/daily_task_planning_controller.dart @@ -27,6 +27,7 @@ class DailyTaskPlanningController extends GetxController { RxMap buildingLoadingStates = {}.obs; final Set buildingsWithDetails = {}; + RxMap todaysAssignedMap = {}.obs; @override void onInit() { super.onInit(); @@ -72,6 +73,8 @@ class DailyTaskPlanningController extends GetxController { required int plannedTask, required String description, required List taskTeam, + required String buildingId, + required String projectId, DateTime? assignmentDate, String? organizationId, String? serviceId, @@ -93,6 +96,9 @@ class DailyTaskPlanningController extends GetxController { if (response == true) { logSafe("Task assigned successfully", level: LogLevel.info); + await fetchBuildingInfra(buildingId, projectId, serviceId); + Get.back(); + showAppSnackbar( title: "Success", message: "Task assigned successfully!", @@ -164,23 +170,19 @@ class DailyTaskPlanningController extends GetxController { level: LogLevel.error, error: e, stackTrace: stack); } finally { isFetchingTasks.value = false; - update(); + update(); // dailyTasks is non-reactive } } - /// Fetch full infra for a single building (floors, workAreas, workItems). - /// Called lazily when user expands a building in the UI. + /// Fetch full infra for a single building (lazy) Future fetchBuildingInfra( String buildingId, String projectId, String? serviceId) async { if (buildingId.isEmpty) return; - // mark loading buildingLoadingStates.putIfAbsent(buildingId, () => true.obs); - buildingLoadingStates[buildingId]!.value = true; - update(); + buildingLoadingStates[buildingId]!.value = true; // Rx change is enough try { - // Re-use getInfraDetails and find the building entry for the requested buildingId final infraResponse = await ApiService.getInfraDetails(projectId, serviceId: serviceId); final infraData = infraResponse?['data'] as List? ?? []; @@ -196,7 +198,6 @@ class DailyTaskPlanningController extends GetxController { return; } - // Build floors & workAreas for this building final building = Building( id: buildingJson['id'], name: buildingJson['buildingName'], @@ -211,7 +212,7 @@ class DailyTaskPlanningController extends GetxController { return WorkArea( id: areaJson['id'], areaName: areaJson['areaName'], - workItems: [], // will populate later + workItems: [], ); }).toList(), ); @@ -220,7 +221,6 @@ class DailyTaskPlanningController extends GetxController { completedWork: (buildingJson['completedWork'] as num?)?.toDouble() ?? 0, ); - // For each workArea, fetch its work items and populate await Future.wait( building.floors.expand((f) => f.workAreas).map((area) async { try { @@ -255,7 +255,6 @@ class DailyTaskPlanningController extends GetxController { } })); - // Merge/replace the building into dailyTasks bool merged = false; for (var t in dailyTasks) { final idx = t.buildings @@ -267,7 +266,6 @@ class DailyTaskPlanningController extends GetxController { } } if (!merged) { - // If not present, add a new TaskPlanningDetailsModel wrapper (fallback) dailyTasks.add(TaskPlanningDetailsModel( id: building.id, name: building.name, @@ -280,7 +278,6 @@ class DailyTaskPlanningController extends GetxController { )); } - // Mark as loaded buildingsWithDetails.add(buildingId.toString()); } catch (e, stack) { logSafe("Error fetching infra for building $buildingId", @@ -288,7 +285,7 @@ class DailyTaskPlanningController extends GetxController { } finally { buildingLoadingStates.putIfAbsent(buildingId, () => false.obs); buildingLoadingStates[buildingId]!.value = false; - update(); + update(); // dailyTasks mutated } } @@ -361,7 +358,7 @@ class DailyTaskPlanningController extends GetxController { } } finally { isFetchingEmployees.value = false; - update(); + // no update(): RxLists/RxBools notify observers } } } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 59db63f..c21ea8e 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -2075,7 +2075,6 @@ class ApiService { final parsed = _parseAndDecryptResponse(response, label: "Assign Daily Task", returnFullResponse: true); if (parsed != null && parsed['success'] == true) { - Get.back(); // Retaining Get.back() as per original logic return true; } return false; diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index 0757a40..cd16993 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -82,10 +82,6 @@ class _AssignTaskBottomSheetState extends State { serviceId: selectedService?.id, organizationId: selectedOrganization?.id, ); - await controller.fetchTaskData( - selectedProjectId, - serviceId: selectedService?.id, - ); } @override @@ -421,8 +417,10 @@ class _AssignTaskBottomSheetState extends State { workItemId: widget.workItemId, plannedTask: target.toInt(), description: description, - taskTeam: selectedTeam.map((e) => e.id).toList(), // pass IDs + taskTeam: selectedTeam.map((e) => e.id).toList(), assignmentDate: widget.assignmentDate, + buildingId: widget.buildingName, + projectId: selectedProjectId!, organizationId: selectedOrganization?.id, serviceId: selectedService?.id, ); diff --git a/lib/view/taskPlanning/daily_task_planning.dart b/lib/view/taskPlanning/daily_task_planning.dart index d30f251..a184b6e 100644 --- a/lib/view/taskPlanning/daily_task_planning.dart +++ b/lib/view/taskPlanning/daily_task_planning.dart @@ -492,18 +492,7 @@ class _DailyTaskPlanningScreenState extends State ), ); - final projectId = - widget.projectId; - if (projectId.isNotEmpty) { - await dailyTaskPlanningController - .fetchTaskData( - projectId, - serviceId: - serviceController - .selectedService - ?.id, - ); - } + }), ], ), From 5f1693869df4ae62b13c02ed6851f804843b0caf Mon Sep 17 00:00:00 2001 From: Manish Date: Sat, 13 Dec 2025 17:21:11 +0530 Subject: [PATCH 5/6] bypass changes to debugg without key.proporties --- android/app/build.gradle | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 5242a55..2643465 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -48,32 +48,30 @@ android { // Define signing configurations for different build types signingConfigs { - release { - // Reference the key alias from key.properties + release { + if (keystorePropertiesFile.exists()) { keyAlias keystoreProperties['keyAlias'] - // Reference the key password from key.properties keyPassword keystoreProperties['keyPassword'] - // Reference the keystore file path from key.properties storeFile file(keystoreProperties['storeFile']) - // Reference the keystore password from key.properties storePassword keystoreProperties['storePassword'] } } +} + // Define different build types (e.g., debug, release) - buildTypes { - release { - // Apply the 'release' signing configuration defined above to the release build + buildTypes { + release { + if (keystorePropertiesFile.exists()) { signingConfig signingConfigs.release - // Enable code minification to reduce app size - minifyEnabled true - // Enable resource shrinking to remove unused resources - shrinkResources true - // Other release specific configurations can be added here, e.g., ProGuard rules } + minifyEnabled true + shrinkResources true } } +} + // Configure Flutter specific settings, pointing to the root of your Flutter project flutter { source = "../.." From 57634d7bd224532edabf780c2c98499cff80d262 Mon Sep 17 00:00:00 2001 From: Manish Date: Mon, 15 Dec 2025 17:14:45 +0530 Subject: [PATCH 6/6] done with update Today's Planned count right after submit of assign task --- .../daily_task_planning_controller.dart | 20 +++++++++++-------- lib/helpers/services/api_endpoints.dart | 4 ++-- .../assign_task_bottom_sheet .dart | 12 ++++++++--- .../taskPlanning/daily_task_planning.dart | 1 + 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/controller/task_planning/daily_task_planning_controller.dart b/lib/controller/task_planning/daily_task_planning_controller.dart index 33cc9b2..ea43d3c 100644 --- a/lib/controller/task_planning/daily_task_planning_controller.dart +++ b/lib/controller/task_planning/daily_task_planning_controller.dart @@ -12,7 +12,8 @@ class DailyTaskPlanningController extends GetxController { RxList employees = [].obs; RxList selectedEmployees = [].obs; List allEmployeesCache = []; - List dailyTasks = []; + RxList dailyTasks = + [].obs; RxMap uploadingStates = {}.obs; MyFormValidator basicValidator = MyFormValidator(); @@ -97,7 +98,6 @@ class DailyTaskPlanningController extends GetxController { if (response == true) { logSafe("Task assigned successfully", level: LogLevel.info); await fetchBuildingInfra(buildingId, projectId, serviceId); - Get.back(); showAppSnackbar( title: "Success", @@ -129,18 +129,17 @@ class DailyTaskPlanningController extends GetxController { final infraData = infraResponse?['data'] as List?; if (infraData == null || infraData.isEmpty) { - dailyTasks = []; + dailyTasks.clear(); //reactive clear return; } - // Filter buildings with 0 planned & completed work final filteredBuildings = infraData.where((b) { final planned = (b['plannedWork'] as num?)?.toDouble() ?? 0; final completed = (b['completedWork'] as num?)?.toDouble() ?? 0; return planned > 0 || completed > 0; }).toList(); - dailyTasks = filteredBuildings.map((buildingJson) { + final mapped = filteredBuildings.map((buildingJson) { final building = Building( id: buildingJson['id'], name: buildingJson['buildingName'], @@ -163,14 +162,19 @@ class DailyTaskPlanningController extends GetxController { ); }).toList(); + dailyTasks.assignAll(mapped); + buildingLoadingStates.clear(); buildingsWithDetails.clear(); } catch (e, stack) { - logSafe("Error fetching daily task data", - level: LogLevel.error, error: e, stackTrace: stack); + logSafe( + "Error fetching daily task data", + level: LogLevel.error, + error: e, + stackTrace: stack, + ); } finally { isFetchingTasks.value = false; - update(); // dailyTasks is non-reactive } } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index fa12d19..bc8df7a 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,9 +1,9 @@ class ApiEndpoints { - // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api"; - static const String baseUrl = "https://api.onfieldwork.com/api"; + // static const String baseUrl = "https://api.onfieldwork.com/api"; static const String getMasterCurrencies = "/Master/currencies/list"; diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index cd16993..5a6c73b 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -24,9 +24,11 @@ class AssignTaskBottomSheet extends StatefulWidget { final String buildingName; final String floorName; final String workAreaName; + final String buildingId; const AssignTaskBottomSheet({ super.key, + required this.buildingId, required this.buildingName, required this.workLocation, required this.floorName, @@ -372,7 +374,7 @@ class _AssignTaskBottomSheetState extends State { } } - void _onAssignTaskPressed() { + Future _onAssignTaskPressed() async { final selectedTeam = controller.selectedEmployees; if (selectedTeam.isEmpty) { @@ -413,16 +415,20 @@ class _AssignTaskBottomSheetState extends State { return; } - controller.assignDailyTask( + final success = await controller.assignDailyTask( workItemId: widget.workItemId, plannedTask: target.toInt(), description: description, taskTeam: selectedTeam.map((e) => e.id).toList(), assignmentDate: widget.assignmentDate, - buildingId: widget.buildingName, + buildingId: widget.buildingId, projectId: selectedProjectId!, organizationId: selectedOrganization?.id, serviceId: selectedService?.id, ); + + if (success) { + Navigator.pop(context); + } } } diff --git a/lib/view/taskPlanning/daily_task_planning.dart b/lib/view/taskPlanning/daily_task_planning.dart index a184b6e..5b3eb19 100644 --- a/lib/view/taskPlanning/daily_task_planning.dart +++ b/lib/view/taskPlanning/daily_task_planning.dart @@ -472,6 +472,7 @@ class _DailyTaskPlanningScreenState extends State ), builder: (context) => AssignTaskBottomSheet( + buildingId: building.id, buildingName: building.name, floorName: