/opt/canhelp/apps/mobile/lib/widgets
Edit: /opt/canhelp/apps/mobile/lib/widgets/app_drawer.dart (14525B)
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../models/user.dart';
import '../providers/admin_preview_provider.dart';
import '../providers/auth_provider.dart';
import '../providers/chat_provider.dart';
import '../providers/locale_provider.dart';
import '../services/api_client.dart';
import '../theme/app_theme.dart';
class AppDrawer extends StatelessWidget {
const AppDrawer({super.key});
@override
Widget build(BuildContext context) {
final auth = context.watch
();
final locale = context.watch();
final user = auth.user;
return Drawer(
width: 300,
backgroundColor: AppTheme.background,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.horizontal(right: Radius.circular(20)),
),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Header ──
_DrawerHeader(user: user),
const Divider(height: 1, color: AppTheme.border),
const SizedBox(height: 4),
// ── Navigation items (scrollable) ──
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildNavItems(context, auth, locale),
),
),
),
const Divider(height: 1, color: AppTheme.border),
// ── Auth action ──
..._buildAuthActions(context, auth, locale),
const SizedBox(height: 8),
],
),
),
);
}
List _buildNavItems(
BuildContext context,
AuthProvider auth,
LocaleProvider locale,
) {
final user = auth.user;
final adminPreview = context.read();
final effectiveIsSpecialist = adminPreview.effectiveIsSpecialist(
user?.role,
);
final items = [];
void push(String path) {
Navigator.of(context).pop();
context.push(path);
}
void go(String path) {
Navigator.of(context).pop();
context.go(path);
}
// Home — always
items.add(
AppDrawerItem(
icon: Icons.home_outlined,
label: locale.t('nav.home'),
onTap: () => go('/'),
),
);
if (!auth.isAuthenticated) {
// ── Guest ──
items.add(
AppDrawerItem(
icon: Icons.search_outlined,
label: locale.t('nav.tasks'),
onTap: () => go('/tasks'),
),
);
items.add(
AppDrawerItem(
icon: Icons.people_outline_rounded,
label: locale.t('nav.specialists'),
onTap: () => go('/specialists'),
),
);
} else if (effectiveIsSpecialist || user?.isAdmin == true) {
// ── Specialist / Admin ──
items.add(
AppDrawerItem(
icon: Icons.search_outlined,
label: locale.t('nav.findTasks'),
onTap: () => go('/tasks'),
),
);
items.add(
AppDrawerItem(
icon: Icons.people_outline_rounded,
label: locale.t('nav.specialists'),
onTap: () => go('/specialists'),
),
);
items.add(_Divider());
items.add(
AppDrawerItem(
icon: Icons.dashboard_outlined,
label: locale.t('nav.dashboard'),
onTap: () => push('/dashboard'),
),
);
items.add(
AppDrawerItem(
icon: Icons.calendar_month_outlined,
label: locale.t('nav.schedule'),
onTap: () => push('/schedule'),
),
);
items.add(
AppDrawerItem(
icon: Icons.person_outline,
label: locale.t('nav.profile'),
onTap: () => go('/profile'),
),
);
items.add(
AppDrawerItem(
icon: Icons.bookmark_outline_rounded,
label: locale.t('nav.favorites'),
onTap: () => push('/favorites'),
),
);
items.add(
AppDrawerItem(
icon: Icons.workspace_premium_outlined,
label: locale.t('nav.pricing', 'Тарифы'),
onTap: () => push('/pricing'),
),
);
// FOMO: show missed orders banner for specialists on free plan (no active paid plan)
if (user?.planId == null &&
effectiveIsSpecialist &&
user?.isAdmin != true) {
items.add(const _MissedOrdersBanner());
}
if (user?.isAdmin == true) {
items.add(_Divider());
items.add(
AppDrawerItem(
icon: Icons.admin_panel_settings_outlined,
label: locale.t('nav.admin', 'Администратор'),
onTap: () => push('/admin'),
),
);
}
} else {
// ── Customer ──
items.add(
AppDrawerItem(
icon: Icons.add_task_outlined,
label: locale.t('nav.newTask'),
onTap: () => push('/tasks/new'),
),
);
items.add(
AppDrawerItem(
icon: Icons.people_outline_rounded,
label: locale.t('nav.specialists'),
onTap: () => go('/specialists'),
),
);
items.add(_Divider());
items.add(
AppDrawerItem(
icon: Icons.dashboard_outlined,
label: locale.t('nav.dashboard'),
onTap: () => push('/dashboard'),
),
);
items.add(
AppDrawerItem(
icon: Icons.person_outline,
label: locale.t('nav.profile'),
onTap: () => go('/profile'),
),
);
items.add(
AppDrawerItem(
icon: Icons.bookmark_outline_rounded,
label: locale.t('nav.favorites'),
onTap: () => push('/favorites'),
),
);
items.add(
AppDrawerItem(
icon: Icons.workspace_premium_outlined,
label: locale.t('nav.pricing', 'Тарифы'),
onTap: () => push('/pricing'),
),
);
}
return items;
}
List _buildAuthActions(
BuildContext context,
AuthProvider auth,
LocaleProvider locale,
) {
if (auth.isAuthenticated) {
return [
AppDrawerItem(
icon: Icons.logout_rounded,
label: locale.t('nav.logout'),
danger: true,
onTap: () {
Navigator.of(context).pop();
context.read().reset();
auth.logout();
},
),
];
}
return [
AppDrawerItem(
icon: Icons.login_rounded,
label: locale.t('auth.login'),
onTap: () {
Navigator.of(context).pop();
context.push('/login');
},
),
AppDrawerItem(
icon: Icons.person_add_outlined,
label: locale.t('nav.register'),
onTap: () {
Navigator.of(context).pop();
context.push('/register');
},
),
];
}
}
// ─── Drawer header ─────────────────────────────────────
class _DrawerHeader extends StatelessWidget {
final User? user;
const _DrawerHeader({required this.user});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
child: Row(
children: [
// Avatar
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: user?.avatar != null
? Colors.transparent
: (user != null ? AppTheme.primary : Colors.transparent),
borderRadius: BorderRadius.circular(AppTheme.inputRadius),
),
clipBehavior: Clip.antiAlias,
child: user?.avatar != null
? Image.network(user!.avatar!, fit: BoxFit.cover)
: user != null
? Center(
child: Text(
user!.fullName.isNotEmpty
? user!.fullName[0].toUpperCase()
: user!.email[0].toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(AppTheme.inputRadius),
child: Image.asset(
'assets/icons/icon-1.png',
width: 48,
height: 48,
fit: BoxFit.cover,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user?.fullName.isNotEmpty == true
? user!.fullName
: (user?.email ?? 'CanHelp'),
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
user != null
? user!.email
: context.watch().t('common.guest'),
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
}
// ─── Divider helper ────────────────────────────────────
class _Divider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 6),
child: Divider(height: 1, color: AppTheme.border),
);
}
}
// ─── Drawer item ───────────────────────────────────────
class AppDrawerItem extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
final bool danger;
const AppDrawerItem({
super.key,
required this.icon,
required this.label,
required this.onTap,
this.danger = false,
});
@override
Widget build(BuildContext context) {
final textColor = danger ? AppTheme.danger : AppTheme.textPrimary;
final iconColor = danger ? AppTheme.danger : AppTheme.textSecondary;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 13),
child: Row(
children: [
Icon(icon, size: 22, color: iconColor),
const SizedBox(width: 14),
Text(
label,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: textColor,
),
),
],
),
),
),
);
}
}
// ── FOMO: missed orders banner ─────────────────────────────────────────────
class _MissedOrdersBanner extends StatefulWidget {
const _MissedOrdersBanner();
@override
State<_MissedOrdersBanner> createState() => _MissedOrdersBannerState();
}
class _MissedOrdersBannerState extends State<_MissedOrdersBanner> {
int? _missed;
@override
void initState() {
super.initState();
_load();
}
Future _load() async {
try {
final data = await ApiClient().getSpecialistFomoDashboard();
final n = data['missedTasksCount'] as int? ?? 0;
if (mounted && n > 0) setState(() => _missed = n);
} catch (_) {}
}
@override
Widget build(BuildContext context) {
if (_missed == null || _missed! <= 0) return const SizedBox.shrink();
final locale = context.watch();
return GestureDetector(
onTap: () {
Navigator.of(context).pop();
context.push('/pricing');
},
child: Container(
margin: const EdgeInsets.fromLTRB(12, 8, 12, 4),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: AppTheme.warningLight,
borderRadius: BorderRadius.circular(AppTheme.inputRadius),
border: Border.all(color: const Color(0xFFFDE68A)),
),
child: Row(
children: [
const Icon(
Icons.lock_outline_rounded,
color: AppTheme.warning,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: RichText(
text: TextSpan(
style: TextStyle(fontSize: 13, color: AppTheme.textPrimary),
children: [
TextSpan(
text: locale.t('fomo.missed_prefix', 'Пропущено '),
),
TextSpan(
text: '$_missed',
style: const TextStyle(fontWeight: FontWeight.w700),
),
TextSpan(
text: ' ${locale.t('fomo.missed_suffix', 'заказов')}',
),
],
),
),
),
const SizedBox(width: 6),
Text(
locale.t('upgrade.cta', 'Pro'),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: AppTheme.warning,
decoration: TextDecoration.underline,
decorationColor: AppTheme.warning,
),
),
],
),
),
);
}
}