tasq/lib/widgets/pass_slip_countdown_banner.dart
Marc Rejohn Castillano 049ab2c794 Added My Schedule tab in attendance screen
Allow 1 Day, Whole Week and Date Range swapping
2026-03-22 11:52:25 +08:00

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),
],
);
}
}