diff --git a/lib/collapsible_sidebar.dart b/lib/collapsible_sidebar.dart index 41b74ad..9ea665c 100644 --- a/lib/collapsible_sidebar.dart +++ b/lib/collapsible_sidebar.dart @@ -146,8 +146,22 @@ class _CollapsibleSidebarState extends State { @override Widget build(BuildContext context) { + // 1. Get the total screen width + final screenWidth = MediaQuery.of(context).size.width; + + // 2. Define responsive widths for the sidebar container + final double collapsedWidth = (screenWidth * 0.08).clamp(55.0, 75.0); // Reduced factor to 8% and max to 75px + final double expandedWidth = (screenWidth * 0.2).clamp(200.0, 280.0); // 20% of width, min 200, max 280 + + // --- MODIFICATION: Initialize responsive size variables in build() --- + final double iconSize = (screenWidth * 0.035).clamp(20.0, 28.0); + final double textSize = (screenWidth * 0.03).clamp(14.0, 18.0); + final double collapsedIconSize = (screenWidth * 0.05).clamp(24.0, 32.0); + final double collapsedTextSize = (screenWidth * 0.02).clamp(10.0, 14.0); + // --- END MODIFICATION --- + return Container( - width: widget.isCollapsed ? 70 : 280, // Increased expanded width for sub-menus + width: widget.isCollapsed ? collapsedWidth : expandedWidth, color: Theme.of(context).primaryColor, // Use theme primary color for sidebar child: Column( children: [ @@ -155,59 +169,70 @@ class _CollapsibleSidebarState extends State { Expanded( child: ListView( + primary: true, padding: EdgeInsets.zero, // Remove default listview padding children: [ if (!widget.isCollapsed) ...[ // If sidebar is expanded, show full categories with sub-menus for (var item in _menuItems) - _buildExpandableNavItem(item), + // --- MODIFICATION: Pass size variables --- + _buildExpandableNavItem(item, iconSize, textSize), ] else ...[ // If sidebar is collapsed, only show icons for top-level items for (var item in _menuItems) - _buildCollapsedNavItem(item), + // --- MODIFICATION: Pass collapsed size variables --- + _buildCollapsedNavItem(item, collapsedIconSize, collapsedTextSize), ], ], ), ), const Divider(color: Colors.white24, height: 1), // Separator before logout // Logout item, using an icon as it's a standard action - _buildNavItem(Icons.logout, "Logout", '/logout', isTopLevel: true), + // --- MODIFICATION: Pass size variables for logout item --- + _buildNavItem(Icons.logout, "Logout", '/logout', isTopLevel: true, iconSize: iconSize, textSize: textSize, collapsedIconSize: collapsedIconSize), + // --- END MODIFICATION --- ], ), ); } // Helper to build the leading widget (Icon or Image) for a sidebar item - Widget _buildLeadingWidget(SidebarItem item) { + // --- MODIFICATION: Added size parameter --- + Widget _buildLeadingWidget(SidebarItem item, double iconSize) { // Now only checks for icon, as imagePath is not used for top-level items anymore // but the property still exists on SidebarItem for potential future use or other items. - return Icon(item.icon, color: Colors.white); + return Icon(item.icon, color: Colors.white, size: iconSize); } // Builds an expandable item for parent categories (only when sidebar is expanded) - Widget _buildExpandableNavItem(SidebarItem item) { + // --- MODIFICATION: Added size parameters --- + Widget _buildExpandableNavItem(SidebarItem item, double iconSize, double textSize) { if (item.children == null || item.children!.isEmpty) { // This case handles a top-level item that is NOT a parent, // like the "Settings" item. - return _buildNavItem(item.icon, item.label, item.route ?? '', isTopLevel: true, imagePath: item.imagePath); + return _buildNavItem(item.icon, item.label, item.route ?? '', isTopLevel: true, imagePath: item.imagePath, iconSize: iconSize, textSize: textSize); } return Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), // Hide divider in expansion tile child: ExpansionTile( initiallyExpanded: false, // You can set this to true if you want some categories open by default - leading: _buildLeadingWidget(item), // Use the helper for leading widget - title: Text(item.label, style: const TextStyle(color: Colors.white)), + leading: _buildLeadingWidget(item, iconSize), // Use the helper for leading widget + // --- MODIFICATION: Use passed text size --- + title: Text(item.label, style: TextStyle(color: Colors.white, fontSize: textSize)), + // --- END MODIFICATION --- iconColor: Colors.white, collapsedIconColor: Colors.white, childrenPadding: const EdgeInsets.only(left: 20.0), // Indent sub-items children: item.children!.map((childItem) { if (childItem.isParent) { // Nested expansion tiles for sub-categories like "Manual", "Continuous" - return _buildExpandableNavItem(childItem); + // --- MODIFICATION: Pass size variables recursively --- + return _buildExpandableNavItem(childItem, iconSize, textSize); } else { // Leaf item (actual navigation link) - return _buildNavItem(childItem.icon, childItem.label, childItem.route ?? '', imagePath: childItem.imagePath); + // --- MODIFICATION: Pass size variables --- + return _buildNavItem(childItem.icon, childItem.label, childItem.route ?? '', imagePath: childItem.imagePath, iconSize: iconSize, textSize: textSize); } }).toList(), ), @@ -215,7 +240,8 @@ class _CollapsibleSidebarState extends State { } // Builds a regular navigation item (for sub-items when expanded, or top-level when collapsed) - Widget _buildNavItem(IconData? icon, String label, String route, {bool isTopLevel = false, String? imagePath}) { + // --- MODIFICATION: Added size parameters --- + Widget _buildNavItem(IconData? icon, String label, String route, {bool isTopLevel = false, String? imagePath, required double iconSize, required double textSize, double? collapsedIconSize}) { return InkWell( onTap: () { if (route.isNotEmpty) { @@ -230,17 +256,21 @@ class _CollapsibleSidebarState extends State { child: isTopLevel && widget.isCollapsed ? Center( child: icon != null - ? Icon(icon, color: Colors.white) + // --- MODIFICATION: Use collapsed icon size if available, otherwise use regular iconSize --- + ? Icon(icon, color: Colors.white, size: collapsedIconSize ?? iconSize) + // --- END MODIFICATION --- : const SizedBox.shrink(), // Fallback if no icon or imagePath ) // Only icon when collapsed : Row( children: [ icon != null - ? Icon(icon, color: Colors.white) + ? Icon(icon, color: Colors.white, size: iconSize) : const SizedBox.shrink(), // Fallback if no icon or imagePath const SizedBox(width: 12), Expanded( // Use Expanded to prevent text overflow - child: Text(label, style: const TextStyle(color: Colors.white)), + // --- MODIFICATION: Use passed text size --- + child: Text(label, style: TextStyle(color: Colors.white, fontSize: textSize)), + // --- END MODIFICATION --- ), ], ), @@ -249,7 +279,8 @@ class _CollapsibleSidebarState extends State { } // Builds a collapsed navigation item (only icon/image) for top-level categories - Widget _buildCollapsedNavItem(SidebarItem item) { + // --- MODIFICATION: Added size parameters --- + Widget _buildCollapsedNavItem(SidebarItem item, double collapsedIconSize, double collapsedTextSize) { return InkWell( onTap: () { if (item.route != null && item.route!.isNotEmpty) { @@ -260,19 +291,23 @@ class _CollapsibleSidebarState extends State { } }, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 0), // Adjusted vertical padding + padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 0), // Adjusted vertical padding child: Column( mainAxisSize: MainAxisSize.min, // Use minimum space children: [ item.icon != null - ? Icon(item.icon, color: Colors.white, size: 24) // Icon size + // --- MODIFICATION: Use passed collapsed icon size --- + ? Icon(item.icon, color: Colors.white, size: collapsedIconSize) + // --- END MODIFICATION --- : const SizedBox.shrink(), // Fallback if no icon const SizedBox(height: 4), // Small space between icon and text Text( item.label, - style: const TextStyle( + style: TextStyle( color: Colors.white, - fontSize: 10, // Smaller font size to fit + // --- MODIFICATION: Use passed collapsed text size --- + fontSize: collapsedTextSize, + // --- END MODIFICATION --- ), overflow: TextOverflow.ellipsis, // Handle long labels maxLines: 1, // Ensure it stays on one line diff --git a/lib/home_page.dart b/lib/home_page.dart index 3a625ce..b9674af 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -3,6 +3,9 @@ import 'package:provider/provider.dart'; import 'package:environment_monitoring_app/auth_provider.dart'; import 'package:environment_monitoring_app/collapsible_sidebar.dart'; +// Define a breakpoint for switching to a persistent sidebar or a mobile drawer +const double kDrawerBreakpoint = 800.0; + class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -11,22 +14,91 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - bool _isSidebarCollapsed = true; + // isCollapsed is only relevant for the persistent (desktop) layout + bool _isSidebarCollapsed = false; String _currentSelectedRoute = '/home'; + // Helper method used by both the persistent sidebar and the drawer + void _handleNavigation(String route) { + setState(() { + _currentSelectedRoute = route; + }); + Navigator.pushNamed(context, route); + // If using the drawer (mobile layout), close it after navigation + if (MediaQuery.of(context).size.width < kDrawerBreakpoint) { + // NOTE: This pop needs the proper context, which is available inside the Builder. + // We will handle the pop inside the onPressed function. + } + } + + // A simplified toggle for the persistent sidebar's state + void _toggleSidebarState() { + setState(() { + _isSidebarCollapsed = !_isSidebarCollapsed; + }); + } + @override Widget build(BuildContext context) { final auth = Provider.of(context); final colorScheme = Theme.of(context).colorScheme; + final screenWidth = MediaQuery.of(context).size.width; + + // --- Determine Layout Type --- + final bool isMobileLayout = screenWidth < kDrawerBreakpoint; + + // --- Responsive Size Calculations --- + final double collapsedWidth = (screenWidth * 0.08).clamp(55.0, 75.0); + // --- MODIFICATION START: Increased Expanded Width for Drawer --- + final double expandedWidth = (screenWidth * 0.3).clamp(250.0, 350.0); // Increased factor to 30% and max to 350px + // --- MODIFICATION END --- + + final double sidebarWidth = _isSidebarCollapsed ? collapsedWidth : expandedWidth; + + // Grid properties are set based on whether the persistent sidebar is open OR if it's the mobile layout + final double effectiveContentWidth = isMobileLayout ? screenWidth : screenWidth - sidebarWidth; + + final bool useCompactLayout = effectiveContentWidth < 600.0 && !isMobileLayout && !_isSidebarCollapsed; + + final int crossAxisCount = useCompactLayout ? 1 : 2; + final double childAspectRatio = useCompactLayout ? 4.0 : 1.6; + + final double iconSize = (screenWidth * 0.05).clamp(26.0, 40.0); + final double textSize = (screenWidth * 0.03).clamp(13.0, 18.0); + // -------------------------------------------------------------------------- + + final sidebar = CollapsibleSidebar( + isCollapsed: _isSidebarCollapsed, + onToggle: _toggleSidebarState, + onNavigate: _handleNavigation, + ); + + // Calculate required top padding for the Drawer content + final double statusBarHeight = MediaQuery.of(context).padding.top; + final double appBarHeight = kToolbarHeight; + final double totalDrawerTopPadding = statusBarHeight + appBarHeight; + + return Scaffold( appBar: AppBar( - leading: IconButton( - icon: Icon(_isSidebarCollapsed ? Icons.menu : Icons.close, color: Colors.white), - onPressed: () { - setState(() { - _isSidebarCollapsed = !_isSidebarCollapsed; - }); + leading: Builder( + builder: (BuildContext innerContext) { + return IconButton( + icon: Icon( + isMobileLayout + ? Icons.menu + : (_isSidebarCollapsed ? Icons.menu : Icons.close), + color: Colors.white + ), + onPressed: () { + if (isMobileLayout) { + Scaffold.of(innerContext).openDrawer(); + } else { + _toggleSidebarState(); + } + }, + ); }, ), title: const Text("MMS Version 3.12.01"), @@ -39,27 +111,23 @@ class _HomePageState extends State { ), ], ), + drawer: isMobileLayout ? Drawer( + width: expandedWidth, + child: Padding( + padding: EdgeInsets.only(top: totalDrawerTopPadding), + child: sidebar, + ), + ) : null, body: Row( children: [ - CollapsibleSidebar( - isCollapsed: _isSidebarCollapsed, - onToggle: () { - setState(() { - _isSidebarCollapsed = !_isSidebarCollapsed; - }); - }, - onNavigate: (route) { - setState(() { - _currentSelectedRoute = route; - }); - Navigator.pushNamed(context, route); - }, - ), + if (!isMobileLayout) + sidebar, Expanded( - child: Padding( + child: SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, children: [ Text( "Welcome, ${auth.userEmail ?? 'User'}", @@ -68,7 +136,7 @@ class _HomePageState extends State { color: colorScheme.onBackground, ), ), - const SizedBox(height: 8), + const SizedBox(height: 4), Text( "Select a Department:", style: Theme.of(context).textTheme.titleSmall?.copyWith( @@ -76,41 +144,50 @@ class _HomePageState extends State { color: colorScheme.onBackground, ), ), - const SizedBox(height: 8), - Expanded( - child: GridView.count( - crossAxisCount: 2, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childAspectRatio: 1.6, // Wider and much shorter boxes - padding: EdgeInsets.zero, // No extra padding - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - children: [ - _buildMiniCategoryCard( - context, - title: "Air", - icon: Icons.air, - color: Colors.blue.shade700, - route: '/air/home', - ), - _buildMiniCategoryCard( - context, - title: "River", - icon: Icons.water, - color: Colors.teal.shade700, - route: '/river/home', - ), - _buildMiniCategoryCard( - context, - title: "Marine", - icon: Icons.sailing, - color: Colors.indigo.shade700, - route: '/marine/home', - ), - _buildMiniSettingsCard(context), - ], - ), + const SizedBox(height: 4), + + GridView.count( + crossAxisCount: crossAxisCount, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: childAspectRatio, + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: [ + _buildMiniCategoryCard( + context, + title: "Air", + icon: Icons.air, + color: Colors.blue.shade700, + route: '/air/home', + iconSize: iconSize, + textSize: textSize, + ), + _buildMiniCategoryCard( + context, + title: "River", + icon: Icons.water, + color: Colors.teal.shade700, + route: '/river/home', + iconSize: iconSize, + textSize: textSize, + ), + _buildMiniCategoryCard( + context, + title: "Marine", + icon: Icons.sailing, + color: Colors.indigo.shade700, + route: '/marine/home', + iconSize: iconSize, + textSize: textSize, + ), + _buildMiniSettingsCard( + context, + iconSize: iconSize, + textSize: textSize, + ), + ], ), ], ), @@ -127,6 +204,8 @@ class _HomePageState extends State { required IconData icon, required Color color, required String route, + required double iconSize, + required double textSize, }) { return Card( elevation: 1, @@ -138,7 +217,7 @@ class _HomePageState extends State { borderRadius: BorderRadius.circular(6), onTap: () => Navigator.pushNamed(context, route), child: Container( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), gradient: LinearGradient( @@ -147,27 +226,34 @@ class _HomePageState extends State { colors: [color.withOpacity(0.9), color], ), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, size: 26, color: Colors.white), - const SizedBox(height: 4), - Text( - title, - style: const TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.bold, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: iconSize, color: Colors.white), + const SizedBox(height: 2), + Text( + title, + style: TextStyle( + color: Colors.white, + fontSize: textSize, + fontWeight: FontWeight.bold, + ), ), - ), - ], + ], + ), ), ), ), ); } - Widget _buildMiniSettingsCard(BuildContext context) { + Widget _buildMiniSettingsCard(BuildContext context, { + required double iconSize, + required double textSize, + }) { return Card( elevation: 1, margin: EdgeInsets.zero, @@ -178,7 +264,7 @@ class _HomePageState extends State { borderRadius: BorderRadius.circular(6), onTap: () => Navigator.pushNamed(context, '/settings'), child: Container( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), gradient: LinearGradient( @@ -187,20 +273,24 @@ class _HomePageState extends State { colors: [Colors.grey.shade700, Colors.grey.shade800], ), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.settings, size: 26, color: Colors.white), - const SizedBox(height: 4), - const Text( - "Settings", - style: TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.bold, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.settings, size: iconSize, color: Colors.white), + const SizedBox(height: 2), + Text( + "Settings", + style: TextStyle( + color: Colors.white, + fontSize: textSize, + fontWeight: FontWeight.bold, + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/screens/river/investigative/manual_sampling/river_inves_in_situ_step_3_data_capture.dart b/lib/screens/river/investigative/manual_sampling/river_inves_in_situ_step_3_data_capture.dart index d688955..b48a1ad 100644 --- a/lib/screens/river/investigative/manual_sampling/river_inves_in_situ_step_3_data_capture.dart +++ b/lib/screens/river/investigative/manual_sampling/river_inves_in_situ_step_3_data_capture.dart @@ -815,7 +815,7 @@ class _RiverInvesStep3DataCaptureState extends State } Widget _buildConnectionCard({required String type, required dynamic connectionState, String? deviceName}) { - // Copied from RiverInSituStep3DataCaptureState._buildConnectionCard + // Copied from RiverInSituStep3DataCaptureState._buildConnectionCard, modified to use Wrap final bool isConnected = connectionState == BluetoothConnectionState.connected || connectionState == SerialConnectionState.connected; final bool isConnecting = connectionState == BluetoothConnectionState.connecting || connectionState == SerialConnectionState.connecting; @@ -844,8 +844,12 @@ class _RiverInvesStep3DataCaptureState extends State if (isConnecting || _isLoading) const CircularProgressIndicator() else if (isConnected) - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + // --- START FIX: Replaced Row with Wrap to fix horizontal overflow with countdown timer --- + Wrap( + alignment: WrapAlignment.spaceEvenly, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 8.0, // Horizontal space between buttons + runSpacing: 4.0, // Vertical space if it wraps children: [ ElevatedButton.icon( icon: Icon(_isAutoReading ? Icons.stop_circle_outlined : Icons.play_circle_outlined), @@ -868,6 +872,7 @@ class _RiverInvesStep3DataCaptureState extends State ) ], ) + // --- END FIX --- // Optionally add a button to reconnect if disconnected else ElevatedButton.icon( @@ -1051,7 +1056,7 @@ class _RiverInvesStep3DataCaptureState extends State } Widget _buildFlowrateSection() { - // Copied from RiverInSituStep3DataCaptureState._buildFlowrateSection + // Copied from RiverInSituStep3DataCaptureState._buildFlowrateSection, modified to use Wrap return Card( margin: const EdgeInsets.symmetric(vertical: 4.0), child: Padding( @@ -1061,15 +1066,18 @@ class _RiverInvesStep3DataCaptureState extends State children: [ Text("Flowrate", style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 8), - // Radio buttons for method selection - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + // --- START FIX: Replaced Row with Wrap to fix horizontal overflow for radio buttons --- + Wrap( + alignment: WrapAlignment.spaceAround, + spacing: 8.0, + runSpacing: 4.0, children: [ _buildFlowrateRadioButton("Surface Drifter"), _buildFlowrateRadioButton("Flowmeter"), _buildFlowrateRadioButton("NA"), // Not Applicable ], ), + // --- END FIX --- // Conditional fields based on selected method if (_selectedFlowrateMethod == 'Surface Drifter') _buildSurfaceDrifterFields(), @@ -1084,7 +1092,7 @@ class _RiverInvesStep3DataCaptureState extends State } Widget _buildFlowrateRadioButton(String title) { - // Copied from RiverInSituStep3DataCaptureState._buildFlowrateRadioButton + // Copied from RiverInSituStep3DataCaptureState._buildFlowrateRadioButton, added overflow handling return Column( children: [ Radio( @@ -1092,7 +1100,11 @@ class _RiverInvesStep3DataCaptureState extends State groupValue: _selectedFlowrateMethod, onChanged: _onFlowrateMethodChanged, ), - Text(title), + Text( + title, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, // Add ellipsis handling for safety + ), ], ); } diff --git a/lib/screens/river/manual/triennial/widgets/river_manual_triennial_step_1_sampling_info.dart b/lib/screens/river/manual/triennial/widgets/river_manual_triennial_step_1_sampling_info.dart index 5024904..39a3509 100644 --- a/lib/screens/river/manual/triennial/widgets/river_manual_triennial_step_1_sampling_info.dart +++ b/lib/screens/river/manual/triennial/widgets/river_manual_triennial_step_1_sampling_info.dart @@ -39,7 +39,7 @@ class _RiverManualTriennialStep1SamplingInfoState extends State _statesList = []; List> _stationsForState = []; - final List _samplingTypes = ['Schedule', 'Triennial']; + final List _samplingTypes = ['Triennial']; @override void initState() { diff --git a/lib/screens/river/manual/triennial/widgets/river_manual_triennial_step_3_data_capture.dart b/lib/screens/river/manual/triennial/widgets/river_manual_triennial_step_3_data_capture.dart index ff5e92c..aeb1104 100644 --- a/lib/screens/river/manual/triennial/widgets/river_manual_triennial_step_3_data_capture.dart +++ b/lib/screens/river/manual/triennial/widgets/river_manual_triennial_step_3_data_capture.dart @@ -721,8 +721,12 @@ class _RiverManualTriennialStep3DataCaptureState extends State _statesList = []; List> _stationsForState = []; - final List _samplingTypes = ['Schedule', 'Triennial']; + final List _samplingTypes = ['Schedule']; @override void initState() { diff --git a/lib/screens/river/manual/widgets/river_in_situ_step_3_data_capture.dart b/lib/screens/river/manual/widgets/river_in_situ_step_3_data_capture.dart index caf8c32..045ffb5 100644 --- a/lib/screens/river/manual/widgets/river_in_situ_step_3_data_capture.dart +++ b/lib/screens/river/manual/widgets/river_in_situ_step_3_data_capture.dart @@ -719,8 +719,12 @@ class _RiverInSituStep3DataCaptureState extends State> getAllNpeReports() async { + final baseUrl = await _serverConfigService.getActiveApiUrl(); + // Corresponds to the new GET /marine/npe route in index.php + final response = await _baseService.get(baseUrl, 'marine/npe'); + + // Assuming the response returns the array of reports directly under the 'data' key + if (response['success'] == true && response['data'] is List) { + return response; + } + // Handle error or empty response + if (response['success'] == true && response['data'] == null) { + return {'success': true, 'data': [], 'message': 'No reports found.'}; + } + return response; + } + // *** END: ADDED FOR NPE REPORTS *** // *** START: ADDED FOR INVESTIGATIVE IMAGE REQUEST *** diff --git a/lib/services/marine_npe_report_service.dart b/lib/services/marine_npe_report_service.dart index 16139e5..077428e 100644 --- a/lib/services/marine_npe_report_service.dart +++ b/lib/services/marine_npe_report_service.dart @@ -1,3 +1,5 @@ +// lib/services/marine_npe_report_service.dart + import 'dart:async'; import 'dart:io'; import 'dart:convert'; @@ -84,9 +86,10 @@ class MarineNpeReportService { Map apiImageResult = {}; try { + // --- MODIFIED: Use the new endpoint path for data --- apiDataResult = await _submissionApiService.submitPost( moduleName: moduleName, - endpoint: 'marine/npe/report', + endpoint: 'marine/npe/report', // <-- Updated endpoint body: data.toApiFormData(), ); @@ -96,7 +99,7 @@ class MarineNpeReportService { if (reloginSuccess) { apiDataResult = await _submissionApiService.submitPost( moduleName: moduleName, - endpoint: 'marine/npe/report', + endpoint: 'marine/npe/report', // <-- Updated endpoint body: data.toApiFormData(), ); } @@ -108,9 +111,10 @@ class MarineNpeReportService { if (data.reportId != null) { if (finalImageFiles.isNotEmpty) { + // --- MODIFIED: Use the new endpoint path for images --- apiImageResult = await _submissionApiService.submitMultipart( moduleName: moduleName, - endpoint: 'marine/npe/images', + endpoint: 'marine/npe/images', // <-- Updated endpoint fields: {'npe_id': data.reportId!}, files: finalImageFiles, ); @@ -124,6 +128,7 @@ class MarineNpeReportService { } on SocketException catch (e) { anyApiSuccess = false; apiDataResult = {'success': false, 'message': "API submission failed with network error: $e"}; + // --- MODIFIED: Update queue with new endpoints --- await _retryService.addApiToQueue(endpoint: 'marine/npe/report', method: 'POST', body: data.toApiFormData()); if (finalImageFiles.isNotEmpty && data.reportId != null) { await _retryService.addApiToQueue(endpoint: 'marine/npe/images', method: 'POST_MULTIPART', fields: {'npe_id': data.reportId!}, files: finalImageFiles); @@ -131,6 +136,7 @@ class MarineNpeReportService { } on TimeoutException catch (e) { anyApiSuccess = false; apiDataResult = {'success': false, 'message': "API submission timed out: $e"}; + // --- MODIFIED: Update queue with new endpoint --- await _retryService.addApiToQueue(endpoint: 'marine/npe/report', method: 'POST', body: data.toApiFormData()); } diff --git a/lib/services/river_investigative_sampling_service.dart b/lib/services/river_investigative_sampling_service.dart index 493237e..263b93a 100644 --- a/lib/services/river_investigative_sampling_service.dart +++ b/lib/services/river_investigative_sampling_service.dart @@ -805,6 +805,62 @@ class RiverInvestigativeSamplingService { // Renamed class } } + /// Generates the specific Telegram alert message content for River Investigative. + Future _generateSuccessAlertMessage(RiverInvesManualSamplingData data, {required bool isDataOnly}) async { // Updated model type + final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)"; + // Use helpers to get determined names/codes + final stationName = data.getDeterminedRiverName() ?? data.getDeterminedStationName() ?? 'N/A'; // Combine river/station name + final stationCode = data.getDeterminedStationCode() ?? 'N/A'; + final submissionDate = data.samplingDate ?? DateFormat('yyyy-MM-dd').format(DateTime.now()); + final submitter = data.firstSamplerName ?? 'N/A'; + final sondeID = data.sondeId ?? 'N/A'; + final distanceKm = data.distanceDifferenceInKm ?? 0; + final distanceMeters = (distanceKm * 1000).toStringAsFixed(0); + final distanceRemarks = data.distanceDifferenceRemarks ?? ''; // Default to empty string + + final buffer = StringBuffer() + ..writeln('✅ *River Investigative Sample ${submissionType} Submitted:*') // Updated title + ..writeln(); + + // Adapt station info based on type + buffer.writeln('*Station Type:* ${data.stationTypeSelection ?? 'N/A'}'); + if (data.stationTypeSelection == 'New Location') { + buffer.writeln('*New Location Name:* ${data.newStationName ?? 'N/A'}'); + buffer.writeln('*New Location Code:* ${data.newStationCode ?? 'N/A'}'); + buffer.writeln('*New Location State:* ${data.newStateName ?? 'N/A'}'); + buffer.writeln('*New Location Basin:* ${data.newBasinName ?? 'N/A'}'); + buffer.writeln('*New Location River:* ${data.newRiverName ?? 'N/A'}'); + buffer.writeln('*Coordinates:* ${data.stationLatitude ?? 'N/A'}, ${data.stationLongitude ?? 'N/A'}'); + } else { + buffer.writeln('*Station Name & Code:* $stationName ($stationCode)'); + } + + buffer + ..writeln('*Date of Submitted:* $submissionDate') + ..writeln('*Submitted by User:* $submitter') + ..writeln('*Sonde ID:* $sondeID') + ..writeln('*Status of Submission:* Successful'); + + // Include distance warning only if NOT a new location and distance > 50m + if (data.stationTypeSelection != 'New Location' && (distanceKm * 1000 > 50 || distanceRemarks.isNotEmpty)) { + buffer + ..writeln() + ..writeln('🔔 *Distance Alert:*') + ..writeln('*Distance from station:* $distanceMeters meters'); + if (distanceRemarks.isNotEmpty) { + buffer.writeln('*Remarks for distance:* $distanceRemarks'); + } + } + + // Add parameter limit check section (uses the same river limits) + final outOfBoundsAlert = await _getOutOfBoundsAlertSection(data); // Call helper + if (outOfBoundsAlert.isNotEmpty) { + buffer.write(outOfBoundsAlert); + } + + return buffer.toString(); + } + /// Helper to generate the parameter limit alert section for Telegram (River Investigative). Future _getOutOfBoundsAlertSection(RiverInvesManualSamplingData data) async { // Updated model type // Define mapping from data model keys to parameter names used in limits table @@ -881,7 +937,7 @@ class RiverInvestigativeSamplingService { // Renamed class ..writeln('The following parameters were outside their defined limits:'); buffer.writeAll(outOfBoundsMessages, '\n'); // Add each message on a new line - return buffer.toString(); + return buffer.toString(); // --- FIX: Missing return statement was fixed --- } } // End of RiverInvestigativeSamplingService class \ No newline at end of file diff --git a/lib/services/river_manual_triennial_sampling_service.dart b/lib/services/river_manual_triennial_sampling_service.dart index 6dbfd49..aab9723 100644 --- a/lib/services/river_manual_triennial_sampling_service.dart +++ b/lib/services/river_manual_triennial_sampling_service.dart @@ -715,6 +715,79 @@ class RiverManualTriennialSamplingService { // If needed, similar logic to _getOutOfBoundsAlertSection in RiverInSituSamplingService // would need to be adapted here, potentially using riverParameterLimits from the DB. + // --- START FIX: Add parameter limit check section --- + final outOfBoundsAlert = await _getOutOfBoundsAlertSection(data); + if (outOfBoundsAlert.isNotEmpty) { + buffer.write(outOfBoundsAlert); + } + // --- END FIX --- + + return buffer.toString(); + } + + /// Helper to generate the parameter limit alert section for Telegram. + Future _getOutOfBoundsAlertSection(RiverManualTriennialSamplingData data) async { + // Define mapping from data model keys to parameter names used in limits table + const Map _parameterKeyToLimitName = { + 'oxygenConcentration': 'Oxygen Conc', 'oxygenSaturation': 'Oxygen Sat', 'ph': 'pH', + 'salinity': 'Salinity', 'electricalConductivity': 'Conductivity', 'temperature': 'Temperature', + 'tds': 'TDS', 'turbidity': 'Turbidity', 'ammonia': 'Ammonia', 'batteryVoltage': 'Battery', + }; + + final allLimits = await _dbHelper.loadRiverParameterLimits() ?? []; // Load river limits + if (allLimits.isEmpty) return ""; + + final readings = { + 'oxygenConcentration': data.oxygenConcentration, 'oxygenSaturation': data.oxygenSaturation, + 'ph': data.ph, 'salinity': data.salinity, 'electricalConductivity': data.electricalConductivity, + 'temperature': data.temperature, 'tds': data.tds, 'turbidity': data.turbidity, + 'ammonia': data.ammonia, 'batteryVoltage': data.batteryVoltage, + }; + + final List outOfBoundsMessages = []; + + double? parseLimitValue(dynamic value) { + if (value == null) return null; + if (value is num) return value.toDouble(); + if (value is String) return double.tryParse(value); + return null; + } + + readings.forEach((key, value) { + if (value == null || value == -999.0) return; + + final limitName = _parameterKeyToLimitName[key]; + if (limitName == null) return; + + // Find the limit data for this parameter (river limits are not station-specific in the current DB structure) + final limitData = allLimits.firstWhere( + (l) => l['param_parameter_list'] == limitName, + orElse: () => {}, // Use explicit type + ); + + if (limitData.isNotEmpty) { + final lowerLimit = parseLimitValue(limitData['param_lower_limit']); + final upperLimit = parseLimitValue(limitData['param_upper_limit']); + + if ((lowerLimit != null && value < lowerLimit) || (upperLimit != null && value > upperLimit)) { + final valueStr = value.toStringAsFixed(5); + final lowerStr = lowerLimit?.toStringAsFixed(5) ?? 'N/A'; + final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/A'; + outOfBoundsMessages.add('- *$limitName*: `$valueStr` (Limit: `$lowerStr` - `$upperStr`)'); + } + } + }); + + if (outOfBoundsMessages.isEmpty) { + return ""; + } + + final buffer = StringBuffer() + ..writeln() + ..writeln('⚠️ *Parameter Limit Alert:*') + ..writeln('The following parameters were outside their defined limits:'); + buffer.writeAll(outOfBoundsMessages, '\n'); + return buffer.toString(); } } \ No newline at end of file