diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e80bc57..f507a7f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ - + >? _parameterLimits; List>? _apiConfigs; List>? _ftpConfigs; + // --- ADDED: State variable for the list of documents --- + List>? _documents; // --- ADDED: State variable for the list of tasks pending manual retry --- List>? _pendingRetries; @@ -76,6 +78,8 @@ class AuthProvider with ChangeNotifier { List>? get parameterLimits => _parameterLimits; List>? get apiConfigs => _apiConfigs; List>? get ftpConfigs => _ftpConfigs; + // --- ADDED: Getter for the list of documents --- + List>? get documents => _documents; // --- ADDED: Getter for the list of tasks pending manual retry --- List>? 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 --- diff --git a/lib/main.dart b/lib/main.dart index 8463371..8fc6e00 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(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)), ], ), ), diff --git a/lib/screens/air/air_home_page.dart b/lib/screens/air/air_home_page.dart index 9cc3c1b..c188ac1 100644 --- a/lib/screens/air/air_home_page.dart +++ b/lib/screens/air/air_home_page.dart @@ -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'), ], ), ]; diff --git a/lib/screens/air/continuous/air_continuous_dashboard.dart b/lib/screens/air/continuous/air_continuous_dashboard.dart deleted file mode 100644 index 5d442d9..0000000 --- a/lib/screens/air/continuous/air_continuous_dashboard.dart +++ /dev/null @@ -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, - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/air/continuous/air_continuous_info_centre_document.dart b/lib/screens/air/continuous/air_continuous_info_centre_document.dart new file mode 100644 index 0000000..c10763f --- /dev/null +++ b/lib/screens/air/continuous/air_continuous_info_centre_document.dart @@ -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 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 createState() => _AirContinuousInfoCentreDocumentState(); +} + +class _AirContinuousInfoCentreDocumentState extends State { + final LocalStorageService _localStorageService = LocalStorageService(); + late List _documents; + + List _documentGroups = []; + String? _selectedGroup; + + Set _downloadedUrls = {}; + Map _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 _getFilteredDocumentsFromProvider() { + final documentsData = Provider.of(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 _checkInitialDownloadStatus() async { + final downloaded = {}; + for (var doc in _documents) { + if (await _localStorageService.isDocumentDownloaded(doc.url)) { + downloaded.add(doc.url); + } + } + if (mounted) { + setState(() { + _downloadedUrls = downloaded; + }); + } + } + + Future _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 _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( + 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( + 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, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/air/investigative/air_investigative_dashboard.dart b/lib/screens/air/investigative/air_investigative_dashboard.dart deleted file mode 100644 index fc39fed..0000000 --- a/lib/screens/air/investigative/air_investigative_dashboard.dart +++ /dev/null @@ -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, - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/air/investigative/air_investigative_info_centre_document.dart b/lib/screens/air/investigative/air_investigative_info_centre_document.dart new file mode 100644 index 0000000..7cc5b40 --- /dev/null +++ b/lib/screens/air/investigative/air_investigative_info_centre_document.dart @@ -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 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 createState() => _AirInvestigativeInfoCentreDocumentState(); +} + +class _AirInvestigativeInfoCentreDocumentState extends State { + final LocalStorageService _localStorageService = LocalStorageService(); + late List _documents; + + List _documentGroups = []; + String? _selectedGroup; + + Set _downloadedUrls = {}; + Map _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 _getFilteredDocumentsFromProvider() { + final documentsData = Provider.of(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 _checkInitialDownloadStatus() async { + final downloaded = {}; + for (var doc in _documents) { + if (await _localStorageService.isDocumentDownloaded(doc.url)) { + downloaded.add(doc.url); + } + } + if (mounted) { + setState(() { + _downloadedUrls = downloaded; + }); + } + } + + Future _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 _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( + 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( + 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, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/air/manual/air_manual_dashboard.dart b/lib/screens/air/manual/air_manual_dashboard.dart deleted file mode 100644 index 4ddf26b..0000000 --- a/lib/screens/air/manual/air_manual_dashboard.dart +++ /dev/null @@ -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, - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/air/manual/air_manual_info_centre_document.dart b/lib/screens/air/manual/air_manual_info_centre_document.dart new file mode 100644 index 0000000..67669df --- /dev/null +++ b/lib/screens/air/manual/air_manual_info_centre_document.dart @@ -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 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 createState() => _AirManualInfoCentreDocumentState(); +} + +class _AirManualInfoCentreDocumentState extends State { + final LocalStorageService _localStorageService = LocalStorageService(); + late List _documents; + + List _documentGroups = []; + String? _selectedGroup; + + Set _downloadedUrls = {}; + Map _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 _getFilteredDocumentsFromProvider() { + final documentsData = Provider.of(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 _checkInitialDownloadStatus() async { + final downloaded = {}; + for (var doc in _documents) { + if (await _localStorageService.isDocumentDownloaded(doc.url)) { + downloaded.add(doc.url); + } + } + if (mounted) { + setState(() { + _downloadedUrls = downloaded; + }); + } + } + + Future _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 _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( + 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( + 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, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/marine/continuous/marine_continuous_dashboard.dart b/lib/screens/marine/continuous/marine_continuous_dashboard.dart deleted file mode 100644 index 873ee09..0000000 --- a/lib/screens/marine/continuous/marine_continuous_dashboard.dart +++ /dev/null @@ -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, - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/marine/continuous/marine_continuous_info_centre_document.dart b/lib/screens/marine/continuous/marine_continuous_info_centre_document.dart new file mode 100644 index 0000000..d425afb --- /dev/null +++ b/lib/screens/marine/continuous/marine_continuous_info_centre_document.dart @@ -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 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 createState() => _MarineContinuousInfoCentreDocumentState(); +} + +class _MarineContinuousInfoCentreDocumentState extends State { + final LocalStorageService _localStorageService = LocalStorageService(); + late List _documents; + + List _documentGroups = []; + String? _selectedGroup; + + Set _downloadedUrls = {}; + Map _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 _getFilteredDocumentsFromProvider() { + final documentsData = Provider.of(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 _checkInitialDownloadStatus() async { + final downloaded = {}; + for (var doc in _documents) { + if (await _localStorageService.isDocumentDownloaded(doc.url)) { + downloaded.add(doc.url); + } + } + if (mounted) { + setState(() { + _downloadedUrls = downloaded; + }); + } + } + + Future _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 _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( + 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( + 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, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/marine/investigative/marine_investigative_dashboard.dart b/lib/screens/marine/investigative/marine_investigative_dashboard.dart deleted file mode 100644 index 80516ab..0000000 --- a/lib/screens/marine/investigative/marine_investigative_dashboard.dart +++ /dev/null @@ -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, - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/marine/investigative/marine_investigative_info_centre_document.dart b/lib/screens/marine/investigative/marine_investigative_info_centre_document.dart new file mode 100644 index 0000000..326212b --- /dev/null +++ b/lib/screens/marine/investigative/marine_investigative_info_centre_document.dart @@ -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 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 createState() => _MarineInvestigativeInfoCentreDocumentState(); +} + +class _MarineInvestigativeInfoCentreDocumentState extends State { + final LocalStorageService _localStorageService = LocalStorageService(); + late List _documents; + + List _documentGroups = []; + String? _selectedGroup; + + Set _downloadedUrls = {}; + Map _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 _getFilteredDocumentsFromProvider() { + final documentsData = Provider.of(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 _checkInitialDownloadStatus() async { + final downloaded = {}; + for (var doc in _documents) { + if (await _localStorageService.isDocumentDownloaded(doc.url)) { + downloaded.add(doc.url); + } + } + if (mounted) { + setState(() { + _downloadedUrls = downloaded; + }); + } + } + + Future _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 _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( + 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( + 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, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/marine/manual/info_centre_document.dart b/lib/screens/marine/manual/info_centre_document.dart index e42df24..1956f19 100644 --- a/lib/screens/marine/manual/info_centre_document.dart +++ b/lib/screens/marine/manual/info_centre_document.dart @@ -1,58 +1,291 @@ +// 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 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 createState() => _MarineInfoCentreDocumentState(); } class _MarineInfoCentreDocumentState extends State { - String? selectedFileName; + final LocalStorageService _localStorageService = LocalStorageService(); + late List _documents; - Future _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 _documentGroups = []; + // Holds the currently selected group from the dropdown + String? _selectedGroup; + + Set _downloadedUrls = {}; + Map _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 _getFilteredDocumentsFromProvider() { + final documentsData = Provider.of(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 _checkInitialDownloadStatus() async { + final downloaded = {}; + for (var doc in _documents) { + if (await _localStorageService.isDocumentDownloaded(doc.url)) { + downloaded.add(doc.url); + } + } + if (mounted) { + setState(() { + _downloadedUrls = downloaded; + }); + } + } + + Future _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 _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( + 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( + 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, + ), + ); + }, + ), ), ], ), ), ); } +} + + +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, + ), + ); + } } \ No newline at end of file diff --git a/lib/screens/marine/manual/marine_manual_dashboard.dart b/lib/screens/marine/manual/marine_manual_dashboard.dart deleted file mode 100644 index 2602da3..0000000 --- a/lib/screens/marine/manual/marine_manual_dashboard.dart +++ /dev/null @@ -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, - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/marine/marine_home_page.dart b/lib/screens/marine/marine_home_page.dart index 0391f10..b09d487 100644 --- a/lib/screens/marine/marine_home_page.dart +++ b/lib/screens/marine/marine_home_page.dart @@ -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'), ], ), ]; diff --git a/lib/screens/river/continuous/river_continuous_dashboard.dart b/lib/screens/river/continuous/river_continuous_dashboard.dart deleted file mode 100644 index c1f787d..0000000 --- a/lib/screens/river/continuous/river_continuous_dashboard.dart +++ /dev/null @@ -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, - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/river/continuous/river_continuous_info_centre_document.dart b/lib/screens/river/continuous/river_continuous_info_centre_document.dart new file mode 100644 index 0000000..312e04a --- /dev/null +++ b/lib/screens/river/continuous/river_continuous_info_centre_document.dart @@ -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 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 createState() => _RiverContinuousInfoCentreDocumentState(); +} + +class _RiverContinuousInfoCentreDocumentState extends State { + final LocalStorageService _localStorageService = LocalStorageService(); + late List _documents; + + List _documentGroups = []; + String? _selectedGroup; + + Set _downloadedUrls = {}; + Map _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 _getFilteredDocumentsFromProvider() { + final documentsData = Provider.of(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 _checkInitialDownloadStatus() async { + final downloaded = {}; + for (var doc in _documents) { + if (await _localStorageService.isDocumentDownloaded(doc.url)) { + downloaded.add(doc.url); + } + } + if (mounted) { + setState(() { + _downloadedUrls = downloaded; + }); + } + } + + Future _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 _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( + 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( + 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, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/river/investigative/river_investigative_dashboard.dart b/lib/screens/river/investigative/river_investigative_dashboard.dart deleted file mode 100644 index 35a6157..0000000 --- a/lib/screens/river/investigative/river_investigative_dashboard.dart +++ /dev/null @@ -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, - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/river/investigative/river_investigative_info_centre_document.dart b/lib/screens/river/investigative/river_investigative_info_centre_document.dart new file mode 100644 index 0000000..74a3baf --- /dev/null +++ b/lib/screens/river/investigative/river_investigative_info_centre_document.dart @@ -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 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 createState() => _RiverInvestigativeInfoCentreDocumentState(); +} + +class _RiverInvestigativeInfoCentreDocumentState extends State { + final LocalStorageService _localStorageService = LocalStorageService(); + late List _documents; + + List _documentGroups = []; + String? _selectedGroup; + + Set _downloadedUrls = {}; + Map _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 _getFilteredDocumentsFromProvider() { + final documentsData = Provider.of(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 _checkInitialDownloadStatus() async { + final downloaded = {}; + for (var doc in _documents) { + if (await _localStorageService.isDocumentDownloaded(doc.url)) { + downloaded.add(doc.url); + } + } + if (mounted) { + setState(() { + _downloadedUrls = downloaded; + }); + } + } + + Future _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 _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( + 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( + 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, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/river/manual/river_manual_dashboard.dart b/lib/screens/river/manual/river_manual_dashboard.dart deleted file mode 100644 index 5ad1d98..0000000 --- a/lib/screens/river/manual/river_manual_dashboard.dart +++ /dev/null @@ -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, - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/river/manual/river_manual_info_centre_document.dart b/lib/screens/river/manual/river_manual_info_centre_document.dart new file mode 100644 index 0000000..9fe080c --- /dev/null +++ b/lib/screens/river/manual/river_manual_info_centre_document.dart @@ -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 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 createState() => _RiverManualInfoCentreDocumentState(); +} + +class _RiverManualInfoCentreDocumentState extends State { + final LocalStorageService _localStorageService = LocalStorageService(); + late List _documents; + + List _documentGroups = []; + String? _selectedGroup; + + Set _downloadedUrls = {}; + Map _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 _getFilteredDocumentsFromProvider() { + final documentsData = Provider.of(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 _checkInitialDownloadStatus() async { + final downloaded = {}; + for (var doc in _documents) { + if (await _localStorageService.isDocumentDownloaded(doc.url)) { + downloaded.add(doc.url); + } + } + if (mounted) { + setState(() { + _downloadedUrls = downloaded; + }); + } + } + + Future _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 _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( + 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( + 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, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/river/river_home_page.dart b/lib/screens/river/river_home_page.dart index 81a4636..c3ad9e8 100644 --- a/lib/screens/river/river_home_page.dart +++ b/lib/screens/river/river_home_page.dart @@ -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'), ], ), ]; diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 187e299..191289b 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -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 _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 deleteUsers(List ids) => _deleteData(_usersTable, 'user_id', ids); Future>?> loadUsers() => _loadData(_usersTable, 'user'); + // --- ADDED: Handlers for the new documents table --- + Future upsertDocuments(List> data) => _upsertData(_documentsTable, 'id', data, 'document'); + Future deleteDocuments(List ids) => _deleteData(_documentsTable, 'id', ids); + Future>?> loadDocuments() => _loadData(_documentsTable, 'document'); + Future upsertTarballStations(List> data) => _upsertData(_tarballStationsTable, 'station_id', data, 'station'); Future deleteTarballStations(List ids) => _deleteData(_tarballStationsTable, 'station_id', ids); Future>?> loadTarballStations() => _loadData(_tarballStationsTable, 'station'); diff --git a/lib/services/local_storage_service.dart b/lib/services/local_storage_service.dart index b418d3d..ed68806 100644 --- a/lib/services/local_storage_service.dart +++ b/lib/services/local_storage_service.dart @@ -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 _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 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 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 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"); + } + } } \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 64a0ece..7299b5c 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include 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); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2db3c22..786ff5c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f92cb6d..35373a0 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) } diff --git a/pubspec.lock b/pubspec.lock index b6e4b15..7a087ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index a333071..a46043b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 3bf1db5..edb244d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include 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")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d4c7652..d0234a0 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows geolocator_windows permission_handler_windows + url_launcher_windows webview_windows )