296 lines
8.5 KiB
Dart
296 lines
8.5 KiB
Dart
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<String, dynamic> 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<TicketMessageDto> 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<String, dynamic> 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>()
|
|
.map((e) => TicketMessageDto.fromJson(Map<String, dynamic>.from(e)))
|
|
.toList()
|
|
: <TicketMessageDto>[];
|
|
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<T> {
|
|
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<String, String> _headers() {
|
|
final headers = <String, String>{
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
};
|
|
final t = TokenStorage.accessToken;
|
|
if (t != null && t.isNotEmpty) headers['Authorization'] = 'Bearer $t';
|
|
return headers;
|
|
}
|
|
|
|
Future<TicketResult<List<TicketDto>>> list({
|
|
int page = 1,
|
|
int limit = 50,
|
|
}) async {
|
|
await AuthInterceptor.ensureFreshToken();
|
|
try {
|
|
final params = <String, String>{
|
|
'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>()
|
|
.map((e) => TicketDto.fromJson(Map<String, dynamic>.from(e)))
|
|
.toList()
|
|
: <TicketDto>[];
|
|
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<TicketResult<TicketDto>> 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<String, dynamic>) {
|
|
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<TicketResult<TicketDto>> 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<String, dynamic>) {
|
|
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<TicketResult<void>> 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<T> _withRetry<T>(Future<T> 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;
|
|
}
|
|
}
|