didvan-app/lib/services/content/ticket_service.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;
}
}