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
|
@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
|
||||||
|
|||||||
@ -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) {
|
||||||
onPressed: () {
|
return IconButton(
|
||||||
setState(() {
|
icon: Icon(
|
||||||
_isSidebarCollapsed = !_isSidebarCollapsed;
|
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"),
|
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,41 +144,50 @@ 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: [
|
||||||
_buildMiniCategoryCard(
|
_buildMiniCategoryCard(
|
||||||
context,
|
context,
|
||||||
title: "Air",
|
title: "Air",
|
||||||
icon: Icons.air,
|
icon: Icons.air,
|
||||||
color: Colors.blue.shade700,
|
color: Colors.blue.shade700,
|
||||||
route: '/air/home',
|
route: '/air/home',
|
||||||
),
|
iconSize: iconSize,
|
||||||
_buildMiniCategoryCard(
|
textSize: textSize,
|
||||||
context,
|
),
|
||||||
title: "River",
|
_buildMiniCategoryCard(
|
||||||
icon: Icons.water,
|
context,
|
||||||
color: Colors.teal.shade700,
|
title: "River",
|
||||||
route: '/river/home',
|
icon: Icons.water,
|
||||||
),
|
color: Colors.teal.shade700,
|
||||||
_buildMiniCategoryCard(
|
route: '/river/home',
|
||||||
context,
|
iconSize: iconSize,
|
||||||
title: "Marine",
|
textSize: textSize,
|
||||||
icon: Icons.sailing,
|
),
|
||||||
color: Colors.indigo.shade700,
|
_buildMiniCategoryCard(
|
||||||
route: '/marine/home',
|
context,
|
||||||
),
|
title: "Marine",
|
||||||
_buildMiniSettingsCard(context),
|
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<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,27 +226,34 @@ class _HomePageState extends State<HomePage> {
|
|||||||
colors: [color.withOpacity(0.9), color],
|
colors: [color.withOpacity(0.9), color],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: FittedBox(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
fit: BoxFit.scaleDown,
|
||||||
children: [
|
child: Column(
|
||||||
Icon(icon, size: 26, color: Colors.white),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
const SizedBox(height: 4),
|
mainAxisSize: MainAxisSize.min,
|
||||||
Text(
|
children: [
|
||||||
title,
|
Icon(icon, size: iconSize, color: Colors.white),
|
||||||
style: const TextStyle(
|
const SizedBox(height: 2),
|
||||||
color: Colors.white,
|
Text(
|
||||||
fontSize: 13,
|
title,
|
||||||
fontWeight: FontWeight.bold,
|
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(
|
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,20 +273,24 @@ class _HomePageState extends State<HomePage> {
|
|||||||
colors: [Colors.grey.shade700, Colors.grey.shade800],
|
colors: [Colors.grey.shade700, Colors.grey.shade800],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: FittedBox(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
fit: BoxFit.scaleDown,
|
||||||
children: [
|
child: Column(
|
||||||
const Icon(Icons.settings, size: 26, color: Colors.white),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
const SizedBox(height: 4),
|
mainAxisSize: MainAxisSize.min,
|
||||||
const Text(
|
children: [
|
||||||
"Settings",
|
Icon(Icons.settings, size: iconSize, color: Colors.white),
|
||||||
style: TextStyle(
|
const SizedBox(height: 2),
|
||||||
color: Colors.white,
|
Text(
|
||||||
fontSize: 13,
|
"Settings",
|
||||||
fontWeight: FontWeight.bold,
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: textSize,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 ***
|
||||||
|
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user