progress in dashboard of user

This commit is contained in:
Hesabix 2025-09-16 00:44:44 +03:30
parent cae7b40378
commit e335d6e915
12 changed files with 331 additions and 51 deletions

View file

@ -48,5 +48,11 @@
, ,
"ok": "OK", "ok": "OK",
"cancel": "Cancel" "cancel": "Cancel"
,
"newBusiness": "New business",
"businesses": "Businesses",
"support": "Support",
"changePassword": "Change password",
"marketing": "Marketing"
} }

View file

@ -46,6 +46,11 @@
"logoutConfirmMessage": "آیا برای خروج مطمئن هستید؟", "logoutConfirmMessage": "آیا برای خروج مطمئن هستید؟",
"menu": "منو" "menu": "منو"
, ,
"newBusiness": "کسب‌وکار جدید",
"businesses": "کسب‌وکارها",
"support": "پشتیبانی",
"changePassword": "تغییر کلمه عبور",
"marketing": "بازاریابی",
"ok": "تایید", "ok": "تایید",
"cancel": "انصراف" "cancel": "انصراف"
} }

View file

@ -339,10 +339,46 @@ abstract class AppLocalizations {
String get menu; String get menu;
/// No description provided for @ok. /// No description provided for @ok.
///
/// In en, this message translates to:
/// **'OK'**
String get ok; String get ok;
/// No description provided for @cancel. /// No description provided for @cancel.
///
/// In en, this message translates to:
/// **'Cancel'**
String get cancel; String get cancel;
/// No description provided for @newBusiness.
///
/// In en, this message translates to:
/// **'New business'**
String get newBusiness;
/// No description provided for @businesses.
///
/// In en, this message translates to:
/// **'Businesses'**
String get businesses;
/// No description provided for @support.
///
/// In en, this message translates to:
/// **'Support'**
String get support;
/// No description provided for @changePassword.
///
/// In en, this message translates to:
/// **'Change password'**
String get changePassword;
/// No description provided for @marketing.
///
/// In en, this message translates to:
/// **'Marketing'**
String get marketing;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -135,4 +135,19 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get cancel => 'Cancel'; String get cancel => 'Cancel';
@override
String get newBusiness => 'New business';
@override
String get businesses => 'Businesses';
@override
String get support => 'Support';
@override
String get changePassword => 'Change password';
@override
String get marketing => 'Marketing';
} }

View file

@ -135,4 +135,19 @@ class AppLocalizationsFa extends AppLocalizations {
@override @override
String get cancel => 'انصراف'; String get cancel => 'انصراف';
@override
String get newBusiness => 'کسب‌وکار جدید';
@override
String get businesses => 'کسب‌وکارها';
@override
String get support => 'پشتیبانی';
@override
String get changePassword => 'تغییر کلمه عبور';
@override
String get marketing => 'بازاریابی';
} }

View file

@ -6,6 +6,11 @@ import 'pages/login_page.dart';
import 'pages/home_page.dart'; import 'pages/home_page.dart';
import 'pages/profile/profile_shell.dart'; import 'pages/profile/profile_shell.dart';
import 'pages/profile/profile_dashboard_page.dart'; import 'pages/profile/profile_dashboard_page.dart';
import 'pages/profile/new_business_page.dart';
import 'pages/profile/businesses_page.dart';
import 'pages/profile/support_page.dart';
import 'pages/profile/change_password_page.dart';
import 'pages/profile/marketing_page.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart';
import 'core/locale_controller.dart'; import 'core/locale_controller.dart';
import 'core/api_client.dart'; import 'core/api_client.dart';
@ -113,6 +118,31 @@ class _MyAppState extends State<MyApp> {
name: 'profile_dashboard', name: 'profile_dashboard',
builder: (context, state) => const ProfileDashboardPage(), builder: (context, state) => const ProfileDashboardPage(),
), ),
GoRoute(
path: '/user/profile/new-business',
name: 'profile_new_business',
builder: (context, state) => const NewBusinessPage(),
),
GoRoute(
path: '/user/profile/businesses',
name: 'profile_businesses',
builder: (context, state) => const BusinessesPage(),
),
GoRoute(
path: '/user/profile/support',
name: 'profile_support',
builder: (context, state) => const SupportPage(),
),
GoRoute(
path: '/user/profile/marketing',
name: 'profile_marketing',
builder: (context, state) => const MarketingPage(),
),
GoRoute(
path: '/user/profile/change-password',
name: 'profile_change_password',
builder: (context, state) => const ChangePasswordPage(),
),
], ],
), ),
], ],

View file

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
class BusinessesPage extends StatelessWidget {
const BusinessesPage({super.key});
@override
Widget build(BuildContext context) {
final t = AppLocalizations.of(context);
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t.businesses, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text('${t.businesses} - sample page'),
],
),
);
}
}

View file

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
class ChangePasswordPage extends StatelessWidget {
const ChangePasswordPage({super.key});
@override
Widget build(BuildContext context) {
final t = AppLocalizations.of(context);
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t.changePassword, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text('${t.changePassword} - sample page'),
],
),
);
}
}

View file

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
class MarketingPage extends StatelessWidget {
const MarketingPage({super.key});
@override
Widget build(BuildContext context) {
final t = AppLocalizations.of(context);
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t.marketing, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text('${t.marketing} - sample page'),
],
),
);
}
}

View file

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
class NewBusinessPage extends StatelessWidget {
const NewBusinessPage({super.key});
@override
Widget build(BuildContext context) {
final t = AppLocalizations.of(context);
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t.newBusiness, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text('${t.newBusiness} - sample page'),
],
),
);
}
}

View file

@ -8,13 +8,20 @@ import '../../widgets/theme_mode_switcher.dart';
import '../../widgets/logout_button.dart'; import '../../widgets/logout_button.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart'; import 'package:hesabix_ui/l10n/app_localizations.dart';
class ProfileShell extends StatelessWidget { class ProfileShell extends StatefulWidget {
final Widget child; final Widget child;
final AuthStore authStore; final AuthStore authStore;
final LocaleController? localeController; final LocaleController? localeController;
final ThemeController? themeController; final ThemeController? themeController;
const ProfileShell({super.key, required this.child, required this.authStore, this.localeController, this.themeController}); const ProfileShell({super.key, required this.child, required this.authStore, this.localeController, this.themeController});
@override
State<ProfileShell> createState() => _ProfileShellState();
}
class _ProfileShellState extends State<ProfileShell> {
int _hoverIndex = -1;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
@ -22,10 +29,19 @@ class ProfileShell extends StatelessWidget {
final bool railExtended = width >= 1100; final bool railExtended = width >= 1100;
final ColorScheme scheme = Theme.of(context).colorScheme; final ColorScheme scheme = Theme.of(context).colorScheme;
final String location = GoRouterState.of(context).uri.toString(); final String location = GoRouterState.of(context).uri.toString();
final bool isDark = Theme.of(context).brightness == Brightness.dark;
final String logoAsset = isDark
? 'assets/images/logo-light.png'
: 'assets/images/logo-light.png';
final t = AppLocalizations.of(context); final t = AppLocalizations.of(context);
final destinations = <_Dest>[ final destinations = <_Dest>[
_Dest(t.dashboard, Icons.dashboard_outlined, Icons.dashboard, '/user/profile/dashboard'), _Dest(t.dashboard, Icons.dashboard_outlined, Icons.dashboard, '/user/profile/dashboard'),
_Dest(t.newBusiness, Icons.add_business, Icons.add_business, '/user/profile/new-business'),
_Dest(t.businesses, Icons.business, Icons.business, '/user/profile/businesses'),
_Dest(t.support, Icons.support_agent, Icons.support_agent, '/user/profile/support'),
_Dest(t.marketing, Icons.campaign, Icons.campaign, '/user/profile/marketing'),
_Dest(t.changePassword, Icons.password, Icons.password, '/user/profile/change-password'),
]; ];
int selectedIndex = 0; int selectedIndex = 0;
@ -44,7 +60,7 @@ class ProfileShell extends StatelessWidget {
} }
Future<void> onLogout() async { Future<void> onLogout() async {
await authStore.saveApiKey(null); await widget.authStore.saveApiKey(null);
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
..hideCurrentSnackBar() ..hideCurrentSnackBar()
@ -67,12 +83,9 @@ class ProfileShell extends StatelessWidget {
title: Row( title: Row(
children: [ children: [
const SizedBox(width: 12), const SizedBox(width: 12),
// Logo placeholder (can replace with AssetImage) Image.asset(logoAsset, height: 28),
CircleAvatar(backgroundColor: appBarFg.withOpacity(0.15), child: Icon(Icons.account_balance, color: appBarFg)),
const SizedBox(width: 12), const SizedBox(width: 12),
Text(t.appTitle, style: TextStyle(color: appBarFg, fontWeight: FontWeight.w700)), Text(t.appTitle, style: TextStyle(color: appBarFg, fontWeight: FontWeight.w700)),
const SizedBox(width: 8),
Text(destinations[selectedIndex].label, style: TextStyle(color: appBarFg.withOpacity(0.85))),
], ],
), ),
leading: useRail leading: useRail
@ -85,15 +98,15 @@ class ProfileShell extends StatelessWidget {
), ),
), ),
actions: [ actions: [
if (themeController != null) ...[ if (widget.themeController != null) ...[
ThemeModeSwitcher(controller: themeController!), ThemeModeSwitcher(controller: widget.themeController!),
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
if (localeController != null) ...[ if (widget.localeController != null) ...[
LanguageSwitcher(controller: localeController!), LanguageSwitcher(controller: widget.localeController!),
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
LogoutButton(authStore: authStore), LogoutButton(authStore: widget.authStore),
], ],
); );
@ -102,35 +115,75 @@ class ProfileShell extends StatelessWidget {
child: SafeArea( child: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: child, child: widget.child,
), ),
), ),
); );
// Side colors and styles
final Color sideBg = Theme.of(context).brightness == Brightness.dark
? scheme.surfaceContainerHighest
: scheme.surfaceContainerLow;
final Color sideFg = scheme.onSurfaceVariant;
final Color activeBg = scheme.primaryContainer;
final Color activeFg = scheme.onPrimaryContainer;
if (useRail) { if (useRail) {
return Scaffold( return Scaffold(
appBar: appBar, appBar: appBar,
body: Row( body: Row(
children: [ children: [
NavigationRail( Container(
selectedIndex: selectedIndex, width: railExtended ? 240 : 88,
extended: railExtended, height: double.infinity,
leading: Padding( color: sideBg,
padding: const EdgeInsets.symmetric(vertical: 12), child: ListView.builder(
child: CircleAvatar( padding: EdgeInsets.zero,
backgroundColor: scheme.primary, itemCount: destinations.length,
child: const Icon(Icons.person, color: Colors.white), itemBuilder: (ctx, i) {
), final d = destinations[i];
final bool isHovered = i == _hoverIndex;
final bool isSelected = i == selectedIndex;
final bool active = isSelected || isHovered;
final double radius = isHovered ? 0 : 8;
return MouseRegion(
onEnter: (_) => setState(() => _hoverIndex = i),
onExit: (_) => setState(() => _hoverIndex = -1),
child: InkWell(
borderRadius: BorderRadius.circular(radius),
onTap: () => onSelect(i),
child: Container(
margin: EdgeInsets.zero,
padding: EdgeInsets.symmetric(horizontal: railExtended ? 12 : 0, vertical: 10),
decoration: BoxDecoration(
color: active ? activeBg : Colors.transparent,
borderRadius: BorderRadius.circular(radius),
),
child: Row(
mainAxisAlignment: railExtended ? MainAxisAlignment.start : MainAxisAlignment.center,
children: [
Icon(d.icon, color: active ? activeFg : sideFg),
if (railExtended) ...[
const SizedBox(width: 12),
Expanded(
child: Text(
d.label,
style: TextStyle(
color: active ? activeFg : sideFg,
fontWeight: active ? FontWeight.w600 : FontWeight.w400,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
],
),
),
),
);
},
), ),
destinations: [
for (final d in destinations)
NavigationRailDestination(
icon: Icon(d.icon),
selectedIcon: Icon(d.selectedIcon),
label: Text(d.label),
),
],
onDestinationSelected: onSelect,
), ),
const VerticalDivider(width: 1), const VerticalDivider(width: 1),
Expanded(child: content), Expanded(child: content),
@ -142,31 +195,31 @@ class ProfileShell extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: appBar, appBar: appBar,
drawer: Drawer( drawer: Drawer(
backgroundColor: sideBg,
child: SafeArea( child: SafeArea(
child: Column( child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [ children: [
UserAccountsDrawerHeader( for (int i = 0; i < destinations.length; i++) ...[
currentAccountPicture: CircleAvatar( Builder(builder: (ctx) {
backgroundColor: scheme.primary, final d = destinations[i];
child: const Icon(Icons.person, color: Colors.white), final bool active = i == selectedIndex;
), return ListTile(
accountName: const Text(''), leading: Icon(d.selectedIcon, color: active ? activeFg : sideFg),
accountEmail: const Text(''), title: Text(d.label, style: TextStyle(color: active ? activeFg : sideFg, fontWeight: active ? FontWeight.w600 : FontWeight.w400)),
), selected: active,
for (int i = 0; i < destinations.length; i++) selectedTileColor: activeBg,
ListTile( onTap: () {
leading: Icon(destinations[i].selectedIcon), Navigator.of(context).pop();
title: Text(destinations[i].label), onSelect(i);
selected: i == selectedIndex, },
onTap: () { );
Navigator.of(context).pop(); }),
onSelect(i); ],
}, const Divider(),
),
const Spacer(),
ListTile( ListTile(
leading: const Icon(Icons.logout), leading: const Icon(Icons.logout),
title: const Text('خروج'), title: Text(t.logout),
onTap: onLogout, onTap: onLogout,
), ),
], ],

View file

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
class SupportPage extends StatelessWidget {
const SupportPage({super.key});
@override
Widget build(BuildContext context) {
final t = AppLocalizations.of(context);
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t.support, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text('${t.support} - sample page'),
],
),
);
}
}