add in info center document for all module

This commit is contained in:
ALim Aidrus 2025-09-05 06:40:15 +08:00
parent f742dd5853
commit c2a95c53cc
33 changed files with 2706 additions and 469 deletions

View File

@ -27,7 +27,7 @@
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- END: STORAGE PERMISSIONS -->
<!-- MMS V4 1.2.03 -->
<!-- MMS V4 1.2.05 -->
<application
android:label="MMS V4 debug"
android:name="${applicationName}"

View File

@ -55,6 +55,8 @@ class AuthProvider with ChangeNotifier {
List<Map<String, dynamic>>? _parameterLimits;
List<Map<String, dynamic>>? _apiConfigs;
List<Map<String, dynamic>>? _ftpConfigs;
// --- ADDED: State variable for the list of documents ---
List<Map<String, dynamic>>? _documents;
// --- ADDED: State variable for the list of tasks pending manual retry ---
List<Map<String, dynamic>>? _pendingRetries;
@ -76,6 +78,8 @@ class AuthProvider with ChangeNotifier {
List<Map<String, dynamic>>? get parameterLimits => _parameterLimits;
List<Map<String, dynamic>>? get apiConfigs => _apiConfigs;
List<Map<String, dynamic>>? get ftpConfigs => _ftpConfigs;
// --- ADDED: Getter for the list of documents ---
List<Map<String, dynamic>>? get documents => _documents;
// --- ADDED: Getter for the list of tasks pending manual retry ---
List<Map<String, dynamic>>? get pendingRetries => _pendingRetries;
@ -221,6 +225,8 @@ class AuthProvider with ChangeNotifier {
// ADDED: Load new data types from the local database
_appSettings = await _dbHelper.loadAppSettings();
_parameterLimits = await _dbHelper.loadParameterLimits();
// --- ADDED: Load documents from the local database cache ---
_documents = await _dbHelper.loadDocuments();
_apiConfigs = await _dbHelper.loadApiConfigs();
_ftpConfigs = await _dbHelper.loadFtpConfigs();
// --- ADDED: Load pending retry tasks from the database ---
@ -291,6 +297,8 @@ class AuthProvider with ChangeNotifier {
// ADDED: Clear new data on logout
_appSettings = null;
_parameterLimits = null;
// --- ADDED: Clear documents list on logout ---
_documents = null;
_apiConfigs = null;
_ftpConfigs = null;
// --- ADDED: Clear pending retry tasks on logout ---

View File

@ -12,10 +12,8 @@ import 'package:environment_monitoring_app/services/air_sampling_service.dart';
import 'package:environment_monitoring_app/services/telegram_service.dart';
import 'package:environment_monitoring_app/services/server_config_service.dart';
import 'package:environment_monitoring_app/services/retry_service.dart';
// START CHANGE: Import the new dedicated Marine services and remove the obsolete one
import 'package:environment_monitoring_app/services/marine_in_situ_sampling_service.dart';
import 'package:environment_monitoring_app/services/marine_tarball_sampling_service.dart';
// END CHANGE
import 'package:environment_monitoring_app/theme.dart';
import 'package:environment_monitoring_app/auth_provider.dart';
@ -35,50 +33,49 @@ import 'package:environment_monitoring_app/screens/river/river_home_page.dart';
import 'package:environment_monitoring_app/screens/marine/marine_home_page.dart';
// Air Screens
import 'package:environment_monitoring_app/screens/air/manual/air_manual_dashboard.dart';
import 'package:environment_monitoring_app/screens/air/manual/air_manual_info_centre_document.dart';
import 'package:environment_monitoring_app/screens/air/manual/air_manual_installation_screen.dart';
import 'package:environment_monitoring_app/screens/air/manual/air_manual_collection_screen.dart';
import 'package:environment_monitoring_app/screens/air/manual/report.dart' as airManualReport;
import 'package:environment_monitoring_app/screens/air/manual/data_status_log.dart' as airManualDataStatusLog;
import 'package:environment_monitoring_app/screens/air/manual/image_request.dart' as airManualImageRequest;
import 'package:environment_monitoring_app/screens/air/continuous/air_continuous_dashboard.dart';
import 'package:environment_monitoring_app/screens/air/continuous/air_continuous_info_centre_document.dart';
import 'package:environment_monitoring_app/screens/air/continuous/overview.dart' as airContinuousOverview;
import 'package:environment_monitoring_app/screens/air/continuous/entry.dart' as airContinuousEntry;
import 'package:environment_monitoring_app/screens/air/continuous/report.dart' as airContinuousReport;
import 'package:environment_monitoring_app/screens/air/investigative/air_investigative_dashboard.dart';
import 'package:environment_monitoring_app/screens/air/investigative/air_investigative_info_centre_document.dart';
import 'package:environment_monitoring_app/screens/air/investigative/overview.dart' as airInvestigativeOverview;
import 'package:environment_monitoring_app/screens/air/investigative/entry.dart' as airInvestigativeEntry;
import 'package:environment_monitoring_app/screens/air/investigative/report.dart' as airInvestigativeReport;
// River Screens
import 'package:environment_monitoring_app/screens/river/manual/river_manual_dashboard.dart';
import 'package:environment_monitoring_app/screens/river/manual/river_manual_info_centre_document.dart';
import 'package:environment_monitoring_app/screens/river/manual/in_situ_sampling.dart' as riverManualInSituSampling;
import 'package:environment_monitoring_app/screens/river/manual/data_status_log.dart' as riverManualDataStatusLog;
import 'package:environment_monitoring_app/screens/river/manual/report.dart' as riverManualReport;
import 'package:environment_monitoring_app/screens/river/manual/triennial_sampling.dart' as riverManualTriennialSampling;
import 'package:environment_monitoring_app/screens/river/manual/image_request.dart' as riverManualImageRequest;
import 'package:environment_monitoring_app/screens/river/continuous/river_continuous_dashboard.dart';
import 'package:environment_monitoring_app/screens/river/continuous/river_continuous_info_centre_document.dart';
import 'package:environment_monitoring_app/screens/river/continuous/overview.dart' as riverContinuousOverview;
import 'package:environment_monitoring_app/screens/river/continuous/entry.dart' as riverContinuousEntry;
import 'package:environment_monitoring_app/screens/river/continuous/report.dart' as riverContinuousReport;
import 'package:environment_monitoring_app/screens/river/investigative/river_investigative_dashboard.dart';
import 'package:environment_monitoring_app/screens/river/investigative/river_investigative_info_centre_document.dart';
import 'package:environment_monitoring_app/screens/river/investigative/overview.dart' as riverInvestigativeOverview;
import 'package:environment_monitoring_app/screens/river/investigative/entry.dart' as riverInvestigativeEntry;
import 'package:environment_monitoring_app/screens/river/investigative/report.dart' as riverInvestigativeReport;
// Marine Screens
import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_dashboard.dart';
import 'package:environment_monitoring_app/screens/marine/manual/info_centre_document.dart' as marineManualInfoCentreDocument;
import 'package:environment_monitoring_app/screens/marine/manual/pre_sampling.dart' as marineManualPreSampling;
import 'package:environment_monitoring_app/screens/marine/manual/in_situ_sampling.dart' as marineManualInSituSampling;
import 'package:environment_monitoring_app/screens/marine/manual/report.dart' as marineManualReport;
import 'package:environment_monitoring_app/screens/marine/manual/data_status_log.dart' as marineManualDataStatusLog;
import 'package:environment_monitoring_app/screens/marine/manual/image_request.dart' as marineManualImageRequest;
import 'package:environment_monitoring_app/screens/marine/continuous/marine_continuous_dashboard.dart';
import 'package:environment_monitoring_app/screens/marine/continuous/marine_continuous_info_centre_document.dart';
import 'package:environment_monitoring_app/screens/marine/continuous/overview.dart' as marineContinuousOverview;
import 'package:environment_monitoring_app/screens/marine/continuous/entry.dart' as marineContinuousEntry;
import 'package:environment_monitoring_app/screens/marine/continuous/report.dart' as marineContinuousReport;
import 'package:environment_monitoring_app/screens/marine/investigative/marine_investigative_dashboard.dart';
import 'package:environment_monitoring_app/screens/marine/investigative/marine_investigative_info_centre_document.dart';
import 'package:environment_monitoring_app/screens/marine/investigative/overview.dart' as marineInvestigativeOverview;
import 'package:environment_monitoring_app/screens/marine/investigative/entry.dart' as marineInvestigativeEntry;
import 'package:environment_monitoring_app/screens/marine/investigative/report.dart' as marineInvestigativeReport;
@ -116,10 +113,8 @@ void main() async {
Provider<TelegramService>(create: (_) => telegramService),
Provider(create: (_) => LocalStorageService()),
// MODIFIED: Provide all dedicated services with their required dependencies
Provider(create: (context) => RiverInSituSamplingService(telegramService)),
Provider(create: (context) => AirSamplingService(databaseHelper, telegramService)),
// FIX: Pass the global telegramService to the marine service constructors
Provider(create: (context) => MarineInSituSamplingService(telegramService)),
Provider(create: (context) => MarineTarballSamplingService(telegramService)),
],
@ -201,7 +196,7 @@ class RootApp extends StatelessWidget {
'/marine/home': (context) => const MarineHomePage(),
// Air Manual
'/air/manual/dashboard': (context) => AirManualDashboard(),
'/air/manual/info': (context) => const AirManualInfoCentreDocument(),
'/air/manual/installation': (context) => const AirManualInstallationScreen(),
'/air/manual/collection': (context) => const AirManualCollectionScreen(),
'/air/manual/report': (context) => airManualReport.AirManualReport(),
@ -209,19 +204,19 @@ class RootApp extends StatelessWidget {
'/air/manual/image-request': (context) => airManualImageRequest.AirManualImageRequest(),
// Air Continuous
'/air/continuous/dashboard': (context) => AirContinuousDashboard(),
'/air/continuous/info': (context) => const AirContinuousInfoCentreDocument(),
'/air/continuous/overview': (context) => airContinuousOverview.OverviewScreen(),
'/air/continuous/entry': (context) => airContinuousEntry.EntryScreen(),
'/air/continuous/report': (context) => airContinuousReport.ReportScreen(),
// Air Investigative
'/air/investigative/dashboard': (context) => AirInvestigativeDashboard(),
'/air/investigative/info': (context) => const AirInvestigativeInfoCentreDocument(),
'/air/investigative/overview': (context) => airInvestigativeOverview.OverviewScreen(),
'/air/investigative/entry': (context) => airInvestigativeEntry.EntryScreen(),
'/air/investigative/report': (context) => airInvestigativeReport.ReportScreen(),
// River Manual
'/river/manual/dashboard': (context) => RiverManualDashboard(),
'/river/manual/info': (context) => const RiverManualInfoCentreDocument(),
'/river/manual/in-situ': (context) => riverManualInSituSampling.RiverInSituSamplingScreen(),
'/river/manual/report': (context) => riverManualReport.RiverManualReport(),
'/river/manual/triennial': (context) => riverManualTriennialSampling.RiverTriennialSampling(),
@ -229,19 +224,18 @@ class RootApp extends StatelessWidget {
'/river/manual/image-request': (context) => riverManualImageRequest.RiverManualImageRequest(),
// River Continuous
'/river/continuous/dashboard': (context) => RiverContinuousDashboard(),
'/river/continuous/info': (context) => const RiverContinuousInfoCentreDocument(),
'/river/continuous/overview': (context) => riverContinuousOverview.OverviewScreen(),
'/river/continuous/entry': (context) => riverContinuousEntry.EntryScreen(),
'/river/continuous/report': (context) => riverContinuousReport.ReportScreen(),
// River Investigative
'/river/investigative/dashboard': (context) => RiverInvestigativeDashboard(),
'/river/investigative/info': (context) => const RiverInvestigativeInfoCentreDocument(),
'/river/investigative/overview': (context) => riverInvestigativeOverview.OverviewScreen(),
'/river/investigative/entry': (context) => riverInvestigativeEntry.EntryScreen(),
'/river/investigative/report': (context) => riverInvestigativeReport.ReportScreen(),
// Marine Manual
'/marine/manual/dashboard': (context) => MarineManualDashboard(),
'/marine/manual/info': (context) => marineManualInfoCentreDocument.MarineInfoCentreDocument(),
'/marine/manual/pre-sampling': (context) => marineManualPreSampling.MarinePreSampling(),
'/marine/manual/in-situ': (context) => marineManualInSituSampling.MarineInSituSampling(),
@ -251,13 +245,13 @@ class RootApp extends StatelessWidget {
'/marine/manual/image-request': (context) => marineManualImageRequest.MarineManualImageRequest(),
// Marine Continuous
'/marine/continuous/dashboard': (context) => MarineContinuousDashboard(),
'/marine/continuous/info': (context) => const MarineContinuousInfoCentreDocument(),
'/marine/continuous/overview': (context) => marineContinuousOverview.OverviewScreen(),
'/marine/continuous/entry': (context) => marineContinuousEntry.EntryScreen(),
'/marine/continuous/report': (context) => marineContinuousReport.ReportScreen(),
// Marine Investigative
'/marine/investigative/dashboard': (context) => MarineInvestigativeDashboard(),
'/marine/investigative/info': (context) => const MarineInvestigativeInfoCentreDocument(),
'/marine/investigative/overview': (context) => marineInvestigativeOverview.OverviewScreen(),
'/marine/investigative/entry': (context) => marineInvestigativeEntry.EntryScreen(),
'/marine/investigative/report': (context) => marineInvestigativeReport.ReportScreen(),
@ -273,14 +267,20 @@ class SplashScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Scaffold(
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 20),
Text('Loading app data...', style: TextStyle(fontSize: 16)),
Image.asset(
'assets/icon4.png',
height: 360,
width: 360,
),
const SizedBox(height: 24),
const CircularProgressIndicator(),
const SizedBox(height: 20),
const Text('Loading app data...', style: TextStyle(fontSize: 16)),
],
),
),

View File

@ -1,3 +1,5 @@
//lib/screens/river/air_home_page.dart
import 'package:flutter/material.dart';
// Re-defining SidebarItem here for self-containment,
@ -30,12 +32,12 @@ class AirHomePage extends StatelessWidget {
label: "Manual",
isParent: true,
children: [
//SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/manual/dashboard'),
// MODIFIED: Added Info Centre Document link for consistency
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/air/manual/info'),
SidebarItem(icon: Icons.construction, label: "Installation", route: '/air/manual/installation'),
SidebarItem(icon: Icons.inventory_2, label: "Collection", route: '/air/manual/collection'),
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/manual/report'),
SidebarItem(icon: Icons.article, label: "Data Log", route: '/air/manual/data-log'),
SidebarItem(icon: Icons.image, label: "Image Request", route: '/air/manual/image-request'),
//SidebarItem(icon: Icons.image, label: "Image Request", route: '/air/manual/image-request'),
],
),
SidebarItem(
@ -43,10 +45,11 @@ class AirHomePage extends StatelessWidget {
label: "Continuous",
isParent: true,
children: [
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/continuous/dashboard'),
SidebarItem(icon: Icons.info, label: "Overview", route: '/air/continuous/overview'),
SidebarItem(icon: Icons.input, label: "Entry", route: '/air/continuous/entry'),
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/continuous/report'),
// MODIFIED: Updated to point to the new Info Centre screen
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/air/continuous/info'),
//SidebarItem(icon: Icons.info, label: "Overview", route: '/air/continuous/overview'),
//SidebarItem(icon: Icons.input, label: "Entry", route: '/air/continuous/entry'),
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/continuous/report'),
],
),
SidebarItem(
@ -54,10 +57,11 @@ class AirHomePage extends StatelessWidget {
label: "Investigative",
isParent: true,
children: [
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/investigative/dashboard'),
SidebarItem(icon: Icons.info, label: "Overview", route: '/air/investigative/overview'),
SidebarItem(icon: Icons.input, label: "Entry", route: '/air/investigative/entry'),
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/investigative/report'),
// MODIFIED: Updated to point to the new Info Centre screen
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/air/investigative/info'),
//SidebarItem(icon: Icons.info, label: "Overview", route: '/air/investigative/overview'),
//SidebarItem(icon: Icons.input, label: "Entry", route: '/air/investigative/entry'),
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/investigative/report'),
],
),
];

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
class AirContinuousDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Air Continuous Monitoring")),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Continuous Monitoring", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 24),
Wrap(
spacing: 16,
runSpacing: 16,
children: [
_buildNavButton(context, "Overview", Icons.info, '/air/continuous/overview'),
_buildNavButton(context, "Entry", Icons.edit, '/air/continuous/entry'),
_buildNavButton(context, "Report", Icons.insert_chart, '/air/continuous/report'),
],
),
],
),
),
);
}
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
return ElevatedButton.icon(
onPressed: () => Navigator.pushNamed(context, route),
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
backgroundColor: Colors.blue[800],
foregroundColor: Colors.white,
),
);
}
}

View File

@ -0,0 +1,272 @@
//lib/screens/air/continuous/air_continuous_info_centre_document.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:environment_monitoring_app/services/local_storage_service.dart';
import '../../../auth_provider.dart';
// This model is used generically across departments
class MarineDocument {
final int id;
final String title;
final String module;
final String? group;
final String url;
final String? departmentName;
MarineDocument({
required this.id,
required this.title,
required this.module,
this.group,
required this.url,
this.departmentName,
});
factory MarineDocument.fromMap(Map<String, dynamic> map) {
return MarineDocument(
id: map['id'],
title: map['title'],
module: map['module'],
group: map['group'],
url: map['url'],
departmentName: map['department_name'],
);
}
}
class AirContinuousInfoCentreDocument extends StatefulWidget {
const AirContinuousInfoCentreDocument({super.key});
@override
State<AirContinuousInfoCentreDocument> createState() => _AirContinuousInfoCentreDocumentState();
}
class _AirContinuousInfoCentreDocumentState extends State<AirContinuousInfoCentreDocument> {
final LocalStorageService _localStorageService = LocalStorageService();
late List<MarineDocument> _documents;
List<String> _documentGroups = [];
String? _selectedGroup;
Set<String> _downloadedUrls = {};
Map<String, double> _downloadProgress = {};
@override
void initState() {
super.initState();
_documents = _getFilteredDocumentsFromProvider();
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
_documentGroups = ['All Documents', ...groups];
_selectedGroup = _documentGroups.first;
_checkInitialDownloadStatus();
}
List<MarineDocument> _getFilteredDocumentsFromProvider() {
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
if (documentsData == null || documentsData.isEmpty) {
return [];
}
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
return allDocuments.where((doc) {
return doc.departmentName == 'Air' && doc.module == 'Continuous';
}).toList();
}
Future<void> _checkInitialDownloadStatus() async {
final downloaded = <String>{};
for (var doc in _documents) {
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
downloaded.add(doc.url);
}
}
if (mounted) {
setState(() {
_downloadedUrls = downloaded;
});
}
}
Future<void> _handleDownload(MarineDocument doc) async {
setState(() {
_downloadProgress[doc.url] = 0.0;
});
try {
await _localStorageService.downloadDocument(
docUrl: doc.url,
onReceiveProgress: (progress) {
if (mounted) {
setState(() {
_downloadProgress[doc.url] = progress;
});
}
},
);
if (mounted) {
setState(() {
_downloadedUrls.add(doc.url);
_downloadProgress.remove(doc.url);
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Download failed for ${doc.title}')),
);
setState(() {
_downloadProgress.remove(doc.url);
});
}
}
}
Future<void> _viewDocument(MarineDocument doc) async {
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
if (localPath != null && mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PdfViewerScreen(
filePath: localPath,
title: doc.title,
),
),
);
}
}
Widget _buildTrailingWidget(MarineDocument doc) {
if (_downloadProgress.containsKey(doc.url)) {
return SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _downloadProgress[doc.url],
strokeWidth: 3,
),
);
}
if (_downloadedUrls.contains(doc.url)) {
return IconButton(
icon: const Icon(Icons.visibility, color: Colors.green),
tooltip: 'View Document',
onPressed: () => _viewDocument(doc),
);
}
return IconButton(
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
tooltip: 'Download for Offline',
onPressed: () => _handleDownload(doc),
);
}
@override
Widget build(BuildContext context) {
final filteredDocs = _selectedGroup == 'All Documents'
? _documents
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
return Scaffold(
appBar: AppBar(
title: const Text("Air Continuous Documents"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<String>(
value: _selectedGroup,
decoration: InputDecoration(
labelText: 'Filter by Group',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
items: _documentGroups.map((String group) {
return DropdownMenuItem<String>(
value: group,
child: Text(group),
);
}).toList(),
onChanged: (newValue) {
setState(() {
_selectedGroup = newValue;
});
},
),
const SizedBox(height: 20),
Text(
_selectedGroup!,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const Divider(height: 20),
Expanded(
child: _documents.isEmpty
? const Center(child: Text("No Air Continuous documents found."))
: ListView.builder(
itemCount: filteredDocs.length,
itemBuilder: (context, index) {
final doc = filteredDocs[index];
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 6.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.red.shade100,
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
),
title: Text(doc.title),
trailing: _buildTrailingWidget(doc),
onTap: _downloadedUrls.contains(doc.url)
? () => _viewDocument(doc)
: null,
),
);
},
),
),
],
),
),
);
}
}
class PdfViewerScreen extends StatelessWidget {
final String filePath;
final String title;
const PdfViewerScreen({
super.key,
required this.filePath,
required this.title,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: PDFView(
filePath: filePath,
enableSwipe: true,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
),
);
}
}

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
class AirInvestigativeDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Air Investigative Study")),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Investigative Study", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 24),
Wrap(
spacing: 16,
runSpacing: 16,
children: [
_buildNavButton(context, "Overview", Icons.info, '/air/investigative/overview'),
_buildNavButton(context, "Entry", Icons.edit, '/air/investigative/entry'),
_buildNavButton(context, "Report", Icons.insert_chart, '/air/investigative/report'),
],
),
],
),
),
);
}
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
return ElevatedButton.icon(
onPressed: () => Navigator.pushNamed(context, route),
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
backgroundColor: Colors.blue[800],
foregroundColor: Colors.white,
),
);
}
}

View File

@ -0,0 +1,272 @@
//lib/screens/air/investigative/air_investigative_info_centre_document.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:environment_monitoring_app/services/local_storage_service.dart';
import '../../../auth_provider.dart';
// This model is used generically across departments
class MarineDocument {
final int id;
final String title;
final String module;
final String? group;
final String url;
final String? departmentName;
MarineDocument({
required this.id,
required this.title,
required this.module,
this.group,
required this.url,
this.departmentName,
});
factory MarineDocument.fromMap(Map<String, dynamic> map) {
return MarineDocument(
id: map['id'],
title: map['title'],
module: map['module'],
group: map['group'],
url: map['url'],
departmentName: map['department_name'],
);
}
}
class AirInvestigativeInfoCentreDocument extends StatefulWidget {
const AirInvestigativeInfoCentreDocument({super.key});
@override
State<AirInvestigativeInfoCentreDocument> createState() => _AirInvestigativeInfoCentreDocumentState();
}
class _AirInvestigativeInfoCentreDocumentState extends State<AirInvestigativeInfoCentreDocument> {
final LocalStorageService _localStorageService = LocalStorageService();
late List<MarineDocument> _documents;
List<String> _documentGroups = [];
String? _selectedGroup;
Set<String> _downloadedUrls = {};
Map<String, double> _downloadProgress = {};
@override
void initState() {
super.initState();
_documents = _getFilteredDocumentsFromProvider();
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
_documentGroups = ['All Documents', ...groups];
_selectedGroup = _documentGroups.first;
_checkInitialDownloadStatus();
}
List<MarineDocument> _getFilteredDocumentsFromProvider() {
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
if (documentsData == null || documentsData.isEmpty) {
return [];
}
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
return allDocuments.where((doc) {
return doc.departmentName == 'Air' && doc.module == 'Investigative';
}).toList();
}
Future<void> _checkInitialDownloadStatus() async {
final downloaded = <String>{};
for (var doc in _documents) {
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
downloaded.add(doc.url);
}
}
if (mounted) {
setState(() {
_downloadedUrls = downloaded;
});
}
}
Future<void> _handleDownload(MarineDocument doc) async {
setState(() {
_downloadProgress[doc.url] = 0.0;
});
try {
await _localStorageService.downloadDocument(
docUrl: doc.url,
onReceiveProgress: (progress) {
if (mounted) {
setState(() {
_downloadProgress[doc.url] = progress;
});
}
},
);
if (mounted) {
setState(() {
_downloadedUrls.add(doc.url);
_downloadProgress.remove(doc.url);
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Download failed for ${doc.title}')),
);
setState(() {
_downloadProgress.remove(doc.url);
});
}
}
}
Future<void> _viewDocument(MarineDocument doc) async {
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
if (localPath != null && mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PdfViewerScreen(
filePath: localPath,
title: doc.title,
),
),
);
}
}
Widget _buildTrailingWidget(MarineDocument doc) {
if (_downloadProgress.containsKey(doc.url)) {
return SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _downloadProgress[doc.url],
strokeWidth: 3,
),
);
}
if (_downloadedUrls.contains(doc.url)) {
return IconButton(
icon: const Icon(Icons.visibility, color: Colors.green),
tooltip: 'View Document',
onPressed: () => _viewDocument(doc),
);
}
return IconButton(
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
tooltip: 'Download for Offline',
onPressed: () => _handleDownload(doc),
);
}
@override
Widget build(BuildContext context) {
final filteredDocs = _selectedGroup == 'All Documents'
? _documents
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
return Scaffold(
appBar: AppBar(
title: const Text("Air Investigative Documents"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<String>(
value: _selectedGroup,
decoration: InputDecoration(
labelText: 'Filter by Group',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
items: _documentGroups.map((String group) {
return DropdownMenuItem<String>(
value: group,
child: Text(group),
);
}).toList(),
onChanged: (newValue) {
setState(() {
_selectedGroup = newValue;
});
},
),
const SizedBox(height: 20),
Text(
_selectedGroup!,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const Divider(height: 20),
Expanded(
child: _documents.isEmpty
? const Center(child: Text("No Air Investigative documents found."))
: ListView.builder(
itemCount: filteredDocs.length,
itemBuilder: (context, index) {
final doc = filteredDocs[index];
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 6.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.red.shade100,
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
),
title: Text(doc.title),
trailing: _buildTrailingWidget(doc),
onTap: _downloadedUrls.contains(doc.url)
? () => _viewDocument(doc)
: null,
),
);
},
),
),
],
),
),
);
}
}
class PdfViewerScreen extends StatelessWidget {
final String filePath;
final String title;
const PdfViewerScreen({
super.key,
required this.filePath,
required this.title,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: PDFView(
filePath: filePath,
enableSwipe: true,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
),
);
}
}

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
class AirManualDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Air Manual Sampling")),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Manual Sampling", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 24),
Wrap(
spacing: 16,
runSpacing: 16,
children: [
_buildNavButton(context, "Overview", Icons.info, '/air/manual/overview'),
_buildNavButton(context, "Entry", Icons.edit, '/air/manual/entry'),
_buildNavButton(context, "Report", Icons.insert_chart, '/air/manual/report'),
],
),
],
),
),
);
}
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
return ElevatedButton.icon(
onPressed: () => Navigator.pushNamed(context, route),
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
backgroundColor: Colors.blue[800],
foregroundColor: Colors.white,
),
);
}
}

View File

@ -0,0 +1,272 @@
//lib/screens/air/manual/air_manual_info_centre_document.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:environment_monitoring_app/services/local_storage_service.dart';
import '../../../auth_provider.dart';
// This model is used generically across departments
class MarineDocument {
final int id;
final String title;
final String module;
final String? group;
final String url;
final String? departmentName;
MarineDocument({
required this.id,
required this.title,
required this.module,
this.group,
required this.url,
this.departmentName,
});
factory MarineDocument.fromMap(Map<String, dynamic> map) {
return MarineDocument(
id: map['id'],
title: map['title'],
module: map['module'],
group: map['group'],
url: map['url'],
departmentName: map['department_name'],
);
}
}
class AirManualInfoCentreDocument extends StatefulWidget {
const AirManualInfoCentreDocument({super.key});
@override
State<AirManualInfoCentreDocument> createState() => _AirManualInfoCentreDocumentState();
}
class _AirManualInfoCentreDocumentState extends State<AirManualInfoCentreDocument> {
final LocalStorageService _localStorageService = LocalStorageService();
late List<MarineDocument> _documents;
List<String> _documentGroups = [];
String? _selectedGroup;
Set<String> _downloadedUrls = {};
Map<String, double> _downloadProgress = {};
@override
void initState() {
super.initState();
_documents = _getFilteredDocumentsFromProvider();
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
_documentGroups = ['All Documents', ...groups];
_selectedGroup = _documentGroups.first;
_checkInitialDownloadStatus();
}
List<MarineDocument> _getFilteredDocumentsFromProvider() {
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
if (documentsData == null || documentsData.isEmpty) {
return [];
}
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
return allDocuments.where((doc) {
return doc.departmentName == 'Air' && doc.module == 'Manual';
}).toList();
}
Future<void> _checkInitialDownloadStatus() async {
final downloaded = <String>{};
for (var doc in _documents) {
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
downloaded.add(doc.url);
}
}
if (mounted) {
setState(() {
_downloadedUrls = downloaded;
});
}
}
Future<void> _handleDownload(MarineDocument doc) async {
setState(() {
_downloadProgress[doc.url] = 0.0;
});
try {
await _localStorageService.downloadDocument(
docUrl: doc.url,
onReceiveProgress: (progress) {
if (mounted) {
setState(() {
_downloadProgress[doc.url] = progress;
});
}
},
);
if (mounted) {
setState(() {
_downloadedUrls.add(doc.url);
_downloadProgress.remove(doc.url);
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Download failed for ${doc.title}')),
);
setState(() {
_downloadProgress.remove(doc.url);
});
}
}
}
Future<void> _viewDocument(MarineDocument doc) async {
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
if (localPath != null && mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PdfViewerScreen(
filePath: localPath,
title: doc.title,
),
),
);
}
}
Widget _buildTrailingWidget(MarineDocument doc) {
if (_downloadProgress.containsKey(doc.url)) {
return SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _downloadProgress[doc.url],
strokeWidth: 3,
),
);
}
if (_downloadedUrls.contains(doc.url)) {
return IconButton(
icon: const Icon(Icons.visibility, color: Colors.green),
tooltip: 'View Document',
onPressed: () => _viewDocument(doc),
);
}
return IconButton(
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
tooltip: 'Download for Offline',
onPressed: () => _handleDownload(doc),
);
}
@override
Widget build(BuildContext context) {
final filteredDocs = _selectedGroup == 'All Documents'
? _documents
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
return Scaffold(
appBar: AppBar(
title: const Text("Air Manual Documents"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<String>(
value: _selectedGroup,
decoration: InputDecoration(
labelText: 'Filter by Group',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
items: _documentGroups.map((String group) {
return DropdownMenuItem<String>(
value: group,
child: Text(group),
);
}).toList(),
onChanged: (newValue) {
setState(() {
_selectedGroup = newValue;
});
},
),
const SizedBox(height: 20),
Text(
_selectedGroup!,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const Divider(height: 20),
Expanded(
child: _documents.isEmpty
? const Center(child: Text("No Air Manual documents found."))
: ListView.builder(
itemCount: filteredDocs.length,
itemBuilder: (context, index) {
final doc = filteredDocs[index];
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 6.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.red.shade100,
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
),
title: Text(doc.title),
trailing: _buildTrailingWidget(doc),
onTap: _downloadedUrls.contains(doc.url)
? () => _viewDocument(doc)
: null,
),
);
},
),
),
],
),
),
);
}
}
class PdfViewerScreen extends StatelessWidget {
final String filePath;
final String title;
const PdfViewerScreen({
super.key,
required this.filePath,
required this.title,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: PDFView(
filePath: filePath,
enableSwipe: true,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
),
);
}
}

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
class MarineContinuousDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Marine Continuous Monitoring")),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Continuous Monitoring", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 24),
Wrap(
spacing: 16,
runSpacing: 16,
children: [
_buildNavButton(context, "Overview", Icons.info, '/marine/continuous/overview'),
_buildNavButton(context, "Entry", Icons.edit, '/marine/continuous/entry'),
_buildNavButton(context, "Report", Icons.insert_chart, '/marine/continuous/report'),
],
),
],
),
),
);
}
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
return ElevatedButton.icon(
onPressed: () => Navigator.pushNamed(context, route),
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
backgroundColor: Colors.blue[800],
foregroundColor: Colors.white,
),
);
}
}

View File

@ -0,0 +1,272 @@
//lib/screens/marine/continuous/marine_continuous_info_centre_document.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:environment_monitoring_app/services/local_storage_service.dart';
import '../../../auth_provider.dart';
// This model is used generically across departments
class MarineDocument {
final int id;
final String title;
final String module;
final String? group;
final String url;
final String? departmentName;
MarineDocument({
required this.id,
required this.title,
required this.module,
this.group,
required this.url,
this.departmentName,
});
factory MarineDocument.fromMap(Map<String, dynamic> map) {
return MarineDocument(
id: map['id'],
title: map['title'],
module: map['module'],
group: map['group'],
url: map['url'],
departmentName: map['department_name'],
);
}
}
class MarineContinuousInfoCentreDocument extends StatefulWidget {
const MarineContinuousInfoCentreDocument({super.key});
@override
State<MarineContinuousInfoCentreDocument> createState() => _MarineContinuousInfoCentreDocumentState();
}
class _MarineContinuousInfoCentreDocumentState extends State<MarineContinuousInfoCentreDocument> {
final LocalStorageService _localStorageService = LocalStorageService();
late List<MarineDocument> _documents;
List<String> _documentGroups = [];
String? _selectedGroup;
Set<String> _downloadedUrls = {};
Map<String, double> _downloadProgress = {};
@override
void initState() {
super.initState();
_documents = _getFilteredDocumentsFromProvider();
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
_documentGroups = ['All Documents', ...groups];
_selectedGroup = _documentGroups.first;
_checkInitialDownloadStatus();
}
List<MarineDocument> _getFilteredDocumentsFromProvider() {
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
if (documentsData == null || documentsData.isEmpty) {
return [];
}
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
return allDocuments.where((doc) {
return doc.departmentName == 'Marine' && doc.module == 'Continuous';
}).toList();
}
Future<void> _checkInitialDownloadStatus() async {
final downloaded = <String>{};
for (var doc in _documents) {
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
downloaded.add(doc.url);
}
}
if (mounted) {
setState(() {
_downloadedUrls = downloaded;
});
}
}
Future<void> _handleDownload(MarineDocument doc) async {
setState(() {
_downloadProgress[doc.url] = 0.0;
});
try {
await _localStorageService.downloadDocument(
docUrl: doc.url,
onReceiveProgress: (progress) {
if (mounted) {
setState(() {
_downloadProgress[doc.url] = progress;
});
}
},
);
if (mounted) {
setState(() {
_downloadedUrls.add(doc.url);
_downloadProgress.remove(doc.url);
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Download failed for ${doc.title}')),
);
setState(() {
_downloadProgress.remove(doc.url);
});
}
}
}
Future<void> _viewDocument(MarineDocument doc) async {
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
if (localPath != null && mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PdfViewerScreen(
filePath: localPath,
title: doc.title,
),
),
);
}
}
Widget _buildTrailingWidget(MarineDocument doc) {
if (_downloadProgress.containsKey(doc.url)) {
return SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _downloadProgress[doc.url],
strokeWidth: 3,
),
);
}
if (_downloadedUrls.contains(doc.url)) {
return IconButton(
icon: const Icon(Icons.visibility, color: Colors.green),
tooltip: 'View Document',
onPressed: () => _viewDocument(doc),
);
}
return IconButton(
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
tooltip: 'Download for Offline',
onPressed: () => _handleDownload(doc),
);
}
@override
Widget build(BuildContext context) {
final filteredDocs = _selectedGroup == 'All Documents'
? _documents
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
return Scaffold(
appBar: AppBar(
title: const Text("Marine Continuous Documents"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<String>(
value: _selectedGroup,
decoration: InputDecoration(
labelText: 'Filter by Group',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
items: _documentGroups.map((String group) {
return DropdownMenuItem<String>(
value: group,
child: Text(group),
);
}).toList(),
onChanged: (newValue) {
setState(() {
_selectedGroup = newValue;
});
},
),
const SizedBox(height: 20),
Text(
_selectedGroup!,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const Divider(height: 20),
Expanded(
child: _documents.isEmpty
? const Center(child: Text("No Marine Continuous documents found."))
: ListView.builder(
itemCount: filteredDocs.length,
itemBuilder: (context, index) {
final doc = filteredDocs[index];
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 6.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.red.shade100,
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
),
title: Text(doc.title),
trailing: _buildTrailingWidget(doc),
onTap: _downloadedUrls.contains(doc.url)
? () => _viewDocument(doc)
: null,
),
);
},
),
),
],
),
),
);
}
}
class PdfViewerScreen extends StatelessWidget {
final String filePath;
final String title;
const PdfViewerScreen({
super.key,
required this.filePath,
required this.title,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: PDFView(
filePath: filePath,
enableSwipe: true,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
),
);
}
}

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
class MarineInvestigativeDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Marine Investigative Study")),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Investigative Study", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 24),
Wrap(
spacing: 16,
runSpacing: 16,
children: [
_buildNavButton(context, "Overview", Icons.info, '/marine/investigative/overview'),
_buildNavButton(context, "Entry", Icons.edit, '/marine/investigative/entry'),
_buildNavButton(context, "Report", Icons.insert_chart, '/marine/investigative/report'),
],
),
],
),
),
);
}
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
return ElevatedButton.icon(
onPressed: () => Navigator.pushNamed(context, route),
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
backgroundColor: Colors.blue[800],
foregroundColor: Colors.white,
),
);
}
}

View File

@ -0,0 +1,272 @@
//lib/screens/marine/investigative/marine_investigative_info_centre_document.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:environment_monitoring_app/services/local_storage_service.dart';
import '../../../auth_provider.dart';
// This model is used generically across departments
class MarineDocument {
final int id;
final String title;
final String module;
final String? group;
final String url;
final String? departmentName;
MarineDocument({
required this.id,
required this.title,
required this.module,
this.group,
required this.url,
this.departmentName,
});
factory MarineDocument.fromMap(Map<String, dynamic> map) {
return MarineDocument(
id: map['id'],
title: map['title'],
module: map['module'],
group: map['group'],
url: map['url'],
departmentName: map['department_name'],
);
}
}
class MarineInvestigativeInfoCentreDocument extends StatefulWidget {
const MarineInvestigativeInfoCentreDocument({super.key});
@override
State<MarineInvestigativeInfoCentreDocument> createState() => _MarineInvestigativeInfoCentreDocumentState();
}
class _MarineInvestigativeInfoCentreDocumentState extends State<MarineInvestigativeInfoCentreDocument> {
final LocalStorageService _localStorageService = LocalStorageService();
late List<MarineDocument> _documents;
List<String> _documentGroups = [];
String? _selectedGroup;
Set<String> _downloadedUrls = {};
Map<String, double> _downloadProgress = {};
@override
void initState() {
super.initState();
_documents = _getFilteredDocumentsFromProvider();
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
_documentGroups = ['All Documents', ...groups];
_selectedGroup = _documentGroups.first;
_checkInitialDownloadStatus();
}
List<MarineDocument> _getFilteredDocumentsFromProvider() {
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
if (documentsData == null || documentsData.isEmpty) {
return [];
}
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
return allDocuments.where((doc) {
return doc.departmentName == 'Marine' && doc.module == 'Investigative';
}).toList();
}
Future<void> _checkInitialDownloadStatus() async {
final downloaded = <String>{};
for (var doc in _documents) {
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
downloaded.add(doc.url);
}
}
if (mounted) {
setState(() {
_downloadedUrls = downloaded;
});
}
}
Future<void> _handleDownload(MarineDocument doc) async {
setState(() {
_downloadProgress[doc.url] = 0.0;
});
try {
await _localStorageService.downloadDocument(
docUrl: doc.url,
onReceiveProgress: (progress) {
if (mounted) {
setState(() {
_downloadProgress[doc.url] = progress;
});
}
},
);
if (mounted) {
setState(() {
_downloadedUrls.add(doc.url);
_downloadProgress.remove(doc.url);
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Download failed for ${doc.title}')),
);
setState(() {
_downloadProgress.remove(doc.url);
});
}
}
}
Future<void> _viewDocument(MarineDocument doc) async {
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
if (localPath != null && mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PdfViewerScreen(
filePath: localPath,
title: doc.title,
),
),
);
}
}
Widget _buildTrailingWidget(MarineDocument doc) {
if (_downloadProgress.containsKey(doc.url)) {
return SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _downloadProgress[doc.url],
strokeWidth: 3,
),
);
}
if (_downloadedUrls.contains(doc.url)) {
return IconButton(
icon: const Icon(Icons.visibility, color: Colors.green),
tooltip: 'View Document',
onPressed: () => _viewDocument(doc),
);
}
return IconButton(
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
tooltip: 'Download for Offline',
onPressed: () => _handleDownload(doc),
);
}
@override
Widget build(BuildContext context) {
final filteredDocs = _selectedGroup == 'All Documents'
? _documents
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
return Scaffold(
appBar: AppBar(
title: const Text("Marine Investigative Documents"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<String>(
value: _selectedGroup,
decoration: InputDecoration(
labelText: 'Filter by Group',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
items: _documentGroups.map((String group) {
return DropdownMenuItem<String>(
value: group,
child: Text(group),
);
}).toList(),
onChanged: (newValue) {
setState(() {
_selectedGroup = newValue;
});
},
),
const SizedBox(height: 20),
Text(
_selectedGroup!,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const Divider(height: 20),
Expanded(
child: _documents.isEmpty
? const Center(child: Text("No Marine Investigative documents found."))
: ListView.builder(
itemCount: filteredDocs.length,
itemBuilder: (context, index) {
final doc = filteredDocs[index];
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 6.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.red.shade100,
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
),
title: Text(doc.title),
trailing: _buildTrailingWidget(doc),
onTap: _downloadedUrls.contains(doc.url)
? () => _viewDocument(doc)
: null,
),
);
},
),
),
],
),
),
);
}
}
class PdfViewerScreen extends StatelessWidget {
final String filePath;
final String title;
const PdfViewerScreen({
super.key,
required this.filePath,
required this.title,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: PDFView(
filePath: filePath,
enableSwipe: true,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
),
);
}
}

View File

@ -1,54 +1,259 @@
// lib/screens/marine/manual/info_centre_document.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:provider/provider.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:environment_monitoring_app/services/local_storage_service.dart';
import '../../../auth_provider.dart';
// (The MarineDocument model class remains the same)
class MarineDocument {
final int id;
final String title;
final String module;
final String? group;
final String url;
final String? departmentName;
MarineDocument({
required this.id,
required this.title,
required this.module,
this.group,
required this.url,
this.departmentName,
});
factory MarineDocument.fromMap(Map<String, dynamic> map) {
return MarineDocument(
id: map['id'],
title: map['title'],
module: map['module'],
group: map['group'],
url: map['url'],
departmentName: map['department_name'],
);
}
}
class MarineInfoCentreDocument extends StatefulWidget {
const MarineInfoCentreDocument({super.key});
@override
State<MarineInfoCentreDocument> createState() => _MarineInfoCentreDocumentState();
}
class _MarineInfoCentreDocumentState extends State<MarineInfoCentreDocument> {
String? selectedFileName;
final LocalStorageService _localStorageService = LocalStorageService();
late List<MarineDocument> _documents;
Future<void> _pickDocument() async {
final result = await FilePicker.platform.pickFiles();
if (result != null && result.files.isNotEmpty) {
setState(() => selectedFileName = result.files.first.name);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Document '${result.files.first.name}' selected")),
// --- STATE FOR NEW UI ---
// Holds all available groups for the dropdown
List<String> _documentGroups = [];
// Holds the currently selected group from the dropdown
String? _selectedGroup;
Set<String> _downloadedUrls = {};
Map<String, double> _downloadProgress = {};
@override
void initState() {
super.initState();
_documents = _getFilteredDocumentsFromProvider();
// --- NEW: Populate the list of groups for the dropdown ---
// Get unique group names from the documents
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
// Add "All Documents" as the first option
_documentGroups = ['All Documents', ...groups];
// Set the initial value of the dropdown
_selectedGroup = _documentGroups.first;
_checkInitialDownloadStatus();
}
List<MarineDocument> _getFilteredDocumentsFromProvider() {
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
if (documentsData == null || documentsData.isEmpty) {
return [];
}
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
return allDocuments.where((doc) {
return doc.departmentName == 'Marine' && doc.module == 'Manual';
}).toList();
}
Future<void> _checkInitialDownloadStatus() async {
final downloaded = <String>{};
for (var doc in _documents) {
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
downloaded.add(doc.url);
}
}
if (mounted) {
setState(() {
_downloadedUrls = downloaded;
});
}
}
Future<void> _handleDownload(MarineDocument doc) async {
setState(() {
_downloadProgress[doc.url] = 0.0;
});
try {
await _localStorageService.downloadDocument(
docUrl: doc.url,
onReceiveProgress: (progress) {
if (mounted) {
setState(() {
_downloadProgress[doc.url] = progress;
});
}
},
);
if (mounted) {
setState(() {
_downloadedUrls.add(doc.url);
_downloadProgress.remove(doc.url);
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Download failed for ${doc.title}')),
);
setState(() {
_downloadProgress.remove(doc.url);
});
}
}
}
Future<void> _viewDocument(MarineDocument doc) async {
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
if (localPath != null && mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PdfViewerScreen(
filePath: localPath,
title: doc.title,
),
),
);
}
}
Widget _buildTrailingWidget(MarineDocument doc) {
if (_downloadProgress.containsKey(doc.url)) {
return SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _downloadProgress[doc.url],
strokeWidth: 3,
),
);
}
if (_downloadedUrls.contains(doc.url)) {
return IconButton(
icon: const Icon(Icons.visibility, color: Colors.green),
tooltip: 'View Document',
onPressed: () => _viewDocument(doc),
);
}
return IconButton(
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
tooltip: 'Download for Offline',
onPressed: () => _handleDownload(doc),
);
}
@override
Widget build(BuildContext context) {
// --- NEW: Filter the list based on the dropdown selection ---
final filteredDocs = _selectedGroup == 'All Documents'
? _documents
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
return Scaffold(
appBar: AppBar(title: Text("Marine Info Centre Document")),
appBar: AppBar(
title: const Text("Marine Manuals"),
),
body: Padding(
padding: const EdgeInsets.all(24),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Upload or View Reference Documents", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 24),
ElevatedButton.icon(
icon: Icon(Icons.upload_file),
label: Text("Select Document"),
onPressed: _pickDocument,
),
SizedBox(height: 16),
if (selectedFileName != null)
Text("Selected: $selectedFileName", style: TextStyle(fontSize: 16)),
SizedBox(height: 24),
ElevatedButton(
onPressed: selectedFileName != null
? () {
// Submit logic here
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Document submitted")),
// --- NEW: Styled Dropdown for filtering ---
DropdownButtonFormField<String>(
value: _selectedGroup,
decoration: InputDecoration(
labelText: 'Filter by Group',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
items: _documentGroups.map((String group) {
return DropdownMenuItem<String>(
value: group,
child: Text(group),
);
}
: null,
child: Text("Submit Document"),
}).toList(),
onChanged: (newValue) {
setState(() {
_selectedGroup = newValue;
});
},
),
const SizedBox(height: 20),
// --- NEW: List title ---
Text(
_selectedGroup!,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const Divider(height: 20),
// --- NEW: Use Expanded to make the list scrollable within the Column ---
Expanded(
child: _documents.isEmpty
? const Center(child: Text("No Marine Manual documents found."))
: ListView.builder(
itemCount: filteredDocs.length,
itemBuilder: (context, index) {
final doc = filteredDocs[index];
// --- NEW: Wrap each item in a Card for modern styling ---
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 6.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
// --- NEW: Styled icon ---
leading: CircleAvatar(
backgroundColor: Colors.red.shade100,
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
),
title: Text(doc.title),
trailing: _buildTrailingWidget(doc),
onTap: _downloadedUrls.contains(doc.url)
? () => _viewDocument(doc)
: null,
),
);
},
),
),
],
),
@ -56,3 +261,31 @@ class _MarineInfoCentreDocumentState extends State<MarineInfoCentreDocument> {
);
}
}
class PdfViewerScreen extends StatelessWidget {
final String filePath;
final String title;
const PdfViewerScreen({
super.key,
required this.filePath,
required this.title,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: PDFView(
filePath: filePath,
enableSwipe: true,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
),
);
}
}

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
class MarineManualDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Marine Manual Sampling")),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Manual Sampling", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 24),
Wrap(
spacing: 16,
runSpacing: 16,
children: [
_buildNavButton(context, "Overview", Icons.info, '/marine/manual/overview'),
_buildNavButton(context, "Entry", Icons.edit, '/marine/manual/entry'),
_buildNavButton(context, "Report", Icons.insert_chart, '/marine/manual/report'),
],
),
],
),
),
);
}
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
return ElevatedButton.icon(
onPressed: () => Navigator.pushNamed(context, route),
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
backgroundColor: Colors.blue[800],
foregroundColor: Colors.white,
),
);
}
}

View File

@ -37,8 +37,8 @@ class MarineHomePage extends StatelessWidget {
SidebarItem(icon: Icons.waves, label: "Tarball Sampling", route: '/marine/manual/tarball'),
SidebarItem(icon: Icons.article, label: "Data Log", route: '/marine/manual/data-log'),
SidebarItem(icon: Icons.image, label: "Image Request", route: '/marine/manual/image-request'),
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/manual/report'),
//SidebarItem(icon: Icons.image, label: "Image Request", route: '/marine/manual/image-request'),
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/manual/report'),
],
),
SidebarItem(
@ -46,10 +46,11 @@ class MarineHomePage extends StatelessWidget {
label: "Continuous",
isParent: true,
children: [
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/marine/continuous/dashboard'),
SidebarItem(icon: Icons.info, label: "Overview", route: '/marine/continuous/overview'),
SidebarItem(icon: Icons.input, label: "Entry", route: '/marine/continuous/entry'),
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/continuous/report'),
// MODIFIED: Updated label, icon, and route for the new Info Centre screen
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/marine/continuous/info'),
//SidebarItem(icon: Icons.info, label: "Overview", route: '/marine/continuous/overview'),
//SidebarItem(icon: Icons.input, label: "Entry", route: '/marine/continuous/entry'),
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/continuous/report'),
],
),
SidebarItem(
@ -57,10 +58,11 @@ class MarineHomePage extends StatelessWidget {
label: "Investigative",
isParent: true,
children: [
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/marine/investigative/dashboard'),
SidebarItem(icon: Icons.info, label: "Overview", route: '/marine/investigative/overview'),
SidebarItem(icon: Icons.input, label: "Entry", route: '/marine/investigative/entry'),
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/investigative/report'),
// MODIFIED: Updated label, icon, and route for the new Info Centre screen
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/marine/investigative/info'),
//SidebarItem(icon: Icons.info, label: "Overview", route: '/marine/investigative/overview'),
//SidebarItem(icon: Icons.input, label: "Entry", route: '/marine/investigative/entry'),
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/investigative/report'),
],
),
];

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
class RiverContinuousDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("River Continuous Monitoring")),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Continuous Monitoring", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 24),
Wrap(
spacing: 16,
runSpacing: 16,
children: [
_buildNavButton(context, "Overview", Icons.info, '/river/continuous/overview'),
_buildNavButton(context, "Entry", Icons.edit, '/river/continuous/entry'),
_buildNavButton(context, "Report", Icons.insert_chart, '/river/continuous/report'),
],
),
],
),
),
);
}
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
return ElevatedButton.icon(
onPressed: () => Navigator.pushNamed(context, route),
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
backgroundColor: Colors.blue[800],
foregroundColor: Colors.white,
),
);
}
}

View File

@ -0,0 +1,272 @@
//lib/screens/river/continuous/river_continuous_info_centre_document.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:environment_monitoring_app/services/local_storage_service.dart';
import '../../../auth_provider.dart';
// This model is used generically across departments
class MarineDocument {
final int id;
final String title;
final String module;
final String? group;
final String url;
final String? departmentName;
MarineDocument({
required this.id,
required this.title,
required this.module,
this.group,
required this.url,
this.departmentName,
});
factory MarineDocument.fromMap(Map<String, dynamic> map) {
return MarineDocument(
id: map['id'],
title: map['title'],
module: map['module'],
group: map['group'],
url: map['url'],
departmentName: map['department_name'],
);
}
}
class RiverContinuousInfoCentreDocument extends StatefulWidget {
const RiverContinuousInfoCentreDocument({super.key});
@override
State<RiverContinuousInfoCentreDocument> createState() => _RiverContinuousInfoCentreDocumentState();
}
class _RiverContinuousInfoCentreDocumentState extends State<RiverContinuousInfoCentreDocument> {
final LocalStorageService _localStorageService = LocalStorageService();
late List<MarineDocument> _documents;
List<String> _documentGroups = [];
String? _selectedGroup;
Set<String> _downloadedUrls = {};
Map<String, double> _downloadProgress = {};
@override
void initState() {
super.initState();
_documents = _getFilteredDocumentsFromProvider();
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
_documentGroups = ['All Documents', ...groups];
_selectedGroup = _documentGroups.first;
_checkInitialDownloadStatus();
}
List<MarineDocument> _getFilteredDocumentsFromProvider() {
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
if (documentsData == null || documentsData.isEmpty) {
return [];
}
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
return allDocuments.where((doc) {
return doc.departmentName == 'River' && doc.module == 'Continuous';
}).toList();
}
Future<void> _checkInitialDownloadStatus() async {
final downloaded = <String>{};
for (var doc in _documents) {
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
downloaded.add(doc.url);
}
}
if (mounted) {
setState(() {
_downloadedUrls = downloaded;
});
}
}
Future<void> _handleDownload(MarineDocument doc) async {
setState(() {
_downloadProgress[doc.url] = 0.0;
});
try {
await _localStorageService.downloadDocument(
docUrl: doc.url,
onReceiveProgress: (progress) {
if (mounted) {
setState(() {
_downloadProgress[doc.url] = progress;
});
}
},
);
if (mounted) {
setState(() {
_downloadedUrls.add(doc.url);
_downloadProgress.remove(doc.url);
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Download failed for ${doc.title}')),
);
setState(() {
_downloadProgress.remove(doc.url);
});
}
}
}
Future<void> _viewDocument(MarineDocument doc) async {
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
if (localPath != null && mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PdfViewerScreen(
filePath: localPath,
title: doc.title,
),
),
);
}
}
Widget _buildTrailingWidget(MarineDocument doc) {
if (_downloadProgress.containsKey(doc.url)) {
return SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _downloadProgress[doc.url],
strokeWidth: 3,
),
);
}
if (_downloadedUrls.contains(doc.url)) {
return IconButton(
icon: const Icon(Icons.visibility, color: Colors.green),
tooltip: 'View Document',
onPressed: () => _viewDocument(doc),
);
}
return IconButton(
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
tooltip: 'Download for Offline',
onPressed: () => _handleDownload(doc),
);
}
@override
Widget build(BuildContext context) {
final filteredDocs = _selectedGroup == 'All Documents'
? _documents
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
return Scaffold(
appBar: AppBar(
title: const Text("River Continuous Documents"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<String>(
value: _selectedGroup,
decoration: InputDecoration(
labelText: 'Filter by Group',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
items: _documentGroups.map((String group) {
return DropdownMenuItem<String>(
value: group,
child: Text(group),
);
}).toList(),
onChanged: (newValue) {
setState(() {
_selectedGroup = newValue;
});
},
),
const SizedBox(height: 20),
Text(
_selectedGroup!,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const Divider(height: 20),
Expanded(
child: _documents.isEmpty
? const Center(child: Text("No River Continuous documents found."))
: ListView.builder(
itemCount: filteredDocs.length,
itemBuilder: (context, index) {
final doc = filteredDocs[index];
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 6.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.red.shade100,
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
),
title: Text(doc.title),
trailing: _buildTrailingWidget(doc),
onTap: _downloadedUrls.contains(doc.url)
? () => _viewDocument(doc)
: null,
),
);
},
),
),
],
),
),
);
}
}
class PdfViewerScreen extends StatelessWidget {
final String filePath;
final String title;
const PdfViewerScreen({
super.key,
required this.filePath,
required this.title,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: PDFView(
filePath: filePath,
enableSwipe: true,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
),
);
}
}

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
class RiverInvestigativeDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("River Investigative Study")),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Investigative Study", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 24),
Wrap(
spacing: 16,
runSpacing: 16,
children: [
_buildNavButton(context, "Overview", Icons.info, '/river/investigative/overview'),
_buildNavButton(context, "Entry", Icons.edit, '/river/investigative/entry'),
_buildNavButton(context, "Report", Icons.insert_chart, '/river/investigative/report'),
],
),
],
),
),
);
}
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
return ElevatedButton.icon(
onPressed: () => Navigator.pushNamed(context, route),
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
backgroundColor: Colors.blue[800],
foregroundColor: Colors.white,
),
);
}
}

View File

@ -0,0 +1,272 @@
//lib/screens/river/investigative/river_investigative_info_centre_document.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:environment_monitoring_app/services/local_storage_service.dart';
import '../../../auth_provider.dart';
// This model is used generically across departments
class MarineDocument {
final int id;
final String title;
final String module;
final String? group;
final String url;
final String? departmentName;
MarineDocument({
required this.id,
required this.title,
required this.module,
this.group,
required this.url,
this.departmentName,
});
factory MarineDocument.fromMap(Map<String, dynamic> map) {
return MarineDocument(
id: map['id'],
title: map['title'],
module: map['module'],
group: map['group'],
url: map['url'],
departmentName: map['department_name'],
);
}
}
class RiverInvestigativeInfoCentreDocument extends StatefulWidget {
const RiverInvestigativeInfoCentreDocument({super.key});
@override
State<RiverInvestigativeInfoCentreDocument> createState() => _RiverInvestigativeInfoCentreDocumentState();
}
class _RiverInvestigativeInfoCentreDocumentState extends State<RiverInvestigativeInfoCentreDocument> {
final LocalStorageService _localStorageService = LocalStorageService();
late List<MarineDocument> _documents;
List<String> _documentGroups = [];
String? _selectedGroup;
Set<String> _downloadedUrls = {};
Map<String, double> _downloadProgress = {};
@override
void initState() {
super.initState();
_documents = _getFilteredDocumentsFromProvider();
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
_documentGroups = ['All Documents', ...groups];
_selectedGroup = _documentGroups.first;
_checkInitialDownloadStatus();
}
List<MarineDocument> _getFilteredDocumentsFromProvider() {
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
if (documentsData == null || documentsData.isEmpty) {
return [];
}
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
return allDocuments.where((doc) {
return doc.departmentName == 'River' && doc.module == 'Investigative';
}).toList();
}
Future<void> _checkInitialDownloadStatus() async {
final downloaded = <String>{};
for (var doc in _documents) {
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
downloaded.add(doc.url);
}
}
if (mounted) {
setState(() {
_downloadedUrls = downloaded;
});
}
}
Future<void> _handleDownload(MarineDocument doc) async {
setState(() {
_downloadProgress[doc.url] = 0.0;
});
try {
await _localStorageService.downloadDocument(
docUrl: doc.url,
onReceiveProgress: (progress) {
if (mounted) {
setState(() {
_downloadProgress[doc.url] = progress;
});
}
},
);
if (mounted) {
setState(() {
_downloadedUrls.add(doc.url);
_downloadProgress.remove(doc.url);
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Download failed for ${doc.title}')),
);
setState(() {
_downloadProgress.remove(doc.url);
});
}
}
}
Future<void> _viewDocument(MarineDocument doc) async {
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
if (localPath != null && mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PdfViewerScreen(
filePath: localPath,
title: doc.title,
),
),
);
}
}
Widget _buildTrailingWidget(MarineDocument doc) {
if (_downloadProgress.containsKey(doc.url)) {
return SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _downloadProgress[doc.url],
strokeWidth: 3,
),
);
}
if (_downloadedUrls.contains(doc.url)) {
return IconButton(
icon: const Icon(Icons.visibility, color: Colors.green),
tooltip: 'View Document',
onPressed: () => _viewDocument(doc),
);
}
return IconButton(
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
tooltip: 'Download for Offline',
onPressed: () => _handleDownload(doc),
);
}
@override
Widget build(BuildContext context) {
final filteredDocs = _selectedGroup == 'All Documents'
? _documents
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
return Scaffold(
appBar: AppBar(
title: const Text("River Investigative Documents"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<String>(
value: _selectedGroup,
decoration: InputDecoration(
labelText: 'Filter by Group',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
items: _documentGroups.map((String group) {
return DropdownMenuItem<String>(
value: group,
child: Text(group),
);
}).toList(),
onChanged: (newValue) {
setState(() {
_selectedGroup = newValue;
});
},
),
const SizedBox(height: 20),
Text(
_selectedGroup!,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const Divider(height: 20),
Expanded(
child: _documents.isEmpty
? const Center(child: Text("No River Investigative documents found."))
: ListView.builder(
itemCount: filteredDocs.length,
itemBuilder: (context, index) {
final doc = filteredDocs[index];
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 6.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.red.shade100,
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
),
title: Text(doc.title),
trailing: _buildTrailingWidget(doc),
onTap: _downloadedUrls.contains(doc.url)
? () => _viewDocument(doc)
: null,
),
);
},
),
),
],
),
),
);
}
}
class PdfViewerScreen extends StatelessWidget {
final String filePath;
final String title;
const PdfViewerScreen({
super.key,
required this.filePath,
required this.title,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: PDFView(
filePath: filePath,
enableSwipe: true,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
),
);
}
}

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
class RiverManualDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("River Manual Sampling")),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Manual Sampling", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 24),
Wrap(
spacing: 16,
runSpacing: 16,
children: [
_buildNavButton(context, "Overview", Icons.info, '/river/manual/overview'),
_buildNavButton(context, "Entry", Icons.edit, '/river/manual/entry'),
_buildNavButton(context, "Report", Icons.insert_chart, '/river/manual/report'),
],
),
],
),
),
);
}
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
return ElevatedButton.icon(
onPressed: () => Navigator.pushNamed(context, route),
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
backgroundColor: Colors.blue[800],
foregroundColor: Colors.white,
),
);
}
}

View File

@ -0,0 +1,272 @@
//lib/screens/river/manual/river_manual_info_centre_document.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:environment_monitoring_app/services/local_storage_service.dart';
import '../../../auth_provider.dart';
// This model is used generically across departments
class MarineDocument {
final int id;
final String title;
final String module;
final String? group;
final String url;
final String? departmentName;
MarineDocument({
required this.id,
required this.title,
required this.module,
this.group,
required this.url,
this.departmentName,
});
factory MarineDocument.fromMap(Map<String, dynamic> map) {
return MarineDocument(
id: map['id'],
title: map['title'],
module: map['module'],
group: map['group'],
url: map['url'],
departmentName: map['department_name'],
);
}
}
class RiverManualInfoCentreDocument extends StatefulWidget {
const RiverManualInfoCentreDocument({super.key});
@override
State<RiverManualInfoCentreDocument> createState() => _RiverManualInfoCentreDocumentState();
}
class _RiverManualInfoCentreDocumentState extends State<RiverManualInfoCentreDocument> {
final LocalStorageService _localStorageService = LocalStorageService();
late List<MarineDocument> _documents;
List<String> _documentGroups = [];
String? _selectedGroup;
Set<String> _downloadedUrls = {};
Map<String, double> _downloadProgress = {};
@override
void initState() {
super.initState();
_documents = _getFilteredDocumentsFromProvider();
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
_documentGroups = ['All Documents', ...groups];
_selectedGroup = _documentGroups.first;
_checkInitialDownloadStatus();
}
List<MarineDocument> _getFilteredDocumentsFromProvider() {
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
if (documentsData == null || documentsData.isEmpty) {
return [];
}
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
return allDocuments.where((doc) {
return doc.departmentName == 'River' && doc.module == 'Manual';
}).toList();
}
Future<void> _checkInitialDownloadStatus() async {
final downloaded = <String>{};
for (var doc in _documents) {
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
downloaded.add(doc.url);
}
}
if (mounted) {
setState(() {
_downloadedUrls = downloaded;
});
}
}
Future<void> _handleDownload(MarineDocument doc) async {
setState(() {
_downloadProgress[doc.url] = 0.0;
});
try {
await _localStorageService.downloadDocument(
docUrl: doc.url,
onReceiveProgress: (progress) {
if (mounted) {
setState(() {
_downloadProgress[doc.url] = progress;
});
}
},
);
if (mounted) {
setState(() {
_downloadedUrls.add(doc.url);
_downloadProgress.remove(doc.url);
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Download failed for ${doc.title}')),
);
setState(() {
_downloadProgress.remove(doc.url);
});
}
}
}
Future<void> _viewDocument(MarineDocument doc) async {
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
if (localPath != null && mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PdfViewerScreen(
filePath: localPath,
title: doc.title,
),
),
);
}
}
Widget _buildTrailingWidget(MarineDocument doc) {
if (_downloadProgress.containsKey(doc.url)) {
return SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _downloadProgress[doc.url],
strokeWidth: 3,
),
);
}
if (_downloadedUrls.contains(doc.url)) {
return IconButton(
icon: const Icon(Icons.visibility, color: Colors.green),
tooltip: 'View Document',
onPressed: () => _viewDocument(doc),
);
}
return IconButton(
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
tooltip: 'Download for Offline',
onPressed: () => _handleDownload(doc),
);
}
@override
Widget build(BuildContext context) {
final filteredDocs = _selectedGroup == 'All Documents'
? _documents
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
return Scaffold(
appBar: AppBar(
title: const Text("River Manual Documents"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<String>(
value: _selectedGroup,
decoration: InputDecoration(
labelText: 'Filter by Group',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
items: _documentGroups.map((String group) {
return DropdownMenuItem<String>(
value: group,
child: Text(group),
);
}).toList(),
onChanged: (newValue) {
setState(() {
_selectedGroup = newValue;
});
},
),
const SizedBox(height: 20),
Text(
_selectedGroup!,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const Divider(height: 20),
Expanded(
child: _documents.isEmpty
? const Center(child: Text("No River Manual documents found."))
: ListView.builder(
itemCount: filteredDocs.length,
itemBuilder: (context, index) {
final doc = filteredDocs[index];
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 6.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.red.shade100,
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
),
title: Text(doc.title),
trailing: _buildTrailingWidget(doc),
onTap: _downloadedUrls.contains(doc.url)
? () => _viewDocument(doc)
: null,
),
);
},
),
),
],
),
),
);
}
}
class PdfViewerScreen extends StatelessWidget {
final String filePath;
final String title;
const PdfViewerScreen({
super.key,
required this.filePath,
required this.title,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: PDFView(
filePath: filePath,
enableSwipe: true,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
),
);
}
}

View File

@ -1,3 +1,5 @@
//lib/screens/river/river_home_page.dart
import 'package:flutter/material.dart';
// Re-defining SidebarItem here for self-containment,
@ -30,12 +32,12 @@ class RiverHomePage extends StatelessWidget {
label: "Manual",
isParent: true,
children: [
//SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/river/manual/dashboard'),
// MODIFIED: Added Info Centre Document link for consistency
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/river/manual/info'),
SidebarItem(icon: Icons.pin_drop, label: "In-Situ Sampling", route: '/river/manual/in-situ'),
SidebarItem(icon: Icons.date_range, label: "Triennial Sampling", route: '/river/manual/triennial'),
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/manual/report'),
SidebarItem(icon: Icons.article, label: "Data Log", route: '/river/manual/data-log'),
SidebarItem(icon: Icons.image, label: "Image Request", route: '/river/manual/image-request'),
//SidebarItem(icon: Icons.image, label: "Image Request", route: '/river/manual/image-request'),
],
),
SidebarItem(
@ -43,10 +45,11 @@ class RiverHomePage extends StatelessWidget {
label: "Continuous",
isParent: true,
children: [
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/river/continuous/dashboard'),
SidebarItem(icon: Icons.info, label: "Overview", route: '/river/continuous/overview'),
SidebarItem(icon: Icons.input, label: "Entry", route: '/river/continuous/entry'),
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/continuous/report'),
// MODIFIED: Updated to point to the new Info Centre screen
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/river/continuous/info'),
//SidebarItem(icon: Icons.info, label: "Overview", route: '/river/continuous/overview'),
//SidebarItem(icon: Icons.input, label: "Entry", route: '/river/continuous/entry'),
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/continuous/report'),
],
),
SidebarItem(
@ -54,10 +57,11 @@ class RiverHomePage extends StatelessWidget {
label: "Investigative",
isParent: true,
children: [
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/river/investigative/dashboard'),
SidebarItem(icon: Icons.info, label: "Overview", route: '/river/investigative/overview'),
SidebarItem(icon: Icons.input, label: "Entry", route: '/river/investigative/entry'),
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/investigative/report'),
// MODIFIED: Updated to point to the new Info Centre screen
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/river/investigative/info'),
// SidebarItem(icon: Icons.info, label: "Overview", route: '/river/investigative/overview'),
//SidebarItem(icon: Icons.input, label: "Entry", route: '/river/investigative/entry'),
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/investigative/report'),
],
),
];

View File

@ -185,6 +185,9 @@ class ApiService {
final syncTasks = {
'profile': {'endpoint': 'profile', 'handler': (d, id) async { if (d.isNotEmpty) await dbHelper.saveProfile(d.first); }},
'allUsers': {'endpoint': 'users', 'handler': (d, id) async { await dbHelper.upsertUsers(d); await dbHelper.deleteUsers(id); }},
// --- ADDED: New sync task for documents ---
'documents': {'endpoint': 'documents', 'handler': (d, id) async { await dbHelper.upsertDocuments(d); await dbHelper.deleteDocuments(id); }},
// --- END ADDED ---
'tarballStations': {'endpoint': 'marine/tarball/stations', 'handler': (d, id) async { await dbHelper.upsertTarballStations(d); await dbHelper.deleteTarballStations(id); }},
'manualStations': {'endpoint': 'marine/manual/stations', 'handler': (d, id) async { await dbHelper.upsertManualStations(d); await dbHelper.deleteManualStations(id); }},
'tarballClassifications': {'endpoint': 'marine/tarball/classifications', 'handler': (d, id) async { await dbHelper.upsertTarballClassifications(d); await dbHelper.deleteTarballClassifications(id); }},
@ -619,7 +622,8 @@ class RiverApiService {
class DatabaseHelper {
static Database? _database;
static const String _dbName = 'app_data.db';
static const int _dbVersion = 19;
// --- ADDED: Incremented DB version for the new table ---
static const int _dbVersion = 20;
static const String _profileTable = 'user_profile';
static const String _usersTable = 'all_users';
@ -641,6 +645,8 @@ class DatabaseHelper {
static const String _ftpConfigsTable = 'ftp_configurations';
static const String _retryQueueTable = 'retry_queue';
static const String _submissionLogTable = 'submission_log';
// --- ADDED: New table name for documents ---
static const String _documentsTable = 'documents';
static const String _modulePreferencesTable = 'module_preferences';
static const String _moduleApiLinksTable = 'module_api_links';
@ -730,6 +736,9 @@ class DatabaseHelper {
)
''');
// END CHANGE
// --- ADDED: Create the documents table on initial creation ---
await db.execute('CREATE TABLE $_documentsTable(id INTEGER PRIMARY KEY, document_json TEXT)');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
@ -814,6 +823,10 @@ class DatabaseHelper {
''');
// END CHANGE
}
// --- ADDED: Upgrade path for the new documents table ---
if (oldVersion < 20) {
await db.execute('CREATE TABLE IF NOT EXISTS $_documentsTable(id INTEGER PRIMARY KEY, document_json TEXT)');
}
}
/// Performs an "upsert": inserts new records or replaces existing ones.
@ -871,6 +884,11 @@ class DatabaseHelper {
Future<void> deleteUsers(List<dynamic> ids) => _deleteData(_usersTable, 'user_id', ids);
Future<List<Map<String, dynamic>>?> loadUsers() => _loadData(_usersTable, 'user');
// --- ADDED: Handlers for the new documents table ---
Future<void> upsertDocuments(List<Map<String, dynamic>> data) => _upsertData(_documentsTable, 'id', data, 'document');
Future<void> deleteDocuments(List<dynamic> ids) => _deleteData(_documentsTable, 'id', ids);
Future<List<Map<String, dynamic>>?> loadDocuments() => _loadData(_documentsTable, 'document');
Future<void> upsertTarballStations(List<Map<String, dynamic>> data) => _upsertData(_tarballStationsTable, 'station_id', data, 'station');
Future<void> deleteTarballStations(List<dynamic> ids) => _deleteData(_tarballStationsTable, 'station_id', ids);
Future<List<Map<String, dynamic>>?> loadTarballStations() => _loadData(_tarballStationsTable, 'station');

View File

@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:path/path.dart' as p;
// --- ADDED: Import dio for downloading ---
import 'package:dio/dio.dart';
import '../models/air_installation_data.dart';
import '../models/air_collection_data.dart';
@ -539,4 +541,69 @@ class LocalStorageService {
debugPrint("Error updating river in-situ log: $e");
}
}
// =======================================================================
// --- ADDED: Part 6: Info Centre Document Management ---
// =======================================================================
final Dio _dio = Dio();
/// Gets the directory for storing Info Centre documents, creating it if it doesn't exist.
Future<Directory?> _getInfoCentreDocumentsDirectory() async {
// We use serverName: '' to ensure documents are stored in a common root MMSV4 folder, not server-specific ones.
final mmsv4Dir = await _getPublicMMSV4Directory(serverName: '');
if (mmsv4Dir == null) return null;
final docDir = Directory(p.join(mmsv4Dir.path, 'info_centre_documents'));
if (!await docDir.exists()) {
await docDir.create(recursive: true);
}
return docDir;
}
/// Constructs the full local file path for a given document URL.
Future<String?> getLocalDocumentPath(String docUrl) async {
final docDir = await _getInfoCentreDocumentsDirectory();
if (docDir == null) return null;
final fileName = p.basename(docUrl);
return p.join(docDir.path, fileName);
}
/// Checks if a document has already been downloaded.
Future<bool> isDocumentDownloaded(String docUrl) async {
final filePath = await getLocalDocumentPath(docUrl);
if (filePath == null) return false;
return await File(filePath).exists();
}
/// Downloads a document from a URL and saves it to the local `MMSV4/info_centre_documents` folder.
Future<void> downloadDocument({
required String docUrl,
required Function(double) onReceiveProgress,
}) async {
final filePath = await getLocalDocumentPath(docUrl);
if (filePath == null) {
throw Exception("Could not get local storage path. Check permissions.");
}
try {
await _dio.download(
docUrl,
filePath,
onReceiveProgress: (received, total) {
if (total != -1) {
onReceiveProgress(received / total);
}
},
);
} catch (e) {
// If the download fails, delete the partially downloaded file to prevent corruption.
final file = File(filePath);
if (await file.exists()) {
await file.delete();
}
throw Exception("Download failed: $e");
}
}
}

View File

@ -7,9 +7,13 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -12,6 +12,7 @@ import geolocator_apple
import path_provider_foundation
import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
@ -21,4 +22,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@ -121,6 +121,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.11"
dio:
dependency: "direct main"
description:
name: dio
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
url: "https://pub.dev"
source: hosted
version: "5.9.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
dropdown_search:
dependency: "direct main"
description:
@ -231,6 +247,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_pdfview:
dependency: "direct main"
description:
name: flutter_pdfview
sha256: c402ad1f51ba8ea73b9fb04c003ca0a9286118ba5ac9787ee2aa58956b3fcf8a
url: "https://pub.dev"
source: hosted
version: "1.4.1+1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -838,6 +862,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
url: "https://pub.dev"
source: hosted
version: "6.3.18"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
url: "https://pub.dev"
source: hosted
version: "6.3.4"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
url: "https://pub.dev"
source: hosted
version: "3.2.3"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
usb_serial:
dependency: "direct main"
description:
@ -944,4 +1032,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.8.0 <4.0.0"
flutter: ">=3.27.0"
flutter: ">=3.29.0"

View File

@ -29,6 +29,10 @@ dependencies:
flutter_svg: ^2.0.9
google_fonts: ^6.1.0
dropdown_search: ^5.0.6 # For searchable dropdowns in forms
# --- ADDED: For opening document URLs ---
url_launcher: ^6.2.6
flutter_pdfview: ^1.3.2
dio: ^5.4.3+1
# --- Device & Hardware Access ---
image_picker: ^1.0.7

View File

@ -10,6 +10,7 @@
#include <file_selector_windows/file_selector_windows.h>
#include <geolocator_windows/geolocator_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <webview_windows/webview_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("GeolocatorWindows"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WebviewWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WebviewWindowsPlugin"));
}

View File

@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
geolocator_windows
permission_handler_windows
url_launcher_windows
webview_windows
)