diff --git a/lib/main.dart b/lib/main.dart index e56138a..d251c3a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:workmanager/workmanager.dart'; import 'screens/login_screen.dart'; import 'services/background_service.dart'; +import 'theme/app_theme.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); - // Workmanager background tasks are only supported on physical iOS and Android devices. if (Platform.isAndroid || Platform.isIOS) { Workmanager().initialize( callbackDispatcher, @@ -30,12 +30,9 @@ class OdooLoyaltyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Odoo Loyalty App', + title: 'Mie Mapan Loyalty App', debugShowCheckedModeBanner: false, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), + theme: AppTheme.lightTheme, home: const LoginScreen(), ); } diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index b36700d..7462a65 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 '../theme/app_theme.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -49,21 +50,13 @@ class _LoginScreenState extends State { if (mounted) { Navigator.pushReplacement( context, - MaterialPageRoute( - builder: (_) => LoyaltyDashboard( - partnerId: session.partnerId, - ), - ), + MaterialPageRoute(builder: (_) => LoyaltyDashboard(partnerId: session.partnerId)), ); } } on OdooException catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Login failed: ${e.message}')), - ); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Login failed: ${e.message}'))); } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: $e')), - ); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e'))); } finally { if (mounted) setState(() => _isLoading = false); } @@ -72,40 +65,57 @@ class _LoginScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Login to Odoo')), - body: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextField( - controller: _usernameController, - decoration: const InputDecoration( - labelText: 'Email / Username', - border: OutlineInputBorder(), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 48.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 64), + Text( + 'Mie Mapan\nMembresia', // Editorial high-end entry + style: Theme.of(context).textTheme.displayMedium?.copyWith( + color: AppTheme.primary, + ), ), - ), - const SizedBox(height: 12), - TextField( - controller: _passwordController, - decoration: const InputDecoration( - labelText: 'Password', - border: OutlineInputBorder(), + const SizedBox(height: 12), + Text( + 'Sign in to access your culinary loyalty tier and discover exclusive offers.', + style: Theme.of(context).textTheme.bodyLarge, ), - obscureText: true, - ), - const SizedBox(height: 24), - SizedBox( - height: 50, - child: _isLoading - ? const Center(child: CircularProgressIndicator()) - : ElevatedButton( - onPressed: _login, - child: const Text('Login', style: TextStyle(fontSize: 18)), - ), - ), - ], + const SizedBox(height: 56), + TextField( + controller: _usernameController, + decoration: const InputDecoration(labelText: 'Email / Username'), + ), + const SizedBox(height: 20), + TextField( + controller: _passwordController, + decoration: const InputDecoration(labelText: 'Password'), + obscureText: true, + ), + const SizedBox(height: 48), + SizedBox( + height: 56, + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Container( + decoration: BoxDecoration( + gradient: AppTheme.primaryGradient, + borderRadius: BorderRadius.circular(12), + ), + child: ElevatedButton( + onPressed: _login, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + ), + child: const Text('Access Membership', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + ), + ), + ), + ], + ), ), ), ); diff --git a/lib/screens/loyalty_dashboard.dart b/lib/screens/loyalty_dashboard.dart index b307768..9af8d55 100644 --- a/lib/screens/loyalty_dashboard.dart +++ b/lib/screens/loyalty_dashboard.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import '../services/odoo_service.dart'; +import '../theme/app_theme.dart'; +import 'notifications_screen.dart'; class LoyaltyDashboard extends StatefulWidget { final int partnerId; - const LoyaltyDashboard({super.key, required this.partnerId}); @override @@ -28,10 +29,10 @@ class _LoyaltyDashboardState extends State { _isLoading = false; }); } catch (e) { - setState(() => _isLoading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error loading loyalty cards: $e')), - ); + if (mounted) { + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error loading loyalty cards: $e'))); + } } } @@ -39,72 +40,99 @@ class _LoyaltyDashboardState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('My Loyalty Programs'), + title: const Text('My Rewards'), actions: [ IconButton( - icon: const Icon(Icons.refresh), - onPressed: () { - setState(() => _isLoading = true); - _fetchLoyaltyData(); - }, - ) + icon: const Icon(Icons.notifications_outlined), + onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const NotificationsScreen())), + ), + const SizedBox(width: 8), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) - : _loyaltyCards.isEmpty - ? const Center( - child: Text( - 'No loyalty cards found.', - style: TextStyle(fontSize: 18), - ), - ) - : ListView.builder( - padding: const EdgeInsets.all(12), - itemCount: _loyaltyCards.length, - itemBuilder: (context, index) { - final card = _loyaltyCards[index]; - return Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + : RefreshIndicator( + onRefresh: _fetchLoyaltyData, + child: _loyaltyCards.isEmpty + ? Center( + child: Text( + 'No active rewards yet.', + style: Theme.of(context).textTheme.titleLarge, ), - margin: const EdgeInsets.symmetric(vertical: 8.0), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${card['program_id']?[1] ?? 'Loyalty Program'}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text('Code: ${card['code'] ?? 'N/A'}'), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Points Available:'), - Text( - '${card['points'] ?? 0} pts', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - color: Colors.green, + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0), + itemCount: _loyaltyCards.length, + itemBuilder: (context, index) { + final card = _loyaltyCards[index]; + return Container( + margin: const EdgeInsets.only(bottom: 32), + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: AppTheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: AppTheme.onSurface.withOpacity(0.06), + blurRadius: 24, + offset: const Offset(0, 8), + ) + ] + ), + 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: BoxDecoration( + color: AppTheme.secondaryContainer, + borderRadius: BorderRadius.circular(24), + ), + child: Text( + 'Gold 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/notifications_screen.dart b/lib/screens/notifications_screen.dart index 04805a9..3d3f97f 100644 --- a/lib/screens/notifications_screen.dart +++ b/lib/screens/notifications_screen.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:odoo_rpc/odoo_rpc.dart'; import '../services/odoo_service.dart'; +import '../theme/app_theme.dart'; class NotificationsScreen extends StatefulWidget { const NotificationsScreen({super.key}); @@ -42,9 +44,7 @@ class _NotificationsScreenState extends State { } catch (e) { if (mounted) { setState(() => _isLoading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error loading notifications: $e')), - ); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error loading notifications: $e'))); } } } @@ -52,23 +52,49 @@ class _NotificationsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Promo Notifications')), + appBar: AppBar(title: const Text('Notifications')), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _notifications.isEmpty - ? const Center(child: Text('No new notifications.', style: TextStyle(fontSize: 16))) + ? const Center(child: Text('No new promos.', style: TextStyle(fontSize: 16))) : ListView.builder( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), itemCount: _notifications.length, itemBuilder: (context, index) { final notif = _notifications[index]; - return Card( + 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( - leading: const Icon(Icons.campaign, color: Colors.amber, size: 36), - title: Text(notif['title'] ?? 'Notice', style: const TextStyle(fontWeight: FontWeight.bold)), + contentPadding: const EdgeInsets.all(16), + leading: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppTheme.primaryContainer.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon(Icons.star, color: AppTheme.primary), + ), + title: Text( + notif['title'] ?? 'Notice', + style: Theme.of(context).textTheme.titleMedium, + ), subtitle: Padding( padding: const EdgeInsets.only(top: 8.0), - child: Text(notif['body'] ?? ''), + child: Text( + notif['body'] ?? '', + style: Theme.of(context).textTheme.bodyMedium, + ), ), ), ); diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart new file mode 100644 index 0000000..3225da3 --- /dev/null +++ b/lib/theme/app_theme.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AppTheme { + // Mapan Core Tokens + static const Color primary = Color(0xFFB20000); + static const Color primaryContainer = Color(0xFFE00101); + static const Color secondary = Color(0xFF825500); + static const Color secondaryContainer = Color(0xFFFEB23D); + static const Color onSecondaryContainer = Color(0xFF6E4700); + + // Surface Hierarchy + static const Color surface = Color(0xFFFCF9F8); + static const Color surfaceContainer = Color(0xFFF0EDED); + static const Color surfaceContainerLow = Color(0xFFF6F3F2); + static const Color surfaceContainerHighest = Color(0xFFE5E2E1); + + // Text & On-Colors + static const Color onSurface = Color(0xFF1C1B1B); + static const Color onSurfaceVariant = Color(0xFF5E3F3A); + static const Color onPrimary = Colors.white; + + /// The Signature 135-degree CTA Gradient for main buttons. + static const LinearGradient primaryGradient = LinearGradient( + colors: [primary, primaryContainer], + begin: Alignment.topLeft, + end: Alignment.bottomRight, // Approximation of 135 degrees + ); + + static ThemeData get lightTheme { + final baseTheme = ThemeData.light(); + + return ThemeData( + useMaterial3: true, + scaffoldBackgroundColor: surface, + colorScheme: const ColorScheme.light( + primary: primary, + primaryContainer: primaryContainer, + secondary: secondary, + secondaryContainer: secondaryContainer, + onSecondaryContainer: onSecondaryContainer, + surface: surface, + onSurface: onSurface, + onSurfaceVariant: onSurfaceVariant, + onPrimary: onPrimary, + error: Color(0xFFBA1A1A), + ), + textTheme: baseTheme.textTheme.copyWith( + displayLarge: GoogleFonts.plusJakartaSans(color: onSurface, fontWeight: FontWeight.bold, letterSpacing: -0.5), + displayMedium: GoogleFonts.plusJakartaSans(color: onSurface, fontWeight: FontWeight.bold), + displaySmall: GoogleFonts.plusJakartaSans(color: onSurface, fontWeight: FontWeight.bold), + headlineMedium: GoogleFonts.plusJakartaSans(color: onSurface, fontWeight: FontWeight.bold), + titleLarge: GoogleFonts.beVietnamPro(color: onSurface, fontWeight: FontWeight.w600), + titleMedium: GoogleFonts.beVietnamPro(color: onSurface, fontWeight: FontWeight.w600), + titleSmall: GoogleFonts.beVietnamPro(color: onSurface, fontWeight: FontWeight.w500), + bodyLarge: GoogleFonts.beVietnamPro(color: onSurfaceVariant), + bodyMedium: GoogleFonts.beVietnamPro(color: onSurfaceVariant), + bodySmall: GoogleFonts.beVietnamPro(color: onSurfaceVariant), + labelLarge: GoogleFonts.beVietnamPro(color: onSurfaceVariant), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + foregroundColor: onPrimary, + backgroundColor: primaryContainer, // Fallback if no gradient is used + elevation: 0, + ), + ), + cardTheme: CardThemeData( + color: surfaceContainerLow, + elevation: 0, // Depth created via tonal shifts + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + margin: EdgeInsets.zero, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: surfaceContainerHighest, + // Using "Ghost Border" logic at 15% opacity + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0x26946E68)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0x26946E68)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: primary.withOpacity(0.4), width: 2), + ), + labelStyle: const TextStyle(color: onSurfaceVariant), + ), + appBarTheme: AppBarTheme( + backgroundColor: surface, + foregroundColor: onSurface, + elevation: 0, + surfaceTintColor: Colors.transparent, + titleTextStyle: GoogleFonts.plusJakartaSans( + color: onSurface, + fontSize: 20, + fontWeight: FontWeight.bold + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 8ab47f5..4ec59f8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -160,6 +168,30 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: db9df7a5898d894eeda4c78143f35c30a243558be439518972366880b80bf88e + url: "https://pub.dev" + source: hosted + version: "8.0.2" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" http: dependency: transitive description: @@ -208,6 +240,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -232,6 +272,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" odoo_rpc: dependency: "direct main" description: @@ -248,6 +304,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -296,6 +376,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" shared_preferences: dependency: "direct main" description: @@ -493,6 +581,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.11.3 <4.0.0" - flutter: ">=3.38.1" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 334eb91..cb4bcde 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: workmanager: ^0.9.0+3 flutter_local_notifications: ^21.0.0 shared_preferences: ^2.5.4 + google_fonts: ^8.0.2 dev_dependencies: flutter_test: