repair river investigative and triennial ftp format

This commit is contained in:
ALim Aidrus 2025-11-20 10:21:01 +08:00
parent 18e853ac83
commit cf22668576
11 changed files with 450 additions and 135 deletions

View File

@ -146,8 +146,22 @@ class _CollapsibleSidebarState extends State<CollapsibleSidebar> {
@override @override
Widget build(BuildContext context) { 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( 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 color: Theme.of(context).primaryColor, // Use theme primary color for sidebar
child: Column( child: Column(
children: [ children: [
@ -155,59 +169,70 @@ class _CollapsibleSidebarState extends State<CollapsibleSidebar> {
Expanded( Expanded(
child: ListView( child: ListView(
primary: true,
padding: EdgeInsets.zero, // Remove default listview padding padding: EdgeInsets.zero, // Remove default listview padding
children: [ children: [
if (!widget.isCollapsed) ...[ if (!widget.isCollapsed) ...[
// If sidebar is expanded, show full categories with sub-menus // If sidebar is expanded, show full categories with sub-menus
for (var item in _menuItems) for (var item in _menuItems)
_buildExpandableNavItem(item), // --- MODIFICATION: Pass size variables ---
_buildExpandableNavItem(item, iconSize, textSize),
] else ...[ ] else ...[
// If sidebar is collapsed, only show icons for top-level items // If sidebar is collapsed, only show icons for top-level items
for (var item in _menuItems) 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 const Divider(color: Colors.white24, height: 1), // Separator before logout
// Logout item, using an icon as it's a standard action // 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 // 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 // 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. // 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) // 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) { if (item.children == null || item.children!.isEmpty) {
// This case handles a top-level item that is NOT a parent, // This case handles a top-level item that is NOT a parent,
// like the "Settings" item. // 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( return Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent), // Hide divider in expansion tile data: Theme.of(context).copyWith(dividerColor: Colors.transparent), // Hide divider in expansion tile
child: ExpansionTile( child: ExpansionTile(
initiallyExpanded: false, // You can set this to true if you want some categories open by default 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 leading: _buildLeadingWidget(item, iconSize), // Use the helper for leading widget
title: Text(item.label, style: const TextStyle(color: Colors.white)), // --- MODIFICATION: Use passed text size ---
title: Text(item.label, style: TextStyle(color: Colors.white, fontSize: textSize)),
// --- END MODIFICATION ---
iconColor: Colors.white, iconColor: Colors.white,
collapsedIconColor: Colors.white, collapsedIconColor: Colors.white,
childrenPadding: const EdgeInsets.only(left: 20.0), // Indent sub-items childrenPadding: const EdgeInsets.only(left: 20.0), // Indent sub-items
children: item.children!.map((childItem) { children: item.children!.map((childItem) {
if (childItem.isParent) { if (childItem.isParent) {
// Nested expansion tiles for sub-categories like "Manual", "Continuous" // Nested expansion tiles for sub-categories like "Manual", "Continuous"
return _buildExpandableNavItem(childItem); // --- MODIFICATION: Pass size variables recursively ---
return _buildExpandableNavItem(childItem, iconSize, textSize);
} else { } else {
// Leaf item (actual navigation link) // 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(), }).toList(),
), ),
@ -215,7 +240,8 @@ class _CollapsibleSidebarState extends State<CollapsibleSidebar> {
} }
// Builds a regular navigation item (for sub-items when expanded, or top-level when collapsed) // 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( return InkWell(
onTap: () { onTap: () {
if (route.isNotEmpty) { if (route.isNotEmpty) {
@ -230,17 +256,21 @@ class _CollapsibleSidebarState extends State<CollapsibleSidebar> {
child: isTopLevel && widget.isCollapsed child: isTopLevel && widget.isCollapsed
? Center( ? Center(
child: icon != null 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 : const SizedBox.shrink(), // Fallback if no icon or imagePath
) // Only icon when collapsed ) // Only icon when collapsed
: Row( : Row(
children: [ children: [
icon != null 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.shrink(), // Fallback if no icon or imagePath
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( // Use Expanded to prevent text overflow 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<CollapsibleSidebar> {
} }
// Builds a collapsed navigation item (only icon/image) for top-level categories // 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( return InkWell(
onTap: () { onTap: () {
if (item.route != null && item.route!.isNotEmpty) { if (item.route != null && item.route!.isNotEmpty) {
@ -260,19 +291,23 @@ class _CollapsibleSidebarState extends State<CollapsibleSidebar> {
} }
}, },
child: Padding( 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( child: Column(
mainAxisSize: MainAxisSize.min, // Use minimum space mainAxisSize: MainAxisSize.min, // Use minimum space
children: [ children: [
item.icon != null 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.shrink(), // Fallback if no icon
const SizedBox(height: 4), // Small space between icon and text const SizedBox(height: 4), // Small space between icon and text
Text( Text(
item.label, item.label,
style: const TextStyle( style: TextStyle(
color: Colors.white, 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 overflow: TextOverflow.ellipsis, // Handle long labels
maxLines: 1, // Ensure it stays on one line maxLines: 1, // Ensure it stays on one line

View File

@ -3,6 +3,9 @@ import 'package:provider/provider.dart';
import 'package:environment_monitoring_app/auth_provider.dart'; import 'package:environment_monitoring_app/auth_provider.dart';
import 'package:environment_monitoring_app/collapsible_sidebar.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 { class HomePage extends StatefulWidget {
const HomePage({super.key}); const HomePage({super.key});
@ -11,22 +14,91 @@ class HomePage extends StatefulWidget {
} }
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
bool _isSidebarCollapsed = true; // isCollapsed is only relevant for the persistent (desktop) layout
bool _isSidebarCollapsed = false;
String _currentSelectedRoute = '/home'; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final auth = Provider.of<AuthProvider>(context); final auth = Provider.of<AuthProvider>(context);
final colorScheme = Theme.of(context).colorScheme; 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: Builder(
icon: Icon(_isSidebarCollapsed ? Icons.menu : Icons.close, color: Colors.white), builder: (BuildContext innerContext) {
return IconButton(
icon: Icon(
isMobileLayout
? Icons.menu
: (_isSidebarCollapsed ? Icons.menu : Icons.close),
color: Colors.white
),
onPressed: () { onPressed: () {
setState(() { if (isMobileLayout) {
_isSidebarCollapsed = !_isSidebarCollapsed; Scaffold.of(innerContext).openDrawer();
}); } else {
_toggleSidebarState();
}
},
);
}, },
), ),
title: const Text("MMS Version 3.12.01"), title: const Text("MMS Version 3.12.01"),
@ -39,27 +111,23 @@ class _HomePageState extends State<HomePage> {
), ),
], ],
), ),
drawer: isMobileLayout ? Drawer(
width: expandedWidth,
child: Padding(
padding: EdgeInsets.only(top: totalDrawerTopPadding),
child: sidebar,
),
) : null,
body: Row( body: Row(
children: [ children: [
CollapsibleSidebar( if (!isMobileLayout)
isCollapsed: _isSidebarCollapsed, sidebar,
onToggle: () {
setState(() {
_isSidebarCollapsed = !_isSidebarCollapsed;
});
},
onNavigate: (route) {
setState(() {
_currentSelectedRoute = route;
});
Navigator.pushNamed(context, route);
},
),
Expanded( Expanded(
child: Padding( child: SingleChildScrollView(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Text( Text(
"Welcome, ${auth.userEmail ?? 'User'}", "Welcome, ${auth.userEmail ?? 'User'}",
@ -68,7 +136,7 @@ class _HomePageState extends State<HomePage> {
color: colorScheme.onBackground, color: colorScheme.onBackground,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 4),
Text( Text(
"Select a Department:", "Select a Department:",
style: Theme.of(context).textTheme.titleSmall?.copyWith( style: Theme.of(context).textTheme.titleSmall?.copyWith(
@ -76,14 +144,14 @@ class _HomePageState extends State<HomePage> {
color: colorScheme.onBackground, color: colorScheme.onBackground,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 4),
Expanded(
child: GridView.count( GridView.count(
crossAxisCount: 2, crossAxisCount: crossAxisCount,
mainAxisSpacing: 8, mainAxisSpacing: 8,
crossAxisSpacing: 8, crossAxisSpacing: 8,
childAspectRatio: 1.6, // Wider and much shorter boxes childAspectRatio: childAspectRatio,
padding: EdgeInsets.zero, // No extra padding padding: EdgeInsets.zero,
shrinkWrap: true, shrinkWrap: true,
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
children: [ children: [
@ -93,6 +161,8 @@ class _HomePageState extends State<HomePage> {
icon: Icons.air, icon: Icons.air,
color: Colors.blue.shade700, color: Colors.blue.shade700,
route: '/air/home', route: '/air/home',
iconSize: iconSize,
textSize: textSize,
), ),
_buildMiniCategoryCard( _buildMiniCategoryCard(
context, context,
@ -100,6 +170,8 @@ class _HomePageState extends State<HomePage> {
icon: Icons.water, icon: Icons.water,
color: Colors.teal.shade700, color: Colors.teal.shade700,
route: '/river/home', route: '/river/home',
iconSize: iconSize,
textSize: textSize,
), ),
_buildMiniCategoryCard( _buildMiniCategoryCard(
context, context,
@ -107,11 +179,16 @@ class _HomePageState extends State<HomePage> {
icon: Icons.sailing, icon: Icons.sailing,
color: Colors.indigo.shade700, color: Colors.indigo.shade700,
route: '/marine/home', route: '/marine/home',
iconSize: iconSize,
textSize: textSize,
),
_buildMiniSettingsCard(
context,
iconSize: iconSize,
textSize: textSize,
), ),
_buildMiniSettingsCard(context),
], ],
), ),
),
], ],
), ),
), ),
@ -127,6 +204,8 @@ class _HomePageState extends State<HomePage> {
required IconData icon, required IconData icon,
required Color color, required Color color,
required String route, required String route,
required double iconSize,
required double textSize,
}) { }) {
return Card( return Card(
elevation: 1, elevation: 1,
@ -138,7 +217,7 @@ class _HomePageState extends State<HomePage> {
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
onTap: () => Navigator.pushNamed(context, route), onTap: () => Navigator.pushNamed(context, route),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
gradient: LinearGradient( gradient: LinearGradient(
@ -147,16 +226,19 @@ class _HomePageState extends State<HomePage> {
colors: [color.withOpacity(0.9), color], colors: [color.withOpacity(0.9), color],
), ),
), ),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(icon, size: 26, color: Colors.white), Icon(icon, size: iconSize, color: Colors.white),
const SizedBox(height: 4), const SizedBox(height: 2),
Text( Text(
title, title,
style: const TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 13, fontSize: textSize,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@ -164,10 +246,14 @@ class _HomePageState extends State<HomePage> {
), ),
), ),
), ),
),
); );
} }
Widget _buildMiniSettingsCard(BuildContext context) { Widget _buildMiniSettingsCard(BuildContext context, {
required double iconSize,
required double textSize,
}) {
return Card( return Card(
elevation: 1, elevation: 1,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
@ -178,7 +264,7 @@ class _HomePageState extends State<HomePage> {
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
onTap: () => Navigator.pushNamed(context, '/settings'), onTap: () => Navigator.pushNamed(context, '/settings'),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
gradient: LinearGradient( gradient: LinearGradient(
@ -187,16 +273,19 @@ class _HomePageState extends State<HomePage> {
colors: [Colors.grey.shade700, Colors.grey.shade800], colors: [Colors.grey.shade700, Colors.grey.shade800],
), ),
), ),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.settings, size: 26, color: Colors.white), Icon(Icons.settings, size: iconSize, color: Colors.white),
const SizedBox(height: 4), const SizedBox(height: 2),
const Text( Text(
"Settings", "Settings",
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 13, fontSize: textSize,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@ -204,6 +293,7 @@ class _HomePageState extends State<HomePage> {
), ),
), ),
), ),
),
); );
} }
} }

View File

@ -815,7 +815,7 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
} }
Widget _buildConnectionCard({required String type, required dynamic connectionState, String? deviceName}) { 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 isConnected = connectionState == BluetoothConnectionState.connected || connectionState == SerialConnectionState.connected;
final bool isConnecting = connectionState == BluetoothConnectionState.connecting || connectionState == SerialConnectionState.connecting; final bool isConnecting = connectionState == BluetoothConnectionState.connecting || connectionState == SerialConnectionState.connecting;
@ -844,8 +844,12 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
if (isConnecting || _isLoading) if (isConnecting || _isLoading)
const CircularProgressIndicator() const CircularProgressIndicator()
else if (isConnected) else if (isConnected)
Row( // --- START FIX: Replaced Row with Wrap to fix horizontal overflow with countdown timer ---
mainAxisAlignment: MainAxisAlignment.spaceEvenly, Wrap(
alignment: WrapAlignment.spaceEvenly,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 8.0, // Horizontal space between buttons
runSpacing: 4.0, // Vertical space if it wraps
children: [ children: [
ElevatedButton.icon( ElevatedButton.icon(
icon: Icon(_isAutoReading ? Icons.stop_circle_outlined : Icons.play_circle_outlined), icon: Icon(_isAutoReading ? Icons.stop_circle_outlined : Icons.play_circle_outlined),
@ -868,6 +872,7 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
) )
], ],
) )
// --- END FIX ---
// Optionally add a button to reconnect if disconnected // Optionally add a button to reconnect if disconnected
else else
ElevatedButton.icon( ElevatedButton.icon(
@ -1051,7 +1056,7 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
} }
Widget _buildFlowrateSection() { Widget _buildFlowrateSection() {
// Copied from RiverInSituStep3DataCaptureState._buildFlowrateSection // Copied from RiverInSituStep3DataCaptureState._buildFlowrateSection, modified to use Wrap
return Card( return Card(
margin: const EdgeInsets.symmetric(vertical: 4.0), margin: const EdgeInsets.symmetric(vertical: 4.0),
child: Padding( child: Padding(
@ -1061,15 +1066,18 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
children: [ children: [
Text("Flowrate", style: Theme.of(context).textTheme.titleLarge), Text("Flowrate", style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8), const SizedBox(height: 8),
// Radio buttons for method selection // --- START FIX: Replaced Row with Wrap to fix horizontal overflow for radio buttons ---
Row( Wrap(
mainAxisAlignment: MainAxisAlignment.spaceAround, alignment: WrapAlignment.spaceAround,
spacing: 8.0,
runSpacing: 4.0,
children: [ children: [
_buildFlowrateRadioButton("Surface Drifter"), _buildFlowrateRadioButton("Surface Drifter"),
_buildFlowrateRadioButton("Flowmeter"), _buildFlowrateRadioButton("Flowmeter"),
_buildFlowrateRadioButton("NA"), // Not Applicable _buildFlowrateRadioButton("NA"), // Not Applicable
], ],
), ),
// --- END FIX ---
// Conditional fields based on selected method // Conditional fields based on selected method
if (_selectedFlowrateMethod == 'Surface Drifter') if (_selectedFlowrateMethod == 'Surface Drifter')
_buildSurfaceDrifterFields(), _buildSurfaceDrifterFields(),
@ -1084,7 +1092,7 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
} }
Widget _buildFlowrateRadioButton(String title) { Widget _buildFlowrateRadioButton(String title) {
// Copied from RiverInSituStep3DataCaptureState._buildFlowrateRadioButton // Copied from RiverInSituStep3DataCaptureState._buildFlowrateRadioButton, added overflow handling
return Column( return Column(
children: [ children: [
Radio<String>( Radio<String>(
@ -1092,7 +1100,11 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
groupValue: _selectedFlowrateMethod, groupValue: _selectedFlowrateMethod,
onChanged: _onFlowrateMethodChanged, onChanged: _onFlowrateMethodChanged,
), ),
Text(title), Text(
title,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis, // Add ellipsis handling for safety
),
], ],
); );
} }

View File

@ -39,7 +39,7 @@ class _RiverManualTriennialStep1SamplingInfoState extends State<RiverManualTrien
List<String> _statesList = []; List<String> _statesList = [];
List<Map<String, dynamic>> _stationsForState = []; List<Map<String, dynamic>> _stationsForState = [];
final List<String> _samplingTypes = ['Schedule', 'Triennial']; final List<String> _samplingTypes = ['Triennial'];
@override @override
void initState() { void initState() {

View File

@ -721,8 +721,12 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
if (isConnecting || _isLoading) if (isConnecting || _isLoading)
const CircularProgressIndicator() const CircularProgressIndicator()
else if (isConnected) else if (isConnected)
Row( // --- START FIX: Replaced Row with Wrap to fix horizontal overflow with countdown timer ---
mainAxisAlignment: MainAxisAlignment.spaceEvenly, Wrap(
alignment: WrapAlignment.spaceEvenly,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 8.0, // Horizontal space between buttons
runSpacing: 4.0, // Vertical space if it wraps
children: [ children: [
// --- START MODIFICATION: Add countdown to Stop Reading button --- // --- START MODIFICATION: Add countdown to Stop Reading button ---
ElevatedButton.icon( ElevatedButton.icon(
@ -747,6 +751,7 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
) )
], ],
) )
// --- END FIX ---
], ],
), ),
), ),
@ -926,14 +931,18 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
children: [ children: [
Text("Flowrate", style: Theme.of(context).textTheme.titleLarge), Text("Flowrate", style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( // --- START FIX: Wrap radio buttons in Expanded/Wrap widgets to prevent horizontal overflow ---
mainAxisAlignment: MainAxisAlignment.spaceAround, Wrap(
alignment: WrapAlignment.spaceAround,
spacing: 8.0,
runSpacing: 4.0,
children: [ children: [
_buildFlowrateRadioButton("Surface Drifter"), _buildFlowrateRadioButton("Surface Drifter"),
_buildFlowrateRadioButton("Flowmeter"), _buildFlowrateRadioButton("Flowmeter"),
_buildFlowrateRadioButton("NA"), _buildFlowrateRadioButton("NA"),
], ],
), ),
// --- END FIX ---
if (_selectedFlowrateMethod == 'Surface Drifter') if (_selectedFlowrateMethod == 'Surface Drifter')
_buildSurfaceDrifterFields(), _buildSurfaceDrifterFields(),
if (_selectedFlowrateMethod == 'Flowmeter') if (_selectedFlowrateMethod == 'Flowmeter')
@ -954,7 +963,11 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
groupValue: _selectedFlowrateMethod, groupValue: _selectedFlowrateMethod,
onChanged: _onFlowrateMethodChanged, onChanged: _onFlowrateMethodChanged,
), ),
Text(title), Text(
title,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis, // Add ellipsis handling for safety
),
], ],
); );
} }

View File

@ -39,7 +39,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
List<String> _statesList = []; List<String> _statesList = [];
List<Map<String, dynamic>> _stationsForState = []; List<Map<String, dynamic>> _stationsForState = [];
final List<String> _samplingTypes = ['Schedule', 'Triennial']; final List<String> _samplingTypes = ['Schedule'];
@override @override
void initState() { void initState() {

View File

@ -719,8 +719,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
if (isConnecting || _isLoading) if (isConnecting || _isLoading)
const CircularProgressIndicator() const CircularProgressIndicator()
else if (isConnected) else if (isConnected)
Row( // --- START FIX: Replaced Row with Wrap to fix horizontal overflow with countdown timer ---
mainAxisAlignment: MainAxisAlignment.spaceEvenly, Wrap(
alignment: WrapAlignment.spaceEvenly,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 8.0, // Horizontal space between buttons
runSpacing: 4.0, // Vertical space if it wraps
children: [ children: [
// --- START MODIFICATION: Add countdown to Stop Reading button --- // --- START MODIFICATION: Add countdown to Stop Reading button ---
ElevatedButton.icon( ElevatedButton.icon(
@ -745,6 +749,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
) )
], ],
) )
// --- END FIX ---
], ],
), ),
), ),
@ -924,14 +929,16 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
children: [ children: [
Text("Flowrate", style: Theme.of(context).textTheme.titleLarge), Text("Flowrate", style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8), const SizedBox(height: 8),
// --- START FIX: Wrap radio buttons in Expanded widgets to prevent horizontal overflow ---
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
_buildFlowrateRadioButton("Surface Drifter"), Expanded(child: _buildFlowrateRadioButton("Surface Drifter")),
_buildFlowrateRadioButton("Flowmeter"), Expanded(child: _buildFlowrateRadioButton("Flowmeter")),
_buildFlowrateRadioButton("NA"), Expanded(child: _buildFlowrateRadioButton("NA")),
], ],
), ),
// --- END FIX ---
if (_selectedFlowrateMethod == 'Surface Drifter') if (_selectedFlowrateMethod == 'Surface Drifter')
_buildSurfaceDrifterFields(), _buildSurfaceDrifterFields(),
if (_selectedFlowrateMethod == 'Flowmeter') if (_selectedFlowrateMethod == 'Flowmeter')
@ -952,7 +959,11 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
groupValue: _selectedFlowrateMethod, groupValue: _selectedFlowrateMethod,
onChanged: _onFlowrateMethodChanged, onChanged: _onFlowrateMethodChanged,
), ),
Text(title), Text(
title,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis, // Add ellipsis handling for safety
),
], ],
); );
} }

View File

@ -115,7 +115,26 @@ class MarineApiService {
final baseUrl = await _serverConfigService.getActiveApiUrl(); final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'marine/maintenance/previous'); return _baseService.get(baseUrl, 'marine/maintenance/previous');
} }
// --- END: ADDED MISSING METHODS --- // --- END: ADDED MISSING METHODS ---
// *** START: ADDED FOR NPE REPORTS ***
/// Fetches all NPE Reports submitted by the user (requires auth).
Future<Map<String, dynamic>> 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 *** // *** START: ADDED FOR INVESTIGATIVE IMAGE REQUEST ***

View File

@ -1,3 +1,5 @@
// lib/services/marine_npe_report_service.dart
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:convert'; import 'dart:convert';
@ -84,9 +86,10 @@ class MarineNpeReportService {
Map<String, dynamic> apiImageResult = {}; Map<String, dynamic> apiImageResult = {};
try { try {
// --- MODIFIED: Use the new endpoint path for data ---
apiDataResult = await _submissionApiService.submitPost( apiDataResult = await _submissionApiService.submitPost(
moduleName: moduleName, moduleName: moduleName,
endpoint: 'marine/npe/report', endpoint: 'marine/npe/report', // <-- Updated endpoint
body: data.toApiFormData(), body: data.toApiFormData(),
); );
@ -96,7 +99,7 @@ class MarineNpeReportService {
if (reloginSuccess) { if (reloginSuccess) {
apiDataResult = await _submissionApiService.submitPost( apiDataResult = await _submissionApiService.submitPost(
moduleName: moduleName, moduleName: moduleName,
endpoint: 'marine/npe/report', endpoint: 'marine/npe/report', // <-- Updated endpoint
body: data.toApiFormData(), body: data.toApiFormData(),
); );
} }
@ -108,9 +111,10 @@ class MarineNpeReportService {
if (data.reportId != null) { if (data.reportId != null) {
if (finalImageFiles.isNotEmpty) { if (finalImageFiles.isNotEmpty) {
// --- MODIFIED: Use the new endpoint path for images ---
apiImageResult = await _submissionApiService.submitMultipart( apiImageResult = await _submissionApiService.submitMultipart(
moduleName: moduleName, moduleName: moduleName,
endpoint: 'marine/npe/images', endpoint: 'marine/npe/images', // <-- Updated endpoint
fields: {'npe_id': data.reportId!}, fields: {'npe_id': data.reportId!},
files: finalImageFiles, files: finalImageFiles,
); );
@ -124,6 +128,7 @@ class MarineNpeReportService {
} on SocketException catch (e) { } on SocketException catch (e) {
anyApiSuccess = false; anyApiSuccess = false;
apiDataResult = {'success': false, 'message': "API submission failed with network error: $e"}; 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()); await _retryService.addApiToQueue(endpoint: 'marine/npe/report', method: 'POST', body: data.toApiFormData());
if (finalImageFiles.isNotEmpty && data.reportId != null) { if (finalImageFiles.isNotEmpty && data.reportId != null) {
await _retryService.addApiToQueue(endpoint: 'marine/npe/images', method: 'POST_MULTIPART', fields: {'npe_id': data.reportId!}, files: finalImageFiles); 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) { } on TimeoutException catch (e) {
anyApiSuccess = false; anyApiSuccess = false;
apiDataResult = {'success': false, 'message': "API submission timed out: $e"}; 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()); await _retryService.addApiToQueue(endpoint: 'marine/npe/report', method: 'POST', body: data.toApiFormData());
} }

View File

@ -805,6 +805,62 @@ class RiverInvestigativeSamplingService { // Renamed class
} }
} }
/// Generates the specific Telegram alert message content for River Investigative.
Future<String> _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). /// Helper to generate the parameter limit alert section for Telegram (River Investigative).
Future<String> _getOutOfBoundsAlertSection(RiverInvesManualSamplingData data) async { // Updated model type Future<String> _getOutOfBoundsAlertSection(RiverInvesManualSamplingData data) async { // Updated model type
// Define mapping from data model keys to parameter names used in limits table // 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:'); ..writeln('The following parameters were outside their defined limits:');
buffer.writeAll(outOfBoundsMessages, '\n'); // Add each message on a new line 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 } // End of RiverInvestigativeSamplingService class

View File

@ -715,6 +715,79 @@ class RiverManualTriennialSamplingService {
// If needed, similar logic to _getOutOfBoundsAlertSection in RiverInSituSamplingService // If needed, similar logic to _getOutOfBoundsAlertSection in RiverInSituSamplingService
// would need to be adapted here, potentially using riverParameterLimits from the DB. // 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<String> _getOutOfBoundsAlertSection(RiverManualTriennialSamplingData data) async {
// Define mapping from data model keys to parameter names used in limits table
const Map<String, String> _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<String> 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: () => <String, dynamic>{}, // 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(); return buffer.toString();
} }
} }