diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..e447105 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,257 @@ +# EnergyMedia Content Manager - AI Coding Agent Instructions + +## Project Overview +**EnergyMedia Content Manager** is a Flutter web application for managing multimedia content (videos, posters, categories) for the EnergyMedia platform. Single-organization focused system with video upload, categorization, playback, and analytics dashboard. + +**Tech Stack:** Flutter 3.1.4+, Supabase (backend/auth/storage), Provider (state), GoRouter (navigation), PlutoGrid (tables), Video Players (appinio_video_player/video_player) + +## Architecture & Key Patterns + +### Dual Supabase Clients +- `supabase` (default): Standard auth schema (`public.users`) for authentication ONLY +- `supabaseML`: Custom `media_library` schema for all media content management +- **Critical:** Always use `supabaseML` for media data, `supabase` for auth only +- **Organization Filter:** ALL queries to `media_files` MUST filter by `organization_fk = 17` +- See [lib/main.dart](lib/main.dart#L35) and [lib/helpers/globals.dart](lib/helpers/globals.dart) + +### State Management (Provider) +All providers declared in [lib/main.dart](lib/main.dart): +- `UserState`: Auth state and current user +- `VisualStateProvider`: Theme/visual preferences (light/dark mode) +- `VideosProvider`: Media files CRUD, upload/download, metadata management +- **Pattern:** Use `context.read()` for one-time actions, `context.watch()` for reactive UI + +### Navigation Structure +``` +/login → /dashboard (stats: reproducciones, videos, categorías) + └── Sidemenu: + ├── Dashboard (default) + ├── Gestor de Videos (PlutoGrid con CRUD) + └── Configuración (placeholder - work in progress) +``` +- **Simplified:** No empresa/negocio selection - single organization (EnergyMedia) +- See [lib/router/router.dart](lib/router/router.dart) + +## Database Schema & Critical Rules + +### Media Library Schema (`media_library`) +**Tables:** +- `media_files`: Main video records (file_name, title, file_url, storage_path, metadata_json, media_category_fk, organization_fk) +- `media_categories`: Video categories (category_name, category_description) +- `media_posters`: Poster/thumbnail associations (media_file_id, poster_file_id) +- View: `vw_media_files_with_posters` - Complete media info with category and poster + +### Organization Filter Rule +**CRITICAL:** ALL operations on `media_files` MUST include `organization_fk = 17` filter: +```dart +// CORRECT - filtered by organization +final response = await supabaseML + .from('media_files') + .select() + .eq('organization_fk', 17); + +// WRONG - missing organization filter +final response = await supabaseML + .from('media_files') // ❌ Returns all organizations! + .select(); +``` +**Always:** Insert/update operations must set `organization_fk: 17` + +### metadata_json Structure +Standard fields in `media_files.metadata_json`: +```json +{ + "uploaded_at": "2026-01-10T10:30:00Z", + "reproducciones": 150, + "categorias": ["tutorial", "energía"], + "original_file_name": "video_original.mp4", + "duration_seconds": 320, + "resolution": "1920x1080", + "last_viewed_at": "2026-01-10T12:00:00Z" +} +``` + +### Supabase Storage +**Bucket:** `energymedia` +- `energymedia/videos/` - Video files +- `energymedia/imagenes/` - Poster/thumbnail images + +## Responsive Design Standards +**Breakpoints:** Mobile ≤800px, Tablet 801-1200px, Desktop >1200px + +**Common pattern:** +```dart +final isMobile = MediaQuery.of(context).size.width <= 800; +// Desktop: PlutoGrid tables with video thumbnails +// Mobile: Card-based lists with posters +``` + +**Key files:** +- Desktop layouts: `lib/pages/videos/*_page.dart` +- Mobile adaptations: Check for conditional rendering in same files +- Reference constant: `mobileSize = 800` in [lib/helpers/constants.dart](lib/helpers/constants.dart#L22) + +## Critical Files & Workflows + +### Global State (`lib/helpers/globals.dart`) +- `currentUser`: Authenticated user model (nullable) +- `supabaseML`: Media Library-specific Supabase client (schema: `media_library`) +- `plutoGridScrollbarConfig()`, `plutoGridStyleConfig()`: Consistent table styling +- **Always check `currentUser != null` before auth-dependent operations** + +### Models (Domain-Driven) +- Media models: `lib/models/media/*.dart` +- Pattern: `fromMap()` for Supabase JSON deserialization +- Key models: `MediaFileModel`, `MediaCategoryModel`, `MediaPosterModel` + +### Video Players +**Libraries:** `appinio_video_player`, `video_player`, `chewie` +**Usage patterns:** +- `VideoPlayerLive`: Full-screen player with controls (chewie-based) +- `VideoScreenNew`: Embedded player (appinio-based) +- `VideoScreenThumbnail`: Generate thumbnails from video +- See [lib/pages/widgets/video_player_*.dart](lib/pages/widgets/) + +### Theme & Styling +**Color Scheme (EnergyMedia):** +- **Primary Gradient:** Cyan→Yellow (linear-gradient(135deg, #4EC9F5, #FFB733)) +- **Accents:** Purple (#6B2F8A), Cyan (#4EC9F5), Yellow (#FFB733), Red (#FF2D2D), Orange (#FF7A3D) +- **Dark Mode Backgrounds:** #0B0B0D (main), #121214 (surface1), #1A1A1D (surface2) +- **Light Mode Backgrounds:** #FFFFFF (main), #F7F7F7 (surface1), #EFEFEF (surface2) +- **Typography:** Google Fonts Poppins (Regular 400, Bold 700) +- Use `AppTheme.of(context)` for colors, not hardcoded values +- See [lib/theme/theme.dart](lib/theme/theme.dart) + +## Development Commands +```bash +# Run (web dev) +flutter run -d chrome + +# Build web (production) +flutter build web + +# Dependencies +flutter pub get + +# Clean build +flutter clean && flutter pub get +``` + +## Common Tasks + +### Adding New Provider +1. Create in `lib/providers/` +2. Extend `ChangeNotifier` +3. Register in [lib/main.dart](lib/main.dart) `MultiProvider.providers` +4. Export in `lib/providers/providers.dart` if needed + +### Creating Responsive Pages +1. Check screen width: `MediaQuery.of(context).size.width` +2. Desktop: Use PlutoGrid for tables, full layouts +3. Mobile: Use ListView with Cards, simplified forms +4. Reference [lib/pages/videos/gestor_videos_page.dart](lib/pages/videos/gestor_videos_page.dart) for pattern + +### Querying Media Data +```dart +// CORRECT - uses media_library schema + organization filter +final response = await supabaseML + .from('media_files') + .select() + .eq('organization_fk', 17) + .order('created_at_timestamp', ascending: false); + +// WRONG - uses default schema +final response = await supabase + .from('media_files') // ❌ Table not found! + .select(); + +// WRONG - missing organization filter +final response = await supabaseML + .from('media_files') + .select(); // ❌ Returns data from ALL organizations! +``` + +### Uploading Media Files +```dart +// 1. Upload video to storage +final videoPath = await supabaseML.storage + .from('energymedia') + .upload('videos/$fileName', videoBytes); + +// 2. Insert record with organization filter +await supabaseML.from('media_files').insert({ + 'file_name': fileName, + 'title': title, + 'file_url': publicUrl, + 'storage_path': 'videos/$fileName', + 'organization_fk': 17, // ⚠️ REQUIRED! + 'metadata_json': { + 'uploaded_at': DateTime.now().toIso8601String(), + 'reproducciones': 0, + 'original_file_name': originalName, + } +}); +``` + +### Working with Video Thumbnails +```dart +// Generate thumbnail from video (VideoScreenThumbnail widget) +VideoScreenThumbnail(video: videoUrl) + +// Display poster from media_posters +Image.network(posterUrl, fit: BoxFit.cover) + +// Fallback: Show placeholder if no poster +if (posterUrl != null) + Image.network(posterUrl) +else + Icon(Icons.video_library, size: 48) +``` + +## Project Conventions + +### Naming +- Files: `snake_case.dart` (e.g., `videos_provider.dart`) +- Classes: `PascalCase` (e.g., `VideosProvider`) +- Private members: `_underscorePrefixed` (e.g., `_selectedVideo`) +- Models suffix: `*Model` (e.g., `MediaFileModel`) + +### File Organization +``` +lib/ + pages/ # Full pages + videos/ # Video management pages + gestor_videos_page.dart + dashboard_page.dart + widgets/ # Video-specific widgets + widgets/ # Shared widgets (video players, etc.) + providers/ # State management + videos_provider.dart + models/ # Data models + media/ # Media domain models + helpers/ # Utilities, globals, extensions + services/ # External service integrations +``` + +### Import Style +- Absolute imports: `import 'package:nethive_neo/...'` +- Barrel exports: Use `lib/models/models.dart`, `lib/providers/providers.dart` + +## Testing & Debugging +- **No formal tests yet** - manual testing in browser/device +- Dev mode: Hot reload enabled (Flutter devtools) +- Check browser console for Supabase RPC errors +- Useful: Flutter Inspector for widget tree debugging + +## Key Reference Documents +- [assets/referencia/tablas_energymedia.txt](assets/referencia/tablas_energymedia.txt): Database schema reference +- [pubspec.yaml](pubspec.yaml): All dependencies and versions +- [lib/helpers/constants.dart](lib/helpers/constants.dart): Environment config, API keys, constants +- GitHub repo: https://github.com/CB-Luna/energymedia_content_manager + +## Known Quirks +- PlutoGrid requires `dependency_override` for `intl: ^0.19.0` compatibility +- Always call `initGlobals()` in main before app initialization +- GoRouter `optionURLReflectsImperativeAPIs` must be `true` for proper routing +- Mobile forms use full-screen modals, not dialogs (better UX on small screens) +- Video thumbnails: Use `VideoScreenThumbnail` widget or fallback to category/poster image diff --git a/assets/referencia/tablas_energymedia.txt b/assets/referencia/tablas_energymedia.txt new file mode 100644 index 0000000..b3a0fa7 --- /dev/null +++ b/assets/referencia/tablas_energymedia.txt @@ -0,0 +1,78 @@ +tabla: "media_posters" + + +| column_name | data_type | +| -------------------- | ------------------------ | +| media_poster_id | bigint | +| media_file_id | bigint | +| poster_file_id | bigint | +| created_at_timestamp | timestamp with time zone | + + + +tabla: "media_files" + +| column_name | data_type | +| -------------------- | ------------------------ | +| media_file_id | bigint | +| file_name | text | +| title | text | +| file_description | text | +| file_type | text | +| mime_type | text | +| file_extension | text | +| file_size_bytes | bigint | +| file_url | text | +| storage_path | text | +| created_at_timestamp | timestamp with time zone | +| updated_at_timestamp | timestamp with time zone | +| uploaded_by_user_id | uuid | +| is_public_file | boolean | +| metadata_json | jsonb | +| seconds | bigint | +| media_category_fk | bigint | +| organization_fk | bigint | + + + +tabla: "media_categories" + +| column_name | data_type | +| -------------------- | ------------------------ | +| media_categories_id | bigint | +| created_at | timestamp with time zone | +| created_by | uuid | +| category_name | text | +| category_description | text | +| media_file_fk | bigint | + + +vista: "vw_media_files_with_posters" + +| column_name | data_type | +| --------------------------- | ------------------------ | +| media_file_id | bigint | +| media_file_name | text | +| media_title | text | +| file_description | text | +| media_type | text | +| media_mime_type | text | +| media_url | text | +| media_storage_path | text | +| media_created_at | timestamp with time zone | +| category_id | bigint | +| category_name | text | +| category_description | text | +| category_created_at | timestamp with time zone | +| category_image_url | text | +| category_image_storage_path | text | +| media_poster_id | bigint | +| poster_file_id | bigint | +| poster_file_name | text | +| poster_title | text | +| poster_url | text | +| poster_storage_path | text | +| poster_created_at | timestamp with time zone | + + +https://github.com/CB-Luna/energymedia_content_manager \ No newline at end of file diff --git a/lib/helpers/globals.dart b/lib/helpers/globals.dart index 3b0ffc9..392d1da 100644 --- a/lib/helpers/globals.dart +++ b/lib/helpers/globals.dart @@ -14,7 +14,7 @@ final GlobalKey snackbarKey = const storage = FlutterSecureStorage(); final supabase = Supabase.instance.client; -late SupabaseClient supabaseLU; +late SupabaseClient supabaseML; late final SharedPreferences prefs; late Configuration? theme; diff --git a/lib/main.dart b/lib/main.dart index 5c9ec34..b6a7113 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,9 +10,7 @@ import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:nethive_neo/providers/user_provider.dart'; import 'package:nethive_neo/providers/visual_state_provider.dart'; import 'package:nethive_neo/providers/users_provider.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; -import 'package:nethive_neo/providers/nethive/navigation_provider.dart'; +import 'package:nethive_neo/providers/videos_provider.dart'; import 'package:nethive_neo/helpers/globals.dart'; import 'package:url_strategy/url_strategy.dart'; @@ -32,7 +30,7 @@ void main() async { ), ); - supabaseLU = SupabaseClient(supabaseUrl, anonKey, schema: 'nethive'); + supabaseML = SupabaseClient(supabaseUrl, anonKey, schema: 'media_library'); await initGlobals(); @@ -45,9 +43,7 @@ void main() async { ChangeNotifierProvider( create: (context) => VisualStateProvider(context)), ChangeNotifierProvider(create: (_) => UsersProvider()), - ChangeNotifierProvider(create: (_) => EmpresasNegociosProvider()), - ChangeNotifierProvider(create: (_) => ComponentesProvider()), - ChangeNotifierProvider(create: (_) => NavigationProvider()), + ChangeNotifierProvider(create: (_) => VideosProvider()), ], child: const MyApp(), ), @@ -79,7 +75,7 @@ class _MyAppState extends State { Widget build(BuildContext context) { return Portal( child: MaterialApp.router( - title: 'NETHIVE', + title: 'EnergyMedia Content Manager', debugShowCheckedModeBanner: false, locale: _locale, localizationsDelegates: const [ diff --git a/lib/models/media/media_category_model.dart b/lib/models/media/media_category_model.dart new file mode 100644 index 0000000..b58df38 --- /dev/null +++ b/lib/models/media/media_category_model.dart @@ -0,0 +1,58 @@ +class MediaCategoryModel { + final int mediaCategoriesId; + final DateTime? createdAt; + final String? createdBy; + final String categoryName; + final String? categoryDescription; + final int? mediaFileFk; + + MediaCategoryModel({ + required this.mediaCategoriesId, + this.createdAt, + this.createdBy, + required this.categoryName, + this.categoryDescription, + this.mediaFileFk, + }); + + factory MediaCategoryModel.fromMap(Map map) { + return MediaCategoryModel( + mediaCategoriesId: map['media_categories_id'] ?? 0, + createdAt: + map['created_at'] != null ? DateTime.parse(map['created_at']) : null, + createdBy: map['created_by'], + categoryName: map['category_name'] ?? '', + categoryDescription: map['category_description'], + mediaFileFk: map['media_file_fk'], + ); + } + + Map toMap() { + return { + 'media_categories_id': mediaCategoriesId, + 'created_at': createdAt?.toIso8601String(), + 'created_by': createdBy, + 'category_name': categoryName, + 'category_description': categoryDescription, + 'media_file_fk': mediaFileFk, + }; + } + + MediaCategoryModel copyWith({ + int? mediaCategoriesId, + DateTime? createdAt, + String? createdBy, + String? categoryName, + String? categoryDescription, + int? mediaFileFk, + }) { + return MediaCategoryModel( + mediaCategoriesId: mediaCategoriesId ?? this.mediaCategoriesId, + createdAt: createdAt ?? this.createdAt, + createdBy: createdBy ?? this.createdBy, + categoryName: categoryName ?? this.categoryName, + categoryDescription: categoryDescription ?? this.categoryDescription, + mediaFileFk: mediaFileFk ?? this.mediaFileFk, + ); + } +} diff --git a/lib/models/media/media_file_model.dart b/lib/models/media/media_file_model.dart new file mode 100644 index 0000000..20ff4d5 --- /dev/null +++ b/lib/models/media/media_file_model.dart @@ -0,0 +1,156 @@ +import 'dart:convert'; + +class MediaFileModel { + final int mediaFileId; + final String fileName; + final String? title; + final String? fileDescription; + final String? fileType; + final String? mimeType; + final String? fileExtension; + final int? fileSizeBytes; + final String? fileUrl; + final String? storagePath; + final DateTime? createdAt; + final DateTime? updatedAt; + final String? uploadedByUserId; + final bool? isPublicFile; + final Map? metadataJson; + final int? seconds; + final int? mediaCategoryFk; + final int organizationFk; + + MediaFileModel({ + required this.mediaFileId, + required this.fileName, + this.title, + this.fileDescription, + this.fileType, + this.mimeType, + this.fileExtension, + this.fileSizeBytes, + this.fileUrl, + this.storagePath, + this.createdAt, + this.updatedAt, + this.uploadedByUserId, + this.isPublicFile, + this.metadataJson, + this.seconds, + this.mediaCategoryFk, + required this.organizationFk, + }); + + factory MediaFileModel.fromMap(Map map) { + return MediaFileModel( + mediaFileId: map['media_file_id'] ?? 0, + fileName: map['file_name'] ?? '', + title: map['title'], + fileDescription: map['file_description'], + fileType: map['file_type'], + mimeType: map['mime_type'], + fileExtension: map['file_extension'], + fileSizeBytes: map['file_size_bytes'], + fileUrl: map['file_url'], + storagePath: map['storage_path'], + createdAt: map['created_at_timestamp'] != null + ? DateTime.parse(map['created_at_timestamp']) + : null, + updatedAt: map['updated_at_timestamp'] != null + ? DateTime.parse(map['updated_at_timestamp']) + : null, + uploadedByUserId: map['uploaded_by_user_id'], + isPublicFile: map['is_public_file'], + metadataJson: map['metadata_json'] != null + ? (map['metadata_json'] is String + ? jsonDecode(map['metadata_json']) + : map['metadata_json']) + : null, + seconds: map['seconds'], + mediaCategoryFk: map['media_category_fk'], + organizationFk: map['organization_fk'] ?? 17, + ); + } + + Map toMap() { + return { + 'media_file_id': mediaFileId, + 'file_name': fileName, + 'title': title, + 'file_description': fileDescription, + 'file_type': fileType, + 'mime_type': mimeType, + 'file_extension': fileExtension, + 'file_size_bytes': fileSizeBytes, + 'file_url': fileUrl, + 'storage_path': storagePath, + 'created_at_timestamp': createdAt?.toIso8601String(), + 'updated_at_timestamp': updatedAt?.toIso8601String(), + 'uploaded_by_user_id': uploadedByUserId, + 'is_public_file': isPublicFile, + 'metadata_json': metadataJson, + 'seconds': seconds, + 'media_category_fk': mediaCategoryFk, + 'organization_fk': organizationFk, + }; + } + + MediaFileModel copyWith({ + int? mediaFileId, + String? fileName, + String? title, + String? fileDescription, + String? fileType, + String? mimeType, + String? fileExtension, + int? fileSizeBytes, + String? fileUrl, + String? storagePath, + DateTime? createdAt, + DateTime? updatedAt, + String? uploadedByUserId, + bool? isPublicFile, + Map? metadataJson, + int? seconds, + int? mediaCategoryFk, + int? organizationFk, + }) { + return MediaFileModel( + mediaFileId: mediaFileId ?? this.mediaFileId, + fileName: fileName ?? this.fileName, + title: title ?? this.title, + fileDescription: fileDescription ?? this.fileDescription, + fileType: fileType ?? this.fileType, + mimeType: mimeType ?? this.mimeType, + fileExtension: fileExtension ?? this.fileExtension, + fileSizeBytes: fileSizeBytes ?? this.fileSizeBytes, + fileUrl: fileUrl ?? this.fileUrl, + storagePath: storagePath ?? this.storagePath, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + uploadedByUserId: uploadedByUserId ?? this.uploadedByUserId, + isPublicFile: isPublicFile ?? this.isPublicFile, + metadataJson: metadataJson ?? this.metadataJson, + seconds: seconds ?? this.seconds, + mediaCategoryFk: mediaCategoryFk ?? this.mediaCategoryFk, + organizationFk: organizationFk ?? this.organizationFk, + ); + } + + // Helper getters for metadata_json + int get reproducciones => metadataJson?['reproducciones'] ?? 0; + DateTime? get uploadedAt => metadataJson?['uploaded_at'] != null + ? DateTime.tryParse(metadataJson!['uploaded_at']) + : null; + List get categorias => + (metadataJson?['categorias'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + []; + String? get originalFileName => metadataJson?['original_file_name']; + int? get durationSeconds => metadataJson?['duration_seconds']; + String? get resolution => metadataJson?['resolution']; + DateTime? get lastViewedAt => metadataJson?['last_viewed_at'] != null + ? DateTime.tryParse(metadataJson!['last_viewed_at']) + : null; +} diff --git a/lib/models/media/media_models.dart b/lib/models/media/media_models.dart new file mode 100644 index 0000000..fecc2d1 --- /dev/null +++ b/lib/models/media/media_models.dart @@ -0,0 +1,5 @@ +// Media models +export 'media_file_model.dart'; +export 'media_category_model.dart'; +export 'media_poster_model.dart'; +export 'media_with_poster_model.dart'; diff --git a/lib/models/media/media_poster_model.dart b/lib/models/media/media_poster_model.dart new file mode 100644 index 0000000..2e9465d --- /dev/null +++ b/lib/models/media/media_poster_model.dart @@ -0,0 +1,47 @@ +class MediaPosterModel { + final int mediaPosterId; + final int mediaFileId; + final int posterFileId; + final DateTime? createdAt; + + MediaPosterModel({ + required this.mediaPosterId, + required this.mediaFileId, + required this.posterFileId, + this.createdAt, + }); + + factory MediaPosterModel.fromMap(Map map) { + return MediaPosterModel( + mediaPosterId: map['media_poster_id'] ?? 0, + mediaFileId: map['media_file_id'] ?? 0, + posterFileId: map['poster_file_id'] ?? 0, + createdAt: map['created_at_timestamp'] != null + ? DateTime.parse(map['created_at_timestamp']) + : null, + ); + } + + Map toMap() { + return { + 'media_poster_id': mediaPosterId, + 'media_file_id': mediaFileId, + 'poster_file_id': posterFileId, + 'created_at_timestamp': createdAt?.toIso8601String(), + }; + } + + MediaPosterModel copyWith({ + int? mediaPosterId, + int? mediaFileId, + int? posterFileId, + DateTime? createdAt, + }) { + return MediaPosterModel( + mediaPosterId: mediaPosterId ?? this.mediaPosterId, + mediaFileId: mediaFileId ?? this.mediaFileId, + posterFileId: posterFileId ?? this.posterFileId, + createdAt: createdAt ?? this.createdAt, + ); + } +} diff --git a/lib/models/media/media_with_poster_model.dart b/lib/models/media/media_with_poster_model.dart new file mode 100644 index 0000000..2751712 --- /dev/null +++ b/lib/models/media/media_with_poster_model.dart @@ -0,0 +1,113 @@ +/// Model for vw_media_files_with_posters view +class MediaWithPosterModel { + final int mediaFileId; + final String? mediaFileName; + final String? mediaTitle; + final String? fileDescription; + final String? mediaType; + final String? mediaMimeType; + final String? mediaUrl; + final String? mediaStoragePath; + final DateTime? mediaCreatedAt; + final int? categoryId; + final String? categoryName; + final String? categoryDescription; + final DateTime? categoryCreatedAt; + final String? categoryImageUrl; + final String? categoryImageStoragePath; + final int? mediaPosterId; + final int? posterFileId; + final String? posterFileName; + final String? posterTitle; + final String? posterUrl; + final String? posterStoragePath; + final DateTime? posterCreatedAt; + + MediaWithPosterModel({ + required this.mediaFileId, + this.mediaFileName, + this.mediaTitle, + this.fileDescription, + this.mediaType, + this.mediaMimeType, + this.mediaUrl, + this.mediaStoragePath, + this.mediaCreatedAt, + this.categoryId, + this.categoryName, + this.categoryDescription, + this.categoryCreatedAt, + this.categoryImageUrl, + this.categoryImageStoragePath, + this.mediaPosterId, + this.posterFileId, + this.posterFileName, + this.posterTitle, + this.posterUrl, + this.posterStoragePath, + this.posterCreatedAt, + }); + + factory MediaWithPosterModel.fromMap(Map map) { + return MediaWithPosterModel( + mediaFileId: map['media_file_id'] ?? 0, + mediaFileName: map['media_file_name'], + mediaTitle: map['media_title'], + fileDescription: map['file_description'], + mediaType: map['media_type'], + mediaMimeType: map['media_mime_type'], + mediaUrl: map['media_url'], + mediaStoragePath: map['media_storage_path'], + mediaCreatedAt: map['media_created_at'] != null + ? DateTime.parse(map['media_created_at']) + : null, + categoryId: map['category_id'], + categoryName: map['category_name'], + categoryDescription: map['category_description'], + categoryCreatedAt: map['category_created_at'] != null + ? DateTime.parse(map['category_created_at']) + : null, + categoryImageUrl: map['category_image_url'], + categoryImageStoragePath: map['category_image_storage_path'], + mediaPosterId: map['media_poster_id'], + posterFileId: map['poster_file_id'], + posterFileName: map['poster_file_name'], + posterTitle: map['poster_title'], + posterUrl: map['poster_url'], + posterStoragePath: map['poster_storage_path'], + posterCreatedAt: map['poster_created_at'] != null + ? DateTime.parse(map['poster_created_at']) + : null, + ); + } + + Map toMap() { + return { + 'media_file_id': mediaFileId, + 'media_file_name': mediaFileName, + 'media_title': mediaTitle, + 'file_description': fileDescription, + 'media_type': mediaType, + 'media_mime_type': mediaMimeType, + 'media_url': mediaUrl, + 'media_storage_path': mediaStoragePath, + 'media_created_at': mediaCreatedAt?.toIso8601String(), + 'category_id': categoryId, + 'category_name': categoryName, + 'category_description': categoryDescription, + 'category_created_at': categoryCreatedAt?.toIso8601String(), + 'category_image_url': categoryImageUrl, + 'category_image_storage_path': categoryImageStoragePath, + 'media_poster_id': mediaPosterId, + 'poster_file_id': posterFileId, + 'poster_file_name': posterFileName, + 'poster_title': posterTitle, + 'poster_url': posterUrl, + 'poster_storage_path': posterStoragePath, + 'poster_created_at': posterCreatedAt?.toIso8601String(), + }; + } + + /// Helper getter to get poster or fallback to category image + String? get displayImageUrl => posterUrl ?? categoryImageUrl; +} diff --git a/lib/models/nethive/topologia_completa_model.dart b/lib/models/nethive/topologia_completa_model.dart index ba26446..233eec2 100644 --- a/lib/models/nethive/topologia_completa_model.dart +++ b/lib/models/nethive/topologia_completa_model.dart @@ -1,7 +1,3 @@ -import 'package:nethive_neo/models/nethive/componente_model.dart'; -import 'package:nethive_neo/models/nethive/conexion_componente_model.dart'; -import 'package:nethive_neo/models/nethive/conexion_alimentacion_model.dart'; - class TopologiaCompleta { final List componentes; final List conexionesDatos; diff --git a/lib/pages/empresa_negocios/empresa_negocios_page.dart b/lib/pages/empresa_negocios/empresa_negocios_page.dart deleted file mode 100644 index c09d3f6..0000000 --- a/lib/pages/empresa_negocios/empresa_negocios_page.dart +++ /dev/null @@ -1,642 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/pages/empresa_negocios/widgets/empresa_selector_sidebar.dart'; -import 'package:nethive_neo/pages/empresa_negocios/widgets/negocios_table.dart'; -import 'package:nethive_neo/pages/empresa_negocios/widgets/negocios_cards_view.dart'; -import 'package:nethive_neo/pages/empresa_negocios/widgets/mobile_empresa_selector.dart'; -import 'package:nethive_neo/pages/empresa_negocios/widgets/negocios_map_view.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class EmpresaNegociosPage extends StatefulWidget { - const EmpresaNegociosPage({Key? key}) : super(key: key); - - @override - State createState() => _EmpresaNegociosPageState(); -} - -class _EmpresaNegociosPageState extends State - with TickerProviderStateMixin { - bool showMapView = false; - late AnimationController _animationController; - late Animation _fadeAnimation; - late Animation _slideAnimation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - duration: const Duration(milliseconds: 800), - vsync: this, - ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); - _slideAnimation = Tween( - begin: const Offset(0, 0.3), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeOutCubic, - )); - _animationController.forward(); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isLargeScreen = MediaQuery.of(context).size.width > 1200; - - return Scaffold( - backgroundColor: AppTheme.of(context).primaryBackground, - body: Container( - decoration: BoxDecoration( - gradient: AppTheme.of(context).darkBackgroundGradient, - ), - child: Consumer( - builder: (context, provider, child) { - if (isLargeScreen) { - // Vista de escritorio - return Row( - children: [ - // Sidebar izquierdo con empresas - SlideTransition( - position: Tween( - begin: const Offset(-1, 0), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeOutCubic, - )), - child: SizedBox( - width: 320, - child: EmpresaSelectorSidebar( - provider: provider, - onEmpresaSelected: (empresaId) { - provider.setEmpresaSeleccionada(empresaId); - }, - ), - ), - ), - - // Área principal - Expanded( - child: SlideTransition( - position: _slideAnimation, - child: FadeTransition( - opacity: _fadeAnimation, - child: Container( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header con título y switch - _buildEnhancedHeader(provider), - - const SizedBox(height: 24), - - // Contenido principal (tabla o mapa) - Expanded( - child: showMapView - ? _buildMapView() - : _buildTableView(provider), - ), - ], - ), - ), - ), - ), - ), - ], - ); - } else { - // Vista móvil/tablet - return Column( - children: [ - // Header móvil - _buildMobileHeader(provider), - - // Contenido principal - Expanded( - child: showMapView - ? _buildMapView() - : NegociosCardsView(provider: provider), - ), - ], - ); - } - }, - ), - ), - // FAB para vista móvil - floatingActionButton: MediaQuery.of(context).size.width <= 800 - ? _buildMobileFAB(context) - : null, - ); - } - - Widget _buildEnhancedHeader(EmpresasNegociosProvider provider) { - return Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 20, - offset: const Offset(0, 8), - ), - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - children: [ - // Icono animado - TweenAnimationBuilder( - duration: const Duration(milliseconds: 1000), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, value, child) { - return Transform.rotate( - angle: value * 0.1, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.white.withOpacity(0.3), - width: 2, - ), - ), - child: Icon( - Icons.business_center, - color: Colors.white, - size: 32, - ), - ), - ); - }, - ), - - const SizedBox(width: 20), - - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Título principal con gradiente - ShaderMask( - shaderCallback: (bounds) => LinearGradient( - colors: [Colors.white, Colors.white.withOpacity(0.8)], - ).createShader(bounds), - child: Text( - provider.empresaSeleccionada != null - ? 'Sucursales de ${provider.empresaSeleccionada!.nombre}' - : 'Gestión de Empresas y Sucursales', - style: const TextStyle( - color: Colors.white, - fontSize: 28, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ), - const SizedBox(height: 12), - if (provider.empresaSeleccionada != null) ...[ - Row( - children: [ - // Badge animado - TweenAnimationBuilder( - duration: const Duration(milliseconds: 600), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, value, child) { - return Transform.scale( - scale: value, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.9), - borderRadius: BorderRadius.circular(25), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.store, - color: AppTheme.of(context).primaryColor, - size: 18, - ), - const SizedBox(width: 8), - Text( - '${provider.negocios.length} sucursales', - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ); - }, - ), - const SizedBox(width: 16), - Expanded( - child: Text( - provider.empresaSeleccionada!.rfc, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ], - ], - ), - ), - - // Switch mejorado para cambiar vista - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.white.withOpacity(0.2), - ), - ), - child: Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.table_chart, - color: Colors.white, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Vista', - style: TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: 8), - Icon( - Icons.map, - color: Colors.white, - size: 20, - ), - ], - ), - const SizedBox(height: 12), - Switch( - value: showMapView, - onChanged: (value) { - setState(() { - showMapView = value; - }); - }, - activeColor: Colors.white, - activeTrackColor: Colors.white.withOpacity(0.3), - inactiveThumbColor: Colors.white.withOpacity(0.7), - inactiveTrackColor: Colors.white.withOpacity(0.1), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildMobileHeader(EmpresasNegociosProvider provider) { - return Container( - padding: const EdgeInsets.fromLTRB(20, 40, 20, 20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(30), - bottomRight: Radius.circular(30), - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - Icons.business, - color: Colors.white, - size: 24, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Text( - 'NETHIVE', - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - letterSpacing: 1.2, - ), - ), - ), - // Switch para modo vista - Switch( - value: showMapView, - onChanged: (value) { - setState(() { - showMapView = value; - }); - }, - activeColor: Colors.white, - ), - ], - ), - if (provider.empresaSeleccionada != null) ...[ - const SizedBox(height: 16), - Text( - provider.empresaSeleccionada!.nombre, - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${provider.negocios.length} sucursales', - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ], - ), - ); - } - - Widget _buildTableView(EmpresasNegociosProvider provider) { - if (provider.empresaSeleccionada == null) { - return _buildEmptyState(); - } - - return Container( - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: Column( - children: [ - // Header de la tabla mejorado - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).modernGradient, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.store, - color: Colors.white, - size: 20, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Sucursales', - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - Text( - 'Gestión y control de ubicaciones', - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 14, - ), - ), - ], - ), - ), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${provider.negocios.length} registros', - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - - // Tabla de negocios - Expanded( - child: NegociosTable(provider: provider), - ), - ], - ), - ); - } - - Widget _buildEmptyState() { - return Center( - child: TweenAnimationBuilder( - duration: const Duration(milliseconds: 1000), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, value, child) { - return Transform.scale( - scale: value, - child: Container( - padding: const EdgeInsets.all(40), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.business_center, - size: 80, - color: Colors.white, - ), - const SizedBox(height: 20), - Text( - 'Selecciona una empresa', - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - Text( - 'Elige una empresa del panel lateral\npara ver y gestionar sus sucursales', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 16, - ), - ), - ], - ), - ), - ); - }, - ), - ); - } - - Widget _buildMapView() { - return Consumer( - builder: (context, provider, child) { - return NegociosMapView(provider: provider); - }, - ); - } - - Widget _buildMobileFAB(BuildContext context) { - return Consumer( - builder: (context, provider, child) { - return FloatingActionButton.extended( - onPressed: () { - _showMobileEmpresaSelector(context, provider); - }, - backgroundColor: AppTheme.of(context).primaryColor, - icon: const Icon(Icons.business, color: Colors.white), - label: Text( - provider.empresaSeleccionada != null - ? provider.empresaSeleccionada!.nombre.length > 15 - ? '${provider.empresaSeleccionada!.nombre.substring(0, 15)}...' - : provider.empresaSeleccionada!.nombre - : 'Empresas', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ); - }, - ); - } - - void _showMobileEmpresaSelector( - BuildContext context, EmpresasNegociosProvider provider) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => DraggableScrollableSheet( - initialChildSize: 0.7, - maxChildSize: 0.95, - minChildSize: 0.3, - builder: (context, scrollController) => MobileEmpresaSelector( - provider: provider, - onEmpresaSelected: (empresaId) { - provider.setEmpresaSeleccionada(empresaId); - }, - ), - ), - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog.dart deleted file mode 100644 index 46538c5..0000000 --- a/lib/pages/empresa_negocios/widgets/add_empresa_dialog.dart +++ /dev/null @@ -1,1543 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class AddEmpresaDialog extends StatefulWidget { - final EmpresasNegociosProvider provider; - - const AddEmpresaDialog({ - Key? key, - required this.provider, - }) : super(key: key); - - @override - State createState() => _AddEmpresaDialogState(); -} - -class _AddEmpresaDialogState extends State - with TickerProviderStateMixin { - final _formKey = GlobalKey(); - final _nombreController = TextEditingController(); - final _rfcController = TextEditingController(); - final _direccionController = TextEditingController(); - final _telefonoController = TextEditingController(); - final _emailController = TextEditingController(); - - bool _isLoading = false; - late AnimationController _scaleController; - late AnimationController _slideController; - late AnimationController _fadeController; - late Animation _scaleAnimation; - late Animation _slideAnimation; - late Animation _fadeAnimation; - bool _isAnimationInitialized = false; - - @override - void initState() { - super.initState(); - _initializeAnimations(); - // Escuchar cambios del provider - widget.provider.addListener(_onProviderChanged); - } - - void _onProviderChanged() { - if (mounted) { - setState(() { - // Forzar rebuild cuando cambie el provider - }); - } - } - - void _initializeAnimations() { - _scaleController = AnimationController( - duration: const Duration(milliseconds: 600), - vsync: this, - ); - _slideController = AnimationController( - duration: const Duration(milliseconds: 800), - vsync: this, - ); - _fadeController = AnimationController( - duration: const Duration(milliseconds: 400), - vsync: this, - ); - - _scaleAnimation = CurvedAnimation( - parent: _scaleController, - curve: Curves.elasticOut, - ); - _slideAnimation = Tween( - begin: const Offset(0, -1), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _slideController, - curve: Curves.easeOutBack, - )); - _fadeAnimation = CurvedAnimation( - parent: _fadeController, - curve: Curves.easeInOut, - ); - - // Pequeño delay para asegurar que el widget esté completamente montado - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _isAnimationInitialized = true; - }); - _startAnimations(); - } - }); - } - - void _startAnimations() { - _fadeController.forward(); - Future.delayed(const Duration(milliseconds: 100), () { - if (mounted) _scaleController.forward(); - }); - Future.delayed(const Duration(milliseconds: 200), () { - if (mounted) _slideController.forward(); - }); - } - - @override - void dispose() { - widget.provider.removeListener(_onProviderChanged); - _scaleController.dispose(); - _slideController.dispose(); - _fadeController.dispose(); - _nombreController.dispose(); - _rfcController.dispose(); - _direccionController.dispose(); - _telefonoController.dispose(); - _emailController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (!_isAnimationInitialized) { - return const SizedBox.shrink(); - } - - // Detectar el tamaño de pantalla - final screenSize = MediaQuery.of(context).size; - final isDesktop = screenSize.width > 1024; - final isTablet = screenSize.width > 768 && screenSize.width <= 1024; - - // Ajustar dimensiones según el tipo de pantalla - final maxWidth = isDesktop ? 900.0 : (isTablet ? 750.0 : 650.0); - final maxHeight = isDesktop ? 700.0 : 750.0; - - return AnimatedBuilder( - animation: - Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]), - builder: (context, child) { - return FadeTransition( - opacity: _fadeAnimation, - child: Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.all(isDesktop ? 40 : 20), - child: Transform.scale( - scale: _scaleAnimation.value, - child: Container( - constraints: BoxConstraints( - maxWidth: maxWidth, - maxHeight: maxHeight, - minHeight: isDesktop ? 600 : 400, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.4), - blurRadius: 40, - offset: const Offset(0, 20), - spreadRadius: 8, - ), - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 60, - offset: const Offset(0, 10), - spreadRadius: 2, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(30), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryBackground, - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - stops: const [0.0, 0.6, 1.0], - ), - ), - child: isDesktop - ? _buildDesktopLayout() - : _buildMobileLayout(), - ), - ), - ), - ), - ), - ); - }, - ); - } - - Widget _buildDesktopLayout() { - return Row( - children: [ - // Header lateral compacto para desktop - Container( - width: 280, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.5), - blurRadius: 25, - offset: const Offset(5, 0), - ), - ], - ), - child: SlideTransition( - position: _slideAnimation, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 25), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Icono compacto - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - Colors.white.withOpacity(0.4), - Colors.white.withOpacity(0.1), - Colors.transparent, - ], - ), - border: Border.all( - color: Colors.white.withOpacity(0.6), - width: 3, - ), - boxShadow: [ - BoxShadow( - color: Colors.white.withOpacity(0.4), - blurRadius: 20, - spreadRadius: 5, - ), - ], - ), - child: const Icon( - Icons.business_center_rounded, - color: Colors.white, - size: 35, - ), - ), - const SizedBox(height: 20), - - // Título compacto - Text( - 'Nueva Empresa', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - letterSpacing: 1.5, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - - Container( - padding: - const EdgeInsets.symmetric(horizontal: 15, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: Colors.white.withOpacity(0.3), - ), - ), - child: Text( - '✨ Registra una nueva empresa', - style: TextStyle( - color: Colors.white.withOpacity(0.95), - fontSize: 14, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - ), - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - ), - ), - - // Contenido principal del formulario - Expanded( - child: Padding( - padding: const EdgeInsets.all(25), - child: Form( - key: _formKey, - child: Column( - children: [ - // Formulario en columnas para aprovechar el espacio - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - // Primera fila - Nombre y RFC - Row( - children: [ - Expanded( - flex: 2, - child: _buildCompactFormField( - controller: _nombreController, - label: 'Nombre de la empresa', - hint: 'Ej: TechCorp Solutions S.A.', - icon: Icons.business_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es requerido'; - } - return null; - }, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildCompactFormField( - controller: _rfcController, - label: 'RFC', - hint: 'Ej: ABC123456789', - icon: Icons.assignment_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El RFC es requerido'; - } - return null; - }, - ), - ), - ], - ), - - const SizedBox(height: 16), - - // Segunda fila - Dirección - _buildCompactFormField( - controller: _direccionController, - label: 'Dirección', - hint: 'Dirección completa de la empresa', - icon: Icons.location_on_rounded, - maxLines: 2, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La dirección es requerida'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - // Tercera fila - Teléfono y Email - Row( - children: [ - Expanded( - child: _buildCompactFormField( - controller: _telefonoController, - label: 'Teléfono', - hint: 'Ej: +52 555 123 4567', - icon: Icons.phone_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El teléfono es requerido'; - } - return null; - }, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildCompactFormField( - controller: _emailController, - label: 'Email', - hint: 'contacto@empresa.com', - icon: Icons.email_rounded, - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El email es requerido'; - } - if (!RegExp( - r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') - .hasMatch(value)) { - return 'Email inválido'; - } - return null; - }, - ), - ), - ], - ), - - const SizedBox(height: 20), - - // Sección de archivos compacta - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context) - .primaryColor - .withOpacity(0.1), - AppTheme.of(context) - .tertiaryColor - .withOpacity(0.1), - AppTheme.of(context) - .secondaryColor - .withOpacity(0.05), - ], - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.4), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.2), - blurRadius: 15, - offset: const Offset(0, 8), - spreadRadius: 2, - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header de la sección - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: AppTheme.of(context) - .primaryGradient, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.4), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: const Icon( - Icons.cloud_upload_rounded, - color: Colors.white, - size: 18, - ), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Archivos Opcionales', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: AppTheme.of(context) - .primaryText, - ), - ), - Text( - 'Logo e imagen de la empresa', - style: TextStyle( - fontSize: 12, - color: AppTheme.of(context) - .secondaryText, - ), - ), - ], - ), - ], - ), - - const SizedBox(height: 16), - - // Botones de archivos en fila - Row( - children: [ - Expanded( - child: _buildCompactFileButton( - label: 'Logo de la empresa', - subtitle: 'PNG, JPG (Max 2MB)', - icon: Icons.image_rounded, - fileName: widget.provider.logoFileName, - file: widget.provider.logoToUpload, - onPressed: widget.provider.selectLogo, - gradient: LinearGradient( - colors: [ - Colors.blue.shade400, - Colors.blue.shade600 - ], - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildCompactFileButton( - label: 'Imagen principal', - subtitle: 'Imagen representativa', - icon: Icons.photo_library_rounded, - fileName: - widget.provider.imagenFileName, - file: widget.provider.imagenToUpload, - onPressed: widget.provider.selectImagen, - gradient: LinearGradient( - colors: [ - Colors.purple.shade400, - Colors.purple.shade600 - ], - ), - ), - ), - ], - ), - ], - ), - ), - - const SizedBox(height: 25), - - // Botones de acción - Row( - children: [ - // Botón cancelar - Expanded( - child: Container( - height: 50, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: AppTheme.of(context) - .secondaryText - .withOpacity(0.4), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: TextButton( - onPressed: _isLoading - ? null - : () { - widget.provider.resetFormData(); - Navigator.of(context).pop(); - }, - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon( - Icons.close_rounded, - color: AppTheme.of(context) - .secondaryText, - size: 18, - ), - const SizedBox(width: 8), - Text( - 'Cancelar', - style: TextStyle( - color: AppTheme.of(context) - .secondaryText, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ), - - const SizedBox(width: 20), - - // Botón crear empresa - Expanded( - flex: 2, - child: Container( - height: 50, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.5), - blurRadius: 20, - offset: const Offset(0, 8), - spreadRadius: 2, - ), - ], - ), - child: ElevatedButton( - onPressed: - _isLoading ? null : _crearEmpresa, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - child: _isLoading - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 3, - valueColor: - AlwaysStoppedAnimation( - Colors.white), - ), - ) - : Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: const [ - Icon( - Icons.add_business_rounded, - color: Colors.white, - size: 20, - ), - SizedBox(width: 12), - Text( - 'Crear Empresa', - style: TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ], - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ], - ), - ), - ), - ), - ], - ); - } - - Widget _buildMobileLayout() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header espectacular con animación (más compacto) - SlideTransition( - position: _slideAnimation, - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 25), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.5), - blurRadius: 25, - offset: const Offset(0, 15), - spreadRadius: 2, - ), - ], - ), - child: Column( - children: [ - // Icono central compacto - Container( - padding: const EdgeInsets.all(18), - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - Colors.white.withOpacity(0.4), - Colors.white.withOpacity(0.1), - Colors.transparent, - ], - ), - border: Border.all( - color: Colors.white.withOpacity(0.6), - width: 3, - ), - boxShadow: [ - BoxShadow( - color: Colors.white.withOpacity(0.4), - blurRadius: 20, - spreadRadius: 5, - ), - ], - ), - child: const Icon( - Icons.business_center_rounded, - color: Colors.white, - size: 35, - ), - ), - - const SizedBox(height: 16), - - // Título compacto - Text( - 'Nueva Empresa', - style: TextStyle( - color: Colors.white, - fontSize: 26, - fontWeight: FontWeight.bold, - letterSpacing: 1.5, - ), - ), - - const SizedBox(height: 8), - - Container( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: Colors.white.withOpacity(0.3), - ), - ), - child: Text( - '✨ Registra una nueva empresa en tu sistema', - style: TextStyle( - color: Colors.white.withOpacity(0.95), - fontSize: 14, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - ), - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - ), - - // Contenido del formulario mejorado - Flexible( - child: SingleChildScrollView( - padding: const EdgeInsets.all(25), - child: Form( - key: _formKey, - child: Column( - children: [ - // Campos del formulario con animaciones compactas - _buildCompactFormField( - controller: _nombreController, - label: 'Nombre de la empresa', - hint: 'Ej: TechCorp Solutions S.A. de C.V.', - icon: Icons.business_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es requerido'; - } - return null; - }, - ), - - _buildCompactFormField( - controller: _rfcController, - label: 'RFC', - hint: 'Ej: ABC123456789', - icon: Icons.assignment_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El RFC es requerido'; - } - return null; - }, - ), - - _buildCompactFormField( - controller: _direccionController, - label: 'Dirección', - hint: 'Dirección completa de la empresa', - icon: Icons.location_on_rounded, - maxLines: 3, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La dirección es requerida'; - } - return null; - }, - ), - - _buildCompactFormField( - controller: _telefonoController, - label: 'Teléfono', - hint: 'Ej: +52 555 123 4567', - icon: Icons.phone_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El teléfono es requerido'; - } - return null; - }, - ), - - _buildCompactFormField( - controller: _emailController, - label: 'Email', - hint: 'contacto@empresa.com', - icon: Icons.email_rounded, - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El email es requerido'; - } - if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') - .hasMatch(value)) { - return 'Email inválido'; - } - return null; - }, - ), - - const SizedBox(height: 20), - - // Sección de archivos compacta para móvil - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor.withOpacity(0.1), - AppTheme.of(context).tertiaryColor.withOpacity(0.1), - AppTheme.of(context).secondaryColor.withOpacity(0.05), - ], - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: - AppTheme.of(context).primaryColor.withOpacity(0.4), - width: 2, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header de la sección - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - ), - child: const Icon( - Icons.cloud_upload_rounded, - color: Colors.white, - size: 18, - ), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Archivos Opcionales', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: AppTheme.of(context).primaryText, - ), - ), - Text( - 'Logo e imagen de la empresa', - style: TextStyle( - fontSize: 12, - color: AppTheme.of(context).secondaryText, - ), - ), - ], - ), - ], - ), - - const SizedBox(height: 16), - - // Botones de archivos mejorados - _buildEnhancedFileButton( - label: 'Logo de la empresa', - subtitle: 'Formato PNG, JPG (Max 2MB)', - icon: Icons.image_rounded, - fileName: widget.provider.logoFileName, - file: widget.provider.logoToUpload, - onPressed: widget.provider.selectLogo, - gradient: LinearGradient( - colors: [ - Colors.blue.shade400, - Colors.blue.shade600 - ], - ), - ), - - const SizedBox(height: 12), - - _buildEnhancedFileButton( - label: 'Imagen principal', - subtitle: 'Imagen representativa de la empresa', - icon: Icons.photo_library_rounded, - fileName: widget.provider.imagenFileName, - file: widget.provider.imagenToUpload, - onPressed: widget.provider.selectImagen, - gradient: LinearGradient( - colors: [ - Colors.purple.shade400, - Colors.purple.shade600 - ], - ), - ), - ], - ), - ), - - const SizedBox(height: 25), - - // Botones de acción compactos - Row( - children: [ - // Botón cancelar - Expanded( - child: Container( - height: 50, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: AppTheme.of(context) - .secondaryText - .withOpacity(0.4), - width: 2, - ), - ), - child: TextButton( - onPressed: _isLoading - ? null - : () { - widget.provider.resetFormData(); - Navigator.of(context).pop(); - }, - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.close_rounded, - color: AppTheme.of(context).secondaryText, - size: 18, - ), - const SizedBox(width: 8), - Text( - 'Cancelar', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 15, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ), - - const SizedBox(width: 16), - - // Botón crear empresa - Expanded( - flex: 2, - child: Container( - height: 50, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.5), - blurRadius: 20, - offset: const Offset(0, 8), - spreadRadius: 2, - ), - ], - ), - child: ElevatedButton( - onPressed: _isLoading ? null : _crearEmpresa, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - child: _isLoading - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 3, - valueColor: AlwaysStoppedAnimation( - Colors.white), - ), - ) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon( - Icons.add_business_rounded, - color: Colors.white, - size: 20, - ), - SizedBox(width: 10), - Text( - 'Crear Empresa', - style: TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ], - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - } - - Widget _buildCompactFormField({ - required TextEditingController controller, - required String label, - required String hint, - required IconData icon, - int maxLines = 1, - TextInputType? keyboardType, - String? Function(String?)? validator, - }) { - return Container( - margin: const EdgeInsets.only(bottom: 16), - child: TextFormField( - controller: controller, - maxLines: maxLines, - keyboardType: keyboardType, - validator: validator, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - decoration: InputDecoration( - labelText: label, - hintText: hint, - prefixIcon: Container( - margin: const EdgeInsets.all(8), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - child: Icon( - icon, - color: Colors.white, - size: 18, - ), - ), - labelStyle: TextStyle( - color: AppTheme.of(context).primaryColor, - fontWeight: FontWeight.w600, - fontSize: 14, - ), - hintStyle: TextStyle( - color: AppTheme.of(context).secondaryText.withOpacity(0.7), - fontSize: 12, - ), - filled: true, - fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.7), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor, - width: 2, - ), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: const BorderSide( - color: Colors.red, - width: 2, - ), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - ), - ), - ); - } - - Widget _buildCompactFileButton({ - required String label, - required String subtitle, - required IconData icon, - required String? fileName, - required dynamic file, - required VoidCallback onPressed, - required Gradient gradient, - }) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onPressed, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - children: [ - // Icono con gradiente - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - gradient: gradient, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - child: Icon( - icon, - color: Colors.white, - size: 20, - ), - ), - - const SizedBox(height: 8), - - // Información del archivo - Text( - label, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.bold, - fontSize: 13, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 4), - Text( - fileName ?? subtitle, - style: TextStyle( - color: fileName != null - ? AppTheme.of(context).primaryColor - : AppTheme.of(context).secondaryText, - fontSize: 11, - fontWeight: - fileName != null ? FontWeight.w600 : FontWeight.normal, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ), - - // Preview de imagen si existe - if (file != null) ...[ - const SizedBox(height: 8), - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: - AppTheme.of(context).primaryColor.withOpacity(0.4), - width: 2, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(6), - child: widget.provider.getImageWidget( - file, - height: 40, - width: 40, - ), - ), - ), - ], - ], - ), - ), - ), - ), - ); - } - - Widget _buildEnhancedFileButton({ - required String label, - required String subtitle, - required IconData icon, - required String? fileName, - required dynamic file, - required VoidCallback onPressed, - required Gradient gradient, - }) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - blurRadius: 15, - offset: const Offset(0, 6), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onPressed, - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - // Icono con gradiente - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - gradient: gradient, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Icon( - icon, - color: Colors.white, - size: 24, - ), - ), - - const SizedBox(width: 16), - - // Información del archivo - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - const SizedBox(height: 4), - Text( - fileName ?? subtitle, - style: TextStyle( - color: fileName != null - ? AppTheme.of(context).primaryColor - : AppTheme.of(context).secondaryText, - fontSize: 13, - fontWeight: fileName != null - ? FontWeight.w600 - : FontWeight.normal, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - - // Preview de imagen si existe - if (file != null) ...[ - const SizedBox(width: 16), - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: - AppTheme.of(context).primaryColor.withOpacity(0.4), - width: 2, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: widget.provider.getImageWidget( - file, - height: 50, - width: 50, - ), - ), - ), - ] else ...[ - const SizedBox(width: 16), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.upload_file, - color: AppTheme.of(context).primaryColor, - size: 20, - ), - ), - ], - ], - ), - ), - ), - ), - ); - } - - Future _crearEmpresa() async { - if (!_formKey.currentState!.validate()) return; - - setState(() { - _isLoading = true; - }); - - try { - final success = await widget.provider.crearEmpresa( - nombre: _nombreController.text.trim(), - rfc: _rfcController.text.trim(), - direccion: _direccionController.text.trim(), - telefono: _telefonoController.text.trim(), - email: _emailController.text.trim(), - ); - - if (mounted) { - if (success) { - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.check_circle, color: Colors.white), - SizedBox(width: 12), - Text( - 'Empresa creada exitosamente', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.green, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.error, color: Colors.white), - SizedBox(width: 12), - Text( - 'Error al crear la empresa', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(Icons.warning, color: Colors.white), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Error: $e', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/add_empresa_dialog.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/add_empresa_dialog.dart deleted file mode 100644 index 7e4d6eb..0000000 --- a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/add_empresa_dialog.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; -import 'empresa_dialog_animations.dart'; -import 'empresa_dialog_header.dart'; -import 'empresa_dialog_form.dart'; - -class AddEmpresaDialog extends StatefulWidget { - final EmpresasNegociosProvider provider; - - const AddEmpresaDialog({ - Key? key, - required this.provider, - }) : super(key: key); - - @override - State createState() => _AddEmpresaDialogState(); -} - -class _AddEmpresaDialogState extends State - with TickerProviderStateMixin { - late EmpresaDialogAnimations _animations; - - @override - void initState() { - super.initState(); - _animations = EmpresaDialogAnimations(vsync: this); - _animations.initialize(); - - // Escuchar cambios del provider - widget.provider.addListener(_onProviderChanged); - } - - void _onProviderChanged() { - if (mounted) { - setState(() { - // Forzar rebuild cuando cambie el provider - }); - } - } - - @override - void dispose() { - widget.provider.removeListener(_onProviderChanged); - _animations.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (!_animations.isInitialized) { - return const SizedBox.shrink(); - } - - // Detectar el tamaño de pantalla - final screenSize = MediaQuery.of(context).size; - final isDesktop = screenSize.width > 1024; - final isTablet = screenSize.width > 768 && screenSize.width <= 1024; - - // Ajustar dimensiones según el tipo de pantalla - final maxWidth = isDesktop ? 900.0 : (isTablet ? 750.0 : 650.0); - final maxHeight = isDesktop ? 700.0 : 750.0; - - return AnimatedBuilder( - animation: _animations.combinedAnimation, - builder: (context, child) { - return FadeTransition( - opacity: _animations.fadeAnimation, - child: Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.all(isDesktop ? 40 : 20), - child: Transform.scale( - scale: _animations.scaleAnimation.value, - child: Container( - constraints: BoxConstraints( - maxWidth: maxWidth, - maxHeight: maxHeight, - minHeight: isDesktop ? 600 : 400, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.4), - blurRadius: 40, - offset: const Offset(0, 20), - spreadRadius: 8, - ), - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 60, - offset: const Offset(0, 10), - spreadRadius: 2, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(30), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryBackground, - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - stops: const [0.0, 0.6, 1.0], - ), - ), - child: isDesktop - ? _buildDesktopLayout() - : _buildMobileLayout(), - ), - ), - ), - ), - ), - ); - }, - ); - } - - Widget _buildDesktopLayout() { - return Row( - children: [ - // Header lateral compacto para desktop - EmpresaDialogHeader( - isDesktop: true, - slideAnimation: _animations.slideAnimation, - ), - - // Contenido principal del formulario - Expanded( - child: EmpresaDialogForm( - provider: widget.provider, - isDesktop: true, - ), - ), - ], - ); - } - - Widget _buildMobileLayout() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header para móvil - EmpresaDialogHeader( - isDesktop: false, - slideAnimation: _animations.slideAnimation, - ), - - // Contenido del formulario para móvil - Flexible( - child: EmpresaDialogForm( - provider: widget.provider, - isDesktop: false, - ), - ), - ], - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_action_buttons.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_action_buttons.dart deleted file mode 100644 index 3756248..0000000 --- a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_action_buttons.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class EmpresaActionButtons extends StatelessWidget { - final bool isLoading; - final bool isDesktop; - final VoidCallback onCancel; - final VoidCallback onSubmit; - - const EmpresaActionButtons({ - Key? key, - required this.isLoading, - required this.isDesktop, - required this.onCancel, - required this.onSubmit, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - // Botón Cancelar - Expanded( - child: Container( - height: isDesktop ? 45 : 50, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: AppTheme.of(context).secondaryText.withOpacity(0.3), - width: 2, - ), - ), - child: TextButton( - onPressed: isLoading ? null : onCancel, - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.close_rounded, - color: AppTheme.of(context).secondaryText, - size: 18, - ), - const SizedBox(width: 8), - Text( - 'Cancelar', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ), - - const SizedBox(width: 16), - - // Botón Crear Empresa - Expanded( - flex: 2, - child: Container( - height: isDesktop ? 45 : 50, - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.4), - blurRadius: 15, - offset: const Offset(0, 5), - spreadRadius: 2, - ), - ], - ), - child: ElevatedButton( - onPressed: isLoading ? null : onSubmit, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - child: isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.business_center_rounded, - color: Colors.white, - size: 20, - ), - const SizedBox(width: 10), - Text( - 'Crear Empresa', - style: TextStyle( - color: Colors.white, - fontSize: isDesktop ? 14 : 16, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ], - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_animations.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_animations.dart deleted file mode 100644 index c99ba64..0000000 --- a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_animations.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter/material.dart'; - -class EmpresaDialogAnimations { - final TickerProvider vsync; - - late AnimationController _scaleController; - late AnimationController _slideController; - late AnimationController _fadeController; - late Animation _scaleAnimation; - late Animation _slideAnimation; - late Animation _fadeAnimation; - late Listenable _combinedAnimation; - bool _isInitialized = false; - - EmpresaDialogAnimations({required this.vsync}); - - // Getters para acceder a las animaciones - Animation get scaleAnimation => _scaleAnimation; - Animation get slideAnimation => _slideAnimation; - Animation get fadeAnimation => _fadeAnimation; - Listenable get combinedAnimation => _combinedAnimation; - bool get isInitialized => _isInitialized; - - void initialize() { - _scaleController = AnimationController( - duration: const Duration(milliseconds: 600), - vsync: vsync, - ); - _slideController = AnimationController( - duration: const Duration(milliseconds: 800), - vsync: vsync, - ); - _fadeController = AnimationController( - duration: const Duration(milliseconds: 400), - vsync: vsync, - ); - - _scaleAnimation = CurvedAnimation( - parent: _scaleController, - curve: Curves.elasticOut, - ); - _slideAnimation = Tween( - begin: const Offset(0, -1), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _slideController, - curve: Curves.easeOutBack, - )); - _fadeAnimation = CurvedAnimation( - parent: _fadeController, - curve: Curves.easeInOut, - ); - - _combinedAnimation = - Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]); - - // Pequeño delay para asegurar que el widget esté completamente montado - WidgetsBinding.instance.addPostFrameCallback((_) { - _isInitialized = true; - _startAnimations(); - }); - } - - void _startAnimations() { - _fadeController.forward(); - Future.delayed(const Duration(milliseconds: 100), () { - _scaleController.forward(); - }); - Future.delayed(const Duration(milliseconds: 200), () { - _slideController.forward(); - }); - } - - void dispose() { - _scaleController.dispose(); - _slideController.dispose(); - _fadeController.dispose(); - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_form.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_form.dart deleted file mode 100644 index 2d79fb2..0000000 --- a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_form.dart +++ /dev/null @@ -1,228 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; -import 'empresa_form_fields.dart'; -import 'empresa_file_section.dart'; -import 'empresa_action_buttons.dart'; - -class EmpresaDialogForm extends StatefulWidget { - final EmpresasNegociosProvider provider; - final bool isDesktop; - - const EmpresaDialogForm({ - Key? key, - required this.provider, - required this.isDesktop, - }) : super(key: key); - - @override - State createState() => _EmpresaDialogFormState(); -} - -class _EmpresaDialogFormState extends State { - final _formKey = GlobalKey(); - final _nombreController = TextEditingController(); - final _rfcController = TextEditingController(); - final _direccionController = TextEditingController(); - final _telefonoController = TextEditingController(); - final _emailController = TextEditingController(); - - bool _isLoading = false; - - @override - void dispose() { - _nombreController.dispose(); - _rfcController.dispose(); - _direccionController.dispose(); - _telefonoController.dispose(); - _emailController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(25), - child: Form( - key: _formKey, - child: widget.isDesktop ? _buildDesktopForm() : _buildMobileForm(), - ), - ); - } - - Widget _buildDesktopForm() { - return Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - // Campos del formulario en filas para desktop - EmpresaFormFields( - isDesktop: true, - nombreController: _nombreController, - rfcController: _rfcController, - direccionController: _direccionController, - telefonoController: _telefonoController, - emailController: _emailController, - ), - - const SizedBox(height: 20), - - // Sección de archivos - EmpresaFileSection( - provider: widget.provider, - isDesktop: true, - ), - - const SizedBox(height: 25), - - // Botones de acción - EmpresaActionButtons( - isLoading: _isLoading, - isDesktop: true, - onCancel: () => _handleCancel(), - onSubmit: () => _crearEmpresa(), - ), - ], - ), - ), - ), - ], - ); - } - - Widget _buildMobileForm() { - return SingleChildScrollView( - child: Column( - children: [ - // Campos del formulario en columnas para móvil - EmpresaFormFields( - isDesktop: false, - nombreController: _nombreController, - rfcController: _rfcController, - direccionController: _direccionController, - telefonoController: _telefonoController, - emailController: _emailController, - ), - - const SizedBox(height: 20), - - // Sección de archivos - EmpresaFileSection( - provider: widget.provider, - isDesktop: false, - ), - - const SizedBox(height: 25), - - // Botones de acción - EmpresaActionButtons( - isLoading: _isLoading, - isDesktop: false, - onCancel: () => _handleCancel(), - onSubmit: () => _crearEmpresa(), - ), - ], - ), - ); - } - - void _handleCancel() { - widget.provider.resetFormData(); - Navigator.of(context).pop(); - } - - Future _crearEmpresa() async { - if (!_formKey.currentState!.validate()) return; - - setState(() { - _isLoading = true; - }); - - try { - final success = await widget.provider.crearEmpresa( - nombre: _nombreController.text.trim(), - rfc: _rfcController.text.trim(), - direccion: _direccionController.text.trim(), - telefono: _telefonoController.text.trim(), - email: _emailController.text.trim(), - ); - - if (mounted) { - if (success) { - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.check_circle, color: Colors.white), - SizedBox(width: 12), - Text( - 'Empresa creada exitosamente', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.green, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.error, color: Colors.white), - SizedBox(width: 12), - Text( - 'Error al crear la empresa', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(Icons.warning, color: Colors.white), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Error: $e', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_header.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_header.dart deleted file mode 100644 index 9c6d807..0000000 --- a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_header.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class EmpresaDialogHeader extends StatelessWidget { - final bool isDesktop; - final Animation slideAnimation; - - const EmpresaDialogHeader({ - Key? key, - required this.isDesktop, - required this.slideAnimation, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - if (isDesktop) { - return _buildDesktopHeader(context); - } else { - return _buildMobileHeader(context); - } - } - - Widget _buildDesktopHeader(BuildContext context) { - return Container( - width: 280, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.5), - blurRadius: 25, - offset: const Offset(5, 0), - ), - ], - ), - child: SlideTransition( - position: slideAnimation, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 25), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildIcon(context), - const SizedBox(height: 20), - _buildTitle(context, fontSize: 24), - const SizedBox(height: 8), - _buildSubtitle(context, fontSize: 14), - ], - ), - ), - ), - ); - } - - Widget _buildMobileHeader(BuildContext context) { - return SlideTransition( - position: slideAnimation, - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 25), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.5), - blurRadius: 25, - offset: const Offset(0, 15), - spreadRadius: 2, - ), - ], - ), - child: Column( - children: [ - _buildIcon(context), - const SizedBox(height: 16), - _buildTitle(context, fontSize: 26), - const SizedBox(height: 8), - _buildSubtitle(context, fontSize: 14, isMobile: true), - ], - ), - ), - ); - } - - Widget _buildIcon(BuildContext context) { - return Container( - padding: EdgeInsets.all(isDesktop ? 20 : 18), - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - Colors.white.withOpacity(0.4), - Colors.white.withOpacity(0.1), - Colors.transparent, - ], - ), - border: Border.all( - color: Colors.white.withOpacity(0.6), - width: 3, - ), - boxShadow: [ - BoxShadow( - color: Colors.white.withOpacity(0.4), - blurRadius: 20, - spreadRadius: 5, - ), - ], - ), - child: Icon( - Icons.business_center_rounded, - color: Colors.white, - size: isDesktop ? 35 : 35, - ), - ); - } - - Widget _buildTitle(BuildContext context, {required double fontSize}) { - return Text( - 'Nueva Empresa', - style: TextStyle( - color: Colors.white, - fontSize: fontSize, - fontWeight: FontWeight.bold, - letterSpacing: 1.5, - ), - textAlign: TextAlign.center, - ); - } - - Widget _buildSubtitle(BuildContext context, - {required double fontSize, bool isMobile = false}) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: Colors.white.withOpacity(0.3), - ), - ), - child: Text( - isMobile - ? '✨ Registra una nueva empresa en tu sistema' - : '✨ Registra una nueva empresa', - style: TextStyle( - color: Colors.white.withOpacity(0.95), - fontSize: fontSize, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - ), - textAlign: TextAlign.center, - ), - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_file_section.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_file_section.dart deleted file mode 100644 index c0970e2..0000000 --- a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_file_section.dart +++ /dev/null @@ -1,425 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class EmpresaFileSection extends StatelessWidget { - final EmpresasNegociosProvider provider; - final bool isDesktop; - - const EmpresaFileSection({ - Key? key, - required this.provider, - required this.isDesktop, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor.withOpacity(0.1), - AppTheme.of(context).tertiaryColor.withOpacity(0.1), - AppTheme.of(context).secondaryColor.withOpacity(0.05), - ], - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.4), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - blurRadius: 15, - offset: const Offset(0, 8), - spreadRadius: 2, - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header de la sección - _buildSectionHeader(context), - - const SizedBox(height: 16), - - // Botones de archivos - if (isDesktop) - _buildDesktopFileButtons(context) - else - _buildMobileFileButtons(context), - ], - ), - ); - } - - Widget _buildSectionHeader(BuildContext context) { - return Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.4), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: const Icon( - Icons.cloud_upload_rounded, - color: Colors.white, - size: 18, - ), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Archivos Opcionales', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: AppTheme.of(context).primaryText, - ), - ), - Text( - 'Logo e imagen de la empresa', - style: TextStyle( - fontSize: 12, - color: AppTheme.of(context).secondaryText, - ), - ), - ], - ), - ], - ); - } - - Widget _buildDesktopFileButtons(BuildContext context) { - return Row( - children: [ - Expanded( - child: _buildCompactFileButton( - context: context, - label: 'Logo de la empresa', - subtitle: 'PNG, JPG (Max 2MB)', - icon: Icons.image_rounded, - fileName: provider.logoFileName, - file: provider.logoToUpload, - onPressed: provider.selectLogo, - gradient: LinearGradient( - colors: [Colors.blue.shade400, Colors.blue.shade600], - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildCompactFileButton( - context: context, - label: 'Imagen principal', - subtitle: 'Imagen representativa', - icon: Icons.photo_library_rounded, - fileName: provider.imagenFileName, - file: provider.imagenToUpload, - onPressed: provider.selectImagen, - gradient: LinearGradient( - colors: [Colors.purple.shade400, Colors.purple.shade600], - ), - ), - ), - ], - ); - } - - Widget _buildMobileFileButtons(BuildContext context) { - return Column( - children: [ - _buildEnhancedFileButton( - context: context, - label: 'Logo de la empresa', - subtitle: 'Formato PNG, JPG (Max 2MB)', - icon: Icons.image_rounded, - fileName: provider.logoFileName, - file: provider.logoToUpload, - onPressed: provider.selectLogo, - gradient: LinearGradient( - colors: [Colors.blue.shade400, Colors.blue.shade600], - ), - ), - const SizedBox(height: 12), - _buildEnhancedFileButton( - context: context, - label: 'Imagen principal', - subtitle: 'Imagen representativa de la empresa', - icon: Icons.photo_library_rounded, - fileName: provider.imagenFileName, - file: provider.imagenToUpload, - onPressed: provider.selectImagen, - gradient: LinearGradient( - colors: [Colors.purple.shade400, Colors.purple.shade600], - ), - ), - ], - ); - } - - Widget _buildCompactFileButton({ - required BuildContext context, - required String label, - required String subtitle, - required IconData icon, - required String? fileName, - required dynamic file, - required VoidCallback onPressed, - required Gradient gradient, - }) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onPressed, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - children: [ - // Icono con gradiente - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - gradient: gradient, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - child: Icon( - icon, - color: Colors.white, - size: 20, - ), - ), - - const SizedBox(height: 8), - - // Información del archivo - Text( - label, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.bold, - fontSize: 13, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 4), - Text( - fileName ?? subtitle, - style: TextStyle( - color: fileName != null - ? AppTheme.of(context).primaryColor - : AppTheme.of(context).secondaryText, - fontSize: 11, - fontWeight: - fileName != null ? FontWeight.w600 : FontWeight.normal, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ), - - // Preview de imagen si existe - if (file != null) ...[ - const SizedBox(height: 8), - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: - AppTheme.of(context).primaryColor.withOpacity(0.4), - width: 2, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(6), - child: provider.getImageWidget( - file, - height: 40, - width: 40, - ), - ), - ), - ], - ], - ), - ), - ), - ), - ); - } - - Widget _buildEnhancedFileButton({ - required BuildContext context, - required String label, - required String subtitle, - required IconData icon, - required String? fileName, - required dynamic file, - required VoidCallback onPressed, - required Gradient gradient, - }) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - blurRadius: 15, - offset: const Offset(0, 6), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onPressed, - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - // Icono con gradiente - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - gradient: gradient, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Icon( - icon, - color: Colors.white, - size: 24, - ), - ), - - const SizedBox(width: 16), - - // Información del archivo - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - const SizedBox(height: 4), - Text( - fileName ?? subtitle, - style: TextStyle( - color: fileName != null - ? AppTheme.of(context).primaryColor - : AppTheme.of(context).secondaryText, - fontSize: 13, - fontWeight: fileName != null - ? FontWeight.w600 - : FontWeight.normal, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - - // Preview de imagen si existe - if (file != null) ...[ - const SizedBox(width: 16), - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: - AppTheme.of(context).primaryColor.withOpacity(0.4), - width: 2, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: provider.getImageWidget( - file, - height: 50, - width: 50, - ), - ), - ), - ] else ...[ - const SizedBox(width: 16), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.upload_file, - color: AppTheme.of(context).primaryColor, - size: 20, - ), - ), - ], - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_form_fields.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_form_fields.dart deleted file mode 100644 index 3b0e5e3..0000000 --- a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_form_fields.dart +++ /dev/null @@ -1,301 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class EmpresaFormFields extends StatelessWidget { - final bool isDesktop; - final TextEditingController nombreController; - final TextEditingController rfcController; - final TextEditingController direccionController; - final TextEditingController telefonoController; - final TextEditingController emailController; - - const EmpresaFormFields({ - Key? key, - required this.isDesktop, - required this.nombreController, - required this.rfcController, - required this.direccionController, - required this.telefonoController, - required this.emailController, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - if (isDesktop) { - return _buildDesktopFields(context); - } else { - return _buildMobileFields(context); - } - } - - Widget _buildDesktopFields(BuildContext context) { - return Column( - children: [ - // Primera fila - Nombre y RFC - Row( - children: [ - Expanded( - flex: 2, - child: _buildFormField( - context: context, - controller: nombreController, - label: 'Nombre de la empresa', - hint: 'Ej: TechCorp Solutions S.A.', - icon: Icons.business_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es requerido'; - } - return null; - }, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildFormField( - context: context, - controller: rfcController, - label: 'RFC', - hint: 'Ej: ABC123456789', - icon: Icons.assignment_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El RFC es requerido'; - } - return null; - }, - ), - ), - ], - ), - - const SizedBox(height: 16), - - // Segunda fila - Dirección - _buildFormField( - context: context, - controller: direccionController, - label: 'Dirección', - hint: 'Dirección completa de la empresa', - icon: Icons.location_on_rounded, - maxLines: 2, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La dirección es requerida'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - // Tercera fila - Teléfono y Email - Row( - children: [ - Expanded( - child: _buildFormField( - context: context, - controller: telefonoController, - label: 'Teléfono', - hint: 'Ej: +52 555 123 4567', - icon: Icons.phone_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El teléfono es requerido'; - } - return null; - }, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildFormField( - context: context, - controller: emailController, - label: 'Email', - hint: 'contacto@empresa.com', - icon: Icons.email_rounded, - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El email es requerido'; - } - if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') - .hasMatch(value)) { - return 'Email inválido'; - } - return null; - }, - ), - ), - ], - ), - ], - ); - } - - Widget _buildMobileFields(BuildContext context) { - return Column( - children: [ - _buildFormField( - context: context, - controller: nombreController, - label: 'Nombre de la empresa', - hint: 'Ej: TechCorp Solutions S.A. de C.V.', - icon: Icons.business_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es requerido'; - } - return null; - }, - ), - _buildFormField( - context: context, - controller: rfcController, - label: 'RFC', - hint: 'Ej: ABC123456789', - icon: Icons.assignment_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El RFC es requerido'; - } - return null; - }, - ), - _buildFormField( - context: context, - controller: direccionController, - label: 'Dirección', - hint: 'Dirección completa de la empresa', - icon: Icons.location_on_rounded, - maxLines: 3, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La dirección es requerida'; - } - return null; - }, - ), - _buildFormField( - context: context, - controller: telefonoController, - label: 'Teléfono', - hint: 'Ej: +52 555 123 4567', - icon: Icons.phone_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El teléfono es requerido'; - } - return null; - }, - ), - _buildFormField( - context: context, - controller: emailController, - label: 'Email', - hint: 'contacto@empresa.com', - icon: Icons.email_rounded, - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El email es requerido'; - } - if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { - return 'Email inválido'; - } - return null; - }, - ), - ], - ); - } - - Widget _buildFormField({ - required BuildContext context, - required TextEditingController controller, - required String label, - required String hint, - required IconData icon, - int maxLines = 1, - TextInputType? keyboardType, - String? Function(String?)? validator, - }) { - return Container( - margin: const EdgeInsets.only(bottom: 16), - child: TextFormField( - controller: controller, - maxLines: maxLines, - keyboardType: keyboardType, - validator: validator, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - decoration: InputDecoration( - labelText: label, - hintText: hint, - prefixIcon: Container( - margin: const EdgeInsets.all(8), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - child: Icon( - icon, - color: Colors.white, - size: 18, - ), - ), - labelStyle: TextStyle( - color: AppTheme.of(context).primaryColor, - fontWeight: FontWeight.w600, - fontSize: 14, - ), - hintStyle: TextStyle( - color: AppTheme.of(context).secondaryText.withOpacity(0.7), - fontSize: 12, - ), - filled: true, - fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.7), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor, - width: 2, - ), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: const BorderSide( - color: Colors.red, - width: 2, - ), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - ), - ), - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog.dart deleted file mode 100644 index 4f07b11..0000000 --- a/lib/pages/empresa_negocios/widgets/add_negocio_dialog.dart +++ /dev/null @@ -1,2052 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class AddNegocioDialog extends StatefulWidget { - final EmpresasNegociosProvider provider; - final String empresaId; - - const AddNegocioDialog({ - Key? key, - required this.provider, - required this.empresaId, - }) : super(key: key); - - @override - State createState() => _AddNegocioDialogState(); -} - -class _AddNegocioDialogState extends State - with TickerProviderStateMixin { - final _formKey = GlobalKey(); - final _nombreController = TextEditingController(); - final _direccionController = TextEditingController(); - final _latitudController = TextEditingController(); - final _longitudController = TextEditingController(); - final _tipoLocalController = TextEditingController(text: 'Sucursal'); - - bool _isLoading = false; - late AnimationController _scaleController; - late AnimationController _slideController; - late AnimationController _fadeController; - late Animation _scaleAnimation; - late Animation _slideAnimation; - late Animation _fadeAnimation; - bool _isAnimationInitialized = false; - - final List _tiposLocal = [ - 'Sucursal', - 'Oficina Central', - 'Bodega', - 'Centro de Distribución', - 'Punto de Venta', - 'Otro' - ]; - - @override - void initState() { - super.initState(); - _initializeAnimations(); - // Escuchar cambios del provider - widget.provider.addListener(_onProviderChanged); - } - - void _onProviderChanged() { - if (mounted) { - setState(() { - // Forzar rebuild cuando cambie el provider - }); - } - } - - void _initializeAnimations() { - _scaleController = AnimationController( - duration: const Duration(milliseconds: 600), - vsync: this, - ); - _slideController = AnimationController( - duration: const Duration(milliseconds: 800), - vsync: this, - ); - _fadeController = AnimationController( - duration: const Duration(milliseconds: 400), - vsync: this, - ); - - _scaleAnimation = CurvedAnimation( - parent: _scaleController, - curve: Curves.elasticOut, - ); - _slideAnimation = Tween( - begin: const Offset(0, -1), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _slideController, - curve: Curves.easeOutBack, - )); - _fadeAnimation = CurvedAnimation( - parent: _fadeController, - curve: Curves.easeInOut, - ); - - // Pequeño delay para asegurar que el widget esté completamente montado - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _isAnimationInitialized = true; - }); - _startAnimations(); - } - }); - } - - void _startAnimations() { - _fadeController.forward(); - Future.delayed(const Duration(milliseconds: 100), () { - if (mounted) _scaleController.forward(); - }); - Future.delayed(const Duration(milliseconds: 200), () { - if (mounted) _slideController.forward(); - }); - } - - @override - void dispose() { - widget.provider.removeListener(_onProviderChanged); - _scaleController.dispose(); - _slideController.dispose(); - _fadeController.dispose(); - _nombreController.dispose(); - _direccionController.dispose(); - _latitudController.dispose(); - _longitudController.dispose(); - _tipoLocalController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (!_isAnimationInitialized) { - return const SizedBox.shrink(); - } - - // Detectar el tamaño de pantalla - final screenSize = MediaQuery.of(context).size; - final isDesktop = screenSize.width > 1024; - final isTablet = screenSize.width > 768 && screenSize.width <= 1024; - - // Ajustar dimensiones según el tipo de pantalla - final maxWidth = isDesktop ? 900.0 : (isTablet ? 750.0 : 650.0); - final maxHeight = isDesktop ? 700.0 : 750.0; - - // Ajustar el padding del header según la pantalla - final headerPadding = isDesktop - ? const EdgeInsets.symmetric(vertical: 20, horizontal: 30) - : const EdgeInsets.all(25); - - // Ajustar el tamaño del icono - final iconSize = isDesktop ? 35.0 : 40.0; - final titleSize = isDesktop ? 24.0 : 28.0; - final subtitleSize = isDesktop ? 14.0 : 16.0; - - return AnimatedBuilder( - animation: - Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]), - builder: (context, child) { - return FadeTransition( - opacity: _fadeAnimation, - child: Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.all(isDesktop ? 40 : 20), - child: Transform.scale( - scale: _scaleAnimation.value, - child: Container( - constraints: BoxConstraints( - maxWidth: maxWidth, - maxHeight: maxHeight, - minHeight: isDesktop ? 600 : 400, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(25), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 30, - offset: const Offset(0, 15), - spreadRadius: 5, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(25), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryBackground, - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - stops: const [0.0, 0.6, 1.0], - ), - ), - child: isDesktop - ? _buildDesktopLayout( - headerPadding, iconSize, titleSize, subtitleSize) - : _buildMobileLayout( - headerPadding, iconSize, titleSize, subtitleSize), - ), - ), - ), - ), - ), - ); - }, - ); - } - - Widget _buildDesktopLayout(EdgeInsets headerPadding, double iconSize, - double titleSize, double subtitleSize) { - return Row( - children: [ - // Header lateral compacto para desktop - Container( - width: 280, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).tertiaryColor.withOpacity(0.4), - blurRadius: 20, - offset: const Offset(5, 0), - ), - ], - ), - child: SlideTransition( - position: _slideAnimation, - child: Padding( - padding: headerPadding, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Icono compacto - Container( - padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - shape: BoxShape.circle, - border: Border.all( - color: Colors.white.withOpacity(0.4), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: Colors.white.withOpacity(0.3), - blurRadius: 15, - spreadRadius: 2, - ), - ], - ), - child: Icon( - Icons.store_mall_directory, - color: Colors.white, - size: iconSize, - ), - ), - const SizedBox(height: 16), - - // Título compacto - Text( - 'Nueva Sucursal', - style: TextStyle( - color: Colors.white, - fontSize: titleSize, - fontWeight: FontWeight.bold, - letterSpacing: 1.2, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - - Text( - 'Añadir una nueva ubicación a tu empresa', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: subtitleSize, - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ), - - // Contenido principal del formulario - Expanded( - child: Padding( - padding: const EdgeInsets.all(25), - child: Form( - key: _formKey, - child: Column( - children: [ - // Información de empresa compacta - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor.withOpacity(0.1), - AppTheme.of(context).primaryColor.withOpacity(0.1), - ], - ), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: - AppTheme.of(context).tertiaryColor.withOpacity(0.3), - width: 1, - ), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.business, - color: Colors.white, - size: 18, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Empresa', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 2), - Text( - widget.provider.empresaSeleccionada?.nombre ?? - "Empresa no seleccionada", - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ], - ), - ), - - const SizedBox(height: 16), - - // Formulario en columnas para aprovechar el espacio - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - // Primera fila - Nombre y Tipo - Row( - children: [ - Expanded( - flex: 2, - child: _buildCompactFormField( - controller: _nombreController, - label: 'Nombre de la sucursal', - hint: 'Ej: Sucursal Centro', - icon: Icons.store, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es requerido'; - } - return null; - }, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildCompactDropdown(), - ), - ], - ), - - const SizedBox(height: 16), - - // Segunda fila - Dirección - _buildCompactFormField( - controller: _direccionController, - label: 'Dirección', - hint: 'Dirección completa de la sucursal', - icon: Icons.location_on_outlined, - maxLines: 2, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La dirección es requerida'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - // Tercera fila - Coordenadas - Row( - children: [ - Expanded( - child: _buildCompactFormField( - controller: _latitudController, - label: 'Latitud', - hint: 'Ej: 19.4326', - icon: Icons.location_searching, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Requerido'; - } - final lat = double.tryParse(value); - if (lat == null) { - return 'Número inválido'; - } - if (lat < -90 || lat > 90) { - return 'Rango: -90 a 90'; - } - return null; - }, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildCompactFormField( - controller: _longitudController, - label: 'Longitud', - hint: 'Ej: -99.1332', - icon: Icons.location_searching, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Requerido'; - } - final lng = double.tryParse(value); - if (lng == null) { - return 'Número inválido'; - } - if (lng < -180 || lng > 180) { - return 'Rango: -180 a 180'; - } - return null; - }, - ), - ), - ], - ), - - const SizedBox(height: 20), - - // Sección de archivos compacta - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context) - .primaryColor - .withOpacity(0.1), - AppTheme.of(context) - .tertiaryColor - .withOpacity(0.1), - AppTheme.of(context) - .secondaryColor - .withOpacity(0.05), - ], - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.4), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.2), - blurRadius: 15, - offset: const Offset(0, 8), - spreadRadius: 2, - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header de la sección - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.4), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: const Icon( - Icons.cloud_upload_rounded, - color: Colors.white, - size: 18, - ), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Archivos Opcionales', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: AppTheme.of(context) - .primaryText, - ), - ), - Text( - 'Logo e imagen de la sucursal', - style: TextStyle( - fontSize: 12, - color: AppTheme.of(context) - .secondaryText, - ), - ), - ], - ), - ], - ), - - const SizedBox(height: 16), - - // Botones de archivos mejorados - _buildEnhancedFileButton( - label: 'Logo de la sucursal', - subtitle: 'Formato PNG, JPG (Max 2MB)', - icon: Icons.image_rounded, - fileName: widget.provider.logoFileName, - file: widget.provider.logoToUpload, - onPressed: widget.provider.selectLogo, - gradient: LinearGradient( - colors: [ - Colors.blue.shade400, - Colors.blue.shade600 - ], - ), - ), - - const SizedBox(height: 12), - - _buildEnhancedFileButton( - label: 'Imagen principal', - subtitle: - 'Imagen representativa de la sucursal', - icon: Icons.photo_library_rounded, - fileName: widget.provider.imagenFileName, - file: widget.provider.imagenToUpload, - onPressed: widget.provider.selectImagen, - gradient: LinearGradient( - colors: [ - Colors.purple.shade400, - Colors.purple.shade600 - ], - ), - ), - ], - ), - ), - - const SizedBox(height: 20), - - // Botones de acción - Row( - children: [ - Expanded( - child: Container( - height: 45, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppTheme.of(context) - .secondaryText - .withOpacity(0.3), - ), - ), - child: TextButton( - onPressed: _isLoading - ? null - : () { - widget.provider.resetFormData(); - Navigator.of(context).pop(); - }, - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text( - 'Cancelar', - style: TextStyle( - color: - AppTheme.of(context).secondaryText, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ), - const SizedBox(width: 16), - Expanded( - flex: 2, - child: Container( - height: 45, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .tertiaryColor - .withOpacity(0.4), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: ElevatedButton( - onPressed: - _isLoading ? null : _crearNegocio, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: - AlwaysStoppedAnimation( - Colors.white), - ), - ) - : Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: const [ - Icon( - Icons.add_location, - color: Colors.white, - size: 18, - ), - SizedBox(width: 8), - Text( - 'Crear Sucursal', - style: TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ], - ), - ), - ), - ), - ], - ); - } - - Widget _buildMobileLayout(EdgeInsets headerPadding, double iconSize, - double titleSize, double subtitleSize) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header para móvil (estructura original pero más compacto) - SlideTransition( - position: _slideAnimation, - child: Container( - width: double.infinity, - padding: headerPadding, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).tertiaryColor.withOpacity(0.4), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - shape: BoxShape.circle, - border: Border.all( - color: Colors.white.withOpacity(0.4), - width: 2, - ), - ), - child: Icon( - Icons.store_mall_directory, - color: Colors.white, - size: iconSize, - ), - ), - const SizedBox(height: 12), - Text( - 'Nueva Sucursal', - style: TextStyle( - color: Colors.white, - fontSize: titleSize, - fontWeight: FontWeight.bold, - letterSpacing: 1.2, - ), - ), - const SizedBox(height: 6), - Text( - 'Añadir nueva ubicación', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: subtitleSize, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ), - - // Contenido del formulario para móvil (estructura original) - Flexible( - child: SingleChildScrollView( - padding: const EdgeInsets.all(25), - child: Form( - key: _formKey, - child: Column( - children: [ - // Información de la empresa - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor.withOpacity(0.1), - AppTheme.of(context).primaryColor.withOpacity(0.1), - ], - ), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: - AppTheme.of(context).tertiaryColor.withOpacity(0.3), - width: 1, - ), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.business, - color: Colors.white, - size: 18, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Empresa', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 2), - Text( - widget.provider.empresaSeleccionada?.nombre ?? - "Empresa no seleccionada", - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ], - ), - ), - - const SizedBox(height: 16), - - // Campos del formulario - _buildCompactFormField( - controller: _nombreController, - label: 'Nombre de la sucursal', - hint: 'Ej: Sucursal Centro, Sede Norte', - icon: Icons.store, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es requerido'; - } - return null; - }, - ), - - _buildCompactFormField( - controller: _direccionController, - label: 'Dirección', - hint: 'Dirección completa de la sucursal', - icon: Icons.location_on_outlined, - maxLines: 3, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La dirección es requerida'; - } - return null; - }, - ), - - // Tipo de local - Container( - margin: const EdgeInsets.only(bottom: 20), - child: DropdownButtonFormField( - value: _tipoLocalController.text, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - ), - dropdownColor: AppTheme.of(context) - .secondaryBackground, // Fondo del dropdown - decoration: InputDecoration( - labelText: 'Tipo de local', - prefixIcon: Container( - margin: const EdgeInsets.all(6), - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.category, - color: Colors.white, - size: 16, - ), - ), - labelStyle: TextStyle( - color: AppTheme.of(context).tertiaryColor, - fontWeight: FontWeight.w600, - fontSize: 14, - ), - filled: true, - fillColor: AppTheme.of(context) - .secondaryBackground - .withOpacity(0.5), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: AppTheme.of(context) - .tertiaryColor - .withOpacity(0.3), - width: 1, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: AppTheme.of(context).tertiaryColor, - width: 2, - ), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - ), - items: _tiposLocal.map((String tipo) { - return DropdownMenuItem( - value: tipo, - child: Container( - constraints: const BoxConstraints( - maxWidth: - 200), // Limitar ancho para evitar overflow - child: Text( - tipo, - style: TextStyle( - fontSize: 14, - color: AppTheme.of(context).primaryText, - ), - overflow: TextOverflow - .ellipsis, // Truncar texto si es muy largo - maxLines: 1, - ), - ), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - _tipoLocalController.text = newValue; - } - }, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Selecciona un tipo'; - } - return null; - }, - isExpanded: - true, // Expandir para usar todo el ancho disponible - icon: Icon( - Icons.arrow_drop_down, - color: AppTheme.of(context).tertiaryColor, - ), - ), - ), - - // Sección de coordenadas - Container( - padding: const EdgeInsets.all(20), - margin: const EdgeInsets.only(bottom: 20), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor.withOpacity(0.1), - AppTheme.of(context).primaryColor.withOpacity(0.1), - ], - ), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: - AppTheme.of(context).tertiaryColor.withOpacity(0.3), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .tertiaryColor - .withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.location_searching, - color: Colors.white, - size: 16, - ), - ), - const SizedBox(width: 12), - Text( - 'Coordenadas Geográficas', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: AppTheme.of(context).primaryText, - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - // Latitud - Expanded( - child: TextFormField( - controller: _latitudController, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp(r'^-?\d*\.?\d*')), - ], - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - ), - decoration: InputDecoration( - labelText: 'Latitud', - hintText: 'Ej: 19.4326', - labelStyle: TextStyle( - color: AppTheme.of(context).tertiaryColor, - fontWeight: FontWeight.w600, - ), - hintStyle: TextStyle( - color: AppTheme.of(context).secondaryText, - ), - filled: true, - fillColor: Colors.white.withOpacity(0.8), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: AppTheme.of(context) - .tertiaryColor - .withOpacity(0.3), - width: 1, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: AppTheme.of(context).tertiaryColor, - width: 2, - ), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 14), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Requerido'; - } - final lat = double.tryParse(value); - if (lat == null) { - return 'Número inválido'; - } - if (lat < -90 || lat > 90) { - return 'Rango: -90 a 90'; - } - return null; - }, - ), - ), - const SizedBox(width: 16), - - // Longitud - Expanded( - child: TextFormField( - controller: _longitudController, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp(r'^-?\d*\.?\d*')), - ], - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - ), - decoration: InputDecoration( - labelText: 'Longitud', - hintText: 'Ej: -99.1332', - labelStyle: TextStyle( - color: AppTheme.of(context).tertiaryColor, - fontWeight: FontWeight.w600, - ), - hintStyle: TextStyle( - color: AppTheme.of(context).secondaryText, - ), - filled: true, - fillColor: Colors.white.withOpacity(0.8), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: AppTheme.of(context) - .tertiaryColor - .withOpacity(0.3), - width: 1, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: AppTheme.of(context).tertiaryColor, - width: 2, - ), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 14), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Requerido'; - } - final lng = double.tryParse(value); - if (lng == null) { - return 'Número inválido'; - } - if (lng < -180 || lng > 180) { - return 'Rango: -180 a 180'; - } - return null; - }, - ), - ), - ], - ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Icon( - Icons.info_outline, - color: Colors.blue, - size: 16, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Puedes obtener las coordenadas desde Google Maps', - style: TextStyle( - color: Colors.blue, - fontSize: 12, - ), - ), - ), - ], - ), - ), - ], - ), - ), - - // Sección de archivos compacta para móvil - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor.withOpacity(0.1), - AppTheme.of(context).tertiaryColor.withOpacity(0.1), - AppTheme.of(context).secondaryColor.withOpacity(0.05), - ], - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: - AppTheme.of(context).primaryColor.withOpacity(0.4), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.2), - blurRadius: 15, - offset: const Offset(0, 8), - spreadRadius: 2, - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header de la sección - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.4), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: const Icon( - Icons.cloud_upload_rounded, - color: Colors.white, - size: 18, - ), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Archivos Opcionales', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: AppTheme.of(context).primaryText, - ), - ), - Text( - 'Logo e imagen de la sucursal', - style: TextStyle( - fontSize: 12, - color: AppTheme.of(context).secondaryText, - ), - ), - ], - ), - ], - ), - - const SizedBox(height: 16), - - // Botones de archivos mejorados - _buildEnhancedFileButton( - label: 'Logo de la sucursal', - subtitle: 'Formato PNG, JPG (Max 2MB)', - icon: Icons.image_rounded, - fileName: widget.provider.logoFileName, - file: widget.provider.logoToUpload, - onPressed: widget.provider.selectLogo, - gradient: LinearGradient( - colors: [ - Colors.blue.shade400, - Colors.blue.shade600 - ], - ), - ), - - const SizedBox(height: 12), - - _buildEnhancedFileButton( - label: 'Imagen principal', - subtitle: 'Imagen representativa de la sucursal', - icon: Icons.photo_library_rounded, - fileName: widget.provider.imagenFileName, - file: widget.provider.imagenToUpload, - onPressed: widget.provider.selectImagen, - gradient: LinearGradient( - colors: [ - Colors.purple.shade400, - Colors.purple.shade600 - ], - ), - ), - ], - ), - ), - - const SizedBox(height: 30), - - // Botones de acción - Row( - children: [ - Expanded( - child: Container( - height: 45, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppTheme.of(context) - .secondaryText - .withOpacity(0.3), - ), - ), - child: TextButton( - onPressed: _isLoading - ? null - : () { - widget.provider.resetFormData(); - Navigator.of(context).pop(); - }, - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text( - 'Cancelar', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ), - const SizedBox(width: 16), - Expanded( - flex: 2, - child: Container( - height: 45, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .tertiaryColor - .withOpacity(0.4), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: ElevatedButton( - onPressed: _isLoading ? null : _crearNegocio, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Colors.white), - ), - ) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon( - Icons.add_location, - color: Colors.white, - size: 18, - ), - SizedBox(width: 8), - Text( - 'Crear Sucursal', - style: TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - } - - Widget _buildCompactFormField({ - required TextEditingController controller, - required String label, - required String hint, - required IconData icon, - int maxLines = 1, - TextInputType? keyboardType, - String? Function(String?)? validator, - }) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - child: TextFormField( - controller: controller, - maxLines: maxLines, - keyboardType: keyboardType, - validator: validator, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - ), - decoration: InputDecoration( - labelText: label, - hintText: hint, - prefixIcon: Container( - margin: const EdgeInsets.all(6), - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - icon, - color: Colors.white, - size: 16, - ), - ), - labelStyle: TextStyle( - color: AppTheme.of(context).tertiaryColor, - fontWeight: FontWeight.w600, - fontSize: 14, - ), - hintStyle: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - filled: true, - fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.5), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: AppTheme.of(context).tertiaryColor.withOpacity(0.3), - width: 1, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: AppTheme.of(context).tertiaryColor, - width: 2, - ), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide( - color: Colors.red, - width: 1, - ), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), - ), - ); - } - - Widget _buildCompactDropdown() { - return Container( - margin: const EdgeInsets.only(bottom: 12), - child: DropdownButtonFormField( - value: _tipoLocalController.text, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - ), - dropdownColor: - AppTheme.of(context).secondaryBackground, // Fondo del dropdown - decoration: InputDecoration( - labelText: 'Tipo de local', - prefixIcon: Container( - margin: const EdgeInsets.all(6), - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.category, - color: Colors.white, - size: 16, - ), - ), - labelStyle: TextStyle( - color: AppTheme.of(context).tertiaryColor, - fontWeight: FontWeight.w600, - fontSize: 14, - ), - filled: true, - fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.5), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: AppTheme.of(context).tertiaryColor.withOpacity(0.3), - width: 1, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: AppTheme.of(context).tertiaryColor, - width: 2, - ), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), - items: _tiposLocal.map((String tipo) { - return DropdownMenuItem( - value: tipo, - child: Container( - constraints: const BoxConstraints( - maxWidth: 200), // Limitar ancho para evitar overflow - child: Text( - tipo, - style: TextStyle( - fontSize: 14, - color: AppTheme.of(context).primaryText, - ), - overflow: - TextOverflow.ellipsis, // Truncar texto si es muy largo - maxLines: 1, - ), - ), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - _tipoLocalController.text = newValue; - } - }, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Selecciona un tipo'; - } - return null; - }, - isExpanded: true, // Expandir para usar todo el ancho disponible - icon: Icon( - Icons.arrow_drop_down, - color: AppTheme.of(context).tertiaryColor, - ), - ), - ); - } - - Widget _buildCompactFileButton({ - required String label, - required IconData icon, - required String? fileName, - required dynamic file, - required VoidCallback onPressed, - }) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: AppTheme.of(context).tertiaryColor.withOpacity(0.3), - ), - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onPressed, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppTheme.of(context).tertiaryColor.withOpacity(0.2), - borderRadius: BorderRadius.circular(6), - ), - child: Icon( - icon, - color: AppTheme.of(context).tertiaryColor, - size: 16, - ), - ), - const SizedBox(height: 8), - Text( - label, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.w600, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - if (fileName != null) ...[ - const SizedBox(height: 4), - Text( - fileName - .split('-') - .last, // Solo mostrar el nombre del archivo - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 10, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ), - ], - ], - ), - ), - ), - ), - ); - } - - Widget _buildEnhancedCompactFileButton({ - required String label, - required String subtitle, - required IconData icon, - required String? fileName, - required dynamic file, - required VoidCallback onPressed, - required LinearGradient gradient, - }) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - gradient: gradient, - boxShadow: [ - BoxShadow( - color: gradient.colors.last.withOpacity(0.3), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onPressed, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: Icon( - icon, - color: gradient.colors.first, - size: 20, - ), - ), - const SizedBox(height: 12), - Text( - label, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.w500, - fontSize: 14, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 4), - Text( - subtitle, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - if (fileName != null) ...[ - const SizedBox(height: 8), - Text( - fileName - .split('-') - .last, // Solo mostrar el nombre del archivo - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 10, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ), - ], - ], - ), - ), - ), - ), - ); - } - - Widget _buildEnhancedFileButton({ - required String label, - required String subtitle, - required IconData icon, - required String? fileName, - required dynamic file, - required VoidCallback onPressed, - required Gradient gradient, - }) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - blurRadius: 15, - offset: const Offset(0, 6), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onPressed, - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - // Icono con gradiente - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - gradient: gradient, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Icon( - icon, - color: Colors.white, - size: 24, - ), - ), - - const SizedBox(width: 16), - - // Información del archivo - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - const SizedBox(height: 4), - Text( - fileName ?? subtitle, - style: TextStyle( - color: fileName != null - ? AppTheme.of(context).primaryColor - : AppTheme.of(context).secondaryText, - fontSize: 13, - fontWeight: fileName != null - ? FontWeight.w600 - : FontWeight.normal, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - - // Preview de imagen si existe - if (file != null) ...[ - const SizedBox(width: 16), - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: - AppTheme.of(context).primaryColor.withOpacity(0.4), - width: 2, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: widget.provider.getImageWidget( - file, - height: 50, - width: 50, - ), - ), - ), - ] else ...[ - const SizedBox(width: 16), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.upload_file, - color: AppTheme.of(context).primaryColor, - size: 20, - ), - ), - ], - ], - ), - ), - ), - ), - ); - } - - Future _crearNegocio() async { - if (!_formKey.currentState!.validate()) return; - - setState(() { - _isLoading = true; - }); - - try { - final success = await widget.provider.crearNegocio( - empresaId: widget.empresaId, - nombre: _nombreController.text.trim(), - direccion: _direccionController.text.trim(), - latitud: double.parse(_latitudController.text.trim()), - longitud: double.parse(_longitudController.text.trim()), - tipoLocal: _tipoLocalController.text.trim(), - ); - - if (mounted) { - if (success) { - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.check_circle, color: Colors.white), - SizedBox(width: 12), - Text( - 'Sucursal creada exitosamente', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.green, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.error, color: Colors.white), - SizedBox(width: 12), - Text( - 'Error al crear la sucursal', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(Icons.warning, color: Colors.white), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Error: $e', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/add_negocio_dialog.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/add_negocio_dialog.dart deleted file mode 100644 index 8a6ec0e..0000000 --- a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/add_negocio_dialog.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; -import 'negocio_dialog_animations.dart'; -import 'negocio_dialog_header.dart'; -import 'negocio_dialog_form.dart'; - -class AddNegocioDialog extends StatefulWidget { - final EmpresasNegociosProvider provider; - final String empresaId; - - const AddNegocioDialog({ - Key? key, - required this.provider, - required this.empresaId, - }) : super(key: key); - - @override - State createState() => _AddNegocioDialogState(); -} - -class _AddNegocioDialogState extends State - with TickerProviderStateMixin { - late NegocioDialogAnimations _animations; - - @override - void initState() { - super.initState(); - _animations = NegocioDialogAnimations(vsync: this); - _animations.initialize(); - - // Escuchar cambios del provider - widget.provider.addListener(_onProviderChanged); - } - - void _onProviderChanged() { - if (mounted) { - setState(() { - // Forzar rebuild cuando cambie el provider - }); - } - } - - @override - void dispose() { - widget.provider.removeListener(_onProviderChanged); - _animations.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (!_animations.isInitialized) { - return const SizedBox.shrink(); - } - - // Detectar el tamaño de pantalla - final screenSize = MediaQuery.of(context).size; - final isDesktop = screenSize.width > 1024; - final isTablet = screenSize.width > 768 && screenSize.width <= 1024; - - // Ajustar dimensiones según el tipo de pantalla - final maxWidth = isDesktop ? 950.0 : (isTablet ? 800.0 : 700.0); - final maxHeight = isDesktop ? 750.0 : 800.0; - - return AnimatedBuilder( - animation: _animations.combinedAnimation, - builder: (context, child) { - return FadeTransition( - opacity: _animations.fadeAnimation, - child: Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.all(isDesktop ? 40 : 20), - child: Transform.scale( - scale: _animations.scaleAnimation.value, - child: Container( - constraints: BoxConstraints( - maxWidth: maxWidth, - maxHeight: maxHeight, - minHeight: isDesktop ? 650 : 450, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.4), - blurRadius: 40, - offset: const Offset(0, 20), - spreadRadius: 8, - ), - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 60, - offset: const Offset(0, 10), - spreadRadius: 2, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(30), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryBackground, - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - stops: const [0.0, 0.6, 1.0], - ), - ), - child: isDesktop - ? _buildDesktopLayout() - : _buildMobileLayout(), - ), - ), - ), - ), - ), - ); - }, - ); - } - - Widget _buildDesktopLayout() { - return Row( - children: [ - // Header lateral compacto para desktop - NegocioDialogHeader( - isDesktop: true, - slideAnimation: _animations.slideAnimation, - ), - - // Contenido principal del formulario - Expanded( - child: NegocioDialogForm( - provider: widget.provider, - isDesktop: true, - empresaId: widget.empresaId, // Pasar empresaId al formulario - ), - ), - ], - ); - } - - Widget _buildMobileLayout() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header para móvil - NegocioDialogHeader( - isDesktop: false, - slideAnimation: _animations.slideAnimation, - ), - - // Contenido del formulario para móvil - Flexible( - child: NegocioDialogForm( - provider: widget.provider, - isDesktop: false, - empresaId: widget.empresaId, // Pasar empresaId al formulario - ), - ), - ], - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_action_buttons.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_action_buttons.dart deleted file mode 100644 index 5257dc1..0000000 --- a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_action_buttons.dart +++ /dev/null @@ -1,134 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class NegocioActionButtons extends StatelessWidget { - final bool isLoading; - final bool isDesktop; - final VoidCallback onCancel; - final VoidCallback onSubmit; - - const NegocioActionButtons({ - Key? key, - required this.isLoading, - required this.isDesktop, - required this.onCancel, - required this.onSubmit, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - // Botón Cancelar - Expanded( - child: Container( - height: isDesktop ? 45 : 50, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: AppTheme.of(context).secondaryText.withOpacity(0.3), - width: 2, - ), - ), - child: TextButton( - onPressed: isLoading ? null : onCancel, - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.close_rounded, - color: AppTheme.of(context).secondaryText, - size: 18, - ), - const SizedBox(width: 8), - Text( - 'Cancelar', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ), - - const SizedBox(width: 16), - - // Botón Crear Negocio - Expanded( - flex: 2, - child: Container( - height: isDesktop ? 45 : 50, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - ], - ), - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.4), - blurRadius: 15, - offset: const Offset(0, 5), - spreadRadius: 2, - ), - ], - ), - child: ElevatedButton( - onPressed: isLoading ? null : onSubmit, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - child: isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.add_business_rounded, - color: Colors.white, - size: 20, - ), - const SizedBox(width: 10), - Text( - 'Crear Negocio', - style: TextStyle( - color: Colors.white, - fontSize: isDesktop ? 14 : 16, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ], - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_animations.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_animations.dart deleted file mode 100644 index 9877f39..0000000 --- a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_animations.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter/material.dart'; - -class NegocioDialogAnimations { - final TickerProvider vsync; - - late AnimationController _scaleController; - late AnimationController _slideController; - late AnimationController _fadeController; - late Animation _scaleAnimation; - late Animation _slideAnimation; - late Animation _fadeAnimation; - late Listenable _combinedAnimation; - bool _isInitialized = false; - - NegocioDialogAnimations({required this.vsync}); - - // Getters para acceder a las animaciones - Animation get scaleAnimation => _scaleAnimation; - Animation get slideAnimation => _slideAnimation; - Animation get fadeAnimation => _fadeAnimation; - Listenable get combinedAnimation => _combinedAnimation; - bool get isInitialized => _isInitialized; - - void initialize() { - _scaleController = AnimationController( - duration: const Duration(milliseconds: 600), - vsync: vsync, - ); - _slideController = AnimationController( - duration: const Duration(milliseconds: 800), - vsync: vsync, - ); - _fadeController = AnimationController( - duration: const Duration(milliseconds: 400), - vsync: vsync, - ); - - _scaleAnimation = CurvedAnimation( - parent: _scaleController, - curve: Curves.elasticOut, - ); - _slideAnimation = Tween( - begin: const Offset(0, -1), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _slideController, - curve: Curves.easeOutBack, - )); - _fadeAnimation = CurvedAnimation( - parent: _fadeController, - curve: Curves.easeInOut, - ); - - _combinedAnimation = - Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]); - - // Pequeño delay para asegurar que el widget esté completamente montado - WidgetsBinding.instance.addPostFrameCallback((_) { - _isInitialized = true; - _startAnimations(); - }); - } - - void _startAnimations() { - _fadeController.forward(); - Future.delayed(const Duration(milliseconds: 100), () { - _scaleController.forward(); - }); - Future.delayed(const Duration(milliseconds: 200), () { - _slideController.forward(); - }); - } - - void dispose() { - _scaleController.dispose(); - _slideController.dispose(); - _fadeController.dispose(); - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_form.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_form.dart deleted file mode 100644 index 4e54849..0000000 --- a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_form.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'negocio_form_fields.dart'; -import 'negocio_empresa_selector.dart'; -import 'negocio_action_buttons.dart'; - -class NegocioDialogForm extends StatefulWidget { - final EmpresasNegociosProvider provider; - final bool isDesktop; - final String empresaId; - - const NegocioDialogForm({ - Key? key, - required this.provider, - required this.isDesktop, - required this.empresaId, - }) : super(key: key); - - @override - State createState() => _NegocioDialogFormState(); -} - -class _NegocioDialogFormState extends State { - final _formKey = GlobalKey(); - final _nombreController = TextEditingController(); - final _direccionController = TextEditingController(); - final _latitudController = TextEditingController(); - final _longitudController = TextEditingController(); - final _tipoLocalController = TextEditingController(); - bool _isLoading = false; - - @override - void dispose() { - _nombreController.dispose(); - _direccionController.dispose(); - _latitudController.dispose(); - _longitudController.dispose(); - _tipoLocalController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(25), - child: Form( - key: _formKey, - child: widget.isDesktop ? _buildDesktopForm() : _buildMobileForm(), - ), - ); - } - - Widget _buildDesktopForm() { - return Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - // Selector de empresa - NegocioEmpresaSelector( - provider: widget.provider, - isDesktop: true, - ), - - const SizedBox(height: 20), - - // Campos del formulario en filas para desktop - NegocioFormFields( - isDesktop: true, - nombreController: _nombreController, - direccionController: _direccionController, - latitudController: _latitudController, - longitudController: _longitudController, - tipoLocalController: _tipoLocalController, - ), - - const SizedBox(height: 25), - - // Botones de acción - NegocioActionButtons( - isLoading: _isLoading, - isDesktop: true, - onCancel: () => _handleCancel(), - onSubmit: () => _crearNegocio(), - ), - ], - ), - ), - ), - ], - ); - } - - Widget _buildMobileForm() { - return SingleChildScrollView( - child: Column( - children: [ - // Selector de empresa - NegocioEmpresaSelector( - provider: widget.provider, - isDesktop: false, - ), - - const SizedBox(height: 20), - - // Campos del formulario en columnas para móvil - NegocioFormFields( - isDesktop: false, - nombreController: _nombreController, - direccionController: _direccionController, - latitudController: _latitudController, - longitudController: _longitudController, - tipoLocalController: _tipoLocalController, - ), - - const SizedBox(height: 25), - - // Botones de acción - NegocioActionButtons( - isLoading: _isLoading, - isDesktop: false, - onCancel: () => _handleCancel(), - onSubmit: () => _crearNegocio(), - ), - ], - ), - ); - } - - void _handleCancel() { - widget.provider.resetFormData(); - Navigator.of(context).pop(); - } - - Future _crearNegocio() async { - if (!_formKey.currentState!.validate()) return; - - setState(() { - _isLoading = true; - }); - - try { - final latitud = double.parse(_latitudController.text.trim()); - final longitud = double.parse(_longitudController.text.trim()); - - final success = await widget.provider.crearNegocio( - empresaId: widget.empresaId, // Usar el empresaId pasado como parámetro - nombre: _nombreController.text.trim(), - direccion: _direccionController.text.trim(), - latitud: latitud, - longitud: longitud, - tipoLocal: _tipoLocalController.text.trim(), - ); - - if (mounted) { - if (success) { - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.check_circle, color: Colors.white), - SizedBox(width: 12), - Text( - 'Negocio creado exitosamente', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.green, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.error, color: Colors.white), - SizedBox(width: 12), - Text( - 'Error al crear el negocio', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(Icons.warning, color: Colors.white), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Error: $e', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_header.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_header.dart deleted file mode 100644 index b5e5219..0000000 --- a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_header.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class NegocioDialogHeader extends StatelessWidget { - final bool isDesktop; - final Animation slideAnimation; - - const NegocioDialogHeader({ - Key? key, - required this.isDesktop, - required this.slideAnimation, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - if (isDesktop) { - return _buildDesktopHeader(context); - } else { - return _buildMobileHeader(context); - } - } - - Widget _buildDesktopHeader(BuildContext context) { - return Container( - width: 300, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - ], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.5), - blurRadius: 25, - offset: const Offset(5, 0), - ), - ], - ), - child: SlideTransition( - position: slideAnimation, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 25), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildIcon(context), - const SizedBox(height: 20), - _buildTitle(context, fontSize: 24), - const SizedBox(height: 8), - _buildSubtitle(context, fontSize: 14), - ], - ), - ), - ), - ); - } - - Widget _buildMobileHeader(BuildContext context) { - return SlideTransition( - position: slideAnimation, - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 25), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - ], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.5), - blurRadius: 25, - offset: const Offset(0, 15), - spreadRadius: 2, - ), - ], - ), - child: Column( - children: [ - _buildIcon(context), - const SizedBox(height: 16), - _buildTitle(context, fontSize: 26), - const SizedBox(height: 8), - _buildSubtitle(context, fontSize: 14, isMobile: true), - ], - ), - ), - ); - } - - Widget _buildIcon(BuildContext context) { - return Container( - padding: EdgeInsets.all(isDesktop ? 20 : 18), - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - Colors.white.withOpacity(0.4), - Colors.white.withOpacity(0.1), - Colors.transparent, - ], - ), - border: Border.all( - color: Colors.white.withOpacity(0.6), - width: 3, - ), - boxShadow: [ - BoxShadow( - color: Colors.white.withOpacity(0.4), - blurRadius: 20, - spreadRadius: 5, - ), - ], - ), - child: Icon( - Icons.store_rounded, - color: Colors.white, - size: isDesktop ? 35 : 35, - ), - ); - } - - Widget _buildTitle(BuildContext context, {required double fontSize}) { - return Text( - 'Nuevo Negocio', - style: TextStyle( - color: Colors.white, - fontSize: fontSize, - fontWeight: FontWeight.bold, - letterSpacing: 1.5, - ), - textAlign: TextAlign.center, - ); - } - - Widget _buildSubtitle(BuildContext context, - {required double fontSize, bool isMobile = false}) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: Colors.white.withOpacity(0.3), - ), - ), - child: Text( - isMobile - ? '🏪 Registra un nuevo negocio o sucursal' - : '🏪 Registra un nuevo negocio', - style: TextStyle( - color: Colors.white.withOpacity(0.95), - fontSize: fontSize, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - ), - textAlign: TextAlign.center, - ), - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_empresa_selector.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_empresa_selector.dart deleted file mode 100644 index 4ee1eb0..0000000 --- a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_empresa_selector.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class NegocioEmpresaSelector extends StatelessWidget { - final EmpresasNegociosProvider provider; - final bool isDesktop; - - const NegocioEmpresaSelector({ - Key? key, - required this.provider, - required this.isDesktop, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).formBackground, - AppTheme.of(context).secondaryBackground.withOpacity(0.8), - ], - ), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.business_rounded, - color: Colors.white, - size: 20, - ), - ), - const SizedBox(width: 12), - Text( - 'Seleccionar Empresa', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 15), - _buildEmpresaDropdown(context), - ], - ), - ); - } - - Widget _buildEmpresaDropdown(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground.withOpacity(0.7), - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - ), - child: DropdownButtonFormField( - value: provider.empresaSeleccionadaId, - decoration: InputDecoration( - hintText: 'Selecciona una empresa', - hintStyle: TextStyle( - color: AppTheme.of(context).secondaryText.withOpacity(0.7), - fontSize: 14, - ), - prefixIcon: Container( - margin: const EdgeInsets.all(8), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - borderRadius: BorderRadius.circular(10), - ), - child: const Icon( - Icons.business_center_rounded, - color: Colors.white, - size: 18, - ), - ), - border: InputBorder.none, - contentPadding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - ), - dropdownColor: AppTheme.of(context).secondaryBackground, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - icon: Icon( - Icons.keyboard_arrow_down_rounded, - color: AppTheme.of(context).primaryColor, - size: 28, - ), - items: provider.empresas.map((empresa) { - return DropdownMenuItem( - value: empresa.id, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.business_rounded, - color: AppTheme.of(context).primaryColor, - size: 16, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - empresa.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.w600, - fontSize: 14, - ), - ), - if (empresa.rfc != null) - Text( - empresa.rfc!, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - ), - ); - }).toList(), - onChanged: (String? newValue) { - provider.setEmpresaSeleccionada(newValue!); - }, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Selecciona una empresa'; - } - return null; - }, - ), - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_form_fields.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_form_fields.dart deleted file mode 100644 index 54dba4d..0000000 --- a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_form_fields.dart +++ /dev/null @@ -1,350 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class NegocioFormFields extends StatelessWidget { - final bool isDesktop; - final TextEditingController nombreController; - final TextEditingController direccionController; - final TextEditingController latitudController; - final TextEditingController longitudController; - final TextEditingController tipoLocalController; - - const NegocioFormFields({ - Key? key, - required this.isDesktop, - required this.nombreController, - required this.direccionController, - required this.latitudController, - required this.longitudController, - required this.tipoLocalController, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - if (isDesktop) { - return _buildDesktopFields(context); - } else { - return _buildMobileFields(context); - } - } - - Widget _buildDesktopFields(BuildContext context) { - return Column( - children: [ - // Primera fila - Nombre del negocio - _buildFormField( - context: context, - controller: nombreController, - label: 'Nombre del negocio', - hint: 'Ej: Sucursal Centro, Tienda Principal', - icon: Icons.store_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es requerido'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - // Segunda fila - Dirección - _buildFormField( - context: context, - controller: direccionController, - label: 'Dirección', - hint: 'Dirección completa del negocio', - icon: Icons.location_on_rounded, - maxLines: 2, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La dirección es requerida'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - // Tercera fila - Coordenadas - Row( - children: [ - Expanded( - child: _buildFormField( - context: context, - controller: latitudController, - label: 'Latitud', - hint: 'Ej: 19.4326', - icon: Icons.location_searching, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')), - ], - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La latitud es requerida'; - } - final lat = double.tryParse(value); - if (lat == null) { - return 'Número inválido'; - } - if (lat < -90 || lat > 90) { - return 'Debe estar entre -90 y 90'; - } - return null; - }, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildFormField( - context: context, - controller: longitudController, - label: 'Longitud', - hint: 'Ej: -99.1332', - icon: Icons.location_searching, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')), - ], - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La longitud es requerida'; - } - final lng = double.tryParse(value); - if (lng == null) { - return 'Número inválido'; - } - if (lng < -180 || lng > 180) { - return 'Debe estar entre -180 y 180'; - } - return null; - }, - ), - ), - ], - ), - - const SizedBox(height: 16), - - // Cuarta fila - Tipo de local - _buildFormField( - context: context, - controller: tipoLocalController, - label: 'Tipo de local', - hint: 'Ej: Sucursal, Matriz, Almacén', - icon: Icons.business, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El tipo de local es requerido'; - } - return null; - }, - ), - ], - ); - } - - Widget _buildMobileFields(BuildContext context) { - return Column( - children: [ - _buildFormField( - context: context, - controller: nombreController, - label: 'Nombre del negocio', - hint: 'Ej: Sucursal Centro, Tienda Principal', - icon: Icons.store_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es requerido'; - } - return null; - }, - ), - _buildFormField( - context: context, - controller: direccionController, - label: 'Dirección', - hint: 'Dirección completa del negocio', - icon: Icons.location_on_rounded, - maxLines: 3, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La dirección es requerida'; - } - return null; - }, - ), - _buildFormField( - context: context, - controller: latitudController, - label: 'Latitud', - hint: 'Ej: 19.4326', - icon: Icons.location_searching, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')), - ], - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La latitud es requerida'; - } - final lat = double.tryParse(value); - if (lat == null) { - return 'Número inválido'; - } - if (lat < -90 || lat > 90) { - return 'Debe estar entre -90 y 90'; - } - return null; - }, - ), - _buildFormField( - context: context, - controller: longitudController, - label: 'Longitud', - hint: 'Ej: -99.1332', - icon: Icons.location_searching, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')), - ], - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La longitud es requerida'; - } - final lng = double.tryParse(value); - if (lng == null) { - return 'Número inválido'; - } - if (lng < -180 || lng > 180) { - return 'Debe estar entre -180 y 180'; - } - return null; - }, - ), - _buildFormField( - context: context, - controller: tipoLocalController, - label: 'Tipo de local', - hint: 'Ej: Sucursal, Matriz, Almacén', - icon: Icons.business, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El tipo de local es requerido'; - } - return null; - }, - ), - ], - ); - } - - Widget _buildFormField({ - required BuildContext context, - required TextEditingController controller, - required String label, - required String hint, - required IconData icon, - int maxLines = 1, - TextInputType? keyboardType, - List? inputFormatters, - String? Function(String?)? validator, - }) { - return Container( - margin: const EdgeInsets.only(bottom: 16), - child: TextFormField( - controller: controller, - maxLines: maxLines, - keyboardType: keyboardType, - inputFormatters: inputFormatters, - validator: validator, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - decoration: InputDecoration( - labelText: label, - hintText: hint, - prefixIcon: Container( - margin: const EdgeInsets.all(8), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).tertiaryColor, - AppTheme.of(context).primaryColor, - ], - ), - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - child: Icon( - icon, - color: Colors.white, - size: 18, - ), - ), - labelStyle: TextStyle( - color: AppTheme.of(context).primaryColor, - fontWeight: FontWeight.w600, - fontSize: 14, - ), - hintStyle: TextStyle( - color: AppTheme.of(context).secondaryText.withOpacity(0.7), - fontSize: 12, - ), - filled: true, - fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.7), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor, - width: 2, - ), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: const BorderSide( - color: Colors.red, - width: 2, - ), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - ), - ), - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/empresa_selector_sidebar.dart b/lib/pages/empresa_negocios/widgets/empresa_selector_sidebar.dart deleted file mode 100644 index 8e75192..0000000 --- a/lib/pages/empresa_negocios/widgets/empresa_selector_sidebar.dart +++ /dev/null @@ -1,686 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/pages/empresa_negocios/widgets/add_empresa_dialog.dart'; -import 'package:nethive_neo/pages/empresa_negocios/widgets/add_negocio_dialog.dart'; -import 'package:nethive_neo/theme/theme.dart'; -import 'package:nethive_neo/helpers/globals.dart'; - -class EmpresaSelectorSidebar extends StatefulWidget { - final EmpresasNegociosProvider provider; - final Function(String) onEmpresaSelected; - - const EmpresaSelectorSidebar({ - Key? key, - required this.provider, - required this.onEmpresaSelected, - }) : super(key: key); - - @override - State createState() => _EmpresaSelectorSidebarState(); -} - -class _EmpresaSelectorSidebarState extends State - with TickerProviderStateMixin { - late AnimationController _pulseController; - late Animation _pulseAnimation; - - @override - void initState() { - super.initState(); - _pulseController = AnimationController( - duration: const Duration(seconds: 2), - vsync: this, - )..repeat(reverse: true); - _pulseAnimation = Tween( - begin: 0.8, - end: 1.0, - ).animate(CurvedAnimation( - parent: _pulseController, - curve: Curves.easeInOut, - )); - } - - @override - void dispose() { - _pulseController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).primaryBackground, - ], - ), - border: Border( - right: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - width: 1, - ), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(2, 0), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header mejorado con gradiente - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - // Logo animado - AnimatedBuilder( - animation: _pulseAnimation, - builder: (context, child) { - return Transform.scale( - scale: _pulseAnimation.value, - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.white.withOpacity(0.4), - width: 2, - ), - ), - child: Icon( - Icons.business_center, - color: Colors.white, - size: 24, - ), - ), - ); - }, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ShaderMask( - shaderCallback: (bounds) => LinearGradient( - colors: [ - Colors.white, - Colors.white.withOpacity(0.8) - ], - ).createShader(bounds), - child: Text( - 'NETHIVE', - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - letterSpacing: 1.5, - ), - ), - ), - Text( - 'Empresas', - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - - // Contador de empresas con efecto - Container( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Colors.white.withOpacity(0.3), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.domain, - color: Colors.white, - size: 16, - ), - const SizedBox(width: 8), - TweenAnimationBuilder( - duration: const Duration(milliseconds: 800), - tween: IntTween( - begin: 0, end: widget.provider.empresas.length), - builder: (context, value, child) { - return Text( - '$value empresas', - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ); - }, - ), - ], - ), - ), - ], - ), - ), - - // Lista de empresas con animaciones escalonadas - Expanded( - child: widget.provider.empresas.isEmpty - ? _buildEmptyState() - : ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: widget.provider.empresas.length, - itemBuilder: (context, index) { - return TweenAnimationBuilder( - duration: Duration(milliseconds: 200 + (index * 100)), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, value, child) { - return Transform.translate( - offset: Offset(-50 * (1 - value), 0), - child: Opacity( - opacity: value, - child: _buildEmpresaCard( - widget.provider.empresas[index], index), - ), - ); - }, - ); - }, - ), - ), - - // Botón añadir empresa mejorado - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - AppTheme.of(context).primaryBackground.withOpacity(0.0), - AppTheme.of(context).primaryBackground, - ], - ), - ), - child: Column( - children: [ - // Línea divisoria con gradiente - Container( - height: 1, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.transparent, - AppTheme.of(context).primaryColor.withOpacity(0.5), - Colors.transparent, - ], - ), - ), - ), - const SizedBox(height: 20), - - // Botón principal - SizedBox( - width: double.infinity, - child: Container( - decoration: BoxDecoration( - gradient: AppTheme.of(context).modernGradient, - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: ElevatedButton.icon( - onPressed: () { - showDialog( - context: context, - builder: (context) => - AddEmpresaDialog(provider: widget.provider), - ); - }, - icon: const Icon(Icons.add, color: Colors.white), - label: const Text( - 'Nueva Empresa', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - ), - ), - ), - ], - ), - ), - - // Información de empresa seleccionada mejorada - if (widget.provider.empresaSeleccionada != null) ...[ - Container( - margin: const EdgeInsets.fromLTRB(16, 0, 16, 16), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor.withOpacity(0.1), - AppTheme.of(context).tertiaryColor.withOpacity(0.1), - ], - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - Icons.check_circle, - color: Colors.white, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Empresa activa', - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - - Text( - widget.provider.empresaSeleccionada!.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - - // Estadísticas - Row( - children: [ - Icon( - Icons.store, - size: 16, - color: AppTheme.of(context).primaryColor, - ), - const SizedBox(width: 6), - TweenAnimationBuilder( - duration: const Duration(milliseconds: 600), - tween: IntTween( - begin: 0, end: widget.provider.negocios.length), - builder: (context, value, child) { - return Text( - '$value sucursales', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ); - }, - ), - ], - ), - const SizedBox(height: 16), - - // Botón para añadir sucursal - SizedBox( - width: double.infinity, - child: Container( - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - child: ElevatedButton.icon( - onPressed: () { - showDialog( - context: context, - builder: (context) => AddNegocioDialog( - provider: widget.provider, - empresaId: - widget.provider.empresaSeleccionada!.id, - ), - ); - }, - icon: const Icon(Icons.add_location, - size: 18, color: Colors.white), - label: const Text( - 'Añadir Sucursal', - style: TextStyle( - fontSize: 14, - color: Colors.white, - fontWeight: FontWeight.w600, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ), - ), - ), - ], - ), - ), - ], - ], - ), - ); - } - - Widget _buildEmpresaCard(dynamic empresa, int index) { - final isSelected = widget.provider.empresaSeleccionadaId == empresa.id; - - return Container( - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - gradient: isSelected - ? AppTheme.of(context).primaryGradient - : LinearGradient( - colors: [ - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected - ? Colors.white.withOpacity(0.3) - : AppTheme.of(context).primaryColor.withOpacity(0.2), - width: isSelected ? 2 : 1, - ), - boxShadow: [ - BoxShadow( - color: isSelected - ? AppTheme.of(context).primaryColor.withOpacity(0.3) - : Colors.black.withOpacity(0.1), - blurRadius: isSelected ? 15 : 8, - offset: Offset(0, isSelected ? 8 : 3), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => widget.onEmpresaSelected(empresa.id), - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - // Logo de la empresa con efectos - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - gradient: isSelected - ? LinearGradient( - colors: [ - Colors.white.withOpacity(0.3), - Colors.white.withOpacity(0.1) - ], - ) - : AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: (isSelected - ? Colors.white - : AppTheme.of(context).primaryColor) - .withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - child: empresa.logoUrl != null && - empresa.logoUrl!.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.network( - "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${empresa.logoUrl}", - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - fit: BoxFit.cover, - ); - }, - ), - ) - : Icon( - Icons.business, - color: isSelected ? Colors.white : Colors.white, - size: 24, - ), - ), - const SizedBox(width: 16), - - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - empresa.nombre, - style: TextStyle( - color: isSelected - ? Colors.white - : AppTheme.of(context).primaryText, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: (isSelected - ? Colors.white - : AppTheme.of(context).primaryColor) - .withOpacity(0.2), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - 'Tecnología', - style: TextStyle( - color: isSelected - ? Colors.white - : AppTheme.of(context).primaryColor, - fontSize: 10, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - - // Información adicional - Row( - children: [ - Icon( - Icons.store, - size: 16, - color: isSelected - ? Colors.white.withOpacity(0.8) - : AppTheme.of(context).secondaryText, - ), - const SizedBox(width: 6), - Text( - 'Sucursales: ${isSelected ? widget.provider.negocios.length : '...'}', - style: TextStyle( - color: isSelected - ? Colors.white.withOpacity(0.9) - : AppTheme.of(context).secondaryText, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ], - ), - ), - ), - ), - ); - } - - Widget _buildEmptyState() { - return Center( - child: Padding( - padding: const EdgeInsets.all(40), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedBuilder( - animation: _pulseAnimation, - builder: (context, child) { - return Transform.scale( - scale: _pulseAnimation.value, - child: Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.3), - blurRadius: 20, - offset: const Offset(0, 8), - ), - ], - ), - child: Icon( - Icons.business, - color: Colors.white, - size: 40, - ), - ), - ); - }, - ), - const SizedBox(height: 20), - Text( - 'Sin empresas', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - 'Añade tu primera empresa\npara comenzar', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/mobile_empresa_selector.dart b/lib/pages/empresa_negocios/widgets/mobile_empresa_selector.dart deleted file mode 100644 index 36e5984..0000000 --- a/lib/pages/empresa_negocios/widgets/mobile_empresa_selector.dart +++ /dev/null @@ -1,472 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; -import 'package:nethive_neo/helpers/globals.dart'; - -class MobileEmpresaSelector extends StatefulWidget { - final EmpresasNegociosProvider provider; - final Function(String) onEmpresaSelected; - - const MobileEmpresaSelector({ - Key? key, - required this.provider, - required this.onEmpresaSelected, - }) : super(key: key); - - @override - State createState() => _MobileEmpresaSelectorState(); -} - -class _MobileEmpresaSelectorState extends State - with TickerProviderStateMixin { - late AnimationController _animationController; - late Animation _fadeAnimation; - final TextEditingController _searchController = TextEditingController(); - List _filteredEmpresas = []; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - duration: const Duration(milliseconds: 300), - vsync: this, - ); - _fadeAnimation = CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - ); - - _filteredEmpresas = widget.provider.empresas; - _animationController.forward(); - - _searchController.addListener(_onSearchChanged); - } - - @override - void dispose() { - _animationController.dispose(); - _searchController.dispose(); - super.dispose(); - } - - void _onSearchChanged() { - final query = _searchController.text.toLowerCase(); - setState(() { - _filteredEmpresas = widget.provider.empresas.where((empresa) { - return empresa.nombre.toLowerCase().contains(query) || - empresa.rfc.toLowerCase().contains(query); - }).toList(); - }); - } - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: _fadeAnimation, - child: Container( - decoration: BoxDecoration( - color: AppTheme.of(context).primaryBackground, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(25), - topRight: Radius.circular(25), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Handle - Container( - margin: const EdgeInsets.symmetric(vertical: 10), - width: 40, - height: 4, - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryText.withOpacity(0.3), - borderRadius: BorderRadius.circular(2), - ), - ), - - // Header - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(25), - topRight: Radius.circular(25), - ), - ), - child: Column( - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.business_center, - color: Colors.white, - size: 24, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Seleccionar Empresa', - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'Elige una empresa para ver sus sucursales', - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 14, - ), - ), - ], - ), - ), - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon( - Icons.close, - color: Colors.white, - ), - ), - ], - ), - const SizedBox(height: 16), - - // Buscador - Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(15), - ), - child: TextField( - controller: _searchController, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - hintText: 'Buscar empresa...', - hintStyle: TextStyle( - color: Colors.white.withOpacity(0.7), - ), - prefixIcon: Icon( - Icons.search, - color: Colors.white.withOpacity(0.7), - ), - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 15, - ), - ), - ), - ), - ], - ), - ), - - // Lista de empresas - Flexible( - child: _filteredEmpresas.isEmpty - ? _buildEmptyState() - : ListView.builder( - padding: const EdgeInsets.all(16), - shrinkWrap: true, - itemCount: _filteredEmpresas.length, - itemBuilder: (context, index) { - return TweenAnimationBuilder( - duration: Duration(milliseconds: 200 + (index * 50)), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, value, child) { - return Transform.translate( - offset: Offset(30 * (1 - value), 0), - child: Opacity( - opacity: value, - child: _buildEmpresaCard( - _filteredEmpresas[index], - index, - ), - ), - ); - }, - ); - }, - ), - ), - - // Footer con información adicional - if (widget.provider.empresaSeleccionada != null) ...[ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - border: Border( - top: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Empresa Actual', - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - widget.provider.empresaSeleccionada!.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 4), - Text( - '${widget.provider.negocios.length} sucursales', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - ), - ), - ], - ), - ), - ], - ], - ), - ), - ); - } - - Widget _buildEmpresaCard(dynamic empresa, int index) { - final isSelected = widget.provider.empresaSeleccionadaId == empresa.id; - - return Container( - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - gradient: isSelected - ? AppTheme.of(context).primaryGradient - : LinearGradient( - colors: [ - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected - ? Colors.white.withOpacity(0.3) - : AppTheme.of(context).primaryColor.withOpacity(0.2), - width: isSelected ? 2 : 1, - ), - boxShadow: [ - BoxShadow( - color: isSelected - ? AppTheme.of(context).primaryColor.withOpacity(0.3) - : Colors.black.withOpacity(0.1), - blurRadius: isSelected ? 15 : 8, - offset: Offset(0, isSelected ? 8 : 3), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - widget.onEmpresaSelected(empresa.id); - Navigator.of(context).pop(); - }, - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - // Logo de la empresa - Container( - width: 60, - height: 60, - decoration: BoxDecoration( - gradient: isSelected - ? LinearGradient( - colors: [ - Colors.white.withOpacity(0.3), - Colors.white.withOpacity(0.1) - ], - ) - : AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: (isSelected - ? Colors.white - : AppTheme.of(context).primaryColor) - .withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - child: empresa.logoUrl != null && empresa.logoUrl!.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.network( - "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${empresa.logoUrl}", - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - fit: BoxFit.cover, - ); - }, - ), - ) - : Icon( - Icons.business, - color: isSelected ? Colors.white : Colors.white, - size: 30, - ), - ), - const SizedBox(width: 16), - - // Información de la empresa - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - empresa.nombre, - style: TextStyle( - color: isSelected - ? Colors.white - : AppTheme.of(context).primaryText, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Text( - empresa.rfc, - style: TextStyle( - color: isSelected - ? Colors.white.withOpacity(0.8) - : AppTheme.of(context).secondaryText, - fontSize: 14, - ), - ), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: (isSelected - ? Colors.white - : AppTheme.of(context).primaryColor) - .withOpacity(0.2), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - 'Tecnología', - style: TextStyle( - color: isSelected - ? Colors.white - : AppTheme.of(context).primaryColor, - fontSize: 10, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - - // Indicador de selección - if (isSelected) - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.check, - color: Colors.white, - size: 20, - ), - ) - else - Icon( - Icons.arrow_forward_ios, - color: AppTheme.of(context).secondaryText, - size: 16, - ), - ], - ), - ), - ), - ), - ); - } - - Widget _buildEmptyState() { - return Center( - child: Padding( - padding: const EdgeInsets.all(40), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: Icon( - Icons.search_off, - color: AppTheme.of(context).primaryColor, - size: 40, - ), - ), - const SizedBox(height: 16), - Text( - 'No se encontraron empresas', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - 'Intenta con otro término de búsqueda', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/negocios_cards_view.dart b/lib/pages/empresa_negocios/widgets/negocios_cards_view.dart deleted file mode 100644 index c57852a..0000000 --- a/lib/pages/empresa_negocios/widgets/negocios_cards_view.dart +++ /dev/null @@ -1,715 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; -import 'package:nethive_neo/helpers/globals.dart'; - -class NegociosCardsView extends StatelessWidget { - final EmpresasNegociosProvider provider; - - const NegociosCardsView({ - Key? key, - required this.provider, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - if (provider.empresaSeleccionada == null) { - return _buildEmptyState(context); - } - - if (provider.negocios.isEmpty) { - return _buildNoDataState(context); - } - - return Container( - padding: const EdgeInsets.all(16), - child: ListView.builder( - itemCount: provider.negocios.length, - itemBuilder: (context, index) { - final negocio = provider.negocios[index]; - return TweenAnimationBuilder( - duration: Duration(milliseconds: 300 + (index * 100)), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, value, child) { - return Transform.translate( - offset: Offset(0, 50 * (1 - value)), - child: Opacity( - opacity: value, - child: Container( - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - ), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: - AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.1), - blurRadius: 15, - offset: const Offset(0, 5), - ), - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - _showNegocioDetails(context, negocio); - }, - borderRadius: BorderRadius.circular(20), - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header con logo y nombre - Row( - children: [ - // Logo del negocio - Container( - width: 60, - height: 60, - decoration: BoxDecoration( - gradient: AppTheme.of(context) - .primaryGradient, - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.3), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: negocio.logoUrl != null && - negocio.logoUrl!.isNotEmpty - ? ClipRRect( - borderRadius: - BorderRadius.circular(15), - child: Image.network( - "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${negocio.logoUrl}", - fit: BoxFit.cover, - errorBuilder: (context, error, - stackTrace) { - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - fit: BoxFit.cover, - ); - }, - ), - ) - : Icon( - Icons.store, - color: Colors.white, - size: 30, - ), - ), - const SizedBox(width: 16), - - // Información principal - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - negocio.nombre, - style: TextStyle( - color: AppTheme.of(context) - .primaryText, - fontSize: 20, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - decoration: BoxDecoration( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.2), - borderRadius: - BorderRadius.circular(15), - border: Border.all( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.4), - ), - ), - child: Text( - negocio.tipoLocal.isNotEmpty - ? negocio.tipoLocal - : 'Sucursal', - style: TextStyle( - color: AppTheme.of(context) - .primaryColor, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - - // Botón de acciones - PopupMenuButton( - icon: Icon( - Icons.more_vert, - color: - AppTheme.of(context).secondaryText, - ), - color: AppTheme.of(context) - .secondaryBackground, - onSelected: (value) { - _handleMenuAction( - context, value, negocio); - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'edit', - child: Row( - children: [ - Icon(Icons.edit, - color: AppTheme.of(context) - .primaryColor), - const SizedBox(width: 8), - Text( - 'Editar', - style: TextStyle( - color: AppTheme.of(context) - .primaryText), - ), - ], - ), - ), - PopupMenuItem( - value: 'components', - child: Row( - children: [ - Icon(Icons.inventory_2, - color: Colors.orange), - const SizedBox(width: 8), - Text( - 'Ver Componentes', - style: TextStyle( - color: AppTheme.of(context) - .primaryText), - ), - ], - ), - ), - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Icon(Icons.delete, - color: Colors.red), - const SizedBox(width: 8), - Text( - 'Eliminar', - style: TextStyle( - color: AppTheme.of(context) - .primaryText), - ), - ], - ), - ), - ], - ), - ], - ), - - const SizedBox(height: 16), - - // Información de ubicación - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.of(context) - .primaryBackground - .withOpacity(0.5), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.2), - ), - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.location_on, - color: AppTheme.of(context) - .primaryColor, - size: 18, - ), - const SizedBox(width: 8), - Text( - 'Ubicación', - style: TextStyle( - color: AppTheme.of(context) - .primaryColor, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - negocio.direccion.isNotEmpty - ? negocio.direccion - : 'Sin dirección', - style: TextStyle( - color: - AppTheme.of(context).primaryText, - fontSize: 14, - height: 1.4, - ), - ), - ], - ), - ), - - const SizedBox(height: 12), - - // Coordenadas y estadísticas - Row( - children: [ - Expanded( - child: _buildInfoChip( - context, - icon: Icons.gps_fixed, - label: 'Coordenadas', - value: - '${negocio.latitud.toStringAsFixed(4)}, ${negocio.longitud.toStringAsFixed(4)}', - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildInfoChip( - context, - icon: Icons.people, - label: 'Empleados', - value: negocio.tipoLocal == 'Sucursal' - ? '95' - : '120', - ), - ), - ], - ), - - const SizedBox(height: 12), - - // Fecha de creación - Row( - children: [ - Icon( - Icons.calendar_today, - color: AppTheme.of(context).secondaryText, - size: 16, - ), - const SizedBox(width: 8), - Text( - 'Creado: ${negocio.fechaCreacion.toString().split(' ')[0]}', - style: TextStyle( - color: - AppTheme.of(context).secondaryText, - fontSize: 12, - ), - ), - ], - ), - ], - ), - ), - ), - ), - ), - ), - ), - ); - }, - ); - }, - ), - ); - } - - Widget _buildInfoChip( - BuildContext context, { - required IconData icon, - required String label, - required String value, - }) { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.of(context).primaryColor.withOpacity(0.1), - AppTheme.of(context).tertiaryColor.withOpacity(0.1), - ], - ), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - icon, - color: AppTheme.of(context).primaryColor, - size: 16, - ), - const SizedBox(width: 6), - Text( - label, - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - value, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ); - } - - Widget _buildEmptyState(BuildContext context) { - return Center( - child: Container( - padding: const EdgeInsets.all(40), - margin: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.business_center, - size: 60, - color: Colors.white, - ), - const SizedBox(height: 16), - Text( - 'Selecciona una empresa', - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - 'Toca el botón de empresas para seleccionar una y ver sus sucursales', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - ), - ), - ], - ), - ), - ); - } - - Widget _buildNoDataState(BuildContext context) { - return Center( - child: Container( - padding: const EdgeInsets.all(40), - margin: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.store_mall_directory, - size: 60, - color: AppTheme.of(context).secondaryText, - ), - const SizedBox(height: 16), - Text( - 'Sin sucursales', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - 'Esta empresa aún no tiene sucursales registradas', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - ), - ), - ], - ), - ), - ); - } - - void _showNegocioDetails(BuildContext context, dynamic negocio) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => DraggableScrollableSheet( - initialChildSize: 0.6, - maxChildSize: 0.9, - minChildSize: 0.3, - builder: (context, scrollController) => Container( - decoration: BoxDecoration( - color: AppTheme.of(context).primaryBackground, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(25), - topRight: Radius.circular(25), - ), - ), - child: Column( - children: [ - // Handle - Container( - margin: const EdgeInsets.symmetric(vertical: 10), - width: 40, - height: 4, - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryText, - borderRadius: BorderRadius.circular(2), - ), - ), - // Contenido del modal - Expanded( - child: SingleChildScrollView( - controller: scrollController, - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Detalles de la Sucursal', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 20), - // Aquí puedes agregar más detalles específicos - Text( - 'Información adicional de ${negocio.nombre}', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 16, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } - - void _handleMenuAction(BuildContext context, String action, dynamic negocio) { - switch (action) { - case 'edit': - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Función de edición próximamente')), - ); - break; - case 'components': - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Ver componentes de ${negocio.nombre}')), - ); - break; - case 'delete': - _showDeleteDialog(context, negocio); - break; - } - } - - void _showDeleteDialog(BuildContext context, dynamic negocio) { - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: AppTheme.of(context).primaryBackground, - title: Text( - 'Confirmar eliminación', - style: TextStyle(color: AppTheme.of(context).primaryText), - ), - content: Text( - '¿Estás seguro de que deseas eliminar la sucursal "${negocio.nombre}"?', - style: TextStyle(color: AppTheme.of(context).secondaryText), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - 'Cancelar', - style: TextStyle(color: AppTheme.of(context).secondaryText), - ), - ), - TextButton( - onPressed: () async { - // Cerrar el diálogo antes de la operación asíncrona - Navigator.pop(context); - - // Mostrar indicador de carga - if (context.mounted) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => Center( - child: CircularProgressIndicator( - color: AppTheme.of(context).primaryColor, - ), - ), - ); - } - - try { - final success = await provider.eliminarNegocio(negocio.id); - - // Cerrar indicador de carga - if (context.mounted) { - Navigator.pop(context); - } - - // Mostrar resultado solo si el contexto sigue válido - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - Icon( - success ? Icons.check_circle : Icons.error, - color: Colors.white, - ), - const SizedBox(width: 12), - Text( - success - ? 'Sucursal eliminada correctamente' - : 'Error al eliminar la sucursal', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: success ? Colors.green : Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } catch (e) { - // Cerrar indicador de carga en caso de error - if (context.mounted) { - Navigator.pop(context); - } - - // Mostrar error solo si el contexto sigue válido - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(Icons.warning, color: Colors.white), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Error: $e', - style: - const TextStyle(fontWeight: FontWeight.w600), - ), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } - }, - child: const Text( - 'Eliminar', - style: TextStyle(color: Colors.red), - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/empresa_negocios/widgets/negocios_map_view.dart b/lib/pages/empresa_negocios/widgets/negocios_map_view.dart deleted file mode 100644 index 02324b5..0000000 --- a/lib/pages/empresa_negocios/widgets/negocios_map_view.dart +++ /dev/null @@ -1,825 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:latlong2/latlong.dart'; -import 'package:go_router/go_router.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/models/nethive/negocio_model.dart'; -import 'package:nethive_neo/theme/theme.dart'; -import 'package:nethive_neo/helpers/globals.dart'; - -class NegociosMapView extends StatefulWidget { - final EmpresasNegociosProvider provider; - - const NegociosMapView({ - Key? key, - required this.provider, - }) : super(key: key); - - @override - State createState() => _NegociosMapViewState(); -} - -class _NegociosMapViewState extends State - with TickerProviderStateMixin { - final MapController _mapController = MapController(); - late AnimationController _markerAnimationController; - late AnimationController _tooltipAnimationController; - late Animation _markerAnimation; - late Animation _tooltipAnimation; - late Animation _tooltipSlideAnimation; - - String? _hoveredNegocioId; - Offset? _tooltipPosition; - bool _showTooltip = false; - - @override - void initState() { - super.initState(); - _markerAnimationController = AnimationController( - duration: const Duration(milliseconds: 300), - vsync: this, - ); - _tooltipAnimationController = AnimationController( - duration: const Duration(milliseconds: 200), - vsync: this, - ); - - _markerAnimation = Tween( - begin: 1.0, - end: 1.4, - ).animate(CurvedAnimation( - parent: _markerAnimationController, - curve: Curves.easeOutBack, - )); - - _tooltipAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _tooltipAnimationController, - curve: Curves.easeOutCubic, - )); - - _tooltipSlideAnimation = Tween( - begin: const Offset(0, 0.5), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _tooltipAnimationController, - curve: Curves.easeOutCubic, - )); - - // Centrar el mapa después de que se construya - WidgetsBinding.instance.addPostFrameCallback((_) { - _centerMapOnNegocios(); - }); - } - - @override - void dispose() { - _markerAnimationController.dispose(); - _tooltipAnimationController.dispose(); - super.dispose(); - } - - void _showTooltipForNegocio(String negocioId, Offset position) { - setState(() { - _hoveredNegocioId = negocioId; - _tooltipPosition = Offset( - position.dx - 300, // Más cerca del cursor - position.dy - 500, // Más cerca del cursor - ); - _showTooltip = true; - }); - - _markerAnimationController.forward(); - _tooltipAnimationController.forward(); - } - - void _hideTooltip() { - _tooltipAnimationController.reverse().then((_) { - if (mounted) { - setState(() { - _hoveredNegocioId = null; - _showTooltip = false; - _tooltipPosition = null; - }); - } - }); - _markerAnimationController.reverse(); - } - - void _centerMapOnNegocios() { - if (widget.provider.negocios.isNotEmpty) { - final bounds = _calculateBounds(); - _mapController.fitCamera( - CameraFit.bounds( - bounds: bounds, - padding: const EdgeInsets.all(50), - maxZoom: 15, - ), - ); - } - } - - LatLngBounds _calculateBounds() { - if (widget.provider.negocios.isEmpty) { - return LatLngBounds( - const LatLng(-90, -180), - const LatLng(90, 180), - ); - } - - double minLat = widget.provider.negocios.first.latitud; - double maxLat = widget.provider.negocios.first.latitud; - double minLng = widget.provider.negocios.first.longitud; - double maxLng = widget.provider.negocios.first.longitud; - - for (final negocio in widget.provider.negocios) { - minLat = minLat < negocio.latitud ? minLat : negocio.latitud; - maxLat = maxLat > negocio.latitud ? maxLat : negocio.latitud; - minLng = minLng < negocio.longitud ? minLng : negocio.longitud; - maxLng = maxLng > negocio.longitud ? maxLng : negocio.longitud; - } - - return LatLngBounds( - LatLng(minLat, minLng), - LatLng(maxLat, maxLng), - ); - } - - @override - Widget build(BuildContext context) { - if (widget.provider.negocios.isEmpty) { - return _buildEmptyMapState(); - } - - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: Stack( - children: [ - // Listener para detectar movimientos del mouse sobre el mapa - MouseRegion( - onExit: (_) => _hideTooltip(), - child: FlutterMap( - mapController: _mapController, - options: MapOptions( - initialCenter: const LatLng(19.4326, -99.1332), - initialZoom: 10, - minZoom: 3, - maxZoom: 18, - ), - children: [ - // Capa de tiles del mapa - TileLayer( - urlTemplate: - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'com.nethive.app', - ), - - // Capa de marcadores - MarkerLayer( - markers: _buildMarkers(), - ), - ], - ), - ), - - // Header del mapa con información - Positioned( - top: 20, - left: 20, - right: 20, - child: _buildMapHeader(), - ), - - // Controles del mapa - Positioned( - bottom: 20, - right: 20, - child: _buildMapControls(), - ), - - // Tooltip flotante con información del negocio - if (_showTooltip && _tooltipPosition != null) - Positioned( - left: _tooltipPosition!.dx - .clamp(20.0, MediaQuery.of(context).size.width - 280), - top: _tooltipPosition!.dy - .clamp(20.0, MediaQuery.of(context).size.height - 150), - child: _buildAnimatedTooltip(), - ), - ], - ), - ), - ); - } - - List _buildMarkers() { - return widget.provider.negocios.map((negocio) { - final isHovered = _hoveredNegocioId == negocio.id; - - return Marker( - point: LatLng(negocio.latitud, negocio.longitud), - width: 50, - height: 50, - child: MouseRegion( - cursor: SystemMouseCursors.click, // Cursor de puntero - onEnter: (event) { - _showTooltipForNegocio(negocio.id, event.position); - }, - onHover: (event) { - // Actualizar posición del tooltip sin recalcular todo - if (_hoveredNegocioId == negocio.id) { - setState(() { - _tooltipPosition = Offset( - event.position.dx - 300, // Más cerca del cursor - event.position.dy - 400, // Más cerca del cursor - ); - }); - } - }, - child: GestureDetector( - onTap: () { - // Navegar a la infraestructura del negocio - context.go('/infrastructure/${negocio.id}'); - }, - child: AnimatedBuilder( - animation: _markerAnimation, - builder: (context, child) { - return Transform.scale( - scale: isHovered ? _markerAnimation.value : 1.0, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: isHovered - ? AppTheme.of(context).primaryGradient - : AppTheme.of(context).modernGradient, - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.4), - blurRadius: isHovered ? 15 : 8, - offset: const Offset(0, 4), - ), - ], - border: Border.all( - color: Colors.white, - width: isHovered ? 3 : 2, - ), - ), - child: ClipOval( - child: _buildMarkerContent(negocio, isHovered), - ), - ), - ); - }, - ), - ), - ), - ); - }).toList(); - } - - Widget _buildMarkerContent(Negocio negocio, bool isHovered) { - // Verificar si tiene imagen válida - if (negocio.imagenUrl != null && negocio.imagenUrl!.isNotEmpty) { - final imageUrl = - "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/imagenes/${negocio.imagenUrl}"; - - return Image.network( - imageUrl, - fit: BoxFit.cover, - width: 40, - height: 40, - errorBuilder: (context, error, stackTrace) { - // Si falla la imagen, mostrar el ícono - return _buildMarkerIcon(isHovered); - }, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) { - return child; - } - // Mientras carga, mostrar el ícono - return _buildMarkerIcon(isHovered); - }, - ); - } else { - // Si no hay imagen, mostrar el ícono - return _buildMarkerIcon(isHovered); - } - } - - Widget _buildMarkerIcon(bool isHovered) { - return Container( - width: 40, - height: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.transparent, - ), - child: Icon( - Icons.store, - color: Colors.white, - size: isHovered ? 22 : 18, - ), - ); - } - - Widget _buildAnimatedTooltip() { - final negocio = widget.provider.negocios.firstWhere( - (n) => n.id == _hoveredNegocioId, - ); - - return SlideTransition( - position: _tooltipSlideAnimation, - child: FadeTransition( - opacity: _tooltipAnimation, - child: ScaleTransition( - scale: _tooltipAnimation, - child: Container( - width: 260, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: AppTheme.of(context).modernGradient, - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 20, - offset: const Offset(0, 8), - ), - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Header con imagen y nombre - Row( - children: [ - // Imagen del negocio o ícono por defecto - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: Colors.white.withOpacity(0.3), - width: 2, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: _buildTooltipImage(negocio), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - negocio.nombre, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - negocio.tipoLocal, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - - // Información de ubicación - Row( - children: [ - Icon( - Icons.location_on, - color: Colors.white.withOpacity(0.8), - size: 16, - ), - const SizedBox(width: 6), - Expanded( - child: Text( - negocio.direccion, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const SizedBox(height: 8), - - // Coordenadas - Row( - children: [ - Icon( - Icons.my_location, - color: Colors.white.withOpacity(0.8), - size: 16, - ), - const SizedBox(width: 6), - Text( - 'Lat: ${negocio.latitud.toStringAsFixed(4)}, Lng: ${negocio.longitud.toStringAsFixed(4)}', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 12, - fontFamily: 'monospace', - ), - ), - ], - ), - const SizedBox(height: 12), - - // Call to action - Container( - width: double.infinity, - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Colors.white.withOpacity(0.3), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.touch_app, - color: Colors.white, - size: 16, - ), - const SizedBox(width: 8), - Text( - 'Clic para ver infraestructura', - style: TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ); - } - - Widget _buildTooltipImage(Negocio negocio) { - // Verificar si tiene imagen válida - if (negocio.imagenUrl != null && negocio.imagenUrl!.isNotEmpty) { - final imageUrl = - "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/imagenes/${negocio.imagenUrl}"; - - return Image.network( - imageUrl, - fit: BoxFit.cover, - width: 50, - height: 50, - errorBuilder: (context, error, stackTrace) { - // Si falla la imagen, mostrar el ícono - return _buildTooltipIcon(); - }, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) { - return child; - } - // Mientras carga, mostrar un spinner - return Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ), - ); - }, - ); - } else { - // Si no hay imagen, mostrar el ícono - return _buildTooltipIcon(); - } - } - - Widget _buildTooltipIcon() { - return Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.store, - color: Colors.white, - size: 24, - ), - ); - } - - Widget _buildMapHeader() { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - Icons.map, - color: Colors.white, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Mapa de Sucursales', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - Text( - '${widget.provider.negocios.length} ubicaciones de ${widget.provider.empresaSeleccionada?.nombre ?? ""}', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - ), - ), - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.location_on, - color: Colors.white, - size: 16, - ), - const SizedBox(width: 6), - Text( - 'OpenStreetMap', - style: TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildMapControls() { - return Column( - children: [ - // Botón de centrar mapa - Container( - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 10, - offset: const Offset(0, 3), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: _centerMapOnNegocios, - borderRadius: BorderRadius.circular(10), - child: Container( - padding: const EdgeInsets.all(12), - child: Icon( - Icons.center_focus_strong, - color: Colors.white, - size: 24, - ), - ), - ), - ), - ), - const SizedBox(height: 12), - - // Botón de zoom in - Container( - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - _mapController.move( - _mapController.camera.center, - _mapController.camera.zoom + 1, - ); - }, - borderRadius: BorderRadius.circular(10), - child: Container( - padding: const EdgeInsets.all(12), - child: Icon( - Icons.add, - color: AppTheme.of(context).primaryColor, - size: 20, - ), - ), - ), - ), - ), - const SizedBox(height: 8), - - // Botón de zoom out - Container( - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - _mapController.move( - _mapController.camera.center, - _mapController.camera.zoom - 1, - ); - }, - borderRadius: BorderRadius.circular(10), - child: Container( - padding: const EdgeInsets.all(12), - child: Icon( - Icons.remove, - color: AppTheme.of(context).primaryColor, - size: 20, - ), - ), - ), - ), - ), - ], - ); - } - - Widget _buildEmptyMapState() { - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Colors.blue.withOpacity(0.1), - Colors.blue.withOpacity(0.3), - AppTheme.of(context).primaryColor.withOpacity(0.2), - ], - ), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: AppTheme.of(context).primaryColor, - width: 2, - ), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).modernGradient, - borderRadius: BorderRadius.circular(20), - ), - child: Icon( - Icons.location_off, - size: 60, - color: Colors.white, - ), - ), - const SizedBox(height: 20), - Text( - 'Sin ubicaciones para mostrar', - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - Text( - 'Selecciona una empresa con sucursales\npara ver sus ubicaciones en el mapa', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 16, - ), - ), - ], - ), - ), - ); - } - - bool get isHovered => _hoveredNegocioId != null; -} diff --git a/lib/pages/empresa_negocios/widgets/negocios_table.dart b/lib/pages/empresa_negocios/widgets/negocios_table.dart deleted file mode 100644 index df94af8..0000000 --- a/lib/pages/empresa_negocios/widgets/negocios_table.dart +++ /dev/null @@ -1,619 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:pluto_grid/pluto_grid.dart'; -import 'package:go_router/go_router.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/pages/widgets/animated_hover_button.dart'; -import 'package:nethive_neo/theme/theme.dart'; -import 'package:provider/provider.dart'; -import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; - -class NegociosTable extends StatelessWidget { - final EmpresasNegociosProvider provider; - - const NegociosTable({ - Key? key, - required this.provider, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return PlutoGrid( - key: UniqueKey(), - configuration: PlutoGridConfiguration( - enableMoveDownAfterSelecting: true, - enableMoveHorizontalInEditing: true, - localeText: const PlutoGridLocaleText.spanish(), - scrollbar: PlutoGridScrollbarConfig( - draggableScrollbar: true, - isAlwaysShown: false, - onlyDraggingThumb: true, - enableScrollAfterDragEnd: true, - scrollbarThickness: 12, - scrollbarThicknessWhileDragging: 16, - hoverWidth: 20, - scrollBarColor: AppTheme.of(context).primaryColor.withOpacity(0.7), - scrollBarTrackColor: Colors.grey.withOpacity(0.2), - scrollbarRadius: const Radius.circular(8), - scrollbarRadiusWhileDragging: const Radius.circular(10), - ), - style: PlutoGridStyleConfig( - gridBorderColor: Colors.grey.withOpacity(0.3), - activatedBorderColor: AppTheme.of(context).primaryColor, - inactivatedBorderColor: Colors.grey.withOpacity(0.3), - gridBackgroundColor: AppTheme.of(context).primaryBackground, - rowColor: AppTheme.of(context).secondaryBackground, - activatedColor: AppTheme.of(context).primaryColor.withOpacity(0.1), - checkedColor: AppTheme.of(context).primaryColor.withOpacity(0.2), - cellTextStyle: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - ), - columnTextStyle: TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - menuBackgroundColor: AppTheme.of(context).secondaryBackground, - gridBorderRadius: BorderRadius.circular(8), - rowHeight: 70, - ), - columnFilter: const PlutoGridColumnFilterConfig( - filters: [ - ...FilterHelper.defaultFilters, - ], - ), - ), - columns: [ - PlutoColumn( - title: 'ID', - field: 'id', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 100, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Text( - rendererContext.cell.value.toString().substring(0, 8) + '...', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ); - }, - ), - PlutoColumn( - title: 'Nombre de Sucursal', - field: 'nombre', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 200, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Container( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - // Logo del negocio - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(6), - ), - child: - rendererContext.row.cells['logo_url']?.value != null && - rendererContext.row.cells['logo_url']!.value - .toString() - .isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(6), - child: Image.network( - rendererContext.row.cells['logo_url']!.value - .toString(), - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - fit: BoxFit.cover, - ); - }, - ), - ) - : Image.asset( - 'assets/images/placeholder_no_image.jpg', - fit: BoxFit.cover, - ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - rendererContext.cell.value.toString(), - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - }, - ), - PlutoColumn( - title: 'Ciudad', - field: 'direccion', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 180, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - // Extraer solo la ciudad de la dirección completa - String direccionCompleta = rendererContext.cell.value.toString(); - String ciudad = _extraerCiudad(direccionCompleta); - return Container( - padding: const EdgeInsets.all(8), - child: Text( - ciudad, - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ); - }, - ), - PlutoColumn( - title: 'Empleados', - field: 'tipo_local', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 120, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - // Simulamos cantidad de empleados basado en el tipo de local - String empleados = - rendererContext.cell.value.toString() == 'Sucursal' - ? '95' - : '120'; - return Container( - padding: const EdgeInsets.all(8), - child: Text( - '$empleados empleados', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ); - }, - ), - PlutoColumn( - title: 'Dirección Completa', - field: 'direccion_completa', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 280, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - // Usamos la dirección del row en lugar del cell - String direccion = - rendererContext.row.cells['direccion']?.value.toString() ?? ''; - return Container( - padding: const EdgeInsets.all(8), - child: Text( - direccion, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ); - }, - ), - PlutoColumn( - title: 'Coordenadas', - field: 'latitud', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 150, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - final latitud = rendererContext.row.cells['latitud']?.value ?? '0'; - final longitud = - rendererContext.row.cells['longitud']?.value ?? '0'; - return Container( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Lat: ${double.parse(latitud.toString()).toStringAsFixed(4)}', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 12, - ), - ), - Text( - 'Lng: ${double.parse(longitud.toString()).toStringAsFixed(4)}', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 12, - ), - ), - ], - ), - ); - }, - ), - PlutoColumn( - title: 'Infraestructura', - field: 'acceder_infraestructura', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 200, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Container( - padding: const EdgeInsets.all(8), - child: Center( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.orange.shade600, - Colors.deepOrange.shade500, - ], - ), - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.orange.withOpacity(0.4), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - final negocioId = - rendererContext.row.cells['id']?.value; - final negocioNombre = - rendererContext.row.cells['nombre']?.value; - final empresaId = - rendererContext.row.cells['empresa_id']?.value; - - if (negocioId != null && - negocioNombre != null && - empresaId != null) { - // Establecer el contexto del negocio en ComponentesProvider - final componentesProvider = - Provider.of(context, - listen: false); - componentesProvider - .setNegocioSeleccionado( - negocioId, - negocioNombre, - empresaId, - ) - .then((_) { - // Navegar al layout principal con el negocio seleccionado - if (context.mounted) { - context.go('/infrastructure/$negocioId'); - } - }); - } - }, - borderRadius: BorderRadius.circular(12), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.developer_board, - color: Colors.white, - size: 18, - ), - const SizedBox(width: 8), - Text( - 'Acceder a\nInfraestructura', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - height: 1.2, - ), - ), - ], - ), - ), - ), - ), - ), - ), - ); - }, - ), - PlutoColumn( - title: 'Acciones', - field: 'editar', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 120, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Botón editar - Tooltip( - message: 'Editar negocio', - child: InkWell( - onTap: () { - // TODO: Implementar edición - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Función de edición próximamente')), - ); - }, - child: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(4), - ), - child: const Icon( - Icons.edit, - color: Colors.white, - size: 16, - ), - ), - ), - ), - const SizedBox(width: 8), - // Botón ver componentes - Tooltip( - message: 'Ver componentes', - child: InkWell( - onTap: () { - // TODO: Navegar a componentes - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Navegando a componentes de ${rendererContext.row.cells['nombre']?.value}', - ), - ), - ); - }, - child: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: Colors.orange, - borderRadius: BorderRadius.circular(4), - ), - child: const Icon( - Icons.inventory_2, - color: Colors.white, - size: 16, - ), - ), - ), - ), - const SizedBox(width: 8), - // Botón eliminar - Tooltip( - message: 'Eliminar negocio', - child: InkWell( - onTap: () { - _showDeleteDialog( - context, - rendererContext.row.cells['id']?.value, - rendererContext.row.cells['nombre']?.value, - ); - }, - child: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(4), - ), - child: const Icon( - Icons.delete, - color: Colors.white, - size: 16, - ), - ), - ), - ), - ], - ); - }, - ), - ], - rows: provider.negociosRows, - onLoaded: (event) { - provider.negociosStateManager = event.stateManager; - }, - createFooter: (stateManager) { - stateManager.setPageSize(10, notify: false); - return PlutoPagination(stateManager); - }, - ); - } - - void _showDeleteDialog( - BuildContext context, String? negocioId, String? nombre) { - if (negocioId == null) return; - - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: AppTheme.of(context).primaryBackground, - title: const Text('Confirmar eliminación'), - content: Text( - '¿Estás seguro de que deseas eliminar la sucursal "$nombre"?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text( - 'Cancelar', - style: TextStyle(color: AppTheme.of(context).secondaryText), - ), - ), - TextButton( - onPressed: () async { - // Cerrar el diálogo antes de la operación asíncrona - Navigator.of(context).pop(); - - // Mostrar indicador de carga - if (context.mounted) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => Center( - child: CircularProgressIndicator( - color: AppTheme.of(context).primaryColor, - ), - ), - ); - } - - try { - final success = await provider.eliminarNegocio(negocioId); - - // Cerrar indicador de carga - if (context.mounted) { - Navigator.of(context).pop(); - } - - // Mostrar resultado solo si el contexto sigue válido - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - Icon( - success ? Icons.check_circle : Icons.error, - color: Colors.white, - ), - const SizedBox(width: 12), - Text( - success - ? 'Sucursal eliminada correctamente' - : 'Error al eliminar la sucursal', - style: - const TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: success ? Colors.green : Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } catch (e) { - // Cerrar indicador de carga en caso de error - if (context.mounted) { - Navigator.of(context).pop(); - } - - // Mostrar error solo si el contexto sigue válido - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(Icons.warning, color: Colors.white), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Error: $e', - style: const TextStyle( - fontWeight: FontWeight.w600), - ), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } - }, - child: const Text( - 'Eliminar', - style: TextStyle(color: Colors.red), - ), - ), - ], - ); - }, - ); - } - - String _extraerCiudad(String direccionCompleta) { - // Lógica para extraer la ciudad de la dirección completa - // Suponiendo que la ciudad es la segunda palabra en la dirección - List partes = direccionCompleta.split(','); - return partes.length > 1 ? partes[1].trim() : direccionCompleta; - } -} diff --git a/lib/pages/infrastructure/infrastructure_layout.dart b/lib/pages/infrastructure/infrastructure_layout.dart deleted file mode 100644 index 766eaa0..0000000 --- a/lib/pages/infrastructure/infrastructure_layout.dart +++ /dev/null @@ -1,582 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:nethive_neo/providers/nethive/navigation_provider.dart'; -import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; -import 'package:nethive_neo/pages/infrastructure/widgets/infrastructure_sidemenu.dart'; -import 'package:nethive_neo/pages/infrastructure/widgets/mobile_navigation_modal.dart'; -import 'package:nethive_neo/pages/infrastructure/pages/dashboard_page.dart'; -import 'package:nethive_neo/pages/infrastructure/pages/inventario_page.dart'; -import 'package:nethive_neo/pages/infrastructure/pages/topologia_page.dart'; -import 'package:nethive_neo/pages/infrastructure/pages/alertas_page.dart'; -import 'package:nethive_neo/pages/infrastructure/pages/configuracion_page.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class InfrastructureLayout extends StatefulWidget { - final String negocioId; - - const InfrastructureLayout({ - Key? key, - required this.negocioId, - }) : super(key: key); - - @override - State createState() => _InfrastructureLayoutState(); -} - -class _InfrastructureLayoutState extends State - with TickerProviderStateMixin { - bool _isSidebarExpanded = true; - late AnimationController _fadeController; - late Animation _fadeAnimation; - - @override - void initState() { - super.initState(); - _fadeController = AnimationController( - duration: const Duration(milliseconds: 500), - vsync: this, - ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _fadeController, - curve: Curves.easeInOut, - )); - - // Establecer el negocio seleccionado - WidgetsBinding.instance.addPostFrameCallback((_) async { - // Primero establecer en NavigationProvider - context - .read() - .setNegocioSeleccionado(widget.negocioId); - - // Luego obtener la información completa y establecer en ComponentesProvider - await _setupComponentesProvider(); - - _fadeController.forward(); - }); - } - - Future _setupComponentesProvider() async { - try { - final navigationProvider = context.read(); - final componentesProvider = context.read(); - - // Esperar a que NavigationProvider cargue la información del negocio - await Future.delayed(const Duration(milliseconds: 100)); - - final negocio = navigationProvider.negocioSeleccionado; - final empresa = navigationProvider.empresaSeleccionada; - - if (negocio != null && empresa != null) { - // Establecer el contexto completo en ComponentesProvider - await componentesProvider.setNegocioSeleccionado( - negocio.id, - negocio.nombre, - empresa.id, - ); - } - } catch (e) { - print('Error al configurar ComponentesProvider: ${e.toString()}'); - } - } - - @override - void dispose() { - _fadeController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isLargeScreen = MediaQuery.of(context).size.width > 1200; - final isMediumScreen = MediaQuery.of(context).size.width > 800; - - // Ajustar sidebar basado en tamaño de pantalla - if (!isLargeScreen && _isSidebarExpanded) { - _isSidebarExpanded = false; - } - - return Scaffold( - backgroundColor: AppTheme.of(context).primaryBackground, - body: FadeTransition( - opacity: _fadeAnimation, - child: Container( - decoration: BoxDecoration( - gradient: AppTheme.of(context).darkBackgroundGradient, - ), - child: Consumer( - builder: (context, navigationProvider, child) { - if (navigationProvider.negocioSeleccionado == null) { - return _buildLoadingScreen(); - } - - if (isMediumScreen) { - // Vista desktop/tablet - return Row( - children: [ - // Sidebar - InfrastructureSidemenu( - isExpanded: _isSidebarExpanded, - onToggle: () { - setState(() { - _isSidebarExpanded = !_isSidebarExpanded; - }); - }, - ), - - // Área principal - Expanded( - child: Column( - children: [ - // Header superior - _buildHeader(navigationProvider), - - // Contenido principal - Expanded( - child: _buildMainContent(navigationProvider), - ), - ], - ), - ), - ], - ); - } else { - // Vista móvil - return Column( - children: [ - // Header móvil - _buildMobileHeader(navigationProvider), - - // Contenido principal - Expanded( - child: _buildMainContent(navigationProvider), - ), - ], - ); - } - }, - ), - ), - ), - - // Drawer para móvil - drawer: MediaQuery.of(context).size.width <= 800 - ? Drawer( - backgroundColor: Colors.transparent, - child: InfrastructureSidemenu( - isExpanded: true, - onToggle: () => Navigator.of(context).pop(), - ), - ) - : null, - ); - } - - Widget _buildLoadingScreen() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - shape: BoxShape.circle, - ), - child: const CircularProgressIndicator( - color: Colors.white, - strokeWidth: 3, - ), - ), - const SizedBox(height: 20), - Text( - 'Cargando infraestructura...', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ); - } - - Widget _buildHeader(NavigationProvider navigationProvider) { - final negocio = navigationProvider.negocioSeleccionado!; - final empresa = navigationProvider.empresaSeleccionada!; - final currentMenuItem = navigationProvider.getMenuItemByIndex( - navigationProvider.selectedMenuIndex, - ); - - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - border: Border( - bottom: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - width: 1, - ), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - // Logo solo de Nethive - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(12), - ), - child: Image.asset( - 'assets/images/favicon.png', - width: 48, - height: 48, - ), - ), - - const SizedBox(width: 20), - - // Breadcrumb mejorado - Expanded( - child: Row( - children: [ - // Empresa - Text( - empresa.nombre, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(width: 8), - Icon( - Icons.arrow_forward_ios, - size: 12, - color: AppTheme.of(context).secondaryText, - ), - const SizedBox(width: 8), - - // Negocio (cuadro verde como en la imagen de referencia) - Container( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.green, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - negocio.nombre, - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - Text( - '(${empresa.nombre})', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 10, - ), - ), - ], - ), - ), - - const SizedBox(width: 8), - Icon( - Icons.arrow_forward_ios, - size: 12, - color: AppTheme.of(context).secondaryText, - ), - const SizedBox(width: 8), - - // Página actual - Text( - currentMenuItem.title, - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - - // Buscador (conservado como en la referencia) - Container( - width: 300, - height: 40, - decoration: BoxDecoration( - color: AppTheme.of(context).formBackground, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - ), - ), - child: TextField( - style: TextStyle(color: AppTheme.of(context).primaryText), - decoration: InputDecoration( - hintText: 'Buscar en infraestructura...', - hintStyle: TextStyle( - color: AppTheme.of(context).hintText, - fontSize: 14, - ), - prefixIcon: Icon( - Icons.search, - color: AppTheme.of(context).primaryColor, - size: 20, - ), - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildMobileHeader(NavigationProvider navigationProvider) { - final negocio = navigationProvider.negocioSeleccionado!; - final currentMenuItem = navigationProvider.getMenuItemByIndex( - navigationProvider.selectedMenuIndex, - ); - - return Container( - padding: const EdgeInsets.fromLTRB(16, 40, 16, 16), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20), - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: Column( - children: [ - Row( - children: [ - // Botón de menú moderno que abre el modal - GestureDetector( - onTap: () => _showMobileNavigationModal(), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.white.withOpacity(0.3), - width: 1, - ), - ), - child: const Icon( - Icons.menu, - color: Colors.white, - size: 24, - ), - ), - ), - - const SizedBox(width: 16), - - // Logo - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset( - 'assets/images/favicon.png', - width: 24, - height: 24, - ), - ), - - const SizedBox(width: 12), - - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'NETHIVE', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - letterSpacing: 1.2, - ), - ), - Text( - currentMenuItem.title, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - - // Indicador de módulo actual - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - currentMenuItem.icon, - color: Colors.white, - size: 20, - ), - ), - ], - ), - - const SizedBox(height: 16), - - // Info del negocio mejorada - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: Colors.white.withOpacity(0.3), - width: 1, - ), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.green, - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.location_on, - color: Colors.white, - size: 16, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - negocio.nombre, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - Text( - negocio.tipoLocal, - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.green.withOpacity(0.8), - borderRadius: BorderRadius.circular(10), - ), - child: const Text( - 'Activo', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ], - ), - ); - } - - // Método para mostrar el modal de navegación móvil - void _showMobileNavigationModal() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - barrierColor: Colors.black.withOpacity(0.5), - builder: (context) => DraggableScrollableSheet( - initialChildSize: 0.85, - maxChildSize: 0.95, - minChildSize: 0.3, - builder: (context, scrollController) => Container( - decoration: BoxDecoration( - color: AppTheme.of(context).primaryBackground, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(25), - topRight: Radius.circular(25), - ), - ), - child: const MobileNavigationModal(), - ), - ), - ); - } - - Widget _buildMainContent(NavigationProvider navigationProvider) { - switch (navigationProvider.selectedMenuIndex) { - case 0: - return const DashboardPage(); - case 1: - return const InventarioPage(); - case 2: - return const TopologiaPage(); - case 3: - return const AlertasPage(); - case 4: - return const ConfiguracionPage(); - default: - return const DashboardPage(); - } - } -} diff --git a/lib/pages/infrastructure/pages/alertas_page.dart b/lib/pages/infrastructure/pages/alertas_page.dart deleted file mode 100644 index 90efff3..0000000 --- a/lib/pages/infrastructure/pages/alertas_page.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class AlertasPage extends StatelessWidget { - const AlertasPage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(16), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.warning, - color: Colors.white, - size: 24, - ), - ), - const SizedBox(width: 16), - const Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Centro de Alertas', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'Monitoreo y gestión de alertas MDF/IDF', - style: TextStyle( - color: Colors.white70, - fontSize: 14, - ), - ), - ], - ), - ), - ], - ), - ), - - const SizedBox(height: 24), - - // Contenido próximamente - Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [Colors.orange, Colors.red], - ), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.warning, - size: 60, - color: Colors.white, - ), - ), - const SizedBox(height: 20), - Text( - 'Centro de Alertas', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - Text( - 'Sistema de monitoreo y alertas para infraestructura\nPróximamente disponible', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 16, - ), - ), - ], - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/infrastructure/pages/configuracion_page.dart b/lib/pages/infrastructure/pages/configuracion_page.dart deleted file mode 100644 index 0c30524..0000000 --- a/lib/pages/infrastructure/pages/configuracion_page.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class ConfiguracionPage extends StatelessWidget { - const ConfiguracionPage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(16), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.settings, - color: Colors.white, - size: 24, - ), - ), - const SizedBox(width: 16), - const Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Configuración', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'Configuración de sistema y infraestructura', - style: TextStyle( - color: Colors.white70, - fontSize: 14, - ), - ), - ], - ), - ), - ], - ), - ), - - const SizedBox(height: 24), - - // Contenido próximamente - Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [Colors.purple, Colors.blue], - ), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.settings, - size: 60, - color: Colors.white, - ), - ), - const SizedBox(height: 20), - Text( - 'Configuración del Sistema', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - Text( - 'Panel de configuración para infraestructura MDF/IDF\nPróximamente disponible', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 16, - ), - ), - ], - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/infrastructure/pages/dashboard_page.dart b/lib/pages/infrastructure/pages/dashboard_page.dart deleted file mode 100644 index bcc2ff5..0000000 --- a/lib/pages/infrastructure/pages/dashboard_page.dart +++ /dev/null @@ -1,828 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:nethive_neo/providers/nethive/navigation_provider.dart'; -import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class DashboardPage extends StatefulWidget { - const DashboardPage({Key? key}) : super(key: key); - - @override - State createState() => _DashboardPageState(); -} - -class _DashboardPageState extends State - with TickerProviderStateMixin { - late AnimationController _animationController; - late Animation _fadeAnimation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - duration: const Duration(milliseconds: 800), - vsync: this, - ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); - _animationController.forward(); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isLargeScreen = MediaQuery.of(context).size.width > 1200; - final isMediumScreen = MediaQuery.of(context).size.width > 800; - final isSmallScreen = MediaQuery.of(context).size.width <= 600; - - return FadeTransition( - opacity: _fadeAnimation, - child: Consumer2( - builder: (context, navigationProvider, componentesProvider, child) { - return Container( - padding: EdgeInsets.all(isSmallScreen ? 12 : 24), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Título de la página - _buildPageTitle(isSmallScreen), - - SizedBox(height: isSmallScreen ? 16 : 24), - - // Cards de estadísticas principales - _buildStatsCards(componentesProvider, isLargeScreen, - isMediumScreen, isSmallScreen), - - SizedBox(height: isSmallScreen ? 16 : 24), - - // Gráficos y métricas - _buildContentSection(componentesProvider, isLargeScreen, - isMediumScreen, isSmallScreen), - - SizedBox(height: isSmallScreen ? 16 : 24), - - // Actividad reciente - _buildActivityFeed( - isLargeScreen, isMediumScreen, isSmallScreen), - ], - ), - ), - ); - }, - ), - ); - } - - Widget _buildPageTitle(bool isSmallScreen) { - return Container( - padding: EdgeInsets.all(isSmallScreen ? 16 : 20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: Row( - children: [ - Container( - padding: EdgeInsets.all(isSmallScreen ? 8 : 12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - Icons.dashboard, - color: Colors.white, - size: isSmallScreen ? 20 : 24, - ), - ), - SizedBox(width: isSmallScreen ? 12 : 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Dashboard MDF/IDF', - style: TextStyle( - color: Colors.white, - fontSize: isSmallScreen ? 18 : 24, - fontWeight: FontWeight.bold, - ), - ), - if (!isSmallScreen) ...[ - Text( - 'Panel de control de infraestructura de red', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - ), - ), - ], - ], - ), - ), - ], - ), - ); - } - - Widget _buildStatsCards(ComponentesProvider componentesProvider, - bool isLargeScreen, bool isMediumScreen, bool isSmallScreen) { - final stats = [ - { - 'title': 'Componentes Totales', - 'value': '${componentesProvider.componentes.length}', - 'icon': Icons.inventory_2, - 'color': Colors.blue, - 'subtitle': 'equipos registrados', - }, - { - 'title': 'Componentes Activos', - 'value': - '${componentesProvider.componentes.where((c) => c.activo).length}', - 'icon': Icons.power, - 'color': Colors.green, - 'subtitle': 'en funcionamiento', - }, - { - 'title': 'En Uso', - 'value': - '${componentesProvider.componentes.where((c) => c.enUso).length}', - 'icon': Icons.trending_up, - 'color': Colors.orange, - 'subtitle': 'siendo utilizados', - }, - { - 'title': 'Categorías', - 'value': '${componentesProvider.categorias.length}', - 'icon': Icons.category, - 'color': Colors.purple, - 'subtitle': 'tipos de equipos', - }, - ]; - - if (isSmallScreen) { - // En móvil: 2x2 grid - return GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 1.1, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - ), - itemCount: stats.length, - itemBuilder: (context, index) { - final stat = stats[index]; - return _buildStatCard( - stat['title'] as String, - stat['value'] as String, - stat['icon'] as IconData, - stat['color'] as Color, - stat['subtitle'] as String, - isSmallScreen, - ); - }, - ); - } else { - // En desktop/tablet: row horizontal - return Row( - children: stats.map((stat) { - final isLast = stat == stats.last; - return Expanded( - child: Row( - children: [ - Expanded( - child: _buildStatCard( - stat['title'] as String, - stat['value'] as String, - stat['icon'] as IconData, - stat['color'] as Color, - stat['subtitle'] as String, - isSmallScreen, - ), - ), - if (!isLast) const SizedBox(width: 16), - ], - ), - ); - }).toList(), - ); - } - } - - Widget _buildStatCard( - String title, - String value, - IconData icon, - Color color, - String subtitle, - bool isSmallScreen, - ) { - return TweenAnimationBuilder( - duration: const Duration(milliseconds: 800), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, animationValue, child) { - return Transform.scale( - scale: 0.8 + (0.2 * animationValue), - child: Container( - padding: EdgeInsets.all(isSmallScreen ? 12 : 20), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: color.withOpacity(0.3), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: color.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Container( - padding: EdgeInsets.all(isSmallScreen ? 6 : 8), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon(icon, - color: color, size: isSmallScreen ? 16 : 20), - ), - const Spacer(), - if (!isSmallScreen) ...[ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - 'MDF/IDF', - style: TextStyle( - color: color, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ], - ), - SizedBox(height: isSmallScreen ? 8 : 16), - TweenAnimationBuilder( - duration: Duration( - milliseconds: 1000 + (animationValue * 500).round()), - tween: IntTween(begin: 0, end: int.tryParse(value) ?? 0), - builder: (context, animatedValue, child) { - return Text( - animatedValue.toString(), - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: isSmallScreen ? 20 : 28, - fontWeight: FontWeight.bold, - ), - ); - }, - ), - const SizedBox(height: 4), - Text( - title, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: isSmallScreen ? 12 : 14, - fontWeight: FontWeight.w600, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - if (!isSmallScreen) ...[ - Text( - subtitle, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - ), - ], - ], - ), - ), - ); - }, - ); - } - - Widget _buildContentSection(ComponentesProvider componentesProvider, - bool isLargeScreen, bool isMediumScreen, bool isSmallScreen) { - if (isSmallScreen) { - // En móvil: columna vertical - return Column( - children: [ - _buildComponentsOverview(componentesProvider, isSmallScreen), - const SizedBox(height: 16), - _buildAlertasRecientes(isSmallScreen), - ], - ); - } else { - // En desktop/tablet: row horizontal - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 2, - child: _buildComponentsOverview(componentesProvider, isSmallScreen), - ), - const SizedBox(width: 24), - Expanded( - child: _buildAlertasRecientes(isSmallScreen), - ), - ], - ); - } - } - - Widget _buildComponentsOverview( - ComponentesProvider componentesProvider, bool isSmallScreen) { - return Container( - padding: EdgeInsets.all(isSmallScreen ? 16 : 20), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.pie_chart, - color: AppTheme.of(context).primaryColor, - size: isSmallScreen ? 18 : 20, - ), - SizedBox(width: isSmallScreen ? 6 : 8), - Expanded( - child: Text( - isSmallScreen - ? 'Componentes por Categoría' - : 'Distribución de Componentes por Categoría', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: isSmallScreen ? 14 : 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - SizedBox(height: isSmallScreen ? 16 : 20), - ...componentesProvider.categorias - .take(isSmallScreen ? 4 : 5) - .map((categoria) { - final componentesDeCategoria = componentesProvider.componentes - .where((c) => c.categoriaId == categoria.id) - .length; - final porcentaje = componentesProvider.componentes.isNotEmpty - ? (componentesDeCategoria / - componentesProvider.componentes.length * - 100) - : 0.0; - - return Container( - margin: EdgeInsets.only(bottom: isSmallScreen ? 8 : 12), - child: Column( - children: [ - Row( - children: [ - Expanded( - flex: isSmallScreen ? 2 : 3, - child: Text( - categoria.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: isSmallScreen ? 12 : 14, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - SizedBox(width: isSmallScreen ? 6 : 8), - Expanded( - flex: isSmallScreen ? 3 : 4, - child: Container( - height: isSmallScreen ? 6 : 8, - decoration: BoxDecoration( - color: AppTheme.of(context).tertiaryBackground, - borderRadius: BorderRadius.circular(4), - ), - child: FractionallySizedBox( - alignment: Alignment.centerLeft, - widthFactor: porcentaje / 100, - child: Container( - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(4), - ), - ), - ), - ), - ), - SizedBox(width: isSmallScreen ? 6 : 8), - SizedBox( - width: isSmallScreen ? 40 : 60, - child: Text( - isSmallScreen - ? '$componentesDeCategoria' - : '$componentesDeCategoria (${porcentaje.toStringAsFixed(1)}%)', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: isSmallScreen ? 10 : 12, - ), - textAlign: TextAlign.end, - ), - ), - ], - ), - ], - ), - ); - }).toList(), - ], - ), - ); - } - - Widget _buildAlertasRecientes(bool isSmallScreen) { - final alertas = [ - { - 'tipo': 'Warning', - 'mensaje': 'Switch en Rack 3 sobrecalentándose', - 'tiempo': '5 min' - }, - { - 'tipo': 'Error', - 'mensaje': 'Pérdida de conectividad en Panel A4', - 'tiempo': '12 min' - }, - { - 'tipo': 'Info', - 'mensaje': 'Mantenimiento programado completado', - 'tiempo': '1 hr' - }, - if (!isSmallScreen) - { - 'tipo': 'Warning', - 'mensaje': 'Capacidad de cable al 85%', - 'tiempo': '2 hrs' - }, - ]; - - return Container( - padding: EdgeInsets.all(isSmallScreen ? 16 : 20), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.warning, - color: Colors.orange, - size: isSmallScreen ? 18 : 20, - ), - SizedBox(width: isSmallScreen ? 6 : 8), - Text( - 'Alertas Recientes', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: isSmallScreen ? 14 : 16, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - SizedBox(height: isSmallScreen ? 12 : 16), - ...alertas.map((alerta) { - Color alertColor; - IconData alertIcon; - - switch (alerta['tipo']) { - case 'Error': - alertColor = Colors.red; - alertIcon = Icons.error; - break; - case 'Warning': - alertColor = Colors.orange; - alertIcon = Icons.warning; - break; - default: - alertColor = Colors.blue; - alertIcon = Icons.info; - } - - return Container( - margin: EdgeInsets.only(bottom: isSmallScreen ? 8 : 12), - padding: EdgeInsets.all(isSmallScreen ? 8 : 12), - decoration: BoxDecoration( - color: alertColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: alertColor.withOpacity(0.3), - ), - ), - child: Row( - children: [ - Icon(alertIcon, - color: alertColor, size: isSmallScreen ? 14 : 16), - SizedBox(width: isSmallScreen ? 6 : 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - alerta['mensaje']!, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: isSmallScreen ? 11 : 12, - fontWeight: FontWeight.w500, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - Text( - 'hace ${alerta['tiempo']}', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: isSmallScreen ? 9 : 10, - ), - ), - ], - ), - ), - ], - ), - ); - }).toList(), - ], - ), - ); - } - - Widget _buildActivityFeed( - bool isLargeScreen, bool isMediumScreen, bool isSmallScreen) { - return Container( - padding: EdgeInsets.all(isSmallScreen ? 16 : 20), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.timeline, - color: AppTheme.of(context).primaryColor, - size: isSmallScreen ? 18 : 20, - ), - SizedBox(width: isSmallScreen ? 6 : 8), - Text( - 'Actividad Reciente', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: isSmallScreen ? 14 : 16, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - SizedBox(height: isSmallScreen ? 12 : 16), - _buildActivityItems(isSmallScreen), - ], - ), - ); - } - - Widget _buildActivityItems(bool isSmallScreen) { - final activities = [ - { - 'title': 'Nuevo componente añadido', - 'description': 'Switch Cisco SG300-28 registrado en Rack 5', - 'time': '10:30 AM', - 'icon': Icons.add_circle, - 'color': Colors.green, - }, - { - 'title': 'Mantenimiento completado', - 'description': 'Revisión de cables en Panel Principal', - 'time': '09:15 AM', - 'icon': Icons.build_circle, - 'color': Colors.blue, - }, - { - 'title': 'Configuración actualizada', - 'description': 'Parámetros de red modificados', - 'time': '08:45 AM', - 'icon': Icons.settings, - 'color': Colors.purple, - }, - ]; - - if (isSmallScreen) { - // En móvil: lista vertical - return Column( - children: activities.map((activity) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - child: _buildActivityItem( - activity['title'] as String, - activity['description'] as String, - activity['time'] as String, - activity['icon'] as IconData, - activity['color'] as Color, - isSmallScreen, - ), - ); - }).toList(), - ); - } else { - // En desktop/tablet: fila horizontal - return Row( - children: activities.map((activity) { - final isLast = activity == activities.last; - return Expanded( - child: Row( - children: [ - Expanded( - child: _buildActivityItem( - activity['title'] as String, - activity['description'] as String, - activity['time'] as String, - activity['icon'] as IconData, - activity['color'] as Color, - isSmallScreen, - ), - ), - if (!isLast) const SizedBox(width: 16), - ], - ), - ); - }).toList(), - ); - } - } - - Widget _buildActivityItem( - String title, - String description, - String time, - IconData icon, - Color color, - bool isSmallScreen, - ) { - return Container( - padding: EdgeInsets.all(isSmallScreen ? 12 : 16), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: color.withOpacity(0.3), - ), - ), - child: isSmallScreen - ? Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: color.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), - child: Icon(icon, color: color, size: 16), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - title, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - Text( - time, - style: TextStyle( - color: color, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - description, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 11, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(icon, color: color, size: 16), - const SizedBox(width: 8), - Text( - time, - style: TextStyle( - color: color, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - title, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 4), - Text( - description, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ); - } -} diff --git a/lib/pages/infrastructure/pages/inventario_page.dart b/lib/pages/infrastructure/pages/inventario_page.dart deleted file mode 100644 index 775076f..0000000 --- a/lib/pages/infrastructure/pages/inventario_page.dart +++ /dev/null @@ -1,1853 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:pluto_grid/pluto_grid.dart'; -import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; -import 'package:nethive_neo/pages/infrastructure/widgets/componentes_cards_view.dart'; -import 'package:nethive_neo/pages/infrastructure/widgets/edit_componente_dialog.dart'; -import 'package:nethive_neo/pages/infrastructure/widgets/add_componente_dialog.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class InventarioPage extends StatefulWidget { - const InventarioPage({Key? key}) : super(key: key); - - @override - State createState() => _InventarioPageState(); -} - -class _InventarioPageState extends State - with TickerProviderStateMixin { - late AnimationController _animationController; - late Animation _fadeAnimation; - - // GlobalKey para manejar el overlay de manera segura - final GlobalKey _overlayKey = GlobalKey(); - OverlayEntry? _loadingOverlay; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - duration: const Duration(milliseconds: 500), - vsync: this, - ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); - _animationController.forward(); - } - - @override - void dispose() { - // Limpiar el overlay si existe antes de dispose - _removeLoadingOverlay(); - _animationController.dispose(); - super.dispose(); - } - - // Método para mostrar overlay de loading de manera segura - void _showLoadingOverlay(String message) { - _removeLoadingOverlay(); // Remover cualquier overlay existente - - if (mounted) { - _loadingOverlay = OverlayEntry( - builder: (context) => Material( - color: Colors.black.withOpacity(0.7), - child: Center( - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 16), - Text( - message, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ), - ), - ); - - Overlay.of(context).insert(_loadingOverlay!); - } - } - - // Método para remover overlay de manera segura - void _removeLoadingOverlay() { - if (_loadingOverlay != null) { - try { - _loadingOverlay!.remove(); - } catch (e) { - // Ignorar errores si el overlay ya fue removido - } - _loadingOverlay = null; - } - } - - @override - Widget build(BuildContext context) { - final isLargeScreen = MediaQuery.of(context).size.width > 1200; - final isMobileScreen = MediaQuery.of(context).size.width <= 800; - - return FadeTransition( - opacity: _fadeAnimation, - child: Consumer( - builder: (context, componentesProvider, child) { - // Vista móvil con tarjetas - if (isMobileScreen) { - return const ComponentesCardsView(); - } - - // Vista de escritorio con tabla - return Container( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header de inventario - _buildInventoryHeader(componentesProvider), - - const SizedBox(height: 24), - - // Estadísticas rápidas (solo en escritorio) - if (isLargeScreen) ...[ - _buildQuickStats(componentesProvider), - const SizedBox(height: 24), - ], - - // Tabla de componentes (escritorio/tablet) - Expanded( - child: _buildComponentsTable(componentesProvider), - ), - ], - ), - ); - }, - ), - ); - } - - Widget _buildInventoryHeader(ComponentesProvider componentesProvider) { - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.inventory_2, - color: Colors.white, - size: 24, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Inventario MDF/IDF', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'Gestión de componentes de infraestructura de red', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - ), - ), - ], - ), - ), - // Botón para añadir componente - ACTUALIZADO - Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: TextButton.icon( - onPressed: () { - // Verificar que tengamos un negocio seleccionado - if (componentesProvider.negocioSeleccionadoId == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Debe seleccionar un negocio antes de añadir componentes'), - backgroundColor: Colors.orange, - ), - ); - return; - } - - // Abrir el diálogo para añadir componente - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AddComponenteDialog( - provider: componentesProvider, - ), - ); - }, - icon: const Icon(Icons.add, color: Colors.white), - label: const Text( - 'Añadir Componente', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildQuickStats(ComponentesProvider componentesProvider) { - return Row( - children: [ - Expanded( - child: _buildStatCard( - 'Total Componentes', - componentesProvider.componentes.length, - Icons.devices, - Colors.blue, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildStatCard( - 'Activos', - componentesProvider.componentes.where((c) => c.activo).length, - Icons.check_circle, - Colors.green, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildStatCard( - 'En Uso', - componentesProvider.componentes.where((c) => c.enUso).length, - Icons.trending_up, - Colors.orange, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildStatCard( - 'Categorías', - componentesProvider.categorias.length, - Icons.category, - Colors.purple, - ), - ), - ], - ); - } - - Widget _buildStatCard(String title, int value, IconData icon, Color color) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: color.withOpacity(0.3), - ), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon(icon, color: color, size: 20), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - value.toString(), - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - Text( - title, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildComponentsTable(ComponentesProvider componentesProvider) { - return Container( - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - children: [ - // Header de la tabla - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).modernGradient, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.table_chart, - color: Colors.white, - size: 20, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Componentes de Red', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'Inventario completo de infraestructura MDF/IDF', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 12, - ), - ), - ], - ), - ), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - '${componentesProvider.componentesRows.length} registros', - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - - // Tabla de componentes con PlutoGrid - Expanded( - child: componentesProvider.componentesRows.isEmpty - ? _buildEmptyState() - : PlutoGrid( - key: UniqueKey(), - configuration: PlutoGridConfiguration( - enableMoveDownAfterSelecting: true, - enableMoveHorizontalInEditing: true, - localeText: const PlutoGridLocaleText.spanish(), - scrollbar: PlutoGridScrollbarConfig( - draggableScrollbar: true, - isAlwaysShown: false, - onlyDraggingThumb: true, - enableScrollAfterDragEnd: true, - scrollbarThickness: 12, - scrollbarThicknessWhileDragging: 16, - hoverWidth: 20, - scrollBarColor: - AppTheme.of(context).primaryColor.withOpacity(0.7), - scrollBarTrackColor: Colors.grey.withOpacity(0.2), - scrollbarRadius: const Radius.circular(8), - scrollbarRadiusWhileDragging: const Radius.circular(10), - ), - style: PlutoGridStyleConfig( - enableRowColorAnimation: true, - gridBorderColor: - AppTheme.of(context).primaryColor.withOpacity(0.5), - disabledIconColor: - AppTheme.of(context).alternate.withOpacity(0.3), - iconColor: - AppTheme.of(context).alternate.withOpacity(0.3), - activatedBorderColor: AppTheme.of(context).primaryColor, - inactivatedBorderColor: Colors.grey.withOpacity(0.3), - gridBackgroundColor: - AppTheme.of(context).primaryBackground, - rowColor: AppTheme.of(context).secondaryBackground, - activatedColor: - AppTheme.of(context).primaryColor.withOpacity(0.1), - checkedColor: - AppTheme.of(context).primaryColor.withOpacity(0.2), - cellTextStyle: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 13, - ), - columnTextStyle: TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.bold, - ), - menuBackgroundColor: - AppTheme.of(context).secondaryBackground, - gridBorderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - rowHeight: 70, - ), - columnFilter: const PlutoGridColumnFilterConfig( - filters: [ - ...FilterHelper.defaultFilters, - ], - ), - ), - columns: [ - PlutoColumn( - title: 'ID', - field: 'id', - width: 200, - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Text( - rendererContext.cell.value - .toString() - .substring(0, 8) + - '...', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 11, - fontWeight: FontWeight.w500, - ), - ); - }, - ), - PlutoColumn( - title: 'Componente', - field: 'nombre', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.left, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Container( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - // Imagen del componente - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.1), - borderRadius: BorderRadius.circular(6), - ), - child: rendererContext.row.cells['imagen_url'] - ?.value != - null && - rendererContext - .row.cells['imagen_url']!.value - .toString() - .isNotEmpty - ? ClipRRect( - borderRadius: - BorderRadius.circular(6), - child: Image.network( - rendererContext - .row.cells['imagen_url']!.value - .toString(), - fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) { - return Icon( - Icons.devices, - color: AppTheme.of(context) - .primaryColor, - size: 16, - ); - }, - ), - ) - : Icon( - Icons.devices, - color: - AppTheme.of(context).primaryColor, - size: 16, - ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - rendererContext.cell.value.toString(), - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 13, - fontWeight: FontWeight.w600, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - }, - ), - PlutoColumn( - title: 'Categoría', - field: 'categoria_nombre', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Container( - padding: const EdgeInsets.all(8), - child: Center( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.3), - ), - ), - child: Text( - rendererContext.cell.value.toString(), - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 11, - fontWeight: FontWeight.w600, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ); - }, - ), - PlutoColumn( - title: 'Estado', - field: 'activo', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - final isActivo = - rendererContext.cell.value.toString() == 'Sí'; - return Container( - padding: const EdgeInsets.all(8), - child: Center( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: (isActivo ? Colors.green : Colors.red) - .withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - isActivo - ? Icons.check_circle - : Icons.cancel, - color: - isActivo ? Colors.green : Colors.red, - size: 12, - ), - const SizedBox(width: 4), - Text( - isActivo ? 'Activo' : 'Inactivo', - style: TextStyle( - color: isActivo - ? Colors.green - : Colors.red, - fontSize: 11, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ), - ); - }, - ), - PlutoColumn( - title: 'En Uso', - field: 'en_uso', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - final enUso = - rendererContext.cell.value.toString() == 'Sí'; - return Container( - padding: const EdgeInsets.all(8), - child: Center( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: (enUso ? Colors.orange : Colors.grey) - .withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - enUso ? 'En Uso' : 'Libre', - style: TextStyle( - color: enUso ? Colors.orange : Colors.grey, - fontSize: 11, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ); - }, - ), - PlutoColumn( - title: 'Ubicación', - field: 'ubicacion', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.left, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Container( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Icon( - Icons.location_on, - color: AppTheme.of(context).primaryColor, - size: 14, - ), - const SizedBox(width: 4), - Expanded( - child: Text( - rendererContext.cell.value - .toString() - .isEmpty - ? 'Sin ubicación' - : rendererContext.cell.value.toString(), - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 12, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - }, - ), - PlutoColumn( - title: 'Descripción', - field: 'descripcion', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.left, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Container( - padding: const EdgeInsets.all(8), - child: Text( - rendererContext.cell.value.toString().isEmpty - ? 'Sin descripción' - : rendererContext.cell.value.toString(), - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ); - }, - ), - PlutoColumn( - title: 'Fecha Registro', - field: 'fecha_registro', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Container( - padding: const EdgeInsets.all(8), - child: Text( - rendererContext.cell.value.toString(), - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 11, - ), - ), - ); - }, - ), - PlutoColumn( - title: 'Acciones', - field: 'editar', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - final componenteId = rendererContext - .row.cells['id']?.value - .toString() ?? - ''; - final componente = componentesProvider - .getComponenteById(componenteId); - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Botón ver detalles - Tooltip( - message: 'Ver detalles', - child: InkWell( - onTap: () => _showComponentDetails( - componente, componentesProvider), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.blue, - borderRadius: BorderRadius.circular(4), - ), - child: const Icon( - Icons.visibility, - color: Colors.white, - size: 14, - ), - ), - ), - ), - const SizedBox(width: 4), - // Botón editar - Tooltip( - message: 'Editar', - child: InkWell( - onTap: () => _editComponent( - componente, componentesProvider), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(4), - ), - child: const Icon( - Icons.edit, - color: Colors.white, - size: 14, - ), - ), - ), - ), - const SizedBox(width: 4), - // Botón eliminar - Tooltip( - message: 'Eliminar', - child: InkWell( - onTap: () => _deleteComponent( - componente, componentesProvider), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(4), - ), - child: const Icon( - Icons.delete, - color: Colors.white, - size: 14, - ), - ), - ), - ), - ], - ); - }, - ), - ], - rows: componentesProvider.componentesRows, - onLoaded: (event) { - componentesProvider.componentesStateManager = - event.stateManager; - }, - createFooter: (stateManager) { - stateManager.setPageSize(10, notify: false); - return PlutoPagination(stateManager); - }, - ), - ), - ], - ), - ); - } - - Widget _buildEmptyState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: Icon( - Icons.inventory_2, - color: AppTheme.of(context).primaryColor, - size: 48, - ), - ), - const SizedBox(height: 16), - Text( - 'No hay componentes registrados', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - 'Añade el primer componente para comenzar', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - ), - ), - ], - ), - ); - } - - // Métodos para manejar las acciones de los botones - void _showComponentDetails(dynamic componente, ComponentesProvider provider) { - if (componente == null) return; - - // Detectar el tamaño de pantalla - final screenSize = MediaQuery.of(context).size; - final isDesktop = screenSize.width > 1024; - final isMobile = screenSize.width <= 768; - - // Obtener la URL de la imagen del componente - final imagenUrl = provider.componentesRows - .where((row) => row.cells['id']?.value == componente.id) - .firstOrNull - ?.cells['imagen_url'] - ?.value - ?.toString(); - - final categoria = provider.getCategoriaById(componente.categoriaId); - - showDialog( - context: context, - barrierDismissible: true, - builder: (context) => Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.all(isDesktop ? 40 : 20), - child: Container( - width: isDesktop ? 900 : (isMobile ? screenSize.width * 0.95 : 700), - height: isDesktop ? 650 : (isMobile ? screenSize.height * 0.8 : 600), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 30, - offset: const Offset(0, 15), - spreadRadius: 5, - ), - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - blurRadius: 40, - offset: const Offset(0, 10), - spreadRadius: 2, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryBackground, - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - stops: const [0.0, 0.6, 1.0], - ), - ), - child: isDesktop - ? _buildDesktopDetailLayout( - componente, provider, categoria, imagenUrl) - : _buildMobileDetailLayout( - componente, provider, categoria, imagenUrl), - ), - ), - ), - ), - ); - } - - Widget _buildDesktopDetailLayout( - dynamic componente, - ComponentesProvider provider, - dynamic categoria, - String? imagenUrl, - ) { - return Row( - children: [ - // Panel izquierdo con imagen espectacular - Container( - width: 350, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.4), - blurRadius: 20, - offset: const Offset(5, 0), - ), - ], - ), - child: Padding( - padding: const EdgeInsets.all(30), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Imagen principal del componente - MÁS GRANDE - Container( - width: 200, - height: 200, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: RadialGradient( - colors: [ - Colors.white.withOpacity(0.3), - Colors.white.withOpacity(0.1), - Colors.transparent, - ], - ), - border: Border.all( - color: Colors.white.withOpacity(0.4), - width: 3, - ), - boxShadow: [ - BoxShadow( - color: Colors.white.withOpacity(0.3), - blurRadius: 30, - spreadRadius: 10, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(17), - child: imagenUrl != null && imagenUrl.isNotEmpty - ? Image.network( - imagenUrl, - fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(17), - ), - child: Center( - child: CircularProgressIndicator( - color: Colors.white, - value: loadingProgress.expectedTotalBytes != - null - ? loadingProgress - .cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), - ), - ); - }, - errorBuilder: (context, error, stackTrace) { - return Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(17), - ), - child: const Icon( - Icons.devices, - color: Colors.white, - size: 80, - ), - ); - }, - ) - : Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(17), - ), - child: const Icon( - Icons.devices, - color: Colors.white, - size: 80, - ), - ), - ), - ), - - const SizedBox(height: 24), - - // Título del componente - Text( - componente.nombre, - style: const TextStyle( - color: Colors.white, - fontSize: 22, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - - const SizedBox(height: 12), - - // Categoría con estilo - if (categoria != null) - Container( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Colors.white.withOpacity(0.3), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.category, - color: Colors.white, - size: 16, - ), - const SizedBox(width: 8), - Text( - categoria.nombre, - style: TextStyle( - color: Colors.white.withOpacity(0.95), - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - - const SizedBox(height: 20), - - // Estados con iconos - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildStatusIndicator( - componente.activo ? 'Activo' : 'Inactivo', - componente.activo ? Icons.check_circle : Icons.cancel, - componente.activo ? Colors.green : Colors.red, - ), - _buildStatusIndicator( - componente.enUso ? 'En Uso' : 'Libre', - componente.enUso - ? Icons.trending_up - : Icons.trending_flat, - componente.enUso ? Colors.orange : Colors.grey, - ), - ], - ), - - const SizedBox(height: 20), - - // ID con estilo - Container( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - 'ID: ${componente.id.substring(0, 8)}...', - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 12, - fontFamily: 'monospace', - ), - ), - ), - ], - ), - ), - ), - - // Panel derecho con detalles - Expanded( - child: Padding( - padding: const EdgeInsets.all(30), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header del panel de detalles - Row( - children: [ - Icon( - Icons.info_outline, - color: AppTheme.of(context).primaryColor, - size: 24, - ), - const SizedBox(width: 12), - Text( - 'Detalles del Componente', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: Icon( - Icons.close, - color: AppTheme.of(context).secondaryText, - ), - style: IconButton.styleFrom( - backgroundColor: - AppTheme.of(context).secondaryBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ], - ), - - const SizedBox(height: 24), - - // Información detallada - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - if (componente.ubicacion != null && - componente.ubicacion!.isNotEmpty) - _buildEnhancedDetailCard( - 'Ubicación', - componente.ubicacion!, - Icons.location_on, - Colors.blue, - ), - if (componente.descripcion != null && - componente.descripcion!.isNotEmpty) - _buildEnhancedDetailCard( - 'Descripción', - componente.descripcion!, - Icons.description, - Colors.purple, - ), - _buildEnhancedDetailCard( - 'Fecha de Registro', - componente.fechaRegistro?.toString().split(' ')[0] ?? - 'No disponible', - Icons.calendar_today, - Colors.green, - ), - _buildEnhancedDetailCard( - 'Estado Operativo', - componente.activo - ? 'Componente activo y operativo' - : 'Componente inactivo', - componente.activo - ? Icons.power_settings_new - : Icons.power_off, - componente.activo ? Colors.green : Colors.red, - ), - _buildEnhancedDetailCard( - 'Estado de Uso', - componente.enUso - ? 'Componente en uso actual' - : 'Componente disponible para uso', - componente.enUso ? Icons.work : Icons.work_off, - componente.enUso ? Colors.orange : Colors.grey, - ), - ], - ), - ), - ), - - // Botones de acción - const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: () { - Navigator.of(context).pop(); - _editComponent(componente, provider); - }, - icon: const Icon(Icons.edit, size: 18), - label: const Text('Editar'), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.of(context).primaryColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: OutlinedButton.icon( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.close, size: 18), - label: const Text('Cerrar'), - style: OutlinedButton.styleFrom( - foregroundColor: AppTheme.of(context).secondaryText, - side: BorderSide( - color: AppTheme.of(context) - .secondaryText - .withOpacity(0.5), - ), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ], - ); - } - - Widget _buildMobileDetailLayout( - dynamic componente, - ComponentesProvider provider, - dynamic categoria, - String? imagenUrl, - ) { - return Column( - children: [ - // Header con imagen para móvil - Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Detalles', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.close, color: Colors.white), - ), - ], - ), - const SizedBox(height: 16), - // Imagen del componente en móvil - Container( - width: 120, - height: 120, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: Colors.white.withOpacity(0.3), width: 2), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(13), - child: imagenUrl != null && imagenUrl.isNotEmpty - ? Image.network( - imagenUrl, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - color: Colors.white.withOpacity(0.1), - child: const Icon( - Icons.devices, - color: Colors.white, - size: 50, - ), - ); - }, - ) - : Container( - color: Colors.white.withOpacity(0.1), - child: const Icon( - Icons.devices, - color: Colors.white, - size: 50, - ), - ), - ), - ), - const SizedBox(height: 12), - Text( - componente.nombre, - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - if (categoria != null) ...[ - const SizedBox(height: 8), - Text( - categoria.nombre, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - ), - ), - ], - ], - ), - ), - - // Contenido de detalles para móvil - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - Row( - children: [ - Expanded( - child: _buildStatusIndicator( - componente.activo ? 'Activo' : 'Inactivo', - componente.activo ? Icons.check_circle : Icons.cancel, - componente.activo ? Colors.green : Colors.red, - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatusIndicator( - componente.enUso ? 'En Uso' : 'Libre', - componente.enUso - ? Icons.trending_up - : Icons.trending_flat, - componente.enUso ? Colors.orange : Colors.grey, - ), - ), - ], - ), - const SizedBox(height: 16), - - if (componente.ubicacion != null && - componente.ubicacion!.isNotEmpty) - _buildEnhancedDetailCard( - 'Ubicación', - componente.ubicacion!, - Icons.location_on, - Colors.blue, - ), - - if (componente.descripcion != null && - componente.descripcion!.isNotEmpty) - _buildEnhancedDetailCard( - 'Descripción', - componente.descripcion!, - Icons.description, - Colors.purple, - ), - - _buildEnhancedDetailCard( - 'Fecha de Registro', - componente.fechaRegistro?.toString().split(' ')[0] ?? - 'No disponible', - Icons.calendar_today, - Colors.green, - ), - - _buildEnhancedDetailCard( - 'ID del Componente', - componente.id.substring(0, 8) + '...', - Icons.fingerprint, - Colors.indigo, - ), - - const SizedBox(height: 20), - - // Botones de acción para móvil - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: () { - Navigator.of(context).pop(); - _editComponent(componente, provider); - }, - icon: const Icon(Icons.edit, size: 18), - label: const Text('Editar'), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.of(context).primaryColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: OutlinedButton.icon( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.close, size: 18), - label: const Text('Cerrar'), - style: OutlinedButton.styleFrom( - foregroundColor: AppTheme.of(context).secondaryText, - side: BorderSide( - color: AppTheme.of(context) - .secondaryText - .withOpacity(0.5), - ), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ], - ); - } - - Widget _buildStatusIndicator(String text, IconData icon, Color color) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withOpacity(0.3)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, color: color, size: 16), - const SizedBox(width: 6), - Text( - text, - style: TextStyle( - color: color, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - } - - Widget _buildEnhancedDetailCard( - String title, String value, IconData icon, Color color) { - return Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - ), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: color.withOpacity(0.3), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: color.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon(icon, color: color, size: 20), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - color: color, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - value, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 13, - height: 1.4, - ), - ), - ], - ), - ), - ], - ), - ); - } - - void _editComponent(dynamic componente, ComponentesProvider provider) { - if (componente == null) return; - - showDialog( - context: context, - builder: (context) => EditComponenteDialog( - provider: provider, - componente: componente, - ), - ); - } - - void _deleteComponent(dynamic componente, ComponentesProvider provider) { - if (componente == null) return; - - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: AppTheme.of(context).primaryBackground, - title: Row( - children: [ - Icon( - Icons.warning, - color: Colors.red, - ), - const SizedBox(width: 8), - Text( - 'Eliminar Componente', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - content: Text( - '¿Estás seguro de que deseas eliminar "${componente.nombre}"?\n\nEsta acción no se puede deshacer.', - style: TextStyle( - color: AppTheme.of(context).primaryText, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text( - 'Cancelar', - style: TextStyle(color: AppTheme.of(context).secondaryText), - ), - ), - ElevatedButton( - onPressed: () async { - // Cerrar el diálogo de confirmación - Navigator.of(context).pop(); - - // Capturar el ScaffoldMessenger antes de la operación asíncrona - final scaffoldMessenger = ScaffoldMessenger.of(context); - - try { - // Mostrar loading de manera segura - _showLoadingOverlay('Eliminando componente...'); - - // Realizar la eliminación - final success = - await provider.eliminarComponente(componente.id); - - // Remover loading de manera segura - _removeLoadingOverlay(); - - // Verificar que el widget sigue montado antes de mostrar mensajes - if (!mounted) return; - - // Mostrar resultado usando el ScaffoldMessenger capturado - if (success) { - scaffoldMessenger.showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.check_circle, color: Colors.white), - SizedBox(width: 12), - Text( - 'Componente eliminado exitosamente', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.green, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - duration: const Duration(seconds: 3), - ), - ); - } else { - scaffoldMessenger.showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.error, color: Colors.white), - SizedBox(width: 12), - Text( - 'Error al eliminar el componente', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - duration: const Duration(seconds: 4), - ), - ); - } - } catch (e) { - // Asegurar que el overlay se remueva en caso de error - _removeLoadingOverlay(); - - // Verificar que el widget sigue montado antes de mostrar error - if (!mounted) return; - - // Mostrar error usando el ScaffoldMessenger capturado - scaffoldMessenger.showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(Icons.warning, color: Colors.white), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Error: $e', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - duration: const Duration(seconds: 4), - ), - ); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - child: const Text('Eliminar'), - ), - ], - ), - ); - } -} diff --git a/lib/pages/infrastructure/pages/topologia_page.dart b/lib/pages/infrastructure/pages/topologia_page.dart deleted file mode 100644 index d4003d4..0000000 --- a/lib/pages/infrastructure/pages/topologia_page.dart +++ /dev/null @@ -1,1292 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_flow_chart/flutter_flow_chart.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:nethive_neo/theme/theme.dart'; -import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; -import 'package:nethive_neo/models/nethive/topologia_completa_model.dart'; -import 'package:nethive_neo/pages/infrastructure/pages/widgets/topologia_page_widgets/rack_view_widget.dart'; -import 'package:nethive_neo/pages/infrastructure/pages/widgets/topologia_page_widgets/floor_plan_view_widget.dart'; - -class TopologiaPage extends StatefulWidget { - const TopologiaPage({Key? key}) : super(key: key); - - @override - State createState() => _TopologiaPageState(); -} - -class _TopologiaPageState extends State - with TickerProviderStateMixin { - late AnimationController _animationController; - late Animation _fadeAnimation; - - String _selectedView = 'network'; // network, rack, floor - bool _isLoading = false; - - // Dashboard para el FlowChart - late Dashboard dashboard; - - // Mapas para elementos y conexiones - Map elementosMap = {}; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - duration: const Duration(milliseconds: 800), - vsync: this, - ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); - - _animationController.forward(); - _initializeDashboard(); - - // Cargar datos después de que el widget esté construido - WidgetsBinding.instance.addPostFrameCallback((_) { - _loadTopologyData(); - }); - } - - void _initializeDashboard() { - dashboard = Dashboard( - blockDefaultZoomGestures: false, - minimumZoomFactor: 0.25, - ); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isMediumScreen = MediaQuery.of(context).size.width > 800; - - return FadeTransition( - opacity: _fadeAnimation, - child: Consumer( - builder: (context, componentesProvider, child) { - if (componentesProvider.isLoadingTopologia || _isLoading) { - return _buildLoadingView(); - } - - // Verificar si hay un negocio seleccionado - if (componentesProvider.negocioSeleccionadoId == null) { - return _buildNoBusinessSelectedView(); - } - - // Verificar si hay componentes - if (componentesProvider.componentesTopologia.isEmpty) { - return _buildNoComponentsView(); - } - - return Container( - padding: EdgeInsets.all(isMediumScreen ? 24 : 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header profesional - _buildProfessionalHeader(componentesProvider), - const SizedBox(height: 24), - - // Controles avanzados - if (isMediumScreen) ...[ - _buildAdvancedControls(componentesProvider), - const SizedBox(height: 24), - ], - - // Vista principal profesional - Expanded( - child: _buildProfessionalTopologyView( - isMediumScreen, componentesProvider), - ), - ], - ), - ); - }, - ), - ); - } - - Widget _buildLoadingView() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator( - strokeWidth: 3, - color: AppTheme.of(context).primaryColor, - ).animate().scale(duration: 600.ms).then(delay: 200.ms).fadeIn(), - const SizedBox(height: 24), - Text( - 'Cargando topología de red...', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ).animate().fadeIn(delay: 400.ms), - const SizedBox(height: 8), - Text( - 'Construyendo infraestructura desde la base de datos', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - ), - ).animate().fadeIn(delay: 600.ms), - ], - ), - ); - } - - Widget _buildNoBusinessSelectedView() { - return Center( - child: Container( - padding: const EdgeInsets.all(40), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.business, - size: 80, - color: Colors.white, - ), - const SizedBox(height: 20), - Text( - 'Selecciona un Negocio', - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - Text( - 'Debe seleccionar un negocio desde la gestión\nde empresas para visualizar su topología', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 16, - ), - ), - ], - ), - ), - ); - } - - Widget _buildNoComponentsView() { - return Center( - child: Container( - padding: const EdgeInsets.all(40), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.device_hub_outlined, - size: 80, - color: AppTheme.of(context).primaryColor, - ), - const SizedBox(height: 20), - Text( - 'Sin Componentes', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - Text( - 'Este negocio no tiene componentes\nregistrados en la infraestructura', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 16, - ), - ), - ], - ), - ), - ); - } - - Widget _buildProfessionalHeader(ComponentesProvider provider) { - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.account_tree, - color: Colors.white, - size: 24, - ), - ).animate().scale(duration: 600.ms), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Topología de Red - ${provider.negocioSeleccionadoNombre ?? "Negocio"}', - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ).animate().fadeIn(delay: 300.ms), - Text( - '${provider.componentesTopologia.length} componentes • ${provider.conexionesDatos.length} conexiones de datos', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - ), - ).animate().fadeIn(delay: 500.ms), - ], - ), - ), - if (provider.problemasTopologia.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.orange.withOpacity(0.3), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.warning, color: Colors.white, size: 16), - const SizedBox(width: 8), - Text( - '${provider.problemasTopologia.length} alertas', - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ).animate().fadeIn(delay: 700.ms), - ], - ), - ).animate().fadeIn().slideY(begin: -0.3, end: 0); - } - - Widget _buildAdvancedControls(ComponentesProvider provider) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - ), - ), - child: Row( - children: [ - // Selector de vista - Expanded( - child: Row( - children: [ - Text( - 'Vista:', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: 12), - _buildViewButton('network', 'Diagrama Interactivo', Icons.hub), - const SizedBox(width: 8), - _buildViewButton('rack', 'Vista Rack', Icons.dns), - const SizedBox(width: 8), - _buildViewButton('floor', 'Plano de Planta', Icons.map), - ], - ), - ), - - // Información de componentes - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Resumen:', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 4), - Row( - children: [ - _buildStatChip('MDF', provider.getComponentesMDF().length, - Colors.blue), - const SizedBox(width: 8), - _buildStatChip('IDF', provider.getComponentesIDF().length, - Colors.green), - const SizedBox(width: 8), - _buildStatChip('Switch', - provider.getComponentesSwitch().length, Colors.purple), - ], - ), - ], - ), - ), - - const SizedBox(width: 16), - - // Controles de la topología - Row( - children: [ - IconButton( - onPressed: () { - _refreshTopology(provider); - }, - icon: const Icon(Icons.refresh), - tooltip: 'Actualizar topología', - style: IconButton.styleFrom( - backgroundColor: - AppTheme.of(context).primaryColor.withOpacity(0.1), - ), - ), - const SizedBox(width: 8), - IconButton( - onPressed: () { - dashboard.setZoomFactor(1.0); - }, - icon: const Icon(Icons.center_focus_strong), - tooltip: 'Centrar vista', - style: IconButton.styleFrom( - backgroundColor: - AppTheme.of(context).primaryColor.withOpacity(0.1), - ), - ), - ], - ), - ], - ), - ).animate().fadeIn(delay: 200.ms).slideY(begin: -0.2, end: 0); - } - - Widget _buildStatChip(String label, int count, Color color) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - '$label: $count', - style: TextStyle( - color: color, - fontSize: 10, - fontWeight: FontWeight.w600, - ), - ), - ); - } - - Widget _buildViewButton(String value, String label, IconData icon) { - final isSelected = _selectedView == value; - return GestureDetector( - onTap: () => setState(() => _selectedView = value), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: isSelected - ? AppTheme.of(context).primaryColor - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: isSelected - ? AppTheme.of(context).primaryColor - : AppTheme.of(context).primaryColor.withOpacity(0.3), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - size: 16, - color: - isSelected ? Colors.white : AppTheme.of(context).primaryColor, - ), - const SizedBox(width: 6), - Text( - label, - style: TextStyle( - color: isSelected - ? Colors.white - : AppTheme.of(context).primaryColor, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ); - } - - Widget _buildProfessionalTopologyView( - bool isMediumScreen, ComponentesProvider provider) { - return Container( - decoration: BoxDecoration( - color: const Color(0xFF0D1117), // Fondo oscuro profesional tipo GitHub - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 20, - offset: const Offset(0, 8), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Stack( - children: [ - // Vista según selección - if (_selectedView == 'network') - _buildInteractiveFlowChart(provider) - else if (_selectedView == 'rack') - _buildRackView(isMediumScreen, provider) - else if (_selectedView == 'floor') - _buildFloorPlanView(isMediumScreen, provider), - - // Leyenda profesional - if (isMediumScreen && _selectedView == 'network') - Positioned( - top: 16, - right: 16, - child: _buildProfessionalLegend(), - ), - - // Panel de información - if (_selectedView == 'network') - Positioned( - top: 16, - left: 16, - child: _buildInfoPanel(provider), - ), - - // Panel de problemas si existen - if (provider.problemasTopologia.isNotEmpty) - Positioned( - bottom: 16, - left: 16, - child: _buildProblemasPanel(provider), - ), - ], - ), - ), - ); - } - - Widget _buildInteractiveFlowChart(ComponentesProvider provider) { - return FutureBuilder( - future: _buildNetworkTopologyFromData(provider), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(color: Colors.white), - ); - } - - return FlowChart( - dashboard: dashboard, - onElementPressed: (context, position, element) { - _showElementDetails(element, provider); - }, - onElementLongPressed: (context, position, element) { - _showElementContextMenu(context, position, element, provider); - }, - onNewConnection: (source, target) { - _handleNewConnection(source, target, provider); - }, - onDashboardTapped: (context, position) { - // Limpiar selecciones - }, - ) - .animate() - .fadeIn(duration: 800.ms) - .scale(begin: const Offset(0.95, 0.95)); - }, - ); - } - - Future _buildNetworkTopologyFromData( - ComponentesProvider provider) async { - dashboard.removeAllElements(); - elementosMap.clear(); - - // Obtener componentes organizados por tipo - final mdfComponents = provider.getComponentesMDF(); - final idfComponents = provider.getComponentesIDF(); - final switchComponents = provider.getComponentesSwitch(); - final routerComponents = provider.getComponentesRouter(); - final serverComponents = provider.getComponentesServidor(); - - print('Construyendo topología con datos reales:'); - print('- MDF: ${mdfComponents.length}'); - print('- IDF: ${idfComponents.length}'); - print('- Switches: ${switchComponents.length}'); - print('- Routers: ${routerComponents.length}'); - print('- Servidores: ${serverComponents.length}'); - - double currentX = 100; - double currentY = 100; - const double espacioX = 220; - const double espacioY = 180; - - // 1. Crear elementos MDF - if (mdfComponents.isNotEmpty) { - double mdfX = currentX + espacioX * 2; - for (var mdfComp in mdfComponents) { - final mdfElement = _createMDFElement(mdfComp, Offset(mdfX, currentY)); - dashboard.addElement(mdfElement); - elementosMap[mdfComp.id] = mdfElement; - mdfX += espacioX * 0.8; - } - currentY += espacioY; - } - - // 2. Crear elementos IDF - if (idfComponents.isNotEmpty) { - double idfX = currentX; - for (var idfComp in idfComponents) { - final idfElement = _createIDFElement(idfComp, Offset(idfX, currentY)); - dashboard.addElement(idfElement); - elementosMap[idfComp.id] = idfElement; - idfX += espacioX; - } - currentY += espacioY; - } - - // 3. Crear routers - if (routerComponents.isNotEmpty) { - double routerX = currentX; - for (var router in routerComponents) { - final routerElement = - _createRouterElement(router, Offset(routerX, currentY)); - dashboard.addElement(routerElement); - elementosMap[router.id] = routerElement; - routerX += espacioX * 0.9; - } - currentY += espacioY; - } - - // 4. Crear switches - if (switchComponents.isNotEmpty) { - double switchX = currentX; - for (var switchComp in switchComponents) { - final switchElement = - _createSwitchElement(switchComp, Offset(switchX, currentY)); - dashboard.addElement(switchElement); - elementosMap[switchComp.id] = switchElement; - switchX += espacioX * 0.8; - } - currentY += espacioY; - } - - // 5. Crear servidores - if (serverComponents.isNotEmpty) { - double serverX = currentX + espacioX; - for (var servidor in serverComponents) { - final serverElement = - _createServerElement(servidor, Offset(serverX, currentY)); - dashboard.addElement(serverElement); - elementosMap[servidor.id] = serverElement; - serverX += espacioX * 0.8; - } - } - - // 6. Crear conexiones basadas en datos reales - _createConnections(provider); - - print('Elementos creados: ${elementosMap.length}'); - print( - 'Conexiones de datos disponibles: ${provider.conexionesDatos.length}'); - } - - FlowElement _createMDFElement( - ComponenteTopologia component, Offset position) { - return FlowElement( - position: position, - size: const Size(180, 140), - text: 'MDF\n${component.nombre}', - textColor: Colors.white, - textSize: 14, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: - component.activo ? const Color(0xFF2196F3) : const Color(0xFF757575), - borderColor: - component.activo ? const Color(0xFF1976D2) : const Color(0xFF424242), - borderThickness: 3, - elevation: component.activo ? 8 : 4, - data: _buildElementData(component, 'MDF'), - handlers: [ - Handler.bottomCenter, - Handler.leftCenter, - Handler.rightCenter, - ], - ); - } - - FlowElement _createIDFElement( - ComponenteTopologia component, Offset position) { - return FlowElement( - position: position, - size: const Size(160, 120), - text: 'IDF\n${component.nombre}', - textColor: Colors.white, - textSize: 12, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: component.activo - ? (component.enUso - ? const Color(0xFF4CAF50) - : const Color(0xFFFF9800)) - : const Color(0xFF757575), - borderColor: component.activo - ? (component.enUso - ? const Color(0xFF388E3C) - : const Color(0xFFF57C00)) - : const Color(0xFF424242), - borderThickness: 2, - elevation: component.activo ? 6 : 2, - data: _buildElementData(component, 'IDF'), - handlers: [ - Handler.topCenter, - Handler.bottomCenter, - Handler.leftCenter, - Handler.rightCenter, - ], - ); - } - - FlowElement _createSwitchElement( - ComponenteTopologia component, Offset position) { - return FlowElement( - position: position, - size: const Size(140, 100), - text: 'Switch\n${component.nombre}', - textColor: Colors.white, - textSize: 10, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: - component.activo ? const Color(0xFF9C27B0) : const Color(0xFF757575), - borderColor: - component.activo ? const Color(0xFF7B1FA2) : const Color(0xFF424242), - borderThickness: 2, - elevation: component.activo ? 4 : 2, - data: _buildElementData(component, 'Switch'), - handlers: [ - Handler.topCenter, - Handler.bottomCenter, - ], - ); - } - - FlowElement _createRouterElement( - ComponenteTopologia component, Offset position) { - return FlowElement( - position: position, - size: const Size(160, 100), - text: 'Router\n${component.nombre}', - textColor: Colors.white, - textSize: 11, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: - component.activo ? const Color(0xFFFF5722) : const Color(0xFF757575), - borderColor: - component.activo ? const Color(0xFFE64A19) : const Color(0xFF424242), - borderThickness: 3, - elevation: component.activo ? 6 : 2, - data: _buildElementData(component, 'Router'), - handlers: [ - Handler.topCenter, - Handler.bottomCenter, - Handler.leftCenter, - Handler.rightCenter, - ], - ); - } - - FlowElement _createServerElement( - ComponenteTopologia component, Offset position) { - return FlowElement( - position: position, - size: const Size(150, 100), - text: 'Servidor\n${component.nombre}', - textColor: Colors.white, - textSize: 11, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: - component.activo ? const Color(0xFFE91E63) : const Color(0xFF757575), - borderColor: - component.activo ? const Color(0xFFC2185B) : const Color(0xFF424242), - borderThickness: 3, - elevation: component.activo ? 6 : 2, - data: _buildElementData(component, 'Server'), - handlers: [ - Handler.topCenter, - Handler.leftCenter, - Handler.rightCenter, - ], - ); - } - - Map _buildElementData( - ComponenteTopologia component, String displayType) { - return { - 'type': displayType, - 'componenteId': component.id, - 'name': component.nombre, - 'categoria': component.categoria, - 'status': component.activo - ? (component.enUso ? 'active' : 'warning') - : 'disconnected', - 'description': component.descripcion ?? 'Sin descripción', - 'ubicacion': component.ubicacion ?? 'Sin ubicación', - 'distribucion': component.nombreDistribucion ?? 'Sin distribución', - 'tipoDistribucion': component.tipoDistribucion, - 'enUso': component.enUso, - 'fechaRegistro': component.fechaRegistro.toString().split(' ')[0], - }; - } - - void _createConnections(ComponentesProvider provider) { - // Crear conexiones basadas en los datos reales - for (var conexion in provider.conexionesDatos) { - if (!conexion.activo) continue; - - final sourceElement = elementosMap[conexion.componenteOrigenId]; - final targetElement = elementosMap[conexion.componenteDestinoId]; - - if (sourceElement != null && targetElement != null) { - // Determinar color y grosor basado en el tipo de conexión - Color connectionColor = _getConnectionColor(conexion, provider); - double thickness = _getConnectionThickness(conexion, provider); - - final connectionParams = ConnectionParams( - destElementId: targetElement.id, - arrowParams: ArrowParams( - color: connectionColor, - thickness: thickness, - ), - ); - - sourceElement.next = [...sourceElement.next ?? [], connectionParams]; - } - } - - // También crear conexiones de energía si es necesario - for (var conexionEnergia in provider.conexionesEnergia) { - if (!conexionEnergia.activo) continue; - - final sourceElement = elementosMap[conexionEnergia.origenId]; - final targetElement = elementosMap[conexionEnergia.destinoId]; - - if (sourceElement != null && targetElement != null) { - final connectionParams = ConnectionParams( - destElementId: targetElement.id, - arrowParams: ArrowParams( - color: Colors.red.withOpacity(0.7), - thickness: 2, - ), - ); - - sourceElement.next = [...sourceElement.next ?? [], connectionParams]; - } - } - } - - Color _getConnectionColor( - ConexionDatos conexion, ComponentesProvider provider) { - // Determinar color basado en el tipo de cable o componentes conectados - if (conexion.nombreCable != null) { - final cableName = conexion.nombreCable!.toLowerCase(); - if (cableName.contains('fibra')) return Colors.cyan; - if (cableName.contains('utp')) return Colors.yellow; - if (cableName.contains('coaxial')) return Colors.orange; - } - - // Color por defecto basado en los componentes - final sourceComponent = - provider.getComponenteTopologiaById(conexion.componenteOrigenId); - final targetComponent = - provider.getComponenteTopologiaById(conexion.componenteDestinoId); - - if (sourceComponent?.esMDF == true || targetComponent?.esMDF == true) { - return Colors.cyan; // Conexiones principales - } - if (sourceComponent?.esIDF == true || targetComponent?.esIDF == true) { - return Colors.yellow; // Conexiones intermedias - } - - return Colors.green; // Conexiones generales - } - - double _getConnectionThickness( - ConexionDatos conexion, ComponentesProvider provider) { - final sourceComponent = - provider.getComponenteTopologiaById(conexion.componenteOrigenId); - final targetComponent = - provider.getComponenteTopologiaById(conexion.componenteDestinoId); - - if (sourceComponent?.esMDF == true || targetComponent?.esMDF == true) { - return 4; // Conexiones principales más gruesas - } - if (sourceComponent?.esIDF == true || targetComponent?.esIDF == true) { - return 3; // Conexiones intermedias - } - - return 2; // Conexiones estándar - } - - void _showElementDetails(FlowElement element, ComponentesProvider provider) { - final data = element.data as Map; - final componenteId = data['componenteId'] as String; - final component = provider.getComponenteTopologiaById(componenteId); - - if (component == null) return; - - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: AppTheme.of(context).primaryBackground, - title: Row( - children: [ - Icon(_getIconForType(data['type']), - color: _getColorForType(data['type'])), - const SizedBox(width: 8), - Expanded(child: Text(data['name'])), - ], - ), - content: Container( - width: double.maxFinite, - constraints: const BoxConstraints(maxHeight: 500), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDetailRow('Tipo', data['type']), - _buildDetailRow('Categoría', component.categoria), - _buildDetailRow('Estado', _getStatusText(data['status'])), - _buildDetailRow('En Uso', component.enUso ? 'Sí' : 'No'), - _buildDetailRow( - 'Ubicación', component.ubicacion ?? 'Sin especificar'), - if (component.tipoDistribucion != null) - _buildDetailRow('Distribución', - '${component.tipoDistribucion} - ${component.nombreDistribucion}'), - _buildDetailRow('Fecha Registro', - component.fechaRegistro.toString().split(' ')[0]), - const SizedBox(height: 16), - const Text('Descripción:', - style: TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - Text(component.descripcion ?? 'Sin descripción'), - const SizedBox(height: 16), - _buildConnectionsInfo(component, provider), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cerrar'), - ), - ], - ), - ); - } - - Widget _buildDetailRow(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 120, - child: Text( - '$label:', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - Expanded(child: Text(value)), - ], - ), - ); - } - - Widget _buildConnectionsInfo( - ComponenteTopologia component, ComponentesProvider provider) { - final conexiones = provider.getConexionesPorComponente(component.id); - final conexionesEnergia = - provider.getConexionesEnergiaPorComponente(component.id); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Conexiones:', - style: TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - if (conexiones.isEmpty && conexionesEnergia.isEmpty) - const Text('Sin conexiones registradas') - else ...[ - if (conexiones.isNotEmpty) ...[ - const Text('Datos:', style: TextStyle(fontWeight: FontWeight.w500)), - ...conexiones.map((c) => Padding( - padding: const EdgeInsets.only(left: 16, top: 4), - child: Text('• ${c.nombreOrigen} ↔ ${c.nombreDestino}'), - )), - ], - if (conexionesEnergia.isNotEmpty) ...[ - const SizedBox(height: 8), - const Text('Energía:', - style: TextStyle(fontWeight: FontWeight.w500)), - ...conexionesEnergia.map((c) => Padding( - padding: const EdgeInsets.only(left: 16, top: 4), - child: Text('• ${c.nombreOrigen} → ${c.nombreDestino}'), - )), - ], - ], - ], - ); - } - - void _showElementContextMenu(BuildContext context, Offset position, - FlowElement element, ComponentesProvider provider) { - // TODO: Implementar menú contextual con opciones reales - } - - void _handleNewConnection( - FlowElement source, FlowElement target, ComponentesProvider provider) { - // TODO: Implementar creación de nueva conexión en la base de datos - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Nueva Conexión'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Conectar desde: ${(source.data as Map)['name']}'), - Text('Hacia: ${(target.data as Map)['name']}'), - const SizedBox(height: 16), - const Text('Funcionalidad próximamente...'), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cerrar'), - ), - ], - ), - ); - } - - Widget _buildProfessionalLegend() { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.8), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.white.withOpacity(0.2)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Leyenda', - style: TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - _buildLegendItem(const Color(0xFF2196F3), 'MDF', Icons.router), - _buildLegendItem(const Color(0xFF4CAF50), 'IDF Activo', Icons.hub), - _buildLegendItem( - const Color(0xFFFF9800), 'IDF Advertencia', Icons.hub), - _buildLegendItem( - const Color(0xFF9C27B0), 'Switch', Icons.network_check), - _buildLegendItem(const Color(0xFFFF5722), 'Router', Icons.router), - _buildLegendItem(const Color(0xFFE91E63), 'Servidor', Icons.dns), - const SizedBox(height: 6), - _buildLegendItem(Colors.cyan, 'Fibra Óptica', Icons.cable), - _buildLegendItem(Colors.yellow, 'Cable UTP', Icons.cable), - _buildLegendItem(Colors.green, 'Conexión General', Icons.cable), - _buildLegendItem(Colors.red, 'Alimentación', Icons.power), - _buildLegendItem(Colors.grey, 'Inactivo', Icons.clear), - ], - ), - ).animate().fadeIn(delay: 1000.ms).slideX(begin: 0.3); - } - - Widget _buildLegendItem(Color color, String label, IconData icon) { - return Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, color: color, size: 12), - const SizedBox(width: 6), - Text( - label, - style: const TextStyle( - color: Colors.white70, - fontSize: 9, - ), - ), - ], - ), - ); - } - - Widget _buildInfoPanel(ComponentesProvider provider) { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.8), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.white.withOpacity(0.2)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Información', - style: TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - const Text( - '• Arrastra los nodos para reposicionar', - style: TextStyle(color: Colors.white70, fontSize: 10), - ), - const Text( - '• Haz clic en un nodo para ver detalles', - style: TextStyle(color: Colors.white70, fontSize: 10), - ), - const Text( - '• Usa zoom con scroll del mouse', - style: TextStyle(color: Colors.white70, fontSize: 10), - ), - const SizedBox(height: 8), - Text( - 'Datos desde: ${provider.negocioSeleccionadoNombre}', - style: const TextStyle(color: Colors.cyan, fontSize: 9), - ), - ], - ), - ).animate().fadeIn(delay: 1200.ms).slideX(begin: -0.3); - } - - Widget _buildProblemasPanel(ComponentesProvider provider) { - return Container( - constraints: const BoxConstraints(maxWidth: 300), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.orange.withOpacity(0.9), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.orange), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - Icon(Icons.warning, color: Colors.white, size: 16), - SizedBox(width: 8), - Text( - 'Alertas de Topología', - style: TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 8), - ...provider.problemasTopologia.take(3).map((problema) => Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Text( - '• $problema', - style: const TextStyle(color: Colors.white, fontSize: 10), - ), - )), - if (provider.problemasTopologia.length > 3) - Text( - '... y ${provider.problemasTopologia.length - 3} más', - style: const TextStyle(color: Colors.white70, fontSize: 9), - ), - ], - ), - ).animate().fadeIn(delay: 1500.ms).slideY(begin: 0.3); - } - - Widget _buildRackView(bool isMediumScreen, ComponentesProvider provider) { - return RackViewWidget( - isMediumScreen: isMediumScreen, - provider: provider, - ); - } - - Widget _buildFloorPlanView( - bool isMediumScreen, ComponentesProvider provider) { - return FloorPlanViewWidget( - isMediumScreen: isMediumScreen, - provider: provider, - ); - } - - IconData _getIconForType(String type) { - switch (type) { - case 'MDF': - return Icons.router; - case 'IDF': - return Icons.hub; - case 'Switch': - return Icons.network_check; - case 'Server': - return Icons.dns; - case 'Router': - return Icons.router; - default: - return Icons.device_unknown; - } - } - - Color _getColorForType(String type) { - switch (type) { - case 'MDF': - return const Color(0xFF2196F3); - case 'IDF': - return const Color(0xFF4CAF50); - case 'Switch': - return const Color(0xFF9C27B0); - case 'Server': - return const Color(0xFFE91E63); - case 'Router': - return const Color(0xFFFF5722); - default: - return Colors.grey; - } - } - - String _getStatusText(String status) { - switch (status) { - case 'active': - return '🟢 Activo'; - case 'warning': - return '🟡 Advertencia'; - case 'error': - return '🔴 Error'; - case 'disconnected': - return '⚫ Desconectado'; - default: - return '❓ Desconocido'; - } - } - - Future _loadTopologyData() async { - final provider = Provider.of(context, listen: false); - - if (provider.negocioSeleccionadoId != null) { - setState(() { - _isLoading = true; - }); - - await provider.getTopologiaPorNegocio(provider.negocioSeleccionadoId!); - - setState(() { - _isLoading = false; - }); - } - } - - Future _refreshTopology(ComponentesProvider provider) async { - if (provider.negocioSeleccionadoId != null) { - setState(() { - _isLoading = true; - }); - - await provider.getTopologiaPorNegocio(provider.negocioSeleccionadoId!); - - setState(() { - _isLoading = false; - }); - } - } -} diff --git a/lib/pages/infrastructure/pages/widgets/topologia_page_widgets/floor_plan_view_widget.dart b/lib/pages/infrastructure/pages/widgets/topologia_page_widgets/floor_plan_view_widget.dart deleted file mode 100644 index e9e4151..0000000 --- a/lib/pages/infrastructure/pages/widgets/topologia_page_widgets/floor_plan_view_widget.dart +++ /dev/null @@ -1,213 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; - -class FloorPlanViewWidget extends StatelessWidget { - final bool isMediumScreen; - final ComponentesProvider provider; - - const FloorPlanViewWidget({ - Key? key, - required this.isMediumScreen, - required this.provider, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(24), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.map, - size: 80, - color: Colors.white.withOpacity(0.7), - ).animate().scale(duration: 600.ms), - const SizedBox(height: 20), - const Text( - 'Plano de Planta', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ).animate().fadeIn(delay: 300.ms), - const SizedBox(height: 8), - Text( - 'Próximamente: Distribución geográfica de componentes\ncon ${_getUbicacionesUnicas().length} ubicaciones identificadas', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white.withOpacity(0.7), - fontSize: 16, - ), - ).animate().fadeIn(delay: 500.ms), - const SizedBox(height: 24), - - // Panel de información adicional para planos - if (isMediumScreen) _buildFloorPlanInfoPanel(), - ], - ), - ), - ); - } - - List _getUbicacionesUnicas() { - final ubicaciones = provider.componentesTopologia - .where((c) => c.ubicacion != null && c.ubicacion!.trim().isNotEmpty) - .map((c) => c.ubicacion!) - .toSet() - .toList(); - return ubicaciones; - } - - Widget _buildFloorPlanInfoPanel() { - final ubicaciones = _getUbicacionesUnicas(); - final componentesPorPiso = _agruparComponentesPorPiso(); - - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.4), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white.withOpacity(0.2)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Información del Plano', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - if (ubicaciones.isNotEmpty) ...[ - Text( - 'Ubicaciones detectadas: ${ubicaciones.length}', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - ), - ), - const SizedBox(height: 8), - ...ubicaciones.take(4).map((ubicacion) { - final componentesEnUbicacion = provider.componentesTopologia - .where((c) => c.ubicacion == ubicacion) - .length; - return Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Text( - '• $ubicacion ($componentesEnUbicacion componentes)', - style: TextStyle( - color: Colors.white.withOpacity(0.7), - fontSize: 12, - ), - ), - ); - }), - if (ubicaciones.length > 4) - Text( - '... y ${ubicaciones.length - 4} más', - style: TextStyle( - color: Colors.white.withOpacity(0.5), - fontSize: 11, - ), - ), - ] else ...[ - Text( - 'No se encontraron ubicaciones específicas', - style: TextStyle( - color: Colors.white.withOpacity(0.7), - fontSize: 14, - ), - ), - ], - if (componentesPorPiso.isNotEmpty) ...[ - const SizedBox(height: 12), - const Text( - 'Distribución por niveles:', - style: TextStyle( - color: Colors.cyan, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 6), - ...componentesPorPiso.entries.take(3).map((entry) => Padding( - padding: const EdgeInsets.only(bottom: 2), - child: Text( - '${entry.key}: ${entry.value} componentes', - style: TextStyle( - color: Colors.white.withOpacity(0.6), - fontSize: 11, - ), - ), - )), - ], - const SizedBox(height: 12), - const Text( - 'Funcionalidades planificadas:', - style: TextStyle( - color: Colors.cyan, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 6), - ...[ - '• Mapa interactivo de ubicaciones', - '• Vista por pisos y áreas', - '• Trazado de rutas de cableado', - '• Ubicación GPS de componentes', - ].map((feature) => Padding( - padding: const EdgeInsets.only(bottom: 2), - child: Text( - feature, - style: TextStyle( - color: Colors.white.withOpacity(0.6), - fontSize: 11, - ), - ), - )), - ], - ), - ).animate().fadeIn(delay: 700.ms).slideY(begin: 0.3); - } - - Map _agruparComponentesPorPiso() { - Map pisos = {}; - - for (var componente in provider.componentesTopologia) { - if (componente.ubicacion != null) { - String ubicacion = componente.ubicacion!.toLowerCase(); - String piso = 'Otros'; - - if (ubicacion.contains('piso') || ubicacion.contains('planta')) { - // Extraer número de piso - RegExp regex = RegExp(r'(piso|planta)\s*(\d+)', caseSensitive: false); - var match = regex.firstMatch(ubicacion); - if (match != null) { - piso = 'Piso ${match.group(2)}'; - } - } else if (ubicacion.contains('pb') || - ubicacion.contains('planta baja')) { - piso = 'Planta Baja'; - } else if (ubicacion.contains('sotano') || - ubicacion.contains('sótano')) { - piso = 'Sótano'; - } else if (ubicacion.contains('azotea') || - ubicacion.contains('terraza')) { - piso = 'Azotea'; - } - - pisos[piso] = (pisos[piso] ?? 0) + 1; - } - } - - return pisos; - } -} diff --git a/lib/pages/infrastructure/pages/widgets/topologia_page_widgets/rack_view_widget.dart b/lib/pages/infrastructure/pages/widgets/topologia_page_widgets/rack_view_widget.dart deleted file mode 100644 index 99dc2b1..0000000 --- a/lib/pages/infrastructure/pages/widgets/topologia_page_widgets/rack_view_widget.dart +++ /dev/null @@ -1,863 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:nethive_neo/helpers/constants.dart'; -import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; -import 'package:nethive_neo/models/nethive/rack_con_componentes_model.dart'; - -class RackViewWidget extends StatelessWidget { - final bool isMediumScreen; - final ComponentesProvider provider; - - const RackViewWidget({ - Key? key, - required this.isMediumScreen, - required this.provider, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - if (provider.isLoadingRacks) { - return _buildLoadingView(); - } - - if (provider.racksConComponentes.isEmpty) { - return _buildEmptyView(); - } - - return Container( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header con estadísticas - _buildRackSummaryHeader(), - const SizedBox(height: 24), - - // Vista principal de racks - Expanded( - child: isMediumScreen - ? _buildDesktopRackView() - : _buildMobileRackView(), - ), - ], - ), - ); - } - - Widget _buildLoadingView() { - return Container( - padding: const EdgeInsets.all(24), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator( - color: Colors.white, - strokeWidth: 3, - ).animate().scale(duration: 600.ms), - const SizedBox(height: 20), - const Text( - 'Cargando vista de racks...', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ).animate().fadeIn(delay: 300.ms), - const SizedBox(height: 8), - const Text( - 'Obteniendo componentes de cada rack', - style: TextStyle( - color: Colors.white70, - fontSize: 14, - ), - ).animate().fadeIn(delay: 500.ms), - ], - ), - ), - ); - } - - Widget _buildEmptyView() { - return Container( - padding: const EdgeInsets.all(24), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.dns, - size: 80, - color: Colors.white.withOpacity(0.5), - ).animate().scale(duration: 600.ms), - const SizedBox(height: 20), - const Text( - 'Sin Racks Detectados', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ).animate().fadeIn(delay: 300.ms), - const SizedBox(height: 8), - const Text( - 'No se encontraron racks registrados\nen este negocio', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white70, - fontSize: 16, - ), - ).animate().fadeIn(delay: 500.ms), - const SizedBox(height: 24), - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.4), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white.withOpacity(0.2)), - ), - child: Column( - children: [ - const Text( - 'Para ver racks aquí:', - style: TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - const Text( - '1. Cree componentes de tipo "Rack"', - style: TextStyle(color: Colors.white70, fontSize: 12), - ), - const SizedBox(height: 4), - const Text( - '2. Asigne otros componentes a los racks', - style: TextStyle(color: Colors.white70, fontSize: 12), - ), - const SizedBox(height: 4), - const Text( - '3. Configure posiciones U si es necesario', - style: TextStyle(color: Colors.white70, fontSize: 12), - ), - ], - ), - ).animate().fadeIn(delay: 700.ms), - ], - ), - ), - ); - } - - Widget _buildRackSummaryHeader() { - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.blue.withOpacity(0.8), - Colors.blue.withOpacity(0.6), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.blue.withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.dns, - color: Colors.white, - size: 24, - ), - ).animate().scale(duration: 600.ms), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Vista de Racks - ${provider.negocioSeleccionadoNombre}', - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ).animate().fadeIn(delay: 300.ms), - const SizedBox(height: 4), - Text( - '${provider.totalRacks} racks • ${provider.totalComponentesEnRacks} componentes • ${provider.porcentajeOcupacionPromedio.toStringAsFixed(1)}% ocupación promedio', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - ), - ).animate().fadeIn(delay: 500.ms), - ], - ), - ), - if (provider.racksConProblemas.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.orange.withOpacity(0.3), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.warning, color: Colors.white, size: 16), - const SizedBox(width: 8), - Text( - '${provider.racksConProblemas.length} alertas', - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ).animate().fadeIn(delay: 700.ms), - ], - ), - ).animate().fadeIn().slideY(begin: -0.3, end: 0); - } - - Widget _buildDesktopRackView() { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Lista de racks - Expanded( - flex: 2, - child: _buildRacksList(), - ), - const SizedBox(width: 24), - // Panel de información - Expanded( - flex: 1, - child: _buildRackInfoPanel(), - ), - ], - ); - } - - Widget _buildMobileRackView() { - return _buildRacksList(); - } - - Widget _buildRacksList() { - return GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isMediumScreen ? 2 : 1, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - childAspectRatio: isMediumScreen ? 1.2 : 1.5, - ), - itemCount: provider.racksConComponentes.length, - itemBuilder: (context, index) { - final rack = provider.racksConComponentes[index]; - return _buildRackCard(rack, index); - }, - ); - } - - Widget _buildRackCard(RackConComponentes rack, int index) { - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.black.withOpacity(0.8), - Colors.black.withOpacity(0.6), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.white.withOpacity(0.3), - width: 1.5, - ), - boxShadow: [ - BoxShadow( - color: Colors.blue.withOpacity(0.2), - blurRadius: 15, - offset: const Offset(0, 8), - ), - BoxShadow( - color: Colors.black.withOpacity(0.4), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(16), - onTap: () => _showRackDetails(rack), - child: Padding( - padding: const EdgeInsets.all(20), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Imagen del rack más grande - Container( - width: 90, - height: 90, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.blue.withOpacity(0.4), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: _buildRackImage(rack), - ), - ), - const SizedBox(width: 20), - - // Información principal - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header del rack - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Colors.blue.withOpacity(0.4), - width: 1, - ), - ), - child: const Icon( - Icons.dns, - color: Colors.blue, - size: 18, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - rack.nombreRack, - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (rack.ubicacionRack != null) - Text( - rack.ubicacionRack!, - style: TextStyle( - color: Colors.white.withOpacity(0.7), - fontSize: 14, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - - // Estadísticas mejoradas - Row( - children: [ - _buildEnhancedStatItem( - rack.cantidadComponentes.toString(), - 'Total', - Colors.blue, - Icons.memory, - ), - const SizedBox(width: 12), - _buildEnhancedStatItem( - rack.componentesActivos.toString(), - 'Activos', - Colors.green, - Icons.check_circle, - ), - const SizedBox(width: 12), - _buildEnhancedStatItem( - '${rack.porcentajeOcupacion.toStringAsFixed(0)}%', - 'Ocupación', - rack.porcentajeOcupacion > 80 - ? Colors.red - : rack.porcentajeOcupacion > 60 - ? Colors.orange - : Colors.green, - Icons.dashboard, - ), - ], - ), - const SizedBox(height: 16), - - // Barra de ocupación mejorada - _buildEnhancedOccupationBar(rack), - - const SizedBox(height: 12), - - // Componentes preview mejorado - if (rack.componentes.isNotEmpty) - _buildEnhancedComponentsPreview(rack), - ], - ), - ), - ], - ), - ), - ), - ), - ).animate().fadeIn(delay: (100 * index).ms).slideY(begin: 0.3); - } - - Widget _buildRackImage(RackConComponentes rack) { - // Buscar la imagen del rack en los componentes - final rackComponent = provider.componentesTopologia - .where((c) => c.id == rack.rackId) - .firstOrNull; - - final imagenUrl = rackComponent?.imagenUrl; - - if (imagenUrl != null && imagenUrl.isNotEmpty) { - // Construir URL completa de Supabase - final fullImageUrl = - "$supabaseUrl/storage/v1/object/public/nethive/componentes/$imagenUrl?${DateTime.now().millisecondsSinceEpoch}"; - - return Image.network( - fullImageUrl, - fit: BoxFit.cover, - width: 90, - height: 90, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Container( - width: 90, - height: 90, - decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.3), - borderRadius: BorderRadius.circular(10), - ), - child: Center( - child: CircularProgressIndicator( - color: Colors.blue, - strokeWidth: 2, - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), - ), - ); - }, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - fit: BoxFit.cover, - width: 90, - height: 90, - ); - }, - ); - } else { - // Usar imagen placeholder local - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - fit: BoxFit.cover, - width: 90, - height: 90, - ); - } - } - - Widget _buildEnhancedStatItem( - String value, String label, Color color, IconData icon) { - return Expanded( - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - color.withOpacity(0.2), - color.withOpacity(0.1), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withOpacity(0.4), width: 1), - boxShadow: [ - BoxShadow( - color: color.withOpacity(0.2), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - color: color, - size: 20, - ), - const SizedBox(height: 4), - Text( - value, - style: TextStyle( - color: color, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - Text( - label, - style: TextStyle( - color: color.withOpacity(0.8), - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ); - } - - Widget _buildEnhancedOccupationBar(RackConComponentes rack) { - final ocupacion = rack.porcentajeOcupacion; - final color = ocupacion > 80 - ? Colors.red - : ocupacion > 60 - ? Colors.orange - : Colors.green; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Ocupación del Rack', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: color.withOpacity(0.2), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: color.withOpacity(0.4)), - ), - child: Text( - '${ocupacion.toStringAsFixed(1)}%', - style: TextStyle( - color: color, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - Container( - height: 8, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(4), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: LinearProgressIndicator( - value: ocupacion / 100, - backgroundColor: Colors.transparent, - valueColor: AlwaysStoppedAnimation(color), - ), - ), - ), - ], - ); - } - - Widget _buildEnhancedComponentsPreview(RackConComponentes rack) { - final componentesOrdenados = rack.componentesOrdenadosPorPosicion; - final maxPreview = 3; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.list_alt, - color: Colors.white.withOpacity(0.8), - size: 16, - ), - const SizedBox(width: 6), - Text( - 'Componentes principales:', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 8), - ...componentesOrdenados.take(maxPreview).map((comp) => Container( - margin: const EdgeInsets.only(bottom: 6), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.05), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: comp.colorEstado.withOpacity(0.3), - width: 1, - ), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: comp.colorEstado.withOpacity(0.2), - borderRadius: BorderRadius.circular(4), - ), - child: Icon( - comp.iconoCategoria, - size: 14, - color: comp.colorEstado, - ), - ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - comp.nombre, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 12, - fontWeight: FontWeight.w500, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (comp.posicionU != null) - Text( - 'Posición U${comp.posicionU}', - style: TextStyle( - color: Colors.white.withOpacity(0.6), - fontSize: 10, - ), - ), - ], - ), - ), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: comp.colorEstado.withOpacity(0.2), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - comp.estadoTexto, - style: TextStyle( - color: comp.colorEstado, - fontSize: 9, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - )), - if (componentesOrdenados.length > maxPreview) - Container( - margin: const EdgeInsets.only(top: 4), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.blue.withOpacity(0.3)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.more_horiz, - color: Colors.blue, - size: 14, - ), - const SizedBox(width: 4), - Text( - '+${componentesOrdenados.length - maxPreview} componentes más', - style: const TextStyle( - color: Colors.blue, - fontSize: 11, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ], - ); - } - - Widget _buildRackInfoPanel() { - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.4), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white.withOpacity(0.2)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Resumen de Racks', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - _buildInfoRow('Total de Racks', provider.totalRacks.toString()), - _buildInfoRow('Componentes Totales', - provider.totalComponentesEnRacks.toString()), - _buildInfoRow( - 'Racks Activos', provider.racksConComponentesActivos.toString()), - _buildInfoRow('Ocupación Promedio', - '${provider.porcentajeOcupacionPromedio.toStringAsFixed(1)}%'), - if (provider.racksConProblemas.isNotEmpty) ...[ - const SizedBox(height: 16), - const Divider(color: Colors.white24), - const SizedBox(height: 16), - const Text( - 'Racks con Alertas', - style: TextStyle( - color: Colors.orange, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - ...provider.racksConProblemas.map((rack) => Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Text( - '• ${rack.nombreRack}', - style: TextStyle( - color: Colors.white.withOpacity(0.7), - fontSize: 12, - ), - ), - )), - ], - const Spacer(), - const Text( - 'Funcionalidades disponibles:', - style: TextStyle( - color: Colors.cyan, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 6), - ...[ - '• Vista detallada de cada rack', - '• Gestión de posiciones U', - '• Estados de componentes', - '• Alertas de ocupación', - ].map((feature) => Padding( - padding: const EdgeInsets.only(bottom: 2), - child: Text( - feature, - style: TextStyle( - color: Colors.white.withOpacity(0.6), - fontSize: 11, - ), - ), - )), - ], - ), - ).animate().fadeIn(delay: 800.ms).slideX(begin: 0.3); - } - - Widget _buildInfoRow(String label, String value) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 12, - ), - ), - Text( - value, - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - } - - void _showRackDetails(RackConComponentes rack) { - // TODO: Implementar modal con detalles completos del rack - print('Mostrar detalles del rack: ${rack.nombreRack}'); - } -} diff --git a/lib/pages/infrastructure/widgets/add_componente_dialog.dart b/lib/pages/infrastructure/widgets/add_componente_dialog.dart deleted file mode 100644 index 6f7a47a..0000000 --- a/lib/pages/infrastructure/widgets/add_componente_dialog.dart +++ /dev/null @@ -1,1255 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class AddComponenteDialog extends StatefulWidget { - final ComponentesProvider provider; - - const AddComponenteDialog({ - Key? key, - required this.provider, - }) : super(key: key); - - @override - State createState() => _AddComponenteDialogState(); -} - -class _AddComponenteDialogState extends State - with TickerProviderStateMixin { - final _formKey = GlobalKey(); - late TextEditingController _nombreController; - late TextEditingController _descripcionController; - late TextEditingController _ubicacionController; - - bool _enUso = false; - bool _activo = true; - int? _categoriaSeleccionada; - bool _isLoading = false; - - // Animaciones - late AnimationController _slideController; - late AnimationController _fadeController; - late Animation _slideAnimation; - late Animation _fadeAnimation; - - @override - void initState() { - super.initState(); - _nombreController = TextEditingController(); - _descripcionController = TextEditingController(); - _ubicacionController = TextEditingController(); - - // Configurar animaciones - _slideController = AnimationController( - duration: const Duration(milliseconds: 600), - vsync: this, - ); - _fadeController = AnimationController( - duration: const Duration(milliseconds: 400), - vsync: this, - ); - - _slideAnimation = Tween( - begin: const Offset(-1.0, 0.0), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _slideController, - curve: Curves.easeOutCubic, - )); - - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _fadeController, - curve: Curves.easeInOut, - )); - - // Iniciar animaciones - _slideController.forward(); - _fadeController.forward(); - } - - @override - void dispose() { - _nombreController.dispose(); - _descripcionController.dispose(); - _ubicacionController.dispose(); - _slideController.dispose(); - _fadeController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final screenSize = MediaQuery.of(context).size; - final isDesktop = screenSize.width > 1024; - final isMobile = screenSize.width <= 768; - - return Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.all(isDesktop ? 40 : 20), - child: Container( - width: isDesktop ? 900 : (isMobile ? screenSize.width * 0.95 : 700), - height: isDesktop ? 650 : (isMobile ? screenSize.height * 0.9 : 600), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 30, - offset: const Offset(0, 15), - spreadRadius: 5, - ), - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - blurRadius: 40, - offset: const Offset(0, 10), - spreadRadius: 2, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryBackground, - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - stops: const [0.0, 0.6, 1.0], - ), - ), - child: FadeTransition( - opacity: _fadeAnimation, - child: isDesktop - ? _buildDesktopLayout() - : _buildMobileLayout(isMobile), - ), - ), - ), - ), - ); - } - - Widget _buildDesktopLayout() { - return Row( - children: [ - // Panel lateral izquierdo con diseño espectacular - Container( - width: 300, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.5), - blurRadius: 25, - offset: const Offset(5, 0), - ), - ], - ), - child: SlideTransition( - position: _slideAnimation, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 25), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Icono principal del componente - Container( - width: 140, - height: 140, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: RadialGradient( - colors: [ - Colors.white.withOpacity(0.3), - Colors.white.withOpacity(0.1), - Colors.transparent, - ], - ), - border: Border.all( - color: Colors.white.withOpacity(0.4), - width: 3, - ), - boxShadow: [ - BoxShadow( - color: Colors.white.withOpacity(0.3), - blurRadius: 30, - spreadRadius: 10, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(17), - child: widget.provider.imagenToUpload != null - ? Image.memory( - widget.provider.imagenToUpload!, - fit: BoxFit.cover, - ) - : Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(17), - ), - child: const Icon( - Icons.add_circle_outline, - color: Colors.white, - size: 60, - ), - ), - ), - ), - - const SizedBox(height: 24), - - // Botón para seleccionar imagen - Container( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () async { - await widget.provider.selectImagen(); - setState(() {}); - }, - icon: const Icon(Icons.image, color: Colors.white), - label: Text( - widget.provider.imagenToUpload != null - ? 'Cambiar Imagen' - : 'Seleccionar Imagen', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white.withOpacity(0.2), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide( - color: Colors.white.withOpacity(0.3), - ), - ), - ), - ), - ), - - const SizedBox(height: 20), - - // Título - const Text( - 'Nuevo Componente', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - letterSpacing: 1.0, - ), - textAlign: TextAlign.center, - ), - - const SizedBox(height: 12), - - // Subtítulo - Text( - 'Registra un nuevo componente\npara tu infraestructura', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - height: 1.4, - ), - textAlign: TextAlign.center, - ), - - const SizedBox(height: 30), - - // Estados predeterminados - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildStatusIndicator( - _activo ? 'Activo' : 'Inactivo', - _activo ? Icons.check_circle : Icons.cancel, - _activo ? Colors.green : Colors.red, - ), - _buildStatusIndicator( - _enUso ? 'En Uso' : 'Libre', - _enUso ? Icons.trending_up : Icons.trending_flat, - _enUso ? Colors.orange : Colors.grey, - ), - ], - ), - - const SizedBox(height: 20), - - // Info del negocio - if (widget.provider.negocioSeleccionadoNombre != null) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - 'Negocio: ${widget.provider.negocioSeleccionadoNombre}', - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - ), - ), - - // Panel principal con formulario - Expanded( - child: Padding( - padding: const EdgeInsets.all(30), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header del formulario - Row( - children: [ - Icon( - Icons.add_box_rounded, - color: AppTheme.of(context).primaryColor, - size: 28, - ), - const SizedBox(width: 12), - Text( - 'Información del Componente', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: Icon( - Icons.close, - color: AppTheme.of(context).secondaryText, - ), - style: IconButton.styleFrom( - backgroundColor: - AppTheme.of(context).secondaryBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ], - ), - - const SizedBox(height: 30), - - // Formulario - Expanded( - child: Form( - key: _formKey, - child: SingleChildScrollView( - child: Column( - children: [ - // Primera fila - Nombre y Categoría - Row( - children: [ - Expanded( - flex: 2, - child: _buildCompactFormField( - controller: _nombreController, - label: 'Nombre del Componente', - hint: 'Ej: Switch Principal MDF', - icon: Icons.devices_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es obligatorio'; - } - return null; - }, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildCategoriaDropdown(), - ), - ], - ), - - const SizedBox(height: 16), - - // Segunda fila - Ubicación - _buildCompactFormField( - controller: _ubicacionController, - label: 'Ubicación', - hint: 'Ej: MDF Principal - Rack 1', - icon: Icons.location_on_rounded, - validator: (value) { - // La ubicación es opcional - return null; - }, - ), - - const SizedBox(height: 16), - - // Tercera fila - Descripción - _buildCompactFormField( - controller: _descripcionController, - label: 'Descripción', - hint: 'Descripción detallada del componente', - icon: Icons.description_rounded, - maxLines: 3, - validator: (value) { - // La descripción es opcional - return null; - }, - ), - - const SizedBox(height: 20), - - // Switches de estado - Row( - children: [ - Expanded( - child: _buildSwitchCard( - title: 'Componente Activo', - subtitle: 'El componente está operativo', - value: _activo, - onChanged: (value) { - setState(() { - _activo = value; - }); - }, - icon: Icons.power_settings_new, - activeColor: Colors.green, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildSwitchCard( - title: 'En Uso', - subtitle: - 'El componente está siendo utilizado', - value: _enUso, - onChanged: (value) { - setState(() { - _enUso = value; - }); - }, - icon: Icons.work, - activeColor: Colors.orange, - ), - ), - ], - ), - ], - ), - ), - ), - ), - - // Botones de acción - const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: _isLoading - ? null - : () => Navigator.of(context).pop(), - icon: const Icon(Icons.close, size: 18), - label: const Text('Cancelar'), - style: OutlinedButton.styleFrom( - foregroundColor: AppTheme.of(context).secondaryText, - side: BorderSide( - color: AppTheme.of(context) - .secondaryText - .withOpacity(0.5), - ), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - const SizedBox(width: 16), - Expanded( - flex: 2, - child: Container( - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.4), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: ElevatedButton.icon( - onPressed: _isLoading ? null : _guardarComponente, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - icon: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Colors.white), - ), - ) - : const Icon(Icons.save_rounded, - color: Colors.white, size: 20), - label: Text( - _isLoading ? 'Guardando...' : 'Crear Componente', - style: const TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ], - ); - } - - Widget _buildMobileLayout(bool isMobile) { - return Column( - children: [ - // Header móvil - Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - ), - child: SafeArea( - bottom: false, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Nuevo Componente', - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.close, color: Colors.white), - ), - ], - ), - const SizedBox(height: 16), - // Imagen/selector móvil - GestureDetector( - onTap: () async { - await widget.provider.selectImagen(); - setState(() {}); - }, - child: Container( - width: 100, - height: 100, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: Colors.white.withOpacity(0.3), width: 2), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(13), - child: widget.provider.imagenToUpload != null - ? Image.memory( - widget.provider.imagenToUpload!, - fit: BoxFit.cover, - ) - : Container( - color: Colors.white.withOpacity(0.1), - child: const Icon( - Icons.add_photo_alternate, - color: Colors.white, - size: 40, - ), - ), - ), - ), - ), - const SizedBox(height: 8), - Text( - widget.provider.imagenToUpload != null - ? 'Toca para cambiar imagen' - : 'Toca para añadir imagen', - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 12, - ), - ), - ], - ), - ), - ), - - // Contenido del formulario para móvil - Expanded( - child: Form( - key: _formKey, - child: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - // Estados en móvil - Row( - children: [ - Expanded( - child: _buildStatusIndicator( - _activo ? 'Activo' : 'Inactivo', - _activo ? Icons.check_circle : Icons.cancel, - _activo ? Colors.green : Colors.red, - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatusIndicator( - _enUso ? 'En Uso' : 'Libre', - _enUso ? Icons.trending_up : Icons.trending_flat, - _enUso ? Colors.orange : Colors.grey, - ), - ), - ], - ), - - const SizedBox(height: 20), - - // Campos del formulario - _buildCompactFormField( - controller: _nombreController, - label: 'Nombre del Componente', - hint: 'Ej: Switch Principal MDF', - icon: Icons.devices_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es obligatorio'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - _buildCategoriaDropdown(), - - const SizedBox(height: 16), - - _buildCompactFormField( - controller: _ubicacionController, - label: 'Ubicación', - hint: 'Ej: MDF Principal - Rack 1', - icon: Icons.location_on_rounded, - ), - - const SizedBox(height: 16), - - _buildCompactFormField( - controller: _descripcionController, - label: 'Descripción', - hint: 'Descripción detallada del componente', - icon: Icons.description_rounded, - maxLines: 3, - ), - - const SizedBox(height: 20), - - // Switches en móvil - _buildSwitchCard( - title: 'Componente Activo', - subtitle: 'El componente está operativo', - value: _activo, - onChanged: (value) { - setState(() { - _activo = value; - }); - }, - icon: Icons.power_settings_new, - activeColor: Colors.green, - ), - - const SizedBox(height: 12), - - _buildSwitchCard( - title: 'En Uso', - subtitle: 'El componente está siendo utilizado', - value: _enUso, - onChanged: (value) { - setState(() { - _enUso = value; - }); - }, - icon: Icons.work, - activeColor: Colors.orange, - ), - - const SizedBox(height: 30), - - // Botones para móvil - Column( - children: [ - Container( - width: double.infinity, - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.4), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: ElevatedButton.icon( - onPressed: _isLoading ? null : _guardarComponente, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - icon: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Colors.white), - ), - ) - : const Icon(Icons.save_rounded, - color: Colors.white, size: 20), - label: Text( - _isLoading ? 'Guardando...' : 'Crear Componente', - style: const TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ), - ), - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: _isLoading - ? null - : () => Navigator.of(context).pop(), - icon: const Icon(Icons.close, size: 18), - label: const Text('Cancelar'), - style: OutlinedButton.styleFrom( - foregroundColor: AppTheme.of(context).secondaryText, - side: BorderSide( - color: AppTheme.of(context) - .secondaryText - .withOpacity(0.5), - ), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - } - - Widget _buildCompactFormField({ - required TextEditingController controller, - required String label, - required String hint, - required IconData icon, - String? Function(String?)? validator, - int? maxLines, - }) { - return Container( - margin: const EdgeInsets.only(bottom: 4), - child: TextFormField( - controller: controller, - validator: validator, - maxLines: maxLines ?? 1, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - ), - decoration: InputDecoration( - labelText: label, - labelStyle: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - ), - hintText: hint, - hintStyle: TextStyle( - color: AppTheme.of(context).secondaryText.withOpacity(0.7), - fontSize: 13, - ), - prefixIcon: Container( - margin: const EdgeInsets.all(8), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Icon( - icon, - color: Colors.white, - size: 20, - ), - ), - filled: true, - fillColor: AppTheme.of(context).secondaryBackground, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - width: 1, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor, - width: 2, - ), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: const BorderSide(color: Colors.red, width: 1), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - ), - ), - ); - } - - Widget _buildCategoriaDropdown() { - return Container( - margin: const EdgeInsets.only(bottom: 4), - child: Theme( - data: Theme.of(context).copyWith( - canvasColor: AppTheme.of(context).secondaryBackground, - shadowColor: AppTheme.of(context).primaryColor.withOpacity(0.3), - ), - child: DropdownButtonFormField( - value: _categoriaSeleccionada, - onChanged: (value) { - if (value != null) { - setState(() { - _categoriaSeleccionada = value; - }); - } - }, - validator: (value) { - if (value == null) { - return 'Seleccione una categoría'; - } - return null; - }, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - ), - icon: Icon( - Icons.arrow_drop_down, - color: AppTheme.of(context).primaryColor, - ), - iconSize: 24, - isExpanded: true, // Esto soluciona el overflow - menuMaxHeight: 300, - decoration: InputDecoration( - labelText: 'Categoría', - labelStyle: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - ), - prefixIcon: Container( - margin: const EdgeInsets.all(8), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: const Icon( - Icons.category_rounded, - color: Colors.white, - size: 20, - ), - ), - filled: true, - fillColor: AppTheme.of(context).secondaryBackground, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - width: 1, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor, - width: 2, - ), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - ), - dropdownColor: AppTheme.of(context).secondaryBackground, - items: widget.provider.categorias.map((categoria) { - return DropdownMenuItem( - value: categoria.id, - child: Container( - constraints: const BoxConstraints(minHeight: 48), - child: Row( - children: [ - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - categoria.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.w500, - fontSize: 14, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ); - }).toList(), - ), - ), - ); - } - - Widget _buildSwitchCard({ - required String title, - required String subtitle, - required bool value, - required Function(bool) onChanged, - required IconData icon, - required Color activeColor, - }) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: value - ? activeColor.withOpacity(0.3) - : AppTheme.of(context).primaryColor.withOpacity(0.1), - ), - boxShadow: [ - BoxShadow( - color: value - ? activeColor.withOpacity(0.1) - : Colors.black.withOpacity(0.02), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: (value ? activeColor : Colors.grey).withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - icon, - color: value ? activeColor : Colors.grey, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - Text( - subtitle, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - ), - ], - ), - ), - Switch( - value: value, - onChanged: onChanged, - activeColor: activeColor, - activeTrackColor: activeColor.withOpacity(0.3), - inactiveThumbColor: Colors.grey, - inactiveTrackColor: Colors.grey.withOpacity(0.3), - ), - ], - ), - ); - } - - Widget _buildStatusIndicator(String text, IconData icon, Color color) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withOpacity(0.3)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, color: color, size: 16), - const SizedBox(width: 6), - Text( - text, - style: TextStyle( - color: color, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - } - - Future _guardarComponente() async { - if (!_formKey.currentState!.validate()) { - return; - } - - if (_categoriaSeleccionada == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Debe seleccionar una categoría'), - backgroundColor: Colors.red, - ), - ); - return; - } - - if (widget.provider.negocioSeleccionadoId == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Error: No se ha seleccionado un negocio'), - backgroundColor: Colors.red, - ), - ); - return; - } - - setState(() { - _isLoading = true; - }); - - try { - final success = await widget.provider.crearComponente( - negocioId: widget.provider.negocioSeleccionadoId!, - categoriaId: _categoriaSeleccionada!, - nombre: _nombreController.text.trim(), - descripcion: _descripcionController.text.trim().isNotEmpty - ? _descripcionController.text.trim() - : null, - enUso: _enUso, - activo: _activo, - ubicacion: _ubicacionController.text.trim().isNotEmpty - ? _ubicacionController.text.trim() - : null, - ); - - if (success) { - if (mounted) { - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.check_circle, color: Colors.white), - SizedBox(width: 12), - Text( - 'Componente creado exitosamente', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.green, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } else { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.error, color: Colors.white), - SizedBox(width: 12), - Text( - 'Error al crear el componente', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(Icons.warning, color: Colors.white), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Error: $e', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } -} diff --git a/lib/pages/infrastructure/widgets/componentes_cards_view.dart b/lib/pages/infrastructure/widgets/componentes_cards_view.dart deleted file mode 100644 index b4774d8..0000000 --- a/lib/pages/infrastructure/widgets/componentes_cards_view.dart +++ /dev/null @@ -1,741 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; -import 'package:nethive_neo/pages/infrastructure/widgets/edit_componente_dialog.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class ComponentesCardsView extends StatefulWidget { - const ComponentesCardsView({Key? key}) : super(key: key); - - @override - State createState() => _ComponentesCardsViewState(); -} - -class _ComponentesCardsViewState extends State - with TickerProviderStateMixin { - late AnimationController _animationController; - late Animation _fadeAnimation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - duration: const Duration(milliseconds: 800), - vsync: this, - ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); - _animationController.forward(); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: _fadeAnimation, - child: Consumer( - builder: (context, componentesProvider, child) { - if (componentesProvider.componentes.isEmpty) { - return _buildEmptyState(); - } - - return Container( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header con filtros - _buildMobileHeader(componentesProvider), - - const SizedBox(height: 16), - - // Lista de tarjetas - Expanded( - child: ListView.builder( - itemCount: componentesProvider.componentes.length, - itemBuilder: (context, index) { - final componente = componentesProvider.componentes[index]; - return TweenAnimationBuilder( - duration: Duration(milliseconds: 300 + (index * 100)), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, value, child) { - return Transform.translate( - offset: Offset(0, 30 * (1 - value)), - child: Opacity( - opacity: value, - child: _buildComponenteCard( - componente, componentesProvider), - ), - ); - }, - ); - }, - ), - ), - ], - ), - ); - }, - ), - ); - } - - Widget _buildMobileHeader(ComponentesProvider componentesProvider) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.inventory_2, - color: Colors.white, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Inventario MDF/IDF', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - Text( - '${componentesProvider.componentes.length} componentes', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 12, - ), - ), - ], - ), - ), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: const Text( - 'Móvil', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - - const SizedBox(height: 12), - - // Buscador móvil - Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), - child: TextField( - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - hintText: 'Buscar componentes...', - hintStyle: TextStyle(color: Colors.white.withOpacity(0.7)), - prefixIcon: Icon( - Icons.search, - color: Colors.white.withOpacity(0.8), - size: 20, - ), - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - ), - onChanged: (value) { - componentesProvider.buscarComponentes(value); - }, - ), - ), - ], - ), - ); - } - - Widget _buildComponenteCard( - dynamic componente, ComponentesProvider componentesProvider) { - // Buscar la categoría del componente - final categoria = componentesProvider.categorias - .where((cat) => cat.id == componente.categoriaId) - .firstOrNull; - - return Container( - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - _showComponenteDetails(componente, categoria); - }, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header de la tarjeta - Row( - children: [ - // Imagen del componente - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: - AppTheme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: componente.imagenUrl != null && - componente.imagenUrl!.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - componente.imagenUrl!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Icon( - Icons.devices, - color: AppTheme.of(context).primaryColor, - size: 24, - ); - }, - ), - ) - : Icon( - Icons.devices, - color: AppTheme.of(context).primaryColor, - size: 24, - ), - ), - - const SizedBox(width: 12), - - // Info principal - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - componente.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - if (categoria != null) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.1), - borderRadius: BorderRadius.circular(6), - ), - child: Text( - categoria.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 10, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - - // Estados - Column( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: - (componente.activo ? Colors.green : Colors.red) - .withOpacity(0.1), - borderRadius: BorderRadius.circular(6), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - componente.activo - ? Icons.check_circle - : Icons.cancel, - color: componente.activo - ? Colors.green - : Colors.red, - size: 10, - ), - const SizedBox(width: 2), - Text( - componente.activo ? 'Activo' : 'Inactivo', - style: TextStyle( - color: componente.activo - ? Colors.green - : Colors.red, - fontSize: 9, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: - (componente.enUso ? Colors.orange : Colors.grey) - .withOpacity(0.1), - borderRadius: BorderRadius.circular(6), - ), - child: Text( - componente.enUso ? 'En Uso' : 'Libre', - style: TextStyle( - color: componente.enUso - ? Colors.orange - : Colors.grey, - fontSize: 9, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ], - ), - - const SizedBox(height: 12), - - // Información adicional - if (componente.descripcion != null && - componente.descripcion!.isNotEmpty) - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppTheme.of(context).tertiaryBackground, - borderRadius: BorderRadius.circular(6), - ), - child: Text( - componente.descripcion!, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - - const SizedBox(height: 8), - - // Footer con ubicación y acciones - Row( - children: [ - if (componente.ubicacion != null && - componente.ubicacion!.isNotEmpty) ...[ - Icon( - Icons.location_on, - color: AppTheme.of(context).primaryColor, - size: 14, - ), - const SizedBox(width: 4), - Expanded( - child: Text( - componente.ubicacion!, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 11, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ] else - Expanded( - child: Text( - 'Sin ubicación específica', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 11, - fontStyle: FontStyle.italic, - ), - ), - ), - - // Botones de acción - Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildActionButton( - icon: Icons.visibility, - color: Colors.blue, - onTap: () => - _showComponenteDetails(componente, categoria), - ), - const SizedBox(width: 4), - _buildActionButton( - icon: Icons.edit, - color: AppTheme.of(context).primaryColor, - onTap: () { - _showEditComponenteDialog(componente); - }, - ), - const SizedBox(width: 4), - _buildActionButton( - icon: Icons.delete, - color: Colors.red, - onTap: () => _confirmDelete(componente), - ), - ], - ), - ], - ), - ], - ), - ), - ), - ), - ); - } - - Widget _buildActionButton({ - required IconData icon, - required Color color, - required VoidCallback onTap, - }) { - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(4), - child: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Icon( - icon, - color: color, - size: 14, - ), - ), - ); - } - - Widget _buildEmptyState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: Icon( - Icons.inventory_2, - color: AppTheme.of(context).primaryColor, - size: 48, - ), - ), - const SizedBox(height: 16), - Text( - 'No hay componentes', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - 'No se encontraron componentes\npara este negocio', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - ), - ), - ], - ), - ); - } - - void _showComponenteDetails(dynamic componente, dynamic categoria) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => Container( - height: MediaQuery.of(context).size.height * 0.8, - decoration: BoxDecoration( - color: AppTheme.of(context).primaryBackground, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Column( - children: [ - // Handle - Container( - margin: const EdgeInsets.only(top: 8), - width: 40, - height: 4, - decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.5), - borderRadius: BorderRadius.circular(2), - ), - ), - - // Header - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Row( - children: [ - Container( - width: 60, - height: 60, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: componente.imagenUrl != null && - componente.imagenUrl!.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.network( - componente.imagenUrl!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return const Icon( - Icons.devices, - color: Colors.white, - size: 30, - ); - }, - ), - ) - : const Icon( - Icons.devices, - color: Colors.white, - size: 30, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - componente.nombre, - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - if (categoria != null) - Text( - categoria.nombre, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - ), - ), - ], - ), - ), - IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.close, color: Colors.white), - ), - ], - ), - ), - - // Contenido - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDetailRow( - 'ID', componente.id.substring(0, 8) + '...'), - _buildDetailRow( - 'Estado', componente.activo ? 'Activo' : 'Inactivo'), - _buildDetailRow('En Uso', componente.enUso ? 'Sí' : 'No'), - if (componente.ubicacion != null && - componente.ubicacion!.isNotEmpty) - _buildDetailRow('Ubicación', componente.ubicacion!), - if (componente.descripcion != null && - componente.descripcion!.isNotEmpty) - _buildDetailRow('Descripción', componente.descripcion!), - _buildDetailRow( - 'Fecha de Registro', - componente.fechaRegistro?.toString().split(' ')[0] ?? - 'No disponible'), - ], - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildDetailRow(String label, String value) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 100, - child: Text( - label, - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ), - Expanded( - child: Text( - value, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 12, - ), - ), - ), - ], - ), - ); - } - - void _confirmDelete(dynamic componente) { - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: AppTheme.of(context).primaryBackground, - title: const Text('Eliminar Componente'), - content: Text('¿Deseas eliminar "${componente.nombre}"?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - 'Cancelar', - style: TextStyle(color: AppTheme.of(context).secondaryText), - ), - ), - TextButton( - onPressed: () { - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Eliminar próximamente')), - ); - }, - child: const Text('Eliminar', style: TextStyle(color: Colors.red)), - ), - ], - ), - ); - } - - void _showEditComponenteDialog(dynamic componente) { - final provider = Provider.of(context, listen: false); - showDialog( - context: context, - builder: (context) => EditComponenteDialog( - provider: provider, - componente: componente, - ), - ); - } -} diff --git a/lib/pages/infrastructure/widgets/edit_componente_dialog.dart b/lib/pages/infrastructure/widgets/edit_componente_dialog.dart deleted file mode 100644 index 7b362ba..0000000 --- a/lib/pages/infrastructure/widgets/edit_componente_dialog.dart +++ /dev/null @@ -1,1497 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/helpers/globals.dart'; -import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; -import 'package:nethive_neo/models/nethive/componente_model.dart'; - -class EditComponenteDialog extends StatefulWidget { - final ComponentesProvider provider; - final Componente componente; - - const EditComponenteDialog({ - Key? key, - required this.provider, - required this.componente, - }) : super(key: key); - - @override - State createState() => _EditComponenteDialogState(); -} - -class _EditComponenteDialogState extends State - with TickerProviderStateMixin { - final _formKey = GlobalKey(); - late TextEditingController _nombreController; - late TextEditingController _descripcionController; - late TextEditingController _ubicacionController; - - bool _isLoading = false; - late AnimationController _scaleController; - late AnimationController _slideController; - late AnimationController _fadeController; - late Animation _scaleAnimation; - late Animation _slideAnimation; - late Animation _fadeAnimation; - bool _isAnimationInitialized = false; - - // Variables del formulario - late int _categoriaSeleccionada; - late bool _activo; - late bool _enUso; - bool _actualizarImagen = false; - - @override - void initState() { - super.initState(); - _initializeControllers(); - _initializeAnimations(); - // Escuchar cambios del provider - widget.provider.addListener(_onProviderChanged); - } - - void _initializeControllers() { - _nombreController = TextEditingController(text: widget.componente.nombre); - _descripcionController = - TextEditingController(text: widget.componente.descripcion ?? ''); - _ubicacionController = - TextEditingController(text: widget.componente.ubicacion ?? ''); - - _categoriaSeleccionada = widget.componente.categoriaId; - _activo = widget.componente.activo; - _enUso = widget.componente.enUso; - } - - void _onProviderChanged() { - if (mounted) { - setState(() { - // Forzar rebuild cuando cambie el provider - }); - } - } - - void _initializeAnimations() { - _scaleController = AnimationController( - duration: const Duration(milliseconds: 600), - vsync: this, - ); - _slideController = AnimationController( - duration: const Duration(milliseconds: 800), - vsync: this, - ); - _fadeController = AnimationController( - duration: const Duration(milliseconds: 400), - vsync: this, - ); - - _scaleAnimation = CurvedAnimation( - parent: _scaleController, - curve: Curves.elasticOut, - ); - _slideAnimation = Tween( - begin: const Offset(0, -1), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _slideController, - curve: Curves.easeOutBack, - )); - _fadeAnimation = CurvedAnimation( - parent: _fadeController, - curve: Curves.easeInOut, - ); - - // Pequeño delay para asegurar que el widget esté completamente montado - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _isAnimationInitialized = true; - }); - _startAnimations(); - } - }); - } - - void _startAnimations() { - _fadeController.forward(); - Future.delayed(const Duration(milliseconds: 100), () { - if (mounted) _scaleController.forward(); - }); - Future.delayed(const Duration(milliseconds: 200), () { - if (mounted) _slideController.forward(); - }); - } - - @override - void dispose() { - widget.provider.removeListener(_onProviderChanged); - _scaleController.dispose(); - _slideController.dispose(); - _fadeController.dispose(); - _nombreController.dispose(); - _descripcionController.dispose(); - _ubicacionController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (!_isAnimationInitialized) { - return const SizedBox.shrink(); - } - - // Detectar el tamaño de pantalla con mejor precisión - final screenSize = MediaQuery.of(context).size; - final isDesktop = screenSize.width > 1024; - final isTablet = screenSize.width > 768 && screenSize.width <= 1024; - final isMobile = screenSize.width <= 768; - - // Ajustar dimensiones según el tipo de pantalla para mejor responsividad - double maxWidth; - double maxHeight; - EdgeInsets insetPadding; - - if (isMobile) { - // Configuración específica para smartphones - maxWidth = screenSize.width * 0.95; // 95% del ancho de pantalla - maxHeight = screenSize.height * 0.9; // 90% del alto de pantalla - insetPadding = const EdgeInsets.all(10); - } else if (isTablet) { - // Configuración para tablets - maxWidth = 750.0; - maxHeight = 700.0; - insetPadding = const EdgeInsets.all(20); - } else { - // Configuración para desktop - maxWidth = 1000.0; - maxHeight = 750.0; - insetPadding = const EdgeInsets.all(40); - } - - return AnimatedBuilder( - animation: - Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]), - builder: (context, child) { - return FadeTransition( - opacity: _fadeAnimation, - child: Dialog( - backgroundColor: Colors.transparent, - insetPadding: insetPadding, - child: Transform.scale( - scale: _scaleAnimation.value, - child: Container( - width: maxWidth, - height: maxHeight, - constraints: BoxConstraints( - maxWidth: maxWidth, - maxHeight: maxHeight, - minHeight: isMobile ? 400 : (isDesktop ? 650 : 500), - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(isMobile ? 20 : 30), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.4), - blurRadius: 40, - offset: const Offset(0, 20), - spreadRadius: 8, - ), - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 60, - offset: const Offset(0, 10), - spreadRadius: 2, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(isMobile ? 20 : 30), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryBackground, - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - stops: const [0.0, 0.6, 1.0], - ), - ), - child: isDesktop - ? _buildDesktopLayout() - : _buildMobileLayout(isMobile), - ), - ), - ), - ), - ), - ); - }, - ); - } - - Widget _buildDesktopLayout() { - final categoria = widget.provider.getCategoriaById(_categoriaSeleccionada); - - return Row( - children: [ - // Header lateral compacto para desktop - Container( - width: 300, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.5), - blurRadius: 25, - offset: const Offset(5, 0), - ), - ], - ), - child: SlideTransition( - position: _slideAnimation, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 25), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Imagen del componente - Container( - width: 120, - height: 120, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - Colors.white.withOpacity(0.4), - Colors.white.withOpacity(0.1), - Colors.transparent, - ], - ), - border: Border.all( - color: Colors.white.withOpacity(0.6), - width: 3, - ), - boxShadow: [ - BoxShadow( - color: Colors.white.withOpacity(0.4), - blurRadius: 20, - spreadRadius: 5, - ), - ], - ), - child: ClipOval( - child: widget.componente.imagenUrl != null && - widget.componente.imagenUrl!.isNotEmpty - ? Image.network( - "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/componentes/${widget.componente.imagenUrl}", - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - padding: const EdgeInsets.all(20), - child: const Icon( - Icons.devices, - color: Colors.white, - size: 40, - ), - ); - }, - ) - : Container( - padding: const EdgeInsets.all(20), - child: const Icon( - Icons.devices, - color: Colors.white, - size: 40, - ), - ), - ), - ), - const SizedBox(height: 20), - - // Título compacto - Text( - 'Editar Componente', - style: TextStyle( - color: Colors.white, - fontSize: 22, - fontWeight: FontWeight.bold, - letterSpacing: 1.5, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - - Container( - padding: - const EdgeInsets.symmetric(horizontal: 15, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: Colors.white.withOpacity(0.3), - ), - ), - child: Text( - '📝 ${widget.componente.nombre}', - style: TextStyle( - color: Colors.white.withOpacity(0.95), - fontSize: 14, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - - const SizedBox(height: 16), - - // Info adicional - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - children: [ - Text( - 'Categoría: ${categoria?.nombre ?? 'N/A'}', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 4), - Text( - 'ID: ${widget.componente.id.substring(0, 8)}...', - style: TextStyle( - color: Colors.white.withOpacity(0.7), - fontSize: 10, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ], - ), - ), - ), - ), - - // Contenido principal del formulario - Expanded( - child: Padding( - padding: const EdgeInsets.all(25), - child: Form( - key: _formKey, - child: Column( - children: [ - // Formulario en columnas para aprovechar el espacio - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - // Primera fila - Nombre y Categoría - Row( - children: [ - Expanded( - flex: 2, - child: _buildCompactFormField( - controller: _nombreController, - label: 'Nombre del componente', - hint: 'Ej: Switch Core Principal', - icon: Icons.devices_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es requerido'; - } - return null; - }, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildCategoriaDropdown(), - ), - ], - ), - - const SizedBox(height: 16), - - // Segunda fila - Ubicación - _buildCompactFormField( - controller: _ubicacionController, - label: 'Ubicación', - hint: 'Ej: MDF Principal - Rack 1', - icon: Icons.location_on_rounded, - validator: (value) { - // La ubicación es opcional - return null; - }, - ), - - const SizedBox(height: 16), - - // Tercera fila - Descripción - _buildCompactFormField( - controller: _descripcionController, - label: 'Descripción', - hint: 'Descripción detallada del componente', - icon: Icons.description_rounded, - maxLines: 3, - validator: (value) { - // La descripción es opcional - return null; - }, - ), - - const SizedBox(height: 20), - - // Switches de estado - Row( - children: [ - Expanded( - child: _buildStatusSwitch( - 'Activo', - 'El componente está operativo', - _activo, - (value) => setState(() => _activo = value), - Icons.power_settings_new, - Colors.green, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildStatusSwitch( - 'En Uso', - 'El componente está siendo utilizado', - _enUso, - (value) => setState(() => _enUso = value), - Icons.trending_up, - Colors.orange, - ), - ), - ], - ), - - const SizedBox(height: 20), - - // Sección de imagen - _buildImageSection(), - - const SizedBox(height: 25), - - // Botones de acción - Row( - children: [ - // Botón cancelar - Expanded( - child: Container( - height: 50, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: AppTheme.of(context) - .secondaryText - .withOpacity(0.4), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: TextButton( - onPressed: _isLoading - ? null - : () { - widget.provider.resetFormData(); - Navigator.of(context).pop(); - }, - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon( - Icons.close_rounded, - color: AppTheme.of(context) - .secondaryText, - size: 18, - ), - const SizedBox(width: 8), - Text( - 'Cancelar', - style: TextStyle( - color: AppTheme.of(context) - .secondaryText, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ), - - const SizedBox(width: 20), - - // Botón guardar cambios - Expanded( - flex: 2, - child: Container( - height: 50, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.5), - blurRadius: 20, - offset: const Offset(0, 8), - spreadRadius: 2, - ), - ], - ), - child: ElevatedButton( - onPressed: - _isLoading ? null : _guardarCambios, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - child: _isLoading - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 3, - valueColor: - AlwaysStoppedAnimation( - Colors.white), - ), - ) - : Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: const [ - Icon( - Icons.save_rounded, - color: Colors.white, - size: 20, - ), - SizedBox(width: 12), - Text( - 'Guardar Cambios', - style: TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ], - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ], - ), - ), - ), - ), - ], - ); - } - - Widget _buildMobileLayout(bool isMobile) { - final categoria = widget.provider.getCategoriaById(_categoriaSeleccionada); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header espectacular con animación - SlideTransition( - position: _slideAnimation, - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 25), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.5), - blurRadius: 25, - offset: const Offset(0, 15), - spreadRadius: 2, - ), - ], - ), - child: Column( - children: [ - // Imagen del componente - Container( - width: 100, - height: 100, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - Colors.white.withOpacity(0.4), - Colors.white.withOpacity(0.1), - Colors.transparent, - ], - ), - border: Border.all( - color: Colors.white.withOpacity(0.6), - width: 3, - ), - boxShadow: [ - BoxShadow( - color: Colors.white.withOpacity(0.4), - blurRadius: 20, - spreadRadius: 5, - ), - ], - ), - child: ClipOval( - child: widget.componente.imagenUrl != null && - widget.componente.imagenUrl!.isNotEmpty - ? Image.network( - "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/componentes/${widget.componente.imagenUrl}", - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - padding: const EdgeInsets.all(20), - child: const Icon( - Icons.devices, - color: Colors.white, - size: 35, - ), - ); - }, - ) - : Container( - padding: const EdgeInsets.all(20), - child: const Icon( - Icons.devices, - color: Colors.white, - size: 35, - ), - ), - ), - ), - - const SizedBox(height: 16), - - // Título - Text( - 'Editar Componente', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - letterSpacing: 1.5, - ), - ), - - const SizedBox(height: 8), - - Container( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: Colors.white.withOpacity(0.3), - ), - ), - child: Text( - '📝 ${widget.componente.nombre}', - style: TextStyle( - color: Colors.white.withOpacity(0.95), - fontSize: 14, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - - // Contenido del formulario - Flexible( - child: SingleChildScrollView( - padding: const EdgeInsets.all(25), - child: Form( - key: _formKey, - child: Column( - children: [ - // Campos del formulario - _buildCompactFormField( - controller: _nombreController, - label: 'Nombre del componente', - hint: 'Ej: Switch Core Principal', - icon: Icons.devices_rounded, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es requerido'; - } - return null; - }, - ), - - _buildCategoriaDropdown(), - - _buildCompactFormField( - controller: _ubicacionController, - label: 'Ubicación', - hint: 'Ej: MDF Principal - Rack 1', - icon: Icons.location_on_rounded, - ), - - _buildCompactFormField( - controller: _descripcionController, - label: 'Descripción', - hint: 'Descripción detallada del componente', - icon: Icons.description_rounded, - maxLines: 3, - ), - - const SizedBox(height: 20), - - // Switches de estado - _buildStatusSwitch( - 'Activo', - 'El componente está operativo', - _activo, - (value) => setState(() => _activo = value), - Icons.power_settings_new, - Colors.green, - ), - - const SizedBox(height: 12), - - _buildStatusSwitch( - 'En Uso', - 'El componente está siendo utilizado', - _enUso, - (value) => setState(() => _enUso = value), - Icons.trending_up, - Colors.orange, - ), - - const SizedBox(height: 20), - - // Sección de imagen - _buildImageSection(), - - const SizedBox(height: 25), - - // Botones de acción - Row( - children: [ - // Botón cancelar - Expanded( - child: Container( - height: 50, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: AppTheme.of(context) - .secondaryText - .withOpacity(0.4), - width: 2, - ), - ), - child: TextButton( - onPressed: _isLoading - ? null - : () { - widget.provider.resetFormData(); - Navigator.of(context).pop(); - }, - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.close_rounded, - color: AppTheme.of(context).secondaryText, - size: 18, - ), - const SizedBox(width: 8), - Text( - 'Cancelar', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 15, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ), - - const SizedBox(width: 16), - - // Botón guardar - Expanded( - flex: 2, - child: Container( - height: 50, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor, - AppTheme.of(context).secondaryColor, - AppTheme.of(context).tertiaryColor, - ], - ), - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context) - .primaryColor - .withOpacity(0.5), - blurRadius: 20, - offset: const Offset(0, 8), - spreadRadius: 2, - ), - ], - ), - child: ElevatedButton( - onPressed: _isLoading ? null : _guardarCambios, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - child: _isLoading - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 3, - valueColor: AlwaysStoppedAnimation( - Colors.white), - ), - ) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon( - Icons.save_rounded, - color: Colors.white, - size: 20, - ), - SizedBox(width: 10), - Text( - 'Guardar', - style: TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ], - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ); - } - - Widget _buildCompactFormField({ - required TextEditingController controller, - required String label, - required String hint, - required IconData icon, - int maxLines = 1, - TextInputType? keyboardType, - String? Function(String?)? validator, - }) { - return Container( - margin: const EdgeInsets.only(bottom: 16), - child: TextFormField( - controller: controller, - maxLines: maxLines, - keyboardType: keyboardType, - validator: validator, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - decoration: InputDecoration( - labelText: label, - hintText: hint, - prefixIcon: Container( - margin: const EdgeInsets.all(8), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - child: Icon( - icon, - color: Colors.white, - size: 18, - ), - ), - labelStyle: TextStyle( - color: AppTheme.of(context).primaryColor, - fontWeight: FontWeight.w600, - fontSize: 14, - ), - hintStyle: TextStyle( - color: AppTheme.of(context).secondaryText.withOpacity(0.7), - fontSize: 12, - ), - filled: true, - fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.7), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor, - width: 2, - ), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: const BorderSide( - color: Colors.red, - width: 2, - ), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - ), - ), - ); - } - - Widget _buildCategoriaDropdown() { - return Container( - margin: const EdgeInsets.only(bottom: 16), - child: DropdownButtonFormField( - value: _categoriaSeleccionada, - onChanged: (value) { - if (value != null) { - setState(() { - _categoriaSeleccionada = value; - }); - } - }, - validator: (value) { - if (value == null) { - return 'Seleccione una categoría'; - } - return null; - }, - decoration: InputDecoration( - labelText: 'Categoría', - prefixIcon: Container( - margin: const EdgeInsets.all(8), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - child: const Icon( - Icons.category_rounded, - color: Colors.white, - size: 18, - ), - ), - labelStyle: TextStyle( - color: AppTheme.of(context).primaryColor, - fontWeight: FontWeight.w600, - fontSize: 14, - ), - filled: true, - fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.7), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: BorderSide( - color: AppTheme.of(context).primaryColor, - width: 2, - ), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - ), - items: widget.provider.categorias.map((categoria) { - return DropdownMenuItem( - value: categoria.id, - child: Container( - child: Text( - categoria.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - ), - ), - ), - ); - }).toList(), - ), - ); - } - - Widget _buildStatusSwitch( - String title, - String subtitle, - bool value, - ValueChanged onChanged, - IconData icon, - Color color, - ) { - return Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - color.withOpacity(0.1), - color.withOpacity(0.05), - ], - ), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: color.withOpacity(0.3), - width: 2, - ), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: color.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - icon, - color: color, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - Text( - subtitle, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - ), - ], - ), - ), - Switch( - value: value, - onChanged: onChanged, - activeColor: color, - activeTrackColor: color.withOpacity(0.3), - inactiveThumbColor: Colors.grey, - inactiveTrackColor: Colors.grey.withOpacity(0.3), - ), - ], - ), - ); - } - - Widget _buildImageSection() { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor.withOpacity(0.1), - AppTheme.of(context).tertiaryColor.withOpacity(0.1), - AppTheme.of(context).secondaryColor.withOpacity(0.05), - ], - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.4), - width: 2, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header de la sección - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - ), - child: const Icon( - Icons.image_rounded, - color: Colors.white, - size: 18, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Imagen del Componente', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: AppTheme.of(context).primaryText, - ), - ), - Text( - 'Actualizar imagen (opcional)', - style: TextStyle( - fontSize: 12, - color: AppTheme.of(context).secondaryText, - ), - ), - ], - ), - ), - ], - ), - - const SizedBox(height: 16), - - // Botón para seleccionar imagen - GestureDetector( - onTap: () async { - await widget.provider.selectImagen(); - setState(() { - _actualizarImagen = widget.provider.imagenToUpload != null; - }); - }, - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 2, - ), - color: AppTheme.of(context).secondaryBackground, - ), - child: Column( - children: [ - // Preview de imagen - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: - AppTheme.of(context).primaryColor.withOpacity(0.4), - width: 2, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: widget.provider.imagenToUpload != null - ? widget.provider.getImageWidget( - widget.provider.imagenToUpload, - height: 80, - width: 80, - ) - : widget.componente.imagenUrl != null && - widget.componente.imagenUrl!.isNotEmpty - ? Image.network( - "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/componentes/${widget.componente.imagenUrl}", - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - padding: const EdgeInsets.all(20), - child: Icon( - Icons.devices, - color: - AppTheme.of(context).primaryColor, - size: 24, - ), - ); - }, - ) - : Container( - padding: const EdgeInsets.all(20), - child: Icon( - Icons.devices, - color: AppTheme.of(context).primaryColor, - size: 24, - ), - ), - ), - ), - - const SizedBox(height: 12), - - // Texto explicativo - Text( - widget.provider.imagenFileName ?? - 'Toca para seleccionar imagen', - style: TextStyle( - color: widget.provider.imagenFileName != null - ? AppTheme.of(context).primaryColor - : AppTheme.of(context).secondaryText, - fontSize: 14, - fontWeight: widget.provider.imagenFileName != null - ? FontWeight.w600 - : FontWeight.normal, - ), - textAlign: TextAlign.center, - ), - - if (widget.provider.imagenFileName == null) - Text( - 'PNG, JPG (Max 2MB)', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ], - ), - ); - } - - Future _guardarCambios() async { - if (!_formKey.currentState!.validate()) return; - - setState(() { - _isLoading = true; - }); - - try { - final success = await widget.provider.actualizarComponente( - componenteId: widget.componente.id, - negocioId: widget.componente.negocioId, - categoriaId: _categoriaSeleccionada, - nombre: _nombreController.text.trim(), - descripcion: _descripcionController.text.trim().isEmpty - ? null - : _descripcionController.text.trim(), - enUso: _enUso, - activo: _activo, - ubicacion: _ubicacionController.text.trim().isEmpty - ? null - : _ubicacionController.text.trim(), - actualizarImagen: _actualizarImagen, - ); - - if (mounted) { - if (success) { - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.check_circle, color: Colors.white), - SizedBox(width: 12), - Text( - 'Componente actualizado exitosamente', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.green, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: const [ - Icon(Icons.error, color: Colors.white), - SizedBox(width: 12), - Text( - 'Error al actualizar el componente', - style: TextStyle(fontWeight: FontWeight.w600), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(Icons.warning, color: Colors.white), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Error: $e', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - ], - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } -} diff --git a/lib/pages/infrastructure/widgets/infrastructure_sidemenu.dart b/lib/pages/infrastructure/widgets/infrastructure_sidemenu.dart deleted file mode 100644 index 30f215a..0000000 --- a/lib/pages/infrastructure/widgets/infrastructure_sidemenu.dart +++ /dev/null @@ -1,463 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:provider/provider.dart'; -import 'package:go_router/go_router.dart'; -import 'package:nethive_neo/providers/nethive/navigation_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class InfrastructureSidemenu extends StatefulWidget { - final bool isExpanded; - final VoidCallback onToggle; - - const InfrastructureSidemenu({ - Key? key, - required this.isExpanded, - required this.onToggle, - }) : super(key: key); - - @override - State createState() => _InfrastructureSidemenuState(); -} - -class _InfrastructureSidemenuState extends State - with TickerProviderStateMixin { - late AnimationController _animationController; - late Animation _fadeAnimation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - duration: const Duration(milliseconds: 300), - vsync: this, - ); - _fadeAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); - _animationController.forward(); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: _fadeAnimation, - child: Consumer( - builder: (context, navigationProvider, child) { - return Container( - width: widget.isExpanded ? 280 : 80, - decoration: BoxDecoration( - gradient: AppTheme.of(context).darkBackgroundGradient, - border: Border( - right: BorderSide( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - width: 1, - ), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(2, 0), - ), - ], - ), - child: Column( - children: [ - // Header con logo y toggle - _buildHeader(navigationProvider), - - // Información del negocio seleccionado - if (widget.isExpanded && - navigationProvider.negocioSeleccionado != null) - _buildBusinessInfo(navigationProvider), - - // Lista de opciones del menú - Expanded( - child: _buildMenuItems(navigationProvider), - ), - - // Footer con información adicional - if (widget.isExpanded) _buildFooter(), - ], - ), - ); - }, - ), - ); - } - - Widget _buildHeader(NavigationProvider navigationProvider) { - return Container( - padding: EdgeInsets.all(widget.isExpanded ? 20 : 15), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: Row( - children: [ - // Toggle button - GestureDetector( - onTap: widget.onToggle, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - widget.isExpanded ? Icons.menu_open : Icons.menu, - color: Colors.white, - size: 24, - ), - ), - ), - - if (widget.isExpanded) ...[ - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ShaderMask( - shaderCallback: (bounds) => LinearGradient( - colors: [Colors.white, Colors.white.withOpacity(0.8)], - ).createShader(bounds), - child: Row( - children: [ - Image.asset( - 'assets/images/favicon.png', - width: 32, - height: 32, - ), - const Gap(8), - const Text( - 'NETHIVE', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - letterSpacing: 1.2, - ), - ), - ], - ), - ), - Text( - 'Infraestructura', - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ], - ], - ), - ); - } - - Widget _buildBusinessInfo(NavigationProvider navigationProvider) { - final negocio = navigationProvider.negocioSeleccionado!; - final empresa = navigationProvider.empresaSeleccionada!; - - return Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor.withOpacity(0.1), - AppTheme.of(context).tertiaryColor.withOpacity(0.1), - ], - ), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 1, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: Colors.green, - borderRadius: BorderRadius.circular(6), - ), - child: const Icon( - Icons.business_center, - color: Colors.white, - size: 16, - ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - empresa.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const SizedBox(height: 8), - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: Colors.green, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.green.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - negocio.nombre, - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Text( - '(${negocio.tipoLocal})', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 11, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildMenuItems(NavigationProvider navigationProvider) { - return ListView.builder( - padding: EdgeInsets.symmetric( - vertical: 8, - horizontal: widget.isExpanded ? 16 : 8, - ), - itemCount: navigationProvider.menuItems.length, - itemBuilder: (context, index) { - final menuItem = navigationProvider.menuItems[index]; - final isSelected = - navigationProvider.selectedMenuIndex == menuItem.index; - final isSpecial = menuItem.isSpecial; - - return TweenAnimationBuilder( - duration: Duration(milliseconds: 200 + (index * 50)), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, value, child) { - return Transform.translate( - offset: Offset(-30 * (1 - value), 0), - child: Opacity( - opacity: value, - child: _buildMenuItem( - menuItem, - isSelected, - isSpecial, - navigationProvider, - ), - ), - ); - }, - ); - }, - ); - } - - Widget _buildMenuItem( - NavigationMenuItem menuItem, - bool isSelected, - bool isSpecial, - NavigationProvider navigationProvider, - ) { - return Container( - margin: const EdgeInsets.only(bottom: 4), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => _handleMenuTap(menuItem, navigationProvider), - borderRadius: BorderRadius.circular(12), - child: Container( - padding: EdgeInsets.all(widget.isExpanded ? 12 : 8), - decoration: BoxDecoration( - gradient: isSelected - ? AppTheme.of(context).primaryGradient - : isSpecial - ? LinearGradient( - colors: [ - Colors.orange.withOpacity(0.1), - Colors.deepOrange.withOpacity(0.1), - ], - ) - : null, - borderRadius: BorderRadius.circular(12), - border: isSpecial - ? Border.all( - color: Colors.orange.withOpacity(0.3), - width: 1, - ) - : null, - ), - child: Row( - children: [ - Icon( - menuItem.icon, - color: isSelected - ? Colors.white - : isSpecial - ? Colors.orange - : AppTheme.of(context).primaryText, - size: 20, - ), - if (widget.isExpanded) ...[ - const SizedBox(width: 12), - Expanded( - child: Text( - menuItem.title, - style: TextStyle( - color: isSelected - ? Colors.white - : isSpecial - ? Colors.orange - : AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: - isSelected ? FontWeight.bold : FontWeight.w500, - ), - ), - ), - if (isSelected) - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.arrow_forward_ios, - color: Colors.white, - size: 12, - ), - ), - ], - ], - ), - ), - ), - ), - ); - } - - Widget _buildFooter() { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - AppTheme.of(context).primaryBackground.withOpacity(0.0), - AppTheme.of(context).primaryBackground, - ], - ), - ), - child: Column( - children: [ - Container( - height: 1, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.transparent, - AppTheme.of(context).primaryColor.withOpacity(0.5), - Colors.transparent, - ], - ), - ), - ), - const SizedBox(height: 16), - Row( - children: [ - Icon( - Icons.shield_outlined, - color: AppTheme.of(context).primaryColor, - size: 16, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Conexión segura', - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ], - ), - ); - } - - void _handleMenuTap( - NavigationMenuItem menuItem, NavigationProvider navigationProvider) { - if (menuItem.isSpecial) { - // Si es "Empresas", regresar a la página de empresas - navigationProvider.clearSelection(); - context.go('/'); - } else { - // Cambiar la selección del menú - navigationProvider.setSelectedMenuIndex(menuItem.index); - - // Aquí puedes agregar navegación específica si es necesario - // Por ahora solo cambiaremos la vista en el layout principal - } - } -} diff --git a/lib/pages/infrastructure/widgets/mobile_navigation_modal.dart b/lib/pages/infrastructure/widgets/mobile_navigation_modal.dart deleted file mode 100644 index 133b2f9..0000000 --- a/lib/pages/infrastructure/widgets/mobile_navigation_modal.dart +++ /dev/null @@ -1,569 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:go_router/go_router.dart'; -import 'package:nethive_neo/providers/nethive/navigation_provider.dart'; -import 'package:nethive_neo/theme/theme.dart'; - -class MobileNavigationModal extends StatefulWidget { - const MobileNavigationModal({Key? key}) : super(key: key); - - @override - State createState() => _MobileNavigationModalState(); -} - -class _MobileNavigationModalState extends State - with TickerProviderStateMixin { - late AnimationController _animationController; - late Animation _fadeAnimation; - late Animation _slideAnimation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - duration: const Duration(milliseconds: 400), - vsync: this, - ); - _fadeAnimation = CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - ); - _slideAnimation = Tween( - begin: const Offset(0, -1), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeOutCubic, - )); - - _animationController.forward(); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: _fadeAnimation, - child: Container( - decoration: BoxDecoration( - color: AppTheme.of(context).primaryBackground.withOpacity(0.95), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(30), - bottomRight: Radius.circular(30), - ), - ), - child: Consumer( - builder: (context, navigationProvider, child) { - return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header del modal - /* _buildModalHeader(navigationProvider), */ - - // Lista de opciones de navegación - _buildNavigationOptions(navigationProvider), - - // Información del negocio - _buildBusinessInfo(navigationProvider), - - // Botón para cerrar - _buildCloseButton(), - - // Padding adicional para evitar overflow - const SizedBox(height: 20), - ], - ), - ); - }, - ), - ), - ); - } - - Widget _buildModalHeader(NavigationProvider navigationProvider) { - return SlideTransition( - position: _slideAnimation, - child: Container( - padding: const EdgeInsets.fromLTRB(24, 40, 24, 20), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(30), - bottomRight: Radius.circular(30), - ), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: Row( - children: [ - // Logo animado - TweenAnimationBuilder( - duration: const Duration(milliseconds: 800), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, value, child) { - return Transform.scale( - scale: 0.8 + (0.2 * value), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: Colors.white.withOpacity(0.3), - width: 2, - ), - ), - child: Image.asset( - 'assets/images/favicon.png', - width: 32, - height: 32, - ), - ), - ); - }, - ), - - const SizedBox(width: 16), - - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ShaderMask( - shaderCallback: (bounds) => LinearGradient( - colors: [Colors.white, Colors.white.withOpacity(0.8)], - ).createShader(bounds), - child: const Text( - 'NETHIVE', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - letterSpacing: 1.5, - ), - ), - ), - Text( - 'Infraestructura MDF/IDF', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - - // Botón de cerrar - GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(10), - ), - child: const Icon( - Icons.close, - color: Colors.white, - size: 24, - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildNavigationOptions(NavigationProvider navigationProvider) { - return Container( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Título de sección - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Row( - children: [ - Icon( - Icons.navigation, - color: AppTheme.of(context).primaryColor, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Módulos de Infraestructura', - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - - // Lista de opciones - ...navigationProvider.menuItems.asMap().entries.map((entry) { - final index = entry.key; - final menuItem = entry.value; - final isSelected = - navigationProvider.selectedMenuIndex == menuItem.index; - final isSpecial = menuItem.isSpecial; - - return TweenAnimationBuilder( - duration: Duration(milliseconds: 300 + (index * 100)), - tween: Tween(begin: 0.0, end: 1.0), - builder: (context, value, child) { - return Transform.translate( - offset: Offset(50 * (1 - value), 0), - child: Opacity( - opacity: value, - child: _buildNavigationItem( - menuItem, - isSelected, - isSpecial, - navigationProvider, - ), - ), - ); - }, - ); - }).toList(), - ], - ), - ); - } - - Widget _buildNavigationItem( - NavigationMenuItem menuItem, - bool isSelected, - bool isSpecial, - NavigationProvider navigationProvider, - ) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - gradient: isSelected - ? AppTheme.of(context).primaryGradient - : isSpecial - ? LinearGradient( - colors: [ - Colors.orange.withOpacity(0.1), - Colors.deepOrange.withOpacity(0.1), - ], - ) - : LinearGradient( - colors: [ - AppTheme.of(context).secondaryBackground, - AppTheme.of(context).tertiaryBackground, - ], - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected - ? Colors.white.withOpacity(0.3) - : isSpecial - ? Colors.orange.withOpacity(0.3) - : AppTheme.of(context).primaryColor.withOpacity(0.2), - width: isSelected ? 2 : 1, - ), - boxShadow: [ - BoxShadow( - color: isSelected - ? AppTheme.of(context).primaryColor.withOpacity(0.3) - : Colors.black.withOpacity(0.1), - blurRadius: isSelected ? 15 : 8, - offset: Offset(0, isSelected ? 8 : 3), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => _handleMenuTap(menuItem, navigationProvider), - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: const EdgeInsets.all(20), - child: Row( - children: [ - // Icono del módulo - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: isSelected - ? Colors.white.withOpacity(0.2) - : isSpecial - ? Colors.orange.withOpacity(0.2) - : AppTheme.of(context) - .primaryColor - .withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - menuItem.icon, - color: isSelected - ? Colors.white - : isSpecial - ? Colors.orange - : AppTheme.of(context).primaryColor, - size: 24, - ), - ), - - const SizedBox(width: 16), - - // Información del módulo - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - menuItem.title, - style: TextStyle( - color: isSelected - ? Colors.white - : isSpecial - ? Colors.orange - : AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - _getMenuItemDescription(menuItem), - style: TextStyle( - color: isSelected - ? Colors.white.withOpacity(0.8) - : isSpecial - ? Colors.orange.withOpacity(0.8) - : AppTheme.of(context).secondaryText, - fontSize: 14, - ), - ), - ], - ), - ), - - // Indicador de selección - if (isSelected) - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.check, - color: Colors.white, - size: 20, - ), - ) - else - Icon( - Icons.arrow_forward_ios, - color: isSpecial - ? Colors.orange.withOpacity(0.6) - : AppTheme.of(context).secondaryText, - size: 16, - ), - ], - ), - ), - ), - ), - ); - } - - Widget _buildBusinessInfo(NavigationProvider navigationProvider) { - final negocio = navigationProvider.negocioSeleccionado; - final empresa = navigationProvider.empresaSeleccionada; - - if (negocio == null || empresa == null) return const SizedBox.shrink(); - - return Container( - margin: const EdgeInsets.all(20), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppTheme.of(context).primaryColor.withOpacity(0.1), - AppTheme.of(context).tertiaryColor.withOpacity(0.1), - ], - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - width: 1, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: AppTheme.of(context).primaryGradient, - borderRadius: BorderRadius.circular(10), - ), - child: const Icon( - Icons.business_center, - color: Colors.white, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Ubicación Actual', - style: TextStyle( - color: AppTheme.of(context).primaryColor, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - const SizedBox(height: 16), - - // Información de la empresa - Text( - empresa.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - - // Información del negocio - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: Colors.green, - borderRadius: BorderRadius.circular(10), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.location_on, - color: Colors.white, - size: 16, - ), - const SizedBox(width: 6), - Text( - negocio.nombre, - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildCloseButton() { - return Container( - padding: const EdgeInsets.all(20), - child: SizedBox( - width: double.infinity, - child: Container( - decoration: BoxDecoration( - gradient: AppTheme.of(context).modernGradient, - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 5), - ), - ], - ), - child: ElevatedButton.icon( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.close, color: Colors.white), - label: const Text( - 'Cerrar Menú', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - ), - ), - ), - ); - } - - String _getMenuItemDescription(NavigationMenuItem menuItem) { - switch (menuItem.title) { - case 'Dashboard': - return 'Métricas y estadísticas generales'; - case 'Inventario': - return 'Gestión de componentes de red'; - case 'Topología': - return 'Visualización de infraestructura'; - case 'Alertas': - return 'Notificaciones del sistema'; - case 'Configuración': - return 'Parámetros y ajustes'; - case 'Empresas': - return 'Volver a gestión empresarial'; - default: - return 'Módulo de infraestructura'; - } - } - - void _handleMenuTap( - NavigationMenuItem menuItem, - NavigationProvider navigationProvider, - ) { - if (menuItem.isSpecial) { - // Si es "Empresas", regresar a la página de empresas - navigationProvider.clearSelection(); - context.go('/'); - } else { - // Cambiar la selección del menú - navigationProvider.setSelectedMenuIndex(menuItem.index); - } - - // Cerrar el modal después de la selección - Navigator.of(context).pop(); - } -} diff --git a/lib/pages/videos/dashboard_page.dart b/lib/pages/videos/dashboard_page.dart new file mode 100644 index 0000000..845da2e --- /dev/null +++ b/lib/pages/videos/dashboard_page.dart @@ -0,0 +1,536 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:nethive_neo/providers/videos_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'package:gap/gap.dart'; + +class DashboardPage extends StatefulWidget { + const DashboardPage({Key? key}) : super(key: key); + + @override + State createState() => _DashboardPageState(); +} + +class _DashboardPageState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + late Animation _fadeAnimation; + Map stats = {}; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 500), + vsync: this, + ); + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + _animationController.forward(); + _loadStats(); + } + + Future _loadStats() async { + final provider = Provider.of(context, listen: false); + final result = await provider.getDashboardStats(); + setState(() { + stats = result; + }); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width <= 800; + + return FadeTransition( + opacity: _fadeAnimation, + child: SingleChildScrollView( + padding: EdgeInsets.all(isMobile ? 16 : 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(), + const Gap(24), + _buildStatsCards(isMobile), + const Gap(24), + if (!isMobile) ...[ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildCategoryChart()), + const Gap(24), + Expanded(child: _buildRecentActivity()), + ], + ), + ] else ...[ + _buildCategoryChart(), + const Gap(24), + _buildRecentActivity(), + ], + ], + ), + ), + ); + } + + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 5), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.dashboard, + size: 32, + color: Colors.white, + ), + ), + const Gap(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Dashboard MDF/IDF', + style: AppTheme.of(context).title1.override( + fontFamily: 'Poppins', + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const Gap(4), + Text( + 'Panel de control de contenido multimedia', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: Colors.white.withOpacity(0.9), + fontSize: 14, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildStatsCards(bool isMobile) { + return isMobile + ? Column( + children: [ + _buildStatCard( + 'Videos Totales', + stats['total_videos']?.toString() ?? '0', + Icons.video_library, + AppTheme.of(context).primaryColor, + ), + const Gap(16), + _buildStatCard( + 'Reproducciones', + stats['total_reproducciones']?.toString() ?? '0', + Icons.play_circle_filled, + AppTheme.of(context).secondaryColor, + ), + const Gap(16), + _buildStatCard( + 'Categorías', + stats['total_categories']?.toString() ?? '0', + Icons.category, + AppTheme.of(context).tertiaryColor, + ), + const Gap(16), + _buildStatCard( + 'Video más visto', + stats['most_viewed_video']?['title'] ?? 'N/A', + Icons.trending_up, + AppTheme.of(context).error, + ), + ], + ) + : Row( + children: [ + Expanded( + child: _buildStatCard( + 'Videos Totales', + stats['total_videos']?.toString() ?? '0', + Icons.video_library, + AppTheme.of(context).primaryColor, + ), + ), + const Gap(16), + Expanded( + child: _buildStatCard( + 'Reproducciones', + stats['total_reproducciones']?.toString() ?? '0', + Icons.play_circle_filled, + AppTheme.of(context).secondaryColor, + ), + ), + const Gap(16), + Expanded( + child: _buildStatCard( + 'Categorías', + stats['total_categories']?.toString() ?? '0', + Icons.category, + AppTheme.of(context).tertiaryColor, + ), + ), + const Gap(16), + Expanded( + child: _buildStatCard( + 'Video más visto', + stats['most_viewed_video']?['title'] ?? 'N/A', + Icons.trending_up, + AppTheme.of(context).error, + ), + ), + ], + ); + } + + Widget _buildStatCard( + String title, String value, IconData icon, Color color) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: color.withOpacity(0.3), + ), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: color, + size: 24, + ), + ), + ], + ), + const Gap(16), + Text( + value, + style: AppTheme.of(context).title1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontSize: 28, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Gap(4), + Text( + title, + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).secondaryText, + fontSize: 14, + ), + ), + ], + ), + ); + } + + Widget _buildCategoryChart() { + final categoriesMap = stats['videos_by_category'] as Map?; + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.pie_chart, + color: AppTheme.of(context).primaryColor, + ), + const Gap(8), + Text( + 'Distribución por Categoría', + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Gap(20), + if (categoriesMap != null && categoriesMap.isNotEmpty) + ...categoriesMap.entries.map( + (entry) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildCategoryBar( + entry.key, + entry.value, + categoriesMap.values.reduce((a, b) => a > b ? a : b), + ), + ), + ) + else + Center( + child: Padding( + padding: const EdgeInsets.all(40), + child: Text( + 'No hay datos de categorías', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildCategoryBar(String category, int count, int maxCount) { + final percentage = maxCount > 0 ? count / maxCount : 0.0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + category, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + ), + ), + Text( + count.toString(), + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Gap(8), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: percentage, + backgroundColor: AppTheme.of(context).tertiaryBackground, + valueColor: AlwaysStoppedAnimation( + AppTheme.of(context).primaryColor, + ), + minHeight: 8, + ), + ), + ], + ); + } + + Widget _buildRecentActivity() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.access_time, + color: AppTheme.of(context).primaryColor, + ), + const Gap(8), + Text( + 'Actividad Reciente', + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Gap(20), + Consumer( + builder: (context, provider, child) { + if (provider.mediaFiles.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(40), + child: Text( + 'No hay actividad reciente', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + ), + ), + ), + ); + } + + final recentVideos = provider.mediaFiles.take(5).toList(); + + return Column( + children: recentVideos.map((video) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.video_library, + color: AppTheme.of(context).primaryColor, + ), + ), + const Gap(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + video.title ?? video.fileName, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Gap(4), + Text( + 'Hace ${_getTimeAgo(video.createdAt)}', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + fontSize: 12, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: AppTheme.of(context) + .secondaryColor + .withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${video.reproducciones} views', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).secondaryColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + }).toList(), + ); + }, + ), + ], + ), + ); + } + + String _getTimeAgo(DateTime? date) { + if (date == null) return 'desconocido'; + + final now = DateTime.now(); + final difference = now.difference(date); + + if (difference.inDays > 7) { + return '${(difference.inDays / 7).floor()} semanas'; + } else if (difference.inDays > 0) { + return '${difference.inDays} días'; + } else if (difference.inHours > 0) { + return '${difference.inHours} horas'; + } else if (difference.inMinutes > 0) { + return '${difference.inMinutes} minutos'; + } else { + return 'hace un momento'; + } + } +} diff --git a/lib/pages/videos/gestor_videos_page.dart b/lib/pages/videos/gestor_videos_page.dart new file mode 100644 index 0000000..339f696 --- /dev/null +++ b/lib/pages/videos/gestor_videos_page.dart @@ -0,0 +1,823 @@ +import 'package:flutter/material.dart'; +import 'package:pluto_grid/pluto_grid.dart'; +import 'package:provider/provider.dart'; +import 'package:nethive_neo/providers/videos_provider.dart'; +import 'package:nethive_neo/models/media/media_models.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'package:nethive_neo/helpers/globals.dart'; +import 'package:nethive_neo/widgets/premium_button.dart'; +import 'package:nethive_neo/pages/videos/widgets/premium_upload_dialog.dart'; +import 'package:gap/gap.dart'; + +class GestorVideosPage extends StatefulWidget { + const GestorVideosPage({Key? key}) : super(key: key); + + @override + State createState() => _GestorVideosPageState(); +} + +class _GestorVideosPageState extends State { + PlutoGridStateManager? _stateManager; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadData(); + } + + Future _loadData() async { + setState(() => _isLoading = true); + final provider = Provider.of(context, listen: false); + await Future.wait([ + provider.loadMediaFiles(), + provider.loadCategories(), + ]); + setState(() => _isLoading = false); + } + + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width <= 800; + + if (_isLoading) { + return Center( + child: CircularProgressIndicator( + color: AppTheme.of(context).primaryColor, + ), + ); + } + + return Consumer( + builder: (context, provider, child) { + if (isMobile) { + return _buildMobileView(provider); + } else { + return _buildDesktopView(provider); + } + }, + ); + } + + Widget _buildDesktopView(VideosProvider provider) { + return Column( + children: [ + _buildToolbar(provider, false), + Expanded( + child: Padding( + padding: const EdgeInsets.all(24), + child: _buildPlutoGrid(provider), + ), + ), + ], + ); + } + + Widget _buildMobileView(VideosProvider provider) { + return Column( + children: [ + _buildToolbar(provider, true), + Expanded( + child: provider.mediaFiles.isEmpty + ? _buildEmptyState() + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: provider.mediaFiles.length, + itemBuilder: (context, index) { + final video = provider.mediaFiles[index]; + return _buildVideoCard(video, provider); + }, + ), + ), + ], + ); + } + + Widget _buildToolbar(VideosProvider provider, bool isMobile) { + return Container( + padding: EdgeInsets.all(isMobile ? 16 : 24), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryBackground, + AppTheme.of(context).secondaryBackground, + ], + ), + border: Border( + bottom: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + width: 1, + ), + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color(0xFF4EC9F5), + const Color(0xFFFFB733), + ], + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.video_library, + color: Color(0xFF0B0B0D), + size: 24, + ), + ), + const Gap(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Gestor de Videos', + style: AppTheme.of(context).title2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + fontSize: 22, + ), + ), + if (!isMobile) ...[ + const Gap(4), + Text( + '${provider.mediaFiles.length} videos disponibles', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + fontSize: 13, + ), + ), + ], + ], + ), + ), + PremiumButton( + text: isMobile ? 'Subir' : 'Subir Video', + icon: Icons.cloud_upload, + onPressed: () => _showUploadDialog(provider), + width: isMobile ? 100 : null, + ), + ], + ), + const Gap(16), + _buildSearchField(provider), + ], + ), + ); + } + + Widget _buildSearchField(VideosProvider provider) { + return Container( + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + ), + ), + child: TextField( + controller: provider.busquedaVideoController, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + ), + decoration: InputDecoration( + hintText: 'Buscar videos por título o descripción...', + hintStyle: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + ), + prefixIcon: Icon( + Icons.search, + color: AppTheme.of(context).primaryColor, + ), + suffixIcon: provider.busquedaVideoController.text.isNotEmpty + ? IconButton( + icon: Icon( + Icons.clear, + color: AppTheme.of(context).tertiaryText, + ), + onPressed: () { + provider.busquedaVideoController.clear(); + provider.searchVideos(''); + }, + ) + : null, + border: InputBorder.none, + contentPadding: const EdgeInsets.all(16), + ), + onChanged: (value) => provider.searchVideos(value), + ), + ); + } + + Widget _buildPlutoGrid(VideosProvider provider) { + final columns = [ + PlutoColumn( + title: 'Vista Previa', + field: 'thumbnail', + type: PlutoColumnType.text(), + width: 120, + enableColumnDrag: false, + enableSorting: false, + enableContextMenu: false, + renderer: (rendererContext) { + final video = + rendererContext.row.cells['video']?.value as MediaFileModel?; + if (video == null) return const SizedBox(); + + return Container( + margin: const EdgeInsets.all(4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: AppTheme.of(context).tertiaryBackground, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: video.fileUrl != null + ? Image.network( + video.fileUrl!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Icon( + Icons.video_library, + size: 32, + color: AppTheme.of(context).tertiaryText, + ), + ) + : Icon( + Icons.video_library, + size: 32, + color: AppTheme.of(context).tertiaryText, + ), + ), + ); + }, + ), + PlutoColumn( + title: 'Título', + field: 'title', + type: PlutoColumnType.text(), + width: 250, + ), + PlutoColumn( + title: 'Archivo', + field: 'fileName', + type: PlutoColumnType.text(), + width: 200, + ), + PlutoColumn( + title: 'Categoría', + field: 'category', + type: PlutoColumnType.text(), + width: 150, + ), + PlutoColumn( + title: 'Reproducciones', + field: 'reproducciones', + type: PlutoColumnType.number(), + width: 120, + textAlign: PlutoColumnTextAlign.center, + ), + PlutoColumn( + title: 'Duración', + field: 'duration', + type: PlutoColumnType.text(), + width: 100, + ), + PlutoColumn( + title: 'Fecha de Creación', + field: 'createdAt', + type: PlutoColumnType.text(), + width: 150, + ), + PlutoColumn( + title: 'Acciones', + field: 'actions', + type: PlutoColumnType.text(), + width: 140, + enableColumnDrag: false, + enableSorting: false, + enableContextMenu: false, + renderer: (rendererContext) { + final video = + rendererContext.row.cells['video']?.value as MediaFileModel?; + if (video == null) return const SizedBox(); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.play_circle_outline, size: 20), + color: const Color(0xFF4EC9F5), + tooltip: 'Reproducir', + onPressed: () => _playVideo(video), + ), + IconButton( + icon: const Icon(Icons.edit, size: 20), + color: const Color(0xFFFFB733), + tooltip: 'Editar', + onPressed: () => _editVideo(video, provider), + ), + IconButton( + icon: const Icon(Icons.delete, size: 20), + color: const Color(0xFFFF2D2D), + tooltip: 'Eliminar', + onPressed: () => _deleteVideo(video, provider), + ), + ], + ); + }, + ), + ]; + + return PlutoGrid( + columns: columns, + rows: provider.videosRows, + onLoaded: (PlutoGridOnLoadedEvent event) { + _stateManager = event.stateManager; + _stateManager!.setShowColumnFilter(true); + }, + configuration: PlutoGridConfiguration( + style: plutoGridStyleConfig(context), + ), + ); + } + + Widget _buildVideoCard(MediaFileModel video, VideosProvider provider) { + return Card( + margin: const EdgeInsets.only(bottom: 16), + color: AppTheme.of(context).secondaryBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (video.fileUrl != null) + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + child: AspectRatio( + aspectRatio: 16 / 9, + child: Image.network( + video.fileUrl!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Container( + color: AppTheme.of(context).tertiaryBackground, + child: Icon( + Icons.video_library, + size: 64, + color: AppTheme.of(context).tertiaryText, + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + video.title ?? video.fileName, + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Gap(8), + if (video.fileDescription != null && + video.fileDescription!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + video.fileDescription!, + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).secondaryText, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + Row( + children: [ + Icon( + Icons.play_circle_filled, + size: 16, + color: AppTheme.of(context).tertiaryText, + ), + const Gap(4), + Text( + '${video.reproducciones} reproducciones', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + fontSize: 12, + ), + ), + if (video.durationSeconds != null) ...[ + const Gap(12), + Icon( + Icons.access_time, + size: 16, + color: AppTheme.of(context).tertiaryText, + ), + const Gap(4), + Text( + _formatDuration(video.durationSeconds!), + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + fontSize: 12, + ), + ), + ], + ], + ), + const Gap(12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: () => _playVideo(video), + icon: const Icon(Icons.play_circle_outline, size: 18), + label: const Text('Reproducir'), + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF4EC9F5), + ), + ), + TextButton.icon( + onPressed: () => _editVideo(video, provider), + icon: const Icon(Icons.edit, size: 18), + label: const Text('Editar'), + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFFFB733), + ), + ), + TextButton.icon( + onPressed: () => _deleteVideo(video, provider), + icon: const Icon(Icons.delete, size: 18), + label: const Text('Eliminar'), + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFFF2D2D), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.video_library_outlined, + size: 80, + color: AppTheme.of(context).tertiaryText, + ), + const Gap(16), + Text( + 'No hay videos disponibles', + style: AppTheme.of(context).title2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + ), + ), + const Gap(8), + Text( + 'Sube tu primer video para comenzar', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + ), + ), + const Gap(24), + ElevatedButton.icon( + onPressed: () { + final provider = + Provider.of(context, listen: false); + _showUploadDialog(provider); + }, + icon: const Icon(Icons.upload_file), + label: const Text('Subir Video'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.of(context).primaryColor, + foregroundColor: const Color(0xFF0B0B0D), + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + ), + ), + ], + ), + ); + } + + Future _showUploadDialog(VideosProvider provider) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => PremiumUploadDialog( + provider: provider, + onSuccess: () { + _loadData(); + }, + ), + ); + } + + void _playVideo(MediaFileModel video) { + // TODO: Implementar reproductor de video + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Reproduciendo: ${video.title ?? video.fileName}'), + ), + ); + } + + Future _editVideo(MediaFileModel video, VideosProvider provider) async { + final titleController = TextEditingController(text: video.title); + final descriptionController = + TextEditingController(text: video.fileDescription); + MediaCategoryModel? selectedCategory = provider.categories + .where((cat) => cat.mediaCategoriesId == video.mediaCategoryFk) + .firstOrNull; + + await showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + backgroundColor: AppTheme.of(context).secondaryBackground, + title: Row( + children: [ + Icon( + Icons.edit, + color: AppTheme.of(context).primaryColor, + ), + const Gap(12), + Expanded( + child: Text( + 'Editar Video', + style: AppTheme.of(context).title2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + content: SizedBox( + width: 500, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: titleController, + decoration: InputDecoration( + labelText: 'Título', + filled: true, + fillColor: AppTheme.of(context).tertiaryBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + const Gap(16), + TextFormField( + controller: descriptionController, + maxLines: 3, + decoration: InputDecoration( + labelText: 'Descripción', + filled: true, + fillColor: AppTheme.of(context).tertiaryBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + ), + const Gap(16), + DropdownButtonFormField( + value: selectedCategory, + decoration: InputDecoration( + labelText: 'Categoría', + filled: true, + fillColor: AppTheme.of(context).tertiaryBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + items: provider.categories.map((category) { + return DropdownMenuItem( + value: category, + child: Text(category.categoryName), + ); + }).toList(), + onChanged: (value) { + setDialogState(() => selectedCategory = value); + }, + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancelar', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + ), + ), + ), + ElevatedButton( + onPressed: () async { + Navigator.pop(context); + + // Actualizar campos + if (titleController.text != video.title) { + await provider.updateVideoTitle( + video.mediaFileId, + titleController.text, + ); + } + + if (descriptionController.text != video.fileDescription) { + await provider.updateVideoDescription( + video.mediaFileId, + descriptionController.text, + ); + } + + if (selectedCategory != null && + selectedCategory!.mediaCategoriesId != + video.mediaCategoryFk) { + await provider.updateVideoCategory( + video.mediaFileId, + selectedCategory!.mediaCategoriesId, + ); + } + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Video actualizado exitosamente'), + backgroundColor: Colors.green, + ), + ); + + await _loadData(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.of(context).primaryColor, + foregroundColor: const Color(0xFF0B0B0D), + ), + child: const Text('Guardar'), + ), + ], + ), + ), + ); + } + + Future _deleteVideo( + MediaFileModel video, VideosProvider provider) async { + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppTheme.of(context).secondaryBackground, + title: Row( + children: [ + const Icon( + Icons.warning, + color: Color(0xFFFF2D2D), + ), + const Gap(12), + Text( + 'Confirmar Eliminación', + style: AppTheme.of(context).title2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + content: Text( + '¿Estás seguro de que deseas eliminar "${video.title ?? video.fileName}"? Esta acción no se puede deshacer.', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).secondaryText, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text( + 'Cancelar', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + ), + ), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF2D2D), + foregroundColor: Colors.white, + ), + child: const Text('Eliminar'), + ), + ], + ), + ); + + if (confirm == true) { + final success = await provider.deleteVideo(video.mediaFileId); + + if (!mounted) return; + + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Video eliminado exitosamente'), + backgroundColor: Colors.green, + ), + ); + await _loadData(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error al eliminar el video'), + backgroundColor: Color(0xFFFF2D2D), + ), + ); + } + } + } + + String _formatDuration(int seconds) { + final duration = Duration(seconds: seconds); + final hours = duration.inHours; + final minutes = duration.inMinutes.remainder(60); + final secs = duration.inSeconds.remainder(60); + + if (hours > 0) { + return '${hours}h ${minutes}m ${secs}s'; + } else if (minutes > 0) { + return '${minutes}m ${secs}s'; + } else { + return '${secs}s'; + } + } +} diff --git a/lib/pages/videos/premium_dashboard_page.dart b/lib/pages/videos/premium_dashboard_page.dart new file mode 100644 index 0000000..1cb2b8a --- /dev/null +++ b/lib/pages/videos/premium_dashboard_page.dart @@ -0,0 +1,1109 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:nethive_neo/providers/videos_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'package:gap/gap.dart'; + +class PremiumDashboardPage extends StatefulWidget { + const PremiumDashboardPage({Key? key}) : super(key: key); + + @override + State createState() => _PremiumDashboardPageState(); +} + +class _PremiumDashboardPageState extends State + with TickerProviderStateMixin { + late AnimationController _fadeController; + late AnimationController _slideController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + Map stats = {}; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _fadeController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + _slideController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _fadeController, curve: Curves.easeOut), + ); + _slideAnimation = Tween( + begin: const Offset(0, 0.1), + end: Offset.zero, + ).animate(CurvedAnimation(parent: _slideController, curve: Curves.easeOut)); + + _fadeController.forward(); + _slideController.forward(); + _loadStats(); + } + + Future _loadStats() async { + final provider = Provider.of(context, listen: false); + final result = await provider.getDashboardStats(); + setState(() { + stats = result; + isLoading = false; + }); + } + + @override + void dispose() { + _fadeController.dispose(); + _slideController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width <= 800; + + return FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: RefreshIndicator( + onRefresh: _loadStats, + color: AppTheme.of(context).primaryColor, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.all(isMobile ? 16 : 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildWelcomeHeader(), + const Gap(32), + _buildStatsCards(isMobile), + const Gap(32), + if (!isMobile) ...[ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 3, child: _buildCategoryChart()), + const Gap(24), + Expanded(flex: 2, child: _buildTopVideos()), + ], + ), + const Gap(24), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 2, child: _buildViewsChart()), + const Gap(24), + Expanded(flex: 3, child: _buildRecentActivity()), + ], + ), + ] else ...[ + _buildCategoryChart(), + const Gap(24), + _buildTopVideos(), + const Gap(24), + _buildViewsChart(), + const Gap(24), + _buildRecentActivity(), + ], + ], + ), + ), + ), + ), + ); + } + + Widget _buildWelcomeHeader() { + return Container( + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF4EC9F5), + const Color(0xFFFFB733), + ], + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(16), + ), + child: const Icon( + Icons.dashboard, + size: 40, + color: Color(0xFF0B0B0D), + ), + ), + const Gap(20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '¡Bienvenido al Dashboard!', + style: AppTheme.of(context).title1.override( + fontFamily: 'Poppins', + color: const Color(0xFF0B0B0D), + fontWeight: FontWeight.bold, + fontSize: 28, + ), + ), + const Gap(8), + Text( + 'Visualiza el rendimiento de tu contenido en tiempo real', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: const Color(0xFF0B0B0D).withOpacity(0.8), + fontSize: 15, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildStatsCards(bool isMobile) { + if (isLoading) { + return _buildLoadingSkeleton(isMobile); + } + + final totalVideos = stats['totalVideos'] ?? 0; + final totalViews = stats['totalReproducciones'] ?? 0; + final categories = stats['totalCategories'] ?? 0; + final avgViews = totalVideos > 0 ? (totalViews / totalVideos).round() : 0; + + final cards = [ + _StatCard( + title: 'Total Videos', + value: totalVideos.toString(), + icon: Icons.video_library, + gradient: const LinearGradient( + colors: [Color(0xFF4EC9F5), Color(0xFF2E8BC0)], + ), + trend: '+12%', + trendUp: true, + ), + _StatCard( + title: 'Reproducciones', + value: _formatNumber(totalViews), + icon: Icons.play_circle_filled, + gradient: const LinearGradient( + colors: [Color(0xFFFFB733), Color(0xFFFF8A00)], + ), + trend: '+23%', + trendUp: true, + ), + _StatCard( + title: 'Categorías', + value: categories.toString(), + icon: Icons.category, + gradient: const LinearGradient( + colors: [Color(0xFF6B2F8A), Color(0xFF9B4FC9)], + ), + trend: '+5%', + trendUp: true, + ), + _StatCard( + title: 'Promedio Vistas', + value: _formatNumber(avgViews), + icon: Icons.trending_up, + gradient: const LinearGradient( + colors: [Color(0xFF00C9A7), Color(0xFF00B894)], + ), + trend: '+8%', + trendUp: true, + ), + ]; + + if (isMobile) { + return Column( + children: cards.map((card) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: card, + ); + }).toList(), + ); + } + + return GridView.count( + crossAxisCount: 4, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisSpacing: 20, + mainAxisSpacing: 20, + childAspectRatio: 1.5, + children: cards, + ); + } + + Widget _buildLoadingSkeleton(bool isMobile) { + return Shimmer.fromColors( + baseColor: AppTheme.of(context).tertiaryBackground, + highlightColor: AppTheme.of(context).secondaryBackground, + child: GridView.count( + crossAxisCount: isMobile ? 1 : 4, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisSpacing: 20, + mainAxisSpacing: 20, + childAspectRatio: isMobile ? 3 : 1.5, + children: List.generate( + 4, + (index) => Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ), + ); + } + + Widget _buildCategoryChart() { + if (isLoading) { + return _buildChartSkeleton('Distribución por Categoría'); + } + + final videosByCategory = + stats['videosByCategory'] as Map? ?? {}; + + return Container( + padding: const EdgeInsets.all(28), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.pie_chart, + color: AppTheme.of(context).primaryColor, + size: 24, + ), + ), + const Gap(12), + Text( + 'Distribución por Categoría', + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ], + ), + const Gap(32), + SizedBox( + height: 280, + child: videosByCategory.isEmpty + ? _buildEmptyChart('No hay datos de categorías') + : PieChart( + PieChartData( + sectionsSpace: 3, + centerSpaceRadius: 60, + sections: _buildPieChartSections(videosByCategory), + borderData: FlBorderData(show: false), + ), + ), + ), + const Gap(24), + _buildLegend(videosByCategory), + ], + ), + ); + } + + List _buildPieChartSections( + Map videosByCategory) { + final colors = [ + const Color(0xFF4EC9F5), + const Color(0xFFFFB733), + const Color(0xFF6B2F8A), + const Color(0xFFFF2D2D), + const Color(0xFF00C9A7), + ]; + + int index = 0; + return videosByCategory.entries.map((entry) { + final color = colors[index % colors.length]; + index++; + + return PieChartSectionData( + value: entry.value.toDouble(), + title: '${entry.value}', + color: color, + radius: 50, + titleStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFF0B0B0D), + fontFamily: 'Poppins', + ), + ); + }).toList(); + } + + Widget _buildLegend(Map videosByCategory) { + final colors = [ + const Color(0xFF4EC9F5), + const Color(0xFFFFB733), + const Color(0xFF6B2F8A), + const Color(0xFFFF2D2D), + const Color(0xFF00C9A7), + ]; + + int index = 0; + return Wrap( + spacing: 16, + runSpacing: 12, + children: videosByCategory.entries.map((entry) { + final color = colors[index % colors.length]; + index++; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(4), + ), + ), + const Gap(8), + Text( + entry.key, + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).secondaryText, + fontSize: 13, + ), + ), + ], + ); + }).toList(), + ); + } + + Widget _buildViewsChart() { + return Container( + padding: const EdgeInsets.all(28), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFFFFB733).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.bar_chart, + color: Color(0xFFFFB733), + size: 24, + ), + ), + const Gap(12), + Text( + 'Reproducciones Semanales', + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ], + ), + const Gap(32), + SizedBox( + height: 200, + child: BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + maxY: 100, + barTouchData: BarTouchData(enabled: true), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + const days = ['L', 'M', 'X', 'J', 'V', 'S', 'D']; + return Text( + days[value.toInt() % 7], + style: TextStyle( + color: AppTheme.of(context).tertiaryText, + fontSize: 12, + fontFamily: 'Poppins', + ), + ); + }, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + getTitlesWidget: (value, meta) { + return Text( + value.toInt().toString(), + style: TextStyle( + color: AppTheme.of(context).tertiaryText, + fontSize: 12, + fontFamily: 'Poppins', + ), + ); + }, + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData(show: false), + gridData: FlGridData( + show: true, + drawVerticalLine: false, + getDrawingHorizontalLine: (value) { + return FlLine( + color: AppTheme.of(context).tertiaryText.withOpacity(0.1), + strokeWidth: 1, + ); + }, + ), + barGroups: [ + BarChartGroupData(x: 0, barRods: [ + BarChartRodData( + toY: 65, + gradient: const LinearGradient( + colors: [Color(0xFF4EC9F5), Color(0xFFFFB733)], + ), + width: 20, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + ) + ]), + BarChartGroupData(x: 1, barRods: [ + BarChartRodData( + toY: 80, + gradient: const LinearGradient( + colors: [Color(0xFF4EC9F5), Color(0xFFFFB733)], + ), + width: 20, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + ) + ]), + BarChartGroupData(x: 2, barRods: [ + BarChartRodData( + toY: 45, + gradient: const LinearGradient( + colors: [Color(0xFF4EC9F5), Color(0xFFFFB733)], + ), + width: 20, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + ) + ]), + BarChartGroupData(x: 3, barRods: [ + BarChartRodData( + toY: 90, + gradient: const LinearGradient( + colors: [Color(0xFF4EC9F5), Color(0xFFFFB733)], + ), + width: 20, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + ) + ]), + BarChartGroupData(x: 4, barRods: [ + BarChartRodData( + toY: 75, + gradient: const LinearGradient( + colors: [Color(0xFF4EC9F5), Color(0xFFFFB733)], + ), + width: 20, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + ) + ]), + BarChartGroupData(x: 5, barRods: [ + BarChartRodData( + toY: 55, + gradient: const LinearGradient( + colors: [Color(0xFF4EC9F5), Color(0xFFFFB733)], + ), + width: 20, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + ) + ]), + BarChartGroupData(x: 6, barRods: [ + BarChartRodData( + toY: 40, + gradient: const LinearGradient( + colors: [Color(0xFF4EC9F5), Color(0xFFFFB733)], + ), + width: 20, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + ) + ]), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildTopVideos() { + final provider = Provider.of(context); + final topVideos = provider.mediaFiles.take(5).toList(); + + return Container( + padding: const EdgeInsets.all(28), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFF6B2F8A).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.star, + color: Color(0xFF6B2F8A), + size: 24, + ), + ), + const Gap(12), + Text( + 'Top 5 Videos', + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ], + ), + const Gap(24), + ...topVideos.asMap().entries.map((entry) { + final index = entry.key; + final video = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color(0xFF4EC9F5), + const Color(0xFFFFB733), + ], + ), + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + '${index + 1}', + style: const TextStyle( + color: Color(0xFF0B0B0D), + fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + ), + ), + ), + ), + const Gap(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + video.title ?? video.fileName, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + '${video.reproducciones} vistas', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ); + }).toList(), + ], + ), + ); + } + + Widget _buildRecentActivity() { + final provider = Provider.of(context); + final recentVideos = provider.mediaFiles.take(6).toList(); + + return Container( + padding: const EdgeInsets.all(28), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFF00C9A7).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.history, + color: Color(0xFF00C9A7), + size: 24, + ), + ), + const Gap(12), + Text( + 'Actividad Reciente', + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ], + ), + const Gap(24), + ...recentVideos.map((video) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color(0xFF4EC9F5), + const Color(0xFFFFB733), + ], + ), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.video_library, + color: Color(0xFF0B0B0D), + ), + ), + const Gap(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + video.title ?? video.fileName, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Gap(4), + Text( + 'Subido hace ${_getTimeAgo(video.createdAt)}', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + fontSize: 12, + ), + ), + ], + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFF00C9A7).withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${video.reproducciones}', + style: const TextStyle( + color: Color(0xFF00C9A7), + fontWeight: FontWeight.bold, + fontSize: 12, + fontFamily: 'Poppins', + ), + ), + ), + ], + ), + ); + }).toList(), + ], + ), + ); + } + + Widget _buildChartSkeleton(String title) { + return Container( + padding: const EdgeInsets.all(28), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppTheme.of(context).title3.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + ), + ), + const Gap(24), + Shimmer.fromColors( + baseColor: AppTheme.of(context).tertiaryBackground, + highlightColor: AppTheme.of(context).secondaryBackground, + child: Container( + height: 300, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ], + ), + ); + } + + Widget _buildEmptyChart(String message) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.pie_chart_outline, + size: 64, + color: AppTheme.of(context).tertiaryText, + ), + const Gap(16), + Text( + message, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + ), + ), + ], + ), + ); + } + + String _formatNumber(int number) { + if (number >= 1000000) { + return '${(number / 1000000).toStringAsFixed(1)}M'; + } else if (number >= 1000) { + return '${(number / 1000).toStringAsFixed(1)}K'; + } + return number.toString(); + } + + String _getTimeAgo(DateTime? timestamp) { + if (timestamp == null) return 'hace un momento'; + + final difference = DateTime.now().difference(timestamp); + + if (difference.inDays > 30) { + return '${(difference.inDays / 30).floor()} meses'; + } else if (difference.inDays > 0) { + return '${difference.inDays} días'; + } else if (difference.inHours > 0) { + return '${difference.inHours} horas'; + } else if (difference.inMinutes > 0) { + return '${difference.inMinutes} minutos'; + } else { + return 'hace un momento'; + } + } +} + +class _StatCard extends StatefulWidget { + final String title; + final String value; + final IconData icon; + final Gradient gradient; + final String? trend; + final bool trendUp; + + const _StatCard({ + required this.title, + required this.value, + required this.icon, + required this.gradient, + this.trend, + this.trendUp = true, + }); + + @override + State<_StatCard> createState() => _StatCardState(); +} + +class _StatCardState extends State<_StatCard> + with SingleTickerProviderStateMixin { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + transform: Matrix4.identity()..scale(_isHovered ? 1.03 : 1.0), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: widget.gradient, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: widget.gradient.colors.first.withOpacity(0.3), + blurRadius: _isHovered ? 20 : 12, + offset: Offset(0, _isHovered ? 8 : 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + widget.icon, + color: const Color(0xFF0B0B0D), + size: 24, + ), + ), + if (widget.trend != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: widget.trendUp + ? Colors.green.withOpacity(0.2) + : Colors.red.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + widget.trendUp + ? Icons.trending_up + : Icons.trending_down, + size: 14, + color: const Color(0xFF0B0B0D), + ), + const Gap(4), + Text( + widget.trend!, + style: const TextStyle( + color: Color(0xFF0B0B0D), + fontSize: 11, + fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.value, + style: const TextStyle( + color: Color(0xFF0B0B0D), + fontSize: 32, + fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + ), + ), + const Gap(4), + Text( + widget.title, + style: TextStyle( + color: const Color(0xFF0B0B0D).withOpacity(0.8), + fontSize: 14, + fontFamily: 'Poppins', + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/videos/videos_layout.dart b/lib/pages/videos/videos_layout.dart new file mode 100644 index 0000000..6736136 --- /dev/null +++ b/lib/pages/videos/videos_layout.dart @@ -0,0 +1,560 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:nethive_neo/providers/visual_state_provider.dart'; +import 'package:nethive_neo/pages/videos/premium_dashboard_page.dart'; +import 'package:nethive_neo/pages/videos/gestor_videos_page.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'package:gap/gap.dart'; + +class VideosLayout extends StatefulWidget { + const VideosLayout({Key? key}) : super(key: key); + + @override + State createState() => _VideosLayoutState(); +} + +class _VideosLayoutState extends State { + int _selectedMenuIndex = 0; + final GlobalKey _scaffoldKey = GlobalKey(); + + final List _menuItems = [ + MenuItem( + title: 'Dashboard', + icon: Icons.dashboard, + index: 0, + ), + MenuItem( + title: 'Gestor de Videos', + icon: Icons.video_library, + index: 1, + ), + MenuItem( + title: 'Configuración', + icon: Icons.settings, + index: 2, + ), + ]; + + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width <= 800; + + return Scaffold( + key: _scaffoldKey, + backgroundColor: AppTheme.of(context).primaryBackground, + drawer: isMobile ? _buildDrawer() : null, + body: Row( + children: [ + if (!isMobile) _buildSideMenu(), + Expanded( + child: Column( + children: [ + _buildHeader(isMobile), + Expanded(child: _buildContent()), + ], + ), + ), + ], + ), + ); + } + + Widget _buildHeader(bool isMobile) { + return Container( + padding: EdgeInsets.all(isMobile ? 16 : 24), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + border: Border( + bottom: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + width: 1, + ), + ), + ), + child: Row( + children: [ + if (isMobile) + IconButton( + icon: const Icon(Icons.menu), + color: AppTheme.of(context).primaryText, + onPressed: () => _scaffoldKey.currentState?.openDrawer(), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.energy_savings_leaf, + color: Color(0xFF0B0B0D), + size: 24, + ), + ), + const Gap(12), + Text( + 'EnergyMedia', + style: AppTheme.of(context).title2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + Text( + _menuItems[_selectedMenuIndex].title, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).secondaryText, + ), + ), + ], + ), + ); + } + + Widget _buildSideMenu() { + return Container( + width: 280, + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + border: Border( + right: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + width: 1, + ), + ), + ), + child: Column( + children: [ + // Header con gradiente premium + Container( + width: double.infinity, + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF4EC9F5), + const Color(0xFFFFB733), + ], + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.energy_savings_leaf, + color: Color(0xFF0B0B0D), + size: 32, + ), + ), + const Gap(16), + Text( + 'EnergyMedia', + style: AppTheme.of(context).title2.override( + fontFamily: 'Poppins', + color: const Color(0xFF0B0B0D), + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), + const Gap(4), + Text( + 'Content Manager', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: const Color(0xFF0B0B0D).withOpacity(0.8), + fontSize: 13, + ), + ), + ], + ), + ), + + // Menu Items + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), + children: _menuItems.map((item) { + final isSelected = _selectedMenuIndex == item.index; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildPremiumMenuItem( + icon: item.icon, + title: item.title, + isSelected: isSelected, + onTap: () { + setState(() => _selectedMenuIndex = item.index); + }, + ), + ); + }).toList(), + ), + ), + + // Theme Toggle en la parte inferior + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + width: 1, + ), + ), + ), + child: Consumer( + builder: (context, visualProvider, _) { + return _buildThemeToggle(visualProvider); + }, + ), + ), + ], + ), + ); + } + + Widget _buildPremiumMenuItem({ + required IconData icon, + required String title, + required bool isSelected, + required VoidCallback onTap, + }) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + gradient: isSelected + ? LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF4EC9F5), + const Color(0xFFFFB733), + ], + ) + : null, + borderRadius: BorderRadius.circular(12), + boxShadow: isSelected + ? [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ] + : null, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + children: [ + Icon( + icon, + color: isSelected + ? const Color(0xFF0B0B0D) + : AppTheme.of(context).secondaryText, + size: 24, + ), + const Gap(16), + Expanded( + child: Text( + title, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: isSelected + ? const Color(0xFF0B0B0D) + : AppTheme.of(context).primaryText, + fontWeight: + isSelected ? FontWeight.bold : FontWeight.w500, + ), + ), + ), + if (isSelected) + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: const Color(0xFF0B0B0D), + shape: BoxShape.circle, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildThemeToggle(VisualStateProvider visualProvider) { + final isDark = AppTheme.themeMode == ThemeMode.dark; + + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Expanded( + child: _buildThemeButton( + icon: Icons.light_mode, + label: 'Claro', + isSelected: !isDark, + onTap: () { + visualProvider.changeThemeMode(ThemeMode.light, context); + }, + ), + ), + const Gap(4), + Expanded( + child: _buildThemeButton( + icon: Icons.dark_mode, + label: 'Oscuro', + isSelected: isDark, + onTap: () { + visualProvider.changeThemeMode(ThemeMode.dark, context); + }, + ), + ), + ], + ), + ); + } + + Widget _buildThemeButton({ + required IconData icon, + required String label, + required bool isSelected, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + gradient: isSelected + ? LinearGradient( + colors: [ + const Color(0xFF4EC9F5), + const Color(0xFFFFB733), + ], + ) + : null, + borderRadius: BorderRadius.circular(8), + boxShadow: isSelected + ? [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ] + : null, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: isSelected + ? const Color(0xFF0B0B0D) + : AppTheme.of(context).secondaryText, + size: 20, + ), + const Gap(4), + Text( + label, + style: TextStyle( + color: isSelected + ? const Color(0xFF0B0B0D) + : AppTheme.of(context).secondaryText, + fontSize: 11, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ); + } + + Widget _buildDrawer() { + return Drawer( + backgroundColor: AppTheme.of(context).secondaryBackground, + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF4EC9F5), + const Color(0xFFFFB733), + ], + ), + ), + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.energy_savings_leaf, + color: Color(0xFF0B0B0D), + size: 32, + ), + ), + const Gap(12), + Text( + 'EnergyMedia', + style: AppTheme.of(context).title2.override( + fontFamily: 'Poppins', + color: const Color(0xFF0B0B0D), + fontWeight: FontWeight.bold, + ), + ), + const Gap(4), + Text( + 'Content Manager', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: const Color(0xFF0B0B0D).withOpacity(0.8), + ), + ), + ], + ), + ), + ), + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), + children: _menuItems.map((item) { + final isSelected = _selectedMenuIndex == item.index; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildPremiumMenuItem( + icon: item.icon, + title: item.title, + isSelected: isSelected, + onTap: () { + setState(() => _selectedMenuIndex = item.index); + Navigator.pop(context); + }, + ), + ); + }).toList(), + ), + ), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + width: 1, + ), + ), + ), + child: Consumer( + builder: (context, visualProvider, _) { + return _buildThemeToggle(visualProvider); + }, + ), + ), + ], + ), + ); + } + + Widget _buildContent() { + switch (_selectedMenuIndex) { + case 0: + return const PremiumDashboardPage(); + case 1: + return const GestorVideosPage(); + case 2: + return _buildWorkInProgress(); + default: + return const PremiumDashboardPage(); + } + } + + Widget _buildWorkInProgress() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.construction, + size: 64, + color: AppTheme.of(context).tertiaryText, + ), + const Gap(16), + Text( + 'Trabajo en Progreso', + style: AppTheme.of(context).title2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + ), + ), + const Gap(8), + Text( + 'Esta sección estará disponible próximamente', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + ), + ), + ], + ), + ); + } +} + +class MenuItem { + final String title; + final IconData icon; + final int index; + + MenuItem({ + required this.title, + required this.icon, + required this.index, + }); +} diff --git a/lib/pages/videos/widgets/premium_upload_dialog.dart b/lib/pages/videos/widgets/premium_upload_dialog.dart new file mode 100644 index 0000000..3792ea0 --- /dev/null +++ b/lib/pages/videos/widgets/premium_upload_dialog.dart @@ -0,0 +1,634 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; +import 'package:nethive_neo/models/media/media_models.dart'; +import 'package:nethive_neo/providers/videos_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'package:nethive_neo/widgets/premium_button.dart'; +import 'package:gap/gap.dart'; + +class PremiumUploadDialog extends StatefulWidget { + final VideosProvider provider; + final VoidCallback onSuccess; + + const PremiumUploadDialog({ + Key? key, + required this.provider, + required this.onSuccess, + }) : super(key: key); + + @override + State createState() => _PremiumUploadDialogState(); +} + +class _PremiumUploadDialogState extends State { + final titleController = TextEditingController(); + final descriptionController = TextEditingController(); + MediaCategoryModel? selectedCategory; + Uint8List? selectedVideo; + String? videoFileName; + Uint8List? selectedPoster; + String? posterFileName; + VideoPlayerController? _videoController; + bool isUploading = false; + + @override + void dispose() { + titleController.dispose(); + descriptionController.dispose(); + _videoController?.dispose(); + super.dispose(); + } + + Future _selectVideo() async { + final result = await widget.provider.selectVideo(); + if (result) { + setState(() { + selectedVideo = widget.provider.webVideoBytes; + videoFileName = widget.provider.videoName; + titleController.text = widget.provider.tituloController.text; + }); + + // Crear video player para preview (solo web) + // Para preview en web, necesitaríamos crear un Blob URL, pero esto es complejo + // Por ahora mostraremos solo el nombre y poster + } + } + + Future _selectPoster() async { + final result = await widget.provider.selectPoster(); + if (result) { + setState(() { + selectedPoster = widget.provider.webPosterBytes; + posterFileName = widget.provider.posterName; + }); + } + } + + Future _uploadVideo() async { + if (titleController.text.isEmpty || + selectedCategory == null || + selectedVideo == null || + videoFileName == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Por favor completa los campos requeridos'), + backgroundColor: const Color(0xFFFF2D2D), + behavior: SnackBarBehavior.floating, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ); + return; + } + + setState(() => isUploading = true); + + final success = await widget.provider.uploadVideo( + title: titleController.text, + description: descriptionController.text.isEmpty + ? null + : descriptionController.text, + categoryId: selectedCategory!.mediaCategoriesId, + ); + + if (!mounted) return; + + setState(() => isUploading = false); + Navigator.pop(context); + + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Row( + children: [ + Icon(Icons.check_circle, color: Colors.white), + Gap(12), + Text('Video subido exitosamente'), + ], + ), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ); + widget.onSuccess(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Row( + children: [ + Icon(Icons.error, color: Colors.white), + Gap(12), + Text('Error al subir el video'), + ], + ), + backgroundColor: const Color(0xFFFF2D2D), + behavior: SnackBarBehavior.floating, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width <= 800; + + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: isMobile ? double.infinity : 900, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.9, + ), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + blurRadius: 40, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildHeader(), + Flexible( + child: SingleChildScrollView( + padding: const EdgeInsets.all(32), + child: + isMobile ? _buildMobileContent() : _buildDesktopContent(), + ), + ), + _buildActions(), + ], + ), + ), + ); + } + + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF4EC9F5), + const Color(0xFFFFB733), + ], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.cloud_upload, + color: Color(0xFF0B0B0D), + size: 28, + ), + ), + const Gap(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Subir Nuevo Video', + style: AppTheme.of(context).title2.override( + fontFamily: 'Poppins', + color: const Color(0xFF0B0B0D), + fontWeight: FontWeight.bold, + fontSize: 22, + ), + ), + const Gap(4), + Text( + 'Comparte tu contenido con el mundo', + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: const Color(0xFF0B0B0D).withOpacity(0.7), + fontSize: 13, + ), + ), + ], + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close, color: Color(0xFF0B0B0D)), + tooltip: 'Cerrar', + ), + ], + ), + ); + } + + Widget _buildDesktopContent() { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 3, child: _buildFormFields()), + const Gap(24), + Expanded(flex: 2, child: _buildPreviewSection()), + ], + ); + } + + Widget _buildMobileContent() { + return Column( + children: [ + _buildFormFields(), + const Gap(24), + _buildPreviewSection(), + ], + ); + } + + Widget _buildFormFields() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLabel('Título del Video *'), + const Gap(8), + _buildTextField( + controller: titleController, + hintText: 'Ej: Tutorial de energía solar', + prefixIcon: Icons.title, + ), + const Gap(20), + _buildLabel('Descripción'), + const Gap(8), + _buildTextField( + controller: descriptionController, + hintText: 'Describe el contenido del video...', + prefixIcon: Icons.description, + maxLines: 4, + ), + const Gap(20), + _buildLabel('Categoría *'), + const Gap(8), + _buildCategoryDropdown(), + ], + ); + } + + Widget _buildPreviewSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLabel('Vista Previa'), + const Gap(12), + _buildVideoSelector(), + const Gap(16), + _buildPosterSelector(), + ], + ); + } + + Widget _buildLabel(String text) { + return Text( + text, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required String hintText, + required IconData prefixIcon, + int maxLines = 1, + }) { + return Container( + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + ), + ), + child: TextField( + controller: controller, + maxLines: maxLines, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + ), + decoration: InputDecoration( + hintText: hintText, + hintStyle: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + ), + prefixIcon: Icon( + prefixIcon, + color: AppTheme.of(context).primaryColor, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.all(16), + ), + ), + ); + } + + Widget _buildCategoryDropdown() { + return Container( + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + ), + ), + child: DropdownButtonFormField( + value: selectedCategory, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.category, + color: AppTheme.of(context).primaryColor, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.all(16), + ), + hint: Text( + 'Selecciona una categoría', + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + ), + ), + dropdownColor: AppTheme.of(context).secondaryBackground, + items: widget.provider.categories.map((category) { + return DropdownMenuItem( + value: category, + child: Text( + category.categoryName, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + ), + ), + ); + }).toList(), + onChanged: (value) { + setState(() => selectedCategory = value); + }, + ), + ); + } + + Widget _buildVideoSelector() { + return GestureDetector( + onTap: _selectVideo, + child: Container( + height: 200, + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: videoFileName != null + ? Colors.green.withOpacity(0.5) + : AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 2, + strokeAlign: BorderSide.strokeAlignInside, + ), + ), + child: selectedVideo != null + ? _buildVideoPreview() + : _buildUploadPlaceholder( + icon: Icons.video_file, + title: 'Seleccionar Video', + subtitle: 'Click para elegir archivo', + ), + ), + ); + } + + Widget _buildPosterSelector() { + return GestureDetector( + onTap: _selectPoster, + child: Container( + height: 150, + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: posterFileName != null + ? Colors.green.withOpacity(0.5) + : AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 2, + ), + ), + child: selectedPoster != null + ? _buildPosterPreview() + : _buildUploadPlaceholder( + icon: Icons.image, + title: 'Miniatura (Opcional)', + subtitle: 'Click para elegir imagen', + ), + ), + ); + } + + Widget _buildVideoPreview() { + return Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(14), + child: Container( + color: Colors.black, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.play_circle_outline, + size: 64, + color: Colors.white.withOpacity(0.8), + ), + const Gap(12), + Text( + videoFileName ?? 'Video seleccionado', + style: const TextStyle( + color: Colors.white, + fontFamily: 'Poppins', + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + Positioned( + top: 12, + right: 12, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.green, + borderRadius: BorderRadius.circular(20), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.check_circle, size: 16, color: Colors.white), + Gap(4), + Text( + 'Cargado', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildPosterPreview() { + return Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(14), + child: Image.memory( + selectedPoster!, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + ), + ), + Positioned( + top: 12, + right: 12, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.green, + borderRadius: BorderRadius.circular(20), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.check_circle, size: 16, color: Colors.white), + Gap(4), + Text( + 'Cargado', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildUploadPlaceholder({ + required IconData icon, + required String title, + required String subtitle, + }) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + icon, + size: 40, + color: AppTheme.of(context).primaryColor, + ), + ), + const Gap(12), + Text( + title, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w600, + ), + ), + const Gap(4), + Text( + subtitle, + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + fontSize: 12, + ), + ), + ], + ), + ); + } + + Widget _buildActions() { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground.withOpacity(0.5), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + PremiumButton( + text: 'Cancelar', + isOutlined: true, + onPressed: () => Navigator.pop(context), + width: 120, + ), + const Gap(12), + PremiumButton( + text: 'Subir Video', + icon: Icons.cloud_upload, + onPressed: _uploadVideo, + isLoading: isUploading, + width: 160, + ), + ], + ), + ); + } +} diff --git a/lib/providers/nethive/componentes_provider.dart b/lib/providers/nethive/componentes_provider.dart deleted file mode 100644 index e045209..0000000 --- a/lib/providers/nethive/componentes_provider.dart +++ /dev/null @@ -1,1062 +0,0 @@ -import 'dart:typed_data'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:pluto_grid/pluto_grid.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -import 'package:nethive_neo/helpers/globals.dart'; -import 'package:nethive_neo/models/nethive/categoria_componente_model.dart'; -import 'package:nethive_neo/models/nethive/componente_model.dart'; -import 'package:nethive_neo/models/nethive/distribucion_model.dart'; -import 'package:nethive_neo/models/nethive/conexion_componente_model.dart'; -import 'package:nethive_neo/models/nethive/conexion_alimentacion_model.dart'; -import 'package:nethive_neo/models/nethive/topologia_completa_model.dart'; -import 'package:nethive_neo/models/nethive/rol_logico_componente_model.dart'; -import 'package:nethive_neo/models/nethive/tipo_distribucion_model.dart'; -import 'package:nethive_neo/models/nethive/detalle_cable_model.dart'; -import 'package:nethive_neo/models/nethive/detalle_switch_model.dart'; -import 'package:nethive_neo/models/nethive/detalle_patch_panel_model.dart'; -import 'package:nethive_neo/models/nethive/detalle_rack_model.dart'; -import 'package:nethive_neo/models/nethive/detalle_organizador_model.dart'; -import 'package:nethive_neo/models/nethive/detalle_ups_model.dart'; -import 'package:nethive_neo/models/nethive/detalle_router_firewall_model.dart'; -import 'package:nethive_neo/models/nethive/detalle_equipo_activo_model.dart'; -import 'package:nethive_neo/models/nethive/vista_conexiones_por_cables_model.dart'; -import 'package:nethive_neo/models/nethive/vista_topologia_por_negocio_model.dart'; -import 'package:nethive_neo/models/nethive/rack_con_componentes_model.dart'; - -class ComponentesProvider extends ChangeNotifier { - // State managers - PlutoGridStateManager? componentesStateManager; - PlutoGridStateManager? categoriasStateManager; - - // Controladores de búsqueda - final busquedaComponenteController = TextEditingController(); - final busquedaCategoriaController = TextEditingController(); - - // Listas principales - List categorias = []; - List rolesLogicos = []; - List tiposDistribucion = []; - List componentes = []; - List componentesRows = []; - List categoriasRows = []; - - // Nueva estructura de topología optimizada - TopologiaCompleta? topologiaCompleta; - List componentesTopologia = []; - List conexionesDatos = []; - List conexionesEnergia = []; - - // Listas para retrocompatibilidad - List distribuciones = []; - List conexiones = []; - List conexionesConCables = []; - List topologiaOptimizada = []; - - // Variables para formularios - String? imagenFileName; - Uint8List? imagenToUpload; - String? negocioSeleccionadoId; - String? negocioSeleccionadoNombre; - String? empresaSeleccionadaId; - int? categoriaSeleccionadaId; - bool showDetallesEspecificos = false; - - // Variables para gestión de topología - bool isLoadingTopologia = false; - bool isLoadingRacks = false; - List problemasTopologia = []; - - // Detalles específicos por tipo de componente - DetalleCable? detalleCable; - DetalleSwitch? detalleSwitch; - DetallePatchPanel? detallePatchPanel; - DetalleRack? detalleRack; - DetalleOrganizador? detalleOrganizador; - DetalleUps? detalleUps; - DetalleRouterFirewall? detalleRouterFirewall; - DetalleEquipoActivo? detalleEquipoActivo; - - // Nueva lista para racks con componentes - List racksConComponentes = []; - - // Variable para controlar si el provider está activo - bool _isDisposed = false; - - ComponentesProvider() { - _inicializarDatos(); - } - - @override - void dispose() { - _isDisposed = true; - busquedaComponenteController.dispose(); - busquedaCategoriaController.dispose(); - super.dispose(); - } - - // Método seguro para notificar listeners - void _safeNotifyListeners() { - if (!_isDisposed) { - notifyListeners(); - } - } - - // INICIALIZACIÓN - Future _inicializarDatos() async { - try { - await Future.wait([ - getCategorias(), - getRolesLogicos(), - getTiposDistribucion(), - ]); - } catch (e) { - print('Error en inicialización: ${e.toString()}'); - } - } - - // MÉTODOS PARA ROLES LÓGICOS - Future getRolesLogicos() async { - try { - final res = await supabaseLU - .from('rol_logico_componente') - .select() - .order('nombre', ascending: true); - - rolesLogicos = (res as List) - .map((rol) => RolLogicoComponente.fromMap(rol)) - .toList(); - - _safeNotifyListeners(); - } catch (e) { - print('Error en getRolesLogicos: ${e.toString()}'); - } - } - - // MÉTODOS PARA TIPOS DE DISTRIBUCIÓN - Future getTiposDistribucion() async { - try { - final res = await supabaseLU - .from('tipo_distribucion') - .select() - .order('nombre', ascending: true); - - tiposDistribucion = (res as List) - .map((tipo) => TipoDistribucion.fromMap(tipo)) - .toList(); - - _safeNotifyListeners(); - } catch (e) { - print('Error en getTiposDistribucion: ${e.toString()}'); - } - } - - // MÉTODO PRINCIPAL PARA OBTENER TOPOLOGÍA COMPLETA - Future getTopologiaPorNegocio(String negocioId) async { - try { - isLoadingTopologia = true; - _safeNotifyListeners(); - - print( - 'Llamando a función RPC fn_topologia_por_negocio con negocio_id: $negocioId'); - - final response = - await supabaseLU.rpc('fn_topologia_por_negocio', params: { - 'p_negocio_id': negocioId, - }).select(); - - /* print('Respuesta RPC recibida: $response'); */ - - if (response != null) { - topologiaCompleta = TopologiaCompleta.fromJson(response); - - // Actualizar listas individuales - componentesTopologia = topologiaCompleta!.componentes; - conexionesDatos = topologiaCompleta!.conexionesDatos; - conexionesEnergia = topologiaCompleta!.conexionesEnergia; - - // Sincronizar con estructuras anteriores para retrocompatibilidad - _sincronizarEstructurasAnteriores(); - - print('Topología cargada exitosamente:'); - print('- Componentes: ${componentesTopologia.length}'); - print('- Conexiones de datos: ${conexionesDatos.length}'); - print('- Conexiones de energía: ${conexionesEnergia.length}'); - - problemasTopologia = _validarTopologiaCompleta(); - } else { - print('Respuesta RPC nula, cargando datos con métodos alternativos'); - await _cargarTopologiaAlternativa(negocioId); - } - } catch (e) { - print('Error en getTopologiaPorNegocio: ${e.toString()}'); - await _cargarTopologiaAlternativa(negocioId); - } finally { - isLoadingTopologia = false; - _safeNotifyListeners(); - } - } - - void _sincronizarEstructurasAnteriores() { - // Convertir ComponenteTopologia a Componente para retrocompatibilidad - componentes = componentesTopologia.map((ct) { - return Componente( - id: ct.id, - negocioId: negocioSeleccionadoId ?? '', - categoriaId: ct.categoriaId, - nombre: ct.nombre, - descripcion: ct.descripcion, - ubicacion: ct.ubicacion, - imagenUrl: ct.imagenUrl, - enUso: ct.enUso, - activo: ct.activo, - fechaRegistro: ct.fechaRegistro, - distribucionId: ct.distribucionId, - ); - }).toList(); - - // Convertir ConexionDatos a ConexionComponente para retrocompatibilidad - conexiones = conexionesDatos.map((cd) { - return ConexionComponente( - id: cd.id, - componenteOrigenId: cd.componenteOrigenId, - componenteDestinoId: cd.componenteDestinoId, - descripcion: cd.descripcion, - activo: cd.activo, - ); - }).toList(); - - // Construir filas para la tabla - _buildComponentesRows(); - } - - Future _cargarTopologiaAlternativa(String negocioId) async { - try { - problemasTopologia = ['Usando método alternativo de carga de datos']; - - await Future.wait([ - getComponentesPorNegocio(negocioId), - getDistribucionesPorNegocio(negocioId), - getConexionesPorNegocio(negocioId), - ]); - - problemasTopologia.addAll(validarTopologia()); - } catch (e) { - problemasTopologia = [ - 'Error al cargar datos de topología: ${e.toString()}' - ]; - } - } - - List _validarTopologiaCompleta() { - List problemas = []; - - if (componentesTopologia.isEmpty) { - problemas.add('No se encontraron componentes para este negocio'); - return problemas; - } - - final mdfComponents = componentesTopologia.where((c) => c.esMDF).toList(); - final idfComponents = componentesTopologia.where((c) => c.esIDF).toList(); - - if (mdfComponents.isEmpty) { - problemas - .add('No se encontraron componentes MDF (distribución principal)'); - } - - if (idfComponents.isEmpty) { - problemas.add( - 'No se encontraron componentes IDF (distribuciones intermedias)'); - } - - final sinUbicacion = componentesTopologia - .where((c) => - c.activo && (c.ubicacion == null || c.ubicacion!.trim().isEmpty)) - .length; - if (sinUbicacion > 0) { - problemas.add('$sinUbicacion componentes activos sin ubicación definida'); - } - - final componentesActivos = - componentesTopologia.where((c) => c.activo).length; - final conexionesDatosActivas = - conexionesDatos.where((c) => c.activo).length; - - if (componentesActivos > 1 && conexionesDatosActivas == 0) { - problemas.add('No se encontraron conexiones de datos entre componentes'); - } - - return problemas; - } - - // MÉTODOS PARA CATEGORÍAS (mantenidos para retrocompatibilidad) - Future getCategorias([String? busqueda]) async { - try { - var query = supabaseLU.from('categoria_componente').select(); - - if (busqueda != null && busqueda.isNotEmpty) { - query = query.ilike('nombre', '%$busqueda%'); - } - - final res = await query.order('nombre', ascending: true); - - categorias = (res as List) - .map((categoria) => CategoriaComponente.fromMap(categoria)) - .toList(); - - _buildCategoriasRows(); - _safeNotifyListeners(); - } catch (e) { - print('Error en getCategorias: ${e.toString()}'); - } - } - - void _buildCategoriasRows() { - categoriasRows.clear(); - - for (CategoriaComponente categoria in categorias) { - categoriasRows.add(PlutoRow(cells: { - 'id': PlutoCell(value: categoria.id), - 'nombre': PlutoCell(value: categoria.nombre), - 'editar': PlutoCell(value: categoria.id), - 'eliminar': PlutoCell(value: categoria.id), - })); - } - } - - // MÉTODOS PARA COMPONENTES (mantenidos para retrocompatibilidad) - Future getComponentesPorNegocio(String negocioId, - [String? busqueda]) async { - try { - var query = supabaseLU.from('componente').select(''' - *, - categoria_componente!inner(id, nombre) - ''').eq('negocio_id', negocioId); - - if (busqueda != null && busqueda.isNotEmpty) { - query = query.or( - 'nombre.ilike.%$busqueda%,descripcion.ilike.%$busqueda%,ubicacion.ilike.%$busqueda%'); - } - - final res = await query.order('fecha_registro', ascending: false); - - componentes = (res as List) - .map((componente) => Componente.fromMap(componente)) - .toList(); - - _buildComponentesRows(); - _safeNotifyListeners(); - } catch (e) { - print('Error en getComponentesPorNegocio: ${e.toString()}'); - } - } - - void _buildComponentesRows() { - componentesRows.clear(); - - for (Componente componente in componentes) { - componentesRows.add(PlutoRow(cells: { - 'id': PlutoCell(value: componente.id), - 'negocio_id': PlutoCell(value: componente.negocioId), - 'categoria_id': PlutoCell(value: componente.categoriaId), - 'categoria_nombre': PlutoCell( - value: getCategoriaById(componente.categoriaId)?.nombre ?? - 'Sin categoría'), - 'nombre': PlutoCell(value: componente.nombre), - 'descripcion': PlutoCell(value: componente.descripcion ?? ''), - 'en_uso': PlutoCell(value: componente.enUso ? 'Sí' : 'No'), - 'activo': PlutoCell(value: componente.activo ? 'Sí' : 'No'), - 'ubicacion': PlutoCell(value: componente.ubicacion ?? ''), - 'fecha_registro': - PlutoCell(value: componente.fechaRegistro.toString().split(' ')[0]), - 'imagen_url': PlutoCell( - value: componente.imagenUrl != null - ? "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/componentes/${componente.imagenUrl}?${DateTime.now().millisecondsSinceEpoch}" - : '', - ), - 'editar': PlutoCell(value: componente.id), - 'eliminar': PlutoCell(value: componente.id), - 'ver_detalles': PlutoCell(value: componente.id), - })); - } - } - - // MÉTODOS PARA DISTRIBUCIONES (mantenidos para retrocompatibilidad) - Future getDistribucionesPorNegocio(String negocioId) async { - try { - final res = await supabaseLU - .from('distribucion') - .select() - .eq('negocio_id', negocioId) - .order('tipo', ascending: false); - - distribuciones = (res as List) - .map((distribucion) => Distribucion.fromMap(distribucion)) - .toList(); - - print('Distribuciones cargadas: ${distribuciones.length}'); - } catch (e) { - print('Error en getDistribucionesPorNegocio: ${e.toString()}'); - distribuciones = []; - } - } - - // MÉTODOS PARA CONEXIONES (mantenidos para retrocompatibilidad) - Future getConexionesPorNegocio(String negocioId) async { - try { - List res; - - try { - res = await supabaseLU - .from('vista_conexiones_por_negocio') - .select() - .eq('negocio_id', negocioId); - } catch (e) { - final componentesDelNegocio = await supabaseLU - .from('componente') - .select('id') - .eq('negocio_id', negocioId); - - if (componentesDelNegocio.isEmpty) { - conexiones = []; - return; - } - - final componenteIds = - componentesDelNegocio.map((comp) => comp['id'] as String).toList(); - - res = await supabaseLU - .from('conexion_componente') - .select() - .or('componente_origen_id.in.(${componenteIds.join(',')}),componente_destino_id.in.(${componenteIds.join(',')})') - .eq('activo', true); - } - - conexiones = (res as List) - .map((conexion) => ConexionComponente.fromMap(conexion)) - .toList(); - - print('Conexiones cargadas: ${conexiones.length}'); - } catch (e) { - print('Error en getConexionesPorNegocio: ${e.toString()}'); - conexiones = []; - } - } - - // MÉTODOS DE VALIDACIÓN (mantenidos para retrocompatibilidad) - List validarTopologia() { - List problemas = []; - - if (componentes.isEmpty) { - problemas.add('No se encontraron componentes para este negocio'); - return problemas; - } - - final mdfComponents = getComponentesPorTipo('mdf'); - final idfComponents = getComponentesPorTipo('idf'); - - if (mdfComponents.isEmpty) { - problemas - .add('No se encontraron componentes MDF (distribución principal)'); - } - - if (idfComponents.isEmpty) { - problemas.add( - 'No se encontraron componentes IDF (distribuciones intermedias)'); - } - - final sinUbicacion = componentes - .where((c) => - c.activo && (c.ubicacion == null || c.ubicacion!.trim().isEmpty)) - .length; - if (sinUbicacion > 0) { - problemas.add('$sinUbicacion componentes activos sin ubicación definida'); - } - - final componentesActivos = componentes.where((c) => c.activo).length; - final conexionesActivas = conexiones.where((c) => c.activo).length; - - if (componentesActivos > 1 && conexionesActivas == 0) { - problemas.add('No se encontraron conexiones entre componentes'); - } - - return problemas; - } - - // MÉTODOS DE UTILIDAD (mantenidos) - CategoriaComponente? getCategoriaById(int categoriaId) { - try { - return categorias.firstWhere((c) => c.id == categoriaId); - } catch (e) { - return null; - } - } - - Componente? getComponenteById(String componenteId) { - try { - return componentes.firstWhere((c) => c.id == componenteId); - } catch (e) { - return null; - } - } - - ComponenteTopologia? getComponenteTopologiaById(String componenteId) { - try { - return componentesTopologia.firstWhere((c) => c.id == componenteId); - } catch (e) { - return null; - } - } - - List getComponentesPorTipo(String tipo) { - return componentes.where((c) { - if (!c.activo) return false; - - final categoria = getCategoriaById(c.categoriaId); - final nombreCategoria = categoria?.nombre.toLowerCase() ?? ''; - - switch (tipo.toLowerCase()) { - case 'mdf': - return c.ubicacion?.toLowerCase().contains('mdf') == true || - nombreCategoria.contains('mdf') || - c.descripcion?.toLowerCase().contains('mdf') == true; - case 'idf': - return c.ubicacion?.toLowerCase().contains('idf') == true || - nombreCategoria.contains('idf') || - c.descripcion?.toLowerCase().contains('idf') == true; - case 'switch': - return nombreCategoria.contains('switch'); - case 'router': - return nombreCategoria.contains('router') || - nombreCategoria.contains('firewall'); - case 'servidor': - case 'server': - return nombreCategoria.contains('servidor') || - nombreCategoria.contains('server'); - default: - return false; - } - }).toList(); - } - - // MÉTODOS PARA IMÁGENES (mantenidos) - Future selectImagen() async { - imagenFileName = null; - imagenToUpload = null; - - FilePickerResult? picker = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['jpg', 'png', 'jpeg'], - ); - - if (picker != null) { - var now = DateTime.now(); - var formatter = DateFormat('yyyyMMddHHmmss'); - var timestamp = formatter.format(now); - - imagenFileName = 'componente-$timestamp-${picker.files.single.name}'; - imagenToUpload = picker.files.single.bytes; - } - - _safeNotifyListeners(); - } - - Future uploadImagen() async { - if (imagenToUpload != null && imagenFileName != null) { - await supabaseLU.storage.from('nethive/componentes').uploadBinary( - imagenFileName!, - imagenToUpload!, - fileOptions: const FileOptions( - cacheControl: '3600', - upsert: false, - ), - ); - return imagenFileName; - } - return null; - } - - // MÉTODOS DE UTILIDAD PARA FORMULARIOS (mantenidos) - void resetFormData() { - imagenFileName = null; - imagenToUpload = null; - categoriaSeleccionadaId = null; - _safeNotifyListeners(); - } - - void buscarComponentes(String busqueda) { - if (negocioSeleccionadoId != null) { - getComponentesPorNegocio( - negocioSeleccionadoId!, busqueda.isEmpty ? null : busqueda); - } - } - - void buscarCategorias(String busqueda) { - getCategorias(busqueda.isEmpty ? null : busqueda); - } - - Widget? getImageWidget(dynamic image, - {double height = 100, double width = 100}) { - if (image == null || image.toString().isEmpty) { - return Container( - height: height, - width: width, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - ), - child: Image.asset( - 'assets/images/placeholder_no_image.jpg', - height: height, - width: width, - fit: BoxFit.cover, - ), - ); - } else if (image is Uint8List) { - return Image.memory( - image, - height: height, - width: width, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - height: height, - width: width, - fit: BoxFit.cover, - ); - }, - ); - } else if (image is String) { - return Image.network( - image, - height: height, - width: width, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - height: height, - width: width, - fit: BoxFit.cover, - ); - }, - ); - } - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - height: height, - width: width, - fit: BoxFit.cover, - ); - } - - // MÉTODOS CRUD MANTENIDOS PARA COMPATIBILIDAD - Future crearComponente({ - required String negocioId, - required int categoriaId, - required String nombre, - String? descripcion, - required bool enUso, - required bool activo, - String? ubicacion, - }) async { - try { - final imagenUrl = await uploadImagen(); - - final res = await supabaseLU.from('componente').insert({ - 'negocio_id': negocioId, - 'categoria_id': categoriaId, - 'nombre': nombre, - 'descripcion': descripcion, - 'en_uso': enUso, - 'activo': activo, - 'ubicacion': ubicacion, - 'imagen_url': imagenUrl, - }).select(); - - if (res.isNotEmpty) { - await getTopologiaPorNegocio(negocioId); - resetFormData(); - return true; - } - return false; - } catch (e) { - print('Error en crearComponente: ${e.toString()}'); - return false; - } - } - - Future actualizarComponente({ - required String componenteId, - required String negocioId, - required int categoriaId, - required String nombre, - String? descripcion, - required bool enUso, - required bool activo, - String? ubicacion, - bool actualizarImagen = false, - }) async { - try { - Map updateData = { - 'categoria_id': categoriaId, - 'nombre': nombre, - 'descripcion': descripcion, - 'en_uso': enUso, - 'activo': activo, - 'ubicacion': ubicacion, - }; - - if (actualizarImagen) { - final imagenUrl = await uploadImagen(); - if (imagenUrl != null) { - updateData['imagen_url'] = imagenUrl; - } - } - - final res = await supabaseLU - .from('componente') - .update(updateData) - .eq('id', componenteId) - .select(); - - if (res.isNotEmpty) { - await getTopologiaPorNegocio(negocioId); - resetFormData(); - return true; - } - return false; - } catch (e) { - print('Error en actualizarComponente: ${e.toString()}'); - return false; - } - } - - Future eliminarComponente(String componenteId) async { - try { - final componenteData = await supabaseLU - .from('componente') - .select('imagen_url') - .eq('id', componenteId) - .maybeSingle(); - - String? imagenUrl; - if (componenteData != null && componenteData['imagen_url'] != null) { - imagenUrl = componenteData['imagen_url'] as String; - } - - await _eliminarDetallesComponente(componenteId); - await supabaseLU.from('componente').delete().eq('id', componenteId); - - if (!_isDisposed && negocioSeleccionadoId != null) { - await getTopologiaPorNegocio(negocioSeleccionadoId!); - } - - if (imagenUrl != null) { - try { - await supabaseLU.storage - .from('nethive') - .remove(["componentes/$imagenUrl"]); - } catch (storageError) { - print( - 'Error al eliminar imagen del storage: ${storageError.toString()}'); - } - } - - return true; - } catch (e) { - print('Error en eliminarComponente: ${e.toString()}'); - return false; - } - } - - Future _eliminarDetallesComponente(String componenteId) async { - try { - await Future.wait([ - supabaseLU - .from('detalle_cable') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_switch') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_patch_panel') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_rack') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_organizador') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_ups') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_router_firewall') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_equipo_activo') - .delete() - .eq('componente_id', componenteId), - supabaseLU.from('conexion_componente').delete().or( - 'componente_origen_id.eq.$componenteId,componente_destino_id.eq.$componenteId'), - ]); - } catch (e) { - print('Error al eliminar detalles del componente: ${e.toString()}'); - } - } - - Future crearCategoria(String nombre) async { - try { - final res = await supabaseLU.from('categoria_componente').insert({ - 'nombre': nombre, - }).select(); - - if (res.isNotEmpty) { - await getCategorias(); - return true; - } - return false; - } catch (e) { - print('Error en crearCategoria: ${e.toString()}'); - return false; - } - } - - Future actualizarCategoria(int id, String nombre) async { - try { - final res = await supabaseLU - .from('categoria_componente') - .update({'nombre': nombre}) - .eq('id', id) - .select(); - - if (res.isNotEmpty) { - await getCategorias(); - return true; - } - return false; - } catch (e) { - print('Error en actualizarCategoria: ${e.toString()}'); - return false; - } - } - - Future eliminarCategoria(int id) async { - try { - await supabaseLU.from('categoria_componente').delete().eq('id', id); - await getCategorias(); - return true; - } catch (e) { - print('Error en eliminarCategoria: ${e.toString()}'); - return false; - } - } - - // MÉTODOS DE GESTIÓN DE TOPOLOGÍA OPTIMIZADA - Future setNegocioSeleccionado( - String negocioId, String negocioNombre, String empresaId) async { - try { - negocioSeleccionadoId = negocioId; - negocioSeleccionadoNombre = negocioNombre; - empresaSeleccionadaId = empresaId; - - _limpiarDatosAnteriores(); - - // Cargar datos en paralelo - await Future.wait([ - getTopologiaPorNegocio(negocioId), - getRacksConComponentes(negocioId), - ]); - - _safeNotifyListeners(); - } catch (e) { - print('Error en setNegocioSeleccionado: ${e.toString()}'); - } - } - - void _limpiarDatosAnteriores() { - topologiaCompleta = null; - componentesTopologia.clear(); - conexionesDatos.clear(); - conexionesEnergia.clear(); - - componentes.clear(); - distribuciones.clear(); - conexiones.clear(); - conexionesConCables.clear(); - topologiaOptimizada.clear(); - componentesRows.clear(); - showDetallesEspecificos = false; - - detalleCable = null; - detalleSwitch = null; - detallePatchPanel = null; - detalleRack = null; - detalleOrganizador = null; - detalleUps = null; - detalleRouterFirewall = null; - detalleEquipoActivo = null; - - racksConComponentes.clear(); - isLoadingRacks = false; - } - - // MÉTODOS DE UTILIDAD PARA TOPOLOGÍA - List getComponentesPorTipoTopologia(String tipo) { - return componentesTopologia.where((c) { - if (!c.activo) return false; - - switch (tipo.toLowerCase()) { - case 'mdf': - return c.esMDF; - case 'idf': - return c.esIDF; - case 'switch': - return c.esSwitch; - case 'router': - return c.esRouter; - case 'servidor': - case 'server': - return c.esServidor; - case 'ups': - return c.esUPS; - case 'rack': - return c.esRack; - case 'patch': - case 'panel': - return c.esPatchPanel; - default: - return false; - } - }).toList() - ..sort((a, b) => a.prioridadTopologia.compareTo(b.prioridadTopologia)); - } - - List getComponentesMDF() { - return getComponentesPorTipoTopologia('mdf'); - } - - List getComponentesIDF() { - return getComponentesPorTipoTopologia('idf'); - } - - List getComponentesSwitch() { - return getComponentesPorTipoTopologia('switch'); - } - - List getComponentesRouter() { - return getComponentesPorTipoTopologia('router'); - } - - List getComponentesServidor() { - return getComponentesPorTipoTopologia('servidor'); - } - - List getConexionesPorComponente(String componenteId) { - return conexionesDatos - .where((c) => - c.componenteOrigenId == componenteId || - c.componenteDestinoId == componenteId) - .toList(); - } - - List getConexionesEnergiaPorComponente(String componenteId) { - return conexionesEnergia - .where((c) => c.origenId == componenteId || c.destinoId == componenteId) - .toList(); - } - - // MÉTODOS PARA CARGAR RACKS CON COMPONENTES - Future getRacksConComponentes(String negocioId) async { - try { - isLoadingRacks = true; - _safeNotifyListeners(); - - print( - 'Llamando a función RPC fn_racks_con_componentes con negocio_id: $negocioId'); - - final response = - await supabaseLU.rpc('fn_racks_con_componentes', params: { - 'p_negocio_id': negocioId, - }); - - print('Respuesta RPC racks: $response'); - - if (response != null && response is List) { - racksConComponentes = - (response).map((rack) => RackConComponentes.fromMap(rack)).toList(); - - print('Racks cargados: ${racksConComponentes.length}'); - for (var rack in racksConComponentes) { - print( - '- ${rack.nombreRack}: ${rack.cantidadComponentes} componentes'); - } - } else { - racksConComponentes = []; - print('No se encontraron racks o respuesta vacía'); - } - } catch (e) { - print('Error en getRacksConComponentes: ${e.toString()}'); - racksConComponentes = []; - } finally { - isLoadingRacks = false; - _safeNotifyListeners(); - } - } - - // MÉTODOS DE UTILIDAD PARA RACKS - RackConComponentes? getRackById(String rackId) { - try { - return racksConComponentes.firstWhere((rack) => rack.rackId == rackId); - } catch (e) { - return null; - } - } - - int get totalRacks => racksConComponentes.length; - - int get totalComponentesEnRacks => racksConComponentes.fold( - 0, (sum, rack) => sum + rack.cantidadComponentes); - - int get racksConComponentesActivos => - racksConComponentes.where((rack) => rack.componentesActivos > 0).length; - - double get porcentajeOcupacionPromedio { - if (racksConComponentes.isEmpty) return 0.0; - - final totalOcupacion = racksConComponentes.fold( - 0.0, (sum, rack) => sum + rack.porcentajeOcupacion); - - return totalOcupacion / racksConComponentes.length; - } - - List get racksOrdenadosPorOcupacion { - final racks = [...racksConComponentes]; - racks - .sort((a, b) => b.porcentajeOcupacion.compareTo(a.porcentajeOcupacion)); - return racks; - } - - List get racksConProblemas { - return racksConComponentes.where((rack) { - // Rack con problemas si tiene componentes inactivos o sin posición U - final componentesInactivos = - rack.componentes.where((c) => !c.activo).length; - - final componentesSinPosicion = - rack.componentes.where((c) => c.posicionU == null).length; - - return componentesInactivos > 0 || componentesSinPosicion > 0; - }).toList(); - } -} diff --git a/lib/providers/nethive/empresas_negocios_provider.dart b/lib/providers/nethive/empresas_negocios_provider.dart deleted file mode 100644 index b569cd2..0000000 --- a/lib/providers/nethive/empresas_negocios_provider.dart +++ /dev/null @@ -1,418 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:pluto_grid/pluto_grid.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -import 'package:nethive_neo/helpers/globals.dart'; -import 'package:nethive_neo/models/nethive/empresa_model.dart'; -import 'package:nethive_neo/models/nethive/negocio_model.dart'; - -class EmpresasNegociosProvider extends ChangeNotifier { - // State managers para las grillas - PlutoGridStateManager? empresasStateManager; - PlutoGridStateManager? negociosStateManager; - - // Controladores de búsqueda - final busquedaEmpresaController = TextEditingController(); - final busquedaNegocioController = TextEditingController(); - - // Listas de datos - List empresas = []; - List negocios = []; - List empresasRows = []; - List negociosRows = []; - - // Variables para formularios - String? logoFileName; - String? imagenFileName; - Uint8List? logoToUpload; - Uint8List? imagenToUpload; - - // Variables de selección - String? empresaSeleccionadaId; - Empresa? empresaSeleccionada; - - // Variable para controlar si el provider está activo - bool _isDisposed = false; - - EmpresasNegociosProvider() { - getEmpresas(); - } - - @override - void dispose() { - _isDisposed = true; - busquedaEmpresaController.dispose(); - busquedaNegocioController.dispose(); - super.dispose(); - } - - // Método seguro para notificar listeners - void _safeNotifyListeners() { - if (!_isDisposed) { - notifyListeners(); - } - } - - // Métodos para empresas - Future getEmpresas([String? busqueda]) async { - try { - var query = supabaseLU.from('empresa').select(); - - if (busqueda != null && busqueda.isNotEmpty) { - query = query.or( - 'nombre.ilike.%$busqueda%,rfc.ilike.%$busqueda%,email.ilike.%$busqueda%'); - } - - final res = await query.order('fecha_creacion', ascending: false); - - empresas = (res as List) - .map((empresa) => Empresa.fromMap(empresa)) - .toList(); - - _buildEmpresasRows(); - _safeNotifyListeners(); - } catch (e) { - print('Error en getEmpresas: ${e.toString()}'); - } - } - - void _buildEmpresasRows() { - empresasRows.clear(); - - for (Empresa empresa in empresas) { - empresasRows.add(PlutoRow(cells: { - 'id': PlutoCell(value: empresa.id), - 'nombre': PlutoCell(value: empresa.nombre), - 'rfc': PlutoCell(value: empresa.rfc), - 'direccion': PlutoCell(value: empresa.direccion), - 'telefono': PlutoCell(value: empresa.telefono), - 'email': PlutoCell(value: empresa.email), - 'fecha_creacion': - PlutoCell(value: empresa.fechaCreacion.toString().split(' ')[0]), - 'logo_url': PlutoCell( - value: empresa.logoUrl != null - ? "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${empresa.logoUrl}?${DateTime.now().millisecondsSinceEpoch}" - : '', - ), - 'imagen_url': PlutoCell( - value: empresa.imagenUrl != null - ? "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/imagenes/${empresa.imagenUrl}?${DateTime.now().millisecondsSinceEpoch}" - : '', - ), - 'editar': PlutoCell(value: empresa.id), - 'eliminar': PlutoCell(value: empresa.id), - 'ver_negocios': PlutoCell(value: empresa.id), - })); - } - } - - Future getNegociosPorEmpresa(String empresaId) async { - try { - final res = await supabaseLU - .from('negocio') - .select() - .eq('empresa_id', empresaId) - .order('fecha_creacion', ascending: false); - - negocios = (res as List) - .map((negocio) => Negocio.fromMap(negocio)) - .toList(); - - _buildNegociosRows(); - _safeNotifyListeners(); - } catch (e) { - print('Error en getNegociosPorEmpresa: ${e.toString()}'); - } - } - - void _buildNegociosRows() { - negociosRows.clear(); - - for (Negocio negocio in negocios) { - negociosRows.add(PlutoRow(cells: { - 'id': PlutoCell(value: negocio.id), - 'empresa_id': PlutoCell(value: negocio.empresaId), - 'nombre': PlutoCell(value: negocio.nombre), - 'direccion': PlutoCell(value: negocio.direccion), - 'direccion_completa': PlutoCell( - value: negocio.direccion), // Nuevo campo para la segunda columna - 'latitud': PlutoCell(value: negocio.latitud.toString()), - 'longitud': PlutoCell(value: negocio.longitud.toString()), - 'tipo_local': PlutoCell(value: negocio.tipoLocal), - 'fecha_creacion': - PlutoCell(value: negocio.fechaCreacion.toString().split(' ')[0]), - 'logo_url': PlutoCell( - value: negocio.logoUrl != null - ? "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${negocio.logoUrl}?${DateTime.now().millisecondsSinceEpoch}" - : '', - ), - 'imagen_url': PlutoCell( - value: negocio.imagenUrl != null - ? "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/imagenes/${negocio.imagenUrl}?${DateTime.now().millisecondsSinceEpoch}" - : '', - ), - 'acceder_infraestructura': PlutoCell(value: negocio.id), - 'editar': PlutoCell(value: negocio.id), - 'eliminar': PlutoCell(value: negocio.id), - 'ver_componentes': PlutoCell(value: negocio.id), - })); - } - } - - // Métodos para subir archivos - Future selectLogo() async { - logoFileName = null; - logoToUpload = null; - - FilePickerResult? picker = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['jpg', 'png', 'jpeg'], - ); - - if (picker != null) { - var now = DateTime.now(); - var formatter = DateFormat('yyyyMMddHHmmss'); - var timestamp = formatter.format(now); - - logoFileName = 'logo-$timestamp-${picker.files.single.name}'; - logoToUpload = picker.files.single.bytes; - - // Notificar inmediatamente después de seleccionar - _safeNotifyListeners(); - } - } - - Future selectImagen() async { - imagenFileName = null; - imagenToUpload = null; - - FilePickerResult? picker = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['jpg', 'png', 'jpeg'], - ); - - if (picker != null) { - var now = DateTime.now(); - var formatter = DateFormat('yyyyMMddHHmmss'); - var timestamp = formatter.format(now); - - imagenFileName = 'imagen-$timestamp-${picker.files.single.name}'; - imagenToUpload = picker.files.single.bytes; - - // Notificar inmediatamente después de seleccionar - _safeNotifyListeners(); - } - } - - Future uploadLogo() async { - if (logoToUpload != null && logoFileName != null) { - await supabaseLU.storage.from('nethive/logos').uploadBinary( - logoFileName!, - logoToUpload!, - fileOptions: const FileOptions( - cacheControl: '3600', - upsert: false, - ), - ); - return logoFileName; - } - return null; - } - - Future uploadImagen() async { - if (imagenToUpload != null && imagenFileName != null) { - await supabaseLU.storage.from('nethive/imagenes').uploadBinary( - imagenFileName!, - imagenToUpload!, - fileOptions: const FileOptions( - cacheControl: '3600', - upsert: false, - ), - ); - return imagenFileName; - } - return null; - } - - // CRUD Empresas - Future crearEmpresa({ - required String nombre, - required String rfc, - required String direccion, - required String telefono, - required String email, - }) async { - try { - final logoUrl = await uploadLogo(); - final imagenUrl = await uploadImagen(); - - final res = await supabaseLU.from('empresa').insert({ - 'nombre': nombre, - 'rfc': rfc, - 'direccion': direccion, - 'telefono': telefono, - 'email': email, - 'logo_url': logoUrl, - 'imagen_url': imagenUrl, - }).select(); - - if (res.isNotEmpty) { - await getEmpresas(); - resetFormData(); - return true; - } - return false; - } catch (e) { - print('Error en crearEmpresa: ${e.toString()}'); - return false; - } - } - - Future crearNegocio({ - required String empresaId, - required String nombre, - required String direccion, - required double latitud, - required double longitud, - required String tipoLocal, - }) async { - try { - final logoUrl = await uploadLogo(); - final imagenUrl = await uploadImagen(); - - final res = await supabaseLU.from('negocio').insert({ - 'empresa_id': empresaId, - 'nombre': nombre, - 'direccion': direccion, - 'latitud': latitud, - 'longitud': longitud, - 'tipo_local': tipoLocal, - 'logo_url': logoUrl, - 'imagen_url': imagenUrl, - }).select(); - - if (res.isNotEmpty) { - await getNegociosPorEmpresa(empresaId); - resetFormData(); - return true; - } - return false; - } catch (e) { - print('Error en crearNegocio: ${e.toString()}'); - return false; - } - } - - Future eliminarEmpresa(String empresaId) async { - try { - // Primero eliminar todos los negocios asociados - await supabaseLU.from('negocio').delete().eq('empresa_id', empresaId); - - // Luego eliminar la empresa - await supabaseLU.from('empresa').delete().eq('id', empresaId); - - // Solo actualizar si el provider sigue activo - if (!_isDisposed) { - await getEmpresas(); - } - return true; - } catch (e) { - print('Error en eliminarEmpresa: ${e.toString()}'); - return false; - } - } - - Future eliminarNegocio(String negocioId) async { - try { - await supabaseLU.from('negocio').delete().eq('id', negocioId); - - // Solo actualizar si el provider sigue activo y hay una empresa seleccionada - if (!_isDisposed && empresaSeleccionadaId != null) { - await getNegociosPorEmpresa(empresaSeleccionadaId!); - } - return true; - } catch (e) { - print('Error en eliminarNegocio: ${e.toString()}'); - return false; - } - } - - // Métodos de utilidad - void setEmpresaSeleccionada(String empresaId) { - empresaSeleccionadaId = empresaId; - empresaSeleccionada = empresas.firstWhere((e) => e.id == empresaId); - getNegociosPorEmpresa(empresaId); - _safeNotifyListeners(); - } - - void resetFormData() { - logoFileName = null; - imagenFileName = null; - logoToUpload = null; - imagenToUpload = null; - _safeNotifyListeners(); - } - - void buscarEmpresas(String busqueda) { - getEmpresas(busqueda.isEmpty ? null : busqueda); - } - - Widget? getImageWidget(dynamic image, - {double height = 100, double width = 100}) { - if (image == null || image.toString().isEmpty) { - return Container( - height: height, - width: width, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - ), - child: Image.asset( - 'assets/images/placeholder_no_image.jpg', - height: height, - width: width, - fit: BoxFit.cover, - ), - ); - } else if (image is Uint8List) { - return Image.memory( - image, - height: height, - width: width, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - height: height, - width: width, - fit: BoxFit.cover, - ); - }, - ); - } else if (image is String) { - return Image.network( - image, - height: height, - width: width, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - height: height, - width: width, - fit: BoxFit.cover, - ); - }, - ); - } - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - height: height, - width: width, - fit: BoxFit.cover, - ); - } -} diff --git a/lib/providers/nethive/navigation_provider.dart b/lib/providers/nethive/navigation_provider.dart deleted file mode 100644 index 40b02a5..0000000 --- a/lib/providers/nethive/navigation_provider.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nethive_neo/models/nethive/negocio_model.dart'; -import 'package:nethive_neo/models/nethive/empresa_model.dart'; -import 'package:nethive_neo/helpers/globals.dart'; - -class NavigationProvider extends ChangeNotifier { - // Estados principales - String? _negocioSeleccionadoId; - Negocio? _negocioSeleccionado; - Empresa? _empresaSeleccionada; - int _selectedMenuIndex = 0; - - // Getters - String? get negocioSeleccionadoId => _negocioSeleccionadoId; - Negocio? get negocioSeleccionado => _negocioSeleccionado; - Empresa? get empresaSeleccionada => _empresaSeleccionada; - int get selectedMenuIndex => _selectedMenuIndex; - - // Lista de opciones del sidemenu - final List menuItems = [ - NavigationMenuItem( - title: 'Dashboard', - icon: Icons.dashboard, - route: '/dashboard', - index: 0, - ), - NavigationMenuItem( - title: 'Inventario', - icon: Icons.inventory_2, - route: '/inventario', - index: 1, - ), - NavigationMenuItem( - title: 'Topología', - icon: Icons.account_tree, - route: '/topologia', - index: 2, - ), - NavigationMenuItem( - title: 'Alertas', - icon: Icons.warning, - route: '/alertas', - index: 3, - ), - NavigationMenuItem( - title: 'Configuración', - icon: Icons.settings, - route: '/configuracion', - index: 4, - ), - NavigationMenuItem( - title: 'Empresas', - icon: Icons.business, - route: '/empresas', - index: 5, - isSpecial: true, // Para diferenciarlo como opción de regreso - ), - ]; - - // Métodos para establecer el negocio seleccionado - Future setNegocioSeleccionado(String negocioId) async { - try { - _negocioSeleccionadoId = negocioId; - - // Obtener datos completos del negocio - final negocioResponse = await supabaseLU.from('negocio').select(''' - *, - empresa!inner(*) - ''').eq('id', negocioId).single(); - - _negocioSeleccionado = Negocio.fromMap(negocioResponse); - _empresaSeleccionada = Empresa.fromMap(negocioResponse['empresa']); - - // Reset menu selection when changing business - _selectedMenuIndex = 0; - - notifyListeners(); - } catch (e) { - print('Error al establecer negocio seleccionado: $e'); - } - } - - // Método para cambiar la selección del menú - void setSelectedMenuIndex(int index) { - _selectedMenuIndex = index; - notifyListeners(); - } - - // Método para limpiar la selección (al regresar a empresas) - void clearSelection() { - _negocioSeleccionadoId = null; - _negocioSeleccionado = null; - _empresaSeleccionada = null; - _selectedMenuIndex = 0; - notifyListeners(); - } - - // Método para obtener el item del menú por índice - NavigationMenuItem getMenuItemByIndex(int index) { - return menuItems.firstWhere((item) => item.index == index); - } - - // Método para obtener el item del menú por ruta - NavigationMenuItem? getMenuItemByRoute(String route) { - try { - return menuItems.firstWhere((item) => item.route == route); - } catch (e) { - return null; - } - } -} - -// Modelo para los items del menú -class NavigationMenuItem { - final String title; - final IconData icon; - final String route; - final int index; - final bool isSpecial; - - NavigationMenuItem({ - required this.title, - required this.icon, - required this.route, - required this.index, - this.isSpecial = false, - }); -} diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index e13fa1d..0492a29 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -1,6 +1,3 @@ export 'package:nethive_neo/providers/visual_state_provider.dart'; export 'package:nethive_neo/providers/users_provider.dart'; export 'package:nethive_neo/providers/user_provider.dart'; -export 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -export 'package:nethive_neo/providers/nethive/componentes_provider.dart'; -export 'package:nethive_neo/providers/nethive/navigation_provider.dart'; diff --git a/lib/providers/videos_provider.dart b/lib/providers/videos_provider.dart new file mode 100644 index 0000000..1b94566 --- /dev/null +++ b/lib/providers/videos_provider.dart @@ -0,0 +1,690 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:pluto_grid/pluto_grid.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:path/path.dart' as p; +import 'package:nethive_neo/helpers/globals.dart'; +import 'package:nethive_neo/models/media/media_models.dart'; + +class VideosProvider extends ChangeNotifier { + // ========== ORGANIZATION CONSTANT ========== + static const int organizationId = 17; + + // ========== STATE MANAGEMENT ========== + PlutoGridStateManager? stateManager; + List videosRows = []; + + // ========== DATA LISTS ========== + List mediaFiles = []; + List categories = []; + List mediaWithPosters = []; + + // ========== CONTROLLERS ========== + final busquedaVideoController = TextEditingController(); + final tituloController = TextEditingController(); + final descripcionController = TextEditingController(); + + // ========== VIDEO/IMAGE UPLOAD STATE ========== + String? videoName; + String? videoUrl; + String? videoStoragePath; + String videoFileExtension = ''; + Uint8List? webVideoBytes; + + String? posterName; + String? posterUrl; + String? posterStoragePath; + String posterFileExtension = ''; + Uint8List? webPosterBytes; + + // ========== LOADING STATE ========== + bool isLoading = false; + String? errorMessage; + + // ========== CONSTRUCTOR ========== + VideosProvider() { + loadMediaFiles(); + loadCategories(); + } + + // ========== LOAD METHODS ========== + + /// Load all media files with organization filter + Future loadMediaFiles() async { + try { + isLoading = true; + errorMessage = null; + notifyListeners(); + + final response = await supabaseML + .from('media_files') + .select() + .eq('organization_fk', organizationId) + .order('created_at_timestamp', ascending: false); + + mediaFiles = (response as List) + .map((item) => MediaFileModel.fromMap(item)) + .toList(); + + await _buildPlutoRows(); + + isLoading = false; + notifyListeners(); + } catch (e) { + errorMessage = 'Error cargando videos: $e'; + isLoading = false; + notifyListeners(); + print('Error en loadMediaFiles: $e'); + } + } + + /// Load media files with posters using view + Future loadMediaWithPosters() async { + try { + isLoading = true; + notifyListeners(); + + final response = await supabaseML + .from('vw_media_files_with_posters') + .select() + .eq('organization_fk', organizationId) + .order('media_created_at', ascending: false); + + mediaWithPosters = (response as List) + .map((item) => MediaWithPosterModel.fromMap(item)) + .toList(); + + isLoading = false; + notifyListeners(); + } catch (e) { + errorMessage = 'Error cargando videos con posters: $e'; + isLoading = false; + notifyListeners(); + print('Error en loadMediaWithPosters: $e'); + } + } + + /// Load all categories + Future loadCategories() async { + try { + final response = await supabaseML + .from('media_categories') + .select() + .order('category_name'); + + categories = (response as List) + .map((item) => MediaCategoryModel.fromMap(item)) + .toList(); + + notifyListeners(); + } catch (e) { + print('Error en loadCategories: $e'); + } + } + + /// Build PlutoGrid rows from media files + Future _buildPlutoRows() async { + videosRows.clear(); + + for (var media in mediaFiles) { + videosRows.add( + PlutoRow( + cells: { + 'id': PlutoCell(value: media.mediaFileId), + 'thumbnail': + PlutoCell(value: media.fileUrl), // Para mostrar thumbnail + 'title': PlutoCell(value: media.title ?? media.fileName), + 'description': PlutoCell(value: media.fileDescription ?? ''), + 'category': + PlutoCell(value: _getCategoryName(media.mediaCategoryFk)), + 'reproducciones': PlutoCell(value: media.reproducciones), + 'duration': PlutoCell(value: media.seconds ?? 0), + 'size': PlutoCell(value: _formatFileSize(media.fileSizeBytes)), + 'created_at': PlutoCell(value: media.createdAt), + 'actions': PlutoCell(value: media.mediaFileId), + }, + ), + ); + } + } + + /// Get category name by ID + String _getCategoryName(int? categoryId) { + if (categoryId == null) return 'Sin categoría'; + try { + return categories + .firstWhere((cat) => cat.mediaCategoriesId == categoryId) + .categoryName; + } catch (e) { + return 'Sin categoría'; + } + } + + /// Format file size to human readable + String _formatFileSize(int? bytes) { + if (bytes == null) return '-'; + if (bytes < 1024) return '$bytes B'; + if (bytes < 1048576) return '${(bytes / 1024).toStringAsFixed(1)} KB'; + if (bytes < 1073741824) return '${(bytes / 1048576).toStringAsFixed(1)} MB'; + return '${(bytes / 1073741824).toStringAsFixed(1)} GB'; + } + + // ========== VIDEO UPLOAD ========== + + /// Select video file from device + Future selectVideo() async { + try { + final ImagePicker picker = ImagePicker(); + final XFile? pickedVideo = await picker.pickVideo( + source: ImageSource.gallery, + ); + + if (pickedVideo == null) return false; + + videoName = pickedVideo.name; + videoFileExtension = p.extension(pickedVideo.name); + webVideoBytes = await pickedVideo.readAsBytes(); + + // Remove extension from name for title + final nameWithoutExt = videoName!.replaceAll(videoFileExtension, ''); + tituloController.text = nameWithoutExt; + + notifyListeners(); + return true; + } catch (e) { + errorMessage = 'Error seleccionando video: $e'; + notifyListeners(); + return false; + } + } + + /// Select poster/thumbnail image + Future selectPoster() async { + try { + final ImagePicker picker = ImagePicker(); + final XFile? pickedImage = await picker.pickImage( + source: ImageSource.gallery, + ); + + if (pickedImage == null) return false; + + posterName = pickedImage.name; + posterFileExtension = p.extension(pickedImage.name); + webPosterBytes = await pickedImage.readAsBytes(); + + notifyListeners(); + return true; + } catch (e) { + errorMessage = 'Error seleccionando poster: $e'; + notifyListeners(); + return false; + } + } + + /// Upload video to Supabase Storage and create record + Future uploadVideo({ + required String title, + String? description, + int? categoryId, + int? durationSeconds, + }) async { + if (webVideoBytes == null || videoName == null) { + errorMessage = 'No hay video seleccionado'; + notifyListeners(); + return false; + } + + try { + isLoading = true; + notifyListeners(); + + // 1. Upload video to storage + final timestamp = DateTime.now().millisecondsSinceEpoch; + final fileName = '${timestamp}_$videoName'; + videoStoragePath = 'videos/$fileName'; + + await supabaseML.storage.from('energymedia').uploadBinary( + videoStoragePath!, + webVideoBytes!, + fileOptions: const FileOptions( + cacheControl: '3600', + upsert: false, + ), + ); + + // 2. Get public URL + videoUrl = supabaseML.storage + .from('energymedia') + .getPublicUrl(videoStoragePath!); + + // 3. Upload poster if exists + int? posterFileId; + if (webPosterBytes != null && posterName != null) { + posterFileId = await _uploadPoster(); + } + + // 4. Create media_files record + final metadataJson = { + 'uploaded_at': DateTime.now().toIso8601String(), + 'reproducciones': 0, + 'original_file_name': videoName, + 'duration_seconds': durationSeconds, + }; + + final response = await supabaseML.from('media_files').insert({ + 'file_name': fileName, + 'title': title, + 'file_description': description, + 'file_type': 'video', + 'mime_type': _getMimeType(videoFileExtension), + 'file_extension': videoFileExtension, + 'file_size_bytes': webVideoBytes!.length, + 'file_url': videoUrl, + 'storage_path': videoStoragePath, + 'organization_fk': organizationId, + 'media_category_fk': categoryId, + 'metadata_json': metadataJson, + 'seconds': durationSeconds, + 'is_public_file': true, + 'uploaded_by_user_id': currentUser?.id, + }).select(); + + // 5. Create poster relationship if exists + if (posterFileId != null && response.isNotEmpty) { + final mediaFileId = response[0]['media_file_id']; + await supabaseML.from('media_posters').insert({ + 'media_file_id': mediaFileId, + 'poster_file_id': posterFileId, + }); + } + + // Clean up + _clearUploadState(); + + // Reload data + await loadMediaFiles(); + + isLoading = false; + notifyListeners(); + return true; + } catch (e) { + errorMessage = 'Error subiendo video: $e'; + isLoading = false; + notifyListeners(); + print('Error en uploadVideo: $e'); + return false; + } + } + + /// Upload poster image (internal helper) + Future _uploadPoster() async { + if (webPosterBytes == null || posterName == null) return null; + + try { + final timestamp = DateTime.now().millisecondsSinceEpoch; + final fileName = '${timestamp}_$posterName'; + posterStoragePath = 'imagenes/$fileName'; + + await supabaseML.storage.from('energymedia').uploadBinary( + posterStoragePath!, + webPosterBytes!, + fileOptions: const FileOptions( + cacheControl: '3600', + upsert: false, + ), + ); + + posterUrl = supabaseML.storage + .from('energymedia') + .getPublicUrl(posterStoragePath!); + + // Create media_files record for poster + final response = await supabaseML.from('media_files').insert({ + 'file_name': fileName, + 'title': 'Poster', + 'file_type': 'image', + 'mime_type': _getMimeType(posterFileExtension), + 'file_extension': posterFileExtension, + 'file_size_bytes': webPosterBytes!.length, + 'file_url': posterUrl, + 'storage_path': posterStoragePath, + 'organization_fk': organizationId, + 'is_public_file': true, + 'uploaded_by_user_id': currentUser?.id, + }).select(); + + return response[0]['media_file_id'] as int; + } catch (e) { + print('Error en _uploadPoster: $e'); + return null; + } + } + + /// Get MIME type from file extension + String _getMimeType(String extension) { + final ext = extension.toLowerCase().replaceAll('.', ''); + switch (ext) { + case 'mp4': + return 'video/mp4'; + case 'webm': + return 'video/webm'; + case 'mov': + return 'video/quicktime'; + case 'avi': + return 'video/x-msvideo'; + case 'jpg': + case 'jpeg': + return 'image/jpeg'; + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + default: + return 'application/octet-stream'; + } + } + + /// Clear upload state + void _clearUploadState() { + videoName = null; + videoUrl = null; + videoStoragePath = null; + videoFileExtension = ''; + webVideoBytes = null; + posterName = null; + posterUrl = null; + posterStoragePath = null; + posterFileExtension = ''; + webPosterBytes = null; + tituloController.clear(); + descripcionController.clear(); + } + + // ========== UPDATE METHODS ========== + + /// Update video title + Future updateVideoTitle(int mediaFileId, String title) async { + try { + await supabaseML + .from('media_files') + .update({'title': title}) + .eq('media_file_id', mediaFileId) + .eq('organization_fk', organizationId); + + await loadMediaFiles(); + return true; + } catch (e) { + errorMessage = 'Error actualizando título: $e'; + notifyListeners(); + print('Error en updateVideoTitle: $e'); + return false; + } + } + + /// Update video description + Future updateVideoDescription( + int mediaFileId, String description) async { + try { + await supabaseML + .from('media_files') + .update({'file_description': description}) + .eq('media_file_id', mediaFileId) + .eq('organization_fk', organizationId); + + await loadMediaFiles(); + return true; + } catch (e) { + errorMessage = 'Error actualizando descripción: $e'; + notifyListeners(); + print('Error en updateVideoDescription: $e'); + return false; + } + } + + /// Update video category + Future updateVideoCategory(int mediaFileId, int? categoryId) async { + try { + await supabaseML + .from('media_files') + .update({'media_category_fk': categoryId}) + .eq('media_file_id', mediaFileId) + .eq('organization_fk', organizationId); + + await loadMediaFiles(); + return true; + } catch (e) { + errorMessage = 'Error actualizando categoría: $e'; + notifyListeners(); + print('Error en updateVideoCategory: $e'); + return false; + } + } + + /// Update video metadata + Future updateVideoMetadata( + int mediaFileId, + Map metadata, + ) async { + try { + await supabaseML + .from('media_files') + .update({'metadata_json': metadata}) + .eq('media_file_id', mediaFileId) + .eq('organization_fk', organizationId); + + await loadMediaFiles(); + return true; + } catch (e) { + errorMessage = 'Error actualizando metadata: $e'; + notifyListeners(); + print('Error en updateVideoMetadata: $e'); + return false; + } + } + + // ========== DELETE METHODS ========== + + /// Delete video and its storage files + Future deleteVideo(int mediaFileId) async { + try { + isLoading = true; + notifyListeners(); + + // Get video info + final response = await supabaseML + .from('media_files') + .select() + .eq('media_file_id', mediaFileId) + .eq('organization_fk', organizationId) + .single(); + + final storagePath = response['storage_path'] as String?; + + // Delete from storage if path exists + if (storagePath != null) { + await supabaseML.storage.from('energymedia').remove([storagePath]); + } + + // Delete associated posters + final posters = await supabaseML + .from('media_posters') + .select('poster_file_id') + .eq('media_file_id', mediaFileId); + + for (var poster in posters) { + await _deletePosterFile(poster['poster_file_id']); + } + + // Delete database record (cascade will delete posters relationship) + await supabaseML + .from('media_files') + .delete() + .eq('media_file_id', mediaFileId) + .eq('organization_fk', organizationId); + + await loadMediaFiles(); + + isLoading = false; + notifyListeners(); + return true; + } catch (e) { + errorMessage = 'Error eliminando video: $e'; + isLoading = false; + notifyListeners(); + print('Error en deleteVideo: $e'); + return false; + } + } + + /// Delete poster file (internal helper) + Future _deletePosterFile(int posterFileId) async { + try { + final response = await supabaseML + .from('media_files') + .select('storage_path') + .eq('media_file_id', posterFileId) + .single(); + + final storagePath = response['storage_path'] as String?; + + if (storagePath != null) { + await supabaseML.storage.from('energymedia').remove([storagePath]); + } + + await supabaseML + .from('media_files') + .delete() + .eq('media_file_id', posterFileId); + } catch (e) { + print('Error en _deletePosterFile: $e'); + } + } + + // ========== ANALYTICS METHODS ========== + + /// Increment view count + Future incrementReproduccion(int mediaFileId) async { + try { + // Get current metadata + final response = await supabaseML + .from('media_files') + .select('metadata_json') + .eq('media_file_id', mediaFileId) + .eq('organization_fk', organizationId) + .single(); + + final metadata = response['metadata_json'] as Map? ?? {}; + final currentCount = metadata['reproducciones'] ?? 0; + + metadata['reproducciones'] = currentCount + 1; + metadata['last_viewed_at'] = DateTime.now().toIso8601String(); + + await updateVideoMetadata(mediaFileId, metadata); + return true; + } catch (e) { + print('Error en incrementReproduccion: $e'); + return false; + } + } + + /// Get dashboard statistics + Future> getDashboardStats() async { + try { + // Total videos + final totalVideos = mediaFiles.length; + + // Total reproducciones + int totalReproducciones = 0; + for (var media in mediaFiles) { + totalReproducciones += media.reproducciones; + } + + // Most viewed video + MediaFileModel? mostViewed; + if (mediaFiles.isNotEmpty) { + mostViewed = mediaFiles.reduce((curr, next) => + curr.reproducciones > next.reproducciones ? curr : next); + } + + // Videos by category + Map videosByCategory = {}; + for (var media in mediaFiles) { + final categoryName = _getCategoryName(media.mediaCategoryFk); + videosByCategory[categoryName] = + (videosByCategory[categoryName] ?? 0) + 1; + } + + // Most viewed category + String? mostViewedCategory; + if (videosByCategory.isNotEmpty) { + mostViewedCategory = videosByCategory.entries + .reduce((a, b) => a.value > b.value ? a : b) + .key; + } + + return { + 'total_videos': totalVideos, + 'total_reproducciones': totalReproducciones, + 'most_viewed_video': mostViewed?.toMap(), + 'videos_by_category': videosByCategory, + 'most_viewed_category': mostViewedCategory, + 'total_categories': categories.length, + }; + } catch (e) { + print('Error en getDashboardStats: $e'); + return {}; + } + } + + // ========== SEARCH & FILTER ========== + + /// Search videos by title or description + void searchVideos(String query) { + if (query.isEmpty) { + _buildPlutoRows(); + notifyListeners(); + return; + } + + videosRows.clear(); + final filteredMedia = mediaFiles.where((media) { + final title = (media.title ?? media.fileName).toLowerCase(); + final description = (media.fileDescription ?? '').toLowerCase(); + final searchQuery = query.toLowerCase(); + return title.contains(searchQuery) || description.contains(searchQuery); + }).toList(); + + for (var media in filteredMedia) { + videosRows.add( + PlutoRow( + cells: { + 'id': PlutoCell(value: media.mediaFileId), + 'thumbnail': PlutoCell(value: media.fileUrl), + 'title': PlutoCell(value: media.title ?? media.fileName), + 'description': PlutoCell(value: media.fileDescription ?? ''), + 'category': + PlutoCell(value: _getCategoryName(media.mediaCategoryFk)), + 'reproducciones': PlutoCell(value: media.reproducciones), + 'duration': PlutoCell(value: media.seconds ?? 0), + 'size': PlutoCell(value: _formatFileSize(media.fileSizeBytes)), + 'created_at': PlutoCell(value: media.createdAt), + 'actions': PlutoCell(value: media.mediaFileId), + }, + ), + ); + } + + notifyListeners(); + } + + // ========== CLEANUP ========== + + @override + void dispose() { + busquedaVideoController.dispose(); + tituloController.dispose(); + descripcionController.dispose(); + super.dispose(); + } +} diff --git a/lib/providers/visual_state_provider.dart b/lib/providers/visual_state_provider.dart index 550df1d..0e4c8da 100644 --- a/lib/providers/visual_state_provider.dart +++ b/lib/providers/visual_state_provider.dart @@ -167,6 +167,12 @@ class VisualStateProvider extends ChangeNotifier { isTaped[index] = true; } + void changeThemeMode(ThemeMode mode, BuildContext context) { + AppTheme.saveThemeMode(mode); + setDarkModeSetting(context, mode); + notifyListeners(); + } + @override void dispose() { primaryColorLightController.dispose(); diff --git a/lib/router/router.dart b/lib/router/router.dart index 28c4c5a..e71d285 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:nethive_neo/helpers/globals.dart'; -import 'package:nethive_neo/pages/empresa_negocios/empresa_negocios_page.dart'; -import 'package:nethive_neo/pages/infrastructure/infrastructure_layout.dart'; +import 'package:nethive_neo/pages/videos/videos_layout.dart'; import 'package:nethive_neo/pages/pages.dart'; import 'package:nethive_neo/services/navigation_service.dart'; @@ -36,23 +35,7 @@ final GoRouter router = GoRouter( path: '/', name: 'root', builder: (BuildContext context, GoRouterState state) { - if (currentUser!.role.roleId == 14 || currentUser!.role.roleId == 13) { - return Container( - color: Colors.amber, - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - child: const Center(child: Text('Empresa Negocios'))); - } else { - return const EmpresaNegociosPage(); - } - }, - ), - GoRoute( - path: '/infrastructure/:negocioId', - name: 'infrastructure', - builder: (BuildContext context, GoRouterState state) { - final negocioId = state.pathParameters['negocioId']!; - return InfrastructureLayout(negocioId: negocioId); + return const VideosLayout(); }, ), GoRoute( diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index ad2ce51..3380d18 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -76,13 +76,11 @@ abstract class AppTheme { ); Gradient primaryGradient = const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, + begin: Alignment(-1.0, -1.0), + end: Alignment(1.0, 1.0), colors: [ - Color(0xFF10B981), // Verde esmeralda - Color(0xFF059669), // Verde intenso - Color(0xFF0D9488), // Verde-azulado - Color(0xFF0F172A), // Azul muy oscuro + Color(0xFF4EC9F5), // Cyan + Color(0xFFFFB733), // Yellow ], ); @@ -91,10 +89,9 @@ abstract class AppTheme { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - Color(0xFF1E40AF), // Azul profundo - Color(0xFF3B82F6), // Azul brillante - Color(0xFF10B981), // Verde esmeralda - Color(0xFF7C3AED), // Púrpura + Color(0xFF6B2F8A), // Purple + Color(0xFF4EC9F5), // Cyan + Color(0xFFFFB733), // Yellow ], ); @@ -103,9 +100,9 @@ abstract class AppTheme { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Color(0xFF0F172A), // Azul muy oscuro - Color(0xFF1E293B), // Azul oscuro - Color(0xFF334155), // Azul gris + Color(0xFF0B0B0D), // Main background + Color(0xFF121214), // Surface 1 + Color(0xFF1A1A1D), // Surface 2 ], ); @@ -135,37 +132,36 @@ abstract class AppTheme { class LightModeTheme extends AppTheme { @override - Color primaryColor = const Color(0xFF10B981); // Verde esmeralda principal + Color primaryColor = const Color(0xFF4EC9F5); // Cyan primary @override - Color secondaryColor = const Color(0xFF059669); // Verde más oscuro + Color secondaryColor = const Color(0xFFFFB733); // Yellow secondary @override - Color tertiaryColor = const Color(0xFF0D9488); // Verde azulado + Color tertiaryColor = const Color(0xFF6B2F8A); // Purple accent @override - Color alternate = const Color(0xFF3B82F6); // Azul de acento + Color alternate = const Color(0xFFFF7A3D); // Orange accent @override - Color primaryBackground = const Color(0xFF0F172A); // Fondo muy oscuro + Color primaryBackground = const Color(0xFFFFFFFF); // Main background (white) @override - Color secondaryBackground = - const Color(0xFF1E293B); // Fondo secundario oscuro + Color secondaryBackground = const Color(0xFFF7F7F7); // Card/Surface 1 @override - Color tertiaryBackground = const Color(0xFF334155); // Fondo terciario + Color tertiaryBackground = const Color(0xFFEFEFEF); // Card/Surface 2 @override Color transparentBackground = - const Color(0xFF1E293B).withOpacity(.1); // Fondo transparente + const Color(0xFFFFFFFF).withOpacity(.1); // Transparent background @override - Color primaryText = const Color(0xFFFFFFFF); // Texto blanco + Color primaryText = const Color(0xFF111111); // Primary text @override - Color secondaryText = const Color(0xFF94A3B8); // Texto gris claro + Color secondaryText = const Color(0xFF2B2B2B); // Secondary text @override - Color tertiaryText = const Color(0xFF64748B); // Texto gris medio + Color tertiaryText = const Color(0xFFB5B7BA); // Disabled/Muted @override - Color hintText = const Color(0xFF475569); // Texto de sugerencia + Color hintText = const Color(0xFFE1E1E1); // Border/Divider @override - Color error = const Color(0xFFEF4444); // Rojo para errores + Color error = const Color(0xFFFF2D2D); // Red accent @override - Color warning = const Color(0xFFF59E0B); // Amarillo para advertencias + Color warning = const Color(0xFFFFB733); // Yellow accent @override - Color success = const Color(0xFF10B981); // Verde para éxito + Color success = const Color(0xFF4EC9F5); // Cyan accent @override Color formBackground = const Color(0xFF10B981).withOpacity(.05); // Fondo de formularios @@ -183,37 +179,36 @@ class LightModeTheme extends AppTheme { class DarkModeTheme extends AppTheme { @override - Color primaryColor = const Color(0xFF10B981); // Verde esmeralda principal + Color primaryColor = const Color(0xFF4EC9F5); // Cyan primary @override - Color secondaryColor = const Color(0xFF059669); // Verde más oscuro + Color secondaryColor = const Color(0xFFFFB733); // Yellow secondary @override - Color tertiaryColor = const Color(0xFF0D9488); // Verde azulado + Color tertiaryColor = const Color(0xFF6B2F8A); // Purple accent @override - Color alternate = const Color(0xFF3B82F6); // Azul de acento + Color alternate = const Color(0xFFFF7A3D); // Orange accent @override - Color primaryBackground = const Color(0xFF0F172A); // Fondo muy oscuro + Color primaryBackground = const Color(0xFF0B0B0D); // Main background (dark) @override - Color secondaryBackground = - const Color(0xFF1E293B); // Fondo secundario oscuro + Color secondaryBackground = const Color(0xFF121214); // Card/Surface 1 @override - Color tertiaryBackground = const Color(0xFF334155); // Fondo terciario + Color tertiaryBackground = const Color(0xFF1A1A1D); // Card/Surface 2 @override Color transparentBackground = - const Color(0xFF1E293B).withOpacity(.3); // Fondo transparente + const Color(0xFF0B0B0D).withOpacity(.3); // Transparent background @override - Color primaryText = const Color(0xFFFFFFFF); // Texto blanco + Color primaryText = const Color(0xFFFFFFFF); // Primary text (white) @override - Color secondaryText = const Color(0xFF94A3B8); // Texto gris claro + Color secondaryText = const Color(0xFFEAEAEA); // Secondary text @override - Color tertiaryText = const Color(0xFF64748B); // Texto gris medio + Color tertiaryText = const Color(0xFF6D6E73); // Muted/Disabled @override - Color hintText = const Color(0xFF475569); // Texto de sugerencia + Color hintText = const Color(0xFF2A2A2E); // Border/Divider @override - Color error = const Color(0xFFEF4444); // Rojo para errores + Color error = const Color(0xFFFF2D2D); // Red accent @override - Color warning = const Color(0xFFF59E0B); // Amarillo para advertencias + Color warning = const Color(0xFFFFB733); // Yellow accent @override - Color success = const Color(0xFF10B981); // Verde para éxito + Color success = const Color(0xFF4EC9F5); // Cyan accent @override Color formBackground = const Color(0xFF10B981).withOpacity(.1); // Fondo de formularios diff --git a/lib/widgets/premium_button.dart b/lib/widgets/premium_button.dart new file mode 100644 index 0000000..6a62b67 --- /dev/null +++ b/lib/widgets/premium_button.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/theme/theme.dart'; + +class PremiumButton extends StatefulWidget { + final String text; + final VoidCallback? onPressed; + final IconData? icon; + final bool isLoading; + final bool isOutlined; + final Color? backgroundColor; + final Color? foregroundColor; + final double? width; + final double? height; + final double borderRadius; + + const PremiumButton({ + Key? key, + required this.text, + this.onPressed, + this.icon, + this.isLoading = false, + this.isOutlined = false, + this.backgroundColor, + this.foregroundColor, + this.width, + this.height, + this.borderRadius = 12, + }) : super(key: key); + + @override + State createState() => _PremiumButtonState(); +} + +class _PremiumButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + bool _isHovered = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final bgColor = widget.backgroundColor ?? AppTheme.of(context).primaryColor; + final fgColor = widget.foregroundColor ?? const Color(0xFF0B0B0D); + + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: AnimatedScale( + scale: _isHovered ? 1.02 : 1.0, + duration: const Duration(milliseconds: 150), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + width: widget.width, + height: widget.height ?? 48, + decoration: BoxDecoration( + gradient: widget.isOutlined + ? null + : (_isHovered + ? LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + bgColor.withOpacity(0.9), + bgColor, + ], + ) + : LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + bgColor, + bgColor.withOpacity(0.8), + ], + )), + borderRadius: BorderRadius.circular(widget.borderRadius), + border: widget.isOutlined + ? Border.all( + color: bgColor, + width: 2, + ) + : null, + boxShadow: widget.isOutlined + ? null + : [ + BoxShadow( + color: bgColor.withOpacity(_isHovered ? 0.5 : 0.3), + blurRadius: _isHovered ? 20 : 12, + offset: Offset(0, _isHovered ? 8 : 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: widget.isLoading ? null : widget.onPressed, + borderRadius: BorderRadius.circular(widget.borderRadius), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.isLoading) + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + widget.isOutlined ? bgColor : fgColor, + ), + ), + ) + else ...[ + if (widget.icon != null) ...[ + Icon( + widget.icon, + color: widget.isOutlined ? bgColor : fgColor, + size: 20, + ), + const SizedBox(width: 8), + ], + Text( + widget.text, + style: TextStyle( + color: widget.isOutlined ? bgColor : fgColor, + fontSize: 15, + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + ), + ), + ], + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c3c2da7..b030075 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -14,6 +14,7 @@ import path_provider_foundation import shared_preferences_foundation import sign_in_with_apple import url_launcher_macos +import wakelock_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) @@ -25,4 +26,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 71be54b..b37b739 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + chewie: + dependency: "direct main" + description: + name: chewie + sha256: "3bc4fbae99a9f39dc9c04ba70ad2a2fb902dbd3d3e36ad8ab3a53f0dcbf311be" + url: "https://pub.dev" + source: hosted + version: "1.0.0" chip_list: dependency: "direct main" description: @@ -153,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -229,10 +245,10 @@ packages: dependency: transitive description: name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" fake_async: dependency: transitive description: @@ -309,10 +325,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08" + sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef" url: "https://pub.dev" source: hosted - version: "0.69.2" + version: "0.69.0" fl_heatmap: dependency: "direct main" description: @@ -589,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" http: dependency: "direct main" description: @@ -1085,6 +1109,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sign_in_with_apple: dependency: transitive description: @@ -1314,6 +1346,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "868a139229acb5018d22aded3eb9cb4767ff43a8216573c086b6c535a4957481" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: e7de6fabe5d96048cd8f4d710f25c3df84bb3cab8b22da6c082bd8f39e316984 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: "90468226c8687adf7b567d9bb42c25588783c4d30509af1fbd663b2dd049f700" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec" + url: "https://pub.dev" + source: hosted + version: "6.6.0" + video_player_web: + dependency: "direct overridden" + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://pub.dev" + source: hosted + version: "2.4.0" vm_service: dependency: transitive description: @@ -1322,6 +1394,38 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.0" + wakelock: + dependency: transitive + description: + name: wakelock + sha256: "78bad4822be81d37e7bc34b6990da7dface2a445255cd37c6f053b51a4ccdb3b" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + wakelock_macos: + dependency: transitive + description: + name: wakelock_macos + sha256: "73581e5d9ed2dd1ba951375c30e63f0eb8c58d7d6286ae9ddf927b88f2aea8d9" + url: "https://pub.dev" + source: hosted + version: "0.1.0+3" + wakelock_platform_interface: + dependency: transitive + description: + name: wakelock_platform_interface + sha256: d0a8a1c02af68077db5df1e0f5e2b745f7b1f2cdcc48e3e0b6f8f4dcc349050e + url: "https://pub.dev" + source: hosted + version: "0.2.1+3" + wakelock_web: + dependency: transitive + description: + name: wakelock_web + sha256: "06b0033d5421712138e7fa482ff5c6280fe12e0a41c40c3fe8fda2c007eb4348" + url: "https://pub.dev" + source: hosted + version: "0.2.0+3" web: dependency: transitive description: @@ -1403,5 +1507,5 @@ packages: source: hosted version: "1.1.1" sdks: - dart: ">=3.7.0-0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 144a447..1b08ecf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,7 +53,6 @@ dependencies: countries_world_map: ^1.1.1 customizable_counter: ^1.0.4 drop_down_list_menu: ^0.0.9 - fl_chart: ^0.69.0 fl_heatmap: ^0.4.4 flip_card: ^0.7.0 flutter_credit_card: ^4.0.1 @@ -74,6 +73,10 @@ dependencies: flutter_graph_view: ^1.1.6 flutter_animate: ^4.2.0 flutter_flow_chart: 3.2.3 + video_player: ^2.6.0 + chewie: ^1.0.0 + shimmer: ^3.0.0 + fl_chart: 0.69.0 dev_dependencies: flutter_test: @@ -83,6 +86,7 @@ dev_dependencies: dependency_overrides: intl: ^0.19.0 # Override para que pluto_grid funcione + video_player_web: ^2.3.0 # Override para compatibilidad con Flutter web flutter: uses-material-design: true