Files
tasq/lib/widgets/sync_pending_badge.dart
T
redz1029 57377b1b21 UI: Cyber/IT design overhaul with semantic status tokens (Material 3)
Re-theme TasQ with a refined enterprise "Cyber/IT" identity on Material 3.
Functionality is unchanged — only color/shape tokens and a few state widgets.

Foundation:
- Hand-tuned light + dark ColorSchemes (deep-slate surfaces, azure/teal/violet
  accents), tightened 12-16dp shape language, flat hairline-outlined cards.
- New AppStatusColors theme extension (success/warning/danger/info/neutral,
  per-brightness) replacing scattered raw Colors.green/orange/red/grey so status
  color is consistent and legible in dark mode.
- New TechChip mono primitive; StatusPill.kind; M3Card hairline variants;
  tightened AppSurfaces radii.

Per-screen:
- Tokenized status colors + radii across dashboard, tickets, tasks, IT service
  requests, attendance, whereabouts, network map, reports charts, auth, and
  shared/admin screens. Added _ticketStatusKind/_taskStatusKind/_isrStatusKind
  mappers so status carries meaning consistently.
- Fixed white pie-slice borders for dark mode; dropped ad-hoc dialog radius
  overrides in favor of the theme default. Categorical palettes (leave-type
  icons, team color picker, distribution charts) intentionally preserved.

Verified: flutter analyze lib clean; flutter build web green; visual QA in
light and dark (dashboard, tickets, attendance).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-26 15:13:47 +08:00

147 lines
3.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/connectivity_provider.dart';
import '../theme/app_status_colors.dart';
/// Two-state "this item is not yet synced" indicator.
///
/// Renders nothing when [isPending] is false. Otherwise shows:
/// - **Amber** chip with a static sync icon when the device is offline
/// ("Pending sync" — queued, will sync when network returns).
/// - **Blue** chip with a rotating sync icon when the device is online
/// ("Syncing…" — replay-in-flight or Brick queue draining).
///
/// Use [compact] for dense list rows where only the icon is needed.
class SyncPendingBadge extends ConsumerWidget {
/// Whether the entity associated with this badge is still queued for sync.
/// Caller is responsible for deriving this from the appropriate state
/// provider (e.g., `offlinePendingTasksProvider.any((t) => t.id == taskId)`).
final bool isPending;
/// Compact (icon-only) variant for dense list rows.
final bool compact;
/// Optional override label — defaults to "Pending sync" (offline) or
/// "Syncing…" (online).
final String? label;
const SyncPendingBadge({
super.key,
required this.isPending,
this.compact = false,
this.label,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
if (!isPending) return const SizedBox.shrink();
final isOnline = ref.watch(isOnlineProvider);
final scheme = Theme.of(context).colorScheme;
final status = AppStatusColors.of(context);
final Color bg;
final Color fg;
final IconData icon;
final String text;
final bool spin;
if (isOnline) {
// Replay-in-flight: blue, animated.
bg = scheme.primaryContainer;
fg = scheme.onPrimaryContainer;
icon = Icons.sync;
text = label ?? 'Syncing…';
spin = true;
} else {
// Queued offline: warning tone, static.
bg = status.warning.container;
fg = status.warning.onContainer;
icon = Icons.sync;
text = label ?? 'Pending sync';
spin = false;
}
final iconWidget = spin
? _RotatingIcon(icon: icon, color: fg, size: compact ? 12 : 14)
: Icon(icon, color: fg, size: compact ? 12 : 14);
if (compact) {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(8),
),
child: iconWidget,
);
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
iconWidget,
const SizedBox(width: 4),
Text(
text,
style: TextStyle(
color: fg,
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
),
],
),
);
}
}
class _RotatingIcon extends StatefulWidget {
final IconData icon;
final Color color;
final double size;
const _RotatingIcon({
required this.icon,
required this.color,
required this.size,
});
@override
State<_RotatingIcon> createState() => _RotatingIconState();
}
class _RotatingIconState extends State<_RotatingIcon>
with SingleTickerProviderStateMixin {
late final AnimationController _ctrl;
@override
void initState() {
super.initState();
_ctrl =
AnimationController(vsync: this, duration: const Duration(seconds: 2))
..repeat();
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: _ctrl,
child: Icon(widget.icon, color: widget.color, size: widget.size),
);
}
}