| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:go_router/go_router.dart';
- import 'package:google_fonts/google_fonts.dart';
- import 'package:intl/intl.dart';
- import '../../../database/database.dart';
- import '../../../providers/auth_provider.dart';
- import '../../../providers/sync_provider.dart';
- class SyncPage extends ConsumerStatefulWidget {
- const SyncPage({super.key});
- @override
- ConsumerState<SyncPage> createState() => _SyncPageState();
- }
- class _SyncPageState extends ConsumerState<SyncPage>
- with SingleTickerProviderStateMixin {
- bool _forceFullSync = false;
- late AnimationController _syncIconController;
- @override
- void initState() {
- super.initState();
- _syncIconController = AnimationController(
- vsync: this,
- duration: const Duration(seconds: 2),
- );
- }
- @override
- void dispose() {
- _syncIconController.dispose();
- super.dispose();
- }
- String _formatLastSyncTime(DateTime? time) {
- if (time == null) return 'Mai';
- final now = DateTime.now();
- final today = DateTime(now.year, now.month, now.day);
- final syncDate = DateTime(time.year, time.month, time.day);
- final timeStr = DateFormat('HH:mm').format(time);
- if (syncDate == today) {
- return 'Oggi, $timeStr';
- } else if (syncDate == today.subtract(const Duration(days: 1))) {
- return 'Ieri, $timeStr';
- } else {
- return DateFormat('dd/MM/yyyy, HH:mm').format(time);
- }
- }
- @override
- Widget build(BuildContext context) {
- final authState = ref.watch(authProvider);
- final syncState = ref.watch(syncProvider);
- final theme = Theme.of(context);
- final agente = authState.agente;
- final colorScheme = theme.colorScheme;
- ref.listen<SyncState>(syncProvider, (prev, next) {
- if (next.isSyncing && !(prev?.isSyncing ?? false)) {
- _syncIconController.repeat();
- } else if (!next.isSyncing && (prev?.isSyncing ?? false)) {
- _syncIconController.stop();
- _syncIconController.reset();
- if (next.error == null && next.progress >= 1.0) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Trasmissione terminata')),
- );
- }
- }
- });
- return Scaffold(
- appBar: AppBar(
- title: const Text('Sincronizzazione'),
- actions: [
- IconButton(
- icon: const Icon(Icons.settings),
- onPressed: () => context.push('/dashboard/sync/settings'),
- ),
- ],
- ),
- body: SingleChildScrollView(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- // Hero section: Toggle + Start button
- LayoutBuilder(
- builder: (context, constraints) {
- final isWide = constraints.maxWidth > 600;
- if (isWide) {
- return Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(flex: 1, child: _buildToggleCard(context, syncState)),
- const SizedBox(width: 16),
- Expanded(
- flex: 2,
- child: _buildActionCard(
- context,
- agente,
- syncState,
- colorScheme,
- ),
- ),
- ],
- );
- }
- return Column(
- children: [
- _buildToggleCard(context, syncState),
- const SizedBox(height: 16),
- _buildActionCard(context, agente, syncState, colorScheme),
- ],
- );
- },
- ),
- const SizedBox(height: 16),
- // Progress section
- if (syncState.phases.isNotEmpty || syncState.isSyncing || syncState.progress >= 1.0)
- _buildProgressSection(context, syncState, colorScheme),
- // Error section
- if (syncState.error != null) ...[
- const SizedBox(height: 16),
- _buildErrorCard(context, syncState, colorScheme),
- ],
- ],
- ),
- ),
- );
- }
- Widget _buildToggleCard(BuildContext context, SyncState syncState) {
- final theme = Theme.of(context);
- return Card(
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'Tipo Sincronizzazione',
- style: theme.textTheme.titleLarge,
- ),
- const SizedBox(height: 8),
- Text(
- 'Scegli tra sincronizzazione parziale delle variazioni o scaricamento integrale di tutti i dati dal server.',
- style: theme.textTheme.bodyMedium?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
- ),
- ),
- const SizedBox(height: 16),
- Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: theme.colorScheme.surface,
- borderRadius: BorderRadius.circular(8),
- border: Border.all(
- color: theme.colorScheme.outlineVariant.withValues(alpha: 0.3),
- ),
- ),
- child: Row(
- children: [
- Switch(
- value: _forceFullSync,
- onChanged: syncState.isSyncing
- ? null
- : (v) => setState(() => _forceFullSync = v),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: Text(
- 'Sincronizzazione completa',
- style: theme.textTheme.labelLarge,
- ),
- ),
- ],
- ),
- ),
- ],
- ),
- ),
- );
- }
- Widget _buildActionCard(
- BuildContext context,
- Agente? agente,
- SyncState syncState,
- ColorScheme colorScheme,
- ) {
- final theme = Theme.of(context);
- return Card(
- child: Padding(
- padding: const EdgeInsets.all(24),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- FilledButton.icon(
- onPressed: syncState.isSyncing || agente == null
- ? null
- : () => ref.read(syncProvider.notifier).startFullSync(
- agente.idAgente,
- agente.codAgente!,
- forceFullSync: _forceFullSync,
- ),
- icon: syncState.isSyncing
- ? RotationTransition(
- turns: _syncIconController,
- child: const Icon(Icons.sync),
- )
- : const Icon(Icons.sync),
- label: Text(
- syncState.isSyncing ? 'Sincronizzazione in corso...' : 'Avvia Sincronizzazione',
- ),
- style: FilledButton.styleFrom(
- padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20),
- textStyle: theme.textTheme.titleLarge,
- ),
- ),
- const SizedBox(height: 12),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- Icons.schedule,
- size: 16,
- color: colorScheme.onSurfaceVariant,
- ),
- const SizedBox(width: 6),
- Text(
- 'Ultimo aggiornamento: ${_formatLastSyncTime(syncState.lastSyncTime)}',
- style: theme.textTheme.bodyMedium?.copyWith(
- color: colorScheme.onSurfaceVariant,
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- );
- }
- Widget _buildProgressSection(
- BuildContext context,
- SyncState syncState,
- ColorScheme colorScheme,
- ) {
- final theme = Theme.of(context);
- final percent = syncState.phases.isNotEmpty
- ? (syncState.progress * 100).toInt()
- : 0;
- return Card(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- // Header
- Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
- decoration: BoxDecoration(
- color: theme.colorScheme.surface,
- border: Border(
- bottom: BorderSide(color: theme.colorScheme.outlineVariant),
- ),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- syncState.currentAgentCode != null
- ? 'Sincronizzazione dati agente (${syncState.currentAgentCode})'
- : 'Sincronizzazione dati agente',
- style: theme.textTheme.titleLarge,
- ),
- if (syncState.agentiCodici.isNotEmpty)
- RichText(
- text: TextSpan(
- style: theme.textTheme.bodyMedium?.copyWith(
- color: colorScheme.onSurfaceVariant,
- ),
- children: [
- const TextSpan(
- text:
- 'Stato complessivo del processo (Agenti: '),
- for (int i = 0;
- i < syncState.agentiCodici.length;
- i++) ...[
- if (i > 0) const TextSpan(text: ', '),
- TextSpan(
- text: syncState.agentiCodici[i],
- style: syncState.agentiCodici[i] ==
- syncState.currentAgentCode
- ? TextStyle(
- fontWeight: FontWeight.bold,
- color: colorScheme.onSurfaceVariant,
- )
- : null,
- ),
- ],
- const TextSpan(text: ')'),
- ],
- ),
- )
- else
- Text(
- 'Stato complessivo del processo',
- style: theme.textTheme.bodyMedium?.copyWith(
- color: colorScheme.onSurfaceVariant,
- ),
- ),
- ],
- ),
- Text(
- '$percent%',
- style: theme.textTheme.headlineMedium?.copyWith(
- color: colorScheme.secondary,
- ),
- ),
- ],
- ),
- ),
- // Progress bar + phases
- Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- // Main progress bar
- ClipRRect(
- borderRadius: BorderRadius.circular(4),
- child: LinearProgressIndicator(
- value: syncState.progress,
- minHeight: 8,
- backgroundColor: theme.colorScheme.surfaceContainerHighest,
- valueColor: AlwaysStoppedAnimation<Color>(
- colorScheme.secondary,
- ),
- ),
- ),
- const SizedBox(height: 16),
- // Phases grid
- ...syncState.phases.asMap().entries.map(
- (entry) => _buildPhaseRow(
- context,
- entry.key,
- entry.value,
- colorScheme,
- ),
- ),
- ],
- ),
- ),
- ],
- ),
- );
- }
- Widget _buildPhaseRow(
- BuildContext context,
- int index,
- SyncPhaseInfo phase,
- ColorScheme colorScheme,
- ) {
- final theme = Theme.of(context);
- final isCompleted = phase.status == SyncPhaseStatus.completed;
- return Container(
- margin: const EdgeInsets.only(bottom: 8),
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
- decoration: BoxDecoration(
- color: isCompleted
- ? Colors.green.shade50
- : theme.colorScheme.surface,
- borderRadius: BorderRadius.circular(8),
- border: Border.all(
- color: theme.colorScheme.outlineVariant.withValues(alpha: 0.2),
- ),
- ),
- child: Row(
- children: [
- // Status dot
- _buildStatusDot(phase.status, colorScheme),
- const SizedBox(width: 12),
- // Phase name
- Expanded(
- child: Text(
- phase.name,
- style: theme.textTheme.bodyMedium?.copyWith(
- color: isCompleted
- ? Colors.green.shade700
- : theme.colorScheme.onSurface,
- ),
- ),
- ),
- // Status text
- _buildStatusText(phase, colorScheme),
- ],
- ),
- );
- }
- Widget _buildStatusDot(SyncPhaseStatus status, ColorScheme colorScheme) {
- Color dotColor;
- List<BoxShadow>? shadow;
- switch (status) {
- case SyncPhaseStatus.waiting:
- dotColor = colorScheme.outlineVariant;
- break;
- case SyncPhaseStatus.downloading:
- case SyncPhaseStatus.processing:
- dotColor = colorScheme.secondary;
- shadow = [
- BoxShadow(
- color: colorScheme.secondary.withValues(alpha: 0.5),
- blurRadius: 6,
- ),
- ];
- break;
- case SyncPhaseStatus.completed:
- dotColor = Colors.green;
- shadow = [
- BoxShadow(
- color: Colors.green.withValues(alpha: 0.5),
- blurRadius: 6,
- ),
- ];
- }
- return AnimatedContainer(
- duration: const Duration(milliseconds: 300),
- width: 10,
- height: 10,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- color: dotColor,
- boxShadow: shadow,
- ),
- );
- }
- Widget _buildStatusText(SyncPhaseInfo phase, ColorScheme colorScheme) {
- Color textColor;
- switch (phase.status) {
- case SyncPhaseStatus.waiting:
- textColor = colorScheme.onSurfaceVariant;
- break;
- case SyncPhaseStatus.downloading:
- case SyncPhaseStatus.processing:
- textColor = colorScheme.secondary;
- break;
- case SyncPhaseStatus.completed:
- textColor = Colors.green.shade600;
- break;
- }
- return Container(
- constraints: const BoxConstraints(minWidth: 140),
- child: Text(
- phase.statusText,
- textAlign: TextAlign.end,
- style: GoogleFonts.jetBrainsMono(
- fontSize: 11,
- fontWeight: FontWeight.w500,
- color: textColor,
- letterSpacing: -0.3,
- ),
- ),
- );
- }
- Widget _buildErrorCard(
- BuildContext context,
- SyncState syncState,
- ColorScheme colorScheme,
- ) {
- return Card(
- color: colorScheme.errorContainer,
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Row(
- children: [
- Icon(
- Icons.error_outline,
- color: colorScheme.onErrorContainer,
- ),
- const SizedBox(width: 12),
- Expanded(
- child: Text(
- syncState.error!,
- style: TextStyle(color: colorScheme.onErrorContainer),
- ),
- ),
- ],
- ),
- ),
- );
- }
- }
|