import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; import '../models/stop.dart'; import '../services/database.dart'; import '../services/geocoding.dart'; import '../hoogerheide_streets.dart'; class BuilderPage extends StatefulWidget { final int routeId; final VoidCallback onSave; const BuilderPage({super.key, required this.routeId, required this.onSave}); @override State createState() => _BuilderPageState(); } class _BuilderPageState extends State { final _db = DatabaseService(); final _geocoding = GeocodingService(); final _streetCtrl = TextEditingController(); final _numCtrl = TextEditingController(); final List<_StreetEntry> _entries = []; final List _currentNums = []; final Map> _currentPapers = {}; bool _saving = false; static const _availablePapers = ['BN', 'AD', 'TEL', 'VK']; static const _npColors = { 'BN': Colors.red, 'AD': Colors.blue, 'TEL': Colors.orange, 'VK': Colors.purple, }; @override void dispose() { _streetCtrl.dispose(); _numCtrl.dispose(); super.dispose(); } void _addHouseNumber() { final n = _numCtrl.text.trim(); if (n.isNotEmpty && !_currentNums.contains(n)) { setState(() { _currentNums.add(n); _currentPapers[n] = {'BN'}; // default newspaper }); } _numCtrl.clear(); } void _togglePaper(String num, String paper) { setState(() { final current = _currentPapers[num] ?? {'BN'}; if (current.contains(paper)) { current.remove(paper); } else { current.add(paper); } _currentPapers[num] = current; }); } void _saveStreet() { if (_currentNums.isEmpty || _streetCtrl.text.trim().isEmpty) return; setState(() { _entries.add(_StreetEntry( street: _streetCtrl.text.trim(), numPapers: Map.from(_currentPapers), )); _currentNums.clear(); _currentPapers.clear(); _streetCtrl.clear(); }); } void _removeEntry(_StreetEntry entry) { setState(() => _entries.remove(entry)); } Future _saveAll() async { if (_entries.isEmpty) return; setState(() => _saving = true); try { var seq = (await _db.getMaxSequence(widget.routeId) ?? -1) + 1; for (final entry in _entries) { for (final npEntry in entry.numPapers.entries) { final houseNum = npEntry.key; final papers = npEntry.value.isEmpty ? {'BN'} : npEntry.value; // Geocode: try GeocodingService first, fallback to hoogerheide_streets.dart LatLng coords; try { coords = await _geocoding.geocode(entry.street, houseNum); } catch (_) { coords = _geocodeFromStreets(entry.street, houseNum); } await _db.insertStop( Stop( street: entry.street, houseNumber: houseNum, newspapers: papers.toList(), lat: coords.latitude, lng: coords.longitude, sequence: seq++, ), widget.routeId, ); } } widget.onSave(); if (mounted) Navigator.pop(context); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error saving: $e'), backgroundColor: Colors.red), ); } } finally { if (mounted) setState(() => _saving = false); } } /// Fallback geocode using hoogerheide_streets.dart with house-number offset LatLng _geocodeFromStreets(String street, String houseNum) { final ns = _normalize(street); // Exact match for (final entry in HoogerheideStreets.streetCoords.entries) { if (_normalize(entry.key) == ns) { return _offsetByHouseNumber(entry.value[0], entry.value[1], houseNum); } } // Partial match for (final entry in HoogerheideStreets.streetCoords.entries) { final ek = _normalize(entry.key); if (ns.contains(ek) || ek.contains(ns)) { return _offsetByHouseNumber(entry.value[0], entry.value[1], houseNum); } } return const LatLng(51.4243390, 4.3238380); } LatLng _offsetByHouseNumber(double baseLat, double baseLng, String houseNum) { final hn = int.tryParse(houseNum.replaceAll(RegExp(r'[A-Za-z]'), '')) ?? 1; return LatLng( baseLat + (hn - 1) * 0.00015, baseLng + ((hn / 25).floor() % 2 == 0 ? 0 : 0.0002), ); } String _normalize(String name) => name.replaceAll(RegExp(r'[–—]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim().toLowerCase(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Add Stops'), actions: [ if (_saving) const Padding( padding: EdgeInsets.all(16), child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ), ) else IconButton( icon: const Icon(Icons.check), onPressed: _entries.isEmpty ? null : _saveAll, tooltip: 'Save all stops', ), ], ), body: ListView( padding: const EdgeInsets.all(16), children: [ // Input card Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: _streetCtrl, decoration: const InputDecoration( labelText: 'Street name', border: OutlineInputBorder(), ), textCapitalization: TextCapitalization.words, ), const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: _numCtrl, decoration: const InputDecoration( labelText: 'House number', border: OutlineInputBorder(), ), onSubmitted: (_) => _addHouseNumber(), ), ), const SizedBox(width: 8), IconButton.filled( onPressed: _addHouseNumber, icon: const Icon(Icons.add), ), ], ), if (_currentNums.isNotEmpty) ...[ const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: _currentNums.map(_buildHouseNumberChip).toList(), ), ], const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _currentNums.isEmpty ? null : _saveStreet, icon: const Icon(Icons.add_road), label: const Text('Add Street'), ), ), ], ), ), ), // Saved entries if (_entries.isNotEmpty) ...[ const SizedBox(height: 16), const Text('Stops to add:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), const SizedBox(height: 8), ..._entries.map((entry) => Card( child: ListTile( leading: const CircleAvatar(child: Icon(Icons.location_on)), title: Text(entry.street), subtitle: Text( entry.numPapers.entries.map((e) => '${e.key}: ${e.value.join(",")}').join('; '), ), trailing: IconButton( icon: const Icon(Icons.delete_outline, color: Colors.red), onPressed: () => _removeEntry(entry), ), ), )), ], ], ), ); } Widget _buildHouseNumberChip(String num) { final current = _currentPapers[num] ?? {'BN'}; return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Text(num, style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(width: 4), GestureDetector( onTap: () => setState(() { _currentNums.remove(num); _currentPapers.remove(num); }), child: const Icon(Icons.close, size: 16), ), ], ), const SizedBox(height: 4), Wrap( spacing: 4, children: _availablePapers.map((np) => FilterChip( label: Text(np, style: const TextStyle(fontSize: 11)), selected: current.contains(np), selectedColor: _npColors[np], onSelected: (_) => _togglePaper(num, np), visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, )).toList(), ), ], ), ); } } class _StreetEntry { final String street; final Map> numPapers; _StreetEntry({required this.street, required this.numPapers}); }