/opt/canhelp/apps/mobile/lib/widgets
NameSizeModeActions
admin_preview_overlay.dart146700644editdlrm
app_bar_actions.dart23230644editdlrm
app_drawer.dart145250644editdlrm
auth_terms_consent.dart39280644editdlrm
plan_badge.dart11030644editdlrm
task_card.dart75050644editdlrm
upgrade_prompt.dart61970644editdlrm
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, ), ), ], ), ), ); } }