From 7e11c304a37484f433a28149449fbc931ffe5594 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Thu, 26 Mar 2026 08:14:10 +0700 Subject: [PATCH] feat: Add branch listing screen with Odoo service integration, accessible from login and dashboard. --- lib/screens/branches_screen.dart | 115 +++++++++++++++++++++++++++++ lib/screens/login_screen.dart | 6 ++ lib/screens/loyalty_dashboard.dart | 5 ++ lib/services/odoo_service.dart | 23 ++++++ 4 files changed, 149 insertions(+) create mode 100644 lib/screens/branches_screen.dart diff --git a/lib/screens/branches_screen.dart b/lib/screens/branches_screen.dart new file mode 100644 index 0000000..63b175f --- /dev/null +++ b/lib/screens/branches_screen.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import '../services/odoo_service.dart'; +import '../theme/app_theme.dart'; + +class BranchesScreen extends StatefulWidget { + const BranchesScreen({super.key}); + + @override + State createState() => _BranchesScreenState(); +} + +class _BranchesScreenState extends State { + List _branches = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _fetchBranches(); + } + + Future _fetchBranches() async { + try { + final branches = await OdooService.getPublicBranches(); + if (mounted) { + setState(() { + _branches = branches; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error loading branches. Check connection.'))); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Find Branch')), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _branches.isEmpty + ? const Center(child: Text('No branches available.', style: TextStyle(fontSize: 16))) + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + itemCount: _branches.length, + itemBuilder: (context, index) { + final branch = _branches[index]; + + final street = branch['street'] != null && branch['street'] != false ? branch['street'] : ''; + final city = branch['city'] != null && branch['city'] != false ? branch['city'] : ''; + final phone = branch['phone'] != null && branch['phone'] != false ? branch['phone'] : ''; + + final addressParts = [street, city].where((e) => e.toString().isNotEmpty).join(', '); + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: AppTheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppTheme.onSurface.withOpacity(0.04), + blurRadius: 16, + offset: const Offset(0, 4), + ) + ] + ), + child: ListTile( + contentPadding: const EdgeInsets.all(16), + leading: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppTheme.secondaryContainer.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: const Icon(Icons.storefront, color: AppTheme.secondary), + ), + title: Text( + branch['name'] ?? 'Mapan Branch', + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + addressParts.isEmpty ? 'No address specified' : addressParts, + style: Theme.of(context).textTheme.bodyMedium, + ), + if (phone.isNotEmpty) ...[ + const SizedBox(height: 4), + Row( + children: [ + const Icon(Icons.phone, size: 14, color: AppTheme.primary), + const SizedBox(width: 4), + Text(phone, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: AppTheme.primary)), + ], + ), + ] + ], + ), + ), + trailing: const Icon(Icons.chevron_right, color: AppTheme.onSurfaceVariant), + ), + ); + }, + ), + ); + } +} diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 7462a65..e03ea24 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -4,6 +4,7 @@ import 'package:odoo_rpc/odoo_rpc.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../services/odoo_service.dart'; import 'loyalty_dashboard.dart'; +import 'branches_screen.dart'; import '../theme/app_theme.dart'; class LoginScreen extends StatefulWidget { @@ -114,6 +115,11 @@ class _LoginScreenState extends State { ), ), ), + const SizedBox(height: 16), + TextButton( + onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const BranchesScreen())), + child: Text('Find Nearest Branch', style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary)), + ), ], ), ), diff --git a/lib/screens/loyalty_dashboard.dart b/lib/screens/loyalty_dashboard.dart index 9af8d55..44aef99 100644 --- a/lib/screens/loyalty_dashboard.dart +++ b/lib/screens/loyalty_dashboard.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../services/odoo_service.dart'; import '../theme/app_theme.dart'; import 'notifications_screen.dart'; +import 'branches_screen.dart'; class LoyaltyDashboard extends StatefulWidget { final int partnerId; @@ -42,6 +43,10 @@ class _LoyaltyDashboardState extends State { appBar: AppBar( title: const Text('My Rewards'), actions: [ + IconButton( + icon: const Icon(Icons.storefront_outlined), + onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const BranchesScreen())), + ), IconButton( icon: const Icon(Icons.notifications_outlined), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const NotificationsScreen())), diff --git a/lib/services/odoo_service.dart b/lib/services/odoo_service.dart index da4a423..ff6171e 100644 --- a/lib/services/odoo_service.dart +++ b/lib/services/odoo_service.dart @@ -30,4 +30,27 @@ class OdooService { 'kwargs': {'fields': ['points', 'program_id', 'code']} }) as List; } + + /// Fetch public branch information using our secure Odoo endpoint + /// This completely isolates the Admin API Key from the Flutter Source Code! + static Future> getPublicBranches() async { + final tempClient = OdooClient('https://erp.mapan.co.id'); + try { + final res = await tempClient.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 { + tempClient.close(); + } + } } +