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: [
IconButton(
icon: const Icon(Icons.person),

View File

@ -140,9 +140,16 @@ void main() async {
Provider(create: (context) => AirSamplingService(databaseHelper, telegramService)),
Provider(create: (context) => MarineTarballSamplingService(telegramService)),
Provider(create: (context) => MarineNpeReportService(Provider.of<TelegramService>(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<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(),
),

View File

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

View File

@ -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<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() {
// 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 {
'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,
};
}
}

View File

@ -13,12 +13,25 @@ class MarineManualPreDepartureChecklistData {
MarineManualPreDepartureChecklistData();
// MODIFIED: This method now builds the nested array structure the PHP controller expects.
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 {
'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
};
}
}

View File

@ -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<String, dynamic> 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,
};

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 = {};
// NEW: State variables for connectivity
// State variables for connectivity
bool _isOnline = true;
late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
final List<String> _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<String, List<String>> _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;
// 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<void> _checkInitialConnectivity() async {
final connectivityResult = await Connectivity().checkConnectivity();
_updateConnectionStatus(connectivityResult);
}
// NEW: Callback method to update UI based on connectivity changes
void _updateConnectionStatus(List<ConnectivityResult> result) {
final bool currentlyOnline = !result.contains(ConnectivityResult.none);
if (_isOnline != currentlyOnline) {
@ -99,23 +119,28 @@ class _MarineManualPreDepartureChecklistScreenState
}
Future<void> _submit() async {
// No form validation needed as Project/Month fields removed
setState(() => _isLoading = true);
try {
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.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<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) {
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'),
),
),
),

View File

@ -22,19 +22,22 @@ class _MarineManualSondeCalibrationScreenState
final _data = MarineManualSondeCalibrationData();
bool _isLoading = false;
// State for connectivity
bool _isOnline = true;
late StreamSubscription<List<ConnectivityResult>> _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<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 {
if (!_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
@ -82,14 +109,20 @@ class _MarineManualSondeCalibrationScreenState
try {
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.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<String> 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: [
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Row(
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()),
child: TextFormField(
decoration: const InputDecoration(
labelText: 'MV Reading', border: OutlineInputBorder()),
keyboardType: TextInputType.number,
onSaved: (val) => onSave(label, 'Initial', double.tryParse(val ?? '')),
onSaved: (val) => onSaveMv(double.tryParse(val ?? '')),
)),
const SizedBox(width: 8),
Expanded(child: TextFormField(
decoration: const InputDecoration(labelText: 'Calibrated', border: OutlineInputBorder()),
Expanded(
child: TextFormField(
decoration: const InputDecoration(
labelText: 'Before Cal', border: OutlineInputBorder()),
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),
const SizedBox(height: 16),
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) {
return DropdownMenuItem<String>(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,
),
],

View File

@ -748,7 +748,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
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(

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/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<Map<String, dynamic>> 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<void> 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<Map<String, dynamic>> _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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> sendImageRequestEmail({
required String recipientEmail,
required List<String> imageUrls,
@ -508,7 +497,6 @@ class MarineApiService {
);
}
// --- START: FIX - Replaced mock with a real API call ---
Future<Map<String, dynamic>> 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<Map<String, dynamic>> 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<String, dynamic> 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<String, File?> imageFiles,
required List<Map<String, dynamic>>? 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<void> _handleTarballSuccessAlert(
Map<String, String> formData, List<Map<String, dynamic>>? 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<String, String> 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<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 {
// ... (RiverApiService code remains unchanged) ...
final BaseApiService _baseService;
final TelegramService _telegramService;
final ServerConfigService _serverConfigService;
@ -1059,7 +1076,7 @@ class RiverApiService {
}
Future<String> _generateInSituAlertMessage(Map<String, String> 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;

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 '../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<Map<String, dynamic>> 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));
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': true,
'message': 'Equipment Maintenance Report submitted (simulation).'
'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<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 '../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<Map<String, dynamic>> 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));
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': true,
'message': 'Pre-Departure Checklist submitted (simulation).'
'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'};
}
}
}

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 '../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<Map<String, dynamic>> 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'};
}
}
}