Teams CRUD
This commit is contained in:
+544
-145
@@ -5,23 +5,41 @@ import '../../models/profile.dart';
|
||||
import '../../models/team.dart';
|
||||
import '../../providers/teams_provider.dart';
|
||||
import '../../providers/profile_provider.dart';
|
||||
import '../../providers/tickets_provider.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:tasq/screens/searchable_multi_select_dropdown.dart';
|
||||
import 'package:tasq/widgets/multi_select_picker.dart';
|
||||
import '../../theme/app_surfaces.dart';
|
||||
import '../../widgets/tasq_adaptive_list.dart';
|
||||
|
||||
final officesProvider = FutureProvider<List<Office>>((ref) async {
|
||||
final data = await Supabase.instance.client.from('offices').select();
|
||||
return (data as List<dynamic>? ?? [])
|
||||
.map((e) => Office.fromMap(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
// Note: `officesProvider` is provided globally in `tickets_provider.dart` so
|
||||
// we reuse that StreamProvider here (avoids duplicate queries).
|
||||
|
||||
class TeamsScreen extends ConsumerWidget {
|
||||
class TeamsScreen extends ConsumerStatefulWidget {
|
||||
const TeamsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<TeamsScreen> createState() => _TeamsScreenState();
|
||||
}
|
||||
|
||||
class _TeamsScreenState extends ConsumerState<TeamsScreen> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String? _selectedOfficeId;
|
||||
String? _selectedLeaderId;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get _hasFilters {
|
||||
return _searchController.text.trim().isNotEmpty ||
|
||||
_selectedOfficeId != null ||
|
||||
_selectedLeaderId != null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final teamsAsync = ref.watch(teamsProvider);
|
||||
final profilesAsync = ref.watch(profilesProvider);
|
||||
final officesAsync = ref.watch(officesProvider);
|
||||
@@ -36,8 +54,98 @@ class TeamsScreen extends ConsumerWidget {
|
||||
final profileById = {for (var p in profiles) p.id: p};
|
||||
final officeById = {for (var o in offices) o.id: o};
|
||||
|
||||
return TasQAdaptiveList<Team>(
|
||||
items: teams,
|
||||
// filters
|
||||
final itStaff = profiles.where((p) => p.role == 'it_staff').toList();
|
||||
final officeOptions = <DropdownMenuItem<String?>>[
|
||||
const DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('All offices'),
|
||||
),
|
||||
...offices.map(
|
||||
(o) => DropdownMenuItem(value: o.id, child: Text(o.name)),
|
||||
),
|
||||
];
|
||||
final leaderOptions = <DropdownMenuItem<String?>>[
|
||||
const DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('All leaders'),
|
||||
),
|
||||
...itStaff.map(
|
||||
(p) => DropdownMenuItem(
|
||||
value: p.id,
|
||||
child: Text(p.fullName.isNotEmpty ? p.fullName : p.id),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final filteredTeams = teams.where((t) {
|
||||
final matchesName =
|
||||
_searchController.text.trim().isEmpty ||
|
||||
t.name.toLowerCase().contains(
|
||||
_searchController.text.trim().toLowerCase(),
|
||||
);
|
||||
final matchesOffice =
|
||||
_selectedOfficeId == null ||
|
||||
t.officeIds.contains(_selectedOfficeId);
|
||||
final matchesLeader =
|
||||
_selectedLeaderId == null || t.leaderId == _selectedLeaderId;
|
||||
return matchesName && matchesOffice && matchesLeader;
|
||||
}).toList();
|
||||
|
||||
final filterHeader = Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 280,
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Search team name',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: DropdownButtonFormField<String?>(
|
||||
isExpanded: true,
|
||||
key: ValueKey(_selectedOfficeId),
|
||||
initialValue: _selectedOfficeId,
|
||||
items: officeOptions,
|
||||
onChanged: (v) => setState(() => _selectedOfficeId = v),
|
||||
decoration: const InputDecoration(labelText: 'Office'),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: DropdownButtonFormField<String?>(
|
||||
isExpanded: true,
|
||||
key: ValueKey(_selectedLeaderId),
|
||||
initialValue: _selectedLeaderId,
|
||||
items: leaderOptions,
|
||||
onChanged: (v) => setState(() => _selectedLeaderId = v),
|
||||
decoration: const InputDecoration(labelText: 'Team leader'),
|
||||
),
|
||||
),
|
||||
if (_hasFilters)
|
||||
TextButton.icon(
|
||||
onPressed: () => setState(() {
|
||||
_searchController.clear();
|
||||
_selectedOfficeId = null;
|
||||
_selectedLeaderId = null;
|
||||
}),
|
||||
icon: const Icon(Icons.close),
|
||||
label: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final listBody = TasQAdaptiveList<Team>(
|
||||
items: filteredTeams,
|
||||
filterHeader: filterHeader,
|
||||
columns: [
|
||||
TasQColumn<Team>(
|
||||
header: 'Name',
|
||||
@@ -93,41 +201,67 @@ class TeamsScreen extends ConsumerWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: actions,
|
||||
),
|
||||
onTap: () => _showTeamDialog(context, ref, team: team),
|
||||
onTap: () => _showTeamDialog(context, team: team),
|
||||
);
|
||||
},
|
||||
rowActions: (team) => [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
tooltip: 'Edit',
|
||||
onPressed: () => _showTeamDialog(context, ref, team: team),
|
||||
onPressed: () => _showTeamDialog(context, team: team),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
tooltip: 'Delete',
|
||||
onPressed: () => _deleteTeam(context, ref, team.id),
|
||||
onPressed: () => _deleteTeam(context, team.id),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 8),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Team Management',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: listBody),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showTeamDialog(context, ref),
|
||||
onPressed: () => _showTeamDialog(context),
|
||||
tooltip: 'Add Team',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showTeamDialog(
|
||||
BuildContext context,
|
||||
WidgetRef ref, {
|
||||
Team? team,
|
||||
}) async {
|
||||
void _showTeamDialog(BuildContext context, {Team? team}) async {
|
||||
final profiles = ref.read(profilesProvider).valueOrNull ?? [];
|
||||
final offices = await ref.read(officesProvider.future);
|
||||
// officesProvider is a StreamProvider (global) — try to read the latest
|
||||
// synchronous value first, otherwise await the first stream event.
|
||||
final officesSync = ref.read(officesProvider).valueOrNull;
|
||||
final List<Office> offices =
|
||||
officesSync ?? await ref.read(officesProvider.future);
|
||||
if (!context.mounted) return;
|
||||
final itStaff = profiles.where((p) => p.role == 'it_staff').toList();
|
||||
final nameController = TextEditingController(text: team?.name ?? '');
|
||||
@@ -145,147 +279,412 @@ class TeamsScreen extends ConsumerWidget {
|
||||
.toList();
|
||||
}
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
shape: AppSurfaces.of(context).dialogShape,
|
||||
title: Text(isEdit ? 'Edit Team' : 'Add Team'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(labelText: 'Team Name'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: leaderId,
|
||||
items: [
|
||||
for (final p in itStaff)
|
||||
DropdownMenuItem(
|
||||
value: p.id,
|
||||
child: Text(
|
||||
p.fullName.isNotEmpty ? p.fullName : p.id,
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (v) => setState(() => leaderId = v),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Team Leader',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SearchableMultiSelectDropdown<Office>(
|
||||
label: 'Offices',
|
||||
items: offices,
|
||||
selectedIds: selectedOffices,
|
||||
getId: (o) => o.id,
|
||||
getLabel: (o) => o.name,
|
||||
onChanged: (ids) => setState(() => selectedOffices = ids),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SearchableMultiSelectDropdown<Profile>(
|
||||
label: 'Team Members',
|
||||
items: itStaff,
|
||||
selectedIds: selectedMembers,
|
||||
getId: (p) => p.id,
|
||||
getLabel: (p) =>
|
||||
p.fullName.isNotEmpty ? p.fullName : p.id,
|
||||
onChanged: (ids) => setState(() => selectedMembers = ids),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final navigator = Navigator.of(context);
|
||||
final name = nameController.text.trim();
|
||||
if (name.isEmpty || leaderId == null) return;
|
||||
final client = Supabase.instance.client;
|
||||
if (isEdit) {
|
||||
await client
|
||||
.from('teams')
|
||||
.update({
|
||||
'name': name,
|
||||
'leader_id': leaderId,
|
||||
'office_ids': selectedOffices,
|
||||
})
|
||||
.eq('id', team.id);
|
||||
// Update team members
|
||||
await client
|
||||
.from('team_members')
|
||||
.delete()
|
||||
.eq('team_id', team.id);
|
||||
if (selectedMembers.isNotEmpty) {
|
||||
await client.from('team_members').insert([
|
||||
for (final userId in selectedMembers)
|
||||
{'team_id': team.id, 'user_id': userId},
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
final insertRes = await client
|
||||
.from('teams')
|
||||
.insert({
|
||||
'name': name,
|
||||
'leader_id': leaderId,
|
||||
'office_ids': selectedOffices,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
final newTeamId = insertRes['id'] as String?;
|
||||
if (newTeamId != null && selectedMembers.isNotEmpty) {
|
||||
await client.from('team_members').insert([
|
||||
for (final userId in selectedMembers)
|
||||
{'team_id': newTeamId, 'user_id': userId},
|
||||
]);
|
||||
}
|
||||
}
|
||||
ref.invalidate(teamsProvider);
|
||||
ref.invalidate(teamMembersProvider);
|
||||
navigator.pop();
|
||||
},
|
||||
child: Text(isEdit ? 'Save' : 'Add'),
|
||||
),
|
||||
final bool isMobileDialog = MediaQuery.of(context).size.width <= 600;
|
||||
const double kDialogMaxWidth = 720;
|
||||
|
||||
// Dialog content builder (captures outer local vars like leaderId,
|
||||
// selectedOffices, selectedMembers, nameController).
|
||||
Widget buildFormContent(StateSetter setState) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(labelText: 'Team Name'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: leaderId,
|
||||
items: [
|
||||
for (final p in itStaff)
|
||||
DropdownMenuItem(
|
||||
value: p.id,
|
||||
child: Text(p.fullName.isNotEmpty ? p.fullName : p.id),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
onChanged: (v) => setState(() => leaderId = v),
|
||||
decoration: const InputDecoration(labelText: 'Team Leader'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Available offices: exclude offices already assigned to other teams
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final allTeams =
|
||||
ref.read(teamsProvider).valueOrNull ?? <Team>[];
|
||||
final assignedOfficeIds = allTeams
|
||||
.where((t) => t.id != team?.id)
|
||||
.expand((t) => t.officeIds)
|
||||
.toSet();
|
||||
final availableOffices = offices
|
||||
.where(
|
||||
(o) =>
|
||||
!assignedOfficeIds.contains(o.id) ||
|
||||
selectedOffices.contains(o.id),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return MultiSelectPicker<Office>(
|
||||
label: 'Offices',
|
||||
items: availableOffices,
|
||||
selectedIds: selectedOffices,
|
||||
getId: (o) => o.id,
|
||||
getLabel: (o) => o.name,
|
||||
onChanged: (ids) => setState(() => selectedOffices = ids),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final members =
|
||||
ref.read(teamMembersProvider).valueOrNull ?? <dynamic>[];
|
||||
final assignedUserIds = members
|
||||
.where((m) => m.teamId != team?.id)
|
||||
.map((m) => m.userId)
|
||||
.toSet();
|
||||
final availableItStaff = itStaff
|
||||
.where(
|
||||
(p) =>
|
||||
!assignedUserIds.contains(p.id) ||
|
||||
selectedMembers.contains(p.id),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return MultiSelectPicker<Profile>(
|
||||
label: 'Team Members',
|
||||
items: availableItStaff,
|
||||
selectedIds: selectedMembers,
|
||||
getId: (p) => p.id,
|
||||
getLabel: (p) => p.fullName.isNotEmpty ? p.fullName : p.id,
|
||||
onChanged: (ids) => setState(() => selectedMembers = ids),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onSave(StateSetter setState, NavigatorState navigator) async {
|
||||
final messenger = ScaffoldMessenger.of(navigator.context);
|
||||
final name = nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Team name is required')));
|
||||
return;
|
||||
}
|
||||
if (leaderId == null) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('Please select a team leader')),
|
||||
);
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (selectedOffices.isEmpty) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Assign at least one office to the team'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure leader included
|
||||
if (leaderId != null && !selectedMembers.contains(leaderId)) {
|
||||
selectedMembers = [leaderId!, ...selectedMembers];
|
||||
}
|
||||
|
||||
final client = Supabase.instance.client;
|
||||
try {
|
||||
if (isEdit) {
|
||||
// update team row
|
||||
final upRes = await client
|
||||
.from('teams')
|
||||
.update({
|
||||
'name': name,
|
||||
'leader_id': leaderId,
|
||||
'office_ids': selectedOffices,
|
||||
})
|
||||
.eq('id', team.id);
|
||||
if (upRes['error'] != null) {
|
||||
final err = upRes['error'];
|
||||
throw Exception(
|
||||
err is Map ? (err['message'] ?? err.toString()) : err.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
// replace members for the team
|
||||
final delRes = await client
|
||||
.from('team_members')
|
||||
.delete()
|
||||
.eq('team_id', team.id);
|
||||
if (delRes['error'] != null) {
|
||||
final err = delRes['error'];
|
||||
throw Exception(
|
||||
err is Map ? (err['message'] ?? err.toString()) : err.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedMembers.isNotEmpty) {
|
||||
final rows = selectedMembers
|
||||
.map((u) => {'team_id': team.id, 'user_id': u})
|
||||
.toList();
|
||||
final memRes = await client.from('team_members').insert(rows);
|
||||
if (memRes is Map && memRes['error'] != null) {
|
||||
final err = memRes['error'];
|
||||
throw Exception(
|
||||
err is Map
|
||||
? (err['message'] ?? err.toString())
|
||||
: err.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
// verify members persisted (handle Map or List response)
|
||||
final dynamic checkRes = await client
|
||||
.from('team_members')
|
||||
.select()
|
||||
.eq('team_id', team.id);
|
||||
if (checkRes is Map && checkRes['error'] != null) {
|
||||
throw Exception('Failed to verify team members');
|
||||
}
|
||||
final List persisted = checkRes is List
|
||||
? List.from(checkRes)
|
||||
: (checkRes is Map && checkRes['data'] is List)
|
||||
? List.from(checkRes['data'] as List)
|
||||
: <dynamic>[];
|
||||
if (persisted.length != selectedMembers.length) {
|
||||
throw Exception('Member count mismatch after save');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// create team and insert members
|
||||
final insertRes = await client
|
||||
.from('teams')
|
||||
.insert({
|
||||
'name': name,
|
||||
'leader_id': leaderId,
|
||||
'office_ids': selectedOffices,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
// normalize inserted row to extract id reliably across client response shapes
|
||||
dynamic insertedRow;
|
||||
final insertResValue = insertRes;
|
||||
if (insertResValue is List && insertResValue.isNotEmpty) {
|
||||
insertedRow = (insertResValue as List).first;
|
||||
} else {
|
||||
if (insertResValue['error'] != null) {
|
||||
final err = insertResValue['error'];
|
||||
throw Exception(
|
||||
err is Map
|
||||
? (err['message'] ?? err.toString())
|
||||
: err.toString(),
|
||||
);
|
||||
}
|
||||
final dataField = insertResValue['data'];
|
||||
if (dataField is List && dataField.isNotEmpty) {
|
||||
insertedRow = dataField.first;
|
||||
} else if (dataField is Map) {
|
||||
insertedRow = dataField;
|
||||
} else if (insertResValue['id'] != null) {
|
||||
insertedRow = insertResValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (insertedRow == null || insertedRow['id'] == null) {
|
||||
throw Exception('Unable to determine created team id');
|
||||
}
|
||||
final teamId = insertedRow['id'] as String;
|
||||
|
||||
if (selectedMembers.isNotEmpty) {
|
||||
final rows = selectedMembers
|
||||
.map((u) => {'team_id': teamId, 'user_id': u})
|
||||
.toList();
|
||||
final memRes = await client.from('team_members').insert(rows);
|
||||
if (memRes is Map && memRes['error'] != null) {
|
||||
final err = memRes['error'];
|
||||
throw Exception(
|
||||
err is Map
|
||||
? (err['message'] ?? err.toString())
|
||||
: err.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
// verify members persisted (handle Map or List response)
|
||||
final dynamic checkRes = await client
|
||||
.from('team_members')
|
||||
.select()
|
||||
.eq('team_id', teamId);
|
||||
if (checkRes is Map && checkRes['error'] != null) {
|
||||
throw Exception('Failed to verify team members');
|
||||
}
|
||||
final List persisted = checkRes is List
|
||||
? List.from(checkRes)
|
||||
: (checkRes is Map && checkRes['data'] is List)
|
||||
? List.from(checkRes['data'] as List)
|
||||
: <dynamic>[];
|
||||
if (persisted.length != selectedMembers.length) {
|
||||
throw Exception('Member count mismatch after save');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('Failed to save team: $e')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ref.invalidate(teamsProvider);
|
||||
ref.invalidate(teamMembersProvider);
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
if (isMobileDialog) {
|
||||
// Mobile: bottom sheet presentation
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: AppSurfaces.of(context).dialogShape,
|
||||
builder: (sheetContext) {
|
||||
return StatefulBuilder(
|
||||
builder: (sheetContext, setState) {
|
||||
final navigator = Navigator.of(sheetContext);
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 12,
|
||||
bottom: MediaQuery.of(sheetContext).viewInsets.bottom + 12,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isEdit ? 'Edit Team' : 'Add Team',
|
||||
style: Theme.of(sheetContext).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
buildFormContent(setState),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => navigator.pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () => onSave(setState, navigator),
|
||||
child: Text(isEdit ? 'Save' : 'Add'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Desktop / Tablet: centered fixed-width AlertDialog
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
|
||||
child: StatefulBuilder(
|
||||
builder: (dialogContext, setState) {
|
||||
final navigator = Navigator.of(dialogContext);
|
||||
return AlertDialog(
|
||||
shape: AppSurfaces.of(dialogContext).dialogShape,
|
||||
title: Text(isEdit ? 'Edit Team' : 'Add Team'),
|
||||
content: buildFormContent(setState),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => navigator.pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => onSave(setState, navigator),
|
||||
child: Text(isEdit ? 'Save' : 'Add'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _deleteTeam(BuildContext context, WidgetRef ref, String teamId) async {
|
||||
final navigator = Navigator.of(context);
|
||||
final confirmed = await showDialog<bool>(
|
||||
void _deleteTeam(BuildContext context, String teamId) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
|
||||
final confirmed = await showDialog<bool?>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
shape: AppSurfaces.of(context).dialogShape,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
shape: AppSurfaces.of(dialogContext).dialogShape,
|
||||
title: const Text('Delete Team'),
|
||||
content: const Text('Are you sure you want to delete this team?'),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: const Text('Are you sure you want to delete this team?'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => navigator.pop(false),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => navigator.pop(true),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
await Supabase.instance.client.from('teams').delete().eq('id', teamId);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
try {
|
||||
final delMembersRes = await Supabase.instance.client
|
||||
.from('team_members')
|
||||
.delete()
|
||||
.eq('team_id', teamId);
|
||||
if (delMembersRes is Map && delMembersRes['error'] != null) {
|
||||
final err = delMembersRes['error'];
|
||||
throw Exception(
|
||||
err is Map ? (err['message'] ?? err.toString()) : err.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
final delTeamRes = await Supabase.instance.client
|
||||
.from('teams')
|
||||
.delete()
|
||||
.eq('id', teamId);
|
||||
if (delTeamRes is Map && delTeamRes['error'] != null) {
|
||||
final err = delTeamRes['error'];
|
||||
throw Exception(
|
||||
err is Map ? (err['message'] ?? err.toString()) : err.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
ref.invalidate(teamsProvider);
|
||||
ref.invalidate(teamMembersProvider);
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('Failed to delete team: $e')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user