/opt/canhelp/apps/mobile/lib/widgets
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),
],
],
),
),
);
}
}