188 lines
5.6 KiB
Dart
188 lines
5.6 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../providers/pass_slip_provider.dart';
|
|
|
|
/// A persistent banner that shows a countdown to the pass slip 1-hour expiry.
|
|
///
|
|
/// Watches [activePassSlipProvider] for the current user's active pass slip.
|
|
/// When active, calculates remaining time until `slipStart + 1 hour`.
|
|
/// Auto-dismisses when the pass slip is completed.
|
|
/// Shows "EXCEEDED" when time has passed the 1-hour mark.
|
|
class PassSlipCountdownBanner extends ConsumerStatefulWidget {
|
|
const PassSlipCountdownBanner({required this.child, super.key});
|
|
|
|
final Widget child;
|
|
|
|
@override
|
|
ConsumerState<PassSlipCountdownBanner> createState() =>
|
|
_PassSlipCountdownBannerState();
|
|
}
|
|
|
|
class _PassSlipCountdownBannerState
|
|
extends ConsumerState<PassSlipCountdownBanner> {
|
|
Timer? _timer;
|
|
Duration _remaining = Duration.zero;
|
|
bool _exceeded = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _startTimer(DateTime expiresAt) {
|
|
_timer?.cancel();
|
|
_updateRemaining(expiresAt);
|
|
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
_updateRemaining(expiresAt);
|
|
});
|
|
}
|
|
|
|
void _stopTimer() {
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
if (mounted) {
|
|
setState(() {
|
|
_remaining = Duration.zero;
|
|
_exceeded = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _updateRemaining(DateTime expiresAt) {
|
|
final now = DateTime.now();
|
|
final diff = expiresAt.difference(now);
|
|
if (mounted) {
|
|
setState(() {
|
|
if (diff.isNegative || diff == Duration.zero) {
|
|
_remaining = Duration.zero;
|
|
_exceeded = true;
|
|
} else {
|
|
_remaining = diff;
|
|
_exceeded = false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
String _formatDuration(Duration d) {
|
|
final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
|
final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
|
if (d.inHours > 0) {
|
|
return '${d.inHours}:$minutes:$seconds';
|
|
}
|
|
return '$minutes:$seconds';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final activeSlip = ref.watch(activePassSlipProvider);
|
|
|
|
// Start/stop timer based on active pass slip state
|
|
if (activeSlip != null && activeSlip.slipStart != null) {
|
|
final expiresAt =
|
|
activeSlip.slipStart!.add(const Duration(hours: 1));
|
|
if (_timer == null) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_startTimer(expiresAt);
|
|
});
|
|
}
|
|
} else if (_timer != null) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_stopTimer();
|
|
});
|
|
}
|
|
|
|
final showBanner =
|
|
activeSlip != null && activeSlip.slipStart != null;
|
|
|
|
if (!showBanner) {
|
|
return widget.child;
|
|
}
|
|
|
|
final now = DateTime.now();
|
|
final hasStarted = now.isAfter(activeSlip.slipStart!) ||
|
|
now.isAtSameMomentAs(activeSlip.slipStart!);
|
|
|
|
final bool isUrgent;
|
|
final Color bgColor;
|
|
final Color fgColor;
|
|
final String message;
|
|
final IconData icon;
|
|
|
|
if (_exceeded) {
|
|
isUrgent = true;
|
|
bgColor = Theme.of(context).colorScheme.errorContainer;
|
|
fgColor = Theme.of(context).colorScheme.onErrorContainer;
|
|
message = 'Pass slip time EXCEEDED — Please return and complete it';
|
|
icon = Icons.warning_amber_rounded;
|
|
} else if (!hasStarted) {
|
|
isUrgent = false;
|
|
bgColor = Theme.of(context).colorScheme.primaryContainer;
|
|
fgColor = Theme.of(context).colorScheme.onPrimaryContainer;
|
|
final untilStart = activeSlip.slipStart!.difference(now);
|
|
message = 'Pass slip starts in ${_formatDuration(untilStart)}';
|
|
icon = Icons.schedule_rounded;
|
|
} else {
|
|
isUrgent = !_exceeded && _remaining.inMinutes < 5;
|
|
bgColor = isUrgent
|
|
? Theme.of(context).colorScheme.errorContainer
|
|
: Theme.of(context).colorScheme.tertiaryContainer;
|
|
fgColor = isUrgent
|
|
? Theme.of(context).colorScheme.onErrorContainer
|
|
: Theme.of(context).colorScheme.onTertiaryContainer;
|
|
message = 'Pass slip expires in ${_formatDuration(_remaining)}';
|
|
icon = Icons.directions_walk_rounded;
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
Material(
|
|
child: InkWell(
|
|
onTap: () => context.go('/attendance'),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
decoration: BoxDecoration(color: bgColor),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, size: 20, color: fgColor),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
message,
|
|
style:
|
|
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: fgColor,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
'Tap to complete',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: fgColor.withValues(alpha: 0.7),
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
Icon(
|
|
Icons.chevron_right,
|
|
size: 18,
|
|
color: fgColor.withValues(alpha: 0.7),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Expanded(child: widget.child),
|
|
],
|
|
);
|
|
}
|
|
}
|