add in info center document for all module
This commit is contained in:
parent
f742dd5853
commit
c2a95c53cc
@ -27,7 +27,7 @@
|
|||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
<!-- END: STORAGE PERMISSIONS -->
|
<!-- END: STORAGE PERMISSIONS -->
|
||||||
|
|
||||||
<!-- MMS V4 1.2.03 -->
|
<!-- MMS V4 1.2.05 -->
|
||||||
<application
|
<application
|
||||||
android:label="MMS V4 debug"
|
android:label="MMS V4 debug"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|||||||
@ -55,6 +55,8 @@ class AuthProvider with ChangeNotifier {
|
|||||||
List<Map<String, dynamic>>? _parameterLimits;
|
List<Map<String, dynamic>>? _parameterLimits;
|
||||||
List<Map<String, dynamic>>? _apiConfigs;
|
List<Map<String, dynamic>>? _apiConfigs;
|
||||||
List<Map<String, dynamic>>? _ftpConfigs;
|
List<Map<String, dynamic>>? _ftpConfigs;
|
||||||
|
// --- ADDED: State variable for the list of documents ---
|
||||||
|
List<Map<String, dynamic>>? _documents;
|
||||||
// --- ADDED: State variable for the list of tasks pending manual retry ---
|
// --- ADDED: State variable for the list of tasks pending manual retry ---
|
||||||
List<Map<String, dynamic>>? _pendingRetries;
|
List<Map<String, dynamic>>? _pendingRetries;
|
||||||
|
|
||||||
@ -76,6 +78,8 @@ class AuthProvider with ChangeNotifier {
|
|||||||
List<Map<String, dynamic>>? get parameterLimits => _parameterLimits;
|
List<Map<String, dynamic>>? get parameterLimits => _parameterLimits;
|
||||||
List<Map<String, dynamic>>? get apiConfigs => _apiConfigs;
|
List<Map<String, dynamic>>? get apiConfigs => _apiConfigs;
|
||||||
List<Map<String, dynamic>>? get ftpConfigs => _ftpConfigs;
|
List<Map<String, dynamic>>? get ftpConfigs => _ftpConfigs;
|
||||||
|
// --- ADDED: Getter for the list of documents ---
|
||||||
|
List<Map<String, dynamic>>? get documents => _documents;
|
||||||
// --- ADDED: Getter for the list of tasks pending manual retry ---
|
// --- ADDED: Getter for the list of tasks pending manual retry ---
|
||||||
List<Map<String, dynamic>>? get pendingRetries => _pendingRetries;
|
List<Map<String, dynamic>>? get pendingRetries => _pendingRetries;
|
||||||
|
|
||||||
@ -221,6 +225,8 @@ class AuthProvider with ChangeNotifier {
|
|||||||
// ADDED: Load new data types from the local database
|
// ADDED: Load new data types from the local database
|
||||||
_appSettings = await _dbHelper.loadAppSettings();
|
_appSettings = await _dbHelper.loadAppSettings();
|
||||||
_parameterLimits = await _dbHelper.loadParameterLimits();
|
_parameterLimits = await _dbHelper.loadParameterLimits();
|
||||||
|
// --- ADDED: Load documents from the local database cache ---
|
||||||
|
_documents = await _dbHelper.loadDocuments();
|
||||||
_apiConfigs = await _dbHelper.loadApiConfigs();
|
_apiConfigs = await _dbHelper.loadApiConfigs();
|
||||||
_ftpConfigs = await _dbHelper.loadFtpConfigs();
|
_ftpConfigs = await _dbHelper.loadFtpConfigs();
|
||||||
// --- ADDED: Load pending retry tasks from the database ---
|
// --- ADDED: Load pending retry tasks from the database ---
|
||||||
@ -291,6 +297,8 @@ class AuthProvider with ChangeNotifier {
|
|||||||
// ADDED: Clear new data on logout
|
// ADDED: Clear new data on logout
|
||||||
_appSettings = null;
|
_appSettings = null;
|
||||||
_parameterLimits = null;
|
_parameterLimits = null;
|
||||||
|
// --- ADDED: Clear documents list on logout ---
|
||||||
|
_documents = null;
|
||||||
_apiConfigs = null;
|
_apiConfigs = null;
|
||||||
_ftpConfigs = null;
|
_ftpConfigs = null;
|
||||||
// --- ADDED: Clear pending retry tasks on logout ---
|
// --- ADDED: Clear pending retry tasks on logout ---
|
||||||
|
|||||||
@ -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/telegram_service.dart';
|
||||||
import 'package:environment_monitoring_app/services/server_config_service.dart';
|
import 'package:environment_monitoring_app/services/server_config_service.dart';
|
||||||
import 'package:environment_monitoring_app/services/retry_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_in_situ_sampling_service.dart';
|
||||||
import 'package:environment_monitoring_app/services/marine_tarball_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/theme.dart';
|
||||||
import 'package:environment_monitoring_app/auth_provider.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';
|
import 'package:environment_monitoring_app/screens/marine/marine_home_page.dart';
|
||||||
|
|
||||||
// Air Screens
|
// 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_installation_screen.dart';
|
||||||
import 'package:environment_monitoring_app/screens/air/manual/air_manual_collection_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/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/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/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/overview.dart' as airContinuousOverview;
|
||||||
import 'package:environment_monitoring_app/screens/air/continuous/entry.dart' as airContinuousEntry;
|
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/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/overview.dart' as airInvestigativeOverview;
|
||||||
import 'package:environment_monitoring_app/screens/air/investigative/entry.dart' as airInvestigativeEntry;
|
import 'package:environment_monitoring_app/screens/air/investigative/entry.dart' as airInvestigativeEntry;
|
||||||
import 'package:environment_monitoring_app/screens/air/investigative/report.dart' as airInvestigativeReport;
|
import 'package:environment_monitoring_app/screens/air/investigative/report.dart' as airInvestigativeReport;
|
||||||
|
|
||||||
// River Screens
|
// 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/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/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/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/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/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/overview.dart' as riverContinuousOverview;
|
||||||
import 'package:environment_monitoring_app/screens/river/continuous/entry.dart' as riverContinuousEntry;
|
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/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/overview.dart' as riverInvestigativeOverview;
|
||||||
import 'package:environment_monitoring_app/screens/river/investigative/entry.dart' as riverInvestigativeEntry;
|
import 'package:environment_monitoring_app/screens/river/investigative/entry.dart' as riverInvestigativeEntry;
|
||||||
import 'package:environment_monitoring_app/screens/river/investigative/report.dart' as riverInvestigativeReport;
|
import 'package:environment_monitoring_app/screens/river/investigative/report.dart' as riverInvestigativeReport;
|
||||||
|
|
||||||
// Marine Screens
|
// 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/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/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/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/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/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/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/overview.dart' as marineContinuousOverview;
|
||||||
import 'package:environment_monitoring_app/screens/marine/continuous/entry.dart' as marineContinuousEntry;
|
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/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/overview.dart' as marineInvestigativeOverview;
|
||||||
import 'package:environment_monitoring_app/screens/marine/investigative/entry.dart' as marineInvestigativeEntry;
|
import 'package:environment_monitoring_app/screens/marine/investigative/entry.dart' as marineInvestigativeEntry;
|
||||||
import 'package:environment_monitoring_app/screens/marine/investigative/report.dart' as marineInvestigativeReport;
|
import 'package:environment_monitoring_app/screens/marine/investigative/report.dart' as marineInvestigativeReport;
|
||||||
@ -116,10 +113,8 @@ void main() async {
|
|||||||
Provider<TelegramService>(create: (_) => telegramService),
|
Provider<TelegramService>(create: (_) => telegramService),
|
||||||
Provider(create: (_) => LocalStorageService()),
|
Provider(create: (_) => LocalStorageService()),
|
||||||
|
|
||||||
// MODIFIED: Provide all dedicated services with their required dependencies
|
|
||||||
Provider(create: (context) => RiverInSituSamplingService(telegramService)),
|
Provider(create: (context) => RiverInSituSamplingService(telegramService)),
|
||||||
Provider(create: (context) => AirSamplingService(databaseHelper, 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) => MarineInSituSamplingService(telegramService)),
|
||||||
Provider(create: (context) => MarineTarballSamplingService(telegramService)),
|
Provider(create: (context) => MarineTarballSamplingService(telegramService)),
|
||||||
],
|
],
|
||||||
@ -201,7 +196,7 @@ class RootApp extends StatelessWidget {
|
|||||||
'/marine/home': (context) => const MarineHomePage(),
|
'/marine/home': (context) => const MarineHomePage(),
|
||||||
|
|
||||||
// Air Manual
|
// Air Manual
|
||||||
'/air/manual/dashboard': (context) => AirManualDashboard(),
|
'/air/manual/info': (context) => const AirManualInfoCentreDocument(),
|
||||||
'/air/manual/installation': (context) => const AirManualInstallationScreen(),
|
'/air/manual/installation': (context) => const AirManualInstallationScreen(),
|
||||||
'/air/manual/collection': (context) => const AirManualCollectionScreen(),
|
'/air/manual/collection': (context) => const AirManualCollectionScreen(),
|
||||||
'/air/manual/report': (context) => airManualReport.AirManualReport(),
|
'/air/manual/report': (context) => airManualReport.AirManualReport(),
|
||||||
@ -209,19 +204,19 @@ class RootApp extends StatelessWidget {
|
|||||||
'/air/manual/image-request': (context) => airManualImageRequest.AirManualImageRequest(),
|
'/air/manual/image-request': (context) => airManualImageRequest.AirManualImageRequest(),
|
||||||
|
|
||||||
// Air Continuous
|
// Air Continuous
|
||||||
'/air/continuous/dashboard': (context) => AirContinuousDashboard(),
|
'/air/continuous/info': (context) => const AirContinuousInfoCentreDocument(),
|
||||||
'/air/continuous/overview': (context) => airContinuousOverview.OverviewScreen(),
|
'/air/continuous/overview': (context) => airContinuousOverview.OverviewScreen(),
|
||||||
'/air/continuous/entry': (context) => airContinuousEntry.EntryScreen(),
|
'/air/continuous/entry': (context) => airContinuousEntry.EntryScreen(),
|
||||||
'/air/continuous/report': (context) => airContinuousReport.ReportScreen(),
|
'/air/continuous/report': (context) => airContinuousReport.ReportScreen(),
|
||||||
|
|
||||||
// Air Investigative
|
// Air Investigative
|
||||||
'/air/investigative/dashboard': (context) => AirInvestigativeDashboard(),
|
'/air/investigative/info': (context) => const AirInvestigativeInfoCentreDocument(),
|
||||||
'/air/investigative/overview': (context) => airInvestigativeOverview.OverviewScreen(),
|
'/air/investigative/overview': (context) => airInvestigativeOverview.OverviewScreen(),
|
||||||
'/air/investigative/entry': (context) => airInvestigativeEntry.EntryScreen(),
|
'/air/investigative/entry': (context) => airInvestigativeEntry.EntryScreen(),
|
||||||
'/air/investigative/report': (context) => airInvestigativeReport.ReportScreen(),
|
'/air/investigative/report': (context) => airInvestigativeReport.ReportScreen(),
|
||||||
|
|
||||||
// River Manual
|
// River Manual
|
||||||
'/river/manual/dashboard': (context) => RiverManualDashboard(),
|
'/river/manual/info': (context) => const RiverManualInfoCentreDocument(),
|
||||||
'/river/manual/in-situ': (context) => riverManualInSituSampling.RiverInSituSamplingScreen(),
|
'/river/manual/in-situ': (context) => riverManualInSituSampling.RiverInSituSamplingScreen(),
|
||||||
'/river/manual/report': (context) => riverManualReport.RiverManualReport(),
|
'/river/manual/report': (context) => riverManualReport.RiverManualReport(),
|
||||||
'/river/manual/triennial': (context) => riverManualTriennialSampling.RiverTriennialSampling(),
|
'/river/manual/triennial': (context) => riverManualTriennialSampling.RiverTriennialSampling(),
|
||||||
@ -229,19 +224,18 @@ class RootApp extends StatelessWidget {
|
|||||||
'/river/manual/image-request': (context) => riverManualImageRequest.RiverManualImageRequest(),
|
'/river/manual/image-request': (context) => riverManualImageRequest.RiverManualImageRequest(),
|
||||||
|
|
||||||
// River Continuous
|
// River Continuous
|
||||||
'/river/continuous/dashboard': (context) => RiverContinuousDashboard(),
|
'/river/continuous/info': (context) => const RiverContinuousInfoCentreDocument(),
|
||||||
'/river/continuous/overview': (context) => riverContinuousOverview.OverviewScreen(),
|
'/river/continuous/overview': (context) => riverContinuousOverview.OverviewScreen(),
|
||||||
'/river/continuous/entry': (context) => riverContinuousEntry.EntryScreen(),
|
'/river/continuous/entry': (context) => riverContinuousEntry.EntryScreen(),
|
||||||
'/river/continuous/report': (context) => riverContinuousReport.ReportScreen(),
|
'/river/continuous/report': (context) => riverContinuousReport.ReportScreen(),
|
||||||
|
|
||||||
// River Investigative
|
// River Investigative
|
||||||
'/river/investigative/dashboard': (context) => RiverInvestigativeDashboard(),
|
'/river/investigative/info': (context) => const RiverInvestigativeInfoCentreDocument(),
|
||||||
'/river/investigative/overview': (context) => riverInvestigativeOverview.OverviewScreen(),
|
'/river/investigative/overview': (context) => riverInvestigativeOverview.OverviewScreen(),
|
||||||
'/river/investigative/entry': (context) => riverInvestigativeEntry.EntryScreen(),
|
'/river/investigative/entry': (context) => riverInvestigativeEntry.EntryScreen(),
|
||||||
'/river/investigative/report': (context) => riverInvestigativeReport.ReportScreen(),
|
'/river/investigative/report': (context) => riverInvestigativeReport.ReportScreen(),
|
||||||
|
|
||||||
// Marine Manual
|
// Marine Manual
|
||||||
'/marine/manual/dashboard': (context) => MarineManualDashboard(),
|
|
||||||
'/marine/manual/info': (context) => marineManualInfoCentreDocument.MarineInfoCentreDocument(),
|
'/marine/manual/info': (context) => marineManualInfoCentreDocument.MarineInfoCentreDocument(),
|
||||||
'/marine/manual/pre-sampling': (context) => marineManualPreSampling.MarinePreSampling(),
|
'/marine/manual/pre-sampling': (context) => marineManualPreSampling.MarinePreSampling(),
|
||||||
'/marine/manual/in-situ': (context) => marineManualInSituSampling.MarineInSituSampling(),
|
'/marine/manual/in-situ': (context) => marineManualInSituSampling.MarineInSituSampling(),
|
||||||
@ -251,13 +245,13 @@ class RootApp extends StatelessWidget {
|
|||||||
'/marine/manual/image-request': (context) => marineManualImageRequest.MarineManualImageRequest(),
|
'/marine/manual/image-request': (context) => marineManualImageRequest.MarineManualImageRequest(),
|
||||||
|
|
||||||
// Marine Continuous
|
// Marine Continuous
|
||||||
'/marine/continuous/dashboard': (context) => MarineContinuousDashboard(),
|
'/marine/continuous/info': (context) => const MarineContinuousInfoCentreDocument(),
|
||||||
'/marine/continuous/overview': (context) => marineContinuousOverview.OverviewScreen(),
|
'/marine/continuous/overview': (context) => marineContinuousOverview.OverviewScreen(),
|
||||||
'/marine/continuous/entry': (context) => marineContinuousEntry.EntryScreen(),
|
'/marine/continuous/entry': (context) => marineContinuousEntry.EntryScreen(),
|
||||||
'/marine/continuous/report': (context) => marineContinuousReport.ReportScreen(),
|
'/marine/continuous/report': (context) => marineContinuousReport.ReportScreen(),
|
||||||
|
|
||||||
// Marine Investigative
|
// Marine Investigative
|
||||||
'/marine/investigative/dashboard': (context) => MarineInvestigativeDashboard(),
|
'/marine/investigative/info': (context) => const MarineInvestigativeInfoCentreDocument(),
|
||||||
'/marine/investigative/overview': (context) => marineInvestigativeOverview.OverviewScreen(),
|
'/marine/investigative/overview': (context) => marineInvestigativeOverview.OverviewScreen(),
|
||||||
'/marine/investigative/entry': (context) => marineInvestigativeEntry.EntryScreen(),
|
'/marine/investigative/entry': (context) => marineInvestigativeEntry.EntryScreen(),
|
||||||
'/marine/investigative/report': (context) => marineInvestigativeReport.ReportScreen(),
|
'/marine/investigative/report': (context) => marineInvestigativeReport.ReportScreen(),
|
||||||
@ -273,14 +267,20 @@ class SplashScreen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
return Scaffold(
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(),
|
Image.asset(
|
||||||
SizedBox(height: 20),
|
'assets/icon4.png',
|
||||||
Text('Loading app data...', style: TextStyle(fontSize: 16)),
|
height: 360,
|
||||||
|
width: 360,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text('Loading app data...', style: TextStyle(fontSize: 16)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
//lib/screens/river/air_home_page.dart
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
// Re-defining SidebarItem here for self-containment,
|
// Re-defining SidebarItem here for self-containment,
|
||||||
@ -30,12 +32,12 @@ class AirHomePage extends StatelessWidget {
|
|||||||
label: "Manual",
|
label: "Manual",
|
||||||
isParent: true,
|
isParent: true,
|
||||||
children: [
|
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.construction, label: "Installation", route: '/air/manual/installation'),
|
||||||
SidebarItem(icon: Icons.inventory_2, label: "Collection", route: '/air/manual/collection'),
|
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.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(
|
SidebarItem(
|
||||||
@ -43,10 +45,11 @@ class AirHomePage extends StatelessWidget {
|
|||||||
label: "Continuous",
|
label: "Continuous",
|
||||||
isParent: true,
|
isParent: true,
|
||||||
children: [
|
children: [
|
||||||
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/continuous/dashboard'),
|
// MODIFIED: Updated to point to the new Info Centre screen
|
||||||
SidebarItem(icon: Icons.info, label: "Overview", route: '/air/continuous/overview'),
|
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/air/continuous/info'),
|
||||||
SidebarItem(icon: Icons.input, label: "Entry", route: '/air/continuous/entry'),
|
//SidebarItem(icon: Icons.info, label: "Overview", route: '/air/continuous/overview'),
|
||||||
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/continuous/report'),
|
//SidebarItem(icon: Icons.input, label: "Entry", route: '/air/continuous/entry'),
|
||||||
|
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/continuous/report'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SidebarItem(
|
SidebarItem(
|
||||||
@ -54,10 +57,11 @@ class AirHomePage extends StatelessWidget {
|
|||||||
label: "Investigative",
|
label: "Investigative",
|
||||||
isParent: true,
|
isParent: true,
|
||||||
children: [
|
children: [
|
||||||
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/investigative/dashboard'),
|
// MODIFIED: Updated to point to the new Info Centre screen
|
||||||
SidebarItem(icon: Icons.info, label: "Overview", route: '/air/investigative/overview'),
|
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/air/investigative/info'),
|
||||||
SidebarItem(icon: Icons.input, label: "Entry", route: '/air/investigative/entry'),
|
//SidebarItem(icon: Icons.info, label: "Overview", route: '/air/investigative/overview'),
|
||||||
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/investigative/report'),
|
//SidebarItem(icon: Icons.input, label: "Entry", route: '/air/investigative/entry'),
|
||||||
|
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/investigative/report'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,272 @@
|
|||||||
|
//lib/screens/air/continuous/air_continuous_info_centre_document.dart
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||||
|
import '../../../auth_provider.dart';
|
||||||
|
|
||||||
|
// This model is used generically across departments
|
||||||
|
class MarineDocument {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String module;
|
||||||
|
final String? group;
|
||||||
|
final String url;
|
||||||
|
final String? departmentName;
|
||||||
|
|
||||||
|
MarineDocument({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.module,
|
||||||
|
this.group,
|
||||||
|
required this.url,
|
||||||
|
this.departmentName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MarineDocument.fromMap(Map<String, dynamic> map) {
|
||||||
|
return MarineDocument(
|
||||||
|
id: map['id'],
|
||||||
|
title: map['title'],
|
||||||
|
module: map['module'],
|
||||||
|
group: map['group'],
|
||||||
|
url: map['url'],
|
||||||
|
departmentName: map['department_name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AirContinuousInfoCentreDocument extends StatefulWidget {
|
||||||
|
const AirContinuousInfoCentreDocument({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AirContinuousInfoCentreDocument> createState() => _AirContinuousInfoCentreDocumentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AirContinuousInfoCentreDocumentState extends State<AirContinuousInfoCentreDocument> {
|
||||||
|
final LocalStorageService _localStorageService = LocalStorageService();
|
||||||
|
late List<MarineDocument> _documents;
|
||||||
|
|
||||||
|
List<String> _documentGroups = [];
|
||||||
|
String? _selectedGroup;
|
||||||
|
|
||||||
|
Set<String> _downloadedUrls = {};
|
||||||
|
Map<String, double> _downloadProgress = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_documents = _getFilteredDocumentsFromProvider();
|
||||||
|
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
|
||||||
|
_documentGroups = ['All Documents', ...groups];
|
||||||
|
_selectedGroup = _documentGroups.first;
|
||||||
|
_checkInitialDownloadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MarineDocument> _getFilteredDocumentsFromProvider() {
|
||||||
|
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
|
||||||
|
if (documentsData == null || documentsData.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
|
||||||
|
|
||||||
|
return allDocuments.where((doc) {
|
||||||
|
return doc.departmentName == 'Air' && doc.module == 'Continuous';
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkInitialDownloadStatus() async {
|
||||||
|
final downloaded = <String>{};
|
||||||
|
for (var doc in _documents) {
|
||||||
|
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
|
||||||
|
downloaded.add(doc.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls = downloaded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleDownload(MarineDocument doc) async {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = 0.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _localStorageService.downloadDocument(
|
||||||
|
docUrl: doc.url,
|
||||||
|
onReceiveProgress: (progress) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = progress;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls.add(doc.url);
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Download failed for ${doc.title}')),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _viewDocument(MarineDocument doc) async {
|
||||||
|
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
|
||||||
|
if (localPath != null && mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PdfViewerScreen(
|
||||||
|
filePath: localPath,
|
||||||
|
title: doc.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTrailingWidget(MarineDocument doc) {
|
||||||
|
if (_downloadProgress.containsKey(doc.url)) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: _downloadProgress[doc.url],
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_downloadedUrls.contains(doc.url)) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.visibility, color: Colors.green),
|
||||||
|
tooltip: 'View Document',
|
||||||
|
onPressed: () => _viewDocument(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
|
||||||
|
tooltip: 'Download for Offline',
|
||||||
|
onPressed: () => _handleDownload(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final filteredDocs = _selectedGroup == 'All Documents'
|
||||||
|
? _documents
|
||||||
|
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Air Continuous Documents"),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedGroup,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Filter by Group',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
),
|
||||||
|
items: _documentGroups.map((String group) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: group,
|
||||||
|
child: Text(group),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedGroup = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
_selectedGroup!,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Divider(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: _documents.isEmpty
|
||||||
|
? const Center(child: Text("No Air Continuous documents found."))
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: filteredDocs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final doc = filteredDocs[index];
|
||||||
|
return Card(
|
||||||
|
elevation: 2.0,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: Colors.red.shade100,
|
||||||
|
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
|
||||||
|
),
|
||||||
|
title: Text(doc.title),
|
||||||
|
trailing: _buildTrailingWidget(doc),
|
||||||
|
onTap: _downloadedUrls.contains(doc.url)
|
||||||
|
? () => _viewDocument(doc)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PdfViewerScreen extends StatelessWidget {
|
||||||
|
final String filePath;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const PdfViewerScreen({
|
||||||
|
super.key,
|
||||||
|
required this.filePath,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
),
|
||||||
|
body: PDFView(
|
||||||
|
filePath: filePath,
|
||||||
|
enableSwipe: true,
|
||||||
|
swipeHorizontal: false,
|
||||||
|
autoSpacing: false,
|
||||||
|
pageFling: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,272 @@
|
|||||||
|
//lib/screens/air/investigative/air_investigative_info_centre_document.dart
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||||
|
import '../../../auth_provider.dart';
|
||||||
|
|
||||||
|
// This model is used generically across departments
|
||||||
|
class MarineDocument {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String module;
|
||||||
|
final String? group;
|
||||||
|
final String url;
|
||||||
|
final String? departmentName;
|
||||||
|
|
||||||
|
MarineDocument({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.module,
|
||||||
|
this.group,
|
||||||
|
required this.url,
|
||||||
|
this.departmentName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MarineDocument.fromMap(Map<String, dynamic> map) {
|
||||||
|
return MarineDocument(
|
||||||
|
id: map['id'],
|
||||||
|
title: map['title'],
|
||||||
|
module: map['module'],
|
||||||
|
group: map['group'],
|
||||||
|
url: map['url'],
|
||||||
|
departmentName: map['department_name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AirInvestigativeInfoCentreDocument extends StatefulWidget {
|
||||||
|
const AirInvestigativeInfoCentreDocument({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AirInvestigativeInfoCentreDocument> createState() => _AirInvestigativeInfoCentreDocumentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AirInvestigativeInfoCentreDocumentState extends State<AirInvestigativeInfoCentreDocument> {
|
||||||
|
final LocalStorageService _localStorageService = LocalStorageService();
|
||||||
|
late List<MarineDocument> _documents;
|
||||||
|
|
||||||
|
List<String> _documentGroups = [];
|
||||||
|
String? _selectedGroup;
|
||||||
|
|
||||||
|
Set<String> _downloadedUrls = {};
|
||||||
|
Map<String, double> _downloadProgress = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_documents = _getFilteredDocumentsFromProvider();
|
||||||
|
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
|
||||||
|
_documentGroups = ['All Documents', ...groups];
|
||||||
|
_selectedGroup = _documentGroups.first;
|
||||||
|
_checkInitialDownloadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MarineDocument> _getFilteredDocumentsFromProvider() {
|
||||||
|
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
|
||||||
|
if (documentsData == null || documentsData.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
|
||||||
|
|
||||||
|
return allDocuments.where((doc) {
|
||||||
|
return doc.departmentName == 'Air' && doc.module == 'Investigative';
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkInitialDownloadStatus() async {
|
||||||
|
final downloaded = <String>{};
|
||||||
|
for (var doc in _documents) {
|
||||||
|
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
|
||||||
|
downloaded.add(doc.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls = downloaded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleDownload(MarineDocument doc) async {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = 0.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _localStorageService.downloadDocument(
|
||||||
|
docUrl: doc.url,
|
||||||
|
onReceiveProgress: (progress) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = progress;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls.add(doc.url);
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Download failed for ${doc.title}')),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _viewDocument(MarineDocument doc) async {
|
||||||
|
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
|
||||||
|
if (localPath != null && mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PdfViewerScreen(
|
||||||
|
filePath: localPath,
|
||||||
|
title: doc.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTrailingWidget(MarineDocument doc) {
|
||||||
|
if (_downloadProgress.containsKey(doc.url)) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: _downloadProgress[doc.url],
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_downloadedUrls.contains(doc.url)) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.visibility, color: Colors.green),
|
||||||
|
tooltip: 'View Document',
|
||||||
|
onPressed: () => _viewDocument(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
|
||||||
|
tooltip: 'Download for Offline',
|
||||||
|
onPressed: () => _handleDownload(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final filteredDocs = _selectedGroup == 'All Documents'
|
||||||
|
? _documents
|
||||||
|
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Air Investigative Documents"),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedGroup,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Filter by Group',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
),
|
||||||
|
items: _documentGroups.map((String group) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: group,
|
||||||
|
child: Text(group),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedGroup = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
_selectedGroup!,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Divider(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: _documents.isEmpty
|
||||||
|
? const Center(child: Text("No Air Investigative documents found."))
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: filteredDocs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final doc = filteredDocs[index];
|
||||||
|
return Card(
|
||||||
|
elevation: 2.0,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: Colors.red.shade100,
|
||||||
|
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
|
||||||
|
),
|
||||||
|
title: Text(doc.title),
|
||||||
|
trailing: _buildTrailingWidget(doc),
|
||||||
|
onTap: _downloadedUrls.contains(doc.url)
|
||||||
|
? () => _viewDocument(doc)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PdfViewerScreen extends StatelessWidget {
|
||||||
|
final String filePath;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const PdfViewerScreen({
|
||||||
|
super.key,
|
||||||
|
required this.filePath,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
),
|
||||||
|
body: PDFView(
|
||||||
|
filePath: filePath,
|
||||||
|
enableSwipe: true,
|
||||||
|
swipeHorizontal: false,
|
||||||
|
autoSpacing: false,
|
||||||
|
pageFling: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
272
lib/screens/air/manual/air_manual_info_centre_document.dart
Normal file
272
lib/screens/air/manual/air_manual_info_centre_document.dart
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
//lib/screens/air/manual/air_manual_info_centre_document.dart
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||||
|
import '../../../auth_provider.dart';
|
||||||
|
|
||||||
|
// This model is used generically across departments
|
||||||
|
class MarineDocument {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String module;
|
||||||
|
final String? group;
|
||||||
|
final String url;
|
||||||
|
final String? departmentName;
|
||||||
|
|
||||||
|
MarineDocument({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.module,
|
||||||
|
this.group,
|
||||||
|
required this.url,
|
||||||
|
this.departmentName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MarineDocument.fromMap(Map<String, dynamic> map) {
|
||||||
|
return MarineDocument(
|
||||||
|
id: map['id'],
|
||||||
|
title: map['title'],
|
||||||
|
module: map['module'],
|
||||||
|
group: map['group'],
|
||||||
|
url: map['url'],
|
||||||
|
departmentName: map['department_name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AirManualInfoCentreDocument extends StatefulWidget {
|
||||||
|
const AirManualInfoCentreDocument({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AirManualInfoCentreDocument> createState() => _AirManualInfoCentreDocumentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AirManualInfoCentreDocumentState extends State<AirManualInfoCentreDocument> {
|
||||||
|
final LocalStorageService _localStorageService = LocalStorageService();
|
||||||
|
late List<MarineDocument> _documents;
|
||||||
|
|
||||||
|
List<String> _documentGroups = [];
|
||||||
|
String? _selectedGroup;
|
||||||
|
|
||||||
|
Set<String> _downloadedUrls = {};
|
||||||
|
Map<String, double> _downloadProgress = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_documents = _getFilteredDocumentsFromProvider();
|
||||||
|
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
|
||||||
|
_documentGroups = ['All Documents', ...groups];
|
||||||
|
_selectedGroup = _documentGroups.first;
|
||||||
|
_checkInitialDownloadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MarineDocument> _getFilteredDocumentsFromProvider() {
|
||||||
|
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
|
||||||
|
if (documentsData == null || documentsData.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
|
||||||
|
|
||||||
|
return allDocuments.where((doc) {
|
||||||
|
return doc.departmentName == 'Air' && doc.module == 'Manual';
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkInitialDownloadStatus() async {
|
||||||
|
final downloaded = <String>{};
|
||||||
|
for (var doc in _documents) {
|
||||||
|
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
|
||||||
|
downloaded.add(doc.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls = downloaded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleDownload(MarineDocument doc) async {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = 0.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _localStorageService.downloadDocument(
|
||||||
|
docUrl: doc.url,
|
||||||
|
onReceiveProgress: (progress) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = progress;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls.add(doc.url);
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Download failed for ${doc.title}')),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _viewDocument(MarineDocument doc) async {
|
||||||
|
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
|
||||||
|
if (localPath != null && mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PdfViewerScreen(
|
||||||
|
filePath: localPath,
|
||||||
|
title: doc.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTrailingWidget(MarineDocument doc) {
|
||||||
|
if (_downloadProgress.containsKey(doc.url)) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: _downloadProgress[doc.url],
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_downloadedUrls.contains(doc.url)) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.visibility, color: Colors.green),
|
||||||
|
tooltip: 'View Document',
|
||||||
|
onPressed: () => _viewDocument(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
|
||||||
|
tooltip: 'Download for Offline',
|
||||||
|
onPressed: () => _handleDownload(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final filteredDocs = _selectedGroup == 'All Documents'
|
||||||
|
? _documents
|
||||||
|
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Air Manual Documents"),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedGroup,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Filter by Group',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
),
|
||||||
|
items: _documentGroups.map((String group) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: group,
|
||||||
|
child: Text(group),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedGroup = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
_selectedGroup!,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Divider(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: _documents.isEmpty
|
||||||
|
? const Center(child: Text("No Air Manual documents found."))
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: filteredDocs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final doc = filteredDocs[index];
|
||||||
|
return Card(
|
||||||
|
elevation: 2.0,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: Colors.red.shade100,
|
||||||
|
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
|
||||||
|
),
|
||||||
|
title: Text(doc.title),
|
||||||
|
trailing: _buildTrailingWidget(doc),
|
||||||
|
onTap: _downloadedUrls.contains(doc.url)
|
||||||
|
? () => _viewDocument(doc)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PdfViewerScreen extends StatelessWidget {
|
||||||
|
final String filePath;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const PdfViewerScreen({
|
||||||
|
super.key,
|
||||||
|
required this.filePath,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
),
|
||||||
|
body: PDFView(
|
||||||
|
filePath: filePath,
|
||||||
|
enableSwipe: true,
|
||||||
|
swipeHorizontal: false,
|
||||||
|
autoSpacing: false,
|
||||||
|
pageFling: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,272 @@
|
|||||||
|
//lib/screens/marine/continuous/marine_continuous_info_centre_document.dart
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||||
|
import '../../../auth_provider.dart';
|
||||||
|
|
||||||
|
// This model is used generically across departments
|
||||||
|
class MarineDocument {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String module;
|
||||||
|
final String? group;
|
||||||
|
final String url;
|
||||||
|
final String? departmentName;
|
||||||
|
|
||||||
|
MarineDocument({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.module,
|
||||||
|
this.group,
|
||||||
|
required this.url,
|
||||||
|
this.departmentName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MarineDocument.fromMap(Map<String, dynamic> map) {
|
||||||
|
return MarineDocument(
|
||||||
|
id: map['id'],
|
||||||
|
title: map['title'],
|
||||||
|
module: map['module'],
|
||||||
|
group: map['group'],
|
||||||
|
url: map['url'],
|
||||||
|
departmentName: map['department_name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MarineContinuousInfoCentreDocument extends StatefulWidget {
|
||||||
|
const MarineContinuousInfoCentreDocument({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MarineContinuousInfoCentreDocument> createState() => _MarineContinuousInfoCentreDocumentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MarineContinuousInfoCentreDocumentState extends State<MarineContinuousInfoCentreDocument> {
|
||||||
|
final LocalStorageService _localStorageService = LocalStorageService();
|
||||||
|
late List<MarineDocument> _documents;
|
||||||
|
|
||||||
|
List<String> _documentGroups = [];
|
||||||
|
String? _selectedGroup;
|
||||||
|
|
||||||
|
Set<String> _downloadedUrls = {};
|
||||||
|
Map<String, double> _downloadProgress = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_documents = _getFilteredDocumentsFromProvider();
|
||||||
|
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
|
||||||
|
_documentGroups = ['All Documents', ...groups];
|
||||||
|
_selectedGroup = _documentGroups.first;
|
||||||
|
_checkInitialDownloadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MarineDocument> _getFilteredDocumentsFromProvider() {
|
||||||
|
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
|
||||||
|
if (documentsData == null || documentsData.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
|
||||||
|
|
||||||
|
return allDocuments.where((doc) {
|
||||||
|
return doc.departmentName == 'Marine' && doc.module == 'Continuous';
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkInitialDownloadStatus() async {
|
||||||
|
final downloaded = <String>{};
|
||||||
|
for (var doc in _documents) {
|
||||||
|
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
|
||||||
|
downloaded.add(doc.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls = downloaded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleDownload(MarineDocument doc) async {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = 0.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _localStorageService.downloadDocument(
|
||||||
|
docUrl: doc.url,
|
||||||
|
onReceiveProgress: (progress) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = progress;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls.add(doc.url);
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Download failed for ${doc.title}')),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _viewDocument(MarineDocument doc) async {
|
||||||
|
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
|
||||||
|
if (localPath != null && mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PdfViewerScreen(
|
||||||
|
filePath: localPath,
|
||||||
|
title: doc.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTrailingWidget(MarineDocument doc) {
|
||||||
|
if (_downloadProgress.containsKey(doc.url)) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: _downloadProgress[doc.url],
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_downloadedUrls.contains(doc.url)) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.visibility, color: Colors.green),
|
||||||
|
tooltip: 'View Document',
|
||||||
|
onPressed: () => _viewDocument(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
|
||||||
|
tooltip: 'Download for Offline',
|
||||||
|
onPressed: () => _handleDownload(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final filteredDocs = _selectedGroup == 'All Documents'
|
||||||
|
? _documents
|
||||||
|
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Marine Continuous Documents"),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedGroup,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Filter by Group',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
),
|
||||||
|
items: _documentGroups.map((String group) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: group,
|
||||||
|
child: Text(group),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedGroup = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
_selectedGroup!,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Divider(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: _documents.isEmpty
|
||||||
|
? const Center(child: Text("No Marine Continuous documents found."))
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: filteredDocs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final doc = filteredDocs[index];
|
||||||
|
return Card(
|
||||||
|
elevation: 2.0,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: Colors.red.shade100,
|
||||||
|
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
|
||||||
|
),
|
||||||
|
title: Text(doc.title),
|
||||||
|
trailing: _buildTrailingWidget(doc),
|
||||||
|
onTap: _downloadedUrls.contains(doc.url)
|
||||||
|
? () => _viewDocument(doc)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PdfViewerScreen extends StatelessWidget {
|
||||||
|
final String filePath;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const PdfViewerScreen({
|
||||||
|
super.key,
|
||||||
|
required this.filePath,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
),
|
||||||
|
body: PDFView(
|
||||||
|
filePath: filePath,
|
||||||
|
enableSwipe: true,
|
||||||
|
swipeHorizontal: false,
|
||||||
|
autoSpacing: false,
|
||||||
|
pageFling: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,272 @@
|
|||||||
|
//lib/screens/marine/investigative/marine_investigative_info_centre_document.dart
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||||
|
import '../../../auth_provider.dart';
|
||||||
|
|
||||||
|
// This model is used generically across departments
|
||||||
|
class MarineDocument {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String module;
|
||||||
|
final String? group;
|
||||||
|
final String url;
|
||||||
|
final String? departmentName;
|
||||||
|
|
||||||
|
MarineDocument({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.module,
|
||||||
|
this.group,
|
||||||
|
required this.url,
|
||||||
|
this.departmentName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MarineDocument.fromMap(Map<String, dynamic> map) {
|
||||||
|
return MarineDocument(
|
||||||
|
id: map['id'],
|
||||||
|
title: map['title'],
|
||||||
|
module: map['module'],
|
||||||
|
group: map['group'],
|
||||||
|
url: map['url'],
|
||||||
|
departmentName: map['department_name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MarineInvestigativeInfoCentreDocument extends StatefulWidget {
|
||||||
|
const MarineInvestigativeInfoCentreDocument({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MarineInvestigativeInfoCentreDocument> createState() => _MarineInvestigativeInfoCentreDocumentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MarineInvestigativeInfoCentreDocumentState extends State<MarineInvestigativeInfoCentreDocument> {
|
||||||
|
final LocalStorageService _localStorageService = LocalStorageService();
|
||||||
|
late List<MarineDocument> _documents;
|
||||||
|
|
||||||
|
List<String> _documentGroups = [];
|
||||||
|
String? _selectedGroup;
|
||||||
|
|
||||||
|
Set<String> _downloadedUrls = {};
|
||||||
|
Map<String, double> _downloadProgress = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_documents = _getFilteredDocumentsFromProvider();
|
||||||
|
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
|
||||||
|
_documentGroups = ['All Documents', ...groups];
|
||||||
|
_selectedGroup = _documentGroups.first;
|
||||||
|
_checkInitialDownloadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MarineDocument> _getFilteredDocumentsFromProvider() {
|
||||||
|
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
|
||||||
|
if (documentsData == null || documentsData.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
|
||||||
|
|
||||||
|
return allDocuments.where((doc) {
|
||||||
|
return doc.departmentName == 'Marine' && doc.module == 'Investigative';
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkInitialDownloadStatus() async {
|
||||||
|
final downloaded = <String>{};
|
||||||
|
for (var doc in _documents) {
|
||||||
|
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
|
||||||
|
downloaded.add(doc.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls = downloaded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleDownload(MarineDocument doc) async {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = 0.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _localStorageService.downloadDocument(
|
||||||
|
docUrl: doc.url,
|
||||||
|
onReceiveProgress: (progress) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = progress;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls.add(doc.url);
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Download failed for ${doc.title}')),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _viewDocument(MarineDocument doc) async {
|
||||||
|
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
|
||||||
|
if (localPath != null && mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PdfViewerScreen(
|
||||||
|
filePath: localPath,
|
||||||
|
title: doc.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTrailingWidget(MarineDocument doc) {
|
||||||
|
if (_downloadProgress.containsKey(doc.url)) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: _downloadProgress[doc.url],
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_downloadedUrls.contains(doc.url)) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.visibility, color: Colors.green),
|
||||||
|
tooltip: 'View Document',
|
||||||
|
onPressed: () => _viewDocument(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
|
||||||
|
tooltip: 'Download for Offline',
|
||||||
|
onPressed: () => _handleDownload(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final filteredDocs = _selectedGroup == 'All Documents'
|
||||||
|
? _documents
|
||||||
|
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Marine Investigative Documents"),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedGroup,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Filter by Group',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
),
|
||||||
|
items: _documentGroups.map((String group) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: group,
|
||||||
|
child: Text(group),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedGroup = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
_selectedGroup!,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Divider(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: _documents.isEmpty
|
||||||
|
? const Center(child: Text("No Marine Investigative documents found."))
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: filteredDocs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final doc = filteredDocs[index];
|
||||||
|
return Card(
|
||||||
|
elevation: 2.0,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: Colors.red.shade100,
|
||||||
|
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
|
||||||
|
),
|
||||||
|
title: Text(doc.title),
|
||||||
|
trailing: _buildTrailingWidget(doc),
|
||||||
|
onTap: _downloadedUrls.contains(doc.url)
|
||||||
|
? () => _viewDocument(doc)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PdfViewerScreen extends StatelessWidget {
|
||||||
|
final String filePath;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const PdfViewerScreen({
|
||||||
|
super.key,
|
||||||
|
required this.filePath,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
),
|
||||||
|
body: PDFView(
|
||||||
|
filePath: filePath,
|
||||||
|
enableSwipe: true,
|
||||||
|
swipeHorizontal: false,
|
||||||
|
autoSpacing: false,
|
||||||
|
pageFling: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,54 +1,259 @@
|
|||||||
|
// lib/screens/marine/manual/info_centre_document.dart
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||||
|
import '../../../auth_provider.dart';
|
||||||
|
|
||||||
|
// (The MarineDocument model class remains the same)
|
||||||
|
class MarineDocument {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String module;
|
||||||
|
final String? group;
|
||||||
|
final String url;
|
||||||
|
final String? departmentName;
|
||||||
|
|
||||||
|
MarineDocument({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.module,
|
||||||
|
this.group,
|
||||||
|
required this.url,
|
||||||
|
this.departmentName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MarineDocument.fromMap(Map<String, dynamic> map) {
|
||||||
|
return MarineDocument(
|
||||||
|
id: map['id'],
|
||||||
|
title: map['title'],
|
||||||
|
module: map['module'],
|
||||||
|
group: map['group'],
|
||||||
|
url: map['url'],
|
||||||
|
departmentName: map['department_name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class MarineInfoCentreDocument extends StatefulWidget {
|
class MarineInfoCentreDocument extends StatefulWidget {
|
||||||
|
const MarineInfoCentreDocument({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MarineInfoCentreDocument> createState() => _MarineInfoCentreDocumentState();
|
State<MarineInfoCentreDocument> createState() => _MarineInfoCentreDocumentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MarineInfoCentreDocumentState extends State<MarineInfoCentreDocument> {
|
class _MarineInfoCentreDocumentState extends State<MarineInfoCentreDocument> {
|
||||||
String? selectedFileName;
|
final LocalStorageService _localStorageService = LocalStorageService();
|
||||||
|
late List<MarineDocument> _documents;
|
||||||
|
|
||||||
Future<void> _pickDocument() async {
|
// --- STATE FOR NEW UI ---
|
||||||
final result = await FilePicker.platform.pickFiles();
|
// Holds all available groups for the dropdown
|
||||||
if (result != null && result.files.isNotEmpty) {
|
List<String> _documentGroups = [];
|
||||||
setState(() => selectedFileName = result.files.first.name);
|
// Holds the currently selected group from the dropdown
|
||||||
|
String? _selectedGroup;
|
||||||
|
|
||||||
|
Set<String> _downloadedUrls = {};
|
||||||
|
Map<String, double> _downloadProgress = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_documents = _getFilteredDocumentsFromProvider();
|
||||||
|
|
||||||
|
// --- NEW: Populate the list of groups for the dropdown ---
|
||||||
|
// Get unique group names from the documents
|
||||||
|
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
|
||||||
|
// Add "All Documents" as the first option
|
||||||
|
_documentGroups = ['All Documents', ...groups];
|
||||||
|
// Set the initial value of the dropdown
|
||||||
|
_selectedGroup = _documentGroups.first;
|
||||||
|
|
||||||
|
_checkInitialDownloadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MarineDocument> _getFilteredDocumentsFromProvider() {
|
||||||
|
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
|
||||||
|
if (documentsData == null || documentsData.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
|
||||||
|
|
||||||
|
return allDocuments.where((doc) {
|
||||||
|
return doc.departmentName == 'Marine' && doc.module == 'Manual';
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkInitialDownloadStatus() async {
|
||||||
|
final downloaded = <String>{};
|
||||||
|
for (var doc in _documents) {
|
||||||
|
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
|
||||||
|
downloaded.add(doc.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls = downloaded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleDownload(MarineDocument doc) async {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = 0.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _localStorageService.downloadDocument(
|
||||||
|
docUrl: doc.url,
|
||||||
|
onReceiveProgress: (progress) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = progress;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls.add(doc.url);
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text("Document '${result.files.first.name}' selected")),
|
SnackBar(content: Text('Download failed for ${doc.title}')),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _viewDocument(MarineDocument doc) async {
|
||||||
|
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
|
||||||
|
if (localPath != null && mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PdfViewerScreen(
|
||||||
|
filePath: localPath,
|
||||||
|
title: doc.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTrailingWidget(MarineDocument doc) {
|
||||||
|
if (_downloadProgress.containsKey(doc.url)) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: _downloadProgress[doc.url],
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_downloadedUrls.contains(doc.url)) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.visibility, color: Colors.green),
|
||||||
|
tooltip: 'View Document',
|
||||||
|
onPressed: () => _viewDocument(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
|
||||||
|
tooltip: 'Download for Offline',
|
||||||
|
onPressed: () => _handleDownload(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text("Marine Info Centre Document")),
|
appBar: AppBar(
|
||||||
|
title: const Text("Marine Manuals"),
|
||||||
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text("Upload or View Reference Documents", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
// --- NEW: Styled Dropdown for filtering ---
|
||||||
SizedBox(height: 24),
|
DropdownButtonFormField<String>(
|
||||||
ElevatedButton.icon(
|
value: _selectedGroup,
|
||||||
icon: Icon(Icons.upload_file),
|
decoration: InputDecoration(
|
||||||
label: Text("Select Document"),
|
labelText: 'Filter by Group',
|
||||||
onPressed: _pickDocument,
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
if (selectedFileName != null)
|
),
|
||||||
Text("Selected: $selectedFileName", style: TextStyle(fontSize: 16)),
|
items: _documentGroups.map((String group) {
|
||||||
SizedBox(height: 24),
|
return DropdownMenuItem<String>(
|
||||||
ElevatedButton(
|
value: group,
|
||||||
onPressed: selectedFileName != null
|
child: Text(group),
|
||||||
? () {
|
|
||||||
// Submit logic here
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text("Document submitted")),
|
|
||||||
);
|
);
|
||||||
}
|
}).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,
|
: null,
|
||||||
child: Text("Submit Document"),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -56,3 +261,31 @@ class _MarineInfoCentreDocumentState extends State<MarineInfoCentreDocument> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PdfViewerScreen extends StatelessWidget {
|
||||||
|
final String filePath;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const PdfViewerScreen({
|
||||||
|
super.key,
|
||||||
|
required this.filePath,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
),
|
||||||
|
body: PDFView(
|
||||||
|
filePath: filePath,
|
||||||
|
enableSwipe: true,
|
||||||
|
swipeHorizontal: false,
|
||||||
|
autoSpacing: false,
|
||||||
|
pageFling: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -37,8 +37,8 @@ class MarineHomePage extends StatelessWidget {
|
|||||||
SidebarItem(icon: Icons.waves, label: "Tarball Sampling", route: '/marine/manual/tarball'),
|
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.article, label: "Data Log", route: '/marine/manual/data-log'),
|
||||||
SidebarItem(icon: Icons.image, label: "Image Request", route: '/marine/manual/image-request'),
|
//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.receipt_long, label: "Report", route: '/marine/manual/report'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SidebarItem(
|
SidebarItem(
|
||||||
@ -46,10 +46,11 @@ class MarineHomePage extends StatelessWidget {
|
|||||||
label: "Continuous",
|
label: "Continuous",
|
||||||
isParent: true,
|
isParent: true,
|
||||||
children: [
|
children: [
|
||||||
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/marine/continuous/dashboard'),
|
// MODIFIED: Updated label, icon, and route for the new Info Centre screen
|
||||||
SidebarItem(icon: Icons.info, label: "Overview", route: '/marine/continuous/overview'),
|
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/marine/continuous/info'),
|
||||||
SidebarItem(icon: Icons.input, label: "Entry", route: '/marine/continuous/entry'),
|
//SidebarItem(icon: Icons.info, label: "Overview", route: '/marine/continuous/overview'),
|
||||||
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/continuous/report'),
|
//SidebarItem(icon: Icons.input, label: "Entry", route: '/marine/continuous/entry'),
|
||||||
|
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/continuous/report'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SidebarItem(
|
SidebarItem(
|
||||||
@ -57,10 +58,11 @@ class MarineHomePage extends StatelessWidget {
|
|||||||
label: "Investigative",
|
label: "Investigative",
|
||||||
isParent: true,
|
isParent: true,
|
||||||
children: [
|
children: [
|
||||||
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/marine/investigative/dashboard'),
|
// MODIFIED: Updated label, icon, and route for the new Info Centre screen
|
||||||
SidebarItem(icon: Icons.info, label: "Overview", route: '/marine/investigative/overview'),
|
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/marine/investigative/info'),
|
||||||
SidebarItem(icon: Icons.input, label: "Entry", route: '/marine/investigative/entry'),
|
//SidebarItem(icon: Icons.info, label: "Overview", route: '/marine/investigative/overview'),
|
||||||
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/investigative/report'),
|
//SidebarItem(icon: Icons.input, label: "Entry", route: '/marine/investigative/entry'),
|
||||||
|
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/investigative/report'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,272 @@
|
|||||||
|
//lib/screens/river/continuous/river_continuous_info_centre_document.dart
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||||
|
import '../../../auth_provider.dart';
|
||||||
|
|
||||||
|
// This model is used generically across departments
|
||||||
|
class MarineDocument {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String module;
|
||||||
|
final String? group;
|
||||||
|
final String url;
|
||||||
|
final String? departmentName;
|
||||||
|
|
||||||
|
MarineDocument({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.module,
|
||||||
|
this.group,
|
||||||
|
required this.url,
|
||||||
|
this.departmentName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MarineDocument.fromMap(Map<String, dynamic> map) {
|
||||||
|
return MarineDocument(
|
||||||
|
id: map['id'],
|
||||||
|
title: map['title'],
|
||||||
|
module: map['module'],
|
||||||
|
group: map['group'],
|
||||||
|
url: map['url'],
|
||||||
|
departmentName: map['department_name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RiverContinuousInfoCentreDocument extends StatefulWidget {
|
||||||
|
const RiverContinuousInfoCentreDocument({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RiverContinuousInfoCentreDocument> createState() => _RiverContinuousInfoCentreDocumentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RiverContinuousInfoCentreDocumentState extends State<RiverContinuousInfoCentreDocument> {
|
||||||
|
final LocalStorageService _localStorageService = LocalStorageService();
|
||||||
|
late List<MarineDocument> _documents;
|
||||||
|
|
||||||
|
List<String> _documentGroups = [];
|
||||||
|
String? _selectedGroup;
|
||||||
|
|
||||||
|
Set<String> _downloadedUrls = {};
|
||||||
|
Map<String, double> _downloadProgress = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_documents = _getFilteredDocumentsFromProvider();
|
||||||
|
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
|
||||||
|
_documentGroups = ['All Documents', ...groups];
|
||||||
|
_selectedGroup = _documentGroups.first;
|
||||||
|
_checkInitialDownloadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MarineDocument> _getFilteredDocumentsFromProvider() {
|
||||||
|
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
|
||||||
|
if (documentsData == null || documentsData.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
|
||||||
|
|
||||||
|
return allDocuments.where((doc) {
|
||||||
|
return doc.departmentName == 'River' && doc.module == 'Continuous';
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkInitialDownloadStatus() async {
|
||||||
|
final downloaded = <String>{};
|
||||||
|
for (var doc in _documents) {
|
||||||
|
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
|
||||||
|
downloaded.add(doc.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls = downloaded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleDownload(MarineDocument doc) async {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = 0.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _localStorageService.downloadDocument(
|
||||||
|
docUrl: doc.url,
|
||||||
|
onReceiveProgress: (progress) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = progress;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls.add(doc.url);
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Download failed for ${doc.title}')),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _viewDocument(MarineDocument doc) async {
|
||||||
|
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
|
||||||
|
if (localPath != null && mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PdfViewerScreen(
|
||||||
|
filePath: localPath,
|
||||||
|
title: doc.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTrailingWidget(MarineDocument doc) {
|
||||||
|
if (_downloadProgress.containsKey(doc.url)) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: _downloadProgress[doc.url],
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_downloadedUrls.contains(doc.url)) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.visibility, color: Colors.green),
|
||||||
|
tooltip: 'View Document',
|
||||||
|
onPressed: () => _viewDocument(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
|
||||||
|
tooltip: 'Download for Offline',
|
||||||
|
onPressed: () => _handleDownload(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final filteredDocs = _selectedGroup == 'All Documents'
|
||||||
|
? _documents
|
||||||
|
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("River Continuous Documents"),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedGroup,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Filter by Group',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
),
|
||||||
|
items: _documentGroups.map((String group) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: group,
|
||||||
|
child: Text(group),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedGroup = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
_selectedGroup!,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Divider(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: _documents.isEmpty
|
||||||
|
? const Center(child: Text("No River Continuous documents found."))
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: filteredDocs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final doc = filteredDocs[index];
|
||||||
|
return Card(
|
||||||
|
elevation: 2.0,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: Colors.red.shade100,
|
||||||
|
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
|
||||||
|
),
|
||||||
|
title: Text(doc.title),
|
||||||
|
trailing: _buildTrailingWidget(doc),
|
||||||
|
onTap: _downloadedUrls.contains(doc.url)
|
||||||
|
? () => _viewDocument(doc)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PdfViewerScreen extends StatelessWidget {
|
||||||
|
final String filePath;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const PdfViewerScreen({
|
||||||
|
super.key,
|
||||||
|
required this.filePath,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
),
|
||||||
|
body: PDFView(
|
||||||
|
filePath: filePath,
|
||||||
|
enableSwipe: true,
|
||||||
|
swipeHorizontal: false,
|
||||||
|
autoSpacing: false,
|
||||||
|
pageFling: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,272 @@
|
|||||||
|
//lib/screens/river/investigative/river_investigative_info_centre_document.dart
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||||
|
import '../../../auth_provider.dart';
|
||||||
|
|
||||||
|
// This model is used generically across departments
|
||||||
|
class MarineDocument {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String module;
|
||||||
|
final String? group;
|
||||||
|
final String url;
|
||||||
|
final String? departmentName;
|
||||||
|
|
||||||
|
MarineDocument({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.module,
|
||||||
|
this.group,
|
||||||
|
required this.url,
|
||||||
|
this.departmentName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MarineDocument.fromMap(Map<String, dynamic> map) {
|
||||||
|
return MarineDocument(
|
||||||
|
id: map['id'],
|
||||||
|
title: map['title'],
|
||||||
|
module: map['module'],
|
||||||
|
group: map['group'],
|
||||||
|
url: map['url'],
|
||||||
|
departmentName: map['department_name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RiverInvestigativeInfoCentreDocument extends StatefulWidget {
|
||||||
|
const RiverInvestigativeInfoCentreDocument({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RiverInvestigativeInfoCentreDocument> createState() => _RiverInvestigativeInfoCentreDocumentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RiverInvestigativeInfoCentreDocumentState extends State<RiverInvestigativeInfoCentreDocument> {
|
||||||
|
final LocalStorageService _localStorageService = LocalStorageService();
|
||||||
|
late List<MarineDocument> _documents;
|
||||||
|
|
||||||
|
List<String> _documentGroups = [];
|
||||||
|
String? _selectedGroup;
|
||||||
|
|
||||||
|
Set<String> _downloadedUrls = {};
|
||||||
|
Map<String, double> _downloadProgress = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_documents = _getFilteredDocumentsFromProvider();
|
||||||
|
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
|
||||||
|
_documentGroups = ['All Documents', ...groups];
|
||||||
|
_selectedGroup = _documentGroups.first;
|
||||||
|
_checkInitialDownloadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MarineDocument> _getFilteredDocumentsFromProvider() {
|
||||||
|
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
|
||||||
|
if (documentsData == null || documentsData.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
|
||||||
|
|
||||||
|
return allDocuments.where((doc) {
|
||||||
|
return doc.departmentName == 'River' && doc.module == 'Investigative';
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkInitialDownloadStatus() async {
|
||||||
|
final downloaded = <String>{};
|
||||||
|
for (var doc in _documents) {
|
||||||
|
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
|
||||||
|
downloaded.add(doc.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls = downloaded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleDownload(MarineDocument doc) async {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = 0.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _localStorageService.downloadDocument(
|
||||||
|
docUrl: doc.url,
|
||||||
|
onReceiveProgress: (progress) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = progress;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls.add(doc.url);
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Download failed for ${doc.title}')),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _viewDocument(MarineDocument doc) async {
|
||||||
|
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
|
||||||
|
if (localPath != null && mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PdfViewerScreen(
|
||||||
|
filePath: localPath,
|
||||||
|
title: doc.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTrailingWidget(MarineDocument doc) {
|
||||||
|
if (_downloadProgress.containsKey(doc.url)) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: _downloadProgress[doc.url],
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_downloadedUrls.contains(doc.url)) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.visibility, color: Colors.green),
|
||||||
|
tooltip: 'View Document',
|
||||||
|
onPressed: () => _viewDocument(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
|
||||||
|
tooltip: 'Download for Offline',
|
||||||
|
onPressed: () => _handleDownload(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final filteredDocs = _selectedGroup == 'All Documents'
|
||||||
|
? _documents
|
||||||
|
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("River Investigative Documents"),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedGroup,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Filter by Group',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
),
|
||||||
|
items: _documentGroups.map((String group) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: group,
|
||||||
|
child: Text(group),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedGroup = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
_selectedGroup!,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Divider(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: _documents.isEmpty
|
||||||
|
? const Center(child: Text("No River Investigative documents found."))
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: filteredDocs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final doc = filteredDocs[index];
|
||||||
|
return Card(
|
||||||
|
elevation: 2.0,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: Colors.red.shade100,
|
||||||
|
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
|
||||||
|
),
|
||||||
|
title: Text(doc.title),
|
||||||
|
trailing: _buildTrailingWidget(doc),
|
||||||
|
onTap: _downloadedUrls.contains(doc.url)
|
||||||
|
? () => _viewDocument(doc)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PdfViewerScreen extends StatelessWidget {
|
||||||
|
final String filePath;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const PdfViewerScreen({
|
||||||
|
super.key,
|
||||||
|
required this.filePath,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
),
|
||||||
|
body: PDFView(
|
||||||
|
filePath: filePath,
|
||||||
|
enableSwipe: true,
|
||||||
|
swipeHorizontal: false,
|
||||||
|
autoSpacing: false,
|
||||||
|
pageFling: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
272
lib/screens/river/manual/river_manual_info_centre_document.dart
Normal file
272
lib/screens/river/manual/river_manual_info_centre_document.dart
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
//lib/screens/river/manual/river_manual_info_centre_document.dart
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||||
|
import '../../../auth_provider.dart';
|
||||||
|
|
||||||
|
// This model is used generically across departments
|
||||||
|
class MarineDocument {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String module;
|
||||||
|
final String? group;
|
||||||
|
final String url;
|
||||||
|
final String? departmentName;
|
||||||
|
|
||||||
|
MarineDocument({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.module,
|
||||||
|
this.group,
|
||||||
|
required this.url,
|
||||||
|
this.departmentName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MarineDocument.fromMap(Map<String, dynamic> map) {
|
||||||
|
return MarineDocument(
|
||||||
|
id: map['id'],
|
||||||
|
title: map['title'],
|
||||||
|
module: map['module'],
|
||||||
|
group: map['group'],
|
||||||
|
url: map['url'],
|
||||||
|
departmentName: map['department_name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RiverManualInfoCentreDocument extends StatefulWidget {
|
||||||
|
const RiverManualInfoCentreDocument({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RiverManualInfoCentreDocument> createState() => _RiverManualInfoCentreDocumentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RiverManualInfoCentreDocumentState extends State<RiverManualInfoCentreDocument> {
|
||||||
|
final LocalStorageService _localStorageService = LocalStorageService();
|
||||||
|
late List<MarineDocument> _documents;
|
||||||
|
|
||||||
|
List<String> _documentGroups = [];
|
||||||
|
String? _selectedGroup;
|
||||||
|
|
||||||
|
Set<String> _downloadedUrls = {};
|
||||||
|
Map<String, double> _downloadProgress = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_documents = _getFilteredDocumentsFromProvider();
|
||||||
|
final groups = _documents.map((doc) => doc.group ?? 'Uncategorized').toSet().toList();
|
||||||
|
_documentGroups = ['All Documents', ...groups];
|
||||||
|
_selectedGroup = _documentGroups.first;
|
||||||
|
_checkInitialDownloadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MarineDocument> _getFilteredDocumentsFromProvider() {
|
||||||
|
final documentsData = Provider.of<AuthProvider>(context, listen: false).documents;
|
||||||
|
if (documentsData == null || documentsData.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDocuments = documentsData.map((map) => MarineDocument.fromMap(map)).toList();
|
||||||
|
|
||||||
|
return allDocuments.where((doc) {
|
||||||
|
return doc.departmentName == 'River' && doc.module == 'Manual';
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkInitialDownloadStatus() async {
|
||||||
|
final downloaded = <String>{};
|
||||||
|
for (var doc in _documents) {
|
||||||
|
if (await _localStorageService.isDocumentDownloaded(doc.url)) {
|
||||||
|
downloaded.add(doc.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls = downloaded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleDownload(MarineDocument doc) async {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = 0.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _localStorageService.downloadDocument(
|
||||||
|
docUrl: doc.url,
|
||||||
|
onReceiveProgress: (progress) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress[doc.url] = progress;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedUrls.add(doc.url);
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Download failed for ${doc.title}')),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress.remove(doc.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _viewDocument(MarineDocument doc) async {
|
||||||
|
final localPath = await _localStorageService.getLocalDocumentPath(doc.url);
|
||||||
|
if (localPath != null && mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PdfViewerScreen(
|
||||||
|
filePath: localPath,
|
||||||
|
title: doc.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTrailingWidget(MarineDocument doc) {
|
||||||
|
if (_downloadProgress.containsKey(doc.url)) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: _downloadProgress[doc.url],
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_downloadedUrls.contains(doc.url)) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.visibility, color: Colors.green),
|
||||||
|
tooltip: 'View Document',
|
||||||
|
onPressed: () => _viewDocument(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.download_for_offline, color: Colors.blueAccent),
|
||||||
|
tooltip: 'Download for Offline',
|
||||||
|
onPressed: () => _handleDownload(doc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final filteredDocs = _selectedGroup == 'All Documents'
|
||||||
|
? _documents
|
||||||
|
: _documents.where((doc) => (doc.group ?? 'Uncategorized') == _selectedGroup).toList();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("River Manual Documents"),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedGroup,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Filter by Group',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
),
|
||||||
|
items: _documentGroups.map((String group) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: group,
|
||||||
|
child: Text(group),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {
|
||||||
|
_selectedGroup = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
_selectedGroup!,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Divider(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: _documents.isEmpty
|
||||||
|
? const Center(child: Text("No River Manual documents found."))
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: filteredDocs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final doc = filteredDocs[index];
|
||||||
|
return Card(
|
||||||
|
elevation: 2.0,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: Colors.red.shade100,
|
||||||
|
child: const Icon(Icons.picture_as_pdf, color: Colors.redAccent),
|
||||||
|
),
|
||||||
|
title: Text(doc.title),
|
||||||
|
trailing: _buildTrailingWidget(doc),
|
||||||
|
onTap: _downloadedUrls.contains(doc.url)
|
||||||
|
? () => _viewDocument(doc)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PdfViewerScreen extends StatelessWidget {
|
||||||
|
final String filePath;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const PdfViewerScreen({
|
||||||
|
super.key,
|
||||||
|
required this.filePath,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
),
|
||||||
|
body: PDFView(
|
||||||
|
filePath: filePath,
|
||||||
|
enableSwipe: true,
|
||||||
|
swipeHorizontal: false,
|
||||||
|
autoSpacing: false,
|
||||||
|
pageFling: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
//lib/screens/river/river_home_page.dart
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
// Re-defining SidebarItem here for self-containment,
|
// Re-defining SidebarItem here for self-containment,
|
||||||
@ -30,12 +32,12 @@ class RiverHomePage extends StatelessWidget {
|
|||||||
label: "Manual",
|
label: "Manual",
|
||||||
isParent: true,
|
isParent: true,
|
||||||
children: [
|
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.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.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.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(
|
SidebarItem(
|
||||||
@ -43,10 +45,11 @@ class RiverHomePage extends StatelessWidget {
|
|||||||
label: "Continuous",
|
label: "Continuous",
|
||||||
isParent: true,
|
isParent: true,
|
||||||
children: [
|
children: [
|
||||||
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/river/continuous/dashboard'),
|
// MODIFIED: Updated to point to the new Info Centre screen
|
||||||
SidebarItem(icon: Icons.info, label: "Overview", route: '/river/continuous/overview'),
|
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/river/continuous/info'),
|
||||||
SidebarItem(icon: Icons.input, label: "Entry", route: '/river/continuous/entry'),
|
//SidebarItem(icon: Icons.info, label: "Overview", route: '/river/continuous/overview'),
|
||||||
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/continuous/report'),
|
//SidebarItem(icon: Icons.input, label: "Entry", route: '/river/continuous/entry'),
|
||||||
|
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/continuous/report'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SidebarItem(
|
SidebarItem(
|
||||||
@ -54,10 +57,11 @@ class RiverHomePage extends StatelessWidget {
|
|||||||
label: "Investigative",
|
label: "Investigative",
|
||||||
isParent: true,
|
isParent: true,
|
||||||
children: [
|
children: [
|
||||||
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/river/investigative/dashboard'),
|
// MODIFIED: Updated to point to the new Info Centre screen
|
||||||
SidebarItem(icon: Icons.info, label: "Overview", route: '/river/investigative/overview'),
|
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/river/investigative/info'),
|
||||||
SidebarItem(icon: Icons.input, label: "Entry", route: '/river/investigative/entry'),
|
// SidebarItem(icon: Icons.info, label: "Overview", route: '/river/investigative/overview'),
|
||||||
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/investigative/report'),
|
//SidebarItem(icon: Icons.input, label: "Entry", route: '/river/investigative/entry'),
|
||||||
|
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/investigative/report'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -185,6 +185,9 @@ class ApiService {
|
|||||||
final syncTasks = {
|
final syncTasks = {
|
||||||
'profile': {'endpoint': 'profile', 'handler': (d, id) async { if (d.isNotEmpty) await dbHelper.saveProfile(d.first); }},
|
'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); }},
|
'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); }},
|
'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); }},
|
'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); }},
|
'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 {
|
class DatabaseHelper {
|
||||||
static Database? _database;
|
static Database? _database;
|
||||||
static const String _dbName = 'app_data.db';
|
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 _profileTable = 'user_profile';
|
||||||
static const String _usersTable = 'all_users';
|
static const String _usersTable = 'all_users';
|
||||||
@ -641,6 +645,8 @@ class DatabaseHelper {
|
|||||||
static const String _ftpConfigsTable = 'ftp_configurations';
|
static const String _ftpConfigsTable = 'ftp_configurations';
|
||||||
static const String _retryQueueTable = 'retry_queue';
|
static const String _retryQueueTable = 'retry_queue';
|
||||||
static const String _submissionLogTable = 'submission_log';
|
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 _modulePreferencesTable = 'module_preferences';
|
||||||
static const String _moduleApiLinksTable = 'module_api_links';
|
static const String _moduleApiLinksTable = 'module_api_links';
|
||||||
@ -730,6 +736,9 @@ class DatabaseHelper {
|
|||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
// END CHANGE
|
// END CHANGE
|
||||||
|
|
||||||
|
// --- ADDED: Create the documents table on initial creation ---
|
||||||
|
await db.execute('CREATE TABLE $_documentsTable(id INTEGER PRIMARY KEY, document_json TEXT)');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||||
@ -814,6 +823,10 @@ class DatabaseHelper {
|
|||||||
''');
|
''');
|
||||||
// END CHANGE
|
// 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.
|
/// Performs an "upsert": inserts new records or replaces existing ones.
|
||||||
@ -871,6 +884,11 @@ class DatabaseHelper {
|
|||||||
Future<void> deleteUsers(List<dynamic> ids) => _deleteData(_usersTable, 'user_id', ids);
|
Future<void> deleteUsers(List<dynamic> ids) => _deleteData(_usersTable, 'user_id', ids);
|
||||||
Future<List<Map<String, dynamic>>?> loadUsers() => _loadData(_usersTable, 'user');
|
Future<List<Map<String, dynamic>>?> loadUsers() => _loadData(_usersTable, 'user');
|
||||||
|
|
||||||
|
// --- ADDED: Handlers for the new documents table ---
|
||||||
|
Future<void> upsertDocuments(List<Map<String, dynamic>> data) => _upsertData(_documentsTable, 'id', data, 'document');
|
||||||
|
Future<void> deleteDocuments(List<dynamic> ids) => _deleteData(_documentsTable, 'id', ids);
|
||||||
|
Future<List<Map<String, dynamic>>?> loadDocuments() => _loadData(_documentsTable, 'document');
|
||||||
|
|
||||||
Future<void> upsertTarballStations(List<Map<String, dynamic>> data) => _upsertData(_tarballStationsTable, 'station_id', data, 'station');
|
Future<void> upsertTarballStations(List<Map<String, dynamic>> data) => _upsertData(_tarballStationsTable, 'station_id', data, 'station');
|
||||||
Future<void> deleteTarballStations(List<dynamic> ids) => _deleteData(_tarballStationsTable, 'station_id', ids);
|
Future<void> deleteTarballStations(List<dynamic> ids) => _deleteData(_tarballStationsTable, 'station_id', ids);
|
||||||
Future<List<Map<String, dynamic>>?> loadTarballStations() => _loadData(_tarballStationsTable, 'station');
|
Future<List<Map<String, dynamic>>?> loadTarballStations() => _loadData(_tarballStationsTable, 'station');
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:path/path.dart' as p;
|
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_installation_data.dart';
|
||||||
import '../models/air_collection_data.dart';
|
import '../models/air_collection_data.dart';
|
||||||
@ -539,4 +541,69 @@ class LocalStorageService {
|
|||||||
debugPrint("Error updating river in-situ log: $e");
|
debugPrint("Error updating river in-situ log: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// --- ADDED: Part 6: Info Centre Document Management ---
|
||||||
|
// =======================================================================
|
||||||
|
|
||||||
|
final Dio _dio = Dio();
|
||||||
|
|
||||||
|
/// Gets the directory for storing Info Centre documents, creating it if it doesn't exist.
|
||||||
|
Future<Directory?> _getInfoCentreDocumentsDirectory() async {
|
||||||
|
// We use serverName: '' to ensure documents are stored in a common root MMSV4 folder, not server-specific ones.
|
||||||
|
final mmsv4Dir = await _getPublicMMSV4Directory(serverName: '');
|
||||||
|
if (mmsv4Dir == null) return null;
|
||||||
|
|
||||||
|
final docDir = Directory(p.join(mmsv4Dir.path, 'info_centre_documents'));
|
||||||
|
if (!await docDir.exists()) {
|
||||||
|
await docDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
return docDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs the full local file path for a given document URL.
|
||||||
|
Future<String?> getLocalDocumentPath(String docUrl) async {
|
||||||
|
final docDir = await _getInfoCentreDocumentsDirectory();
|
||||||
|
if (docDir == null) return null;
|
||||||
|
|
||||||
|
final fileName = p.basename(docUrl);
|
||||||
|
return p.join(docDir.path, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a document has already been downloaded.
|
||||||
|
Future<bool> isDocumentDownloaded(String docUrl) async {
|
||||||
|
final filePath = await getLocalDocumentPath(docUrl);
|
||||||
|
if (filePath == null) return false;
|
||||||
|
return await File(filePath).exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads a document from a URL and saves it to the local `MMSV4/info_centre_documents` folder.
|
||||||
|
Future<void> downloadDocument({
|
||||||
|
required String docUrl,
|
||||||
|
required Function(double) onReceiveProgress,
|
||||||
|
}) async {
|
||||||
|
final filePath = await getLocalDocumentPath(docUrl);
|
||||||
|
if (filePath == null) {
|
||||||
|
throw Exception("Could not get local storage path. Check permissions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _dio.download(
|
||||||
|
docUrl,
|
||||||
|
filePath,
|
||||||
|
onReceiveProgress: (received, total) {
|
||||||
|
if (total != -1) {
|
||||||
|
onReceiveProgress(received / total);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// If the download fails, delete the partially downloaded file to prevent corruption.
|
||||||
|
final file = File(filePath);
|
||||||
|
if (await file.exists()) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
throw Exception("Download failed: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -7,9 +7,13 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import geolocator_apple
|
|||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
@ -21,4 +22,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
90
pubspec.lock
90
pubspec.lock
@ -121,6 +121,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
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:
|
dropdown_search:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -231,6 +247,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
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:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -838,6 +862,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
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:
|
usb_serial:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -944,4 +1032,4 @@ packages:
|
|||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0 <4.0.0"
|
dart: ">=3.8.0 <4.0.0"
|
||||||
flutter: ">=3.27.0"
|
flutter: ">=3.29.0"
|
||||||
|
|||||||
@ -29,6 +29,10 @@ dependencies:
|
|||||||
flutter_svg: ^2.0.9
|
flutter_svg: ^2.0.9
|
||||||
google_fonts: ^6.1.0
|
google_fonts: ^6.1.0
|
||||||
dropdown_search: ^5.0.6 # For searchable dropdowns in forms
|
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 ---
|
# --- Device & Hardware Access ---
|
||||||
image_picker: ^1.0.7
|
image_picker: ^1.0.7
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <geolocator_windows/geolocator_windows.h>
|
#include <geolocator_windows/geolocator_windows.h>
|
||||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
#include <webview_windows/webview_windows_plugin.h>
|
#include <webview_windows/webview_windows_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
WebviewWindowsPluginRegisterWithRegistrar(
|
WebviewWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("WebviewWindowsPlugin"));
|
registry->GetRegistrarForPlugin("WebviewWindowsPlugin"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
file_selector_windows
|
file_selector_windows
|
||||||
geolocator_windows
|
geolocator_windows
|
||||||
permission_handler_windows
|
permission_handler_windows
|
||||||
|
url_launcher_windows
|
||||||
webview_windows
|
webview_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user