repair river investigative and triennial ftp format
This commit is contained in:
parent
18e853ac83
commit
cf22668576
@ -146,8 +146,22 @@ class _CollapsibleSidebarState extends State<CollapsibleSidebar> {
|
||||
|
||||
@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<CollapsibleSidebar> {
|
||||
|
||||
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<CollapsibleSidebar> {
|
||||
}
|
||||
|
||||
// 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<CollapsibleSidebar> {
|
||||
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<CollapsibleSidebar> {
|
||||
}
|
||||
|
||||
// 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<CollapsibleSidebar> {
|
||||
}
|
||||
},
|
||||
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
|
||||
|
||||
@ -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<HomePage> {
|
||||
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<AuthProvider>(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),
|
||||
leading: Builder(
|
||||
builder: (BuildContext innerContext) {
|
||||
return IconButton(
|
||||
icon: Icon(
|
||||
isMobileLayout
|
||||
? Icons.menu
|
||||
: (_isSidebarCollapsed ? Icons.menu : Icons.close),
|
||||
color: Colors.white
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isSidebarCollapsed = !_isSidebarCollapsed;
|
||||
});
|
||||
if (isMobileLayout) {
|
||||
Scaffold.of(innerContext).openDrawer();
|
||||
} else {
|
||||
_toggleSidebarState();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
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(
|
||||
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<HomePage> {
|
||||
color: colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Select a Department:",
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
@ -76,14 +144,14 @@ class _HomePageState extends State<HomePage> {
|
||||
color: colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
const SizedBox(height: 4),
|
||||
|
||||
GridView.count(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
childAspectRatio: 1.6, // Wider and much shorter boxes
|
||||
padding: EdgeInsets.zero, // No extra padding
|
||||
childAspectRatio: childAspectRatio,
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
@ -93,6 +161,8 @@ class _HomePageState extends State<HomePage> {
|
||||
icon: Icons.air,
|
||||
color: Colors.blue.shade700,
|
||||
route: '/air/home',
|
||||
iconSize: iconSize,
|
||||
textSize: textSize,
|
||||
),
|
||||
_buildMiniCategoryCard(
|
||||
context,
|
||||
@ -100,6 +170,8 @@ class _HomePageState extends State<HomePage> {
|
||||
icon: Icons.water,
|
||||
color: Colors.teal.shade700,
|
||||
route: '/river/home',
|
||||
iconSize: iconSize,
|
||||
textSize: textSize,
|
||||
),
|
||||
_buildMiniCategoryCard(
|
||||
context,
|
||||
@ -107,11 +179,16 @@ class _HomePageState extends State<HomePage> {
|
||||
icon: Icons.sailing,
|
||||
color: Colors.indigo.shade700,
|
||||
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 Color color,
|
||||
required String route,
|
||||
required double iconSize,
|
||||
required double textSize,
|
||||
}) {
|
||||
return Card(
|
||||
elevation: 1,
|
||||
@ -138,7 +217,7 @@ class _HomePageState extends State<HomePage> {
|
||||
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,16 +226,19 @@ class _HomePageState extends State<HomePage> {
|
||||
colors: [color.withOpacity(0.9), color],
|
||||
),
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 26, color: Colors.white),
|
||||
const SizedBox(height: 4),
|
||||
Icon(icon, size: iconSize, color: Colors.white),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontSize: textSize,
|
||||
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(
|
||||
elevation: 1,
|
||||
margin: EdgeInsets.zero,
|
||||
@ -178,7 +264,7 @@ class _HomePageState extends State<HomePage> {
|
||||
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,16 +273,19 @@ class _HomePageState extends State<HomePage> {
|
||||
colors: [Colors.grey.shade700, Colors.grey.shade800],
|
||||
),
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.settings, size: 26, color: Colors.white),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
Icon(Icons.settings, size: iconSize, color: Colors.white),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
"Settings",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontSize: textSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@ -204,6 +293,7 @@ class _HomePageState extends State<HomePage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -815,7 +815,7 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
|
||||
}
|
||||
|
||||
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<RiverInvesStep3DataCapture>
|
||||
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<RiverInvesStep3DataCapture>
|
||||
)
|
||||
],
|
||||
)
|
||||
// --- END FIX ---
|
||||
// Optionally add a button to reconnect if disconnected
|
||||
else
|
||||
ElevatedButton.icon(
|
||||
@ -1051,7 +1056,7 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
|
||||
}
|
||||
|
||||
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<RiverInvesStep3DataCapture>
|
||||
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<RiverInvesStep3DataCapture>
|
||||
}
|
||||
|
||||
Widget _buildFlowrateRadioButton(String title) {
|
||||
// Copied from RiverInSituStep3DataCaptureState._buildFlowrateRadioButton
|
||||
// Copied from RiverInSituStep3DataCaptureState._buildFlowrateRadioButton, added overflow handling
|
||||
return Column(
|
||||
children: [
|
||||
Radio<String>(
|
||||
@ -1092,7 +1100,11 @@ class _RiverInvesStep3DataCaptureState extends State<RiverInvesStep3DataCapture>
|
||||
groupValue: _selectedFlowrateMethod,
|
||||
onChanged: _onFlowrateMethodChanged,
|
||||
),
|
||||
Text(title),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis, // Add ellipsis handling for safety
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ class _RiverManualTriennialStep1SamplingInfoState extends State<RiverManualTrien
|
||||
|
||||
List<String> _statesList = [];
|
||||
List<Map<String, dynamic>> _stationsForState = [];
|
||||
final List<String> _samplingTypes = ['Schedule', 'Triennial'];
|
||||
final List<String> _samplingTypes = ['Triennial'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
@ -721,8 +721,12 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
||||
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: [
|
||||
// --- START MODIFICATION: Add countdown to Stop Reading button ---
|
||||
ElevatedButton.icon(
|
||||
@ -747,6 +751,7 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
||||
)
|
||||
],
|
||||
)
|
||||
// --- END FIX ---
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -926,14 +931,18 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
||||
children: [
|
||||
Text("Flowrate", style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
// --- START FIX: Wrap radio buttons in Expanded/Wrap widgets to prevent horizontal overflow ---
|
||||
Wrap(
|
||||
alignment: WrapAlignment.spaceAround,
|
||||
spacing: 8.0,
|
||||
runSpacing: 4.0,
|
||||
children: [
|
||||
_buildFlowrateRadioButton("Surface Drifter"),
|
||||
_buildFlowrateRadioButton("Flowmeter"),
|
||||
_buildFlowrateRadioButton("NA"),
|
||||
],
|
||||
),
|
||||
// --- END FIX ---
|
||||
if (_selectedFlowrateMethod == 'Surface Drifter')
|
||||
_buildSurfaceDrifterFields(),
|
||||
if (_selectedFlowrateMethod == 'Flowmeter')
|
||||
@ -954,7 +963,11 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
||||
groupValue: _selectedFlowrateMethod,
|
||||
onChanged: _onFlowrateMethodChanged,
|
||||
),
|
||||
Text(title),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis, // Add ellipsis handling for safety
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
||||
|
||||
List<String> _statesList = [];
|
||||
List<Map<String, dynamic>> _stationsForState = [];
|
||||
final List<String> _samplingTypes = ['Schedule', 'Triennial'];
|
||||
final List<String> _samplingTypes = ['Schedule'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
@ -719,8 +719,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
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: [
|
||||
// --- START MODIFICATION: Add countdown to Stop Reading button ---
|
||||
ElevatedButton.icon(
|
||||
@ -745,6 +749,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
)
|
||||
],
|
||||
)
|
||||
// --- END FIX ---
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -924,14 +929,16 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
children: [
|
||||
Text("Flowrate", style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 8),
|
||||
// --- START FIX: Wrap radio buttons in Expanded widgets to prevent horizontal overflow ---
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildFlowrateRadioButton("Surface Drifter"),
|
||||
_buildFlowrateRadioButton("Flowmeter"),
|
||||
_buildFlowrateRadioButton("NA"),
|
||||
Expanded(child: _buildFlowrateRadioButton("Surface Drifter")),
|
||||
Expanded(child: _buildFlowrateRadioButton("Flowmeter")),
|
||||
Expanded(child: _buildFlowrateRadioButton("NA")),
|
||||
],
|
||||
),
|
||||
// --- END FIX ---
|
||||
if (_selectedFlowrateMethod == 'Surface Drifter')
|
||||
_buildSurfaceDrifterFields(),
|
||||
if (_selectedFlowrateMethod == 'Flowmeter')
|
||||
@ -952,7 +959,11 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
groupValue: _selectedFlowrateMethod,
|
||||
onChanged: _onFlowrateMethodChanged,
|
||||
),
|
||||
Text(title),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis, // Add ellipsis handling for safety
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -115,7 +115,26 @@ class MarineApiService {
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
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 ***
|
||||
|
||||
|
||||
@ -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<String, dynamic> 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());
|
||||
}
|
||||
|
||||
|
||||
@ -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).
|
||||
Future<String> _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
|
||||
@ -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<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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user