import 'package:flutter/material.dart'; import 'package:odoo_rpc/odoo_rpc.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../main.dart'; import '../screens/login_screen.dart'; import 'config.dart'; import 'theme_manager.dart'; class OdooService { static final OdooService _instance = OdooService._internal(); factory OdooService() => _instance; OdooService._internal(); OdooClient? client; void connect(String url, {OdooSession? session}) { client = OdooClient(url, sessionId: session); } /// Returns the session cookie header value for authenticated image loading. String get sessionCookie { final sessionId = client?.sessionId?.id ?? ''; return 'session_id=$sessionId'; } /// Returns the URL for the full notification image. String notificationImageUrl(int notifId) => '${AppConfig.odooUrl}/web/image/mapan.app.notification/$notifId/image'; /// Returns the URL for a carousel image (uploaded). String carouselImageUrl(int slideId) => '${AppConfig.odooUrl}/web/image/mapan.app.carousel/$slideId/image'; /// Returns the URL for a promo image (uploaded). String promoImageUrl(int promoId) => '${AppConfig.odooUrl}/web/image/mapan.app.promo/$promoId/image'; Future _handleSessionExpired() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove('odoo_session'); final state = navigatorKey.currentState; if (state != null) { state.pushAndRemoveUntil( MaterialPageRoute( builder: (_) => const LoginScreen(sessionExpired: true), ), (route) => false, ); } } Future _performWithSessionCheck(Future Function() operation) async { try { return await operation(); } on OdooSessionExpiredException { await _handleSessionExpired(); rethrow; } catch (e) { if (e.toString().toLowerCase().contains('session expired') || e.toString().toLowerCase().contains('session_expired')) { await _handleSessionExpired(); } rethrow; } } Future callKw(Map params) async { if (client == null) throw Exception("Connect to Odoo first"); return _performWithSessionCheck(() => client!.callKw(params)); } Future callRPC(String path, String method, dynamic params) async { if (client == null) throw Exception("Connect to Odoo first"); return _performWithSessionCheck(() => client!.callRPC(path, method, params)); } Future login(String db, String username, String password) async { if (client == null) throw Exception("Connect to Odoo first"); return await client!.authenticate(db, username, password); } Future> getLoyaltyCards(int partnerId) async { // Only fetch cards from 'loyalty' type programs (multi-tier: Silver/Gold/Platinum). // Excludes subscriptions, coupons, gift cards, promotions, eWallets, etc. return await callKw({ 'model': 'loyalty.card', 'method': 'search_read', 'args': [ [ ['partner_id', '=', partnerId], ['program_id.program_type', '=', 'loyalty'], ['program_id.active', '=', true], ], ], 'kwargs': { 'fields': [ 'points', 'program_id', 'code', 'subscription_start_date', 'subscription_end_date', ] } }) as List; } /// Fetch subscription cards only — displayed as a "My Subscriptions" list. Future> getSubscriptionCards(int partnerId) async { return await callKw({ 'model': 'loyalty.card', 'method': 'search_read', 'args': [ [ ['partner_id', '=', partnerId], ['program_id.program_type', '=', 'subscription'], ['program_id.active', '=', true], ], ], 'kwargs': { 'fields': [ 'points', 'program_id', 'code', 'subscription_start_date', 'subscription_end_date', ] } }) as List; } /// Fetch carousel slides and promo highlights for the home screen. Future> getCmsContent() async { final response = await callRPC( '/api/loyalty/cms_content', 'call', {}, ); if (response != null && response['status'] == 'success') { return response as Map; } return {'carousel': [], 'promos': []}; } /// Fetch app configuration (About Us URL, Contact Us URL, Branding & Theme). static Future> getAppConfig() async { final activeClient = OdooService().client; final clientToUse = activeClient ?? OdooClient(AppConfig.odooUrl); try { final res = await clientToUse.callRPC('/api/loyalty/app_config', 'call', {}); if (res != null && res['status'] == 'success') { final configMap = { 'about_us_url': (res['about_us_url'] as String?) ?? '', 'contact_us_url': (res['contact_us_url'] as String?) ?? '', 'brand_logo': (res['brand_logo'] as String?) ?? '', 'primary_color': (res['primary_color'] as String?) ?? '#C62828', 'secondary_color': (res['secondary_color'] as String?) ?? '#FF8F00', 'background_color': (res['background_color'] as String?) ?? '#FAF6EE', 'background_gradient_color': (res['background_gradient_color'] as String?) ?? '#F3EAD3', }; // Save and apply new branding and theme colors dynamically await ThemeManager.instance.updateConfig( primaryHex: configMap['primary_color']!, secondaryHex: configMap['secondary_color']!, backgroundHex: configMap['background_color']!, backgroundGradientHex: configMap['background_gradient_color']!, brandLogoB64: configMap['brand_logo']!, ); return configMap; } return {'about_us_url': '', 'contact_us_url': ''}; } catch (_) { return {'about_us_url': '', 'contact_us_url': ''}; } finally { if (activeClient == null) { clientToUse.close(); } } } /// Fetch loyalty point history for the current user. Future> getOrderHistory() async { final response = await callRPC( '/api/loyalty/order_history', 'call', {}, ); if (response != null && response['status'] == 'success') { return response['data'] as List; } return []; } Future sendOtp({ String? email, String? phone, String? phoneOrEmail, required String type, }) async { return await callRPC( '/api/loyalty/send_otp', 'call', { 'email': ?email, 'phone': ?phone, 'phone_or_email': ?phoneOrEmail, 'type': type, }, ); } Future signUpMember({ required String name, required String phone, required String email, required String birthDate, required String gender, required String password, required String otp, }) async { return await callRPC( '/api/loyalty/signup_member', 'call', { 'name': name, 'phone': phone, 'email': email, 'birth_date': birthDate, 'gender': gender, 'password': password, 'otp': otp, }, ); } Future activateAccount({ required String phone, required String email, required String birthDate, required String password, required String otp, }) async { return await callRPC( '/api/loyalty/activate_account', 'call', { 'phone': phone, 'email': email, 'birth_date': birthDate, 'password': password, 'otp': otp, }, ); } Future resetPassword({ required String phoneOrEmail, required String otp, required String password, }) async { return await callRPC( '/api/loyalty/reset_password', 'call', { 'phone_or_email': phoneOrEmail, 'otp': otp, 'password': password, }, ); } Future deleteAccount(String password) async { return await callRPC( '/api/loyalty/delete_account', 'call', { 'password': password, }, ); } /// Fetch public branch information (includes lat/lng for geolocation sorting). static Future> getPublicBranches() async { final activeClient = OdooService().client; final clientToUse = activeClient ?? OdooClient(AppConfig.odooUrl); try { final res = await clientToUse.callRPC( '/api/loyalty/branches', 'call', {} ); if (res != null && res['status'] == 'success') { return res['data'] as List; } else { throw Exception(res?['message'] ?? 'Failed to load branches'); } } catch (e) { rethrow; } finally { if (activeClient == null) { clientToUse.close(); } } } }