bharat_erp/lib/modules/rbac/presentation/widgets/rbac_widgets.dart
2026-07-24 14:01:50 +05:30

674 lines
18 KiB
Dart

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<String> 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<ArrowScrollRow> createState() => _ArrowScrollRowState();
}
class _ArrowScrollRowState extends State<ArrowScrollRow> {
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<bool> 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,
),
],
),
);
}
}