fix marine maintenance form
This commit is contained in:
parent
de4c0c471c
commit
a11c0d8df8
@ -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),
|
||||
|
||||
@ -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(),
|
||||
),
|
||||
|
||||
1
lib/models/marine_inves_manual_sampling_data.dart
Normal file
1
lib/models/marine_inves_manual_sampling_data.dart
Normal file
@ -0,0 +1 @@
|
||||
// lib/models/marine_inves_manual_sampling_data.dart
|
||||
@ -1,23 +1,140 @@
|
||||
class MarineManualEquipmentMaintenanceData {
|
||||
int? performedByUserId;
|
||||
String? equipmentName;
|
||||
String? maintenanceDate;
|
||||
String? maintenanceType;
|
||||
String? workDescription;
|
||||
String? partsReplaced;
|
||||
String? status;
|
||||
String? remarks;
|
||||
import 'dart:convert';
|
||||
|
||||
class MarineManualEquipmentMaintenanceData {
|
||||
int? conductedByUserId;
|
||||
String? maintenanceDate;
|
||||
String? lastMaintenanceDate;
|
||||
String? scheduleMaintenance;
|
||||
bool isReplacement = false;
|
||||
|
||||
String? timeStart;
|
||||
String? timeEnd;
|
||||
String? location;
|
||||
|
||||
// Part 1 - YSI
|
||||
Map<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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
//lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_1_sampling_info.dart
|
||||
@ -0,0 +1 @@
|
||||
//lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart
|
||||
@ -0,0 +1 @@
|
||||
//lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_3_data_capture.dart
|
||||
@ -0,0 +1 @@
|
||||
//lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_4_summary.dart
|
||||
@ -0,0 +1 @@
|
||||
//lib/screens/marine/investigative/marine_investigative_manual_sampling.dart
|
||||
File diff suppressed because it is too large
Load Diff
@ -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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
@ -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'};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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'};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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'};
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user