diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies
index 9c071df..34031d9 100644
--- a/.flutter-plugins-dependencies
+++ b/.flutter-plugins-dependencies
@@ -1 +1 @@
-{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"app_badge_plus","path":"/home/suherdy/.pub-cache/hosted/pub.dev/app_badge_plus-1.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_apple","path":"/home/suherdy/.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.10/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_ios-6.4.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"workmanager_apple","path":"/home/suherdy/.pub-cache/hosted/pub.dev/workmanager_apple-0.9.1+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"app_badge_plus","path":"/home/suherdy/.pub-cache/hosted/pub.dev/app_badge_plus-1.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_android-2.2.22/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/permission_handler_android-13.0.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.21/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.29/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"workmanager_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/workmanager_android-0.9.0+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"app_badge_plus","path":"/home/suherdy/.pub-cache/hosted/pub.dev/app_badge_plus-1.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"flutter_local_notifications_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-8.0.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"flutter_local_notifications_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-3.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"permission_handler_html","path":"/home/suherdy/.pub-cache/hosted/pub.dev/permission_handler_html-0.1.3+5/","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.2/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"app_badge_plus","dependencies":[]},{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux","flutter_local_notifications_windows"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"flutter_local_notifications_windows","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]},{"name":"workmanager","dependencies":["workmanager_android","workmanager_apple"]},{"name":"workmanager_android","dependencies":[]},{"name":"workmanager_apple","dependencies":[]}],"date_created":"2026-06-13 21:28:54.078112","version":"3.41.5","swift_package_manager_enabled":{"ios":false,"macos":false}}
\ No newline at end of file
+{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"app_badge_plus","path":"/home/suherdy/.pub-cache/hosted/pub.dev/app_badge_plus-1.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/suherdy/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_apple","path":"/home/suherdy/.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.10/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_ios-6.4.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"workmanager_apple","path":"/home/suherdy/.pub-cache/hosted/pub.dev/workmanager_apple-0.9.1+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"app_badge_plus","path":"/home/suherdy/.pub-cache/hosted/pub.dev/app_badge_plus-1.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_android-2.2.22/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/permission_handler_android-13.0.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.21/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.29/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"workmanager_android","path":"/home/suherdy/.pub-cache/hosted/pub.dev/workmanager_android-0.9.0+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"app_badge_plus","path":"/home/suherdy/.pub-cache/hosted/pub.dev/app_badge_plus-1.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/suherdy/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.14/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"flutter_local_notifications_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-8.0.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"flutter_local_notifications_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-3.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"geolocator_web","path":"/home/suherdy/.pub-cache/hosted/pub.dev/geolocator_web-4.1.4/","dependencies":[],"dev_dependency":false},{"name":"permission_handler_html","path":"/home/suherdy/.pub-cache/hosted/pub.dev/permission_handler_html-0.1.3+5/","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"/home/suherdy/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/suherdy/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.2/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"app_badge_plus","dependencies":[]},{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux","flutter_local_notifications_windows"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"flutter_local_notifications_windows","dependencies":[]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]},{"name":"workmanager","dependencies":["workmanager_android","workmanager_apple"]},{"name":"workmanager_android","dependencies":[]},{"name":"workmanager_apple","dependencies":[]}],"date_created":"2026-06-14 08:33:22.634142","version":"3.41.5","swift_package_manager_enabled":{"ios":false,"macos":false}}
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 4e203d5..2747bfc 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -2,6 +2,8 @@
+
+
createState() => _AccountScreenState();
+}
+
+class _AccountScreenState extends State {
+ final _phraseController = TextEditingController();
+ final _passwordController = TextEditingController();
+ bool _isLoading = false;
+
+ String _aboutUsUrl = '';
+ String _contactUsUrl = '';
+
+ @override
+ void initState() {
+ super.initState();
+ _loadAppConfig();
+ }
+
+ Future _loadAppConfig() async {
+ try {
+ final config = await OdooService.getAppConfig();
+ if (mounted) {
+ setState(() {
+ _aboutUsUrl = config['about_us_url'] ?? '';
+ _contactUsUrl = config['contact_us_url'] ?? '';
+ });
+ }
+ } catch (_) {}
+ }
+
+ Future _launchUrl(String url) async {
+ if (url.isEmpty) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('URL not configured yet.')),
+ );
+ return;
+ }
+ final uri = Uri.tryParse(url);
+ if (uri != null && await canLaunchUrl(uri)) {
+ await launchUrl(uri, mode: LaunchMode.externalApplication);
+ } else {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Could not open link.')),
+ );
+ }
+ }
+ }
+
+ void _showTerms() {
+ AgreementDialog.show(context, 'Terms & Conditions', AgreementTexts.termsAndConditions);
+ }
+
+ void _showPrivacy() {
+ AgreementDialog.show(context, 'Privacy Policy', AgreementTexts.privacyPolicy);
+ }
+
+ void _logout() async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.remove('odoo_session');
+ await prefs.remove('last_seen_notification_id');
+ await prefs.remove('last_device_notified_id');
+ await prefs.remove('read_notification_ids');
+ await NotificationService().clearBadge();
+ if (mounted) {
+ Navigator.of(context).pushAndRemoveUntil(
+ MaterialPageRoute(builder: (_) => const LoginScreen()),
+ (route) => false,
+ );
+ }
+ }
+
+ void _showDeleteConfirmationDialog() {
+ _phraseController.clear();
+ _passwordController.clear();
+ showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) {
+ return StatefulBuilder(
+ builder: (context, setDialogState) {
+ return AlertDialog(
+ title: const Text(
+ 'Delete Account Permanently',
+ style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
+ ),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ const Text(
+ 'WARNING: This is a permanent action. All your loyalty points, card tier history, and reward history will be deleted and cannot be recovered.',
+ style: TextStyle(
+ color: AppTheme.onSurface,
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
+ ),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'To confirm, type "DELETE MY ACCOUNT" below:',
+ style: TextStyle(color: AppTheme.onSurfaceVariant, fontSize: 13),
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _phraseController,
+ decoration: const InputDecoration(hintText: 'DELETE MY ACCOUNT'),
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Enter your current password:',
+ style: TextStyle(color: AppTheme.onSurfaceVariant, fontSize: 13),
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _passwordController,
+ obscureText: true,
+ decoration: const InputDecoration(hintText: 'Password'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: const Text('Cancel',
+ style: TextStyle(color: AppTheme.onSurface, fontWeight: FontWeight.bold)),
+ ),
+ TextButton(
+ onPressed: _isLoading
+ ? null
+ : () async {
+ final phrase = _phraseController.text.trim();
+ final password = _passwordController.text;
+ if (phrase != 'DELETE MY ACCOUNT') {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Verification phrase is incorrect.')),
+ );
+ return;
+ }
+ if (password.isEmpty) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Please enter your password.')),
+ );
+ return;
+ }
+ setDialogState(() => _isLoading = true);
+ try {
+ final service = OdooService();
+ final response = await service.deleteAccount(password);
+ if (response != null && response['status'] == 'success') {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.remove('odoo_session');
+ if (context.mounted) {
+ Navigator.of(context).pop();
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(response['message'] ?? 'Account deleted.')),
+ );
+ Navigator.of(context).pushAndRemoveUntil(
+ MaterialPageRoute(builder: (_) => const LoginScreen()),
+ (route) => false,
+ );
+ }
+ } else {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(response?['message'] ?? 'Deletion failed.')),
+ );
+ }
+ }
+ } catch (e) {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error: $e')),
+ );
+ }
+ } finally {
+ setDialogState(() => _isLoading = false);
+ }
+ },
+ child: _isLoading
+ ? const SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(strokeWidth: 2, color: Colors.red),
+ )
+ : const Text('Delete My Account',
+ style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
+ ),
+ ],
+ );
+ },
+ );
+ },
+ );
+ }
+
+ @override
+ void dispose() {
+ _phraseController.dispose();
+ _passwordController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SingleChildScrollView(
+ child: Column(
+ children: [
+ const SizedBox(height: 16),
+
+ // ── Info Section ─────────────────────────────────────────────
+ _SectionHeader(label: 'Info'),
+ _MenuItem(
+ icon: Icons.info_outline_rounded,
+ label: 'About Us',
+ onTap: () => _launchUrl(_aboutUsUrl),
+ ),
+ _MenuItem(
+ icon: Icons.phone_rounded,
+ label: 'Contact Us',
+ onTap: () => _launchUrl(_contactUsUrl),
+ ),
+
+ const SizedBox(height: 8),
+
+ // ── Legal Section ────────────────────────────────────────────
+ _SectionHeader(label: 'Legal'),
+ _MenuItem(
+ icon: Icons.description_outlined,
+ label: 'Terms & Conditions',
+ onTap: _showTerms,
+ ),
+ _MenuItem(
+ icon: Icons.lock_outline_rounded,
+ label: 'Privacy Policy',
+ onTap: _showPrivacy,
+ ),
+
+ const SizedBox(height: 8),
+
+ // ── Account Section ──────────────────────────────────────────
+ _SectionHeader(label: 'Account'),
+ _MenuItem(
+ icon: Icons.logout_rounded,
+ label: 'Log Out',
+ onTap: _logout,
+ ),
+ _MenuItem(
+ icon: Icons.delete_outline_rounded,
+ label: 'Delete Account',
+ labelColor: const Color(0xFFB02500),
+ iconColor: const Color(0xFFB02500),
+ onTap: _showDeleteConfirmationDialog,
+ ),
+
+ const SizedBox(height: 32),
+ ],
+ ),
+ );
+ }
+}
+
+class _SectionHeader extends StatelessWidget {
+ final String label;
+ const _SectionHeader({required this.label});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(20, 12, 20, 4),
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ label.toUpperCase(),
+ style: Theme.of(context).textTheme.labelLarge?.copyWith(
+ color: AppTheme.onSurfaceVariant,
+ letterSpacing: 1.2,
+ fontSize: 11,
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _MenuItem extends StatelessWidget {
+ final IconData icon;
+ final String label;
+ final VoidCallback onTap;
+ final Color? labelColor;
+ final Color? iconColor;
+
+ const _MenuItem({
+ required this.icon,
+ required this.label,
+ required this.onTap,
+ this.labelColor,
+ this.iconColor,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Material(
+ color: AppTheme.surfaceContainerLowest,
+ child: InkWell(
+ onTap: onTap,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
+ decoration: const BoxDecoration(
+ border: Border(
+ bottom: BorderSide(color: AppTheme.surfaceContainer, width: 1),
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(icon,
+ size: 22,
+ color: iconColor ?? AppTheme.onSurfaceVariant),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Text(
+ label,
+ style: Theme.of(context).textTheme.bodyLarge?.copyWith(
+ color: labelColor ?? AppTheme.onSurface,
+ ),
+ ),
+ ),
+ Icon(Icons.chevron_right,
+ size: 20, color: AppTheme.outlineVariant),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/screens/branches_screen.dart b/lib/screens/branches_screen.dart
index d72715a..6502ef1 100644
--- a/lib/screens/branches_screen.dart
+++ b/lib/screens/branches_screen.dart
@@ -1,4 +1,6 @@
+import 'dart:math';
import 'package:flutter/material.dart';
+import 'package:geolocator/geolocator.dart';
import 'package:url_launcher/url_launcher.dart';
import '../services/odoo_service.dart';
import '../theme/app_theme.dart';
@@ -13,37 +15,121 @@ class BranchesScreen extends StatefulWidget {
class _BranchesScreenState extends State {
List _branches = [];
bool _isLoading = true;
+ Position? _userPosition;
+ bool _locationDenied = false;
@override
void initState() {
super.initState();
- _fetchBranches();
+ _fetchBranchesWithLocation();
}
- Future _fetchBranches() async {
+ Future _fetchBranchesWithLocation() async {
+ setState(() => _isLoading = true);
+
+ // Try to get user location
+ Position? pos;
+ try {
+ bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
+ if (serviceEnabled) {
+ LocationPermission permission = await Geolocator.checkPermission();
+ if (permission == LocationPermission.denied) {
+ permission = await Geolocator.requestPermission();
+ }
+ if (permission == LocationPermission.whileInUse ||
+ permission == LocationPermission.always) {
+ pos = await Geolocator.getCurrentPosition(
+ locationSettings: const LocationSettings(
+ accuracy: LocationAccuracy.medium,
+ timeLimit: Duration(seconds: 8),
+ ),
+ );
+ }
+ }
+ } catch (_) {
+ // Location not available — proceed without it
+ }
+
+ // Fetch branches from Odoo
try {
final branches = await OdooService.getPublicBranches();
+
if (mounted) {
setState(() {
- _branches = branches;
+ _userPosition = pos;
+ _locationDenied = pos == null;
+
+ if (pos != null) {
+ // Sort branches by distance from user
+ _branches = List.from(branches)
+ ..sort((a, b) {
+ final p = pos!;
+ final da = _distanceTo(p, a);
+ final db = _distanceTo(p, b);
+ return da.compareTo(db);
+ });
+ } else {
+ // Fallback: alphabetical sort
+ _branches = List.from(branches)
+ ..sort((a, b) => (a['name'] as String? ?? '')
+ .compareTo(b['name'] as String? ?? ''));
+ }
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() => _isLoading = false);
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error loading branches. Check connection.')));
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Error loading branches. Check connection.')),
+ );
}
}
}
+ /// Haversine formula — returns distance in kilometres.
+ double _distanceTo(Position pos, dynamic branch) {
+ final lat = _toDouble(branch['partner_latitude']);
+ final lng = _toDouble(branch['partner_longitude']);
+ if (lat == 0.0 && lng == 0.0) return double.maxFinite;
+
+ const R = 6371.0;
+ final dLat = _degToRad(lat - pos.latitude);
+ final dLng = _degToRad(lng - pos.longitude);
+ final a = sin(dLat / 2) * sin(dLat / 2) +
+ cos(_degToRad(pos.latitude)) *
+ cos(_degToRad(lat)) *
+ sin(dLng / 2) *
+ sin(dLng / 2);
+ final c = 2 * atan2(sqrt(a), sqrt(1 - a));
+ return R * c;
+ }
+
+ double _degToRad(double deg) => deg * (pi / 180);
+
+ double _toDouble(dynamic val) {
+ if (val == null || val == false) return 0.0;
+ if (val is num) return val.toDouble();
+ return double.tryParse(val.toString()) ?? 0.0;
+ }
+
+ String _formatDistance(double km) {
+ if (km == double.maxFinite) return '';
+ if (km < 1) return '${(km * 1000).toStringAsFixed(0)} m';
+ return '${km.toStringAsFixed(1)} km';
+ }
+
Future _launchMaps(String queryTerm) async {
final query = Uri.encodeComponent(queryTerm);
final url = Uri.parse('https://www.google.com/maps/search/?api=1&query=$query');
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
} else {
- if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Could not open map.')));
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Could not open map.')),
+ );
+ }
}
}
@@ -57,85 +143,193 @@ class _BranchesScreenState extends State {
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
} else {
- if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Could not open WhatsApp.')));
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Could not open WhatsApp.')),
+ );
+ }
}
}
@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: const BoxDecoration(
- color: AppTheme.surfaceContainerLow,
- borderRadius: BorderRadius.zero,
- // Spec rules: "Don't use standard drop shadows"
- ),
- child: ListTile(
- contentPadding: const EdgeInsets.all(16),
- onTap: () => _launchMaps('${branch['name']} ${addressParts}'),
- leading: Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: AppTheme.secondaryContainer.withOpacity(0.2),
- shape: BoxShape.rectangle,
- ),
- 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,
+ return _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : RefreshIndicator(
+ onRefresh: _fetchBranchesWithLocation,
+ child: Column(
+ children: [
+ // Location status banner
+ if (_locationDenied)
+ Container(
+ width: double.infinity,
+ color: AppTheme.surfaceContainerLow,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+ child: Row(
+ children: [
+ const Icon(Icons.location_off, size: 16, color: AppTheme.onSurfaceVariant),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ 'Location not available. Showing branches alphabetically.',
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: AppTheme.onSurfaceVariant,
),
- if (phone.isNotEmpty) ...[
- const SizedBox(height: 4),
- Row(
+ ),
+ ),
+ ],
+ ),
+ )
+ else if (_userPosition != null)
+ Container(
+ width: double.infinity,
+ color: AppTheme.surfaceContainerLow,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+ child: Row(
+ children: [
+ const Icon(Icons.my_location, size: 16, color: AppTheme.secondary),
+ const SizedBox(width: 8),
+ Text(
+ 'Sorted by distance from your location',
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: AppTheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // Branch list
+ Expanded(
+ child: _branches.isEmpty
+ ? const Center(
+ child: Text('No branches available.', style: TextStyle(fontSize: 16)),
+ )
+ : ListView.builder(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ 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(', ');
+
+ final distance = _userPosition != null
+ ? _distanceTo(_userPosition!, branch)
+ : null;
+ final distanceLabel =
+ distance != null ? _formatDistance(distance) : '';
+
+ return Container(
+ margin: const EdgeInsets.only(bottom: 12),
+ decoration: const BoxDecoration(
+ color: AppTheme.surfaceContainerLow,
+ borderRadius: BorderRadius.zero,
+ ),
+ child: ListTile(
+ contentPadding: const EdgeInsets.all(16),
+ onTap: () => _launchMaps(
+ '${branch['name']} $addressParts'),
+ leading: Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: AppTheme.secondaryContainer
+ .withValues(alpha: 0.2),
+ shape: BoxShape.rectangle,
+ ),
+ child: const Icon(Icons.storefront,
+ color: AppTheme.secondary),
+ ),
+ title: Row(
+ children: [
+ Expanded(
+ child: Text(
+ branch['name'] ?? 'Mapan Branch',
+ style: Theme.of(context)
+ .textTheme
+ .titleMedium,
+ ),
+ ),
+ if (distanceLabel.isNotEmpty)
+ Container(
+ margin: const EdgeInsets.only(left: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8, vertical: 3),
+ decoration: const BoxDecoration(
+ color: AppTheme.secondaryContainer,
+ ),
+ child: Text(
+ distanceLabel,
+ style: Theme.of(context)
+ .textTheme
+ .labelLarge
+ ?.copyWith(
+ color: AppTheme.onSecondaryContainer,
+ fontSize: 11,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ],
+ ),
+ subtitle: Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- const Icon(Icons.phone, size: 14, color: AppTheme.onSurfaceVariant),
- const SizedBox(width: 4),
- Text(phone, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: AppTheme.onSurfaceVariant)),
+ 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.onSurfaceVariant),
+ const SizedBox(width: 4),
+ Text(phone,
+ style: Theme.of(context)
+ .textTheme
+ .bodySmall
+ ?.copyWith(
+ color: AppTheme.onSurfaceVariant)),
+ ],
+ ),
+ ]
],
),
- ]
- ],
- ),
- ),
- trailing: phone.isNotEmpty
- ? IconButton(
- icon: const Icon(Icons.chat_bubble, color: AppTheme.onSurface),
- onPressed: () => _launchWhatsApp(phone),
- tooltip: 'Chat on WhatsApp',
- )
- : const Icon(Icons.chevron_right, color: AppTheme.onSurfaceVariant),
+ ),
+ trailing: phone.isNotEmpty
+ ? IconButton(
+ icon: const Icon(Icons.chat_bubble,
+ color: AppTheme.onSurface),
+ onPressed: () => _launchWhatsApp(phone),
+ tooltip: 'Chat on WhatsApp',
+ )
+ : const Icon(Icons.chevron_right,
+ color: AppTheme.onSurfaceVariant),
+ ),
+ );
+ },
),
- );
- },
),
- );
+ ],
+ ),
+ );
}
}
diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart
index 126da8f..5da9175 100644
--- a/lib/screens/login_screen.dart
+++ b/lib/screens/login_screen.dart
@@ -4,7 +4,7 @@ import 'package:odoo_rpc/odoo_rpc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../services/odoo_service.dart';
import '../services/config.dart';
-import 'loyalty_dashboard.dart';
+import 'main_shell.dart';
import 'branches_screen.dart';
import 'activation_screen.dart';
import 'signup_screen.dart';
@@ -43,7 +43,7 @@ class _LoginScreenState extends State {
Navigator.pushReplacement(
context,
MaterialPageRoute(
- builder: (_) => LoyaltyDashboard(partnerId: session.partnerId),
+ builder: (_) => MainShell(partnerId: session.partnerId),
),
);
}
diff --git a/lib/screens/loyalty_dashboard.dart b/lib/screens/loyalty_dashboard.dart
index ed4eb69..cb13e5f 100644
--- a/lib/screens/loyalty_dashboard.dart
+++ b/lib/screens/loyalty_dashboard.dart
@@ -1,13 +1,12 @@
-import 'dart:async';
import 'package:flutter/material.dart';
-import 'package:shared_preferences/shared_preferences.dart';
import '../services/odoo_service.dart';
-import '../services/notification_service.dart';
import '../theme/app_theme.dart';
-import 'notifications_screen.dart';
-import 'branches_screen.dart';
-import 'settings_screen.dart';
+import '../widgets/carousel_widget.dart';
+import '../widgets/promo_card_widget.dart';
+import '../widgets/subscription_list_widget.dart';
+/// Home tab — shows loyalty card, subscriptions, carousel, and promo highlights.
+/// Notification polling and AppBar are handled by MainShell.
class LoyaltyDashboard extends StatefulWidget {
final int partnerId;
const LoyaltyDashboard({super.key, required this.partnerId});
@@ -18,249 +17,166 @@ class LoyaltyDashboard extends StatefulWidget {
class _LoyaltyDashboardState extends State {
List _loyaltyCards = [];
+ List _subscriptions = [];
+ List _carouselSlides = [];
+ List _promos = [];
bool _isLoading = true;
- int _unreadNotificationCount = 0;
- Timer? _notificationTimer;
-
- // Shared pref keys
- static const _kLastNotified = 'last_device_notified_id';
@override
void initState() {
super.initState();
- _fetchLoyaltyData();
- _fetchNotificationCount();
- _notificationTimer = Timer.periodic(const Duration(seconds: 10), (_) {
- _fetchNotificationCount();
- });
+ _fetchAll();
}
- @override
- void dispose() {
- _notificationTimer?.cancel();
- super.dispose();
- }
-
- Future _fetchNotificationCount() async {
+ Future _fetchAll() async {
+ setState(() => _isLoading = true);
try {
- final client = OdooService().client;
- if (client == null) return;
+ final results = await Future.wait([
+ OdooService().getLoyaltyCards(widget.partnerId),
+ OdooService().getSubscriptionCards(widget.partnerId),
+ OdooService().getCmsContent(),
+ ]);
- final response = await client.callRPC(
- '/api/loyalty/fetch_notifications',
- 'call',
- {'last_id': 0},
- );
+ final cards = results[0] as List;
+ final subs = results[1] as List;
+ final cms = results[2] as Map;
- if (response != null && response['status'] == 'success') {
- final List notifs = response['data'] ?? [];
- final prefs = await SharedPreferences.getInstance();
- final lastNotifiedId = prefs.getInt(_kLastNotified) ?? 0;
-
- // Check read list
- final readIds = prefs.getStringList('read_notification_ids');
- int unreadCount = 0;
-
- if (readIds == null) {
- final initialRead = notifs.map((n) => (n['id'] as int? ?? 0).toString()).toList();
- await prefs.setStringList('read_notification_ids', initialRead);
- unreadCount = 0;
- } else {
- unreadCount = notifs
- .where((n) => !readIds.contains((n['id'] as int? ?? 0).toString()))
- .length;
- }
-
- int highestNewId = lastNotifiedId;
- final List toNotify = [];
-
- for (var notif in notifs) {
- final id = notif['id'] as int? ?? 0;
- if (id > lastNotifiedId) {
- toNotify.add(notif);
- if (id > highestNewId) highestNewId = id;
- }
- }
-
- // Show device tray notifications for any not yet shown
- if (toNotify.isNotEmpty) {
- final notifService = NotificationService();
- for (final notif in toNotify) {
- await notifService.showNotification(
- id: notif['id'] as int,
- title: notif['title'] ?? 'Mie Mapan',
- body: notif['body'] ?? '',
- );
- }
- await prefs.setInt(_kLastNotified, highestNewId);
- }
-
- // Always update system badge to match the unread count
- await NotificationService().setBadge(unreadCount);
-
- if (mounted) {
- setState(() => _unreadNotificationCount = unreadCount);
- }
+ if (mounted) {
+ setState(() {
+ _loyaltyCards = cards;
+ _subscriptions = subs;
+ _carouselSlides = (cms['carousel'] as List?) ?? [];
+ _promos = (cms['promos'] as List?) ?? [];
+ _isLoading = false;
+ });
}
- } catch (e) {
- // ignore
- }
- }
-
- Future _fetchLoyaltyData() async {
- try {
- final cards = await OdooService().getLoyaltyCards(widget.partnerId);
- await _fetchNotificationCount();
- setState(() {
- _loyaltyCards = cards;
- _isLoading = false;
- });
} catch (e) {
if (mounted) {
setState(() => _isLoading = false);
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error loading loyalty cards: $e')));
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error loading data: $e')),
+ );
}
}
}
-
@override
Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(
- title: const Text('My Rewards'),
- actions: [
- IconButton(
- icon: const Icon(Icons.storefront),
- onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const BranchesScreen())),
- ),
- Stack(
- clipBehavior: Clip.none,
- children: [
- IconButton(
- icon: const Icon(Icons.notifications),
- onPressed: () async {
- await Navigator.push(context, MaterialPageRoute(builder: (_) => const NotificationsScreen()));
- _fetchNotificationCount();
- },
+ if (_isLoading) {
+ return const Center(child: CircularProgressIndicator());
+ }
+
+ return RefreshIndicator(
+ onRefresh: _fetchAll,
+ child: ListView(
+ children: [
+ // ── Loyalty Card ─────────────────────────────────────────────
+ if (_loyaltyCards.isEmpty)
+ Padding(
+ padding: const EdgeInsets.fromLTRB(24, 32, 24, 0),
+ child: Text(
+ 'No active loyalty card yet.',
+ style: Theme.of(context).textTheme.titleLarge,
+ textAlign: TextAlign.center,
),
- if (_unreadNotificationCount > 0)
- Positioned(
- right: 8,
- top: 8,
- child: Container(
- padding: const EdgeInsets.all(4),
- decoration: const BoxDecoration(
- color: Colors.red,
- shape: BoxShape.circle,
- ),
- constraints: const BoxConstraints(
- minWidth: 16,
- minHeight: 16,
- ),
- child: Text(
- '$_unreadNotificationCount',
- style: const TextStyle(
- color: Colors.white,
- fontSize: 10,
- fontWeight: FontWeight.bold,
- ),
- textAlign: TextAlign.center,
- ),
- ),
- ),
- ],
- ),
- IconButton(
- icon: const Icon(Icons.settings),
- onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())),
- ),
- const SizedBox(width: 8),
+ )
+ else
+ ..._loyaltyCards.map((card) => _LoyaltyCardTile(card: card)),
+
+ // ── Subscriptions ─────────────────────────────────────────────
+ if (_subscriptions.isNotEmpty)
+ SubscriptionListWidget(subscriptions: _subscriptions),
+
+ const SizedBox(height: 20),
+
+ // ── Carousel ──────────────────────────────────────────────────
+ if (_carouselSlides.isNotEmpty) ...[
+ CarouselWidget(slides: _carouselSlides),
+ const SizedBox(height: 24),
+ ],
+
+ // ── Promo Highlights ─────────────────────────────────────────
+ if (_promos.isNotEmpty) ...[
+ PromoCardRow(promos: _promos),
+ const SizedBox(height: 24),
+ ],
+ ],
+ ),
+ );
+ }
+}
+
+class _LoyaltyCardTile extends StatelessWidget {
+ final dynamic card;
+ const _LoyaltyCardTile({required this.card});
+
+ @override
+ Widget build(BuildContext context) {
+ final programName = (card['program_id']?[1] as String? ?? '').toLowerCase();
+ String tier = 'Member';
+ if (programName.contains('silver')) tier = 'Silver Member';
+ if (programName.contains('gold')) tier = 'Gold Member';
+ if (programName.contains('platinum')) tier = 'Platinum Member';
+
+ return Container(
+ margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
+ padding: const EdgeInsets.all(24),
+ decoration: const BoxDecoration(
+ color: AppTheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.zero,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Text(
+ '${card['program_id']?[1] ?? 'Loyalty Program'}',
+ style: Theme.of(context).textTheme.titleLarge,
+ softWrap: true,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ decoration: const BoxDecoration(
+ color: AppTheme.secondaryContainer,
+ borderRadius: BorderRadius.zero,
+ ),
+ child: Text(
+ tier,
+ style: Theme.of(context).textTheme.labelLarge?.copyWith(
+ color: AppTheme.onSecondaryContainer,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 20),
+ Text('Membership Code', style: Theme.of(context).textTheme.bodyMedium),
+ const SizedBox(height: 4),
+ Text('${card['code'] ?? 'N/A'}',
+ style: Theme.of(context).textTheme.titleMedium),
+ const SizedBox(height: 16),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Text('Available Points',
+ style: Theme.of(context).textTheme.bodyMedium),
+ Text(
+ '${card['points'] ?? 0}',
+ style: Theme.of(context).textTheme.displayMedium?.copyWith(
+ color: AppTheme.primary,
+ ),
+ ),
+ ],
+ ),
],
),
- body: _isLoading
- ? const Center(child: CircularProgressIndicator())
- : RefreshIndicator(
- onRefresh: _fetchLoyaltyData,
- child: _loyaltyCards.isEmpty
- ? Center(
- child: Text(
- 'No active rewards yet.',
- style: Theme.of(context).textTheme.titleLarge,
- ),
- )
- : ListView.builder(
- padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 40.0),
- itemCount: _loyaltyCards.length,
- itemBuilder: (context, index) {
- final card = _loyaltyCards[index];
- return Container(
- margin: const EdgeInsets.only(bottom: 40),
- padding: const EdgeInsets.all(32),
- decoration: const BoxDecoration(
- color: AppTheme.surfaceContainerHighest, // Soft Lift without shadow
- borderRadius: BorderRadius.zero,
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Expanded(
- child: Text(
- '${card['program_id']?[1] ?? 'Loyalty Program'}',
- style: Theme.of(context).textTheme.titleLarge,
- softWrap: true,
- ),
- ),
- const SizedBox(width: 16),
- Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- decoration: const BoxDecoration(
- color: AppTheme.secondaryContainer,
- borderRadius: BorderRadius.zero, // Editorial block
- ),
- child: Text(
- (() {
- final programName = (card['program_id']?[1] as String? ?? '').toLowerCase();
- if (programName.contains('silver')) return 'Silver Member';
- if (programName.contains('gold')) return 'Gold Member';
- if (programName.contains('platinum')) return 'Platinum Member';
- return 'Member';
- })(),
- style: Theme.of(context).textTheme.labelLarge?.copyWith(
- color: AppTheme.onSecondaryContainer,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- ],
- ),
- const SizedBox(height: 32),
- Text('Membership Code', style: Theme.of(context).textTheme.bodyMedium),
- const SizedBox(height: 4),
- Text('${card['code'] ?? 'N/A'}', style: Theme.of(context).textTheme.titleMedium),
- const SizedBox(height: 24),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Text('Available Points', style: Theme.of(context).textTheme.bodyMedium),
- Text(
- '${card['points'] ?? 0}',
- style: Theme.of(context).textTheme.displayMedium?.copyWith(
- color: AppTheme.primary,
- ),
- ),
- ],
- ),
- ],
- ),
- );
- },
- ),
- ),
);
}
}
diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart
new file mode 100644
index 0000000..29f012d
--- /dev/null
+++ b/lib/screens/main_shell.dart
@@ -0,0 +1,204 @@
+import 'dart:async';
+import 'dart:convert';
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import '../services/odoo_service.dart';
+import '../services/notification_service.dart';
+import '../services/theme_manager.dart';
+import '../theme/app_theme.dart';
+import 'notifications_screen.dart';
+import 'loyalty_dashboard.dart';
+import 'branches_screen.dart';
+import 'orders_screen.dart';
+import 'account_screen.dart';
+
+class MainShell extends StatefulWidget {
+ final int partnerId;
+ const MainShell({super.key, required this.partnerId});
+
+ @override
+ State createState() => _MainShellState();
+}
+
+class _MainShellState extends State {
+ int _currentIndex = 0;
+ int _unreadNotificationCount = 0;
+ Timer? _notificationTimer;
+
+ static const _kLastNotified = 'last_device_notified_id';
+
+ late final List _pages;
+
+ @override
+ void initState() {
+ super.initState();
+ _pages = [
+ LoyaltyDashboard(partnerId: widget.partnerId),
+ const BranchesScreen(),
+ const OrdersScreen(),
+ const AccountScreen(),
+ ];
+ _fetchNotificationCount();
+ _notificationTimer = Timer.periodic(const Duration(seconds: 30), (_) {
+ _fetchNotificationCount();
+ });
+ }
+
+ @override
+ void dispose() {
+ _notificationTimer?.cancel();
+ super.dispose();
+ }
+
+ Future _fetchNotificationCount() async {
+ try {
+ final client = OdooService().client;
+ if (client == null) return;
+
+ final response = await client.callRPC(
+ '/api/loyalty/fetch_notifications',
+ 'call',
+ {'last_id': 0},
+ );
+
+ if (response != null && response['status'] == 'success') {
+ final List notifs = response['data'] ?? [];
+ final prefs = await SharedPreferences.getInstance();
+ final lastNotifiedId = prefs.getInt(_kLastNotified) ?? 0;
+ final readIds = prefs.getStringList('read_notification_ids');
+
+ int unreadCount = 0;
+ if (readIds == null) {
+ final initialRead = notifs.map((n) => (n['id'] as int? ?? 0).toString()).toList();
+ await prefs.setStringList('read_notification_ids', initialRead);
+ unreadCount = 0;
+ } else {
+ unreadCount = notifs
+ .where((n) => !readIds.contains((n['id'] as int? ?? 0).toString()))
+ .length;
+ }
+
+ int highestNewId = lastNotifiedId;
+ final List toNotify = [];
+ for (var notif in notifs) {
+ final id = notif['id'] as int? ?? 0;
+ if (id > lastNotifiedId) {
+ toNotify.add(notif);
+ if (id > highestNewId) highestNewId = id;
+ }
+ }
+
+ if (toNotify.isNotEmpty) {
+ final notifService = NotificationService();
+ for (final notif in toNotify) {
+ await notifService.showNotification(
+ id: notif['id'] as int,
+ title: notif['title'] ?? 'Mie Mapan',
+ body: notif['body'] ?? '',
+ );
+ }
+ await prefs.setInt(_kLastNotified, highestNewId);
+ }
+
+ await NotificationService().setBadge(unreadCount);
+
+ if (mounted) {
+ setState(() => _unreadNotificationCount = unreadCount);
+ }
+ }
+ } catch (e) {
+ // ignore
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final navLabels = ['Home', 'Branches', 'Orders', 'Account'];
+ final navIcons = [
+ Icons.home_rounded,
+ Icons.location_on_rounded,
+ Icons.receipt_long_rounded,
+ Icons.person_rounded,
+ ];
+
+ final colorScheme = Theme.of(context).colorScheme;
+
+ return Scaffold(
+ appBar: AppBar(
+ title: ThemeManager.instance.brandLogo.isNotEmpty
+ ? Image.memory(
+ base64Decode(ThemeManager.instance.brandLogo),
+ height: 36,
+ fit: BoxFit.contain,
+ errorBuilder: (context, error, stackTrace) => const Text('Mie Mapan'),
+ )
+ : const Text('Mie Mapan'),
+ actions: [
+ Stack(
+ clipBehavior: Clip.none,
+ children: [
+ IconButton(
+ icon: const Icon(Icons.notifications_rounded),
+ tooltip: 'Notifications',
+ onPressed: () async {
+ await Navigator.push(
+ context,
+ MaterialPageRoute(builder: (_) => const NotificationsScreen()),
+ );
+ _fetchNotificationCount();
+ },
+ ),
+ if (_unreadNotificationCount > 0)
+ Positioned(
+ right: 6,
+ top: 6,
+ child: Container(
+ padding: const EdgeInsets.all(3),
+ decoration: const BoxDecoration(
+ color: Colors.red,
+ shape: BoxShape.circle,
+ ),
+ constraints: const BoxConstraints(minWidth: 18, minHeight: 18),
+ child: Text(
+ _unreadNotificationCount > 99
+ ? '99+'
+ : '$_unreadNotificationCount',
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 10,
+ fontWeight: FontWeight.bold,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(width: 4),
+ ],
+ ),
+ body: IndexedStack(
+ index: _currentIndex,
+ children: _pages,
+ ),
+ bottomNavigationBar: NavigationBar(
+ selectedIndex: _currentIndex,
+ onDestinationSelected: (index) {
+ setState(() => _currentIndex = index);
+ },
+ backgroundColor: AppTheme.surfaceContainerLowest,
+ indicatorColor: colorScheme.primary,
+ destinations: List.generate(4, (i) {
+ return NavigationDestination(
+ icon: Icon(navIcons[i],
+ color: i == _currentIndex
+ ? colorScheme.onSecondaryContainer
+ : colorScheme.onSurfaceVariant),
+ selectedIcon: Icon(navIcons[i], color: colorScheme.onSecondaryContainer),
+ label: navLabels[i],
+ );
+ }),
+ ),
+ );
+ }
+}
diff --git a/lib/screens/notification_detail_screen.dart b/lib/screens/notification_detail_screen.dart
index ead02aa..4cdcdca 100644
--- a/lib/screens/notification_detail_screen.dart
+++ b/lib/screens/notification_detail_screen.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:flutter_html/flutter_html.dart';
import '../theme/app_theme.dart';
import '../services/odoo_service.dart';
@@ -10,20 +11,19 @@ class NotificationDetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = notif['title'] as String? ?? 'Notice';
- final body = notif['body'] as String? ?? '';
+ final bodyHtml = notif['body'] as String? ?? '';
return Scaffold(
appBar: AppBar(
title: const Text('Notification Detail'),
),
body: SingleChildScrollView(
- padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
- // Header icon block
+ // Header block
Container(
- padding: const EdgeInsets.symmetric(vertical: 32),
+ padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 24),
color: AppTheme.surfaceContainerLow,
child: Column(
children: [
@@ -40,89 +40,115 @@ class NotificationDetailScreen extends StatelessWidget {
),
),
const SizedBox(height: 20),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 24),
- child: Text(
- title,
- textAlign: TextAlign.center,
- style:
- Theme.of(context).textTheme.headlineMedium?.copyWith(
- color: AppTheme.onSurface,
- fontSize: 20,
- ),
- ),
+ Text(
+ title,
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.headlineMedium?.copyWith(
+ color: AppTheme.onSurface,
+ fontSize: 20,
+ ),
),
],
),
),
+ // Full image (authenticated)
if (notif['has_image'] == true) ...[
- const SizedBox(height: 24),
- ClipRRect(
- borderRadius: BorderRadius.circular(8),
- child: Image.network(
- OdooService().notificationImageUrl(notif['id'] as int),
- headers: {
- 'Cookie': OdooService().sessionCookie,
- },
- fit: BoxFit.cover,
- loadingBuilder: (context, child, loadingProgress) {
- if (loadingProgress == null) return child;
- return Container(
- height: 200,
- color: AppTheme.surfaceContainerLow,
- child: const Center(child: CircularProgressIndicator()),
- );
- },
- errorBuilder: (context, error, stackTrace) {
- return Container(
- height: 150,
- color: AppTheme.surfaceContainerLow,
- child: const Center(
- child: Icon(Icons.broken_image_outlined, size: 48, color: AppTheme.onSurfaceVariant),
- ),
- );
- },
+ const SizedBox(height: 16),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: ClipRect(
+ child: Image.network(
+ OdooService().notificationImageUrl(notif['id'] as int),
+ headers: {'Cookie': OdooService().sessionCookie},
+ fit: BoxFit.cover,
+ loadingBuilder: (context, child, loadingProgress) {
+ if (loadingProgress == null) return child;
+ return Container(
+ height: 200,
+ color: AppTheme.surfaceContainerLow,
+ child: const Center(child: CircularProgressIndicator()),
+ );
+ },
+ errorBuilder: (context, error, stackTrace) {
+ return Container(
+ height: 150,
+ color: AppTheme.surfaceContainerLow,
+ child: const Center(
+ child: Icon(Icons.broken_image_outlined,
+ size: 48, color: AppTheme.onSurfaceVariant),
+ ),
+ );
+ },
+ ),
),
),
],
- const SizedBox(height: 24),
+ const SizedBox(height: 20),
- // Body label
- Text(
- 'Message',
- style: Theme.of(context).textTheme.labelLarge?.copyWith(
- color: AppTheme.onSurfaceVariant,
- letterSpacing: 1.2,
- ),
+ // Message label
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: Text(
+ 'MESSAGE',
+ style: Theme.of(context).textTheme.labelLarge?.copyWith(
+ color: AppTheme.onSurfaceVariant,
+ letterSpacing: 1.2,
+ ),
+ ),
+ ),
+ const SizedBox(height: 8),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24),
+ child: Divider(
+ color: AppTheme.surfaceContainerHighest,
+ thickness: 2,
+ height: 2,
+ ),
),
const SizedBox(height: 8),
- // Divider line (editorial style)
- const Divider(
- color: AppTheme.surfaceContainerHighest,
- thickness: 2,
- height: 2,
- ),
- const SizedBox(height: 16),
-
- // Body text
- body.isEmpty
- ? Text(
- 'No additional details.',
- style: Theme.of(context).textTheme.bodyLarge?.copyWith(
- color: AppTheme.onSurfaceVariant,
- fontStyle: FontStyle.italic,
- ),
+ // Rich HTML body
+ bodyHtml.isEmpty
+ ? Padding(
+ padding: const EdgeInsets.all(24),
+ child: Text(
+ 'No additional details.',
+ style: Theme.of(context).textTheme.bodyLarge?.copyWith(
+ color: AppTheme.onSurfaceVariant,
+ fontStyle: FontStyle.italic,
+ ),
+ ),
)
- : Text(
- body,
- style: Theme.of(context).textTheme.bodyLarge?.copyWith(
+ : Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12),
+ child: Html(
+ data: bodyHtml,
+ style: {
+ 'body': Style(
color: AppTheme.onSurface,
- height: 1.7,
+ fontFamily: 'Manrope',
+ fontSize: FontSize(15),
+ lineHeight: const LineHeight(1.7),
),
+ 'p': Style(margin: Margins.only(bottom: 12)),
+ 'h1': Style(
+ color: AppTheme.onSurface,
+ fontSize: FontSize(22),
+ fontWeight: FontWeight.bold,
+ ),
+ 'h2': Style(
+ color: AppTheme.onSurface,
+ fontSize: FontSize(18),
+ fontWeight: FontWeight.bold,
+ ),
+ 'a': Style(color: AppTheme.secondary),
+ },
+ ),
),
+
+ const SizedBox(height: 40),
],
),
),
diff --git a/lib/screens/orders_screen.dart b/lib/screens/orders_screen.dart
new file mode 100644
index 0000000..fbfc715
--- /dev/null
+++ b/lib/screens/orders_screen.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+import '../theme/app_theme.dart';
+
+/// Orders tab — placeholder screen for future ordering features.
+class OrdersScreen extends StatelessWidget {
+ const OrdersScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(40),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ padding: const EdgeInsets.all(28),
+ decoration: BoxDecoration(
+ color: AppTheme.surfaceContainerLow,
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ Icons.receipt_long_rounded,
+ size: 56,
+ color: AppTheme.secondary,
+ ),
+ ),
+ const SizedBox(height: 28),
+ Text(
+ 'Orders',
+ style: Theme.of(context).textTheme.headlineMedium,
+ ),
+ const SizedBox(height: 12),
+ Text(
+ 'Online ordering is coming soon!\nStay tuned for updates.',
+ style: Theme.of(context).textTheme.bodyLarge?.copyWith(
+ color: AppTheme.onSurfaceVariant,
+ height: 1.6,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/screens/promo_detail_screen.dart b/lib/screens/promo_detail_screen.dart
new file mode 100644
index 0000000..dce8a69
--- /dev/null
+++ b/lib/screens/promo_detail_screen.dart
@@ -0,0 +1,99 @@
+import 'dart:convert';
+import 'dart:typed_data';
+import 'package:flutter/material.dart';
+import 'package:flutter_html/flutter_html.dart';
+import '../theme/app_theme.dart';
+
+/// Promo detail screen — mirrors notification detail but for promo highlights.
+/// Shows the promo image (full size), title, and rich HTML body content.
+class PromoDetailScreen extends StatelessWidget {
+ final dynamic promo;
+ const PromoDetailScreen({super.key, required this.promo});
+
+ @override
+ Widget build(BuildContext context) {
+ final title = promo['name'] as String? ?? 'Promo';
+ final bodyHtml = promo['body'] as String? ?? '';
+ final base64Img = promo['image_128'] as String?;
+
+ return Scaffold(
+ appBar: AppBar(title: Text(title)),
+ body: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Promo image (full width)
+ if (base64Img != null && base64Img.isNotEmpty)
+ _buildPromoImage(base64Img),
+
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 20, 20, 8),
+ child: Text(
+ title,
+ style: Theme.of(context).textTheme.titleLarge,
+ ),
+ ),
+
+ if (bodyHtml.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12),
+ child: Html(
+ data: bodyHtml,
+ style: {
+ 'body': Style(
+ color: AppTheme.onSurface,
+ fontFamily: 'Manrope',
+ fontSize: FontSize(15),
+ lineHeight: const LineHeight(1.6),
+ ),
+ 'p': Style(margin: Margins.only(bottom: 12)),
+ 'h1': Style(
+ color: AppTheme.onSurface,
+ fontSize: FontSize(22),
+ fontWeight: FontWeight.bold,
+ ),
+ 'h2': Style(
+ color: AppTheme.onSurface,
+ fontSize: FontSize(18),
+ fontWeight: FontWeight.bold,
+ ),
+ 'a': Style(color: AppTheme.secondary),
+ },
+ ),
+ )
+ else
+ Padding(
+ padding: const EdgeInsets.all(20),
+ child: Text(
+ 'No content available.',
+ style: Theme.of(context).textTheme.bodyMedium,
+ ),
+ ),
+
+ const SizedBox(height: 32),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildPromoImage(String base64Img) {
+ try {
+ final Uint8List bytes = base64Decode(base64Img);
+ return Image.memory(
+ bytes,
+ width: double.infinity,
+ height: 220,
+ fit: BoxFit.cover,
+ );
+ } catch (_) {
+ return Container(
+ height: 220,
+ color: AppTheme.surfaceContainer,
+ child: const Center(
+ child: Icon(Icons.local_offer_rounded, size: 56, color: AppTheme.outlineVariant),
+ ),
+ );
+ }
+ }
+}
diff --git a/lib/services/odoo_service.dart b/lib/services/odoo_service.dart
index 94ed2b3..c73be4b 100644
--- a/lib/services/odoo_service.dart
+++ b/lib/services/odoo_service.dart
@@ -1,5 +1,6 @@
import 'package:odoo_rpc/odoo_rpc.dart';
import 'config.dart';
+import 'theme_manager.dart';
class OdooService {
static final OdooService _instance = OdooService._internal();
@@ -13,7 +14,6 @@ class OdooService {
}
/// Returns the session cookie header value for authenticated image loading.
- /// Usage: Image.network(url, headers: {'Cookie': OdooService().sessionCookie})
String get sessionCookie {
final sessionId = client?.sessionId?.id ?? '';
return 'session_id=$sessionId';
@@ -23,6 +23,14 @@ class OdooService {
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 login(String db, String username, String password) async {
if (client == null) throw Exception("Connect to Odoo first");
return await client!.authenticate(db, username, password);
@@ -30,17 +38,103 @@ class OdooService {
Future> getLoyaltyCards(int partnerId) async {
if (client == null) throw Exception("Connect to Odoo first");
-
+
+ // Only fetch cards from 'loyalty' type programs (multi-tier: Silver/Gold/Platinum).
+ // Excludes subscriptions, coupons, gift cards, promotions, eWallets, etc.
return await client!.callKw({
'model': 'loyalty.card',
'method': 'search_read',
'args': [
- [['partner_id', '=', partnerId]],
+ [
+ ['partner_id', '=', partnerId],
+ ['program_id.program_type', '=', 'loyalty'],
+ ],
],
'kwargs': {'fields': ['points', 'program_id', 'code']}
}) as List;
}
+ /// Fetch subscription cards only — displayed as a "My Subscriptions" list.
+ Future> getSubscriptionCards(int partnerId) async {
+ if (client == null) throw Exception("Connect to Odoo first");
+
+ return await client!.callKw({
+ 'model': 'loyalty.card',
+ 'method': 'search_read',
+ 'args': [
+ [
+ ['partner_id', '=', partnerId],
+ ['program_id.program_type', '=', 'subscription'],
+ ],
+ ],
+ 'kwargs': {
+ 'fields': [
+ 'program_id',
+ 'code',
+ 'subscription_start_date',
+ 'subscription_end_date',
+ ]
+ }
+ }) as List;
+ }
+
+ /// Fetch carousel slides and promo highlights for the home screen.
+ Future