From a11c0d8df8af71c9608a88b7d91458bf91aa0488 Mon Sep 17 00:00:00 2001 From: ALim Aidrus Date: Thu, 23 Oct 2025 14:57:35 +0800 Subject: [PATCH] fix marine maintenance form --- lib/home_page.dart | 2 +- lib/main.dart | 13 +- .../marine_inves_manual_sampling_data.dart | 1 + ...ine_manual_equipment_maintenance_data.dart | 149 ++- ...e_manual_pre_departure_checklist_data.dart | 19 +- .../marine_manual_sonde_calibration_data.dart | 77 +- ...ine_inves_manual_step_1_sampling_info.dart | 1 + .../marine_inves_manual_step_2_site_info.dart | 1 + ...rine_inves_manual_step_3_data_capture.dart | 1 + .../marine_inves_manual_step_4_summary.dart | 1 + .../marine_investigative_manual_sampling.dart | 1 + ...e_manual_equipment_maintenance_screen.dart | 974 ++++++++++++++++-- ...manual_pre_departure_checklist_screen.dart | 185 ++-- ...arine_manual_sonde_calibration_screen.dart | 319 ++++-- lib/screens/settings.dart | 2 +- lib/services/api_service.dart | 80 +- ..._manual_equipment_maintenance_service.dart | 71 +- .../marine_manual_pre_departure_service.dart | 50 +- ...rine_manual_sonde_calibration_service.dart | 46 +- 19 files changed, 1653 insertions(+), 340 deletions(-) create mode 100644 lib/models/marine_inves_manual_sampling_data.dart create mode 100644 lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_1_sampling_info.dart create mode 100644 lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart create mode 100644 lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_3_data_capture.dart create mode 100644 lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_4_summary.dart create mode 100644 lib/screens/marine/investigative/marine_investigative_manual_sampling.dart diff --git a/lib/home_page.dart b/lib/home_page.dart index 0ac68d0..7da25d2 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -29,7 +29,7 @@ class _HomePageState extends State { }); }, ), - title: const Text("MMS Version 3.5.01"), + title: const Text("MMS Version 3.7.01"), actions: [ IconButton( icon: const Icon(Icons.person), diff --git a/lib/main.dart b/lib/main.dart index bf7143f..cbcf208 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -140,9 +140,16 @@ void main() async { Provider(create: (context) => AirSamplingService(databaseHelper, telegramService)), Provider(create: (context) => MarineTarballSamplingService(telegramService)), Provider(create: (context) => MarineNpeReportService(Provider.of(context, listen: false))), - Provider(create: (context) => MarineManualPreDepartureService()), - Provider(create: (context) => MarineManualSondeCalibrationService()), - Provider(create: (context) => MarineManualEquipmentMaintenanceService()), + // --- UPDATED: Inject ApiService into the service constructors --- + Provider(create: (context) => MarineManualPreDepartureService( + Provider.of(context, listen: false) + )), + Provider(create: (context) => MarineManualSondeCalibrationService( + Provider.of(context, listen: false) + )), + Provider(create: (context) => MarineManualEquipmentMaintenanceService( + Provider.of(context, listen: false) + )), ], child: const RootApp(), ), diff --git a/lib/models/marine_inves_manual_sampling_data.dart b/lib/models/marine_inves_manual_sampling_data.dart new file mode 100644 index 0000000..6886dad --- /dev/null +++ b/lib/models/marine_inves_manual_sampling_data.dart @@ -0,0 +1 @@ +// lib/models/marine_inves_manual_sampling_data.dart \ No newline at end of file diff --git a/lib/models/marine_manual_equipment_maintenance_data.dart b/lib/models/marine_manual_equipment_maintenance_data.dart index d999614..f04c4e6 100644 --- a/lib/models/marine_manual_equipment_maintenance_data.dart +++ b/lib/models/marine_manual_equipment_maintenance_data.dart @@ -1,23 +1,140 @@ -class MarineManualEquipmentMaintenanceData { - int? performedByUserId; - String? equipmentName; - String? maintenanceDate; - String? maintenanceType; - String? workDescription; - String? partsReplaced; - String? status; - String? remarks; +import 'dart:convert'; +class MarineManualEquipmentMaintenanceData { + int? conductedByUserId; + String? maintenanceDate; + String? lastMaintenanceDate; + String? scheduleMaintenance; + bool isReplacement = false; + + String? timeStart; + String? timeEnd; + String? location; + + // Part 1 - YSI + Map ysiSondeChecks = {}; + String? ysiSondeComments; + Map> ysiSensorChecks = {}; + String? ysiSensorComments; + Map> ysiReplacements = {}; + + // Part 2 - Van Dorn Sampler + Map> vanDornChecks = {}; + String? vanDornComments; + String? vanDornCurrentSerial; + String? vanDornNewSerial; + Map> vanDornReplacements = {}; + + + // Constructor to initialize maps + MarineManualEquipmentMaintenanceData() { + // Init Part 1 Sonde Checks + ysiSondeChecks = {'Inspect': false, 'Clean': false}; + + // Init Part 1 Sensor Checks + [ + 'pH', + 'Conductivity', + 'Turbidity', + 'Dissolved Oxygen', + ].forEach((item) => ysiSensorChecks[item] = {'Inspect': false, 'Clean': false}); + + // Init Part 1 Replacements + [ + 'YSI EXO 2 Multiparameter Sonde', + 'pH', + 'Conductivity', + 'Turbidity', + 'Dissolved Oxygen', + ].forEach((item) => + ysiReplacements[item] = {'Current Serial': '', 'New Serial': ''}); + + // Init Part 2 Van Dorn Checks + vanDornChecks['Inside body, outside body & outlet valves cleaning'] = {'Inspect': false, 'Clean': false}; + vanDornChecks['Check cable assembly & tubing assembly'] = {'Inspect': false}; + vanDornChecks['Check messenger and rope'] = {'Inspect': false}; + + + // Init Part 2 Van Dorn Replacements + [ + 'End seals with air / drain valve', + 'Tubing Assembly', + 'Cable Assembly', + 'Main Tube Transparent', + ].forEach((item) => + vanDornReplacements[item] = {'Last Date': '', 'New Date': ''}); + } + + // MODIFIED: This method now builds the complex nested structure the PHP controller expects. Map toApiFormData() { + + // 1. YSI Sensor Checks List + List> ysiSensorChecksList = []; + ysiSensorChecks.forEach((sensorName, checks) { + ysiSensorChecksList.add({ + 'sensor_name': sensorName, + 'inspect_checked': checks['Inspect'] ?? false, + 'clean_checked': checks['Clean'] ?? false, + }); + }); + + // 2. YSI Replacements List + List> ysiReplacementsList = []; + ysiReplacements.forEach((itemName, serials) { + ysiReplacementsList.add({ + 'item_name': itemName, + 'current_serial': serials['Current Serial'] ?? '', + 'new_serial': serials['New Serial'] ?? '', + }); + }); + + // 3. Van Dorn Checks List + List> vanDornChecksList = []; + vanDornChecks.forEach((scopeName, checks) { + vanDornChecksList.add({ + 'scope_name': scopeName, + 'inspect_checked': checks['Inspect'] ?? false, + 'clean_checked': checks.containsKey('Clean') ? (checks['Clean'] ?? false) : null, // Handle items with no clean check + }); + }); + + // 4. Van Dorn Replacements List + List> vanDornReplacementsList = []; + vanDornReplacements.forEach((partName, dates) { + vanDornReplacementsList.add({ + 'part_name': partName, + // Send null if string is empty, which PHP controller expects + 'last_replacement_date': dates['Last Date']?.isEmpty ?? true ? null : dates['Last Date'], + 'new_replacement_date': dates['New Date']?.isEmpty ?? true ? null : dates['New Date'], + }); + }); + + // 5. Build final payload matching the controller return { - 'performed_by_user_id': performedByUserId.toString(), - 'equipment_name': equipmentName, + 'conducted_by_user_id': conductedByUserId.toString(), 'maintenance_date': maintenanceDate, - 'maintenance_type': maintenanceType, - 'work_description': workDescription, - 'parts_replaced': partsReplaced, - 'status': status, - 'remarks': remarks, + 'last_maintenance_date': lastMaintenanceDate?.isEmpty ?? true ? null : lastMaintenanceDate, + 'schedule_maintenance': scheduleMaintenance, + 'is_replacement': isReplacement, + 'time_start': timeStart?.isEmpty ?? true ? null : timeStart, + 'time_end': timeEnd?.isEmpty ?? true ? null : timeEnd, + 'location': location, + + // YSI Sonde main checks + 'ysi_sonde_inspect': ysiSondeChecks['Inspect'] ?? false, + 'ysi_sonde_clean': ysiSondeChecks['Clean'] ?? false, + + 'ysi_sonde_comments': ysiSondeComments, + 'ysi_sensor_comments': ysiSensorComments, + 'van_dorn_comments': vanDornComments, + 'van_dorn_current_serial': vanDornCurrentSerial, + 'van_dorn_new_serial': vanDornNewSerial, + + // The formatted lists + 'ysi_sensor_checks': ysiSensorChecksList, + 'ysi_replacements': ysiReplacementsList, + 'van_dorn_checks': vanDornChecksList, + 'van_dorn_replacements': vanDornReplacementsList, }; } } \ No newline at end of file diff --git a/lib/models/marine_manual_pre_departure_checklist_data.dart b/lib/models/marine_manual_pre_departure_checklist_data.dart index 724d5af..e493613 100644 --- a/lib/models/marine_manual_pre_departure_checklist_data.dart +++ b/lib/models/marine_manual_pre_departure_checklist_data.dart @@ -13,12 +13,25 @@ class MarineManualPreDepartureChecklistData { MarineManualPreDepartureChecklistData(); + // MODIFIED: This method now builds the nested array structure the PHP controller expects. Map toApiFormData() { + + // Create the 'items' list required by the API + List> itemsList = []; + + // Iterate over the checklist items and build the list + checklistItems.forEach((itemName, itemChecked) { + itemsList.add({ + 'item_name': itemName, + 'item_checked': itemChecked, + 'item_remark': remarks[itemName] ?? '' // Get the corresponding remark + }); + }); + return { - 'reporter_user_id': reporterUserId.toString(), + 'reporter_user_id': reporterUserId.toString(), // The controller gets this from auth, but good to send. 'submission_date': submissionDate, - 'checklist_items': jsonEncode(checklistItems), - 'remarks': jsonEncode(remarks), + 'items': itemsList, // Send the formatted list }; } } \ No newline at end of file diff --git a/lib/models/marine_manual_sonde_calibration_data.dart b/lib/models/marine_manual_sonde_calibration_data.dart index 149d53a..e68e44b 100644 --- a/lib/models/marine_manual_sonde_calibration_data.dart +++ b/lib/models/marine_manual_sonde_calibration_data.dart @@ -1,44 +1,59 @@ class MarineManualSondeCalibrationData { int? calibratedByUserId; - String? sondeId; - String? calibrationDateTime; - // pH values - double? ph4Initial; - double? ph4Calibrated; - double? ph7Initial; - double? ph7Calibrated; - double? ph10Initial; - double? ph10Calibrated; + // Header fields from PDF + String? sondeSerialNumber; + String? firmwareVersion; + String? korVersion; + String? location; + String? startDateTime; + String? endDateTime; - // Other parameters - double? condInitial; - double? condCalibrated; - double? doInitial; - double? doCalibrated; - double? turbidityInitial; - double? turbidityCalibrated; + // pH values (with Mv) + double? ph7Mv; + double? ph7Before; + double? ph7After; + double? ph10Mv; + double? ph10Before; + double? ph10After; + + // Other parameters (Mv removed per PDF) + double? condBefore; + double? condAfter; + double? doBefore; + double? doAfter; + double? turbidity0Before; + double? turbidity0After; + double? turbidity124Before; + double? turbidity124After; String? calibrationStatus; - String? remarks; + String? remarks; // Matches "COMMENT/OBSERVATION" Map toApiFormData() { + // This flat structure matches MarineSondeCalibrationController.php return { 'calibrated_by_user_id': calibratedByUserId.toString(), - 'sonde_id': sondeId, - 'calibration_datetime': calibrationDateTime, - 'ph_4_initial': ph4Initial?.toString(), - 'ph_4_calibrated': ph4Calibrated?.toString(), - 'ph_7_initial': ph7Initial?.toString(), - 'ph_7_calibrated': ph7Calibrated?.toString(), - 'ph_10_initial': ph10Initial?.toString(), - 'ph_10_calibrated': ph10Calibrated?.toString(), - 'cond_initial': condInitial?.toString(), - 'cond_calibrated': condCalibrated?.toString(), - 'do_initial': doInitial?.toString(), - 'do_calibrated': doCalibrated?.toString(), - 'turbidity_initial': turbidityInitial?.toString(), - 'turbidity_calibrated': turbidityCalibrated?.toString(), + 'sonde_serial_number': sondeSerialNumber, + 'firmware_version': firmwareVersion, + 'kor_version': korVersion, + 'location': location, + 'start_datetime': startDateTime, + 'end_datetime': endDateTime, + 'ph_7_mv': ph7Mv?.toString(), + 'ph_7_before': ph7Before?.toString(), + 'ph_7_after': ph7After?.toString(), + 'ph_10_mv': ph10Mv?.toString(), + 'ph_10_before': ph10Before?.toString(), + 'ph_10_after': ph10After?.toString(), + 'cond_before': condBefore?.toString(), + 'cond_after': condAfter?.toString(), + 'do_before': doBefore?.toString(), + 'do_after': doAfter?.toString(), + 'turbidity_0_before': turbidity0Before?.toString(), + 'turbidity_0_after': turbidity0After?.toString(), + 'turbidity_124_before': turbidity124Before?.toString(), + 'turbidity_124_after': turbidity124After?.toString(), 'calibration_status': calibrationStatus, 'remarks': remarks, }; diff --git a/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_1_sampling_info.dart b/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_1_sampling_info.dart new file mode 100644 index 0000000..5df359c --- /dev/null +++ b/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_1_sampling_info.dart @@ -0,0 +1 @@ +//lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_1_sampling_info.dart \ No newline at end of file diff --git a/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart b/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart new file mode 100644 index 0000000..f050e61 --- /dev/null +++ b/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart @@ -0,0 +1 @@ +//lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart \ No newline at end of file diff --git a/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_3_data_capture.dart b/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_3_data_capture.dart new file mode 100644 index 0000000..73693db --- /dev/null +++ b/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_3_data_capture.dart @@ -0,0 +1 @@ +//lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_3_data_capture.dart \ No newline at end of file diff --git a/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_4_summary.dart b/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_4_summary.dart new file mode 100644 index 0000000..fe7b178 --- /dev/null +++ b/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_4_summary.dart @@ -0,0 +1 @@ +//lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_4_summary.dart \ No newline at end of file diff --git a/lib/screens/marine/investigative/marine_investigative_manual_sampling.dart b/lib/screens/marine/investigative/marine_investigative_manual_sampling.dart new file mode 100644 index 0000000..9ee892f --- /dev/null +++ b/lib/screens/marine/investigative/marine_investigative_manual_sampling.dart @@ -0,0 +1 @@ +//lib/screens/marine/investigative/marine_investigative_manual_sampling.dart \ No newline at end of file diff --git a/lib/screens/marine/manual/reports/marine_manual_equipment_maintenance_screen.dart b/lib/screens/marine/manual/reports/marine_manual_equipment_maintenance_screen.dart index 2d271aa..b6ee192 100644 --- a/lib/screens/marine/manual/reports/marine_manual_equipment_maintenance_screen.dart +++ b/lib/screens/marine/manual/reports/marine_manual_equipment_maintenance_screen.dart @@ -1,3 +1,5 @@ +// lib/screens/marine/manual/reports/marine_manual_equipment_maintenance_screen.dart + import 'dart:async'; import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; @@ -25,15 +27,75 @@ class _MarineManualEquipmentMaintenanceScreenState bool _isOnline = true; late StreamSubscription> _connectivitySubscription; - final _dateController = TextEditingController(); - final _performedByController = TextEditingController(); + // --- Controllers --- + final _maintenanceDateController = TextEditingController(); + final _lastMaintenanceDateController = TextEditingController(); + // Controller removed for Schedule Maintenance dropdown + + // Renamed controllers, moved to header + final _timeStartController = TextEditingController(); + final _timeEndController = TextEditingController(); + final _locationController = TextEditingController(); + + // YSI controllers + final _ysiSondeCommentsController = TextEditingController(); + final _ysiSensorCommentsController = TextEditingController(); + + // Van Dorn controllers + final _vanDornCommentsController = TextEditingController(); + final _vanDornCurrentSerialController = TextEditingController(); + final _vanDornNewSerialController = TextEditingController(); + + // Controllers for the dynamic tables + final Map _ysiCurrentSerialControllers = {}; + final Map _ysiNewSerialControllers = {}; + final Map _vanDornLastDateControllers = {}; + final Map _vanDornNewDateControllers = {}; + + // --- State for Previous Record feature --- + bool _showPreviousRecordDropdown = false; + bool _isFetchingPreviousRecords = false; + List> _previousRecords = []; + int? _selectedPreviousRecordId; // Use ID to track selected record + // --- End State --- + + // List of scopes for the YSI Sonde section (Section I) + final List _ysiSondeScopes = [ + '1) Upper body of instrument', + '2) Side body of instrument', + '3) Bottom body of instrument', + '4) Sonde guard and calibration cup', + ]; + + // Map to hold the scope text for YSI sensors (Section II) + final Map _ysiSensorScopes = { + 'pH': '1) Sensor body', + 'Conductivity': '2) Lenses', + 'Turbidity': '3) Pin connector', + 'Dissolved Oxygen': '4) Retaining Nut Kit', + }; @override void initState() { super.initState(); - final auth = Provider.of(context, listen: false); - _dateController.text = DateFormat('yyyy-MM-dd').format(DateTime.now()); - _performedByController.text = auth.profileData?['username'] ?? 'Unknown User'; + _maintenanceDateController.text = + DateFormat('yyyy-MM-dd').format(DateTime.now()); + + // Set defaults for Schedule Maintenance and Time Start + _data.scheduleMaintenance = 'Yes'; + _timeStartController.text = DateFormat('HH:mm').format(DateTime.now()); // Default Time Start + + // --- Init Dynamic Controllers --- + _data.ysiReplacements.forEach((item, values) { + _ysiCurrentSerialControllers[item] = TextEditingController(); + _ysiNewSerialControllers[item] = TextEditingController(); + }); + _data.vanDornReplacements.forEach((part, values) { + _vanDornLastDateControllers[part] = TextEditingController(); + _vanDornNewDateControllers[part] = TextEditingController(); + }); + + // --- Connectivity --- _checkInitialConnectivity(); _connectivitySubscription = Connectivity().onConnectivityChanged.listen(_updateConnectionStatus); @@ -42,8 +104,21 @@ class _MarineManualEquipmentMaintenanceScreenState @override void dispose() { _connectivitySubscription.cancel(); - _dateController.dispose(); - _performedByController.dispose(); + _maintenanceDateController.dispose(); + _lastMaintenanceDateController.dispose(); + _timeStartController.dispose(); // Renamed + _timeEndController.dispose(); // Renamed + _locationController.dispose(); // Renamed + _ysiSondeCommentsController.dispose(); + _ysiSensorCommentsController.dispose(); + _vanDornCommentsController.dispose(); + _vanDornCurrentSerialController.dispose(); + _vanDornNewSerialController.dispose(); + + _ysiCurrentSerialControllers.values.forEach((c) => c.dispose()); + _ysiNewSerialControllers.values.forEach((c) => c.dispose()); + _vanDornLastDateControllers.values.forEach((c) => c.dispose()); + _vanDornNewDateControllers.values.forEach((c) => c.dispose()); super.dispose(); } @@ -67,20 +142,194 @@ class _MarineManualEquipmentMaintenanceScreenState } } + Future _selectDate(TextEditingController controller) async { + final date = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime(2101)); + if (date != null) { + controller.text = DateFormat('yyyy-MM-dd').format(date); + } + } + + Future _selectTime(TextEditingController controller) async { + final time = await showTimePicker( + context: context, initialTime: TimeOfDay.now()); + if (time != null && mounted) { + controller.text = time.format(context); + } + } + + // --- Previous Record Logic --- + + Future _fetchPreviousRecords() async { + if (_previousRecords.isNotEmpty) return; // Already fetched + + setState(() => _isFetchingPreviousRecords = true); + + try { + final auth = Provider.of(context, listen: false); + final service = + Provider.of(context, listen: false); + + final result = await service.getPreviousMaintenanceLogs(authProvider: auth); + + if (mounted) { + if (result['success'] == true && result['data'] != null) { + setState(() { + _previousRecords = List>.from(result['data']); + }); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(result['message'] ?? 'Failed to fetch previous records.'), + backgroundColor: Colors.red, + )); + // If fetching fails, reset the dropdown + setState(() => _showPreviousRecordDropdown = false); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("An error occurred fetching records: $e"), + backgroundColor: Colors.red, + )); + setState(() => _showPreviousRecordDropdown = false); + } + } finally { + if (mounted) { + setState(() => _isFetchingPreviousRecords = false); + } + } + } + + void _populateFormWithRecord(Map record) { + // 1. Last Maintenance Date + // The previous record's 'maintenance_date' becomes the new 'last_maintenance_date' + _lastMaintenanceDateController.text = record['maintenance_date'] ?? ''; + + // 2. YSI Serial Numbers + if (record['ysi_replacements'] is List) { + final List ysiReplacements = record['ysi_replacements']; + for (var item in ysiReplacements) { + if (item is Map) { + final String itemName = item['item_name'] ?? ''; + final String currentSerial = item['current_serial'] ?? ''; + final String newSerial = item['new_serial'] ?? ''; + + // Use new_serial if available, otherwise fall back to current_serial + final serialToUse = (newSerial.isNotEmpty) ? newSerial : currentSerial; + + if (_ysiCurrentSerialControllers.containsKey(itemName)) { + _ysiCurrentSerialControllers[itemName]!.text = serialToUse; + } + } + } + } + + // 3. Van Dorn Serial Number + final String vdCurrentSerial = record['van_dorn_current_serial'] ?? ''; + final String vdNewSerial = record['van_dorn_new_serial'] ?? ''; + _vanDornCurrentSerialController.text = (vdNewSerial.isNotEmpty) ? vdNewSerial : vdCurrentSerial; + + // 4. Van Dorn Replacement Dates + if (record['van_dorn_replacements'] is List) { + final List vanDornReplacements = record['van_dorn_replacements']; + for (var part in vanDornReplacements) { + if (part is Map) { + final String partName = part['part_name'] ?? ''; + final String lastDate = part['last_replacement_date'] ?? ''; + final String newDate = part['new_replacement_date'] ?? ''; + + // Use new_date if available, otherwise fall back to last_date + final dateToUse = (newDate.isNotEmpty) ? newDate : lastDate; + + if (_vanDornLastDateControllers.containsKey(partName)) { + _vanDornLastDateControllers[partName]!.text = dateToUse; + } + } + } + } + + // Update the UI + setState(() {}); + } + + void _clearPopulatedData() { + setState(() { + _lastMaintenanceDateController.text = ''; + _ysiCurrentSerialControllers.values.forEach((c) => c.text = ''); + _vanDornCurrentSerialController.text = ''; + _vanDornLastDateControllers.values.forEach((c) => c.text = ''); + _selectedPreviousRecordId = null; + }); + } + + // --- End Previous Record Logic --- + Future _submit() async { - if (!_formKey.currentState!.validate()) return; + if (!_formKey.currentState!.validate()) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("Please fill in all required fields."), + backgroundColor: Colors.orange, + )); + return; + } + // Form validation passed, save the form fields to _data where applicable _formKey.currentState!.save(); setState(() => _isLoading = true); try { final auth = Provider.of(context, listen: false); - final service = Provider.of(context, listen: false); + final service = + Provider.of(context, listen: false); - _data.performedByUserId = auth.profileData?['user_id']; - _data.maintenanceDate = _dateController.text; + _data.conductedByUserId = auth.profileData?['user_id']; + _data.maintenanceDate = _maintenanceDateController.text; + _data.lastMaintenanceDate = _lastMaintenanceDateController.text; + // scheduleMaintenance is set via Dropdown onSaved - final result = await service.submitMaintenanceReport(data: _data, authProvider: auth); + // Assign header fields + _data.timeStart = _timeStartController.text; + _data.location = _locationController.text; + // --- MODIFICATION START: Auto-populate Time End --- + // Check if Time End controller is empty + if (_timeEndController.text.isEmpty) { + // If empty, use current time + _data.timeEnd = DateFormat('HH:mm').format(DateTime.now()); + // Optionally update the controller as well, though not strictly necessary for submission + // _timeEndController.text = _data.timeEnd!; + } else { + // If not empty, use the value entered by the user + _data.timeEnd = _timeEndController.text; + } + // --- MODIFICATION END --- + + + // Assign comments and serials + _data.ysiSondeComments = _ysiSondeCommentsController.text; + _data.ysiSensorComments = _ysiSensorCommentsController.text; + _data.vanDornComments = _vanDornCommentsController.text; + _data.vanDornCurrentSerial = _vanDornCurrentSerialController.text; + _data.vanDornNewSerial = _vanDornNewSerialController.text; + + // Assign dynamic table values from their controllers + _data.ysiReplacements.forEach((item, values) { + values['Current Serial'] = _ysiCurrentSerialControllers[item]?.text ?? ''; // Added null check + values['New Serial'] = _ysiNewSerialControllers[item]?.text ?? ''; // Added null check + }); + _data.vanDornReplacements.forEach((part, values) { + values['Last Date'] = _vanDornLastDateControllers[part]?.text ?? ''; // Added null check + values['New Date'] = _vanDornNewDateControllers[part]?.text ?? ''; // Added null check + }); + + // Submit the data + final result = + await service.submitMaintenanceReport(data: _data, authProvider: auth); + + // Handle the result if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(result['message']), @@ -91,7 +340,8 @@ class _MarineManualEquipmentMaintenanceScreenState } on SocketException { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text("Submission failed. Please check your network connection."), + content: + Text("Submission failed. Please check your network connection."), backgroundColor: Colors.red, )); } @@ -109,6 +359,7 @@ class _MarineManualEquipmentMaintenanceScreenState } } + @override Widget build(BuildContext context) { return Scaffold( @@ -124,7 +375,7 @@ class _MarineManualEquipmentMaintenanceScreenState padding: const EdgeInsets.all(8.0), child: const Text( 'No Internet Connection. You cannot submit the report.', - style: TextStyle(color: Colors.white), + style: const TextStyle(color: Colors.white), textAlign: TextAlign.center, ), ), @@ -134,101 +385,37 @@ class _MarineManualEquipmentMaintenanceScreenState child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Card( - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Report Details', style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: 16), - TextFormField( - decoration: const InputDecoration(labelText: 'Equipment Name / ID *', border: OutlineInputBorder()), - validator: (val) => val == null || val.isEmpty ? 'This field is required' : null, - onSaved: (val) => _data.equipmentName = val, - ), - const SizedBox(height: 16), - TextFormField( - controller: _dateController, - readOnly: true, - decoration: const InputDecoration(labelText: 'Maintenance Date', border: OutlineInputBorder()), - ), - const SizedBox(height: 16), - DropdownButtonFormField( - decoration: const InputDecoration(labelText: 'Maintenance Type *', border: OutlineInputBorder()), - items: ['Routine', 'Repair', 'Inspection', 'Replacement'].map((String value) { - return DropdownMenuItem(value: value, child: Text(value)); - }).toList(), - onChanged: (val) => _data.maintenanceType = val, - validator: (val) => val == null ? 'Please select a type' : null, - ), - const SizedBox(height: 16), - DropdownButtonFormField( - decoration: const InputDecoration(labelText: 'Final Status *', border: OutlineInputBorder()), - items: ['Completed', 'Pending Parts', 'In Progress', 'Requires Follow-up'].map((String value) { - return DropdownMenuItem(value: value, child: Text(value)); - }).toList(), - onChanged: (val) => _data.status = val, - validator: (val) => val == null ? 'Please select a status' : null, - ), - const SizedBox(height: 16), - TextFormField( - controller: _performedByController, - readOnly: true, - decoration: const InputDecoration(labelText: 'Performed By', border: OutlineInputBorder()), - ), - ], - ), - ), - ), + _buildHeaderCard(), const SizedBox(height: 16), - Card( - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Description & Remarks', style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: 16), - TextFormField( - decoration: const InputDecoration(labelText: 'Description of Work Done', border: OutlineInputBorder(), alignLabelWithHint: true), - maxLines: 4, - onSaved: (val) => _data.workDescription = val, - ), - const SizedBox(height: 16), - TextFormField( - decoration: const InputDecoration(labelText: 'Parts Replaced (if any)', border: OutlineInputBorder(), alignLabelWithHint: true), - maxLines: 3, - onSaved: (val) => _data.partsReplaced = val, - ), - const SizedBox(height: 16), - TextFormField( - decoration: const InputDecoration(labelText: 'General Remarks', border: OutlineInputBorder(), alignLabelWithHint: true), - maxLines: 3, - onSaved: (val) => _data.remarks = val, - ), - ], - ), - ), - ), + _buildPart1YsiCard(), + const SizedBox(height: 16), + _buildPart1YsiReplacementCard(), + const SizedBox(height: 16), + _buildPart2VanDornCard(), + const SizedBox(height: 16), + _buildPart2VanDornReplacementCard(), const SizedBox(height: 24), Row( children: [ Expanded( child: OutlinedButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: _isLoading ? null : () => Navigator.of(context).pop(), // Disable cancel during loading child: const Text('Cancel'))), const SizedBox(width: 10.0), Expanded( child: ElevatedButton( onPressed: _isLoading || !_isOnline ? null : _submit, child: _isLoading - ? const CircularProgressIndicator(color: Colors.white) + ? const SizedBox( // Use SizedBox for consistent button height + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: Colors.white, + ), + ) : const Text('Submit'), ), ) @@ -243,4 +430,597 @@ class _MarineManualEquipmentMaintenanceScreenState ), ); } -} \ No newline at end of file + + // --- Card Widgets --- + + Widget _buildHeaderCard() { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Header Information', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + // --- NEW FIELD: Use Previous Record --- + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Use previous record data', + border: OutlineInputBorder(), + ), + value: _showPreviousRecordDropdown ? 'Yes' : 'No', + items: ['Yes', 'No'].map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: _isLoading || !_isOnline ? null : (val) { // Disable if loading or offline + setState(() { + if (val == 'Yes') { + _showPreviousRecordDropdown = true; + _fetchPreviousRecords(); // Fetch data when user selects Yes + } else { + _showPreviousRecordDropdown = false; + _clearPopulatedData(); // Clear fields if user selects No + } + }); + }, + ), + // --- NEW FIELD: Previous Record Dropdown --- + if (_showPreviousRecordDropdown) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: _isFetchingPreviousRecords + ? const Center(child: CircularProgressIndicator()) + : DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Select previous record (last 4 months)', // Updated label + border: OutlineInputBorder(), + ), + value: _selectedPreviousRecordId, + isExpanded: true, // Allow text to wrap if needed + hint: Text(_previousRecords.isEmpty + ? 'No records found' + : 'Select a record'), + disabledHint: const Text('Fetching records...'), + items: _previousRecords.map((record) { + // --- MODIFIED: Display Date and Name --- + String conductedByName = record['conducted_by_name'] ?? 'Unknown User'; + String maintenanceDate = record['maintenance_date'] ?? 'Unknown Date'; + String displayText = '$maintenanceDate (by $conductedByName)'; + // --- END MODIFICATION --- + return DropdownMenuItem( + value: record['maintenance_id'] as int, // Assuming API returns 'maintenance_id' + // Use FittedBox to prevent overflow, consider TextOverflow.ellipsis too + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text(displayText), + ), + ); + }).toList(), + onChanged: _isLoading || _previousRecords.isEmpty ? null : (val) { // Disable if loading or no records + if (val == null) return; + // Find the full record map and populate form + final selectedRecord = _previousRecords + .firstWhere((r) => r['maintenance_id'] == val); + setState(() { + _selectedPreviousRecordId = val; + _populateFormWithRecord(selectedRecord); + }); + }, + ), + ), + const SizedBox(height: 16), + // --- END NEW FIELDS --- + TextFormField( + controller: _maintenanceDateController, + readOnly: true, + decoration: const InputDecoration( + labelText: 'Maintenance Date *', + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.calendar_month)), + onTap: _isLoading ? null : () => _selectDate(_maintenanceDateController), // Disable tap when loading + validator: (val) => val == null || val.isEmpty ? 'Date is required' : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: _lastMaintenanceDateController, + readOnly: true, + decoration: const InputDecoration( + labelText: 'Last Maintenance Date', + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.calendar_month)), + onTap: _isLoading ? null : () => _selectDate(_lastMaintenanceDateController), // Disable tap when loading + ), + const SizedBox(height: 16), + // Changed to DropdownButtonFormField + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Schedule Maintenance', + border: OutlineInputBorder(), + ), + value: _data.scheduleMaintenance, // Set from initState + items: ['Yes', 'No'].map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: _isLoading ? null : (val) { // Disable when loading + setState(() { + _data.scheduleMaintenance = val; // Update data on change + }); + }, + onSaved: (val) { + _data.scheduleMaintenance = val; // Save data on form save + }, + // Add validator if required + // validator: (val) => val == null ? 'Please select Yes or No' : null, + ), + const SizedBox(height: 16), + // Fields MOVED HERE + TextFormField( + controller: _locationController, // Renamed controller + decoration: const InputDecoration(labelText: 'Location', border: OutlineInputBorder()), + readOnly: _isLoading, // Disable when loading + onSaved: (val) => _data.location = val, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextFormField( + controller: _timeStartController, // Renamed controller + readOnly: true, // Always read-only as it's defaulted + decoration: const InputDecoration( + labelText: 'Time Start', + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.access_time)), + // onTap removed + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + controller: _timeEndController, // Renamed controller + readOnly: true, // Make readOnly to prevent manual edit after selection + decoration: const InputDecoration( + labelText: 'Time End', + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.access_time)), + onTap: _isLoading ? null : () => _selectTime(_timeEndController), // Disable tap when loading + // onSaved is handled in _submit logic + ), + ), + ], + ), + // END MOVED FIELDS + const SizedBox(height: 8), + CheckboxListTile( + title: const Text('Sonde / Sensor/Tip/Part Replacement'), + value: _data.isReplacement, + onChanged: _isLoading ? null : (val) { // Disable when loading + setState(() => _data.isReplacement = val ?? false); + }, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ) + ], + ), + ), + ); + } + + Widget _buildPart1YsiCard() { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: AbsorbPointer( // Disable interaction when loading + absorbing: _isLoading, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Part 1 - YSI EXO 2 & Sensor', style: Theme.of(context).textTheme.titleLarge), + // Location and Time fields removed from here + const Divider(height: 24), + // Use the correct widget for Section I (Sonde) + _buildSondeChecklistSection( + 'I. YSI EXO 2 Multiparameter Sonde', _ysiSondeScopes), + TextFormField( + controller: _ysiSondeCommentsController, + decoration: const InputDecoration( + labelText: 'Comment/Observation', + border: OutlineInputBorder(), + alignLabelWithHint: true), + maxLines: 3, + readOnly: _isLoading, + onSaved: (val) => _data.ysiSondeComments = val, + ), + const Divider(height: 24), + // Use the correct widget for Section II (Sensor) + _buildSensorChecklistSection('II. EXO Sensor', _data.ysiSensorChecks), + TextFormField( + controller: _ysiSensorCommentsController, + decoration: const InputDecoration( + labelText: 'Comment/Observation', + border: OutlineInputBorder(), + alignLabelWithHint: true), + maxLines: 3, + readOnly: _isLoading, + onSaved: (val) => _data.ysiSensorComments = val, + ), + ], + ), + ), + ), + ); + } + + Widget _buildPart1YsiReplacementCard() { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: AbsorbPointer( // Disable interaction when loading + absorbing: _isLoading, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Part 1 - Sonde / Sensor / Tip Replacement', + style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + ..._data.ysiReplacements.keys.map((item) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item, style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextFormField( + controller: _ysiCurrentSerialControllers[item], + decoration: const InputDecoration( + labelText: 'Current Serial Number', + border: OutlineInputBorder()), + readOnly: _isLoading, + // onSaved handled in _submit + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + controller: _ysiNewSerialControllers[item], + decoration: const InputDecoration( + labelText: 'New Serial Number', + border: OutlineInputBorder()), + readOnly: _isLoading, + // onSaved handled in _submit + ), + ), + ], + ), + ], + ), + ); + }).toList(), + ], + ), + ), + ), + ); + } + + Widget _buildPart2VanDornCard() { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: AbsorbPointer( // Disable interaction when loading + absorbing: _isLoading, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Part 2 - Van Dorn Sampler', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 8), + Text('Schedule Maintenance - Inspection & Cleaning', + style: Theme.of(context).textTheme.titleMedium), + const Divider(height: 24), + // Use the correct widget for Section III (Van Dorn Checks) + _buildChecklistSection( + 'III. Van Dorn Sampler', _data.vanDornChecks), + TextFormField( + controller: _vanDornCommentsController, + decoration: const InputDecoration( + labelText: 'Comment/Observation', + border: OutlineInputBorder(), + alignLabelWithHint: true), + maxLines: 3, + readOnly: _isLoading, + onSaved: (val) => _data.vanDornComments = val, + ), + ], + ), + ), + ), + ); + } + + Widget _buildPart2VanDornReplacementCard() { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: AbsorbPointer( // Disable interaction when loading + absorbing: _isLoading, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Part 2 - Van Dorn Sampler Part Replacement', + style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextFormField( + controller: _vanDornCurrentSerialController, + decoration: const InputDecoration( + labelText: 'Current Serial Number', + border: OutlineInputBorder()), + readOnly: _isLoading, + // onSaved handled in _submit + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + controller: _vanDornNewSerialController, + decoration: const InputDecoration( + labelText: 'New Serial Number', + border: OutlineInputBorder()), + readOnly: _isLoading, + // onSaved handled in _submit + ), + ), + ], + ), + const Divider(height: 24), + ..._data.vanDornReplacements.keys.map((part) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(part, style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextFormField( + controller: _vanDornLastDateControllers[part], + readOnly: true, // Always readOnly, selected via picker + decoration: const InputDecoration( + labelText: 'Last Replacement Date', + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.calendar_month)), + onTap: _isLoading ? null : () => _selectDate(_vanDornLastDateControllers[part]!), // Disable tap + // onSaved handled in _submit + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + controller: _vanDornNewDateControllers[part], + readOnly: true, // Always readOnly, selected via picker + decoration: const InputDecoration( + labelText: 'New Replacement Date', + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.calendar_month)), + onTap: _isLoading ? null : () => _selectDate(_vanDornNewDateControllers[part]!), // Disable tap + // onSaved handled in _submit + ), + ), + ], + ), + ], + ), + ); + }).toList(), + ], + ), + ), + ), + ); + } + + // --- Helper Widgets --- + + // Widget for Section I (YSI Sonde) - Uses List for scopes + Widget _buildSondeChecklistSection(String title, List scopes) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: scopes.map((scope) => Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text(scope, style: Theme.of(context).textTheme.bodyLarge), + )).toList(), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: CheckboxListTile( + title: const Text('Inspect'), + value: _data.ysiSondeChecks['Inspect'], + onChanged: _isLoading ? null : (val) { // Disable when loading + setState(() => _data.ysiSondeChecks['Inspect'] = val ?? false); + }, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ), + ), + Expanded( + child: CheckboxListTile( + title: const Text('Clean'), + value: _data.ysiSondeChecks['Clean'], + onChanged: _isLoading ? null : (val) { // Disable when loading + setState(() => _data.ysiSondeChecks['Clean'] = val ?? false); + }, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ), + ), + ], + ), + ], + ); + } + + // Helper widget for the Sensor checklist (Part 1, Section II) - Uses Map for items + Widget _buildSensorChecklistSection( + String title, Map> items) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + Table( + columnWidths: const { + 0: FlexColumnWidth(2), // Sensor + 1: FlexColumnWidth(3), // Scope + 2: FlexColumnWidth(1), // Inspect + 3: FlexColumnWidth(1), // Clean + }, + border: TableBorder.all(color: Colors.grey.shade400), + children: [ + TableRow( + decoration: BoxDecoration(color: Colors.grey.shade600), + children: [ + _tableHeader('Sensor'), + _tableHeader('Scope'), + _tableHeader('Inspect'), + _tableHeader('Clean'), + ], + ), + ..._ysiSensorScopes.keys.map((sensorName) { + final scopeText = _ysiSensorScopes[sensorName]!; + final itemChecks = items[sensorName]!; // Use sensorName as key + return TableRow( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(sensorName), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(scopeText), + ), + Checkbox( + value: itemChecks['Inspect'], + onChanged: _isLoading ? null : (val) { // Disable when loading + setState(() => itemChecks['Inspect'] = val ?? false); + }, + activeColor: Theme.of(context).colorScheme.primary, + ), + Checkbox( + value: itemChecks['Clean'], + onChanged: _isLoading ? null : (val) { // Disable when loading + setState(() => itemChecks['Clean'] = val ?? false); + }, + activeColor: Theme.of(context).colorScheme.primary, + ), + ], + ); + }).toList(), + ], + ), + const SizedBox(height: 16), + ], + ); + } + + // Helper widget for Van Dorn checklist (Part 2, Section III) - Uses Map for items + Widget _buildChecklistSection( + String title, Map> items) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + Table( + columnWidths: const { + 0: FlexColumnWidth(3), + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + }, + border: TableBorder.all(color: Colors.grey.shade400), + children: [ + TableRow( + decoration: BoxDecoration(color: Colors.grey.shade600), + children: [ + _tableHeader('Scope'), + _tableHeader('Inspect'), + _tableHeader('Clean'), + ], + ), + ...items.keys.map((scope) { + return TableRow( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(scope), + ), + Checkbox( + value: items[scope]!['Inspect'], + onChanged: _isLoading ? null : (val) { // Disable when loading + setState(() => items[scope]!['Inspect'] = val ?? false); + }, + activeColor: Theme.of(context).colorScheme.primary, + ), + // Conditionally render the "Clean" checkbox + items[scope]!.containsKey('Clean') + ? Checkbox( + value: items[scope]!['Clean'], + onChanged: _isLoading ? null : (val) { // Disable when loading + setState(() => items[scope]!['Clean'] = val ?? false); + }, + activeColor: Theme.of(context).colorScheme.primary, + ) + : Container(), // Render empty if 'Clean' is not applicable + ], + ); + }).toList(), + ], + ), + const SizedBox(height: 16), + ], + ); + } + + Widget _tableHeader(String text) { + // Use contrasting color for header text if needed, else rely on theme + final headerColor = Colors.grey.shade600; // Match TableRow background + final textColor = headerColor.computeLuminance() > 0.5 ? Colors.black87 : Colors.white; + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text(text, style: TextStyle(fontWeight: FontWeight.bold, color: textColor)), + ); + } +} // End of State class \ No newline at end of file diff --git a/lib/screens/marine/manual/reports/marine_manual_pre_departure_checklist_screen.dart b/lib/screens/marine/manual/reports/marine_manual_pre_departure_checklist_screen.dart index 528befb..16e1fad 100644 --- a/lib/screens/marine/manual/reports/marine_manual_pre_departure_checklist_screen.dart +++ b/lib/screens/marine/manual/reports/marine_manual_pre_departure_checklist_screen.dart @@ -23,63 +23,83 @@ class _MarineManualPreDepartureChecklistScreenState final Map _remarksVisibility = {}; - // NEW: State variables for connectivity + // State variables for connectivity bool _isOnline = true; late StreamSubscription> _connectivitySubscription; - - final List _checklistItemsText = [ - 'MMWQM Standard Operation Procedure (SOP)', - 'Back-up Sampling Sheet and Chain of Custody form', - 'YSI EXO2 Sonde Include Sensor (pH/Turbidity/Conductivity/Dissolved Oxygen)', - 'Varn Dorn Sampler with Rope and Messenger', - 'Laptop', - 'Smart pre-installed with application (apps for manual sampling - MMS)', - 'GPS Navigation', - 'Calibration standards (pH/Turbidity/Conductivity)', - 'Distilled water (D.I)', - 'Universal pH Indicator paper', - 'Personal Floating Devices (PFD)', - 'First aid kits', - 'Sampling Shoes', - 'Sufficient set of cooler box and sampling bottles', - 'Ice packets', - 'Disposable gloves', - 'Black plastic bags', - 'Maker pen, pen and brown tapes', - 'Zipper Bags', - 'Aluminium Foil', - ]; + // All checklist items from the PDF are now grouped + final Map> _checklistSections = { + 'INTERNAL - IN-SITU SAMPLING': [ // Section title matches PDF + 'Marine manual Standard Operation Procedure (SOP)', // Item text matches PDF + 'Back-up Sampling Sheet & Chain of Custody form', // Item text matches PDF + 'Calibration worksheet', // Item text matches PDF + 'YSI EXO 2 Sonde include sensor (pH/Turbidity/Conductivity/Dissolved Oxygen)', // Item text matches PDF + 'Spare set sensor (pH/Turbidity/Conductivity/Dissolved Oxygen)', // Item text matches PDF + 'YSI serial cable', // Item text matches PDF + 'Van Dorn Sampler (with rope & messenger)', // Item text matches PDF + 'Laptop', // Item text matches PDF + 'Smartphone pre-installed with application (Apps for manual sampling-MMS)', // Item text matches PDF + 'GPS navigation', // Item text matches PDF + 'Calibration standards (pH/Turbidity/Conductivity)', // Item text matches PDF + 'Distilled water (D.I.)', // Item text matches PDF + 'Universal pH indicator paper', // Item text matches PDF + 'Alcohol swab', // Item text matches PDF + 'Personal Floating Devices (PFD)', // Item text matches PDF + 'First aid kits', // Item text matches PDF + 'Disposable gloves', // Item text matches PDF + 'Black plastic bags', // Item text matches PDF + 'Marker pen, pen, clear tapes, brown tapes & scissors', // Item text matches PDF + 'Energizer battery', // Item text matches PDF + 'EXO battery opener and EXO magnet', // Item text matches PDF + 'Laminated white paper', // Item text matches PDF + 'Clear glass bottle (blue сар)', // Item text matches PDF + 'Proper sampling attires & shoes', // Item text matches PDF + 'Raincoat/Poncho', // Item text matches PDF + 'Ice packets', // Item text matches PDF + ], + 'INTERNAL-TARBALL SAMPLING': [ // Section title matches PDF + 'Measuring tape (100 meter)', // Item text matches PDF + 'Steel raking', // Item text matches PDF + 'Aluminum foil', // Item text matches PDF + 'Zipper bags', // Item text matches PDF + ], + 'EXTERNAL - LABORATORY': [ // Section title matches PDF + 'Sufficient sets of cooler box and sampling bottles with label', // Item text matches PDF + 'Field duplicate sampling bottles (if any)', // Item text matches PDF + 'Blank samples sampling bottles (if any)', // Item text matches PDF + 'Preservatives (acid & alkaline)', // Item text matches PDF + ], + }; @override void initState() { super.initState(); - for (var item in _checklistItemsText) { - _data.checklistItems[item] = false; - _data.remarks[item] = ''; - _remarksVisibility[item] = false; - } + // Iterate through the map structure to initialize data + _checklistSections.forEach((section, items) { + for (var item in items) { + _data.checklistItems[item] = true; // MODIFIED: Default to 'Yes' + _data.remarks[item] = ''; + _remarksVisibility[item] = false; + } + }); - // NEW: Check initial connection and start listening for changes + // Check initial connection and start listening for changes _checkInitialConnectivity(); _connectivitySubscription = Connectivity().onConnectivityChanged.listen(_updateConnectionStatus); } - // NEW: Dispose the connectivity listener to prevent memory leaks @override void dispose() { _connectivitySubscription.cancel(); super.dispose(); } - // NEW: Method to check the first time the screen loads Future _checkInitialConnectivity() async { final connectivityResult = await Connectivity().checkConnectivity(); _updateConnectionStatus(connectivityResult); } - // NEW: Callback method to update UI based on connectivity changes void _updateConnectionStatus(List result) { final bool currentlyOnline = !result.contains(ConnectivityResult.none); if (_isOnline != currentlyOnline) { @@ -99,23 +119,28 @@ class _MarineManualPreDepartureChecklistScreenState } Future _submit() async { + // No form validation needed as Project/Month fields removed + setState(() => _isLoading = true); try { final auth = Provider.of(context, listen: false); - final service = Provider.of(context, listen: false); + final service = + Provider.of(context, listen: false); _data.reporterUserId = auth.profileData?['user_id']; - _data.reporterName = auth.profileData?['user_name']; + _data.reporterName = auth.profileData?['user_name']; // Use user_name as reporterName _data.submissionDate = DateTime.now().toIso8601String().split('T').first; - final result = await service.submitChecklist(data: _data, authProvider: auth); + final result = + await service.submitChecklist(data: _data, authProvider: auth); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(result['message']), - backgroundColor: result['success'] == true ? Colors.green : Colors.red, + backgroundColor: + result['success'] == true ? Colors.green : Colors.red, ), ); if (result['success'] == true) Navigator.of(context).pop(); @@ -124,7 +149,8 @@ class _MarineManualPreDepartureChecklistScreenState if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text("Submission failed. Please check your network connection."), + content: + Text("Submission failed. Please check your network connection."), backgroundColor: Colors.red, ), ); @@ -153,7 +179,7 @@ class _MarineManualPreDepartureChecklistScreenState ), body: Column( children: [ - // NEW: Offline banner that shows when there's no internet + // Offline banner if (!_isOnline) Container( width: double.infinity, @@ -170,16 +196,9 @@ class _MarineManualPreDepartureChecklistScreenState padding: const EdgeInsets.all(16.0), child: Column( children: [ - ListView.separated( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: _checklistItemsText.length, - itemBuilder: (context, index) { - final item = _checklistItemsText[index]; - return _buildChecklistItem(item); - }, - separatorBuilder: (context, index) => const SizedBox(height: 8), - ), + // Build UI from the section map + ..._buildSectionWidgets(), + const SizedBox(height: 24), const Divider(), const SizedBox(height: 10), @@ -194,10 +213,11 @@ class _MarineManualPreDepartureChecklistScreenState const SizedBox(width: 10.0), Expanded( child: ElevatedButton( - // NEW: Submit button is disabled when loading OR offline - onPressed: _isLoading || !_isOnline ? null : _submit, + onPressed: + _isLoading || !_isOnline ? null : _submit, child: _isLoading - ? const CircularProgressIndicator(color: Colors.white) + ? const CircularProgressIndicator( + color: Colors.white) : const Text('Submit'), ), ) @@ -212,6 +232,38 @@ class _MarineManualPreDepartureChecklistScreenState ); } + // Helper method to build the list of sections and their items + List _buildSectionWidgets() { + List widgets = []; + _checklistSections.forEach((title, items) { + // Add the section header + widgets.add( + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + title, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ); + + // Add the list of items for this section + widgets.add( + ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return _buildChecklistItem(item); + }, + separatorBuilder: (context, index) => const SizedBox(height: 8), + ), + ); + }); + return widgets; + } + Widget _buildChecklistItem(String title) { return Card( elevation: 2, @@ -233,17 +285,19 @@ class _MarineManualPreDepartureChecklistScreenState ToggleSwitch( minWidth: 70.0, cornerRadius: 20.0, + // Use theme colors activeBgColor: [Theme.of(context).colorScheme.primary], - activeFgColor: Colors.white, - inactiveBgColor: Colors.grey[700], - inactiveFgColor: Colors.white, - initialLabelIndex: _data.checklistItems[title]! ? 0 : 1, + activeFgColor: Colors.white, // Keep white for contrast on primary + inactiveBgColor: Colors.grey[700], // Dark theme inactive + inactiveFgColor: Colors.white, // White for contrast on dark grey + // MODIFIED: Logic reversed for ['No', 'Yes'] + initialLabelIndex: _data.checklistItems[title]! ? 1 : 0, // No: 0, Yes: 1 totalSwitches: 2, - labels: const ['Yes', 'No'], + labels: const ['No', 'Yes'], // MODIFIED: Swapped labels radiusStyle: true, onToggle: (index) { setState(() { - _data.checklistItems[title] = index == 0; + _data.checklistItems[title] = index == 1; // MODIFIED: 1 is Yes (true), 0 is No (false) }); }, ), @@ -253,12 +307,17 @@ class _MarineManualPreDepartureChecklistScreenState if (_remarksVisibility[title]!) TextFormField( initialValue: _data.remarks[title], + // Rely on theme for input decoration decoration: const InputDecoration( labelText: 'Remarks', - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 10), border: OutlineInputBorder(), + alignLabelWithHint: true, // Improves look for multi-line ), + maxLines: 3, // Allow multiple lines for remarks onChanged: (value) { + // Save remarks as user types, no need for onSaved with controllers _data.remarks[title] = value; }, ), @@ -270,16 +329,20 @@ class _MarineManualPreDepartureChecklistScreenState _remarksVisibility[title] = !_remarksVisibility[title]!; }); }, + // Use theme icon color implicitly icon: Icon( _remarksVisibility[title]! ? Icons.remove_circle_outline : Icons.add_comment_outlined, size: 16, ), + // Use theme text color implicitly label: Text( - _data.remarks[title]!.isNotEmpty + _remarksVisibility[title]! + ? 'Hide Remarks' // Change label when visible + : (_data.remarks[title]!.isNotEmpty ? 'Edit Remarks' - : 'Add Remarks', + : 'Add Remarks'), ), ), ), diff --git a/lib/screens/marine/manual/reports/marine_manual_sonde_calibration_screen.dart b/lib/screens/marine/manual/reports/marine_manual_sonde_calibration_screen.dart index 6c52dac..235419f 100644 --- a/lib/screens/marine/manual/reports/marine_manual_sonde_calibration_screen.dart +++ b/lib/screens/marine/manual/reports/marine_manual_sonde_calibration_screen.dart @@ -22,19 +22,22 @@ class _MarineManualSondeCalibrationScreenState final _data = MarineManualSondeCalibrationData(); bool _isLoading = false; - // State for connectivity bool _isOnline = true; late StreamSubscription> _connectivitySubscription; - // Text Controllers - final _sondeIdController = TextEditingController(); - final _dateTimeController = TextEditingController(); + final _sondeSerialController = TextEditingController(); + final _firmwareController = TextEditingController(); + final _korController = TextEditingController(); + final _locationController = TextEditingController(); + final _startDateTimeController = TextEditingController(); + final _endDateTimeController = TextEditingController(); final _remarksController = TextEditingController(); @override void initState() { super.initState(); - _dateTimeController.text = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()); + _startDateTimeController.text = + DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()); _checkInitialConnectivity(); _connectivitySubscription = Connectivity().onConnectivityChanged.listen(_updateConnectionStatus); @@ -43,8 +46,12 @@ class _MarineManualSondeCalibrationScreenState @override void dispose() { _connectivitySubscription.cancel(); - _sondeIdController.dispose(); - _dateTimeController.dispose(); + _sondeSerialController.dispose(); + _firmwareController.dispose(); + _korController.dispose(); + _locationController.dispose(); + _startDateTimeController.dispose(); + _endDateTimeController.dispose(); _remarksController.dispose(); super.dispose(); } @@ -69,6 +76,26 @@ class _MarineManualSondeCalibrationScreenState } } + Future _selectDateTime(TextEditingController controller) async { + final date = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime(2101), + ); + if (date == null) return; + + final time = await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(DateTime.now()), + ); + if (time == null) return; + + final dateTime = + DateTime(date.year, date.month, date.day, time.hour, time.minute); + controller.text = DateFormat('yyyy-MM-dd HH:mm').format(dateTime); + } + Future _submit() async { if (!_formKey.currentState!.validate()) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( @@ -82,14 +109,20 @@ class _MarineManualSondeCalibrationScreenState try { final auth = Provider.of(context, listen: false); - final service = Provider.of(context, listen: false); + final service = + Provider.of(context, listen: false); _data.calibratedByUserId = auth.profileData?['user_id']; - _data.sondeId = _sondeIdController.text; - _data.calibrationDateTime = _dateTimeController.text; + _data.sondeSerialNumber = _sondeSerialController.text; + _data.firmwareVersion = _firmwareController.text; + _data.korVersion = _korController.text; + _data.location = _locationController.text; + _data.startDateTime = _startDateTimeController.text; + _data.endDateTime = _endDateTimeController.text; _data.remarks = _remarksController.text; - final result = await service.submitCalibration(data: _data, authProvider: auth); + final result = + await service.submitCalibration(data: _data, authProvider: auth); if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( @@ -101,7 +134,8 @@ class _MarineManualSondeCalibrationScreenState } on SocketException { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text("Submission failed. Please check your network connection."), + content: + Text("Submission failed. Please check your network connection."), backgroundColor: Colors.red, )); } @@ -147,33 +181,7 @@ class _MarineManualSondeCalibrationScreenState children: [ _buildGeneralInfoCard(), const SizedBox(height: 16), - _buildCalibrationCard( - title: 'pH Calibration', - parameters: ['pH 4', 'pH 7', 'pH 10'], - onSave: (param, type, value) { - if (value == null) return; - if (param == 'pH 4' && type == 'Initial') _data.ph4Initial = value; - if (param == 'pH 4' && type == 'Calibrated') _data.ph4Calibrated = value; - if (param == 'pH 7' && type == 'Initial') _data.ph7Initial = value; - if (param == 'pH 7' && type == 'Calibrated') _data.ph7Calibrated = value; - if (param == 'pH 10' && type == 'Initial') _data.ph10Initial = value; - if (param == 'pH 10' && type == 'Calibrated') _data.ph10Calibrated = value; - }, - ), - const SizedBox(height: 16), - _buildCalibrationCard( - title: 'Other Parameters', - parameters: ['Conductivity', 'Dissolved Oxygen', 'Turbidity'], - onSave: (param, type, value) { - if (value == null) return; - if (param == 'Conductivity' && type == 'Initial') _data.condInitial = value; - if (param == 'Conductivity' && type == 'Calibrated') _data.condCalibrated = value; - if (param == 'Dissolved Oxygen' && type == 'Initial') _data.doInitial = value; - if (param == 'Dissolved Oxygen' && type == 'Calibrated') _data.doCalibrated = value; - if (param == 'Turbidity' && type == 'Initial') _data.turbidityInitial = value; - if (param == 'Turbidity' && type == 'Calibrated') _data.turbidityCalibrated = value; - }, - ), + _buildCalibrationTableCard(), const SizedBox(height: 16), _buildSummaryCard(), const SizedBox(height: 24), @@ -188,7 +196,8 @@ class _MarineManualSondeCalibrationScreenState child: ElevatedButton( onPressed: _isLoading || !_isOnline ? null : _submit, child: _isLoading - ? const CircularProgressIndicator(color: Colors.white) + ? const CircularProgressIndicator( + color: Colors.white) : const Text('Submit'), ), ) @@ -213,18 +222,69 @@ class _MarineManualSondeCalibrationScreenState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('General Information', style: Theme.of(context).textTheme.titleLarge), + Text('General Information', + style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 16), TextFormField( - controller: _sondeIdController, - decoration: const InputDecoration(labelText: 'Sonde ID *', border: OutlineInputBorder()), - validator: (val) => val == null || val.isEmpty ? 'Sonde ID is required' : null, + controller: _sondeSerialController, + decoration: const InputDecoration( + labelText: 'Sonde Serial Number *', + border: OutlineInputBorder()), + validator: (val) => + val == null || val.isEmpty ? 'Serial Number is required' : null, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextFormField( + controller: _firmwareController, + decoration: const InputDecoration( + labelText: 'Firmware Version', + border: OutlineInputBorder()), + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + controller: _korController, + decoration: const InputDecoration( + labelText: 'KOR Version', border: OutlineInputBorder()), + ), + ), + ], ), const SizedBox(height: 16), TextFormField( - controller: _dateTimeController, + controller: _locationController, + decoration: const InputDecoration( + labelText: 'Location *', border: OutlineInputBorder()), + validator: (val) => + val == null || val.isEmpty ? 'Location is required' : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: _startDateTimeController, readOnly: true, - decoration: const InputDecoration(labelText: 'Calibration Date & Time', border: OutlineInputBorder()), + decoration: const InputDecoration( + labelText: 'Start Date/Time *', + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.calendar_month)), + onTap: () => _selectDateTime(_startDateTimeController), + validator: (val) => + val == null || val.isEmpty ? 'Start Time is required' : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: _endDateTimeController, + readOnly: true, + decoration: const InputDecoration( + labelText: 'End Date/Time *', + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.calendar_month)), + onTap: () => _selectDateTime(_endDateTimeController), + validator: (val) => + val == null || val.isEmpty ? 'End Time is required' : null, ), ], ), @@ -232,11 +292,7 @@ class _MarineManualSondeCalibrationScreenState ); } - Widget _buildCalibrationCard({ - required String title, - required List parameters, - required Function(String, String, double?) onSave, - }) { + Widget _buildCalibrationTableCard() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), @@ -245,38 +301,142 @@ class _MarineManualSondeCalibrationScreenState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: 8), - ...parameters.map((param) => _buildParameterRow(param, onSave)).toList(), + Text('Calibration Parameters', + style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + _buildSectionHeader('pH'), + // MODIFIED: Renamed to clarify 3 columns + _buildParameterRowThreeColumn( + 'pH 7.00 (mV 0+30)', + onSaveMv: (val) => _data.ph7Mv = val, + onSaveBefore: (val) => _data.ph7Before = val, + onSaveAfter: (val) => _data.ph7After = val, + ), + _buildParameterRowThreeColumn( + 'pH 10.00 (mV-180+30)', + onSaveMv: (val) => _data.ph10Mv = val, + onSaveBefore: (val) => _data.ph10Before = val, + onSaveAfter: (val) => _data.ph10After = val, + ), + const Divider(height: 24), + _buildSectionHeader('SP Conductivity (µS/cm)'), + // NEW: Using 2-column widget + _buildParameterRowTwoColumn( + '50,000 (Marine)', + onSaveBefore: (val) => _data.condBefore = val, + onSaveAfter: (val) => _data.condAfter = val, + ), + const Divider(height: 24), + _buildSectionHeader('Turbidity (NTU)'), + // NEW: Using 2-column widget + _buildParameterRowTwoColumn( + '0.0 (D.I.)', + onSaveBefore: (val) => _data.turbidity0Before = val, + onSaveAfter: (val) => _data.turbidity0After = val, + ), + _buildParameterRowTwoColumn( + '124 (Marine)', + onSaveBefore: (val) => _data.turbidity124Before = val, + onSaveAfter: (val) => _data.turbidity124After = val, + ), + const Divider(height: 24), + _buildSectionHeader('Dissolved Oxygen (%)'), + // NEW: Using 2-column widget + _buildParameterRowTwoColumn( + '100.0 (Air Saturated)', + onSaveBefore: (val) => _data.doBefore = val, + onSaveAfter: (val) => _data.doAfter = val, + ), ], ), ), ); } - Widget _buildParameterRow(String label, Function(String, String, double?) onSave) { + Widget _buildSectionHeader(String title) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text(title, style: Theme.of(context).textTheme.titleMedium), + ); + } + + // MODIFIED: Renamed to _buildParameterRowThreeColumn + Widget _buildParameterRowThreeColumn( + String label, { + required Function(double?) onSaveMv, + required Function(double?) onSaveBefore, + required Function(double?) onSaveAfter, + }) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(flex: 2, child: Text(label, style: Theme.of(context).textTheme.titleMedium)), - Expanded( - flex: 3, - child: Row( - children: [ - Expanded(child: TextFormField( - decoration: const InputDecoration(labelText: 'Initial', border: OutlineInputBorder()), - keyboardType: TextInputType.number, - onSaved: (val) => onSave(label, 'Initial', double.tryParse(val ?? '')), - )), - const SizedBox(width: 8), - Expanded(child: TextFormField( - decoration: const InputDecoration(labelText: 'Calibrated', border: OutlineInputBorder()), - keyboardType: TextInputType.number, - onSaved: (val) => onSave(label, 'Calibrated', double.tryParse(val ?? '')), - )), - ], - ), + Text(label, style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'MV Reading', border: OutlineInputBorder()), + keyboardType: TextInputType.number, + onSaved: (val) => onSaveMv(double.tryParse(val ?? '')), + )), + const SizedBox(width: 8), + Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Before Cal', border: OutlineInputBorder()), + keyboardType: TextInputType.number, + onSaved: (val) => onSaveBefore(double.tryParse(val ?? '')), + )), + const SizedBox(width: 8), + Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'After Cal', border: OutlineInputBorder()), + keyboardType: TextInputType.number, + onSaved: (val) => onSaveAfter(double.tryParse(val ?? '')), + )), + ], + ), + ], + ), + ); + } + + // NEW: Widget for parameters without MV Reading + Widget _buildParameterRowTwoColumn( + String label, { + required Function(double?) onSaveBefore, + required Function(double?) onSaveAfter, + }) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Before Cal', border: OutlineInputBorder()), + keyboardType: TextInputType.number, + onSaved: (val) => onSaveBefore(double.tryParse(val ?? '')), + )), + const SizedBox(width: 8), + Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'After Cal', border: OutlineInputBorder()), + keyboardType: TextInputType.number, + onSaved: (val) => onSaveAfter(double.tryParse(val ?? '')), + )), + ], ), ], ), @@ -295,19 +455,24 @@ class _MarineManualSondeCalibrationScreenState Text('Summary', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 16), DropdownButtonFormField( - decoration: const InputDecoration(labelText: 'Overall Status *', border: OutlineInputBorder()), + decoration: const InputDecoration( + labelText: 'Overall Status *', border: OutlineInputBorder()), items: ['Pass', 'Fail', 'Pass with Issues'].map((String value) { return DropdownMenuItem(value: value, child: Text(value)); }).toList(), onChanged: (val) { _data.calibrationStatus = val; }, - validator: (val) => val == null || val.isEmpty ? 'Status is required' : null, + onSaved: (val) => _data.calibrationStatus = val, + validator: (val) => + val == null || val.isEmpty ? 'Status is required' : null, ), const SizedBox(height: 16), TextFormField( controller: _remarksController, - decoration: const InputDecoration(labelText: 'Remarks', border: OutlineInputBorder()), + decoration: const InputDecoration( + labelText: 'Comment/Observation', + border: OutlineInputBorder()), maxLines: 3, ), ], diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index f66ac24..296d9d1 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -748,7 +748,7 @@ class _SettingsScreenState extends State { ListTile( leading: const Icon(Icons.info_outline), title: const Text('App Version'), - subtitle: const Text('MMS Version 3.5.01'), + subtitle: const Text('MMS Version 3.7.01'), dense: true, ), ListTile( diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 66d3691..afbb166 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -18,6 +18,12 @@ import 'package:environment_monitoring_app/models/air_installation_data.dart'; import 'package:environment_monitoring_app/models/river_in_situ_sampling_data.dart'; import 'package:environment_monitoring_app/services/server_config_service.dart'; +// --- ADDED: Imports for the new data models --- +import 'package:environment_monitoring_app/models/marine_manual_pre_departure_checklist_data.dart'; +import 'package:environment_monitoring_app/models/marine_manual_sonde_calibration_data.dart'; +import 'package:environment_monitoring_app/models/marine_manual_equipment_maintenance_data.dart'; +// --- END ADDED --- + // ======================================================================= // Part 1: Unified API Service // ======================================================================= @@ -41,7 +47,8 @@ class ApiService { // --- END: FIX FOR CONSTRUCTOR ERROR --- // --- Core API Methods --- - + // ... (keep all existing ApiService methods: login, register, getProfile, syncAllData, etc.) ... + // ... (code omitted for brevity) ... Future> login(String email, String password) async { final baseUrl = await _serverConfigService.getActiveApiUrl(); return _baseService.post(baseUrl, 'auth/login', {'email': email, 'password': password}); @@ -154,18 +161,11 @@ class ApiService { return result; } - /// Validates the current session token by making a lightweight API call. - /// Throws [SessionExpiredException] if the token is invalid (401). Future validateToken() async { final baseUrl = await _serverConfigService.getActiveApiUrl(); - // A simple GET request to an authenticated endpoint like /profile is perfect for validation. - // The underlying _handleResponse in BaseApiService will automatically throw the exception on 401. await _baseService.get(baseUrl, 'profile'); } - // --- REWRITTEN FOR DELTA SYNC --- - - /// Helper method to make a delta-sync API call. Future> _fetchDelta(String endpoint, String? lastSyncTimestamp) async { final baseUrl = await _serverConfigService.getActiveApiUrl(); String url = endpoint; @@ -175,7 +175,6 @@ class ApiService { return _baseService.get(baseUrl, url); } - /// Orchestrates a full DELTA sync from the server to the local database. Future> syncAllData({String? lastSyncTimestamp}) async { debugPrint('ApiService: Starting DELTA data sync. Since: $lastSyncTimestamp'); try { @@ -189,10 +188,8 @@ class ApiService { 'allUsers': { 'endpoint': 'users', 'handler': (d, id) async { - // START CHANGE: Use custom upsert method for users await dbHelper.upsertUsers(d); await dbHelper.deleteUsers(id); - // END CHANGE } }, 'documents': { @@ -286,7 +283,6 @@ class ApiService { await dbHelper.deleteAppSettings(id); } }, - // --- START: REPLACED GENERIC LIMITS WITH SPECIFIC SYNC TASKS --- 'npeParameterLimits': { 'endpoint': 'npe-parameter-limits', 'handler': (d, id) async { @@ -308,7 +304,6 @@ class ApiService { await dbHelper.deleteRiverParameterLimits(id); } }, - // --- END: REPLACED GENERIC LIMITS WITH SPECIFIC SYNC TASKS --- 'apiConfigs': { 'endpoint': 'api-configs', 'handler': (d, id) async { @@ -353,17 +348,13 @@ class ApiService { return {'success': true, 'message': 'Delta sync successful.'}; } catch (e) { debugPrint('ApiService: Delta data sync failed: $e'); - // Re-throw the original exception so AuthProvider can catch specific types like SessionExpiredException rethrow; } } - // --- START: NEW METHOD FOR REGISTRATION SCREEN --- - /// Fetches only the public master data required for the registration screen. Future> syncRegistrationData() async { debugPrint('ApiService: Starting registration data sync...'); try { - // Define only the tasks needed for registration final syncTasks = { 'departments': { 'endpoint': 'departments', @@ -388,13 +379,11 @@ class ApiService { }, }; - // Fetch all deltas in parallel, always a full fetch (since = null) final fetchFutures = syncTasks.map((key, value) => MapEntry(key, _fetchDelta(value['endpoint'] as String, null))); final results = await Future.wait(fetchFutures.values); final resultData = Map.fromIterables(fetchFutures.keys, results); - // Process and save all changes for (var entry in resultData.entries) { final key = entry.key; final result = entry.value; @@ -415,8 +404,6 @@ class ApiService { return {'success': false, 'message': 'Registration data sync failed: $e'}; } } -// --- END: NEW METHOD FOR REGISTRATION SCREEN --- - } // ======================================================================= @@ -424,6 +411,7 @@ class ApiService { // ======================================================================= class AirApiService { + // ... (AirApiService code remains unchanged) ... final BaseApiService _baseService; final TelegramService? _telegramService; final ServerConfigService _serverConfigService; @@ -485,6 +473,7 @@ class MarineApiService { MarineApiService(this._baseService, this._telegramService, this._serverConfigService, this._dbHelper); + // ... (keep existing MarineApiService methods: sendImageRequestEmail, getManualSamplingImages, getTarballStations, etc.) ... Future> sendImageRequestEmail({ required String recipientEmail, required List imageUrls, @@ -508,7 +497,6 @@ class MarineApiService { ); } - // --- START: FIX - Replaced mock with a real API call --- Future> getManualSamplingImages({ required int stationId, required DateTime samplingDate, @@ -523,9 +511,6 @@ class MarineApiService { final response = await _baseService.get(baseUrl, endpoint); - // The backend now returns a root 'data' key which the base service handles. - // However, the PHP controller wraps the results again in a 'data' key inside the main data object. - // We need to extract this nested list. if (response['success'] == true && response['data'] is Map && response['data']['data'] is List) { return { 'success': true, @@ -533,11 +518,8 @@ class MarineApiService { 'message': response['message'], }; } - - // Return the original response if the structure isn't as expected, or if it's an error. return response; } - // --- END: FIX --- Future> getTarballStations() async { final baseUrl = await _serverConfigService.getActiveApiUrl(); @@ -711,7 +693,6 @@ class MarineApiService { final limitName = _parameterKeyToLimitName[key]; if (limitName == null) return; - // START MODIFICATION: Only check for station-specific limits Map limitData = {}; if (stationId != null) { @@ -720,7 +701,6 @@ class MarineApiService { orElse: () => {}, ); } - // END MODIFICATION if (limitData.isNotEmpty) { final lowerLimit = parseLimitValue(limitData['param_lower_limit']); @@ -753,6 +733,7 @@ class MarineApiService { required Map imageFiles, required List>? appSettings, }) async { + // ... (existing tarball submission logic) ... final baseUrl = await _serverConfigService.getActiveApiUrl(); final dataResult = await _baseService.post(baseUrl, 'marine/tarball/sample', formData); if (dataResult['success'] != true) @@ -789,6 +770,7 @@ class MarineApiService { Future _handleTarballSuccessAlert( Map formData, List>? appSettings, {required bool isDataOnly}) async { + // ... (existing tarball alert logic) ... debugPrint("Triggering Telegram alert logic..."); try { final message = _generateTarballAlertMessage(formData, isDataOnly: isDataOnly); @@ -802,6 +784,7 @@ class MarineApiService { } String _generateTarballAlertMessage(Map formData, {required bool isDataOnly}) { + // ... (existing tarball message generation) ... final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)"; final stationName = formData['tbl_station_name'] ?? 'N/A'; final stationCode = formData['tbl_station_code'] ?? 'N/A'; @@ -831,9 +814,43 @@ class MarineApiService { return buffer.toString(); } + + // --- START: ADDED NEW METHODS FOR F-MM01, F-MM02, F-MM03 --- + + /// Submits the Pre-Departure Checklist (F-MM03) + Future> submitPreDepartureChecklist(MarineManualPreDepartureChecklistData data) async { + final baseUrl = await _serverConfigService.getActiveApiUrl(); + // The data.toApiFormData() method now formats the data correctly for the new controller + return _baseService.post(baseUrl, 'marine/checklist', data.toApiFormData()); // + } + + /// Submits the Sonde Calibration (F-MM02) + Future> submitSondeCalibration(MarineManualSondeCalibrationData data) async { + final baseUrl = await _serverConfigService.getActiveApiUrl(); + // The data.toApiFormData() method formats the data for the PHP controller + return _baseService.post(baseUrl, 'marine/calibration', data.toApiFormData()); // + } + + /// Submits the Equipment Maintenance Log (F-MM01) + Future> submitMaintenanceLog(MarineManualEquipmentMaintenanceData data) async { + final baseUrl = await _serverConfigService.getActiveApiUrl(); + // The data.toApiFormData() method formats the data correctly for the new normalized controller + return _baseService.post(baseUrl, 'marine/maintenance', data.toApiFormData()); // + } + + /// Fetches a list of previous equipment maintenance logs (F-MM01) + Future> getPreviousMaintenanceLogs() async { + final baseUrl = await _serverConfigService.getActiveApiUrl(); + // This endpoint should return a list of logs from the last 3-4 months. + // e.g., {'success': true, 'data': [{'maintenance_id': 1, 'maintenance_date': '2025-09-15', ...}, ...]} + return _baseService.get(baseUrl, 'marine/maintenance/previous'); + } +// --- END: ADDED NEW METHODS --- + } class RiverApiService { + // ... (RiverApiService code remains unchanged) ... final BaseApiService _baseService; final TelegramService _telegramService; final ServerConfigService _serverConfigService; @@ -1059,7 +1076,7 @@ class RiverApiService { } Future _generateInSituAlertMessage(Map formData, {required bool isDataOnly}) async { - final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)"; + final submissionType = isDataOnly ? "(Data Only)" : "(Data &Images)"; final stationName = formData['r_man_station_name'] ?? 'N/A'; final stationCode = formData['r_man_station_code'] ?? 'N/A'; final submissionDate = formData['r_man_date'] ?? DateFormat('yyyy-MM-dd').format(DateTime.now()); @@ -1172,6 +1189,7 @@ class RiverApiService { // ======================================================================= class DatabaseHelper { + // ... (DatabaseHelper code remains unchanged) ... static Database? _database; static const String _dbName = 'app_data.db'; static const int _dbVersion = 23; diff --git a/lib/services/marine_manual_equipment_maintenance_service.dart b/lib/services/marine_manual_equipment_maintenance_service.dart index d035690..2e64064 100644 --- a/lib/services/marine_manual_equipment_maintenance_service.dart +++ b/lib/services/marine_manual_equipment_maintenance_service.dart @@ -1,17 +1,74 @@ +// lib/services/marine_manual_equipment_maintenance_service.dart + +import 'dart:async'; +import 'dart:io'; + import '../auth_provider.dart'; import '../models/marine_manual_equipment_maintenance_data.dart'; +import 'api_service.dart'; +import 'base_api_service.dart'; // Import for SessionExpiredException class MarineManualEquipmentMaintenanceService { + final ApiService _apiService; + + MarineManualEquipmentMaintenanceService(this._apiService); + Future> submitMaintenanceReport({ required MarineManualEquipmentMaintenanceData data, required AuthProvider authProvider, }) async { - // TODO: Implement the full online/offline submission logic here. - print("Submitting Equipment Maintenance Report..."); - await Future.delayed(const Duration(seconds: 1)); - return { - 'success': true, - 'message': 'Equipment Maintenance Report submitted (simulation).' - }; + try { + // Call the existing method in MarineApiService + return await _apiService.marine.submitMaintenanceLog(data); + } on SessionExpiredException { + // Handle session expiry by attempting a silent relogin + final bool reloginSuccess = await authProvider.attemptSilentRelogin(); + if (reloginSuccess) { + // Retry the submission once if relogin was successful + return await _apiService.marine.submitMaintenanceLog(data); + } else { + return { + 'success': false, + 'message': 'Session expired. Please log in again.' + }; + } + } on SocketException { + // Handle network errors + return { + 'success': false, + 'message': 'Submission failed. Please check your network connection.' + }; + } on TimeoutException { + // Handle timeout errors + return { + 'success': false, + 'message': 'Submission timed out. Please check your network connection.' + }; + } catch (e) { + // Handle any other unexpected errors + return {'success': false, 'message': 'An unexpected error occurred: $e'}; + } + } + + /// Fetches previous maintenance logs to populate the form + Future> getPreviousMaintenanceLogs({ + required AuthProvider authProvider, + }) async { + try { + return await _apiService.marine.getPreviousMaintenanceLogs(); + } on SessionExpiredException { + final bool reloginSuccess = await authProvider.attemptSilentRelogin(); + if (reloginSuccess) { + return await _apiService.marine.getPreviousMaintenanceLogs(); + } else { + return {'success': false, 'message': 'Session expired. Please log in again.'}; + } + } on SocketException { + return {'success': false, 'message': 'Network error. Could not fetch previous records.'}; + } on TimeoutException { + return {'success': false, 'message': 'Request timed out. Could not fetch previous records.'}; + } catch (e) { + return {'success': false, 'message': 'An unexpected error occurred: $e'}; + } } } \ No newline at end of file diff --git a/lib/services/marine_manual_pre_departure_service.dart b/lib/services/marine_manual_pre_departure_service.dart index d182884..4ebc277 100644 --- a/lib/services/marine_manual_pre_departure_service.dart +++ b/lib/services/marine_manual_pre_departure_service.dart @@ -1,18 +1,52 @@ +// lib/services/marine_manual_pre_departure_service.dart + +import 'dart:async'; +import 'dart:io'; + import '../auth_provider.dart'; import '../models/marine_manual_pre_departure_checklist_data.dart'; +import 'api_service.dart'; +import 'base_api_service.dart'; // Import for SessionExpiredException class MarineManualPreDepartureService { + final ApiService _apiService; + + MarineManualPreDepartureService(this._apiService); + Future> submitChecklist({ required MarineManualPreDepartureChecklistData data, required AuthProvider authProvider, }) async { - // TODO: Implement the full online/offline submission logic here, - // similar to your MarineNpeReportService. - print("Submitting Pre-Departure Checklist..."); - await Future.delayed(const Duration(seconds: 1)); - return { - 'success': true, - 'message': 'Pre-Departure Checklist submitted (simulation).' - }; + try { + // Call the existing method in MarineApiService + return await _apiService.marine.submitPreDepartureChecklist(data); + } on SessionExpiredException { + // Handle session expiry by attempting a silent relogin + final bool reloginSuccess = await authProvider.attemptSilentRelogin(); + if (reloginSuccess) { + // Retry the submission once if relogin was successful + return await _apiService.marine.submitPreDepartureChecklist(data); + } else { + return { + 'success': false, + 'message': 'Session expired. Please log in again.' + }; + } + } on SocketException { + // Handle network errors + return { + 'success': false, + 'message': 'Submission failed. Please check your network connection.' + }; + } on TimeoutException { + // Handle timeout errors + return { + 'success': false, + 'message': 'Submission timed out. Please check your network connection.' + }; + } catch (e) { + // Handle any other unexpected errors + return {'success': false, 'message': 'An unexpected error occurred: $e'}; + } } } \ No newline at end of file diff --git a/lib/services/marine_manual_sonde_calibration_service.dart b/lib/services/marine_manual_sonde_calibration_service.dart index 8c594dd..9dcaea6 100644 --- a/lib/services/marine_manual_sonde_calibration_service.dart +++ b/lib/services/marine_manual_sonde_calibration_service.dart @@ -1,14 +1,52 @@ +// lib/services/marine_manual_sonde_calibration_service.dart + +import 'dart:async'; +import 'dart:io'; + import '../auth_provider.dart'; import '../models/marine_manual_sonde_calibration_data.dart'; +import 'api_service.dart'; +import 'base_api_service.dart'; // Import for SessionExpiredException class MarineManualSondeCalibrationService { + final ApiService _apiService; + + MarineManualSondeCalibrationService(this._apiService); + Future> submitCalibration({ required MarineManualSondeCalibrationData data, required AuthProvider authProvider, }) async { - // TODO: Implement online/offline submission logic. - print("Submitting Sonde Calibration..."); - await Future.delayed(const Duration(seconds: 1)); - return {'success': true, 'message': 'Sonde Calibration submitted (simulation).'}; + try { + // Call the existing method in MarineApiService + return await _apiService.marine.submitSondeCalibration(data); + } on SessionExpiredException { + // Handle session expiry by attempting a silent relogin + final bool reloginSuccess = await authProvider.attemptSilentRelogin(); + if (reloginSuccess) { + // Retry the submission once if relogin was successful + return await _apiService.marine.submitSondeCalibration(data); + } else { + return { + 'success': false, + 'message': 'Session expired. Please log in again.' + }; + } + } on SocketException { + // Handle network errors + return { + 'success': false, + 'message': 'Submission failed. Please check your network connection.' + }; + } on TimeoutException { + // Handle timeout errors + return { + 'success': false, + 'message': 'Submission timed out. Please check your network connection.' + }; + } catch (e) { + // Handle any other unexpected errors + return {'success': false, 'message': 'An unexpected error occurred: $e'}; + } } } \ No newline at end of file