import 'dart:async'; import 'dart:convert'; import 'dart:developer' as dev; import 'dart:io' show SocketException; // ignore: depend_on_referenced_packages import 'package:http/http.dart' as http; import 'package:didvan/config/auth_config.dart'; import 'package:didvan/services/auth/auth_interceptor.dart'; import 'package:didvan/services/auth/token_storage.dart'; class TicketMessageDto { final String id; final String body; final String senderId; final DateTime? createdAt; const TicketMessageDto({ required this.id, required this.body, required this.senderId, this.createdAt, }); factory TicketMessageDto.fromJson(Map json) { DateTime? ca; final c = json['createdAt']; if (c is String && c.isNotEmpty) ca = DateTime.tryParse(c); return TicketMessageDto( id: json['id']?.toString() ?? '', body: json['body']?.toString() ?? '', senderId: json['senderId']?.toString() ?? '', createdAt: ca, ); } } class TicketDto { final String id; final String title; final String senderId; final String receiver; final String status; final DateTime? createdAt; final DateTime? updatedAt; final List messages; const TicketDto({ required this.id, required this.title, required this.senderId, required this.receiver, required this.status, this.createdAt, this.updatedAt, this.messages = const [], }); factory TicketDto.fromJson(Map json) { DateTime? ca, ua; final c = json['createdAt']; final u = json['updatedAt']; if (c is String && c.isNotEmpty) ca = DateTime.tryParse(c); if (u is String && u.isNotEmpty) ua = DateTime.tryParse(u); final msgsRaw = json['messages']; final msgs = msgsRaw is List ? msgsRaw .whereType() .map((e) => TicketMessageDto.fromJson(Map.from(e))) .toList() : []; return TicketDto( id: json['id']?.toString() ?? '', title: json['title']?.toString() ?? '', senderId: json['senderId']?.toString() ?? '', receiver: json['receiver']?.toString() ?? '', status: json['status']?.toString() ?? 'open', createdAt: ca, updatedAt: ua, messages: msgs, ); } } class TicketResult { final bool isSuccess; final int? statusCode; final T? data; final String? errorMessage; const TicketResult({ required this.isSuccess, this.statusCode, this.data, this.errorMessage, }); } class TicketService { TicketService._(); static final TicketService instance = TicketService._(); String get _base => AuthConfig.apiBaseUrl; static const _timeout = Duration(seconds: 15); Map _headers() { final headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', }; final t = TokenStorage.accessToken; if (t != null && t.isNotEmpty) headers['Authorization'] = 'Bearer $t'; return headers; } Future>> list({ int page = 1, int limit = 50, }) async { await AuthInterceptor.ensureFreshToken(); try { final params = { 'page': page.toString(), 'limit': limit.toString(), 'sortBy': 'createdAt:DESC', 't': DateTime.now().millisecondsSinceEpoch.toString(), }; final uri = Uri.parse('$_base/ticketing/tickets') .replace(queryParameters: params); final r = await _withRetry( () => http.get(uri, headers: _headers()).timeout(_timeout), ); if (r.statusCode == 200) { final body = json.decode(r.body); final dataRaw = body is Map ? body['data'] : null; final list = dataRaw is List ? dataRaw .whereType() .map((e) => TicketDto.fromJson(Map.from(e))) .toList() : []; return TicketResult(isSuccess: true, statusCode: 200, data: list); } return TicketResult( isSuccess: false, statusCode: r.statusCode, errorMessage: _extractError(r.body) ?? 'خطا در دریافت تیکت‌ها.', ); } catch (e) { dev.log('list error: $e', name: 'TicketService'); return const TicketResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } Future> getOne(String id) async { await AuthInterceptor.ensureFreshToken(); try { final ts = DateTime.now().millisecondsSinceEpoch.toString(); final r = await _withRetry( () => http .get( Uri.parse('$_base/ticketing/tickets/$id?t=$ts'), headers: _headers(), ) .timeout(_timeout), ); if (r.statusCode == 200) { final body = json.decode(r.body); if (body is Map) { return TicketResult( isSuccess: true, statusCode: 200, data: TicketDto.fromJson(body), ); } } return TicketResult( isSuccess: false, statusCode: r.statusCode, errorMessage: _extractError(r.body) ?? 'خطا در دریافت تیکت.', ); } catch (e) { dev.log('getOne error: $e', name: 'TicketService'); return const TicketResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } Future> create({ required String title, required String body, required String receiver, }) async { await AuthInterceptor.ensureFreshToken(); try { final r = await _withRetry(() => http .post( Uri.parse('$_base/ticketing/tickets'), headers: _headers(), body: json.encode({ 'title': title, 'body': body, 'receiver': receiver, }), ) .timeout(_timeout)); if (r.statusCode >= 200 && r.statusCode < 300) { final body = json.decode(r.body); if (body is Map) { return TicketResult( isSuccess: true, statusCode: r.statusCode, data: TicketDto.fromJson(body), ); } return const TicketResult(isSuccess: true); } return TicketResult( isSuccess: false, statusCode: r.statusCode, errorMessage: _extractError(r.body) ?? 'خطا در ایجاد تیکت.', ); } catch (e) { dev.log('create error: $e', name: 'TicketService'); return const TicketResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } Future> reply({ required String ticketId, required String body, }) async { await AuthInterceptor.ensureFreshToken(); try { final r = await _withRetry(() => http .post( Uri.parse('$_base/ticketing/tickets/$ticketId/reply'), headers: _headers(), body: json.encode({'body': body}), ) .timeout(_timeout)); if (r.statusCode >= 200 && r.statusCode < 300) { return const TicketResult(isSuccess: true); } return TicketResult( isSuccess: false, statusCode: r.statusCode, errorMessage: _extractError(r.body) ?? 'خطا در ارسال پاسخ.', ); } catch (e) { dev.log('reply error: $e', name: 'TicketService'); return const TicketResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } Future _withRetry(Future Function() body) async { for (var attempt = 0; attempt < 3; attempt++) { try { return await body(); } on SocketException { if (attempt == 2) rethrow; await Future.delayed(Duration(milliseconds: 250 * (attempt + 1))); } on http.ClientException { if (attempt == 2) rethrow; await Future.delayed(Duration(milliseconds: 250 * (attempt + 1))); } on TimeoutException { if (attempt == 2) rethrow; } } throw StateError('retry loop exited without return'); } String? _extractError(String body) { if (body.isEmpty) return null; try { final data = json.decode(body); if (data is Map) { final msg = data['message'] ?? data['error']; if (msg is String && msg.isNotEmpty) return msg; if (msg is List && msg.isNotEmpty) return msg.first.toString(); } } catch (_) {} return null; } }