import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../shared/widgets/app_data_table.dart'; import '../../../../shared/widgets/app_table_action_icon.dart'; import '../../../../shared/models/user_management_models.dart'; import '../../../../shared/widgets/app_card.dart'; bool isProtectedRole(RoleCardModel role) => role.id == '1' || role.name.toLowerCase() == 'super admin'; ({Color color, IconData icon}) roleCardAppearance(int index) { const styles = [ (color: Color(0xFF2563EB), icon: Icons.shield_outlined), (color: Color(0xFF16A34A), icon: Icons.admin_panel_settings_outlined), (color: Color(0xFFCA8A04), icon: Icons.receipt_long_outlined), (color: Color(0xFF0891B2), icon: Icons.inventory_outlined), (color: Color(0xFF7C3AED), icon: Icons.account_balance_wallet_outlined), (color: Color(0xFFEA580C), icon: Icons.inventory_2_outlined), ]; return styles[index % styles.length]; } class RbacStatCard extends StatelessWidget { const RbacStatCard({ super.key, required this.label, required this.value, required this.icon, required this.color, }); final String label; final String value; final IconData icon; final Color color; @override Widget build(BuildContext context) { final theme = Theme.of(context); return AppCard( elevation: 0, enableHover: true, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, size: 20, color: color), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( value, style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, ), ), Text( label, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), ], ), ), ); } } class StatusBadge extends StatelessWidget { const StatusBadge({super.key, required this.label, required this.color}); final String label; final Color color; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 6), Text( label, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: color, fontWeight: FontWeight.w600, ), ), ], ), ); } } class RoleBadge extends StatelessWidget { const RoleBadge({super.key, required this.label}); final String label; @override Widget build(BuildContext context) { final secondary = Theme.of(context).colorScheme.secondary; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: secondary.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: BoxDecoration(color: secondary, shape: BoxShape.circle), ), const SizedBox(width: 6), Flexible( child: Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: secondary, fontWeight: FontWeight.w600, ), ), ), ], ), ); } } class UserRolesCell extends StatelessWidget { const UserRolesCell({super.key, required this.roles}); final List roles; @override Widget build(BuildContext context) { if (roles.isEmpty) { return const Text('—'); } final theme = Theme.of(context); final extraCount = roles.length - 1; final tooltipMessage = roles.join('\n'); return Tooltip( message: tooltipMessage, preferBelow: true, waitDuration: const Duration(milliseconds: 250), child: SizedBox( height: 28, child: Row( children: [ Flexible( child: RoleBadge(label: roles.first), ), if (extraCount > 0) ...[ const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest .withValues(alpha: 0.8), borderRadius: BorderRadius.circular(12), ), child: Text( '+$extraCount', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), ), ], ], ), ), ); } } class EmployeeCodeBadge extends StatelessWidget { const EmployeeCodeBadge({super.key, required this.code}); /// Uniform width for employee code pills in data tables. static const double tableWidth = 108; final String code; @override Widget build(BuildContext context) { final theme = Theme.of(context); final display = code.trim().isEmpty ? '—' : code.trim(); return SizedBox( width: tableWidth, child: Container( height: 22, padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(20), ), child: Center( child: Tooltip( message: display == '—' ? '' : display, child: Text( display, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: theme.textTheme.labelSmall?.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.onSurfaceVariant, height: 1, ), ), ), ), ), ); } } class UserTableUserCell extends StatelessWidget { const UserTableUserCell({super.key, required this.user}); final ManagedUserModel user; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Row( children: [ UserAvatarChip( name: user.fullName, initials: user.initialsDisplay, radius: 16, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ AppTableCell.text( user.fullName, style: AppTypography.label2(weight: AppTypography.semiBold), ), AppTableCell.text( user.email, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), ], ); } } class UserAvatarChip extends StatelessWidget { const UserAvatarChip({ super.key, required this.name, this.initials, this.radius = 20, }); final String name; final String? initials; final double radius; @override Widget build(BuildContext context) { final colors = [ const Color(0xFF2563EB), const Color(0xFF0891B2), const Color(0xFF7C3AED), const Color(0xFFEA580C), const Color(0xFF16A34A), ]; final color = colors[name.hashCode.abs() % colors.length]; final display = initials ?? (name.isNotEmpty ? name.trim()[0].toUpperCase() : 'U'); return CircleAvatar( radius: radius, backgroundColor: color.withValues(alpha: 0.12), child: Text( display.length > 2 ? display.substring(0, 2) : display, style: TextStyle( fontFamily: AppTypography.fontFamily, color: color, fontWeight: AppTypography.bold, fontSize: radius * 0.6, ), ), ); } } class UserTableActions extends StatelessWidget { const UserTableActions({ super.key, required this.user, required this.onEdit, required this.onResetPassword, required this.onDeactivate, this.canEdit = true, this.canResetPassword = true, this.canDeactivate = true, }); static const columnWidth = 120.0; final ManagedUserModel user; final VoidCallback onEdit; final VoidCallback onResetPassword; final VoidCallback onDeactivate; final bool canEdit; final bool canResetPassword; final bool canDeactivate; @override Widget build(BuildContext context) { final muted = Theme.of(context).colorScheme.onSurfaceVariant; return AppTableActions( children: [ if (canEdit) AppTableActionIcon( tooltip: 'Edit user', icon: Icons.edit_outlined, color: muted, onPressed: onEdit, ), if (canResetPassword) AppTableActionIcon( tooltip: 'Reset password', icon: Icons.vpn_key_outlined, color: muted, onPressed: onResetPassword, ), if (canDeactivate) AppTableActionIcon( tooltip: 'Deactivate user', icon: Icons.person_off_outlined, color: muted, onPressed: onDeactivate, ), ], ); } } class UserTableActionsHeader extends StatelessWidget { const UserTableActionsHeader({super.key}); @override Widget build(BuildContext context) { return const SizedBox( width: UserTableActions.columnWidth, child: Align( alignment: Alignment.centerRight, child: Text('ACTIONS'), ), ); } } class UserTableActionsCell extends StatelessWidget { const UserTableActionsCell({super.key, required this.child}); final Widget child; @override Widget build(BuildContext context) { return SizedBox( width: UserTableActions.columnWidth, child: Align( alignment: Alignment.centerRight, child: Padding( padding: const EdgeInsets.only(right: 4), child: child, ), ), ); } } class RolePill extends StatelessWidget { const RolePill({ super.key, required this.label, required this.selected, required this.onTap, }); final String label; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { final primary = Theme.of(context).colorScheme.primary; return Material( color: selected ? primary : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(24), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(24), child: Container( constraints: const BoxConstraints(maxWidth: 200), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), border: Border.all( color: selected ? primary : Theme.of(context).colorScheme.outline.withValues(alpha: 0.35), ), ), child: Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelLarge?.copyWith( color: selected ? Theme.of(context).colorScheme.onPrimary : Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), ), ), ); } } class ArrowScrollRow extends StatefulWidget { const ArrowScrollRow({ super.key, required this.height, required this.itemCount, required this.itemBuilder, this.separatorWidth = 8, this.scrollStep = 200, }); final double height; final int itemCount; final IndexedWidgetBuilder itemBuilder; final double separatorWidth; final double scrollStep; @override State createState() => _ArrowScrollRowState(); } class _ArrowScrollRowState extends State { final _controller = ScrollController(); bool _canScrollLeft = false; bool _canScrollRight = false; @override void initState() { super.initState(); _controller.addListener(_updateScrollButtons); WidgetsBinding.instance.addPostFrameCallback((_) => _updateScrollButtons()); } @override void didUpdateWidget(covariant ArrowScrollRow oldWidget) { super.didUpdateWidget(oldWidget); WidgetsBinding.instance.addPostFrameCallback((_) => _updateScrollButtons()); } @override void dispose() { _controller.removeListener(_updateScrollButtons); _controller.dispose(); super.dispose(); } void _updateScrollButtons() { if (!_controller.hasClients) return; final position = _controller.position; final canLeft = position.pixels > position.minScrollExtent + 0.5; final canRight = position.pixels < position.maxScrollExtent - 0.5; if (canLeft == _canScrollLeft && canRight == _canScrollRight) return; setState(() { _canScrollLeft = canLeft; _canScrollRight = canRight; }); } void _scrollBy(double delta) { if (!_controller.hasClients) return; final target = (_controller.offset + delta).clamp( _controller.position.minScrollExtent, _controller.position.maxScrollExtent, ); _controller.animateTo( target, duration: const Duration(milliseconds: 250), curve: Curves.easeOut, ); } @override Widget build(BuildContext context) { return SizedBox( height: widget.height, child: Row( children: [ _ScrollArrowButton( icon: Icons.chevron_left, enabled: _canScrollLeft, onPressed: () => _scrollBy(-widget.scrollStep), ), Expanded( child: ScrollConfiguration( behavior: ScrollConfiguration.of(context).copyWith( dragDevices: { PointerDeviceKind.touch, PointerDeviceKind.mouse, PointerDeviceKind.trackpad, PointerDeviceKind.stylus, }, ), child: ListView.separated( controller: _controller, scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), itemCount: widget.itemCount, separatorBuilder: (_, __) => SizedBox(width: widget.separatorWidth), itemBuilder: widget.itemBuilder, ), ), ), _ScrollArrowButton( icon: Icons.chevron_right, enabled: _canScrollRight, onPressed: () => _scrollBy(widget.scrollStep), ), ], ), ); } } class _ScrollArrowButton extends StatelessWidget { const _ScrollArrowButton({ required this.icon, required this.enabled, required this.onPressed, }); final IconData icon; final bool enabled; final VoidCallback onPressed; @override Widget build(BuildContext context) { final theme = Theme.of(context); return SizedBox( width: 32, height: 32, child: IconButton( padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, tooltip: icon == Icons.chevron_left ? 'Scroll left' : 'Scroll right', onPressed: enabled ? onPressed : null, icon: Icon( icon, size: 22, color: enabled ? theme.colorScheme.onSurface : theme.colorScheme.onSurface.withValues(alpha: 0.3), ), ), ); } } class ModulePermissionRow extends StatelessWidget { const ModulePermissionRow({ super.key, required this.icon, required this.color, required this.label, required this.value, required this.onChanged, }); final IconData icon; final Color color; final String label; final bool value; final ValueChanged onChanged; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, size: 16, color: color), ), const SizedBox(width: 12), Expanded(child: Text(label)), Text( 'View only', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(width: 8), Checkbox( value: value, onChanged: (v) => onChanged(v ?? false), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ], ), ); } }