import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:mileograph_flutter/services/data_service.dart'; enum _SpeedUnit { kph, mph } class NewTractionPage extends StatefulWidget { const NewTractionPage({super.key}); @override State createState() => _NewTractionPageState(); } class _NewTractionPageState extends State { final _formKey = GlobalKey(); late final Map _controllers; bool _preserved = false; bool _remoteControl = false; bool _cabAirConditioning = false; bool _cabDoorControl = false; bool _submitting = false; _SpeedUnit _speedUnit = _SpeedUnit.kph; String _status = 'unknown'; String _domain = 'unknown'; String _type = 'O'; static const _typeOptions = ['D', 'E', 'U', 'S', 'DMU', 'EMU', 'SMU', 'O']; static const _domainOptions = [ 'mainline', 'heritage', 'industrial', 'museum', 'private', 'unknown', ]; static const _statusOptions = [ 'active', 'stored', 'overhaul', 'withdrawn', 'preserved', 'scrapped', 'unknown', ]; @override void initState() { super.initState(); _controllers = { 'number': TextEditingController(), 'evn': TextEditingController(), 'name': TextEditingController(), 'class': TextEditingController(), 'operator': TextEditingController(), 'notes': TextEditingController(), 'livery': TextEditingController(), 'location': TextEditingController(), 'owner': TextEditingController(), 'power_unit': TextEditingController(), 'headlights': TextEditingController(), 'pantograph': TextEditingController(), 'misc': TextEditingController(), 'coupling': TextEditingController(), 'axle_arrangement': TextEditingController(), 'track_gauge': TextEditingController(), 'loco_braking': TextEditingController(), 'train_braking': TextEditingController(), 'max_speed': TextEditingController(), 'buffer_type': TextEditingController(), 'drawgear_strength': TextEditingController(), 'train_heating': TextEditingController(), 'route_restriction': TextEditingController(), 'safety_systems': TextEditingController(), 'width': TextEditingController(), 'height': TextEditingController(), 'length': TextEditingController(), 'weight': TextEditingController(), 'power': TextEditingController(), 'tractive_effort': TextEditingController(), 'electrical_voltage': TextEditingController(), 'traction_motors': TextEditingController(), 'build_date': TextEditingController(), }; } @override void dispose() { for (final controller in _controllers.values) { controller.dispose(); } super.dispose(); } String _value(String key, {String fallback = ''}) { final text = _controllers[key]?.text.trim() ?? ''; if (text.isEmpty) return fallback; return text; } String? _textOrNull(String key) { final text = _controllers[key]?.text.trim() ?? ''; if (text.isEmpty) return null; return text; } num? _parseNumber(String key) { final raw = _controllers[key]?.text.trim(); if (raw == null || raw.isEmpty) return null; final parsed = double.tryParse(raw); if (parsed == null) return null; return parsed % 1 == 0 ? parsed.toInt() : parsed; } bool get _statusIsActive => _status.toLowerCase() == 'active'; String? _validateBuildDate(String? input) { final value = (input ?? '').trim(); if (value.isEmpty) return null; final regex = RegExp( r'^(\d{2})(\d{2}|[Xx]{2})-((0[1-9]|1[0-2])|[Xx]{2})-((0[1-9]|[12]\d|3[01])|[Xx]{2})$', ); final match = regex.firstMatch(value); if (match == null) { return 'Use YYYY-MM-DD; allow XX for unknown DD/YY'; } final year = match.group(1)! + match.group(2)!; final monthPart = match.group(3)!; final dayPart = match.group(4)!; final monthUnknown = monthPart.toLowerCase() == 'xx'; final dayUnknown = dayPart.toLowerCase() == 'xx'; if (monthUnknown && !dayUnknown) { return 'If month is XX, day must be XX'; } // Validate actual calendar date when fully specified and year is numeric. final yearHasUnknown = year.toLowerCase().contains('x'); if (!monthUnknown && !dayUnknown && !yearHasUnknown) { final month = int.parse(monthPart); final day = int.parse(dayPart); final yearInt = int.parse(year); try { final dt = DateTime(yearInt, month, day); if (dt.year != yearInt || dt.month != month || dt.day != day) { return 'Enter a valid calendar date'; } } catch (_) { return 'Enter a valid calendar date'; } } return null; } double? _maxSpeedInKph() { final raw = _controllers['max_speed']?.text.trim(); if (raw == null || raw.isEmpty) return null; final parsed = double.tryParse(raw); if (parsed == null) return null; if (_speedUnit == _SpeedUnit.kph) return parsed; return parsed * 1.60934; } Map _buildPayload() { final isActive = _statusIsActive; final payload = { 'number': _value('number'), 'class': _value('class'), 'type': _type, 'status': _status, 'operational': isActive, 'gettable': isActive, 'preserved': _preserved, 'remote_control': _remoteControl, 'cab_air_conditioning': _cabAirConditioning, 'cab_door_control': _cabDoorControl, }; void addIfPresent(String key, dynamic value) { if (value == null) return; if (value is String && value.trim().isEmpty) return; payload[key] = value; } addIfPresent('evn', _textOrNull('evn')); addIfPresent('name', _textOrNull('name')); addIfPresent('operator', _textOrNull('operator')); addIfPresent('notes', _textOrNull('notes')); addIfPresent('domain', _domain); addIfPresent('livery', _textOrNull('livery')); addIfPresent('owner', _textOrNull('owner')); addIfPresent('location', _textOrNull('location')); addIfPresent('power_unit', _textOrNull('power_unit')); addIfPresent('headlights', _textOrNull('headlights')); addIfPresent('pantograph', _textOrNull('pantograph')); addIfPresent('misc', _textOrNull('misc')); addIfPresent('coupling', _textOrNull('coupling')); addIfPresent('axle_arrangement', _textOrNull('axle_arrangement')); addIfPresent('track_gauge', _parseNumber('track_gauge')); addIfPresent('loco_braking', _textOrNull('loco_braking')); addIfPresent('train_braking', _textOrNull('train_braking')); addIfPresent('max_speed', _maxSpeedInKph()); addIfPresent('buffer_type', _textOrNull('buffer_type')); addIfPresent('drawgear_strength', _textOrNull('drawgear_strength')); addIfPresent('train_heating', _textOrNull('train_heating')); addIfPresent('route_restriction', _textOrNull('route_restriction')); addIfPresent('safety_systems', _textOrNull('safety_systems')); addIfPresent('width', _parseNumber('width')); addIfPresent('height', _parseNumber('height')); addIfPresent('length', _parseNumber('length')); addIfPresent('weight', _parseNumber('weight')); addIfPresent('power', _parseNumber('power')); addIfPresent('tractive_effort', _parseNumber('tractive_effort')); addIfPresent('electrical_voltage', _textOrNull('electrical_voltage')); addIfPresent('traction_motors', _textOrNull('traction_motors')); addIfPresent('build_date', _textOrNull('build_date')); return payload; } Future _handleSubmit() async { final form = _formKey.currentState; if (form == null) return; if (!form.validate()) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Please fill the required fields.')), ); return; } FocusScope.of(context).unfocus(); setState(() => _submitting = true); final messenger = ScaffoldMessenger.of(context); try { await context.read().createLoco(_buildPayload()); if (!mounted) return; messenger.showSnackBar( const SnackBar(content: Text('Traction added successfully')), ); Navigator.of(context).pop(_controllers['class']?.text.trim()); } catch (e) { if (!mounted) return; messenger.showSnackBar( SnackBar(content: Text('Failed to add traction: $e')), ); setState(() => _submitting = false); } } @override Widget build(BuildContext context) { final isActive = _statusIsActive; final size = MediaQuery.of(context).size; final isNarrow = size.width < 720; final fieldWidth = isNarrow ? double.infinity : 340.0; Widget textField( String key, String label, { bool required = false, int maxLines = 1, String? helper, String? suffixText, TextInputType? keyboardType, double? widthOverride, String? Function(String?)? validator, }) { return SizedBox( width: widthOverride ?? fieldWidth, child: TextFormField( controller: _controllers[key], decoration: InputDecoration( labelText: required ? '$label *' : label, helperText: helper, suffixText: suffixText, border: const OutlineInputBorder(), ), keyboardType: keyboardType, maxLines: maxLines, validator: (val) { if (required && (val == null || val.trim().isEmpty)) { return 'Required'; } return validator?.call(val); }, ), ); } Widget numberField(String key, String label, {String? suffixText}) { return textField( key, label, keyboardType: const TextInputType.numberWithOptions(decimal: true), suffixText: suffixText, ); } Widget dropdownField({ required String label, required List options, required String value, required ValueChanged onChanged, bool required = false, double? widthOverride, }) { return SizedBox( width: widthOverride ?? fieldWidth, child: DropdownButtonFormField( value: value, decoration: InputDecoration( labelText: required ? '$label *' : label, border: const OutlineInputBorder(), ), items: options .map((opt) => DropdownMenuItem(value: opt, child: Text(opt))) .toList(), onChanged: (val) { if (val != null) onChanged(val); }, validator: required ? (val) => val == null || val.isEmpty ? 'Required' : null : null, ), ); } return SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ TextButton.icon( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.arrow_back), label: const Text('Back'), ), ], ), const SizedBox(height: 12), Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Basics', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Wrap( spacing: 12, runSpacing: 12, children: [ textField('number', 'Number', required: true), textField('class', 'Class', required: true), dropdownField( label: 'Type', options: _typeOptions, value: _type, required: true, onChanged: (val) => setState(() => _type = val), ), dropdownField( label: 'Status', options: _statusOptions, value: _status, required: true, onChanged: (val) => setState(() => _status = val), ), textField('name', 'Name'), textField('operator', 'Operator'), textField('evn', 'EVN'), dropdownField( label: 'Domain', options: _domainOptions, value: _domain, onChanged: (val) => setState(() => _domain = val), ), textField('livery', 'Livery'), textField('owner', 'Owner'), textField('location', 'Location'), ], ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ Chip( avatar: Icon( isActive ? Icons.check_circle : Icons.block, color: isActive ? Colors.green : Colors.grey, ), label: Text('Gettable: ${isActive ? 'Yes' : 'No'}'), ), Chip( avatar: Icon( isActive ? Icons.check_circle : Icons.block, color: isActive ? Colors.green : Colors.grey, ), label: Text( 'Operational: ${isActive ? 'Yes' : 'No'}', ), ), ], ), const SizedBox(height: 4), Text( 'Status controls availability: active sets gettable and operational to true, everything else sets them to false.', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 16), Text( 'Equipment & Capabilities', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Wrap( spacing: 12, runSpacing: 12, children: [ SizedBox( width: fieldWidth, child: SwitchListTile( contentPadding: EdgeInsets.zero, value: _preserved, onChanged: (v) => setState(() => _preserved = v), title: const Text('Preserved'), ), ), SizedBox( width: fieldWidth, child: SwitchListTile( contentPadding: EdgeInsets.zero, value: _remoteControl, onChanged: (v) => setState(() => _remoteControl = v), title: const Text('Remote control'), ), ), SizedBox( width: fieldWidth, child: SwitchListTile( contentPadding: EdgeInsets.zero, value: _cabAirConditioning, onChanged: (v) => setState(() => _cabAirConditioning = v), title: const Text('Cab air conditioning'), ), ), SizedBox( width: fieldWidth, child: SwitchListTile( contentPadding: EdgeInsets.zero, value: _cabDoorControl, onChanged: (v) => setState(() => _cabDoorControl = v), title: const Text('Cab door control'), ), ), ], ), const SizedBox(height: 8), Wrap( spacing: 12, runSpacing: 12, children: [ textField('power_unit', 'Power unit'), textField('headlights', 'Headlights'), textField('pantograph', 'Pantograph'), textField('misc', 'Misc'), textField('coupling', 'Coupling'), textField('axle_arrangement', 'Axle arrangement'), textField('loco_braking', 'Loco braking'), textField('train_braking', 'Train braking'), textField('buffer_type', 'Buffer type'), textField('drawgear_strength', 'Drawgear strength'), textField('train_heating', 'Train heating'), textField('route_restriction', 'Route restriction'), textField('safety_systems', 'Safety systems'), ], ), const SizedBox(height: 16), Text( 'Dimensions & Performance', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Wrap( spacing: 12, runSpacing: 12, children: [ numberField('weight', 'Weight', suffixText: 'tonnes'), numberField('length', 'Length', suffixText: 'mm'), numberField('width', 'Width', suffixText: 'mm'), numberField('height', 'Height', suffixText: 'mm'), numberField( 'track_gauge', 'Track gauge', suffixText: 'mm', ), numberField('power', 'Power', suffixText: 'kW'), numberField( 'tractive_effort', 'Tractive effort', suffixText: 'kN', ), SizedBox( width: fieldWidth, child: Row( children: [ Expanded( child: numberField( 'max_speed', 'Max speed (${_speedUnit == _SpeedUnit.kph ? 'km/h' : 'mph'})', ), ), const SizedBox(width: 8), SizedBox( width: 120, child: DropdownButtonFormField<_SpeedUnit>( value: _speedUnit, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Units', ), items: const [ DropdownMenuItem( value: _SpeedUnit.kph, child: Text('km/h'), ), DropdownMenuItem( value: _SpeedUnit.mph, child: Text('mph'), ), ], onChanged: (val) { if (val != null) { setState(() => _speedUnit = val); } }, ), ), ], ), ), ], ), const SizedBox(height: 16), Text( 'Electrical & Build', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Wrap( spacing: 12, runSpacing: 12, children: [ textField('electrical_voltage', 'Electrical voltage'), textField('traction_motors', 'Traction motors'), textField( 'build_date', 'Build date', helper: 'Format YYYY-MM-DD, use XX for unknown DD/YY', validator: _validateBuildDate, ), ], ), const SizedBox(height: 16), textField( 'notes', 'Notes', maxLines: 3, helper: 'Optional notes', widthOverride: double.infinity, ), const SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _submitting ? null : _handleSubmit, icon: _submitting ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, ), ) : const Icon(Icons.save), label: Text(_submitting ? 'Submitting...' : 'Submit'), ), ), ], ), ), ), ), ], ), ), ); } }