delivery_app/lib/pages/builder_page.dart

316 lines
9.8 KiB
Dart

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<BuilderPage> createState() => _BuilderPageState();
}
class _BuilderPageState extends State<BuilderPage> {
final _db = DatabaseService();
final _geocoding = GeocodingService();
final _streetCtrl = TextEditingController();
final _numCtrl = TextEditingController();
final List<_StreetEntry> _entries = [];
final List<String> _currentNums = [];
final Map<String, Set<String>> _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<void> _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<String, Set<String>> numPapers;
_StreetEntry({required this.street, required this.numPapers});
}