From 5c6c6289cd07c97f908683d8313274b833bd097a Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 12 Dec 2025 15:24:37 +0530 Subject: [PATCH 01/26] 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 02/26] 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 03/26] 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 04/26] 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 05/26] 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 06/26] 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: From e2fc81ba0e56f9fa19927e26d451427a4c641385 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 12 Dec 2025 15:24:37 +0530 Subject: [PATCH 07/26] 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 5789100..ce08c89 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 2aa6d13a7182640282fa74861048c921aa993da9 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Sat, 13 Dec 2025 14:34:02 +0530 Subject: [PATCH 08/26] 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 b2e3398bb1e05b07726649e4b26c258daeb0bd83 Mon Sep 17 00:00:00 2001 From: Manish Date: Sat, 13 Dec 2025 17:21:11 +0530 Subject: [PATCH 09/26] 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 c45cb6158ee85a193ffc8d4cbf5bd3f9583b5e54 Mon Sep 17 00:00:00 2001 From: Manish Date: Mon, 15 Dec 2025 17:14:45 +0530 Subject: [PATCH 10/26] 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 b39cb40..7d94d8b 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: From 55f36fac6deb3b85a966fcc643d7ab5d674bc616 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 17 Dec 2025 10:37:48 +0530 Subject: [PATCH 11/26] implemented assign employee feature for infra project module --- lib/helpers/services/api_endpoints.dart | 19 +- lib/helpers/services/api_service.dart | 38 +++ .../assign_project_allocation_request.dart | 25 ++ .../assign_employee_infra_bottom_sheet.dart | 299 ++++++++++++++++++ .../infra_project_details_screen.dart | 30 ++ 5 files changed, 403 insertions(+), 8 deletions(-) create mode 100644 lib/model/infra_project/assign_project_allocation_request.dart create mode 100644 lib/view/infraProject/assign_employee_infra_bottom_sheet.dart diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 7d94d8b..29b41ff 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -5,7 +5,6 @@ class ApiEndpoints { // static const String baseUrl = "https://mapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.onfieldwork.com/api"; - static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = "/Master/expenses-categories"; @@ -48,7 +47,8 @@ class ApiEndpoints { static const String getProjects = "/project/list"; static const String getGlobalProjects = "/project/list/basic"; static const String getTodaysAttendance = "/attendance/project/team"; - static const String getAttendanceForDashboard = "/dashboard/get/attendance/employee/:projectId"; + static const String getAttendanceForDashboard = + "/dashboard/get/attendance/employee/:projectId"; static const String getAttendanceLogs = "/attendance/project/log"; static const String getAttendanceLogView = "/attendance/log/attendance"; static const String getRegularizationLogs = "/attendance/regularize"; @@ -142,7 +142,6 @@ class ApiEndpoints { static const String manageOrganizationHierarchy = "/organization/hierarchy/manage"; - // Service Project Module API Endpoints static const String getServiceProjectsList = "/serviceproject/list"; static const String getServiceProjectDetail = "/serviceproject/details"; @@ -151,10 +150,14 @@ class ApiEndpoints { "/serviceproject/job/details"; static const String editServiceProjectJob = "/serviceproject/job/edit"; static const String createServiceProjectJob = "/serviceproject/job/create"; - static const String serviceProjectUpateJobAttendance = "/serviceproject/job/attendance"; - static const String serviceProjectUpateJobAttendanceLog = "/serviceproject/job/attendance/log"; - static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list"; - static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation"; + static const String serviceProjectUpateJobAttendance = + "/serviceproject/job/attendance"; + static const String serviceProjectUpateJobAttendanceLog = + "/serviceproject/job/attendance/log"; + static const String getServiceProjectUpateJobAllocationList = + "/serviceproject/get/allocation/list"; + static const String manageServiceProjectUpateJobAllocation = + "/serviceproject/manage/allocation"; static const String getTeamRoles = "/master/team-roles/list"; static const String getServiceProjectBranches = "/serviceproject/branch/list"; @@ -168,5 +171,5 @@ class ApiEndpoints { static const String getInfraProjectsList = "/project/list"; static const String getInfraProjectDetail = "/project/details"; static const String getInfraProjectTeamList = "/project/allocation"; - + static const String assignInfraProjectAllocation = "/project/allocation"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index f56341e..618410b 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -52,6 +52,8 @@ import 'package:on_field_work/model/infra_project/infra_project_details.dart'; import 'package:on_field_work/model/dashboard/collection_overview_model.dart'; import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart'; import 'package:on_field_work/model/infra_project/infra_team_list_model.dart'; +import 'package:on_field_work/model/infra_project/assign_project_allocation_request.dart'; + class ApiService { static const bool enableLogs = true; @@ -2008,6 +2010,42 @@ class ApiService { label: "Comment Task", returnFullResponse: true); return parsed != null && parsed['success'] == true; } + + static Future assignEmployeesToProject({ + required List allocations, + }) async { + if (allocations.isEmpty) { + _log( + "No allocations provided for assignEmployeesToProject", + level: LogLevel.error, + ); + return null; + } + + final endpoint = ApiEndpoints.assignInfraProjectAllocation; + final payload = allocations.map((e) => e.toJson()).toList(); + + final response = await _safeApiCall( + endpoint, + method: 'POST', + body: payload, + ); + + if (response == null) return null; + + final parsedJson = _parseAndDecryptResponse( + response, + label: "AssignInfraProjectAllocation", + returnFullResponse: true, + ); + + if (parsedJson == null || parsedJson is! Map) { + return null; + } + + return ProjectAllocationResponse.fromJson(parsedJson); + } + static Future getInfraProjectTeamListApi({ required String projectId, String? serviceId, diff --git a/lib/model/infra_project/assign_project_allocation_request.dart b/lib/model/infra_project/assign_project_allocation_request.dart new file mode 100644 index 0000000..f64b826 --- /dev/null +++ b/lib/model/infra_project/assign_project_allocation_request.dart @@ -0,0 +1,25 @@ +class AssignProjectAllocationRequest { + final String employeeId; + final String projectId; + final String jobRoleId; + final String serviceId; + final bool status; + + AssignProjectAllocationRequest({ + required this.employeeId, + required this.projectId, + required this.jobRoleId, + required this.serviceId, + required this.status, + }); + + Map toJson() { + return { + "employeeId": employeeId, + "projectId": projectId, + "jobRoleId": jobRoleId, + "serviceId": serviceId, + "status": status, + }; + } +} diff --git a/lib/view/infraProject/assign_employee_infra_bottom_sheet.dart b/lib/view/infraProject/assign_employee_infra_bottom_sheet.dart new file mode 100644 index 0000000..fac4332 --- /dev/null +++ b/lib/view/infraProject/assign_employee_infra_bottom_sheet.dart @@ -0,0 +1,299 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'package:on_field_work/controller/tenant/organization_selection_controller.dart'; +import 'package:on_field_work/helpers/widgets/my_spacing.dart'; +import 'package:on_field_work/helpers/widgets/my_text.dart'; +import 'package:on_field_work/helpers/widgets/tenant/organization_selector.dart'; +import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart'; +import 'package:on_field_work/model/employees/employee_model.dart'; +import 'package:on_field_work/model/employees/multiple_select_bottomsheet.dart'; +import 'package:on_field_work/controller/tenant/service_controller.dart'; +import 'package:on_field_work/helpers/widgets/tenant/service_selector.dart'; +import 'package:on_field_work/model/tenant/tenant_services_model.dart'; +import 'package:on_field_work/helpers/services/api_service.dart'; +import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; +import 'package:on_field_work/model/infra_project/assign_project_allocation_request.dart'; + + +class JobRole { + final String id; + final String name; + + JobRole({required this.id, required this.name}); + + factory JobRole.fromJson(Map json) { + return JobRole( + id: json['id'].toString(), + name: json['name'] ?? '', + ); + } +} + +class AssignEmployeeBottomSheet extends StatefulWidget { + final String projectId; + + const AssignEmployeeBottomSheet({ + super.key, + required this.projectId, + }); + + @override + State createState() => + _AssignEmployeeBottomSheetState(); +} + +class _AssignEmployeeBottomSheetState extends State { + late final OrganizationController _organizationController; + late final ServiceController _serviceController; + + final RxList _selectedEmployees = [].obs; + + Organization? _selectedOrganization; + JobRole? _selectedRole; + + final RxBool _isLoadingRoles = false.obs; + final RxList _roles = [].obs; + + @override + void initState() { + super.initState(); + + _organizationController = Get.put( + OrganizationController(), + tag: 'assign_employee_org', + ); + + _serviceController = Get.put( + ServiceController(), + tag: 'assign_employee_service', + ); + + _organizationController.fetchOrganizations(widget.projectId); + _serviceController.fetchServices(widget.projectId); + + _fetchRoles(); + } + + Future _fetchRoles() async { + try { + _isLoadingRoles.value = true; + final res = await ApiService.getRoles(); + if (res != null) { + _roles.assignAll( + res.map((e) => JobRole.fromJson(e)).toList(), + ); + } + } finally { + _isLoadingRoles.value = false; + } + } + + @override + void dispose() { + Get.delete(tag: 'assign_employee_org'); + Get.delete(tag: 'assign_employee_service'); + super.dispose(); + } + + Future _openEmployeeSelector() async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => EmployeeSelectionBottomSheet( + title: 'Select Employee(s)', + multipleSelection: true, + initiallySelected: _selectedEmployees.toList(), + ), + ); + + if (result != null && result is List) { + _selectedEmployees.assignAll(result); + } + } + + void _handleAssign() async { + if (_selectedEmployees.isEmpty || + _selectedRole == null || + _serviceController.selectedService == null) { + Get.snackbar('Error', 'Please complete all selections'); + return; + } + + final allocations = _selectedEmployees + .map( + (e) => AssignProjectAllocationRequest( + employeeId: e.id, + projectId: widget.projectId, + jobRoleId: _selectedRole!.id, + serviceId: _serviceController.selectedService!.id, + status: true, + ), + ) + .toList(); + + final res = await ApiService.assignEmployeesToProject( + allocations: allocations, + ); + + if (res?.success == true) { + Navigator.of(context).pop(true); // ๐Ÿ”ฅ triggers refresh + } else { + Get.snackbar('Error', res?.message ?? 'Assignment failed'); + } + } + + @override + Widget build(BuildContext context) { + return BaseBottomSheet( + title: 'Assign Employee', + submitText: 'Assign', + isSubmitting: false, + onCancel: () => Navigator.of(context).pop(), + onSubmit: _handleAssign, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + //ORGANIZATION + MyText.bodySmall( + 'Organization', + fontWeight: 600, + color: Colors.grey.shade700, + ), + MySpacing.height(6), + + OrganizationSelector( + controller: _organizationController, + height: 44, + onSelectionChanged: (Organization? org) async { + _selectedOrganization = org; + _selectedEmployees.clear(); + _selectedRole = null; + _serviceController.clearSelection(); + }, + ), + + MySpacing.height(20), + + ///EMPLOYEES (SEARCH) + MyText.bodySmall( + 'Employees', + fontWeight: 600, + color: Colors.grey.shade700, + ), + MySpacing.height(6), + + Obx( + () => InkWell( + onTap: _openEmployeeSelector, + child: _dropdownBox( + _selectedEmployees.isEmpty + ? 'Select employee(s)' + : '${_selectedEmployees.length} employee(s) selected', + icon: Icons.search, + ), + ), + ), + + MySpacing.height(20), + + ///SERVICE + MyText.bodySmall( + 'Service', + fontWeight: 600, + color: Colors.grey.shade700, + ), + MySpacing.height(6), + + ServiceSelector( + controller: _serviceController, + height: 44, + onSelectionChanged: (Service? service) async { + _selectedRole = null; + }, + ), + + MySpacing.height(20), + + /// JOB ROLE + MyText.bodySmall( + 'Job Role', + fontWeight: 600, + color: Colors.grey.shade700, + ), + MySpacing.height(6), + + Obx(() { + if (_isLoadingRoles.value) { + return _skeleton(); + } + + return PopupMenuButton( + onSelected: (role) { + _selectedRole = role; + setState(() {}); + }, + itemBuilder: (context) { + if (_roles.isEmpty) { + return const [ + PopupMenuItem( + enabled: false, + child: Text('No roles found'), + ), + ]; + } + return _roles + .map( + (r) => PopupMenuItem( + value: r, + child: Text(r.name), + ), + ) + .toList(); + }, + child: _dropdownBox( + _selectedRole?.name ?? 'Select role', + ), + ); + }), + ], + ), + ); + } + + Widget _dropdownBox(String text, {IconData icon = Icons.arrow_drop_down}) { + return Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + text, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 13), + ), + ), + Icon(icon, color: Colors.grey), + ], + ), + ); + } + + Widget _skeleton() { + return Container( + height: 44, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(12), + ), + ); + } +} diff --git a/lib/view/infraProject/infra_project_details_screen.dart b/lib/view/infraProject/infra_project_details_screen.dart index c4af071..82cb2ad 100644 --- a/lib/view/infraProject/infra_project_details_screen.dart +++ b/lib/view/infraProject/infra_project_details_screen.dart @@ -18,6 +18,8 @@ import 'package:on_field_work/controller/infra_project/infra_project_screen_deta import 'package:on_field_work/view/taskPlanning/daily_progress_report.dart'; import 'package:on_field_work/view/taskPlanning/daily_task_planning.dart'; import 'package:on_field_work/model/infra_project/infra_team_list_model.dart'; +import 'package:on_field_work/view/infraProject/assign_employee_infra_bottom_sheet.dart'; + class InfraProjectDetailsScreen extends StatefulWidget { final String projectId; @@ -77,6 +79,21 @@ class _InfraProjectDetailsScreenState extends State _tabController = TabController(length: _tabs.length, vsync: this); } + void _openAssignEmployeeBottomSheet() async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => AssignEmployeeBottomSheet( + projectId: widget.projectId, + ), + ); + if (result == true) { + controller.fetchProjectTeamList(); + Get.snackbar('Success', 'Employee assigned successfully'); + } + } + @override void dispose() { _tabController.dispose(); @@ -487,6 +504,19 @@ class _InfraProjectDetailsScreenState extends State projectName: widget.projectName, backgroundColor: appBarColor, ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + _openAssignEmployeeBottomSheet(); + }, + backgroundColor: contentTheme.primary, + icon: const Icon(Icons.person_add), + label: MyText( + 'Assign Employee', + fontSize: 14, + color: Colors.white, + fontWeight: 500, + ), + ), body: Stack( children: [ Container( From fe5cae988930834d80d509d0c3cbc16ba8514471 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 17 Dec 2025 12:03:30 +0530 Subject: [PATCH 12/26] UI change in Manage Reporting bottom sheet --- .../manage_reporting_bottom_sheet.dart | 87 +++++++++++++------ 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 4aabaf1..3136d5e 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -36,6 +36,10 @@ class _ManageReportingBottomSheetState final TextEditingController _primaryController = TextEditingController(); final TextEditingController _secondaryController = TextEditingController(); + final FocusNode _mainEmployeeFocus = FocusNode(); + final FocusNode _primaryFocus = FocusNode(); + final FocusNode _secondaryFocus = FocusNode(); + final RxList _filteredPrimary = [].obs; final RxList _filteredSecondary = [].obs; final RxList _selectedPrimary = [].obs; @@ -69,6 +73,10 @@ class _ManageReportingBottomSheetState @override void dispose() { + _mainEmployeeFocus.dispose(); + _primaryFocus.dispose(); + _secondaryFocus.dispose(); + _primaryController.dispose(); _secondaryController.dispose(); _selectEmployeeController.dispose(); @@ -368,6 +376,7 @@ class _ManageReportingBottomSheetState _buildSearchSection( label: "Primary Reporting Manager*", controller: _primaryController, + focusNode: _primaryFocus, filteredList: _filteredPrimary, selectedList: _selectedPrimary, isPrimary: true, @@ -379,6 +388,7 @@ class _ManageReportingBottomSheetState _buildSearchSection( label: "Secondary Reporting Manager", controller: _secondaryController, + focusNode: _secondaryFocus, filteredList: _filteredSecondary, selectedList: _selectedSecondary, isPrimary: false, @@ -390,8 +400,10 @@ class _ManageReportingBottomSheetState final safeWrappedContent = SafeArea( child: SingleChildScrollView( padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewPadding.bottom + 20, - left: 16, right: 16, top: 8, + bottom: MediaQuery.of(context).viewPadding.bottom + 20, + left: 16, + right: 16, + top: 8, ), child: content, ), @@ -417,7 +429,7 @@ class _ManageReportingBottomSheetState isSubmitting: _isSubmitting, onCancel: _handleCancel, onSubmit: _handleSubmit, - child: safeWrappedContent, + child: safeWrappedContent, ); } @@ -449,6 +461,7 @@ class _ManageReportingBottomSheetState Widget _buildSearchSection({ required String label, required TextEditingController controller, + required FocusNode focusNode, required RxList filteredList, required RxList selectedList, required bool isPrimary, @@ -459,20 +472,10 @@ class _ManageReportingBottomSheetState MyText.bodyMedium(label, fontWeight: 600), MySpacing.height(8), - // Search field - TextField( + _searchBar( controller: controller, - decoration: InputDecoration( - hintText: "Type to search employees...", - isDense: true, - filled: true, - fillColor: Colors.grey[50], - prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - ), + focusNode: focusNode, + hint: "Type to search employees...", ), // Dropdown suggestions @@ -567,19 +570,10 @@ class _ManageReportingBottomSheetState children: [ MyText.bodyMedium("Select Employee *", fontWeight: 600), MySpacing.height(8), - TextField( + _searchBar( controller: _selectEmployeeController, - decoration: InputDecoration( - hintText: "Type to search employee...", - isDense: true, - filled: true, - fillColor: Colors.grey[50], - prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - ), + focusNode: _mainEmployeeFocus, + hint: "Type to search employee...", ), Obx(() { if (_filteredEmployees.isEmpty) return const SizedBox.shrink(); @@ -641,4 +635,41 @@ class _ManageReportingBottomSheetState ], ); } + + Widget _searchBar({ + required TextEditingController controller, + required FocusNode focusNode, + required String hint, + }) { + return GestureDetector( + onTap: () => focusNode.requestFocus(), + child: Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + color: Colors.white, + ), + child: Row( + children: [ + const Icon(Icons.search, size: 18, color: Colors.grey), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: controller, + focusNode: focusNode, + decoration: InputDecoration( + hintText: hint, + border: InputBorder.none, + isDense: true, + ), + style: const TextStyle(fontSize: 13), + ), + ), + ], + ), + ), + ); + } } From 7da7fadbc9b0dedaec9154cc89734a5c801bcf08 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 17 Dec 2025 15:11:27 +0530 Subject: [PATCH 13/26] removed gemini bot feature --- lib/helpers/services/gemini_service.dart | 36 -- lib/view/dashboard/dashboard_screen.dart | 6 - lib/view/dashboard/gemini_chat_card.dart | 574 ----------------------- 3 files changed, 616 deletions(-) delete mode 100644 lib/helpers/services/gemini_service.dart delete mode 100644 lib/view/dashboard/gemini_chat_card.dart diff --git a/lib/helpers/services/gemini_service.dart b/lib/helpers/services/gemini_service.dart deleted file mode 100644 index dcaf907..0000000 --- a/lib/helpers/services/gemini_service.dart +++ /dev/null @@ -1,36 +0,0 @@ -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 832cf42..dff0b48 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -20,7 +20,6 @@ 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}); @@ -604,11 +603,6 @@ Widget build(BuildContext context) { ], ), ), - - // Floating Gemini assistant overlay - GeminiFloatingAssistant( - dashboardController: dashboardController, - ), ], ), ), diff --git a/lib/view/dashboard/gemini_chat_card.dart b/lib/view/dashboard/gemini_chat_card.dart deleted file mode 100644 index be05c70..0000000 --- a/lib/view/dashboard/gemini_chat_card.dart +++ /dev/null @@ -1,574 +0,0 @@ -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, - ); - - // 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 - 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 { - // NOTE: Assuming GeminiService.generateText handles the API call - 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(); - // 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."; - if (placeholderIndex >= 0 && - _chatHistory.length > placeholderIndex) { - _chatHistory.removeAt(placeholderIndex); - } - _isAnalyzingData.value = false; - 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 && !_isLoading.value, - ), - ), - Obx( - () => IconButton( - splashRadius: 18, - iconSize: 18, - onPressed: (_isLoading.value || - _isAnalyzingData.value || - _promptController.text - .trim() - .isEmpty) - ? null - : _handlePromptSubmission, - icon: _isLoading.value && _promptController.text.trim().isNotEmpty // Show loading only for text submission - ? 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, color: Colors.white), - label: const Text( - 'Ask Me Anything', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ), - ); - } - - // Dim background when open - Full screen scrim - Widget _buildScrim() { - return Obx( - () => _isOpen.value - ? FadeTransition( - opacity: _opacityAnimation, - child: GestureDetector( - onTap: _toggleOpen, - // 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 - ), - ), - ), - ) - : const SizedBox.shrink(), - ); - } - - @override - Widget build(BuildContext context) { - // 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(), - ), - ], - ), - ); - } - - @override - void dispose() { - _promptController.dispose(); - _scrollController.dispose(); - _animController.dispose(); - super.dispose(); - } -} \ No newline at end of file From b96ed51da2c121d93c383e748a7bff20b3112ba1 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 12 Dec 2025 15:24:37 +0530 Subject: [PATCH 14/26] 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 5789100..ce08c89 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 a6743cfd9bb0fdc5b37047a0dbb1a945612d56d7 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Sat, 13 Dec 2025 14:34:02 +0530 Subject: [PATCH 15/26] 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 c9f4795de6c50e7f198b90522110dd5f4a35e79e Mon Sep 17 00:00:00 2001 From: Manish Date: Sat, 13 Dec 2025 17:21:11 +0530 Subject: [PATCH 16/26] 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 a95e0be48bd3b0473525aac62838498e031daffd Mon Sep 17 00:00:00 2001 From: Manish Date: Mon, 15 Dec 2025 17:14:45 +0530 Subject: [PATCH 17/26] 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 b39cb40..7d94d8b 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: From 874c2cff4dd2633a9a891366632c1eb33a073fc7 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 17 Dec 2025 10:37:48 +0530 Subject: [PATCH 18/26] implemented assign employee feature for infra project module --- lib/helpers/services/api_endpoints.dart | 19 +- lib/helpers/services/api_service.dart | 38 +++ .../assign_project_allocation_request.dart | 25 ++ .../assign_employee_infra_bottom_sheet.dart | 299 ++++++++++++++++++ .../infra_project_details_screen.dart | 30 ++ 5 files changed, 403 insertions(+), 8 deletions(-) create mode 100644 lib/model/infra_project/assign_project_allocation_request.dart create mode 100644 lib/view/infraProject/assign_employee_infra_bottom_sheet.dart diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 7d94d8b..29b41ff 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -5,7 +5,6 @@ class ApiEndpoints { // static const String baseUrl = "https://mapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.onfieldwork.com/api"; - static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = "/Master/expenses-categories"; @@ -48,7 +47,8 @@ class ApiEndpoints { static const String getProjects = "/project/list"; static const String getGlobalProjects = "/project/list/basic"; static const String getTodaysAttendance = "/attendance/project/team"; - static const String getAttendanceForDashboard = "/dashboard/get/attendance/employee/:projectId"; + static const String getAttendanceForDashboard = + "/dashboard/get/attendance/employee/:projectId"; static const String getAttendanceLogs = "/attendance/project/log"; static const String getAttendanceLogView = "/attendance/log/attendance"; static const String getRegularizationLogs = "/attendance/regularize"; @@ -142,7 +142,6 @@ class ApiEndpoints { static const String manageOrganizationHierarchy = "/organization/hierarchy/manage"; - // Service Project Module API Endpoints static const String getServiceProjectsList = "/serviceproject/list"; static const String getServiceProjectDetail = "/serviceproject/details"; @@ -151,10 +150,14 @@ class ApiEndpoints { "/serviceproject/job/details"; static const String editServiceProjectJob = "/serviceproject/job/edit"; static const String createServiceProjectJob = "/serviceproject/job/create"; - static const String serviceProjectUpateJobAttendance = "/serviceproject/job/attendance"; - static const String serviceProjectUpateJobAttendanceLog = "/serviceproject/job/attendance/log"; - static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list"; - static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation"; + static const String serviceProjectUpateJobAttendance = + "/serviceproject/job/attendance"; + static const String serviceProjectUpateJobAttendanceLog = + "/serviceproject/job/attendance/log"; + static const String getServiceProjectUpateJobAllocationList = + "/serviceproject/get/allocation/list"; + static const String manageServiceProjectUpateJobAllocation = + "/serviceproject/manage/allocation"; static const String getTeamRoles = "/master/team-roles/list"; static const String getServiceProjectBranches = "/serviceproject/branch/list"; @@ -168,5 +171,5 @@ class ApiEndpoints { static const String getInfraProjectsList = "/project/list"; static const String getInfraProjectDetail = "/project/details"; static const String getInfraProjectTeamList = "/project/allocation"; - + static const String assignInfraProjectAllocation = "/project/allocation"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index f56341e..618410b 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -52,6 +52,8 @@ import 'package:on_field_work/model/infra_project/infra_project_details.dart'; import 'package:on_field_work/model/dashboard/collection_overview_model.dart'; import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart'; import 'package:on_field_work/model/infra_project/infra_team_list_model.dart'; +import 'package:on_field_work/model/infra_project/assign_project_allocation_request.dart'; + class ApiService { static const bool enableLogs = true; @@ -2008,6 +2010,42 @@ class ApiService { label: "Comment Task", returnFullResponse: true); return parsed != null && parsed['success'] == true; } + + static Future assignEmployeesToProject({ + required List allocations, + }) async { + if (allocations.isEmpty) { + _log( + "No allocations provided for assignEmployeesToProject", + level: LogLevel.error, + ); + return null; + } + + final endpoint = ApiEndpoints.assignInfraProjectAllocation; + final payload = allocations.map((e) => e.toJson()).toList(); + + final response = await _safeApiCall( + endpoint, + method: 'POST', + body: payload, + ); + + if (response == null) return null; + + final parsedJson = _parseAndDecryptResponse( + response, + label: "AssignInfraProjectAllocation", + returnFullResponse: true, + ); + + if (parsedJson == null || parsedJson is! Map) { + return null; + } + + return ProjectAllocationResponse.fromJson(parsedJson); + } + static Future getInfraProjectTeamListApi({ required String projectId, String? serviceId, diff --git a/lib/model/infra_project/assign_project_allocation_request.dart b/lib/model/infra_project/assign_project_allocation_request.dart new file mode 100644 index 0000000..f64b826 --- /dev/null +++ b/lib/model/infra_project/assign_project_allocation_request.dart @@ -0,0 +1,25 @@ +class AssignProjectAllocationRequest { + final String employeeId; + final String projectId; + final String jobRoleId; + final String serviceId; + final bool status; + + AssignProjectAllocationRequest({ + required this.employeeId, + required this.projectId, + required this.jobRoleId, + required this.serviceId, + required this.status, + }); + + Map toJson() { + return { + "employeeId": employeeId, + "projectId": projectId, + "jobRoleId": jobRoleId, + "serviceId": serviceId, + "status": status, + }; + } +} diff --git a/lib/view/infraProject/assign_employee_infra_bottom_sheet.dart b/lib/view/infraProject/assign_employee_infra_bottom_sheet.dart new file mode 100644 index 0000000..fac4332 --- /dev/null +++ b/lib/view/infraProject/assign_employee_infra_bottom_sheet.dart @@ -0,0 +1,299 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'package:on_field_work/controller/tenant/organization_selection_controller.dart'; +import 'package:on_field_work/helpers/widgets/my_spacing.dart'; +import 'package:on_field_work/helpers/widgets/my_text.dart'; +import 'package:on_field_work/helpers/widgets/tenant/organization_selector.dart'; +import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart'; +import 'package:on_field_work/model/employees/employee_model.dart'; +import 'package:on_field_work/model/employees/multiple_select_bottomsheet.dart'; +import 'package:on_field_work/controller/tenant/service_controller.dart'; +import 'package:on_field_work/helpers/widgets/tenant/service_selector.dart'; +import 'package:on_field_work/model/tenant/tenant_services_model.dart'; +import 'package:on_field_work/helpers/services/api_service.dart'; +import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; +import 'package:on_field_work/model/infra_project/assign_project_allocation_request.dart'; + + +class JobRole { + final String id; + final String name; + + JobRole({required this.id, required this.name}); + + factory JobRole.fromJson(Map json) { + return JobRole( + id: json['id'].toString(), + name: json['name'] ?? '', + ); + } +} + +class AssignEmployeeBottomSheet extends StatefulWidget { + final String projectId; + + const AssignEmployeeBottomSheet({ + super.key, + required this.projectId, + }); + + @override + State createState() => + _AssignEmployeeBottomSheetState(); +} + +class _AssignEmployeeBottomSheetState extends State { + late final OrganizationController _organizationController; + late final ServiceController _serviceController; + + final RxList _selectedEmployees = [].obs; + + Organization? _selectedOrganization; + JobRole? _selectedRole; + + final RxBool _isLoadingRoles = false.obs; + final RxList _roles = [].obs; + + @override + void initState() { + super.initState(); + + _organizationController = Get.put( + OrganizationController(), + tag: 'assign_employee_org', + ); + + _serviceController = Get.put( + ServiceController(), + tag: 'assign_employee_service', + ); + + _organizationController.fetchOrganizations(widget.projectId); + _serviceController.fetchServices(widget.projectId); + + _fetchRoles(); + } + + Future _fetchRoles() async { + try { + _isLoadingRoles.value = true; + final res = await ApiService.getRoles(); + if (res != null) { + _roles.assignAll( + res.map((e) => JobRole.fromJson(e)).toList(), + ); + } + } finally { + _isLoadingRoles.value = false; + } + } + + @override + void dispose() { + Get.delete(tag: 'assign_employee_org'); + Get.delete(tag: 'assign_employee_service'); + super.dispose(); + } + + Future _openEmployeeSelector() async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => EmployeeSelectionBottomSheet( + title: 'Select Employee(s)', + multipleSelection: true, + initiallySelected: _selectedEmployees.toList(), + ), + ); + + if (result != null && result is List) { + _selectedEmployees.assignAll(result); + } + } + + void _handleAssign() async { + if (_selectedEmployees.isEmpty || + _selectedRole == null || + _serviceController.selectedService == null) { + Get.snackbar('Error', 'Please complete all selections'); + return; + } + + final allocations = _selectedEmployees + .map( + (e) => AssignProjectAllocationRequest( + employeeId: e.id, + projectId: widget.projectId, + jobRoleId: _selectedRole!.id, + serviceId: _serviceController.selectedService!.id, + status: true, + ), + ) + .toList(); + + final res = await ApiService.assignEmployeesToProject( + allocations: allocations, + ); + + if (res?.success == true) { + Navigator.of(context).pop(true); // ๐Ÿ”ฅ triggers refresh + } else { + Get.snackbar('Error', res?.message ?? 'Assignment failed'); + } + } + + @override + Widget build(BuildContext context) { + return BaseBottomSheet( + title: 'Assign Employee', + submitText: 'Assign', + isSubmitting: false, + onCancel: () => Navigator.of(context).pop(), + onSubmit: _handleAssign, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + //ORGANIZATION + MyText.bodySmall( + 'Organization', + fontWeight: 600, + color: Colors.grey.shade700, + ), + MySpacing.height(6), + + OrganizationSelector( + controller: _organizationController, + height: 44, + onSelectionChanged: (Organization? org) async { + _selectedOrganization = org; + _selectedEmployees.clear(); + _selectedRole = null; + _serviceController.clearSelection(); + }, + ), + + MySpacing.height(20), + + ///EMPLOYEES (SEARCH) + MyText.bodySmall( + 'Employees', + fontWeight: 600, + color: Colors.grey.shade700, + ), + MySpacing.height(6), + + Obx( + () => InkWell( + onTap: _openEmployeeSelector, + child: _dropdownBox( + _selectedEmployees.isEmpty + ? 'Select employee(s)' + : '${_selectedEmployees.length} employee(s) selected', + icon: Icons.search, + ), + ), + ), + + MySpacing.height(20), + + ///SERVICE + MyText.bodySmall( + 'Service', + fontWeight: 600, + color: Colors.grey.shade700, + ), + MySpacing.height(6), + + ServiceSelector( + controller: _serviceController, + height: 44, + onSelectionChanged: (Service? service) async { + _selectedRole = null; + }, + ), + + MySpacing.height(20), + + /// JOB ROLE + MyText.bodySmall( + 'Job Role', + fontWeight: 600, + color: Colors.grey.shade700, + ), + MySpacing.height(6), + + Obx(() { + if (_isLoadingRoles.value) { + return _skeleton(); + } + + return PopupMenuButton( + onSelected: (role) { + _selectedRole = role; + setState(() {}); + }, + itemBuilder: (context) { + if (_roles.isEmpty) { + return const [ + PopupMenuItem( + enabled: false, + child: Text('No roles found'), + ), + ]; + } + return _roles + .map( + (r) => PopupMenuItem( + value: r, + child: Text(r.name), + ), + ) + .toList(); + }, + child: _dropdownBox( + _selectedRole?.name ?? 'Select role', + ), + ); + }), + ], + ), + ); + } + + Widget _dropdownBox(String text, {IconData icon = Icons.arrow_drop_down}) { + return Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + text, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 13), + ), + ), + Icon(icon, color: Colors.grey), + ], + ), + ); + } + + Widget _skeleton() { + return Container( + height: 44, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(12), + ), + ); + } +} diff --git a/lib/view/infraProject/infra_project_details_screen.dart b/lib/view/infraProject/infra_project_details_screen.dart index a6a3c51..f5fc4b1 100644 --- a/lib/view/infraProject/infra_project_details_screen.dart +++ b/lib/view/infraProject/infra_project_details_screen.dart @@ -18,6 +18,8 @@ import 'package:on_field_work/controller/infra_project/infra_project_screen_deta import 'package:on_field_work/view/taskPlanning/daily_progress_report.dart'; import 'package:on_field_work/view/taskPlanning/daily_task_planning.dart'; import 'package:on_field_work/model/infra_project/infra_team_list_model.dart'; +import 'package:on_field_work/view/infraProject/assign_employee_infra_bottom_sheet.dart'; + class InfraProjectDetailsScreen extends StatefulWidget { final String projectId; @@ -83,6 +85,21 @@ class _InfraProjectDetailsScreenState extends State _tabController = TabController(length: _tabs.length, vsync: this); } + void _openAssignEmployeeBottomSheet() async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => AssignEmployeeBottomSheet( + projectId: widget.projectId, + ), + ); + if (result == true) { + controller.fetchProjectTeamList(); + Get.snackbar('Success', 'Employee assigned successfully'); + } + } + @override void dispose() { _tabController.dispose(); @@ -493,6 +510,19 @@ class _InfraProjectDetailsScreenState extends State projectName: widget.projectName, backgroundColor: appBarColor, ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + _openAssignEmployeeBottomSheet(); + }, + backgroundColor: contentTheme.primary, + icon: const Icon(Icons.person_add), + label: MyText( + 'Assign Employee', + fontSize: 14, + color: Colors.white, + fontWeight: 500, + ), + ), body: Stack( children: [ Container( From 8155468cd4b15950efb856429e4bd7625de7a65c Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 17 Dec 2025 12:03:30 +0530 Subject: [PATCH 19/26] UI change in Manage Reporting bottom sheet --- .../manage_reporting_bottom_sheet.dart | 87 +++++++++++++------ 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart index 4aabaf1..3136d5e 100644 --- a/lib/view/employees/manage_reporting_bottom_sheet.dart +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -36,6 +36,10 @@ class _ManageReportingBottomSheetState final TextEditingController _primaryController = TextEditingController(); final TextEditingController _secondaryController = TextEditingController(); + final FocusNode _mainEmployeeFocus = FocusNode(); + final FocusNode _primaryFocus = FocusNode(); + final FocusNode _secondaryFocus = FocusNode(); + final RxList _filteredPrimary = [].obs; final RxList _filteredSecondary = [].obs; final RxList _selectedPrimary = [].obs; @@ -69,6 +73,10 @@ class _ManageReportingBottomSheetState @override void dispose() { + _mainEmployeeFocus.dispose(); + _primaryFocus.dispose(); + _secondaryFocus.dispose(); + _primaryController.dispose(); _secondaryController.dispose(); _selectEmployeeController.dispose(); @@ -368,6 +376,7 @@ class _ManageReportingBottomSheetState _buildSearchSection( label: "Primary Reporting Manager*", controller: _primaryController, + focusNode: _primaryFocus, filteredList: _filteredPrimary, selectedList: _selectedPrimary, isPrimary: true, @@ -379,6 +388,7 @@ class _ManageReportingBottomSheetState _buildSearchSection( label: "Secondary Reporting Manager", controller: _secondaryController, + focusNode: _secondaryFocus, filteredList: _filteredSecondary, selectedList: _selectedSecondary, isPrimary: false, @@ -390,8 +400,10 @@ class _ManageReportingBottomSheetState final safeWrappedContent = SafeArea( child: SingleChildScrollView( padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewPadding.bottom + 20, - left: 16, right: 16, top: 8, + bottom: MediaQuery.of(context).viewPadding.bottom + 20, + left: 16, + right: 16, + top: 8, ), child: content, ), @@ -417,7 +429,7 @@ class _ManageReportingBottomSheetState isSubmitting: _isSubmitting, onCancel: _handleCancel, onSubmit: _handleSubmit, - child: safeWrappedContent, + child: safeWrappedContent, ); } @@ -449,6 +461,7 @@ class _ManageReportingBottomSheetState Widget _buildSearchSection({ required String label, required TextEditingController controller, + required FocusNode focusNode, required RxList filteredList, required RxList selectedList, required bool isPrimary, @@ -459,20 +472,10 @@ class _ManageReportingBottomSheetState MyText.bodyMedium(label, fontWeight: 600), MySpacing.height(8), - // Search field - TextField( + _searchBar( controller: controller, - decoration: InputDecoration( - hintText: "Type to search employees...", - isDense: true, - filled: true, - fillColor: Colors.grey[50], - prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - ), + focusNode: focusNode, + hint: "Type to search employees...", ), // Dropdown suggestions @@ -567,19 +570,10 @@ class _ManageReportingBottomSheetState children: [ MyText.bodyMedium("Select Employee *", fontWeight: 600), MySpacing.height(8), - TextField( + _searchBar( controller: _selectEmployeeController, - decoration: InputDecoration( - hintText: "Type to search employee...", - isDense: true, - filled: true, - fillColor: Colors.grey[50], - prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - ), + focusNode: _mainEmployeeFocus, + hint: "Type to search employee...", ), Obx(() { if (_filteredEmployees.isEmpty) return const SizedBox.shrink(); @@ -641,4 +635,41 @@ class _ManageReportingBottomSheetState ], ); } + + Widget _searchBar({ + required TextEditingController controller, + required FocusNode focusNode, + required String hint, + }) { + return GestureDetector( + onTap: () => focusNode.requestFocus(), + child: Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + color: Colors.white, + ), + child: Row( + children: [ + const Icon(Icons.search, size: 18, color: Colors.grey), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: controller, + focusNode: focusNode, + decoration: InputDecoration( + hintText: hint, + border: InputBorder.none, + isDense: true, + ), + style: const TextStyle(fontSize: 13), + ), + ), + ], + ), + ), + ); + } } From 4d012b78f7480becab8ef2f6053321f76dc3ea27 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 17 Dec 2025 15:11:27 +0530 Subject: [PATCH 20/26] removed gemini bot feature --- lib/helpers/services/gemini_service.dart | 36 -- lib/view/dashboard/dashboard_screen.dart | 6 - lib/view/dashboard/gemini_chat_card.dart | 574 ----------------------- 3 files changed, 616 deletions(-) delete mode 100644 lib/helpers/services/gemini_service.dart delete mode 100644 lib/view/dashboard/gemini_chat_card.dart diff --git a/lib/helpers/services/gemini_service.dart b/lib/helpers/services/gemini_service.dart deleted file mode 100644 index dcaf907..0000000 --- a/lib/helpers/services/gemini_service.dart +++ /dev/null @@ -1,36 +0,0 @@ -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 832cf42..dff0b48 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -20,7 +20,6 @@ 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}); @@ -604,11 +603,6 @@ Widget build(BuildContext context) { ], ), ), - - // Floating Gemini assistant overlay - GeminiFloatingAssistant( - dashboardController: dashboardController, - ), ], ), ), diff --git a/lib/view/dashboard/gemini_chat_card.dart b/lib/view/dashboard/gemini_chat_card.dart deleted file mode 100644 index be05c70..0000000 --- a/lib/view/dashboard/gemini_chat_card.dart +++ /dev/null @@ -1,574 +0,0 @@ -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, - ); - - // 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 - 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 { - // NOTE: Assuming GeminiService.generateText handles the API call - 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(); - // 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."; - if (placeholderIndex >= 0 && - _chatHistory.length > placeholderIndex) { - _chatHistory.removeAt(placeholderIndex); - } - _isAnalyzingData.value = false; - 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 && !_isLoading.value, - ), - ), - Obx( - () => IconButton( - splashRadius: 18, - iconSize: 18, - onPressed: (_isLoading.value || - _isAnalyzingData.value || - _promptController.text - .trim() - .isEmpty) - ? null - : _handlePromptSubmission, - icon: _isLoading.value && _promptController.text.trim().isNotEmpty // Show loading only for text submission - ? 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, color: Colors.white), - label: const Text( - 'Ask Me Anything', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ), - ); - } - - // Dim background when open - Full screen scrim - Widget _buildScrim() { - return Obx( - () => _isOpen.value - ? FadeTransition( - opacity: _opacityAnimation, - child: GestureDetector( - onTap: _toggleOpen, - // 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 - ), - ), - ), - ) - : const SizedBox.shrink(), - ); - } - - @override - Widget build(BuildContext context) { - // 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(), - ), - ], - ), - ); - } - - @override - void dispose() { - _promptController.dispose(); - _scrollController.dispose(); - _animController.dispose(); - super.dispose(); - } -} \ No newline at end of file From 74377288ebff193def3126ca000f141d8cdfd320 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 12 Dec 2025 15:24:37 +0530 Subject: [PATCH 21/26] added gemini api --- lib/helpers/services/app_initializer.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/helpers/services/app_initializer.dart b/lib/helpers/services/app_initializer.dart index f22542f..6b48339 100644 --- a/lib/helpers/services/app_initializer.dart +++ b/lib/helpers/services/app_initializer.dart @@ -8,7 +8,6 @@ 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 { @@ -73,8 +72,7 @@ Future _setupFirebase() async { // Firebase Core await Firebase.initializeApp(); logSafe("๐Ÿ”ฅ Firebase initialized."); - await GeminiService.initialize(); - logSafe("โœจ Gemini service initialized (Gemini 2.5 Flash)."); + } /// --------------------------------------------------------------------------- From 6b1a64f3b1c60e0b05068541f766bef2e733744c Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 12 Dec 2025 15:24:37 +0530 Subject: [PATCH 22/26] added gemini api --- lib/helpers/services/gemini_service.dart | 36 ++ lib/view/dashboard/gemini_chat_card.dart | 548 +++++++++++++++++++++++ 2 files changed, 584 insertions(+) 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/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/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(); + } +} From 42c34ad26a4e8266d7b096e6edaa6a6e7aaaee39 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Sat, 13 Dec 2025 14:34:02 +0530 Subject: [PATCH 23/26] imprroved --- lib/view/dashboard/gemini_chat_card.dart | 70 ++++++++++++++++-------- 1 file changed, 48 insertions(+), 22 deletions(-) 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 2e1b3065df14c287dd2700d0afb583e7b0de7b68 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 17 Dec 2025 15:11:27 +0530 Subject: [PATCH 24/26] removed gemini bot feature --- lib/helpers/services/gemini_service.dart | 36 -- lib/view/dashboard/gemini_chat_card.dart | 574 ----------------------- 2 files changed, 610 deletions(-) delete mode 100644 lib/helpers/services/gemini_service.dart delete mode 100644 lib/view/dashboard/gemini_chat_card.dart diff --git a/lib/helpers/services/gemini_service.dart b/lib/helpers/services/gemini_service.dart deleted file mode 100644 index dcaf907..0000000 --- a/lib/helpers/services/gemini_service.dart +++ /dev/null @@ -1,36 +0,0 @@ -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/gemini_chat_card.dart b/lib/view/dashboard/gemini_chat_card.dart deleted file mode 100644 index be05c70..0000000 --- a/lib/view/dashboard/gemini_chat_card.dart +++ /dev/null @@ -1,574 +0,0 @@ -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, - ); - - // 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 - 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 { - // NOTE: Assuming GeminiService.generateText handles the API call - 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(); - // 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."; - if (placeholderIndex >= 0 && - _chatHistory.length > placeholderIndex) { - _chatHistory.removeAt(placeholderIndex); - } - _isAnalyzingData.value = false; - 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 && !_isLoading.value, - ), - ), - Obx( - () => IconButton( - splashRadius: 18, - iconSize: 18, - onPressed: (_isLoading.value || - _isAnalyzingData.value || - _promptController.text - .trim() - .isEmpty) - ? null - : _handlePromptSubmission, - icon: _isLoading.value && _promptController.text.trim().isNotEmpty // Show loading only for text submission - ? 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, color: Colors.white), - label: const Text( - 'Ask Me Anything', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ), - ); - } - - // Dim background when open - Full screen scrim - Widget _buildScrim() { - return Obx( - () => _isOpen.value - ? FadeTransition( - opacity: _opacityAnimation, - child: GestureDetector( - onTap: _toggleOpen, - // 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 - ), - ), - ), - ) - : const SizedBox.shrink(), - ); - } - - @override - Widget build(BuildContext context) { - // 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(), - ), - ], - ), - ); - } - - @override - void dispose() { - _promptController.dispose(); - _scrollController.dispose(); - _animController.dispose(); - super.dispose(); - } -} \ No newline at end of file From 48a4eb2ca7d6fb0abfa904917bb544c91b6ffc32 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 17 Dec 2025 17:31:04 +0530 Subject: [PATCH 25/26] swiched to intial build.gradle file --- android/app/build.gradle | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2643465..5242a55 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -48,30 +48,32 @@ android { // Define signing configurations for different build types signingConfigs { - release { - if (keystorePropertiesFile.exists()) { + release { + // Reference the key alias from key.properties 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 { - if (keystorePropertiesFile.exists()) { + buildTypes { + release { + // Apply the 'release' signing configuration defined above to the release build 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 73bd5cdc92e4df33558777832ef8d60c126fd00c Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 17 Dec 2025 17:43:02 +0530 Subject: [PATCH 26/26] removed firebase_ai package from pubsec.yaml --- pubspec.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index ce08c89..5789100 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.2.1 + firebase_core: ^4.0.0 firebase_messaging: ^16.0.0 googleapis_auth: ^2.0.0 device_info_plus: ^12.3.0 @@ -88,7 +88,6 @@ 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: