/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/admin_preview_overlay.dart (14670B)
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import '../models/dashboard_data.dart'; import '../providers/admin_preview_provider.dart'; import '../providers/auth_provider.dart'; import '../providers/locale_provider.dart'; import '../theme/app_theme.dart'; /// Floating admin preview widget — mirrors web AdminRolePreview. /// Shown only when user is admin. Allows switching preview role and plan. class AdminPreviewOverlay extends StatefulWidget { const AdminPreviewOverlay({super.key}); @override State createState() => _AdminPreviewOverlayState(); } class _AdminPreviewOverlayState extends State { bool _expanded = false; static const _tierColors = { 'free': Color(0xFFF1F5F9), 'pro': Color(0xFFEFF6FF), 'ultimate': Color(0xFFFFFBEB), }; static const _tierTextColors = { 'free': Color(0xFF64748B), 'pro': Color(0xFF2563EB), 'ultimate': Color(0xFFD97706), }; @override Widget build(BuildContext context) { final auth = context.watch(); final preview = context.watch(); final locale = context.watch(); if (auth.user?.isAdmin != true) return const SizedBox.shrink(); final userId = auth.user!.id; return Positioned( bottom: 90, right: 12, child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ if (_expanded) _buildPanel(context, preview, locale, userId), const SizedBox(height: 8), _buildFab(preview), ], ), ); } Widget _buildFab(AdminPreviewProvider preview) { final active = preview.isActive; return GestureDetector( onTap: () => setState(() => _expanded = !_expanded), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: active ? AppTheme.warning : const Color(0xFF1F1F1F), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.25), blurRadius: 8, offset: const Offset(0, 3), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( active ? Icons.visibility : Icons.admin_panel_settings_outlined, color: Colors.white, size: 16, ), const SizedBox(width: 6), Text( active ? 'Preview: ${preview.previewRole}' : 'Admin Preview', style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 4), Icon( _expanded ? Icons.expand_more : Icons.expand_less, color: Colors.white, size: 16, ), ], ), ), ); } Widget _buildPanel( BuildContext context, AdminPreviewProvider preview, LocaleProvider locale, String userId, ) { return Container( width: 260, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.cardRadius), border: Border.all(color: AppTheme.border), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.12), blurRadius: 16, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ // Header Padding( padding: const EdgeInsets.fromLTRB(14, 12, 14, 8), child: Row( children: [ const Icon( Icons.admin_panel_settings_outlined, size: 16, color: AppTheme.textSecondary, ), const SizedBox(width: 6), const Text( 'Admin Preview', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: AppTheme.textPrimary, ), ), const Spacer(), GestureDetector( onTap: () => setState(() => _expanded = false), child: const Icon( Icons.close, size: 16, color: AppTheme.textSecondary, ), ), ], ), ), const Divider(height: 1, color: AppTheme.border), // Role toggle Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Role', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppTheme.textSecondary, ), ), const SizedBox(height: 6), Row( children: [ _RoleButton( label: 'Customer', active: preview.previewRole == 'customer', onTap: () => _toggleRole(context, preview, 'customer'), ), const SizedBox(width: 6), _RoleButton( label: 'Specialist', active: preview.previewRole == 'specialist', onTap: () => _toggleRole(context, preview, 'specialist'), ), ], ), ], ), ), // Plan list (only when role is selected) if (preview.previewRole != null) ...[ const Divider(height: 1, color: AppTheme.border), Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 4), child: const Text( 'Plan', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppTheme.textSecondary, ), ), ), if (preview.loadingPlans) const Padding( padding: EdgeInsets.symmetric(vertical: 10), child: Center( child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ), ), ) else ...preview.plans.map( (plan) => _PlanRow( plan: plan, isActive: plan.id == preview.activePlanId, tierBg: _tierColors[plan.tier] ?? AppTheme.surfaceVariant, tierFg: _tierTextColors[plan.tier] ?? AppTheme.textSecondary, onTap: () => _setPlan(context, preview, userId, plan.id), ), ), // Restore button if (preview.origPlanId != null) Padding( padding: const EdgeInsets.fromLTRB(14, 4, 14, 10), child: GestureDetector( onTap: () => _restorePlan(context, preview, userId), child: Container( padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: AppTheme.dangerLight, borderRadius: BorderRadius.circular( AppTheme.buttonRadius, ), border: Border.all(color: const Color(0xFFFCA5A5)), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.restore, size: 14, color: AppTheme.danger), SizedBox(width: 6), Text( 'Restore original plan', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppTheme.danger, ), ), ], ), ), ), ) else const SizedBox(height: 10), ] else const SizedBox(height: 4), // Go to Admin Panel button const Divider(height: 1, color: AppTheme.border), Padding( padding: const EdgeInsets.all(10), child: GestureDetector( onTap: () { setState(() => _expanded = false); context.push('/admin'); }, child: Container( padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: const Color(0xFF1F1F1F), borderRadius: BorderRadius.circular(AppTheme.buttonRadius), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.admin_panel_settings_outlined, size: 14, color: Colors.white, ), SizedBox(width: 6), Text( 'Admin Panel', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white, ), ), ], ), ), ), ), ], ), ); } void _toggleRole( BuildContext context, AdminPreviewProvider preview, String role, ) { if (preview.previewRole == role) { preview.setPreviewRole(null); } else { // Sync active plan from current user on first open final auth = context.read(); if (preview.activePlanId == null) { preview.setActivePlanId(auth.user?.planId); } preview.setPreviewRole(role); } } Future _setPlan( BuildContext context, AdminPreviewProvider preview, String userId, String planId, ) async { await preview.setPlan(userId, planId); // Refresh user session to reflect new plan in the UI if (context.mounted) { await context.read().refreshUser(); } } Future _restorePlan( BuildContext context, AdminPreviewProvider preview, String userId, ) async { await preview.restorePlan(userId); if (context.mounted) { await context.read().refreshUser(); } } } // ── Sub-widgets ─────────────────────────────────────────────────────────── class _RoleButton extends StatelessWidget { final String label; final bool active; final VoidCallback onTap; const _RoleButton({ required this.label, required this.active, required this.onTap, }); @override Widget build(BuildContext context) { return Expanded( child: GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric(vertical: 7), decoration: BoxDecoration( color: active ? AppTheme.primary : AppTheme.surfaceVariant, borderRadius: BorderRadius.circular(AppTheme.buttonRadius), border: Border.all( color: active ? AppTheme.primary : AppTheme.border, ), ), child: Text( label, textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: active ? Colors.white : AppTheme.textSecondary, ), ), ), ), ); } } class _PlanRow extends StatelessWidget { final PlanItem plan; final bool isActive; final Color tierBg; final Color tierFg; final VoidCallback onTap; const _PlanRow({ required this.plan, required this.isActive, required this.tierBg, required this.tierFg, required this.onTap, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), color: isActive ? AppTheme.primaryLight : Colors.transparent, child: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: tierBg, borderRadius: BorderRadius.circular(6), ), child: Text( plan.tier.toUpperCase(), style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: tierFg, ), ), ), const SizedBox(width: 8), Expanded( child: Text( plan.name, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppTheme.textPrimary, ), ), ), if (plan.price > 0) Text( '€${plan.price.toStringAsFixed(0)}', style: const TextStyle( fontSize: 11, color: AppTheme.textSecondary, ), ), if (isActive) ...[ const SizedBox(width: 6), const Icon(Icons.check_circle, size: 14, color: AppTheme.success), ], ], ), ), ); } }