base creada

This commit is contained in:
Abraham
2026-01-10 21:12:17 -08:00
parent 8bfc7d60c3
commit 9adadbd354
62 changed files with 5392 additions and 22447 deletions

257
.github/copilot-instructions.md vendored Normal file
View File

@@ -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<T>()` for one-time actions, `context.watch<T>()` 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

View File

@@ -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

View File

@@ -14,7 +14,7 @@ final GlobalKey<ScaffoldMessengerState> snackbarKey =
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
final supabase = Supabase.instance.client; final supabase = Supabase.instance.client;
late SupabaseClient supabaseLU; late SupabaseClient supabaseML;
late final SharedPreferences prefs; late final SharedPreferences prefs;
late Configuration? theme; late Configuration? theme;

View File

@@ -10,9 +10,7 @@ import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:nethive_neo/providers/user_provider.dart'; import 'package:nethive_neo/providers/user_provider.dart';
import 'package:nethive_neo/providers/visual_state_provider.dart'; import 'package:nethive_neo/providers/visual_state_provider.dart';
import 'package:nethive_neo/providers/users_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/videos_provider.dart';
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
import 'package:nethive_neo/providers/nethive/navigation_provider.dart';
import 'package:nethive_neo/helpers/globals.dart'; import 'package:nethive_neo/helpers/globals.dart';
import 'package:url_strategy/url_strategy.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(); await initGlobals();
@@ -45,9 +43,7 @@ void main() async {
ChangeNotifierProvider( ChangeNotifierProvider(
create: (context) => VisualStateProvider(context)), create: (context) => VisualStateProvider(context)),
ChangeNotifierProvider(create: (_) => UsersProvider()), ChangeNotifierProvider(create: (_) => UsersProvider()),
ChangeNotifierProvider(create: (_) => EmpresasNegociosProvider()), ChangeNotifierProvider(create: (_) => VideosProvider()),
ChangeNotifierProvider(create: (_) => ComponentesProvider()),
ChangeNotifierProvider(create: (_) => NavigationProvider()),
], ],
child: const MyApp(), child: const MyApp(),
), ),
@@ -79,7 +75,7 @@ class _MyAppState extends State<MyApp> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Portal( return Portal(
child: MaterialApp.router( child: MaterialApp.router(
title: 'NETHIVE', title: 'EnergyMedia Content Manager',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
locale: _locale, locale: _locale,
localizationsDelegates: const [ localizationsDelegates: const [

View File

@@ -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<String, dynamic> 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<String, dynamic> 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,
);
}
}

View File

@@ -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<String, dynamic>? 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<String, dynamic> 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<String, dynamic> 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<String, dynamic>? 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<String> get categorias =>
(metadataJson?['categorias'] as List<dynamic>?)
?.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;
}

View File

@@ -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';

View File

@@ -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<String, dynamic> 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<String, dynamic> 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,
);
}
}

View File

@@ -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<String, dynamic> 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<String, dynamic> 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;
}

View File

@@ -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 { class TopologiaCompleta {
final List<ComponenteTopologia> componentes; final List<ComponenteTopologia> componentes;
final List<ConexionDatos> conexionesDatos; final List<ConexionDatos> conexionesDatos;

View File

@@ -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<EmpresaNegociosPage> createState() => _EmpresaNegociosPageState();
}
class _EmpresaNegociosPageState extends State<EmpresaNegociosPage>
with TickerProviderStateMixin {
bool showMapView = false;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
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<EmpresasNegociosProvider>(
builder: (context, provider, child) {
if (isLargeScreen) {
// Vista de escritorio
return Row(
children: [
// Sidebar izquierdo con empresas
SlideTransition(
position: Tween<Offset>(
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<double>(
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<double>(
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<double>(
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<EmpresasNegociosProvider>(
builder: (context, provider, child) {
return NegociosMapView(provider: provider);
},
);
}
Widget _buildMobileFAB(BuildContext context) {
return Consumer<EmpresasNegociosProvider>(
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);
},
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<AddEmpresaDialog> createState() => _AddEmpresaDialogState();
}
class _AddEmpresaDialogState extends State<AddEmpresaDialog>
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,
),
),
],
);
}
}

View File

@@ -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<Color>(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,
),
),
],
),
),
),
),
],
);
}
}

View File

@@ -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<double> _scaleAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
late Listenable _combinedAnimation;
bool _isInitialized = false;
EmpresaDialogAnimations({required this.vsync});
// Getters para acceder a las animaciones
Animation<double> get scaleAnimation => _scaleAnimation;
Animation<Offset> get slideAnimation => _slideAnimation;
Animation<double> 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<Offset>(
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();
}
}

View File

@@ -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<EmpresaDialogForm> createState() => _EmpresaDialogFormState();
}
class _EmpresaDialogFormState extends State<EmpresaDialogForm> {
final _formKey = GlobalKey<FormState>();
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<void> _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;
});
}
}
}
}

View File

@@ -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<Offset> 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,
),
);
}
}

View File

@@ -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,
),
),
],
],
),
),
),
),
);
}
}

View File

@@ -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),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<AddNegocioDialog> createState() => _AddNegocioDialogState();
}
class _AddNegocioDialogState extends State<AddNegocioDialog>
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
),
),
],
);
}
}

View File

@@ -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<Color>(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,
),
),
],
),
),
),
),
],
);
}
}

View File

@@ -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<double> _scaleAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
late Listenable _combinedAnimation;
bool _isInitialized = false;
NegocioDialogAnimations({required this.vsync});
// Getters para acceder a las animaciones
Animation<double> get scaleAnimation => _scaleAnimation;
Animation<Offset> get slideAnimation => _slideAnimation;
Animation<double> 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<Offset>(
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();
}
}

View File

@@ -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<NegocioDialogForm> createState() => _NegocioDialogFormState();
}
class _NegocioDialogFormState extends State<NegocioDialogForm> {
final _formKey = GlobalKey<FormState>();
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<void> _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;
});
}
}
}
}

View File

@@ -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<Offset> 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,
),
);
}
}

View File

@@ -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<String>(
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<String>(
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;
},
),
);
}
}

View File

@@ -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<TextInputFormatter>? 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),
),
),
);
}
}

View File

@@ -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<EmpresaSelectorSidebar> createState() => _EmpresaSelectorSidebarState();
}
class _EmpresaSelectorSidebarState extends State<EmpresaSelectorSidebar>
with TickerProviderStateMixin {
late AnimationController _pulseController;
late Animation<double> _pulseAnimation;
@override
void initState() {
super.initState();
_pulseController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
_pulseAnimation = Tween<double>(
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<int>(
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<double>(
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<int>(
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,
),
),
],
),
),
);
}
}

View File

@@ -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<MobileEmpresaSelector> createState() => _MobileEmpresaSelectorState();
}
class _MobileEmpresaSelectorState extends State<MobileEmpresaSelector>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
final TextEditingController _searchController = TextEditingController();
List<dynamic> _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<double>(
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,
),
),
],
),
),
);
}
}

View File

@@ -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<double>(
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<String>(
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),
),
),
],
),
);
}
}

View File

@@ -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<NegociosMapView> createState() => _NegociosMapViewState();
}
class _NegociosMapViewState extends State<NegociosMapView>
with TickerProviderStateMixin {
final MapController _mapController = MapController();
late AnimationController _markerAnimationController;
late AnimationController _tooltipAnimationController;
late Animation<double> _markerAnimation;
late Animation<double> _tooltipAnimation;
late Animation<Offset> _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<double>(
begin: 1.0,
end: 1.4,
).animate(CurvedAnimation(
parent: _markerAnimationController,
curve: Curves.easeOutBack,
));
_tooltipAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _tooltipAnimationController,
curve: Curves.easeOutCubic,
));
_tooltipSlideAnimation = Tween<Offset>(
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<Marker> _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<Color>(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;
}

View File

@@ -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<ComponentesProvider>(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<String> partes = direccionCompleta.split(',');
return partes.length > 1 ? partes[1].trim() : direccionCompleta;
}
}

View File

@@ -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<InfrastructureLayout> createState() => _InfrastructureLayoutState();
}
class _InfrastructureLayoutState extends State<InfrastructureLayout>
with TickerProviderStateMixin {
bool _isSidebarExpanded = true;
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_fadeAnimation = Tween<double>(
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<NavigationProvider>()
.setNegocioSeleccionado(widget.negocioId);
// Luego obtener la información completa y establecer en ComponentesProvider
await _setupComponentesProvider();
_fadeController.forward();
});
}
Future<void> _setupComponentesProvider() async {
try {
final navigationProvider = context.read<NavigationProvider>();
final componentesProvider = context.read<ComponentesProvider>();
// 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<NavigationProvider>(
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();
}
}
}

View File

@@ -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,
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -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,
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -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<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
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<NavigationProvider, ComponentesProvider>(
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<double>(
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<int>(
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,
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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<String> _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<String, int> _agruparComponentesPorPiso() {
Map<String, int> 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;
}
}

View File

@@ -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>(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}');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<ComponentesCardsView> createState() => _ComponentesCardsViewState();
}
class _ComponentesCardsViewState extends State<ComponentesCardsView>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
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<ComponentesProvider>(
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<double>(
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 ? '' : '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<ComponentesProvider>(context, listen: false);
showDialog(
context: context,
builder: (context) => EditComponenteDialog(
provider: provider,
componente: componente,
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<InfrastructureSidemenu> createState() => _InfrastructureSidemenuState();
}
class _InfrastructureSidemenuState extends State<InfrastructureSidemenu>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fadeAnimation = Tween<double>(
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<NavigationProvider>(
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<double>(
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
}
}
}

View File

@@ -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<MobileNavigationModal> createState() => _MobileNavigationModalState();
}
class _MobileNavigationModalState extends State<MobileNavigationModal>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
_slideAnimation = Tween<Offset>(
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<NavigationProvider>(
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<double>(
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<double>(
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();
}
}

View File

@@ -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<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
Map<String, dynamic> stats = {};
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_animationController.forward();
_loadStats();
}
Future<void> _loadStats() async {
final provider = Provider.of<VideosProvider>(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<String, dynamic>?;
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<Color>(
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<VideosProvider>(
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';
}
}
}

View File

@@ -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<GestorVideosPage> createState() => _GestorVideosPageState();
}
class _GestorVideosPageState extends State<GestorVideosPage> {
PlutoGridStateManager? _stateManager;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
setState(() => _isLoading = true);
final provider = Provider.of<VideosProvider>(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<VideosProvider>(
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<VideosProvider>(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<void> _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<void> _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<MediaCategoryModel>(
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<void> _deleteVideo(
MediaFileModel video, VideosProvider provider) async {
final confirm = await showDialog<bool>(
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';
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<VideosLayout> createState() => _VideosLayoutState();
}
class _VideosLayoutState extends State<VideosLayout> {
int _selectedMenuIndex = 0;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final List<MenuItem> _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<VisualStateProvider>(
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<VisualStateProvider>(
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,
});
}

View File

@@ -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<PremiumUploadDialog> createState() => _PremiumUploadDialogState();
}
class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
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<void> _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<void> _selectPoster() async {
final result = await widget.provider.selectPoster();
if (result) {
setState(() {
selectedPoster = widget.provider.webPosterBytes;
posterFileName = widget.provider.posterName;
});
}
}
Future<void> _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<MediaCategoryModel>(
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,
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<Empresa> empresas = [];
List<Negocio> negocios = [];
List<PlutoRow> empresasRows = [];
List<PlutoRow> 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<void> 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<dynamic>)
.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<void> 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<dynamic>)
.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<void> 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<void> 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<String?> 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<String?> 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<bool> 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<bool> 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<bool> 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<bool> 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,
);
}
}

View File

@@ -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<NavigationMenuItem> 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<void> 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,
});
}

View File

@@ -1,6 +1,3 @@
export 'package:nethive_neo/providers/visual_state_provider.dart'; export 'package:nethive_neo/providers/visual_state_provider.dart';
export 'package:nethive_neo/providers/users_provider.dart'; export 'package:nethive_neo/providers/users_provider.dart';
export 'package:nethive_neo/providers/user_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';

View File

@@ -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<PlutoRow> videosRows = [];
// ========== DATA LISTS ==========
List<MediaFileModel> mediaFiles = [];
List<MediaCategoryModel> categories = [];
List<MediaWithPosterModel> 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<void> 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<dynamic>)
.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<void> 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<dynamic>)
.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<void> loadCategories() async {
try {
final response = await supabaseML
.from('media_categories')
.select()
.order('category_name');
categories = (response as List<dynamic>)
.map((item) => MediaCategoryModel.fromMap(item))
.toList();
notifyListeners();
} catch (e) {
print('Error en loadCategories: $e');
}
}
/// Build PlutoGrid rows from media files
Future<void> _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<bool> 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<bool> 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<bool> 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<int?> _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<bool> 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<bool> 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<bool> 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<bool> updateVideoMetadata(
int mediaFileId,
Map<String, dynamic> 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<bool> 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<void> _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<bool> 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<String, dynamic>? ?? {};
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<Map<String, dynamic>> 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<String, int> 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();
}
}

View File

@@ -167,6 +167,12 @@ class VisualStateProvider extends ChangeNotifier {
isTaped[index] = true; isTaped[index] = true;
} }
void changeThemeMode(ThemeMode mode, BuildContext context) {
AppTheme.saveThemeMode(mode);
setDarkModeSetting(context, mode);
notifyListeners();
}
@override @override
void dispose() { void dispose() {
primaryColorLightController.dispose(); primaryColorLightController.dispose();

View File

@@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:nethive_neo/helpers/globals.dart'; import 'package:nethive_neo/helpers/globals.dart';
import 'package:nethive_neo/pages/empresa_negocios/empresa_negocios_page.dart'; import 'package:nethive_neo/pages/videos/videos_layout.dart';
import 'package:nethive_neo/pages/infrastructure/infrastructure_layout.dart';
import 'package:nethive_neo/pages/pages.dart'; import 'package:nethive_neo/pages/pages.dart';
import 'package:nethive_neo/services/navigation_service.dart'; import 'package:nethive_neo/services/navigation_service.dart';
@@ -36,23 +35,7 @@ final GoRouter router = GoRouter(
path: '/', path: '/',
name: 'root', name: 'root',
builder: (BuildContext context, GoRouterState state) { builder: (BuildContext context, GoRouterState state) {
if (currentUser!.role.roleId == 14 || currentUser!.role.roleId == 13) { return const VideosLayout();
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);
}, },
), ),
GoRoute( GoRoute(

View File

@@ -76,13 +76,11 @@ abstract class AppTheme {
); );
Gradient primaryGradient = const LinearGradient( Gradient primaryGradient = const LinearGradient(
begin: Alignment.topLeft, begin: Alignment(-1.0, -1.0),
end: Alignment.bottomRight, end: Alignment(1.0, 1.0),
colors: <Color>[ colors: <Color>[
Color(0xFF10B981), // Verde esmeralda Color(0xFF4EC9F5), // Cyan
Color(0xFF059669), // Verde intenso Color(0xFFFFB733), // Yellow
Color(0xFF0D9488), // Verde-azulado
Color(0xFF0F172A), // Azul muy oscuro
], ],
); );
@@ -91,10 +89,9 @@ abstract class AppTheme {
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: <Color>[ colors: <Color>[
Color(0xFF1E40AF), // Azul profundo Color(0xFF6B2F8A), // Purple
Color(0xFF3B82F6), // Azul brillante Color(0xFF4EC9F5), // Cyan
Color(0xFF10B981), // Verde esmeralda Color(0xFFFFB733), // Yellow
Color(0xFF7C3AED), // Púrpura
], ],
); );
@@ -103,9 +100,9 @@ abstract class AppTheme {
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: <Color>[ colors: <Color>[
Color(0xFF0F172A), // Azul muy oscuro Color(0xFF0B0B0D), // Main background
Color(0xFF1E293B), // Azul oscuro Color(0xFF121214), // Surface 1
Color(0xFF334155), // Azul gris Color(0xFF1A1A1D), // Surface 2
], ],
); );
@@ -135,37 +132,36 @@ abstract class AppTheme {
class LightModeTheme extends AppTheme { class LightModeTheme extends AppTheme {
@override @override
Color primaryColor = const Color(0xFF10B981); // Verde esmeralda principal Color primaryColor = const Color(0xFF4EC9F5); // Cyan primary
@override @override
Color secondaryColor = const Color(0xFF059669); // Verde más oscuro Color secondaryColor = const Color(0xFFFFB733); // Yellow secondary
@override @override
Color tertiaryColor = const Color(0xFF0D9488); // Verde azulado Color tertiaryColor = const Color(0xFF6B2F8A); // Purple accent
@override @override
Color alternate = const Color(0xFF3B82F6); // Azul de acento Color alternate = const Color(0xFFFF7A3D); // Orange accent
@override @override
Color primaryBackground = const Color(0xFF0F172A); // Fondo muy oscuro Color primaryBackground = const Color(0xFFFFFFFF); // Main background (white)
@override @override
Color secondaryBackground = Color secondaryBackground = const Color(0xFFF7F7F7); // Card/Surface 1
const Color(0xFF1E293B); // Fondo secundario oscuro
@override @override
Color tertiaryBackground = const Color(0xFF334155); // Fondo terciario Color tertiaryBackground = const Color(0xFFEFEFEF); // Card/Surface 2
@override @override
Color transparentBackground = Color transparentBackground =
const Color(0xFF1E293B).withOpacity(.1); // Fondo transparente const Color(0xFFFFFFFF).withOpacity(.1); // Transparent background
@override @override
Color primaryText = const Color(0xFFFFFFFF); // Texto blanco Color primaryText = const Color(0xFF111111); // Primary text
@override @override
Color secondaryText = const Color(0xFF94A3B8); // Texto gris claro Color secondaryText = const Color(0xFF2B2B2B); // Secondary text
@override @override
Color tertiaryText = const Color(0xFF64748B); // Texto gris medio Color tertiaryText = const Color(0xFFB5B7BA); // Disabled/Muted
@override @override
Color hintText = const Color(0xFF475569); // Texto de sugerencia Color hintText = const Color(0xFFE1E1E1); // Border/Divider
@override @override
Color error = const Color(0xFFEF4444); // Rojo para errores Color error = const Color(0xFFFF2D2D); // Red accent
@override @override
Color warning = const Color(0xFFF59E0B); // Amarillo para advertencias Color warning = const Color(0xFFFFB733); // Yellow accent
@override @override
Color success = const Color(0xFF10B981); // Verde para éxito Color success = const Color(0xFF4EC9F5); // Cyan accent
@override @override
Color formBackground = Color formBackground =
const Color(0xFF10B981).withOpacity(.05); // Fondo de formularios const Color(0xFF10B981).withOpacity(.05); // Fondo de formularios
@@ -183,37 +179,36 @@ class LightModeTheme extends AppTheme {
class DarkModeTheme extends AppTheme { class DarkModeTheme extends AppTheme {
@override @override
Color primaryColor = const Color(0xFF10B981); // Verde esmeralda principal Color primaryColor = const Color(0xFF4EC9F5); // Cyan primary
@override @override
Color secondaryColor = const Color(0xFF059669); // Verde más oscuro Color secondaryColor = const Color(0xFFFFB733); // Yellow secondary
@override @override
Color tertiaryColor = const Color(0xFF0D9488); // Verde azulado Color tertiaryColor = const Color(0xFF6B2F8A); // Purple accent
@override @override
Color alternate = const Color(0xFF3B82F6); // Azul de acento Color alternate = const Color(0xFFFF7A3D); // Orange accent
@override @override
Color primaryBackground = const Color(0xFF0F172A); // Fondo muy oscuro Color primaryBackground = const Color(0xFF0B0B0D); // Main background (dark)
@override @override
Color secondaryBackground = Color secondaryBackground = const Color(0xFF121214); // Card/Surface 1
const Color(0xFF1E293B); // Fondo secundario oscuro
@override @override
Color tertiaryBackground = const Color(0xFF334155); // Fondo terciario Color tertiaryBackground = const Color(0xFF1A1A1D); // Card/Surface 2
@override @override
Color transparentBackground = Color transparentBackground =
const Color(0xFF1E293B).withOpacity(.3); // Fondo transparente const Color(0xFF0B0B0D).withOpacity(.3); // Transparent background
@override @override
Color primaryText = const Color(0xFFFFFFFF); // Texto blanco Color primaryText = const Color(0xFFFFFFFF); // Primary text (white)
@override @override
Color secondaryText = const Color(0xFF94A3B8); // Texto gris claro Color secondaryText = const Color(0xFFEAEAEA); // Secondary text
@override @override
Color tertiaryText = const Color(0xFF64748B); // Texto gris medio Color tertiaryText = const Color(0xFF6D6E73); // Muted/Disabled
@override @override
Color hintText = const Color(0xFF475569); // Texto de sugerencia Color hintText = const Color(0xFF2A2A2E); // Border/Divider
@override @override
Color error = const Color(0xFFEF4444); // Rojo para errores Color error = const Color(0xFFFF2D2D); // Red accent
@override @override
Color warning = const Color(0xFFF59E0B); // Amarillo para advertencias Color warning = const Color(0xFFFFB733); // Yellow accent
@override @override
Color success = const Color(0xFF10B981); // Verde para éxito Color success = const Color(0xFF4EC9F5); // Cyan accent
@override @override
Color formBackground = Color formBackground =
const Color(0xFF10B981).withOpacity(.1); // Fondo de formularios const Color(0xFF10B981).withOpacity(.1); // Fondo de formularios

View File

@@ -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<PremiumButton> createState() => _PremiumButtonState();
}
class _PremiumButtonState extends State<PremiumButton>
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',
),
),
],
],
),
),
),
),
),
),
);
}
}

View File

@@ -14,6 +14,7 @@ import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
import sign_in_with_apple import sign_in_with_apple
import url_launcher_macos import url_launcher_macos
import wakelock_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
@@ -25,4 +26,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin")) SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
} }

View File

@@ -97,6 +97,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" 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: chip_list:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -153,6 +161,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -229,10 +245,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: equatable name: equatable
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.7" version: "2.0.8"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -309,10 +325,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_chart name: fl_chart
sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08" sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.69.2" version: "0.69.0"
fl_heatmap: fl_heatmap:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -589,6 +605,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1085,6 +1109,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" 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: sign_in_with_apple:
dependency: transitive dependency: transitive
description: description:
@@ -1314,6 +1346,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" 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: vm_service:
dependency: transitive dependency: transitive
description: description:
@@ -1322,6 +1394,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.0" 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: web:
dependency: transitive dependency: transitive
description: description:
@@ -1403,5 +1507,5 @@ packages:
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
sdks: sdks:
dart: ">=3.7.0-0 <4.0.0" dart: ">=3.7.0 <4.0.0"
flutter: ">=3.22.0" flutter: ">=3.29.0"

View File

@@ -53,7 +53,6 @@ dependencies:
countries_world_map: ^1.1.1 countries_world_map: ^1.1.1
customizable_counter: ^1.0.4 customizable_counter: ^1.0.4
drop_down_list_menu: ^0.0.9 drop_down_list_menu: ^0.0.9
fl_chart: ^0.69.0
fl_heatmap: ^0.4.4 fl_heatmap: ^0.4.4
flip_card: ^0.7.0 flip_card: ^0.7.0
flutter_credit_card: ^4.0.1 flutter_credit_card: ^4.0.1
@@ -74,6 +73,10 @@ dependencies:
flutter_graph_view: ^1.1.6 flutter_graph_view: ^1.1.6
flutter_animate: ^4.2.0 flutter_animate: ^4.2.0
flutter_flow_chart: 3.2.3 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: dev_dependencies:
flutter_test: flutter_test:
@@ -83,6 +86,7 @@ dev_dependencies:
dependency_overrides: dependency_overrides:
intl: ^0.19.0 # Override para que pluto_grid funcione intl: ^0.19.0 # Override para que pluto_grid funcione
video_player_web: ^2.3.0 # Override para compatibilidad con Flutter web
flutter: flutter:
uses-material-design: true uses-material-design: true