2025-09-22 21:21:46 +03:30
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
|
import '../../core/auth_store.dart';
|
2025-09-25 01:01:27 +03:30
|
|
|
import '../../core/locale_controller.dart';
|
2025-09-22 21:21:46 +03:30
|
|
|
import '../../core/calendar_controller.dart';
|
2025-09-25 01:01:27 +03:30
|
|
|
import '../../theme/theme_controller.dart';
|
|
|
|
|
import '../../widgets/settings_menu_button.dart';
|
|
|
|
|
import '../../widgets/user_account_menu_button.dart';
|
|
|
|
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
2025-09-22 21:21:46 +03:30
|
|
|
|
|
|
|
|
class BusinessShell extends StatefulWidget {
|
|
|
|
|
final int businessId;
|
|
|
|
|
final Widget child;
|
2025-09-25 01:01:27 +03:30
|
|
|
final AuthStore authStore;
|
|
|
|
|
final LocaleController? localeController;
|
|
|
|
|
final CalendarController? calendarController;
|
|
|
|
|
final ThemeController? themeController;
|
2025-09-22 21:21:46 +03:30
|
|
|
|
|
|
|
|
const BusinessShell({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.businessId,
|
|
|
|
|
required this.child,
|
2025-09-25 01:01:27 +03:30
|
|
|
required this.authStore,
|
|
|
|
|
this.localeController,
|
|
|
|
|
this.calendarController,
|
|
|
|
|
this.themeController,
|
2025-09-22 21:21:46 +03:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<BusinessShell> createState() => _BusinessShellState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _BusinessShellState extends State<BusinessShell> {
|
2025-09-25 01:01:27 +03:30
|
|
|
int _hoverIndex = -1;
|
|
|
|
|
bool _isBasicToolsExpanded = false;
|
|
|
|
|
bool _isPeopleExpanded = false;
|
2025-09-22 21:21:46 +03:30
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
// اضافه کردن listener برای AuthStore
|
|
|
|
|
widget.authStore.addListener(() {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
setState(() {});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final width = MediaQuery.of(context).size.width;
|
|
|
|
|
final bool useRail = width >= 700;
|
|
|
|
|
final bool railExtended = width >= 1100;
|
|
|
|
|
final ColorScheme scheme = Theme.of(context).colorScheme;
|
2025-09-25 01:01:27 +03:30
|
|
|
String location = '/business/${widget.businessId}/dashboard'; // default location
|
|
|
|
|
try {
|
|
|
|
|
location = GoRouterState.of(context).uri.toString();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// اگر GoRouterState در دسترس نیست، از default استفاده کن
|
|
|
|
|
}
|
2025-09-22 21:21:46 +03:30
|
|
|
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);
|
2025-09-25 01:01:27 +03:30
|
|
|
|
|
|
|
|
// ساختار متمرکز منو
|
|
|
|
|
final menuItems = <_MenuItem>[
|
|
|
|
|
_MenuItem(
|
|
|
|
|
label: t.businessDashboard,
|
|
|
|
|
icon: Icons.dashboard_outlined,
|
|
|
|
|
selectedIcon: Icons.dashboard,
|
|
|
|
|
path: '/business/${widget.businessId}/dashboard',
|
|
|
|
|
type: _MenuItemType.simple,
|
|
|
|
|
),
|
|
|
|
|
_MenuItem(
|
|
|
|
|
label: t.practicalTools,
|
|
|
|
|
icon: Icons.category,
|
|
|
|
|
selectedIcon: Icons.category,
|
|
|
|
|
path: null, // آیتم جداکننده
|
|
|
|
|
type: _MenuItemType.separator,
|
|
|
|
|
),
|
|
|
|
|
_MenuItem(
|
|
|
|
|
label: t.people,
|
|
|
|
|
icon: Icons.people,
|
|
|
|
|
selectedIcon: Icons.people,
|
|
|
|
|
path: null, // برای منوی بازشونده
|
|
|
|
|
type: _MenuItemType.expandable,
|
|
|
|
|
children: [
|
|
|
|
|
_MenuItem(
|
|
|
|
|
label: t.peopleList,
|
|
|
|
|
icon: Icons.list,
|
|
|
|
|
selectedIcon: Icons.list,
|
|
|
|
|
path: '/business/${widget.businessId}/people-list',
|
|
|
|
|
type: _MenuItemType.simple,
|
|
|
|
|
),
|
|
|
|
|
_MenuItem(
|
|
|
|
|
label: t.receipts,
|
|
|
|
|
icon: Icons.receipt,
|
|
|
|
|
selectedIcon: Icons.receipt,
|
|
|
|
|
path: '/business/${widget.businessId}/receipts',
|
|
|
|
|
type: _MenuItemType.simple,
|
|
|
|
|
hasAddButton: true,
|
|
|
|
|
),
|
|
|
|
|
_MenuItem(
|
|
|
|
|
label: t.payments,
|
|
|
|
|
icon: Icons.payment,
|
|
|
|
|
selectedIcon: Icons.payment,
|
|
|
|
|
path: '/business/${widget.businessId}/payments',
|
|
|
|
|
type: _MenuItemType.simple,
|
|
|
|
|
hasAddButton: true,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
_MenuItem(
|
|
|
|
|
label: t.settings,
|
|
|
|
|
icon: Icons.settings,
|
|
|
|
|
selectedIcon: Icons.settings,
|
|
|
|
|
path: null, // برای منوی بازشونده
|
|
|
|
|
type: _MenuItemType.expandable,
|
|
|
|
|
children: [
|
|
|
|
|
_MenuItem(
|
|
|
|
|
label: t.businessSettings,
|
|
|
|
|
icon: Icons.business,
|
|
|
|
|
selectedIcon: Icons.business,
|
|
|
|
|
path: '/business/${widget.businessId}/business-settings',
|
|
|
|
|
type: _MenuItemType.simple,
|
|
|
|
|
),
|
|
|
|
|
_MenuItem(
|
|
|
|
|
label: t.printDocuments,
|
|
|
|
|
icon: Icons.print,
|
|
|
|
|
selectedIcon: Icons.print,
|
|
|
|
|
path: '/business/${widget.businessId}/print-documents',
|
|
|
|
|
type: _MenuItemType.simple,
|
|
|
|
|
),
|
|
|
|
|
_MenuItem(
|
|
|
|
|
label: t.usersAndPermissions,
|
|
|
|
|
icon: Icons.people_outline,
|
|
|
|
|
selectedIcon: Icons.people,
|
|
|
|
|
path: '/business/${widget.businessId}/users-permissions',
|
|
|
|
|
type: _MenuItemType.simple,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2025-09-22 21:21:46 +03:30
|
|
|
];
|
|
|
|
|
|
|
|
|
|
int selectedIndex = 0;
|
2025-09-25 01:01:27 +03:30
|
|
|
for (int i = 0; i < menuItems.length; i++) {
|
|
|
|
|
final item = menuItems[i];
|
|
|
|
|
if (item.type == _MenuItemType.separator) continue; // نادیده گرفتن آیتم جداکننده
|
|
|
|
|
|
|
|
|
|
if (item.type == _MenuItemType.simple && item.path != null && location.startsWith(item.path!)) {
|
2025-09-22 21:21:46 +03:30
|
|
|
selectedIndex = i;
|
|
|
|
|
break;
|
2025-09-25 01:01:27 +03:30
|
|
|
} else if (item.type == _MenuItemType.expandable && item.children != null) {
|
|
|
|
|
for (int j = 0; j < item.children!.length; j++) {
|
|
|
|
|
final child = item.children![j];
|
|
|
|
|
if (child.path != null && location.startsWith(child.path!)) {
|
|
|
|
|
selectedIndex = i;
|
|
|
|
|
// تنظیم وضعیت باز بودن منو
|
|
|
|
|
if (i == 2) _isPeopleExpanded = true; // اشخاص در ایندکس 2
|
|
|
|
|
if (i == 3) _isBasicToolsExpanded = true; // تنظیمات در ایندکس 3
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-22 21:21:46 +03:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> onSelect(int index) async {
|
2025-09-25 01:01:27 +03:30
|
|
|
final item = menuItems[index];
|
|
|
|
|
if (item.type == _MenuItemType.separator) return; // آیتم جداکننده قابل کلیک نیست
|
|
|
|
|
|
|
|
|
|
if (item.type == _MenuItemType.simple && item.path != null) {
|
|
|
|
|
try {
|
|
|
|
|
if (GoRouterState.of(context).uri.toString() != item.path!) {
|
|
|
|
|
context.go(item.path!);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// اگر GoRouterState در دسترس نیست، مستقیماً به مسیر برود
|
|
|
|
|
context.go(item.path!);
|
|
|
|
|
}
|
|
|
|
|
} else if (item.type == _MenuItemType.expandable) {
|
|
|
|
|
// تغییر وضعیت باز/بسته بودن منو
|
|
|
|
|
if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded;
|
|
|
|
|
if (item.label == t.settings) _isBasicToolsExpanded = !_isBasicToolsExpanded;
|
|
|
|
|
setState(() {});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> onSelectChild(int parentIndex, int childIndex) async {
|
|
|
|
|
final parent = menuItems[parentIndex];
|
|
|
|
|
if (parent.type == _MenuItemType.expandable && parent.children != null) {
|
|
|
|
|
final child = parent.children![childIndex];
|
|
|
|
|
if (child.path != null) {
|
|
|
|
|
try {
|
|
|
|
|
if (GoRouterState.of(context).uri.toString() != child.path!) {
|
|
|
|
|
context.go(child.path!);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// اگر GoRouterState در دسترس نیست، مستقیماً به مسیر برود
|
|
|
|
|
context.go(child.path!);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> onLogout() async {
|
|
|
|
|
await widget.authStore.saveApiKey(null);
|
|
|
|
|
if (!context.mounted) return;
|
|
|
|
|
ScaffoldMessenger.of(context)
|
|
|
|
|
..hideCurrentSnackBar()
|
|
|
|
|
..showSnackBar(const SnackBar(content: Text('خروج انجام شد')));
|
|
|
|
|
context.go('/login');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isExpanded(_MenuItem item) {
|
|
|
|
|
if (item.label == t.people) return _isPeopleExpanded;
|
|
|
|
|
if (item.label == t.settings) return _isBasicToolsExpanded;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int getTotalMenuItemsCount() {
|
|
|
|
|
int count = 0;
|
|
|
|
|
for (final item in menuItems) {
|
|
|
|
|
if (item.type == _MenuItemType.separator) {
|
|
|
|
|
count++; // آیتم جداکننده هم شمرده میشود
|
|
|
|
|
} else {
|
|
|
|
|
count++; // آیتم اصلی
|
|
|
|
|
if (item.type == _MenuItemType.expandable && isExpanded(item) && railExtended) {
|
|
|
|
|
count += item.children?.length ?? 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-22 21:21:46 +03:30
|
|
|
}
|
2025-09-25 01:01:27 +03:30
|
|
|
return count;
|
2025-09-22 21:21:46 +03:30
|
|
|
}
|
|
|
|
|
|
2025-09-25 01:01:27 +03:30
|
|
|
int getMenuIndexFromTotalIndex(int totalIndex) {
|
|
|
|
|
int currentIndex = 0;
|
|
|
|
|
for (int i = 0; i < menuItems.length; i++) {
|
|
|
|
|
if (currentIndex == totalIndex) return i;
|
|
|
|
|
currentIndex++;
|
|
|
|
|
|
|
|
|
|
final item = menuItems[i];
|
|
|
|
|
if (item.type == _MenuItemType.expandable && isExpanded(item) && railExtended) {
|
|
|
|
|
final childrenCount = item.children?.length ?? 0;
|
|
|
|
|
if (totalIndex >= currentIndex && totalIndex < currentIndex + childrenCount) {
|
|
|
|
|
return i;
|
|
|
|
|
}
|
|
|
|
|
currentIndex += childrenCount;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
2025-09-22 21:21:46 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Brand top bar with contrast color
|
|
|
|
|
final Color appBarBg = Theme.of(context).brightness == Brightness.dark
|
|
|
|
|
? scheme.surfaceContainerHighest
|
|
|
|
|
: scheme.primary;
|
|
|
|
|
final Color appBarFg = Theme.of(context).brightness == Brightness.dark
|
|
|
|
|
? scheme.onSurfaceVariant
|
|
|
|
|
: scheme.onPrimary;
|
|
|
|
|
|
|
|
|
|
final appBar = AppBar(
|
|
|
|
|
backgroundColor: appBarBg,
|
|
|
|
|
foregroundColor: appBarFg,
|
2025-09-25 01:01:27 +03:30
|
|
|
automaticallyImplyLeading: !useRail,
|
|
|
|
|
titleSpacing: 0,
|
2025-09-22 21:21:46 +03:30
|
|
|
title: Row(
|
|
|
|
|
children: [
|
|
|
|
|
const SizedBox(width: 12),
|
2025-09-25 01:01:27 +03:30
|
|
|
Image.asset(logoAsset, height: 28),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Text(t.appTitle, style: TextStyle(color: appBarFg, fontWeight: FontWeight.w700)),
|
2025-09-22 21:21:46 +03:30
|
|
|
],
|
|
|
|
|
),
|
2025-09-25 01:01:27 +03:30
|
|
|
leading: useRail
|
|
|
|
|
? null
|
|
|
|
|
: Builder(
|
|
|
|
|
builder: (ctx) => IconButton(
|
|
|
|
|
icon: Icon(Icons.menu, color: appBarFg),
|
|
|
|
|
onPressed: () => Scaffold.of(ctx).openDrawer(),
|
|
|
|
|
tooltip: t.menu,
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-09-22 21:21:46 +03:30
|
|
|
actions: [
|
2025-09-25 01:01:27 +03:30
|
|
|
SettingsMenuButton(
|
|
|
|
|
localeController: widget.localeController,
|
|
|
|
|
calendarController: widget.calendarController,
|
|
|
|
|
themeController: widget.themeController,
|
2025-09-22 21:21:46 +03:30
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8),
|
2025-09-25 01:01:27 +03:30
|
|
|
UserAccountMenuButton(authStore: widget.authStore),
|
|
|
|
|
const SizedBox(width: 8),
|
2025-09-22 21:21:46 +03:30
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
2025-09-25 01:01:27 +03:30
|
|
|
final content = Container(
|
|
|
|
|
color: scheme.surface,
|
|
|
|
|
child: SafeArea(
|
|
|
|
|
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;
|
|
|
|
|
|
2025-09-22 21:21:46 +03:30
|
|
|
if (useRail) {
|
|
|
|
|
return Scaffold(
|
|
|
|
|
appBar: appBar,
|
|
|
|
|
body: Row(
|
|
|
|
|
children: [
|
2025-09-25 01:01:27 +03:30
|
|
|
Container(
|
|
|
|
|
width: railExtended ? 240 : 88,
|
|
|
|
|
height: double.infinity,
|
|
|
|
|
color: sideBg,
|
|
|
|
|
child: ListView.builder(
|
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
|
itemCount: getTotalMenuItemsCount(),
|
|
|
|
|
itemBuilder: (ctx, index) {
|
|
|
|
|
final menuIndex = getMenuIndexFromTotalIndex(index);
|
|
|
|
|
final item = menuItems[menuIndex];
|
|
|
|
|
final bool isHovered = index == _hoverIndex;
|
|
|
|
|
final bool isSelected = menuIndex == selectedIndex;
|
|
|
|
|
final bool active = isSelected || isHovered;
|
|
|
|
|
final BorderRadius br = (isSelected && useRail)
|
|
|
|
|
? BorderRadius.zero
|
|
|
|
|
: (isHovered ? BorderRadius.zero : BorderRadius.circular(8));
|
|
|
|
|
final Color bgColor = active
|
|
|
|
|
? (isHovered && !isSelected ? activeBg.withValues(alpha: 0.85) : activeBg)
|
|
|
|
|
: Colors.transparent;
|
|
|
|
|
|
|
|
|
|
// اگر آیتم بازشونده است و در حالت باز است، زیرآیتمها را نمایش بده
|
|
|
|
|
if (item.type == _MenuItemType.expandable && isExpanded(item) && railExtended) {
|
|
|
|
|
if (index == getMenuIndexFromTotalIndex(index)) {
|
|
|
|
|
// آیتم اصلی
|
|
|
|
|
return MouseRegion(
|
|
|
|
|
onEnter: (_) => setState(() => _hoverIndex = index),
|
|
|
|
|
onExit: (_) => setState(() => _hoverIndex = -1),
|
|
|
|
|
child: InkWell(
|
|
|
|
|
borderRadius: br,
|
|
|
|
|
onTap: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded;
|
|
|
|
|
if (item.label == t.settings) _isBasicToolsExpanded = !_isBasicToolsExpanded;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
child: Container(
|
|
|
|
|
margin: EdgeInsets.zero,
|
|
|
|
|
padding: EdgeInsets.symmetric(
|
|
|
|
|
horizontal: railExtended ? 16 : 8,
|
|
|
|
|
vertical: 8,
|
|
|
|
|
),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: bgColor,
|
|
|
|
|
borderRadius: br,
|
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(
|
|
|
|
|
active ? item.selectedIcon : item.icon,
|
|
|
|
|
color: active ? activeFg : sideFg,
|
|
|
|
|
size: 24,
|
|
|
|
|
),
|
|
|
|
|
if (railExtended) ...[
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Text(
|
|
|
|
|
item.label,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: active ? activeFg : sideFg,
|
|
|
|
|
fontWeight: active ? FontWeight.w600 : FontWeight.w400,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Icon(
|
|
|
|
|
isExpanded(item) ? Icons.expand_less : Icons.expand_more,
|
|
|
|
|
color: sideFg,
|
|
|
|
|
size: 20,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// زیرآیتمها
|
|
|
|
|
final childIndex = index - getMenuIndexFromTotalIndex(index) - 1;
|
|
|
|
|
if (childIndex < (item.children?.length ?? 0)) {
|
|
|
|
|
final child = item.children![childIndex];
|
|
|
|
|
return MouseRegion(
|
|
|
|
|
onEnter: (_) => setState(() => _hoverIndex = index),
|
|
|
|
|
onExit: (_) => setState(() => _hoverIndex = -1),
|
|
|
|
|
child: InkWell(
|
|
|
|
|
borderRadius: br,
|
|
|
|
|
onTap: () => onSelectChild(menuIndex, childIndex),
|
|
|
|
|
child: Container(
|
|
|
|
|
margin: EdgeInsets.zero,
|
|
|
|
|
padding: EdgeInsets.symmetric(
|
|
|
|
|
horizontal: railExtended ? 24 : 16, // بیشتر indent برای زیرآیتم
|
|
|
|
|
vertical: 8,
|
|
|
|
|
),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: bgColor,
|
|
|
|
|
borderRadius: br,
|
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(
|
|
|
|
|
child.icon,
|
|
|
|
|
color: sideFg,
|
|
|
|
|
size: 20,
|
|
|
|
|
),
|
|
|
|
|
if (railExtended) ...[
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Text(
|
|
|
|
|
child.label,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: sideFg,
|
|
|
|
|
fontWeight: FontWeight.w400,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
if (child.hasAddButton)
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.add, size: 16),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
// Navigate to add new receipt/payment
|
|
|
|
|
if (child.label == t.receipts) {
|
|
|
|
|
// Navigate to add receipt
|
|
|
|
|
} else if (child.label == t.payments) {
|
|
|
|
|
// Navigate to add payment
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// آیتم ساده، آیتم بازشونده در حالت بسته، یا آیتم جداکننده
|
|
|
|
|
if (item.type == _MenuItemType.separator) {
|
|
|
|
|
// آیتم جداکننده
|
|
|
|
|
return Container(
|
|
|
|
|
margin: EdgeInsets.symmetric(
|
|
|
|
|
horizontal: railExtended ? 16 : 8,
|
|
|
|
|
vertical: 8,
|
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
if (railExtended) ...[
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Divider(
|
|
|
|
|
color: sideFg.withValues(alpha: 0.3),
|
|
|
|
|
thickness: 1,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Text(
|
|
|
|
|
item.label,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: sideFg.withValues(alpha: 0.7),
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Divider(
|
|
|
|
|
color: sideFg.withValues(alpha: 0.3),
|
|
|
|
|
thickness: 1,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
] else ...[
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Divider(
|
|
|
|
|
color: sideFg.withValues(alpha: 0.3),
|
|
|
|
|
thickness: 1,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// آیتم ساده یا آیتم بازشونده در حالت بسته
|
|
|
|
|
return MouseRegion(
|
|
|
|
|
onEnter: (_) => setState(() => _hoverIndex = index),
|
|
|
|
|
onExit: (_) => setState(() => _hoverIndex = -1),
|
|
|
|
|
child: InkWell(
|
|
|
|
|
borderRadius: br,
|
|
|
|
|
onTap: () {
|
|
|
|
|
if (item.type == _MenuItemType.expandable) {
|
|
|
|
|
setState(() {
|
|
|
|
|
if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded;
|
|
|
|
|
if (item.label == t.settings) _isBasicToolsExpanded = !_isBasicToolsExpanded;
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
onSelect(menuIndex);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
child: Container(
|
|
|
|
|
margin: EdgeInsets.zero,
|
|
|
|
|
padding: EdgeInsets.symmetric(
|
|
|
|
|
horizontal: railExtended ? 16 : 8,
|
|
|
|
|
vertical: 8,
|
|
|
|
|
),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: bgColor,
|
|
|
|
|
borderRadius: br,
|
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(
|
|
|
|
|
active ? item.selectedIcon : item.icon,
|
|
|
|
|
color: active ? activeFg : sideFg,
|
|
|
|
|
size: 24,
|
|
|
|
|
),
|
|
|
|
|
if (railExtended) ...[
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Text(
|
|
|
|
|
item.label,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: active ? activeFg : sideFg,
|
|
|
|
|
fontWeight: active ? FontWeight.w600 : FontWeight.w400,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
if (item.type == _MenuItemType.expandable)
|
|
|
|
|
Icon(
|
|
|
|
|
isExpanded(item) ? Icons.expand_less : Icons.expand_more,
|
|
|
|
|
color: sideFg,
|
|
|
|
|
size: 20,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
},
|
|
|
|
|
),
|
2025-09-22 21:21:46 +03:30
|
|
|
),
|
|
|
|
|
const VerticalDivider(thickness: 1, width: 1),
|
2025-09-25 01:01:27 +03:30
|
|
|
Expanded(child: content),
|
2025-09-22 21:21:46 +03:30
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
2025-09-25 01:01:27 +03:30
|
|
|
}
|
|
|
|
|
|
2025-09-22 21:21:46 +03:30
|
|
|
return Scaffold(
|
|
|
|
|
appBar: appBar,
|
2025-09-25 01:01:27 +03:30
|
|
|
drawer: Drawer(
|
|
|
|
|
backgroundColor: sideBg,
|
|
|
|
|
child: SafeArea(
|
|
|
|
|
child: ListView(
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
|
|
|
children: [
|
|
|
|
|
// آیتمهای منو
|
|
|
|
|
for (int i = 0; i < menuItems.length; i++) ...[
|
|
|
|
|
Builder(builder: (ctx) {
|
|
|
|
|
final item = menuItems[i];
|
|
|
|
|
final bool active = i == selectedIndex;
|
|
|
|
|
|
|
|
|
|
if (item.type == _MenuItemType.separator) {
|
|
|
|
|
// آیتم جداکننده در منوی موبایل
|
|
|
|
|
return Container(
|
|
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Divider(
|
|
|
|
|
color: sideFg.withValues(alpha: 0.3),
|
|
|
|
|
thickness: 1,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Text(
|
|
|
|
|
item.label,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: sideFg.withValues(alpha: 0.7),
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Divider(
|
|
|
|
|
color: sideFg.withValues(alpha: 0.3),
|
|
|
|
|
thickness: 1,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} else if (item.type == _MenuItemType.simple) {
|
|
|
|
|
return ListTile(
|
|
|
|
|
leading: Icon(item.selectedIcon, color: active ? activeFg : sideFg),
|
|
|
|
|
title: Text(item.label, style: TextStyle(color: active ? activeFg : sideFg, fontWeight: active ? FontWeight.w600 : FontWeight.w400)),
|
|
|
|
|
selected: active,
|
|
|
|
|
selectedTileColor: activeBg,
|
|
|
|
|
onTap: () {
|
|
|
|
|
context.pop();
|
|
|
|
|
onSelect(i);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
} else if (item.type == _MenuItemType.expandable) {
|
|
|
|
|
return ExpansionTile(
|
|
|
|
|
leading: Icon(item.icon, color: sideFg),
|
|
|
|
|
title: Text(item.label, style: TextStyle(color: sideFg)),
|
|
|
|
|
initiallyExpanded: isExpanded(item),
|
|
|
|
|
onExpansionChanged: (expanded) {
|
|
|
|
|
setState(() {
|
|
|
|
|
if (item.label == t.people) _isPeopleExpanded = expanded;
|
|
|
|
|
if (item.label == t.settings) _isBasicToolsExpanded = expanded;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
children: item.children?.map((child) => ListTile(
|
|
|
|
|
leading: const SizedBox(width: 24),
|
|
|
|
|
title: Text(child.label),
|
|
|
|
|
trailing: child.hasAddButton ? IconButton(
|
|
|
|
|
icon: const Icon(Icons.add, size: 20),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
context.pop();
|
|
|
|
|
// Navigate to add new receipt/payment
|
|
|
|
|
if (child.label == t.receipts) {
|
|
|
|
|
// Navigate to add receipt
|
|
|
|
|
} else if (child.label == t.payments) {
|
|
|
|
|
// Navigate to add payment
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
) : null,
|
|
|
|
|
onTap: () {
|
|
|
|
|
context.pop();
|
|
|
|
|
onSelectChild(i, item.children!.indexOf(child));
|
|
|
|
|
},
|
|
|
|
|
)).toList() ?? [],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
const Divider(),
|
|
|
|
|
ListTile(
|
|
|
|
|
leading: const Icon(Icons.logout),
|
|
|
|
|
title: Text(t.logout),
|
|
|
|
|
onTap: onLogout,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2025-09-22 21:21:46 +03:30
|
|
|
),
|
2025-09-25 01:01:27 +03:30
|
|
|
),
|
|
|
|
|
body: content,
|
|
|
|
|
);
|
2025-09-22 21:21:46 +03:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 01:01:27 +03:30
|
|
|
enum _MenuItemType { simple, expandable, separator }
|
|
|
|
|
|
|
|
|
|
class _MenuItem {
|
2025-09-22 21:21:46 +03:30
|
|
|
final String label;
|
|
|
|
|
final IconData icon;
|
|
|
|
|
final IconData selectedIcon;
|
2025-09-25 01:01:27 +03:30
|
|
|
final String? path;
|
|
|
|
|
final _MenuItemType type;
|
|
|
|
|
final List<_MenuItem>? children;
|
|
|
|
|
final bool hasAddButton;
|
|
|
|
|
|
|
|
|
|
const _MenuItem({
|
|
|
|
|
required this.label,
|
|
|
|
|
required this.icon,
|
|
|
|
|
required this.selectedIcon,
|
|
|
|
|
this.path,
|
|
|
|
|
required this.type,
|
|
|
|
|
this.children,
|
|
|
|
|
this.hasAddButton = false,
|
|
|
|
|
});
|
|
|
|
|
}
|