initial commit: push everything
This commit is contained in:
995
lib/views/Dashboard/Cart/cartpage.dart
Normal file
995
lib/views/Dashboard/Cart/cartpage.dart
Normal file
@@ -0,0 +1,995 @@
|
||||
part of '../deliveries/deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// CART PAGE (Active Deliveries)
|
||||
// -------------------------------------------------------------------------
|
||||
class Cartpage extends StatefulWidget {
|
||||
const Cartpage({super.key});
|
||||
|
||||
@override
|
||||
State<Cartpage> createState() => _CartpageState();
|
||||
}
|
||||
|
||||
class _CartpageState extends State<Cartpage> with AutomaticKeepAliveClientMixin {
|
||||
final DeliveryProvider _provider = DeliveryProvider();
|
||||
final CreateDeliveryLogProvider _deliveryLogProvider = CreateDeliveryLogProvider();
|
||||
|
||||
List<Map<String, dynamic>> _activeDeliveries = <Map<String, dynamic>>[];
|
||||
StreamSubscription<void>? _pollerSubscription;
|
||||
bool _fetching = false;
|
||||
final Map<String, Timer> _deliveryTimers = <String, Timer>{};
|
||||
final Map<String, Map<String, dynamic>> _deliveryBasePayload = <String, Map<String, dynamic>>{};
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchActive();
|
||||
_startPolling();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollerSubscription?.cancel();
|
||||
_stopAllTimers();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startPolling() {
|
||||
_pollerSubscription?.cancel();
|
||||
_pollerSubscription = Stream.periodic(
|
||||
const Duration(seconds: 10),
|
||||
(_) {},
|
||||
).asyncMap((_) async {
|
||||
if (!_fetching && mounted) {
|
||||
await _fetchActive();
|
||||
}
|
||||
}).listen(
|
||||
(_) {},
|
||||
onError: (error) {
|
||||
debugPrint('[CART][STREAM ERROR] $error');
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _fetchActive() async {
|
||||
if (_fetching) return;
|
||||
|
||||
_fetching = true;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userId = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
|
||||
|
||||
if (userId == 0) {
|
||||
debugPrint('[CART] No user ID found');
|
||||
_fetching = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current date in YYYY-MM-DD format
|
||||
final now = DateTime.now();
|
||||
final today = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||
|
||||
// Hardcoded API endpoint for cart page only: v2/deliveries/getdeliveries
|
||||
final bool isLive = ApiConstants.mainRoute == 'live';
|
||||
final baseUrl = isLive
|
||||
? 'https://jupiter.nearle.app/live/api/v2/deliveries/getdeliveries'
|
||||
: 'https://jupiter.nearle.app/dev/api/v2/deliveries/getdeliveries';
|
||||
|
||||
final uri = Uri.parse(baseUrl).replace(queryParameters: {
|
||||
'userid': userId.toString(),
|
||||
'fromdate': today,
|
||||
'todate': today,
|
||||
't': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
});
|
||||
|
||||
debugPrint('[CART] Fetching from: $uri');
|
||||
|
||||
// Fetch deliveries from API directly using http
|
||||
// Hardcoded endpoint for cart page: v2/deliveries/getdeliveries
|
||||
final httpClient = http.Client();
|
||||
List<dynamic> items = [];
|
||||
try {
|
||||
final response = await httpClient.get(uri);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final decoded = json.decode(response.body);
|
||||
final data = decoded is Map<String, dynamic>
|
||||
? (decoded['details'] ?? decoded['data'] ?? decoded)
|
||||
: decoded;
|
||||
|
||||
items = data is List
|
||||
? data
|
||||
: (data is Map && data['items'] is List ? data['items'] as List : []);
|
||||
} else {
|
||||
debugPrint('[CART] API error: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[CART] Error fetching from API: $e');
|
||||
} finally {
|
||||
httpClient.close();
|
||||
}
|
||||
|
||||
debugPrint('[CART] Raw API items count: ${items.length}');
|
||||
|
||||
// Debug: Print all order statuses to see what we're getting
|
||||
for (final item in items) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
final orderId = (item['orderid'] ?? '').toString();
|
||||
final status = (item['orderstatus'] ?? '').toString();
|
||||
debugPrint('[CART] Order $orderId has status: "$status" (raw: ${item['orderstatus']})');
|
||||
}
|
||||
}
|
||||
|
||||
// Filter for ACTIVE status only
|
||||
final activeOrders = items
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.where((order) {
|
||||
final status = (order['orderstatus']?.toString().toLowerCase() ?? '').trim();
|
||||
final isActive = status == 'active';
|
||||
if (isActive) {
|
||||
debugPrint('[CART] ✅ Found active order: ${order['orderid']}');
|
||||
} else {
|
||||
debugPrint('[CART] ❌ Order ${order['orderid']} has status: "$status" (not active)');
|
||||
}
|
||||
return isActive;
|
||||
})
|
||||
.toList();
|
||||
|
||||
debugPrint('[CART] Found ${activeOrders.length} active deliveries out of ${items.length} total');
|
||||
|
||||
// Sort by order ID or step number if available
|
||||
activeOrders.sort((a, b) {
|
||||
final stepA = (a['step'] ?? a['Step'] ?? 0).toString();
|
||||
final stepB = (b['step'] ?? b['Step'] ?? 0).toString();
|
||||
final stepAInt = int.tryParse(stepA) ?? 0;
|
||||
final stepBInt = int.tryParse(stepB) ?? 0;
|
||||
if (stepAInt != stepBInt) return stepAInt.compareTo(stepBInt);
|
||||
|
||||
final orderIdA = (a['orderid'] ?? '').toString();
|
||||
final orderIdB = (b['orderid'] ?? '').toString();
|
||||
return orderIdA.compareTo(orderIdB);
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_activeDeliveries = activeOrders;
|
||||
});
|
||||
}
|
||||
|
||||
// Get all active order IDs
|
||||
final activeOrderIds = activeOrders
|
||||
.map((o) => (o['orderid'] ?? '').toString())
|
||||
.where((id) => id.isNotEmpty)
|
||||
.toSet();
|
||||
|
||||
// ✅ CRITICAL: Start/restart timers for ALL active deliveries
|
||||
// This ensures every active delivery posts logs every 30 seconds
|
||||
for (final order in activeOrders) {
|
||||
final orderId = (order['orderid'] ?? '').toString();
|
||||
if (orderId.isEmpty) {
|
||||
debugPrint('[CART] ⚠️ Skipping order with empty orderId');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always restart timer to ensure it's running (handles edge cases)
|
||||
if (_deliveryTimers.containsKey(orderId)) {
|
||||
debugPrint('[CART] 🔄 Restarting timer for active delivery: $orderId');
|
||||
_deliveryTimers[orderId]?.cancel();
|
||||
_deliveryTimers.remove(orderId);
|
||||
// Also clear base payload to force reload
|
||||
_deliveryBasePayload.remove(orderId);
|
||||
}
|
||||
|
||||
debugPrint('[CART] ▶️ Starting timer for active delivery: $orderId');
|
||||
await _startDeliveryPosting(order);
|
||||
debugPrint('[CART] ✅ Timer started successfully for: $orderId');
|
||||
}
|
||||
|
||||
// ✅ Stop timers for deliveries that are no longer active
|
||||
final timersToStop = _deliveryTimers.keys
|
||||
.where((id) => !activeOrderIds.contains(id))
|
||||
.toList();
|
||||
|
||||
for (final id in timersToStop) {
|
||||
debugPrint('[CART] Stopping timer for orderId: $id (no longer active)');
|
||||
_stopDeliveryPosting(id);
|
||||
}
|
||||
|
||||
debugPrint('[CART] Active timers: ${_deliveryTimers.keys.toList()}');
|
||||
} catch (e) {
|
||||
debugPrint('[CART] Error fetching active deliveries: $e');
|
||||
} finally {
|
||||
_fetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startDeliveryPosting(Map<String, dynamic> order) async {
|
||||
final orderId = (order['orderid'] ?? '').toString();
|
||||
if (orderId.isEmpty) {
|
||||
debugPrint('[CART][DELIVERYLOG] ⚠️ Cannot start timer: empty orderId');
|
||||
return;
|
||||
}
|
||||
|
||||
// Safety check: If timer already exists, cancel it first (shouldn't happen after cleanup above)
|
||||
if (_deliveryTimers.containsKey(orderId)) {
|
||||
debugPrint('[CART][DELIVERYLOG] ⚠️ Timer already exists for $orderId, canceling old one');
|
||||
_deliveryTimers[orderId]?.cancel();
|
||||
_deliveryTimers.remove(orderId);
|
||||
}
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG] 🚀 Starting 30-second timer for orderId: $orderId');
|
||||
|
||||
// Get starttime from SharedPreferences (saved when order became active via updateActiveStatus)
|
||||
// If not found, use activetime from order data, or current time as fallback
|
||||
String startTime = '';
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final deliveryId = (order['deliveryid'] ?? 0).toString();
|
||||
|
||||
// Method 1: Get from SharedPreferences (saved when order became active)
|
||||
startTime = prefs.getString('delivery_starttime_$deliveryId') ?? '';
|
||||
if (startTime.isNotEmpty) {
|
||||
debugPrint('[CART][DELIVERYLOG] ✅ Loaded starttime from SharedPreferences: $startTime');
|
||||
}
|
||||
|
||||
// Method 2: Fallback - try to get from order data (starttime field)
|
||||
if (startTime.isEmpty) {
|
||||
startTime = (order['starttime'] ?? order['startTime'] ?? '').toString();
|
||||
if (startTime.isNotEmpty) {
|
||||
debugPrint('[CART][DELIVERYLOG] ✅ Loaded starttime from order data: $startTime');
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Fallback - try activetime from order data
|
||||
if (startTime.isEmpty) {
|
||||
final activetime = (order['activetime'] ?? order['activTime'] ?? '').toString();
|
||||
if (activetime.isNotEmpty) {
|
||||
startTime = activetime;
|
||||
debugPrint('[CART][DELIVERYLOG] ✅ Loaded starttime from activetime: $startTime');
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: Last fallback - current time (shouldn't happen if updateActiveStatus was called)
|
||||
if (startTime.isEmpty) {
|
||||
final now = DateTime.now();
|
||||
startTime = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
||||
debugPrint('[CART][DELIVERYLOG] ⚠️ Using current time as starttime fallback: $startTime');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[CART][DELIVERYLOG] ❌ Error getting starttime: $e');
|
||||
// Set a fallback starttime even on error
|
||||
final now = DateTime.now();
|
||||
startTime = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
// CRITICAL: Ensure starttime is never empty
|
||||
if (startTime.isEmpty) {
|
||||
final now = DateTime.now();
|
||||
startTime = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
||||
debugPrint('[CART][DELIVERYLOG] ⚠️ Final fallback: starttime was empty, using: $startTime');
|
||||
}
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG] 📝 Final starttime for orderId $orderId: $startTime');
|
||||
|
||||
// Create base payload with starttime
|
||||
final base = <String, dynamic>{
|
||||
'logid': 0,
|
||||
'tenantid': order['tenantid'] ?? 0,
|
||||
'partnerid': order['partnerid'] ?? 0,
|
||||
'locationid': order['locationid'] ?? 0,
|
||||
'orderheaderid': order['orderheaderid'] ?? 0,
|
||||
'deliveryid': order['deliveryid'] ?? 0,
|
||||
'userid': order['userid'] ?? 0,
|
||||
'orderid': orderId,
|
||||
'orderstatus': 'active',
|
||||
'starttime': startTime, // Include starttime in base payload
|
||||
};
|
||||
|
||||
_deliveryBasePayload[orderId] = base;
|
||||
|
||||
// Save to SharedPreferences for persistence
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('deliverylog_${orderId}_tenantid', (base['tenantid'] ?? 0).toString());
|
||||
await prefs.setString('deliverylog_${orderId}_partnerid', (base['partnerid'] ?? 0).toString());
|
||||
await prefs.setString('deliverylog_${orderId}_locationid', (base['locationid'] ?? 0).toString());
|
||||
await prefs.setString('deliverylog_${orderId}_orderheaderid', (base['orderheaderid'] ?? 0).toString());
|
||||
await prefs.setString('deliverylog_${orderId}_deliveryid', (base['deliveryid'] ?? 0).toString());
|
||||
await prefs.setString('deliverylog_${orderId}_userid', (base['userid'] ?? 0).toString());
|
||||
await prefs.setString('deliverylog_${orderId}_orderid', orderId);
|
||||
await prefs.setString('deliverylog_${orderId}_orderstatus', 'active');
|
||||
await prefs.setString('deliverylog_${orderId}_starttime', startTime); // Save starttime
|
||||
} catch (e) {
|
||||
debugPrint('[CART][DELIVERYLOG] Error saving payload: $e');
|
||||
}
|
||||
|
||||
// Post once immediately (don't await - let it run in background)
|
||||
_postDeliveryLog(orderId, order);
|
||||
debugPrint('[CART][DELIVERYLOG] 📤 Posted initial log for orderId: $orderId');
|
||||
|
||||
// Then every 30 seconds - CRITICAL: This ensures logs are posted every 30 seconds
|
||||
final timer = Timer.periodic(const Duration(seconds: 30), (t) {
|
||||
debugPrint('[CART][DELIVERYLOG] ⏰ Timer tick for orderId: $orderId (30 seconds elapsed)');
|
||||
_postDeliveryLog(orderId, order);
|
||||
});
|
||||
|
||||
_deliveryTimers[orderId] = timer;
|
||||
debugPrint('[CART][DELIVERYLOG] ✅ Timer registered for orderId: $orderId (will post every 30 seconds)');
|
||||
}
|
||||
|
||||
void _postDeliveryLog(String orderId, Map<String, dynamic> order) {
|
||||
if (!mounted) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Widget disposed, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ⏰ Posting log for orderId: $orderId at ${DateTime.now()}');
|
||||
|
||||
// Use Future.microtask to ensure the async operation runs independently
|
||||
Future.microtask(() => _performPost(orderId));
|
||||
}
|
||||
|
||||
Future<void> _performPost(String orderId) async {
|
||||
try {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] 🔄 Starting _performPost for orderId: $orderId');
|
||||
|
||||
Map<String, dynamic>? base = _deliveryBasePayload[orderId];
|
||||
|
||||
if (base == null) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Base is null, loading from SharedPreferences');
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (!mounted) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Widget unmounted after prefs load');
|
||||
return;
|
||||
}
|
||||
|
||||
base = {
|
||||
'logid': 0,
|
||||
'tenantid': int.tryParse(prefs.getString('deliverylog_${orderId}_tenantid') ?? '0') ?? 0,
|
||||
'partnerid': int.tryParse(prefs.getString('deliverylog_${orderId}_partnerid') ?? '0') ?? 0,
|
||||
'locationid': int.tryParse(prefs.getString('deliverylog_${orderId}_locationid') ?? '0') ?? 0,
|
||||
'orderheaderid': int.tryParse(prefs.getString('deliverylog_${orderId}_orderheaderid') ?? '0') ?? 0,
|
||||
'deliveryid': int.tryParse(prefs.getString('deliverylog_${orderId}_deliveryid') ?? '0') ?? 0,
|
||||
'userid': int.tryParse(prefs.getString('deliverylog_${orderId}_userid') ?? '0') ?? 0,
|
||||
'orderid': prefs.getString('deliverylog_${orderId}_orderid') ?? orderId,
|
||||
'orderstatus': prefs.getString('deliverylog_${orderId}_orderstatus') ?? 'active',
|
||||
'starttime': prefs.getString('deliverylog_${orderId}_starttime') ?? '', // Load starttime
|
||||
};
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Base loaded from prefs: $base');
|
||||
} catch (e) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Error loading base: $e');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, base is guaranteed to be non-null (either from cache or created above)
|
||||
final basePayload = base; // Flow analysis ensures base is non-null here
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Getting coordinates...');
|
||||
|
||||
// CRITICAL: Get coordinates with retry logic - NEVER post with null or '0' coordinates
|
||||
final coords = await _getValidCoordinates().timeout(
|
||||
const Duration(seconds: 10), // Increased timeout to allow retries
|
||||
onTimeout: () {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ❌ Coordinate timeout after retries');
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
if (!mounted) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Widget unmounted after coords');
|
||||
return;
|
||||
}
|
||||
|
||||
// CRITICAL: Validate coordinates - NEVER post with null, '0', or invalid coordinates
|
||||
if (coords == null || coords.$1.isEmpty || coords.$2.isEmpty ||
|
||||
coords.$1 == '0' || coords.$2 == '0') {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ❌ SKIPPING POST: Invalid coordinates (lat=${coords?.$1 ?? 'null'}, lng=${coords?.$2 ?? 'null'})');
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ Will retry on next timer tick (30 seconds)');
|
||||
return; // Skip this post - don't send invalid coordinates
|
||||
}
|
||||
|
||||
// Validate coordinate ranges
|
||||
final latDouble = double.tryParse(coords.$1) ?? 0.0;
|
||||
final lngDouble = double.tryParse(coords.$2) ?? 0.0;
|
||||
if (latDouble == 0 || lngDouble == 0 ||
|
||||
latDouble.abs() > 90 || lngDouble.abs() > 180) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ❌ SKIPPING POST: Invalid coordinate ranges (lat=$latDouble, lng=$lngDouble)');
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ Will retry on next timer tick (30 seconds)');
|
||||
return; // Skip this post - don't send invalid coordinates
|
||||
}
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ✅ Valid coordinates: lat=${coords.$1}, lng=${coords.$2}');
|
||||
|
||||
// Cumulative KM is tracked exclusively by LiveTrackingService (high-frequency, every 3s).
|
||||
// Do not accumulate here to avoid race conditions with concurrent writers.
|
||||
|
||||
final now = DateTime.now();
|
||||
final logdate = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
||||
|
||||
// CRITICAL: Ensure starttime is always included in payload
|
||||
final starttimeValue = basePayload['starttime']?.toString() ?? '';
|
||||
if (starttimeValue.isEmpty) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ WARNING: starttime is empty in basePayload, using fallback');
|
||||
}
|
||||
|
||||
// CRITICAL: Use validated coordinates - guaranteed to be non-null and valid at this point
|
||||
final payload = {
|
||||
...basePayload,
|
||||
'logdate': logdate,
|
||||
'latitude': coords.$1, // Guaranteed non-null and valid
|
||||
'longitude': coords.$2, // Guaranteed non-null and valid
|
||||
'starttime': starttimeValue.isNotEmpty ? starttimeValue : '', // CRITICAL: Always include starttime
|
||||
};
|
||||
|
||||
// Validate payload has all required fields
|
||||
final requiredFields = ['tenantid', 'partnerid', 'locationid', 'orderheaderid', 'deliveryid', 'userid', 'orderid', 'orderstatus', 'starttime'];
|
||||
final missingFields = requiredFields.where((field) => payload[field] == null || payload[field] == '').toList();
|
||||
if (missingFields.isNotEmpty) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ WARNING: Missing fields in payload: $missingFields');
|
||||
}
|
||||
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createDeliveryLogLive
|
||||
: ApiConstants.createDeliveryLogDev;
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG][POST] 📤 Sending to API: $url');
|
||||
debugPrint('[CART][DELIVERYLOG][POST] 📦 Payload: $payload');
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ✅ starttime in payload: "${payload['starttime']}"');
|
||||
|
||||
await _deliveryLogProvider
|
||||
.createDeliveryLog(url, payload)
|
||||
.timeout(
|
||||
const Duration(seconds: 8),
|
||||
onTimeout: () {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ API timeout for orderId: $orderId');
|
||||
throw TimeoutException('API timeout', const Duration(seconds: 8));
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ✅ SUCCESS for orderId: $orderId at ${DateTime.now()}');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ❌ ERROR for orderId: $orderId - $e');
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Stack trace: $stackTrace');
|
||||
}
|
||||
}
|
||||
|
||||
Future<(String lat, String lng)?> _getValidCoordinates({int retryCount = 0}) async {
|
||||
const maxRetries = 3;
|
||||
|
||||
try {
|
||||
final bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
debugPrint('[CART][COORDS] Location service disabled, trying last known position');
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && lastPos.latitude != 0 && lastPos.longitude != 0) {
|
||||
final lat = lastPos.latitude.toString();
|
||||
final lng = lastPos.longitude.toString();
|
||||
debugPrint('[CART][COORDS] ✅ Using last known position: $lat, $lng');
|
||||
return (lat, lng);
|
||||
}
|
||||
// Retry if we haven't exceeded max retries
|
||||
if (retryCount < maxRetries) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return _getValidCoordinates(retryCount: retryCount + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever ||
|
||||
permission == LocationPermission.denied) {
|
||||
debugPrint('[CART][COORDS] Permission denied, trying last known position');
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && lastPos.latitude != 0 && lastPos.longitude != 0) {
|
||||
final lat = lastPos.latitude.toString();
|
||||
final lng = lastPos.longitude.toString();
|
||||
debugPrint('[CART][COORDS] ✅ Using last known position: $lat, $lng');
|
||||
return (lat, lng);
|
||||
}
|
||||
// Retry if we haven't exceeded max retries
|
||||
if (retryCount < maxRetries) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return _getValidCoordinates(retryCount: retryCount + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Position? position;
|
||||
try {
|
||||
// Try to get current position with higher accuracy
|
||||
position = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high, // Changed to high for better accuracy
|
||||
timeLimit: Duration(seconds: 8), // Increased timeout
|
||||
),
|
||||
).timeout(const Duration(seconds: 8));
|
||||
} catch (e) {
|
||||
debugPrint('[CART][COORDS] Timeout getting current position: $e, trying last known');
|
||||
position = await Geolocator.getLastKnownPosition();
|
||||
}
|
||||
|
||||
if (position != null && position.latitude != 0 && position.longitude != 0) {
|
||||
final lat = position.latitude.toString();
|
||||
final lng = position.longitude.toString();
|
||||
|
||||
// Validate coordinates are within valid GPS ranges
|
||||
final latDouble = double.tryParse(lat) ?? 0.0;
|
||||
final lngDouble = double.tryParse(lng) ?? 0.0;
|
||||
if (latDouble.abs() <= 90 && lngDouble.abs() <= 180) {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('last_lat', lat);
|
||||
await prefs.setString('last_lng', lng);
|
||||
} catch (_) {}
|
||||
debugPrint('[CART][COORDS] ✅ Got valid coordinates: $lat, $lng');
|
||||
return (lat, lng);
|
||||
} else {
|
||||
debugPrint('[CART][COORDS] ⚠️ Invalid coordinate ranges: $lat, $lng');
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to SharedPreferences cached coordinates
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final lat = (prefs.getString('last_lat') ?? '').trim();
|
||||
final lng = (prefs.getString('last_lng') ?? '').trim();
|
||||
if (lat.isNotEmpty && lng.isNotEmpty && lat != '0' && lng != '0') {
|
||||
final latDouble = double.tryParse(lat) ?? 0.0;
|
||||
final lngDouble = double.tryParse(lng) ?? 0.0;
|
||||
if (latDouble != 0 && lngDouble != 0 && latDouble.abs() <= 90 && lngDouble.abs() <= 180) {
|
||||
debugPrint('[CART][COORDS] ✅ Using cached coordinates: $lat, $lng');
|
||||
return (lat, lng);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Retry if we haven't exceeded max retries
|
||||
if (retryCount < maxRetries) {
|
||||
debugPrint('[CART][COORDS] ⚠️ Retry ${retryCount + 1}/$maxRetries to get coordinates');
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return _getValidCoordinates(retryCount: retryCount + 1);
|
||||
}
|
||||
|
||||
debugPrint('[CART][COORDS] ❌ Failed to get valid coordinates after $maxRetries retries');
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('[CART][COORDS] ❌ Error getting coordinates: $e');
|
||||
// Retry if we haven't exceeded max retries
|
||||
if (retryCount < maxRetries) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return _getValidCoordinates(retryCount: retryCount + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _stopDeliveryPosting(String orderId) {
|
||||
_deliveryTimers[orderId]?.cancel();
|
||||
_deliveryTimers.remove(orderId);
|
||||
_deliveryBasePayload.remove(orderId);
|
||||
debugPrint('[CART][DELIVERYLOG] Stopped timer for orderId: $orderId');
|
||||
}
|
||||
|
||||
void _stopAllTimers() {
|
||||
for (final timer in _deliveryTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_deliveryTimers.clear();
|
||||
_deliveryBasePayload.clear();
|
||||
debugPrint('[CART][DELIVERYLOG] Stopped all timers');
|
||||
}
|
||||
|
||||
// Method to stop delivery posting for a specific order (called when order is completed)
|
||||
void stopDeliveryPostingForOrder(String orderId) {
|
||||
_stopDeliveryPosting(orderId);
|
||||
// Refresh the list to remove completed orders
|
||||
if (mounted) {
|
||||
_fetchActive();
|
||||
}
|
||||
}
|
||||
|
||||
double _parseD(dynamic v) {
|
||||
if (v == null) return 0.0;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
|
||||
const double R = 6371.0;
|
||||
final double dLat = _toRadians(lat2 - lat1);
|
||||
final double dLon = _toRadians(lon2 - lon1);
|
||||
final double a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||
math.cos(_toRadians(lat1)) *
|
||||
math.cos(_toRadians(lat2)) *
|
||||
math.sin(dLon / 2) *
|
||||
math.sin(dLon / 2);
|
||||
final double c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
|
||||
double _toRadians(double degrees) {
|
||||
return degrees * math.pi / 180.0;
|
||||
}
|
||||
|
||||
String _distanceKmDisplay(Map<String, dynamic> m) {
|
||||
// Try API distance first
|
||||
final String apiKmsStr = (m['actualkms'] ?? m['distance'] ?? '0').toString();
|
||||
final double apiKms = double.tryParse(apiKmsStr) ?? 0.0;
|
||||
if (apiKms > 0) {
|
||||
return apiKms < 10 ? apiKmsStr : apiKms.toStringAsFixed(0);
|
||||
}
|
||||
|
||||
// Try rider location to delivery location
|
||||
final double rLat = _parseD(m['riderslat']);
|
||||
final double rLon = _parseD(m['riderslon']);
|
||||
final double dLat = _parseD(m['droplat'] ?? m['deliverylat']);
|
||||
final double dLon = _parseD(m['droplon'] ?? m['deliverylong']);
|
||||
|
||||
if (rLat != 0 && rLon != 0 && dLat != 0 && dLon != 0) {
|
||||
final double km = _haversineKm(rLat, rLon, dLat, dLon);
|
||||
return km.toStringAsFixed(km < 10 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Fallback: pickup to delivery
|
||||
final double pLat = _parseD(m['pickuplat']);
|
||||
final double pLon = _parseD(m['pickuplon']);
|
||||
if (pLat != 0 && pLon != 0 && dLat != 0 && dLon != 0) {
|
||||
final double km = _haversineKm(pLat, pLon, dLat, dLon);
|
||||
return km.toStringAsFixed(km < 10 ? 1 : 0);
|
||||
}
|
||||
|
||||
return '0';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
toolbarHeight: 70,
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(
|
||||
"Active Deliveries",
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
bottom: const PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Divider(height: 1, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: _activeDeliveries.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 80),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -11),
|
||||
child: Image.asset(
|
||||
"assets/images/Nearle Bike.png",
|
||||
errorBuilder: (c, e, s) => const Icon(
|
||||
Icons.delivery_dining,
|
||||
size: 120,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -11),
|
||||
child: Text(
|
||||
"No Active Deliveries",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: _fetchActive,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
itemCount: _activeDeliveries.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = _activeDeliveries[index];
|
||||
final customerName = (item['deliverycustomer'] ?? item['customer'] ?? 'Customer').toString();
|
||||
final address = (item['deliveryaddress'] ?? item['address'] ?? 'Address not available').toString();
|
||||
final storeName = (item['pickupcustomer'] ?? item['store'] ?? 'Store').toString();
|
||||
final orderId = (item['orderid'] ?? '').toString();
|
||||
final distance = _distanceKmDisplay(item);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// -------------------------------------------
|
||||
// TOP CUSTOMER DETAILS
|
||||
// -------------------------------------------
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.orange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 2,
|
||||
height: 30,
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -3),
|
||||
child: Text(
|
||||
customerName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, 8),
|
||||
child: Text(
|
||||
address,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.black87,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Distance: $distance km',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.blueGrey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
// Just launch dialer; PiP is handled only from navigation screen
|
||||
final phone =
|
||||
(item['deliverycontactno'] ?? '').toString();
|
||||
final bool success = await launchPhoneDialer(
|
||||
phone.isNotEmpty ? phone : '9876543210',
|
||||
);
|
||||
if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Could not launch dialer'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/phone-call .png',
|
||||
height: 27,
|
||||
width: 27,
|
||||
errorBuilder: (c, e, s) =>
|
||||
const Icon(Icons.phone, size: 27, color: Colors.green),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
// -------------------------------------------
|
||||
// STORE DETAILS
|
||||
// -------------------------------------------
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/shoppingbag.png',
|
||||
height: 32,
|
||||
width: 32,
|
||||
errorBuilder: (c, e, s) => const Icon(
|
||||
Icons.shopping_bag,
|
||||
size: 32,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
storeName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
_showMyOptionsSheet(
|
||||
context,
|
||||
item,
|
||||
null, // No parent state for cart page
|
||||
);
|
||||
// Refresh cart page after skip (order will no longer be active)
|
||||
if (mounted) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
_fetchActive();
|
||||
}
|
||||
},
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, -5),
|
||||
child: Text(
|
||||
'Skip>>',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"Order ID: #$orderId",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
// -------------------------------------------
|
||||
// SLIDER BUTTON
|
||||
// -------------------------------------------
|
||||
SliderButton(
|
||||
properties: SliderButtonProperties(
|
||||
height: 50,
|
||||
buttonSize: 45,
|
||||
width: MediaQuery.of(context).size.width - 56,
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
dismissThresholds: 0.90,
|
||||
action: () async {
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
if (!context.mounted) return false;
|
||||
|
||||
// Navigate to delivery map screen (same as deliveries page)
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _DeliveryMapScreen(
|
||||
delivery: item,
|
||||
parentState: null, // No parent state for cart page
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Refresh cart page when returning from map screen
|
||||
// (in case delivery was completed/cancelled)
|
||||
if (mounted) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
_fetchActive();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
label: const Text(
|
||||
'Slide to start Delivery',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
icon: ClipOval(
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
child: SizedBox(
|
||||
width: 45,
|
||||
height: 45,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
329
lib/views/Dashboard/deliveries/card.dart
Normal file
329
lib/views/Dashboard/deliveries/card.dart
Normal file
@@ -0,0 +1,329 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DELIVERY CARD
|
||||
// -------------------------------------------------------------------------
|
||||
class DeliveryCard extends StatelessWidget {
|
||||
final Map<String, dynamic> item;
|
||||
final int displayStep;
|
||||
final String distanceStr;
|
||||
final bool enabled;
|
||||
final bool isSkipped;
|
||||
|
||||
const DeliveryCard({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.displayStep,
|
||||
required this.distanceStr,
|
||||
this.enabled = true,
|
||||
this.isSkipped = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String customerName = (item['deliverycustomer'] ?? 'Customer')
|
||||
.toString();
|
||||
final String address = (item['deliveryaddress'] ?? 'Address not available')
|
||||
.toString();
|
||||
final String tenantName = (item['tenantname'] ?? 'Store').toString();
|
||||
final String orderId = (item['orderid'] ?? '').toString();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: isSkipped ? Border.all(color: Colors.orange, width: 2) : null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isSkipped)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'SKIPPED',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange.shade900,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isSkipped) const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.orange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 2,
|
||||
height: 30,
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -3),
|
||||
child: Text(
|
||||
customerName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 17.5,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, 8),
|
||||
child: Text(
|
||||
address,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.black87,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Distance: $distanceStr km',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
color: Colors.blueGrey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
// Just launch dialer; PiP is handled only from navigation screen
|
||||
final phone = (item['deliverycontactno'] ?? '').toString();
|
||||
final bool success = await launchPhoneDialer(
|
||||
phone.isNotEmpty ? phone : '9876543210',
|
||||
);
|
||||
if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Could not launch dialer'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/phone-call .png',
|
||||
height: 27,
|
||||
width: 27,
|
||||
errorBuilder: (c, e, s) =>
|
||||
const Icon(Icons.phone, size: 27, color: Colors.green),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/shoppingbag.png',
|
||||
height: 32,
|
||||
width: 32,
|
||||
errorBuilder: (c, e, s) => const Icon(
|
||||
Icons.shopping_bag,
|
||||
size: 32,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
tenantName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
if (!isSkipped)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
final parentState = context
|
||||
.findAncestorStateOfType<
|
||||
_MyDeliveriesState
|
||||
>();
|
||||
if (parentState != null) {
|
||||
_showMyOptionsSheet(
|
||||
context,
|
||||
item,
|
||||
parentState,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, -5),
|
||||
child: Text(
|
||||
'Skip>>',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'Order ID: #$orderId',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
SliderButton(
|
||||
properties: SliderButtonProperties(
|
||||
height: 50,
|
||||
buttonSize: 45,
|
||||
width: MediaQuery.of(context).size.width - 56,
|
||||
backgroundColor: enabled
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey.shade400,
|
||||
dismissThresholds: 0.90,
|
||||
action: enabled
|
||||
? () async {
|
||||
// Reduce delay to make it feel snappier
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
if (!context.mounted) return false;
|
||||
final parentState = context
|
||||
.findAncestorStateOfType<_MyDeliveriesState>();
|
||||
|
||||
// ✅ BLOCK: Check if there's already an active delivery (and this isn't it)
|
||||
if (parentState != null) {
|
||||
final currentOrderId = (item['orderid'] ?? '')
|
||||
.toString();
|
||||
final activeOrderIds = parentState._activeDeliveries
|
||||
.map((d) => (d['orderid'] ?? '').toString())
|
||||
.where((id) => id.isNotEmpty)
|
||||
.toSet();
|
||||
|
||||
// Block if there's a different active delivery: just ignore the swipe
|
||||
if (parentState._activeDeliveries.isNotEmpty &&
|
||||
!activeOrderIds.contains(currentOrderId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to delivery map screen
|
||||
if (context.mounted) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _DeliveryMapScreen(
|
||||
delivery: item,
|
||||
parentState: parentState,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
: () async => null,
|
||||
label: Transform.translate(
|
||||
offset: const Offset(-0.5, 0),
|
||||
child: Text(
|
||||
isSkipped
|
||||
? 'Slide to resume Delivery'
|
||||
: (enabled
|
||||
? 'Slide to start Delivery'
|
||||
: 'Complete previous delivery'),
|
||||
style: const TextStyle(
|
||||
fontSize: 18.5,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
icon: ClipOval(
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
child: SizedBox(
|
||||
width: 45,
|
||||
height: 45,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$displayStep',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: enabled
|
||||
? const ui.Color.fromARGB(255, 0, 0, 0)
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1762
lib/views/Dashboard/deliveries/deliveries.dart
Normal file
1762
lib/views/Dashboard/deliveries/deliveries.dart
Normal file
File diff suppressed because it is too large
Load Diff
278
lib/views/Dashboard/deliveries/done.dart
Normal file
278
lib/views/Dashboard/deliveries/done.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DELIVERIES DONE SCREEN
|
||||
// -------------------------------------------------------------------------
|
||||
class DeliveriesDone extends StatefulWidget {
|
||||
final bool isCancelled;
|
||||
final int bonusPoints;
|
||||
|
||||
const DeliveriesDone({
|
||||
super.key,
|
||||
this.isCancelled = false,
|
||||
this.bonusPoints = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DeliveriesDone> createState() => _DeliveriesDoneState();
|
||||
}
|
||||
|
||||
class _DeliveriesDoneState extends State<DeliveriesDone> {
|
||||
late ConfettiController _confettiController;
|
||||
final GlobalKey<ScratcherState> _scratcherKey = GlobalKey<ScratcherState>();
|
||||
double _opacity = 0.0;
|
||||
bool _isScratched = false; // Track scratch state
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_confettiController = ConfettiController(
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_confettiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Check if we should show the scratch card
|
||||
final bool showScratchCard = !widget.isCancelled && widget.bonusPoints > 0;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Stack(
|
||||
children: [
|
||||
// Main Content
|
||||
SafeArea(
|
||||
child: showScratchCard
|
||||
? _buildScratchCardContent()
|
||||
: _buildStandardContent(),
|
||||
),
|
||||
|
||||
// Confetti Layer (on top)
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConfettiWidget(
|
||||
confettiController: _confettiController,
|
||||
blastDirectionality: BlastDirectionality.explosive,
|
||||
shouldLoop: false,
|
||||
colors: const [
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.pink,
|
||||
Colors.orange,
|
||||
Colors.purple,
|
||||
],
|
||||
createParticlePath: drawStar,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStandardContent() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(),
|
||||
Lottie.asset(
|
||||
widget.isCancelled
|
||||
? 'assets/lotties/Error Occurred!.json'
|
||||
: 'assets/lotties/result page succes.json',
|
||||
height: 220,
|
||||
repeat: false,
|
||||
errorBuilder:
|
||||
(c, e, s) => Icon(
|
||||
widget.isCancelled ? Icons.cancel : Icons.check_circle,
|
||||
size: 120,
|
||||
color: widget.isCancelled ? Colors.red : Colors.green,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
widget.isCancelled ? 'Delivery Cancelled' : 'Delivery Completed!',
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
widget.isCancelled
|
||||
? 'This order was cancelled.'
|
||||
: 'Great job! Your delivery was successful.',
|
||||
style: const TextStyle(fontSize: 16, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Spacer(),
|
||||
_buildDoneButton(isEnabled: true),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScratchCardContent() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(),
|
||||
Text(
|
||||
'You won a Scratch Card!',
|
||||
style: TextStyle(
|
||||
fontSize: 28, // Increased
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Scratch to reveal your bonus points',
|
||||
style: TextStyle(
|
||||
fontSize: 18, // Increased
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 250,
|
||||
height: 250,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Scratcher(
|
||||
key: _scratcherKey,
|
||||
brushSize: 50,
|
||||
threshold: 50,
|
||||
color: ColorConstants.primaryColor,
|
||||
onChange: (value) {
|
||||
// Optional: haptic feedback or sound while scratching
|
||||
},
|
||||
onThreshold: () {
|
||||
_confettiController.play();
|
||||
setState(() {
|
||||
_opacity = 1.0;
|
||||
_isScratched = true; // Enable button
|
||||
});
|
||||
},
|
||||
// Custom cover content instead of just solid color
|
||||
image: Image.asset(
|
||||
'assets/images/nearlelauncher.png',
|
||||
fit: BoxFit.scaleDown,
|
||||
width: 100, // Constrain width so it fits nicely
|
||||
height: 100,
|
||||
),
|
||||
child: Container(
|
||||
width: 250,
|
||||
height: 250,
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.monetization_on,
|
||||
size: 80,
|
||||
color: Colors.amber,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'${widget.bonusPoints}',
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Points',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_buildDoneButton(isEnabled: _isScratched),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDoneButton({required bool isEnabled}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isEnabled ? ColorConstants.primaryColor : Colors.grey,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: isEnabled ? () {
|
||||
Get.offAll(() => const BottomPage(initialIndex: 1));
|
||||
} : null,
|
||||
child: const Text(
|
||||
'Done',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Path drawStar(Size size) {
|
||||
// Method to draw star shape for confetti
|
||||
double degToRad(double deg) => deg * (math.pi / 180.0);
|
||||
|
||||
const numberOfPoints = 5;
|
||||
final halfWidth = size.width / 2;
|
||||
final externalRadius = halfWidth;
|
||||
final internalRadius = halfWidth / 2.5;
|
||||
final degreesPerStep = degToRad(360 / numberOfPoints);
|
||||
final halfDegreesPerStep = degreesPerStep / 2;
|
||||
final path = Path();
|
||||
final fullAngle = degToRad(360);
|
||||
path.moveTo(size.width, halfWidth);
|
||||
|
||||
for (double step = 0; step < fullAngle; step += degreesPerStep) {
|
||||
path.lineTo(
|
||||
halfWidth + externalRadius * math.cos(step),
|
||||
halfWidth + externalRadius * math.sin(step),
|
||||
);
|
||||
path.lineTo(
|
||||
halfWidth + internalRadius * math.cos(step + halfDegreesPerStep),
|
||||
halfWidth + internalRadius * math.sin(step + halfDegreesPerStep),
|
||||
);
|
||||
}
|
||||
path.close();
|
||||
return path;
|
||||
}
|
||||
}
|
||||
831
lib/views/Dashboard/deliveries/map.dart
Normal file
831
lib/views/Dashboard/deliveries/map.dart
Normal file
@@ -0,0 +1,831 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SCREEN 1: DELIVERY MAP PREVIEW (Shows route, has "Start" button)
|
||||
// -------------------------------------------------------------------------
|
||||
class _DeliveryMapScreen extends StatefulWidget {
|
||||
final Map<String, dynamic> delivery;
|
||||
final _MyDeliveriesState? parentState;
|
||||
const _DeliveryMapScreen({
|
||||
required this.delivery,
|
||||
this.parentState,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_DeliveryMapScreen> createState() => _DeliveryMapScreenState();
|
||||
}
|
||||
|
||||
class _DeliveryMapScreenState extends State<_DeliveryMapScreen> {
|
||||
GoogleMapController? _mapController;
|
||||
late final LatLng _pickupLocation;
|
||||
late final LatLng _dropLocation;
|
||||
final Set<Marker> _markers = {};
|
||||
final Set<Polyline> _polylines = {};
|
||||
late final PolylinePoints _polylinePoints;
|
||||
bool _isLoadingRoute = true;
|
||||
bool _isNavigating = false; // Prevent multiple clicks
|
||||
|
||||
static const String _googleApiKey = 'AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_polylinePoints = PolylinePoints(apiKey: _googleApiKey);
|
||||
_resolveLocationsFromDelivery();
|
||||
_setMarkers();
|
||||
_isLoadingRoute = false;
|
||||
_createRealRoute();
|
||||
}
|
||||
|
||||
double _parseD(dynamic v) {
|
||||
if (v == null) return 0.0;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
void _resolveLocationsFromDelivery() {
|
||||
final d = widget.delivery;
|
||||
final double pickLat = _parseD(d['pickuplat'] ?? d['PickupLat']);
|
||||
final double pickLon = _parseD(d['pickuplon'] ?? d['PickupLon']);
|
||||
final double dropLat = _parseD(
|
||||
d['droplat'] ?? d['DropLat'] ?? d['deliverylat'],
|
||||
);
|
||||
final double dropLon = _parseD(
|
||||
d['droplon'] ?? d['DropLon'] ?? d['deliverylong'],
|
||||
);
|
||||
final double riderLat = _parseD(d['riderslat']);
|
||||
final double riderLon = _parseD(d['riderslon']);
|
||||
|
||||
final bool hasPickup = pickLat != 0 && pickLon != 0;
|
||||
final bool hasDrop = dropLat != 0 && dropLon != 0;
|
||||
|
||||
final LatLng pickup = hasPickup
|
||||
? LatLng(pickLat, pickLon)
|
||||
: (riderLat != 0 && riderLon != 0
|
||||
? LatLng(riderLat, riderLon)
|
||||
: const LatLng(10.998356, 76.977596));
|
||||
|
||||
final LatLng drop = hasDrop
|
||||
? LatLng(dropLat, dropLon)
|
||||
: const LatLng(11.004556, 76.967696);
|
||||
|
||||
_pickupLocation = pickup;
|
||||
_dropLocation = drop;
|
||||
}
|
||||
|
||||
void _setMarkers() {
|
||||
_markers.addAll([
|
||||
Marker(
|
||||
markerId: const MarkerId('pickup'),
|
||||
position: _pickupLocation,
|
||||
infoWindow: const InfoWindow(title: 'Pickup Location'),
|
||||
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
|
||||
),
|
||||
Marker(
|
||||
markerId: const MarkerId('drop'),
|
||||
position: _dropLocation,
|
||||
infoWindow: const InfoWindow(title: 'Drop Location'),
|
||||
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _createRealRoute() async {
|
||||
try {
|
||||
final request = PolylineRequest(
|
||||
origin: PointLatLng(
|
||||
_pickupLocation.latitude,
|
||||
_pickupLocation.longitude,
|
||||
),
|
||||
destination: PointLatLng(
|
||||
_dropLocation.latitude,
|
||||
_dropLocation.longitude,
|
||||
),
|
||||
mode: TravelMode.driving,
|
||||
);
|
||||
|
||||
final result = await _polylinePoints.getRouteBetweenCoordinates(
|
||||
request: request,
|
||||
);
|
||||
|
||||
if (result.points.isNotEmpty) {
|
||||
final routePoints = result.points
|
||||
.map((e) => LatLng(e.latitude, e.longitude))
|
||||
.toList();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_polylines.add(
|
||||
Polyline(
|
||||
polylineId: const PolylineId('real_route'),
|
||||
color: ColorConstants.primaryColor,
|
||||
width: 6,
|
||||
points: routePoints,
|
||||
),
|
||||
);
|
||||
_isLoadingRoute = false;
|
||||
});
|
||||
|
||||
_fitMapToRoute();
|
||||
}
|
||||
} else {
|
||||
debugPrint('[MAP_PREVIEW] No route found');
|
||||
if (mounted) {
|
||||
setState(() => _isLoadingRoute = false);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[MAP_PREVIEW] Error creating route: $e');
|
||||
if (mounted) {
|
||||
setState(() => _isLoadingRoute = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Future<void> _fitMapToRoute() async {
|
||||
if (!mounted) return;
|
||||
if (_mapController == null) return;
|
||||
|
||||
// Check if controller is still alive (important!)
|
||||
try {
|
||||
await _mapController!.getVisibleRegion();
|
||||
} catch (e) {
|
||||
debugPrint("❌ Map controller is dead. Skip animateCamera.");
|
||||
return;
|
||||
}
|
||||
|
||||
final bounds = LatLngBounds(
|
||||
southwest: LatLng(
|
||||
math.min(_pickupLocation.latitude, _dropLocation.latitude),
|
||||
math.min(_pickupLocation.longitude, _dropLocation.longitude),
|
||||
),
|
||||
northeast: LatLng(
|
||||
math.max(_pickupLocation.latitude, _dropLocation.latitude),
|
||||
math.max(_pickupLocation.longitude, _dropLocation.longitude),
|
||||
),
|
||||
);
|
||||
|
||||
// Try animate safely
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
await _mapController!.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(bounds, 80),
|
||||
);
|
||||
return;
|
||||
} catch (e) {
|
||||
await Future.delayed(const Duration(milliseconds: 150));
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint("❌ animateCamera failed after retries (map probably disposed)");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
SizedBox.expand(
|
||||
child: GoogleMap(
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: _pickupLocation,
|
||||
zoom: 14.5,
|
||||
),
|
||||
onMapCreated: (controller) {
|
||||
_mapController = controller;
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (!_isLoadingRoute && _polylines.isNotEmpty) {
|
||||
_fitMapToRoute();
|
||||
}
|
||||
});
|
||||
},
|
||||
markers: _markers,
|
||||
polylines: _polylines,
|
||||
zoomControlsEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 50,
|
||||
left: 16,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, constraints) {
|
||||
final double maxSheetHeight =
|
||||
MediaQuery.of(ctx).size.height * 0.35;
|
||||
// ignore: unused_local_variable
|
||||
final double allowedHeight = math.min(
|
||||
constraints.maxHeight,
|
||||
maxSheetHeight,
|
||||
);
|
||||
return ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Customer Details',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xxLarge(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
// Just launch dialer; PiP is handled only from navigation screen
|
||||
final phone =
|
||||
(widget.delivery['deliverycontactno'] ?? '')
|
||||
.toString();
|
||||
final bool success = await launchPhoneDialer(
|
||||
phone.isNotEmpty ? phone : '9876543210',
|
||||
);
|
||||
if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Could not launch dialer'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/phone-call .png',
|
||||
height: 27,
|
||||
width: 27,
|
||||
errorBuilder: (c, e, s) => const Icon(
|
||||
Icons.phone,
|
||||
size: 27,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(thickness: 1),
|
||||
const SizedBox(height: 8),
|
||||
_buildCustomerInfo(),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: _isNavigating ? null : () async {
|
||||
// Prevent multiple clicks
|
||||
if (_isNavigating || !mounted || !context.mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isNavigating = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Capture screen size before navigation
|
||||
final screenSize = MediaQuery.of(context).size *
|
||||
MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
final dc = Get.put(
|
||||
DeliveriesController(),
|
||||
permanent: true,
|
||||
);
|
||||
final d = widget.delivery;
|
||||
final int deliveryId =
|
||||
int.tryParse(
|
||||
'${d['deliveryid'] ?? d['DeliveryId'] ?? 0}',
|
||||
) ??
|
||||
0;
|
||||
final String orderId = (d['orderid'] ??
|
||||
d['OrderId'] ??
|
||||
'')
|
||||
.toString();
|
||||
final int orderHeaderId =
|
||||
int.tryParse(
|
||||
'${d['orderheaderid'] ?? d['OrderHeaderId'] ?? 0}',
|
||||
) ??
|
||||
0;
|
||||
// Save ridertime start at the moment navigation is started
|
||||
try {
|
||||
if (deliveryId > 0) {
|
||||
final prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
// 1) Save rider time start (existing behaviour)
|
||||
await prefs.setString(
|
||||
'ridertime_start_$deliveryId',
|
||||
DateTime.now().toIso8601String(),
|
||||
);
|
||||
// 2) Save ETA end time for this order so PiP timer can resume correctly
|
||||
final rawEta = d['eta'];
|
||||
int etaMinutes = 0;
|
||||
if (rawEta != null) {
|
||||
etaMinutes =
|
||||
int.tryParse(rawEta.toString()) ?? 0;
|
||||
}
|
||||
if (etaMinutes > 0) {
|
||||
final now = DateTime.now();
|
||||
final endTime = now
|
||||
.add(Duration(minutes: etaMinutes))
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000; // store seconds
|
||||
await prefs.setInt(
|
||||
'eta_endtime_$orderId',
|
||||
endTime,
|
||||
);
|
||||
debugPrint(
|
||||
'[ACTIVE][ETA] Saved eta_endtime_$orderId -> $endTime (eta=$etaMinutes min)',
|
||||
);
|
||||
}
|
||||
debugPrint(
|
||||
'[ACTIVE] Saved ridertime_start for deliveryId=$deliveryId',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error saving ridertime_start: $e',
|
||||
);
|
||||
}
|
||||
|
||||
final parentState =
|
||||
widget.parentState ??
|
||||
context
|
||||
.findAncestorStateOfType<
|
||||
_MyDeliveriesState
|
||||
>();
|
||||
|
||||
// ✅ CRITICAL: Check active delivery BEFORE navigation
|
||||
if (parentState != null) {
|
||||
final currentOrderId = (d['orderid'] ??
|
||||
d['OrderId'] ??
|
||||
'')
|
||||
.toString();
|
||||
final activeOrderIds = parentState
|
||||
._activeDeliveries
|
||||
.map(
|
||||
(del) => (del['orderid'] ??
|
||||
del['OrderId'] ??
|
||||
'')
|
||||
.toString(),
|
||||
)
|
||||
.where((id) => id.isNotEmpty)
|
||||
.toSet();
|
||||
|
||||
// Block if there's a different active delivery
|
||||
if (parentState._activeDeliveries.isNotEmpty &&
|
||||
!activeOrderIds.contains(currentOrderId)) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.orange.shade400,
|
||||
Colors.red.shade500,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.warning_rounded,
|
||||
color: Colors.white,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Title
|
||||
Text(
|
||||
'Active Delivery in Progress',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: FontConstants.xxLarge(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Message
|
||||
Text(
|
||||
'Please complete your active delivery first before starting another delivery.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
fontSize: FontConstants.medium(context),
|
||||
height: 1.4,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Action Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
// Navigate to active delivery
|
||||
if (parentState._activeDeliveries.isNotEmpty) {
|
||||
parentState.startDelivery(parentState._activeDeliveries.first);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.red.shade600,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.two_wheeler,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'View Active Delivery',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.regular(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Close Button
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
child: Text(
|
||||
'Close',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return; // Return early to prevent starting another delivery
|
||||
}
|
||||
}
|
||||
|
||||
if (deliveryId > 0 && orderId.isNotEmpty) {
|
||||
String riderLatStr = '0';
|
||||
String riderLngStr = '0';
|
||||
try {
|
||||
final position =
|
||||
await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.medium,
|
||||
timeLimit: Duration(seconds: 3),
|
||||
),
|
||||
).timeout(const Duration(seconds: 3));
|
||||
riderLatStr = position.latitude
|
||||
.toStringAsFixed(6);
|
||||
riderLngStr = position.longitude
|
||||
.toStringAsFixed(6);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error getting rider location: $e',
|
||||
);
|
||||
try {
|
||||
final lastPos =
|
||||
await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null) {
|
||||
riderLatStr = lastPos.latitude
|
||||
.toStringAsFixed(6);
|
||||
riderLngStr = lastPos.longitude
|
||||
.toStringAsFixed(6);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ✅ CRITICAL: Navigate FIRST, then handle status updates
|
||||
if (!mounted || !context.mounted) {
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate immediately - this must happen
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _RiderNavigationScreen(
|
||||
delivery: widget.delivery,
|
||||
parentState: widget.parentState,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Reset navigation state after navigation completes
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
|
||||
// Continue with status updates and PiP in background
|
||||
debugPrint(
|
||||
'[ACTIVE] Updating status for deliveryId=$deliveryId orderId=$orderId lat=$riderLatStr lng=$riderLngStr',
|
||||
);
|
||||
|
||||
final ok = await dc.updateActiveStatus(
|
||||
deliveryId: deliveryId,
|
||||
orderHeaderId: orderHeaderId,
|
||||
ridersLat: riderLatStr,
|
||||
ridersLng: riderLngStr,
|
||||
orderId: orderId,
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'[ACTIVE] Status update result: $ok',
|
||||
);
|
||||
|
||||
// ✅ CRITICAL: ENFORCE PiP when delivery becomes active (compulsory)
|
||||
if (ok && !dc.isPipEnabled.value) {
|
||||
try {
|
||||
debugPrint('[ACTIVE] Delivery is now active - Enforcing PiP mode');
|
||||
final floating = Floating();
|
||||
const rational = Rational.landscape();
|
||||
|
||||
final height = (screenSize.height * 0.5).toInt();
|
||||
final width = (screenSize.width * 0.9).toInt();
|
||||
|
||||
final arguments = ImmediatePiP(
|
||||
aspectRatio: rational,
|
||||
sourceRectHint: math.Rectangle<int>(
|
||||
((screenSize.width - width) ~/ 2).toInt(),
|
||||
((screenSize.height - height) ~/ 2).toInt(),
|
||||
width,
|
||||
height,
|
||||
),
|
||||
);
|
||||
|
||||
await floating.enable(arguments);
|
||||
dc.isPipEnabled.value = true;
|
||||
debugPrint('[ACTIVE] PiP enabled successfully');
|
||||
|
||||
// Also try method channel as backup
|
||||
try {
|
||||
const channel = MethodChannel('nearle/pip');
|
||||
await channel.invokeMethod<bool>('enterPip');
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
debugPrint('[ACTIVE] Error enabling PiP: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (parentState != null) {
|
||||
final previousActive =
|
||||
parentState._activeDeliveryOrderId;
|
||||
if (previousActive != null &&
|
||||
previousActive != orderId) {
|
||||
parentState._stopDeliveryPosting(
|
||||
previousActive,
|
||||
);
|
||||
}
|
||||
|
||||
parentState._activeDeliveryOrderId = orderId;
|
||||
d['orderstatus'] = 'active';
|
||||
|
||||
final orderKey = parentState._getOrderKey(d);
|
||||
if (parentState._skippedOrdersCache
|
||||
.containsKey(orderKey)) {
|
||||
parentState._skippedOrdersCache.remove(
|
||||
orderKey,
|
||||
);
|
||||
parentState._skippedOrderTimestamps.remove(
|
||||
orderKey,
|
||||
);
|
||||
await parentState._saveSkippedOrdersCache();
|
||||
debugPrint(
|
||||
'[ACTIVE] Removed from skipped cache (resumed): $orderKey',
|
||||
);
|
||||
}
|
||||
|
||||
await parentState._startDeliveryPosting(d);
|
||||
|
||||
try {
|
||||
final prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
'active_delivery_order_id',
|
||||
orderId,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error saving active delivery ID: $e',
|
||||
);
|
||||
}
|
||||
|
||||
// ignore: invalid_use_of_protected_member
|
||||
parentState.setState(() {});
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'[ACTIVE] Invalid deliveryId: $deliveryId or orderId: $orderId',
|
||||
);
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error in navigation flow: $e',
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: _isNavigating
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Starting...',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xLarge(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
'Start Navigation',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xLarge(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomerInfo() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
'Name:',
|
||||
(widget.delivery['deliverycustomer'] ?? 'Customer').toString(),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildInfoRow(
|
||||
'Address:',
|
||||
(widget.delivery['deliveryaddress'] ?? 'Address not available')
|
||||
.toString(),
|
||||
isExpanded: true,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildInfoRow(
|
||||
'Order ID:',
|
||||
'#${(widget.delivery['orderid'] ?? '').toString()}',
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildInfoRow(
|
||||
'Distance:',
|
||||
'${(widget.delivery['kms'] ?? '0').toString()} km',
|
||||
valueColor: Colors.red,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(
|
||||
String label,
|
||||
String value, {
|
||||
bool isExpanded = false,
|
||||
Color? valueColor,
|
||||
}) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xLarge(context),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
isExpanded
|
||||
? Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.large(context),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: valueColor ?? Colors.black87,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.large(context),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: valueColor ?? Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
41
lib/views/Dashboard/deliveries/map_btn.dart
Normal file
41
lib/views/Dashboard/deliveries/map_btn.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// MAP VIEW BUTTON (Customer locations)
|
||||
// -------------------------------------------------------------------------
|
||||
class MapViewRow extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> deliveries;
|
||||
final Map<String, int>? preservedStepNumbers;
|
||||
|
||||
const MapViewRow({
|
||||
super.key,
|
||||
required this.deliveries,
|
||||
this.preservedStepNumbers,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MultiCustomerMapScreen(
|
||||
deliveries: deliveries,
|
||||
preservedStepNumbers: preservedStepNumbers ?? {},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/customermap.png',
|
||||
color: ColorConstants.primaryColor,
|
||||
height: 32,
|
||||
width: 32,
|
||||
errorBuilder: (c, e, s) =>
|
||||
const Icon(Icons.map, size: 32, color: Colors.blue),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
894
lib/views/Dashboard/deliveries/multi_map.dart
Normal file
894
lib/views/Dashboard/deliveries/multi_map.dart
Normal file
@@ -0,0 +1,894 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
class _StepPoint {
|
||||
final int step;
|
||||
final LatLng position;
|
||||
_StepPoint(this.step, this.position);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// MULTI CUSTOMER MAP (Shows all deliveries with real step numbers)
|
||||
// -------------------------------------------------------------------------
|
||||
class MultiCustomerMapScreen extends StatefulWidget {
|
||||
final List<Map<String, dynamic>> deliveries;
|
||||
final Map<String, int> preservedStepNumbers;
|
||||
|
||||
const MultiCustomerMapScreen({
|
||||
super.key,
|
||||
required this.deliveries,
|
||||
this.preservedStepNumbers = const {},
|
||||
});
|
||||
|
||||
@override
|
||||
State<MultiCustomerMapScreen> createState() => _MultiCustomerMapScreenState();
|
||||
}
|
||||
|
||||
class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
|
||||
GoogleMapController? mapController;
|
||||
Set<Marker> markers = {};
|
||||
Set<Polyline> polylines = {};
|
||||
LatLng? currentLocation;
|
||||
bool _isLoading = true;
|
||||
bool _mapReady = false;
|
||||
final PolylinePoints _polylinePoints = PolylinePoints(
|
||||
apiKey: 'AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q',
|
||||
);
|
||||
final Map<String, Map<String, dynamic>> _deliveryMap = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.deliveries.isNotEmpty) {
|
||||
final firstDelivery = widget.deliveries.first;
|
||||
final dropLat = _parseD(
|
||||
firstDelivery['droplat'] ?? firstDelivery['deliverylat'] ?? 0,
|
||||
);
|
||||
final dropLon = _parseD(
|
||||
firstDelivery['droplon'] ?? firstDelivery['deliverylong'] ?? 0,
|
||||
);
|
||||
if (dropLat != 0 && dropLon != 0) {
|
||||
currentLocation = LatLng(dropLat, dropLon);
|
||||
} else {
|
||||
currentLocation = const LatLng(11.018356, 77.012596);
|
||||
}
|
||||
} else {
|
||||
currentLocation = const LatLng(11.018356, 77.012596);
|
||||
}
|
||||
_loadMapData();
|
||||
}
|
||||
|
||||
double _parseD(dynamic v) {
|
||||
if (v == null) return 0.0;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
int _getStepNumber(Map<String, dynamic> order) {
|
||||
final dynamic raw = order['step'] ?? order['Step'];
|
||||
final int step = raw == null
|
||||
? 0
|
||||
: (raw is num ? raw.toInt() : int.tryParse(raw.toString()) ?? 0);
|
||||
return step;
|
||||
}
|
||||
|
||||
String _getOrderKey(Map<String, dynamic> order) {
|
||||
final deliveryId = (order['deliveryid'] ?? '').toString();
|
||||
final orderId = (order['orderid'] ?? '').toString();
|
||||
return deliveryId.isNotEmpty ? 'delivery_$deliveryId' : 'order_$orderId';
|
||||
}
|
||||
|
||||
int _getPreservedOrCurrentStep(Map<String, dynamic> order) {
|
||||
final orderKey = _getOrderKey(order);
|
||||
if (widget.preservedStepNumbers.containsKey(orderKey)) {
|
||||
return widget.preservedStepNumbers[orderKey]!;
|
||||
}
|
||||
final currentStep = _getStepNumber(order);
|
||||
return currentStep;
|
||||
}
|
||||
|
||||
Future<void> _loadMapData() async {
|
||||
try {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
_getCurrentLocation().then((_) {
|
||||
if (mounted) {
|
||||
_createDeliveryMarkers();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[CUSTOMER_MAP] Error loading map data: $e');
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getCurrentLocation() async {
|
||||
try {
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && mounted) {
|
||||
setState(() {
|
||||
currentLocation = LatLng(lastPos.latitude, lastPos.longitude);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && mounted) {
|
||||
setState(() {
|
||||
currentLocation = LatLng(lastPos.latitude, lastPos.longitude);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Position? position;
|
||||
try {
|
||||
position = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.medium,
|
||||
distanceFilter: 0,
|
||||
timeLimit: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[CUSTOMER_MAP] Timeout getting location, using last known: $e',
|
||||
);
|
||||
position = await Geolocator.getLastKnownPosition();
|
||||
}
|
||||
|
||||
final safePosition = position;
|
||||
if (safePosition != null && mounted) {
|
||||
setState(() {
|
||||
currentLocation = LatLng(
|
||||
safePosition.latitude,
|
||||
safePosition.longitude,
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[CUSTOMER_MAP] Error getting location: $e');
|
||||
try {
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && mounted) {
|
||||
setState(() {
|
||||
currentLocation = LatLng(lastPos.latitude, lastPos.longitude);
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
int? _getNextDeliveryStep() {
|
||||
// Find the first non-skipped delivery (next delivery from user location)
|
||||
for (int i = 0; i < widget.deliveries.length; i++) {
|
||||
final delivery = widget.deliveries[i];
|
||||
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
|
||||
.trim();
|
||||
if (status != 'skipped') {
|
||||
final stepNumber = _getPreservedOrCurrentStep(delivery);
|
||||
if (stepNumber > 0) {
|
||||
return stepNumber;
|
||||
} else {
|
||||
// Calculate display step for orders without step
|
||||
final ordersWithStepBefore = widget.deliveries
|
||||
.sublist(0, i)
|
||||
.where((o) => _getPreservedOrCurrentStep(o) > 0)
|
||||
.length;
|
||||
final totalOrdersWithStep = widget.deliveries
|
||||
.where((o) => _getPreservedOrCurrentStep(o) > 0)
|
||||
.length;
|
||||
return totalOrdersWithStep + (i - ordersWithStepBefore) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _createDeliveryMarkers() async {
|
||||
Set<Marker> tempMarkers = {};
|
||||
List<_StepPoint> stepPoints = [];
|
||||
_deliveryMap.clear();
|
||||
|
||||
final nextStep = _getNextDeliveryStep();
|
||||
|
||||
for (int i = 0; i < widget.deliveries.length; i++) {
|
||||
final delivery = widget.deliveries[i];
|
||||
final stepNumber = _getPreservedOrCurrentStep(delivery);
|
||||
|
||||
int displayStep;
|
||||
if (stepNumber > 0) {
|
||||
displayStep = stepNumber;
|
||||
} else {
|
||||
final ordersWithStepBefore = widget.deliveries
|
||||
.sublist(0, i)
|
||||
.where((o) => _getPreservedOrCurrentStep(o) > 0)
|
||||
.length;
|
||||
final totalOrdersWithStep = widget.deliveries
|
||||
.where((o) => _getPreservedOrCurrentStep(o) > 0)
|
||||
.length;
|
||||
displayStep = totalOrdersWithStep + (i - ordersWithStepBefore) + 1;
|
||||
}
|
||||
|
||||
final double lat = _parseD(
|
||||
delivery['droplat'] ?? delivery['deliverylat'],
|
||||
);
|
||||
final double lon = _parseD(
|
||||
delivery['droplon'] ?? delivery['deliverylong'],
|
||||
);
|
||||
if (lat == 0 || lon == 0) continue;
|
||||
|
||||
final customerName = (delivery['deliverycustomer'] ?? 'Customer ${i + 1}')
|
||||
.toString();
|
||||
final orderId = (delivery['orderid'] ?? '').toString();
|
||||
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
|
||||
.trim();
|
||||
final isSkipped = status == 'skipped';
|
||||
final isNext = displayStep == nextStep;
|
||||
|
||||
// Store delivery data for dialog
|
||||
_deliveryMap['delivery_$orderId'] = delivery;
|
||||
|
||||
final icon = await _createCircularMarkerBitmap(
|
||||
displayStep,
|
||||
isSkipped: isSkipped,
|
||||
isNext: isNext,
|
||||
);
|
||||
|
||||
tempMarkers.add(
|
||||
Marker(
|
||||
markerId: MarkerId('delivery_$orderId'),
|
||||
position: LatLng(lat, lon),
|
||||
icon: icon,
|
||||
infoWindow: InfoWindow(
|
||||
title: isSkipped
|
||||
? 'Step $displayStep: $customerName (SKIPPED)'
|
||||
: 'Step $displayStep: $customerName',
|
||||
snippet: 'Order #$orderId',
|
||||
),
|
||||
onTap: () {
|
||||
_showCustomerDetailsSheet(delivery, displayStep);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
stepPoints.add(_StepPoint(displayStep, LatLng(lat, lon)));
|
||||
}
|
||||
|
||||
if (currentLocation != null) {
|
||||
tempMarkers.add(
|
||||
Marker(
|
||||
markerId: const MarkerId('current_location'),
|
||||
position: currentLocation!,
|
||||
icon: BitmapDescriptor.defaultMarkerWithHue(
|
||||
BitmapDescriptor.hueAzure,
|
||||
),
|
||||
infoWindow: const InfoWindow(title: 'You are here'),
|
||||
),
|
||||
);
|
||||
stepPoints.insert(0, _StepPoint(0, currentLocation!));
|
||||
}
|
||||
|
||||
stepPoints.sort((a, b) => a.step.compareTo(b.step));
|
||||
|
||||
Set<Polyline> newPolylines = {};
|
||||
int segmentIndex = 0;
|
||||
|
||||
for (int i = 0; i < stepPoints.length - 1; i++) {
|
||||
final start = stepPoints[i].position;
|
||||
final end = stepPoints[i + 1].position;
|
||||
|
||||
try {
|
||||
// ignore: deprecated_member_use
|
||||
final request = PolylineRequest(
|
||||
origin: PointLatLng(start.latitude, start.longitude),
|
||||
destination: PointLatLng(end.latitude, end.longitude),
|
||||
mode: TravelMode.driving,
|
||||
);
|
||||
|
||||
final result = await _polylinePoints.getRouteBetweenCoordinates(
|
||||
request: request,
|
||||
);
|
||||
|
||||
if (result.points.isNotEmpty) {
|
||||
final routePoints = result.points
|
||||
.map((p) => LatLng(p.latitude, p.longitude))
|
||||
.toList();
|
||||
|
||||
newPolylines.add(
|
||||
Polyline(
|
||||
polylineId: PolylineId('segment_$segmentIndex'),
|
||||
points: routePoints,
|
||||
width: 6,
|
||||
color: Colors.blue,
|
||||
startCap: Cap.roundCap,
|
||||
endCap: Cap.roundCap,
|
||||
jointType: JointType.round,
|
||||
geodesic: true,
|
||||
),
|
||||
);
|
||||
segmentIndex++;
|
||||
} else {
|
||||
newPolylines.add(
|
||||
Polyline(
|
||||
polylineId: PolylineId('segment_fallback_$segmentIndex'),
|
||||
points: [start, end],
|
||||
width: 4,
|
||||
color: Colors.blue.shade200,
|
||||
),
|
||||
);
|
||||
segmentIndex++;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[CUSTOMER_MAP] Directions error for segment $i: $e');
|
||||
newPolylines.add(
|
||||
Polyline(
|
||||
polylineId: PolylineId('segment_error_$segmentIndex'),
|
||||
points: [start, end],
|
||||
width: 4,
|
||||
color: Colors.blue.shade200,
|
||||
),
|
||||
);
|
||||
segmentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
markers = tempMarkers;
|
||||
polylines = newPolylines;
|
||||
});
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
_fitBoundsToMarkersAndPolylines();
|
||||
}
|
||||
|
||||
Future<void> _fitBoundsToMarkersAndPolylines() async {
|
||||
if (mapController == null) return;
|
||||
|
||||
double minLat = double.infinity;
|
||||
double maxLat = -double.infinity;
|
||||
double minLng = double.infinity;
|
||||
double maxLng = -double.infinity;
|
||||
|
||||
bool hasPoint = false;
|
||||
|
||||
for (final m in markers) {
|
||||
final pos = m.position;
|
||||
minLat = math.min(minLat, pos.latitude);
|
||||
maxLat = math.max(maxLat, pos.latitude);
|
||||
minLng = math.min(minLng, pos.longitude);
|
||||
maxLng = math.max(maxLng, pos.longitude);
|
||||
hasPoint = true;
|
||||
}
|
||||
|
||||
for (final poly in polylines) {
|
||||
for (final pos in poly.points) {
|
||||
minLat = math.min(minLat, pos.latitude);
|
||||
maxLat = math.max(maxLat, pos.latitude);
|
||||
minLng = math.min(minLng, pos.longitude);
|
||||
maxLng = math.max(maxLng, pos.longitude);
|
||||
hasPoint = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPoint) return;
|
||||
|
||||
final bounds = LatLngBounds(
|
||||
southwest: LatLng(minLat, minLng),
|
||||
northeast: LatLng(maxLat, maxLng),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
await mapController!.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(bounds, 80),
|
||||
);
|
||||
} catch (e) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () async {
|
||||
try {
|
||||
await mapController!.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(bounds, 80),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[CUSTOMER_MAP] Retry failed: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<BitmapDescriptor> _createCircularMarkerBitmap(
|
||||
int number, {
|
||||
bool isSkipped = false,
|
||||
bool isNext = false,
|
||||
}) async {
|
||||
const double size = 70;
|
||||
final pictureRecorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(pictureRecorder);
|
||||
final center = Offset(size / 2, size / 2);
|
||||
|
||||
final paint = Paint()
|
||||
..color = isSkipped ? Colors.orange : ColorConstants.primaryColor;
|
||||
canvas.drawCircle(center, 15, paint);
|
||||
|
||||
final border = Paint()
|
||||
..color = isSkipped ? Colors.orange.shade900 : Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = isSkipped ? 4 : 3;
|
||||
canvas.drawCircle(center, 15, border);
|
||||
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: number.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 19,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
center.dx - textPainter.width / 2,
|
||||
center.dy - textPainter.height / 2,
|
||||
),
|
||||
);
|
||||
|
||||
// Draw "NEXT" indicator (road sign with down arrow) on top
|
||||
if (isNext) {
|
||||
// Draw road sign background (rectangle)
|
||||
final signPaint = Paint()
|
||||
..color = Colors.green
|
||||
..style = PaintingStyle.fill;
|
||||
final signRect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(center: Offset(size / 2, 8), width: 45, height: 30),
|
||||
const Radius.circular(4),
|
||||
);
|
||||
canvas.drawRRect(signRect, signPaint);
|
||||
|
||||
// Draw border
|
||||
final signBorder = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5;
|
||||
canvas.drawRRect(signRect, signBorder);
|
||||
|
||||
// Draw "NEXT" text
|
||||
final nextTextPainter = TextPainter(
|
||||
text: const TextSpan(
|
||||
text: 'NEXT',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
nextTextPainter.layout();
|
||||
nextTextPainter.paint(
|
||||
canvas,
|
||||
Offset(size / 2 - nextTextPainter.width / 2, 4),
|
||||
);
|
||||
|
||||
// Draw down arrow
|
||||
final arrowPath = Path();
|
||||
arrowPath.moveTo(size / 2, 18);
|
||||
arrowPath.lineTo(size / 2 - 4, 24);
|
||||
arrowPath.lineTo(size / 2 + 4, 24);
|
||||
arrowPath.close();
|
||||
final arrowPaint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawPath(arrowPath, arrowPaint);
|
||||
}
|
||||
|
||||
final img = await pictureRecorder.endRecording().toImage(
|
||||
size.toInt(),
|
||||
size.toInt(),
|
||||
);
|
||||
final data = await img.toByteData(format: ui.ImageByteFormat.png);
|
||||
return BitmapDescriptor.bytes(data!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
void _showCustomerDetailsSheet(
|
||||
Map<String, dynamic> delivery,
|
||||
int stepNumber,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: true,
|
||||
enableDrag: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
final customerName = (delivery['deliverycustomer'] ?? 'Customer')
|
||||
.toString();
|
||||
final address = (delivery['deliveryaddress'] ?? 'Address not available')
|
||||
.toString();
|
||||
final phone = (delivery['deliverycontactno'] ?? '').toString();
|
||||
final orderId = (delivery['orderid'] ?? '').toString();
|
||||
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
|
||||
.trim();
|
||||
final isSkipped = status == 'skipped';
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with title and close icon
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Customer Details',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.cancel,
|
||||
size: 34,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Divider(thickness: 1.5),
|
||||
|
||||
// Step number and status
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isSkipped
|
||||
? Colors.orange
|
||||
: ColorConstants.primaryColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Step $stepNumber',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isSkipped) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'SKIPPED',
|
||||
style: TextStyle(
|
||||
color: Colors.orange,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Customer name
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.person, size: 20, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Customer Name',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
customerName,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Delivery address
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on,
|
||||
size: 20,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Delivery Address',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
address,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Phone number
|
||||
if (phone.isNotEmpty)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.phone, size: 20, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Contact Number',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
phone,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Order ID
|
||||
Text(
|
||||
'Order ID: $orderId',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Call button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
// Close bottom sheet first
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Small delay to ensure bottom sheet is closed
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 300),
|
||||
);
|
||||
|
||||
// Then make the call
|
||||
final bool success = await launchPhoneDialer(
|
||||
phone.isNotEmpty ? phone : '9876543210',
|
||||
);
|
||||
if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Could not launch dialer'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.phone,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text(
|
||||
'Call',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 21,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
LatLng initialTarget;
|
||||
double initialZoom = 13;
|
||||
|
||||
if (currentLocation != null) {
|
||||
initialTarget = currentLocation!;
|
||||
} else if (widget.deliveries.isNotEmpty) {
|
||||
final firstDelivery = widget.deliveries.first;
|
||||
final lat = _parseD(
|
||||
firstDelivery['droplat'] ?? firstDelivery['deliverylat'],
|
||||
);
|
||||
final lon = _parseD(
|
||||
firstDelivery['droplon'] ?? firstDelivery['deliverylong'],
|
||||
);
|
||||
initialTarget = (lat != 0 && lon != 0)
|
||||
? LatLng(lat, lon)
|
||||
: const LatLng(11.0168, 76.9558);
|
||||
} else {
|
||||
initialTarget = const LatLng(11.0168, 76.9558);
|
||||
}
|
||||
|
||||
final initialCameraPosition = CameraPosition(
|
||||
target: initialTarget,
|
||||
zoom: initialZoom,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(70),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
elevation: 0,
|
||||
toolbarHeight: 80,
|
||||
leadingWidth: double.infinity,
|
||||
leading: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Colors.white,
|
||||
size: 26,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const Text(
|
||||
'Delivery Route',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: GoogleMap(
|
||||
initialCameraPosition: initialCameraPosition,
|
||||
markers: markers,
|
||||
polylines: polylines,
|
||||
myLocationEnabled: true,
|
||||
myLocationButtonEnabled: true,
|
||||
onMapCreated: (controller) {
|
||||
mapController = controller;
|
||||
_mapReady = true;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Future.delayed(const Duration(milliseconds: 150), () {
|
||||
if (_mapReady) _fitBoundsToMarkersAndPolylines();
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1004
lib/views/Dashboard/deliveries/nav.dart
Normal file
1004
lib/views/Dashboard/deliveries/nav.dart
Normal file
File diff suppressed because it is too large
Load Diff
172
lib/views/Dashboard/deliveries/pip.dart
Normal file
172
lib/views/Dashboard/deliveries/pip.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PIP INFO CARD (Shown when PiP mode is enabled)
|
||||
// -------------------------------------------------------------------------
|
||||
class PipInfoCard extends StatefulWidget {
|
||||
final String orderId;
|
||||
final int etaMinutes; // ETA in minutes from API (always treat as minutes)
|
||||
|
||||
const PipInfoCard({
|
||||
super.key,
|
||||
required this.orderId,
|
||||
required this.etaMinutes,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PipInfoCard> createState() => _PipInfoCardState();
|
||||
}
|
||||
|
||||
class _PipInfoCardState extends State<PipInfoCard> {
|
||||
late final CountDownController _controller;
|
||||
int _durationSeconds = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = CountDownController();
|
||||
// Load remaining ETA from SharedPreferences so timer doesn't reset
|
||||
_initDuration();
|
||||
}
|
||||
|
||||
Future<void> _initDuration() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final endKey = 'eta_endtime_${widget.orderId}';
|
||||
final endSeconds = prefs.getInt(endKey);
|
||||
final nowSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
int remaining = 0;
|
||||
if (endSeconds != null && endSeconds > nowSeconds) {
|
||||
remaining = endSeconds - nowSeconds;
|
||||
} else {
|
||||
// Fallback: use full ETA from API
|
||||
final int safeEtaMinutes =
|
||||
widget.etaMinutes < 0 ? 0 : widget.etaMinutes;
|
||||
remaining = safeEtaMinutes * 60;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_durationSeconds = remaining.clamp(0, 24 * 60 * 60);
|
||||
});
|
||||
} catch (_) {
|
||||
// In case of error, just fall back to raw ETA minutes
|
||||
final int safeEtaMinutes = widget.etaMinutes < 0 ? 0 : widget.etaMinutes;
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_durationSeconds = safeEtaMinutes * 60;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// Expanded PiP: timer + key delivery details, but still relatively compact
|
||||
body: Center(
|
||||
child: widget.orderId.isEmpty || widget.orderId == 'N/A'
|
||||
? Text(
|
||||
'No Active Order',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
)
|
||||
: Card(
|
||||
color: Colors.white,
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Container(
|
||||
width: 220,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 6,
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Timer
|
||||
SizedBox(
|
||||
width: 72,
|
||||
height: 72,
|
||||
child: _durationSeconds <= 0
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorConstants.primaryColor,
|
||||
width: 3,
|
||||
),
|
||||
color: Colors.white,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Out',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorConstants.primaryColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
)
|
||||
: CircularCountDownTimer(
|
||||
duration: _durationSeconds,
|
||||
initialDuration: 0,
|
||||
controller: _controller,
|
||||
width: 72,
|
||||
height: 72,
|
||||
ringColor: ColorConstants.primaryColor
|
||||
.withValues(alpha: 0.15),
|
||||
fillColor: ColorConstants.primaryColor,
|
||||
backgroundColor: Colors.white,
|
||||
strokeWidth: 5,
|
||||
strokeCap: StrokeCap.round,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorConstants.primaryColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
isReverse: true,
|
||||
isReverseAnimation: true,
|
||||
isTimerTextShown: true,
|
||||
autoStart: true,
|
||||
timeFormatterFunction:
|
||||
(defaultFormatter, duration) {
|
||||
return duration.inSeconds <= 0
|
||||
? 'Out'
|
||||
: defaultFormatter(duration);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Active order id
|
||||
Text(
|
||||
'Active Order ID: ${widget.orderId}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
1031
lib/views/Dashboard/deliveries/sheet.dart
Normal file
1031
lib/views/Dashboard/deliveries/sheet.dart
Normal file
File diff suppressed because it is too large
Load Diff
411
lib/views/Dashboard/deliveries/skip_sheet.dart
Normal file
411
lib/views/Dashboard/deliveries/skip_sheet.dart
Normal file
@@ -0,0 +1,411 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SKIP REASONS SHEET
|
||||
// -------------------------------------------------------------------------
|
||||
Future<void> _showMyOptionsSheet(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> delivery,
|
||||
_MyDeliveriesState? parentState,
|
||||
) async {
|
||||
int selected = -1;
|
||||
bool isLoading = false;
|
||||
|
||||
final List<String> skipReasons = [
|
||||
'Customer unreachable',
|
||||
'Customer not at the location',
|
||||
];
|
||||
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
Widget optionBox(String title, int index) {
|
||||
final bool isSelected = selected == index;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (!isLoading) {
|
||||
setState(() => selected = index);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? ColorConstants.primaryColor.withOpacity(0.1)
|
||||
: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey.shade300,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? Icons.check_circle : Icons.circle_outlined,
|
||||
color: isSelected
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleConfirm() async {
|
||||
if (selected == -1 || isLoading) return;
|
||||
|
||||
// Check skip limits before proceeding
|
||||
final dc = Get.put(DeliveriesController(), permanent: true);
|
||||
final skipStatus = await dc.checkSkipStatus();
|
||||
final int skipCount = skipStatus['count'] ?? 0;
|
||||
bool applyPenalty = false;
|
||||
|
||||
if (skipCount >= 2) {
|
||||
// Show Styled Alert Dialog
|
||||
final bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
elevation: 5,
|
||||
backgroundColor: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: Colors.red.shade600,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Skip Limit Reached',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'You have exceeded the limit of 2 skips within 3 hours.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Colors.orange.shade200,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Colors.orange.shade800, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Proceeding will forfeit your bonus points for this session.",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.orange.shade900,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade600,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Confirm Skip",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirm != true) return; // User cancelled or dismissed
|
||||
applyPenalty = true;
|
||||
}
|
||||
|
||||
setState(() => isLoading = true);
|
||||
|
||||
try {
|
||||
final dc = Get.put(DeliveriesController(), permanent: true);
|
||||
final d = delivery;
|
||||
final int deliveryId =
|
||||
int.tryParse('${d['deliveryid'] ?? 0}') ?? 0;
|
||||
final int orderHeaderId =
|
||||
int.tryParse('${d['orderheaderid'] ?? 0}') ?? 0;
|
||||
final String reason = selected >= 0 && selected < skipReasons.length
|
||||
? skipReasons[selected]
|
||||
: '';
|
||||
|
||||
if (deliveryId > 0 && orderHeaderId > 0) {
|
||||
String riderLatStr = '0';
|
||||
String riderLngStr = '0';
|
||||
try {
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
timeLimit: const Duration(seconds: 5),
|
||||
);
|
||||
riderLatStr = position.latitude.toStringAsFixed(6);
|
||||
riderLngStr = position.longitude.toStringAsFixed(6);
|
||||
} catch (e) {
|
||||
debugPrint('[SKIP] Error getting rider location: $e');
|
||||
try {
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null) {
|
||||
riderLatStr = lastPos.latitude.toStringAsFixed(6);
|
||||
riderLngStr = lastPos.longitude.toStringAsFixed(6);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'[SKIP] Updating status for deliveryId=$deliveryId orderHeaderId=$orderHeaderId reason="$reason"',
|
||||
);
|
||||
|
||||
final ok = await dc.updateSkippedStatus(
|
||||
deliveryId: deliveryId,
|
||||
orderHeaderId: orderHeaderId,
|
||||
ridersLat: riderLatStr,
|
||||
ridersLng: riderLngStr,
|
||||
notes: reason,
|
||||
);
|
||||
|
||||
debugPrint('[SKIP] Status update result: $ok');
|
||||
|
||||
if (ok && context.mounted) {
|
||||
// Register skip locally only on success
|
||||
await dc.registerSkip(applyPenalty: applyPenalty);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context); // Close the sheet
|
||||
|
||||
if (parentState != null) {
|
||||
// Only update list if called from the list view
|
||||
parentState.markOrderAsSkipped(d, reason);
|
||||
} else {
|
||||
// If called from Navigation/Map (where parentState is null),
|
||||
// we just close the sheet and let the caller handle the UI update
|
||||
// typically by popping the route or showing a snackbar.
|
||||
// We DO NOT force navigation to "MyDeliveries".
|
||||
debugPrint(
|
||||
'[SKIP] Skipped from Nav/Map screen, sheet closed.',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Failed to skip delivery. Please try again.',
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'[SKIP] Invalid deliveryId: $deliveryId or orderHeaderId: $orderHeaderId',
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Invalid delivery information.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[SKIP] Error updating skip status: $e');
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('An error occurred. Please try again.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 30),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Select Reason',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.cancel,
|
||||
color: Colors.red, size: 32),
|
||||
onPressed: isLoading ? null : () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
optionBox('Customer unreachable', 0),
|
||||
optionBox('Customer not at the location', 1),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: (selected == -1 || isLoading)
|
||||
? Colors.grey.shade300
|
||||
: ColorConstants.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed:
|
||||
(selected == -1 || isLoading) ? null : handleConfirm,
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'Confirm',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: (selected == -1 || isLoading)
|
||||
? Colors.black45
|
||||
: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
3085
lib/views/Dashboard/home/homepage.dart
Normal file
3085
lib/views/Dashboard/home/homepage.dart
Normal file
File diff suppressed because it is too large
Load Diff
145
lib/views/Dashboard/home/homepage_banner.dart
Normal file
145
lib/views/Dashboard/home/homepage_banner.dart
Normal file
@@ -0,0 +1,145 @@
|
||||
// Active Delivery Banner Widget for Home Page
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class ActiveDeliveryBanner extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> activeDeliveries;
|
||||
final Function(Map<String, dynamic>) onTap;
|
||||
|
||||
const ActiveDeliveryBanner({
|
||||
super.key,
|
||||
required this.activeDeliveries,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
String _getDeliveryAddress(Map<String, dynamic> delivery) {
|
||||
final address =
|
||||
delivery['deliveryaddress'] ??
|
||||
delivery['DeliveryAddress'] ??
|
||||
delivery['address'] ??
|
||||
'';
|
||||
if (address.toString().length > 40) {
|
||||
return '${address.toString().substring(0, 40)}...';
|
||||
}
|
||||
return address.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (activeDeliveries.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// Show first active delivery (or show count if multiple)
|
||||
final delivery = activeDeliveries.first;
|
||||
final count = activeDeliveries.length;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => onTap(delivery),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
// Active indicator icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.two_wheeler,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Delivery info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
count > 1
|
||||
? '$count Active Deliveries'
|
||||
: 'Active Delivery',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
if (count > 1) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'$count',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_getDeliveryAddress(delivery),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 17,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Arrow icon
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
611
lib/views/Dashboard/orders/orderstaus_button.dart
Normal file
611
lib/views/Dashboard/orders/orderstaus_button.dart
Normal file
@@ -0,0 +1,611 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'dart:async';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:slide_to_submit_button/slide_to_submit_button.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
/// ------------------------------
|
||||
/// MAIN WIDGET WITH TWO BUTTONS
|
||||
/// ------------------------------
|
||||
class OrderStatusRow extends StatefulWidget {
|
||||
final String currentStatus;
|
||||
final Future<bool> Function(String newStatus, {String? notes, String? proofImagePath}) onStatusChange;
|
||||
final bool enabled;
|
||||
|
||||
const OrderStatusRow({
|
||||
super.key,
|
||||
required this.currentStatus,
|
||||
required this.onStatusChange,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OrderStatusRow> createState() => _OrderStatusRowState();
|
||||
}
|
||||
|
||||
class _OrderStatusRowState extends State<OrderStatusRow> {
|
||||
bool _isProcessing = false;
|
||||
String? _overrideStatus;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant OrderStatusRow oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.currentStatus != widget.currentStatus &&
|
||||
mounted &&
|
||||
_overrideStatus != null &&
|
||||
widget.currentStatus == _overrideStatus) {
|
||||
setState(() {
|
||||
_overrideStatus = null;
|
||||
});
|
||||
} else if (oldWidget.currentStatus != widget.currentStatus &&
|
||||
_overrideStatus != null) {
|
||||
_overrideStatus = null;
|
||||
}
|
||||
}
|
||||
|
||||
String get _effectiveStatus => _overrideStatus ?? widget.currentStatus;
|
||||
|
||||
Future<bool> _onStatusChanged(String newStatus, {String? notes, String? proofImagePath}) async {
|
||||
debugPrint('[OSR] _onStatusChanged: $newStatus, proofImagePath: $proofImagePath');
|
||||
if (_isProcessing) return false;
|
||||
|
||||
// Optimistic UI: flip status immediately to avoid visible lag.
|
||||
setState(() {
|
||||
_isProcessing = true;
|
||||
_overrideStatus = newStatus;
|
||||
});
|
||||
|
||||
bool success = false;
|
||||
bool timedOut = false;
|
||||
try {
|
||||
// Hard cap wait time to keep UI from spinning indefinitely.
|
||||
success = await widget
|
||||
.onStatusChange(newStatus, notes: notes, proofImagePath: proofImagePath)
|
||||
.timeout(
|
||||
const Duration(seconds: 3),
|
||||
onTimeout: () {
|
||||
timedOut = true;
|
||||
// Assume success on timeout to avoid UI rollback; data will
|
||||
// refresh from server on next fetch.
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (_) {
|
||||
success = false;
|
||||
} finally {
|
||||
if (!mounted) return success;
|
||||
setState(() {
|
||||
_isProcessing = false;
|
||||
// If API failed, revert the optimistic status.
|
||||
// If timed out, keep the optimistic status (server likely completed).
|
||||
_overrideStatus = (success || timedOut) ? newStatus : null;
|
||||
});
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// REJECT SHEET
|
||||
// ------------------------------
|
||||
void _showRejectSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
String selectedReason = "";
|
||||
bool isLoading = false;
|
||||
final List<String> reasons = [
|
||||
"Customer not reachable",
|
||||
"Wrong address",
|
||||
"Out of delivery area",
|
||||
"Other reason",
|
||||
];
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Reject Order",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.red),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
for (String reason in reasons)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: GestureDetector(
|
||||
onTap: () => setModalState(() {
|
||||
selectedReason = reason;
|
||||
}),
|
||||
child: Container(
|
||||
height: 55,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: selectedReason == reason
|
||||
? Colors.red
|
||||
: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
reason,
|
||||
style: TextStyle(
|
||||
color: selectedReason == reason
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: selectedReason.isEmpty || isLoading
|
||||
? null
|
||||
: () async {
|
||||
setModalState(() => isLoading = true);
|
||||
final success = await _onStatusChanged(
|
||||
"REJECTED",
|
||||
notes: selectedReason,
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, success);
|
||||
}
|
||||
},
|
||||
child: isLoading
|
||||
? const CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
)
|
||||
: const Text(
|
||||
"Reject Order",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// CANCEL SHEET
|
||||
// ------------------------------
|
||||
void _showCancelSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
bool confirmCancel = false;
|
||||
bool isLoading = false;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Cancel this order?",
|
||||
style:
|
||||
TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
"Once cancelled, this order will return to pending state.",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
CheckboxListTile(
|
||||
value: confirmCancel,
|
||||
onChanged: (value) => setModalState(
|
||||
() => confirmCancel = value ?? false,
|
||||
),
|
||||
title: const Text("I confirm to cancel this order"),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: !confirmCancel
|
||||
? null
|
||||
: () async {
|
||||
setModalState(() => isLoading = true);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Order Cancelled"),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: isLoading
|
||||
? const CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
)
|
||||
: const Text(
|
||||
"Confirm Cancel",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String currentStatus = _effectiveStatus;
|
||||
final bool showCancel =
|
||||
currentStatus == "ARRIVED" || currentStatus == "PICKED";
|
||||
final String leftText = showCancel ? "CANCEL" : "REJECT";
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// LEFT BUTTON (Reject / Cancel)
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (showCancel) {
|
||||
_showCancelSheet(context);
|
||||
} else {
|
||||
_showRejectSheet(context);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red, //
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
leftText,
|
||||
style: TextStyle(
|
||||
fontSize: 19,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// RIGHT BUTTON (ACCEPT / ARRIVED / PICKED)
|
||||
OrderStatusButton(
|
||||
key: ValueKey(currentStatus),
|
||||
currentStatus: currentStatus,
|
||||
onStatusChange: (status, {proofImagePath}) =>
|
||||
_onStatusChanged(status, proofImagePath: proofImagePath),
|
||||
enabled: widget.enabled && !_isProcessing,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isProcessing)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.12),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(12),
|
||||
bottomRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
height: 22,
|
||||
width: 22,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ------------------------------
|
||||
/// STATUS BUTTON LOGIC (NO DELIVERY)
|
||||
/// ------------------------------
|
||||
class OrderStatusButton extends StatefulWidget {
|
||||
final String currentStatus;
|
||||
final Future<bool> Function(String newStatus, {String? proofImagePath}) onStatusChange;
|
||||
final bool enabled;
|
||||
|
||||
const OrderStatusButton({
|
||||
super.key,
|
||||
required this.currentStatus,
|
||||
required this.onStatusChange,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OrderStatusButton> createState() => _OrderStatusButtonState();
|
||||
}
|
||||
|
||||
class _OrderStatusButtonState extends State<OrderStatusButton> {
|
||||
String get _buttonText => widget.currentStatus;
|
||||
|
||||
Color _getButtonColor() {
|
||||
switch (_buttonText) {
|
||||
case "ARRIVED":
|
||||
return Colors.orange;
|
||||
case "PICKED":
|
||||
return ColorConstants.primaryColor;
|
||||
default:
|
||||
return Colors.green;
|
||||
}
|
||||
}
|
||||
|
||||
void _showStatusSheet() {
|
||||
if (_buttonText == "PICKED") return; // final stage now
|
||||
|
||||
bool isConfirmLoading = false;
|
||||
|
||||
// Determine next status (single step)
|
||||
String? nextStatus;
|
||||
if (_buttonText == "ACCEPT") {
|
||||
nextStatus = "ACCEPTED";
|
||||
} else if (_buttonText == "ACCEPTED") {
|
||||
nextStatus = "ARRIVED";
|
||||
} else if (_buttonText == "ARRIVED") {
|
||||
nextStatus = "PICKED";
|
||||
}
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
// If for some reason we don't have a valid next status, show nothing
|
||||
if (nextStatus == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Move Your Order to",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -5),
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.cancel,
|
||||
color: Colors.red,
|
||||
size: 36,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Slider to confirm moving to next status wrapped with SafeArea
|
||||
SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: SlideToSubmit.custom(
|
||||
height: 55,
|
||||
sliderWidth: 40,
|
||||
padding: const EdgeInsets.all(8),
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: _getButtonColor().withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
foregroundDecoration: BoxDecoration(
|
||||
color: _getButtonColor(),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
slider: Center(
|
||||
child: ClipOval(
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 24,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
hint: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Slide to mark $nextStatus',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: const Color.fromARGB(255, 15, 14, 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
onSubmit: (controller) async {
|
||||
if (isConfirmLoading) return;
|
||||
|
||||
setModalState(() => isConfirmLoading = true);
|
||||
|
||||
// Close sheet after slide completes
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
// Trigger status change callback
|
||||
WidgetsBinding.instance.addPostFrameCallback((
|
||||
_,
|
||||
) async {
|
||||
if (nextStatus == "PICKED") {
|
||||
debugPrint('[OSB] Status is PICKED, launching camera...');
|
||||
// Trigger Camera
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? photo = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 50, // Optimize size
|
||||
);
|
||||
|
||||
if (photo != null) {
|
||||
debugPrint('[OSB] Photo taken: ${photo.path}');
|
||||
// Process with image
|
||||
await widget.onStatusChange(
|
||||
nextStatus!,
|
||||
proofImagePath: photo.path,
|
||||
);
|
||||
} else {
|
||||
debugPrint('[OSB] Camera cancelled or photo null');
|
||||
}
|
||||
} else {
|
||||
debugPrint('[OSB] Status NOT PICKED (is $nextStatus), normal flow');
|
||||
// Normal flow
|
||||
await widget.onStatusChange(nextStatus!);
|
||||
}
|
||||
|
||||
try {
|
||||
controller.reset();
|
||||
} catch (_) {}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: widget.enabled ? _showStatusSheet : null,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.enabled ? _getButtonColor() : Colors.grey,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
_buttonText,
|
||||
style: TextStyle(
|
||||
fontSize: 19,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
464
lib/views/Dashboard/profile/Profilepage.dart
Normal file
464
lib/views/Dashboard/profile/Profilepage.dart
Normal file
@@ -0,0 +1,464 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; // ⭐ REQUIRED
|
||||
import 'package:nearle/views/Dashboard/profile/informations/help_center.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/profile.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/saved_address.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/faq.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/notifications_page.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/support_ticket.dart';
|
||||
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/views/onboardscreens/Sign_in.dart';
|
||||
import 'package:nearle/controllers/profile_controller.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/order_alert_sound.dart';
|
||||
import 'package:nearle/controllers/rewards_controller.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/rewards_card.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/rider_rewards_page.dart';
|
||||
import 'package:nearle/utils/mqtt_service.dart';
|
||||
|
||||
class ProfilePage extends StatefulWidget {
|
||||
const ProfilePage({super.key});
|
||||
|
||||
@override
|
||||
State<ProfilePage> createState() => _ProfilePageState();
|
||||
}
|
||||
|
||||
class _ProfilePageState extends State<ProfilePage> {
|
||||
late final ProfileController _profileController =
|
||||
Get.isRegistered<ProfileController>()
|
||||
? Get.find<ProfileController>()
|
||||
: Get.put(ProfileController(), permanent: true);
|
||||
|
||||
final RewardsController _rewardsController = Get.put(RewardsController());
|
||||
|
||||
String _name = '';
|
||||
String _email = '';
|
||||
String _contact = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProfilePrefs();
|
||||
ever(_profileController.userName, (_) => _assignFromController());
|
||||
ever(_profileController.userEmail, (_) => _assignFromController());
|
||||
ever(_profileController.userContact, (_) => _assignFromController());
|
||||
ever(_profileController.userAddress, (_) => _assignFromController());
|
||||
_profileController.loadFromPrefs();
|
||||
}
|
||||
|
||||
Future<void> _loadProfilePrefs() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_name = prefs.getString('user_name') ?? '';
|
||||
_email = prefs.getString('user_email') ?? '';
|
||||
_contact = prefs.getString('contactno') ?? '';
|
||||
_contact = prefs.getString('contactno') ?? '';
|
||||
});
|
||||
|
||||
final userId = prefs.getInt('userid') ?? 0;
|
||||
if (userId > 0) {
|
||||
_rewardsController.fetchBonusSummary(userId);
|
||||
}
|
||||
}
|
||||
|
||||
void _assignFromController() {
|
||||
setState(() {
|
||||
if (_profileController.userName.value.trim().isNotEmpty) {
|
||||
_name = _profileController.userName.value.trim();
|
||||
}
|
||||
if (_profileController.userEmail.value.trim().isNotEmpty) {
|
||||
_email = _profileController.userEmail.value.trim();
|
||||
}
|
||||
if (_profileController.userContact.value.trim().isNotEmpty) {
|
||||
_contact = _profileController.userContact.value.trim();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
elevation: 0,
|
||||
toolbarHeight: 70.h, // ⭐ responsive
|
||||
title: Padding(
|
||||
padding: EdgeInsets.only(top: 12.h),
|
||||
child: Text(
|
||||
"PROFILE",
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xxxLarge(context).sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1.h),
|
||||
child: Divider(height: 1.h, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
|
||||
body: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 40.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
/// ⭐ PROFILE CARD RESPONSIVE
|
||||
Container(
|
||||
padding: EdgeInsets.all(16.r),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor,
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 22.sp,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
_name.isNotEmpty ? _name : "—",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.phone,
|
||||
color: Colors.white,
|
||||
size: 20.sp,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
_contact.isNotEmpty ? _contact : "—",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.email,
|
||||
color: Colors.white,
|
||||
size: 20.sp,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
_email.isNotEmpty ? _email : "—",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
/// ⭐ Circle Avatar Responsive
|
||||
Obx(() {
|
||||
final path = _profileController.imagePath.value;
|
||||
final hasImage =
|
||||
path.isNotEmpty && File(path).existsSync();
|
||||
|
||||
return CircleAvatar(
|
||||
radius: 40.r,
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
backgroundImage: hasImage
|
||||
? FileImage(File(path))
|
||||
: null,
|
||||
child: !hasImage
|
||||
? Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 40.sp,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
// ⭐ REWARDS SECTION
|
||||
RewardsCard(controller: _rewardsController),
|
||||
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
_buildHeader("Your Information"),
|
||||
_buildBox([
|
||||
_buildInfoTile(Icons.person, "Profile"),
|
||||
Divider(),
|
||||
_buildInfoTile(Icons.location_on, "Saved Address"),
|
||||
Divider(),
|
||||
_buildInfoTile(Icons.card_giftcard, "Rewards"),
|
||||
Divider(),
|
||||
_buildInfoTile(Icons.notifications, "Notification"),
|
||||
]),
|
||||
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
_buildHeader("Support"),
|
||||
_buildBox([
|
||||
_buildInfoTile(Icons.support_agent, "Help Centre"),
|
||||
Divider(),
|
||||
_buildInfoTile(Icons.local_activity, "Support tickets"),
|
||||
]),
|
||||
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
_buildHeader("Other Information"),
|
||||
_buildBox([
|
||||
_buildInfoTile(Icons.translate, "Faq"),
|
||||
Divider(),
|
||||
// _buildInfoTile(Icons.sticky_note_2, "Terms & Conditions"),
|
||||
// Divider(),
|
||||
_buildInfoTile(
|
||||
Icons.notifications_active,
|
||||
"Order alert sound",
|
||||
),
|
||||
]),
|
||||
|
||||
SizedBox(height: 55.h),
|
||||
|
||||
/// ⭐ Logout Button Responsive
|
||||
SizedBox(
|
||||
height: 55.h,
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _showLogoutDialog(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.secondaryColor,
|
||||
side: BorderSide(color: Colors.black, width: 0.2.w),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Logout",
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 15.h),
|
||||
|
||||
Center(
|
||||
child: Text(
|
||||
"App Version 1.2.19",
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
color: Colors.grey.shade600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(String title) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(8.r),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBox(List<Widget> children) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Column(children: children),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoTile(IconData icon, String title) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, size: 26.sp),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
trailing: Icon(Icons.arrow_forward_ios, size: 22.sp),
|
||||
onTap: () => _handleNavigation(title),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleNavigation(String title) {
|
||||
switch (title) {
|
||||
case "Profile":
|
||||
Get.to(() => Profile());
|
||||
break;
|
||||
case "Saved Address":
|
||||
Get.to(() => const SavedAddressPage());
|
||||
break;
|
||||
case "Notification":
|
||||
Get.to(() => const NotificationsPage());
|
||||
break;
|
||||
case "Help Centre":
|
||||
Get.to(() => HelpCenter());
|
||||
break;
|
||||
case "Support tickets":
|
||||
Get.to(() => SupportTicket());
|
||||
break;
|
||||
case "Rewards":
|
||||
Get.to(() => const RiderRewardsPage());
|
||||
break;
|
||||
case "Faq":
|
||||
Get.to(() => const FaqPage());
|
||||
break;
|
||||
// case "Terms & Conditions":
|
||||
// Get.to(() => const TermsCondition());
|
||||
// break;
|
||||
case "Order alert sound":
|
||||
Get.to(() => const OrderAlertSoundPage());
|
||||
break;
|
||||
default:
|
||||
debugPrint("Tapped on $title — no page linked yet.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showLogoutDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return Center(
|
||||
child: FittedBox(
|
||||
child: AlertDialog(
|
||||
backgroundColor: ColorConstants.secondaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
title: Text(
|
||||
"Logout",
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
"Are you sure you want to logout?",
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
"No",
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color.fromARGB(255, 153, 121, 167),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userId =
|
||||
prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
|
||||
if (userId != 0) {
|
||||
await prefs.remove('skipped_orders_cache_$userId');
|
||||
}
|
||||
NearleMqttService().disconnect();
|
||||
prefs.setBool('logged_out', true);
|
||||
Get.offAll(() => const SignIn());
|
||||
},
|
||||
child: Text(
|
||||
"Yes",
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
95
lib/views/Dashboard/profile/informations/faq.dart
Normal file
95
lib/views/Dashboard/profile/informations/faq.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class FaqController extends GetxController {
|
||||
WebViewController? webViewController;
|
||||
var isLoading = true.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
initializeWebView();
|
||||
}
|
||||
|
||||
void initializeWebView() {
|
||||
webViewController = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(const Color(0x00000000))
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageStarted: (url) {
|
||||
isLoading.value = true;
|
||||
print('Started loading: $url');
|
||||
},
|
||||
onPageFinished: (url) {
|
||||
isLoading.value = false;
|
||||
print('Finished loading: $url');
|
||||
},
|
||||
onWebResourceError: (error) {
|
||||
isLoading.value = false;
|
||||
print('WebView error: ${error.description}');
|
||||
},
|
||||
),
|
||||
);
|
||||
loadFaqUrl();
|
||||
}
|
||||
|
||||
Future<void> loadFaqUrl() async {
|
||||
if (webViewController != null) {
|
||||
try {
|
||||
await webViewController!.loadRequest(
|
||||
Uri.parse('https://nearle.in/faq'),
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error loading URL: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FaqPage extends StatelessWidget {
|
||||
const FaqPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(FaqController());
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70,
|
||||
leading: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Colors.white,
|
||||
), // :small_blue_diamond: white back arrow
|
||||
onPressed: () {
|
||||
Navigator.pop(context); // goes back to previous screen
|
||||
},
|
||||
),
|
||||
title: const Text(
|
||||
'FAQ',
|
||||
style: TextStyle(
|
||||
fontSize: 26, // :small_blue_diamond: larger font size
|
||||
color: Colors.white, // :small_blue_diamond: white text
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
body: Obx(() {
|
||||
final wvc = controller.webViewController;
|
||||
return Stack(
|
||||
children: [
|
||||
if (wvc != null) WebViewWidget(controller: wvc),
|
||||
if (controller.isLoading.value)
|
||||
const LinearProgressIndicator(minHeight: 2),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
380
lib/views/Dashboard/profile/informations/help_center.dart
Normal file
380
lib/views/Dashboard/profile/informations/help_center.dart
Normal file
@@ -0,0 +1,380 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
class HelpCenter extends StatelessWidget {
|
||||
const HelpCenter({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: const Text(
|
||||
'Help Center',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header text
|
||||
Text(
|
||||
"We're here to help you with anything and \neverything on Nearle Xpress",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontFamily: FontConstants.fontFamily
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"We make sure your delivery experience is smooth and clear. Whether you’re on your first trip or your hundredth, we’ve got your back. Browse through frequently asked questions or reach out directly if you need further help.",
|
||||
style: TextStyle(fontSize: 18,color: Colors.grey.shade700, height: 1.4, fontFamily: FontConstants.fontFamily),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search help',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 12),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
Text(
|
||||
'FAQ',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily,color: ColorConstants.primaryColor),
|
||||
),
|
||||
Divider(),
|
||||
|
||||
|
||||
|
||||
_FaqTile(
|
||||
title: 'What is Nearle Xpress?',
|
||||
initiallyExpanded: true,
|
||||
child: const Text(
|
||||
'Nearle Xpress is a delivery app for riders who complete local deliveries for nearby stores and markets. It helps riders accept tasks, manage pickup and drop points, and update deliveries in real time.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
|
||||
),
|
||||
),
|
||||
_FaqTile(
|
||||
title: 'How do I accept a delivery task?',
|
||||
child: const Text(
|
||||
'You can accept tasks from the Home screen when a new order appears. Tap on the order to view details and then press Accept.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
|
||||
),
|
||||
),
|
||||
_FaqTile(
|
||||
title: 'How do I update the delivery status?',
|
||||
child: const Text(
|
||||
'Open the active task and use the status buttons to mark Pickup, On the way, and Delivered. Ensure accurate updates for better tracking.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
|
||||
),
|
||||
),
|
||||
_FaqTile(
|
||||
title: 'Can I view my past deliveries?',
|
||||
child: const Text(
|
||||
'Yes. Go to the History section from your dashboard to see completed deliveries and earnings.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
|
||||
),
|
||||
),
|
||||
_FaqTile(
|
||||
title: 'What if I face an issue during delivery?',
|
||||
child: const Text(
|
||||
'Use the Help Center to report an issue or contact support. Provide order details and a short description of the problem.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10,),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text("Still stuck? Help is a mail away!",style: TextStyle(fontSize: 18,fontFamily: FontConstants.fontFamily,fontWeight: FontWeight.bold,color: ColorConstants.primaryColor),),
|
||||
],
|
||||
)
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Padding(padding: EdgeInsets.all(16),
|
||||
child: SizedBox(
|
||||
height: 55,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
foregroundColor: ColorConstants.primaryColor,
|
||||
side: BorderSide(color: ColorConstants.primaryColor, width: 1.2),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const HelpCenterMessage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'Send a message',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold,fontFamily: FontConstants.fontFamily,color: Colors.white),
|
||||
),
|
||||
),
|
||||
),),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FaqTile extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget child;
|
||||
final bool initiallyExpanded;
|
||||
|
||||
const _FaqTile({
|
||||
required this.title,
|
||||
required this.child,
|
||||
this.initiallyExpanded = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
initiallyExpanded: initiallyExpanded,
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
collapsedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(fontWeight: FontWeight.w600,fontSize: 18,fontFamily: FontConstants.fontFamily),
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// -------------------------------message page------------------------------------
|
||||
class HelpCenterMessage extends StatefulWidget {
|
||||
const HelpCenterMessage({super.key});
|
||||
|
||||
@override
|
||||
State<HelpCenterMessage> createState() => _HelpCenterMessageState();
|
||||
}
|
||||
|
||||
class _HelpCenterMessageState extends State<HelpCenterMessage> {
|
||||
final _subjectController = TextEditingController();
|
||||
final _messageController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subjectController.dispose();
|
||||
_messageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 80,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
'Help Centre',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Heading
|
||||
Text(
|
||||
'Send Us a Message',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Not finding what you're looking for in the FAQs? Don't worry—we're here to help!",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.grey.shade700,
|
||||
height: 1.4,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Subject label
|
||||
Text(
|
||||
'Subject',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _subjectController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type Something',
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.5),
|
||||
),
|
||||
),
|
||||
validator: (v) => (v == null || v.trim().isEmpty) ? 'Please enter a subject' : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// Message label
|
||||
Text(
|
||||
'Your Message',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _messageController,
|
||||
minLines: 5,
|
||||
maxLines: 8,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type Something',
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
alignLabelWithHint: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.5),
|
||||
),
|
||||
),
|
||||
validator: (v) => (v == null || v.trim().isEmpty) ? 'Please enter your message' : null,
|
||||
),
|
||||
SizedBox(height: 30,),
|
||||
SizedBox(
|
||||
height: 55,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: () {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Message sent')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'Send a message',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
176
lib/views/Dashboard/profile/informations/notifications_page.dart
Normal file
176
lib/views/Dashboard/profile/informations/notifications_page.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class NotificationsPage extends StatefulWidget {
|
||||
const NotificationsPage({super.key});
|
||||
|
||||
@override
|
||||
State<NotificationsPage> createState() => _NotificationsPageState();
|
||||
}
|
||||
|
||||
class _NotificationsPageState extends State<NotificationsPage> {
|
||||
List<Map<String, dynamic>> _items = const [];
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString('notifications_log');
|
||||
List<Map<String, dynamic>> parsed = [];
|
||||
if (raw != null && raw.isNotEmpty) {
|
||||
try {
|
||||
final list = jsonDecode(raw) as List<dynamic>;
|
||||
parsed = list.map((e) => (e as Map).map((k, v) => MapEntry(k.toString(), v))).toList();
|
||||
} catch (_) {}
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_items = parsed;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70, // increases AppBar height
|
||||
elevation: 4,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white), // white back arrow
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
'Notifications',
|
||||
style: const TextStyle(
|
||||
fontSize: 26, // larger font size
|
||||
color: Colors.white, // white text
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
).copyWith(fontFamily: FontConstants.fontFamily), // keep your font
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_sweep, color: Colors.white), // white icon
|
||||
onPressed: () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('notifications_log');
|
||||
if (!mounted) return;
|
||||
setState(() => _items = const []);
|
||||
// ignore: use_build_context_synchronously
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Notifications cleared')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
body: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _items.isEmpty
|
||||
? Center(child: Text('No notifications yet',style: TextStyle(fontSize: 20,fontFamily: FontConstants.fontFamily),))
|
||||
: RefreshIndicator(
|
||||
onRefresh: _load,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: _items.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final it = _items[index];
|
||||
final title = (it['title'] ?? 'Nearle').toString();
|
||||
final body = (it['body'] ?? '').toString();
|
||||
final time = (it['time'] ?? '').toString();
|
||||
final imageUrl = (it['imageUrl'] ?? '').toString();
|
||||
final imagePath = (it['imagePath'] ?? '').toString();
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 1.5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.notifications_active, color: Colors.purple),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
if (time.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
time,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (body.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
body,
|
||||
style: TextStyle(fontFamily: FontConstants.fontFamily, fontSize: 14),
|
||||
),
|
||||
),
|
||||
if (imagePath.isNotEmpty || imageUrl.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: imagePath.isNotEmpty
|
||||
? Image.file(
|
||||
File(imagePath),
|
||||
height: 170,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Image.network(
|
||||
imageUrl,
|
||||
height: 170,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
206
lib/views/Dashboard/profile/informations/order_alert_sound.dart
Normal file
206
lib/views/Dashboard/profile/informations/order_alert_sound.dart
Normal file
@@ -0,0 +1,206 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class OrderAlertSoundPage extends StatefulWidget {
|
||||
const OrderAlertSoundPage({super.key});
|
||||
|
||||
@override
|
||||
State<OrderAlertSoundPage> createState() => _OrderAlertSoundPageState();
|
||||
}
|
||||
|
||||
class _OrderAlertSoundPageState extends State<OrderAlertSoundPage> {
|
||||
static const String _prefsKey = 'order_alert_sound';
|
||||
static const String _defaultSound = 'assets/audio/alert-1.mp3';
|
||||
|
||||
final AudioPlayer _player = AudioPlayer();
|
||||
String _selected = _defaultSound;
|
||||
bool _loading = true;
|
||||
|
||||
// Available sounds from assets/audio/ folder
|
||||
final List<_SoundOption> _options = const [
|
||||
_SoundOption(
|
||||
label: 'Alert 1 (Default)',
|
||||
assetPath: 'assets/audio/alert-1.mp3',
|
||||
),
|
||||
_SoundOption(label: 'Alert 2', assetPath: 'assets/audio/alert-2.mp3'),
|
||||
_SoundOption(label: 'Alert 3', assetPath: 'assets/audio/alert-3.mp3'),
|
||||
_SoundOption(label: 'Alert 4', assetPath: 'assets/audio/alert-4.mp3'),
|
||||
_SoundOption(label: 'Alert 5', assetPath: 'assets/audio/alert-5.mp3'),
|
||||
_SoundOption(label: 'Alert 6', assetPath: 'assets/audio/alert-6.mp3'),
|
||||
_SoundOption(label: 'Alert 7', assetPath: 'assets/audio/alert-7.mp3'),
|
||||
_SoundOption(label: 'Alert 8', assetPath: 'assets/audio/alert-8.mp3'),
|
||||
_SoundOption(label: 'Alert 9', assetPath: 'assets/audio/alert-9.mp3'),
|
||||
_SoundOption(label: 'Alert 10', assetPath: 'assets/audio/alert-10.mp3'),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSelection();
|
||||
}
|
||||
|
||||
Future<void> _loadSelection() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final saved = prefs.getString(_prefsKey);
|
||||
setState(() {
|
||||
_selected = (saved != null && saved.isNotEmpty) ? saved : _defaultSound;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveSelection(BuildContext context, String assetPath) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefsKey, assetPath);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Order alert sound updated'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _preview(BuildContext context, String assetPath) async {
|
||||
try {
|
||||
await _player.stop();
|
||||
await _player.play(AssetSource(assetPath.replaceFirst('assets/', '')));
|
||||
// Note: AssetSource expects relative to assets/ root; hence replaceFirst
|
||||
} catch (_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Preview unavailable for: $assetPath'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_player.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70, // :small_blue_diamond: increases app bar height
|
||||
leading: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Colors.white,
|
||||
), // :small_blue_diamond: white back arrow
|
||||
onPressed: () {
|
||||
Navigator.pop(context); // goes back to previous screen
|
||||
},
|
||||
),
|
||||
title: const Text(
|
||||
'Orders alert Sound',
|
||||
style: TextStyle(
|
||||
fontSize: 26, // :small_blue_diamond: larger font size
|
||||
color: Colors.white, // :small_blue_diamond: white text
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
body: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
color: Colors.grey.shade200,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.volume_up, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Current: ${_options.firstWhereOrNull((o) => o.assetPath == _selected)?.label ?? 'Unknown'}',
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'Tap a sound to select',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: _options.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final opt = _options[index];
|
||||
final isSelected = _selected == opt.assetPath;
|
||||
return ListTile(
|
||||
title: Text(
|
||||
opt.label,
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
leading: Radio<String>(
|
||||
value: opt.assetPath,
|
||||
groupValue: _selected,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() => _selected = value);
|
||||
_saveSelection(context, value);
|
||||
},
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
onPressed: () => _preview(context, opt.assetPath),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => _selected = opt.assetPath);
|
||||
_saveSelection(context, opt.assetPath);
|
||||
},
|
||||
subtitle: isSelected
|
||||
? const Text(
|
||||
'Selected',
|
||||
style: TextStyle(fontSize: 12),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SoundOption {
|
||||
final String label;
|
||||
final String assetPath;
|
||||
const _SoundOption({required this.label, required this.assetPath});
|
||||
}
|
||||
280
lib/views/Dashboard/profile/informations/profile.dart
Normal file
280
lib/views/Dashboard/profile/informations/profile.dart
Normal file
@@ -0,0 +1,280 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/controllers/profile_controller.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class Profile extends StatefulWidget {
|
||||
const Profile({super.key});
|
||||
|
||||
@override
|
||||
State<Profile> createState() => _ProfileState();
|
||||
}
|
||||
|
||||
class _ProfileState extends State<Profile> {
|
||||
String _name = '';
|
||||
String _email = '';
|
||||
String _contact = '';
|
||||
String _address = '';
|
||||
final List<Worker> _workers = [];
|
||||
late final ProfileController _profileController =
|
||||
Get.isRegistered<ProfileController>()
|
||||
? Get.find<ProfileController>()
|
||||
: Get.put(ProfileController(), permanent: true);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProfile();
|
||||
// Keep in sync with controller
|
||||
_workers.addAll([
|
||||
ever(_profileController.userName, (_) => _assignFromController()),
|
||||
ever(_profileController.userEmail, (_) => _assignFromController()),
|
||||
ever(_profileController.userContact, (_) => _assignFromController()),
|
||||
ever(_profileController.userAddress, (_) => _assignFromController()),
|
||||
]);
|
||||
_profileController.loadFromPrefs();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final worker in _workers) {
|
||||
worker.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadProfile() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_name = prefs.getString('user_name') ?? '';
|
||||
_email = prefs.getString('user_email') ?? '';
|
||||
_contact = prefs.getString('contactno') ?? '';
|
||||
_address = prefs.getString('user_address') ?? '';
|
||||
});
|
||||
debugPrint('[PROFILE_DETAILS] Loaded - Name: "$_name", Email: "$_email", Contact: "$_contact"');
|
||||
}
|
||||
|
||||
void _assignFromController() {
|
||||
setState(() {
|
||||
if (_profileController.userName.value.trim().isNotEmpty) {
|
||||
_name = _profileController.userName.value.trim();
|
||||
}
|
||||
if (_profileController.userEmail.value.trim().isNotEmpty) {
|
||||
_email = _profileController.userEmail.value.trim();
|
||||
}
|
||||
if (_profileController.userContact.value.trim().isNotEmpty) {
|
||||
_contact = _profileController.userContact.value.trim();
|
||||
}
|
||||
if (_profileController.userAddress.value.trim().isNotEmpty) {
|
||||
_address = _profileController.userAddress.value.trim();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final width = size.width;
|
||||
// ignore: unused_local_variable
|
||||
final height = size.height;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(width * 0.04),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Profile image
|
||||
Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
// White border circle
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4), // border thickness
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white, // white border
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 60,
|
||||
backgroundColor: Colors.grey.shade400,
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
size: 60,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Name
|
||||
_buildLabel("Enter name", required: true),
|
||||
_buildTextField(
|
||||
hintText: _name.isNotEmpty ? _name : "EX: Vijayan",
|
||||
value: _name.isNotEmpty ? _name : null,
|
||||
readOnly: true,
|
||||
disabled: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Contact No
|
||||
_buildLabel("Contact no", required: true),
|
||||
_buildTextField(
|
||||
hintText: _contact.isNotEmpty
|
||||
? "+91 $_contact"
|
||||
: "EX: +91 8838304677",
|
||||
value: _contact.isNotEmpty ? "+91 $_contact" : null,
|
||||
keyboardType: TextInputType.phone,
|
||||
readOnly: true,
|
||||
disabled: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Email Id
|
||||
_buildLabel("Email Id"),
|
||||
_buildTextField(
|
||||
hintText: _email.isNotEmpty ? _email : "EX: gmail@gmail.com",
|
||||
value: _email.isNotEmpty ? _email : null,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
readOnly: true,
|
||||
disabled: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Location
|
||||
_buildLabel("Address"),
|
||||
_buildTextField(hintText: _address.isNotEmpty ? _address : " EX: R.s puram", value: _address.isNotEmpty ? _address : null, readOnly: true, disabled: true),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(width * 0.04),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: _handleBackNavigation,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF5C1D8D), // Purple button color
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Back",
|
||||
style: TextStyle(
|
||||
fontSize: 21,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleBackNavigation() async {
|
||||
final navigator = Navigator.of(context);
|
||||
if (navigator.canPop()) {
|
||||
navigator.pop();
|
||||
return;
|
||||
}
|
||||
final rootNavigator = Get.key.currentState;
|
||||
if (rootNavigator != null && rootNavigator.canPop()) {
|
||||
rootNavigator.pop();
|
||||
return;
|
||||
}
|
||||
if (Get.isOverlaysOpen) {
|
||||
Get.back(closeOverlays: true);
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
}
|
||||
|
||||
// Text label widget
|
||||
Widget _buildLabel(String text, {bool required = false}) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
children: required
|
||||
? const [
|
||||
TextSpan(
|
||||
text: " *",
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Reusable TextField
|
||||
Widget _buildTextField({
|
||||
required String hintText,
|
||||
String? value,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
required bool readOnly,
|
||||
bool disabled = false,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
child: SizedBox(
|
||||
height: 55,
|
||||
width: 350,
|
||||
child: TextFormField(
|
||||
keyboardType: keyboardType,
|
||||
readOnly: readOnly,
|
||||
enabled: !disabled,
|
||||
enableInteractiveSelection: false,
|
||||
initialValue: value,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/controllers/rewards_controller.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/rewards_card.dart';
|
||||
|
||||
class RiderRewardsPage extends StatelessWidget {
|
||||
const RiderRewardsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final RewardsController rewardsController = Get.isRegistered<RewardsController>()
|
||||
? Get.find<RewardsController>()
|
||||
: Get.put(RewardsController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
"REWARDS",
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: FontConstants.xxxLarge(context).sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
|
||||
child: RewardsCard(
|
||||
controller: rewardsController,
|
||||
showFullDetails: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
169
lib/views/Dashboard/profile/informations/saved_address.dart
Normal file
169
lib/views/Dashboard/profile/informations/saved_address.dart
Normal file
@@ -0,0 +1,169 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
|
||||
class SavedAddressPage extends StatefulWidget {
|
||||
const SavedAddressPage({super.key});
|
||||
|
||||
@override
|
||||
State<SavedAddressPage> createState() => _SavedAddressPageState();
|
||||
}
|
||||
|
||||
class _SavedAddressPageState extends State<SavedAddressPage> {
|
||||
final TextEditingController _addressController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadAddress();
|
||||
}
|
||||
|
||||
Future<void> _loadAddress() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final address = (prefs.getString('user_address') ?? '').trim();
|
||||
_addressController.text = address;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_addressController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final addressText = _addressController.text.trim();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FB),
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70,
|
||||
elevation: 3,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
'Saved Address',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: size.width * 0.05, vertical: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 🏠 Header Section
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.15),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Icon(
|
||||
Icons.location_on_rounded,
|
||||
color: ColorConstants.primaryColor,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Current Address',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
addressText.isNotEmpty ? addressText : 'No address saved yet.',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.grey.shade700,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 28),
|
||||
|
||||
// ✨ Info Section
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline_rounded,
|
||||
color: ColorConstants.primaryColor, size: 24),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Your saved address is used for deliveries, pickups, and nearby service accuracy.',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade800,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
503
lib/views/Dashboard/profile/informations/support_ticket.dart
Normal file
503
lib/views/Dashboard/profile/informations/support_ticket.dart
Normal file
@@ -0,0 +1,503 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:nearle/controllers/support_ticket.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class SupportTicket extends StatefulWidget {
|
||||
const SupportTicket({super.key});
|
||||
|
||||
@override
|
||||
State<SupportTicket> createState() => _SupportTicketState();
|
||||
}
|
||||
|
||||
class _SupportTicketState extends State<SupportTicket>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController;
|
||||
|
||||
// Form
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _subjectCtrl = TextEditingController();
|
||||
final _messageCtrl = TextEditingController();
|
||||
String _category = 'Account';
|
||||
String _priority = 'Medium';
|
||||
int _attachmentCount = 0;
|
||||
|
||||
// Image
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
final List<XFile> _attachments = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
_tabController.addListener(() => setState(() {}));
|
||||
Get.put(SupportTicketController());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subjectCtrl.dispose();
|
||||
_messageCtrl.dispose();
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
'Support Ticket',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
indicatorColor: ColorConstants.primaryColor,
|
||||
labelColor: ColorConstants.primaryColor,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
labelStyle: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
'Create Tickets',
|
||||
style: TextStyle(fontFamily: FontConstants.fontFamily, fontSize: 20),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'My Tickets',
|
||||
style: TextStyle(fontFamily: FontConstants.fontFamily, fontSize: 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [_buildCreateForm(), _buildTicketsList()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: _tabController.index == 0
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SizedBox(
|
||||
height: 55,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: _submitTicket,
|
||||
child: Text(
|
||||
'Submit Ticket',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// CREATE FORM
|
||||
// ===============================================
|
||||
Widget _buildCreateForm() {
|
||||
final controller = Get.find<SupportTicketController>();
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Create a new support ticket',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tell us what went wrong. We\'ll get back to you as soon as possible.',
|
||||
style: TextStyle(fontSize: 18, color: Colors.grey.shade700, height: 1.4, fontFamily: FontConstants.fontFamily),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Category
|
||||
Text('Category', style: _labelStyle()),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
// ignore: deprecated_member_use
|
||||
value: _category,
|
||||
items: ['Account', 'Orders', 'Payments', 'App issue', 'Other']
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(e, style: TextStyle(fontFamily: FontConstants.fontFamily))))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _category = v ?? _category),
|
||||
decoration: _inputDecoration(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Priority
|
||||
Text('Priority', style: _labelStyle()),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: ['Low', 'Medium', 'High'].map((p) {
|
||||
final selected = _priority == p;
|
||||
return ChoiceChip(
|
||||
label: Text(p, style: TextStyle(fontFamily: FontConstants.fontFamily, fontWeight: FontWeight.w600, fontSize: 16)),
|
||||
selected: selected,
|
||||
selectedColor: ColorConstants.primaryColor.withOpacity(0.15),
|
||||
labelStyle: TextStyle(
|
||||
color: selected ? ColorConstants.primaryColor : Colors.black87,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
onSelected: (_) => setState(() => _priority = p),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Subject
|
||||
Text('Subject', style: _labelStyle()),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _subjectCtrl,
|
||||
decoration: _inputDecoration(hint: 'Type Something'),
|
||||
style: TextStyle(fontFamily: FontConstants.fontFamily),
|
||||
validator: (v) => (v == null || v.trim().isEmpty) ? 'Please enter a subject' : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Message
|
||||
Text('Describe the issue', style: _labelStyle()),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _messageCtrl,
|
||||
minLines: 5,
|
||||
maxLines: 8,
|
||||
decoration: _inputDecoration(hint: 'Type Something'),
|
||||
style: TextStyle(fontFamily: FontConstants.fontFamily),
|
||||
validator: (v) => (v == null || v.trim().isEmpty) ? 'Please enter your message' : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Attachments
|
||||
Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: _addAttachment,
|
||||
icon: const Icon(Icons.attach_file),
|
||||
label: Text('Add screenshot', style: TextStyle(fontFamily: FontConstants.fontFamily)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
if (_attachmentCount > 0)
|
||||
Text('$_attachmentCount attached', style: TextStyle(fontWeight: FontWeight.w600, fontFamily: FontConstants.fontFamily)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_attachments.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _attachments.asMap().entries.map((entry) {
|
||||
final idx = entry.key;
|
||||
final file = entry.value;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.file(File(file.path), width: 80, height: 80, fit: BoxFit.cover),
|
||||
),
|
||||
Positioned(
|
||||
top: -8,
|
||||
right: -8,
|
||||
child: InkWell(
|
||||
onTap: () => setState(() {
|
||||
_attachments.removeAt(idx);
|
||||
_attachmentCount = _attachments.length;
|
||||
}),
|
||||
child: Container(
|
||||
width: 22,
|
||||
height: 22,
|
||||
decoration: BoxDecoration(color: Colors.black.withOpacity(0.6), shape: BoxShape.circle),
|
||||
child: const Icon(Icons.close, size: 16, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
// Submit loading
|
||||
Obx(() => controller.isSubmitting.value
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// MY TICKETS LIST
|
||||
// ===============================================
|
||||
Widget _buildTicketsList() {
|
||||
final controller = Get.find<SupportTicketController>();
|
||||
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.errorMessage.value.isNotEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: Colors.red),
|
||||
const SizedBox(height: 12),
|
||||
Text('Failed to load tickets', style: TextStyle(fontFamily: FontConstants.fontFamily, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
Text(controller.errorMessage.value, textAlign: TextAlign.center, style: TextStyle(color: Colors.grey.shade600, fontFamily: FontConstants.fontFamily)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(onPressed: controller.fetchTickets, child: const Text('Retry')),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.tickets.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
itemCount: controller.tickets.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, i) {
|
||||
final t = controller.tickets[i];
|
||||
final statusColor = _getPriorityColor(t.priority);
|
||||
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 2,
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
title: Text(t.subject, style: TextStyle(fontWeight: FontWeight.w700, fontFamily: FontConstants.fontFamily)),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text('Category: ${t.category} • Priority: ${t.priority}', style: TextStyle(fontFamily: FontConstants.fontFamily)),
|
||||
const SizedBox(height: 4),
|
||||
Text('Created: ${_formatDate(t.created)}', style: TextStyle(fontFamily: FontConstants.fontFamily)),
|
||||
],
|
||||
),
|
||||
trailing: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(color: statusColor.withOpacity(0.15), borderRadius: BorderRadius.circular(20)),
|
||||
child: Text(t.priority, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily)),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.support_agent, size: 48, color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
Text('No tickets yet', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, fontFamily: FontConstants.fontFamily)),
|
||||
const SizedBox(height: 8),
|
||||
Text('Create your first ticket from the Create tab.', style: TextStyle(color: Colors.grey.shade700, fontFamily: FontConstants.fontFamily)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// HELPERS
|
||||
// ===============================================
|
||||
TextStyle _labelStyle() => TextStyle(fontSize: 20, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily);
|
||||
|
||||
InputDecoration _inputDecoration({String? hint}) {
|
||||
return InputDecoration(
|
||||
hintText: hint,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.grey.shade300)),
|
||||
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.grey.shade300)),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.5)),
|
||||
hintStyle: TextStyle(fontFamily: FontConstants.fontFamily),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getPriorityColor(String priority) {
|
||||
return switch (priority.toLowerCase()) {
|
||||
'high' => Colors.red,
|
||||
'medium' => Colors.orange,
|
||||
'low' => Colors.green,
|
||||
_ => Colors.grey,
|
||||
};
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.day}/${date.month}/${date.year} ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// IMAGE PICKER
|
||||
// ===============================================
|
||||
Future<void> _addAttachment() async {
|
||||
final source = await showModalBottomSheet<ImageSource>(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
builder: (ctx) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: Text('Gallery', style: TextStyle(fontFamily: FontConstants.fontFamily)),
|
||||
onTap: () => Navigator.pop(ctx, ImageSource.gallery),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: Text('Camera', style: TextStyle(fontFamily: FontConstants.fontFamily)),
|
||||
onTap: () => Navigator.pop(ctx, ImageSource.camera),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (source == null) return;
|
||||
|
||||
try {
|
||||
if (source == ImageSource.gallery) {
|
||||
final multi = await _picker.pickMultiImage(imageQuality: 85);
|
||||
if (multi.isNotEmpty) {
|
||||
setState(() => _attachments.addAll(multi));
|
||||
} else {
|
||||
final one = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 85);
|
||||
if (one != null) setState(() => _attachments.add(one));
|
||||
}
|
||||
} else {
|
||||
final captured = await _picker.pickImage(source: ImageSource.camera, imageQuality: 85);
|
||||
if (captured != null) setState(() => _attachments.add(captured));
|
||||
}
|
||||
} catch (_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to pick image', style: TextStyle(fontFamily: FontConstants.fontFamily))),
|
||||
);
|
||||
}
|
||||
|
||||
setState(() => _attachmentCount = _attachments.length);
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// SUBMIT TICKET
|
||||
// ===============================================
|
||||
Future<void> _submitTicket() async {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
|
||||
final controller = Get.find<SupportTicketController>();
|
||||
final success = await controller.createTicket(
|
||||
userid: 1242,
|
||||
category: _category,
|
||||
priority: _priority,
|
||||
subject: _subjectCtrl.text.trim(),
|
||||
issue: _messageCtrl.text.trim(),
|
||||
attachments: _attachments.isEmpty ? null : _attachments,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
_subjectCtrl.clear();
|
||||
_messageCtrl.clear();
|
||||
_attachments.clear();
|
||||
_attachmentCount = 0;
|
||||
setState(() {});
|
||||
|
||||
_tabController.animateTo(1);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Ticket Submitted!'),
|
||||
content: const Text('Your ticket has been created and saved. Our team will get back to you soon.'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK')),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to submit ticket: ${controller.errorMessage.value}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
100
lib/views/Dashboard/profile/informations/terms_condition.dart
Normal file
100
lib/views/Dashboard/profile/informations/terms_condition.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
// ===== Controller =====
|
||||
class TermsController extends GetxController {
|
||||
WebViewController? webViewController;
|
||||
var isLoading = true.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
initializeWebView();
|
||||
}
|
||||
|
||||
void initializeWebView() {
|
||||
webViewController = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(const Color(0x00000000))
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageStarted: (url) {
|
||||
isLoading.value = true;
|
||||
print('Started loading: $url');
|
||||
},
|
||||
onPageFinished: (url) {
|
||||
isLoading.value = false;
|
||||
print('Finished loading: $url');
|
||||
},
|
||||
onWebResourceError: (error) {
|
||||
isLoading.value = false;
|
||||
print('WebView error: ${error.description}');
|
||||
},
|
||||
),
|
||||
);
|
||||
loadTermsUrl();
|
||||
}
|
||||
|
||||
Future<void> loadTermsUrl() async {
|
||||
if (webViewController != null) {
|
||||
try {
|
||||
await webViewController!.loadRequest(
|
||||
Uri.parse('https://nearle.in/terms'),
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error loading URL: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Page =====
|
||||
class TermsCondition extends StatelessWidget {
|
||||
const TermsCondition({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(TermsController());
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70,
|
||||
leading: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: const Text(
|
||||
'Terms & Conditions',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Obx(() {
|
||||
final wvc = controller.webViewController;
|
||||
return Stack(
|
||||
children: [
|
||||
if (wvc != null) WebViewWidget(controller: wvc),
|
||||
if (controller.isLoading.value)
|
||||
const LinearProgressIndicator(minHeight: 2),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
474
lib/views/Dashboard/profile/rewards_card.dart
Normal file
474
lib/views/Dashboard/profile/rewards_card.dart
Normal file
@@ -0,0 +1,474 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/controllers/rewards_controller.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/rider_rewards_page.dart';
|
||||
|
||||
class RewardsCard extends StatelessWidget {
|
||||
final RewardsController controller;
|
||||
final bool showFullDetails;
|
||||
|
||||
const RewardsCard({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.showFullDetails = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Original Rewards Card (The Gradient Card)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (!showFullDetails) {
|
||||
Get.to(() => const RiderRewardsPage());
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(16.r),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF2C3E50), // Dark blue/grey
|
||||
Color(0xFF4CA1AF), // Tealish
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Nearle Rewards",
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
"Ride more to get more rewards 🚴",
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white70,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
border: Border.all(color: Colors.white30, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.stars_rounded, // Coin-like icon
|
||||
color: Colors.amberAccent,
|
||||
size: 24.sp,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Obx(() {
|
||||
return Text(
|
||||
controller.isLoading.value
|
||||
? "..."
|
||||
: "${controller.totalPoints.value}",
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.amberAccent,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Container(
|
||||
padding: EdgeInsets.all(12.r),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Keep riding correctly to get more points!",
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
"Earn 100 points to unlock new rewards",
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white70,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.emoji_events,
|
||||
color: Colors.amber,
|
||||
size: 32.sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (showFullDetails) ...[
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// 2. Surprise Gift Section
|
||||
_buildSurpriseGiftCard(),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// 3. The 4 Cards Section
|
||||
Text(
|
||||
"Redeem Your Points",
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Card 1: Data Recharge
|
||||
_buildRewardOptionCard(
|
||||
title: "Data Recharge",
|
||||
subtitle: "Get free data for 1 month",
|
||||
points: "300 Points",
|
||||
icon: Icons.wifi,
|
||||
color1: const Color(0xFF11998e),
|
||||
color2: const Color(0xFF38ef7d),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Card 2: Bonus Fuel
|
||||
_buildRewardOptionCard(
|
||||
title: "Bonus Fuel",
|
||||
subtitle: "Fuel support for your vehicle",
|
||||
points: "600 Points",
|
||||
icon: Icons.local_gas_station,
|
||||
color1: const Color(0xFFFF5F6D),
|
||||
color2: const Color(0xFFFFC371),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Card 3: Gadgets
|
||||
_buildRewardOptionCard(
|
||||
title: "Gadgets Support",
|
||||
subtitle: "Powerbank or New Mobile support",
|
||||
points: "1000 - 1500 Points",
|
||||
icon: Icons.devices_other,
|
||||
color1: const Color(0xFF2193b0),
|
||||
color2: const Color(0xFF6dd5ed),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Card 4: Vehicle Support
|
||||
_buildRewardOptionCard(
|
||||
title: "Vehicle Support",
|
||||
subtitle: "New vehicle or 50% loan support",
|
||||
points: "2000 Bonus Points",
|
||||
description: "Earn 2000 bonus without any loses in bonus point",
|
||||
icon: Icons.motorcycle,
|
||||
color1: const Color(0xFF8E2DE2),
|
||||
color2: const Color(0xFF4A00E0),
|
||||
isPremium: true,
|
||||
),
|
||||
|
||||
SizedBox(height: 30.h),
|
||||
|
||||
// 4. Bottom Warning / Info Section
|
||||
Container(
|
||||
padding: EdgeInsets.all(16.r),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Colors.red.shade700, size: 24.sp),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Note: If you miss any deliveries or if requirements are not met for each delivery, negative bonus points will affect your board.",
|
||||
style: TextStyle(
|
||||
fontSize: 15.sp,
|
||||
color: Colors.red.shade900,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSurpriseGiftCard() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.purple.withOpacity(0.1),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
right: -20,
|
||||
top: -20,
|
||||
child: Icon(
|
||||
Icons.card_giftcard,
|
||||
size: 100.sp,
|
||||
color: Colors.purple.withOpacity(0.05),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(20.r),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(12.r),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple.shade50,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Icon(Icons.card_giftcard, color: Colors.purple, size: 30.sp),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Monthly Surprise Gift",
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
"If you didn't skip any orders in 1 month!",
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRewardOptionCard({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required String points,
|
||||
required IconData icon,
|
||||
required Color color1,
|
||||
required Color color2,
|
||||
String? description,
|
||||
bool isPremium = false,
|
||||
}) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Decorative Background Circle
|
||||
Positioned(
|
||||
right: -30,
|
||||
top: -30,
|
||||
child: Container(
|
||||
width: 120.w,
|
||||
height: 120.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [color1.withOpacity(0.2), color2.withOpacity(0.0)],
|
||||
begin: Alignment.bottomLeft,
|
||||
end: Alignment.topRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.all(20.r),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(10.r),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [color1, color2],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: Colors.white, size: 24.sp),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.shade50,
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
border: Border.all(color: Colors.amber.shade200),
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
points,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.amber.shade900,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 15.sp,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
if (description != null) ...[
|
||||
SizedBox(height: 12.h),
|
||||
Container(
|
||||
padding: EdgeInsets.all(10.r),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.star_outline, size: 16.sp, color: Colors.blueGrey),
|
||||
SizedBox(width: 6.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black87,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
587
lib/views/Dashboard/summary/summary.dart
Normal file
587
lib/views/Dashboard/summary/summary.dart
Normal file
@@ -0,0 +1,587 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:nearle/Models/summary/riderweeklykms.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/controllers/summary_controller.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class Summary extends StatefulWidget {
|
||||
const Summary({super.key});
|
||||
|
||||
@override
|
||||
State<Summary> createState() => _SummaryState();
|
||||
}
|
||||
|
||||
class _SummaryState extends State<Summary> {
|
||||
final SummaryController controller = Get.put(SummaryController());
|
||||
int _userId = 0;
|
||||
int _refreshTick = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refreshData();
|
||||
}
|
||||
|
||||
Future<void> _refreshData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final uid = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_userId = uid;
|
||||
});
|
||||
}
|
||||
|
||||
if (uid > 0) {
|
||||
await controller.fetchSummaryStats(uid);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("❌ Error fetching summary: $e");
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_refreshTick++;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// RESPONSIVE CARD
|
||||
// ------------------------
|
||||
Widget _buildCard({
|
||||
required String title,
|
||||
required String value,
|
||||
required String imagePath,
|
||||
bool isCancelled = false,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
border: Border.all(
|
||||
color: isCancelled
|
||||
? const Color(0xFFFF5C5C)
|
||||
: const Color.fromARGB(255, 159, 139, 163),
|
||||
width: 1.2.w,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
imagePath,
|
||||
height: 36.h,
|
||||
width: 36.w,
|
||||
color: isCancelled
|
||||
? const Color(0xFFFF5C5C)
|
||||
: const Color(0xFF9C27B0),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.grey.shade700,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 30.h),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 29.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// RESPONSIVE CANCELLED CARD
|
||||
// ------------------------
|
||||
Widget _buildCancelledCard(String value) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(
|
||||
color: const Color.fromARGB(255, 232, 167, 167),
|
||||
width: 1.3.w,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/cancel.png',
|
||||
height: 36.h,
|
||||
width: 36.w,
|
||||
color: const Color(0xFFFF5C5C),
|
||||
),
|
||||
SizedBox(width: 15.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Cancelled Orders',
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.grey.shade700,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 34.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// MAIN UI
|
||||
// ------------------------
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
toolbarHeight: 70.h,
|
||||
title: Padding(
|
||||
padding: EdgeInsets.only(top: 12.h),
|
||||
child: Text(
|
||||
"SUMMARY",
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xxxLarge(context).sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1.h),
|
||||
child: Divider(height: 1.h, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
body: Obx(() {
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: EdgeInsets.all(16.r),
|
||||
child: Column(
|
||||
children: [
|
||||
GridView.count(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 12.w,
|
||||
mainAxisSpacing: 12.h,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
_buildCard(
|
||||
title: 'Today',
|
||||
value: controller.today.value.toString(),
|
||||
imagePath: 'assets/images/today.png',
|
||||
),
|
||||
_buildCard(
|
||||
title: 'Week',
|
||||
value: controller.week.value.toString(),
|
||||
imagePath: 'assets/images/week.png',
|
||||
),
|
||||
_buildCard(
|
||||
title: 'Month',
|
||||
value: controller.month.value.toString(),
|
||||
imagePath: 'assets/images/week.png',
|
||||
),
|
||||
_buildCard(
|
||||
title: 'Total',
|
||||
value: controller.total.value.toString(),
|
||||
imagePath: 'assets/images/total.png',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 12.h),
|
||||
_buildCancelledCard(controller.cancelled.value.toString()),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"Statistics",
|
||||
style: TextStyle(
|
||||
fontSize: 26.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 10.h),
|
||||
TotalDistanceCard(userId: _userId, refreshTick: _refreshTick),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// RESPONSIVE GRAPH CARD
|
||||
// ========================================================
|
||||
class TotalDistanceCard extends StatefulWidget {
|
||||
final int userId;
|
||||
final int refreshTick;
|
||||
|
||||
const TotalDistanceCard({
|
||||
super.key,
|
||||
required this.userId,
|
||||
this.refreshTick = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TotalDistanceCard> createState() => _TotalDistanceCardState();
|
||||
}
|
||||
|
||||
class _TotalDistanceCardState extends State<TotalDistanceCard> {
|
||||
late Future<Map<String, dynamic>> _futureKms;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureKms = _fetchWeeklyKms();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TotalDistanceCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.userId != widget.userId ||
|
||||
oldWidget.refreshTick != widget.refreshTick) {
|
||||
setState(() {
|
||||
_futureKms = _fetchWeeklyKms();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
double _toDouble(dynamic v) {
|
||||
if (v == null) return 0.0;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _fetchWeeklyKms() async {
|
||||
if (widget.userId == 0) {
|
||||
return {'details': <RiderWeeklyKms>[], 'total_kms': 0.0};
|
||||
}
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
'https://jupiter.nearle.app/live/api/v1/partners/getriderweeklykms?userid=${widget.userId}',
|
||||
);
|
||||
final response = await http.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
|
||||
if (data is Map && data['status'] == true) {
|
||||
final rawDetails = (data['details'] is List)
|
||||
? data['details'] as List
|
||||
: const [];
|
||||
|
||||
final details = rawDetails
|
||||
.map((e) => RiderWeeklyKms.fromJson(e))
|
||||
.toList();
|
||||
|
||||
final total = _toDouble(data['total_kms']);
|
||||
return {'details': details, 'total_kms': total};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ _fetchWeeklyKms Error: $e');
|
||||
}
|
||||
|
||||
return {'details': <RiderWeeklyKms>[], 'total_kms': 0.0};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
|
||||
return FutureBuilder<Map<String, dynamic>>(
|
||||
future: _futureKms,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24.h),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(color: Colors.deepPurple),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final List<RiderWeeklyKms> details =
|
||||
snapshot.data?['details'] ?? <RiderWeeklyKms>[];
|
||||
|
||||
final double totalKms = snapshot.data?['total_kms'] ?? 0.0;
|
||||
|
||||
return _buildDistanceCard(details, totalKms, size);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDistanceCard(
|
||||
List<RiderWeeklyKms> details,
|
||||
double totalKms,
|
||||
Size size,
|
||||
) {
|
||||
final double maxY = details.isEmpty ? 10 : _getMaxY(details);
|
||||
final double chartHeight = (size.height * 0.25).clamp(160.h, 280.h);
|
||||
final double leftInterval = _calculateInterval(maxY);
|
||||
final double maxK = _getMaxKms(details);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: EdgeInsets.only(top: 4.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(
|
||||
color: const Color.fromARGB(255, 222, 161, 235),
|
||||
width: 1.3.w,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Total Distance",
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${totalKms.toStringAsFixed(2)} Km",
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 30.h),
|
||||
|
||||
if (details.isEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.h),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"No weekly data available",
|
||||
style: TextStyle(color: Colors.grey, fontSize: 16.sp),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
height: chartHeight,
|
||||
width: double.infinity,
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
maxY: maxY,
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (value) => FlLine(
|
||||
color: Colors.grey.withOpacity(0.12),
|
||||
strokeWidth: 1,
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 60.w,
|
||||
interval: leftInterval,
|
||||
getTitlesWidget: (value, _) => Text(
|
||||
"${value.toInt()} km",
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, _) {
|
||||
final idx = value.toInt();
|
||||
if (idx >= 0 && idx < details.length) {
|
||||
return Text(
|
||||
details[idx].day,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black87,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
barGroups: List.generate(details.isEmpty ? 7 : details.length, (
|
||||
i,
|
||||
) {
|
||||
final kms = details.isEmpty ? 0.0 : details[i].kms.toDouble();
|
||||
|
||||
return BarChartGroupData(
|
||||
x: i,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: kms,
|
||||
color: (details.isNotEmpty && kms == maxK)
|
||||
? const Color(0xFF8124DB)
|
||||
: const Color(0xFFB274F3),
|
||||
width: 20.w,
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
barTouchData: BarTouchData(
|
||||
enabled: true,
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
tooltipPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12.w,
|
||||
vertical: 8.h,
|
||||
),
|
||||
getTooltipItem: (group, index, rod, rodIndex) {
|
||||
return BarTooltipItem(
|
||||
"${rod.toY.toStringAsFixed(2)} Km",
|
||||
TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _getMaxY(List<RiderWeeklyKms> details) {
|
||||
if (details.isEmpty) return 10;
|
||||
double maxVal = details.map((e) => e.kms).reduce(max);
|
||||
if (maxVal <= 5) return 10;
|
||||
final double withPadding = maxVal * 1.2;
|
||||
return _roundUpNice(withPadding);
|
||||
}
|
||||
|
||||
double _roundUpNice(double v) {
|
||||
final exponent = pow(10, (log(v) / ln10).floor());
|
||||
final mantissa = v / exponent;
|
||||
|
||||
double niceMantissa;
|
||||
if (mantissa <= 1) {
|
||||
niceMantissa = 1;
|
||||
} else if (mantissa <= 2)
|
||||
niceMantissa = 2;
|
||||
else if (mantissa <= 5)
|
||||
niceMantissa = 5;
|
||||
else
|
||||
niceMantissa = 10;
|
||||
|
||||
return (niceMantissa * exponent).ceilToDouble();
|
||||
}
|
||||
|
||||
double _calculateInterval(double maxY) {
|
||||
const int desiredTicks = 5;
|
||||
double rough = max(1, (maxY / desiredTicks));
|
||||
|
||||
final exponent = pow(10, (log(rough) / ln10).floor());
|
||||
final mantissa = rough / exponent;
|
||||
|
||||
double niceMantissa;
|
||||
if (mantissa <= 1) {
|
||||
niceMantissa = 1;
|
||||
} else if (mantissa <= 2)
|
||||
niceMantissa = 2;
|
||||
else if (mantissa <= 5)
|
||||
niceMantissa = 5;
|
||||
else
|
||||
niceMantissa = 10;
|
||||
|
||||
return (niceMantissa * exponent).toDouble();
|
||||
}
|
||||
|
||||
double _getMaxKms(List<RiderWeeklyKms> details) {
|
||||
if (details.isEmpty) return 0;
|
||||
return details.map((e) => e.kms).reduce(max);
|
||||
}
|
||||
}
|
||||
36
lib/views/helpers/constants/Colorconstants.dart
Normal file
36
lib/views/helpers/constants/Colorconstants.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColorConstants {
|
||||
static const primaryColor = Color(0xFF662582);
|
||||
static Color? primaryColor1 = const Color(0xFFE7D3EF);
|
||||
static Color? secondaryColor = Colors.white;
|
||||
static Color? ternaryColor = "#E7D3EF".toColor();
|
||||
static Color? darkGreyColor = "575756".toColor();
|
||||
static Color? lightGrey = "b2b2b2".toColor();
|
||||
static Color? lightGreyBg = Colors.grey.shade100;
|
||||
static Color? greenColor = "00b894".toColor();
|
||||
static Color? mintColor = "69c0ac".toColor();
|
||||
static Color restaurantColor = Colors.amber[100]!;
|
||||
static Color groceriesColor = Colors.purple[100]!;
|
||||
static Color shoppingColor = Colors.orange[100]!;
|
||||
static Color healthColor = Colors.cyan[100]!;
|
||||
static Color handymanColor = Colors.red[100]!;
|
||||
static const blueColor = 0xff007AC2;
|
||||
static const redColor = 0xffEF3F42;
|
||||
static const orangeColor = 0xffFAAB53;
|
||||
static Color? lightColor = const Color.fromRGBO(244, 244, 244, 1);
|
||||
}
|
||||
|
||||
extension ColorExtenstion on String {
|
||||
// ignore: body_might_complete_normally_nullable
|
||||
Color? toColor() {
|
||||
var hexColor = replaceAll("#", "");
|
||||
if (hexColor.length == 6) {
|
||||
hexColor = "FF$hexColor";
|
||||
}
|
||||
if (hexColor.length == 8) {
|
||||
return Color(int.parse("0x$hexColor"));
|
||||
}
|
||||
}
|
||||
}
|
||||
111
lib/views/helpers/constants/Font_constant.dart
Normal file
111
lib/views/helpers/constants/Font_constant.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
// ignore: file_names
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FontConstants {
|
||||
static String fontFamily = 'Proxima Nova';
|
||||
|
||||
// Base screen width for scaling (iPhone standard: 375)
|
||||
static const double _baseWidth = 375.0;
|
||||
|
||||
// Base font sizes (for baseWidth = 375)
|
||||
static const double _baseExtraSmall = 10.0;
|
||||
static const double _baseSmall = 12.0;
|
||||
static const double _baseMedium = 14.0;
|
||||
static const double _baseRegular = 16.0;
|
||||
static const double _baseLarge = 20.0;
|
||||
static const double _baseXLarge = 21.0;
|
||||
static const double _baseXXLarge = 22.0;
|
||||
static const double _baseXXXLarge = 24.0;
|
||||
static const double _baseHuge = 30.0;
|
||||
|
||||
/// Get fixed font size (no width scaling) so small & large phones look same.
|
||||
static double getResponsiveFontSize(BuildContext context, double baseSize) {
|
||||
return baseSize;
|
||||
}
|
||||
|
||||
/// Extra Small Text (10px base) - For labels, captions
|
||||
static double extraSmall(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseExtraSmall);
|
||||
|
||||
/// Small Text (12px base) - For small labels, timestamps
|
||||
static double small(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseSmall);
|
||||
|
||||
/// Medium Text (14px base) - For body text, descriptions
|
||||
static double medium(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseMedium);
|
||||
|
||||
/// Regular Text (16px base) - Standard body text, most common
|
||||
static double regular(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseRegular);
|
||||
|
||||
/// Large Text (18px base) - For subheadings, important text
|
||||
static double large(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseLarge);
|
||||
|
||||
/// Extra Large Text (20px base) - For headings, titles
|
||||
static double xLarge(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseXLarge);
|
||||
|
||||
/// 2X Large Text (22px base) - For main headings
|
||||
static double xxLarge(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseXXLarge);
|
||||
|
||||
/// 3X Large Text (24px base) - For prominent headings
|
||||
static double xxxLarge(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseXXXLarge);
|
||||
|
||||
/// Huge Text (26px base) - For hero text, very prominent headings
|
||||
static double huge(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseHuge);
|
||||
}
|
||||
|
||||
class ReusableTextWidget extends StatelessWidget {
|
||||
final String text;
|
||||
final double? fontSize;
|
||||
final double? textHeight;
|
||||
final String? fontFamily;
|
||||
final FontWeight? fontWeight;
|
||||
final FontStyle? fontStyle;
|
||||
final Color? color;
|
||||
final TextAlign? textAlign;
|
||||
final int? maxLines;
|
||||
final TextDecoration? isUnderText;
|
||||
|
||||
const ReusableTextWidget({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.fontSize,
|
||||
this.textHeight,
|
||||
this.fontFamily,
|
||||
this.fontWeight,
|
||||
this.fontStyle,
|
||||
this.color,
|
||||
this.textAlign,
|
||||
this.maxLines,
|
||||
this.isUnderText,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
softWrap: true,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize ?? FontConstants.medium(context),
|
||||
decoration: isUnderText,
|
||||
fontFamily: fontFamily ?? FontConstants.fontFamily,
|
||||
decorationColor: color,
|
||||
fontWeight: fontWeight ?? FontWeight.normal,
|
||||
fontStyle: fontStyle ?? FontStyle.normal,
|
||||
color: color ?? Colors.grey.shade900,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
decorationStyle: TextDecorationStyle.solid,
|
||||
decorationThickness: 1,
|
||||
height: textHeight,
|
||||
),
|
||||
maxLines: maxLines,
|
||||
textAlign: textAlign ?? TextAlign.start,
|
||||
);
|
||||
}
|
||||
}
|
||||
78
lib/views/helpers/constants/apiconstants.dart
Normal file
78
lib/views/helpers/constants/apiconstants.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
class ApiConstants {
|
||||
static String mainDev = "dev";
|
||||
static String mainRoute = "live";
|
||||
|
||||
//Delivery Queue - v2
|
||||
static String deliveryQueueDev =
|
||||
"https://jupiter.nearle.app/$mainDev/api/v2/deliveries/getdeliveryqueues";
|
||||
static String deliveryQueueLive =
|
||||
"https://jupiter.nearle.app/$mainRoute/api/v2/deliveries/getdeliveryqueues";
|
||||
|
||||
//Current Delivery - v1
|
||||
static String currentDeliveryDev =
|
||||
"https://jupiter.nearle.app/$mainDev/api/v1/deliveries/getdeliveries";
|
||||
static String currentDeliveryLive =
|
||||
"https://jupiter.nearle.app/$mainRoute/api/v1/deliveries/getdeliveries";
|
||||
|
||||
//Current Delivery V3 - v3 (date-bounded)
|
||||
static String currentDeliveryV3Dev =
|
||||
"https://jupiter.nearle.app/$mainDev/api/v3/deliveries/getdeliveries";
|
||||
static String currentDeliveryV3Live =
|
||||
"https://jupiter.nearle.app/$mainRoute/api/v3/deliveries/getdeliveries";
|
||||
|
||||
//Update Delivery - v1
|
||||
static String updateDeliveryDev =
|
||||
"https://queue.workolik.com/live/api/v1/deliveries/updatedelivery";
|
||||
static String updateDeliveryLive =
|
||||
"https://queue.workolik.com/live/api/v1/deliveries/updatedelivery";
|
||||
|
||||
//Get Rider Log - v1
|
||||
static String getRiderLogDev =
|
||||
"https://jupiter.nearle.app/$mainDev/api/v1/partners/getriderlog";
|
||||
static String getRiderLogLive =
|
||||
"https://jupiter.nearle.app/$mainRoute/api/v1/partners/getriderlog";
|
||||
|
||||
//Create Rider Log - v2
|
||||
static String createRiderLogDev =
|
||||
"https://queue.workolik.com/live/api/v2/partners/createriderlog";
|
||||
static String createRiderLogLive =
|
||||
"https://queue.workolik.com/live/api/v2/partners/createriderlog";
|
||||
|
||||
//Update Rider Log - v1
|
||||
static String updateRiderLogDev =
|
||||
"https://jupiter.nearle.app/$mainDev/api/v1/partners/updateriderlog";
|
||||
static String updateRiderLogLive =
|
||||
"https://jupiter.nearle.app/$mainRoute/api/v1/partners/updateriderlog";
|
||||
|
||||
//Get Rider Count - v1
|
||||
static String getRiderCountDev =
|
||||
"https://jupiter.nearle.app/$mainDev/api/v1/partners/getridercount";
|
||||
static String getRiderCountLive =
|
||||
"https://jupiter.nearle.app/$mainRoute/api/v1/partners/getridercount";
|
||||
|
||||
//Create Break Rider Log - v2
|
||||
static String createBreakRiderLogDev =
|
||||
"https://queue.workolik.com/live/api/v2/partners/createbreaklog";
|
||||
static String createBreakRiderLogLive =
|
||||
"https://queue.workolik.com/live/api/v2/partners/createbreaklog";
|
||||
|
||||
//Update Break Rider Log - v2
|
||||
static String updateBreakRiderLogDev =
|
||||
"https://queue.workolik.com/live/api/v2/partners/updatebreaklog";
|
||||
static String updateBreakRiderLogLive =
|
||||
"https://queue.workolik.com/live/api/v2/partners/updatebreaklog";
|
||||
|
||||
//Create Delivery Log - v2
|
||||
static String createDeliveryLogDev =
|
||||
"https://queue.workolik.com/live/api/v2/deliveries/createdeliverylog";
|
||||
static String createDeliveryLogLive =
|
||||
"https://queue.workolik.com/live/api/v2/deliveries/createdeliverylog";
|
||||
|
||||
//Summary API - v2
|
||||
static String summaryApiLive =
|
||||
'https://jupiter.nearle.app/$mainRoute/api/v2/partners';
|
||||
|
||||
//Summary Rider Weekly KMs - v1
|
||||
static String summaryriderkmLive =
|
||||
'https://jupiter.nearle.app/$mainRoute/api/v1/partners/getriderweeklykms';
|
||||
}
|
||||
18
lib/views/helpers/constants/mqtt_constants.dart
Normal file
18
lib/views/helpers/constants/mqtt_constants.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
class MqttConstants {
|
||||
static const String brokerHost = '66.116.225.226'; // Updated with VPS IP
|
||||
static const int brokerPort = 1883;
|
||||
static const String username = 'admin';
|
||||
static const String passwordString = 'Package@321#'; // Provided by user
|
||||
|
||||
// Topic Structure
|
||||
static const String topicRiderStatus = 'nearle/riders/{riderId}/status';
|
||||
static const String topicRiderProfile = 'nearle/riders/{riderId}/profile';
|
||||
static const String topicRiderLocation = 'nearle/riders/{riderId}/location';
|
||||
static const String topicRiderTelemetry = 'nearle/riders/{riderId}/telemetry';
|
||||
static const String topicRiderLogs = 'nearle/riders/{riderId}/logs';
|
||||
|
||||
// Status Values
|
||||
static const String statusOnline = 'Online';
|
||||
static const String statusOffline = 'Offline';
|
||||
static const String statusIdle = 'Idle';
|
||||
}
|
||||
140
lib/views/introscreens/intro1.dart
Normal file
140
lib/views/introscreens/intro1.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class Intro1 extends GetResponsiveView {
|
||||
Intro1({super.key});
|
||||
|
||||
@override
|
||||
Widget builder() {
|
||||
// 🔹 Use `screen.height` and `screen.width` safely here
|
||||
final height = screen.height;
|
||||
final width = screen.width;
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.dark.copyWith(
|
||||
// Make the status bar area white instead of black, with dark icons
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(height * 0.12),
|
||||
child: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
flexibleSpace: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Container(
|
||||
height: height * 0.14,
|
||||
width: width * 0.25,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomRight: Radius.circular(width * 0.25),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
bottom: -height * 0.12,
|
||||
left: -width * 0.1,
|
||||
right: -width * 0.1,
|
||||
child: Container(
|
||||
width: width * 1.2,
|
||||
height: height * 0.28,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF3EAF9),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(width * 0.8),
|
||||
topRight: Radius.circular(width * 0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: height * 0.08),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.08),
|
||||
child: Text(
|
||||
'Welcome to',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.045,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.1),
|
||||
child: Text(
|
||||
'Nearle !',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.045,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.08),
|
||||
child: Text(
|
||||
'Find delivery opportunities anytime,\nanywhere | Earn with ease!',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.022,
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.09),
|
||||
child: Image.asset(
|
||||
'assets/images/intro1.png',
|
||||
height: height * 0.35,
|
||||
width: width * 0.75,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
144
lib/views/introscreens/intro2.dart
Normal file
144
lib/views/introscreens/intro2.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class Intro2 extends GetResponsiveView {
|
||||
Intro2({super.key});
|
||||
|
||||
@override
|
||||
Widget builder() {
|
||||
final height = screen.height;
|
||||
final width = screen.width;
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.dark.copyWith(
|
||||
// Make the status bar area white instead of black, with dark icons
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(height * 0.12),
|
||||
child: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
flexibleSpace: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Container(
|
||||
height: height * 0.14,
|
||||
width: width * 0.25,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(width * 0.25),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Bottom curve
|
||||
Positioned(
|
||||
bottom: -height * 0.12,
|
||||
left: -width * 0.1,
|
||||
right: -width * 0.1,
|
||||
child: Container(
|
||||
width: width * 1.2,
|
||||
height: height * 0.28,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF3EAF9),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(width * 0.8),
|
||||
topRight: Radius.circular(width * 0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Center content
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: height * 0.08),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Title
|
||||
Column(
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.08),
|
||||
child: Text(
|
||||
'Orders That',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.045,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.1),
|
||||
child: Text(
|
||||
'Find You',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.045,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.08),
|
||||
child: Text(
|
||||
'Get assigned deliveries based on your\nlocation for faster and smarter work.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.022,
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Image in center
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.09),
|
||||
child: Image.asset(
|
||||
'assets/images/intro2.png',
|
||||
height: height * 0.35,
|
||||
width: width * 0.75,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
152
lib/views/introscreens/intro3.dart
Normal file
152
lib/views/introscreens/intro3.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class Intro3 extends GetResponsiveView {
|
||||
final VoidCallback onFinish;
|
||||
|
||||
Intro3({
|
||||
super.key,
|
||||
required this.onFinish,
|
||||
required PageController controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget builder() {
|
||||
final height = screen.height;
|
||||
final width = screen.width;
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light.copyWith(
|
||||
// Keep status bar transparent over the purple gradient on this screen
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
statusBarBrightness: Brightness.dark,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(height * 0.35),
|
||||
child: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light.copyWith(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
statusBarBrightness: Brightness.dark,
|
||||
),
|
||||
flexibleSpace: Container(
|
||||
width: double.infinity,
|
||||
height: height * 0.36,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
ColorConstants.primaryColor,
|
||||
ColorConstants.primaryColor,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(width * 0.49),
|
||||
bottomRight: Radius.circular(width * 0.49),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: width * 0.06,
|
||||
vertical: height * 0.02,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: height * 0.06),
|
||||
Text(
|
||||
'Deliver. Earn.\nRepeat.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.045,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
height: 1.2,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.02),
|
||||
Text(
|
||||
'Track your trips, and enjoy \npayouts every week.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.019,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
height: 1.4,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.05),
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, -height * 0.01),
|
||||
child: Image.asset(
|
||||
'assets/images/intro3.png',
|
||||
height: height * 0.35,
|
||||
width: width * 0.75,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.04),
|
||||
|
||||
// Button at bottom
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.08),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: onFinish,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Start",
|
||||
style: TextStyle(
|
||||
fontSize: 21,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.03),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
82
lib/views/introscreens/introscreen.dart
Normal file
82
lib/views/introscreens/introscreen.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/introscreens/intro1.dart';
|
||||
import 'package:nearle/views/introscreens/intro2.dart';
|
||||
import 'package:nearle/views/introscreens/intro3.dart';
|
||||
import 'package:nearle/views/onboardscreens/Sign_in.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
|
||||
|
||||
class Introscreen extends StatefulWidget {
|
||||
const Introscreen({super.key});
|
||||
|
||||
@override
|
||||
State<Introscreen> createState() => _IntroscreenState();
|
||||
}
|
||||
|
||||
class _IntroscreenState extends State<Introscreen> {
|
||||
late final PageController _controller;
|
||||
static const String _prefsHasSeenIntroKey = 'has_seen_intro';
|
||||
|
||||
void _finishOnboarding() {
|
||||
_completeOnboarding();
|
||||
}
|
||||
|
||||
Future<void> _completeOnboarding() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefsHasSeenIntroKey, true);
|
||||
} catch (_) {}
|
||||
|
||||
if (!mounted) return;
|
||||
Get.offAll(() => const SignIn());
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = PageController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
alignment: Alignment.bottomCenter, // positions the indicator
|
||||
children: [
|
||||
PageView(
|
||||
controller: _controller,
|
||||
children: [
|
||||
Intro1(),
|
||||
Intro2(),
|
||||
Intro3(controller: _controller, onFinish: _finishOnboarding),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
// Move indicator slightly up so it doesn't sit under the bottom curve
|
||||
alignment: const Alignment(0, 0.55),
|
||||
child: SmoothPageIndicator(
|
||||
controller: _controller,
|
||||
count: 3,
|
||||
effect: ExpandingDotsEffect(
|
||||
expansionFactor: 3, // How much the active dot expands
|
||||
dotHeight: 7,
|
||||
dotWidth: 7,
|
||||
spacing: 8,
|
||||
dotColor: Colors.grey,
|
||||
activeDotColor: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
188
lib/views/introscreens/splashscreen.dart
Normal file
188
lib/views/introscreens/splashscreen.dart
Normal file
@@ -0,0 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/introscreens/introscreen.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/widget/Bottom_page.dart';
|
||||
import 'package:nearle/views/onboardscreens/Sign_in.dart';
|
||||
import 'package:nearle/views/onboardscreens/signin_banner.dart';
|
||||
import 'package:nearle/views/updatescreen/UpdateScreen.dart';
|
||||
import 'package:new_version_plus/new_version_plus.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:nearle/views/onboardscreens/Mpin.dart';
|
||||
import 'package:nearle/controllers/auth.dart';
|
||||
|
||||
class Splashscreen extends StatefulWidget {
|
||||
const Splashscreen({super.key});
|
||||
|
||||
@override
|
||||
State<Splashscreen> createState() => _SplashscreenState();
|
||||
}
|
||||
|
||||
class _SplashscreenState extends State<Splashscreen> {
|
||||
late final ImageProvider _logoProvider;
|
||||
bool _imagePrecached = false;
|
||||
bool _hasCheckedUpdate = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_logoProvider = const AssetImage("assets/images/nearlesplash2.png");
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
await precacheImage(_logoProvider, context);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_imagePrecached = true;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_imagePrecached = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// FAST splash: 1 second
|
||||
Timer(const Duration(seconds: 1), () {
|
||||
if (mounted) {
|
||||
_startNextStep();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// FAST second step: 0.2 sec
|
||||
void _startNextStep() {
|
||||
Timer(const Duration(milliseconds: 200), () {
|
||||
if (mounted && !_hasCheckedUpdate) {
|
||||
_checkForUpdateAndNavigate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkForUpdateAndNavigate() async {
|
||||
if (_hasCheckedUpdate) return;
|
||||
_hasCheckedUpdate = true;
|
||||
|
||||
try {
|
||||
final newVersion = NewVersionPlus(
|
||||
iOSId: '284882215',
|
||||
androidId: "com.nearle.partner",
|
||||
);
|
||||
|
||||
final status = await newVersion.getVersionStatus();
|
||||
|
||||
if (status != null && status.canUpdate) {
|
||||
if (mounted) {
|
||||
Get.offAll(
|
||||
() => UpdateScreen(
|
||||
mCurrentVersion: status.localVersion,
|
||||
mUpdateVersion: status.storeVersion,
|
||||
mIsForceUpdate: true,
|
||||
),
|
||||
transition: Transition.fadeIn,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
_navigateToNextScreen();
|
||||
}
|
||||
|
||||
Future<void> _navigateToNextScreen() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final bool isLoggedOut = prefs.getBool('logged_out') ?? false;
|
||||
final bool hasSeenIntro = prefs.getBool('has_seen_intro') ?? false;
|
||||
final int? savedUserId = prefs.getInt('userid');
|
||||
|
||||
// 🚀 Check for App Update (Force Login if version changed)
|
||||
try {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final currentVersion = packageInfo.version;
|
||||
final lastRunVersion = prefs.getString('last_run_version');
|
||||
|
||||
if (lastRunVersion != null && lastRunVersion != currentVersion) {
|
||||
debugPrint(
|
||||
'[SPLASH] App update detected: $lastRunVersion -> $currentVersion. Forcing re-verification.',
|
||||
);
|
||||
|
||||
final String? savedPhone = prefs.getString('contactno');
|
||||
final int? savedUserId = prefs.getInt('userid');
|
||||
|
||||
// Update stored version
|
||||
await prefs.setString('last_run_version', currentVersion);
|
||||
|
||||
if (savedUserId != null && savedPhone != null && savedPhone.isNotEmpty) {
|
||||
debugPrint('[SPLASH] User was logged in. Redirecting to MPIN page.');
|
||||
|
||||
// Initialize AuthController and set the phone for MPIN verification
|
||||
final auth = Get.put(AuthController());
|
||||
auth.currentPhone = savedPhone;
|
||||
|
||||
// Clear sensitive session data to force re-verification
|
||||
await prefs.remove('userid');
|
||||
await prefs.remove('partnerid');
|
||||
await prefs.remove('onduty');
|
||||
await prefs.setBool('logged_out', true);
|
||||
|
||||
if (mounted) {
|
||||
Get.offAll(() => Mpin());
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
debugPrint('[SPLASH] User was not logged in. Redirecting to Sign In.');
|
||||
if (mounted) {
|
||||
Get.offAll(() => const SignIn());
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Save current version for next run
|
||||
await prefs.setString('last_run_version', currentVersion);
|
||||
} catch (e) {
|
||||
debugPrint('[SPLASH] Version check error: $e');
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (!isLoggedOut && savedUserId != null && savedUserId > 0) {
|
||||
final onduty = prefs.getInt('onduty') ?? 0;
|
||||
|
||||
if (onduty == 0) {
|
||||
Get.offAll(() => const SigninBanner());
|
||||
} else {
|
||||
Get.offAll(() => const BottomPage());
|
||||
}
|
||||
} else {
|
||||
if (hasSeenIntro) {
|
||||
Get.offAll(() => const SignIn());
|
||||
} else {
|
||||
Get.offAll(() => Introscreen());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: ColorConstants.secondaryColor,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: _imagePrecached
|
||||
? Image(
|
||||
image: _logoProvider,
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
128
lib/views/offline/offline_page.dart
Normal file
128
lib/views/offline/offline_page.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:nearle/views/introscreens/splashscreen.dart';
|
||||
|
||||
class OfflinePage extends StatelessWidget {
|
||||
const OfflinePage({super.key});
|
||||
|
||||
Future<void> _handleRetry(BuildContext context) async {
|
||||
try {
|
||||
final results = await Connectivity().checkConnectivity();
|
||||
final isOnline = results.isNotEmpty && results.any((r) => r != ConnectivityResult.none);
|
||||
if (isOnline) {
|
||||
// Return to normal app flow; Splashscreen decides login vs home
|
||||
Get.offAll(() => Splashscreen());
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Still offline. Please check your internet connection.'),
|
||||
backgroundColor: Colors.black87,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final height = MediaQuery.of(context).size.height;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
bottom: -height * 0.12,
|
||||
left: -width * 0.1,
|
||||
right: -width * 0.1,
|
||||
child: Container(
|
||||
width: width * 1.2,
|
||||
height: height * 0.28,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF3EAF9),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(width * 0.8),
|
||||
topRight: Radius.circular(width * 0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.wifi_off,
|
||||
color: ColorConstants.primaryColor,
|
||||
size: 64,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'You are offline',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Please check your internet connection. We\'ll reconnect automatically when you\'re back online.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[700],
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: 180,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () => _handleRetry(context),
|
||||
child: const Text(
|
||||
'Retry',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
322
lib/views/onboardscreens/Creat_mpin.dart
Normal file
322
lib/views/onboardscreens/Creat_mpin.dart
Normal file
@@ -0,0 +1,322 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
import 'package:nearle/controllers/auth.dart';
|
||||
import 'package:nearle/views/onboardscreens/Mpin.dart';
|
||||
|
||||
class CreateMpin extends GetResponsiveView {
|
||||
CreateMpin({super.key});
|
||||
|
||||
@override
|
||||
Widget builder() {
|
||||
return const _CreateMpinBody();
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateMpinBody extends StatefulWidget {
|
||||
const _CreateMpinBody();
|
||||
|
||||
@override
|
||||
State<_CreateMpinBody> createState() => _CreateMpinBodyState();
|
||||
}
|
||||
|
||||
class _CreateMpinBodyState extends State<_CreateMpinBody> {
|
||||
final AuthController _auth = Get.put(AuthController());
|
||||
final List<TextEditingController> _newMpinControllers = List.generate(
|
||||
4,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
final List<TextEditingController> _confirmMpinControllers = List.generate(
|
||||
4,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
|
||||
final List<FocusNode> _newFocusNodes = List.generate(4, (_) => FocusNode());
|
||||
final List<FocusNode> _confirmFocusNodes = List.generate(
|
||||
4,
|
||||
(_) => FocusNode(),
|
||||
);
|
||||
|
||||
bool isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_newFocusNodes[0].requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var c in [..._newMpinControllers, ..._confirmMpinControllers]) {
|
||||
c.dispose();
|
||||
}
|
||||
for (var f in [..._newFocusNodes, ..._confirmFocusNodes]) {
|
||||
f.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onMpinChange(
|
||||
String value,
|
||||
int index,
|
||||
List<TextEditingController> controllers,
|
||||
List<FocusNode> nodes,
|
||||
) {
|
||||
if (value.isNotEmpty && index < 3) {
|
||||
nodes[index + 1].requestFocus();
|
||||
} else if (value.isEmpty && index > 0) {
|
||||
nodes[index - 1].requestFocus();
|
||||
}
|
||||
|
||||
final isGroupFilled = controllers.every((c) => c.text.isNotEmpty);
|
||||
if (controllers == _newMpinControllers && isGroupFilled) {
|
||||
_confirmFocusNodes[0].requestFocus();
|
||||
}
|
||||
if (controllers == _confirmMpinControllers && isGroupFilled) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
String getMpin(List<TextEditingController> controllers) =>
|
||||
controllers.map((c) => c.text).join();
|
||||
|
||||
bool get isMpinMatched =>
|
||||
getMpin(_newMpinControllers) == getMpin(_confirmMpinControllers);
|
||||
|
||||
bool get isAllFilled => [
|
||||
..._newMpinControllers,
|
||||
..._confirmMpinControllers,
|
||||
].every((c) => c.text.isNotEmpty);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screen = context.width < 600
|
||||
? "mobile"
|
||||
: context.width < 1100
|
||||
? "tablet"
|
||||
: "desktop";
|
||||
|
||||
final height = Get.height;
|
||||
final width = Get.width;
|
||||
|
||||
// Adjust scale based on device
|
||||
final scale = screen == "mobile"
|
||||
? 1.0
|
||||
: screen == "tablet"
|
||||
? 1.3
|
||||
: 1.6;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.06),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: height * 0.04 * scale),
|
||||
|
||||
SizedBox(
|
||||
height: height * 0.25 * scale,
|
||||
width: width * 0.6,
|
||||
child: Image.asset(
|
||||
"assets/images/CreateMpin.png",
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.03 * scale),
|
||||
|
||||
Text(
|
||||
"Create Your MPIN",
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.04 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.015 * scale),
|
||||
|
||||
Text(
|
||||
"Enter a 4-digit MPIN and confirm it to secure your account.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.02 * scale,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.04 * scale),
|
||||
|
||||
_buildMpinField(
|
||||
"Enter New MPIN",
|
||||
_newMpinControllers,
|
||||
_newFocusNodes,
|
||||
scale,
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.03 * scale),
|
||||
|
||||
_buildMpinField(
|
||||
"Confirm MPIN",
|
||||
_confirmMpinControllers,
|
||||
_confirmFocusNodes,
|
||||
scale,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: height * 0.05,
|
||||
left: width * 0.04,
|
||||
child: InkWell(
|
||||
onTap: () => Get.back(),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(width * 0.02),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black12,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_back,
|
||||
color: Colors.black,
|
||||
size: width * 0.06,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(width * 0.04),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: height * 0.065 * scale,
|
||||
child: ElevatedButton(
|
||||
onPressed: isAllFilled && isMpinMatched && !isLoading
|
||||
? () async {
|
||||
setState(() => isLoading = true);
|
||||
final newPin = getMpin(_newMpinControllers);
|
||||
// Set PIN directly - user ID should already be available from login flow
|
||||
final ok = await _auth.setPin(newPin);
|
||||
setState(() => isLoading = false);
|
||||
if (ok) {
|
||||
// After setting PIN, go to verify PIN page to sign-in with new PIN
|
||||
Get.to(() => Mpin());
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Failed',
|
||||
'Unable to set PIN. Please try again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(width * 0.03),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
height: height * 0.03,
|
||||
width: height * 0.03,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
"Continue",
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.024 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMpinField(
|
||||
String label,
|
||||
List<TextEditingController> controllers,
|
||||
List<FocusNode> focusNodes,
|
||||
double scale,
|
||||
) {
|
||||
final height = Get.height;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.02 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.015 * scale),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(4, (index) {
|
||||
return SizedBox(
|
||||
width: 50 * scale,
|
||||
height: 55 * scale,
|
||||
child: TextField(
|
||||
controller: controllers[index],
|
||||
focusNode: focusNodes[index],
|
||||
textAlign: TextAlign.center,
|
||||
obscureText: true,
|
||||
maxLength: 1,
|
||||
keyboardType: TextInputType.number,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.025 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
counterText: "",
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
borderSide: const BorderSide(color: Colors.grey),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
borderSide: const BorderSide(
|
||||
color: ColorConstants.primaryColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: (val) =>
|
||||
_onMpinChange(val, index, controllers, focusNodes),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
343
lib/views/onboardscreens/Mpin.dart
Normal file
343
lib/views/onboardscreens/Mpin.dart
Normal file
@@ -0,0 +1,343 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/views/onboardscreens/Sign_in.dart';
|
||||
import 'package:nearle/views/onboardscreens/otp_page.dart';
|
||||
import 'package:nearle/widget/Bottom_page.dart';
|
||||
import 'package:nearle/controllers/auth.dart';
|
||||
import 'package:nearle/views/onboardscreens/signin_banner.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class Mpin extends GetResponsiveView {
|
||||
Mpin({super.key});
|
||||
|
||||
@override
|
||||
Widget builder() {
|
||||
return const _MpinView();
|
||||
}
|
||||
}
|
||||
|
||||
class _MpinView extends StatefulWidget {
|
||||
const _MpinView();
|
||||
|
||||
@override
|
||||
State<_MpinView> createState() => _MpinViewState();
|
||||
}
|
||||
|
||||
class _MpinViewState extends State<_MpinView> {
|
||||
final AuthController _auth = Get.put(AuthController());
|
||||
final List<TextEditingController> _mpinControllers = List.generate(
|
||||
4,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
final List<FocusNode> _focusNodes = List.generate(4, (_) => FocusNode());
|
||||
|
||||
bool isVerifying = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_focusNodes[0].requestFocus();
|
||||
_maybeShowMasterPinReminder();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var c in _mpinControllers) {
|
||||
c.dispose();
|
||||
}
|
||||
for (var f in _focusNodes) {
|
||||
f.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onMpinChange(String value, int index) {
|
||||
if (value.isNotEmpty && index < 3) {
|
||||
_focusNodes[index + 1].requestFocus();
|
||||
} else if (value.isEmpty && index > 0) {
|
||||
_focusNodes[index - 1].requestFocus();
|
||||
}
|
||||
|
||||
final filled = _mpinControllers.every((c) => c.text.isNotEmpty);
|
||||
if (filled) {
|
||||
FocusScope.of(context).unfocus();
|
||||
_submitMpin(); // auto-verify when 4 digits are filled
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
String getMpin() => _mpinControllers.map((c) => c.text).join();
|
||||
bool get isMpinFilled => _mpinControllers.every((c) => c.text.isNotEmpty);
|
||||
|
||||
void _clearMpinAndFocus() {
|
||||
for (final c in _mpinControllers) {
|
||||
c.clear();
|
||||
}
|
||||
if (_focusNodes.isNotEmpty) {
|
||||
FocusScope.of(context).requestFocus(_focusNodes[0]);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _submitMpin() async {
|
||||
if (!mounted || isVerifying || !isMpinFilled) return;
|
||||
|
||||
// Register retry callback so AuthController bottom-sheet "Retry" button
|
||||
// can clear MPIN boxes and bring back keyboard when PIN is wrong.
|
||||
_auth.onPinRetry = _clearMpinAndFocus;
|
||||
|
||||
setState(() => isVerifying = true);
|
||||
final mpin = getMpin();
|
||||
final ok = await _auth.verifyPinWithServer(mpin);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => isVerifying = false);
|
||||
|
||||
if (ok) {
|
||||
// ✅ Wait a moment to ensure onduty is saved, then read it
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final onduty = prefs.getInt('onduty') ?? 0;
|
||||
|
||||
debugPrint('[MPIN] After verification - onduty=$onduty');
|
||||
|
||||
// Navigate based on onduty value
|
||||
if (onduty == 0) {
|
||||
debugPrint('[MPIN] Navigating to Introscreen (onduty=0)');
|
||||
Get.offAll(() => SigninBanner());
|
||||
} else {
|
||||
debugPrint('[MPIN] Navigating to BottomPage (onduty=1)');
|
||||
Get.offAll(() => const BottomPage());
|
||||
}
|
||||
} else {
|
||||
// Wrong MPIN: show alert/snackbar and keep user on MPIN screen
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _maybeShowMasterPinReminder() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final forceMasterPin =
|
||||
prefs.getBool(AuthController.forceMasterPinPrefKey) ?? false;
|
||||
if (forceMasterPin) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Use ${AuthController.masterPinValue} as PIN to continue.',
|
||||
),
|
||||
duration: const Duration(seconds: 4),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final height = size.height;
|
||||
final width = size.width;
|
||||
|
||||
double scale = 1.0;
|
||||
if (Get.width < 380) scale = 0.9;
|
||||
if (Get.width > 800) scale = 1.2;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.05),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: height * 0.04),
|
||||
|
||||
SizedBox(
|
||||
height: height * 0.25 * scale,
|
||||
width: width * 0.6,
|
||||
child: Image.asset(
|
||||
"assets/images/Mpin.png",
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.04),
|
||||
|
||||
Text(
|
||||
"Enter Your MPIN",
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.05 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorConstants.primaryColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.01),
|
||||
|
||||
Text(
|
||||
"Access your account securely",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.024 * scale,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.06),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(4, (index) {
|
||||
final isFilled = _mpinControllers[index].text.isNotEmpty;
|
||||
return SizedBox(
|
||||
width: 50 * scale,
|
||||
height: 55 * scale,
|
||||
|
||||
child: RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
|
||||
onKey: (event) {
|
||||
if (event is RawKeyDownEvent &&
|
||||
event.logicalKey ==
|
||||
LogicalKeyboardKey.backspace &&
|
||||
_mpinControllers[index].text.isEmpty &&
|
||||
index > 0) {
|
||||
_mpinControllers[index - 1].clear();
|
||||
_focusNodes[index - 1].requestFocus();
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
|
||||
child: TextField(
|
||||
controller: _mpinControllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
textAlign: TextAlign.center,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 1,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.028 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isFilled ? Colors.white : Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
counterText: "",
|
||||
contentPadding: EdgeInsets.zero,
|
||||
filled: true,
|
||||
fillColor: isFilled
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.transparent,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF662582),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: (value) => _onMpinChange(value, index),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.02),
|
||||
|
||||
// Retry + Forget Pin row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Clear all MPIN boxes and focus first, bringing up keyboard
|
||||
for (final c in _mpinControllers) {
|
||||
c.clear();
|
||||
}
|
||||
if (_focusNodes.isNotEmpty) {
|
||||
FocusScope.of(context).requestFocus(_focusNodes[0]);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.022 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
await _auth.sendOtp();
|
||||
Get.to(OtpPage());
|
||||
},
|
||||
child: Transform.translate(
|
||||
offset: Offset(-18, 0),
|
||||
child: Text(
|
||||
"Forget Pin?",
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.025 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorConstants.primaryColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: height * 0.05,
|
||||
left: width * 0.04,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Get.to(SignIn());
|
||||
},
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(width * 0.02),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black12,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_back,
|
||||
color: Colors.black,
|
||||
size: width * 0.06,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
339
lib/views/onboardscreens/Sign_in.dart
Normal file
339
lib/views/onboardscreens/Sign_in.dart
Normal file
@@ -0,0 +1,339 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/views/onboardscreens/otp_page.dart';
|
||||
import 'package:nearle/views/onboardscreens/Mpin.dart';
|
||||
import 'package:nearle/controllers/auth.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:nearle/providers/notifications/notificationservce.dart';
|
||||
|
||||
class LoginController extends GetxController {
|
||||
var isChecked = true.obs;
|
||||
final AuthController auth = Get.put(AuthController());
|
||||
}
|
||||
|
||||
class SignIn extends StatefulWidget {
|
||||
const SignIn({super.key});
|
||||
|
||||
@override
|
||||
State<SignIn> createState() => _SignInState();
|
||||
}
|
||||
|
||||
class _SignInState extends State<SignIn> {
|
||||
final LoginController controller = Get.put(LoginController());
|
||||
final TextEditingController phoneController = TextEditingController();
|
||||
final FocusNode _phoneFocusNode = FocusNode();
|
||||
|
||||
bool isPhoneValid = false;
|
||||
bool isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
FocusScope.of(context).requestFocus(_phoneFocusNode);
|
||||
// Request notification permission the first time Sign In screen is shown
|
||||
NotificationServce.initialize(context);
|
||||
});
|
||||
}
|
||||
|
||||
bool _validatePhoneNumber(String number) {
|
||||
final RegExp regExp = RegExp(r'^[6-9]\d{9}$');
|
||||
return regExp.hasMatch(number);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneFocusNode.dispose();
|
||||
phoneController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final height = size.height;
|
||||
final width = size.width;
|
||||
double scale = 1.0;
|
||||
if (width < 380) scale = 0.9;
|
||||
if (width > 800) scale = 1.2;
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
left: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: width * 0.05,
|
||||
vertical: height * 0.02,
|
||||
),
|
||||
child: GetBuilder<LoginController>(
|
||||
builder: (_) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: height * 0.04),
|
||||
// Restore hero image at the top like before
|
||||
Image.asset(
|
||||
'assets/images/Nearle Bike.png',
|
||||
height: height * 0.28,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(height: height * 0.03),
|
||||
Text(
|
||||
"Sign In",
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.05 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorConstants.primaryColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.02),
|
||||
Text(
|
||||
"Enter your mobile number to get started.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black54,
|
||||
fontSize: FontConstants.xLarge(context),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.05),
|
||||
|
||||
// Phone number field
|
||||
SizedBox(
|
||||
height: height * 0.07,
|
||||
width: width * 0.9,
|
||||
child: TextField(
|
||||
controller: phoneController,
|
||||
focusNode: _phoneFocusNode,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 10,
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.large(context),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
isPhoneValid = _validatePhoneNumber(value);
|
||||
});
|
||||
|
||||
if (value.length == 10 && isPhoneValid) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
labelText: 'Enter mobile number',
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: width * 0.04,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
prefixIcon: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: width * 0.02,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
"assets/images/in.png",
|
||||
height: height * 0.045,
|
||||
width: width * 0.09,
|
||||
),
|
||||
SizedBox(width: width * 0.01),
|
||||
Text(
|
||||
"+91",
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.large(context),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF662582),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Validation message
|
||||
if (!isPhoneValid && phoneController.text.isNotEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: width * 0.02,
|
||||
top: height * 0.005,
|
||||
),
|
||||
child: Text(
|
||||
'Enter a valid 10-digit mobile number',
|
||||
style: TextStyle(
|
||||
color: Colors.red.shade700,
|
||||
fontSize: width * 0.03,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.02),
|
||||
|
||||
// Terms text with clickable T&C and Privacy Policy
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
children: [
|
||||
const TextSpan(text: 'By continuing, you agree to '),
|
||||
TextSpan(
|
||||
text: 'T&C',
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
final uri = Uri.parse(
|
||||
'https://nearle.in/terms',
|
||||
);
|
||||
final ok = await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
if (!ok) {
|
||||
await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.inAppWebView,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const TextSpan(text: ' and '),
|
||||
TextSpan(
|
||||
text: 'Privacy Policy',
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
final uri = Uri.parse(
|
||||
'https://nearle.in/privacy',
|
||||
);
|
||||
final ok = await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
if (!ok) {
|
||||
await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.inAppWebView,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.02),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom Button
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(width * 0.04),
|
||||
child: SizedBox(
|
||||
height: height * 0.065,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: isPhoneValid && !isLoading
|
||||
? () async {
|
||||
setState(() => isLoading = true);
|
||||
final decision = await controller.auth.precheckPhone(
|
||||
phoneController.text,
|
||||
);
|
||||
setState(() => isLoading = false);
|
||||
if (decision == AuthNext.notRegistered) {
|
||||
return;
|
||||
} else if (decision == AuthNext.otp) {
|
||||
await controller.auth.sendOtp(phoneController.text);
|
||||
Get.to(OtpPage());
|
||||
} else if (decision == AuthNext.verifyPin) {
|
||||
Get.to(() => Mpin());
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Unable to proceed. Please try again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isPhoneValid
|
||||
? const Color(0xFF662582)
|
||||
: Colors.grey.shade400,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: height * 0.015),
|
||||
),
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
height: height * 0.035,
|
||||
width: height * 0.035,
|
||||
child: const CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'Next',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: width * 0.06,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
395
lib/views/onboardscreens/otp_page.dart
Normal file
395
lib/views/onboardscreens/otp_page.dart
Normal file
@@ -0,0 +1,395 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
import 'package:nearle/views/onboardscreens/Sign_in.dart';
|
||||
import 'package:nearle/controllers/auth.dart';
|
||||
import 'package:nearle/views/onboardscreens/Creat_mpin.dart';
|
||||
import 'package:sms_autofill/sms_autofill.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class OtpPage extends GetResponsiveView {
|
||||
OtpPage({super.key});
|
||||
|
||||
@override
|
||||
Widget? phone() => _OtpPageLayout();
|
||||
@override
|
||||
Widget? tablet() => _OtpPageLayout(scale: 1.2);
|
||||
@override
|
||||
Widget? desktop() => _OtpPageLayout(scale: 1.3);
|
||||
}
|
||||
|
||||
class _OtpPageLayout extends StatefulWidget {
|
||||
final double scale;
|
||||
const _OtpPageLayout({this.scale = 1.0});
|
||||
|
||||
@override
|
||||
State<_OtpPageLayout> createState() => _OtpPageLayoutState();
|
||||
}
|
||||
|
||||
class _OtpPageLayoutState extends State<_OtpPageLayout> with CodeAutoFill {
|
||||
final AuthController _auth = Get.put(AuthController());
|
||||
final List<TextEditingController> _otpControllers = List.generate(
|
||||
6,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
|
||||
|
||||
bool isVerifying = false;
|
||||
int _secondsRemaining = 60;
|
||||
Timer? _timer;
|
||||
String _appSignature = '';
|
||||
// Consent fallback removed due to plugin AGP incompatibility
|
||||
int _smsDefaultProvider = 0; // 0 = normal, 1 = passkey provider
|
||||
int? _smsPassKey; // when provider is passkey based
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTimer();
|
||||
_listenForOtp();
|
||||
_loadOtpProviderPrefs();
|
||||
// Ensure cursor starts in first box
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _focusNodes.isNotEmpty) {
|
||||
_focusNodes[0].requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_secondsRemaining = 60;
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_secondsRemaining > 0) {
|
||||
setState(() => _secondsRemaining--);
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _resendOtp() async {
|
||||
setState(() => _secondsRemaining = 60);
|
||||
_timer?.cancel();
|
||||
_startTimer();
|
||||
await _auth.sendOtp();
|
||||
}
|
||||
|
||||
Future<void> _listenForOtp() async {
|
||||
try {
|
||||
await SmsAutoFill().unregisterListener();
|
||||
listenForCode();
|
||||
// Fetch and cache app hash for SMS retriever compatibility
|
||||
try {
|
||||
final sig = await SmsAutoFill().getAppSignature;
|
||||
if (sig.isNotEmpty) {
|
||||
_appSignature = sig;
|
||||
// Helpful for integrating with SMS provider templates
|
||||
debugPrint('[OTP] App signature hash: $_appSignature');
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch (_) {}
|
||||
|
||||
// Consent fallback temporarily disabled; use SMS Retriever with app hash
|
||||
}
|
||||
|
||||
Future<void> _loadOtpProviderPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_smsDefaultProvider = prefs.getInt('smsDefaultProvider') ?? 0;
|
||||
_smsPassKey = prefs.getInt('smsPassKey');
|
||||
if (_smsDefaultProvider == 1 && _smsPassKey != null) {
|
||||
final passKeyStr = _smsPassKey!.toString().padLeft(6, '0');
|
||||
// Prefill only if fields are empty
|
||||
if (mounted && !_otpControllers.any((c) => c.text.isNotEmpty)) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
for (int i = 0; i < 6 && i < passKeyStr.length; i++) {
|
||||
_otpControllers[i].text = passKeyStr[i];
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@override
|
||||
void codeUpdated() {
|
||||
final received = code ?? '';
|
||||
if (received.isNotEmpty) {
|
||||
final digits = received.replaceAll(RegExp(r'\D'), '');
|
||||
if (digits.length >= 6) {
|
||||
final otp = digits.substring(0, 6);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
_otpControllers[i].text = otp[i];
|
||||
}
|
||||
setState(() {});
|
||||
// hide keyboard on autofill and verify with delay before navigation
|
||||
FocusScope.of(context).unfocus();
|
||||
_autoVerify(delayBeforeNav: true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _autoVerify({bool delayBeforeNav = false}) async {
|
||||
if (!mounted) return;
|
||||
if (!_otpControllers.every((c) => c.text.isNotEmpty)) return;
|
||||
if (isVerifying) return;
|
||||
setState(() => isVerifying = true);
|
||||
final entered = getOtp();
|
||||
bool ok = false;
|
||||
// Accept provider passkey as valid OTP when enabled
|
||||
if (_smsDefaultProvider == 1 &&
|
||||
_smsPassKey != null &&
|
||||
entered == _smsPassKey!.toString().padLeft(6, '0')) {
|
||||
ok = true;
|
||||
} else {
|
||||
ok = await _auth.verifyOtp(entered);
|
||||
}
|
||||
setState(() => isVerifying = false);
|
||||
if (ok) {
|
||||
if (delayBeforeNav) {
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
}
|
||||
Get.to(() => CreateMpin());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final controller in _otpControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (final node in _focusNodes) {
|
||||
node.dispose();
|
||||
}
|
||||
_timer?.cancel();
|
||||
try {
|
||||
cancel();
|
||||
} catch (_) {}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onOtpChange(String value, int index) {
|
||||
if (value.isNotEmpty && index < 5) {
|
||||
_focusNodes[index + 1].requestFocus();
|
||||
} else if (value.isNotEmpty && index == 5) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
if (value.isEmpty && index > 0) {
|
||||
// backspace: jump focus back
|
||||
_focusNodes[index - 1].requestFocus();
|
||||
_otpControllers[index - 1].selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: _otpControllers[index - 1].text.length,
|
||||
);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
String getOtp() => _otpControllers.map((e) => e.text).join();
|
||||
bool get isOtpFilled => _otpControllers.every((c) => c.text.isNotEmpty);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scale = widget.scale;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20 * scale,
|
||||
vertical: 20 * scale,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 60 * scale),
|
||||
Image.asset(
|
||||
'assets/images/verify.png',
|
||||
fit: BoxFit.contain,
|
||||
height: 200 * scale,
|
||||
),
|
||||
SizedBox(height: 24 * scale),
|
||||
Text(
|
||||
"Verify OTP",
|
||||
style: TextStyle(
|
||||
fontSize: 28 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24 * scale),
|
||||
Text(
|
||||
"Enter the 6-digit code sent to your number",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black54,
|
||||
fontSize: 19 * scale,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 40 * scale),
|
||||
|
||||
// OTP Fields (6 boxes)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(6, (index) {
|
||||
final isFilled = _otpControllers[index].text.isNotEmpty;
|
||||
return SizedBox(
|
||||
width: 45 * scale,
|
||||
height: 50 * scale,
|
||||
child: TextField(
|
||||
controller: _otpControllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
autofocus: index == 0,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
maxLength: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 17 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isFilled ? Colors.white : Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
filled: true,
|
||||
fillColor: isFilled
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.transparent,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.grey,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: ColorConstants.primaryColor,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
),
|
||||
),
|
||||
onChanged: (value) => _onOtpChange(value, index),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
SizedBox(height: 16 * scale),
|
||||
|
||||
// Resend OTP
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _secondsRemaining == 0
|
||||
? () async {
|
||||
await _resendOtp();
|
||||
}
|
||||
: null,
|
||||
child: Transform.translate(
|
||||
offset: Offset(-10, 0),
|
||||
child: Text(
|
||||
_secondsRemaining == 0
|
||||
? "Resend OTP"
|
||||
: "Resend in 00:${_secondsRemaining.toString().padLeft(2, '0')}",
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
color: _secondsRemaining == 0
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Back button
|
||||
Positioned(
|
||||
top: 40 * scale,
|
||||
left: 16 * scale,
|
||||
child: InkWell(
|
||||
onTap: () => Get.to(() => SignIn()),
|
||||
borderRadius: BorderRadius.circular(30 * scale),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(8 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black12,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_back,
|
||||
color: Colors.black,
|
||||
size: 28 * scale,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
child: SizedBox(
|
||||
height: 55 * scale,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: isOtpFilled && !isVerifying
|
||||
? () async {
|
||||
// Reuse common verification flow (same as auto-verify)
|
||||
await _autoVerify();
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isOtpFilled
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey.shade400,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10 * scale),
|
||||
),
|
||||
),
|
||||
child: isVerifying
|
||||
? SizedBox(
|
||||
height: 30 * scale,
|
||||
width: 30 * scale,
|
||||
child: const CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
"Verify",
|
||||
style: TextStyle(
|
||||
fontSize: 21 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
208
lib/views/onboardscreens/signin_banner.dart
Normal file
208
lib/views/onboardscreens/signin_banner.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:slider_button_lite/feature/presentation/slider_button/slider.dart';
|
||||
import 'package:slider_button_lite/feature/presentation/slider_button/slider_button_prop.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/controllers/riderlog.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/widget/Bottom_page.dart';
|
||||
|
||||
class SigninBanner extends StatefulWidget {
|
||||
const SigninBanner({super.key});
|
||||
|
||||
@override
|
||||
State<SigninBanner> createState() => _SigninBannerState();
|
||||
}
|
||||
|
||||
class _SigninBannerState extends State<SigninBanner> {
|
||||
String _shiftText = 'Your shift: -';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// ✅ CRITICAL: Load shift info in background, don't block UI
|
||||
_loadShiftInfo();
|
||||
}
|
||||
|
||||
Future<void> _loadShiftInfo() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
final s = (prefs.getString('starttime') ?? '').trim();
|
||||
final e = (prefs.getString('endtime') ?? '').trim();
|
||||
_shiftText = (s.isEmpty || e.isEmpty)
|
||||
? 'Your shift: -'
|
||||
: 'Your shift: $s – $e';
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Keep default text on error
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final width = size.width;
|
||||
final height = size.height;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.01),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: height * 0.02),
|
||||
|
||||
Text(
|
||||
"Welcome Back!",
|
||||
style: TextStyle(
|
||||
fontSize: width * 0.07,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF6A1B9A),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: height * 0.01),
|
||||
|
||||
Text(
|
||||
"Start your ride and make today amazing!",
|
||||
style: TextStyle(
|
||||
fontSize: width * 0.04,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: height * 0.04),
|
||||
|
||||
CircleAvatar(
|
||||
radius: width * 0.14,
|
||||
backgroundColor: const Color(0xFFEDE7F6),
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: const Color(0xFF6A1B9A),
|
||||
size: width * 0.12,
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.015),
|
||||
|
||||
// ✅ CRITICAL: Show shift text immediately (no FutureBuilder blocking)
|
||||
Text(
|
||||
_shiftText,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: width * 0.04,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.04),
|
||||
|
||||
Image.asset('assets/images/signin_banner.png'),
|
||||
SizedBox(height: height * 0.05),
|
||||
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final sliderWidth = constraints.maxWidth;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SliderButton(
|
||||
properties: SliderButtonProperties(
|
||||
height: height * 0.07,
|
||||
width: sliderWidth,
|
||||
buttonSize: height * 0.065,
|
||||
disable: false,
|
||||
isLoading: false,
|
||||
backgroundColor: const Color(0xFF6A1B9A),
|
||||
disableButtonColor: const Color(0xFFCCCCDD),
|
||||
dismissThresholds: 0.9,
|
||||
action: () async {
|
||||
try {
|
||||
final rlc = Get.find<RiderLogController>();
|
||||
// Ensure any previous break is ended when coming online
|
||||
debugPrint(
|
||||
'[SIGNIN_BANNER] Ending break before going online',
|
||||
);
|
||||
final breakEnded = await rlc
|
||||
.endBreakAuto()
|
||||
.timeout(
|
||||
const Duration(seconds: 12),
|
||||
onTimeout: () => false,
|
||||
);
|
||||
debugPrint(
|
||||
'[SIGNIN_BANNER] endBreakAuto -> $breakEnded',
|
||||
);
|
||||
// Set rider ON duty (onduty = 1)
|
||||
final ok = await rlc.setOnDuty(true);
|
||||
if (ok && context.mounted) {
|
||||
Get.offAll(() => const BottomPage());
|
||||
}
|
||||
// Show snackbar if still on this screen (unlikely after navigation)
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
ok
|
||||
? "You're now on duty"
|
||||
: "Failed to update status",
|
||||
),
|
||||
backgroundColor: ok
|
||||
? Colors.deepPurple
|
||||
: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[SIGNIN_BANNER] Error updating status: $e',
|
||||
);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
label: Text(
|
||||
'Slide to Start',
|
||||
style: TextStyle(
|
||||
fontSize: width * 0.05,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
alignLabel: Alignment.center,
|
||||
icon: ClipOval(
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
child: SizedBox(
|
||||
width: height * 0.065,
|
||||
height: height * 0.065,
|
||||
child: const Icon(
|
||||
Icons.arrow_forward_ios_outlined,
|
||||
color: Color(0xFF6A1B9A),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: height * 0.04),
|
||||
// Add extra bottom padding for devices with navigation bars
|
||||
SizedBox(height: MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/views/onboardscreens/splashscreen.dart
Normal file
46
lib/views/onboardscreens/splashscreen.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/introscreens/introscreen.dart';
|
||||
|
||||
|
||||
class Splashscreen extends StatefulWidget {
|
||||
const Splashscreen({super.key});
|
||||
|
||||
@override
|
||||
State<Splashscreen> createState() => _SplashscreenState();
|
||||
}
|
||||
|
||||
class _SplashscreenState extends State<Splashscreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Timer(const Duration(seconds: 3), () {
|
||||
Get.to(() => Introscreen());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.secondaryColor,
|
||||
elevation: 0,
|
||||
),
|
||||
backgroundColor: ColorConstants.secondaryColor,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
|
||||
children: [
|
||||
SizedBox(height: 230,),
|
||||
Center(child: Image.asset("assets/images/splashimg.png",fit: BoxFit.contain,height:180,width: 180,)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
264
lib/views/updatescreen/UpdateScreen.dart
Normal file
264
lib/views/updatescreen/UpdateScreen.dart
Normal file
@@ -0,0 +1,264 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'dart:io';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:new_version_plus/new_version_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/views/introscreens/introscreen.dart';
|
||||
import 'package:nearle/widget/Bottom_page.dart';
|
||||
import 'package:nearle/views/onboardscreens/signin_banner.dart';
|
||||
|
||||
class UpdateScreen extends StatefulWidget {
|
||||
final bool mIsForceUpdate;
|
||||
final String mCurrentVersion;
|
||||
final String mUpdateVersion;
|
||||
|
||||
const UpdateScreen({
|
||||
super.key,
|
||||
this.mIsForceUpdate = true,
|
||||
required this.mCurrentVersion,
|
||||
required this.mUpdateVersion,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UpdateScreen> createState() => _UpdateScreenState();
|
||||
}
|
||||
|
||||
class _UpdateScreenState extends State<UpdateScreen>
|
||||
with WidgetsBindingObserver {
|
||||
bool _isCheckingVersion = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed && !_isCheckingVersion) {
|
||||
checkVersionAndNavigate();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> checkVersionAndNavigate() async {
|
||||
if (_isCheckingVersion) return;
|
||||
_isCheckingVersion = true;
|
||||
|
||||
try {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
final newVersion = NewVersionPlus(
|
||||
iOSId: '284882215',
|
||||
androidId: "com.nearle.partner",
|
||||
);
|
||||
|
||||
final status = await newVersion.getVersionStatus();
|
||||
|
||||
if (status != null) {
|
||||
if (!status.canUpdate) {
|
||||
if (mounted) {
|
||||
_navigateToNextScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error checking version: $e");
|
||||
} finally {
|
||||
_isCheckingVersion = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _navigateToNextScreen() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final isLoggedOut = prefs.getBool('logged_out') == true;
|
||||
final savedUserId = prefs.getInt('userid');
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (!isLoggedOut && savedUserId != null && savedUserId > 0) {
|
||||
final onduty = prefs.getInt('onduty') ?? 0;
|
||||
|
||||
if (onduty == 0) {
|
||||
Get.offAll(() => const SigninBanner());
|
||||
} else {
|
||||
Get.offAll(() => const BottomPage());
|
||||
}
|
||||
} else {
|
||||
Get.offAll(() => Introscreen());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double h = Get.height;
|
||||
double w = Get.width;
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (widget.mIsForceUpdate) {
|
||||
SystemNavigator.pop();
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
systemOverlayStyle: const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
),
|
||||
),
|
||||
|
||||
body: SafeArea(
|
||||
top: true,
|
||||
bottom: false, // Bottom safe area handled in bottomNavigationBar
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
children: [
|
||||
/// top spacing (responsive)
|
||||
SizedBox(height: h * 0.06),
|
||||
|
||||
/// 🚀 Image (auto scales)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: w * 0.08),
|
||||
child: Image.asset(
|
||||
"assets/images/update.png",
|
||||
height: h * 0.28,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: h * 0.03),
|
||||
|
||||
/// 🔥 Title
|
||||
Text(
|
||||
"A New Update is Available!",
|
||||
style: TextStyle(
|
||||
fontSize: h * 0.025,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
SizedBox(height: h * 0.015),
|
||||
|
||||
/// 📄 Subtitle
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: w * 0.08),
|
||||
child: Text(
|
||||
"New features are here to make your app experience even smoother and more user-friendly!",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: h * 0.018,
|
||||
color: Colors.grey,
|
||||
height: 1.4,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: h * 0.03),
|
||||
|
||||
/// Version Text
|
||||
Text(
|
||||
"Available version: ${widget.mUpdateVersion}",
|
||||
style: TextStyle(
|
||||
fontSize: h * 0.017,
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
|
||||
Spacer(), // pushes content for large screens
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
/// ⭐ Bottom Button (fixed, responsive) with bottom safe area
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
w * 0.06,
|
||||
0,
|
||||
w * 0.06,
|
||||
h * 0.03,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: h * 0.065,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
onPressed: () => downloadActions(),
|
||||
child: Text(
|
||||
"Update Now",
|
||||
style: TextStyle(
|
||||
fontSize: h * 0.022,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void downloadActions() async {
|
||||
String url;
|
||||
var s = Platform.isAndroid ? "Android" : "Ios";
|
||||
|
||||
if (s == "Android") {
|
||||
url = 'https://play.google.com/store/apps/details?id=com.nearle.partner';
|
||||
} else {
|
||||
url = 'https://apps.apple.com/us/app/nearle/id1596895375ls=1';
|
||||
}
|
||||
|
||||
final uri = Uri.parse(url);
|
||||
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
throw 'Could not launch App';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user