Added programmer role and fixed snackbar not showing
This commit is contained in:
parent
9f7791e56f
commit
81853c4367
|
|
@ -8,6 +8,7 @@ import 'models/profile.dart';
|
||||||
import 'providers/profile_provider.dart';
|
import 'providers/profile_provider.dart';
|
||||||
import 'services/background_location_service.dart';
|
import 'services/background_location_service.dart';
|
||||||
import 'theme/app_theme.dart';
|
import 'theme/app_theme.dart';
|
||||||
|
import 'utils/snackbar.dart';
|
||||||
|
|
||||||
class TasqApp extends ConsumerWidget {
|
class TasqApp extends ConsumerWidget {
|
||||||
const TasqApp({super.key});
|
const TasqApp({super.key});
|
||||||
|
|
@ -36,6 +37,7 @@ class TasqApp extends ConsumerWidget {
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
title: 'TasQ',
|
title: 'TasQ',
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
|
scaffoldMessengerKey: scaffoldMessengerKey,
|
||||||
theme: AppTheme.light(),
|
theme: AppTheme.light(),
|
||||||
darkTheme: AppTheme.dark(),
|
darkTheme: AppTheme.dark(),
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ final attendanceLogsProvider = StreamProvider<List<AttendanceLog>>((ref) {
|
||||||
|
|
||||||
final hasFullAccess =
|
final hasFullAccess =
|
||||||
profile.role == 'admin' ||
|
profile.role == 'admin' ||
|
||||||
|
profile.role == 'programmer' ||
|
||||||
profile.role == 'dispatcher' ||
|
profile.role == 'dispatcher' ||
|
||||||
profile.role == 'it_staff';
|
profile.role == 'it_staff';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ final leavesProvider = StreamProvider<List<LeaveOfAbsence>>((ref) {
|
||||||
|
|
||||||
final hasFullAccess =
|
final hasFullAccess =
|
||||||
profile.role == 'admin' ||
|
profile.role == 'admin' ||
|
||||||
|
profile.role == 'programmer' ||
|
||||||
profile.role == 'dispatcher' ||
|
profile.role == 'dispatcher' ||
|
||||||
profile.role == 'it_staff';
|
profile.role == 'it_staff';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@ final passSlipsProvider = StreamProvider<List<PassSlip>>((ref) {
|
||||||
if (profile == null) return Stream.value(const <PassSlip>[]);
|
if (profile == null) return Stream.value(const <PassSlip>[]);
|
||||||
|
|
||||||
final isAdmin = profile.role == 'admin' || profile.role == 'dispatcher';
|
final isAdmin = profile.role == 'admin' || profile.role == 'dispatcher';
|
||||||
|
final hasFullAccess = isAdmin || profile.role == 'programmer';
|
||||||
|
|
||||||
final wrapper = StreamRecoveryWrapper<PassSlip>(
|
final wrapper = StreamRecoveryWrapper<PassSlip>(
|
||||||
stream: isAdmin
|
stream: hasFullAccess
|
||||||
? client
|
? client
|
||||||
.from('pass_slips')
|
.from('pass_slips')
|
||||||
.stream(primaryKey: ['id'])
|
.stream(primaryKey: ['id'])
|
||||||
|
|
@ -30,7 +31,7 @@ final passSlipsProvider = StreamProvider<List<PassSlip>>((ref) {
|
||||||
.order('requested_at', ascending: false),
|
.order('requested_at', ascending: false),
|
||||||
onPollData: () async {
|
onPollData: () async {
|
||||||
final query = client.from('pass_slips').select();
|
final query = client.from('pass_slips').select();
|
||||||
final data = isAdmin
|
final data = hasFullAccess
|
||||||
? await query.order('requested_at', ascending: false)
|
? await query.order('requested_at', ascending: false)
|
||||||
: await query
|
: await query
|
||||||
.eq('user_id', profile.id)
|
.eq('user_id', profile.id)
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,8 @@ class ProfileController {
|
||||||
final isAdminProvider = Provider<bool>((ref) {
|
final isAdminProvider = Provider<bool>((ref) {
|
||||||
final profileAsync = ref.watch(currentProfileProvider);
|
final profileAsync = ref.watch(currentProfileProvider);
|
||||||
return profileAsync.maybeWhen(
|
return profileAsync.maybeWhen(
|
||||||
data: (profile) => profile?.role == 'admin',
|
data: (profile) =>
|
||||||
|
profile?.role == 'admin' || profile?.role == 'programmer',
|
||||||
orElse: () => false,
|
orElse: () => false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,7 @@ final tasksProvider = StreamProvider<List<Task>>((ref) {
|
||||||
|
|
||||||
final isGlobal =
|
final isGlobal =
|
||||||
profile.role == 'admin' ||
|
profile.role == 'admin' ||
|
||||||
|
profile.role == 'programmer' ||
|
||||||
profile.role == 'dispatcher' ||
|
profile.role == 'dispatcher' ||
|
||||||
profile.role == 'it_staff';
|
profile.role == 'it_staff';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ final ticketsProvider = StreamProvider<List<Ticket>>((ref) {
|
||||||
|
|
||||||
final isGlobal =
|
final isGlobal =
|
||||||
profile.role == 'admin' ||
|
profile.role == 'admin' ||
|
||||||
|
profile.role == 'programmer' ||
|
||||||
profile.role == 'dispatcher' ||
|
profile.role == 'dispatcher' ||
|
||||||
profile.role == 'it_staff';
|
profile.role == 'it_staff';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,10 @@ final swapRequestsProvider = StreamProvider<List<SwapRequest>>((ref) {
|
||||||
return Stream.value(const <SwapRequest>[]);
|
return Stream.value(const <SwapRequest>[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
final isAdmin = profile.role == 'admin' || profile.role == 'dispatcher';
|
final isAdmin =
|
||||||
|
profile.role == 'admin' ||
|
||||||
|
profile.role == 'programmer' ||
|
||||||
|
profile.role == 'dispatcher';
|
||||||
|
|
||||||
final wrapper = StreamRecoveryWrapper<SwapRequest>(
|
final wrapper = StreamRecoveryWrapper<SwapRequest>(
|
||||||
stream: isAdmin
|
stream: isAdmin
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,13 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
final role = profileAsync is AsyncData
|
final role = profileAsync is AsyncData
|
||||||
? (profileAsync.value)?.role
|
? (profileAsync.value)?.role
|
||||||
: null;
|
: null;
|
||||||
final isAdmin = role == 'admin';
|
final isAdmin = role == 'admin' || role == 'programmer';
|
||||||
final isReportsRoute = state.matchedLocation == '/reports';
|
final isReportsRoute = state.matchedLocation == '/reports';
|
||||||
final hasReportsAccess =
|
final hasReportsAccess =
|
||||||
role == 'admin' || role == 'dispatcher' || role == 'it_staff';
|
role == 'admin' ||
|
||||||
|
role == 'programmer' ||
|
||||||
|
role == 'dispatcher' ||
|
||||||
|
role == 'it_staff';
|
||||||
|
|
||||||
if (!isSignedIn && !isAuthRoute) {
|
if (!isSignedIn && !isAuthRoute) {
|
||||||
return '/login';
|
return '/login';
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
||||||
'standard',
|
'standard',
|
||||||
'dispatcher',
|
'dispatcher',
|
||||||
'it_staff',
|
'it_staff',
|
||||||
|
'programmer',
|
||||||
'admin',
|
'admin',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,9 +132,10 @@ class _AttendanceScreenState extends ConsumerState<AttendanceScreen>
|
||||||
ColorScheme colors,
|
ColorScheme colors,
|
||||||
Profile profile,
|
Profile profile,
|
||||||
) {
|
) {
|
||||||
final isAdmin = profile.role == 'admin';
|
final isAdmin = profile.role == 'admin' || profile.role == 'programmer';
|
||||||
final canFileLeave =
|
final canFileLeave =
|
||||||
profile.role == 'admin' ||
|
profile.role == 'admin' ||
|
||||||
|
profile.role == 'programmer' ||
|
||||||
profile.role == 'dispatcher' ||
|
profile.role == 'dispatcher' ||
|
||||||
profile.role == 'it_staff';
|
profile.role == 'it_staff';
|
||||||
|
|
||||||
|
|
@ -209,7 +210,7 @@ class _AttendanceScreenState extends ConsumerState<AttendanceScreen>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showPassSlipDialog(BuildContext context, Profile profile) {
|
void _showPassSlipDialog(BuildContext context, Profile profile) {
|
||||||
final isAdmin = profile.role == 'admin';
|
final isAdmin = profile.role == 'admin' || profile.role == 'programmer';
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
showWarningSnackBar(context, 'Admins cannot file pass slips.');
|
showWarningSnackBar(context, 'Admins cannot file pass slips.');
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,7 @@ final dashboardMetricsProvider = Provider<AsyncValue<DashboardMetrics>>((ref) {
|
||||||
.where(
|
.where(
|
||||||
(profile) =>
|
(profile) =>
|
||||||
profile.role == 'admin' ||
|
profile.role == 'admin' ||
|
||||||
|
profile.role == 'programmer' ||
|
||||||
profile.role == 'dispatcher' ||
|
profile.role == 'dispatcher' ||
|
||||||
profile.role == 'it_staff',
|
profile.role == 'it_staff',
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -241,9 +241,15 @@ class _ItServiceRequestDetailScreenState
|
||||||
if (profile == null) return false;
|
if (profile == null) return false;
|
||||||
final request = ref.read(itServiceRequestByIdProvider(widget.requestId));
|
final request = ref.read(itServiceRequestByIdProvider(widget.requestId));
|
||||||
if (request == null) return false;
|
if (request == null) return false;
|
||||||
// Admin, dispatcher can always edit; IT Staff and creator can edit in certain statuses
|
// Admin, programmer, dispatcher can always edit; IT Staff and creator can edit in certain statuses
|
||||||
if (profile.role == 'admin' || profile.role == 'dispatcher') return true;
|
if (profile.role == 'admin' ||
|
||||||
if (profile.role == 'it_staff') return true;
|
profile.role == 'programmer' ||
|
||||||
|
profile.role == 'dispatcher') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (profile.role == 'it_staff') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (request.creatorId == profile.id &&
|
if (request.creatorId == profile.id &&
|
||||||
(request.status == 'draft' || request.status == 'pending_approval')) {
|
(request.status == 'draft' || request.status == 'pending_approval')) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -259,7 +265,11 @@ class _ItServiceRequestDetailScreenState
|
||||||
bool get _canChangeStatus {
|
bool get _canChangeStatus {
|
||||||
final profile = ref.read(currentProfileProvider).valueOrNull;
|
final profile = ref.read(currentProfileProvider).valueOrNull;
|
||||||
if (profile == null) return false;
|
if (profile == null) return false;
|
||||||
if (profile.role == 'admin' || profile.role == 'dispatcher') return true;
|
if (profile.role == 'admin' ||
|
||||||
|
profile.role == 'programmer' ||
|
||||||
|
profile.role == 'dispatcher') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// Assigned IT staff can change status
|
// Assigned IT staff can change status
|
||||||
final assignments =
|
final assignments =
|
||||||
ref.read(itServiceRequestAssignmentsProvider).valueOrNull ?? [];
|
ref.read(itServiceRequestAssignmentsProvider).valueOrNull ?? [];
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ class _ItServiceRequestsListScreenState
|
||||||
final isPrivileged =
|
final isPrivileged =
|
||||||
currentProfile != null &&
|
currentProfile != null &&
|
||||||
(currentProfile.role == 'admin' ||
|
(currentProfile.role == 'admin' ||
|
||||||
|
currentProfile.role == 'programmer' ||
|
||||||
currentProfile.role == 'dispatcher' ||
|
currentProfile.role == 'dispatcher' ||
|
||||||
currentProfile.role == 'it_staff');
|
currentProfile.role == 'it_staff');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -663,8 +663,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showSuccessSnackBar(
|
showSuccessSnackBarGlobal(
|
||||||
context,
|
|
||||||
'Task resumed',
|
'Task resumed',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -682,16 +681,14 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showInfoSnackBar(
|
showInfoSnackBarGlobal(
|
||||||
context,
|
|
||||||
'Task paused',
|
'Task paused',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showErrorSnackBar(
|
showErrorSnackBarGlobal(
|
||||||
context,
|
|
||||||
e.toString(),
|
e.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -4310,8 +4307,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
// Validate IT staff assignment before starting or completing
|
// Validate IT staff assignment before starting or completing
|
||||||
if ((value == 'in_progress' || value == 'completed') &&
|
if ((value == 'in_progress' || value == 'completed') &&
|
||||||
!hasAssignedItStaff) {
|
!hasAssignedItStaff) {
|
||||||
showWarningSnackBar(
|
showWarningSnackBarGlobal(
|
||||||
context,
|
|
||||||
'Please assign at least one IT Staff member before ${value == 'in_progress' ? 'starting' : 'completing'} this task.',
|
'Please assign at least one IT Staff member before ${value == 'in_progress' ? 'starting' : 'completing'} this task.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
@ -4327,7 +4323,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// surface validation or other errors to user
|
// surface validation or other errors to user
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showErrorSnackBar(context, e.toString());
|
showErrorSnackBarGlobal(e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -4362,6 +4358,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
}
|
}
|
||||||
final isGlobal =
|
final isGlobal =
|
||||||
profile.role == 'admin' ||
|
profile.role == 'admin' ||
|
||||||
|
profile.role == 'programmer' ||
|
||||||
profile.role == 'dispatcher' ||
|
profile.role == 'dispatcher' ||
|
||||||
profile.role == 'it_staff';
|
profile.role == 'it_staff';
|
||||||
if (isGlobal) {
|
if (isGlobal) {
|
||||||
|
|
@ -4390,7 +4387,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
|
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showErrorSnackBar(context, 'Failed to read file');
|
showErrorSnackBarGlobal('Failed to read file');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -4399,7 +4396,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
const maxSizeBytes = 25 * 1024 * 1024;
|
const maxSizeBytes = 25 * 1024 * 1024;
|
||||||
if (bytes.length > maxSizeBytes) {
|
if (bytes.length > maxSizeBytes) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showErrorSnackBar(context, 'File size exceeds 25MB limit');
|
showErrorSnackBarGlobal('File size exceeds 25MB limit');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -4457,12 +4454,12 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
|
|
||||||
if (uploadSuccess) {
|
if (uploadSuccess) {
|
||||||
debugPrint('Showing success message and reloading attachments');
|
debugPrint('Showing success message and reloading attachments');
|
||||||
showSuccessSnackBar(context, 'File uploaded successfully');
|
showSuccessSnackBarGlobal('File uploaded successfully');
|
||||||
// Reload attachments list (non-blocking)
|
// Reload attachments list (non-blocking)
|
||||||
_loadAttachments(taskId);
|
_loadAttachments(taskId);
|
||||||
debugPrint('Attachment reload triggered');
|
debugPrint('Attachment reload triggered');
|
||||||
} else {
|
} else {
|
||||||
showErrorSnackBar(context, 'Upload failed: $errorMessage');
|
showErrorSnackBarGlobal('Upload failed: $errorMessage');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -4536,15 +4533,15 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (savePath != null && savePath.isNotEmpty) {
|
if (savePath != null && savePath.isNotEmpty) {
|
||||||
showSuccessSnackBar(context, 'File saved to: $savePath');
|
showSuccessSnackBarGlobal('File saved to: $savePath');
|
||||||
} else {
|
} else {
|
||||||
showInfoSnackBar(context, 'Download cancelled');
|
showInfoSnackBarGlobal('Download cancelled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Download error: $e');
|
debugPrint('Download error: $e');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showErrorSnackBar(context, 'Download error: $e');
|
showErrorSnackBarGlobal('Download error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4576,7 +4573,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showSuccessSnackBar(context, 'Attachment deleted');
|
showSuccessSnackBarGlobal('Attachment deleted');
|
||||||
// Reload attachments list
|
// Reload attachments list
|
||||||
await _loadAttachments(taskId);
|
await _loadAttachments(taskId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||||
data: (profile) =>
|
data: (profile) =>
|
||||||
profile != null &&
|
profile != null &&
|
||||||
(profile.role == 'admin' ||
|
(profile.role == 'admin' ||
|
||||||
|
profile.role == 'programmer' ||
|
||||||
profile.role == 'dispatcher' ||
|
profile.role == 'dispatcher' ||
|
||||||
profile.role == 'it_staff'),
|
profile.role == 'it_staff'),
|
||||||
orElse: () => false,
|
orElse: () => false,
|
||||||
|
|
@ -862,8 +863,7 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||||
);
|
);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(dialogContext).pop();
|
Navigator.of(dialogContext).pop();
|
||||||
showSuccessSnackBar(
|
showSuccessSnackBarGlobal(
|
||||||
context,
|
|
||||||
'Task "$title" has been created successfully.',
|
'Task "$title" has been created successfully.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
|
||||||
final canEdit =
|
final canEdit =
|
||||||
profile != null &&
|
profile != null &&
|
||||||
(profile.role == 'admin' ||
|
(profile.role == 'admin' ||
|
||||||
|
profile.role == 'programmer' ||
|
||||||
profile.role == 'dispatcher' ||
|
profile.role == 'dispatcher' ||
|
||||||
profile.role == 'it_staff' ||
|
profile.role == 'it_staff' ||
|
||||||
profile.id == ticket.creatorId);
|
profile.id == ticket.creatorId);
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ class WorkforceScreen extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final profileAsync = ref.watch(currentProfileProvider);
|
final profileAsync = ref.watch(currentProfileProvider);
|
||||||
final role = profileAsync.valueOrNull?.role ?? 'standard';
|
final role = profileAsync.valueOrNull?.role ?? 'standard';
|
||||||
final isAdmin = role == 'admin' || role == 'dispatcher';
|
final isAdmin =
|
||||||
|
role == 'admin' || role == 'programmer' || role == 'dispatcher';
|
||||||
|
|
||||||
return ResponsiveBody(
|
return ResponsiveBody(
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
|
|
@ -1486,6 +1487,7 @@ class _ScheduleGeneratorPanelState
|
||||||
(profile) =>
|
(profile) =>
|
||||||
profile.role == 'it_staff' ||
|
profile.role == 'it_staff' ||
|
||||||
profile.role == 'admin' ||
|
profile.role == 'admin' ||
|
||||||
|
profile.role == 'programmer' ||
|
||||||
profile.role == 'dispatcher',
|
profile.role == 'dispatcher',
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:awesome_snackbar_content/awesome_snackbar_content.dart';
|
import 'package:awesome_snackbar_content/awesome_snackbar_content.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
/// A global messenger key used to show snackbars from contexts without a
|
||||||
|
/// Scaffold (e.g. dialogs, background callbacks, or tests).
|
||||||
|
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
|
||||||
|
GlobalKey<ScaffoldMessengerState>();
|
||||||
|
|
||||||
/// Helper wrappers around `awesome_snackbar_content` so that callers
|
/// Helper wrappers around `awesome_snackbar_content` so that callers
|
||||||
/// can show snackbars with a consistent look and only specify a message and
|
/// can show snackbars with a consistent look and only specify a message and
|
||||||
|
|
@ -28,6 +34,7 @@ void showAwesomeSnackBar(
|
||||||
required String title,
|
required String title,
|
||||||
required String message,
|
required String message,
|
||||||
required SnackType snackType,
|
required SnackType snackType,
|
||||||
|
bool retry = true,
|
||||||
}) {
|
}) {
|
||||||
// Add margin and padding so even very short messages feel substantial.
|
// Add margin and padding so even very short messages feel substantial.
|
||||||
final snackBar = SnackBar(
|
final snackBar = SnackBar(
|
||||||
|
|
@ -47,39 +54,147 @@ void showAwesomeSnackBar(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context)
|
// Prefer a local ScaffoldMessenger when available, but prefer the global
|
||||||
..hideCurrentSnackBar()
|
// `scaffoldMessengerKey` if it's already ready. This increases reliability
|
||||||
..showSnackBar(snackBar);
|
// when callers are inside dialogs or other overlays.
|
||||||
|
final globalMessenger = scaffoldMessengerKey.currentState;
|
||||||
|
final localMessenger = ScaffoldMessenger.maybeOf(context);
|
||||||
|
final messenger = localMessenger ?? globalMessenger;
|
||||||
|
|
||||||
|
if (messenger == null) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'showAwesomeSnackBar: no messenger available; scheduling fallback',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the scaffold messenger is not available yet, schedule a post-frame
|
||||||
|
// callback that uses the global messenger (if it becomes available).
|
||||||
|
if (retry) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final gm = scaffoldMessengerKey.currentState;
|
||||||
|
if (gm != null) {
|
||||||
|
try {
|
||||||
|
gm
|
||||||
|
..hideCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'showAwesomeSnackBar: shown via global messenger (post-frame)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('showAwesomeSnackBar: post-frame show failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'showAwesomeSnackBar: global messenger still null after frame',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
messenger
|
||||||
|
..hideCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'showAwesomeSnackBar: shown via ${identical(messenger, globalMessenger) ? 'global' : 'local'} messenger',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) debugPrint('showAwesomeSnackBar: show failed: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showSuccessSnackBar(BuildContext context, String message) =>
|
void showSuccessSnackBar(BuildContext context, String message) {
|
||||||
showAwesomeSnackBar(
|
showSuccessSnackBarGlobal(message);
|
||||||
context,
|
}
|
||||||
title: 'Success',
|
|
||||||
message: message,
|
|
||||||
snackType: SnackType.success,
|
|
||||||
);
|
|
||||||
|
|
||||||
void showErrorSnackBar(BuildContext context, String message) =>
|
void showErrorSnackBar(BuildContext context, String message) {
|
||||||
showAwesomeSnackBar(
|
showErrorSnackBarGlobal(message);
|
||||||
context,
|
}
|
||||||
title: 'Error',
|
|
||||||
message: message,
|
|
||||||
snackType: SnackType.error,
|
|
||||||
);
|
|
||||||
|
|
||||||
void showInfoSnackBar(BuildContext context, String message) =>
|
void showInfoSnackBar(BuildContext context, String message) {
|
||||||
showAwesomeSnackBar(
|
showInfoSnackBarGlobal(message);
|
||||||
context,
|
}
|
||||||
title: 'Info',
|
|
||||||
message: message,
|
|
||||||
snackType: SnackType.info,
|
|
||||||
);
|
|
||||||
|
|
||||||
void showWarningSnackBar(BuildContext context, String message) =>
|
void showWarningSnackBar(BuildContext context, String message) {
|
||||||
showAwesomeSnackBar(
|
showWarningSnackBarGlobal(message);
|
||||||
context,
|
}
|
||||||
title: 'Warning',
|
|
||||||
message: message,
|
/// Global helpers that use the app-level `scaffoldMessengerKey` directly.
|
||||||
snackType: SnackType.warning,
|
/// Use these from places where a valid `BuildContext` with a Scaffold may
|
||||||
);
|
/// not be available (e.g. dialog builders or background callbacks).
|
||||||
|
void showAwesomeSnackBarGlobal({
|
||||||
|
required String title,
|
||||||
|
required String message,
|
||||||
|
required SnackType snackType,
|
||||||
|
}) {
|
||||||
|
final gm = scaffoldMessengerKey.currentState;
|
||||||
|
if (gm == null) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('showAwesomeSnackBarGlobal: global messenger null');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final snackBar = SnackBar(
|
||||||
|
elevation: 0,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
gm
|
||||||
|
..hideCurrentSnackBar()
|
||||||
|
..showSnackBar(snackBar);
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('showAwesomeSnackBarGlobal: shown');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('showAwesomeSnackBarGlobal: show failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showSuccessSnackBarGlobal(String message) => showAwesomeSnackBarGlobal(
|
||||||
|
title: 'Success',
|
||||||
|
message: message,
|
||||||
|
snackType: SnackType.success,
|
||||||
|
);
|
||||||
|
|
||||||
|
void showErrorSnackBarGlobal(String message) => showAwesomeSnackBarGlobal(
|
||||||
|
title: 'Error',
|
||||||
|
message: message,
|
||||||
|
snackType: SnackType.error,
|
||||||
|
);
|
||||||
|
|
||||||
|
void showInfoSnackBarGlobal(String message) => showAwesomeSnackBarGlobal(
|
||||||
|
title: 'Info',
|
||||||
|
message: message,
|
||||||
|
snackType: SnackType.info,
|
||||||
|
);
|
||||||
|
|
||||||
|
void showWarningSnackBarGlobal(String message) => showAwesomeSnackBarGlobal(
|
||||||
|
title: 'Warning',
|
||||||
|
message: message,
|
||||||
|
snackType: SnackType.warning,
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -404,7 +404,7 @@ List<NavSection> _buildSections(String role) {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (role == 'admin' || role == 'dispatcher') {
|
if (role == 'admin' || role == 'programmer' || role == 'dispatcher') {
|
||||||
return [
|
return [
|
||||||
NavSection(label: 'Operations', items: mainItems),
|
NavSection(label: 'Operations', items: mainItems),
|
||||||
NavSection(
|
NavSection(
|
||||||
|
|
@ -508,7 +508,7 @@ List<NavItem> _standardNavItems() {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<NavItem> _primaryItemsForRole(String role) {
|
List<NavItem> _primaryItemsForRole(String role) {
|
||||||
if (role == 'admin') {
|
if (role == 'admin' || role == 'programmer') {
|
||||||
return [
|
return [
|
||||||
NavItem(
|
NavItem(
|
||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- Add `programmer` to the `user_role` enum.
|
||||||
|
--
|
||||||
|
-- This is used by the application for role-based access checks. It must be
|
||||||
|
-- committed before any other schema objects (e.g., RLS policies) reference it.
|
||||||
|
|
||||||
|
ALTER TYPE user_role ADD VALUE IF NOT EXISTS 'programmer';
|
||||||
117
supabase/migrations/20260316090000_add_programmer_role.sql
Normal file
117
supabase/migrations/20260316090000_add_programmer_role.sql
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
-- Add `programmer` role to admin-level access checks without granting approval privileges.
|
||||||
|
--
|
||||||
|
-- NOTE: This role should have the same access as admins in the UI and for
|
||||||
|
-- non-approval data access. However, it must NOT be able to approve/reject
|
||||||
|
-- pass slips, leave applications, swap requests, etc.
|
||||||
|
|
||||||
|
-- NOTE: The `programmer` enum value is added in a prior migration so
|
||||||
|
-- it can safely be used in RLS policies and other schema objects.
|
||||||
|
|
||||||
|
-- Teams: allow programmers to manage teams like admins.
|
||||||
|
DROP POLICY IF EXISTS "Admins can manage teams (select)" ON public.teams;
|
||||||
|
DROP POLICY IF EXISTS "Admins can manage teams (write)" ON public.teams;
|
||||||
|
|
||||||
|
CREATE POLICY "Admins can manage teams (select)" ON public.teams
|
||||||
|
FOR SELECT
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'programmer')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE POLICY "Admins can manage teams (write)" ON public.teams
|
||||||
|
FOR ALL
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'programmer')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'programmer')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Team members: allow programmers to view/insert like admins.
|
||||||
|
DROP POLICY IF EXISTS "Admins can manage team_members (select)" ON public.team_members;
|
||||||
|
DROP POLICY IF EXISTS "Admins can manage team_members (write)" ON public.team_members;
|
||||||
|
|
||||||
|
CREATE POLICY "Admins can manage team_members (select)" ON public.team_members
|
||||||
|
FOR SELECT
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'programmer')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE POLICY "Admins can manage team_members (write)" ON public.team_members
|
||||||
|
FOR ALL
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'programmer')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'programmer')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Pass slips: allow programmers to view all slips like admins/dispatchers.
|
||||||
|
DROP POLICY IF EXISTS "pass_slips_select" ON pass_slips;
|
||||||
|
CREATE POLICY "pass_slips_select" ON pass_slips FOR SELECT TO authenticated
|
||||||
|
USING (
|
||||||
|
user_id = auth.uid()
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'dispatcher', 'programmer')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Leaves: allow programmers to view/file leaves like admins/dispatchers/it_staff.
|
||||||
|
DROP POLICY IF EXISTS "Privileged users can view all leaves" ON leave_of_absence;
|
||||||
|
CREATE POLICY "Privileged users can view all leaves"
|
||||||
|
ON leave_of_absence FOR SELECT
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM profiles
|
||||||
|
WHERE profiles.id = auth.uid()
|
||||||
|
AND profiles.role IN ('admin', 'dispatcher', 'it_staff', 'programmer')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Privileged users can file own leaves" ON leave_of_absence;
|
||||||
|
CREATE POLICY "Privileged users can file own leaves"
|
||||||
|
ON leave_of_absence FOR INSERT
|
||||||
|
WITH CHECK (
|
||||||
|
user_id = auth.uid()
|
||||||
|
AND filed_by = auth.uid()
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM profiles
|
||||||
|
WHERE profiles.id = auth.uid()
|
||||||
|
AND profiles.role IN ('admin', 'dispatcher', 'it_staff', 'programmer')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Swap request participants: allow programmers to view/insert participant rows.
|
||||||
|
DROP POLICY IF EXISTS "Swap participants: select" ON public.swap_request_participants;
|
||||||
|
CREATE POLICY "Swap participants: select" ON public.swap_request_participants
|
||||||
|
FOR SELECT
|
||||||
|
USING (
|
||||||
|
user_id = auth.uid()
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'dispatcher', 'programmer')
|
||||||
|
)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM public.swap_requests s WHERE s.id = swap_request_id AND (s.requester_id = auth.uid() OR s.recipient_id = auth.uid())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Swap participants: insert" ON public.swap_request_participants;
|
||||||
|
CREATE POLICY "Swap participants: insert" ON public.swap_request_participants
|
||||||
|
FOR INSERT
|
||||||
|
WITH CHECK (
|
||||||
|
user_id = auth.uid()
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'dispatcher', 'programmer')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
-- Ensure office management works for the new `programmer` role.
|
||||||
|
--
|
||||||
|
-- If RLS is enabled for offices, insert/update/delete operations can fail unless
|
||||||
|
-- there is an explicit policy allowing those roles.
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS offices ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Allow any authenticated user to read offices (used for dropdowns/filters).
|
||||||
|
DROP POLICY IF EXISTS "Offices: select auth" ON offices;
|
||||||
|
CREATE POLICY "Offices: select auth" ON offices
|
||||||
|
FOR SELECT
|
||||||
|
USING (auth.role() IS NOT NULL);
|
||||||
|
|
||||||
|
-- Allow admin/dispatcher/programmer to insert/update/delete offices.
|
||||||
|
DROP POLICY IF EXISTS "Offices: manage" ON offices;
|
||||||
|
CREATE POLICY "Offices: manage" ON offices
|
||||||
|
FOR ALL
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM profiles p
|
||||||
|
WHERE p.id = auth.uid()
|
||||||
|
AND p.role IN ('admin', 'dispatcher', 'programmer')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM profiles p
|
||||||
|
WHERE p.id = auth.uid()
|
||||||
|
AND p.role IN ('admin', 'dispatcher', 'programmer')
|
||||||
|
)
|
||||||
|
);
|
||||||
Loading…
Reference in New Issue
Block a user