Miler rider app: surface system, visible design language, backend lifecycle
Design system - MilerSurface ladder (canvas → working → raised → floating) with MilerPanel as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1. - Visible vocabulary applied across Home, Deliveries, Activity, Account and the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x), canvas wells for anything that opens, small filled tags for shelf labels, demoted placeholders. Recorded in DESIGN_SYSTEM.md §6. - One icon family: 222 Material glyphs migrated to Lucide; none left outside lib/xpress. - Colour semantics corrected: amber only for what is genuinely owed, brand red reserved for the live stop, disabled primaries go neutral rather than pale. Data and lifecycle - lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart makes admin sequence the single ordering authority; service_day.dart, and stop_area.dart rewritten against live Coimbatore addresses (digit-token stripping, city stoplist, street suffixes, stammer collapse). - countLabel states the load once, in bags. Testing - 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity, sheets and verify, with test/failures/ now gitignored (diff debris). - New pins: home_gutter_test, stop_area_test, plus updated structural bounds. Note: this commit also carries pre-existing working-tree deletions that were present before this work (API_SPEC.md, README.md, demo test fixtures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
260
lib/xpress/providers/Riderlog/riderlog_provider.dart
Normal file
260
lib/xpress/providers/Riderlog/riderlog_provider.dart
Normal file
@@ -0,0 +1,260 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
// Combined Riderlog providers:
|
||||
|
||||
/// Hardcoded known-good IPs for hosts where carrier DNS returns broken nodes.
|
||||
///
|
||||
/// **Empty since the delivery line moved to Doormile.** The one entry here
|
||||
/// pinned `queue.workolik.com` to 66.116.225.226, because some Indian carriers'
|
||||
/// DNS resolved that host to a CDN node that answered 404. Nothing calls that
|
||||
/// host any more — every delivery-line write goes to `api.doormile.com` via
|
||||
/// [DeliveryApi] — so the pin was routing advice about a server this app no
|
||||
/// longer talks to.
|
||||
///
|
||||
/// It is deliberately not re-pointed at the Doormile host: that IP belongs to
|
||||
/// the old backend, and pinning it for `api.doormile.com` would send every
|
||||
/// request to the wrong server with the right SNI. If Doormile ever needs the
|
||||
/// same workaround, add it here with its own verified address.
|
||||
const _knownGoodIPs = <String, String>{};
|
||||
|
||||
/// Creates an IOClient that:
|
||||
/// 1. Bypasses SSL certificate errors
|
||||
/// 2. Forces known-good IPs for hosts where carrier DNS returns broken CDN nodes
|
||||
/// 3. Manually does TLS upgrade with correct SNI (hostname, not IP)
|
||||
IOClient _buildSslBypassClient() {
|
||||
final httpClient = HttpClient()
|
||||
..badCertificateCallback = (X509Certificate cert, String host, int port) =>
|
||||
true;
|
||||
|
||||
httpClient
|
||||
.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
|
||||
final host = uri.host;
|
||||
final port = uri.port;
|
||||
|
||||
// Use known-good IP if available, else resolve normally (prefer IPv4)
|
||||
InternetAddress? target;
|
||||
final knownIP = _knownGoodIPs[host];
|
||||
if (knownIP != null) {
|
||||
target = InternetAddress(knownIP);
|
||||
debugPrint('[SSL_CLIENT] Using known-good IP: $knownIP for $host');
|
||||
} else {
|
||||
try {
|
||||
final addresses = await InternetAddress.lookup(
|
||||
host,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
if (addresses.isNotEmpty) target = addresses.first;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (uri.scheme == 'https') {
|
||||
final socketFuture = Socket.connect(target ?? InternetAddress(host), port)
|
||||
.then(
|
||||
(plain) => SecureSocket.secure(
|
||||
plain,
|
||||
host: host, // SNI = original hostname for Nginx routing
|
||||
onBadCertificate: (_) => true,
|
||||
supportedProtocols: ['http/1.1'],
|
||||
),
|
||||
)
|
||||
.then((s) => s as Socket);
|
||||
return ConnectionTask.fromSocket<Socket>(socketFuture, () {});
|
||||
}
|
||||
|
||||
return Socket.startConnect(target ?? InternetAddress(host), port);
|
||||
};
|
||||
|
||||
return IOClient(httpClient);
|
||||
}
|
||||
|
||||
class CreateRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> createRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
const maxAttempts = 3;
|
||||
try {
|
||||
debugPrint('createRiderLog payload ${json.encode(data)}');
|
||||
} catch (_) {}
|
||||
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client.post(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('createRiderLog url $urldata (attempt $attempt)');
|
||||
debugPrint('createRiderLog status ${response.statusCode}');
|
||||
debugPrint('createRiderLog response ${response.body}');
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
return json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint(
|
||||
'createRiderLog failed: HTTP ${response.statusCode} (attempt $attempt/$maxAttempts)',
|
||||
);
|
||||
// On 404/5xx, wait and retry to potentially hit a different CDN node
|
||||
if (attempt < maxAttempts) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('createRiderLog exception (attempt $attempt): $e');
|
||||
if (attempt < maxAttempts) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('createRiderLog failed after $maxAttempts attempts');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> updateRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('updateRiderLog url: $urldata');
|
||||
debugPrint('updateRiderLog response: ${response.body}');
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return decoded;
|
||||
} else {
|
||||
debugPrint(
|
||||
'⚠️ updateRiderLog: Expected Map but got ${decoded.runtimeType}',
|
||||
);
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
debugPrint('❌ updateRiderLog failed with code ${response.statusCode}');
|
||||
return {};
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Exception in updateRiderLog: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GetRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> getRiderLog(String urldata) async {
|
||||
Map<String, dynamic>? getRiderLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('getRiderLog response ${response.body}');
|
||||
debugPrint('getRiderLog url ${urldata.toString()}');
|
||||
getRiderLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return getRiderLogResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getRiderCount(String urldata) async {
|
||||
Map<String, dynamic>? getRiderCountResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('getRiderCount response ${response.body}');
|
||||
debugPrint('getRiderCount url ${urldata.toString()}');
|
||||
getRiderCountResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return getRiderCountResponse;
|
||||
}
|
||||
}
|
||||
|
||||
class BreakRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> createBreakRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
Map<String, dynamic>? breakLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await post(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('createBreakRiderLog url $urldata');
|
||||
debugPrint('createBreakRiderLog response ${response.body}');
|
||||
breakLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return breakLogResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> updateBreakRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
Map<String, dynamic>? breakLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('updateBreakRiderLog url $urldata');
|
||||
debugPrint('updateBreakRiderLog response ${response.body}');
|
||||
breakLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return breakLogResponse;
|
||||
}
|
||||
}
|
||||
143
lib/xpress/providers/delivery/delivery_provider.dart
Normal file
143
lib/xpress/providers/delivery/delivery_provider.dart
Normal file
@@ -0,0 +1,143 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:miler/xpress/views/helpers/constants/apiconstants.dart';
|
||||
|
||||
class DeliveryProvider {
|
||||
final http.Client _client;
|
||||
|
||||
DeliveryProvider({http.Client? client}) : _client = client ?? http.Client();
|
||||
|
||||
Future<http.Response> _getWithRetry(Uri uri, {int maxAttempts = 3}) async {
|
||||
int attempt = 0;
|
||||
while (true) {
|
||||
attempt++;
|
||||
try {
|
||||
return await _client.get(uri);
|
||||
} on SocketException catch (_) {
|
||||
if (attempt >= maxAttempts) rethrow;
|
||||
} on http.ClientException catch (_) {
|
||||
if (attempt >= maxAttempts) rethrow;
|
||||
}
|
||||
// Exponential backoff: 300ms, 600ms
|
||||
final delayMs = 300 * attempt;
|
||||
await Future.delayed(Duration(milliseconds: delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<dynamic>> getDeliveryQueues({
|
||||
required bool live,
|
||||
required int userid,
|
||||
String? orderstatus,
|
||||
}) async {
|
||||
final base = live
|
||||
? ApiConstants.deliveryQueueLive
|
||||
: ApiConstants.deliveryQueueDev;
|
||||
|
||||
// Get current date in YYYY-MM-DD format
|
||||
final now = DateTime.now();
|
||||
final mm = now.month.toString().padLeft(2, '0');
|
||||
final dd = now.day.toString().padLeft(2, '0');
|
||||
final yyyy = now.year.toString();
|
||||
final today = "$yyyy-$mm-$dd";
|
||||
|
||||
final qp = <String, String>{
|
||||
'userid': userid.toString(),
|
||||
'fromdate': today,
|
||||
'todate': today,
|
||||
't': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
};
|
||||
if (orderstatus != null && orderstatus.isNotEmpty) {
|
||||
qp['orderstatus'] = orderstatus;
|
||||
}
|
||||
final uri = Uri.parse(base).replace(queryParameters: qp);
|
||||
|
||||
final res = await _getWithRetry(uri);
|
||||
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
// Log URL and raw status
|
||||
// ignore: avoid_print
|
||||
print('[DELIVERIES][GET] URL: ${uri.toString()}');
|
||||
final decoded = json.decode(res.body);
|
||||
|
||||
final data = decoded is Map<String, dynamic>
|
||||
? (decoded['details'] ?? decoded['data'] ?? decoded)
|
||||
: decoded;
|
||||
|
||||
if (data is List) {
|
||||
try {
|
||||
// Pretty log the list if deliveries are found
|
||||
if (data.isNotEmpty) {
|
||||
// ignore: avoid_print
|
||||
// print('[DELIVERIES][GET] Data: ${json.encode(data)}'); // Heavy log, disabled for performance
|
||||
} else {
|
||||
// ignore: avoid_print
|
||||
// print('[DELIVERIES][GET] Data: []');
|
||||
}
|
||||
} catch (_) {}
|
||||
return data;
|
||||
}
|
||||
if (data is Map && data['items'] is List) return data['items'] as List;
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
throw Exception('Failed (${res.statusCode})');
|
||||
}
|
||||
|
||||
Future<List<dynamic>> getDeliveryQueuesPicked({
|
||||
required bool live,
|
||||
required int userid,
|
||||
}) async {
|
||||
// Use v3 getdeliveries with fromdate/todate as today's date (dynamic)
|
||||
final base = live
|
||||
? ApiConstants.currentDeliveryV3Live
|
||||
: ApiConstants.currentDeliveryV3Dev;
|
||||
// Get current date in YYYY-MM-DD format (dynamically updated each day)
|
||||
final now = DateTime.now();
|
||||
final mm = now.month.toString().padLeft(2, '0');
|
||||
final dd = now.day.toString().padLeft(2, '0');
|
||||
final yyyy = now.year.toString();
|
||||
final today = "$yyyy-$mm-$dd";
|
||||
|
||||
final qp = <String, String>{
|
||||
'userid': userid.toString(),
|
||||
'fromdate': today,
|
||||
'todate': today,
|
||||
't': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
};
|
||||
final uri = Uri.parse(base).replace(queryParameters: qp);
|
||||
|
||||
final res = await _getWithRetry(uri);
|
||||
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
// ignore: avoid_print
|
||||
print('[DELIVERIES][GET_PICKED] URL: ${uri.toString()}');
|
||||
final decoded = json.decode(res.body);
|
||||
|
||||
// The API returns {"code":200,"details":[...],"message":"Success","status":true}
|
||||
final data = decoded is Map<String, dynamic>
|
||||
? (decoded['details'] ?? decoded['data'] ?? decoded)
|
||||
: decoded;
|
||||
|
||||
if (data is List) {
|
||||
try {
|
||||
if (data.isNotEmpty) {
|
||||
// ignore: avoid_print
|
||||
// print('[DELIVERIES][GET_PICKED] Data: ${json.encode(data)}'); // Heavy log, disabled
|
||||
} else {
|
||||
// ignore: avoid_print
|
||||
// print('[DELIVERIES][GET_PICKED] Data: []');
|
||||
}
|
||||
} catch (_) {}
|
||||
return data;
|
||||
}
|
||||
if (data is Map && data['items'] is List) return data['items'] as List;
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
throw Exception('Failed (${res.statusCode})');
|
||||
}
|
||||
}
|
||||
283
lib/xpress/providers/deliverylog/deliverylog_provider.dart
Normal file
283
lib/xpress/providers/deliverylog/deliverylog_provider.dart
Normal file
@@ -0,0 +1,283 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
// Combined Deliverylog providers:
|
||||
|
||||
/// Hardcoded known-good IPs for hosts where carrier DNS returns broken CDN nodes.
|
||||
/// Confirmed: 66.116.225.226 = 200 OK, 125.21.240.67 = 404/405.
|
||||
/// Empty since the delivery line moved to Doormile — see the note on the
|
||||
/// matching map in `providers/Riderlog/riderlog_provider.dart`. The old entry
|
||||
/// pinned `queue.workolik.com`, a host nothing calls now, and must not be
|
||||
/// re-pointed at `api.doormile.com`.
|
||||
const _knownGoodIPs = <String, String>{};
|
||||
|
||||
/// Creates an IOClient that:
|
||||
/// 1. Bypasses SSL certificate errors
|
||||
/// 2. Forces known-good IPs to avoid broken CDN nodes from carrier DNS
|
||||
/// 3. Manually does TLS upgrade with correct SNI (hostname, not IP)
|
||||
IOClient _buildSslBypassClient() {
|
||||
final httpClient = HttpClient()
|
||||
..badCertificateCallback = (X509Certificate cert, String host, int port) =>
|
||||
true;
|
||||
|
||||
httpClient
|
||||
.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
|
||||
final host = uri.host;
|
||||
final port = uri.port;
|
||||
|
||||
InternetAddress? target;
|
||||
final knownIP = _knownGoodIPs[host];
|
||||
if (knownIP != null) {
|
||||
target = InternetAddress(knownIP);
|
||||
} else {
|
||||
try {
|
||||
final addresses = await InternetAddress.lookup(
|
||||
host,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
if (addresses.isNotEmpty) target = addresses.first;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (uri.scheme == 'https') {
|
||||
final socketFuture = Socket.connect(target ?? InternetAddress(host), port)
|
||||
.then(
|
||||
(plain) => SecureSocket.secure(
|
||||
plain,
|
||||
host: host,
|
||||
onBadCertificate: (_) => true,
|
||||
supportedProtocols: ['http/1.1'],
|
||||
),
|
||||
)
|
||||
.then((s) => s as Socket);
|
||||
return ConnectionTask.fromSocket<Socket>(socketFuture, () {});
|
||||
}
|
||||
|
||||
return Socket.startConnect(target ?? InternetAddress(host), port);
|
||||
};
|
||||
|
||||
return IOClient(httpClient);
|
||||
}
|
||||
|
||||
class CreateDeliveryLogProvider {
|
||||
Future<Map<String, dynamic>?> createDeliveryLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data, {
|
||||
bool wrapInArray = true,
|
||||
}) async {
|
||||
Map<String, dynamic>? result;
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final body = json.encode(wrapInArray ? [data] : data);
|
||||
final response = await client
|
||||
.post(
|
||||
url,
|
||||
body: body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
debugPrint('createDeliveryLog url $urldata');
|
||||
debugPrint(body);
|
||||
debugPrint('createDeliveryLog response ${response.body}');
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
result = json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
debugPrint('createDeliveryLog parsed ${result.toString()}');
|
||||
} else {
|
||||
debugPrint('createDeliveryLog failed: HTTP ${response.statusCode}');
|
||||
}
|
||||
} on TimeoutException catch (e) {
|
||||
debugPrint('createDeliveryLog timeout: $e');
|
||||
} catch (e) {
|
||||
debugPrint('createDeliveryLog error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateDeliveryProvider {
|
||||
Future<Map<String, dynamic>?> updateDelivery(
|
||||
Map<String, dynamic> data,
|
||||
String urldata,
|
||||
) async {
|
||||
Map<String, dynamic>? updateDeliveryResponse;
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client
|
||||
.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
debugPrint('updateDelivery url $urldata');
|
||||
debugPrint('updateDelivery status ${response.statusCode}');
|
||||
debugPrint('updateDelivery response ${response.body}');
|
||||
debugPrint(json.encode(data));
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
updateDeliveryResponse =
|
||||
json.decode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint('updateDelivery failed: HTTP ${response.statusCode}');
|
||||
}
|
||||
} on TimeoutException catch (e) {
|
||||
debugPrint('updateDelivery timeout: $e');
|
||||
} catch (e) {
|
||||
debugPrint('updateDelivery error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return updateDeliveryResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> updateArrivedDelivery(
|
||||
Map<String, dynamic> data,
|
||||
String urldata,
|
||||
) async {
|
||||
Map<String, dynamic>? updateDeliveryResponse;
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client
|
||||
.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
debugPrint('updateArrived url $urldata');
|
||||
debugPrint('updateArrived status ${response.statusCode}');
|
||||
debugPrint(json.encode(data));
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
updateDeliveryResponse =
|
||||
json.decode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint('updateArrived failed: HTTP ${response.statusCode}');
|
||||
}
|
||||
} on TimeoutException catch (e) {
|
||||
debugPrint('updateArrived timeout: $e');
|
||||
} catch (e) {
|
||||
debugPrint('updateArrived error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return updateDeliveryResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> updatePickedDelivery(
|
||||
Map<String, dynamic> data,
|
||||
String urldata,
|
||||
) async {
|
||||
Map<String, dynamic>? updateDeliveryResponse;
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client
|
||||
.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
debugPrint('updatePicked url $urldata');
|
||||
debugPrint('updatePicked status ${response.statusCode}');
|
||||
debugPrint(json.encode(data));
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
updateDeliveryResponse =
|
||||
json.decode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint('updatePicked failed: HTTP ${response.statusCode}');
|
||||
}
|
||||
} on TimeoutException catch (e) {
|
||||
debugPrint('updatePicked timeout: $e');
|
||||
} catch (e) {
|
||||
debugPrint('updatePicked error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return updateDeliveryResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> updateActiveDelivery(
|
||||
Map<String, dynamic> data,
|
||||
String urldata,
|
||||
) async {
|
||||
Map<String, dynamic>? updateDeliveryResponse;
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client
|
||||
.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
debugPrint('updateActive url $urldata');
|
||||
debugPrint('updateActive status ${response.statusCode}');
|
||||
debugPrint(json.encode(data));
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
updateDeliveryResponse =
|
||||
json.decode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint('updateActive failed: HTTP ${response.statusCode}');
|
||||
}
|
||||
} on TimeoutException catch (e) {
|
||||
debugPrint('updateActive timeout: $e');
|
||||
} catch (e) {
|
||||
debugPrint('updateActive error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return updateDeliveryResponse;
|
||||
}
|
||||
}
|
||||
|
||||
class GetDeliveryLogProvider {
|
||||
Future<Map<String, dynamic>?> getDeliveryLog(String urldata) async {
|
||||
Map<String, dynamic>? result;
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client.get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('getDeliveryLog url $urldata');
|
||||
debugPrint('getDeliveryLog response ${response.body}');
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
result = json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('getDeliveryLog error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
410
lib/xpress/providers/notifications/notificationservce.dart
Normal file
410
lib/xpress/providers/notifications/notificationservce.dart
Normal file
@@ -0,0 +1,410 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:miler/helpers/http_overrides.dart';
|
||||
|
||||
// Top-level background handler required by Firebase Messaging
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
HttpOverrides.global = MyHttpOverrides();
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
} catch (_) {}
|
||||
|
||||
await NotificationServce.display(message);
|
||||
}
|
||||
|
||||
class NotificationServce {
|
||||
static final FirebaseMessaging _firebaseMessaging =
|
||||
FirebaseMessaging.instance;
|
||||
static final FlutterLocalNotificationsPlugin _notificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
static final AudioPlayer _player = AudioPlayer();
|
||||
static String _channelId = 'Doormile';
|
||||
static bool _isPlaying = false;
|
||||
|
||||
static const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||
'Doormile',
|
||||
'Doormile Notification',
|
||||
description: 'Channel for Doormile notifications',
|
||||
importance: Importance.max,
|
||||
playSound: true,
|
||||
enableVibration: true,
|
||||
showBadge: true,
|
||||
);
|
||||
|
||||
static Future<void> initialize(BuildContext context) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final alreadyInit = prefs.getBool('notifications_init_done') ?? false;
|
||||
if (alreadyInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
await FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
final existing = (prefs.getString('order_alert_sound') ?? '').trim();
|
||||
if (existing.isEmpty) {
|
||||
await prefs.setString('order_alert_sound', 'assets/audio/alert-1.mp3');
|
||||
}
|
||||
await prefs.setBool('notifications_init_done', true);
|
||||
} catch (_) {}
|
||||
|
||||
await _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>()
|
||||
?.createNotificationChannel(channel);
|
||||
|
||||
await _applyChannelSoundFromPrefs();
|
||||
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
|
||||
iOS: DarwinInitializationSettings(
|
||||
requestSoundPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestAlertPermission: true,
|
||||
defaultPresentSound: true,
|
||||
defaultPresentBadge: true,
|
||||
defaultPresentBanner: true,
|
||||
defaultPresentAlert: true,
|
||||
defaultPresentList: true,
|
||||
),
|
||||
);
|
||||
|
||||
await _notificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse:
|
||||
(NotificationResponse response) async {},
|
||||
);
|
||||
|
||||
RemoteMessage? initialMessage = await _firebaseMessaging
|
||||
.getInitialMessage();
|
||||
if (initialMessage != null) {
|
||||
await _handleInitialMessage(initialMessage);
|
||||
}
|
||||
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
|
||||
await _handleMessage(message);
|
||||
});
|
||||
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async {
|
||||
await _handleMessageOpenedApp(message);
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Removed duplicated background handler (this was breaking your notifications)
|
||||
|
||||
static Future<void> _handleInitialMessage(RemoteMessage message) async {
|
||||
if (message.notification != null) {
|
||||
await display(message);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _handleMessage(RemoteMessage message) async {
|
||||
if (message.notification != null) {
|
||||
await display(message);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _handleMessageOpenedApp(RemoteMessage message) async {}
|
||||
|
||||
static Future<void> _applyChannelSoundFromPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
String sel = (prefs.getString('order_alert_sound') ?? '').trim();
|
||||
if (sel.isEmpty) {
|
||||
sel = 'assets/audio/alert-1.mp3';
|
||||
}
|
||||
|
||||
final fileName = sel.split('/').last;
|
||||
final base = fileName.split('.').first;
|
||||
final rawName = base.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_');
|
||||
|
||||
final androidImpl = _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>();
|
||||
|
||||
if (androidImpl != null) {
|
||||
_channelId = 'Doormile_$rawName';
|
||||
|
||||
final custom = AndroidNotificationChannel(
|
||||
_channelId,
|
||||
'Doormile Notification',
|
||||
description: 'Channel for Doormile notifications',
|
||||
importance: Importance.max,
|
||||
playSound: true,
|
||||
sound: RawResourceAndroidNotificationSound(rawName),
|
||||
enableVibration: true,
|
||||
showBadge: true,
|
||||
);
|
||||
|
||||
await androidImpl.createNotificationChannel(custom);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<String?> _downloadAndSaveImage(
|
||||
String imageUrl,
|
||||
String fileName,
|
||||
) async {
|
||||
try {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final filePath = '${directory.path}/$fileName';
|
||||
final response = await http.get(Uri.parse(imageUrl));
|
||||
if (response.statusCode == 200) {
|
||||
final file = File(filePath);
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
return filePath;
|
||||
}
|
||||
return null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static String? _extractImageUrl(RemoteMessage message) {
|
||||
String? imageUrl = message.data['image'] as String?;
|
||||
imageUrl ??= message.notification?.android?.imageUrl;
|
||||
imageUrl ??= message.notification?.apple?.imageUrl;
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
static Future<void> _playSelectedSound({int times = 1}) async {
|
||||
try {
|
||||
if (_isPlaying) return;
|
||||
|
||||
_isPlaying = true;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
String selected = (prefs.getString('order_alert_sound') ?? '').trim();
|
||||
if (selected.isEmpty) {
|
||||
selected = 'assets/audio/alert-1.mp3';
|
||||
}
|
||||
|
||||
final rel = selected.startsWith('assets/')
|
||||
? selected.replaceFirst('assets/', '')
|
||||
: selected;
|
||||
|
||||
await _player.stop();
|
||||
await _player.setReleaseMode(ReleaseMode.stop);
|
||||
|
||||
for (int i = 0; i < times; i++) {
|
||||
await _player.play(AssetSource(rel));
|
||||
try {
|
||||
await _player.onPlayerComplete.first;
|
||||
} catch (_) {}
|
||||
if (i < times - 1) {
|
||||
await Future.delayed(const Duration(milliseconds: 120));
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
} finally {
|
||||
_isPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Lightweight local notification helper for in-app events (no FCM message).
|
||||
static Future<void> showLocalNotification({
|
||||
required String title,
|
||||
required String body,
|
||||
bool playSound = true,
|
||||
String? payload,
|
||||
}) async {
|
||||
try {
|
||||
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
_channelId,
|
||||
'Doormile Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
playSound: playSound,
|
||||
enableVibration: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false,
|
||||
autoCancel: true,
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: playSound,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
),
|
||||
);
|
||||
|
||||
await _notificationsPlugin.show(
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
notificationDetails,
|
||||
payload: payload,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<void> display(RemoteMessage message) async {
|
||||
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
final payload = jsonEncode({'id': id.toString(), 'data': message.data});
|
||||
|
||||
final appState = WidgetsBinding.instance.lifecycleState;
|
||||
final bool isForeground = appState == AppLifecycleState.resumed;
|
||||
|
||||
if (isForeground) {
|
||||
await _playSelectedSound(times: 5);
|
||||
}
|
||||
|
||||
NotificationDetails notificationDetails;
|
||||
|
||||
final imageUrl = _extractImageUrl(message);
|
||||
String? persistedImageUrl = imageUrl;
|
||||
String? persistedImagePath;
|
||||
|
||||
if (imageUrl != null && imageUrl.isNotEmpty) {
|
||||
final imagePath = await _downloadAndSaveImage(
|
||||
imageUrl,
|
||||
'notification_image.jpg',
|
||||
);
|
||||
persistedImagePath = imagePath;
|
||||
|
||||
notificationDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
_channelId,
|
||||
'Doormile Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
playSound: !isForeground,
|
||||
enableVibration: true,
|
||||
fullScreenIntent: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false,
|
||||
autoCancel: true,
|
||||
styleInformation: imagePath != null
|
||||
? BigPictureStyleInformation(FilePathAndroidBitmap(imagePath))
|
||||
: const DefaultStyleInformation(true, true),
|
||||
),
|
||||
iOS: const DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
notificationDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
_channelId,
|
||||
'Doormile Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
playSound: !isForeground,
|
||||
enableVibration: true,
|
||||
fullScreenIntent: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false,
|
||||
autoCancel: true,
|
||||
),
|
||||
iOS: const DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isForeground) {
|
||||
final ctx = Get.context;
|
||||
if (ctx != null) {
|
||||
final title =
|
||||
message.notification?.title ?? message.data['title'] ?? 'Doormile';
|
||||
final body = message.notification?.body ?? message.data['body'] ?? '';
|
||||
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
if (body.isNotEmpty) Text(body),
|
||||
],
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await _notificationsPlugin.show(
|
||||
id,
|
||||
message.notification?.title ?? message.data['title'] ?? 'Doormile',
|
||||
message.notification?.body ?? message.data['body'] ?? 'Notification',
|
||||
notificationDetails,
|
||||
payload: payload,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final nowIso = DateTime.now().toIso8601String();
|
||||
|
||||
final title =
|
||||
message.notification?.title ?? message.data['title'] ?? 'Doormile';
|
||||
final body = message.notification?.body ?? message.data['body'] ?? '';
|
||||
|
||||
final entry = {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'body': body,
|
||||
'time': nowIso,
|
||||
'data': message.data,
|
||||
if (persistedImageUrl != null) 'imageUrl': persistedImageUrl,
|
||||
if (persistedImagePath != null) 'imagePath': persistedImagePath,
|
||||
};
|
||||
|
||||
final existingRaw = prefs.getString('notifications_log');
|
||||
List<dynamic> list = [];
|
||||
|
||||
if (existingRaw != null && existingRaw.isNotEmpty) {
|
||||
try {
|
||||
list = jsonDecode(existingRaw) as List<dynamic>;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
list.insert(0, entry);
|
||||
|
||||
if (list.length > 100) list = list.sublist(0, 100);
|
||||
|
||||
await prefs.setString('notifications_log', jsonEncode(list));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
29
lib/xpress/providers/summary/riderweeklykms.dart
Normal file
29
lib/xpress/providers/summary/riderweeklykms.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:miler/xpress/Models/summary/riderweeklykms.dart';
|
||||
import 'package:miler/xpress/controllers/riderkm.dart';
|
||||
|
||||
class RiderWeeklyKmProvider extends ChangeNotifier {
|
||||
final RiderWeeklyKmController _controller = RiderWeeklyKmController();
|
||||
|
||||
bool isLoading = false;
|
||||
String? error;
|
||||
List<RiderWeeklyKms> kmsList = [];
|
||||
double totalKms = 0.0;
|
||||
|
||||
Future<void> fetchRiderWeeklyKms(int userId) async {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final result = await _controller.getRiderWeeklyKms(userId);
|
||||
kmsList = result['details'];
|
||||
totalKms = result['total_kms'];
|
||||
} catch (e) {
|
||||
error = e.toString();
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
31
lib/xpress/providers/summary/summary.dart
Normal file
31
lib/xpress/providers/summary/summary.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:miler/xpress/models/summary/deliverystats.dart';
|
||||
import 'package:miler/xpress/views/helpers/constants/apiconstants.dart';
|
||||
|
||||
class SummaryProvider {
|
||||
final String baseUrl = ApiConstants.summaryApiLive;
|
||||
|
||||
Future<DeliveryStats?> fetchSummaryStats(int userId) async {
|
||||
final url = Uri.parse('$baseUrl/getdeliverystats?userid=$userId');
|
||||
|
||||
try {
|
||||
print(url);
|
||||
final response = await http.get(url);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded['status'] == true && decoded['data'] != null) {
|
||||
return DeliveryStats.fromJson(decoded['data']);
|
||||
} else {
|
||||
print('API returned false status: ${decoded['message']}');
|
||||
}
|
||||
} else {
|
||||
print('something went wrong');
|
||||
}
|
||||
} catch (e) {
|
||||
print('something went wrong');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
137
lib/xpress/providers/support/support_ticket.dart
Normal file
137
lib/xpress/providers/support/support_ticket.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||
import 'package:get/get_state_manager/src/simple/get_controllers.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:miler/xpress/Models/supportticket/support_ticket.dart';
|
||||
import 'package:miler/xpress/data/delivery_api.dart';
|
||||
|
||||
class SupportTicketController extends GetxController {
|
||||
final RxList<SupportTicketModel> tickets = <SupportTicketModel>[].obs;
|
||||
final RxBool isLoading = true.obs;
|
||||
final RxString errorMessage = ''.obs;
|
||||
final RxBool isSubmitting = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchTickets();
|
||||
}
|
||||
|
||||
Future<void> fetchTickets() async {
|
||||
try {
|
||||
isLoading(true);
|
||||
errorMessage('');
|
||||
|
||||
const userId = 1242;
|
||||
final url = Uri.parse('${DeliveryApi.getSupportTickets}?userid=$userId');
|
||||
|
||||
final response = await http.get(
|
||||
url,
|
||||
headers: {'Accept': 'application/json'},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Server error: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final Map<String, dynamic> jsonResponse = json.decode(response.body);
|
||||
if (jsonResponse['status'] != true) {
|
||||
throw Exception(jsonResponse['message'] ?? 'Unknown error');
|
||||
}
|
||||
|
||||
final List<dynamic> data = jsonResponse['data'];
|
||||
tickets.assignAll(
|
||||
data.map((e) => SupportTicketModel.fromJson(e)).toList(),
|
||||
);
|
||||
} catch (e) {
|
||||
errorMessage(e.toString());
|
||||
} finally {
|
||||
isLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> createTicket({
|
||||
required int userid,
|
||||
required String category,
|
||||
required String priority,
|
||||
required String subject,
|
||||
required String issue,
|
||||
List<XFile>? attachments,
|
||||
}) async {
|
||||
try {
|
||||
isSubmitting(true);
|
||||
|
||||
// Step 1: Upload image if attached (adjust endpoint if needed)
|
||||
String? imageUrl;
|
||||
if (attachments != null && attachments.isNotEmpty) {
|
||||
// For simplicity, assume first image; upload to a temp endpoint or your main one
|
||||
final imageFile = File(attachments.first.path);
|
||||
final imageBytes = await imageFile.readAsBytes();
|
||||
final imageName = attachments.first.name;
|
||||
|
||||
// Example image upload (replace with your actual image upload endpoint)
|
||||
final uploadUrl = Uri.parse(DeliveryApi.uploadImage); // Adjust URL
|
||||
final imageRequest = http.MultipartRequest('POST', uploadUrl)
|
||||
..files.add(
|
||||
http.MultipartFile.fromBytes(
|
||||
'image',
|
||||
imageBytes,
|
||||
filename: imageName,
|
||||
),
|
||||
);
|
||||
imageRequest.headers['Accept'] = 'application/json';
|
||||
|
||||
final imageResponse = await imageRequest.send();
|
||||
if (imageResponse.statusCode == 200) {
|
||||
final imageJson = await http.Response.fromStream(imageResponse);
|
||||
imageUrl = json.decode(
|
||||
imageJson.body,
|
||||
)['image_url']; // Assume response has 'image_url'
|
||||
} else {
|
||||
throw Exception('Image upload failed');
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Create ticket with POST
|
||||
final postUrl = Uri.parse(DeliveryApi.createSupportTicket);
|
||||
final body = json.encode({
|
||||
'userid': userid,
|
||||
'category': category,
|
||||
'priority': priority,
|
||||
'subject': subject,
|
||||
'issue': issue,
|
||||
'image': imageUrl, // null if no image
|
||||
});
|
||||
|
||||
final response = await http.post(
|
||||
postUrl,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to create ticket: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final Map<String, dynamic> jsonResponse = json.decode(response.body);
|
||||
if (jsonResponse['status'] != true) {
|
||||
throw Exception(jsonResponse['message'] ?? 'Unknown error');
|
||||
}
|
||||
|
||||
// Refresh tickets to show new one
|
||||
await fetchTickets();
|
||||
return true;
|
||||
} catch (e) {
|
||||
errorMessage(e.toString());
|
||||
return false;
|
||||
} finally {
|
||||
isSubmitting(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user