fix marine maintenance form

This commit is contained in:
ALim Aidrus 2025-10-23 14:57:35 +08:00
parent de4c0c471c
commit a11c0d8df8
19 changed files with 1653 additions and 340 deletions

View File

@ -29,7 +29,7 @@ class _HomePageState extends State<HomePage> {
}); });
}, },
), ),
title: const Text("MMS Version 3.5.01"), title: const Text("MMS Version 3.7.01"),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.person), icon: const Icon(Icons.person),

View File

@ -140,9 +140,16 @@ void main() async {
Provider(create: (context) => AirSamplingService(databaseHelper, telegramService)), Provider(create: (context) => AirSamplingService(databaseHelper, telegramService)),
Provider(create: (context) => MarineTarballSamplingService(telegramService)), Provider(create: (context) => MarineTarballSamplingService(telegramService)),
Provider(create: (context) => MarineNpeReportService(Provider.of<TelegramService>(context, listen: false))), Provider(create: (context) => MarineNpeReportService(Provider.of<TelegramService>(context, listen: false))),
Provider(create: (context) => MarineManualPreDepartureService()), // --- UPDATED: Inject ApiService into the service constructors ---
Provider(create: (context) => MarineManualSondeCalibrationService()), Provider(create: (context) => MarineManualPreDepartureService(
Provider(create: (context) => MarineManualEquipmentMaintenanceService()), Provider.of<ApiService>(context, listen: false)
)),
Provider(create: (context) => MarineManualSondeCalibrationService(
Provider.of<ApiService>(context, listen: false)
)),
Provider(create: (context) => MarineManualEquipmentMaintenanceService(
Provider.of<ApiService>(context, listen: false)
)),
], ],
child: const RootApp(), child: const RootApp(),
), ),

View File

@ -0,0 +1 @@
// lib/models/marine_inves_manual_sampling_data.dart

View File

@ -1,23 +1,140 @@
class MarineManualEquipmentMaintenanceData { import 'dart:convert';
int? performedByUserId;
String? equipmentName;
String? maintenanceDate;
String? maintenanceType;
String? workDescription;
String? partsReplaced;
String? status;
String? remarks;
class MarineManualEquipmentMaintenanceData {
int? conductedByUserId;
String? maintenanceDate;
String? lastMaintenanceDate;
String? scheduleMaintenance;
bool isReplacement = false;
String? timeStart;
String? timeEnd;
String? location;
// Part 1 - YSI
Map<String, bool> ysiSondeChecks = {};
String? ysiSondeComments;
Map<String, Map<String, bool>> ysiSensorChecks = {};
String? ysiSensorComments;
Map<String, Map<String, String>> ysiReplacements = {};
// Part 2 - Van Dorn Sampler
Map<String, Map<String, bool>> vanDornChecks = {};
String? vanDornComments;
String? vanDornCurrentSerial;
String? vanDornNewSerial;
Map<String, Map<String, String>> 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<String, dynamic> toApiFormData() { Map<String, dynamic> toApiFormData() {
// 1. YSI Sensor Checks List
List<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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 { return {
'performed_by_user_id': performedByUserId.toString(), 'conducted_by_user_id': conductedByUserId.toString(),
'equipment_name': equipmentName,
'maintenance_date': maintenanceDate, 'maintenance_date': maintenanceDate,
'maintenance_type': maintenanceType, 'last_maintenance_date': lastMaintenanceDate?.isEmpty ?? true ? null : lastMaintenanceDate,
'work_description': workDescription, 'schedule_maintenance': scheduleMaintenance,
'parts_replaced': partsReplaced, 'is_replacement': isReplacement,
'status': status, 'time_start': timeStart?.isEmpty ?? true ? null : timeStart,
'remarks': remarks, '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,
}; };
} }
} }

View File

@ -13,12 +13,25 @@ class MarineManualPreDepartureChecklistData {
MarineManualPreDepartureChecklistData(); MarineManualPreDepartureChecklistData();
// MODIFIED: This method now builds the nested array structure the PHP controller expects.
Map<String, dynamic> toApiFormData() { Map<String, dynamic> toApiFormData() {
// Create the 'items' list required by the API
List<Map<String, dynamic>> 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 { return {
'reporter_user_id': reporterUserId.toString(), 'reporter_user_id': reporterUserId.toString(), // The controller gets this from auth, but good to send.
'submission_date': submissionDate, 'submission_date': submissionDate,
'checklist_items': jsonEncode(checklistItems), 'items': itemsList, // Send the formatted list
'remarks': jsonEncode(remarks),
}; };
} }
} }

View File

@ -1,44 +1,59 @@
class MarineManualSondeCalibrationData { class MarineManualSondeCalibrationData {
int? calibratedByUserId; int? calibratedByUserId;
String? sondeId;
String? calibrationDateTime;
// pH values // Header fields from PDF
double? ph4Initial; String? sondeSerialNumber;
double? ph4Calibrated; String? firmwareVersion;
double? ph7Initial; String? korVersion;
double? ph7Calibrated; String? location;
double? ph10Initial; String? startDateTime;
double? ph10Calibrated; String? endDateTime;
// Other parameters // pH values (with Mv)
double? condInitial; double? ph7Mv;
double? condCalibrated; double? ph7Before;
double? doInitial; double? ph7After;
double? doCalibrated; double? ph10Mv;
double? turbidityInitial; double? ph10Before;
double? turbidityCalibrated; 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? calibrationStatus;
String? remarks; String? remarks; // Matches "COMMENT/OBSERVATION"
Map<String, dynamic> toApiFormData() { Map<String, dynamic> toApiFormData() {
// This flat structure matches MarineSondeCalibrationController.php
return { return {
'calibrated_by_user_id': calibratedByUserId.toString(), 'calibrated_by_user_id': calibratedByUserId.toString(),
'sonde_id': sondeId, 'sonde_serial_number': sondeSerialNumber,
'calibration_datetime': calibrationDateTime, 'firmware_version': firmwareVersion,
'ph_4_initial': ph4Initial?.toString(), 'kor_version': korVersion,
'ph_4_calibrated': ph4Calibrated?.toString(), 'location': location,
'ph_7_initial': ph7Initial?.toString(), 'start_datetime': startDateTime,
'ph_7_calibrated': ph7Calibrated?.toString(), 'end_datetime': endDateTime,
'ph_10_initial': ph10Initial?.toString(), 'ph_7_mv': ph7Mv?.toString(),
'ph_10_calibrated': ph10Calibrated?.toString(), 'ph_7_before': ph7Before?.toString(),
'cond_initial': condInitial?.toString(), 'ph_7_after': ph7After?.toString(),
'cond_calibrated': condCalibrated?.toString(), 'ph_10_mv': ph10Mv?.toString(),
'do_initial': doInitial?.toString(), 'ph_10_before': ph10Before?.toString(),
'do_calibrated': doCalibrated?.toString(), 'ph_10_after': ph10After?.toString(),
'turbidity_initial': turbidityInitial?.toString(), 'cond_before': condBefore?.toString(),
'turbidity_calibrated': turbidityCalibrated?.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, 'calibration_status': calibrationStatus,
'remarks': remarks, 'remarks': remarks,
}; };

View File

@ -0,0 +1 @@
//lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_1_sampling_info.dart

View File

@ -0,0 +1 @@
//lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart

View File

@ -0,0 +1 @@
//lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_3_data_capture.dart

View File

@ -0,0 +1 @@
//lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_4_summary.dart

View File

@ -0,0 +1 @@
//lib/screens/marine/investigative/marine_investigative_manual_sampling.dart

View File

@ -23,63 +23,83 @@ class _MarineManualPreDepartureChecklistScreenState
final Map<String, bool> _remarksVisibility = {}; final Map<String, bool> _remarksVisibility = {};
// NEW: State variables for connectivity // State variables for connectivity
bool _isOnline = true; bool _isOnline = true;
late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription; late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
// All checklist items from the PDF are now grouped
final List<String> _checklistItemsText = [ final Map<String, List<String>> _checklistSections = {
'MMWQM Standard Operation Procedure (SOP)', 'INTERNAL - IN-SITU SAMPLING': [ // Section title matches PDF
'Back-up Sampling Sheet and Chain of Custody form', 'Marine manual Standard Operation Procedure (SOP)', // Item text matches PDF
'YSI EXO2 Sonde Include Sensor (pH/Turbidity/Conductivity/Dissolved Oxygen)', 'Back-up Sampling Sheet & Chain of Custody form', // Item text matches PDF
'Varn Dorn Sampler with Rope and Messenger', 'Calibration worksheet', // Item text matches PDF
'Laptop', 'YSI EXO 2 Sonde include sensor (pH/Turbidity/Conductivity/Dissolved Oxygen)', // Item text matches PDF
'Smart pre-installed with application (apps for manual sampling - MMS)', 'Spare set sensor (pH/Turbidity/Conductivity/Dissolved Oxygen)', // Item text matches PDF
'GPS Navigation', 'YSI serial cable', // Item text matches PDF
'Calibration standards (pH/Turbidity/Conductivity)', 'Van Dorn Sampler (with rope & messenger)', // Item text matches PDF
'Distilled water (D.I)', 'Laptop', // Item text matches PDF
'Universal pH Indicator paper', 'Smartphone pre-installed with application (Apps for manual sampling-MMS)', // Item text matches PDF
'Personal Floating Devices (PFD)', 'GPS navigation', // Item text matches PDF
'First aid kits', 'Calibration standards (pH/Turbidity/Conductivity)', // Item text matches PDF
'Sampling Shoes', 'Distilled water (D.I.)', // Item text matches PDF
'Sufficient set of cooler box and sampling bottles', 'Universal pH indicator paper', // Item text matches PDF
'Ice packets', 'Alcohol swab', // Item text matches PDF
'Disposable gloves', 'Personal Floating Devices (PFD)', // Item text matches PDF
'Black plastic bags', 'First aid kits', // Item text matches PDF
'Maker pen, pen and brown tapes', 'Disposable gloves', // Item text matches PDF
'Zipper Bags', 'Black plastic bags', // Item text matches PDF
'Aluminium Foil', '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 @override
void initState() { void initState() {
super.initState(); super.initState();
for (var item in _checklistItemsText) { // Iterate through the map structure to initialize data
_data.checklistItems[item] = false; _checklistSections.forEach((section, items) {
for (var item in items) {
_data.checklistItems[item] = true; // MODIFIED: Default to 'Yes'
_data.remarks[item] = ''; _data.remarks[item] = '';
_remarksVisibility[item] = false; _remarksVisibility[item] = false;
} }
});
// NEW: Check initial connection and start listening for changes // Check initial connection and start listening for changes
_checkInitialConnectivity(); _checkInitialConnectivity();
_connectivitySubscription = _connectivitySubscription =
Connectivity().onConnectivityChanged.listen(_updateConnectionStatus); Connectivity().onConnectivityChanged.listen(_updateConnectionStatus);
} }
// NEW: Dispose the connectivity listener to prevent memory leaks
@override @override
void dispose() { void dispose() {
_connectivitySubscription.cancel(); _connectivitySubscription.cancel();
super.dispose(); super.dispose();
} }
// NEW: Method to check the first time the screen loads
Future<void> _checkInitialConnectivity() async { Future<void> _checkInitialConnectivity() async {
final connectivityResult = await Connectivity().checkConnectivity(); final connectivityResult = await Connectivity().checkConnectivity();
_updateConnectionStatus(connectivityResult); _updateConnectionStatus(connectivityResult);
} }
// NEW: Callback method to update UI based on connectivity changes
void _updateConnectionStatus(List<ConnectivityResult> result) { void _updateConnectionStatus(List<ConnectivityResult> result) {
final bool currentlyOnline = !result.contains(ConnectivityResult.none); final bool currentlyOnline = !result.contains(ConnectivityResult.none);
if (_isOnline != currentlyOnline) { if (_isOnline != currentlyOnline) {
@ -99,23 +119,28 @@ class _MarineManualPreDepartureChecklistScreenState
} }
Future<void> _submit() async { Future<void> _submit() async {
// No form validation needed as Project/Month fields removed
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
final auth = Provider.of<AuthProvider>(context, listen: false); final auth = Provider.of<AuthProvider>(context, listen: false);
final service = Provider.of<MarineManualPreDepartureService>(context, listen: false); final service =
Provider.of<MarineManualPreDepartureService>(context, listen: false);
_data.reporterUserId = auth.profileData?['user_id']; _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; _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) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(result['message']), 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(); if (result['success'] == true) Navigator.of(context).pop();
@ -124,7 +149,8 @@ class _MarineManualPreDepartureChecklistScreenState
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text("Submission failed. Please check your network connection."), content:
Text("Submission failed. Please check your network connection."),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@ -153,7 +179,7 @@ class _MarineManualPreDepartureChecklistScreenState
), ),
body: Column( body: Column(
children: [ children: [
// NEW: Offline banner that shows when there's no internet // Offline banner
if (!_isOnline) if (!_isOnline)
Container( Container(
width: double.infinity, width: double.infinity,
@ -170,16 +196,9 @@ class _MarineManualPreDepartureChecklistScreenState
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
ListView.separated( // Build UI from the section map
physics: const NeverScrollableScrollPhysics(), ..._buildSectionWidgets(),
shrinkWrap: true,
itemCount: _checklistItemsText.length,
itemBuilder: (context, index) {
final item = _checklistItemsText[index];
return _buildChecklistItem(item);
},
separatorBuilder: (context, index) => const SizedBox(height: 8),
),
const SizedBox(height: 24), const SizedBox(height: 24),
const Divider(), const Divider(),
const SizedBox(height: 10), const SizedBox(height: 10),
@ -194,10 +213,11 @@ class _MarineManualPreDepartureChecklistScreenState
const SizedBox(width: 10.0), const SizedBox(width: 10.0),
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(
// NEW: Submit button is disabled when loading OR offline onPressed:
onPressed: _isLoading || !_isOnline ? null : _submit, _isLoading || !_isOnline ? null : _submit,
child: _isLoading child: _isLoading
? const CircularProgressIndicator(color: Colors.white) ? const CircularProgressIndicator(
color: Colors.white)
: const Text('Submit'), : const Text('Submit'),
), ),
) )
@ -212,6 +232,38 @@ class _MarineManualPreDepartureChecklistScreenState
); );
} }
// Helper method to build the list of sections and their items
List<Widget> _buildSectionWidgets() {
List<Widget> 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) { Widget _buildChecklistItem(String title) {
return Card( return Card(
elevation: 2, elevation: 2,
@ -233,17 +285,19 @@ class _MarineManualPreDepartureChecklistScreenState
ToggleSwitch( ToggleSwitch(
minWidth: 70.0, minWidth: 70.0,
cornerRadius: 20.0, cornerRadius: 20.0,
// Use theme colors
activeBgColor: [Theme.of(context).colorScheme.primary], activeBgColor: [Theme.of(context).colorScheme.primary],
activeFgColor: Colors.white, activeFgColor: Colors.white, // Keep white for contrast on primary
inactiveBgColor: Colors.grey[700], inactiveBgColor: Colors.grey[700], // Dark theme inactive
inactiveFgColor: Colors.white, inactiveFgColor: Colors.white, // White for contrast on dark grey
initialLabelIndex: _data.checklistItems[title]! ? 0 : 1, // MODIFIED: Logic reversed for ['No', 'Yes']
initialLabelIndex: _data.checklistItems[title]! ? 1 : 0, // No: 0, Yes: 1
totalSwitches: 2, totalSwitches: 2,
labels: const ['Yes', 'No'], labels: const ['No', 'Yes'], // MODIFIED: Swapped labels
radiusStyle: true, radiusStyle: true,
onToggle: (index) { onToggle: (index) {
setState(() { 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]!) if (_remarksVisibility[title]!)
TextFormField( TextFormField(
initialValue: _data.remarks[title], initialValue: _data.remarks[title],
// Rely on theme for input decoration
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Remarks', labelText: 'Remarks',
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), contentPadding:
EdgeInsets.symmetric(horizontal: 12, vertical: 10),
border: OutlineInputBorder(), border: OutlineInputBorder(),
alignLabelWithHint: true, // Improves look for multi-line
), ),
maxLines: 3, // Allow multiple lines for remarks
onChanged: (value) { onChanged: (value) {
// Save remarks as user types, no need for onSaved with controllers
_data.remarks[title] = value; _data.remarks[title] = value;
}, },
), ),
@ -270,16 +329,20 @@ class _MarineManualPreDepartureChecklistScreenState
_remarksVisibility[title] = !_remarksVisibility[title]!; _remarksVisibility[title] = !_remarksVisibility[title]!;
}); });
}, },
// Use theme icon color implicitly
icon: Icon( icon: Icon(
_remarksVisibility[title]! _remarksVisibility[title]!
? Icons.remove_circle_outline ? Icons.remove_circle_outline
: Icons.add_comment_outlined, : Icons.add_comment_outlined,
size: 16, size: 16,
), ),
// Use theme text color implicitly
label: Text( label: Text(
_data.remarks[title]!.isNotEmpty _remarksVisibility[title]!
? 'Hide Remarks' // Change label when visible
: (_data.remarks[title]!.isNotEmpty
? 'Edit Remarks' ? 'Edit Remarks'
: 'Add Remarks', : 'Add Remarks'),
), ),
), ),
), ),

View File

@ -22,19 +22,22 @@ class _MarineManualSondeCalibrationScreenState
final _data = MarineManualSondeCalibrationData(); final _data = MarineManualSondeCalibrationData();
bool _isLoading = false; bool _isLoading = false;
// State for connectivity
bool _isOnline = true; bool _isOnline = true;
late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription; late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
// Text Controllers final _sondeSerialController = TextEditingController();
final _sondeIdController = TextEditingController(); final _firmwareController = TextEditingController();
final _dateTimeController = TextEditingController(); final _korController = TextEditingController();
final _locationController = TextEditingController();
final _startDateTimeController = TextEditingController();
final _endDateTimeController = TextEditingController();
final _remarksController = TextEditingController(); final _remarksController = TextEditingController();
@override @override
void initState() { void initState() {
super.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(); _checkInitialConnectivity();
_connectivitySubscription = _connectivitySubscription =
Connectivity().onConnectivityChanged.listen(_updateConnectionStatus); Connectivity().onConnectivityChanged.listen(_updateConnectionStatus);
@ -43,8 +46,12 @@ class _MarineManualSondeCalibrationScreenState
@override @override
void dispose() { void dispose() {
_connectivitySubscription.cancel(); _connectivitySubscription.cancel();
_sondeIdController.dispose(); _sondeSerialController.dispose();
_dateTimeController.dispose(); _firmwareController.dispose();
_korController.dispose();
_locationController.dispose();
_startDateTimeController.dispose();
_endDateTimeController.dispose();
_remarksController.dispose(); _remarksController.dispose();
super.dispose(); super.dispose();
} }
@ -69,6 +76,26 @@ class _MarineManualSondeCalibrationScreenState
} }
} }
Future<void> _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<void> _submit() async { Future<void> _submit() async {
if (!_formKey.currentState!.validate()) { if (!_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar( ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
@ -82,14 +109,20 @@ class _MarineManualSondeCalibrationScreenState
try { try {
final auth = Provider.of<AuthProvider>(context, listen: false); final auth = Provider.of<AuthProvider>(context, listen: false);
final service = Provider.of<MarineManualSondeCalibrationService>(context, listen: false); final service =
Provider.of<MarineManualSondeCalibrationService>(context, listen: false);
_data.calibratedByUserId = auth.profileData?['user_id']; _data.calibratedByUserId = auth.profileData?['user_id'];
_data.sondeId = _sondeIdController.text; _data.sondeSerialNumber = _sondeSerialController.text;
_data.calibrationDateTime = _dateTimeController.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; _data.remarks = _remarksController.text;
final result = await service.submitCalibration(data: _data, authProvider: auth); final result =
await service.submitCalibration(data: _data, authProvider: auth);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
@ -101,7 +134,8 @@ class _MarineManualSondeCalibrationScreenState
} on SocketException { } on SocketException {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar( 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, backgroundColor: Colors.red,
)); ));
} }
@ -147,33 +181,7 @@ class _MarineManualSondeCalibrationScreenState
children: [ children: [
_buildGeneralInfoCard(), _buildGeneralInfoCard(),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildCalibrationCard( _buildCalibrationTableCard(),
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;
},
),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildSummaryCard(), _buildSummaryCard(),
const SizedBox(height: 24), const SizedBox(height: 24),
@ -188,7 +196,8 @@ class _MarineManualSondeCalibrationScreenState
child: ElevatedButton( child: ElevatedButton(
onPressed: _isLoading || !_isOnline ? null : _submit, onPressed: _isLoading || !_isOnline ? null : _submit,
child: _isLoading child: _isLoading
? const CircularProgressIndicator(color: Colors.white) ? const CircularProgressIndicator(
color: Colors.white)
: const Text('Submit'), : const Text('Submit'),
), ),
) )
@ -213,18 +222,69 @@ class _MarineManualSondeCalibrationScreenState
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('General Information', style: Theme.of(context).textTheme.titleLarge), Text('General Information',
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: _sondeIdController, controller: _sondeSerialController,
decoration: const InputDecoration(labelText: 'Sonde ID *', border: OutlineInputBorder()), decoration: const InputDecoration(
validator: (val) => val == null || val.isEmpty ? 'Sonde ID is required' : null, 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), const SizedBox(height: 16),
TextFormField( 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, 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({ Widget _buildCalibrationTableCard() {
required String title,
required List<String> parameters,
required Function(String, String, double?) onSave,
}) {
return Card( return Card(
elevation: 2, elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
@ -245,38 +301,142 @@ class _MarineManualSondeCalibrationScreenState
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, style: Theme.of(context).textTheme.titleLarge), Text('Calibration Parameters',
const SizedBox(height: 8), style: Theme.of(context).textTheme.titleLarge),
...parameters.map((param) => _buildParameterRow(param, onSave)).toList(), 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( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Row(
children: [ children: [
Expanded(flex: 2, child: Text(label, style: Theme.of(context).textTheme.titleMedium)),
Expanded( Expanded(
flex: 3, child: TextFormField(
child: Row( decoration: const InputDecoration(
children: [ labelText: 'MV Reading', border: OutlineInputBorder()),
Expanded(child: TextFormField(
decoration: const InputDecoration(labelText: 'Initial', border: OutlineInputBorder()),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onSaved: (val) => onSave(label, 'Initial', double.tryParse(val ?? '')), onSaved: (val) => onSaveMv(double.tryParse(val ?? '')),
)), )),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: TextFormField( Expanded(
decoration: const InputDecoration(labelText: 'Calibrated', border: OutlineInputBorder()), child: TextFormField(
decoration: const InputDecoration(
labelText: 'Before Cal', border: OutlineInputBorder()),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onSaved: (val) => onSave(label, 'Calibrated', double.tryParse(val ?? '')), 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), Text('Summary', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 16), const SizedBox(height: 16),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
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) { items: ['Pass', 'Fail', 'Pass with Issues'].map((String value) {
return DropdownMenuItem<String>(value: value, child: Text(value)); return DropdownMenuItem<String>(value: value, child: Text(value));
}).toList(), }).toList(),
onChanged: (val) { onChanged: (val) {
_data.calibrationStatus = 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), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: _remarksController, controller: _remarksController,
decoration: const InputDecoration(labelText: 'Remarks', border: OutlineInputBorder()), decoration: const InputDecoration(
labelText: 'Comment/Observation',
border: OutlineInputBorder()),
maxLines: 3, maxLines: 3,
), ),
], ],

View File

@ -748,7 +748,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
ListTile( ListTile(
leading: const Icon(Icons.info_outline), leading: const Icon(Icons.info_outline),
title: const Text('App Version'), title: const Text('App Version'),
subtitle: const Text('MMS Version 3.5.01'), subtitle: const Text('MMS Version 3.7.01'),
dense: true, dense: true,
), ),
ListTile( ListTile(

View File

@ -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/models/river_in_situ_sampling_data.dart';
import 'package:environment_monitoring_app/services/server_config_service.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 // Part 1: Unified API Service
// ======================================================================= // =======================================================================
@ -41,7 +47,8 @@ class ApiService {
// --- END: FIX FOR CONSTRUCTOR ERROR --- // --- END: FIX FOR CONSTRUCTOR ERROR ---
// --- Core API Methods --- // --- Core API Methods ---
// ... (keep all existing ApiService methods: login, register, getProfile, syncAllData, etc.) ...
// ... (code omitted for brevity) ...
Future<Map<String, dynamic>> login(String email, String password) async { Future<Map<String, dynamic>> login(String email, String password) async {
final baseUrl = await _serverConfigService.getActiveApiUrl(); final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.post(baseUrl, 'auth/login', {'email': email, 'password': password}); return _baseService.post(baseUrl, 'auth/login', {'email': email, 'password': password});
@ -154,18 +161,11 @@ class ApiService {
return result; return result;
} }
/// Validates the current session token by making a lightweight API call.
/// Throws [SessionExpiredException] if the token is invalid (401).
Future<void> validateToken() async { Future<void> validateToken() async {
final baseUrl = await _serverConfigService.getActiveApiUrl(); 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'); await _baseService.get(baseUrl, 'profile');
} }
// --- REWRITTEN FOR DELTA SYNC ---
/// Helper method to make a delta-sync API call.
Future<Map<String, dynamic>> _fetchDelta(String endpoint, String? lastSyncTimestamp) async { Future<Map<String, dynamic>> _fetchDelta(String endpoint, String? lastSyncTimestamp) async {
final baseUrl = await _serverConfigService.getActiveApiUrl(); final baseUrl = await _serverConfigService.getActiveApiUrl();
String url = endpoint; String url = endpoint;
@ -175,7 +175,6 @@ class ApiService {
return _baseService.get(baseUrl, url); return _baseService.get(baseUrl, url);
} }
/// Orchestrates a full DELTA sync from the server to the local database.
Future<Map<String, dynamic>> syncAllData({String? lastSyncTimestamp}) async { Future<Map<String, dynamic>> syncAllData({String? lastSyncTimestamp}) async {
debugPrint('ApiService: Starting DELTA data sync. Since: $lastSyncTimestamp'); debugPrint('ApiService: Starting DELTA data sync. Since: $lastSyncTimestamp');
try { try {
@ -189,10 +188,8 @@ class ApiService {
'allUsers': { 'allUsers': {
'endpoint': 'users', 'endpoint': 'users',
'handler': (d, id) async { 'handler': (d, id) async {
// START CHANGE: Use custom upsert method for users
await dbHelper.upsertUsers(d); await dbHelper.upsertUsers(d);
await dbHelper.deleteUsers(id); await dbHelper.deleteUsers(id);
// END CHANGE
} }
}, },
'documents': { 'documents': {
@ -286,7 +283,6 @@ class ApiService {
await dbHelper.deleteAppSettings(id); await dbHelper.deleteAppSettings(id);
} }
}, },
// --- START: REPLACED GENERIC LIMITS WITH SPECIFIC SYNC TASKS ---
'npeParameterLimits': { 'npeParameterLimits': {
'endpoint': 'npe-parameter-limits', 'endpoint': 'npe-parameter-limits',
'handler': (d, id) async { 'handler': (d, id) async {
@ -308,7 +304,6 @@ class ApiService {
await dbHelper.deleteRiverParameterLimits(id); await dbHelper.deleteRiverParameterLimits(id);
} }
}, },
// --- END: REPLACED GENERIC LIMITS WITH SPECIFIC SYNC TASKS ---
'apiConfigs': { 'apiConfigs': {
'endpoint': 'api-configs', 'endpoint': 'api-configs',
'handler': (d, id) async { 'handler': (d, id) async {
@ -353,17 +348,13 @@ class ApiService {
return {'success': true, 'message': 'Delta sync successful.'}; return {'success': true, 'message': 'Delta sync successful.'};
} catch (e) { } catch (e) {
debugPrint('ApiService: Delta data sync failed: $e'); debugPrint('ApiService: Delta data sync failed: $e');
// Re-throw the original exception so AuthProvider can catch specific types like SessionExpiredException
rethrow; rethrow;
} }
} }
// --- START: NEW METHOD FOR REGISTRATION SCREEN ---
/// Fetches only the public master data required for the registration screen.
Future<Map<String, dynamic>> syncRegistrationData() async { Future<Map<String, dynamic>> syncRegistrationData() async {
debugPrint('ApiService: Starting registration data sync...'); debugPrint('ApiService: Starting registration data sync...');
try { try {
// Define only the tasks needed for registration
final syncTasks = { final syncTasks = {
'departments': { 'departments': {
'endpoint': '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) => final fetchFutures = syncTasks.map((key, value) =>
MapEntry(key, _fetchDelta(value['endpoint'] as String, null))); MapEntry(key, _fetchDelta(value['endpoint'] as String, null)));
final results = await Future.wait(fetchFutures.values); final results = await Future.wait(fetchFutures.values);
final resultData = Map.fromIterables(fetchFutures.keys, results); final resultData = Map.fromIterables(fetchFutures.keys, results);
// Process and save all changes
for (var entry in resultData.entries) { for (var entry in resultData.entries) {
final key = entry.key; final key = entry.key;
final result = entry.value; final result = entry.value;
@ -415,8 +404,6 @@ class ApiService {
return {'success': false, 'message': 'Registration data sync failed: $e'}; return {'success': false, 'message': 'Registration data sync failed: $e'};
} }
} }
// --- END: NEW METHOD FOR REGISTRATION SCREEN ---
} }
// ======================================================================= // =======================================================================
@ -424,6 +411,7 @@ class ApiService {
// ======================================================================= // =======================================================================
class AirApiService { class AirApiService {
// ... (AirApiService code remains unchanged) ...
final BaseApiService _baseService; final BaseApiService _baseService;
final TelegramService? _telegramService; final TelegramService? _telegramService;
final ServerConfigService _serverConfigService; final ServerConfigService _serverConfigService;
@ -485,6 +473,7 @@ class MarineApiService {
MarineApiService(this._baseService, this._telegramService, this._serverConfigService, this._dbHelper); MarineApiService(this._baseService, this._telegramService, this._serverConfigService, this._dbHelper);
// ... (keep existing MarineApiService methods: sendImageRequestEmail, getManualSamplingImages, getTarballStations, etc.) ...
Future<Map<String, dynamic>> sendImageRequestEmail({ Future<Map<String, dynamic>> sendImageRequestEmail({
required String recipientEmail, required String recipientEmail,
required List<String> imageUrls, required List<String> imageUrls,
@ -508,7 +497,6 @@ class MarineApiService {
); );
} }
// --- START: FIX - Replaced mock with a real API call ---
Future<Map<String, dynamic>> getManualSamplingImages({ Future<Map<String, dynamic>> getManualSamplingImages({
required int stationId, required int stationId,
required DateTime samplingDate, required DateTime samplingDate,
@ -523,9 +511,6 @@ class MarineApiService {
final response = await _baseService.get(baseUrl, endpoint); 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) { if (response['success'] == true && response['data'] is Map && response['data']['data'] is List) {
return { return {
'success': true, 'success': true,
@ -533,11 +518,8 @@ class MarineApiService {
'message': response['message'], 'message': response['message'],
}; };
} }
// Return the original response if the structure isn't as expected, or if it's an error.
return response; return response;
} }
// --- END: FIX ---
Future<Map<String, dynamic>> getTarballStations() async { Future<Map<String, dynamic>> getTarballStations() async {
final baseUrl = await _serverConfigService.getActiveApiUrl(); final baseUrl = await _serverConfigService.getActiveApiUrl();
@ -711,7 +693,6 @@ class MarineApiService {
final limitName = _parameterKeyToLimitName[key]; final limitName = _parameterKeyToLimitName[key];
if (limitName == null) return; if (limitName == null) return;
// START MODIFICATION: Only check for station-specific limits
Map<String, dynamic> limitData = {}; Map<String, dynamic> limitData = {};
if (stationId != null) { if (stationId != null) {
@ -720,7 +701,6 @@ class MarineApiService {
orElse: () => {}, orElse: () => {},
); );
} }
// END MODIFICATION
if (limitData.isNotEmpty) { if (limitData.isNotEmpty) {
final lowerLimit = parseLimitValue(limitData['param_lower_limit']); final lowerLimit = parseLimitValue(limitData['param_lower_limit']);
@ -753,6 +733,7 @@ class MarineApiService {
required Map<String, File?> imageFiles, required Map<String, File?> imageFiles,
required List<Map<String, dynamic>>? appSettings, required List<Map<String, dynamic>>? appSettings,
}) async { }) async {
// ... (existing tarball submission logic) ...
final baseUrl = await _serverConfigService.getActiveApiUrl(); final baseUrl = await _serverConfigService.getActiveApiUrl();
final dataResult = await _baseService.post(baseUrl, 'marine/tarball/sample', formData); final dataResult = await _baseService.post(baseUrl, 'marine/tarball/sample', formData);
if (dataResult['success'] != true) if (dataResult['success'] != true)
@ -789,6 +770,7 @@ class MarineApiService {
Future<void> _handleTarballSuccessAlert( Future<void> _handleTarballSuccessAlert(
Map<String, String> formData, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly}) async { Map<String, String> formData, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly}) async {
// ... (existing tarball alert logic) ...
debugPrint("Triggering Telegram alert logic..."); debugPrint("Triggering Telegram alert logic...");
try { try {
final message = _generateTarballAlertMessage(formData, isDataOnly: isDataOnly); final message = _generateTarballAlertMessage(formData, isDataOnly: isDataOnly);
@ -802,6 +784,7 @@ class MarineApiService {
} }
String _generateTarballAlertMessage(Map<String, String> formData, {required bool isDataOnly}) { String _generateTarballAlertMessage(Map<String, String> formData, {required bool isDataOnly}) {
// ... (existing tarball message generation) ...
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)"; final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
final stationName = formData['tbl_station_name'] ?? 'N/A'; final stationName = formData['tbl_station_name'] ?? 'N/A';
final stationCode = formData['tbl_station_code'] ?? 'N/A'; final stationCode = formData['tbl_station_code'] ?? 'N/A';
@ -831,9 +814,43 @@ class MarineApiService {
return buffer.toString(); return buffer.toString();
} }
// --- START: ADDED NEW METHODS FOR F-MM01, F-MM02, F-MM03 ---
/// Submits the Pre-Departure Checklist (F-MM03)
Future<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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 { class RiverApiService {
// ... (RiverApiService code remains unchanged) ...
final BaseApiService _baseService; final BaseApiService _baseService;
final TelegramService _telegramService; final TelegramService _telegramService;
final ServerConfigService _serverConfigService; final ServerConfigService _serverConfigService;
@ -1172,6 +1189,7 @@ class RiverApiService {
// ======================================================================= // =======================================================================
class DatabaseHelper { class DatabaseHelper {
// ... (DatabaseHelper code remains unchanged) ...
static Database? _database; static Database? _database;
static const String _dbName = 'app_data.db'; static const String _dbName = 'app_data.db';
static const int _dbVersion = 23; static const int _dbVersion = 23;

View File

@ -1,17 +1,74 @@
// lib/services/marine_manual_equipment_maintenance_service.dart
import 'dart:async';
import 'dart:io';
import '../auth_provider.dart'; import '../auth_provider.dart';
import '../models/marine_manual_equipment_maintenance_data.dart'; import '../models/marine_manual_equipment_maintenance_data.dart';
import 'api_service.dart';
import 'base_api_service.dart'; // Import for SessionExpiredException
class MarineManualEquipmentMaintenanceService { class MarineManualEquipmentMaintenanceService {
final ApiService _apiService;
MarineManualEquipmentMaintenanceService(this._apiService);
Future<Map<String, dynamic>> submitMaintenanceReport({ Future<Map<String, dynamic>> submitMaintenanceReport({
required MarineManualEquipmentMaintenanceData data, required MarineManualEquipmentMaintenanceData data,
required AuthProvider authProvider, required AuthProvider authProvider,
}) async { }) async {
// TODO: Implement the full online/offline submission logic here. try {
print("Submitting Equipment Maintenance Report..."); // Call the existing method in MarineApiService
await Future.delayed(const Duration(seconds: 1)); 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 { return {
'success': true, 'success': false,
'message': 'Equipment Maintenance Report submitted (simulation).' '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<Map<String, dynamic>> 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'};
}
}
} }

View File

@ -1,18 +1,52 @@
// lib/services/marine_manual_pre_departure_service.dart
import 'dart:async';
import 'dart:io';
import '../auth_provider.dart'; import '../auth_provider.dart';
import '../models/marine_manual_pre_departure_checklist_data.dart'; import '../models/marine_manual_pre_departure_checklist_data.dart';
import 'api_service.dart';
import 'base_api_service.dart'; // Import for SessionExpiredException
class MarineManualPreDepartureService { class MarineManualPreDepartureService {
final ApiService _apiService;
MarineManualPreDepartureService(this._apiService);
Future<Map<String, dynamic>> submitChecklist({ Future<Map<String, dynamic>> submitChecklist({
required MarineManualPreDepartureChecklistData data, required MarineManualPreDepartureChecklistData data,
required AuthProvider authProvider, required AuthProvider authProvider,
}) async { }) async {
// TODO: Implement the full online/offline submission logic here, try {
// similar to your MarineNpeReportService. // Call the existing method in MarineApiService
print("Submitting Pre-Departure Checklist..."); return await _apiService.marine.submitPreDepartureChecklist(data);
await Future.delayed(const Duration(seconds: 1)); } 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 { return {
'success': true, 'success': false,
'message': 'Pre-Departure Checklist submitted (simulation).' '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'};
}
}
} }

View File

@ -1,14 +1,52 @@
// lib/services/marine_manual_sonde_calibration_service.dart
import 'dart:async';
import 'dart:io';
import '../auth_provider.dart'; import '../auth_provider.dart';
import '../models/marine_manual_sonde_calibration_data.dart'; import '../models/marine_manual_sonde_calibration_data.dart';
import 'api_service.dart';
import 'base_api_service.dart'; // Import for SessionExpiredException
class MarineManualSondeCalibrationService { class MarineManualSondeCalibrationService {
final ApiService _apiService;
MarineManualSondeCalibrationService(this._apiService);
Future<Map<String, dynamic>> submitCalibration({ Future<Map<String, dynamic>> submitCalibration({
required MarineManualSondeCalibrationData data, required MarineManualSondeCalibrationData data,
required AuthProvider authProvider, required AuthProvider authProvider,
}) async { }) async {
// TODO: Implement online/offline submission logic. try {
print("Submitting Sonde Calibration..."); // Call the existing method in MarineApiService
await Future.delayed(const Duration(seconds: 1)); return await _apiService.marine.submitSondeCalibration(data);
return {'success': true, 'message': 'Sonde Calibration submitted (simulation).'}; } 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'};
}
} }
} }