first commit

This commit is contained in:
2026-08-11 13:16:33 +05:30
parent 18531bf46b
commit c8350563d9
388 changed files with 69656 additions and 0 deletions

348
lib/controllers/auth.dart Normal file
View File

@@ -0,0 +1,348 @@
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:miler/views/helpers/constants/Font_constant.dart';
import 'package:miler/views/helpers/constants/Colorconstants.dart';
import 'package:miler/views/helpers/widgets/app_widgets.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/providers/auth/auth_provider.dart';
import 'package:miler/utils/device.dart';
import 'package:miler/controllers/profile_controller.dart';
import 'package:miler/Models/login/login.dart';
import 'package:miler/data/api_config.dart';
enum AuthNext { verifyPin, otp, notRegistered, error }
class AuthController extends GetxController {
final RxBool sendingOtp = false.obs;
String? currentPhone;
final AuthProvider _api = AuthProvider();
AuthNext? lastDecision;
// Optional callback used by MPIN screen to clear and refocus fields when user taps "Retry"
VoidCallback? onPinRetry;
static const String _prefsUserIdKey = 'userid';
static const String _prefsPendingPinUserIdKey = 'pending_pin_userid';
static const String _prefsUserNameKey = 'user_name';
static const String _prefsUserEmailKey = 'user_email';
static const String _prefsContactNoKey = 'contactno';
static const String _prefsAddressKey = 'user_address';
static const String _prefsForceMasterPinKey = 'force_master_pin';
static const String _masterPinValue = '1234';
static const String forceMasterPinPrefKey = _prefsForceMasterPinKey;
static const String masterPinValue = _masterPinValue;
bool _forceMasterPinFlow = false;
Future<void> _notifyProfileController() async {
try {
if (Get.isRegistered<ProfileController>()) {
final prefs = await SharedPreferences.getInstance();
final pc = Get.find<ProfileController>();
await pc.loadFromPrefs();
pc.setProfile(
name: prefs.getString(_prefsUserNameKey),
email: prefs.getString(_prefsUserEmailKey),
contact: prefs.getString(_prefsContactNoKey),
address: prefs.getString(_prefsAddressKey),
);
}
} catch (_) {}
}
String _normalizePhone(String input) {
final digitsOnly = input.replaceAll(RegExp(r'\D'), '');
if (digitsOnly.length >= 10) {
return digitsOnly.substring(digitsOnly.length - 10);
}
return digitsOnly;
}
void _showBottomSheet({required String title, required String message}) {
Get.bottomSheet(
Builder(
builder: (context) => Container(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16 + MediaQuery.of(context).viewPadding.bottom,
),
decoration: BoxDecoration(
color: ColorConstants.pureSurface,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.info_outline, color: ColorConstants.primary, size: 40),
const SizedBox(height: 12),
Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w700,
fontFamily: FontConstants.fontFamily,
fontSize: 20,
),
),
const SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontFamily: FontConstants.fontFamily,
),
),
const SizedBox(height: 16),
MilerButton(
label: 'Retry',
onPressed: () {
Get.back();
// If MPIN screen has registered a retry callback, run it
onPinRetry?.call();
},
),
],
),
),
),
isScrollControlled: true,
backgroundColor: Colors.transparent,
);
}
Future<AuthNext> precheckPhone(String phone) async {
try {
final normalized = _normalizePhone(phone);
currentPhone = normalized;
final prefs = await SharedPreferences.getInstance();
// The mocked "Demo Rider" (userid 9999) that used to be written here is
// gone. It bypassed the server entirely and left a fake identity in prefs
// that outlived the session it was created for — every screen reading
// 'userid' got 9999 until the app was reinstalled. The real user is
// established by verify-pin and nowhere else.
await prefs.setString(_prefsContactNoKey, normalized);
// On the live backend, ask whether this phone already belongs to an
// active miler account with a PIN on file. If it does, go straight to the
// MPIN screen: OTP delivery isn't live yet, and the OTP path ends at
// Create-MPIN, which would overwrite the PIN the account was issued.
// Seeded development accounts take exactly this branch — enter the phone,
// enter the seeded MPIN, done.
final exists = await _api.milerAccountExists(normalized);
if (exists) {
lastDecision = AuthNext.verifyPin;
return lastDecision!;
}
// Unknown number (or legacy backend) — fall through to the OTP step.
lastDecision = AuthNext.otp;
return lastDecision!;
} catch (e) {
debugPrint('Precheck phone error: $e');
lastDecision = AuthNext.error;
return lastDecision!;
}
}
Future<bool> sendOtp([String? phoneArg]) async {
if (sendingOtp.value) return false;
if (phoneArg != null && phoneArg.isNotEmpty) {
currentPhone = _normalizePhone(phoneArg);
}
sendingOtp.value = true;
try {
await Future.delayed(const Duration(milliseconds: 500));
return true;
} finally {
sendingOtp.value = false;
}
}
Future<bool> verifyOtp(String code) async {
// Automatically succeed for mocked login
return true;
}
Future<bool> setPin(String newPin) async {
try {
final prefs = await SharedPreferences.getInstance();
int? userId =
prefs.getInt(_prefsPendingPinUserIdKey) ??
prefs.getInt(_prefsUserIdKey);
if (newPin.length != 4 || int.tryParse(newPin) == null) {
_showBottomSheet(
title: 'Invalid PIN',
message: 'Please enter a valid 4-digit PIN.',
);
return false;
}
if (userId == null) {
_showBottomSheet(
title: 'Error',
message: 'User ID not found. Please try again.',
);
return false;
}
// Demo mode: the mocked login flow stores a fake rider id (9999) that
// the live server rejects, so updatePin fails. Save the PIN locally and
// report success so the demo Create-MPIN flow works without a real
// account or server call.
if (userId == 9999) {
await prefs.setString('dbPin', newPin);
await prefs.remove(_prefsPendingPinUserIdKey);
return true;
}
final int pinNum = int.parse(newPin);
final res = await _api.updatePin(userId: userId, pin: pinNum);
if (res.statusCode >= 200 && res.statusCode < 300) {
await prefs.setString('dbPin', newPin);
await prefs.remove(_prefsPendingPinUserIdKey);
return true;
}
final bodyPreview = res.body.length > 200
? '${res.body.substring(0, 200)}...'
: res.body;
_showBottomSheet(
title: 'Failed (${res.statusCode})',
message: 'Unable to set PIN. Server said: $bodyPreview',
);
return false;
} catch (e) {
debugPrint('setPin error: $e');
_showBottomSheet(
title: 'Error',
message: 'Something went wrong while setting the PIN.',
);
return false;
}
}
/// Refresh session on backend with latest deviceId/FCM for the current phone.
Future<bool> refreshSession({String? phone}) async {
try {
final prefs = await SharedPreferences.getInstance();
final String? usePhone = phone ?? currentPhone;
if (usePhone == null || usePhone.isEmpty) {
return false;
}
final deviceId = await DeviceUtils.ensureDeviceId(prefs);
final fcmToken = await DeviceUtils.ensureFcmToken(prefs);
final Login loginRes = await _api.loginParsed(
contactNo: usePhone,
deviceType: Platform.operatingSystem,
configId: 6,
deviceId: deviceId,
fcmToken: fcmToken,
);
if (loginRes.userid != null) {
await prefs.setInt(_prefsUserIdKey, loginRes.userid!);
}
try {
final String? name = loginRes.fullname ?? loginRes.firstname;
final String? email = loginRes.email;
final String contact = (loginRes.contactno ?? usePhone).toString();
final String? address = loginRes.address;
if (name != null && name.trim().isNotEmpty) {
await prefs.setString(_prefsUserNameKey, name.trim());
}
if (email != null && email.trim().isNotEmpty) {
await prefs.setString(_prefsUserEmailKey, email.trim());
}
if (contact.isNotEmpty) {
final normalizedContact = _normalizePhone(contact);
await prefs.setString(_prefsContactNoKey, normalizedContact);
}
if (address != null && address.trim().isNotEmpty) {
await prefs.setString(_prefsAddressKey, address.trim());
}
await _notifyProfileController();
} catch (_) {}
currentPhone = _normalizePhone(usePhone);
return loginRes.status == true;
} catch (_) {
return false;
}
}
Future<bool> verifyPinWithServer(String inputPin) async {
final prefs = await SharedPreferences.getInstance();
// The mocked-login bypass that used to sit here accepted ANY four digits
// and logged the rider in without asking the server. It is gone: a PIN
// check that cannot fail is not a PIN check.
// Authenticate phone + PIN against POST /miler/verify-pin. Only a real
// success (server ok + bearer token stored) logs the rider in.
try {
final String phone =
currentPhone ?? prefs.getString(_prefsContactNoKey) ?? '';
final int? pinNum = int.tryParse(inputPin);
if (phone.isEmpty || pinNum == null || inputPin.length != 4) {
_showBottomSheet(
title: 'Invalid PIN',
message: 'Please enter your 4-digit PIN and try again.',
);
return false;
}
final deviceId = await DeviceUtils.ensureDeviceId(prefs);
final fcmToken = await DeviceUtils.ensureFcmToken(prefs);
final Login res = await _api.loginParsed(
contactNo: phone,
deviceType: Platform.operatingSystem,
configId: 6,
deviceId: deviceId,
fcmToken: fcmToken,
pin: pinNum,
pinRaw: inputPin,
);
final String? token = await ApiConfig.getToken();
final bool ok = res.status == true && token != null && token.isNotEmpty;
if (ok) {
await prefs.setString('dbPin', inputPin);
await prefs.setBool('logged_out', false);
currentPhone = _normalizePhone(phone);
// Overwrite any stale demo profile with the REAL logged-in identity.
// The UI reads the display name from 'user_name'; loginParsed only wrote
// 'username'/'firstname', so mirror the real name/email/contact here.
final String realName =
(res.fullname != null && res.fullname!.trim().isNotEmpty)
? res.fullname!.trim()
: (prefs.getString('username') ??
'${res.firstname ?? ''} ${res.lastname ?? ''}')
.trim();
if (realName.isNotEmpty) {
await prefs.setString(_prefsUserNameKey, realName);
}
final String realEmail = (res.email ?? '').trim();
if (realEmail.isNotEmpty) {
await prefs.setString(_prefsUserEmailKey, realEmail);
}
await prefs.setString(_prefsContactNoKey, _normalizePhone(phone));
await _notifyProfileController();
return true;
}
_showBottomSheet(
title: 'Login failed',
message: (res.message != null && res.message!.trim().isNotEmpty)
? res.message!
: 'Incorrect phone number or PIN. Please try again.',
);
return false;
} catch (e) {
debugPrint('verifyPinWithServer error: $e');
_showBottomSheet(
title: 'Connection error',
message: 'Could not reach the server. Check your internet and retry.',
);
return false;
}
}
}

View File

@@ -0,0 +1,55 @@
import 'dart:async';
import 'dart:io';
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
mixin ConnectivityControllerMixin on GetxController {
final RxBool isOnline = true.obs;
Timer? _tick;
@protected
Future<bool> checkInternet() async {
try {
final result = await InternetAddress.lookup('example.com');
return result.isNotEmpty && result.first.rawAddress.isNotEmpty;
} catch (_) {
return false;
}
}
@protected
void onConnectivityOnline() {}
@protected
void onConnectivityOffline() {}
void _startWatcher() {
_tick?.cancel();
_tick = Timer.periodic(const Duration(seconds: 5), (_) async {
final ok = await checkInternet();
final prev = isOnline.value;
if (ok != prev) {
isOnline.value = ok;
if (ok) {
onConnectivityOnline();
} else {
onConnectivityOffline();
}
} else {
isOnline.value = ok;
}
});
}
@override
void onInit() {
super.onInit();
_startWatcher();
}
@override
void onClose() {
_tick?.cancel();
super.onClose();
}
}

View File

@@ -0,0 +1,66 @@
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Single source of truth for the rider's online / on-duty state.
///
/// Duty state used to be re-derived from raw SharedPreferences in a dozen
/// places (homepage field, pickups gate, done screen…), and the `onduty` int is
/// clobbered by a flaky server rider-log sync — which is what auto-kicked riders
/// offline after a pickup. This controller centralises the read with the
/// corrected priority so every screen agrees.
///
/// Priority when resolving "is the rider online?":
/// 1. onduty == 1 → a fresh server shift is authoritative.
/// 2. explicit `online` → the rider's toggle; SURVIVES the flaky onduty sync
/// (every legit offline path — manual toggle, auto
/// shift-end — sets online=false, so it's trustworthy).
/// 3. onduty == 0 → offline.
/// 4. otherwise → default online.
class DutyController extends GetxController {
/// Whether the rider is online (on duty) and may receive bookings / act.
final RxBool isOnline = false.obs;
static const String _kOnduty = 'onduty';
static const String _kOnline = 'online';
/// Convenience accessor — registers the controller on first use.
static DutyController get to => Get.isRegistered<DutyController>()
? Get.find<DutyController>()
: Get.put(DutyController(), permanent: true);
/// True if the rider may receive bookings and start pickups/deliveries.
bool get canWork => isOnline.value;
/// Resolves and broadcasts the current online state from persisted prefs.
Future<bool> load() async {
final prefs = await SharedPreferences.getInstance();
final int onduty = prefs.getInt(_kOnduty) ?? -1;
final bool? savedOnline = prefs.getBool(_kOnline);
bool online;
if (onduty == 1) {
online = true;
} else if (savedOnline != null) {
online = savedOnline;
} else if (onduty == 0) {
online = false;
} else {
online = true;
}
// Keep the persisted bool consistent so the fallback stays correct.
await prefs.setBool(_kOnline, online);
isOnline.value = online;
return online;
}
/// Persists + broadcasts a new online state. Call after a successful duty
/// toggle so every listener updates immediately. Going offline also clears
/// the raw onduty flag so a stale `1` can't resurrect an online state.
Future<void> setOnlineLocal(bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_kOnline, value);
if (!value) await prefs.setInt(_kOnduty, 0);
isOnline.value = value;
}
}

View File

@@ -0,0 +1,181 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/providers/pickuplog/pickuplog_provider.dart';
import 'package:miler/views/helpers/constants/apiconstants.dart';
import 'package:miler/background/foreground_service.dart' as fg;
import 'package:geolocator/geolocator.dart';
/// Controller for managing active pickup logs
/// Now delegates to the background service for actual logging
class LogController extends GetxController {
final CreatePickupLogProvider _logProvider = CreatePickupLogProvider();
static const String _offlineLogKey = 'offline_pickup_logs';
bool _isFlushing = false;
/// Start the pickup log streaming service (via foreground service)
Future<void> startLogging() async {
debugPrint('[ACTIVE_COLLECTION_LOG] Requesting start logging...');
// Attempt to flush offline logs on start
flushOfflineLogs();
// Only show foreground notification when rider is actually on duty
try {
final prefs = await SharedPreferences.getInstance();
final int onduty = prefs.getInt('onduty') ?? 0;
if (onduty != 1) {
debugPrint(
'[ACTIVE_COLLECTION_LOG] Skipping startLogging because onduty=$onduty',
);
return;
}
} catch (_) {
// If prefs fail, continue with best-effort start
}
if (Platform.isAndroid) {
if (await FlutterForegroundTask.isRunningService) {
debugPrint(
'[ACTIVE_COLLECTION_LOG] Foreground service already running',
);
return;
}
debugPrint(
'[ACTIVE_COLLECTION_LOG] Starting foreground service for collection logs',
);
FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'doormile_bg_service',
channelName: 'Background Service',
channelDescription:
'Keeps Miler online updates running in background.',
channelImportance: NotificationChannelImportance.LOW,
priority: NotificationPriority.LOW,
),
iosNotificationOptions: const IOSNotificationOptions(
showNotification: true,
playSound: false,
),
foregroundTaskOptions: ForegroundTaskOptions(
interval: 30000, // 30 seconds
isOnceEvent: false,
autoRunOnBoot: false,
allowWakeLock: true,
allowWifiLock: true,
),
);
// Check permissions before starting service to prevent Android 14 crash
final permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
debugPrint(
'[ACTIVE_COLLECTION_LOG] Location permission missing, skipping service start',
);
return;
}
try {
await FlutterForegroundTask.startService(
notificationTitle: 'Miler is running',
notificationText: 'You are Currently on Duty !',
callback: fg.riderLogCallback,
);
} catch (e) {
debugPrint('[ACTIVE_COLLECTION_LOG] Failed to start service: $e');
}
} else {
debugPrint(
'[ACTIVE_COLLECTION_LOG] iOS/Web not fully supported for background service yet',
);
}
}
/// Stop the pickup log streaming service
/// Note: This might stop rider logs too if they share the service.
/// Usually we only stop if the user goes off-duty or logs out.
void stopLogging() {
debugPrint(
'[ACTIVE_COLLECTION_LOG] Stop logging requested (no-op to preserve rider logs)',
);
// We do not stop the service here because it might be running for Rider Logs.
// The service should be stopped by RiderLogController when going off-duty.
}
// ---------------- Offline Queue Logic (Foreground Helper) ----------------
/// Call this on app start or network restoration
Future<void> flushOfflineLogs() async {
if (_isFlushing) return;
_isFlushing = true;
try {
final prefs = await SharedPreferences.getInstance();
final List<String> queue = prefs.getStringList(_offlineLogKey) ?? [];
if (queue.isEmpty) return;
debugPrint(
'[ACTIVE_COLLECTION_LOG][OFFLINE] Flushing ${queue.length} offline logs...',
);
final List<String> remaining = [];
bool anySuccess = false;
for (final itemStr in queue) {
try {
final Map<String, dynamic> item = jsonDecode(itemStr);
final String orderId = item['orderId'] ?? '';
final Map<String, dynamic> payload = Map<String, dynamic>.from(
item['payload'] ?? {},
);
if (payload.isEmpty) continue;
debugPrint(
'[ACTIVE_COLLECTION_LOG][OFFLINE] Retrying for orderId: $orderId',
);
final result = await _logProvider
.createPickupLog(payload)
.timeout(const Duration(seconds: 8));
if (result != null) {
debugPrint(
'[ACTIVE_COLLECTION_LOG][OFFLINE] Success for orderId: $orderId',
);
anySuccess = true;
} else {
remaining.add(itemStr);
}
} catch (e) {
debugPrint(
'[ACTIVE_COLLECTION_LOG][OFFLINE] Error processing item: $e',
);
remaining.add(itemStr);
}
}
if (anySuccess || remaining.length != queue.length) {
await prefs.setStringList(_offlineLogKey, remaining);
debugPrint(
'[ACTIVE_COLLECTION_LOG][OFFLINE] Flush complete. Remaining: ${remaining.length}',
);
}
} catch (e) {
debugPrint('[ACTIVE_COLLECTION_LOG][OFFLINE] Flush error: $e');
} finally {
_isFlushing = false;
}
}
}

123
lib/controllers/pickup.dart Normal file
View File

@@ -0,0 +1,123 @@
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class PickupController extends GetxController {
// Exposed reactive fields if needed in UI
final RxString orderId = ''.obs;
final RxString orderStatus = ''.obs;
final RxInt tenantId = 0.obs;
final RxInt partnerId = 0.obs;
final RxInt locationId = 0.obs;
final RxInt orderHeaderId = 0.obs;
final RxInt pickupId = 0.obs;
final RxInt userId = 0.obs;
final RxString latitude = ''.obs;
final RxString longitude = ''.obs;
// Preference keys (aligned with existing usage in app)
static const String kTenantId = 'pickup_tenantid';
static const String kPartnerId = 'pickup_partnerid';
static const String kLocationId = 'pickup_locationid';
static const String kOrderHeaderId = 'pickup_orderheaderid';
static const String kPickupId = 'pickup_pickupid';
static const String kUserId = 'pickup_userid';
static const String kOrderId = 'pickup_orderid';
static const String kOrderStatus = 'pickup_orderstatus';
static const String kLat = 'pickup_latitude';
static const String kLng = 'pickup_longitude';
Future<void> saveFromQueueItem(Map<String, dynamic> pickup) async {
final prefs = await SharedPreferences.getInstance();
final int? tenantid = pickup['tenantid'] is int
? pickup['tenantid'] as int
: int.tryParse('${pickup['tenantid'] ?? ''}');
final int? partnerid = pickup['partnerid'] is int
? pickup['partnerid'] as int
: int.tryParse('${pickup['partnerid'] ?? ''}');
final int? locationid = pickup['locationid'] is int
? pickup['locationid'] as int
: int.tryParse('${pickup['locationid'] ?? ''}');
final int? orderheaderid = pickup['orderheaderid'] is int
? pickup['orderheaderid'] as int
: int.tryParse('${pickup['orderheaderid'] ?? ''}');
final int? pickupid = pickup['pickupid'] is int
? pickup['pickupid'] as int
: int.tryParse('${pickup['pickupid'] ?? ''}');
final int? userid = pickup['userid'] is int
? pickup['userid'] as int
: int.tryParse('${pickup['userid'] ?? ''}');
final String oid = (pickup['orderid'] ?? '').toString();
final String ostatus = (pickup['orderstatus'] ?? '').toString();
// Prefer pickuplat/long, fallback to droplat/lon
final String lat = (pickup['pickuplat'] ?? pickup['droplat'] ?? '')
.toString();
final String lon = (pickup['pickuplong'] ?? pickup['droplon'] ?? '')
.toString();
if (tenantid != null) await prefs.setInt(kTenantId, tenantid);
if (partnerid != null) await prefs.setInt(kPartnerId, partnerid);
if (locationid != null) await prefs.setInt(kLocationId, locationid);
if (orderheaderid != null)
await prefs.setInt(kOrderHeaderId, orderheaderid);
if (pickupid != null) await prefs.setInt(kPickupId, pickupid);
if (userid != null) await prefs.setInt(kUserId, userid);
if (oid.isNotEmpty) await prefs.setString(kOrderId, oid);
if (ostatus.isNotEmpty) await prefs.setString(kOrderStatus, ostatus);
if (lat.isNotEmpty) await prefs.setString(kLat, lat);
if (lon.isNotEmpty) await prefs.setString(kLng, lon);
// Update observables
orderId.value = oid;
orderStatus.value = ostatus;
tenantId.value = tenantid ?? 0;
partnerId.value = partnerid ?? 0;
locationId.value = locationid ?? 0;
orderHeaderId.value = orderheaderid ?? 0;
pickupId.value = pickupid ?? 0;
userId.value = userid ?? 0;
latitude.value = lat;
longitude.value = lon;
}
Future<void> loadFromPrefs() async {
final prefs = await SharedPreferences.getInstance();
tenantId.value = prefs.getInt(kTenantId) ?? 0;
partnerId.value = prefs.getInt(kPartnerId) ?? 0;
locationId.value = prefs.getInt(kLocationId) ?? 0;
orderHeaderId.value = prefs.getInt(kOrderHeaderId) ?? 0;
pickupId.value = prefs.getInt(kPickupId) ?? 0;
userId.value = prefs.getInt(kUserId) ?? 0;
orderId.value = prefs.getString(kOrderId) ?? '';
orderStatus.value = prefs.getString(kOrderStatus) ?? '';
latitude.value = prefs.getString(kLat) ?? '';
longitude.value = prefs.getString(kLng) ?? '';
}
Future<void> clear() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(kTenantId);
await prefs.remove(kPartnerId);
await prefs.remove(kLocationId);
await prefs.remove(kOrderHeaderId);
await prefs.remove(kPickupId);
await prefs.remove(kUserId);
await prefs.remove(kOrderId);
await prefs.remove(kOrderStatus);
await prefs.remove(kLat);
await prefs.remove(kLng);
orderId.value = '';
orderStatus.value = '';
tenantId.value = 0;
partnerId.value = 0;
locationId.value = 0;
orderHeaderId.value = 0;
pickupId.value = 0;
userId.value = 0;
latitude.value = '';
longitude.value = '';
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
import 'dart:io';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ProfileController extends GetxController {
final RxString imagePath = ''.obs;
final RxString userName = ''.obs;
final RxString userEmail = ''.obs;
final RxString userContact = ''.obs;
final RxString userAddress = ''.obs;
void setImagePath(String path) {
imagePath.value = path;
}
Future<void> loadFromPrefs() async {
final prefs = await SharedPreferences.getInstance();
// The picked photo's path was being written on every change and never read
// back, so the avatar reverted to the initial on every cold start. It is
// restored here, and dropped if the file has since gone from disk — an
// image-picker path can point into a cache the OS is free to clear.
final String savedImage = (prefs.getString('profile_image_path') ?? '')
.trim();
if (savedImage.isNotEmpty) {
bool stillThere = false;
try {
stillThere = File(savedImage).existsSync();
} catch (_) {}
imagePath.value = stillThere ? savedImage : '';
if (!stillThere) await prefs.remove('profile_image_path');
}
userName.value = (prefs.getString('user_name') ?? '').trim();
userEmail.value = (prefs.getString('user_email') ?? '').trim();
userContact.value = (prefs.getString('contactno') ?? '').trim();
userAddress.value = (prefs.getString('user_address') ?? '').trim();
}
void setProfile({
String? name,
String? email,
String? contact,
String? address,
}) {
if (name != null && name.trim().isNotEmpty) userName.value = name.trim();
if (email != null && email.trim().isNotEmpty)
userEmail.value = email.trim();
if (contact != null && contact.trim().isNotEmpty)
userContact.value = contact.trim();
if (address != null && address.trim().isNotEmpty)
userAddress.value = address.trim();
}
File? get fileOrNull {
final path = imagePath.value;
if (path.isEmpty) return null;
return File(path);
}
}

View File

@@ -0,0 +1,45 @@
import 'package:get/get.dart';
import 'package:miler/data/miler_api.dart';
/// The rider's reward-points balance.
///
/// ── Two things this used to do and no longer does ──
///
/// It called a second, legacy backend (`jupiter.doormile.app/.../
/// getuserbonussummary`) when the new-API flag was off, and it filled an empty
/// balance with seeded demo points in debug builds. Both are gone: there is one
/// backend now, and a points balance is a number about somebody's pay — the
/// only honest answer to "the server said zero" is zero.
///
/// ── Why the number is expected to be zero ──
///
/// `bonuspoints` is not written by anything server-side yet. `total_bonus` on
/// the earnings response is therefore always 0, and that is the backend's
/// answer rather than a bug here. See [MilerApi.missingFromBackend].
class RewardsController extends GetxController {
final RxInt totalPoints = 0.obs;
final RxBool isLoading = true.obs;
final RxString error = ''.obs;
Future<void> fetchBonusSummary(int userId) async {
isLoading.value = true;
error.value = '';
try {
// No dedicated bonus-summary endpoint exists; the figure rides on
// earnings.
final res = await MilerApi.earnings(period: 'monthly');
if (res.ok) {
totalPoints.value =
int.tryParse('${res.map['total_bonus'] ?? 0}') ?? 0;
} else {
error.value = res.message.isEmpty
? "Couldn't load your points"
: res.message;
}
} catch (e) {
error.value = "Couldn't load your points";
} finally {
isLoading.value = false;
}
}
}

View File

@@ -0,0 +1,31 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:miler/Models/summary/riderweeklykms.dart';
import 'package:miler/data/miler_api.dart';
import 'package:miler/views/helpers/constants/apiconstants.dart';
class RiderWeeklyKmController {
final String baseUrl = ApiConstants.summaryriderkmLive;
/// `GET /miler/earnings?period=weekly` → `data:{ total_kms, breakdown }`.
///
/// [userId] is unused: the token identifies the rider, and asking for someone
/// else's kilometres is not a thing the endpoint offers. Kept on the
/// signature because the provider above passes it and the call sites read
/// better for naming whose distance they mean.
Future<Map<String, dynamic>> getRiderWeeklyKms(int userId) async {
final res = await MilerApi.earnings(period: 'weekly');
if (!res.ok) {
throw Exception('Failed to fetch (code: ${res.status})');
}
final breakdown = res.map['breakdown'];
final details = (breakdown is List ? breakdown : const [])
.whereType<Map>()
.map((e) => RiderWeeklyKms.fromJson(Map<String, dynamic>.from(e)))
.toList();
return {
'details': details,
'total_kms': (res.map['total_kms'] ?? 0).toDouble(),
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
import 'package:get/get.dart';
import 'package:miler/Models/summary/pickupstats.dart';
import 'package:miler/providers/summary/summary.dart';
import 'package:flutter/foundation.dart';
class SummaryController extends GetxController {
final SummaryProvider _provider = SummaryProvider();
// Observables
var today = 0.obs;
var week = 0.obs;
var month = 0.obs;
var total = 0.obs;
var cancelled = 0.obs;
var isLoading = false.obs;
// Fetch stats and update values
Future<void> fetchSummaryStats(int userId) async {
try {
isLoading.value = true;
final PickupStats? stats = await _provider.fetchSummaryStats(userId);
// A record of all zeros is what a freshly-seeded account returns, and it
// is indistinguishable from no record at all — treat both as empty so
// the demo figures can fill in.
if (stats != null && (stats.total > 0 || stats.month > 0)) {
today.value = stats.today;
week.value = stats.week;
month.value = stats.month;
total.value = stats.total;
cancelled.value = stats.cancelled;
} else {
_applyZero();
}
} catch (e) {
debugPrint('Controller error: $e');
_applyZero();
} finally {
isLoading.value = false;
}
}
/// Nothing came back — show zeros.
///
/// This used to fill the tab with seeded demo figures in debug builds. That
/// was defensible while the endpoint was unfinished and indefensible now that
/// it is live: the one thing a pay screen must never do is invent a number,
/// and a debug-only invention is still the number the person testing the app
/// reports back as real. An empty Earnings tab is information — it says the
/// fetch returned nothing — and the honest zero is what makes a broken
/// endpoint visible instead of plausible.
void _applyZero() {
today.value = 0;
week.value = 0;
month.value = 0;
total.value = 0;
cancelled.value = 0;
}
}