sync_page.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import 'package:google_fonts/google_fonts.dart';
  5. import 'package:intl/intl.dart';
  6. import '../../../database/database.dart';
  7. import '../../../providers/auth_provider.dart';
  8. import '../../../providers/sync_provider.dart';
  9. class SyncPage extends ConsumerStatefulWidget {
  10. const SyncPage({super.key});
  11. @override
  12. ConsumerState<SyncPage> createState() => _SyncPageState();
  13. }
  14. class _SyncPageState extends ConsumerState<SyncPage>
  15. with SingleTickerProviderStateMixin {
  16. bool _forceFullSync = false;
  17. late AnimationController _syncIconController;
  18. @override
  19. void initState() {
  20. super.initState();
  21. _syncIconController = AnimationController(
  22. vsync: this,
  23. duration: const Duration(seconds: 2),
  24. );
  25. }
  26. @override
  27. void dispose() {
  28. _syncIconController.dispose();
  29. super.dispose();
  30. }
  31. String _formatLastSyncTime(DateTime? time) {
  32. if (time == null) return 'Mai';
  33. final now = DateTime.now();
  34. final today = DateTime(now.year, now.month, now.day);
  35. final syncDate = DateTime(time.year, time.month, time.day);
  36. final timeStr = DateFormat('HH:mm').format(time);
  37. if (syncDate == today) {
  38. return 'Oggi, $timeStr';
  39. } else if (syncDate == today.subtract(const Duration(days: 1))) {
  40. return 'Ieri, $timeStr';
  41. } else {
  42. return DateFormat('dd/MM/yyyy, HH:mm').format(time);
  43. }
  44. }
  45. @override
  46. Widget build(BuildContext context) {
  47. final authState = ref.watch(authProvider);
  48. final syncState = ref.watch(syncProvider);
  49. final theme = Theme.of(context);
  50. final agente = authState.agente;
  51. final colorScheme = theme.colorScheme;
  52. ref.listen<SyncState>(syncProvider, (prev, next) {
  53. if (next.isSyncing && !(prev?.isSyncing ?? false)) {
  54. _syncIconController.repeat();
  55. } else if (!next.isSyncing && (prev?.isSyncing ?? false)) {
  56. _syncIconController.stop();
  57. _syncIconController.reset();
  58. if (next.error == null && next.progress >= 1.0) {
  59. ScaffoldMessenger.of(context).showSnackBar(
  60. const SnackBar(content: Text('Trasmissione terminata')),
  61. );
  62. }
  63. }
  64. });
  65. return Scaffold(
  66. appBar: AppBar(
  67. title: const Text('Sincronizzazione'),
  68. actions: [
  69. IconButton(
  70. icon: const Icon(Icons.settings),
  71. onPressed: () => context.push('/dashboard/sync/settings'),
  72. ),
  73. ],
  74. ),
  75. body: SingleChildScrollView(
  76. padding: const EdgeInsets.all(16),
  77. child: Column(
  78. crossAxisAlignment: CrossAxisAlignment.stretch,
  79. children: [
  80. // Hero section: Toggle + Start button
  81. LayoutBuilder(
  82. builder: (context, constraints) {
  83. final isWide = constraints.maxWidth > 600;
  84. if (isWide) {
  85. return Row(
  86. crossAxisAlignment: CrossAxisAlignment.start,
  87. children: [
  88. Expanded(flex: 1, child: _buildToggleCard(context, syncState)),
  89. const SizedBox(width: 16),
  90. Expanded(
  91. flex: 2,
  92. child: _buildActionCard(
  93. context,
  94. agente,
  95. syncState,
  96. colorScheme,
  97. ),
  98. ),
  99. ],
  100. );
  101. }
  102. return Column(
  103. children: [
  104. _buildToggleCard(context, syncState),
  105. const SizedBox(height: 16),
  106. _buildActionCard(context, agente, syncState, colorScheme),
  107. ],
  108. );
  109. },
  110. ),
  111. const SizedBox(height: 16),
  112. // Progress section
  113. if (syncState.phases.isNotEmpty || syncState.isSyncing || syncState.progress >= 1.0)
  114. _buildProgressSection(context, syncState, colorScheme),
  115. // Error section
  116. if (syncState.error != null) ...[
  117. const SizedBox(height: 16),
  118. _buildErrorCard(context, syncState, colorScheme),
  119. ],
  120. ],
  121. ),
  122. ),
  123. );
  124. }
  125. Widget _buildToggleCard(BuildContext context, SyncState syncState) {
  126. final theme = Theme.of(context);
  127. return Card(
  128. child: Padding(
  129. padding: const EdgeInsets.all(16),
  130. child: Column(
  131. crossAxisAlignment: CrossAxisAlignment.start,
  132. children: [
  133. Text(
  134. 'Tipo Sincronizzazione',
  135. style: theme.textTheme.titleLarge,
  136. ),
  137. const SizedBox(height: 8),
  138. Text(
  139. 'Scegli tra sincronizzazione parziale delle variazioni o scaricamento integrale di tutti i dati dal server.',
  140. style: theme.textTheme.bodyMedium?.copyWith(
  141. color: theme.colorScheme.onSurfaceVariant,
  142. ),
  143. ),
  144. const SizedBox(height: 16),
  145. Container(
  146. padding: const EdgeInsets.all(12),
  147. decoration: BoxDecoration(
  148. color: theme.colorScheme.surface,
  149. borderRadius: BorderRadius.circular(8),
  150. border: Border.all(
  151. color: theme.colorScheme.outlineVariant.withValues(alpha: 0.3),
  152. ),
  153. ),
  154. child: Row(
  155. children: [
  156. Switch(
  157. value: _forceFullSync,
  158. onChanged: syncState.isSyncing
  159. ? null
  160. : (v) => setState(() => _forceFullSync = v),
  161. ),
  162. const SizedBox(width: 12),
  163. Expanded(
  164. child: Text(
  165. 'Sincronizzazione completa',
  166. style: theme.textTheme.labelLarge,
  167. ),
  168. ),
  169. ],
  170. ),
  171. ),
  172. ],
  173. ),
  174. ),
  175. );
  176. }
  177. Widget _buildActionCard(
  178. BuildContext context,
  179. Agente? agente,
  180. SyncState syncState,
  181. ColorScheme colorScheme,
  182. ) {
  183. final theme = Theme.of(context);
  184. return Card(
  185. child: Padding(
  186. padding: const EdgeInsets.all(24),
  187. child: Column(
  188. mainAxisAlignment: MainAxisAlignment.center,
  189. children: [
  190. FilledButton.icon(
  191. onPressed: syncState.isSyncing || agente == null
  192. ? null
  193. : () => ref.read(syncProvider.notifier).startFullSync(
  194. agente.idAgente,
  195. agente.codAgente!,
  196. forceFullSync: _forceFullSync,
  197. ),
  198. icon: syncState.isSyncing
  199. ? RotationTransition(
  200. turns: _syncIconController,
  201. child: const Icon(Icons.sync),
  202. )
  203. : const Icon(Icons.sync),
  204. label: Text(
  205. syncState.isSyncing ? 'Sincronizzazione in corso...' : 'Avvia Sincronizzazione',
  206. ),
  207. style: FilledButton.styleFrom(
  208. padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20),
  209. textStyle: theme.textTheme.titleLarge,
  210. ),
  211. ),
  212. const SizedBox(height: 12),
  213. Row(
  214. mainAxisAlignment: MainAxisAlignment.center,
  215. children: [
  216. Icon(
  217. Icons.schedule,
  218. size: 16,
  219. color: colorScheme.onSurfaceVariant,
  220. ),
  221. const SizedBox(width: 6),
  222. Text(
  223. 'Ultimo aggiornamento: ${_formatLastSyncTime(syncState.lastSyncTime)}',
  224. style: theme.textTheme.bodyMedium?.copyWith(
  225. color: colorScheme.onSurfaceVariant,
  226. ),
  227. ),
  228. ],
  229. ),
  230. ],
  231. ),
  232. ),
  233. );
  234. }
  235. Widget _buildProgressSection(
  236. BuildContext context,
  237. SyncState syncState,
  238. ColorScheme colorScheme,
  239. ) {
  240. final theme = Theme.of(context);
  241. final percent = syncState.phases.isNotEmpty
  242. ? (syncState.progress * 100).toInt()
  243. : 0;
  244. return Card(
  245. child: Column(
  246. crossAxisAlignment: CrossAxisAlignment.stretch,
  247. children: [
  248. // Header
  249. Container(
  250. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
  251. decoration: BoxDecoration(
  252. color: theme.colorScheme.surface,
  253. border: Border(
  254. bottom: BorderSide(color: theme.colorScheme.outlineVariant),
  255. ),
  256. ),
  257. child: Row(
  258. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  259. children: [
  260. Column(
  261. crossAxisAlignment: CrossAxisAlignment.start,
  262. children: [
  263. Text(
  264. syncState.currentAgentCode != null
  265. ? 'Sincronizzazione dati agente (${syncState.currentAgentCode})'
  266. : 'Sincronizzazione dati agente',
  267. style: theme.textTheme.titleLarge,
  268. ),
  269. if (syncState.agentiCodici.isNotEmpty)
  270. RichText(
  271. text: TextSpan(
  272. style: theme.textTheme.bodyMedium?.copyWith(
  273. color: colorScheme.onSurfaceVariant,
  274. ),
  275. children: [
  276. const TextSpan(
  277. text:
  278. 'Stato complessivo del processo (Agenti: '),
  279. for (int i = 0;
  280. i < syncState.agentiCodici.length;
  281. i++) ...[
  282. if (i > 0) const TextSpan(text: ', '),
  283. TextSpan(
  284. text: syncState.agentiCodici[i],
  285. style: syncState.agentiCodici[i] ==
  286. syncState.currentAgentCode
  287. ? TextStyle(
  288. fontWeight: FontWeight.bold,
  289. color: colorScheme.onSurfaceVariant,
  290. )
  291. : null,
  292. ),
  293. ],
  294. const TextSpan(text: ')'),
  295. ],
  296. ),
  297. )
  298. else
  299. Text(
  300. 'Stato complessivo del processo',
  301. style: theme.textTheme.bodyMedium?.copyWith(
  302. color: colorScheme.onSurfaceVariant,
  303. ),
  304. ),
  305. ],
  306. ),
  307. Text(
  308. '$percent%',
  309. style: theme.textTheme.headlineMedium?.copyWith(
  310. color: colorScheme.secondary,
  311. ),
  312. ),
  313. ],
  314. ),
  315. ),
  316. // Progress bar + phases
  317. Padding(
  318. padding: const EdgeInsets.all(16),
  319. child: Column(
  320. crossAxisAlignment: CrossAxisAlignment.stretch,
  321. children: [
  322. // Main progress bar
  323. ClipRRect(
  324. borderRadius: BorderRadius.circular(4),
  325. child: LinearProgressIndicator(
  326. value: syncState.progress,
  327. minHeight: 8,
  328. backgroundColor: theme.colorScheme.surfaceContainerHighest,
  329. valueColor: AlwaysStoppedAnimation<Color>(
  330. colorScheme.secondary,
  331. ),
  332. ),
  333. ),
  334. const SizedBox(height: 16),
  335. // Phases grid
  336. ...syncState.phases.asMap().entries.map(
  337. (entry) => _buildPhaseRow(
  338. context,
  339. entry.key,
  340. entry.value,
  341. colorScheme,
  342. ),
  343. ),
  344. ],
  345. ),
  346. ),
  347. ],
  348. ),
  349. );
  350. }
  351. Widget _buildPhaseRow(
  352. BuildContext context,
  353. int index,
  354. SyncPhaseInfo phase,
  355. ColorScheme colorScheme,
  356. ) {
  357. final theme = Theme.of(context);
  358. final isCompleted = phase.status == SyncPhaseStatus.completed;
  359. return Container(
  360. margin: const EdgeInsets.only(bottom: 8),
  361. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
  362. decoration: BoxDecoration(
  363. color: isCompleted
  364. ? Colors.green.shade50
  365. : theme.colorScheme.surface,
  366. borderRadius: BorderRadius.circular(8),
  367. border: Border.all(
  368. color: theme.colorScheme.outlineVariant.withValues(alpha: 0.2),
  369. ),
  370. ),
  371. child: Row(
  372. children: [
  373. // Status dot
  374. _buildStatusDot(phase.status, colorScheme),
  375. const SizedBox(width: 12),
  376. // Phase name
  377. Expanded(
  378. child: Text(
  379. phase.name,
  380. style: theme.textTheme.bodyMedium?.copyWith(
  381. color: isCompleted
  382. ? Colors.green.shade700
  383. : theme.colorScheme.onSurface,
  384. ),
  385. ),
  386. ),
  387. // Status text
  388. _buildStatusText(phase, colorScheme),
  389. ],
  390. ),
  391. );
  392. }
  393. Widget _buildStatusDot(SyncPhaseStatus status, ColorScheme colorScheme) {
  394. Color dotColor;
  395. List<BoxShadow>? shadow;
  396. switch (status) {
  397. case SyncPhaseStatus.waiting:
  398. dotColor = colorScheme.outlineVariant;
  399. break;
  400. case SyncPhaseStatus.downloading:
  401. case SyncPhaseStatus.processing:
  402. dotColor = colorScheme.secondary;
  403. shadow = [
  404. BoxShadow(
  405. color: colorScheme.secondary.withValues(alpha: 0.5),
  406. blurRadius: 6,
  407. ),
  408. ];
  409. break;
  410. case SyncPhaseStatus.completed:
  411. dotColor = Colors.green;
  412. shadow = [
  413. BoxShadow(
  414. color: Colors.green.withValues(alpha: 0.5),
  415. blurRadius: 6,
  416. ),
  417. ];
  418. }
  419. return AnimatedContainer(
  420. duration: const Duration(milliseconds: 300),
  421. width: 10,
  422. height: 10,
  423. decoration: BoxDecoration(
  424. shape: BoxShape.circle,
  425. color: dotColor,
  426. boxShadow: shadow,
  427. ),
  428. );
  429. }
  430. Widget _buildStatusText(SyncPhaseInfo phase, ColorScheme colorScheme) {
  431. Color textColor;
  432. switch (phase.status) {
  433. case SyncPhaseStatus.waiting:
  434. textColor = colorScheme.onSurfaceVariant;
  435. break;
  436. case SyncPhaseStatus.downloading:
  437. case SyncPhaseStatus.processing:
  438. textColor = colorScheme.secondary;
  439. break;
  440. case SyncPhaseStatus.completed:
  441. textColor = Colors.green.shade600;
  442. break;
  443. }
  444. return Container(
  445. constraints: const BoxConstraints(minWidth: 140),
  446. child: Text(
  447. phase.statusText,
  448. textAlign: TextAlign.end,
  449. style: GoogleFonts.jetBrainsMono(
  450. fontSize: 11,
  451. fontWeight: FontWeight.w500,
  452. color: textColor,
  453. letterSpacing: -0.3,
  454. ),
  455. ),
  456. );
  457. }
  458. Widget _buildErrorCard(
  459. BuildContext context,
  460. SyncState syncState,
  461. ColorScheme colorScheme,
  462. ) {
  463. return Card(
  464. color: colorScheme.errorContainer,
  465. child: Padding(
  466. padding: const EdgeInsets.all(16),
  467. child: Row(
  468. children: [
  469. Icon(
  470. Icons.error_outline,
  471. color: colorScheme.onErrorContainer,
  472. ),
  473. const SizedBox(width: 12),
  474. Expanded(
  475. child: Text(
  476. syncState.error!,
  477. style: TextStyle(color: colorScheme.onErrorContainer),
  478. ),
  479. ),
  480. ],
  481. ),
  482. ),
  483. );
  484. }
  485. }