Files
2026-05-25 05:10:43 -04:00

342 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/dashboard_providers.dart';
import '../../../widgets/error_view.dart';
import '../../../core/providers/auth_provider.dart';
class TeamScreen extends ConsumerWidget {
const TeamScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final async = ref.watch(teamProvider);
final myRole = ref.watch(authProvider).employee?.role ?? '';
final isOwner = myRole == 'OWNER';
return Scaffold(
appBar: AppBar(title: const Text('Team')),
floatingActionButton: isOwner
? FloatingActionButton.extended(
onPressed: () async {
final invited = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (_) => const _InviteSheet(),
);
if (invited == true) ref.invalidate(teamProvider);
},
icon: const Icon(Icons.person_add_outlined),
label: const Text('Invite'),
)
: null,
body: async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => ErrorView(
message: 'Failed to load team',
onRetry: () => ref.invalidate(teamProvider),
),
data: (members) {
if (members.isEmpty) {
return const Center(
child: Text('No team members',
style: TextStyle(color: Color(0xFF9CA3AF))),
);
}
return RefreshIndicator(
onRefresh: () async => ref.invalidate(teamProvider),
child: ListView.separated(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
itemCount: members.length,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (_, i) => _MemberCard(
member: members[i],
isOwner: isOwner,
onDeactivate: () async {
await ref
.read(teamServiceProvider)
.deactivateMember(members[i].id);
ref.invalidate(teamProvider);
},
onReactivate: () async {
await ref
.read(teamServiceProvider)
.reactivateMember(members[i].id);
ref.invalidate(teamProvider);
},
onRoleChange: (role) async {
await ref
.read(teamServiceProvider)
.updateRole(members[i].id, role);
ref.invalidate(teamProvider);
},
),
),
);
},
),
);
}
}
class _MemberCard extends StatelessWidget {
final dynamic member;
final bool isOwner;
final VoidCallback onDeactivate;
final VoidCallback onReactivate;
final ValueChanged<String> onRoleChange;
const _MemberCard({
required this.member,
required this.isOwner,
required this.onDeactivate,
required this.onReactivate,
required this.onRoleChange,
});
@override
Widget build(BuildContext context) {
final isActive = member.isActive as bool;
final role = member.role as String;
return Card(
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
CircleAvatar(
backgroundColor: isActive
? const Color(0xFFE1EFFE)
: const Color(0xFFF3F4F6),
radius: 22,
child: Text(
(member.firstName as String)
.substring(0, 1)
.toUpperCase(),
style: TextStyle(
color: isActive
? const Color(0xFF1A56DB)
: const Color(0xFF9CA3AF),
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
member.fullName as String,
style: TextStyle(
fontWeight: FontWeight.w600,
color: isActive ? null : const Color(0xFF9CA3AF),
),
),
const SizedBox(width: 8),
_RoleBadge(role: role),
],
),
Text(
member.email as String,
style: const TextStyle(
fontSize: 12, color: Color(0xFF6B7280)),
),
if (!isActive)
const Text(
'Inactive',
style: TextStyle(
fontSize: 11, color: Color(0xFF9CA3AF)),
),
],
),
),
if (isOwner && role != 'OWNER')
PopupMenuButton<String>(
onSelected: (v) {
if (v == 'deactivate') onDeactivate();
if (v == 'reactivate') onReactivate();
if (v == 'MANAGER' || v == 'AGENT') onRoleChange(v);
},
itemBuilder: (_) => [
if (role != 'MANAGER')
const PopupMenuItem(
value: 'MANAGER', child: Text('Make Manager')),
if (role != 'AGENT')
const PopupMenuItem(
value: 'AGENT', child: Text('Make Agent')),
const PopupMenuDivider(),
if (isActive)
const PopupMenuItem(
value: 'deactivate',
child: Text('Deactivate',
style: TextStyle(color: Color(0xFFE02424))),
)
else
const PopupMenuItem(
value: 'reactivate',
child: Text('Reactivate',
style: TextStyle(color: Color(0xFF0E9F6E))),
),
],
),
],
),
),
);
}
}
class _RoleBadge extends StatelessWidget {
final String role;
const _RoleBadge({required this.role});
@override
Widget build(BuildContext context) {
final colors = {
'OWNER': (const Color(0xFFFFF3CD), const Color(0xFF92400E)),
'MANAGER': (const Color(0xFFE1EFFE), const Color(0xFF1E40AF)),
'AGENT': (const Color(0xFFF3F4F6), const Color(0xFF374151)),
};
final (bg, fg) = colors[role] ?? (const Color(0xFFF3F4F6), const Color(0xFF374151));
return Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration:
BoxDecoration(color: bg, borderRadius: BorderRadius.circular(4)),
child: Text(role,
style: TextStyle(
fontSize: 10, color: fg, fontWeight: FontWeight.w600)),
);
}
}
class _InviteSheet extends ConsumerStatefulWidget {
const _InviteSheet();
@override
ConsumerState<_InviteSheet> createState() => _InviteSheetState();
}
class _InviteSheetState extends ConsumerState<_InviteSheet> {
final _formKey = GlobalKey<FormState>();
bool _loading = false;
final _firstName = TextEditingController();
final _lastName = TextEditingController();
final _email = TextEditingController();
String _role = 'AGENT';
@override
void dispose() {
for (final c in [_firstName, _lastName, _email]) {
c.dispose();
}
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _loading = true);
try {
await ref.read(teamServiceProvider).inviteMember({
'firstName': _firstName.text.trim(),
'lastName': _lastName.text.trim(),
'email': _email.text.trim(),
'role': _role,
});
if (mounted) Navigator.of(context).pop(true);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Error: $e')));
}
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text('Invite Team Member',
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
const Spacer(),
if (_loading)
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2))
else
TextButton(
onPressed: _submit, child: const Text('Invite')),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _firstName,
decoration: const InputDecoration(labelText: 'First Name'),
validator: (v) =>
v == null || v.isEmpty ? 'Required' : null,
),
const SizedBox(height: 12),
TextFormField(
controller: _lastName,
decoration: const InputDecoration(labelText: 'Last Name'),
validator: (v) =>
v == null || v.isEmpty ? 'Required' : null,
),
const SizedBox(height: 12),
TextFormField(
controller: _email,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(labelText: 'Email'),
validator: (v) =>
v == null || v.isEmpty ? 'Required' : null,
),
const SizedBox(height: 12),
InputDecorator(
decoration: const InputDecoration(labelText: 'Role'),
child: DropdownButton<String>(
value: _role,
isExpanded: true,
underline: const SizedBox(),
items: const [
DropdownMenuItem(value: 'MANAGER', child: Text('Manager')),
DropdownMenuItem(value: 'AGENT', child: Text('Agent')),
],
onChanged: (v) => setState(() => _role = v!),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _loading ? null : _submit,
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48)),
child: const Text('Send Invitation'),
),
],
),
),
),
);
}
}