Fabio Dalle Ave 5 дней назад
Родитель
Сommit
917e4831e5
3 измененных файлов с 370 добавлено и 0 удалено
  1. 4 0
      AGENTS.md
  2. 97 0
      lib/services/database_export_service.dart
  3. 269 0
      lib/ui/widgets/persistent_sidebar.dart

+ 4 - 0
AGENTS.md

@@ -0,0 +1,4 @@
+# Project Commands
+
+- Use `fvm flutter` instead of `flutter` for all Flutter commands (e.g. `fvm flutter analyze`, `fvm flutter build`)
+- Use `fvm dart` instead of `dart`

+ 97 - 0
lib/services/database_export_service.dart

@@ -0,0 +1,97 @@
+import 'dart:io';
+
+import 'package:file_picker/file_picker.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+import 'package:path/path.dart' as p;
+import 'package:path_provider/path_provider.dart';
+import 'package:share_plus/share_plus.dart';
+
+import '../core/constants.dart';
+
+class DatabaseExportService {
+  DatabaseExportService._();
+
+  static String _exportFileName() {
+    final date = DateFormat('yyyy-MM-dd_HHmm').format(DateTime.now());
+    return 'Ristoris_export_$date.db';
+  }
+
+  static Future<void> export(BuildContext context) async {
+    if (kIsWeb) {
+      _showError(context, 'Esportazione non supportata su web.');
+      return;
+    }
+
+    try {
+      final sourceFile = File(
+        Platform.isAndroid
+            ? '${AppConstants.kAndroidDbDir}/${AppConstants.kDbName}'
+            : p.join(
+                (await getApplicationDocumentsDirectory()).path,
+                AppConstants.kDbName,
+              ),
+      );
+
+      if (!await sourceFile.exists()) {
+        if (!context.mounted) return;
+        _showError(context, 'File database non trovato.');
+        return;
+      }
+
+      final fileName = _exportFileName();
+
+      if (Platform.isAndroid || Platform.isIOS) {
+        final tempDir = await getTemporaryDirectory();
+        final exportFile = File(p.join(tempDir.path, fileName));
+        await sourceFile.copy(exportFile.path);
+        await Share.shareXFiles([XFile(exportFile.path)]);
+      } else if (Platform.isLinux) {
+        final home = Platform.environment['HOME'] ?? '';
+        final downloadsDir = Directory(p.join(home, 'Downloads'));
+        if (!await downloadsDir.exists()) {
+          await downloadsDir.create(recursive: true);
+        }
+        final destFile = File(p.join(downloadsDir.path, fileName));
+        await sourceFile.copy(destFile.path);
+      } else {
+        final result = await FilePicker.platform.saveFile(
+          dialogTitle: 'Esporta database',
+          fileName: fileName,
+          bytes: await sourceFile.readAsBytes(),
+        );
+
+        if (result == null) {
+          if (!context.mounted) return;
+          _showInfo(context, 'Esportazione annullata.');
+          return;
+        }
+      }
+
+      if (!context.mounted) return;
+      _showSuccess(context, 'Database esportato con successo.');
+    } catch (e) {
+      if (!context.mounted) return;
+      _showError(context, 'Errore durante l\'esportazione: $e');
+    }
+  }
+
+  static void _showSuccess(BuildContext context, String message) {
+    ScaffoldMessenger.of(context).showSnackBar(
+      SnackBar(content: Text(message), backgroundColor: Colors.green),
+    );
+  }
+
+  static void _showError(BuildContext context, String message) {
+    ScaffoldMessenger.of(context).showSnackBar(
+      SnackBar(content: Text(message), backgroundColor: Colors.red),
+    );
+  }
+
+  static void _showInfo(BuildContext context, String message) {
+    ScaffoldMessenger.of(context).showSnackBar(
+      SnackBar(content: Text(message)),
+    );
+  }
+}

+ 269 - 0
lib/ui/widgets/persistent_sidebar.dart

@@ -0,0 +1,269 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
+import '../../providers/auth_provider.dart';
+import '../../core/responsive.dart';
+
+class PersistentSidebar extends ConsumerWidget {
+  final VoidCallback? onExportDatabase;
+
+  const PersistentSidebar({super.key, this.onExportDatabase});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final agente = ref.watch(authProvider).agente;
+    final currentPath = GoRouterState.of(context).matchedLocation;
+
+    return Container(
+      width: ResponsiveBreakpoints.sidebarWidth,
+      height: double.infinity,
+      decoration: BoxDecoration(
+        color: Theme.of(context).colorScheme.surface,
+        border: Border(
+          right: BorderSide(
+            color: Theme.of(context).colorScheme.outlineVariant,
+          ),
+        ),
+      ),
+      child: Column(
+        children: [
+          // Brand
+          Padding(
+            padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
+            child: Align(
+              alignment: Alignment.centerLeft,
+              child: Text(
+                'PowerAge',
+                style: Theme.of(context).textTheme.headlineMedium?.copyWith(
+                  color: Theme.of(context).colorScheme.secondary,
+                  fontWeight: FontWeight.bold,
+                ),
+              ),
+            ),
+          ),
+
+          // Agent profile
+          Padding(
+            padding: const EdgeInsets.symmetric(horizontal: 24),
+            child: Row(
+              children: [
+                CircleAvatar(
+                  radius: 24,
+                  backgroundColor:
+                      Theme.of(context).colorScheme.secondaryContainer,
+                  child: Text(
+                    agente?.codAgente ?? '??',
+                    style: TextStyle(
+                      color:
+                          Theme.of(context).colorScheme.onSecondaryContainer,
+                      fontWeight: FontWeight.bold,
+                      fontSize: 16,
+                    ),
+                  ),
+                ),
+                const SizedBox(width: 16),
+                Expanded(
+                  child: Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      Text(
+                        agente?.codAgente ?? '',
+                        style:
+                            Theme.of(context).textTheme.titleLarge?.copyWith(
+                              fontWeight: FontWeight.bold,
+                            ),
+                        overflow: TextOverflow.ellipsis,
+                      ),
+                      Text(
+                        agente?.ragioneSociale ?? '',
+                        style: Theme.of(context).textTheme.bodyMedium
+                            ?.copyWith(
+                              color: Theme.of(
+                                context,
+                              ).colorScheme.onSurfaceVariant,
+                            ),
+                        overflow: TextOverflow.ellipsis,
+                      ),
+                    ],
+                  ),
+                ),
+              ],
+            ),
+          ),
+
+          const SizedBox(height: 32),
+
+          // Nav items
+          Expanded(
+            child: ListView(
+              padding: const EdgeInsets.symmetric(horizontal: 8),
+              children: [
+                _NavItem(
+                  icon: Icons.analytics_outlined,
+                  label: 'Dashboard',
+                  selected: currentPath == '/dashboard',
+                  onTap: () => context.go('/dashboard'),
+                ),
+                _NavItem(
+                  icon: Icons.shopping_cart_outlined,
+                  label: 'Ordini',
+                  selected: currentPath.startsWith('/dashboard/orders'),
+                  onTap: () => context.go('/dashboard/orders'),
+                ),
+                _NavItem(
+                  icon: Icons.groups_outlined,
+                  label: 'Clienti',
+                  selected: currentPath.startsWith('/dashboard/customers'),
+                  onTap: () => context.go('/dashboard/customers'),
+                ),
+                _NavItem(
+                  icon: Icons.menu_book_outlined,
+                  label: 'Catalogo',
+                  selected: currentPath == '/dashboard/catalog',
+                  onTap: () => context.go('/dashboard/catalog'),
+                ),
+                _NavItem(
+                  icon: Icons.local_offer_outlined,
+                  label: 'Promozioni',
+                  selected: currentPath == '/dashboard/promotions',
+                  onTap: () => context.go('/dashboard/promotions'),
+                ),
+                _NavItem(
+                  icon: Icons.event_note_outlined,
+                  label: 'Scadenze',
+                  selected: currentPath == '/dashboard/payments',
+                  onTap: () => context.go('/dashboard/payments'),
+                ),
+                _NavItem(
+                  icon: Icons.bar_chart_outlined,
+                  label: 'Statistiche',
+                  selected: currentPath == '/dashboard/statistics',
+                  onTap: () => context.go('/dashboard/statistics'),
+                ),
+                _NavItem(
+                  icon: Icons.sync_outlined,
+                  label: 'Sincronizzazione',
+                  selected: currentPath.startsWith('/dashboard/sync'),
+                  onTap: () => context.go('/dashboard/sync'),
+                ),
+              ],
+            ),
+          ),
+
+          // Bottom actions
+          Container(
+            padding: const EdgeInsets.all(8),
+            decoration: BoxDecoration(
+              border: Border(
+                top: BorderSide(
+                  color: Theme.of(
+                    context,
+                  ).colorScheme.outlineVariant.withAlpha(80),
+                ),
+              ),
+            ),
+            child: Column(
+              children: [
+                _NavItem(
+                  icon: Icons.download_outlined,
+                  label: 'Esporta database',
+                  onTap: onExportDatabase,
+                ),
+                _NavItem(
+                  icon: Icons.person_search_outlined,
+                  label: 'Cambia agente',
+                  onTap: () => context.go('/dashboard/switch-agent'),
+                ),
+                _NavItem(
+                  icon: Icons.logout,
+                  label: 'Logout',
+                  iconColor: Theme.of(context).colorScheme.error,
+                  textColor: Theme.of(context).colorScheme.error,
+                  onTap: () =>
+                      ref.read(authProvider.notifier).logout(),
+                ),
+                Padding(
+                  padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
+                  child: Align(
+                    alignment: Alignment.centerLeft,
+                    child: Text(
+                      'v2.1.0',
+                      style: Theme.of(context).textTheme.labelSmall?.copyWith(
+                        color: Theme.of(context).colorScheme.outline,
+                      ),
+                    ),
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _NavItem extends StatelessWidget {
+  final IconData icon;
+  final String label;
+  final bool selected;
+  final VoidCallback? onTap;
+  final Color? iconColor;
+  final Color? textColor;
+
+  const _NavItem({
+    required this.icon,
+    required this.label,
+    this.selected = false,
+    this.onTap,
+    this.iconColor,
+    this.textColor,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final effectiveIconColor =
+        iconColor ??
+        (selected
+            ? Theme.of(context).colorScheme.onSecondaryContainer
+            : Theme.of(context).colorScheme.onSurfaceVariant);
+    final effectiveTextColor =
+        textColor ??
+        (selected
+            ? Theme.of(context).colorScheme.onSecondaryContainer
+            : Theme.of(context).colorScheme.onSurfaceVariant);
+
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
+      child: Material(
+        color: selected
+            ? Theme.of(context).colorScheme.surfaceContainerHighest
+            : Colors.transparent,
+        borderRadius: BorderRadius.circular(12),
+        child: InkWell(
+          borderRadius: BorderRadius.circular(12),
+          onTap: onTap,
+          child: Padding(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+            child: Row(
+              children: [
+                Icon(icon, size: 22, color: effectiveIconColor),
+                const SizedBox(width: 16),
+                Expanded(
+                  child: Text(
+                    label,
+                    style: TextStyle(
+                      color: effectiveTextColor,
+                      fontWeight: selected ? FontWeight.bold : FontWeight.w500,
+                      fontSize: 14,
+                    ),
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}