/
opt
/
canhelp
/
apps
/
mobile
/
lib
/
services
/
/opt/canhelp/apps/mobile/lib/services
mkdir
upload
Name
Size
Mode
Actions
api_client.dart
59021
0644
edit
dl
rm
push_service.dart
4495
0644
edit
dl
rm
storage_service.dart
9090
0644
edit
dl
rm
Edit:
/opt/canhelp/apps/mobile/lib/services/api_client.dart
(59021B)
import 'dart:convert'; import 'dart:io' show Platform; import 'dart:typed_data'; import 'package:flutter/foundation.dart' show kReleaseMode; import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart' as http_parser; import '../models/app_notification.dart'; import '../models/availability.dart'; import '../models/category.dart'; import '../models/chat_room.dart'; import '../models/dashboard_data.dart'; import '../models/location.dart'; import '../models/offer.dart'; import '../models/portfolio_item.dart'; import '../models/price_list.dart'; import '../models/review.dart'; import '../models/specialist_card.dart'; import '../models/task.dart'; import '../models/user.dart'; import 'storage_service.dart'; class ApiException implements Exception { final int statusCode; final String message; final Map<String, dynamic>? body; ApiException(this.statusCode, this.message, [this.body]); @override String toString() => 'ApiException($statusCode): $message'; } class ApiClient { static final ApiClient _instance = ApiClient._internal(); factory ApiClient() => _instance; ApiClient._internal(); final StorageService _storage = StorageService(); final http.Client _http = http.Client(); /// Base URL without any path suffix. /// Prefer the build-time API_URL define, then fall back to production, /// then platform-specific localhost for local dev. String get origin { const env = String.fromEnvironment('API_URL'); if (env.isNotEmpty) return env; if (kReleaseMode) return 'https://api.canhelp.gr'; try { if (Platform.isAndroid) return 'http://10.0.2.2:4000'; } catch (_) {} return 'http://localhost:4000'; } String get _origin => origin; /// REST API base: /api String get baseUrl => '$_origin/api'; /// Better Auth base: /api/auth String get _authUrl => '$_origin/api/auth'; String? _webVerificationCallback() { const webUrl = String.fromEnvironment('WEB_URL'); if (webUrl.isEmpty) return null; return '${webUrl.replaceAll(RegExp(r'/$'), '')}/dashboard'; } // ── helpers ────────────────────────────────────────────── Map<String, String> _jsonHeaders([String? token]) => { 'Content-Type': 'application/json', 'Accept': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }; Future<dynamic> _handleResponse(http.Response res) async { if (res.statusCode >= 200 && res.statusCode < 300) { if (res.body.isEmpty) return <String, dynamic>{}; return jsonDecode(res.body); } final Map<String, dynamic> body = res.body.isNotEmpty ? jsonDecode(res.body) as Map<String, dynamic> : <String, dynamic>{}; final message = (body['message'] ?? body['error'] ?? 'Request failed') as String; throw ApiException(res.statusCode, message, body); } Future<dynamic> _request( String method, String path, { Map<String, dynamic>? body, Map<String, String>? queryParams, bool auth = false, }) async { final uri = Uri.parse( '$baseUrl$path', ).replace(queryParameters: queryParams); final token = auth ? await _storage.getAccessToken() : null; final headers = _jsonHeaders(token); http.Response res; switch (method) { case 'GET': res = await _http.get(uri, headers: headers); case 'POST': res = await _http.post(uri, headers: headers, body: jsonEncode(body)); case 'PATCH': res = await _http.patch(uri, headers: headers, body: jsonEncode(body)); case 'DELETE': res = await _http.delete(uri, headers: headers); default: throw UnsupportedError('HTTP method $method'); } return _handleResponse(res); } // ── Auth (Better Auth endpoints) ───────────────────────── /// Extracts the Bearer token from a Better Auth response. String? _extractToken(http.Response res, Map<String, dynamic> data) { final bodyToken = data['token'] as String?; if (bodyToken != null && bodyToken.isNotEmpty) return bodyToken; return res.headers['set-auth-token']; } /// Sign in with email + password. Future<Map<String, dynamic>> login({ required String email, required String password, }) async { final res = await _http.post( Uri.parse('$_authUrl/sign-in/email'), headers: _jsonHeaders(), body: jsonEncode({'email': email, 'password': password}), ); final data = await _handleResponse(res) as Map<String, dynamic>; final token = _extractToken(res, data); if (token == null) throw ApiException(500, 'No token in response'); final userId = (data['user'] as Map<String, dynamic>?)?['id'] as String?; await _storage.saveTokens(accessToken: token, userId: userId); return data; } /// Sign in with Google ID token (native mobile flow). /// Exchanges the Google [idToken] for a Better Auth session token. Future<Map<String, dynamic>> signInWithGoogle({ required String idToken, String? accessToken, }) async { final res = await _http.post( Uri.parse('$_authUrl/sign-in/social'), headers: _jsonHeaders(), body: jsonEncode({ 'provider': 'google', 'idToken': { 'token': idToken, if (accessToken != null) 'accessToken': accessToken, }, }), ); final data = await _handleResponse(res) as Map<String, dynamic>; final token = _extractToken(res, data); if (token == null) throw ApiException(500, 'No token in response'); final userId = (data['user'] as Map<String, dynamic>?)?['id'] as String?; await _storage.saveTokens(accessToken: token, userId: userId); return data; } /// Sign in with Apple identity token (native iOS flow). Future<Map<String, dynamic>> signInWithApple({ required String idToken, String? authorizationCode, }) async { final res = await _http.post( Uri.parse('$_authUrl/sign-in/social'), headers: _jsonHeaders(), body: jsonEncode({ 'provider': 'apple', 'idToken': { 'token': idToken, if (authorizationCode != null) 'accessToken': authorizationCode, }, }), ); final data = await _handleResponse(res) as Map<String, dynamic>; final token = _extractToken(res, data); if (token == null) throw ApiException(500, 'No token in response'); final userId = (data['user'] as Map<String, dynamic>?)?['id'] as String?; await _storage.saveTokens(accessToken: token, userId: userId); return data; } /// Register with email + password + profile fields. Future<Map<String, dynamic>> register({ required String email, required String password, required String firstName, required String lastName, String? locale, }) async { final callbackUrl = _webVerificationCallback(); final res = await _http.post( Uri.parse('$_authUrl/sign-up/email'), headers: _jsonHeaders(), body: jsonEncode({ 'email': email, 'password': password, 'name': '$firstName $lastName', 'firstName': firstName, 'lastName': lastName, 'role': 'user', if (locale != null && locale.isNotEmpty) 'locale': locale, if (callbackUrl != null) 'callbackURL': callbackUrl, }), ); final data = await _handleResponse(res) as Map<String, dynamic>; final token = _extractToken(res, data); if (token != null && token.isNotEmpty) { final userId = (data['user'] as Map<String, dynamic>?)?['id'] as String?; await _storage.saveTokens(accessToken: token, userId: userId); } return data; } /// Confirm password reset with [token] from the email link. Future<void> resetPassword(String token, String newPassword) async { final res = await _http.post( Uri.parse('$_authUrl/reset-password'), headers: _jsonHeaders(), body: jsonEncode({'token': token, 'newPassword': newPassword}), ); if (res.statusCode >= 400) { final raw = res.body.trim(); if (raw.isNotEmpty) { try { final body = jsonDecode(raw); throw ApiException( res.statusCode, body['message'] ?? body['error'] ?? 'Error', ); } catch (e) { if (e is ApiException) rethrow; } } throw ApiException(res.statusCode, 'Error ${res.statusCode}'); } } /// Send a password reset email. Future<void> forgotPassword(String email) async { final res = await _http.post( Uri.parse('$_authUrl/request-password-reset'), headers: _jsonHeaders(), body: jsonEncode({ 'email': email, 'redirectTo': 'canhelp://reset-password', }), ); if (res.statusCode >= 400) { final raw = res.body.trim(); if (raw.isNotEmpty) { try { final body = jsonDecode(raw); throw ApiException( res.statusCode, body['message'] ?? body['error'] ?? 'Error', ); } catch (e) { if (e is ApiException) rethrow; } } throw ApiException(res.statusCode, 'Error ${res.statusCode}'); } } /// Resend the email-verification link to [email]. /// Used after a sign-in attempt fails with EMAIL_NOT_VERIFIED. Future<void> sendVerificationEmail(String email, {String? locale}) async { final callbackUrl = _webVerificationCallback(); final res = await _http.post( Uri.parse('$_authUrl/send-verification-email'), headers: _jsonHeaders(), body: jsonEncode({ 'email': email, if (locale != null && locale.isNotEmpty) 'locale': locale, if (callbackUrl != null) 'callbackURL': callbackUrl, }), ); if (res.statusCode >= 400) { final raw = res.body.trim(); if (raw.isNotEmpty) { try { final body = jsonDecode(raw); throw ApiException( res.statusCode, body['message'] ?? body['error'] ?? 'Error', ); } catch (e) { if (e is ApiException) rethrow; } } throw ApiException(res.statusCode, 'Error ${res.statusCode}'); } } /// Sign out — revokes the session on the server, then clears local storage. Future<void> logout() async { final token = await _storage.getAccessToken(); if (token != null) { try { await _http.post( Uri.parse('$_authUrl/sign-out'), headers: _jsonHeaders(token), body: jsonEncode({}), ); } catch (_) { // Ignore errors — always clear locally } } await _storage.deleteTokens(); } // ── Users ──────────────────────────────────────────────── Future<User> getProfile() async { final data = await _request('GET', '/users/me', auth: true); return User.fromJson(data as Map<String, dynamic>); } /// Returns the raw /users/me JSON (includes fields like siteSettings). Future<Map<String, dynamic>> getRawProfile() async { return await _request('GET', '/users/me', auth: true) as Map<String, dynamic>; } Future<User> updateProfile(Map<String, dynamic> fields) async { final data = await _request('PATCH', '/users/me', body: fields, auth: true); return User.fromJson(data as Map<String, dynamic>); } Future<User> becomeSpecialist() async { final data = await _request( 'POST', '/users/me/become-specialist', auth: true, ); return User.fromJson(data as Map<String, dynamic>); } // ── Specialist Cards ───────────────────────────────────── Future<({List<SpecialistCard> cards, int? maxCards})> getMySpecialistCards({ String? locale, }) async { final data = await _request( 'GET', '/specialist-cards/my', queryParams: { if (locale != null && locale.isNotEmpty) 'locale': locale, }, auth: true, ) as Map<String, dynamic>; final cards = (data['cards'] as List<dynamic>) .map((e) => SpecialistCard.fromJson(e as Map<String, dynamic>)) .toList(); return (cards: cards, maxCards: data['maxCards'] as int?); } Future<SpecialistCard> createSpecialistCard({ required String title, String? descriptionLocale, }) async { final data = await _request( 'POST', '/specialist-cards', body: { 'title': title, if (descriptionLocale != null && descriptionLocale.isNotEmpty) 'descriptionLocale': descriptionLocale, }, auth: true, ); return SpecialistCard.fromJson(data as Map<String, dynamic>); } Future<SpecialistCard> updateSpecialistCard( String id, Map<String, dynamic> fields, ) async { final data = await _request( 'PATCH', '/specialist-cards/$id', body: fields, auth: true, ); return SpecialistCard.fromJson(data as Map<String, dynamic>); } Future<Map<String, dynamic>> suggestCategory( String cardId, { required String name, required String locale, }) async { final data = await _request( 'POST', '/specialist-cards/$cardId/category-suggestions', body: {'name': name, 'locale': locale}, auth: true, ); return Map<String, dynamic>.from(data as Map); } Future<String> generateSpecialistDescription({ required List<String> categories, required String locale, }) async { final data = await _request( 'POST', '/specialist-cards/generate-description', body: {'categories': categories, 'locale': locale}, auth: true, ) as Map<String, dynamic>; return data['description'] as String? ?? ''; } Future<void> deleteSpecialistCard(String id) async { await _request('DELETE', '/specialist-cards/$id', auth: true); } // ── My Reviews ─────────────────────────────────────────── Future<List<Review>> getMyReviews() async { final data = await _request('GET', '/reviews/my', auth: true) as List<dynamic>; return data.map((e) { final m = e as Map<String, dynamic>; final reviewJson = m['review'] as Map<String, dynamic>; final authorJson = m['author'] as Map<String, dynamic>?; return Review.fromJson({ ...reviewJson, if (authorJson != null) 'author': authorJson, }); }).toList(); } // ── Tasks ──────────────────────────────────────────────── /// GET /tasks — paginated. Returns the paginated wrapper. Future<({List<Task> data, int total, int page, int limit})> getTasks({ int page = 1, int limit = 20, String? status, String? category, String? location, }) async { final qp = <String, String>{ 'page': page.toString(), 'limit': limit.toString(), if (status != null) 'status': status, if (category != null) 'category': category, if (location != null) 'location': location, }; final data = await _request('GET', '/tasks', queryParams: qp) as Map<String, dynamic>; final items = (data['data'] as List<dynamic>).map((e) { final item = e as Map<String, dynamic>; // API returns { task: {...}, customer: {...} } return Task.fromJson(item['task'] as Map<String, dynamic>? ?? item); }).toList(); return ( data: items, total: data['total'] as int? ?? items.length, page: data['page'] as int? ?? page, limit: data['limit'] as int? ?? limit, ); } Future<({List<Task> data, int total, int page, int limit})> searchTasks({ String? query, String? category, String? location, String? status, double? budgetMin, double? budgetMax, bool forMe = false, int page = 1, int limit = 20, }) async { final endpoint = forMe ? '/tasks/for-me' : '/tasks'; final qp = <String, String>{ 'page': page.toString(), 'limit': limit.toString(), if (query != null && query.isNotEmpty) 'q': query, if (category != null) 'category': category, if (location != null) 'location': location, if (status != null) 'status': status, if (budgetMin != null) 'budgetMin': budgetMin.toStringAsFixed(0), if (budgetMax != null) 'budgetMax': budgetMax.toStringAsFixed(0), }; final data = await _request('GET', endpoint, queryParams: qp, auth: forMe) as Map<String, dynamic>; final items = (data['data'] as List<dynamic>).map((e) { final item = e as Map<String, dynamic>; // /tasks returns { task: {...}, customer: {...} }, unwrap if needed final taskJson = item.containsKey('task') ? item['task'] as Map<String, dynamic> : item; return Task.fromJson(taskJson); }).toList(); return ( data: items, total: data['total'] as int? ?? items.length, page: data['page'] as int? ?? page, limit: data['limit'] as int? ?? limit, ); } /// GET /tasks/my — returns tasks owned by the current user. Future<List<Task>> getMyTasks() async { final data = await _request('GET', '/tasks/my', auth: true) as List<dynamic>; return data.map((e) => Task.fromJson(e as Map<String, dynamic>)).toList(); } /// GET /tasks/:id — returns { task, customer }. Future<Task> getTask(String id) async { final data = await _request('GET', '/tasks/$id') as Map<String, dynamic>; // API returns { task: {...}, customer: {...} } final taskJson = data['task'] as Map<String, dynamic>? ?? data; final customerJson = data['customer'] as Map<String, dynamic>?; return Task.fromJson(taskJson, customer: customerJson); } Future<Task> createTask({ required String title, required String description, double? budget, bool budgetNegotiable = false, String? category, String? location, String locale = 'en', DateTime? deadline, DateTime? expiresAt, String? timeSlot, String? district, String? street, String? houseNumber, List<String>? images, String? confidentialNote, }) async { final data = await _request( 'POST', '/tasks', auth: true, body: { 'title': title, 'description': description, if (budget != null) 'budget': budget, 'budgetNegotiable': budgetNegotiable, if (category != null) 'category': category, if (location != null) 'location': location, 'locale': locale, if (deadline != null) 'deadline': deadline.toUtc().toIso8601String(), if (expiresAt != null) 'expiresAt': expiresAt.toUtc().toIso8601String(), if (timeSlot != null && timeSlot.isNotEmpty) 'timeSlot': timeSlot, if (district != null && district.isNotEmpty) 'district': district, if (street != null && street.isNotEmpty) 'street': street, if (houseNumber != null && houseNumber.isNotEmpty) 'houseNumber': houseNumber, if (images != null && images.isNotEmpty) 'images': images, if (confidentialNote != null && confidentialNote.isNotEmpty) 'confidentialNote': confidentialNote, 'status': 'open', }, ); return Task.fromJson(data as Map<String, dynamic>); } /// PATCH /tasks/:id — update an existing task (owner only, draft/open status). Future<Task> updateTask( String id, { String? title, String? description, double? budget, bool? budgetNegotiable, String? category, String? location, DateTime? deadline, DateTime? expiresAt, String? timeSlot, String? district, String? street, String? houseNumber, String? confidentialNote, String? status, }) async { final body = <String, dynamic>{ if (title != null) 'title': title, if (description != null) 'description': description, if (budget != null) 'budget': budget, if (budgetNegotiable != null) 'budgetNegotiable': budgetNegotiable, if (category != null) 'category': category, if (location != null) 'location': location, if (deadline != null) 'deadline': deadline.toUtc().toIso8601String(), if (expiresAt != null) 'expiresAt': expiresAt.toUtc().toIso8601String(), if (timeSlot != null) 'timeSlot': timeSlot, if (district != null) 'district': district, if (street != null) 'street': street, if (houseNumber != null) 'houseNumber': houseNumber, if (confidentialNote != null) 'confidentialNote': confidentialNote, if (status != null) 'status': status, }; final data = await _request('PATCH', '/tasks/$id', body: body, auth: true); return Task.fromJson(data as Map<String, dynamic>); } Future<String> uploadFile(List<int> bytes, String filename) async { final token = await _storage.getAccessToken(); if (token == null) throw ApiException(401, 'Not authenticated'); final ext = filename.contains('.') ? filename.split('.').last.toLowerCase() : 'jpg'; final mimeType = switch (ext) { 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp', _ => 'image/jpeg', }; final uri = Uri.parse('$baseUrl/uploads'); final req = http.MultipartRequest('POST', uri); req.headers['Authorization'] = 'Bearer $token'; req.files.add( http.MultipartFile.fromBytes( 'file', bytes, filename: filename, contentType: http_parser.MediaType.parse(mimeType), ), ); final streamed = await _http.send(req); final resp = await http.Response.fromStream(streamed); if (resp.statusCode >= 400) { final body = resp.body.isNotEmpty ? (jsonDecode(resp.body) as Map<String, dynamic>) : <String, dynamic>{}; final msg = body['error'] as String? ?? body['message'] as String? ?? 'Upload failed'; throw ApiException(resp.statusCode, msg); } final body = jsonDecode(resp.body) as Map<String, dynamic>; final url = body['url'] as String? ?? body['path'] as String? ?? ''; return url.startsWith('http') ? url : '$_origin$url'; } // ── Offers ─────────────────────────────────────────────── /// GET /offers?taskId — returns [{ offer, specialist }]. /// Only the task owner can call this. Future<List<Offer>> getOffers({required String taskId}) async { final data = await _request( 'GET', '/offers', queryParams: {'taskId': taskId}, auth: true, ) as List<dynamic>; return data .map( (e) => Offer.fromJson( (e as Map<String, dynamic>)['offer'] as Map<String, dynamic>, ), ) .toList(); } /// GET /offers?taskId — returns raw [{ offer, specialist }] including specialist info. Future<List<Map<String, dynamic>>> getTaskOffersRaw({ required String taskId, }) async { final data = await _request( 'GET', '/offers', queryParams: {'taskId': taskId}, auth: true, ) as List<dynamic>; return data.map((e) => e as Map<String, dynamic>).toList(); } /// GET /offers/my — returns [{ offer, task }]. Future<List<Offer>> getMyOffers() async { final data = await _request('GET', '/offers/my', auth: true) as List<dynamic>; return data .map( (e) => Offer.fromJson( (e as Map<String, dynamic>)['offer'] as Map<String, dynamic>, ), ) .toList(); } /// GET /offers/my — returns [OfferWithTask] including task info. Future<List<OfferWithTask>> getMyOffersWithTask() async { final data = await _request('GET', '/offers/my', auth: true) as List<dynamic>; return data .map((e) => OfferWithTask.fromJson(e as Map<String, dynamic>)) .toList(); } Future<Offer> createOffer({ required String taskId, required double price, String? message, }) async { final data = await _request( 'POST', '/offers', auth: true, body: { 'taskId': taskId, 'price': price, if (message != null && message.isNotEmpty) 'message': message, }, ); return Offer.fromJson(data as Map<String, dynamic>); } Future<Offer> acceptOffer(String id) async { final data = await _request('PATCH', '/offers/$id/accept', auth: true); return Offer.fromJson(data as Map<String, dynamic>); } Future<Offer> declineOffer(String id) async { final data = await _request('PATCH', '/offers/$id/decline', auth: true); return Offer.fromJson(data as Map<String, dynamic>); } // ── Categories ─────────────────────────────────────────── Future<List<Category>> getCategories() async { final data = await _request('GET', '/categories') as List<dynamic>; return data .map((e) => Category.fromJson(e as Map<String, dynamic>)) .toList(); } // ── Locations ──────────────────────────────────────────── Future<List<Location>> getLocations() async { final data = await _request('GET', '/locations') as List<dynamic>; return data .map((e) => Location.fromJson(e as Map<String, dynamic>)) .toList(); } Future<List<Map<String, dynamic>>> getSkillSuggestions([ List<String> categorySlugs = const [], ]) async { final query = categorySlugs .map((slug) => 'category=${Uri.encodeQueryComponent(slug)}') .join('&'); final data = await _request( 'GET', '/skill-suggestions${query.isEmpty ? '' : '?$query'}', ) as List<dynamic>; return data .whereType<Map>() .map((e) => Map<String, dynamic>.from(e)) .toList(); } // ── i18n ───────────────────────────────────────────────── Future<List<String>> getLocales() async { final data = await _request('GET', '/i18n/locales') as List<dynamic>; return data.map((e) => e as String).toList(); } Future<Map<String, dynamic>> getTranslations(String locale) async { final data = await _request( 'GET', '/i18n/$locale', ).timeout(const Duration(seconds: 5)); return data as Map<String, dynamic>; } // ── Specialists ────────────────────────────────────────── Future<({List<User> data, int total, int page, int limit})> getSpecialists({ String? query, String? category, String? location, List<String>? languages, String? locale, int page = 1, int limit = 20, }) async { final qp = <String, String>{ 'page': page.toString(), 'limit': limit.toString(), if (query != null && query.isNotEmpty) 'q': query, if (category != null) 'category': category, if (location != null) 'location': location, if (languages != null && languages.isNotEmpty) 'language': languages.join(','), if (locale != null && locale.isNotEmpty) 'locale': locale, }; final data = await _request('GET', '/users/specialists', queryParams: qp) as Map<String, dynamic>; final List<dynamic> rawList = data['data'] as List<dynamic>? ?? (data is List ? data as List<dynamic> : []); final items = rawList .map((e) => User.fromJson(e as Map<String, dynamic>)) .toList(); final blockedIds = await _storage.getBlockedUserIds(); final visibleItems = items .where((u) => !blockedIds.contains(u.id)) .toList(); return ( data: visibleItems, total: visibleItems.length, page: data['page'] as int? ?? page, limit: data['limit'] as int? ?? limit, ); } Future<({User user, List<Review> reviews, List<SpecialistCard> cards})> getSpecialist(String id, {String? locale}) async { if (await _storage.isUserBlocked(id)) { throw ApiException(403, 'User is blocked'); } final data = await _request( 'GET', '/users/$id/profile', queryParams: { if (locale != null && locale.isNotEmpty) 'locale': locale, }, ) as Map<String, dynamic>; final user = User.fromJson(data); final reviews = await getReviews(id); final cards = await getSpecialistCards(id, locale: locale); return (user: user, reviews: reviews, cards: cards); } Future<List<SpecialistCard>> getSpecialistCards( String userId, { String? locale, }) async { final data = await _request( 'GET', '/specialist-cards/user/$userId', queryParams: { if (locale != null && locale.isNotEmpty) 'locale': locale, }, ) as List<dynamic>; return data .map((e) => SpecialistCard.fromJson(e as Map<String, dynamic>)) .toList(); } Future<List<PriceListGroup>> getPriceList( String cardId, { String? locale, }) async { final data = await _request( 'GET', '/price-list/$cardId', queryParams: { if (locale != null && locale.isNotEmpty) 'locale': locale, }, ) as List<dynamic>; return data .map((e) => PriceListGroup.fromJson(e as Map<String, dynamic>)) .toList(); } Future<PriceListGroup> createPriceListGroup( String cardId, { required String title, required String locale, }) async { final data = await _request( 'POST', '/price-list/$cardId/groups', body: {'title': title, 'locale': locale, 'autoTranslate': true}, auth: true, ); return PriceListGroup.fromJson(data as Map<String, dynamic>); } Future<void> deletePriceListGroup(String groupId) => _request('DELETE', '/price-list/groups/$groupId', auth: true); Future<PriceListItem> createPriceListItem( String groupId, { required String name, String? description, String? price, String? unit, required String locale, }) async { final data = await _request( 'POST', '/price-list/groups/$groupId/items', body: { 'name': name, if (description != null && description.isNotEmpty) 'description': description, if (price != null && price.isNotEmpty) 'price': price, if (unit != null && unit.isNotEmpty) 'unit': unit, 'locale': locale, 'autoTranslate': true, }, auth: true, ); return PriceListItem.fromJson(data as Map<String, dynamic>); } Future<void> deletePriceListItem(String itemId) => _request('DELETE', '/price-list/items/$itemId', auth: true); // ── Favorites ──────────────────────────────────────────── Future<List<User>> getFavorites() async { final data = await _request('GET', '/favorites/specialists', auth: true) as List<dynamic>; final blockedIds = await _storage.getBlockedUserIds(); return data .map((e) { final m = e as Map<String, dynamic>; return User.fromJson(m['specialist'] as Map<String, dynamic>? ?? m); }) .where((u) => !blockedIds.contains(u.id)) .toList(); } Future<void> addFavorite(String specialistId) async { await _request('POST', '/favorites/specialists/$specialistId', auth: true); } Future<void> removeFavorite(String specialistId) async { await _request( 'DELETE', '/favorites/specialists/$specialistId', auth: true, ); } // ── Reviews ────────────────────────────────────────────── Future<List<Review>> getReviews(String targetId) async { final data = await _request('GET', '/reviews/user/$targetId') as List<dynamic>; return data.map((e) { final m = e as Map<String, dynamic>; // Server returns { review: {...}, author: {...} } final reviewMap = (m['review'] as Map<String, dynamic>? ?? m); final authorMap = m['author'] as Map<String, dynamic>?; if (authorMap != null) reviewMap['author'] = authorMap; return Review.fromJson(reviewMap); }).toList(); } Future<Review> createReview({ required String targetId, required double rating, String? comment, String? taskId, }) async { final data = await _request( 'POST', '/reviews', auth: true, body: { 'targetId': targetId, 'rating': rating, if (comment != null) 'comment': comment, if (taskId != null) 'taskId': taskId, }, ); return Review.fromJson(data as Map<String, dynamic>); } // ── Notifications ──────────────────────────────────────── Future<List<AppNotification>> getNotifications() async { final data = await _request('GET', '/notifications', auth: true) as List<dynamic>; return data .map((e) => AppNotification.fromJson(e as Map<String, dynamic>)) .toList(); } Future<void> markNotificationRead(String id) async { await _request('PATCH', '/notifications/$id/read', auth: true); } Future<void> markAllNotificationsRead() async { await _request('PATCH', '/notifications/read-all', auth: true); } Future<void> deleteNotification(String id) async { await _request('DELETE', '/notifications/$id', auth: true); } Future<void> deleteAllNotifications() async { await _request('DELETE', '/notifications', auth: true); } Future<void> registerPushToken({ required String token, required String platform, String? locale, }) async { await _request( 'POST', '/users/me/push-token', auth: true, body: { 'token': token, 'platform': platform, if (locale != null) 'locale': locale, }, ); } Future<void> unregisterPushToken(String token) async { await _request( 'POST', '/users/me/push-token/remove', auth: true, body: {'token': token}, ); } // ── Chat ───────────────────────────────────────────────── Future<List<ChatRoom>> getChatRooms() async { final blockedIds = await _storage.getBlockedUserIds(); final data = await _request('GET', '/chat/rooms', auth: true) as List<dynamic>; return data.map((e) => ChatRoom.fromJson(e as Map<String, dynamic>)).where(( room, ) { final otherId = room.otherUser?.id; return otherId == null || !blockedIds.contains(otherId); }).toList(); } Future<List<ChatMessage>> getChatMessages( String roomId, { int limit = 50, }) async { final blockedIds = await _storage.getBlockedUserIds(); final data = await _request( 'GET', '/chat/rooms/$roomId/messages', queryParams: {'limit': limit.toString()}, auth: true, ) as List<dynamic>; return data .map((e) => ChatMessage.fromJson(e as Map<String, dynamic>)) .where((m) => !blockedIds.contains(m.senderId)) .toList(); } /// REST fallback for sending a message (preferred: use Socket.IO) Future<ChatMessage> sendMessageRest( String roomId, String body, { String locale = 'el', String? imageUrl, }) async { final data = await _request( 'POST', '/chat/messages', body: { 'roomId': roomId, 'body': body, 'locale': locale, if (imageUrl != null) 'imageUrl': imageUrl, }, auth: true, ); return ChatMessage.fromJson(data as Map<String, dynamic>); } /// Get or create a task-linked chat room. Future<ChatRoom> createOrGetChatRoom({ required String specialistId, String? taskId, }) async { final data = await _request( 'POST', '/chat/rooms', body: { 'specialistId': specialistId, if (taskId != null) 'taskId': taskId, }, auth: true, ); return ChatRoom.fromJson(data as Map<String, dynamic>); } /// Get or create a direct (non-task) room with another user. Future<({ChatRoom room, bool created})> getChatDirectRoom( String userId, ) async { if (await _storage.isUserBlocked(userId)) { throw ApiException(403, 'User is blocked'); } final data = await _request('GET', '/chat/direct/$userId', auth: true) as Map<String, dynamic>; return ( room: ChatRoom.fromJson(data['room'] as Map<String, dynamic>), created: data['created'] as bool? ?? false, ); } // ── User Safety ───────────────────────────────────────── Future<Map<String, dynamic>> createReport({ required String targetType, required String targetId, required String reason, String? description, }) async { final data = await _request( 'POST', '/reports', auth: true, body: { 'targetType': targetType, 'targetId': targetId, 'reason': reason, if (description != null && description.trim().isNotEmpty) 'description': description.trim(), }, ); return data as Map<String, dynamic>; } // ── Availability / Schedule ────────────────────────────── Future<List<AvailabilityDay>> getMyAvailability(String monthKey) async { final data = await _request( 'GET', '/availability/my', queryParams: {'month': monthKey}, auth: true, ) as List<dynamic>; return data .map((e) => AvailabilityDay.fromJson(e as Map<String, dynamic>)) .toList(); } Future<void> setAvailability({ required String date, required AvailabilityStatus? status, }) async { await _request( 'POST', '/availability/set', auth: true, body: { 'date': date, 'status': status == null ? null : status == AvailabilityStatus.available ? 'available' : 'busy', }, ); } Future<void> setAvailabilityRange({ required String from, required String to, required AvailabilityStatus? status, }) async { await _request( 'POST', '/availability/set-range', auth: true, body: { 'from': from, 'to': to, 'status': status == null ? null : status == AvailabilityStatus.available ? 'available' : 'busy', }, ); } // ── Referral ────────────────────────────────────────────── /// Returns referralCode, referralCount, referralUrl for the current user. Future<Map<String, dynamic>> getReferralInfo() async { final data = await _request('GET', '/users/me/referral', auth: true); return data as Map<String, dynamic>; } // ── Balance history ─────────────────────────────────────── Future<List<Map<String, dynamic>>> getBalanceHistory({int limit = 50}) async { final data = await _request( 'GET', '/payments/balance-history', auth: true, queryParams: {'limit': '$limit'}, ); return (data as List<dynamic>) .map((e) => Map<String, dynamic>.from(e as Map)) .toList(); } Future<List<Map<String, dynamic>>> getPlanHistory({int limit = 50}) async { final data = await _request( 'GET', '/users/me/plan-history', auth: true, queryParams: {'limit': '$limit'}, ); return (data as List<dynamic>) .map((e) => Map<String, dynamic>.from(e as Map)) .toList(); } Future<List<Map<String, dynamic>>> getPaymentHistory({String? ref}) async { final data = await _request( 'GET', '/payments/history', auth: true, queryParams: ref == null || ref.isEmpty ? null : {'ref': ref}, ); return (data as List<dynamic>) .map((e) => Map<String, dynamic>.from(e as Map)) .toList(); } // ── Plans / Dashboard ───────────────────────────────────── Future<DashboardData> getMyDashboard() async { final data = await _request('GET', '/dashboard/me', auth: true); return DashboardData.fromJson(data as Map<String, dynamic>); } Future<List<PlanItem>> getPlans({String? role}) async { final qp = <String, String>{}; if (role != null) qp['role'] = role; final data = await _request('GET', '/plans', queryParams: qp.isEmpty ? null : qp) as List<dynamic>; return data .map((e) => PlanItem.fromJson(e as Map<String, dynamic>)) .toList(); } Future<Map<String, dynamic>> purchasePlan( String planId, { bool payFromBalance = true, }) async { final data = await _request( 'POST', '/payments/plan/purchase', body: {'planId': planId, 'payFromBalance': payFromBalance}, auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> previewPlanChange(String planId) async { final data = await _request( 'POST', '/payments/plan/preview', body: {'planId': planId}, auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> getDailyUsage() async { final data = await _request('GET', '/plans/daily-usage', auth: true); return data as Map<String, dynamic>; } // ── Statuses ────────────────────────────────────────────── Future<List<String>> getMyStatuses() async { final data = await _request('GET', '/statuses/my', auth: true); final list = data as List<dynamic>; return list .map((e) => (e as Map<String, dynamic>)['type'] as String) .toList(); } Future<void> toggleStatus(String type) async { await _request( 'POST', '/statuses/toggle', body: {'type': type}, auth: true, ); } // ── Auto-response ───────────────────────────────────────── Future<Map<String, dynamic>> getAutoResponse() async { final data = await _request('GET', '/auto-response', auth: true); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> updateAutoResponse( Map<String, dynamic> settings, ) async { final data = await _request( 'PUT', '/auto-response', body: settings, auth: true, ); return data as Map<String, dynamic>; } // ── FOMO ────────────────────────────────────────────────── Future<Map<String, dynamic>> getSpecialistFomoDashboard() async { final data = await _request('GET', '/fomo/specialist', auth: true); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> getCustomerFomoDashboard() async { final data = await _request('GET', '/fomo/customer', auth: true); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> getTaskStats(String taskId) async { final data = await _request('GET', '/tasks/$taskId/stats', auth: true); return data as Map<String, dynamic>; } /// Check if the current user can message [userId]. /// Returns { allowed, reason?, code?, senderTier?, recipientTier? } Future<Map<String, dynamic>> canMessageUser(String userId) async { final data = await _request('GET', '/chat/can-message/$userId', auth: true); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> getPlatformStats() async { final data = await _request('GET', '/stats'); return data as Map<String, dynamic>; } // ── Admin ───────────────────────────────────────────────── Future<Map<String, dynamic>> getAdminStats() async { final data = await _request('GET', '/admin/stats', auth: true); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> getAdminUsers({ int page = 1, int limit = 20, String? q, String? role, }) async { final qp = <String, String>{ 'page': page.toString(), 'limit': limit.toString(), if (q != null && q.isNotEmpty) 'q': q, if (role != null && role.isNotEmpty) 'role': role, }; final data = await _request( 'GET', '/admin/users', queryParams: qp, auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> getAdminUser(String id) async { final data = await _request('GET', '/admin/users/$id', auth: true); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> adminActivateUser(String id) async { final data = await _request( 'PATCH', '/admin/users/$id/activate', auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> adminDeactivateUser(String id) async { final data = await _request( 'PATCH', '/admin/users/$id/deactivate', auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> adminSetUserRole(String id, String role) async { final data = await _request( 'PATCH', '/admin/users/$id/role', body: {'role': role}, auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> adminUpdateUser( String id, { String? planId, bool clearPlan = false, }) async { final data = await _request( 'PATCH', '/admin/users/$id', body: { if (clearPlan) 'planId': null else if (planId != null) 'planId': planId, }, auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> getAdminSpecialistCards({ int page = 1, String status = 'review', }) async { final data = await _request( 'GET', '/admin/specialist-cards', queryParams: {'page': '$page', 'limit': '30', 'status': status}, auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> adminUpdateSpecialistCard( String id, { required String publicationStatus, String? adminComment, }) async { final data = await _request( 'PATCH', '/admin/specialist-cards/$id', body: { 'publicationStatus': publicationStatus, 'isActive': publicationStatus == 'active', if (adminComment != null && adminComment.isNotEmpty) 'adminComment': adminComment, }, auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> adminTopUpBalance( String id, double amount, { String? description, }) async { final data = await _request( 'POST', '/admin/users/$id/balance/topup', body: { 'amount': amount, if (description != null && description.isNotEmpty) 'description': description, }, auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> adminWithdrawBalance( String id, double amount, { String? description, }) async { final data = await _request( 'POST', '/admin/users/$id/balance/withdraw', body: { 'amount': amount, if (description != null && description.isNotEmpty) 'description': description, }, auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> getAdminTasks({ int page = 1, int limit = 20, String? q, String? status, }) async { final qp = <String, String>{ 'page': page.toString(), 'limit': limit.toString(), if (q != null && q.isNotEmpty) 'q': q, if (status != null && status.isNotEmpty) 'status': status, }; final data = await _request( 'GET', '/admin/tasks', queryParams: qp, auth: true, ); return data as Map<String, dynamic>; } Future<void> adminCancelTask(String id) async { await _request('PATCH', '/admin/tasks/$id/cancel', auth: true); } Future<void> adminDeleteTask(String id) async { await _request('DELETE', '/admin/tasks/$id', auth: true); } Future<Map<String, dynamic>> getAdminReports({ int page = 1, int limit = 20, }) async { final qp = <String, String>{ 'page': page.toString(), 'limit': limit.toString(), }; final data = await _request( 'GET', '/admin/reports', queryParams: qp, auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> adminUpdateReportStatus( String id, String status, ) async { final data = await _request( 'PATCH', '/admin/reports/$id/status', body: {'status': status}, auth: true, ); return data as Map<String, dynamic>; } // ── Admin Support Tickets ───────────────────────────────── Future<Map<String, dynamic>> getAdminSupportTickets({ int page = 1, int limit = 20, String? status, }) async { final qp = <String, String>{ 'page': page.toString(), 'limit': limit.toString(), }; if (status != null && status.isNotEmpty) qp['status'] = status; final data = await _request('GET', '/support', queryParams: qp, auth: true); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> adminReplyToTicket( String id, { required String reply, String status = 'resolved', }) async { final data = await _request( 'PATCH', '/support/$id/reply', body: {'adminReply': reply, 'status': status}, auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> adminUpdateTicketStatus( String id, String status, ) async { final data = await _request( 'PATCH', '/support/$id/status', body: {'status': status}, auth: true, ); return data as Map<String, dynamic>; } // ── Admin Backup ────────────────────────────────────────── Future<List<String>> adminGetBackupTables() async { final data = await _request('GET', '/admin/backup/tables', auth: true) as Map<String, dynamic>; return (data['tables'] as List<dynamic>).cast<String>(); } Future<Map<String, dynamic>> adminGetBackupSettings() async { final data = await _request('GET', '/admin/backup/settings', auth: true); return data as Map<String, dynamic>; } Future<void> adminSetBackupAutoEnabled(bool enabled) async { await _request( 'PUT', '/admin/backup/settings', body: {'autoEnabled': enabled}, auth: true, ); } Future<List<Map<String, dynamic>>> adminListBackups() async { final data = await _request('GET', '/admin/backup/list', auth: true) as Map<String, dynamic>; return (data['files'] as List<dynamic>).cast<Map<String, dynamic>>(); } /// Export full DB or a single [table] as .sql.gz bytes. Future<Uint8List> adminExportBackup({String? table}) async { final token = await _storage.getAccessToken(); if (token == null) throw ApiException(401, 'Not authenticated'); final p = table != null ? '/admin/backup/export/$table' : '/admin/backup/export'; final uri = Uri.parse('$baseUrl$p'); final res = await _http.get( uri, headers: {'Authorization': 'Bearer $token', 'Accept': 'application/gzip'}, ); if (res.statusCode >= 400) { throw ApiException(res.statusCode, 'Export failed'); } return res.bodyBytes; } /// Import from .sql.gz [bytes] for the full DB or a single [table]. Future<void> adminImportBackup(Uint8List bytes, {String? table}) async { final token = await _storage.getAccessToken(); if (token == null) throw ApiException(401, 'Not authenticated'); final p = table != null ? '/admin/backup/import/$table' : '/admin/backup/import'; final uri = Uri.parse('$baseUrl$p'); final req = http.MultipartRequest('POST', uri) ..headers['Authorization'] = 'Bearer $token' ..files.add( http.MultipartFile.fromBytes( 'file', bytes, filename: table != null ? '$table.sql.gz' : 'backup.sql.gz', contentType: http_parser.MediaType.parse('application/gzip'), ), ); final streamed = await _http.send(req); final resp = await http.Response.fromStream(streamed); if (resp.statusCode >= 400) { final body = resp.body.isNotEmpty ? jsonDecode(resp.body) as Map<String, dynamic> : <String, dynamic>{}; throw ApiException( resp.statusCode, body['error'] as String? ?? body['message'] as String? ?? 'Import failed', body, ); } } /// Restore from a stored backup (name: "backup_today" | "backup_yesterday"). Future<void> adminRestoreBackup(String name) async { await _request( 'POST', '/admin/backup/restore', body: {'name': name}, auth: true, ); } // ── Portfolio ───────────────────────────────────────────── Future<List<PortfolioItem>> getMyPortfolio() async { final data = await _request('GET', '/portfolio/my', auth: true); final list = data as List<dynamic>; return list .map((e) => PortfolioItem.fromJson(e as Map<String, dynamic>)) .toList(); } Future<List<PortfolioItem>> getUserPortfolio(String userId) async { final data = await _request('GET', '/portfolio/user/$userId', auth: false); final list = data as List<dynamic>; return list .map((e) => PortfolioItem.fromJson(e as Map<String, dynamic>)) .toList(); } Future<PortfolioItem> uploadPortfolioItem( List<int> bytes, String filename, { String? title, String? description, String? cardId, }) async { final token = await _storage.getAccessToken(); if (token == null) throw ApiException(401, 'Not authenticated'); final ext = filename.contains('.') ? filename.split('.').last.toLowerCase() : 'jpg'; final mimeType = switch (ext) { 'png' => 'image/png', 'webp' => 'image/webp', 'gif' => 'image/gif', _ => 'image/jpeg', }; final uri = Uri.parse('$baseUrl/portfolio'); final req = http.MultipartRequest('POST', uri); req.headers['Authorization'] = 'Bearer $token'; req.files.add( http.MultipartFile.fromBytes( 'file', bytes, filename: filename, contentType: http_parser.MediaType.parse(mimeType), ), ); if (title != null && title.isNotEmpty) req.fields['title'] = title; if (description != null && description.isNotEmpty) { req.fields['description'] = description; } if (cardId != null && cardId.isNotEmpty) req.fields['cardId'] = cardId; final streamed = await _http.send(req); final resp = await http.Response.fromStream(streamed); if (resp.statusCode >= 400) { final body = resp.body.isNotEmpty ? (jsonDecode(resp.body) as Map<String, dynamic>) : <String, dynamic>{}; final msg = body['error'] as String? ?? body['message'] as String? ?? 'Upload failed'; throw ApiException(resp.statusCode, msg, body); } return PortfolioItem.fromJson( jsonDecode(resp.body) as Map<String, dynamic>, ); } Future<PortfolioItem> updatePortfolioItem( String id, { String? title, String? description, }) async { final data = await _request( 'PATCH', '/portfolio/$id', body: { if (title != null) 'title': title, if (description != null) 'description': description, }, auth: true, ); return PortfolioItem.fromJson(data as Map<String, dynamic>); } Future<void> deletePortfolioItem(String id) async { await _request('DELETE', '/portfolio/$id', auth: true); } // ── Top-up ──────────────────────────────────────────────── Future<Map<String, dynamic>> initTopup(double amount) async { final data = await _request( 'POST', '/payments/topup/init', body: {'amount': amount}, auth: true, ); return data as Map<String, dynamic>; } // ── Support Tickets ───────────────────────────────────────── Future<Map<String, dynamic>> createSupportTicket({ required String type, required String subject, required String body, }) async { final data = await _request( 'POST', '/support', body: {'type': type, 'subject': subject, 'body': body}, auth: true, ); return data as Map<String, dynamic>; } Future<Map<String, dynamic>> getMySupportTickets({int page = 1}) async { final data = await _request( 'GET', '/support/my', queryParams: {'page': '$page'}, auth: true, ); return data as Map<String, dynamic>; } }
Save
cmd:
run