Resolve merge conflicts: README + ignore IDE files
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../app_config.dart';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/address_entry.dart';
|
||||
import '../models/address_record.dart';
|
||||
import '../models/auth_session.dart';
|
||||
@@ -14,68 +15,61 @@ abstract class AddressRepository {
|
||||
Future<List<AddressRecord>> getMyAddresses({required AuthSession session});
|
||||
}
|
||||
|
||||
class HttpAddressRepository implements AddressRepository {
|
||||
const HttpAddressRepository({http.Client? client}) : _client = client;
|
||||
|
||||
final http.Client? _client;
|
||||
class LocalAddressRepository implements AddressRepository {
|
||||
const LocalAddressRepository();
|
||||
static const String _localAddressPrefix = 'local_addresses_';
|
||||
|
||||
@override
|
||||
Future<void> saveAddress({
|
||||
required AuthSession session,
|
||||
required AddressEntry address,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/addresses');
|
||||
|
||||
late final http.Response response;
|
||||
try {
|
||||
response = await (_client ?? http.Client()).post(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer ${session.token}',
|
||||
},
|
||||
body: jsonEncode(address.toJson()),
|
||||
);
|
||||
} catch (_) {
|
||||
throw AddressException(
|
||||
'No se pudo conectar con el backend en ${AppConfig.apiBaseUrl}.',
|
||||
);
|
||||
}
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
final payload = jsonDecode(response.body);
|
||||
final message = payload['message']?.toString() ?? 'Error al guardar la dirección.';
|
||||
throw AddressException(message);
|
||||
}
|
||||
await _persistLocalAddress(session: session, address: address);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AddressRecord>> getMyAddresses({required AuthSession session}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/addresses/me');
|
||||
return _loadLocalAddresses(session);
|
||||
}
|
||||
|
||||
late final http.Response response;
|
||||
try {
|
||||
response = await (_client ?? http.Client()).get(
|
||||
uri,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer ${session.token}',
|
||||
},
|
||||
);
|
||||
} catch (_) {
|
||||
throw AddressException(
|
||||
'No se pudo conectar con el backend en ${AppConfig.apiBaseUrl}.',
|
||||
);
|
||||
Future<void> _persistLocalAddress({required AuthSession session, required AddressEntry address}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = '$_localAddressPrefix${session.email.toLowerCase()}';
|
||||
final existing = await _loadLocalAddresses(session);
|
||||
final records = <AddressRecord>[
|
||||
AddressRecord(
|
||||
id: DateTime.now().millisecondsSinceEpoch,
|
||||
houseNumber: address.houseNumber,
|
||||
colonia: address.colonia,
|
||||
street: address.street,
|
||||
),
|
||||
...existing,
|
||||
];
|
||||
|
||||
final encoded = jsonEncode(
|
||||
records
|
||||
.map(
|
||||
(record) => <String, dynamic>{
|
||||
'id': record.id,
|
||||
'houseNumber': record.houseNumber,
|
||||
'colonia': record.colonia,
|
||||
'street': record.street,
|
||||
},
|
||||
)
|
||||
.toList(growable: false),
|
||||
);
|
||||
await prefs.setString(key, encoded);
|
||||
}
|
||||
|
||||
Future<List<AddressRecord>> _loadLocalAddresses(AuthSession session) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = '$_localAddressPrefix${session.email.toLowerCase()}';
|
||||
final raw = prefs.getString(key);
|
||||
if (raw == null || raw.trim().isEmpty) {
|
||||
return <AddressRecord>[];
|
||||
}
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
final payload = jsonDecode(response.body);
|
||||
final message = payload['message']?.toString() ?? 'Error al obtener las direcciones.';
|
||||
throw AddressException(message);
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(response.body);
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! List) {
|
||||
return <AddressRecord>[];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../app_config.dart';
|
||||
import '../models/auth_session.dart';
|
||||
import 'local_seed_repository.dart';
|
||||
|
||||
abstract class AuthRepository {
|
||||
Future<AuthSession?> restoreSession();
|
||||
@@ -13,18 +12,31 @@ abstract class AuthRepository {
|
||||
Future<void> signOut();
|
||||
}
|
||||
|
||||
class HttpAuthRepository implements AuthRepository {
|
||||
const HttpAuthRepository({http.Client? client}) : _client = client;
|
||||
|
||||
final http.Client? _client;
|
||||
class LocalAuthRepository implements AuthRepository {
|
||||
const LocalAuthRepository();
|
||||
|
||||
static const String _tokenKey = 'auth_token';
|
||||
static const String _emailKey = 'auth_email';
|
||||
static const String _nameKey = 'auth_name';
|
||||
static const String _sessionKey = 'auth_session_json';
|
||||
static const String _usersKey = 'auth_users_json';
|
||||
|
||||
@override
|
||||
Future<AuthSession?> restoreSession() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final sessionJson = prefs.getString(_sessionKey);
|
||||
if (sessionJson != null && sessionJson.trim().isNotEmpty) {
|
||||
final decoded = jsonDecode(sessionJson);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final token = decoded['token']?.toString();
|
||||
final email = decoded['email']?.toString();
|
||||
final name = decoded['displayName']?.toString();
|
||||
if (token != null && email != null && token.isNotEmpty && email.isNotEmpty) {
|
||||
return AuthSession(token: token, email: email, displayName: name ?? email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final token = prefs.getString(_tokenKey);
|
||||
final email = prefs.getString(_emailKey);
|
||||
final name = prefs.getString(_nameKey);
|
||||
@@ -53,44 +65,71 @@ class HttpAuthRepository implements AuthRepository {
|
||||
}
|
||||
|
||||
Future<AuthSession> _authenticate({required String endpoint, required Map<String, dynamic> body}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}$endpoint');
|
||||
final email = body['email']?.toString().trim();
|
||||
final password = body['password']?.toString();
|
||||
if (email == null || password == null || email.isEmpty) {
|
||||
throw AuthException('Ingresa correo y contraseña válidos.');
|
||||
}
|
||||
|
||||
late final http.Response response;
|
||||
try {
|
||||
response = await (_client ?? http.Client()).post(
|
||||
uri,
|
||||
headers: const <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: jsonEncode(body),
|
||||
final seedData = await LocalSeedRepository.instance.load();
|
||||
final profile = seedData.profileForCredentials(email, password);
|
||||
if (profile != null) {
|
||||
final session = AuthSession(
|
||||
token: 'local-${profile.email}',
|
||||
email: profile.email,
|
||||
displayName: profile.name,
|
||||
);
|
||||
} catch (_) {
|
||||
throw AuthException(
|
||||
'No se pudo conectar con el backend en ${AppConfig.apiBaseUrl}. Verifica que el servicio esté activo.',
|
||||
await _persistSession(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
final localUsers = await _loadLocalUsers();
|
||||
final normalizedEmail = email.toLowerCase();
|
||||
final localMatch = localUsers.where((user) {
|
||||
final userEmail = user['email']?.toString().trim().toLowerCase();
|
||||
final userPassword = user['password']?.toString();
|
||||
return userEmail == normalizedEmail && userPassword == password;
|
||||
}).toList(growable: false);
|
||||
|
||||
if (localMatch.isNotEmpty) {
|
||||
final user = localMatch.first;
|
||||
final displayName = user['name']?.toString().trim();
|
||||
final session = AuthSession(
|
||||
token: 'local-${email.toLowerCase()}',
|
||||
email: email,
|
||||
displayName: displayName == null || displayName.isEmpty ? email : displayName,
|
||||
);
|
||||
await _persistSession(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
final Map<String, dynamic> payload = _decodeJson(response.body);
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
final message = payload['message']?.toString() ?? 'Credenciales inválidas o usuario no disponible.';
|
||||
throw AuthException(message);
|
||||
if (endpoint == '/auth/register') {
|
||||
final displayName = body['name']?.toString().trim();
|
||||
final alreadyInSeed = seedData.profileForCredentials(email, password) != null ||
|
||||
seedData.demoProfiles.any((profile) => profile.email.trim().toLowerCase() == normalizedEmail);
|
||||
final alreadyInLocal = localUsers.any((user) => user['email']?.toString().trim().toLowerCase() == normalizedEmail);
|
||||
if (alreadyInSeed || alreadyInLocal) {
|
||||
throw AuthException('Este correo ya está registrado.');
|
||||
}
|
||||
|
||||
final createdUser = <String, dynamic>{
|
||||
'name': displayName == null || displayName.isEmpty ? email : displayName,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'createdAt': DateTime.now().toIso8601String(),
|
||||
};
|
||||
await _saveLocalUsers(<Map<String, dynamic>>[...localUsers, createdUser]);
|
||||
|
||||
final session = AuthSession(
|
||||
token: 'local-$email',
|
||||
email: email,
|
||||
displayName: displayName == null || displayName.isEmpty ? email : displayName,
|
||||
);
|
||||
await _persistSession(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
final token = payload['token']?.toString();
|
||||
if (token == null || token.isEmpty) {
|
||||
throw AuthException('El backend respondió sin token de sesión.');
|
||||
}
|
||||
|
||||
final user = payload['user'] is Map<String, dynamic>
|
||||
? payload['user'] as Map<String, dynamic>
|
||||
: <String, dynamic>{};
|
||||
final emailValue = (user['email'] ?? body['email'])?.toString() ?? body['email'].toString();
|
||||
final displayName = (user['name'] ?? user['fullName'] ?? body['name'] ?? emailValue).toString();
|
||||
|
||||
final session = AuthSession(token: token, email: emailValue, displayName: displayName);
|
||||
await _persistSession(session);
|
||||
return session;
|
||||
throw AuthException('Usuario o contraseña no encontrados en los perfiles locales.');
|
||||
}
|
||||
|
||||
Future<void> _persistSession(AuthSession session) async {
|
||||
@@ -98,19 +137,36 @@ class HttpAuthRepository implements AuthRepository {
|
||||
await prefs.setString(_tokenKey, session.token);
|
||||
await prefs.setString(_emailKey, session.email);
|
||||
await prefs.setString(_nameKey, session.displayName);
|
||||
await prefs.setString(
|
||||
_sessionKey,
|
||||
jsonEncode(
|
||||
<String, dynamic>{
|
||||
'token': session.token,
|
||||
'email': session.email,
|
||||
'displayName': session.displayName,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _decodeJson(String responseBody) {
|
||||
if (responseBody.trim().isEmpty) {
|
||||
return <String, dynamic>{};
|
||||
Future<List<Map<String, dynamic>>> _loadLocalUsers() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_usersKey);
|
||||
if (raw == null || raw.trim().isEmpty) {
|
||||
return <Map<String, dynamic>>[];
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(responseBody);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return decoded;
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! List) {
|
||||
return <Map<String, dynamic>>[];
|
||||
}
|
||||
|
||||
return <String, dynamic>{};
|
||||
return decoded.whereType<Map<String, dynamic>>().toList(growable: false);
|
||||
}
|
||||
|
||||
Future<void> _saveLocalUsers(List<Map<String, dynamic>> users) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_usersKey, jsonEncode(users));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -119,6 +175,7 @@ class HttpAuthRepository implements AuthRepository {
|
||||
await prefs.remove(_tokenKey);
|
||||
await prefs.remove(_emailKey);
|
||||
await prefs.remove(_nameKey);
|
||||
await prefs.remove(_sessionKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
159
lib/services/local_seed_repository.dart
Normal file
159
lib/services/local_seed_repository.dart
Normal file
@@ -0,0 +1,159 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
import '../models/colony_route.dart';
|
||||
import '../models/calendar_event_entry.dart';
|
||||
import '../models/demo_profile.dart';
|
||||
import '../models/route_guide_entry.dart';
|
||||
import '../models/route_notification.dart';
|
||||
import '../models/truck_route.dart';
|
||||
|
||||
class LocalSeedRepository {
|
||||
LocalSeedRepository._();
|
||||
|
||||
static final LocalSeedRepository instance = LocalSeedRepository._();
|
||||
Future<LocalSeedData>? _cachedLoad;
|
||||
|
||||
Future<LocalSeedData> load() {
|
||||
return _cachedLoad ??= _loadInternal();
|
||||
}
|
||||
|
||||
Future<LocalSeedData> _loadInternal() async {
|
||||
try {
|
||||
final routesJson = await rootBundle.loadString('assets/json/rutas.json');
|
||||
final notificationsJson = await rootBundle.loadString('assets/json/notificaciones.json');
|
||||
final colonyRoutesJson = await rootBundle.loadString('assets/json/colonias-rutas.json');
|
||||
final profilesJson = await rootBundle.loadString('assets/json/perfiles.json');
|
||||
final calendarEventsJson = await rootBundle.loadString('assets/json/calendario.json');
|
||||
final routeGuidesJson = await rootBundle.loadString('assets/json/guia-rutas.json');
|
||||
|
||||
final routes = _decodeList(routesJson).map(TruckRoute.fromJson).toList(growable: false);
|
||||
final notifications = _decodeList(notificationsJson).map(RouteNotification.fromJson).toList(growable: false);
|
||||
final colonyRoutes = _decodeList(colonyRoutesJson).map(ColonyRoute.fromJson).toList(growable: false);
|
||||
final profiles = _decodeList(profilesJson).map(DemoProfile.fromJson).toList(growable: false);
|
||||
final calendarEvents = _decodeList(calendarEventsJson).map(CalendarEventEntry.fromJson).toList(growable: false);
|
||||
final routeGuides = _decodeList(routeGuidesJson).map(RouteGuideEntry.fromJson).toList(growable: false);
|
||||
|
||||
return LocalSeedData(
|
||||
routes: routes,
|
||||
notifications: notifications,
|
||||
colonyRoutes: colonyRoutes,
|
||||
demoProfiles: profiles,
|
||||
calendarEvents: calendarEvents,
|
||||
routeGuides: routeGuides,
|
||||
);
|
||||
} catch (_) {
|
||||
return LocalSeedData.empty();
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _decodeList(String responseBody) {
|
||||
final decoded = jsonDecode(responseBody);
|
||||
if (decoded is! List) {
|
||||
return <Map<String, dynamic>>[];
|
||||
}
|
||||
|
||||
return decoded.whereType<Map<String, dynamic>>().toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
class LocalSeedData {
|
||||
const LocalSeedData({
|
||||
required this.routes,
|
||||
required this.notifications,
|
||||
required this.colonyRoutes,
|
||||
required this.demoProfiles,
|
||||
required this.calendarEvents,
|
||||
required this.routeGuides,
|
||||
});
|
||||
|
||||
final List<TruckRoute> routes;
|
||||
final List<RouteNotification> notifications;
|
||||
final List<ColonyRoute> colonyRoutes;
|
||||
final List<DemoProfile> demoProfiles;
|
||||
final List<CalendarEventEntry> calendarEvents;
|
||||
final List<RouteGuideEntry> routeGuides;
|
||||
|
||||
const LocalSeedData.empty()
|
||||
: routes = const <TruckRoute>[],
|
||||
notifications = const <RouteNotification>[],
|
||||
colonyRoutes = const <ColonyRoute>[],
|
||||
demoProfiles = const <DemoProfile>[],
|
||||
calendarEvents = const <CalendarEventEntry>[],
|
||||
routeGuides = const <RouteGuideEntry>[];
|
||||
|
||||
TruckRoute? get defaultRoute => routes.isEmpty ? null : routes.first;
|
||||
|
||||
TruckRoute? routeForColonia(String? colonia) {
|
||||
if (colonia == null || colonia.trim().isEmpty) {
|
||||
return defaultRoute;
|
||||
}
|
||||
|
||||
final routeId = colonyRouteForColonia(colonia)?.routeId;
|
||||
if (routeId == null) {
|
||||
return defaultRoute;
|
||||
}
|
||||
|
||||
return routeById(routeId) ?? defaultRoute;
|
||||
}
|
||||
|
||||
ColonyRoute? colonyRouteForColonia(String colonia) {
|
||||
final normalized = colonia.trim().toLowerCase();
|
||||
for (final item in colonyRoutes) {
|
||||
if (item.colonia.trim().toLowerCase() == normalized) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
TruckRoute? routeById(String routeId) {
|
||||
for (final route in routes) {
|
||||
if (route.routeId == routeId) {
|
||||
return route;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
DemoProfile? profileForCredentials(String email, String password) {
|
||||
final normalizedEmail = email.trim().toLowerCase();
|
||||
for (final profile in demoProfiles) {
|
||||
if (profile.email.trim().toLowerCase() == normalizedEmail && profile.password == password) {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
DemoProfile? profileForColonia(String? colonia) {
|
||||
if (colonia == null || colonia.trim().isEmpty) {
|
||||
return demoProfiles.isEmpty ? null : demoProfiles.first;
|
||||
}
|
||||
|
||||
final normalized = colonia.trim().toLowerCase();
|
||||
for (final profile in demoProfiles) {
|
||||
if (profile.colonia.trim().toLowerCase() == normalized) {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
return demoProfiles.isEmpty ? null : demoProfiles.first;
|
||||
}
|
||||
|
||||
List<CalendarEventEntry> eventsForDay(DateTime day) {
|
||||
final normalized = DateTime(day.year, day.month, day.day);
|
||||
return calendarEvents
|
||||
.where((event) => event.date.year == normalized.year && event.date.month == normalized.month && event.date.day == normalized.day)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
RouteGuideEntry? guideForRouteId(String routeId) {
|
||||
for (final guide in routeGuides) {
|
||||
if (guide.routeId == routeId) {
|
||||
return guide;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user