A better state and alerts

This commit is contained in:
2026-02-23 20:19:07 +08:00
parent 0b900d3480
commit 1074572905
17 changed files with 673 additions and 428 deletions
+51 -30
View File
@@ -9,6 +9,7 @@ import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart';
import '../../theme/app_surfaces.dart';
import '../../widgets/tasq_adaptive_list.dart';
import '../../utils/snackbar.dart';
class OfficesScreen extends ConsumerStatefulWidget {
const OfficesScreen({super.key});
@@ -170,6 +171,7 @@ class _OfficesScreenState extends ConsumerState<OfficesScreen> {
context: context,
builder: (dialogContext) {
final servicesAsync = ref.watch(servicesOnceProvider);
bool saving = false;
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
@@ -184,6 +186,7 @@ class _OfficesScreenState extends ConsumerState<OfficesScreen> {
decoration: const InputDecoration(
labelText: 'Office name',
),
enabled: !saving,
),
const SizedBox(height: 12),
servicesAsync.when(
@@ -205,8 +208,9 @@ class _OfficesScreenState extends ConsumerState<OfficesScreen> {
),
),
],
onChanged: (v) =>
setState(() => selectedServiceId = v),
onChanged: saving
? null
: (v) => setState(() => selectedServiceId = v),
);
},
loading: () => const Padding(
@@ -220,37 +224,54 @@ class _OfficesScreenState extends ConsumerState<OfficesScreen> {
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
onPressed: saving
? null
: () => Navigator.of(dialogContext).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () async {
final name = nameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Name is required.')),
);
return;
}
final controller = ref.read(officesControllerProvider);
if (office == null) {
await controller.createOffice(
name: name,
serviceId: selectedServiceId,
);
} else {
await controller.updateOffice(
id: office.id,
name: name,
serviceId: selectedServiceId,
);
}
ref.invalidate(officesProvider);
if (context.mounted) {
Navigator.of(dialogContext).pop();
}
},
child: Text(office == null ? 'Create' : 'Save'),
onPressed: saving
? null
: () async {
final name = nameController.text.trim();
if (name.isEmpty) {
showWarningSnackBar(context, 'Name is required.');
return;
}
setState(() => saving = true);
final controller = ref.read(
officesControllerProvider,
);
if (office == null) {
await controller.createOffice(
name: name,
serviceId: selectedServiceId,
);
} else {
await controller.updateOffice(
id: office.id,
name: name,
serviceId: selectedServiceId,
);
}
ref.invalidate(officesProvider);
if (context.mounted) {
Navigator.of(dialogContext).pop();
showSuccessSnackBar(
context,
office == null
? 'Office "$name" has been created successfully.'
: 'Office "$name" has been updated successfully.',
);
}
},
child: saving
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(office == null ? 'Create' : 'Save'),
),
],
);
+15 -38
View File
@@ -16,6 +16,7 @@ import '../../utils/app_time.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart';
import '../../widgets/tasq_adaptive_list.dart';
import '../../utils/snackbar.dart';
class UserManagementScreen extends ConsumerStatefulWidget {
const UserManagementScreen({super.key});
@@ -501,16 +502,12 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
final role = _selectedRole ?? profile.role;
final fullName = _fullNameController.text.trim();
if (fullName.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Full name is required.')));
showWarningSnackBar(context, 'Full name is required.');
return false;
}
if (_selectedOfficeIds.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Select at least one office.')),
);
showWarningSnackBar(context, 'Select at least one office.');
return false;
}
@@ -542,15 +539,11 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
ref.invalidate(userOfficesProvider);
if (!context.mounted) return true;
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('User updated.')));
showSuccessSnackBar(context, 'User "$fullName" updated successfully.');
return true;
} catch (error) {
if (!context.mounted) return false;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Update failed: $error')));
showErrorSnackBar(context, 'Update failed: $error');
return false;
} finally {
setDialogState(() => _isSaving = false);
@@ -598,9 +591,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
if (!dialogContext.mounted) return;
Navigator.of(dialogContext).pop();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Password updated.')),
);
showSuccessSnackBar(context, 'Password updated.');
} catch (error) {
final msg = error.toString();
if (msg.contains('Unauthorized') ||
@@ -608,20 +599,15 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
msg.contains('expired')) {
await ref.read(authControllerProvider).signOut();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Session expired — please sign in again.',
),
),
showInfoSnackBar(
context,
'Session expired — please sign in again.',
);
return;
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Reset failed: $error')),
);
showErrorSnackBar(context, 'Reset failed: $error');
}
},
child: const Text('Update password'),
@@ -645,12 +631,9 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
ref.invalidate(currentProfileProvider);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
locked ? 'User locked (app-level).' : 'User unlocked (app-level).',
),
),
showInfoSnackBar(
context,
locked ? 'User locked (app-level).' : 'User unlocked (app-level).',
);
} catch (error) {
final msg = error.toString();
@@ -659,18 +642,12 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
msg.contains('expired')) {
await ref.read(authControllerProvider).signOut();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Session expired — please sign in again.'),
),
);
showInfoSnackBar(context, 'Session expired — please sign in again.');
return;
}
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Lock update failed: $error')));
showErrorSnackBar(context, 'Lock update failed: $error');
} finally {
if (mounted) {
setState(() => _isSaving = false);
+4 -9
View File
@@ -8,6 +8,7 @@ import 'package:go_router/go_router.dart';
import '../../providers/auth_provider.dart';
import '../../widgets/responsive_body.dart';
import '../../utils/snackbar.dart';
class LoginScreen extends ConsumerStatefulWidget {
const LoginScreen({super.key});
@@ -43,15 +44,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
if (response.session != null && mounted) {
context.go('/tickets');
} else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Check your email to confirm sign-in.')),
);
showInfoSnackBar(context, 'Check your email to confirm sign-in.');
}
} on Exception catch (error) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Sign in failed: $error')));
showErrorSnackBar(context, 'Sign in failed: $error');
}
} finally {
if (mounted) {
@@ -75,9 +72,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
}
} on Exception catch (error) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('OAuth failed: $error')));
showErrorSnackBar(context, 'OAuth failed: $error');
}
} finally {
if (mounted) {
+3 -6
View File
@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
import '../../providers/auth_provider.dart';
import '../../providers/tickets_provider.dart';
import '../../widgets/responsive_body.dart';
import '../../utils/snackbar.dart';
class SignUpScreen extends ConsumerStatefulWidget {
const SignUpScreen({super.key});
@@ -46,9 +47,7 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
Future<void> _handleSignUp() async {
if (!_formKey.currentState!.validate()) return;
if (_selectedOfficeIds.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Select at least one office.')),
);
showWarningSnackBar(context, 'Select at least one office.');
return;
}
setState(() => _isLoading = true);
@@ -66,9 +65,7 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
}
} on Exception catch (error) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Sign up failed: $error')));
showErrorSnackBar(context, 'Sign up failed: $error');
}
} finally {
if (mounted) {
+7 -18
View File
@@ -8,6 +8,7 @@ import '../../providers/tickets_provider.dart';
import '../../providers/user_offices_provider.dart';
import '../../widgets/multi_select_picker.dart';
import '../../widgets/responsive_body.dart';
import '../../utils/snackbar.dart';
class ProfileScreen extends ConsumerStatefulWidget {
const ProfileScreen({super.key});
@@ -265,17 +266,13 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
fullName: _fullNameController.text.trim(),
);
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Profile updated.')));
showSuccessSnackBar(context, 'Profile updated.');
// Refresh providers so other UI picks up the change immediately
ref.invalidate(currentProfileProvider);
ref.invalidate(profilesProvider);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Update failed: $e')));
showErrorSnackBar(context, 'Update failed: $e');
} finally {
if (mounted) setState(() => _savingDetails = false);
}
@@ -294,14 +291,10 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
_newPasswordController.clear();
_confirmPasswordController.clear();
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Password updated.')));
showSuccessSnackBar(context, 'Password updated.');
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Password update failed: $e')));
showErrorSnackBar(context, 'Password update failed: $e');
} finally {
if (mounted) setState(() => _changingPassword = false);
}
@@ -331,15 +324,11 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
}
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Offices updated.')));
showSuccessSnackBar(context, 'Offices updated.');
ref.invalidate(userOfficesProvider);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Failed to save offices: $e')));
showErrorSnackBar(context, 'Failed to save offices: $e');
} finally {
if (mounted) setState(() => _savingOffices = false);
}
+7 -16
View File
@@ -29,6 +29,7 @@ import '../../widgets/status_pill.dart';
import '../../theme/app_surfaces.dart';
import '../../widgets/task_assignment_section.dart';
import '../../widgets/typing_dots.dart';
import '../../utils/snackbar.dart';
// Simple image embed builder to support data-URI and network images
class _ImageEmbedBuilder extends quill.EmbedBuilder {
@@ -1669,10 +1670,6 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
final ext =
file.extension ??
'png';
final messenger =
ScaffoldMessenger.of(
context,
);
String? url;
try {
url = await ref
@@ -1685,22 +1682,16 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
extension: ext,
);
} catch (e) {
messenger.showSnackBar(
SnackBar(
content: Text(
'Upload error: $e',
),
),
showErrorSnackBar(
context,
'Upload error: $e',
);
return;
}
if (url == null) {
messenger.showSnackBar(
const SnackBar(
content: Text(
'Image upload failed (no URL returned)',
),
),
showErrorSnackBar(
context,
'Image upload failed (no URL returned)',
);
return;
}
+58 -31
View File
@@ -19,6 +19,7 @@ import '../../widgets/responsive_body.dart';
import '../../widgets/tasq_adaptive_list.dart';
import '../../widgets/typing_dots.dart';
import '../../theme/app_surfaces.dart';
import '../../utils/snackbar.dart';
// request metadata options used in task creation/editing dialogs
const List<String> _requestTypeOptions = [
@@ -426,9 +427,10 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
await showDialog<void>(
context: context,
builder: (dialogContext) {
bool saving = false;
final officesAsync = ref.watch(officesProvider);
return StatefulBuilder(
builder: (context, setState) {
final officesAsync = ref.watch(officesProvider);
return AlertDialog(
shape: AppSurfaces.of(context).dialogShape,
title: const Text('Create Task'),
@@ -442,6 +444,7 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
decoration: const InputDecoration(
labelText: 'Task title',
),
enabled: !saving,
),
const SizedBox(height: 12),
TextField(
@@ -450,6 +453,7 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
labelText: 'Description',
),
maxLines: 3,
enabled: !saving,
),
const SizedBox(height: 12),
officesAsync.when(
@@ -474,8 +478,11 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
),
)
.toList(),
onChanged: (value) =>
setState(() => selectedOfficeId = value),
onChanged: saving
? null
: (value) => setState(
() => selectedOfficeId = value,
),
),
const SizedBox(height: 12),
// optional request metadata inputs
@@ -492,8 +499,11 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
),
)
.toList(),
onChanged: (value) =>
setState(() => selectedRequestType = value),
onChanged: saving
? null
: (value) => setState(
() => selectedRequestType = value,
),
),
if (selectedRequestType == 'Other') ...[
const SizedBox(height: 8),
@@ -518,9 +528,11 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
),
)
.toList(),
onChanged: (value) => setState(
() => selectedRequestCategory = value,
),
onChanged: saving
? null
: (value) => setState(
() => selectedRequestCategory = value,
),
),
],
);
@@ -537,32 +549,47 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
onPressed: saving
? null
: () => Navigator.of(dialogContext).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () async {
final title = titleController.text.trim();
final description = descriptionController.text.trim();
final officeId = selectedOfficeId;
if (title.isEmpty || officeId == null) {
return;
}
await ref
.read(tasksControllerProvider)
.createTask(
title: title,
description: description,
officeId: officeId,
requestType: selectedRequestType,
requestTypeOther: requestTypeOther,
requestCategory: selectedRequestCategory,
);
if (context.mounted) {
Navigator.of(dialogContext).pop();
}
},
child: const Text('Create'),
onPressed: saving
? null
: () async {
final title = titleController.text.trim();
final description = descriptionController.text.trim();
final officeId = selectedOfficeId;
if (title.isEmpty || officeId == null) {
return;
}
setState(() => saving = true);
await ref
.read(tasksControllerProvider)
.createTask(
title: title,
description: description,
officeId: officeId,
requestType: selectedRequestType,
requestTypeOther: requestTypeOther,
requestCategory: selectedRequestCategory,
);
if (context.mounted) {
Navigator.of(dialogContext).pop();
showSuccessSnackBar(
context,
'Task "$title" has been created successfully.',
);
}
},
child: saving
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Create'),
),
],
);
+12 -20
View File
@@ -11,6 +11,7 @@ import '../../utils/supabase_response.dart';
import 'package:tasq/widgets/multi_select_picker.dart';
import '../../theme/app_surfaces.dart';
import '../../widgets/tasq_adaptive_list.dart';
import '../../utils/snackbar.dart';
// Note: `officesProvider` is provided globally in `tickets_provider.dart` so
// we reuse that StreamProvider here (avoids duplicate queries).
@@ -368,26 +369,18 @@ class _TeamsScreenState extends ConsumerState<TeamsScreen> {
}
Future<void> onSave(StateSetter setState, NavigatorState navigator) async {
final messenger = ScaffoldMessenger.of(navigator.context);
final ctx = context; // capture for async use
final name = nameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Team name is required')));
showWarningSnackBar(ctx, 'Team name is required');
return;
}
if (leaderId == null) {
messenger.showSnackBar(
const SnackBar(content: Text('Please select a team leader')),
);
showWarningSnackBar(ctx, 'Please select a team leader');
return;
}
if (selectedOffices.isEmpty) {
messenger.showSnackBar(
const SnackBar(
content: Text('Assign at least one office to the team'),
),
);
showWarningSnackBar(ctx, 'Assign at least one office to the team');
return;
}
@@ -506,9 +499,9 @@ class _TeamsScreenState extends ConsumerState<TeamsScreen> {
}
}
} catch (e) {
messenger.showSnackBar(
SnackBar(content: Text('Failed to save team: $e')),
);
if (!mounted) return;
// ignore: use_build_context_synchronously
showErrorSnackBar(context, 'Failed to save team: $e');
return;
}
@@ -604,8 +597,6 @@ class _TeamsScreenState extends ConsumerState<TeamsScreen> {
}
void _deleteTeam(BuildContext context, String teamId) async {
final messenger = ScaffoldMessenger.of(context);
final confirmed = await showDialog<bool?>(
context: context,
builder: (dialogContext) => AlertDialog(
@@ -630,6 +621,7 @@ class _TeamsScreenState extends ConsumerState<TeamsScreen> {
if (confirmed != true) return;
final ctx = context; // capture before async gaps
try {
final delMembersRes = await ref
.read(supabaseClientProvider)
@@ -650,9 +642,9 @@ class _TeamsScreenState extends ConsumerState<TeamsScreen> {
ref.invalidate(teamsProvider);
ref.invalidate(teamMembersProvider);
} catch (e) {
messenger.showSnackBar(
SnackBar(content: Text('Failed to delete team: $e')),
);
if (!mounted) return;
// ignore: use_build_context_synchronously
showErrorSnackBar(ctx, 'Failed to delete team: $e');
return;
}
}
+50 -28
View File
@@ -16,6 +16,7 @@ import '../../widgets/responsive_body.dart';
import '../../widgets/tasq_adaptive_list.dart';
import '../../widgets/typing_dots.dart';
import '../../theme/app_surfaces.dart';
import '../../utils/snackbar.dart';
class TicketsListScreen extends ConsumerStatefulWidget {
const TicketsListScreen({super.key});
@@ -319,6 +320,7 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
await showDialog<void>(
context: context,
builder: (dialogContext) {
bool saving = false;
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
@@ -337,6 +339,7 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
decoration: const InputDecoration(
labelText: 'Subject',
),
enabled: !saving,
),
const SizedBox(height: 12),
TextField(
@@ -345,6 +348,7 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
labelText: 'Description',
),
maxLines: 3,
enabled: !saving,
),
const SizedBox(height: 12),
officesAsync.when(
@@ -364,8 +368,10 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
),
)
.toList(),
onChanged: (value) =>
setState(() => selectedOffice = value),
onChanged: saving
? null
: (value) =>
setState(() => selectedOffice = value),
decoration: const InputDecoration(
labelText: 'Office',
),
@@ -382,35 +388,51 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
onPressed: saving
? null
: () => Navigator.of(dialogContext).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () async {
final subject = subjectController.text.trim();
final description = descriptionController.text.trim();
if (subject.isEmpty ||
description.isEmpty ||
selectedOffice == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Fill out all fields.')),
);
return;
}
await ref
.read(ticketsControllerProvider)
.createTicket(
subject: subject,
description: description,
officeId: selectedOffice!.id,
);
// Supabase stream will emit the new ticket — no explicit
// invalidation required and avoids a temporary reload.
if (context.mounted) {
Navigator.of(dialogContext).pop();
}
},
child: const Text('Create'),
onPressed: saving
? null
: () async {
final subject = subjectController.text.trim();
final description = descriptionController.text.trim();
if (subject.isEmpty ||
description.isEmpty ||
selectedOffice == null) {
showWarningSnackBar(
context,
'Fill out all fields.',
);
return;
}
setState(() => saving = true);
await ref
.read(ticketsControllerProvider)
.createTicket(
subject: subject,
description: description,
officeId: selectedOffice!.id,
);
// Supabase stream will emit the new ticket — no explicit
// invalidation required and avoids a temporary reload.
if (context.mounted) {
Navigator.of(dialogContext).pop();
showSuccessSnackBar(
context,
'Ticket "$subject" has been created successfully.',
);
}
},
child: saving
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Create'),
),
],
);
+45 -23
View File
@@ -13,6 +13,7 @@ import '../../providers/profile_provider.dart';
import '../../providers/workforce_provider.dart';
import '../../widgets/responsive_body.dart';
import '../../theme/app_surfaces.dart';
import '../../utils/snackbar.dart';
class WorkforceScreen extends ConsumerWidget {
const WorkforceScreen({super.key});
@@ -438,7 +439,11 @@ class _ScheduleTile extends ConsumerWidget {
.where((profile) => profile.id != currentUserId)
.toList();
if (staff.isEmpty) {
_showMessage(context, 'No IT staff available for swaps.');
_showMessage(
context,
'No IT staff available for swaps.',
type: SnackType.warning,
);
return;
}
@@ -572,17 +577,36 @@ class _ScheduleTile extends ConsumerWidget {
);
ref.invalidate(swapRequestsProvider);
if (!context.mounted) return;
_showMessage(context, 'Swap request sent.');
_showMessage(context, 'Swap request sent.', type: SnackType.success);
} catch (error) {
if (!context.mounted) return;
_showMessage(context, 'Swap request failed: $error');
_showMessage(
context,
'Swap request failed: $error',
type: SnackType.error,
);
}
}
void _showMessage(BuildContext context, String message) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
void _showMessage(
BuildContext context,
String message, {
SnackType type = SnackType.warning,
}) {
switch (type) {
case SnackType.success:
showSuccessSnackBar(context, message);
break;
case SnackType.error:
showErrorSnackBar(context, message);
break;
case SnackType.info:
showInfoSnackBar(context, message);
break;
case SnackType.warning:
showWarningSnackBar(context, message);
break;
}
}
Future<void> _showAlert(
@@ -647,7 +671,11 @@ class _ScheduleTile extends ConsumerWidget {
controller.animateTo(1);
return;
}
_showMessage(context, 'Swap request already sent. See Swaps panel.');
_showMessage(
context,
'Swap request already sent. See Swaps panel.',
type: SnackType.info,
);
}
String _statusLabel(String status) {
@@ -884,11 +912,11 @@ class _ScheduleGeneratorPanelState
final start = _startDate;
final end = _endDate;
if (start == null || end == null) {
_showMessage('Select a date range.');
showWarningSnackBar(context, 'Select a date range.');
return;
}
if (end.isBefore(start)) {
_showMessage('End date must be after start date.');
showWarningSnackBar(context, 'End date must be after start date.');
return;
}
setState(() => _isGenerating = true);
@@ -896,7 +924,7 @@ class _ScheduleGeneratorPanelState
final staff = _sortedStaff();
if (staff.isEmpty) {
if (!mounted) return;
_showMessage('No IT staff available for scheduling.');
showWarningSnackBar(context, 'No IT staff available for scheduling.');
return;
}
@@ -919,7 +947,7 @@ class _ScheduleGeneratorPanelState
});
if (generated.isEmpty) {
_showMessage('No shifts could be generated.');
showInfoSnackBar(context, 'No shifts could be generated.');
}
} finally {
if (mounted) {
@@ -1099,7 +1127,7 @@ class _ScheduleGeneratorPanelState
Future<void> _openDraftEditor({_DraftSchedule? existing}) async {
final staff = _sortedStaff();
if (staff.isEmpty) {
_showMessage('No IT staff available.');
showWarningSnackBar(context, 'No IT staff available.');
return;
}
@@ -1305,14 +1333,14 @@ class _ScheduleGeneratorPanelState
final start = _startDate;
final end = _endDate;
if (start == null || end == null) {
_showMessage('Select a date range.');
showWarningSnackBar(context, 'Select a date range.');
return;
}
final schedules = ref.read(dutySchedulesProvider).valueOrNull ?? [];
final conflict = _findConflict(_draftSchedules, schedules);
if (conflict != null) {
_showMessage(conflict);
showInfoSnackBar(context, conflict);
return;
}
@@ -1334,14 +1362,14 @@ class _ScheduleGeneratorPanelState
await ref.read(workforceControllerProvider).insertSchedules(payload);
ref.invalidate(dutySchedulesProvider);
if (!mounted) return;
_showMessage('Schedule committed.');
showSuccessSnackBar(context, 'Schedule committed.');
setState(() {
_draftSchedules = [];
_warnings = [];
});
} catch (error) {
if (!mounted) return;
_showMessage('Commit failed: $error');
showErrorSnackBar(context, 'Commit failed: $error');
} finally {
if (mounted) {
setState(() => _isSaving = false);
@@ -1762,12 +1790,6 @@ class _ScheduleGeneratorPanelState
return value;
}
}
void _showMessage(String message) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
}
class _SwapRequestsPanel extends ConsumerWidget {
+85
View File
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:awesome_snackbar_content/awesome_snackbar_content.dart';
/// Helper wrappers around `awesome_snackbar_content` so that callers
/// can show snackbars with a consistent look and only specify a message and
/// semantic type.
/// Enumeration used by callers; the mapping to the packages
/// [ContentType] is internal.
enum SnackType { success, info, warning, error }
ContentType _mapSnackType(SnackType t) {
switch (t) {
case SnackType.success:
return ContentType.success;
case SnackType.info:
return ContentType.help;
case SnackType.warning:
return ContentType.warning;
case SnackType.error:
return ContentType.failure;
}
}
/// Core function used by all of the convenience helpers below.
void showAwesomeSnackBar(
BuildContext context, {
required String title,
required String message,
required SnackType snackType,
}) {
// Add margin and padding so even very short messages feel substantial.
final snackBar = SnackBar(
elevation: 0,
behavior: SnackBarBehavior.floating,
// give floating snackbar some breathing room
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
content: Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
child: AwesomeSnackbarContent(
title: title,
message: message,
contentType: _mapSnackType(snackType),
),
),
);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(snackBar);
}
void showSuccessSnackBar(BuildContext context, String message) =>
showAwesomeSnackBar(
context,
title: 'Success',
message: message,
snackType: SnackType.success,
);
void showErrorSnackBar(BuildContext context, String message) =>
showAwesomeSnackBar(
context,
title: 'Error',
message: message,
snackType: SnackType.error,
);
void showInfoSnackBar(BuildContext context, String message) =>
showAwesomeSnackBar(
context,
title: 'Info',
message: message,
snackType: SnackType.info,
);
void showWarningSnackBar(BuildContext context, String message) =>
showAwesomeSnackBar(
context,
title: 'Warning',
message: message,
snackType: SnackType.warning,
);